我自己的 Python Web 框架

打印 上一主题 下一主题

主题 747|帖子 747|积分 2241

原文地址: https://healeycodes.com/my-own-python-web-framework
在过去的几个月里,我一直在从头开始建立我自己的软件工具--像编程语言文本编辑器CLI工具。在周末,我建立了一个概念验证的网络框架,通过Build Output API部署到Vercel。
一个基于文件系统的规范,允许任何框架为Vercel构建,并利用Vercel的基础设施构建块,如边缘函数、边缘中间件、增量静态再生(ISR)、图像优化等。
Jar是一个玩具Python网络框架,用大约200行代码实现(见 cli.py )。我建立它是为了探索一些围绕框架API的想法,并从作者方面探索框架。请不要真的使用它。它之所以被称为Jar,是因为它几乎没有任何功能,你需要自己去填充它!
它使用文件系统路由并支持。
Jar项目的结构是这样的:
  1. project/
  2. ├─ pages/
  3. │  ├─ index.py
  4. ├─ public/
  5. │  ├─ favicon.ico
复制代码
API理念

我对Jar的个人使用情况是在没有前台框架的情况下建立小型动态网站。受到Next.js的API的一点启发,比如 getServerSideProps 和 getStaticProps ,Jar的API是由三个函数签名定义的。

  • 数据函数在构建页面和重新生成的页面时被调用。当它在服务器上被调用时,它会收到一个带有方法、路径、头信息和正文的请求对象。
  • render函数接收data函数的返回值,并返回一个 body, info 的元组,其中的信息可以改变响应的状态代码和头文件。
  • 配置函数定义了页面的类型(构建、新鲜或再生)。
这是一个生成页面的例子kitchen sink example:
  1. import time
  2. def render(data):
  3.     return f"<h1>Last regenerated at: {data['time']}</h1>", {}
  4. def data(request=None):
  5.     return {
  6.         "time": time.time()
  7.     }
  8. def config():
  9.     return {
  10.         "regenerate": {
  11.             "every": 5
  12.         }
  13.     }
复制代码
因为我们是在Python领域,我希望API是灵活的。数据和配置函数是可选的(而且它们不需要接受任何参数)。因此,最小的Jar页面看起来像这样。
  1. render = lambda: (“Hi! I'm a little page.”, {})
复制代码
构建CLI

在对Jar的CLI进行原型设计时,Build Output API的文档例子足够全面,我没有遇到任何重大问题。通过试验和错误,没过多久我就通过构建和部署真正的项目来测试Jar(从头到尾大约需要6秒钟)。
Jar需要在构建时和在服务器上渲染页面,并使用大量的动态导入和元编程来减少代码行和复杂性。
为了把用户编写的页面当作 Python 模块,在运行时要像这样导入。
  1. module_location = "project/pages/index.py"
  2. spec = importlib.util.spec_from_file_location("", module_location)
  3. page = importlib.util.module_from_spec(spec)
  4. spec.loader.exec_module(page)
  5. # `page` can now be called like `page.render()`
复制代码
这意味着动态导入的构建页面可以在构建时被调用以生成静态文件。
  1. # `page` is a dynamically imported module e.g. it exists at `pages/index.py`
  2. with open(os.path.join(build_dir, f".vercel/output/static/{request_path}"), "w") as f:
  3.     res = call_render(page)
  4.     f.write(res['body'])
  5.     build_config['overrides'][request_path] = {
  6.         'contentType': res['headers']['Content-Type']
  7.     }
复制代码
为了创建新鲜和再生的页面,Jar创建了使用 python3.9 运行时的无服务器函数。用于创建构建页面的相同函数(例如 call_data , call_render )被写入一个处理文件,以便它们可以根据需要在服务器上运行。当我说相同的函数时,我的意思是它们是真的从内存中读取的。
  1. def create_handler(path, module_location):
  2.     # the following functions are used at build time to generated build pages
  3.     # and are also used on the server to generated fresh/regenerated pages
  4.     # so we bundle them into a handler file
  5.     with open(path, "w") as f:
  6.         # imports
  7.         f.write("import json\nimport inspect\nimport importlib.util\n")
  8.         f.write('\n')
  9.         # request class
  10.         request_source = inspect.getsource(Request)
  11.         f.write(request_source)
  12.         f.write('\n')
  13.         # call_data function
  14.         call_data_source = inspect.getsource(call_data)
  15.         f.write(call_data_source)
  16.         f.write('\n')
  17.         # call_render function
  18.         call_render_source = inspect.getsource(call_render)
  19.         f.write(call_render_source)
  20.         f.write('\n')
  21.         # app function
  22.         app_source = inspect.getsource(app)
  23.         f.write(app_source.replace("__MODULE_LOCATION", module_location))
  24.         f.write('\n')
复制代码
构建输出API要求像包这样的外部文件被包含在函数的文件系统中。
一个无服务器功能在文件系统中被表示为一个名称上带有 .func 后缀的目录,包含在 .vercel/output/functions 目录中。
从概念上讲,你可以把这个 .func 目录看作是无服务器功能的文件系统挂载: .func 目录以下的文件被包括在内(递归), .func 目录以上的文件则不包括在内。私人文件可以安全地放在这个目录中,因为它们不会被终端用户直接访问。然而,它们可以被无服务器功能执行的代码所引用。
在 .func 目录下必须包含一个名为 .vc-config.json 的配置文件,其中包含Vercel应该如何构建无服务器功能的信息。
在Jar中,所有的项目文件都被复制到每个函数目录中,以保持简单(更成熟的框架会分割和捆绑以避免每个函数的大小限制)。 .vc-config.json 文件对每个也是一样的。
  1. {"handler": "__handler.app", "runtime": "python3.9", "environment": {}}
复制代码
函数之间的唯一区别是处理程序在运行时导入的模块(又称页面文件)。
Jar中的一个新的/再生的页面与Serverless/Reperender函数一一对应。当一个请求进入Vercel的边缘网络时,它最终会被路由到处理文件,该文件调用相关页面的 data 和 render 函数,然后回复给客户端。
关于Vercel内部的一些进一步阅读:
文档

无论用户的规模或数量如何,我都喜欢为我的副项目编写文档。它记录了我的想法,帮助我捕捉任何粗糙的边缘,并给我完成项目最后 10% 所需的推动力。也意味着我以后可以随时把东西捡回来!
我为 Jar 写了文档……用 Jar!请在此处查看项目文件。文档使用 marko markdown 包和 Prism.js 进行语法高亮显示(所有 Jar 页面都是纯 Python,没有导入或特殊语法)。
Serverless/Prerender Functions 不知道其函数目录之外的任何内容,因此在使用第三方包时,需要将其安装在项目的根目录下。有一些成熟的方法可以使它正常工作(比如 Python 虚拟环境),但到目前为止我还没有遇到任何问题,只是通过使用 pip 的 --target 参数在本地安装包。
下面是一个示例,在构建和部署 Jar 文档网站的脚本中:
  1. python3 framework/cli.py build examples/docs
  2. # project packages must be installed locally
  3. # so they are bundled correctly when deployed
  4. cd examples/docs && pip3 install -r requirements.txt --target . && cd ../..
  5. 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 在序列化构建配置时如何对键进行排序。
这是我花了三十分钟才找到并修复的错误:
  1. with open(os.path.join(build_dir, '.vercel/output/config.json'), 'w') as f:
  2. -     json.dump(build_config, f)
  3. +     json.dump(build_config, f, sort_keys=True)
复制代码
做完这个项目,从idea到production一路走来,感觉好像剥了几个计算层。我更喜欢 web 框架 → 编译器 → 生产的流程。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

没腿的鸟

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表