模板引擎的作用就是将模板渲染成html,
,常见的js模板引擎有Pug,Nunjucks,Mustache等。网上一些制作模板引擎的文章大部分是用正则表达式做一些hack工作,看完能收获的东西很少。本文将使用编译原理那套理论来打造自己的模板引擎。之前玩过一年Django,还是偏爱那套模板引擎,这次就打算自己用js写一个,就叫jstemp
- html = render(template,data)
写一个库,不可能一次性把所有功能全部实现,所以我们第一版就挑一些比较核心的功能
- var jstemp = require('jstemp');
- // 渲染变量
- jstemp.render('{{value}}', {value: 'hello world'});// hello world
- // 渲染if/elseif/else表达式
- jstemp.render('{% if value1 %}hello{% elseif value %}world{% else %}byebye{% endif %}', {value: 'hello world'});// world
- // 渲染列表
- jstemp.render('{%for item : list %}{{item}}{%endfor%}', {list:[1, 2, 3]});// 123
词法分析就是将字符串分割成一个一个有意义的token,每个token都有它要表达的意义,供语法分析器去建AST。
jstemp的token类型如下
- {
- EOF: 0, // 文件结束
- Character: 1, // 字符串
- Variable: 2, // 变量开始{{
- VariableName: 3, // 变量名
- IfStatement: 4,// if 语句
- IfCondition: 5,// if 条件
- ElseIfStatement: 6,// else if 语句
- ElseStatement: 7,// else 语句
- EndTag: 8,// }},%}这种闭合标签
- EndIfStatement: 9,// endif标签
- ForStatement: 10,// for 语句
- ForItemName: 11,// for item 的变量名
- ForListName: 12,// for list 的变量名
- EndForStatement: 13// endfor 标签
- };
一般来说,词法分析有几种方法(欢迎补充)
作者本着自虐的心理,采取了第三种方法。
举例说明有穷状态自动机,解析
的过程
- <p>{{value}}</p>
结果是
,
- {type:Character,value:'<p>'}
,
- {type:Variable}
,
- {type:VariableName, valueName: 'value'}
,
- {type:EndTag}
这五个token。(当然如果你喜欢,可以把
- {type:Character,value:'</p>'}
当作一个token,但是我这里分成了五个)。最后因为考虑到空格和if/elseif/else,for等情况,状态机又复杂了许多。
- {{value}}
代码的话就是一个循环加一堆switch 转化状态(特别很累,也很容易出错),有一些情况我也没考虑全。截一部分代码下来看
- nextToken() {
- Tokenizer.currentToken = '';
- while (this.baseoffset < this.template.length) {
- switch (this.state) {
- case Tokenizer.InitState:
- if (this.template[this.baseoffset] === '{') {
- this.state = Tokenizer.LeftBraceState;
- this.baseoffset++;
- }
- else if (this.template[this.baseoffset] === '\\') {
- this.state = Tokenizer.EscapeState;
- this.baseoffset++;
- }
- else {
- this.state = Tokenizer.CharState;
- Tokenizer.currentToken += this.template[this.baseoffset++];
- }
- break;
- case Tokenizer.CharState:
- if (this.template[this.baseoffset] === '{') {
- this.state = Tokenizer.LeftBraceState;
- this.baseoffset++;
- return TokenType.Character;
- }
- else if (this.template[this.baseoffset] === '\\') {
- this.state = Tokenizer.EscapeState;
- this.baseoffset++;
- }
- else {
- Tokenizer.currentToken += this.template[this.baseoffset++];
- }
- break;
- case Tokenizer.LeftBraceState:
- if (this.template[this.baseoffset] === '{') {
- this.baseoffset++;
- this.state = Tokenizer.BeforeVariableState;
- return TokenType.Variable;
- }
- else if (this.template[this.baseoffset] === '%') {
- this.baseoffset++;
- this.state = Tokenizer.BeforeStatementState;
- }
- else {
- this.state = Tokenizer.CharState;
- Tokenizer.currentToken += '{' + this.template[this.baseoffset++];
- }
- break;
- // ...此处省去无数case
- default:
- console.log(this.state, this.template[this.baseoffset]);
- throw Error('错误的语法');
- }
- }
- if (this.state === Tokenizer.InitState) {
- return TokenType.EOF;
- }
- else if (this.state === Tokenizer.CharState) {
- this.state = Tokenizer.InitState;
- return TokenType.Character;
- }
- else {
- throw Error('错误的语法');
- }
- }
具体代码看这里
当我们将字符串序列化成一个个token后,就需要建AST树。树的根节点rootNode为一个childNodes数组用来连接子节点
- let rootNode = {childNodes:[]}
字符串节点
- {
- type: 'character',
- value: '123'
- }
变量节点
- {
- type: 'variable',
- valueName: 'name'
- }
if 表达式的节点和for表达式节点可以嵌套其他语句,所以要多一个childNodes数组来装语句内的表达式,childNodes 可以装任意的node,然后我们解析的时候递归向下解析。elseifNodes 装elseif/else 节点,解析的时候,当if的conditon为false的时候,按顺序取elseifNodes数组里的节点,谁的condition为true,就执行谁的childNodes,然后返回结果。
- // if node
- {
- type: 'if',
- condition: '',
- elseifNodes: [],
- childNodes: [],
- }
- // elseif node
- {
- type: 'elseif',
- // 其实这个属性没用
- condition: '',
- childNodes: []
- }
- // else node
- {
- type: 'elseif',
- // 其实这个属性没用
- condition: true,
- childNodes: []
- }
for节点
- {
- type: 'for',
- itemName: '',
- listName: '',
- childNodes: []
- }
举例:
- let template = `
- <p>how to</p>
- {%for num : list %}
- let say{{num.num}}
- {%endfor%}
- {%if obj%}
- {{obj.test}}
- {%else%}
- hello world
- {%endif%}
- `;
- // AST树为
- let rootNode = {
- childNode:[
- {
- type:'char',
- value: '<p>how to</p>'
- },
- {
- type:'for',
- itemName: 'num',
- listName: 'list',
- childNodes:[
- {
- type:'char',
- value:'let say',
- },
- {
- type: 'variable',
- valueName: 'num.num'
- }
- ]
- },
- {
- type:'if',
- condition: 'obj',
- childNodes: [
- {
- type: 'variable',
- valueName: 'obj.test'
- }
- ],
- elseifNodes: [
- {
- type: 'elseif',
- condition:true,
- childNodes:[
- {
- type: 'char',
- value: 'hello world'
- }
- ]
- }
- ]
- }
- ]
- }
具体建树逻辑可以看代码
解析变量节点
从rootNode节点开始解析
- let html = '';
- for (let node of rootNode.childNodes) {
- html += calStatement(env, node);
- }
calStatement为所有语句的解析入口
- function calStatement(env, node) {
- let html = '';
- switch (node.type) {
- case NodeType.Character:
- html += node.value;
- break;
- case NodeType.Variable:
- html += calVariable(env, node.valueName);
- break;
- case NodeType.IfStatement:
- html += calIfStatement(env, node);
- break;
- case NodeType.ForStatement:
- html += calForStatement(env, node);
- break;
- default:
- throw Error('未知node type');
- }
- return html;
- }
解析变量
- // env为数据变量如{value:'hello world'},valueName为变量名
- function calVariable(env, valueName) {
- if (!valueName) {
- return '';
- }
- let result = env;
- for (let name of valueName.split('.')) {
- result = result[name];
- }
- return result;
- }
解析if 语句及condition 条件
- // 目前只支持变量值判断,不支持||,&&,<=之类的表达式
- function calConditionStatement(env, condition) {
- if (typeof condition === 'string') {
- return calVariable(env, condition) ? true: false;
- }
- return condition ? true: false;
- }
- function calIfStatement(env, node) {
- let status = calConditionStatement(env, node.condition);
- let result = '';
- if (status) {
- for (let childNode of node.childNodes) {
- // 递归向下解析子节点
- result += calStatement(env, childNode);
- }
- return result;
- }
- for (let elseifNode of node.elseifNodes) {
- let elseIfStatus = calConditionStatement(env, elseifNode.condition);
- if (elseIfStatus) {
- for (let childNode of elseifNode.childNodes) {
- // 递归向下解析子节点
- result += calStatement(env, childNode);
- }
- return result;
- }
- }
- return result;
- }
解析for节点
- function calForStatement(env, node) {
- let result = '';
- let obj = {};
- let name = node.itemName.split('.')[0];
- for (let item of env[node.listName]) {
- obj[name] = item;
- let statementEnv = Object.assign(env, obj);
- for (let childNode of node.childNodes) {
- // 递归向下解析子节点
- result += calStatement(statementEnv, childNode);
- }
- }
- return result;
- }
目前的实现的jstemp功能还比较单薄,存在以下不足:
...
未来将一步步完善,另外无耻求个star
github地址
来源: https://juejin.im/post/5a04a08ff265da430e4e9d42