Sanic 是 Python 编程语言环境下的一个高性能异步 Web 开发框架,也是我一直想要学习和掌握的 Web 开发框架。只是待在 Laravel 的舒适圈久了,面对这种需要自己完善补充各种基础功能的「微框架」,缺乏用起来的行动力。今天忽然心血来潮想要再次体验一下 Sanic,趁着这股热乎劲儿,我决定写一个 Session 扩展练练手。
是的,你没看错。作为一个 Web 开发框架,Sanic 连 Session 功能都没提供,就是如此「简洁」。它有一个第三方的 Session 库,但这个库已经两年多没有更新,估计作者已经放弃了维护。这是选择用户热度不高的框架所要承担的代价,很多在其他框架上看起来本就该有的功能,到了这些冷门框架上就不得不自己来「造轮子」。所幸我目前只是抱着学习的目的,没有进度要求,所以压力不大。
在开始写这个扩展前,需要先了解一下 Sanic 框架提供的相关基础功能和 Session 处理的相关流程。有 Web 开发基础的朋友应该都了解,Session 无非就是用来识别并隔离当前访问用户的信息。这个识别的基础离不开 Cookie。所以从流程上来看,主要就是以下步骤:
- 浏览器携带 Cookie 发起请求。
- 框架接收到请求后从请求的 Cookie 中寻找设定的 Session ID。
- 如果找到了 Session ID,就从服务器存储的 Session 中找对应的数据,并取出来运行相应的程序逻辑。
- 如果没找到,就生成一个新的 Session ID,并运行相应的程序逻辑。最后返回 Cookie 信息给浏览器。
根据以上步骤,开始写代码。
from sanic import Sanic, redirect
from sanic.response import html
app = Sanic('zzxworld')
@app.get("
async def home(request):
count = request.ctx.session.get('count')
count = 0 if count is None else count + 1
request.ctx.session.set('count', count)
return html('<h1>Session Demo</h1>'
'<div><code>Count: '+str(count)+'</code></div>'
'<p><a href="
@app.get("
async def logout(request):
request.ctx.session.remove()
return redirect("
使用 sanic app
命令运行项目,然后在浏览器中访问 http://localhost:8000
,不出意外的话应该会出现一个错误页面。因为上面的代码目前还只是「伪代码」,request.ctx
后的 session
对象目前还不存在,也是接下来要完成的。不过从以上代码可以看出我最终要实现的目的:
- 首次访问这个程序时,页面的 Count 后会显示 0。
- 每刷新一次页面,Count 后的存储在 Session 中的数字会加 1。
- 点击 reset 连接会跳转到
/reset
页面,此页面负责清空 Session,然后再返回到主页面。此时 Count 后的数字再次归 0。
有了目标后,正式开始扩展开发。通过查阅 Sanic 的自定义扩展文档,了解到可以通过继承 sanic_ext
包中的 Extension
来添加扩展对象。其中 name
属性和 startup
方法是必须的。照猫画虎写一个:
from sanic_ext import Extension
from time import time
from datetime import datetime
import uuid
class Session(Extension):
"""zzxworld 的 Session 扩展"""
name="zzxSession"
_cookieName="sessid" # Cookie 中的 Session ID 命名
_store = None # Session 存储对象
def startup(self, bootstrap):
"""扩展入口"""
self._store = MemorySessionStore()
# 注册请求时的 session 绑定操作
self.app.request_middleware.appendleft(self.startSession)
# 注册请求结束后的 session 保存操作
self.app.response_middleware.append(self.saveSession)
def startSession(self, request):
"""给每个请求附加 session 操作对象"""
request.ctx.session = SessionItem(
self._store,
request.cookies.get(self._cookieName))
def saveSession(self, request, response):
"""在每个请求结束时保存 session 内容"""
lifeDatetime = None # Session 和相关 Cookie 的生命周期
# 获取 Cookie 中的 Session ID
sessionId = request.cookies.get(self._cookieName)
if sessionId is None:
# 没有找到 Session ID 时初始化新的 Cookie
sessionId = uuid.uuid4().hex # 随机生成一串唯一字符作为 Session ID
lifeSeconds = 3600 # 默认 1 小时有效期
lifeDatetime = datetime.fromtimestamp(time()+lifeSeconds)
response.add_cookie(self._cookieName, sessionId,
max_age = lifeSeconds,
expires = lifeDatetime,
secure = False)
# 保存当前请求中的 Session 数据
self._store.save(sessionId, request.ctx.session.get(), lifeDatetime)
以上代码关键部分都有注释,就不再赘述逻辑了。最后需要使用 sanic_ext
中的 Extend
注册扩展:
from sanic_ext import Extend
Extend.register(Session)
目前的 Session 扩展,注册了依然还是无法使用。注意看代码就会发现,其中还有两个对象没有完成,一个是 MemorySessionStore
,这个用来作为全局的 session 内容存储器。根据存储方式的区别,可以分别创建不同的存储器。比如想要把 session 存储在 Redis 中,就可以创建一个 RedisSessionStore
对象。另外一个需要完成的对象是 SessionItem
,它用来保存每次请求时的 session 操作,并提供一些 session 的操作接口。让我们先来完成 MemorySessionStore
对象:
class MemorySessionStore():
"""使用内存的 Session 存储器"""
_data = {}
def get(self, sessionId):
"""获取指定 Session ID 的内容"""
if sessionId in self._data:
return self._data[sessionId]['value']
return {}
def save(self, sessionId, sessionData, lifeDatetime=None):
"""保存指定 Session ID 的内容"""
if sessionId not in self._data:
self._data[sessionId] = {}
self._data[sessionId]['value'] = sessionData
if lifeDatetime is not None:
self._data[sessionId]['life_timestamp'] = lifeDatetime.timestamp()
最后是 SessionItem
对象:
class SessionItem():
"""基于每个请求的 Session 操作对象"""
_data = {}
def __init__(self, store, sessionId):
"""初始化 session 数据"""
self._data = store.get(sessionId)
def get(self, name = None):
"""获取指定名称或所有 session"""
if name:
return self._data[name] if name in self._data else None
else:
return self._data
def set(self, name, value):
"""设置指定名称的 sesion"""
self._data[name] = value
def remove(self, name = None):
"""删除指定名称或清空 session"""
if name:
del self._data[name]
else:
self._data = {}
把以上代码都组织到同一个文件里,然后运行 sanic
命令,打开浏览器测试一下效果。不出意外的话,每次刷新页面都会看到 Count 后的数字会加 1。同时打开一个新的浏览器试试,两边的结果应该互不干扰。
这个用于 Sanic 的 session 扩展至此算是基本满足了需求。如果要在实际项目中使用还需要进一步完善。比如放在 MemorySessionStore
中的 session 还需要一个清除过期数据的策略。更好的方式是用专业的 K/V 数据库来管理,比如 Redis。另外在设置 Cookie 的部分,参数都是「写死」的,最好能通过外部配置的方式来调整相关参数。不过本文的目的只是尝试体验 Sanic 的扩展开发流程,就先止步于此吧。