实现多级作用域
在将 ast 转换成 html 的时候,有一个很常见的场景就是多级作用域,比如在一个 for 循环中再嵌套一个 for 循环。而如何在做这个作用域分割,其实也是很简单,就是通过递归。
比如我的对一个 ast 树的处理方法命名为:processAst(ast, scope),再比如最初的 scope 是
{ list: [ { subs: [1, 2, 3] }, { subs: [4, 5, 6] } ] } |
那么 processAst 就可以这么实现:
function processAst(ast, scope) { ... if(ast.for) { const list = scope[ast.item]; // ast.item 自然就是列表的 key ,比如上面的 list list.forEach(item => { processAst(ast.children, Object.assign({}, scope, { [ast.key]: item, // ast.key 则是 for key in list 中的 key })) }) } ... } |
就简单通过一个递归,就可以把作用域一直传递下去了。
Filter 功能实现
实现上面功能后,组件就已经具备基本的模板渲染能力,不过在用模板引擎的时候,还有一个很常用的功能就是 filter 。一般来说 filter 的使用方式都是这这样 {{ test | filter1 | filter2 }},这个的实现也说一下,这一块的实现我参考了 vue 的解析的方式,还是蛮有意思的。
还是举个例子:
{{ test | filter1 | filter2 }}
在构建 AST 的时候,就可以获取到其中的test | filter1 | filter2,然后我们可以很简单的就获取到 filter1 和 filter2 这两个字符串。起初我的实现方式,是把这些 filter 字符串扔进 ast 节点的 filters 数组中,在渲染的时候再一个一个拿出来处理。
不过后来又觉得为了性能考虑,能够在 AST 阶段就能做完的工作就不要放到渲染阶段了。因此就改成 vue 的方法组合方式。也就是把上面字符串变成:
_$f('filter2', _$f('filter1', test))
预先用个方法包裹起来,在渲染的时候,就不需要再通过循环去获取 filter 并且执行了。具体实现如下:
const filterRE = /(?:\|\s*\w+\s*)+$/; const filterSplitRE = /\s*\|\s*/; function processFilter(expr, escape) { let result = expr; const matches = expr.match(filterRE); if (matches) { const arr = matches[0].trim().split(filterSplitRE); result = expr.slice(0, matches.index); // add filter method wrapping utils.forEach(arr, name => { if (!name) { return; } // do not escape if has safe filter if (name === 'safe') { escape = false; return; } result = `_$f('${name}', ${result})`; }); } return escape ? `_$f('escape', ${result})` : result; } |
上面还有一个就是对 safe 的处理,如果有 safe 这个 filter ,就不做 escape 了。完成这个之后,有 filter 的 variable 都会变成_$f('filter2', _$f('filter1', test))这种形式了。因此,此前的 computedExpression 方法也要做一些改造了。
function processFilter(filterName, str) { const filter = filters[filterName] || globalFilters[filterName]; if (!filter) { throw new Error(`unknown filter ${filterName}`); } return filter(str); } function computedExpression(obj, expression) { const methodBody = `return (${expression})`; const funcString = obj ? `with(_$o){ ${methodBody} }` : methodBody; const func = new Function('_$o', '_$f', funcString); try { const result = func(obj, processFilter); return (result === undefined || result === null) ? '' : result; } catch (e) { // only catch the not defined error if (e.message.indexOf('is not defined') >= 0) { return ''; } else { throw e; } } } |
其实也是很简单,就是在 new Function 的时候,多传入一个获取 filter 的方法即可,然后有 filter 的 variable 就能被正常识别解析了。
至此,AST 构建、AST 到 html 的转换、多级作用域以及 Filter 的实现,都已经基本讲解完成。
贴一下自己实现的一个模板引擎轮子:https://github.com/whxaxes/mus
算是实现了大部分模板引擎该有的功能,欢迎各路豪杰 star 。