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('

Session Demo

'

'

Count: '+str(count)+'
'

'

reset

')

@app.get('/reset')

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 的扩展开发流程,就先止步于此吧。