js模板引擎2 -- 解析模板
模板语法规则:
- 变量渲染使用
<%=变量名称%>
语法; - 条件判断使用
<% if(expr) { %> <% } %>
js语法; - 列表渲染使用
<% for(var i = 0; i < expr; i++) { %> <% } %>
js语法; - 模板注释使用
<%# %>
;
下面我们一步步的实现一个正则表达式来识别出模板语法规则部分。
正则匹配
语法规则以<%
开始,以%>
结尾,对应正则为 /<%([\w\W]*?)%>/
。
关于正则表达式的贪婪模式和非贪婪模式可以参考:正则基础之——贪婪与非贪婪模式。
这个正则表达式还不能识别变量名称和注释,扩展一下:
/<%(#?)(=)?([\w\W]*?)%>/
过滤一下语句首尾的空白符:
/<%(#?)(=)?[ \t]*([\w\W]*?)[ \t]*%>/
因为我们要取出所有的捕获组,所以要加上全局标识:
/<%(#?)(=)?[ \t]*([\w\W]*?)[ \t]*%>/g
正则表达式的主体部分已基本完成,下面使用exec()
方法测试一下:
假设模板为:
<% for (var i = 0; i < list.length; i ++) { %>
<li>索引 <%= i + 1 %> :<%= list[i] %></li>
<% } %>
测试代码:
var reg = /<%(#?)(=)?[ \t]*([\w\W]*?)[ \t]*%>/g;
var str = `
<% for (var i = 0; i < list.length; i ++) { %>
<li>索引 <%= i + 1 %> :<%= list[i] %></li>
<% } %>
`;
var match;
while(match = reg.exec(str)) {
console.log(match);
}
输出结果:
[
'<% for (var i = 0; i < list.length; i ++) { %>',
'',
undefined,
'for (var i = 0; i < list.length; i ++) {',
index: 5,
input: '\n' +
' <% for (var i = 0; i < list.length; i ++) { %>\n' +
' <li>索引 <%= i + 1 %> :<%= list[i] %></li>\n' +
' <% } %>\n',
groups: undefined
]
[
'<%= i + 1 %>',
'',
'=',
'i + 1',
index: 67,
input: '\n' +
' <% for (var i = 0; i < list.length; i ++) { %>\n' +
' <li>索引 <%= i + 1 %> :<%= list[i] %></li>\n' +
' <% } %>\n',
groups: undefined
]
[
'<%= list[i] %>',
'',
'=',
'list[i]',
index: 81,
input: '\n' +
' <% for (var i = 0; i < list.length; i ++) { %>\n' +
' <li>索引 <%= i + 1 %> :<%= list[i] %></li>\n' +
' <% } %>\n',
groups: undefined
]
[
'<% } %>',
'',
undefined,
'}',
index: 105,
input: '\n' +
' <% for (var i = 0; i < list.length; i ++) { %>\n' +
' <li>索引 <%= i + 1 %> :<%= list[i] %></li>\n' +
' <% } %>\n',
groups: undefined
]
匹配到了我们想要的结果。
解析模板
模板字符串的各个部分转换为token对象保存,通过token对象我们需要知道各个部分的类型。
一般有两种类型,一种是模板语法部分,这部分类型标记为express
。一种是非模板语法部分,这部分标记为string
。
后面构造js可执行代码时需要用到这个类型值。
此外还有各个部分的字符串字面量,在原模板字符串中的起始下标,如果是express
类型还需要提取出js代码语句。
token对象的构造函数:
function Token(type, value, preToken) {
this.type = type; // 类型 string / express
this.value = value; // 字符串值
this.script = null; // 如果是express 对应的代码
if(preToken) {
this.start = preToken.end; // 字符串值在原模板字符串中起始位置
} else {
this.start = 0;
}
this.end = this.start + value.length;// 字符串值在原模板字符串中结束位置
}
下面是把模板字符串的各个部分转换为token对象的逻辑,根据正则匹配结果的下标区分出string
类型部分和express
类型部分:
var reg = {
test: /<%(#?)(=)?[ \t]*([\w\W]*?)[ \t]*%>/g,
// 对识别出的模板语法语句做进一步处理变量
use: function (match, comment, output, code) {
if(output === '=') { // 变量 / 表达式
output = true;
} else {
output = false;
}
if(comment) { // 注释
code = '/*' + code + '*/';
output = false;
}
return {
code: code,
output: output
}
}
};
function tplToToken(str,reg) {
var tokens = [],
match,
preToken,
index = 0;
while((match = reg.test.exec(str)) !== null) {
preToken = tokens[tokens.length - 1];
// 如果匹配结果的开始下标比上一次匹配结果的结束下标大,说明两个模板语法之间有字符串
if(match.index > index) {
preToken = new Token('string', str.slice(index,match.index), preToken);
tokens.push(preToken);
}
preToken = new Token('express', match[0], preToken);
preToken.script = reg.use.apply(reg,match);
tokens.push(preToken);
// 保存本次匹配结果的结束下标
index = match.index + match[0].length;
}
return tokens;
}
调用tplToToken
函数会输出以下结果:
[
{
type: 'string',
value: '\n ',
script: null,
start: 0,
end: 5
},
{
type: 'express',
value: '<% for (var i = 0; i < list.length; i ++) { %>',
script: { code: 'for (var i = 0; i < list.length; i ++) {', output: false },
start: 5,
end: 51
},
{
type: 'string',
value: '\n <li>索引 ',
script: null,
start: 51,
end: 67
},
{
type: 'express',
value: '<%= i + 1 %>',
script: { code: 'i + 1', output: true },
start: 67,
end: 79
},
{
type: 'string',
value: ' :',
script: null,
start: 79,
end: 81
},
{
type: 'express',
value: '<%= list[i] %>',
script: { code: 'list[i]', output: true },
start: 81,
end: 95
},
{
type: 'string',
value: '</li>\n ',
script: null,
start: 95,
end: 105
},
{
type: 'express',
value: '<% } %>',
script: { code: '}', output: false },
start: 105,
end: 112
}
]
至此,模板的解析已经完成了,接下来要根据解析结果构造可执行的js代码,也就是模板的编译部分。