mm_tpl 2.8.0 → 2.10.0

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.
package/README.md CHANGED
@@ -177,7 +177,7 @@ const eachResult = tpl.render(`
177
177
 
178
178
  ```javascript
179
179
  // 设置全局数据
180
- Tpl.global_bag.siteName = "我的网站";
180
+ Tpl.global_bag.set('siteName', "我的网站");
181
181
 
182
182
  // 设置视图变量
183
183
  tpl.set({
@@ -746,8 +746,8 @@ const restored = tpl.escape(escaped, true);
746
746
  // 全局钩子单例,往里加的钩子对所有实例生效
747
747
  Tpl.global_hook.addAction('header', function (m) { return '<header>头部</header>'; });
748
748
 
749
- // 全局数据袋,渲染时注入模型的 globalBag 字段
750
- Tpl.global_bag.siteName = '我的网站';
749
+ // 全局数据袋(mm_vars 变量实例,varsAdmin('os') 共用),渲染时以 list() 副本注入模型的 globalBag 字段
750
+ Tpl.global_bag.set('siteName', '我的网站');
751
751
 
752
752
  // 全局语言包字典,lang_dir 与 langs 都为空时实例按深拷贝副本兜底复用
753
753
  Tpl.global_dict = { zh_cn: { name: '超级美眉' }, en: { name: 'MM' } };
package/README_EN.md CHANGED
@@ -175,7 +175,7 @@ const eachResult = tpl.render(`
175
175
 
176
176
  ```javascript
177
177
  // Set global data
178
- Tpl.global_bag.siteName = "My Website";
178
+ Tpl.global_bag.set('siteName', "My Website");
179
179
 
180
180
  // Set view variables
181
181
  tpl.set({
@@ -753,7 +753,7 @@ The module mounts no global variables; state shared across instances lives on st
753
753
  Tpl.global_hook.addAction('header', function (m) { return '<header>Header</header>'; });
754
754
 
755
755
  // Global data bag, injected into the model's globalBag field at render time
756
- Tpl.global_bag.siteName = 'My Website';
756
+ Tpl.global_bag.set('siteName', 'My Website');
757
757
 
758
758
  // Global language dictionary, used as a deep-copied fallback when both lang_dir and langs are empty
759
759
  Tpl.global_dict = { zh_cn: { name: 'MM Girl' }, en: { name: 'MM' } };
package/lib/hook.js CHANGED
@@ -1,59 +1,80 @@
1
- const {
2
- types
3
- } = require('util');
4
- const {
5
- TplUtil
6
- } = require('./util.js');
1
+ const mm_action = require('mm_action');
2
+ const mm_filter = require('mm_filter');
3
+ const mm_func = require('mm_func');
7
4
 
8
5
  /**
9
- * 钩子管理类
6
+ * 钩子管理类(组合门面)
7
+ *
8
+ * 组合 mm_action(动作钩子)、mm_filter(过滤钩子)、mm_func(函数钩子)
9
+ * 三个基础模块,对上保持原有 Hook 公共 API 不变:
10
+ * - addAction / delAction / runAction —— 委托给 mm_action
11
+ * - addFilter / delFilter / runFilter —— 委托给 mm_filter
12
+ * - addFunc / delFunc / runFunc —— 委托给 mm_func
13
+ *
14
+ * 三个基础模块实例均通过 xxxAdmin(scope) 的 Admin 工厂按作用域注册:
15
+ * - 相同作用域的 Hook 共用同一组钩子实例(Admin 按名称缓存),
16
+ * 默认作用域为 os(代表整个系统通用),外部模块可用
17
+ * require('mm_action').actionAdmin('os') 等方式直接拿到
18
+ * mm_tpl 全局钩子正在使用的实例,实现共用与统一管理;
19
+ * - 实例级钩子需要隔离时,由调用方通过 Hook.genScope() 显式生成
20
+ * 唯一作用域传入。
21
+ *
10
22
  * @class
11
23
  */
12
24
  class Hook {
25
+ /**
26
+ * 默认作用域(系统级通用),与其他模块的 xxxAdmin('os') 统一
27
+ */
28
+ static GLOBAL_SCOPE = 'os';
29
+
30
+ /**
31
+ * 生成唯一作用域名(tpl_hook_N),供需要实例级隔离的调用方使用
32
+ * @returns {string} 唯一作用域名称
33
+ */
34
+ static genScope() {
35
+ Hook._seq = (Hook._seq || 0) + 1;
36
+ return `tpl_hook_${Hook._seq}`;
37
+ }
38
+
13
39
  /**
14
40
  * 创建钩子实例
15
41
  * @param {Object} logger - 日志对象,需含 error 方法,缺省使用 console
42
+ * @param {string} [scope] - 作用域名称,缺省为 os(系统级通用);
43
+ * 相同作用域共用同一组钩子实例,需要隔离时传入 Hook.genScope() 生成的唯一作用域
16
44
  */
17
- constructor(logger) {
45
+ constructor(logger, scope) {
18
46
  /**
19
47
  * 日志对象
20
48
  */
21
49
  this._logger = logger || console;
22
- this._func = {};
23
- this._action = {};
24
- this._filter = {};
50
+ /**
51
+ * 作用域名称
52
+ */
53
+ this._scope = scope || Hook.GLOBAL_SCOPE;
54
+ this.scope = this._scope;
55
+ /**
56
+ * 动作钩子模块(Admin 按作用域注册/共用)
57
+ */
58
+ this.action = mm_action.actionAdmin(this._scope, { logger: this._logger });
59
+ /**
60
+ * 过滤钩子模块(Admin 按作用域注册/共用)
61
+ */
62
+ this.filter = mm_filter.filterAdmin(this._scope, { logger: this._logger });
63
+ /**
64
+ * 函数钩子模块(Admin 按作用域注册/共用)
65
+ */
66
+ this.func = mm_func.funcAdmin(this._scope, { logger: this._logger });
25
67
  }
26
- }
27
68
 
28
- /**
29
- * 排序钩子列表
30
- * @param {Array} list - 钩子列表
31
- * @returns {Array} 排序后的列表
32
- */
33
- Hook.prototype.sort = function (list) {
34
- return list.sort((o1, o2) => {
35
- return o2.sort - o1.sort
36
- });
37
- };
38
-
39
- /**
40
- * 获取函数别名
41
- * @param {Function} func - 钩子函数
42
- * @param {string} alias - 别名
43
- * @returns {string} 函数别名
44
- */
45
- Hook.prototype._getName = function (func, alias) {
46
- if (!alias) {
47
- var str = func.toString();
48
- var name = TplUtil.between(str, 'function ', '(').trim();
49
- if (name) {
50
- alias = name;
51
- } else {
52
- alias = TplUtil.md5((new Date()).toString()).substring(0, 8);
53
- }
69
+ /**
70
+ * 获取作用域名称
71
+ * 外部模块可用该作用域通过 xxxAdmin(scope) 拿到同一组钩子实例
72
+ * @returns {string} 作用域名称
73
+ */
74
+ getScope() {
75
+ return this._scope;
54
76
  }
55
- return alias;
56
- };
77
+ }
57
78
 
58
79
  /**
59
80
  * 添加函数钩子
@@ -64,16 +85,7 @@ Hook.prototype._getName = function (func, alias) {
64
85
  * @returns {string} 函数别名
65
86
  */
66
87
  Hook.prototype.addFunc = function (name, func, sort = 100, alias = "") {
67
- alias = this._getName(func, alias);
68
- var obj = {
69
- func,
70
- sort,
71
- alias
72
- }
73
- var arr = this._func[name] || [];
74
- arr.push(obj);
75
- this._func[name] = this.sort(arr);
76
- return alias;
88
+ return this.func.addFunc(name, func, sort, alias);
77
89
  };
78
90
 
79
91
  /**
@@ -85,20 +97,14 @@ Hook.prototype.addFunc = function (name, func, sort = 100, alias = "") {
85
97
  * @returns {string} 函数别名
86
98
  */
87
99
  Hook.prototype.addAction = function (name, func, sort = 100, alias = "") {
88
- alias = this._getName(func, alias);
89
- var obj = {
90
- func,
91
- sort,
92
- alias
93
- }
94
- var arr = this._action[name] || [];
95
- arr.push(obj);
96
- this._action[name] = this.sort(arr);
97
- return alias;
100
+ return this.action.addAction(name, func, sort, alias);
98
101
  };
99
102
 
100
103
  /**
101
104
  * 添加过滤钩子
105
+ * mm_tpl 约定过滤函数签名为 func(model, ret, ...args)(模型在前),
106
+ * mm_filter 约定为 func(value, ...args)(管道值在前),此处做参数顺序适配;
107
+ * 过滤函数返回假值时保留原值
102
108
  * @param {string} name - 钩子名称
103
109
  * @param {Function} func - 钩子函数
104
110
  * @param {number} sort - 执行顺序
@@ -106,101 +112,52 @@ Hook.prototype.addAction = function (name, func, sort = 100, alias = "") {
106
112
  * @returns {string} 函数别名
107
113
  */
108
114
  Hook.prototype.addFilter = function (name, func, sort = 100, alias = "") {
109
- alias = this._getName(func, alias);
110
- var obj = {
111
- func,
112
- sort,
113
- alias
114
- }
115
- var arr = this._filter[name] || [];
116
- arr.push(obj);
117
- this._filter[name] = this.sort(arr);
118
- return alias;
115
+ alias = mm_filter.Filter.getName(func, alias);
116
+ return this.filter.addFilter(name, function (value, model, ...args) {
117
+ return func(model, value, ...args);
118
+ }, sort, alias);
119
119
  };
120
120
 
121
121
  /**
122
122
  * 删除函数钩子
123
123
  * @param {String} name 钩子名称
124
124
  * @param {String} alias 函数别名
125
+ * @returns {boolean} 是否删除成功
125
126
  */
126
- Hook.prototype.delFunc = function (name, alias) {
127
- var arr = this._func[name];
128
- if (arr) {
129
- var list = [];
130
- for (var i = 0; i < arr.length; i++) {
131
- var item = arr[i];
132
- if (item.alias !== alias) {
133
- list.push(item);
134
- }
135
- }
136
- this._func[name] = list;
137
- }
127
+ Hook.prototype.delFunc = function (name, alias = "") {
128
+ return this.func.delFunc(name, alias);
138
129
  };
139
130
 
140
131
  /**
141
132
  * 删除动作钩子
142
133
  * @param {String} name 钩子名称
143
134
  * @param {String} alias 函数别名
135
+ * @returns {boolean} 是否删除成功
144
136
  */
145
- Hook.prototype.delAction = function (name, alias) {
146
- var arr = this._action[name];
147
- if (arr) {
148
- var list = [];
149
- for (var i = 0; i < arr.length; i++) {
150
- var item = arr[i];
151
- if (item.alias !== alias) {
152
- list.push(item);
153
- }
154
- }
155
- this._action[name] = list;
156
- }
137
+ Hook.prototype.delAction = function (name, alias = "") {
138
+ return this.action.delAction(name, alias);
157
139
  };
158
140
 
159
141
  /**
160
142
  * 删除过滤钩子
161
143
  * @param {String} name 钩子名称
162
144
  * @param {String} alias 函数别名
145
+ * @returns {boolean} 是否删除成功
163
146
  */
164
147
  Hook.prototype.delFilter = function (name, alias = "") {
165
- var arr = this._filter[name];
166
- if (arr) {
167
- var list = [];
168
- for (var i = 0; i < arr.length; i++) {
169
- var item = arr[i];
170
- if (item.alias !== alias) {
171
- list.push(item);
172
- }
173
- }
174
- this._filter[name] = list;
175
- }
148
+ return this.filter.delFilter(name, alias);
176
149
  };
177
150
 
178
151
  /**
179
152
  * 执行函数钩子
180
153
  * @param {string} name - 钩子名称
181
- * @param {Object} param - 参数
182
- * @param {Function} func - 钩子函数
154
+ * @param {Object} model - 模型对象
155
+ * @param {Object} req - 请求参数
156
+ * @param {...any} args - 附加参数
157
+ * @returns {Promise<Object>} 执行结果(最后一个真值)
183
158
  */
184
- Hook.prototype.runFunc = async function (name, model, req, ...args) {
185
- var ret;
186
- var list = this._func[name];
187
- if (list) {
188
- for (var i = 0; i < list.length; i++) {
189
- var item = list[i];
190
- try {
191
- var ret_new = item.func(model, req, ...args);
192
- if (types.isPromise(ret_new)) {
193
- ret_new = await ret_new;
194
- if (ret_new) {
195
- ret = ret_new;
196
- }
197
- }
198
- } catch (err) {
199
- this._logger.error(`[${this.constructor.name}] 执行函数钩子失败 ${item.alias}`, err);
200
- }
201
- }
202
- }
203
- return ret;
159
+ Hook.prototype.runFunc = function (name, model, req, ...args) {
160
+ return this.func.runFunc(name, model, req, ...args);
204
161
  };
205
162
 
206
163
  /**
@@ -208,19 +165,15 @@ Hook.prototype.runFunc = async function (name, model, req, ...args) {
208
165
  * @param {string} name - 钩子名称
209
166
  * @param {Object} model - 模型对象
210
167
  * @param {...any} args - 附加参数
211
- * @returns {Object} 执行结果
168
+ * @returns {string} 执行结果(各动作返回值按顺序字符串拼接)
212
169
  */
213
170
  Hook.prototype.runAction = function (name, model, ...args) {
171
+ var results = this.action.runAction(name, model, ...args);
214
172
  var ret = '';
215
- var list = this._action[name];
216
- if (list) {
217
- for (var i = 0; i < list.length; i++) {
218
- var o = list[i];
219
- try {
220
- ret += o.func(model, ...args);
221
- } catch (err) {
222
- this._logger.error(`[${this.constructor.name}] 执行动作钩子失败 ${o.alias}`, err);
223
- }
173
+ for (var i = 0; i < results.length; i++) {
174
+ var r = results[i];
175
+ if (r !== undefined && r !== null) {
176
+ ret += String(r);
224
177
  }
225
178
  }
226
179
  return ret;
@@ -235,59 +188,18 @@ Hook.prototype.runAction = function (name, model, ...args) {
235
188
  * @returns {Object} 过滤后的参数
236
189
  */
237
190
  Hook.prototype.runFilter = function (name, model, ret, ...args) {
238
- var list = this._filter[name];
239
- if (list) {
240
- for (var i = 0; i < list.length; i++) {
241
- var item = list[i];
242
- try {
243
- ret = item.func(model, ret, ...args) || ret;
244
- } catch (err) {
245
- this._logger.error(`[${this.constructor.name}] 执行过滤钩子失败 ${item.alias}`, err);
246
- }
247
- }
248
- }
249
- return ret;
191
+ return this.filter.runFilter(name, ret, model, ...args);
250
192
  };
251
-
252
193
  /**
253
194
  * 删除钩子
195
+ * 同时删除该别名下的函数钩子、动作钩子与过滤钩子
254
196
  * @param {string} name - 钩子名称
255
197
  * @param {string} alias - 函数别名
256
198
  */
257
- Hook.prototype.del = function (name, alias) {
258
- var arr = this._func[name];
259
- if (arr) {
260
- var list = [];
261
- for (var i = 0; i < arr.length; i++) {
262
- var item = arr[i];
263
- if (item.alias !== alias) {
264
- list.push(item);
265
- }
266
- }
267
- this._func[name] = list;
268
- }
269
- arr = this._action[name];
270
- if (arr) {
271
- var list = [];
272
- for (var i = 0; i < arr.length; i++) {
273
- var item = arr[i];
274
- if (item.alias !== alias) {
275
- list.push(item);
276
- }
277
- }
278
- this._action[name] = list;
279
- }
280
- arr = this._filter[name];
281
- if (arr) {
282
- var list = [];
283
- for (var i = 0; i < arr.length; i++) {
284
- var item = arr[i];
285
- if (item.alias !== alias) {
286
- list.push(item);
287
- }
288
- }
289
- this._filter[name] = list;
290
- }
199
+ Hook.prototype.del = function (name, alias = "") {
200
+ this.func.delFunc(name, alias);
201
+ this.action.delAction(name, alias);
202
+ this.filter.delFilter(name, alias);
291
203
  };
292
204
 
293
205
  /**
@@ -295,15 +207,9 @@ Hook.prototype.del = function (name, alias) {
295
207
  * @param {string} name - 钩子名称
296
208
  */
297
209
  Hook.prototype.clear = function (name) {
298
- if (name) {
299
- delete this._func[name];
300
- delete this._action[name];
301
- delete this._filter[name];
302
- } else {
303
- this._func = {};
304
- this._action = {};
305
- this._filter = {};
306
- }
210
+ this.func.clear(name);
211
+ this.action.clear(name);
212
+ this.filter.clear(name);
307
213
  };
308
214
 
309
- module.exports = Hook;
215
+ module.exports = Hook;
package/lib/lang.js CHANGED
@@ -154,6 +154,50 @@ Lang.prototype.buildDict = function (force) {
154
154
  return Object.assign({}, dict);
155
155
  };
156
156
 
157
+ /**
158
+ * 将嵌套语言包字典规范化为 mm_lang 的扁平词条表
159
+ * mm_lang 的 Lang 只接受 `语言.层级键 → 文本` 的扁平入参,控制键(default、silent)原样保留
160
+ * @param {Object} dict 语言包字典,语言代码对应嵌套词条对象
161
+ * @returns {Object} 扁平词条表
162
+ */
163
+ function flattenLangPack(dict) {
164
+ var flat = {};
165
+ for (var key in dict) {
166
+ if (!Object.prototype.hasOwnProperty.call(dict, key)) {
167
+ continue;
168
+ }
169
+ var val = dict[key];
170
+ if (val && typeof (val) === 'object' && !Array.isArray(val)) {
171
+ flattenInto(val, key, flat);
172
+ } else {
173
+ flat[key] = val;
174
+ }
175
+ }
176
+ return flat;
177
+ }
178
+
179
+ /**
180
+ * 递归展开嵌套词条为点号路径键 - 内部函数
181
+ * @param {Object} obj 嵌套词条对象
182
+ * @param {string} prefix 键前缀
183
+ * @param {Object} flat 输出容器
184
+ * @returns {void}
185
+ */
186
+ function flattenInto(obj, prefix, flat) {
187
+ for (var key in obj) {
188
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) {
189
+ continue;
190
+ }
191
+ var val = obj[key];
192
+ var path = prefix + '.' + key;
193
+ if (val && typeof (val) === 'object' && !Array.isArray(val)) {
194
+ flattenInto(val, path, flat);
195
+ } else {
196
+ flat[path] = val;
197
+ }
198
+ }
199
+ }
200
+
157
201
  /**
158
202
  * 创建语言实例
159
203
  * 语言包字典在实例间共享,仅当前语言各自独立
@@ -166,7 +210,7 @@ Lang.prototype.create = function (config, force) {
166
210
  dict.default = config.default_lang || dict.default || 'zh_cn';
167
211
  // 词条缺失时静默返回 undefined,日志由模板引擎自身的 log 体系统一输出
168
212
  dict.silent = true;
169
- var lang = new BaseLang(dict);
213
+ var lang = new BaseLang(flattenLangPack(dict));
170
214
  this.registerPlural(lang, config.plural_rules);
171
215
  return lang;
172
216
  };
package/lib/tpl.js CHANGED
@@ -1,4 +1,7 @@
1
1
  const engine = require('art-template');
2
+ const {
3
+ varsAdmin
4
+ } = require('mm_vars');
2
5
  const {
3
6
  Base
4
7
  } = require('./base.js');
@@ -144,13 +147,19 @@ class Tpl extends Base {
144
147
  * 全局钩子实例
145
148
  * 所有引擎实例共享,与实例级 this.hook 一起参与执行,全局先执行、实例级后执行
146
149
  * 宿主应用注册跨实例钩子时使用,如 Tpl.global_hook.addFunc(...)
150
+ * 以默认作用域 Hook.GLOBAL_SCOPE(os,代表整个系统通用)在 mm_action/mm_filter/mm_func
151
+ * 的 Admin 工厂中注册,外部模块可用 actionAdmin('os') 等同名作用域取出同一组钩子实例共用
147
152
  */
148
- static global_hook = new Hook();
153
+ static global_hook = new Hook(null, Hook.GLOBAL_SCOPE);
149
154
  /**
150
- * 全局数据袋
151
- * 渲染时注入模型的 globalBag 字段,供模板跨实例共享数据
155
+ * 全局数据袋(mm_vars 变量实例)
156
+ * 以默认作用域 os(整个系统通用)通过 varsAdmin('os') 获取,
157
+ * 外部模块可用 require('mm_vars').varsAdmin('os') 取出同一实例,实现共用与统一管理。
158
+ * 写入用 set(key, value)、读取用 get(key)、删除用 del(key);
159
+ * 渲染时以 list() 副本注入模型的 globalBag 字段(模板内直接 m.globalBag.key 取值),
160
+ * 模板内的修改只影响本次渲染的副本,不会写回全局
152
161
  */
153
- static global_bag = {};
162
+ static global_bag = varsAdmin('os');
154
163
  /**
155
164
  * 全局语言包字典
156
165
  * 实例未配置任何语言包时的回退字典,形如 { 语言代码: 词条对象 }
@@ -198,6 +207,18 @@ class Tpl extends Base {
198
207
  */
199
208
  this.error = null;
200
209
 
210
+ /**
211
+ * 编译缓存:模板源码(+options 指纹)→ 已编译渲染函数(LRU)
212
+ * art-template 对字符串源码每次 render 都重新编译,区块页面(几十个模板/请求)
213
+ * 会把 CPU 耗在重复编译上;body 即缓存键,内容变更后键自然变化,无需手动失效
214
+ */
215
+ this._tpl_cache = new Map();
216
+
217
+ /**
218
+ * 编译缓存上限,超过后淘汰最久未命中的条目,可通过 config.cache_size 调整
219
+ */
220
+ this._tpl_cache_max = Number(this.config.cache_size) || 512;
221
+
201
222
  /**
202
223
  * 字符转义规则
203
224
  */
@@ -240,8 +261,10 @@ class Tpl extends Base {
240
261
 
241
262
  /**
242
263
  * 实例级钩子,仅作用于当前实例;与全局钩子 Tpl.global_hook 一起参与执行,全局先执行、实例级后执行
264
+ * 通过 Hook.genScope() 显式生成唯一作用域(tpl_hook_N)在 Admin 工厂中注册,实例间相互隔离;
265
+ * 注意不能省略作用域,否则会默认共用系统级 os 作用域,与全局钩子重复执行
243
266
  */
244
- this.hook = new Hook(this.config.logger);
267
+ this.hook = new Hook(this.config.logger, Hook.genScope());
245
268
 
246
269
  /**
247
270
  * 初始化
@@ -537,22 +560,14 @@ Tpl.prototype.tc = function (key, count, ...args) {
537
560
 
538
561
  /**
539
562
  * 获取支持的语言列表
540
- * 过滤掉无词条的语言,Lang 内置的 zh_cn、en 空字典不计入
563
+ * 语言列表来自 mm_lang 的扁平词条表,无词条的语言不会出现,无需过滤空字典
541
564
  * @returns {Array<string>} 语言代码数组
542
565
  */
543
566
  Tpl.prototype.getLangs = function () {
544
567
  if (!this.lang) {
545
568
  return [];
546
569
  }
547
- var langs = this.lang.getLangs();
548
- var list = [];
549
- for (var i = 0; i < langs.length; i++) {
550
- var dict = this.lang.config[langs[i]];
551
- if (dict && Object.keys(dict).length) {
552
- list.push(langs[i]);
553
- }
554
- }
555
- return list;
570
+ return this.lang.getLangs();
556
571
  };
557
572
 
558
573
  /**
@@ -580,7 +595,8 @@ Tpl.prototype.loadLang = function (lang) {
580
595
  return null;
581
596
  }
582
597
  this.reloadLangs();
583
- var dict = this.lang ? this.lang.config[lang] : null;
598
+ // 通过 getDict 导出该语言的嵌套词条字典(形态与语言包文件一致)
599
+ var dict = this.lang ? this.lang.getDict(lang) : null;
584
600
  if (!dict) {
585
601
  this.log('warn', '语言包不存在:' + lang);
586
602
  return null;
@@ -956,14 +972,45 @@ Tpl.prototype._renderMain = function (body, model, options) {
956
972
  if (body) {
957
973
  var m = Object.assign({}, {
958
974
  viewBag: this.viewBag,
959
- globalBag: Tpl.global_bag
975
+ globalBag: Tpl.global_bag.list()
960
976
  }, model);
961
977
  this.loadFunc(m);
962
- ret = this.engine.render(body, m, options);
978
+ ret = this._getCompiled(body, options)(m);
963
979
  }
964
980
  return ret;
965
981
  };
966
982
 
983
+ /**
984
+ * 获取已编译渲染函数(LRU 缓存)
985
+ * 未命中时经 engine.compile 编译后入缓存;命中时移至最新位。
986
+ * options 非空时并入键(编译结果可能受 rules 等选项影响),空 options 直接以源码为键。
987
+ * @param {string} body - 模板源码(调用方已完成转义)
988
+ * @param {Object} options - 配置参数
989
+ * @returns {Function} 渲染函数,入参为数据模型
990
+ */
991
+ Tpl.prototype._getCompiled = function (body, options) {
992
+ var key = body;
993
+ if (options !== undefined && options !== null && Object.keys(options).length) {
994
+ key = body + '\u0000' + JSON.stringify(options);
995
+ }
996
+ var cache = this._tpl_cache;
997
+ var fn = cache.get(key);
998
+ if (fn) {
999
+ // LRU 触碰:删除后重插,使其移到最新位
1000
+ cache.delete(key);
1001
+ cache.set(key, fn);
1002
+ return fn;
1003
+ }
1004
+ // 与 engine.render(body, m, options) 等价:art-template 的 render 内部即 compile(source, options)(data)
1005
+ fn = this.engine.compile(body, options);
1006
+ cache.set(key, fn);
1007
+ if (cache.size > this._tpl_cache_max) {
1008
+ // 淘汰最旧(Map 迭代序即插入序,首个键即最久未触碰)
1009
+ cache.delete(cache.keys().next().value);
1010
+ }
1011
+ return fn;
1012
+ };
1013
+
967
1014
  /**
968
1015
  * 套用本次渲染的临时选项
969
1016
  * options.style 与 options.lang 只对当前这次渲染生效,由 _restoreTempOptions 恢复
package/package.json CHANGED
@@ -1,66 +1,70 @@
1
- {
2
- "name": "mm_tpl",
3
- "version": "2.8.0",
4
- "description": "Super Meimei Template Engine - High-performance template engine based on art-template with MVC pattern, hook system, theme switching and caching mechanism",
5
- "main": "index.js",
6
- "io": true,
7
- "priority": 100,
8
- "scripts": {
9
- "dev": "nodemon test.js",
10
- "demo": "node example/demo.js",
11
- "test": "node test.js",
12
- "coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage"
13
- },
14
- "repository": {
15
- "type": "git",
16
- "url": "git+https://gitee.com/qiuwenwu91/mm_tpl.git"
17
- },
18
- "keywords": [
19
- "mm_mod",
20
- "mm_infra",
21
- "mm_common",
22
- "mm_web",
23
- "template",
24
- "engine",
25
- "mvc",
26
- "web",
27
- "view",
28
- "render",
29
- "art-template",
30
- "hook",
31
- "theme",
32
- "cache",
33
- "nodejs",
34
- "javascript"
35
- ],
36
- "layer": "infra",
37
- "group": "common",
38
- "author": "邱文武",
39
- "license": "MIT",
40
- "bugs": {
41
- "url": "https://gitee.com/qiuwenwu91/mm_tpl/issues"
42
- },
43
- "homepage": "https://gitee.com/qiuwenwu91/mm_tpl#readme",
44
- "dependencies": {
45
- "art-template": "4.13.4",
46
- "mm_corejs": "^1.1.6",
47
- "mm_lang": "^2.0.0"
48
- },
49
- "engines": {
50
- "node": ">=12.0.0"
51
- },
52
- "files": [
53
- "index.js",
54
- "hook.js",
55
- "tpl_rule.js",
56
- "lib/",
57
- "doc/",
58
- "README.md",
59
- "README_EN.md",
60
- "package.json"
61
- ],
62
- "devDependencies": {
63
- "eslint": "^9.39.5",
64
- "eslint-plugin-jsdoc": "^65.0.0"
65
- }
66
- }
1
+ {
2
+ "name": "mm_tpl",
3
+ "version": "2.10.0",
4
+ "description": "Super Meimei Template Engine - High-performance template engine based on art-template with MVC pattern, hook system, theme switching and caching mechanism",
5
+ "main": "index.js",
6
+ "io": true,
7
+ "priority": 100,
8
+ "scripts": {
9
+ "dev": "nodemon test.js",
10
+ "demo": "node example/demo.js",
11
+ "test": "node test.js",
12
+ "coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://gitee.com/qiuwenwu91/mm_tpl.git"
17
+ },
18
+ "keywords": [
19
+ "mm_mod",
20
+ "mm_infra",
21
+ "mm_common",
22
+ "mm_web",
23
+ "template",
24
+ "engine",
25
+ "mvc",
26
+ "web",
27
+ "view",
28
+ "render",
29
+ "art-template",
30
+ "hook",
31
+ "theme",
32
+ "cache",
33
+ "nodejs",
34
+ "javascript"
35
+ ],
36
+ "layer": "infra",
37
+ "group": "common",
38
+ "author": "邱文武",
39
+ "license": "MIT",
40
+ "bugs": {
41
+ "url": "https://gitee.com/qiuwenwu91/mm_tpl/issues"
42
+ },
43
+ "homepage": "https://gitee.com/qiuwenwu91/mm_tpl#readme",
44
+ "dependencies": {
45
+ "art-template": "4.13.4",
46
+ "mm_action": "^1.1.0",
47
+ "mm_corejs": "^1.1.6",
48
+ "mm_filter": "^1.0.0",
49
+ "mm_func": "^1.0.0",
50
+ "mm_lang": "^3.0.0",
51
+ "mm_vars": "^1.0.1"
52
+ },
53
+ "engines": {
54
+ "node": ">=12.0.0"
55
+ },
56
+ "files": [
57
+ "index.js",
58
+ "hook.js",
59
+ "tpl_rule.js",
60
+ "lib/",
61
+ "doc/",
62
+ "README.md",
63
+ "README_EN.md",
64
+ "package.json"
65
+ ],
66
+ "devDependencies": {
67
+ "eslint": "^9.39.5",
68
+ "eslint-plugin-jsdoc": "^65.0.0"
69
+ }
70
+ }