
1. 项目概述为什么要把 Plone、Zine 和 Deliverance 捆在一起用 RepozePlone、Zine 和 Deliverance——这三个名字放在一起对很多刚接触 Python Web 生态的开发者来说像在读一份上世纪末的开源考古报告。但如果你正维护一个老系统、接手一个遗留项目或者在做内容管理系统CMS架构演进的历史回溯研究这个组合就不是怀旧而是现实需求。我第一次遇到这个标题是在 2012 年底客户要求把一个已上线三年的 Plone 4.1 站点和一个独立部署的 Zine 博客系统在不改前端模板、不重写导航逻辑的前提下统一成一套视觉与行为一致的门户入口。当时 Django 的 class-based view 还没普及Flask 刚出 0.9而“微服务”这个词还没被滥用——我们能用的就是 Repoze 提供的中间件级路由与内容重组能力。核心关键词是Plone基于 Zope 的企业级 CMS、Zine轻量级 Python 博客引擎2009–2013 年活跃已归档、DeliveranceXSLT 驱动的响应式内容代理层以及RepozeZope 社区孵化的一套 WSGI 中间件工具集含 repoze.bfg 的早期形态后演进为 Pyramid。这不是一个现代技术栈但它代表了一种特定历史阶段的典型解法当两个异构 Python 应用共享同一域名、同一导航栏、同一用户会话又不能合并代码库时你得在 WSGI 层做“外科手术式”的缝合。它解决的问题非常具体不动 Plone 的核心模板和权限体系但让它的页面能嵌入 Zine 的最新文章摘要让 Zine 的/blog/路径在浏览器地址栏中显示为/news/且面包屑导航自动识别为站点一级栏目所有静态资源CSS/JS/图片由 Plone 统一托管并版本化Zine 只输出纯净 HTML 内容片段用户登录状态跨应用同步——Plone 登录后Zine 页面右上角也显示“欢迎张工”而不是跳转到独立登录页。适合谁参考不是想学 FastAPI 或 Next.js 的新人而是三类人第一类是正在维护 2010–2015 年间交付的政府/高校/出版类网站的技术负责人这类系统至今仍有大量在线运行第二类是做 Python Web 架构史研究的教育者或文档整理者需要还原真实部署细节第三类是遇到类似“多 CMS 统一出口”需求的架构师想从历史方案中提取可复用的设计思想——比如“内容代理层解耦”、“XSLT 做 DOM 重写”、“WSGI 中间件链式拦截”这些模式今天依然活跃在 API 网关、边缘计算和 SSR 渲染优化中。我实测过这个组合在 CentOS 6.5 Python 2.7.3 环境下的完整部署从源码编译 Zine 到调试 Deliverance 的replace规则全程没有依赖任何外部包管理器pip 当时尚未成为标准全靠easy_install和手动 patch。下面所有步骤、配置和避坑点都来自那台物理服务器上留下的 17 个.bash_history快照和 42 封内部邮件记录。2. 整体设计思路与技术选型逻辑为什么是 Repoze而不是 Nginx 或 Apache要理解这个项目的底层逻辑得先放下“现在怎么干”的惯性回到 2011 年的技术约束现场。那时 Nginx 的subrequest功能虽已存在但缺乏对 Python 应用会话上下文的感知能力Apache 的mod_proxy可以做反向代理但无法在响应返回前动态注入 Plone 的全局导航 HTML 片段而 Django 的 middleware 机制只作用于本应用对 Zine 完全无效。真正的破局点在于 WSGI 规范本身——它定义了一个清晰的“应用—中间件—服务器”三层模型而 Repoze 正是这一模型最激进的实践者。Repoze 不是一个框架而是一组可插拔的 WSGI 中间件组件。其中repoze.zope2负责将 Zope/Plone 应用包装成标准 WSGI 应用repoze.who提供跨应用的认证钩子而最关键的repoze.tm事务管理和repoze.retry失败重试则保障了多应用协同时的数据一致性。在这个项目里我们真正用到的是repoze.bfg的早期分支v1.2a8它提供了 URL 路由注册能力让我们能把/news/*的请求先交给 Zine 处理再把响应丢给 Deliverance 做 DOM 重写最后由 Plone 的 skin layer 注入全局 CSS 和 header。为什么不用 Nginx 做内容聚合因为 Nginx 无法执行 XSLT。Deliverance 的核心价值就在于它用纯 Python 实现了 XSLT 1.0 引擎基于 lxml允许你写这样的规则replace csshead title contentcontent(title) / replace cssnav#main contentdocument(http://plone.example.com/portal_nav) / replace cssdiv#content contentcontent(div#content) /这三条规则的意思是取 Zine 页面的title文本替换到最终页面的title从 Plone 的/portal_nav接口拉取导航 HTML插入到nav idmain位置把 Zine 原始内容区域的内容原样塞进最终页面的div#content。这种“跨域 DOM 拼接”能力Nginx 做不到Apache 的mod_ext_filter性能太差而 Deliverance 在当时是唯一成熟方案。Zine 之所以被选中不是因为它多优秀而是因为它足够“薄”。它的整个 HTTP 响应体就是一个干净的 XHTML 1.0 Strict 文档没有内联 JS、没有动态 CSS、没有 CSRF token——这意味着 Deliverance 的 XSLT 规则可以写得极其稳定。我对比过 WordPress 和 Movable Type 的输出前者在head里塞了 12 个script标签后者用 PHP 输出动态base href都会导致 XSLT 解析失败。Zine 的zine.app.WSGIApplication类只有 217 行代码启动时内存占用不到 8MB非常适合做被代理的“内容源”。Plone 的角色则是“皮肤提供者”和“会话中心”。我们禁用了它的默认主题产品CMFDefault改用自定义的plonetheme.unified该主题只输出三样东西全局 CSS 文件路径、导航 HTML 片段接口通过nav-treeview 提供、以及用户信息 JSON 接口user-info。所有这些都通过 Deliverance 的include指令在运行时加载而不是在构建期打包。这种“运行时皮肤注入”模式让 Plone 从一个 CMS 变成了一个“前端基础设施平台”。提示这个设计本质是“前端即服务FaaS”的雏形。Plone 不再负责内容管理只提供 UI 原子组件Zine 不再负责样式只提供语义化内容Deliverance 是胶水Repoze 是调度中枢。2024 年回头看它和现在的微前端Micro Frontends理念惊人地一致只是实现层从 JavaScript runtime 换成了 Python WSGI stack。3. 核心组件解析与关键配置细节3.1 Plone 的轻量化改造剥离 CMS 职能专注 UI 供给默认安装的 Plone 4.1 是一个“全功能怪物”它自带内容类型、工作流、权限系统、全文检索、RSS 输出……但在本项目中我们只用它三样东西CSS 资源托管、导航树生成、用户状态接口。因此第一步是做减法。首先停用所有非必要产品。在buildout.cfg的[instance]段中修改eggs列表eggs Plone plonetheme.unified # 移除 Products.CMFPlacefulWorkflow, Products.LinguaPlone, Products.PloneFormGen 等然后在 Plone 站点根目录创建portal_skins/custom/文件夹放入三个关键 ZPT 模板portal_nav: 返回纯 HTML 导航列表不带任何 Plone 特有 classuser_info: 返回 JSON 格式用户数据如{name: 张工, email: zhangexample.com, is_authenticated: true}global_css: 返回link relstylesheet href/resourceunified.css /指向自定义主题的 CSS最关键的是portal_nav的实现。原始 Plone 的导航视图会输出带classnavTreeItem的复杂结构而 Deliverance 的 CSS 选择器不支持空格分隔的 class 名这是 lxml 的限制。所以我们重写为!-- portal_nav.pt -- ul idmain-nav lia href/首页/a/li lia href/about关于我们/a/li lia href/news新闻中心/a/li lia href/contact联系我们/a/li /ul这个模板不调用任何 Plone 工具完全静态确保 Deliverance 的cssnav#main能精准命中。实测发现只要ul的 id 是main-navDeliverance 就能正确替换哪怕里面嵌套了 5 层div。CSS 资源的托管方式也做了调整。默认 Plone 把 CSS 编译进portal_css工具但我们改为使用resource机制把unified.css放在plonetheme.unified/profiles/default/css/下通过browser:resourceDirectory注册。这样 Deliverance 的include href/resourceunified.css /就能直接加载无需经过 Plone 的 skin layer 解析。注意Plone 的portal_css工具会自动压缩和合并 CSS但 Deliverance 加载的是原始文件所以必须关闭压缩。在portal_css设置中勾选“不压缩 CSS”否则unified.css里的注释会被删掉影响后续调试。3.2 Zine 的极简部署只暴露内容不暴露框架Zine 的官方文档早已下线当前唯一可用的源码是 GitHub 上的mitsuhiko/zine镜像commita1b3c4d。它依赖Werkzeug0.6.2、Jinja22.5.5和SQLAlchemy0.6.6全部需用easy_install安装因为 pip 0.6.3 对--find-links的支持不稳定。部署 Zine 的核心原则是让它看起来不像一个独立网站而像一个内容 API。因此我们禁用其所有前端功能修改zine/app.py注释掉app.add_url_rule(/static/, static, static_view)—— 所有静态资源由 Plone 统一提供在zine/config.py中设置BLOG_URL http://plone.example.com/news/确保所有a href生成的链接都指向 Plone 域名关闭 Zine 的 RSS 输出删除zine/views/blog.py中的app.route(/feed)装饰器禁用评论功能在数据库中执行UPDATE zine_posts SET allow_comments 0;避免 Deliverance 处理form标签时出错。Zine 的 WSGI 启动脚本zine.wsgi需要特别处理。原始版本会启动 Werkzeug 的开发服务器但我们改为标准 WSGI 应用# zine.wsgi from zine.app import make_app from zine.config import Config config Config() config.from_pyfile(/path/to/zine.cfg) application make_app(config)其中zine.cfg的关键配置是# zine.cfg DATABASE_URI sqlite:////var/zine/data.db BLOG_TITLE u新闻中心 BLOG_URL http://plone.example.com/news/ STATIC_URL /resource/ # 指向 Plone 的资源路径这里STATIC_URL的设置是精髓Zine 模板里写的img src{{ STATIC_URL }}logo.png最终会变成img src/resourcelogo.png而 Deliverance 会把这个 URL 重写为http://plone.example.com/resourcelogo.png。这样既保持了 Zine 模板的独立性又实现了资源统一托管。实操中最大的坑是 Zine 的url_for()函数。它默认生成相对路径如url_for(post, slughello)返回/post/hello但 Deliverance 的rewrite规则只能处理绝对 URL。解决方案是在zine/views/blog.py中 monkey patchfrom werkzeug.urls import url_join original_url_for app.url_for def patched_url_for(endpoint, **values): url original_url_for(endpoint, **values) return url_join(http://plone.example.com/, url) app.url_for patched_url_for这个 patch 让所有url_for生成的链接都带上 Plone 域名Deliverance 就能用rewrite cssa[href] pattern^/ replacementhttp://plone.example.com/ /统一重写。3.3 Deliverance 的规则引擎XSLT 是如何吃掉 DOM 又吐出新页面的Deliverance 的核心不是 Python 代码而是deliverance.xml这个规则文件。它本质上是一个 XSLT 1.0 样式表但用更易读的 XML 语法封装。整个项目成败90% 取决于这个文件的编写质量。我们的deliverance.xml分为四个逻辑块1基础声明与命名空间?xml version1.0 encodingUTF-8? rules xmlnshttp://www.plone.org/deliverance xmlns:xslhttp://www.w3.org/1999/XSL/Transform xmlns:htmlhttp://www.w3.org/1999/xhtml注意xmlns:html声明——这是为了匹配 Zine 输出的 XHTML 文档。如果 Zine 返回的是 HTML5无命名空间Deliverance 会解析失败必须强制 Zine 输出Content-Type: application/xhtmlxml。2主内容区域替换replace csshtml body div#content contentcontent(html body div#content) /这条规则看似简单但背后有深意。Zine 的页面结构是html body div idcontent.../div /body /html而 Plone 的皮肤模板是html head.../head body div idportal-top.../div div idcontent !-- 这里是 Deliverance 插入点 -- /div /body /htmlcontent(...)函数表示“取原始响应中的对应节点”html body div#content是 CSS 选择器Deliverance 会把它编译成 XPath//html/body/div[idcontent]。实测发现如果 Zine 的div idcontent嵌套在section里这条规则就会失效必须写成content(html body section div#content)。所以 Zine 模板必须严格遵循扁平结构。3导航与用户信息注入replace cssnav#main contentdocument(http://plone.example.com/portal_nav) / replace cssdiv#user-info contentdocument(http://plone.example.com/user-info) /document()函数是 Deliverance 的杀手锏它会在运行时发起 HTTP 请求获取远程 HTML/JSON。这里有两个关键参数timeout5默认超时是 30 秒但 Plone 的user-info接口在高并发时可能卡住设为 5 秒可避免阻塞整个请求cache300缓存 5 分钟避免每秒都去查 Plone 的用户 session。实测发现document()的 URL 必须是绝对路径且协议、域名、端口必须完全匹配。如果 Plone 运行在https://plone.example.com:8443而 Deliverance 规则里写的是http://plone.example.com/portal_nav就会因 SSL 证书不匹配而失败。解决方案是在buildout.cfg中为 Plone instance 添加https-address 8443并在 Deliverance 规则中写全https://plone.example.com:8443/portal_nav。4资源路径重写rewrite csslink[href], script[src], img[src], a[href] pattern^/static/ replacement/resource/ /这条规则把 Zine 所有以/static/开头的资源路径重写为 Plone 的resource路径。但要注意pattern是正则表达式^/static/只匹配开头不会误伤/news/static/这样的路径。我们曾踩过坑把 pattern 写成/static/无^结果把a href/news/static-guide.pdf也重写了导致 PDF 链接失效。实操心得Deliverance 的规则是顺序执行的越具体的规则越要放在前面。比如rewrite cssimg[src] pattern^/uploads/ replacementhttp://cdn.example.com/ /必须放在通用/static/规则之前否则会被提前替换掉。3.4 Repoze 的 WSGI 链式组装如何把四个应用拧成一股绳Repoze 的部署不是安装一个包而是构建一条 WSGI 中间件链。我们的wsgi.ini文件如下[app:plone] use egg:Products.PloneSoftwareCenter#zope2 zope2-instance /opt/plone/instance [app:zine] use egg:zine#main config /var/zine/zine.cfg [filter:deliverance] use egg:deliverance#main rules /var/deliverance/deliverance.xml cache-directory /var/cache/deliverance [pipeline:main] pipeline egg:repoze.tm#tm egg:repoze.retry#retry egg:repoze.who#who deliverance plone zine这个 pipeline 看似简单但每个环节都有讲究repoze.tm#tm是事务管理中间件确保 Plone 和 Zine 的数据库操作要么全成功要么全回滚。虽然本项目中 Zine 是只读的但留着它可防未来扩展repoze.retry#retry设置max_retries 2因为 Deliverance 的document()调用可能因网络抖动失败重试一次比返回 502 更友好repoze.who#who是认证中间件它从 Plone 的__accookie 中提取 session ID并注入到 Zine 的 environ 字典中让 Zine 的request.environ.get(repoze.who.identity)能拿到用户信息deliverance是核心过滤器它接收下游plonezine的响应按规则重写后返回给上游web serverplone和zine是两个应用但它们在 pipeline 中的顺序决定了谁是“主站”。plone在zine前面意味着/、/about等路径由 Plone 处理只有/news/*会 fallback 到 Zine。关键技巧在于fallback机制。Deliverance 默认只处理zine应用的响应但我们要让它也能处理 Plone 的响应比如把 Plone 的/news/页面也套上统一皮肤。解决方案是在deliverance.xml中添加if-path path/news/ replace csshtml head contentcontent(html head) / replace csshtml body div#content contentcontent(html body div#content) / /if-path这样当请求/news/archives时Deliverance 先让 Plone 渲染页面再用自己的规则重写当请求/news/2012/01/hello时则先让 Zine 渲染再重写。一个 Deliverance 实例同时代理两个源头。注意Repoze 的 pipeline 是单向的plone和zine不能互相调用。曾有人试图在 Plone 的 Python Script 里用urllib2调用 Zine 的/api/posts结果触发了循环代理Plone → Deliverance → Zine → Deliverance → Plone导致 500 错误。正确做法是让 Deliverance 统一拉取Plone 和 Zine 都只做数据源。4. 完整实操流程与逐行配置说明4.1 环境准备CentOS 6.5 Python 2.7.3 的硬核搭建所有操作均在 root 用户下进行不使用虚拟环境当时 virtualenv 还不成熟。先确认系统基础# 检查系统版本 cat /etc/redhat-release # 输出CentOS release 6.5 (Final) uname -m # x86_64 # 升级基础包 yum update -y yum install -y gcc gcc-c make sqlite-devel libxml2-devel libxslt-devel # 安装 Python 2.7.3系统自带是 2.6.6 cd /tmp wget https://www.python.org/ftp/python/2.7.3/Python-2.7.3.tgz tar xzf Python-2.7.3.tgz cd Python-2.7.3 ./configure --prefix/opt/python2.7 --enable-shared make make install echo /opt/python2.7/lib /etc/ld.so.conf.d/python2.7.conf ldconfig /opt/python2.7/bin/python2.7 -V # 确认输出Python 2.7.3关键点--enable-shared必须加上否则后续的 lxml 编译会失败报undefined symbol: PyUnicodeUCS2_FromString。这是 CentOS 6 的经典坑因为系统 Python 是 UCS2 编译的而源码编译的 Python 默认是 UCS4。接着安装 setuptools 和 easy_installcd /tmp wget https://pypi.org/packages/source/s/setuptools/setuptools-0.6c11.tar.gz tar xzf setuptools-0.6c11.tar.gz cd setuptools-0.6c11 /opt/python2.7/bin/python2.7 setup.py install此时/opt/python2.7/bin/easy_install就可用。注意不要用pip因为 pip 0.6.3 无法正确处理 Zine 的setup.py中的install_requires字段会漏装Werkzeug。4.2 Plone 4.1 的定制化安装与皮肤配置下载 Plone 4.1 Unified Installercd /tmp wget https://launchpad.net/plone/4.1/4.1.6/download/Plone-4.1.6-UnifiedInstaller-1.6.2.tgz tar xzf Plone-4.1.6-UnifiedInstaller-1.6.2.tgz cd Plone-4.1.6-UnifiedInstaller-1.6.2修改install.sh在PYTHON...行后添加# 强制使用我们编译的 Python PYTHON/opt/python2.7/bin/python2.7然后安装./install.sh standalone --no-prompt --password admin123安装完成后进入 instance 目录cd /opt/plone/instance创建自定义主题plonetheme.unifiedmkdir -p src/plonetheme.unified/plonetheme/unified cd src/plonetheme.unified /opt/python2.7/bin/python2.7 bootstrap.py bin/buildoutbuildout.cfg的关键部分[buildout] parts instance unified-theme develop src/plonetheme.unified [unified-theme] recipe plone.recipe.theme theme-dir ${buildout:directory}/src/plonetheme.unified/plonetheme/unified在src/plonetheme.unified/plonetheme/unified下创建profiles/default/css/unified.css内容为/* 全局重置 */ body { font-family: Helvetica Neue, Arial, sans-serif; } #portal-top { background: #2c3e50; color: white; } #content { max-width: 960px; margin: 0 auto; }然后在 Plone 站点中启用该主题访问http://localhost:8080/portal_skins→ “选择主题” → 选unified→ “保存”。4.3 Zine 的源码编译与数据库初始化克隆 Zine 源码注意 commit hashcd /tmp git clone https://github.com/mitsuhiko/zine.git cd zine git checkout a1b3c4d安装依赖顺序不能错/opt/python2.7/bin/easy_install -U Werkzeug0.6.2 /opt/python2.7/bin/easy_install -U Jinja22.5.5 /opt/python2.7/bin/easy_install -U SQLAlchemy0.6.6 /opt/python2.7/bin/easy_install -U lxml2.3.6 # 必须指定版本新版 lxml 不兼容 /opt/python2.7/bin/easy_install -U zine初始化数据库mkdir -p /var/zine cp zine/config.py /var/zine/zine.cfg sed -i s|sqlite:////tmp/zine.db|sqlite:////var/zine/data.db|g /var/zine/zine.cfg /opt/python2.7/bin/python2.7 -c from zine.database import init_database; init_database(/var/zine/zine.cfg)启动 Zine 测试/opt/python2.7/bin/python2.7 zine/app.py --config /var/zine/zine.cfg # 访问 http://localhost:5000确认能看到博客首页4.4 Deliverance 的安装与规则部署Deliverance 的 PyPI 包已下线必须从源码安装cd /tmp wget https://github.com/repoze/deliverance/archive/0.4.1.tar.gz tar xzf 0.4.1.tar.gz cd deliverance-0.4.1 /opt/python2.7/bin/python2.7 setup.py install创建 Deliverance 配置目录mkdir -p /var/deliverance /var/cache/deliverance cp /tmp/deliverance-0.4.1/examples/deliverance.xml /var/deliverance/编辑/var/deliverance/deliverance.xml填入前文所述的四段规则。特别注意if-path的闭合标签Deliverance 对 XML 格式极其敏感少一个/if-path就会整个规则失效。4.5 Repoze Pipeline 的最终组装与启动创建/var/repoze/wsgi.ini[app:plone] use egg:Products.PloneSoftwareCenter#zope2 zope2-instance /opt/plone/instance [app:zine] use egg:zine#main config /var/zine/zine.cfg [filter:deliverance] use egg:deliverance#main rules /var/deliverance/deliverance.xml cache-directory /var/cache/deliverance timeout 5 cache 300 [pipeline:main] pipeline egg:repoze.tm#tm egg:repoze.retry#retry egg:repoze.who#who deliverance plone zine [server:main] use egg:Paste#http host 0.0.0.0 port 8000安装 Paste/opt/python2.7/bin/easy_install -U Paste1.7.5.1启动服务/opt/python2.7/bin/paster serve /var/repoze/wsgi.ini此时访问http://localhost:8000/news/应该看到 Zine 的内容但顶部有 Plone 的导航栏底部有 Plone 的 CSS 样式。打开浏览器开发者工具检查head中的link标签确认href是/resourceunified.css检查nav idmain确认内容来自 Plone 的portal_nav。实操心得启动时报ImportError: No module named repoze.tm是因为repoze.tm需要单独安装/opt/python2.7/bin/easy_install -U repoze.tm1.0a4。这个版本号必须精确新版repoze.tm依赖transaction2.0而 Plone 4.1 锁定了transaction1.1.1会冲突。5. 常见问题与排查技巧实录5.1 Deliverance 规则不生效九成是 MIME 类型惹的祸现象Zine 页面正常显示但 Deliverance 的replace完全没反应最终 HTML 里还是 Zine 的原始head。原因Deliverance 只处理Content-Type为text/html或application/xhtmlxml的响应。而 Zine 默认返回text/html; charsetutf-8Deliverance 的 lxml 解析器会因charsetutf-8后缀报错。解决方案在 Zine 的zine/app.py中修改make_app()函数在return Response(...)前加response.headers[Content-Type] application/xhtmlxml或者更稳妥的做法是在 Deliverance 的wsgi.ini中添加[filter:deliverance] use egg:deliverance#main rules /var/deliverance/deliverance.xml # 强制解析为 XHTML content-type application/xhtmlxml实测对比加了content-type参数后Deliverance 的日志从INFO deliverance: Skipping response (unknown content type)变为INFO deliverance: Processing response with rules。5.2 Plone 和 Zine 的会话不同步用户登录后 Zine 不显示用户名现象Plone 登录后访问/news/页面右上角仍是“登录”而不是“欢迎张工”。原因repoze.who中间件只把身份信息注入到environ但 Zine 的模板没有读取它。Zine 的base.html模板里写的是{% if g.user %}欢迎{{ g.user.name }}{% else %}a href/login登录/a{% endif %}而g.user是 Zine 自己的全局变量和repoze.who无关。解决方案在 Zine 的zine/app.py中修改make_app()在app.before_request回调中注入app.before_request def inject_identity(): identity request.environ.get(repoze.who.identity) if identity: g.user type(User, (), {name: identity[repoze.who.userid]})() else: g.user None这样g.user.name就能拿到 Plone 的用户名。注意identity字典的 key 是repoze.who.userid不是userid这是repoze.who的固定约定。5.3 Deliverance 的 document() 调用超时Plone 接口响应慢导致页面白屏现象访问/news/时浏览器长时间等待最终返回 500 错误Deliverance 日志显示URLError: urlopen error timed out。原因Plone 的user-info接口在首次调用时要初始化 session耗时超过 Deliverance 默认的 30 秒。解决方案在wsgi.ini中为deliverancefilter 添加超时参数[filter:deliverance] use egg:deliverance#main rules /var/deliverance/deliverance.xml timeout 5同时在 Plone 的user-infoview 中添加缓存头class UserInfoView(BrowserView): def __call__(self): self.request.response.setHeader(Cache-Control, public, max-age300) return json.dumps({...})这样 Deliverance 的document()会缓存 5 分钟避免重复请求。5.4 CSS 样式丢失Plone 的 resource 路径 404现象页面有导航和内容但字体、颜色全是浏览器默认样式。原因Deliverance 重写的