Plone与Drupal模块机制深度对比:从架构原理到生产选型 1. 这不是简单的“谁更好”而是搞懂两个老牌CMS如何用模块解决真实业务问题Plone 和 Drupal 都不是新面孔——它们在内容管理系统CMS领域各自深耕了二十年以上。Plone 基于 Python以企业级文档管理、权限粒度和开箱即用的合规性见长Drupal 基于 PHP以灵活的数据建模能力、庞大的社区生态和前端集成自由度著称。当标题里出现 “Add-On Modules”附加模块它真正指向的从来不是“装个插件就完事”的表层操作而是一套完整的扩展治理逻辑模块从哪来、怎么选、如何验证兼容性、升级时会不会崩掉整站权限体系、第三方模块的代码质量是否经得起审计、甚至模块作者是否还在维护——这些才是真实项目里让技术负责人半夜睁眼的问题。我做过 7 个 Plone 5/6 的政务内网系统迁移也主导过 4 个 Drupal 8/9 的高校门户重构最深的体会是模块不是功能补丁而是系统DNA的延伸。Plone 的 add-on 模块如plone.app.contenttypes、collective.easyform往往深度耦合 Zope Component ArchitectureZCA和 Acquisition 机制一个模块的configure.zcml文件写错一行整个站点可能连登录页都打不开而 Drupal 的 module如webform、paragraphs、media_library则依赖严格的钩子hook执行顺序和依赖注入容器composer.json里一个版本号写错drush updb就会卡在半途数据库 schema 变更失败却没报具体哪张表出错。这不是理论差异是每天在终端里敲命令、看日志、改配置时踩出来的坑。本文不讲“Plone 更安全”或“Drupal 更灵活”这种空话只聚焦一件事当你站在项目启动节点面对一堆模块列表如何像老司机一样判断哪个模块能进生产环境、哪个该立刻拉黑、哪个需要自己 fork 后重写核心逻辑。适合正在评估技术栈的架构师、接手遗留系统的运维工程师、以及被客户临时加需求逼到墙角的开发同学——你不需要从头学 Python 或 PHP但必须知道模块背后那套“运行时契约”到底是什么。2. 模块设计哲学与底层机制为什么同样的功能在两边实现路径截然不同2.1 Plone 的模块Zope 生态下的“组件化封装”强契约、弱自由度Plone 的 add-on 模块本质是 Zope 应用服务器生态的产物。它的模块不是独立运行的代码包而是注册到 Zope 全局组件注册表Global Component Registry中的可插拔服务。这意味着每个模块必须严格遵循 ZCAZope Component Architecture规范通过configure.zcml文件声明其提供的适配器Adapter、实用工具Utility、视图View等组件并明确指定其接口Interface和上下文Context。举个典型例子plone.app.dexterity模块之所以能让你在后台创建自定义内容类型是因为它注册了一个IDexterityFTI接口的适配器将内容类型定义映射为 Zope 的 Factory Type Information 对象而collective.solr模块要接管搜索则必须提供一个ISearch接口的替代实现并在zcml中用adapter标签覆盖默认搜索服务。这种设计带来两个硬性约束第一模块间存在强依赖链。比如plone.restapi提供 RESTful API必须依赖plone.behavior行为机制支持而plone.behavior又依赖zope.interface和zope.schema。这个依赖不是 Composer 或 pip 的简单包依赖而是运行时组件注册的拓扑关系——如果 A 模块注册了一个IContentProvider接口的适配器B 模块想覆盖它就必须在zcml中用adapter for* provides.interfaces.IContentProvider /显式声明覆盖且加载顺序必须在 A 之后通过z3c.autoinclude的eggs顺序或zcml的includeOverrides控制。我曾遇到一个案例客户要求在新闻列表页插入广告位我们选了collective.panels模块但它注册的IPanel接口与plone.app.mosaic可视化编辑器的ILayoutBehavior冲突导致页面编辑器无法加载。最终解决方案不是禁用其中一个而是重写collective.panels的configure.zcml将其适配器注册范围从*收窄为INavigationRoot避开 mosaic 的行为上下文。第二模块升级系统重启。Plone 的模块加载发生在 Zope 实例启动阶段所有zcml文件被解析、组件注册完成才开始处理 HTTP 请求。这意味着任何模块的zcml修改、Python 代码变更都必须重启整个 Zope 进程bin/instance restart。没有热重载没有按需加载。这直接决定了 Plone 模块的发布节奏主流模块如Products.CMFPlone自身每半年才发一次大版本因为每次升级都要做全站回归测试——测试点包括权限继承链是否断裂比如plone.app.workflow升级后Editor角色能否继续编辑子文件夹、内容迁移脚本是否触发plone.app.contenttypes升级常附带upgradeStep脚本把旧Document类型转为新Document类型若脚本未执行前台显示为空白。提示Plone 模块的setup.py中entry_points字段是关键。z3c.autoinclude.plugin必须声明为plone否则zcml不会被自动扫描plone.recipe.zope2instance的eggs列表顺序决定zcml加载优先级靠后的 egg 可以覆盖前面的同名组件。2.2 Drupal 的模块钩子驱动的“事件总线”高自由度、高风险点Drupal 的模块module则构建在一套松耦合的钩子hook系统之上。核心思想是Drupal 内核定义一系列“关键时刻”如hook_node_insert、hook_form_alter、hook_views_pre_render模块只需实现对应命名的函数内核就会在执行流程中自动调用。这种设计赋予了极高的灵活性——你可以用hook_form_alter修改任意表单字段用hook_entity_presave在实体保存前注入自定义逻辑甚至用hook_menu_alter重写整个路由系统。但自由度的背面是失控风险钩子执行无序、无事务保障、无依赖隔离。最典型的失控场景是钩子执行顺序不可控。假设模块 A 和模块 B 都实现了hook_form_alterA 想给表单加一个隐藏字段B 想移除某个提交按钮。如果 B 先执行它删掉了按钮A 再执行时试图给已不存在的按钮设值就会触发 PHP Notice如果 A 先执行B 删除按钮时字段还在一切正常。Drupal 8 引入了hook_module_implements_alter来干预钩子执行顺序但实际项目中90% 的模块开发者根本不会写这个钩子导致线上环境行为随机。我接手过一个 Drupal 8 站点客户投诉“有时表单提交成功有时直接 500”查日志发现是webform模块和token模块的hook_form_alter冲突token在表单中注入 token 替换逻辑webform在同一位置修改表单结构两者都假设自己是最后一个操作者。最终方案不是改模块而是在settings.php中强制设置module_implements_cache_clear()并手动排序hook_form_alter的执行数组。第二个高风险点是模块依赖的“软耦合”陷阱。Drupal 的.info.yml文件声明依赖用dependencies:字段如dependencies: - drupal:node - drupal:views。但这只是安装时的检查运行时并不保证依赖模块的钩子一定被调用。更危险的是“隐式依赖”模块 C 调用views_get_view(my_view)但它没在dependencies里声明views安装时不会报错但运行时views模块未启用直接 PHP Fatal Error。Plone 的zcml依赖是硬检查——如果plone.app.contenttypes依赖plone.behavior而后者未安装Zope 启动时就会抛ConfigurationExecutionError并停止绝不会让系统带病运行。注意Drupal 9 开始强制要求模块使用 PSR-4 自动加载但大量遗留模块仍用hook_autoload_info手动注册类。这意味着composer install后必须运行drush cache:rebuild清除类加载缓存否则新模块的类找不到——这是新手最常卡住的点错误信息却是“Class not found”完全不提示跟缓存有关。2.3 模块分发与生命周期PyPI vs. Drupal.org两种社区治理模式Plone 模块主要托管在 PyPIPython Package Index通过pip install安装。这看似简单实则暗藏玄机PyPI 上的plone.app.contenttypes包其setup.py中install_requires声明了对Products.CMFPlone5.2.0,6.0.0的依赖但Products.CMFPlone本身又是一个独立的 PyPI 包它内部又依赖Zope2、Acquisition等更低层包。整个依赖树是纯 Python 的语义化版本控制pip会尝试满足所有约束但 Plone 官方强烈建议使用pip install -c https://dist.plone.org/release/6.0.0b3/constraints.txt约束文件来锁定所有包的精确版本因为plone.app.contenttypes2.5.0在Plone 5.2和Plone 6.0下行为完全不同——前者用Archetypes兼容层后者用Dexterity原生实现API 表面一致底层存储结构却天差地别。Drupal 模块则集中在 Drupal.org通过drush pm:download或composer require drupal/module_name安装。Drupal.org 的模块页面会明确标注“Compatibility”兼容 Drupal 8/9/10和“Status”维护状态Active, Maintenance only, Unsupported。但这里有个致命盲区模块的“Last commit”日期不等于“Last security update”日期。我审计过 12 个客户站点发现平均每个站点有 3.7 个模块处于 “Maintenance only” 状态作者声明不再主动开发只修严重 bug但其中 2 个模块在过去 6 个月有安全更新另外 1 个模块的安全更新由 Drupal 安全团队代为发布标记为 “Security team maintained”。这意味着你不能只看作者是否活跃必须订阅 Drupal Security Advisories 邮件列表并定期运行drush pm:security检查。3. 实操选型指南从 5 个真实业务场景出发拆解模块选择决策树3.1 场景一需要多语言内容管理且翻译流程必须对接外部 TMS翻译管理系统客户需求某跨国企业官网需支持中/英/日/德四语所有内容翻译必须走 SDL Trados Server翻译人员不接触 CMS 后台只在 Trados 界面操作。Plone 方案首选plone.app.multilingualPAM。它不是简单添加语言切换器而是为每个内容对象创建语言变体Language Variant每个变体是独立的ContentItem共享同一 UUID。关键优势在于其ITranslationService接口——你可以实现自己的TradosTranslationService类重写translate_content方法调用 Trados Server 的 REST API 提交待翻译内容并在回调中更新 Plone 内容。PAM 的zcml注册了ITranslationService的默认实现你的自定义服务只需在configure.zcml中用adapter factory.trados.TradosTranslationService /覆盖即可。实测下来PAM 与 Trados 集成的稳定度极高因为整个流程基于 Plone 的事务机制如果 Trados API 调用失败Plone 事务回滚内容状态不变如果成功plone.app.multilingual的translate视图会自动创建目标语言变体并填充翻译文本。Drupal 方案content_translation是核心模块但仅提供基础 UI。要对接 Trados必须用tmgmtTranslation Management Tool模块。tmgmt的设计哲学是“任务驱动”它把翻译请求抽象为tmgmt_job实体每个 job 关联源内容、目标语言、TMS 适配器。tmgmt_trados子模块提供 Trados 适配器但它的实现是“异步轮询”——tmgmt创建 job 后后台 cron 每 5 分钟调用tmgmt_trados_api_check_status()查询 Trados 是否完成。问题来了如果 Trados Server 响应超时cron 会重试但tmgmt_job状态可能卡在 “in progress”导致后续 job 积压。我们曾因此导致客户日语版内容延迟上线 48 小时。最终方案是绕过tmgmt直接用hook_entity_presave监听内容保存调用 Trados API 同步提交用state字段标记内容翻译状态translated,pending,failed完全自主控制流程。对比结论Plone 的接口契约更利于企业级集成Drupal 的钩子更易上手但需自行兜底异常。PAM 的ITranslationService是明确的扩展点tmgmt的 job 机制则要求你理解其状态机。3.2 场景二内容需支持复杂数据结构如“产品”包含多个“规格参数表”、“用户评价”、“相关配件”客户需求电商平台后台运营人员需在一个页面内管理产品全量信息避免跳转多个表单。Plone 方案plone.app.contenttypesplone.formwidget.contenttreecollective.z3cform.datagridfield。plone.app.contenttypes提供ProductDexterity 类型基础plone.formwidget.contenttree让用户从内容树中选择“相关配件”其他 Product 对象collective.z3cform.datagridfield则用于“规格参数表”——它渲染为表格每行是ParameterRowschema包含name字符串、value字符串、unit选择项三个字段。关键技巧在于datagridfield的factory参数必须传入一个SchemaFactory类该类的__call__方法返回ParameterRowschema否则表格无法动态添加行。这个细节在官方文档里藏得很深我第一次用时调试了 3 小时才定位到。Drupal 方案paragraphs模块是绝对首选。它允许你定义可复用的“段落类型”Paragraph Type如spec_table、review_list、accessory_grid。每个段落类型有自己的字段spec_table有field_param_name、field_param_value然后在Product内容类型中添加field_paragraphs字段类型为 “Paragraphs”允许添加任意数量的段落。paragraphs的强大在于其view mode系统前台展示时spec_table段落可渲染为 HTML 表格review_list段落可渲染为评论卡片流互不干扰。但陷阱在于性能如果一个产品有 50 个review_list段落每个段落加载 5 条评论paragraphs默认会为每个段落单独查询数据库产生 N1 查询。必须启用paragraphs的cache设置并在hook_preprocess_paragraph中手动合并查询。对比结论Plone 的 datagridfield 更轻量适合结构固定Drupal 的 paragraphs 更灵活但需主动优化性能。collective.z3cform.datagridfield的 schema 是静态定义的改字段要重装模块paragraphs的段落类型可在后台随时增删字段零代码。3.3 场景三需要细粒度权限控制如“法务部只能编辑合同模板库且不能删除历史版本”客户需求企业知识库不同部门对同一内容库有不同操作权限且需保留完整编辑历史。Plone 方案plone.app.versioningbehaviorplone.app.workflowcollective.workspace。plone.app.versioningbehavior为内容类型启用版本控制每次编辑生成新版本旧版本只读plone.app.workflow提供simple_publication_workflow可自定义状态private,pending,published及状态转换权限collective.workspace则是关键——它创建“工作区”Workspace容器为每个工作区分配独立的成员角色WorkspaceMember,WorkspaceManager并在工作区内覆盖全局权限。例如为“合同模板库”创建 Workspace将法务部成员加入赋予WorkspaceMember角色该角色在工作区内有Modify portal content权限可编辑但无Delete objects权限不能删除且plone.app.versioningbehavior保证所有编辑都生成新版本旧版本自动归档到Versions文件夹权限继承自父容器无法被修改。Drupal 方案content_moderation核心workspaces核心group模块。content_moderation提供内容审核流Draft, Archived, Published但默认不控制删除权限workspaces模块创建独立的工作区Workspace每个工作区有独立的数据库快照内容在工作区间复制而非引用group模块则创建“组”Group为组分配角色group_admin,group_member再为角色分配权限。要实现法务部只能编辑不能删除需在group中创建 “Legal Team” 组为其group_member角色勾选Edit any product content但取消勾选Delete any product content。难点在于workspaces的快照机制当法务部在工作区 A 编辑合同该编辑只存在于 A 的快照中要发布到生产环境需手动执行workspace:publish命令否则前台看不到。这增加了运维复杂度。对比结论Plone 的权限模型是“容器内覆盖”天然适合部门隔离Drupal 的权限是“角色全局分配”需配合 workspaces 才能实现空间隔离。collective.workspace的权限是动态计算的getSecurityManager().checkPermission(Modify portal content, obj)group模块的权限是静态配置的更易审计。3.4 场景四需高性能全文检索且支持同义词、停用词、拼音搜索客户需求大型文档库100 万 PDF用户搜索“云计算”应匹配“云服务”、“cloud computing”、“yun ji suan”。Plone 方案collective.solr是事实标准。它不是 Plone 自己实现搜索而是作为 Solr 服务器的客户端将 Plone 内容索引推送到 Solr搜索请求全部转发给 Solr。collective.solr的核心价值在于其schema.xml适配它预置了text_general字段类型内置SynonymFilterFactory同义词、StopFilterFactory停用词、ICUFoldingFilterFactory大小写折叠并针对中文集成了SmartChineseAnalyzer。要支持拼音搜索只需在schema.xml中为title字段添加PinyinTransformFilterFactory然后重建索引bin/instance run scripts/reindex_solr.py。实测 100 万文档Solr 查询响应 200mscollective.solr的索引同步是异步队列collective.solr.queue不影响前台响应。Drupal 方案search_apisearch_api_solr。search_api是抽象层search_api_solr是 Solr 后端实现。它同样支持同义词、停用词但配置分散同义词在 Solr 的synonyms.txt文件中维护停用词在stopwords.txt拼音搜索需额外安装pinyin插件并修改managed-schema。最大痛点是索引同步时机不可控。search_api默认用SearchApiIndex::updateIndex()方法批量更新但该方法在hook_cron中执行受 Drupal cron 运行频率限制默认每小时一次。如果客户要求实时索引PDF 上传后 5 秒内可搜到必须用search_api的track_changes功能监听hook_entity_insert等钩子但这会显著增加 PHP 执行时间。我们曾因此导致上传大文件时超时。对比结论Plone 的collective.solr是“索引即服务”配置集中Drupal 的search_api_solr是“索引抽象层”配置分散但可扩展。前者开箱即用后者需深入 Solr 底层。3.5 场景五需与现有 .NET 企业服务总线ESB集成调用 SOAP 接口获取主数据客户需求CMS 需从 ESB 获取客户主数据Customer Master Data在用户注册时实时校验。Plone 方案requests库 plone.app.async。Plone 6 内置requests可直接调用 SOAP 接口。但 SOAP 调用耗时ESB 响应常 2s不能阻塞前台请求。plone.app.async提供异步任务队列用async.queueJob()提交任务任务在后台线程执行结果通过async.getJobResult()获取。关键技巧是任务函数必须是模块顶层函数不能是类方法且参数必须是 JSON 序列化类型。我们封装了esb_client.soap_call()函数接收customer_id调用 ESB将结果存入 Plone 的portal_catalog作为缓存有效期 1 小时。这样前台注册时先查缓存缓存失效再异步调用 ESB。Drupal 方案php-soap扩展 queueAPI。Drupal 本身不带 SOAP 客户端需确保服务器安装php-soap扩展然后用原生SoapClient类。queueAPI 提供queue服务用queue-createItem()添加任务。但queue的默认后端是数据库高并发下易成瓶颈。我们改用redis作为队列后端queue.redis模块并用hook_user_insert触发任务。难点在于 SOAP WSDL 解析ESB 的 WSDL 常含复杂嵌套类型SoapClient自动生成的类可能与 Drupal 的实体结构冲突需手动编写WSDL解析器将 SOAP 响应 XML 映射为 Drupal 数组。对比结论Plone 的 Python 生态对 SOAP 更友好requestslxml处理 XML 更自然Drupal 的 PHP SOAP 扩展更底层但需更多手工适配。plone.app.async的任务模型更贴近现代微服务queueAPI 则更传统。4. 模块部署与运维实战从开发环境到生产环境的 7 个关键步骤4.1 步骤一环境初始化——用约束文件锁定所有依赖Plone 的buildout.cfg是灵魂。它不只是安装脚本而是整个运行时环境的蓝图。一个健壮的buildout.cfg必须包含[buildout] extends https://dist.plone.org/release/6.0.0b3/constraints.txt parts instance find-links https://dist.plone.org/release/6.0.0b3/ allow-picked-versions false [instance] recipe plone.recipe.zope2instance eggs Plone Pillow collective.solr plone.app.contenttypes关键点extends指向官方约束文件它锁定了Plone6.0.0b3、Zope24.8.3、Acquisition4.7.1等所有包的精确版本。allow-picked-versions false强制buildout严格遵守约束拒绝任何版本漂移。我见过太多项目因pip install时setuptools升级到 60导致zcml解析失败就是因为没用约束文件。Drupal 的composer.json同样关键{ require: { drupal/core-composer-scaffold: ^9.5, drupal/core-recommended: ^9.5 }, config: { sort-packages: true, platform-check: false }, extra: { drupal-scaffold: { locations: { web-root: web/ } } } }core-recommended是 Drupal 官方推荐的约束包它声明了drupal/core及其所有依赖的兼容版本范围。platform-check: false是必须的——它禁用 Composer 对 PHP 版本的严格检查因为 Drupal 9.5 要求 PHP 8.0但很多生产服务器仍是 PHP 7.4此时需手动覆盖platform.php配置。drupal-scaffold确保web/目录结构正确这是 Drupal 8 的标准布局。实操心得Plone 的buildout每次运行都会重新下载所有 eggs耗时很长。用buildout -nno download跳过下载或在 CI 中缓存~/.buildout/eggs/目录。Drupal 的composer install也要缓存vendor/目录否则每次 CI 构建都重装 200 包。4.2 步骤二模块安装——Plone 用 buildoutDrupal 用 composer命令不能混Plone 安装模块的唯一正统方式是修改buildout.cfg的eggs列表然后运行./bin/buildout。例如安装collective.easyform[instance] eggs Plone collective.easyform # ... 其他模块运行./bin/buildout后buildout会从 PyPI 下载collective.easyform及其依赖将 egg 安装到parts/instance/eggs/目录解析collective.easyform的configure.zcml生成parts/instance/etc/site.zcml的 include 语句重启 Zope 实例。严禁用pip install collective.easyform因为pip安装的 egg 不会被buildout管理zcml不会自动加载模块形同虚设。Drupal 安装模块必须用composer requirecd /path/to/drupal composer require drupal/webformcomposer会修改composer.json的require字段下载模块到web/modules/contrib/webform/更新vendor/autoload.php的类映射运行drush en webform启用模块。严禁直接下载 ZIP 包解压到modules/目录因为composer不知道这个模块存在drush pm:info webform会报错且composer update时可能被覆盖。注意Plone 的buildout输出日志极详细Installing instance.后会列出所有加载的zcml文件路径这是排查模块未生效的第一线索。Drupal 的composer require输出简略要确认是否成功必须运行drush pm:list | grep webform。4.3 步骤三配置导入——Plone 用 Generic SetupDrupal 用 Configuration ManagementPlone 的配置内容类型、工作流、权限设置全部通过Generic Setup导出为 XML 文件。在profiles/default/目录下有types.xml内容类型定义、workflow.xml工作流配置、rolemap.xml权限映射。部署时将整个profiles/目录打包为myaddon-1.0.tar.gz用bin/instance run scripts/import_profile.py myaddon:default导入。关键技巧import_profile.py的第二个参数是profile_id格式为package_name:profile_namepackage_name必须与setup.py中entry_points的z3c.autoinclude.plugin名称一致。Drupal 的配置用Configuration ManagementCM。所有配置内容类型、视图、区块放置导出为 YAML 文件存于config/sync/目录。部署命令是drush cim -y # 导入配置 drush cex -y # 导出配置cim会对比config/sync/与数据库中的配置生成差异并应用。但 CM 有两大陷阱第一config/sync/目录必须是 Git 仓库的干净工作区否则cim会拒绝执行第二某些配置如system.site的 site name是“敏感配置”默认不导出需在settings.php中设置$settings[config_exclude_modules] [system.site];才能导出。实操心得Plone 的 Generic Setup 导入是幂等的多次运行无副作用Drupal 的cim则可能破坏数据。我们规定所有cim操作前必须drush sql-dump backup.sql且只在非生产环境运行cim生产环境用drush php-eval手动执行配置变更。4.4 步骤四数据库迁移——Plone 用 upgradeStepDrupal 用 hook_update_NPlone 的模块升级必须提供upgradeStep。在upgrades/目录下创建to2000.py升级到 2.0.0 版本from plone import api def to2000(context): Upgrade to 2000 portal api.portal.get() # 迁移逻辑如为所有 Document 添加 description 字段 brains api.content.find(portal_typeDocument) for brain in brains: obj brain.getObject() if not hasattr(obj, description): obj.description obj.reindexObject()然后在profiles/default/metadata.xml中声明metadata version2000/version dependencies dependencyprofile-plone.app.contenttypes:default/dependency /dependencies /metadata升级时访问/plone-upgrade页面选择目标版本点击执行。upgradeStep在事务中运行失败则回滚。Drupal 的模块升级用hook_update_N。在mymodule.install文件中/** * Add description field to node. */ function mymodule_update_8101() { $database \Drupal::database(); // SQL 迁移为 node_field_data 表添加 description 字段 $database-schema()-addField(node_field_data, description, [ type text, size big, ]); }升级命令是drush updb -y。updb会查找mymodule_update_8101函数并执行。但 Drupal 的updb不是事务性的——如果 SQL 执行一半失败数据库可能处于不一致状态。必须手动编写回滚脚本hook_post_update_NAME。提示Plone 的upgradeStep可以调用api.content.find()安全可靠Drupal 的hook_update_N必须用原始 SQL 或DatabaseAPI不能调用实体 API因为实体定义可能还未加载。4.5 步骤五缓存清理——Plone 清 Zope 缓存Drupal 清多重缓存层Plone 的缓存分三层Zope 的RAMCache内存缓存、plone.app.caching的代理缓存Varnish、浏览器缓存。生产环境必须清RAMCachebin/instance clearcache该命令清除 Zope 的RAMCache实例但不影响 Varnish。Varnish 需单独curl -X PURGE。clearcache是安全的它只清内存缓存不重启进程。Drupal 的缓存是七层地狱PHP OPcache、Drupal 的cache.backend.database、cache.backend.apcu、page_cache、dynamic_page_cache、render、default。清缓存命令是drush cr # 清所有缓存 drush cr render # 只清渲染缓存但drush cr不清 OPcache需手动opcache_reset()。更糟的是drush cr在多服务器环境下只清当前服务器的缓存其他服务器缓存未失效导致用户看到过期内容。必须用redis作为统一缓存后端并在