著有《React 源码》《React 用到的一些算法》《javascript地月星》等多个专栏。欢迎关注。
文章不好写,要是有帮助别忘了点个赞,收藏~你的鼓励是我继续挖干货的的动力🔥。
另外,本文为原创内容,商业转载请联系作者获得授权,非商业转载需注明出处,感谢理解~
本文代码实现来自冴羽。创建思路和内容均为原创。
前言
看到模版编译冴羽,觉得其中的过程不好理解,特别是字符串的处理,这里加一点那里加一点。所以尝试把模版编译变得好理解一点。希望这篇足够好理解。
第一部分 目标字符串
我们要把模版编译成目标字符串的样子,先了解目标字符串是什么样的。
模版:
<% for ( var i = 0; i < users.length; i++ ) { %>
<li><a href="<%=users[i].url%>"><%=users[i].name%></a></li>
<% } %>
编译成:
var __p = '';
for ( var i = 0; i < users.length; i++ ) {
__p += '<li><a href="' + users[i].url +'">' + users[i].name +'</a></li>'
}
return __p;
运行后输出:
<li><a href="http://localhost"></a></li>
<li><a href="http://localhost">Casper</a></li>
<li><a href="http://localhost">Frank</a></li>
用函数包裹
function render(){
var __p = '';
for ( var i = 0; i < users.length; i++ ) {
__p += '<li><a href="' + users[i].url +'">' + users[i].name +'</a></li>'
}
return __p;
}
用new Function实现,var 函数名 = new Function(函数参数,函数体)
我们要使用new Function创建函数,还需要变成字符串的形式,new Function会将字符串转换成上面的可执行脚本。
let source =
"var __p = '';"+
"for ( var i = 0; i < users.length; i++ ) {"+
" __p += '<li><a href=\"' + users[i].url +'\">' + users[i].name +'</a></li>'"+
"}" +
"return __p;"
let render = new Function('obj',source);
这个字符串将是我们目标,我们要把原本的模版弄成这个字符串的样子就基本实现模版编译了。
所以我们要不断的把编译函数输出的字符串和这个结果对照。
加上with
定义作用域,users[i].url实际上就是obj.user[i].url
function render(obj){
with(obj||{}) {
var __p = '';
for ( var i = 0; i < users.length; i++ ) {
__p += '<li><a href="' + users[i].url +'">' + users[i].name +'</a></li>'
}
}
return __p;
}
var source =
"with(obj||{}){"+
" var __p = '';"+
" for ( var i = 0; i < users.length; i++ ) {"+
" __p += '<li><a href=\"' + users[i].url +'\">' + users[i].name +'</a></li>'"+
" }" +
"}" +
"return __p;"
let render = new Function('obj',source);
直接用上面手写的拼接字符串测试
使用jsdom在nodejs中能够使用dom
import { JSDOM } from 'jsdom';
// 创建一个虚拟 DOM 环境
const dom = new JSDOM(`
<!DOCTYPE html>
<html>
<head>
<title>template</title>
</head>
<body>
<div id="container"></div>
</body>
</html>
`);
const document = dom.window.document;
var results = document.getElementById("container");
var data = {
users: [
{ "url": "http://localhost" },
{ "name": "Casper", "url": "http://localhost" },
{ "name": "Frank", "url": "http://localhost" }
]
}
var source =
"with(obj||{}){"+
" var __p = '';"+
" for ( var i = 0; i < users.length; i++ ) {"+
" __p += '<li><a href=\"' + users[i].url +'\">' + users[i].name +'</a></li>'"+
" }" +
"}" +
"return __p;"
let compiled = new Function('obj',source);
results.innerHTML = compiled(data);
console.log(results.innerHTML);
<li><a href="http://localhost">undefined</a></li><li><a href="http://localhost">Casper</a></li><li><a href="http://localhost">Frank</a></li>
掘金的“写文章"系统的粘贴功能似乎有点问题,没有处理转义符
\,从笔记上粘贴到这里,很多转义符丢失了,所以我手动一个个加上了\,可能有疏漏,导致运行的错误。
第二部分 实现模版编译函数
我们要如何才能从模版编译到字符串格式呢?
使用正则匹配到<%=xxx%>和<%xxx%>匹配
let reg = new RegExp(/<%=([\s\S]+?)%>|<%([\s\S]+?)%>/,'g')
text.replace(reg,function(match, p1, p2, offset) {
match是整个匹配的子字符串的内容包含了模版符号<%=xxx1%> 和 <%xxx2%>
p1是正则中第一个()的内容xxx1
p2是xxx2
offset是匹配到的位置
replace支持全局匹配,正则加了'g',进行循环的匹配
})
处理不匹配的内容
除了匹配的内容还有位于两个<%=%>或<%%>之间正则没有匹配到的内容
<% for ( var i = 0; i < users.length; i++ ) { %>
<li><a href="<%=users[i].url%>"><%=users[i].name%></a></li>
<% } %>
<li><a href="和"></a></li>就是不匹配的。
let reg = new RegExp(/<%=([\s\S]+?)%>|<%([\s\S]+?)%>/,'g')
let index = 0;
let source = '';
text.replace(reg,function(match, p1, p2, offset) {
source += text.slice(index, offset)
// index更新为 匹配子字符串的长度+匹配到的位置
index = offset + match.length;
})
把匹配的和不匹配的加起来就是完整的内容
function template(text) {
let reg = new RegExp(/<%=([\s\S]+?)%>|<%([\s\S]+?)%>/, 'g');
let index = 0;
let source = '';
text.replace(reg, function (match, p1, p2, offset) {
source += text.slice(index, offset);
index = offset + match.length;
if (p1) {
source += p1;
} else if (p2) {
source += p2;
}
console.log(source);
console.log('--------');
})
}
输出:目前差不多是这个字符串:
"for ( var i = 0; i < users.length; i++ ) {" +
" <li><a href=\"users[i].url\">users[i].name</a></li>" +
" }"
我们的编译目标是:
"var __p = '';"+
"for ( var i = 0; i < users.length; i++ ) {"+
" __p += '<li><a href=\"' + users[i].url +'\">' + users[i].name +'</a></li>'"+
"}" +
"return __p;"
还要加上__p。users[i].url/name也还没有从字符串中独立出来。
加上__p, 提出user[i].url/name,顺便加上with
function template(text) {
let reg = new RegExp(/<%=([\s\S]+?)%>|<%([\s\S]+?)%>/, 'g');
let index = 0;
let source = 'var __p = \'';
text.replace(reg, function (match, p1, p2, offset) {
source += text.slice(index, offset);
index = offset + match.length;
if (p1) {
source += '\'+'+p1 + '+\'';
} else if (p2) {
source += '\';'+ p2 + '\n__p+=\'';
}
console.log(source);
console.log('--------');
})
source +='\'; return __p';
source = 'with(obj){' + source + '}';//顺便加上
console.log(source);
}
输出:
"with(obj){var __p = '"+
" '; for ( var i = 0; i < users.length; i++ ) { "+
"__p+='"+
" <li><a href=\"'+users[i].url+'\">'+users[i].name+'</a></li>"+
" '; } "+
"__p+=''; return __p;}"
修复Bug
"var __p = '"和下一个"';for(....)"之间空格太多了出现语法错误。加上trim()
source += text.slice(index, offset).trim();
输出:
"with(obj){var __p = ''; for ( var i = 0; i < users.length; i++ ) { "+
"__p+='<li><a href=\"'+users[i].url+'\">'+users[i].name+'</a></li>'; } "+
"__p+=''; return __p;}"
完成✅。
最终的代码
import { JSDOM } from 'jsdom';
function template(text) {
let reg = new RegExp(/<%=([\s\S]+?)%>|<%([\s\S]+?)%>/, 'g');
let index = 0;
let source = 'var __p = \'';
text.replace(reg, function (match, p1, p2, offset) {
source += text.slice(index, offset).trim();
index = offset + match.length;
if (p1) {
source += '\'+'+p1 + '+\'';
} else if (p2) {
source += '\';'+ p2 + '\n__p+=\'';
}
// console.log(source);
// console.log('--------');
})
source +='\'; return __p;';
source = 'with(obj){' + source + '}';
console.log(source);
let render = new Function('obj',source);
return render;
}
第三部分 转义符
举个例子:
字符串"var a = '1\\n23';console.log(a)"
\n是在字符串中是换行,\\n是让字符串把\当成普通的斜线,它的换行含义不表现在当前字符串中"var a = '1\\n23';console.log(a)",
而是表现在脚本var a = '1\n23';console.log(a)的'1\n23'中。
var fn = new Function("var a = '1\\n23';console.log(a)");会把字符串转换成可执行脚本:
function fn (){
var a = '1\n23';console.log(a);
}
在可执行脚本var a = '1\n23'的字符串'1\n23'中生效。输出1换行23。
而 var fn = new Function("var a = '1\n23';console.log(a)");会报错,因为字符串\n会换行,双引号字符串换行需要需要+做拼接处理。
1\\23同理,\23的含义不表现在"var a = '1\\23';console.log(a)",而表现在下一个字符串中var a = '1\23';console.log(a)。
然后有很多形态:
var log = new Function("var a = '1\\23';console.log(a)");
var log = new Function("var a = '1\\n23';console.log(a)");
var log = new Function("var a = '\taaa';console.log(a)");相当于"var a = ' aaa';..."
var log = new Function("var a = '\\taaa';console.log(a)");相当于var a = ' aaa';...
var log = new Function("var a = '\"sssa\"';console.log(a)"); "和外部的"冲突了,\"避免冲突,相当于var a = '"sssa"'
var log = new Function("var a = '\'sss\'';console.log(a)");相当于var a = ' 'sss' ';... '冲突了所以报错。var a = ''sss'';console.log(a)。
var log = new Function('var a = "\"sss\"";console.log(a)');同上报错,var a = ""sss"";console.log(a)。
var log = new Function('var a = "\\\'sss2\\\'";console.log(a)');相当于var a = "\'sss2\'",输出 ‘sss2‘,把引号当成普通字符串打印了出来。
如果模版中有转义符
<script type="text/html" id="user_tmpl">
<% for ( var i = 0; i < users.length; i++ ) { %>
1\\23 a\nbc 'abc' \u2028 ggg \u2029 hhh
<li><a href="<%=users[i].url%>"><%=users[i].name%></a></li>
<% } %>
</script>
document.getElementById(user_tmpl)是字符串,
"<% for ( var i = 0; i < users.length; i++ ) { %>" +
" 1\\23 a\nbc 'abc' \u2028 ggg \u2029 hhh" +
" <li><a href=\"<%=users[i].url%>\"><%=users[i].name%></a></li>" +
"<% } %>"
1\\23而不是1\23因为字符串中使用\23会提示语法错。这也是我们后续在正则中用\\而不是\进行匹配的原因。我们是为了处理\\而不是\。
var escape = {
...
'\\': '\\\\',
'\': '\\' -->❌无效的字符
}
"\✅ 1\23❌不允许使用八进制转义序列。请使用语法“\x13”。 a\nbc 'abc' \u2028 ggg \u2029 hhh"
不处理\
如果模版中有转义符并且我们不想这个转义符在字符串中生效,而是在脚本内的字符串中生效,我们需要对这些转义符做处理。
处理'因为会和字符串拼接冲突例如模版text= "...'abc'...", source='with(){for(){__p=\' \'abc\' \''}},__p有一对‘,'abc‘有一对'。
处理\n因为用双引号的字符串直接换行没有做拼接会报错。
处理\\因为字符串"1\\\\23",脚本中1\\23,脚本会把\当成普通字符,在控制台中打印1\23。不处理\\,"1\\23"不先变成"1\\\\23",那么脚本中1\23,脚本把\23当成转义符,可能导致错误。
处理\u2028是行分隔符会被浏览器理解为换行,而在Javascript的字符串表达式中是不允许换行的,从而导致错误。
处理\u2029是段落分隔符。unicode中的特殊字符都建议进行处理。
function template(text) {
let reg = new RegExp(/<%=([\s\S]+?)%>|<%([\s\S]+?)%>/, 'g');
// \和'
var reg2 = /[\\\'\n\u2028\u2029]/g;
var escape = {
'\'': '\\\'',// \\\'分解成 \\和\'
'\\': '\\\\', // 1\\23
'\n': '\\n',
'\u2028': '\\u2028',
'\u2029': '\\u2029'
}
let index = 0;
let source = 'var __p = \'';
text.replace(reg, function (match, p1, p2, offset) {
source += text.slice(index, offset).trim().replace(reg2, function (match) {
return escape[match];
});
index = offset + match.length;
if (p1) {
source += '\'+'+p1 + '+\'';
} else if (p2) {
source += '\';'+ p2 + '\n__p+=\'';
}
// console.log(source);
// console.log('--------');
})
source +='\'; return __p;';
source = 'with(obj){' + source + '}';
console.log(source);
let render = new Function('obj',source);
return render;
}