js模板引擎4--构造渲染函数
在上一篇中我们已经将模板解析为了一条条的js语句,那么只要把这些语句连起来,构造一个可执行函数,然后传入模板数据,就可以得到填充过数据的html片段。
// 构造渲染函数
function buildRender(scriptTokens) {
var codeArr = ['function(' + DATA + ') {','"use strict";'];
// 变量声明
var varDeclare = 'var';
variableContext.forEach(function(item, index) {
if(index === 0) {
varDeclare = 'var ' + item.name + ' = ' + item.value;
} else {
varDeclare += ', ' + item.name + ' = ' + item.value;
}
});
codeArr.push(varDeclare);
scriptTokens.forEach(function(item) {
codeArr.push(item.code);
});
codeArr.push('return ' + OUT_CODE);
codeArr.push('}');
// 构造渲染函数 利用闭包特性注入顶级变量
var render = (new Function(GLOBAL_DATA,"return " + codeArr.join('\n') + ";"))(globalThis);
return render;
}
用例子测试一下:
var tplStr = `
<% if (isAdmin) { %>
<!--fhj-->
<h1><%=title%></h1>
<ul>
<% for (var i = 0; i < list.length; i ++) { %>
<li>索引 <%= i + 1 %> :<%= list[i] %></li>
<% } %>
</ul>
<% var a = list.name; console.log(a); %>
<% } %>
`;
var tplTokens = tplToToken(tplStr,reg);
var scripts = getScriptTokens(tplTokens);
console.log(buildRender(scripts).toString());
生成的渲染函数如下(没有格式化):
function($DATA) {
"use strict";
var isAdmin = $DATA.isAdmin, $RES_OUT = "", title = $DATA.title, i = $DATA.i, list = $DATA.list, a = $DATA.a, console = $GLOBAL_DATA.console
$RES_OUT += ""
if (isAdmin) {
$RES_OUT += " <!--fhj--> <h1>"
$RES_OUT += title
$RES_OUT += "</h1> <ul> "
for (var i = 0; i < list.length; i ++) {
$RES_OUT += " <li>索引 "
$RES_OUT += i + 1
$RES_OUT += " :"
$RES_OUT += list[i]
$RES_OUT += "</li> "
}
$RES_OUT += " </ul> "
var a = list.name; console.log(a);
$RES_OUT += ""
}
return $RES_OUT
}
传入数据试试:
var data = {
title: '基本例子',
isAdmin: true,
list: ['文艺', '博客', '摄影', '电影', '民谣', '旅行', '吉他']
};
console.log(buildRender(scripts)(data));
输出结果(格式化后):
<!--fhj-->
<h1>基本例子</h1>
<ul>
<li>索引 1 :文艺</li>
<li>索引 2 :博客</li>
<li>索引 3 :摄影</li>
<li>索引 4 :电影</li>
<li>索引 5 :民谣</li>
<li>索引 6 :旅行</li>
<li>索引 7 :吉他</li>
</ul>
至此,我们已经初步实现了js模板引擎的功能。
把前面的代码优化一下:
;(function(globalThis) {
var TPL_TOKEN_TYPE_STRING = 'string';
var TPL_TOKEN_TYPE_EXPRESSION = 'expression';
var jsTokens = {
default: /((['"])(?:(?!\2|\\).|\\(?:\r\n|[\s\S]))*(\2)?|`(?:[^`\\$]|\\[\s\S]|\$(?!\{)|\$\{(?:[^{}]|\{[^}]*\}?)*\}?)*(`)?)|(\/\/.*)|(\/\*(?:[^*]|\*(?!\/))*(\*\/)?)|(\/(?!\*)(?:\[(?:(?![\]\\]).|\\.)*\]|(?![\/\]\\]).|\\.)+\/(?:(?!\s*(?:\b|[\u0080-\uFFFF$\\'"~({]|[+\-!](?!=)|\.?\d))|[gmiyu]{1,5}\b(?![\u0080-\uFFFF$\\]|\s*(?:[+\-*%&|^<>!=?({]|\/(?![\/*])))))|(0[xX][\da-fA-F]+|0[oO][0-7]+|0[bB][01]+|(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?)|((?!\d)(?:(?!\s)[$\w\u0080-\uFFFF]|\\u[\da-fA-F]{4}|\\u\{[\da-fA-F]+\})+)|(--|\+\+|&&|\|\||=>|\.{3}|(?:[+\-\/%&|^]|\*{1,2}|<{1,2}|>{1,3}|!=?|={1,2})=?|[?~.,:;[\](){}])|(\s+)|(^$|[\s\S])/g,
matchToToken: function(match) {
var token = {type: "invalid", value: match[0]}
if (match[ 1]) token.type = "string" , token.closed = !!(match[3] || match[4])
else if (match[ 5]) token.type = "comment"
else if (match[ 6]) token.type = "comment", token.closed = !!match[7]
else if (match[ 8]) token.type = "regex"
else if (match[ 9]) token.type = "number"
else if (match[10]) token.type = "name"
else if (match[11]) token.type = "punctuator"
else if (match[12]) token.type = "whitespace"
return token
}
};
var isJsKeywords = {
reservedKeywords: {
'abstract': true,
'await': true,
'boolean': true,
'break': true,
'byte': true,
'case': true,
'catch': true,
'char': true,
'class': true,
'const': true,
'continue': true,
'debugger': true,
'default': true,
'delete': true,
'do': true,
'double': true,
'else': true,
'enum': true,
'export': true,
'extends': true,
'false': true,
'final': true,
'finally': true,
'float': true,
'for': true,
'function': true,
'goto': true,
'if': true,
'implements': true,
'import': true,
'in': true,
'instanceof': true,
'int': true,
'interface': true,
'let': true,
'long': true,
'native': true,
'new': true,
'null': true,
'package': true,
'private': true,
'protected': true,
'public': true,
'return': true,
'short': true,
'static': true,
'super': true,
'switch': true,
'synchronized': true,
'this': true,
'throw': true,
'transient': true,
'true': true,
'try': true,
'typeof': true,
'var': true,
'void': true,
'volatile': true,
'while': true,
'with': true,
'yield': true
},
test: function(str) {
return this.reservedKeywords.hasOwnProperty(str);
}
};
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 Token(type, value, preToken) {
this.type = type;
this.value = value;
this.script = null;
if(preToken) {
this.line = preToken.value.split('\n').length - 1 + preToken.line;
this.start = preToken.end;
} else {
this.start = 0;
this.line = 0;
}
this.end = this.start + value.length; // 包含起点start,不包含终点end
}
function CompileTemplate(tplTokens, globalRunTime) {
this.variableContext = [];
this.variableContextMap = {};
this.scriptTokens = [];
this.tplTokens = tplTokens;
this.globalRunTime = globalRunTime;
// 预置变量
this.persetVariable = {
OUT_CODE: '$RES_OUT',
DATA: '$DATA',// 模板数据变量名
GLOBAL_DATA: '$GLOBAL_DATA'// 全局变量名
};
this.init();
}
CompileTemplate.prototype = {
constructor: CompileTemplate,
init: function() {
this.getScriptTokens();
},
// 从js表达式中提取出变量,用来构建执行上下文
getVariableContext: function(code) {
var that = this;
var jsExpressTokens = code.match(jsTokens.default).map(function(item) {
jsTokens.default.lastIndex = 0;
return jsTokens.matchToToken(jsTokens.default.exec(item));
}).map(function(item) {
if(item.type === 'name' && isJsKeywords.test(item.value)) {
item.type = 'keywords';
}
return item;
});
// console.log(jsExpressTokens);
// return;
var ignore = false; // 跳过变量的属性
var variableTokens = jsExpressTokens.filter(function(item) {
if(item.type === 'name' && !ignore) {
return true;
}
ignore = item.type === 'punctuator' && item.value === '.';
return false;
});
// console.log(variableTokens);
// return;
variableTokens.forEach(function(item) {
var val;
if(!Object.prototype.hasOwnProperty.call(that.variableContextMap, item.value)) {
if(item.value === that.persetVariable.OUT_CODE) {
val = '""';
} else if(Object.prototype.hasOwnProperty.call(that.globalRunTime, item.value)) {
val = that.persetVariable.GLOBAL_DATA + '.' + item.value;
} else {
val = that.persetVariable.DATA + '.' + item.value;
}
that.variableContextMap[item.value] = val;
that.variableContext.push({
name: item.value,
value: val
});
}
});
},
getScriptTokens: function() {
var tplTokens = this.tplTokens;
var scripts = [];
for(var i = 0; i < tplTokens.length; i++) {
var source = tplTokens[i].value.replace(/\n/g,'');
var code = '';
if(tplTokens[i].type === TPL_TOKEN_TYPE_STRING) {
code = this.persetVariable.OUT_CODE + ' += "' + source + '"';
} else if(tplTokens[i].type === TPL_TOKEN_TYPE_EXPRESSION) {
if(tplTokens[i].script.output) {
code = this.persetVariable.OUT_CODE + ' += ' + tplTokens[i].script.code;
} else {
// js表达式
code = tplTokens[i].script.code;
}
this.getVariableContext(code);
}
scripts.push({
source: source,
tplToken: tplTokens[i],
code: code
});
}
this.scriptTokens = scripts;
return scripts;
},
// 构造渲染函数
buildRender: function() {
var codeArr = ['function(' + this.persetVariable.DATA + ') {','"use strict";'];
var varDeclare = 'var';
this.variableContext.forEach(function(item, index) {
if(index === 0) {
varDeclare = 'var ' + item.name + ' = ' + item.value;
} else {
varDeclare += ', ' + item.name + ' = ' + item.value;
}
});
codeArr.push(varDeclare);
this.scriptTokens.forEach(function(item) {
codeArr.push(item.code);
});
codeArr.push('return ' + this.persetVariable.OUT_CODE);
codeArr.push('}');
// console.log(codeArr.join('\n'));
var render = (new Function(this.persetVariable.GLOBAL_DATA,"return " + codeArr.join('\n') + ";"))(this.globalRunTime);
return render;
}
};
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(TPL_TOKEN_TYPE_STRING, str.slice(index,match.index), preToken);
tokens.push(preToken);
}
preToken = new Token(TPL_TOKEN_TYPE_EXPRESSION, match[0], preToken);
preToken.script = reg.use.apply(reg,match);
tokens.push(preToken);
// 保存本次匹配结果的结束下标
index = match.index + match[0].length;
}
return tokens;
}
function templateRender(tplId,tplData) {
var tplStr = document.getElementById(tplId).innerHTML;
var tplTokens = tplToToken(tplStr,reg);
var compiler = new CompileTemplate(tplTokens, globalThis);
var render = compiler.buildRender();
return render(tplData);
}
globalThis.templateRender = templateRender;
globalThis.__templateRender = function(tplStr,tplData) {
var tplTokens = tplToToken(tplStr,reg);
var compiler = new CompileTemplate(tplTokens, globalThis);
var render = compiler.buildRender();
return render(tplData);
}
})( typeof self !== 'undefined' ? self
: typeof window !== 'undefined' ? window
: typeof global !== 'undefined' ? global : {});
遗留问题
前面提到的问题有两个:
- 模板内声明的变量会被作为$DATA的一个属性处理;
- 模板变量值的确定还有一种简单的方法;
模板内声明的变量
针对第一个问题,在不更换js解析器的情况下,没法完美解决 issue。
模板变量值的确定
在template.js中没有使用解析js语句提取变量的思路确定模板变量的值,而是遍历模板数据对象的键,然后使用eval
函数运行变量声明语句,下面是相关源码(注意看headerCode部分):
function compiler(tpl, opt) {
var mainCode = parse(tpl, opt);
var headerCode = '\n' +
' var html = (function (__data__, __modifierMap__) {\n' +
' var __str__ = "", __code__ = "";\n' +
' for(var key in __data__) {\n' +
' __str__+=("var " + key + "=__data__[\'" + key + "\'];");\n' +
' }\n' +
' eval(__str__);\n\n';
var footerCode = '\n' +
' ;return __code__;\n' +
' }(__data__, __modifierMap__));\n' +
' return html;\n';
var code = headerCode + mainCode + footerCode;
code = code.replace(/[\r]/g, ' '); // ie 7 8 会报错,不知道为什么
try {
var Render = new Function('__data__', '__modifierMap__', code);
// Render.toString = function () {
// return mainCode;
// }
return Render;
} catch(e) {
e.temp = 'function anonymous(__data__, __modifierMap__) {' + code + '}';
throw e;
}
}
依照这个思路把我们之前写的代码进行改造,删除jsTokens
和isJsKeywords
,同时删除CompileTemplate
的getVariableContext
方法:
;(function(globalThis) {
var TPL_TOKEN_TYPE_STRING = 'string';
var TPL_TOKEN_TYPE_EXPRESSION = 'expression';
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 Token(type, value, preToken) {
this.type = type;
this.value = value;
this.script = null;
if(preToken) {
this.line = preToken.value.split('\n').length - 1 + preToken.line;
this.start = preToken.end;
} else {
this.start = 0;
this.line = 0;
}
this.end = this.start + value.length; // 包含起点start,不包含终点end
}
function CompileTemplate(tplTokens, globalRunTime) {
this.scriptTokens = [];
this.tplTokens = tplTokens;
this.globalRunTime = globalRunTime;
// 预置变量
this.persetVariable = {
OUT_CODE: '$RES_OUT',
GLOBAL_DATA: '$GLOBAL_DATA'// 全局变量名
};
this.init();
}
CompileTemplate.prototype = {
constructor: CompileTemplate,
init: function() {
this.getScriptTokens();
},
getScriptTokens: function() {
var tplTokens = this.tplTokens;
var scripts = [];
for(var i = 0; i < tplTokens.length; i++) {
var source = tplTokens[i].value.replace(/\n/g,'');
var code = '';
if(tplTokens[i].type === TPL_TOKEN_TYPE_STRING) {
code = this.persetVariable.OUT_CODE + ' += "' + source + '"';
} else if(tplTokens[i].type === TPL_TOKEN_TYPE_EXPRESSION) {
if(tplTokens[i].script.output) {
code = this.persetVariable.OUT_CODE + ' += ' + tplTokens[i].script.code;
} else {
// js表达式
code = tplTokens[i].script.code;
}
}
scripts.push({
source: source,
tplToken: tplTokens[i],
code: code
});
}
this.scriptTokens = scripts;
return scripts;
},
// 构造渲染函数
buildRender: function() {
var codeArr = ['function($DATA) {', 'var ' + this.persetVariable.OUT_CODE + ' = "";'];
var varDeclare = "var varStatement = '';" + "\n"
+ " for(var key in $DATA) {" + "\n"
+ " varStatement += ('var ' + key + ' = ' + '$DATA[\"' + key + '\"]' + ';');" + "\n"
+ " }" + "\n"
+ " eval(varStatement);" + "\n"
codeArr.push(varDeclare);
this.scriptTokens.forEach(function(item) {
codeArr.push(item.code);
});
codeArr.push('return ' + this.persetVariable.OUT_CODE);
codeArr.push('}');
// console.log(codeArr.join('\n'));
var render = (new Function(this.persetVariable.GLOBAL_DATA,"return " + codeArr.join('\n') + ";"))(this.globalRunTime);
return render;
}
};
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(TPL_TOKEN_TYPE_STRING, str.slice(index,match.index), preToken);
tokens.push(preToken);
}
preToken = new Token(TPL_TOKEN_TYPE_EXPRESSION, match[0], preToken);
preToken.script = reg.use.apply(reg,match);
tokens.push(preToken);
// 保存本次匹配结果的结束下标
index = match.index + match[0].length;
}
return tokens;
}
function templateRender(tplId,tplData) {
var tplStr = document.getElementById(tplId).innerHTML;
var tplTokens = tplToToken(tplStr,reg);
var compiler = new CompileTemplate(tplTokens, globalThis);
var render = compiler.buildRender();
return render(tplData);
}
globalThis.templateRender = templateRender;
globalThis.__templateRender = function(tplStr,tplData) {
var tplTokens = tplToToken(tplStr,reg);
var compiler = new CompileTemplate(tplTokens, globalThis);
var render = compiler.buildRender();
return render(tplData);
}
})( typeof self !== 'undefined' ? self
: typeof window !== 'undefined' ? window
: typeof global !== 'undefined' ? global : {});
用前面的例子测试一下:
var tplStr = `
<% if (isAdmin) { %>
<!--fhj-->
<h1><%=title%></h1>
<ul>
<% for (var i = 0; i < list.length; i ++) { %>
<li>索引 <%= i + 1 %> :<%= list[i] %></li>
<% } %>
</ul>
<% var a = list.name; console.log(a); %>
<% } %>
`;
var data = {
title: '基本例子',
isAdmin: true,
list: ['文艺', '博客', '摄影', '电影', '民谣', '旅行', '吉他']
};
console.log(__templateRender(tplStr,data));
输出结果如下:
<!--fhj-->
<h1>基本例子</h1>
<ul>
<li>索引 1 :文艺</li>
<li>索引 2 :博客</li>
<li>索引 3 :摄影</li>
<li>索引 4 :电影</li>
<li>索引 5 :民谣</li>
<li>索引 6 :旅行</li>
<li>索引 7 :吉他</li>
</ul>