在上一章中簡單的進行了在開發中經常要經過的一個步驟——詳細設計,在詳細設計中定義了數據模型,有哪些接口,以及頁面的長相。比較簡單,并沒有對服務器端Python項目的結構進行設計,也沒有對前端文件的組織進行規劃。但是這個詳細設計的目的是達到了,在我個人的實際經驗中,項目的開發很少有按照軟件工程中的瀑布模型來做的。大部分都是明確階段目標,然后開發,然后再次明確,再次開發,不斷迭代。
因此說到前篇文章的目的也就是指導后端開發提供一個什么樣的接口,輸出什么樣的數據結構。也是指導前端應該怎么獲取數據。在日常工作中,設計到前后端甚至是服務器和客戶端的這種模式也是一樣,兩方人員在項目初期只要協定好接口和數據結構就行,隨著項目的進行不斷的調整。當然這種情況是說內部人員之間的溝通,如果是和外部人員的溝通情況就不一樣了。
回到正題,后端的開發主要功能是提供接口,要提供哪些接口一定定義好了,模型也建立好了,現在需要做的就是搭一個項目,實現接口。
## 13.1 項目結構
項目使用了webpy這個微型的python框架,項目結構是依然按照之前對todos進行服務器端擴展時的結構,?[onlinetodos](https://github.com/the5fire/onlinetodos)
~~~
.
├── __init__.py
├── handlers.py
├── init_sqlite.py
├── models.py
├── server.py
├── static
│?? ├── css
│?? │?? ├── body.css
│?? │?? └── semantic.min.css
│?? ├── img
│?? │?? └── bg.jpg
│?? └── js
│?? ├── backbone.js
│?? ├── jquery.js
│?? ├── json2.js
│?? └── underscore.js
├── templates
│?? └── index.html
└── wechat.db
~~~
可以先忽略其中的靜態文件的部分,下一章實現的時候會進行調整。只說后端的實現,主要分為三個部分:server部分、handler部分、和models部分,也就是上面對應的名字,這些名字本身就說明了這部分的功能。server主要是啟動一個web服務器,其中進行了url的定義,對應url接受到的請求會傳遞到handlers中對應的類方法中,在方法中會調用Models中的Model類來獲取數據,然后再返回給客戶端(瀏覽器)。
## 13.2 server部分詳解
這部分功能上已經介紹了,這里貼出代碼詳細介紹:
~~~
#!/usr/bin/env python
#coding:utf-8
import web
from web.httpserver import StaticMiddleware
urls = (
'/', 'IndexHandler', # 返回首頁
'/topic', 'TopicHandler',
'/topic/(\d+)', 'TopicHandler',
'/message', 'MessageHandler',
'/user', 'UserHandler',
'/user/(\d+)', 'UserHandler',
'/login', 'LoginHandler',
'/logout', 'LogoutHandler',
)
app = web.application(urls, globals())
application = app.wsgifunc(StaticMiddleware)
if web.config.get('_session') is None:
session = web.session.Session(
app,
web.session.DiskStore('sessions'),
initializer={'login': False, 'user': None}
)
web.config._session = session
from handlers import ( # NOQA
IndexHandler, RegisteHandler,
LoginHandler, LogoutHandler,
TopicHandler, MessageHandler
)
def main():
app.run()
if __name__ == "__main__":
main()
~~~
這里首先是定義了url對應的handlers中的類,然后通過webpy的靜態文件Middleware來處理靜態文件的請求,接著初始化了項目的session。最后從handlers中引入所有用到的Handler。這里需要注意的是,handlers的引入需要在session定義的下面,因為handlers中需要用到session。
## 13.3 handler中的邏輯
這里面主要邏輯是處理來自瀏覽器對相應的url的請求,因為項目需要處理用戶登錄,因此要引入前面定義的session來保存用戶的狀態。
來看代碼:
~~~
#coding:utf-8
import copy
import json
import hashlib
import sqlite3
from datetime import datetime
import web
from models import Message, User, Topic
session = web.config._session
CACHE_USER = {}
def sha1(data):
return hashlib.sha1(data).hexdigest()
def bad_request(message):
raise web.BadRequest(message=message)
# 首頁
class IndexHandler:
def GET(self):
render = web.template.render('templates/')
return render.index()
class UserHandler:
def GET(self):
# 獲取當前登錄的用戶數據
user = session.user
return json.dumps(user)
def POST(self):
data = web.data()
data = json.loads(data)
username = data.get("username")
password = data.get("password")
password_repeat = data.get("password_repeat")
if password != password_repeat:
return bad_request('兩次密碼輸入不一致')
user_data = {
"username": username,
"password": sha1(password),
"registed_time": datetime.now(),
}
try:
user_id = User.create(**user_data)
except sqlite3.IntegrityError:
return bad_request('用戶名已存在!')
user = User.get_by_id(user_id)
session.login = True
session.user = user
result = {
'id': user_id,
'username': username,
}
return json.dumps(result)
class LoginHandler:
def POST(self):
data = web.data()
data = json.loads(data)
username = data.get("username")
password = data.get("password")
user = User.get_by_username_password(
username=username,
password=sha1(password)
)
if not user:
return bad_request('用戶名或密碼錯誤!')
session.login = True
session.user = user
result = {
'id': user.get('id'),
'username': user.get('username'),
}
return json.dumps(result)
class LogoutHandler:
def GET(self):
session.login = False
session.user = None
session.kill()
return web.tempredirect('/#login')
class TopicHandler:
def GET(self, pk=None):
if pk:
topic = Topic.get_by_id(pk)
return json.dumps(topic)
topics = Topic.get_all()
result = []
for t in topics:
topic = dict(t)
try:
user = CACHE_USER[t.owner_id]
except KeyError:
user = User.get_by_id(t.owner_id)
CACHE_USER[t.owner_id] = user
topic['owner_name'] = user.username
result.append(topic)
return json.dumps(result)
def POST(self):
if not session.user or not session.user.id:
return bad_request('請先登錄!')
data = web.data()
data = json.loads(data)
topic_data = {
"title": data.get('title'),
"owner_id": session.user.id,
"created_time": datetime.now(),
}
try:
topic_id = Topic.create(**topic_data)
except sqlite3.IntegrityError:
return bad_request('你已創建過該名稱!')
result = {
"id": topic_id,
"title": topic_data.get('title'),
"owner_id": session.user.id,
"owner_name": session.user.username,
"created_time": str(topic_data.get('created_time')),
}
return json.dumps(result)
def PUT(self, obj_id=None):
pass
def DELETE(self, obj_id=None):
pass
class MessageHandler:
def GET(self):
topic_id = web.input().get('topic_id')
if topic_id:
messages = Message.get_by_topic(topic_id) or []
else:
messages = Message.get_all()
result = []
current_user_id = session.user.id
for m in messages:
try:
user = CACHE_USER[m.user_id]
except KeyError:
user = User.get_by_id(m.user_id)
CACHE_USER[m.user_id] = user
message = dict(m)
message['user_name'] = user.username
message['is_mine'] = (current_user_id == user.id)
result.append(message)
return json.dumps(result)
def POST(self):
data = web.data()
data = json.loads(data)
if not (session.user and session.user.id):
return bad_request("請先登錄!")
message_data = {
"content": data.get("content"),
"topic_id": data.get("topic_id"),
"user_id": session.user.id,
"created_time": datetime.now(),
}
m_id = Message.create(**message_data)
result = {
"id": m_id,
"content": message_data.get("content"),
"topic_id": message_data.get("topic_id"),
"user_id": session.user.id,
"user_name": session.user.username,
"created_time": str(message_data.get("created_time")),
"is_mine": True,
}
return json.dumps(result)
~~~
別看代碼這么多,所有的具體的Handler的處理邏輯都是一樣的——接受post請求,驗證用戶狀態,存儲;或者是接受get請求,調用Model獲取數據,組織成json,然后返回。相當簡單了,對吧。
## 13.4 models中的實現
這部分功能就是現實數據庫的增刪改查,行為基本一致,因此提出一個基礎類來完成基本的操作。如果基礎類滿足不了需求,需要在各子類中實現自己的邏輯。
來看下實現代碼:
~~~
#coding:utf-8
import web
db = web.database(dbn='sqlite', db="wechat.db")
class DBManage(object):
@classmethod
def table(cls):
return cls.__name__.lower()
@classmethod
def get_by_id(cls, id):
itertodo = db.select(cls.table(), where="id=$id", vars=locals())
return next(iter(itertodo), None)
@classmethod
def get_all(cls):
# inspect.ismethod(cls.get_all)
return db.select(cls.table())
@classmethod
def create(cls, **kwargs):
return db.insert(cls.table(), **kwargs)
@classmethod
def update(cls, **kwargs):
db.update(cls.table(), where="id=$id", vars={"id": kwargs.pop('id')}, **kwargs)
@classmethod
def delete(cls, id):
db.delete(cls.table(), where="id=$id", vars=locals())
class User(DBManage):
id = None
username = None
password = None
registed_time = None
@classmethod
def get_by_username_password(cls, username, password):
itertodo = db.select(cls.table(), where="username=$username and password=$password", vars=locals())
return next(iter(itertodo), None)
class Topic(DBManage):
id = None
title = None
created_time = None
owner = None
class Message(DBManage):
id = None
content = None
top_id = None
user_id = None
reply_to = None
@classmethod
def get_by_topic(cls, topic_id):
return db.select(cls.table(), where="topic_id=$topic_id", vars=locals())
~~~
在操作的同時還是定義了模型的屬性,不過目前并沒有用的上,如果打算進一步抽象的話是要用到的。
## 13.5 總結
整個后端的實現并不復雜,只是簡單的數據庫CRUD操作,也沒有進行更深一步的抽象,不過滿足接口需求就好,等前端實現的時候可能需要調整。
這個項目已經托管在github上了:?[wechat](https://github.com/the5fire/wechat)?,歡迎圍觀以及貢獻代碼。
- 關于
- 前言
- 第一章 Hello Backbonejs
- 第二章 Backbonejs中的Model實踐
- 第三章 Backbonejs中的Collections實踐
- 第四章 Backbonejs中的Router實踐
- 第五章 Backbonejs中的View實踐
- 第六章 實戰演練:todos分析(一)
- 第七章 實戰演練:todos分析(二)View的應用
- 第八章 實戰演練:todos分析(三)總結
- 第九章 后端環境搭建:web.py的使用
- 第十章 實戰演練:擴展todos到Server端(backbonejs+webpy)
- 第十一章 前后端實戰演練:Web聊天室-功能分析
- 第十二章 前后端實戰演練:Web聊天室-詳細設計
- 第十三章 前后端實戰演練:Web聊天室-服務器端開發
- 第十四章 前后端實戰演練:Web聊天室-前端開發
- 第十五章 引入requirejs
- 第十六章 補充異常處理
- 第十七章 定制Backbonejs
- 第十八章 再次總結的說
- Backbonejs相關資源