mm_tpl 2.7.0 → 2.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ # 目录结构
2
+ app/shop/
3
+ config.js # { defaults: { theme, style, lang } }
4
+ static/ # 静态资源根目录,模板、样式、脚本、语言包与覆盖目录都放这里
5
+ diy/ # 用户自定义覆盖目录,优先级最高
6
+ component/ # 覆盖全部主题的组件
7
+ ad.html
8
+ patch.js
9
+ js/ # 应用级脚本,各主题共用的公共脚本
10
+ utils.js
11
+ lang/ # 应用级语言包(基础)
12
+ zh_cn.json
13
+ en_us.json
14
+ template/
15
+ default/ # 默认主题(业务命名)
16
+ index.html # 模板直接放主题目录下,按功能划分
17
+ article/ # 功能目录
18
+ list.html
19
+ component/ # 组件:可被功能页面复用的公共片段
20
+ bar.html
21
+ ad.html
22
+ js/ # 主题级脚本(覆盖应用级同名脚本)
23
+ app.js
24
+ lang/ # 主题语言包(覆盖全局)
25
+ zh_cn.json
26
+ en_us.json
27
+ style/ # 只放样式,不放模板
28
+ base/ # 基础风格:各风格共享的公共样式
29
+ common.css
30
+ light/ # 默认风格(颜色/明暗命名),只放差异文件
31
+ light.css
32
+ blue.css
33
+ dark/
34
+ dark.css
35
+ festival/ # 扩展主题
36
+ index.html
37
+ js/promo.js
38
+ lang/zh_cn.json
39
+ style/
40
+ red/red.css
41
+
42
+ ## 资源分类
43
+
44
+ | 资源 | 语义 | 目录 | 分层维度 |
45
+ |---|---|---|---|
46
+ | 模板(页面)`.html` | 覆盖:同名取优先级最高的一个 | `static/template/{主题}/` 下按功能划分 | 仅主题 |
47
+ | 组件 `.html` | 覆盖:同上,但走独立的组件目录链 | `static/template/{主题}/component/`、`static/diy/component/` | 仅主题 |
48
+ | 样式 `.css` | 引用多个:收集全部,同名覆盖 | `static/template/{主题}/style/{风格}/` | 主题 + 风格 |
49
+ | 脚本 `.js` | 引用多个:收集全部,同名覆盖 | `static/js/`、`static/template/{主题}/js/` | 仅主题 |
50
+ | 语言包 `.json` | 合并:深度合并,高优先级覆盖 | `static/lang/`、`static/template/{主题}/lang/` | 仅主题 |
51
+
52
+ 所有资源目录都在 `static/` 下,应用根目录只放 `config.js` 等配置文件,便于整目录部署与缓存。
53
+
54
+ **模板按功能划分,不按风格分层**:主题目录下直接建功能目录,如 `static/template/default/article/list.html`、`static/template/default/user/profile.html`。风格是视觉变体,只影响样式,不改变模板结构;某风格需要不同外观时改 CSS,需要不同文案或结构时用 `model` 变量。
55
+
56
+ **组件放 `component/` 目录,与页面区分**:组件是可被多个页面复用的公共片段(页头、侧栏、广告位等),放在主题的 `component/` 下,与功能目录平级、互不混淆。页面通过 `$component('bar')` 引用,不必写目录前缀与扩展名。
57
+
58
+ **样式按风格分层**:只有样式进 `style/{风格}/` 目录,因此 `style` 下不会出现模板文件。
59
+
60
+ **脚本同样不按风格分层**:脚本不因颜色明暗而变,公共脚本放应用级 `static/js/` 供各主题共用。若某风格需要不同行为,参数化比复制脚本更合适——模板输出风格变量供脚本读取。
61
+
62
+ ## 查找优先级(高 → 低)
63
+
64
+ 模板:
65
+
66
+ 1. `static/diy/` 自定义覆盖目录
67
+ 2. `static/template/{当前主题}/`
68
+ 3. `static/template/{默认主题}/`
69
+
70
+ 组件(与模板链同构,每级多一个 `component/` 前缀):
71
+
72
+ 1. `static/diy/component/`
73
+ 2. `static/template/{当前主题}/component/`
74
+ 3. `static/template/{默认主题}/component/`
75
+
76
+ 样式:
77
+
78
+ 1. `static/diy/` 自定义覆盖目录
79
+ 2. `static/template/{当前主题}/style/{当前风格}/`
80
+ 3. `static/template/{当前主题}/style/{基础风格}/`
81
+ 4. `static/template/{默认主题}/style/{当前风格}/`
82
+ 5. `static/template/{默认主题}/style/{基础风格}/`
83
+
84
+ 基础风格目录名由配置项 `base_style` 指定(默认 `base`),与当前风格同名时该层不重复,为空则不启用。只回退主题,不回退风格。
85
+
86
+ 脚本:
87
+
88
+ 1. `static/diy/` 自定义覆盖目录
89
+ 2. `static/template/{当前主题}/js/`
90
+ 3. `static/template/{默认主题}/js/`
91
+ 4. `static/js/` 应用级
92
+
93
+ 语言包(低 → 高):
94
+
95
+ 1. `static/lang/`
96
+ 2. `static/template/{默认主题}/lang/`
97
+ 3. `static/template/{当前主题}/lang/`
98
+
99
+ ## 组件用法
100
+
101
+ 组件名相对组件目录解析,**不相对调用方所在文件解析**,因此同一个名字在任何位置引用结果一致;名称可含子目录,如 `article/card`。
102
+
103
+ ```html
104
+ <!-- 页面引用组件:省略 ./component/ 前缀与 .html 后缀 -->
105
+ <div>${@$component('bar')}</div>
106
+
107
+ <!-- 组件内可再引用其他组件,支持嵌套 -->
108
+ <!-- static/template/default/component/bar.html -->
109
+ <div class="ad">${@$component('ad')}</div>
110
+
111
+ <!-- 传数据给组件:组件内直接读该字段 -->
112
+ ${@$component('user-card', { user: item })}
113
+
114
+ <!-- 子目录内的组件 -->
115
+ ${@$component('article/card')}
116
+ ```
117
+
118
+ ```js
119
+ // 服务端渲染组件
120
+ tpl.component('bar', { message: 'hello' }); // 返回渲染结果字符串
121
+ tpl.getComponentFile('bar'); // 返回命中的组件文件全路径
122
+ ```
123
+
124
+ 组件找不到时返回空字符串并置 `engine.error`(`code: 10000`),不抛错;`component_dir` 为空时返回空字符串并置 `code: 10001`。组件为公共片段,默认不写缓存,需要时可由 `options` 显式指定 `cache` 与 `cache_filename`。
125
+
126
+ ## 输出顺序
127
+
128
+ 样式与脚本属于「引用多个」类资源,收集后**按优先级升序输出**(低优先级在前,高优先级在后):
129
+
130
+ - 样式:基础风格在前打底,当前风格与 `diy/` 在后覆盖,后加载的规则才生效
131
+ - 脚本:应用级公共脚本在前,主题脚本与 `diy/` 在后,依赖顺序才成立
132
+
133
+ 同名文件只保留优先级最高的一份,因此结果中不会出现重复文件名。
134
+
135
+ 模板与组件取优先级最高的一个;语言包按优先级降序合并,当前主题覆盖默认主题。
136
+
137
+ ## 文件类型语义
138
+
139
+ - 模板:同名覆盖,取优先级最高的文件;当前主题缺文件时回退到默认主题的同名路径
140
+ - 组件:同名覆盖,当前主题缺组件时回退到默认主题的组件目录,`static/diy/component/` 可全局覆盖
141
+ - 样式:收集各级目录下的全部 css,同名文件只保留优先级最高的一份,按升序输出
142
+ - 脚本:收集各级目录下的全部 js,同名文件只保留优先级最高的一份,按升序输出
143
+ - 语言包:文件名即语言代码(小写下划线,如 `zh_cn.json`),同一语言的词条按目录优先级合并
144
+
145
+ ## 配置项
146
+
147
+ - `theme_dir`:主题目录,默认 `'./static/template/'`,相对应用根目录
148
+ - `style_dir`:样式目录,默认 `'./style/'`,相对主题目录
149
+ - `component_dir`:组件目录,默认 `'./component/'`,相对主题目录(`diy/` 下同名);为空则不启用组件
150
+ - `diy_dir`:自定义覆盖目录,默认 `'./static/diy/'`,相对应用根目录
151
+ - `lang_dir`:应用级语言包目录,默认 `'./static/lang/'`,相对应用根目录
152
+ - `theme_lang_dir`:主题级语言包目录,默认 `'./lang/'`,相对主题目录
153
+ - `js_dir`:应用级脚本目录,默认 `'./static/js/'`,相对应用根目录
154
+ - `theme_js_dir`:主题级脚本目录,默认 `'./js/'`,相对主题目录
155
+ - `extname`:模板与组件的文件扩展名,默认 `'.html'`,可自行设定为 `'.art'` 等其他后缀;`getFile`、`getComponentFile`、`view`、`component` 在入参省略扩展名时会自动补全
156
+ - `base_style`:基础风格目录名,默认 `'base'`,为空则不启用该层
157
+
158
+ ## 注意事项
159
+
160
+ - `static/` 是静态资源根目录,模板、样式、脚本、语言包与覆盖目录都放其中;应用根目录只留 `config.js` 等配置文件
161
+ - 模板按功能划分,主题目录下可有任意深度的功能目录;扩展主题只需放有差异的模板,缺文件时回退到默认主题的同名路径
162
+ - 公共片段一律放 `component/`,不要散落在功能目录里;组件名不相对调用方解析,因此引用位置不影响结果
163
+ - 因不回退风格,各风格目录只放差异样式,公共样式统一放基础风格目录;某风格需要不同于其他风格的公共样式时,在自己目录内放同名文件覆盖即可
164
+ - 样式与脚本都支持 `*` 通配符按名筛选(`*.css`、`app*`、`*common*`),便于模板内分组引用
165
+ - 需要覆盖全部主题时用 `diy/` 目录,其优先级最高;组件对应 `static/diy/component/`
@@ -0,0 +1,381 @@
1
+ # 模板 / 组件 / 样式 / 脚本 / 语言 五者获取方式总结
2
+
3
+ > 下文路径均以**应用根目录**为基准;模板、组件、样式、脚本、语言包与覆盖目录统一放在静态资源根目录 `static/` 下。
4
+
5
+ ## 一图看清
6
+
7
+ ```
8
+ 请求 → options { style?, lang? } (theme 为部署级配置,见下方说明)
9
+ │
10
+ ├── 模板:view('article/list.html', data, options)
11
+ │ └─ static/template/{theme}/article/list.html
12
+ │ 按功能划分目录,走三段回退链,不按风格分层
13
+ │
14
+ ├── 组件:$component('bar') 引用公共片段
15
+ │ └─ static/template/{theme}/component/bar.html
16
+ │ 走独立的组件目录链(每级多一个 component/),
17
+ │ 同样三段回退、不按风格分层,名字相对组件目录解析
18
+ │
19
+ ├── 样式:$css('main.css') 取单个,$css('*.css') 取一组
20
+ │ └─ static/template/{theme}/style/{style}/,与模板不同链
21
+ │
22
+ ├── 脚本:$js('app.js') 取单个,$js('*.js') 取一组
23
+ │ └─ 只按主题分层,不含风格;
24
+ │ 查找链(高→低):static/diy → 当前主题 → 默认主题 → 应用级 static/js/
25
+ │ 输出顺序反过来(低→高),保证依赖在前、业务在后
26
+ │
27
+ └── 语言:模板里 $t('key', { name })
28
+ └─ 四层合并:static/lang/ → 默认主题 lang/ → 当前主题 lang/ → 配置项 langs
29
+ 全局打底,主题覆盖
30
+ ```
31
+
32
+ 五类资源的语义不同,这是理解全部规则的基础:
33
+
34
+ | 资源 | 语义 | 分层维度 |
35
+ |---|---|---|
36
+ | 模板 `.html` | **覆盖**:同名取优先级最高的一个 | 仅主题 |
37
+ | 组件 `.html` | **覆盖**:同名取优先级最高的一个 | 仅主题 |
38
+ | 样式 `.css` | **引用多个**:收集全部,同名覆盖 | 主题 + 风格 |
39
+ | 脚本 `.js` | **引用多个**:收集全部,同名覆盖 | 仅主题 |
40
+ | 语言包 `.json` | **合并**:深度合并,高优先级覆盖 | 仅主题 |
41
+
42
+ ## 一、模板
43
+
44
+ | 项 | 说明 |
45
+ |---|---|
46
+ | **入口** | `engine.view(source, data, options)` 或模板内 `$view(source, extra, options)`(`view` 别名同样可用) |
47
+ | **目录** | `static/template/{theme}/` 下按功能划分,如 `static/template/{theme}/article/list.html` |
48
+ | **source** | 相对路径,拼在目录后;省略扩展名时自动补 `extname`;绝对路径直接用;`options.dir` 指定则只在该目录找 |
49
+ | **回退链** | `static/diy` → `当前theme` → `默认theme` |
50
+ | **找不到** | 置 `engine.error` 并返回空字符串,不抛错 |
51
+ | **缓存** | 按 `cache_dims` 缓存渲染结果(`cache_type` 可选 file / memory);art-template 另外缓存模板编译结果 |
52
+
53
+ ```js
54
+ // 三段依次尝试,命中即用
55
+ static/diy/{source}
56
+ static/template/{当前theme}/{source}
57
+ static/template/{默认theme}/{source}
58
+ ```
59
+
60
+ - **按功能划分,不按风格分层**:主题目录下直接建功能目录(`article/list.html`、`user/profile.html`);风格是视觉变体,只影响样式
61
+ - 扩展主题只需放有差异的模板,缺文件时回退到默认主题的同名路径
62
+ - `static/diy/` 优先级最高,用于覆盖全部主题的模板
63
+
64
+ **theme 与 style 语义**:theme 按业务命名(`default`/`shop`/`festival`),style 按颜色/明暗命名(`light`/`dark`/`blue`/`base`)。
65
+
66
+ **后缀名可配置**:`extname` 默认 `'.html'`,可设为 `'.art'` 等其他后缀。`getFile` 与 `view` 在入参省略扩展名时按 `extname` 补全,因此 `view('./article/list')` 与 `view('./article/list.html')` 等价。
67
+
68
+ **为什么模板不按风格分层**:风格是视觉变体,换 `light`/`dark` 不该换 HTML 结构;把模板放进 `style/{风格}/` 会让同一份结构调整时要改多处,且 `style` 目录语义混乱。风格差异交给 CSS,文案差异交给语言包与 `model` 变量。
69
+
70
+ **为什么主题不参与传参**:主题是部署级配置,且默认不在 `cache_dims` 中(不参与缓存分层)。若允许 `options.theme` 传参,同一实例渲染不同主题会命中同一份缓存。需要多主题时用 `fork({ theme })` 派生实例,缓存按实例隔离。
71
+
72
+ **为什么找不到文件不抛错**:模板常被拆成多个片段,一个子模板缺失不应导致整页渲染失败。调用方取到空字符串后再检查 `engine.error` 决定是否记录或降级。
73
+
74
+ ### 组件
75
+
76
+ 组件是可被多个页面复用的公共片段(页头、侧栏、广告位等),比模板多一条独立的目录链,专门放 `component/` 目录。
77
+
78
+ | 项 | 说明 |
79
+ |---|---|
80
+ | **入口** | `engine.component(name, model, options)` 或模板内 `$component(name, model?)`(`component` 别名同样可用) |
81
+ | **目录** | `static/template/{theme}/component/`,目录名由配置项 `component_dir` 指定(默认 `'./component/'`,相对主题目录;`static/diy/` 下同名子目录) |
82
+ | **name** | 相对组件目录的路径,如 `bar`、`article/card`,可含子目录;省略扩展名时自动补 `extname`;`options.dir` 指定则只在该目录找 |
83
+ | **返回值** | 渲染结果字符串;`getComponentFile(name)` 返回命中的组件文件全路径 |
84
+ | **回退链** | `static/diy/component` → `当前theme/component` → `默认theme/component` |
85
+ | **找不到** | 置 `engine.error`(`code: 10000`)并返回空字符串,不抛错;`component_dir` 为空时置 `code: 10001` |
86
+ | **缓存** | 组件是公共片段,默认不写缓存;需要时由 `options` 显式指定 `cache` 与 `cache_filename` |
87
+
88
+ ```js
89
+ // 组件三段依次尝试,命中即用;与模板链同构,每级多一个固定的 component/ 前缀
90
+ static/diy/component/{name}
91
+ static/template/{当前theme}/component/{name}
92
+ static/template/{默认theme}/component/{name}
93
+ ```
94
+
95
+ ```html
96
+ <!-- 页面引用组件:省略 ./component/ 前缀与扩展名 -->
97
+ <div>${@$component('bar')}</div>
98
+
99
+ <!-- 组件内可再引用其他组件,支持嵌套 -->
100
+ <!-- static/template/default/component/bar.html -->
101
+ <div class="ad">${@$component('ad')}</div>
102
+
103
+ <!-- 传数据给组件:组件内直接读该字段 -->
104
+ ${@$component('user-card', { user: item })}
105
+
106
+ <!-- 子目录内的组件 -->
107
+ ${@$component('article/card')}
108
+ ```
109
+
110
+ ```js
111
+ // 服务端等价写法
112
+ tpl.component('bar', { message: 'hello' }); // 返回渲染结果字符串
113
+ tpl.getComponentFile('bar'); // 返回命中的组件文件全路径
114
+ tpl.getComponentDirs(); // 组件目录链,当前主题=默认主题时去重
115
+ ```
116
+
117
+ 以示例应用为例(当前主题 `default`):
118
+
119
+ | 调用 | 命中 |
120
+ |---|---|
121
+ | `getComponentDirs()` | `['.../static/diy/component', '.../static/template/default/component']` |
122
+ | `getComponentFile('bar')` | `.../static/template/default/component/bar.html` |
123
+ | `getComponentFile('ad')` | `.../static/diy/component/ad.html`(`diy` 覆盖) |
124
+ | festival 主题下 `getComponentFile('bar')` | `.../static/template/default/component/bar.html`(当前主题缺组件,回退默认主题) |
125
+
126
+ - **组件名相对组件目录解析,不相对调用方所在文件解析**:这是与 `$view('./x.html')` 的重要区别——`$view` 的相对路径以模板目录链的根(`static/diy/`、主题目录)为基准,若在 `component/` 内用 `$view('./ad.html')` 引用同目录的兄弟文件,会解析到主题根下而非 `component/` 内;组件走自己的链,`$component('bar')` 始终相对组件目录查找,因此同一个名字在任何位置引用结果一致
127
+ - **model 与调用方数据合并**:`$component(name, model)` 内部执行 `Object.assign({}, 调用方model, model)`,调用方的数据与 `$view` / `$css` / `$t` / `hookAction` 等模板函数都会传给组件,传入的同名字段覆盖调用方。model 可直接写对象字面量,也可传变量
128
+ - **找不到组件不抛错**:与模板同理,公共片段缺失不应导致整页渲染失败;调用方取到空字符串后再检查 `engine.error`
129
+ - **公共片段一律放 `component/`**:与功能目录平级、互不混淆,引用时不必写目录前缀与扩展名
130
+ - `component_dir` 为空则不启用组件,此时 `component()` 直接返回空字符串并置 `code: 10001`
131
+ - **后缀名可配置**:`extname` 同时作用于模板与组件,`getComponentFile` 与 `component` 在入参省略扩展名时按 `extname` 补全
132
+
133
+ **为什么把组件独立成一条链**:公共片段放 `component/` 后与功能页面分离,引用时不必写 `./component/` 前缀与扩展名,页面改目录结构时组件引用不受影响;且 `static/diy/component/` 可一次性覆盖全部主题的该组件。
134
+
135
+ ## 二、样式
136
+
137
+ | 项 | 说明 |
138
+ |---|---|
139
+ | **入口** | 模板里 `$css(name, theme?, style?)`;服务端 `getCssFile()` / `matchCssFiles()` / `getCssFiles()` |
140
+ | **名称** | 精确名(`main.css`)或通配符(`*.css`、`blue*`、`*common*`) |
141
+ | **目录** | `static/template/{theme}/style/{style}/`,比模板多风格与基础风格两级 |
142
+ | **回退链** | `static/diy` → `当前theme+当前style` → `当前theme+base` → `默认theme+当前style` → `默认theme+base` |
143
+ | **返回值** | 精确名返回路径或 `null`;含 `*` 返回路径数组(无命中为空数组) |
144
+ | **顺序** | 按优先级**升序**输出:base 在前打底,当前风格与 `diy/` 在后覆盖 |
145
+
146
+ 只回退主题,不回退风格:`light` 缺的样式不会从 `dark` 继承,两者的公共样式放 `base`。某风格需要不同于其他风格的公共样式时,在自己目录内放同名文件覆盖 `base` 即可;`base_style` 为空则不启用该层,与当前风格同名时该层不重复。
147
+
148
+ ```html
149
+ <!-- 精确引用:命中当前风格或基础风格的该文件 -->
150
+ <link rel="stylesheet" href="${$css('common.css')}">
151
+ <link rel="stylesheet" href="${$css('light.css')}">
152
+
153
+ <!-- 通配符:返回数组,配合 each 批量输出 -->
154
+ ${each $css('*.css') value}
155
+ <link rel="stylesheet" href="${value}">
156
+ ${/each}
157
+
158
+ <!-- 只引当前风格自己的样式,公共样式单独引(style 由 model 传入,见下方说明) -->
159
+ ${each $css(style + '*') value}
160
+ <link rel="stylesheet" href="${value}">
161
+ ${/each}
162
+ <link rel="stylesheet" href="${$css('*common*')[0]}">
163
+
164
+ <!-- 显式取其他主题、风格的样式 -->
165
+ <link rel="stylesheet" href="${$css('dark.css', 'default', 'dark')}">
166
+
167
+ <!-- 不存在就跳过(避免 404) -->
168
+ ${if $css('reset.css')}
169
+ <link rel="stylesheet" href="${$css('reset.css')}">
170
+ ${/if}
171
+ ```
172
+
173
+ ```js
174
+ // 服务端等价写法
175
+ tpl.getCssFile('common.css'); // 单个路径,不存在为 null
176
+ tpl.matchCssFiles('*.css'); // 全部样式,数组
177
+ tpl.getCssFiles(); // 同上,matchCssFiles('*.css') 的简写
178
+ tpl.matchCssFiles('blue*', 'default', 'dark'); // 指定主题、风格
179
+ ```
180
+
181
+ **通配符语义**:沿用目录搜索的既有约定,`*后缀` 为结尾匹配、`前缀*` 为开头匹配、`*包含*` 为包含匹配,不支持 `?` 与中间通配(`a*c`)。`*.css` 即全部样式,等价于 `getCssFiles()`。
182
+
183
+ **为什么精确名返回单个、通配符返回数组**:调用方一眼能看出是"这一个文件"还是"这一组文件"。若统一返回数组,精确引用要写 `[0]`,还得额外判空;若统一返回单个,通配符就失去意义。按名称里有无 `*` 分派,两种用法都自然。
184
+
185
+ **为什么输出顺序是升序**:CSS 在浏览器中后加载者生效。若高优先级目录排在前面,`base` 的公共规则反而会覆盖具体风格,与"base 打底、风格覆盖"的预期相反。因此去重仍由高优先级胜出(同名只留一份),但排列按优先级升序,让高优先级位于后方真正生效。
186
+
187
+ **为什么两个入口都保留**:`$css` 用于模板内引用——判存在性、指定其他主题风格、用通配符分组;`getCssFiles()` / `matchCssFiles()` 用于服务端批量处理。两者都基于同一条回退链,结果一致。
188
+
189
+ ## 三、脚本
190
+
191
+ | 项 | 说明 |
192
+ |---|---|
193
+ | **入口** | 模板里 `$js(name, theme?)`;服务端 `getJsFile()` / `matchJsFiles()` / `getJsFiles()` |
194
+ | **名称** | 精确名(`app.js`)或通配符(`*.js`、`app*`、`*promo*`) |
195
+ | **目录** | `static/js/`(应用级)、`static/template/{theme}/js/`(主题级)、`static/diy/`(覆盖) |
196
+ | **回退链** | `static/diy` → `当前theme` → `默认theme` → 应用级 |
197
+ | **返回值** | 精确名返回路径或 `null`;含 `*` 返回路径数组(无命中为空数组) |
198
+ | **分层** | **仅主题,不按风格** |
199
+ | **顺序** | 按优先级**升序**输出:应用级在前,主题级与 `diy/` 在后 |
200
+
201
+ ```js
202
+ // 四级依次收集,同名只留优先级最高的一份
203
+ static/diy/{name}
204
+ static/template/{当前theme}/js/{name}
205
+ static/template/{默认theme}/js/{name}
206
+ static/js/{name} // 应用级公共脚本
207
+ ```
208
+
209
+ ```html
210
+ <!-- 精确引用:命中当前主题、默认主题或应用级的该文件 -->
211
+ <script src="${$js('utils.js')}"></script>
212
+ <script src="${$js('app.js')}"></script>
213
+
214
+ <!-- 通配符 *.js:全部脚本,按执行顺序排列(应用级 → 默认主题 → 当前主题 → diy) -->
215
+ ${each $js('*.js') value}
216
+ <script src="${value}"></script>
217
+ ${/each}
218
+
219
+ <!-- 前缀匹配:按功能模块分组,只引 app 开头的脚本 -->
220
+ ${each $js('app*') value}
221
+ <script src="${value}"></script>
222
+ ${/each}
223
+
224
+ <!-- 包含匹配:跨目录取同名族脚本 -->
225
+ ${each $js('*promo*') value}
226
+ <script src="${value}"></script>
227
+ ${/each}
228
+
229
+ <!-- 通配结果也是数组,可取首个(即优先级最低、依赖顺序在前的那个) -->
230
+ <script src="${$js('app*')[0]}"></script>
231
+
232
+ <!-- 不存在就跳过(避免 404):精确名返回 null,可直接判真值 -->
233
+ ${if $js('chart.js')}
234
+ <script src="${$js('chart.js')}"></script>
235
+ ${/if}
236
+
237
+ <!-- 显式取某主题的脚本 -->
238
+ <script src="${$js('promo.js', 'festival')}"></script>
239
+ ```
240
+
241
+ ```js
242
+ // 服务端等价写法
243
+ tpl.getJsFile('app.js'); // 单个路径,不存在为 null
244
+ tpl.getJsFile('app*'); // 含通配符返回数组,同上
245
+ tpl.matchJsFiles('*.js'); // 全部脚本,数组,顺序即执行顺序
246
+ tpl.getJsFiles(); // 同上,matchJsFiles('*.js') 的简写
247
+ tpl.matchJsFiles('app*'); // 前缀匹配:['.../app.js']
248
+ tpl.matchJsFiles('*promo*'); // 包含匹配:['.../promo.js']
249
+ tpl.matchJsFiles('nope*'); // 无命中:空数组 [],不是 null
250
+ tpl.matchJsFiles('promo*', 'festival'); // 指定主题
251
+ ```
252
+
253
+ 以示例应用(站点默认主题 `default`、当前主题 `festival`)为例,各通配符的实际命中:
254
+
255
+ | 写法 | 命中(basename,数组顺序即执行顺序) |
256
+ |---|---|
257
+ | `$js('*.js')` | `['utils.js', 'app.js', 'promo.js', 'patch.js']` |
258
+ | `$js('app*')` | `['app.js']` |
259
+ | `$js('*promo*')` | `['promo.js']` |
260
+ | `$js('utils*')` | `['utils.js']` |
261
+ | `$js('nope*')` | `[]` |
262
+ | `$js('app.js')` | 单个路径 `.../static/template/default/js/app.js` |
263
+
264
+ `*.js` 的结果可直接看出分层:`utils.js` 来自应用级 `static/js/`,`app.js` 来自默认主题(festival 未提供,回退命中),`promo.js` 来自当前主题 `festival/js/`,`patch.js` 来自 `static/diy/`。
265
+
266
+ **通配符语义**:与样式完全一致,沿用目录搜索的既有约定——`*后缀` 为结尾匹配、`前缀*` 为开头匹配、`*包含*` 为包含匹配,不支持 `?` 与中间通配(`a*c`)。`*.js` 即全部脚本,等价于 `getJsFiles()`。无命中时返回空数组而非 `null`,因此 `each` 循环与 `.length` 判断都无需额外防护。
267
+
268
+ **精确名与通配符的返回值差异**:与样式同理,名称含 `*` 返回数组,否则返回单个路径或 `null`。因此 `${if $js('chart.js')}` 可直接判存在性,而 `${$js('*.js').length}` 可直接判组内数量。
269
+
270
+ **为什么脚本不按风格分层**:
271
+
272
+ 1. 风格是视觉变体(颜色/明暗),脚本不因颜色而变,换 `light`/`dark` 不该换脚本
273
+ 2. 若按风格分层,`light/js`、`dark/js` 会各存一份,或需要再引入一个 JS 版的 `base` 层——等于把样式的结构硬搬过来,多一层却无实际区分
274
+ 3. 主题是业务变体(如 `festival` 的促销脚本),脚本确实需要按主题覆盖
275
+
276
+ **为什么脚本顺序更关键**:CSS 顺序影响层叠,脚本顺序决定执行。若应用级的 `utils.js` 排到主题脚本之后,依赖它的主题脚本会拿到 `undefined`。升序输出保证「公共基础在前、业务实现在后」。
277
+
278
+ **风格相关的行为怎么做**:用参数化而非复制脚本。把风格名经 `model` 传给模板,模板输出到页面,脚本自行读取:
279
+
280
+ ```js
281
+ // 派生实例(状态持久):tpl.getStyle() 即当前风格
282
+ const tpl_dark = tpl.fork({ style: 'dark' });
283
+ tpl_dark.render('<script>window.__style = \'${style}\';</script>', {
284
+ style: tpl_dark.getStyle()
285
+ });
286
+ // 传参渲染:options.style 只对本次生效,model 需传入同值
287
+ tpl.render('<script>window.__style = \'${style}\';</script>', { style: 'dark' }, { style: 'dark' });
288
+ ```
289
+
290
+ ```html
291
+ <script>window.__style = '${style}';</script>
292
+ ```
293
+
294
+ 引擎本身不向模板注入 `$style` 之类的变量——`$css` / `$js` 只接受显式传入的名称,因此风格名需由调用方经 `model` 提供(`style` 即上例中 model 的字段名,可自定)。
295
+
296
+ ## 四、语言包
297
+
298
+ | 项 | 说明 |
299
+ |---|---|
300
+ | **入口** | 模板里 `$t(key, params?)`,服务端 `tpl.t(key, params?)` |
301
+ | **数据源** | 应用级 `static/lang/` + 主题级 `static/template/{theme}/lang/` + 配置项 `langs` |
302
+ | **代码** | 语言代码统一用小写下划线(`zh_cn`、`en_us`),与语言包文件名一致,便于浏览器端引用 |
303
+ | **合并** | 深合并,**优先级从高到低**:配置项 `langs` → 当前主题 → 默认主题 → 应用级 |
304
+ | **回退** | 请求的 lang 不存在 → 用 `default_lang`;词条缺失 → 默认语言 |
305
+ | **缓存** | 按「目录组 + 文件指纹」缓存,文件改动自动失效 |
306
+ | **取值** | `key` 支持点号 `nav.home` |
307
+ | **插值** | 命名 `{name}`(传对象)与位置 `{0}`(传值)都支持 |
308
+
309
+ ```js
310
+ // 合并示例
311
+ static/lang/zh_cn.json static/template/festival/lang/zh_cn.json
312
+ { {
313
+ "title": "商店", "title": "新春大促",
314
+ "nav": { "home": "首页" } "nav": { "home": "回家" }
315
+ } }
316
+
317
+ // 深合并结果
318
+ {
319
+ "title": "新春大促", // 主题覆盖
320
+ "nav": { "home": "回家" } // 主题覆盖
321
+ }
322
+ ```
323
+
324
+ ```html
325
+ <h1>${$t('title')}</h1>
326
+
327
+ <!-- 命名插值:可读,不怕参数顺序 -->
328
+ <p>${$t('welcome', { name: user.name })}</p>
329
+
330
+ <!-- 位置插值:兼容旧语言包 -->
331
+ <p>${$t('hello', user.name)}</p>
332
+ ```
333
+
334
+ ```json
335
+ { "welcome": "欢迎,{name}!", "hello": "你好,{0}!" }
336
+ ```
337
+
338
+ ## 五者对照
339
+
340
+ | | 模板 | 组件 | 样式 | 脚本 | 语言包 |
341
+ |---|---|---|---|---|---|
342
+ | **入口** | `view()` / `$view()` | `component()` / `$component()` | `$css()` | `$js()` | `$t()` |
343
+ | **目录** | `static/template/{t}/` 下按功能划分 | `static/template/{t}/component/` | `static/template/{t}/style/{s}/` | `static/js/` + `static/template/{t}/js/` | `static/lang/` + `static/template/{t}/lang/` |
344
+ | **分层** | 仅主题 | 仅主题 | 主题 + 风格 | 仅主题 | 仅主题 |
345
+ | **回退** | 三段:static/diy → 当前主题 → 默认主题 | 三段:static/diy/component → 当前主题/component → 默认主题/component | 五段:含风格与 base,精确名沿链查找、通配符全链匹配 | 四级:精确名沿链查找、通配符全链匹配 | lang 整体回退默认,词条逐级回退 |
346
+ | **语义** | 覆盖,取第一个命中 | 覆盖,取第一个命中 | 收集全部,同名去重 | 收集全部,同名去重 | 深合并 |
347
+ | **顺序** | 无(只取一个) | 无(只取一个) | 升序:base 在前,风格在后 | 升序:应用级在前,主题在后 | 降序合并:主题覆盖应用级 |
348
+ | **不存在时** | 返回空串 + `engine.error` | 返回空串 + `engine.error`(未启用组件目录时为 `code: 10001`) | `null` / 空数组 | `null` / 空数组 | 用默认语言包 |
349
+
350
+ 组件与模板的差异只在入口与目录:`$component()` 走 `static/template/{t}/component/`、回退链每级多一个 `component/` 前缀,其余(覆盖语义、仅主题分层、只取第一个命中、返回空串 + `engine.error`)与模板一致。
351
+
352
+ ## 一句话记住
353
+
354
+ - **模板**:`theme` 定目录、**按功能划分**,三段回退找文件,不按风格分层。
355
+ - **组件**:公共片段放 `component/`,走与模板同构的独立三段链,`$component('bar')` 的名字相对组件目录解析(不相对调用方),`static/diy/component/` 可覆盖全部主题。
356
+ - **样式**:`style/{风格}/` 下按风格分层,`$css` 精确取单个、通配取一组,输出升序让风格覆盖 base。
357
+ - **脚本**:**只按主题分两层**,应用级在前保证依赖,换风格不换脚本。
358
+ - **语言包**:**全局打底 + 主题覆盖**,深合并后交给 `$t`,插值支持命名与位置。
359
+
360
+ ## 数据流
361
+
362
+ ```
363
+ options { style?, lang? }
364
+ │
365
+ ├─ 模板:getTemplateDirs() 给出三段链(static/diy → 当前主题 → 默认主题)
366
+ │ 按功能路径拼接 → 依次 statSync,命中即读取渲染
367
+ │
368
+ ├─ 组件:getComponentDirs() 给出三段链(static/diy/component → 当前主题 → 默认主题)
369
+ │ 按组件名拼接 → 依次 statSync,命中即渲染;
370
+ │ 组件名不相对调用方所在文件解析,换位置引用结果一致
371
+ │
372
+ ├─ 样式:getStyleDirs() 给出五段链(含风格与 base)
373
+ │ $css() 精确名沿链 find 第一个命中 → 返回路径或 null
374
+ │ $css('*.css') 等通配符遍历全链 → 去重后按升序返回数组
375
+ │
376
+ ├─ 脚本:$js() 走 getJsDirs() 四级链 → 去重后按升序返回,应用级在前
377
+ │
378
+ └─ 语言:Lang 按目录组读文件 → 深合并 → 按指纹缓存 → $t 闭包引用
379
+ ```
380
+
381
+ 五者**互不影响**:换主题动模板、组件、样式、脚本与主题语言包;换风格只动样式(模板与组件不随风格变);换语言只动语言包。