首页 > temp > python入门教程 >
-
我自己的 Python Web 框架
原文地址: https://healeycodes.com/my-own-python-web-framework
在过去的几个月里,我一直在从头开始建立我自己的软件工具--像编程语言、文本编辑器和CLI工具。在周末,我建立了一个概念验证的网络框架,通过Build Output API部署到Vercel。
一个基于文件系统的规范,允许任何框架为Vercel构建,并利用Vercel的基础设施构建块,如边缘函数、边缘中间件、增量静态再生(ISR)、图像优化等。
Jar是一个玩具Python网络框架,用大约200行代码实现(见 cli.py )。我建立它是为了探索一些围绕框架API的想法,并从作者方面探索框架。请不要真的使用它。它之所以被称为Jar,是因为它几乎没有任何功能,你需要自己去填充它!
它使用文件系统路由并支持。
-
建立页面,又称静态文件
-
刷新页面,又称无服务器功能
-
重新生成的页面,又称预渲染功能
Jar项目的结构是这样的:
project/
├─ pages/
│ ├─ index.py
├─ public/
│ ├─ favicon.ico
API理念
我对Jar的个人使用情况是在没有前台框架的情况下建立小型动态网站。受到Next.js的API的一点启发,比如 getServerSideProps
和 getStaticProps
,Jar的API是由三个函数签名定义的。
-
数据函数在构建页面和重新生成的页面时被调用。当它在服务器上被调用时,它会收到一个带有方法、路径、头信息和正文的请求对象。
-
render函数接收data函数的返回值,并返回一个
body
,info
的元组,其中的信息可以改变响应的状态代码和头文件。 -
配置函数定义了页面的类型(构建、新鲜或再生)。
这是一个生成页面的例子kitchen sink example:
import time
def render(data):
return f"<h1>Last regenerated at: {data['time']}</h1>", {}
def data(request=None):
return {
"time": time.time()
}
def config():
return {
"regenerate": {
"every": 5
}
}
因为我们是在Python领域,我希望API是灵活的。数据和配置函数是可选的(而且它们不需要接受任何参数)。因此,最小的Jar页面看起来像这样。
render = lambda: (“Hi! I'm a little page.”, {})
构建CLI
在对Jar的CLI进行原型设计时,Build Output API的文档和例子足够全面,我没有遇到任何重大问题。通过试验和错误,没过多久我就通过构建和部署真正的项目来测试Jar(从头到尾大约需要6秒钟)。
Jar需要在构建时和在服务器上渲染页面,并使用大量的动态导入和元编程来减少代码行和复杂性。
为了把用户编写的页面当作 Python 模块,在运行时要像这样导入。
module_location = "project/pages/index.py"
spec = importlib.util.spec_from_file_location("", module_location)
page = importlib.util.module_from_spec(spec)
spec.loader.exec_module(page)
# `page` can now be called like `page.render()`
这意味着动态导入的构建页面可以在构建时被调用以生成静态文件。
# `page` is a dynamically imported module e.g. it exists at `pages/index.py`
with open(os.path.join(build_dir, f".vercel/output/static/{request_path}"), "w") as f:
res = call_render(page)
f.write(res['body'])
build_config['overrides'][request_path] = {
'contentType': res['headers']['Content-Type']
}
为了创建新鲜和再生的页面,Jar创建了使用 python3.9
运行时的无服务器函数。用于创建构建页面的相同函数(例如 call_data
, call_render
)被写入一个处理文件,以便它们可以根据需要在服务器上运行。当我说相同的函数时,我的意思是它们是真的从内存中读取的。
def create_handler(path, module_location):
# the following functions are used at build time to generated build pages
# and are also used on the server to generated fresh/regenerated pages
# so we bundle them into a handler file
with open(path, "w") as f:
# imports
f.write("import json\nimport inspect\nimport importlib.util\n")
f.write('\n')
# request class
request_source = inspect.getsource(Request)
f.write(request_source)
f.write('\n')
# call_data function
call_data_source = inspect.getsource(call_data)
f.write(call_data_source)
f.write('\n')
# call_render function
call_render_source = inspect.getsource(call_render)
f.write(call_render_source)
f.write('\n')
# app function
app_source = inspect.getsource(app)
f.write(app_source.replace("__MODULE_LOCATION", module_location))
f.write('\n')
构建输出API要求像包这样的外部文件被包含在函数的文件系统中。
一个无服务器功能在文件系统中被表示为一个名称上带有 .func 后缀的目录,包含在 .vercel/output/functions 目录中。
从概念上讲,你可以把这个 .func 目录看作是无服务器功能的文件系统挂载: .func 目录以下的文件被包括在内(递归), .func 目录以上的文件则不包括在内。私人文件可以安全地放在这个目录中,因为它们不会被终端用户直接访问。然而,它们可以被无服务器功能执行的代码所引用。
在 .func 目录下必须包含一个名为 .vc-config.json 的配置文件,其中包含Vercel应该如何构建无服务器功能的信息。
在Jar中,所有的项目文件都被复制到每个函数目录中,以保持简单(更成熟的框架会分割和捆绑以避免每个函数的大小限制)。 .vc-config.json 文件对每个也是一样的。
{"handler": "__handler.app", "runtime": "python3.9", "environment": {}}
函数之间的唯一区别是处理程序在运行时导入的模块(又称页面文件)。
Jar中的一个新的/再生的页面与Serverless/Reperender函数一一对应。当一个请求进入Vercel的边缘网络时,它最终会被路由到处理文件,该文件调用相关页面的 data 和 render 函数,然后回复给客户端。
关于Vercel内部的一些进一步阅读:
-
Vercel基础设施的幕后花絮
-
Runtime Developer Reference
-
Build your own web framework
-
SvelteKit's adapter-vercel
-
Vercel's CLI vercel/vercel
文档
无论用户的规模或数量如何,我都喜欢为我的副项目编写文档。它记录了我的想法,帮助我捕捉任何粗糙的边缘,并给我完成项目最后 10% 所需的推动力。也意味着我以后可以随时把东西捡回来!
我为 Jar 写了文档……用 Jar!请在此处查看项目文件。文档使用 marko markdown 包和 Prism.js 进行语法高亮显示(所有 Jar 页面都是纯 Python,没有导入或特殊语法)。
Serverless/Prerender Functions 不知道其函数目录之外的任何内容,因此在使用第三方包时,需要将其安装在项目的根目录下。有一些成熟的方法可以使它正常工作(比如 Python 虚拟环境),但到目前为止我还没有遇到任何问题,只是通过使用 pip 的 --target 参数在本地安装包。
下面是一个示例,在构建和部署 Jar 文档网站的脚本中:
python3 framework/cli.py build examples/docs
# project packages must be installed locally
# so they are bundled correctly when deployed
cd examples/docs && pip3 install -r requirements.txt --target . && cd ../..
cd build && vercel --prebuilt --prod && cd ..
文档涵盖了这个问题,以及有关 API 的更多详细信息,以及每种页面类型的示例。
Tests
有一条有趣的公理说 everything is a compiler, a database, or a combination of both。 Web 框架绝对是编译器——测试编译器(应该具有确定性输出)的一种快速方法是快照测试。
Jar 的测试套件构建两个项目并对文件进行快照测试。对于真正的端到端测试,它可以部署然后卷曲它们以验证生产中的行为没有分歧。
说到确定性输出,我实际上遇到了一个错误,在 CI 中测试有时会失败。该错误是由于 Python 的 json.dumps 在序列化构建配置时如何对键进行排序。
这是我花了三十分钟才找到并修复的错误:
with open(os.path.join(build_dir, '.vercel/output/config.json'), 'w') as f:
- json.dump(build_config, f)
+ json.dump(build_config, f, sort_keys=True)
做完这个项目,从idea到production一路走来,感觉好像剥了几个计算层。我更喜欢 web 框架 → 编译器 → 生产的流程。