Liquid 模板引擎运行原理与 TagInclude 执行流程
本文说明本项目中 Liquid 模板从 .liquid 源码到 HTML 输出 的完整链路,并以 liquidExtend\tags\TagInclude 为例拆解 每个方法在何时被调用。与实现冲突时以代码为准。
依赖:composer.json 中 liquid/liquid 1.4.25;业务扩展在 extend/liquidExtend/。
相关文档:home-liquid-rendering-and-cod-one-page-product.md(Layout / Template 分层)、theme-decoration-and-liquid-rendering.md(装修与积木块)。
一、总览:三阶段
一次前台页面渲染可概括为三个阶段:
| 阶段 | 入口 | 产出 |
|---|---|---|
| 初始化 | HomeBaseController::fetch() | 配置 Liquid、注册 Tag/Filter、准备变量 $data |
| 解析 parse | liquidExtend\template\Template::parse($source) | 根 Document AST(节点树),可选写入 APCu/File 缓存 |
| 渲染 render | Template::render($data) | 遍历 AST,输出 HTML 字符串 |
sequenceDiagram participant C as Controller participant H as HomeBaseController::fetch participant T as liquidExtend Template participant D as Document AST participant Ctx as Liquid Context participant Tag as TagInclude C->>H: fetch(file, data) H->>H: mergeData / registerTag / setCache H->>T: parse(layout liquid) T->>D: tokenize + new Document Note over D,Tag: 遇 {% include %} 时构造 TagInclude 并 parse 子 snippet H->>T: render(data) T->>Ctx: new Context(assigns) T->>D: root.render(context) D->>Tag: tag.render(context) Tag->>D: 子 document.render(context) D-->>H: HTML
二、入口:HomeBaseController::fetch()
主入口在 app/home/HomeBaseController.php 的 fetch() 方法,典型调用链为:业务 Controller → $this->fetch('', $renderData)。
2.1 Liquid 全局配置
Liquid::set('INCLUDE_SUFFIX', 'liquid'); // snippet 文件后缀
Liquid::set('INCLUDE_PREFIX', ''); // 无前缀(Shopify 风格,非 Rails _partial)
Liquid::set('ESCAPE_BY_DEFAULT', false);2.2 模板引擎实例
$template = new liquidExtendTemplate($include_path);
// $include_path = public_path('theme/default/snippets')liquidExtend\template\Template 继承 Liquid\Template,构造时传入的 path 作为根 FileSystem 的默认根目录(layout 解析时由 Document 持有;各 Tag 往往再建自己的 OemsaasLocal / OemsaasDatabase)。
2.3 变量注入
handlerTemplate($data):写入app('Context')->template等路由模板名。mergeData($data):合并店铺 Context、page_ca、cdn_url、国家/货币等,得到传给 Liquid 的$data数组。variableEncrypt($data):敏感字段处理。
这些键值在 render($data) 时成为 Context 的顶层 assign,模板内 {{ product.title }}、{% if cart_number %} 等均从这里解析。
2.4 解析缓存(AST 缓存)
当请求参数 liquid_cache 不为 false 时(默认开启):
- 品牌 + file 主题 +
liquid_cache_type=apcu→liquidExtend\cache\Apcu - 否则 →
liquidExtend\file\File(runtime/liquid/cache/{themeName}/)
缓存的是 parse 后的 Document 对象树,不是最终 HTML。
2.5 注册 Filter 与 Tag
$template->registerFilter(new OemsaasFilter());
foreach (extend/liquidExtend/tags/*.php as $tagFile) {
$tagName = strtolower(Str::snake(str_replace(['Tag', '.php'], '', $filename)));
// TagInclude.php → include,TagGetBlocks.php → get_blocks,TagRender.php → render
$template->registerTag($tagName, $tagClass);
}自定义 Tag 会覆盖 Liquid\Template::$tags 中同名内置 Tag。因此项目里的 {% include %} 实际由 liquidExtend\tags\TagInclude 处理,而非 vendor 里的 Liquid\Tag\TagInclude。
2.6 parse 与 render
$template->parse(file_get_contents($file_fullPath)); // 或 parse($content)
$html_source = $template->render($data);三、解析阶段(parse)
⚡⚡⚡ 解析阶段 = 把 .liquid 源码用正则切成 token 数组,然后遍历数组,把每块变成 AST节点(纯文本 / Variable / Tag),构建出 Document 节点树。
3.1 Template::parse($source)
逻辑在 vendor/liquid/liquid/src/Liquid/Template.php:
- 若未设置 cache → 直接
parseAlways($source)。 - 若有 cache → 以
md5($source)为 key 读缓存的Document。 - 若缓存未命中,或
root->hasIncludes() === true→ 重新parseAlways并写缓存。
parseAlways 做两件事:
$tokens = Template::tokenize($source);
$this->root = new Document($tokens, $this->fileSystem);3.2 第一步:Template::tokenize($source) — 正则切块
用 TOKENIZATION_REGEXP 把源码切成 token 数组:
// Liquid.php 第128行
'TOKENIZATION_REGEXP' => '/(' . TAG_START . '.*?' . TAG_END . '|' . VARIABLE_START . '.*?' . VARIABLE_END . ')/'
// 即 /({%.*?%}|{{.*?}})/以这段模板为例:
<section>
<h1>{{ page_title }}</h1>
{% if show_icon %}
{% include 'icon-cart' %}
{% endif %}
</section>tokenize 后得到的数组:
[
"<section>\n <h1>",
"{{ page_title }}",
"\n ",
"{% if show_icon %}",
"\n ",
"{% include 'icon-cart' %}",
"\n ",
"{% endif %}",
"\n</section>"
]
注意:此时还不知道里面是 if / include / for,只是按 {% %} 和 {{ }} 分隔符切块,每个 token 整体是一块。
3.3 第二步:Document 消费 token — 生成 nodelist
Document 继承 AbstractBlock,构造时调 parse($tokens),逐个 array_shift 消费 token 数组:
// AbstractBlock::parse() 核心循环
while (count($tokens)) {
$token = array_shift($tokens); // 弹出一个
if ($tagRegexp->match($token)) {
// 解析出 tag 名 + markup({% if show_icon %} → tag="if", markup="show_icon")
$tagName = ...;
$this->nodelist[] = new $tagName($markup, $tokens, $this->fileSystem);
} elseif ($variableRegexp->match($token)) {
$this->nodelist[] = $this->createVariable($token);
} else {
$this->nodelist[] = $token; // 纯文本直接进 nodelist
}
}三种 token 的处理方式:
| Token 类型 | 处理方式 | 示例 |
|---|---|---|
| 纯文本 | 直接进 $this->nodelist[] | "<section>" |
{{ var }} | new Variable(...) 进 nodelist | Variable("page_title") |
{% tag %} | new $tagClass($markup, $tokens, $fileSystem) 进 nodelist | TagIf("show_icon") |
3.4 块级标签如何消费 token(解耦设计)
问题:Document 怎么知道 {% if %} 的结束标签是 {% endif %}、{% for %} 是 {% endfor %}?
答案:不把判断逻辑写在 Document 里,而是把 $tokens 数组传引用给每个 Tag 对象,让块级标签自己消费。
// Document 的 parse 循环里
$token = array_shift($tokens); // 弹出 "{% if ok %}"
new TagIf($markup, $tokens, $fs); // 把剩余数组传进去
// TagIf 继承 AbstractBlock,构造时调 parse($tokens):
while (count($tokens)) {
$token = array_shift($tokens); // 继续弹出,直到遇到 "{% endif %}"
if ($tagRegexp->matches[1] == $this->blockDelimiter()) {
// blockDelimiter() 返回 "endif",匹配则 return
$this->endTag();
return; // 把控制权交回 Document 的外层循环
}
// 其余 token 进 TagIf 自己的 nodelist
}这样 Document 完全不关心每种块级标签的结束标签是什么,新增块级标签只需加一个类,Document 无需改动。
include标签不同:include 是自闭合标签,{% include 'x' %} 没有配套的 {% endinclude %},所以 TagInclude::parse() 不消费 $tokens 数组,直接读子文件返回。
3.5 以完整例子走一遍 token 消费过程
模板:
A {% include 'icon' %} B {% if ok %} C {% endif %} Dtokenize 后:
$tokens = [
0 => "A ",
1 => "{% include 'icon' %}",
2 => " B ",
3 => "{% if ok %}",
4 => " C ",
5 => "{% endif %}",
6 => " D "
]Document parse 循环:
第1次 shift:"A " → 纯文本 → nodelist: ["A "]
第2次 shift:"{% include 'icon' %}" → new TagInclude,传 $tokens(从 " B " 开始)
TagInclude 读文件建子 AST,不消费 $tokens
nodelist: ["A ", TagInclude对象]
第3次 shift:" B " → 纯文本 → nodelist: ["A ", TagInclude, " B "]
第4次 shift:"{% if ok %}" → new TagIf,传 $tokens(从 " C " 开始)
TagIf 内部 shift " C " → 进 TagIf.nodelist
TagIf 内部 shift "{% endif %}" → 匹配 endif → return
nodelist: ["A ", TagInclude, " B ", TagIf对象]
第5次 shift:" D " → 纯文本 → nodelist: ["A ", TagInclude, " B ", TagIf, " D "]
最终 Document.nodelist:
Document.nodelist = [
"A ", // 纯文本
TagInclude('icon'), // 自闭合,不吃额外 token
" B ",
TagIf('ok'), // 块级,内部 nodelist = [" C "]
" D "
]3.6 $tokens 参数的作用总结
| 标签类型 | 是否使用 $tokens | 用途 |
|---|---|---|
| 块级标签(if/for/section等) | 必须使用 | 继续弹 token,找到自己的 {% endxxx %} 并消费中间所有内容 |
| 自闭合标签(include/render) | 不使用 | 只读文件,不消费外层 token 数组 |
本质:Document 把 $tokens 数组传给块级标签,让标签自己从数组里弹出元素找到自己的结束边界。这是一种解耦——Document 不需要知道各种 endif、endfor、endxxx 在哪,每种块级标签自己负责边界解析。
3.7 markup 是什么
markup = {% %} 或 {{ }} 去掉分隔符后的参数字符串。
{% include 'icon-cart' with product %}
↑^^^^^^^^^^^^^^^^^^^^^^^^ markup = "'icon-cart' with product"
{{ product.title | escape }}
↑^^^^^^^^^^^^^^^^^^^^^ markup = "product.title | escape"
每个 Tag 类用正则把 markup 解析成具体参数(如 templateName、variable)。
markup vs nodelist:
| markup | nodelist | |
|---|---|---|
| 是什么 | tag 声明行里的参数 | tag 内部的子节点 |
| 内容 | 字符串,如 show_icon、'icon-cart' with product | 对象数组,如 [Variable, TagInclude, string] |
| 用途 | 决定 tag 怎么工作(include 哪个文件、if 判断什么条件) | 决定 tag 输出什么 HTML |
3.8 $fileSystem 参数的作用
$fileSystem 是模板引擎的「文件读取器」,用来读 include/section/render 等标签引用的模板文件。
// TagInclude 读 snippet 文件
$source = $this->fileSystem->readTemplateFile('icon-cart');3.9 TagInclude 为什么要 new自己的 FileSystem
父级 Document传进来的 $fileSystem 通常被 TagInclude 覆盖:
// TagInclude::__construct 第105-114行
$include_path = public_path('theme/default/snippets');
if (theme['file_system'] == 'database') {
$fileSystem = new FileSystem\OemsaasDatabase($include_path);
} else {
$fileSystem = new FileSystem\OemsaasLocal($include_path);
}
parent::__construct($markup, $tokens, $fileSystem); // 用自己的,覆盖父级原因:父级 Document 的 FileSystem 读 layout 目录;TagInclude 要读 snippets 目录,路径不同,所以 TagInclude 必须 new 一个自己的 FileSystem 实例。
父级传进来的第 3 个参数在 TagInclude 里没有用到,自己 new了一个专用的。
3.10 hasIncludes() 与缓存策略
Document::hasIncludes()(vendor)会检查 nodelist 里是否存在 instanceof Liquid\Tag\TagInclude 且 hasIncludes() 为 true 的节点。
注意:项目自定义的 liquidExtend\tags\TagInclude 并不继承 Liquid\Tag\TagInclude,因此 vendor 的 instanceof 匹配不到 业务 Tag。结果是:根 layout 的 hasIncludes() 往往对 {% include %} 返回 false,根 AST 更容易被整页缓存;snippet 子树的缓存则完全依赖 TagInclude 自身的 parse() / render() 内逻辑。
四、渲染阶段(render)
4.1 Template::render($assigns)
new Context($assigns, $registers)。- 把
registerFilter注册的 filter 挂到Context的Filterbank。 - 调用
$this->root->render($context)。
4.2 Context 变量栈
4.2.1 为什么需要栈
模板里变量会嵌套定义:
{% assign name = "Alice" %}
{% for item in items %}
{{ name }} ← 这里想读到外层定义的 Alice
{{ item }} ← 循环内部定义的 item
{% endfor %}渲染 for 循环时,name 和 item 需要同时存在。循环每次迭代结束时 pop,新迭代开始时 push 新的 item,不会污染外层。
4.2.2 栈的结构
$context->stack = [
['name' => 'Alice'], // 第0层:全局/外层
['item' => 'apple'], // 第1层:for 循环新压入的
]$assigns 是「数组的数组」,每个元素是一层作用域。
4.2.3 四个核心操作
| 操作 | 代码 | 行为 |
|---|---|---|
| get | get($key) | 从栈顶向栈底查找,子作用域可读到父作用域变量 |
| set | set($key, $value) | 默认只写栈顶,不影响其他层 |
| push | push() | 压入新空层 |
| pop | pop() | 弹出当前层,恢复外层 |
get 的查找顺序:
get('name') → 先查第1层,没有 → 查第0层,找到 'Alice'
get('item') → 第1层有,直接返回
set 的写入位置:
set('item', 'banana') → 只改第1层,第0层的 name 不受影响
4.2.4 TagInclude 的 push/set/render/pop 流程
4.2.4.1 源码解析
TagInclude::render()(extend/liquidExtend/tags/TagInclude.php 第179-256行)渲染子 snippet 时,严格按以下顺序操作 Context:
public function render(Context $context)
{
// ... 读文件、建子 Document(省略)...
$result = '';
$variable = $context->get($this->variable); // ① 从父 Context取出 with/for 的变量
$context->push(); // ② 压入新层
foreach ($this->attributes as $key => $value) {
$context->set($key, $context->get($value)); // ③ 设置 attributes(key: value形式)
}
if ($this->collection) { // ④ for 循环:每个元素渲染一次
foreach ($variable as $item) {
$context->set($this->templateName, $item);
$result .= $this->document->render($context);
}
} else { // ⑤ 普通 include 或 with
if (!is_null($this->variable)) {
$context->set($this->templateName, $variable); // 设置 with 的变量
}
$result .= $this->document->render($context);
}
$context->pop(); // ⑥ 弹出新层,清掉本次设置的变量
return $result;
}4.2.4.2 四步流程分解
第一步:$context->get($this->variable) — 从父 Context 取 with/for 的变量
{% include 'product-card' with p %}此时 p 在父模板的 Context 第0层里,get('p') 从栈顶往栈底找到它,暂存到 $variable。
第二步:$context->push() — 压入新空层
$context->stack = [
['name' => 'Alice', 'shop' => 'StoreA', ...], // 第0层:父模板全局变量
[] // 第1层:新空层
]新层是空的,保护第0层的所有变量不被子 snippet 污染。
第三步:$context->set($this->templateName, $variable) — 在新层写入 include 变量
$context->set('product-card', $variable);现在栈顶(第1层)有 product-card,子 snippet 里 {{ product-card.title }} 能读到。
同时 attributes 里的 key: value 形式也会被逐条 set 进栈顶:
foreach ($this->attributes as $key => $value) {
$context->set($key, $context->get($value));
}第四步:$this->document->render($context) — 子 Document 用完整栈渲染
子 snippet 里的 {{ product.title }} → get('product') → 在栈顶第1层找到。
{{ name }} → get('name') → 第1层没有 → 查第0层,找到 'Alice'(子 snippet仍能读到父变量,非隔离)。
第五步:$context->pop() — 弹出新层
$context->stack = [
['name' => 'Alice', 'shop' => 'StoreA', ...] //恢复第0层
]第1层所有变量(product-card、attributes 等)全部清掉,恢复父模板状态。
4.2.4.3 三种用法的对比
| 用法 | 代码 | push/pop 行为 |
|---|---|---|
| 普通 include | {% include 'icon-cart' %} | push → pop,不设额外变量 |
| with 单个 | {% include 'product-card' with p %} | push → set(‘product-card’, p) → render → pop |
| for 循环 | {% include 'product-card' for items %} | push → 每次迭代 set(‘product-card’, item) → render → pop;所有迭代结束后 pop |
4.2.4.4 注意:非隔离作用域
TagInclude 使用 push/pop 不是隔离作用域——子 snippet 通过 get() 仍能读到父模板所有 assign(name、shop 等)。若需完全隔离,使用 {% render %}(TagRender + IsolatedContextHelper),它会用全新的空 Context 而非栈。
4.3 AbstractBlock::render() / renderAll()
对 nodelist 顺序遍历:
- 对象且有
render方法 → 调用$token->render($context),拼接返回值。 - 字符串 → 原样输出。
Variable→ 求值并输出。
每渲染一个节点会调用 $context->tick()(若设置了 tick 回调)。
五、TagInclude 详解:方法与调用时机
源文件:extend/liquidExtend/tags/TagInclude.php
继承:Basetags → Liquid\AbstractTag。
5.1 类职责简述
解析 {% include 'snippet_name' [with|for variable] [, key: value] %},从 snippets 目录 加载子模板,在共享 Context 下渲染子 Document,返回 HTML 片段。
5.2 方法调用时序表
| 方法 | 何时调用 | 调用方 | 作用 |
|---|---|---|---|
__construct($markup, &$tokens) | parse 阶段,AbstractBlock 遇到 {% include ... %} 时 new TagInclude(...) | AbstractBlock::parse() | 解析 markup、选 FileSystem、触发 parse() |
parse(&$tokens) | 构造末尾,AbstractTag::__construct 固定调用 $this->parse($tokens) | AbstractTag::__construct | 读 snippet 文件、tokenize、建子 Document、写 AST 缓存 |
hasIncludes() | parse 阶段(子 Document 缓存判断);vendor Document::hasIncludes 对自定义 Tag 通常不调 | TagInclude::parse 内;理论上 vendor Document | 判断子 AST 是否含嵌套 include,决定是否缓存子 Document |
render($context) | render 阶段,父 Document::renderAll 遍历到该节点时 | AbstractBlock::renderAll() | 可能再次加载子 Document、push 作用域、渲染子树、pop、指纹 |
Basetags 上的 params() 不被 TagInclude 使用;Basetags::render() 为空实现,被 TagInclude::render 覆盖。
5.3 __construct 逐步说明
时机:仅 parse 阶段一次(每个 {% include %} 对应一个 TagInclude 实例)。
-
正则解析 markup
- 提取
templateName(如icon-cart)。 - 可选
with/for及变量表达式 →$this->collection、$this->variable。
- 提取
-
extractAttributes($markup)(继承自AbstractTag)- 扫描
key: value形式命名参数 →$this->attributes。
- 扫描
-
选择 FileSystem
- 路径基准:
public_path('theme/default/snippets')。 theme.file_system == database→OemsaasDatabase;否则OemsaasLocal(支持theme_dir覆盖)。
- 路径基准:
-
parent::__construct($markup, $tokens, $fileSystem)- 保存 markup、fileSystem。
- 立即调用
$this->parse($tokens)。
5.4 parse(&$tokens) 逐步说明
时机:构造时 自动调用一次;不会在 render() 里再次调用(但 render() 内有重复的「读文件 + 建 Document」逻辑,见下)。
-
readTemplateFile($templateName)→ 得到 snippet 源码字符串。- 磁盘:
snippets/{name}.liquid(INCLUDE_PREFIX为空、INCLUDE_SUFFIX为liquid)。 - 主题覆盖:
theme/{theme_dir}/snippets/存在则优先。
- 磁盘:
-
子 Document AST 缓存(
liquidExtendTemplate::getCache()):- 无 cache →
tokenize+new Document。 - 有 cache → key 为
md5($source),读缓存;未命中或document->hasIncludes()→ 重建并write。
- 无 cache →
-
不消费
$tokens中 layout 剩余部分(include 非块标签)。
5.5 hasIncludes() 逐步说明
时机:
- 主要在
parse()写子 Document 缓存前 判断能否缓存。 - vendor 根
Document::hasIncludes()因instanceof限制,一般不会调用到自定义TagInclude::hasIncludes()。
逻辑:
- 若
$this->document->hasIncludes()→ true(子 snippet AST 内还有 vendor 意义上的 include/extends/block 组合)。 - 否则检查缓存 key 与当前
md5($source)是否一致。 - 一致 → false(可认为缓存有效);否则 true。
5.6 render($context) 逐步说明
时机:每次页面 render、遍历 AST 到该 include 节点时执行(同一请求内多个 include 各执行一次)。
-
性能日志:
PerformanceService::logRuntime开始。 -
再次加载子 Document(与
parse()类似,受liquid_include_method请求参数影响):file(默认):按md5($source)读/写 AST 缓存。- 非
file:按templateName作为 cache key。 - 若 AST 未就绪或
hasIncludes(),重新 tokenize +new Document。
设计意图:parse 阶段与 render 阶段都能拿到最新 snippet;render 阶段重复读文件是为 DB 主题或缓存失效场景兜底,也是性能优化关注点。
-
$context->get($this->variable)- 解析
with/for绑定的变量(for 时为可遍历集合)。
- 解析
-
$context->push()- 新作用域层;下面
set的变量仅影响 include 子树(pop 后恢复)。
- 新作用域层;下面
-
命名参数
foreach ($this->attributes as $key => $value) { $context->set($key, $context->get($value)); } -
分支渲染
for集合:每项set($templateName, $item),累加$this->document->render($context)。with单对象:set($templateName, $variable)后 render 一次。- 无 with/for:直接
$this->document->render($context)(子 snippet 可读父级全部变量)。
-
$context->pop()- 弹出 include 临时作用域。
-
appendFingerPrint($result, $templateName)- 当
website_js_mode为空时,为 HTML 追加 snippet 指纹(调试/溯源,见app/common.php)。
- 当
-
性能日志结束。
5.7 TagInclude 执行流(单节点)
flowchart TD A["AbstractBlock 遇到 {% include 'x' %}"] --> B["new TagInclude(markup, tokens)"] B --> C["TagInclude::__construct"] C --> D["parent::__construct → parse()"] D --> E["读 snippets/x.liquid"] E --> F["tokenize → new Document 子 AST"] F --> G["TagInclude 放入父 nodelist"] H["Template::render(data)"] --> I["Document::renderAll"] I --> J["TagInclude::render(context)"] J --> K["可选:再次加载子 Document"] K --> L["context.push()"] L --> M["设置 attributes / with / for 变量"] M --> N["子 Document::render(context)"] N --> O["context.pop()"] O --> P["appendFingerPrint → 返回 HTML 片段"]
六、文件系统:snippet 如何解析路径
TagInclude 使用 OemsaasLocal / OemsaasDatabase,而非 layout 构造时传入的默认 LocalFileSystem。
OemsaasLocal::fullPath() 要点:
- 模板名仅允许安全字符。
- 拼接
{root}/{name}.liquid。 - 若配置了
theme_dir,优先theme/{theme_dir}/snippets/{name}.liquid。 readTemplateFile失败时返回空字符串(需注意空 snippet 行为)。
TagSection 使用 sections/ 目录;TagTemplate 使用 templates/;逻辑与 include 类似,仅根路径不同。
七、同类 Tag 对照
| Tag | 类 | 加载目录 | 作用域 | 典型用途 |
|---|---|---|---|---|
include | TagInclude | snippets/ | 共享父 Context(push/pop) | 旧版 partial |
render | TagRender | snippets/ | 隔离 Context + 显式参数 | 对齐 Shopify OS 2.0 |
section | TagSection | sections/ | 共享 + 装修缓存 | 主题积木 section |
template | TagTemplate | templates/ | 共享 | 页面主体 {% template %} |
get_blocks | TagGetBlocks | 无文件 | 向 Context merge 数据 | 装修积木列表 |
数据类 Tag(get_products 等)在 render() 里查库/缓存并 context->merge(),不返回 HTML,供后续 {% for %} / {{ }} 使用。
八、缓存层次小结
| 层级 | Key | 内容 | 失效条件 |
|---|---|---|---|
| 根 layout AST | md5(layout源码) | 整页 Document | vendor hasIncludes(对自定义 include 常不触发) |
| snippet AST | md5(snippet源码) 或 templateName | 子 Document | TagInclude::hasIncludes()、render 时重建 |
| 业务 HTML 片段 | LiquidCacheService 等 | section 级 HTML | TagSection 的 cached 属性 |
调试参数:
?liquid_cache=0:关闭 AST 缓存。?liquid_include_method=file:include AST 按文件 md5 缓存(默认)。
九、从模板源码到 Tag 的完整例子
假设 theme.liquid 片段:
<main>
{% get_blocks route: template_route, limit: 10 %}
{% for block in blocks %}
{% section block.type, section: block.params %}
{% endfor %}
{% include 'social-icons' %}
</main>Parse 阶段(一次):
tokenize整份 layout。Document依次实例化:TagGetBlocks、TagFor(块)、TagSection、TagInclude('social-icons')。TagInclude构造 →parse→ 加载snippets/social-icons.liquid为子 AST。
Render 阶段(每次请求):
Context注入mergeData的product、blocks等。TagGetBlocks::render→context->merge(['blocks' => ...])。TagFor循环 → 每次迭代TagSection::render。TagInclude::render→ push → 渲染 social-icons 子树 → pop。- 拼接所有节点输出为 HTML。
十、排障切入点
| 现象 | 建议查看 |
|---|---|
| include 404 / 空内容 | OemsaasLocal::fullPath、theme_dir、DB 主题 OemsaasDatabase |
| 子 snippet 变量未定义 | include 共享父作用域;若用 render 则需显式传参 |
| 改 snippet 不生效 | AST 缓存 runtime/liquid/cache/ 或 APCu;liquid_cache=0 |
| 嵌套 include 性能差 | render 阶段重复 readTemplateFile + hasIncludes 导致重建 AST |
| 自定义 Tag 未生效 | extend/liquidExtend/tags/ 文件名与 registerTag 蛇形命名 |
维护:Liquid 扩展 Tag、缓存策略或 TagInclude 行为变更时,请同步更新本文。