
1. 为什么这次主题升级不是“可选项”而是Plone项目生命周期的必经关卡如果你手头正维护一个运行了五六年、甚至更久的Plone站点它的主题还扎在portal_skins的土壤里——用老派的custom、mytheme皮肤层覆盖HTML结构靠一堆.pt模板文件硬生生拼出首页、栏目页和内容页那我得直说这个主题在Plone 5.2之后就进入了倒计时阶段。这不是危言耸听而是Plone核心团队从2014年就开始明确释放的技术信号portal_skins被标记为deprecated已弃用所有新功能开发、安全补丁、性能优化都绕开它走到了Plone 6它已被彻底移除。你今天拖着不升级明天遇到一个关键安全更新就得在“停站打补丁”和“强行兼容旧主题”之间二选一——而后者往往意味着自己重写半套渲染逻辑。Diazo不是另一个炫技工具它是Plone 5时代主题架构的操作系统级替换。它把“样式控制权”从Zope2的模板引擎手里交还给前端开发者熟悉的HTML/CSS/JS工作流。你不再需要懂tal:define怎么嵌套、metal:use-macro怎么继承、context/plone怎么调用视图方法你只需要会写静态HTML会配CSS选择器就能完成90%的主题定制。我去年帮一家高校图书馆迁移三个老旧Plone 4站点其中最棘手的不是功能重构而是他们那个用了十年的Sunburst变体主题——首页轮播图用portal_javascripts注入jQuery插件侧边栏菜单靠portal_kss动态加载整个结构像用胶带缠起来的电路板。迁移到Diazo后我们把轮播图逻辑全挪到static/js/home-slider.js里用script srcthemelibtheme/js/home-slider.js引入菜单数据改由Plone REST API提供前端用Fetch拉取并渲染。上线后前端同事第一次能独立修改轮播图文案和图片顺序不用再等Python后端发版——这才是Diazo带来的真实生产力跃迁。关键词里的“Planet Plone”不是随便贴的标签。它代表的是Plone社区的真实生态当你把主题升级为Diazo标准格式它就自动具备了跨版本兼容性、主题市场分发能力、以及与Plone官方工具链如plonetheme.barceloneta的无缝协作能力。你写的rules.xml规则可以复用在Plone 5.2、5.3、6.0甚至未来6.5上你放在static/css/里的SCSS编译产物能被plone.staticresources统一管理缓存你提交到GitHub的manifest.cfg会被Planet Plone自动抓取索引让其他开发者一眼看到你的主题支持哪些Plone版本。这背后是一整套工程化实践从资源路径注册机制、主题预览沙箱、到CSS/JS资源依赖图谱分析。所以别把它当成一次“换皮肤”的小动作这是把你的主题从手工作坊升级为现代化工厂的关键一步。2. 整体设计思路为什么是“渐进式拆解”而不是“一刀切重写”很多人看到“升级Diazo”第一反应是建个新包、复制全部代码、然后推倒重来。我试过两次这种方案结果都是项目延期三个月团队加班到凌晨三点最后上线发现首页导航栏错位、搜索框样式丢失、移动端折叠菜单失效——因为老主题里埋了太多“幽灵依赖”某个CSS类名被五个不同模板引用某个JS函数在portal_javascripts里被三次重定义某个图片路径硬编码在main_template.pt的img srclogo.png里。直接重写等于在没做地质勘探的情况下开挖地铁隧道。我的方案是“三层剥洋葱法”最外层是资源层CSS/JS/图片中间层是结构层HTML骨架与规则映射最内层是逻辑层模板覆盖与视图定制。这三层完全解耦可以按团队节奏分批推进互不影响线上服务。资源层迁移是风险最低、收益最高的起点。它不碰任何业务逻辑只做文件搬家和路径修正。我把这个阶段称为“静态资产归仓”把散落在skins/mytheme/下的main.css、custom.js、images/logo.png全部挪进src/mytheme/theme/static/然后用thememytheme/main.css替代原来的/resourcemytheme/main.css。这个过程我写了自动化脚本三分钟跑完连CSS里url(../images/bg.jpg)这种相对路径都能自动转成url(./images/bg.jpg)。关键是做完这步你的站点外观几乎零变化但所有静态资源已进入Plone 5的现代资源管道——这意味着后续你可以直接启用plone.staticresources的Brotli压缩、HTTP/2 Server Push、甚至CDN自动回源。结构层重构是核心攻坚。这里的关键认知是Diazo的index.html不是最终页面而是HTML模具rules.xml才是真正的“模具说明书”。很多开发者卡在这一步是因为试图把index.html写成完整页面——结果发现Plone动态生成的div idcontent-core内容总对不上位置。正确做法是用浏览器“查看源码”功能复制Plone 5默认主题Barceloneta渲染后的纯净HTML删掉所有script、link、meta标签只保留body内结构框架再把div idcontent-core替换成div idcontent classdiazo-content-placeholder。这样rules.xml里一句replace css:theme#content css:content#content-core/就能精准缝合。我见过最典型的错误是把header整个写死在index.html里结果Plone的header里包含动态登录状态、语言切换器、搜索框——这些必须通过replace或copy规则从Plone原生结构中提取而不是手写。逻辑层收口是最考验经验的部分。它解决的是“老主题里那些非CSS/JS能搞定的定制需求”。比如你原来用skins/mytheme/front-page.pt覆盖首页现在要把它变成browser/views/homepage_view.py里的一个Zope3视图类再通过browser:page注册。难点在于老模板里可能有python:here.getFolderContents()这种Zope2表达式新视图里得改成self.context.getFolderContents()原来用tal:repeat遍历的内容现在要用property装饰器封装成可序列化的字典。我的经验是先用z3c.jbot做过渡——把front-page.pt重命名为plone.app.layout.viewlets.common.FrontPageViewlet这样既保留原有逻辑又脱离portal_skins依赖等所有Diazo规则跑稳后再逐步把jbot模板替换成纯Python视图。这样团队可以并行工作前端组调rules.xml后端组写视图互不阻塞。这套思路的价值在于它把一个看似庞大的系统升级拆解成可度量、可验证、可回滚的原子任务。每个环节都有明确的成功标志资源层迁移完成浏览器Network面板里所有theme请求返回200结构层完成/theming-controlpanel里预览效果与原站一致逻辑层完成所有自定义页面URL如/homepage返回正确内容且无500错误。没有模糊地带没有“差不多就行”。3. 核心细节解析从manifest.cfg到rules.xml的实操陷阱与避坑指南3.1manifest.cfg不只是元数据更是主题的“出生证明”很多开发者把manifest.cfg当成可有可无的配置文件随手填个名字和版本号就完事。但实际在Plone 5环境中它是主题能否被识别、能否被正确加载的第一道闸门。我曾遇到一个案例客户主题在本地开发环境一切正常部署到生产服务器后Theming控制面板里根本看不到这个主题。排查三天才发现manifest.cfg里title My Theme用了中文空格全角空格而Plone的zcml解析器对UTF-8 BOM和不可见字符极其敏感——它把整个文件当成了乱码直接跳过加载。标准manifest.cfg必须包含以下字段缺一不可[theme] title My Awesome Theme description A modern Diazo theme for Plone 5 version 1.0.0 preview preview.png author Your Name author-email youremail.com license GPL homepage https://github.com/yourname/mytheme # 关键字段base-theme指定继承关系 base-theme plonetheme.barceloneta # 关键字段development-mode决定是否启用热重载 development-mode on特别注意base-theme字段。如果你的老主题基于Sunburst这里不能填sunburst——因为Plone 5里Sunburst已不再是独立主题包而是作为plonetheme.barceloneta的兼容层存在。正确做法是设为plonetheme.barceloneta然后在static/css/里创建overrides.css把Sunburst的CSS规则逐条复制过来。这样既能保留视觉一致性又能获得Barceloneta的响应式网格、无障碍支持、以及Plone 6的平滑升级路径。development-mode on是调试期的生命线。开启后每次修改rules.xml或index.html无需重启Zope实例只需刷新浏览器即可生效。但上线前务必改为off否则会暴露/thememytheme/下的所有文件路径带来安全风险。我习惯在CI/CD流程里加一道检查grep development-mode on manifest.cfg exit 1确保生产包永远不带调试模式。3.2static/目录结构为什么子目录命名比文件名更重要static/目录不是简单的文件夹它是Plone资源注册系统的命名空间根目录。它的结构直接决定了你在rules.xml里怎么写CSS选择器在index.html里怎么写link路径。常见错误是把所有文件堆在根目录下static/ ├── main.css ├── custom.js ├── logo.png └── background.jpg这种结构会导致两个问题一是CSS里url(logo.png)这种路径在不同页面层级下会404因为Plone的URL路径深度不同二是无法利用Plone的资源分组机制——比如你想让首页JS只在首页加载而通用JS全局加载就必须用子目录隔离。我的标准结构是static/ ├── css/ │ ├── base.css # 基础重置、字体、排版 │ ├── layout.css # 网格、容器、间距 │ └── components/ # 按组件拆分header.css, footer.css, carousel.css ├── js/ │ ├── vendor/ # 第三方库jquery.min.js, swiper-bundle.min.js │ ├── utils/ # 工具函数dom-utils.js, api-client.js │ └── pages/ # 页面级脚本home.js, search.js, article.js ├── images/ │ ├── icons/ # SVG图标menu-icon.svg, search-icon.svg │ ├── logos/ # 品牌标识logo-dark.svg, logo-light.svg │ └── backgrounds/ # 背景图hero-bg.jpg, section-bg.jpg └── fonts/ # 自定义字体inter-variable-font.woff2这样设计的好处是在rules.xml里可以精确控制资源加载。比如首页轮播图JS我用这条规则append css:theme-childrenhead script typetext/javascript srcthememytheme/js/pages/home.js/ /append而通用工具JS则放在append css:theme-childrenbody里确保DOM就绪后再执行。更重要的是当Plone 6引入plone.staticresources时这种结构能自动被识别为模块化资源包js/pages/home.js会被打包进home-bundle.jscss/components/carousel.css会被合并进components-bundle.css极大减少HTTP请求数。3.3rules.xml从“选择器暴力匹配”到“语义化精准缝合”Diazo规则的核心是replace、copy、append、remove四个指令但新手常犯的致命错误是用CSS选择器硬匹配Plone生成的动态ID。比如Plone 5的正文区域ID是#content-core但老主题里可能写成replace css:theme#content css:content#content-core/。表面看没问题但一旦Plone升级到5.2#content-core可能变成#content-core-wrapper规则就失效了。我的解决方案是用语义化class名替代ID匹配。Plone所有核心视图都遵循一套语义化class命名规范div classdocumentContent→ 文档正文内容div classportletWrapper portletNavigationTree→ 导航树端口div classportletWrapper portletCalendar→ 日历端口div classvisualClear→ 清除浮动占位符所以正确的规则应该是!-- 不要这样 -- replace css:theme#content css:content#content-core/ !-- 要这样 -- replace css:theme#content css:content.documentContent/ !-- 导航树也同理 -- replace css:theme#portal-column-one css:content.portletWrapper.portletNavigationTree/更进一步我建议在index.html里主动添加语义化class!-- index.html -- div idcontent classdiazo-content diazo-document-content !-- 这里是占位内容 -- /div div idnavigation classdiazo-navigation diazo-portlet-navigation !-- 这里是占位导航 -- /div然后规则写成replace css:theme.diazo-document-content css:content.documentContent/ replace css:theme.diazo-navigation css:content.portletWrapper.portletNavigationTree/这样做的好处是即使Plone未来改变内部class名你只需调整rules.xml里的一处css:content值而index.html的结构保持稳定。我在一个政府网站项目里用这套方案成功支撑了从Plone 5.1到5.3的三次大版本升级rules.xml只改动了7行。3.4 CSS资源注册为什么theme路径必须出现在registry.xml里很多开发者以为把CSS放进static/css/就万事大吉结果发现样式不生效。根源在于Plone的CSS Registryportal_css不会自动扫描theme路径你必须显式注册。老主题用cssregistry.xml注册新主题必须用registry.xmlZCML格式。标准registry.xml片段configure xmlnshttp://namespaces.zope.org/zope xmlns:plonehttp://namespaces.plone.org/plone !-- 注册主题CSS -- plone:staticResource namemytheme-css typestylesheet expressionstring:${theme-path}/css/base.css / plone:staticResource namemytheme-layout-css typestylesheet expressionstring:${theme-path}/css/layout.css / !-- 关键设置加载顺序 -- plone:resourceDirectory namemytheme directorytheme/static / /configure重点在expressionstring:${theme-path}/css/base.css。${theme-path}是Plone内置变量自动解析为thememytheme这样生成的最终URL就是/thememytheme/css/base.css。如果写成硬编码/thememytheme/css/base.css在多语言站点或虚拟主机环境下会出错。更关键的是加载顺序。Plone默认CSS加载顺序是base.css→layout.css→components/*.css。如果你的overrides.css需要覆盖layout.css里的规则就必须在registry.xml里把它放在最后plone:staticResource namemytheme-overrides-css typestylesheet expressionstring:${theme-path}/css/overrides.css /否则!important满天飞后期维护成本爆炸。我见过一个电商主题因为overrides.css加载太早导致所有响应式断点都被覆盖移动端直接显示桌面版布局——查了两天才发现是registry.xml里顺序错了。4. 实操过程从Plone 4 Sunburst主题到Plone 5 Diazo主题的完整迁移流水线4.1 环境准备与基线确认建立可验证的迁移起点在动任何代码前必须建立三个基线快照这是避免“越改越乱”的唯一保障Plone 4环境快照用buildout锁定当前Plone 4.3.20版本导出portal_skins所有自定义文件bin/instance run scripts/export_skins.py生成skins-export.zip。同时记录portal_css和portal_javascripts里所有自定义资源的路径、顺序、启用状态。Plone 5开发环境用pip install Plone5.2.10新建干净实例安装plonetheme.barceloneta作为基准主题。访问http://localhost:8080/Plone/theming-controlpanel确认Barceloneta主题能正常预览且所有默认页面首页、文档、文件夹渲染无误。视觉基线比对用Puppeteer写个自动化脚本对Plone 4和Plone 5的同一页面如/Plone/front-page截取全屏图用pixelmatch库比对像素差异。目标是Plone 4截图与Plone 5Barceloneta截图的差异率5%证明新环境基线可靠。提示不要在Plone 5里直接安装老主题。Plone 5的portal_skins已阉割老主题的skins/mytheme/main_template.pt会被忽略导致页面空白。必须从Diazo主题包开始构建。4.2 静态资源迁移三步完成“零感知”搬家第一步创建主题包骨架用mr.bob模板快速生成标准Diazo主题pip install mrbob bobtemplates.plone mrbob -O mytheme bobtemplates.plone:theme回答问题时Theme name:My Awesome ThemePackage name:mythemeTheme base:plonetheme.barcelonetaInclude example rules:Yes生成后目录结构是mytheme/ ├── src/ │ └── mytheme/ │ ├── theme/ │ │ ├── static/ # 空目录待填充 │ │ ├── index.html # 基础HTML骨架 │ │ └── rules.xml # 基础规则 │ ├── __init__.py │ └── configure.zcml └── setup.py第二步自动化资源搬运写个Python脚本migrate_assets.py读取Plone 4的skins-export.zip按规则迁移import zipfile import os from pathlib import Path def migrate_css(zip_path): with zipfile.ZipFile(zip_path) as z: for file in z.filelist: if file.filename.endswith(.css) and mytheme in file.filename: # 解析路径skins/mytheme/main.css - static/css/main.css target_path Path(src/mytheme/theme/static/css) / Path(file.filename).name target_path.parent.mkdir(exist_okTrue) with open(target_path, wb) as f: f.write(z.read(file)) def migrate_images(zip_path): with zipfile.ZipFile(zip_path) as z: for file in z.filelist: if file.filename.lower().endswith((.png, .jpg, .gif, .svg)): # skins/mytheme/images/logo.png - static/images/logos/logo.png parts Path(file.filename).parts if len(parts) 2 and parts[1] mytheme and images in parts[2]: target_name _.join(parts[3:]) # 合并多级目录名 target_path Path(src/mytheme/theme/static/images/logos) / target_name target_path.parent.mkdir(exist_okTrue) with open(target_path, wb) as f: f.write(z.read(file))运行后static/目录自动填充且CSS/JS文件名保持原样避免路径引用错误。第三步资源注册与路径修正编辑src/mytheme/theme/configure.zcml添加资源注册configure xmlnshttp://namespaces.zope.org/zope xmlns:plonehttp://namespaces.plone.org/plone include packageplone.staticresources / !-- 注册静态资源目录 -- plone:staticResource namemytheme typetheme directorystatic / !-- 注册CSS资源 -- plone:staticResource namemytheme-css typestylesheet expressionstring:${theme-path}/css/main.css / /configure然后在Plone 5的ZMI里进入portal_css删除所有指向/resourcemytheme/的旧条目新增一条ID:mytheme-cssExpression:string:${theme-path}/css/main.cssEnabled:TrueInsert before:barceloneta-main-css此时刷新页面Network面板应看到/thememytheme/css/main.css返回200且CSS生效。这是第一个里程碑静态资源已接管。4.3 HTML骨架与规则编写用“反向工程法”重建视觉一致性第一步获取Plone 5的纯净HTML骨架访问Plone 5的/Plone/front-page右键“查看页面源代码”复制全部HTML。用VS Code打开执行以下操作删除所有script标签包括script src...和内联script删除所有link relstylesheet标签除了link relstylesheet href/thememytheme/...删除所有meta标签除了meta namegenerator contentPlone在body内找到div idcontent-core将其替换为div idcontent classdiazo-content h1 classdocumentFirstHeadingFront Page/h1 p classdocumentDescriptionWelcome to our site./p div classdocumentContent !-- Plone will inject content here -- /div /div保存为src/mytheme/theme/static/index.html。第二步编写最小可行规则集src/mytheme/theme/static/rules.xml初始内容?xml version1.0 encodingUTF-8? rules xmlnshttp://namespaces.plone.org/diazo xmlns:csshttp://namespaces.plone.org/diazo/css xmlns:xslhttp://www.w3.org/1999/XSL/Transform !-- 基础替换内容区 -- replace css:theme#content .documentContent css:content.documentContent / !-- 替换标题 -- replace css:theme#content h1.documentFirstHeading css:contenth1.documentFirstHeading / !-- 替换描述 -- replace css:theme#content p.documentDescription css:contentp.documentDescription / !-- 移除Plone默认的编辑工具栏 -- remove css:content.documentEditable / /rules此时在Theming控制面板里启用主题首页应显示与Plone 4几乎一致的视觉效果。如果标题错位检查index.html里h1的class名是否与Plone 5生成的class完全匹配用浏览器开发者工具Inspect确认。第三步渐进式增强规则针对老主题的特殊需求逐条添加规则。例如老主题有自定义顶部横幅Plone 4里用skins/mytheme/header.pt覆盖Plone 5里先在index.html添加占位header idcustom-header classdiazo-header div classheader-content !-- Custom banner will be injected here -- /div /header然后写规则replace css:theme#custom-header .header-content css:content.portalHeader /每加一条规则就刷新页面验证效果。我的经验是一次只加一条规则验证通过再加下一条。避免“改一堆再测试”那样出问题根本定位不到源头。4.4 模板覆盖迁移从portal_skins到z3c.jbot再到纯视图场景还原老主题有一个skins/mytheme/front-page.pt用于渲染首页轮播图和新闻列表。第一阶段z3c.jbot过渡安装z3c.jbot在buildout.cfg的eggs里添加z3c.jbot创建src/mytheme/mytheme/browser/templates/目录复制front-page.pt到该目录重命名为plone.app.layout.viewlets.common.FrontPageViewlet.pt编辑src/mytheme/mytheme/configure.zcml添加include packagez3c.jbot / jbot:template layermytheme.interfaces.IMyThemeLayer templateplone.app.layout.viewlets.common.FrontPageViewlet.pt /此时首页轮播图恢复但已脱离portal_skins运行在Zope3视图层。第二阶段纯Python视图重构创建src/mytheme/mytheme/browser/views/homepage_view.pyfrom Products.Five.browser import BrowserView from Products.CMFCore.utils import getToolByName class HomePageView(BrowserView): def get_news_items(self): 获取最新新闻 catalog getToolByName(self.context, portal_catalog) return catalog( portal_typeNews Item, sort_oncreated, sort_orderreverse, review_statepublished, sort_limit5 ) def get_carousel_items(self): 获取轮播图素材 # 这里可以调用Plone REST API或自定义查询 return [ {title: Item 1, image: /thememytheme/images/carousel1.jpg}, {title: Item 2, image: /thememytheme/images/carousel2.jpg}, ]创建src/mytheme/mytheme/browser/templates/homepage_view.ptdiv classhomepage-carousel div classcarousel-inner tal:items repeatitem view/get_carousel_items div classcarousel-item img src${item/image} alt${item/title} / h3${item/title}/h3 /div /tal:items /div /div div classhomepage-news h2Latest News/h2 ul tal:news repeatnews view/get_news_items lia href${news/getURL}${news/Title}/a/li /tal:news /ul /div在configure.zcml里注册视图browser:page for* namehomepage class.views.homepage_view.HomePageView templatetemplates/homepage_view.pt permissionzope2.View /在rules.xml里调用replace css:theme#content .homepage-section href/homepage /此时所有业务逻辑都在Python里前端只负责展示彻底解耦。上线后前端改轮播图样式后端改新闻查询逻辑互不影响。5. 常见问题与排查技巧实录那些文档里不会写的血泪教训5.1 “页面空白”问题排查速查表现象可能原因排查步骤解决方案启用主题后整个页面变白Network面板无任何theme请求manifest.cfg语法错误或缺失关键字段1. 检查manifest.cfg是否有BOM或全角空格2. 运行bin/instance debug进入调试模式执行app.portal_themes.listThemes()看主题是否被识别重写manifest.cfg用vim -b检查BOM确保title、version、base-theme三字段存在页面有HTML结构但无样式Network面板显示thememytheme/css/main.css404CSS资源未正确注册或路径错误1. 进入ZMIportal_css确认mytheme-css条目存在且Enabled为True2. 在浏览器Console执行window.themePath看是否返回/thememytheme检查registry.xml中expression是否用${theme-path}而非硬编码确认static/css/main.css文件存在且权限为644页面样式部分生效但某些元素错位rules.xml规则未覆盖到动态生成的class名1. 用浏览器Inspector查看目标元素的实际class名2. 对比rules.xml中css:content值是否完全匹配用css:content.someClass.someOtherClass替代css:content#id启用development-mode on实时调试5.2 CSS加载顺序混乱导致样式覆盖失败典型症状自定义按钮样式button.my-btn { color: red; }不生效浏览器开发者工具显示它被barceloneta-main.css里的button { color: blue; }覆盖。根本原因Plone的CSS Registry按注册顺序加载barceloneta-main.css在mytheme-css之前加载且button选择器权重更高。独家技巧用insert-before属性强制插入顺序。在registry.xml里plone:staticResource namemytheme-css typestylesheet expressionstring:${theme-path}/css/main.css insert-beforebarceloneta-main-css !-- 关键 -- /如果barceloneta-main-css不存在如Plone 5.1用insert-afterplone-base-css。我的经验是永远把自定义CSS放在plone-base-css之后、barceloneta-main-css之前这样既能继承基础样式又能被主题样式覆盖。5.3 Diazo规则不生效的隐藏陷阱陷阱1replace与copy的语义混淆新手常写replace css:theme#nav css:content.portletNavigationTree/结果导航栏内容为空。因为replace是替换整个节点而.portletNavigationTree是端口容器里面还有.portletHeader、.portletItem等子节点。正确做法是copy内容copy css:theme#nav css:content.portletNavigationTree .portletItem /陷阱2applyPrefix未启用导致图片路径404CSS里写background: url(../images/logo.png);但在portal_css里applyPrefix设为False导致路径解析为/images/logo.png而非/thememytheme/images/logo.png。解决方案在ZMIportal_css里找到你的CSS条目勾选Apply prefix或在registry.xml里加plone:staticResource namemytheme-css typestylesheet expressionstring:${theme-path}/css/main.css apply-prefixtrue !-- 关键 -- /陷阱3append脚本执行时机错误首页轮播图JS写append css:theme-childrenheadscript src.../script/append但脚本依赖document.getElementById(carousel)而#carousel在body里head里执行时DOM未就绪。解决方案改用append css:theme-childrenbody或在JS里包装document.addEventListener(DOMContentLoaded, ...)。5.4 生产环境部署的终极 checklistmanifest.cfg检查development-mode offversion字段为语义化版本如1.2.0base-theme指向稳定版本如plonetheme.barceloneta2.0.0,3.0.0静态资源压缩在buildout.cfg里启用plone.staticresources的压缩[instance] eggs plone.staticresources environment-vars PLONE_STATIC_RESOURCES_COMPRESSon PLONE_STATIC_RESOURCES_BUNDLESon缓存头设置在Nginx/Apache配置里为theme路径添加强缓存location ~ ^/theme\\.*\.(css|js|png|jpg|gif|svg)$ { expires 1