「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」javascript
想必前端的同窗都接触过 HTML 模板语法,大多数可能都是以 {{
的形式(Mustache 风格)去表示的,好比 Vue 的模板语法,Vue 经过对模板字符串的遍历解析,最终生成了 HTML:html
<span>Message: {{ msg }}</span>
复制代码
除了上面这种类型,还有一个叫作 ERB-style 的模板标记语法,也很是的常见,它就是咱们接下来要实现的这一种。前端
虽然咱们此次实现的是 ERB 风格,可是这也只是一个标记,若是您读懂了本文的内容,您能够换成任意喜欢的标记方法,好比,若是想使用 {{
的方式,也彻底没问题。java
不过,本文仍是以 ERB 风格为例。webpack
它的语法也比较简单,主要有两种表示:git
<% ... %>
能够包裹一个 JavaScript 语句:<%for ( let i = 0; i < 10; i++ ) { %>
<% console.log(i) %>
<% } %>
复制代码
<%= ... %>
能够获取当前执行环境下的变量:假设咱们写好了模板函数,就叫 template
。github
咱们的使用方法会是:web
const render = template('<div><%= data.name %></div>');
console.log(render({name: 'hi'})) // <div>hi</div>
复制代码
咱们再举一个使用 <%= ... %>
的例子,那就是在 webpack 中的一个使用场景:后端
// @filename: webpack.config.js
plugins: [
new HtmlWebpackPlugin({
title: 'Custom template',
// Load a custom template (lodash by default)
template: 'index.html'
})
]
// @filename: index.html
<!DOCTYPE html>
<html> <head> <meta charset="utf-8"/> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> </body> </html>
复制代码
通过上面的举例,想必你们都很清楚 ERB 风格的模板是什么了吧?markdown
除了上面提到的两种标签语法,还有其余的标签,好比 <%- ... %>
,其实它的转换原理和 <%= ... %>
是同样的,只不过额外转义了内部的 HTML 字符串的,可是本文不会讲解如何转义 HTML 字符串,因此那种记法就略过了。想了解原理的同窗推荐阅读 这篇文章
接下来咱们就来实现 ERB 风格的模板引擎。
ps: 下面讲解的代码其实就是 underscorejs 的 _.template
的思路,只不过略过了对一些边界状况的兼容。
咱们有一个 index.html 文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script id="templ" type="text/html"> <%for ( var i = 0; i < data.list.length; i++ ) { %> <li> <a data-id="<%= data.list[i].id %>"> <%=data.list[i].name %> </a> </li> <% } %> </script>
<script src="./template.js"></script>
</body>
</html>
复制代码
首先,咱们先获取这段模板:
let content = document.querySelector('#templ').innerHTML
复制代码
咱们的模板引擎最核心的原理是什么呢?是对 new Funtion
的使用。事实上,咱们能够经过以下方法构造一个函数:
const print = new Function('str', 'console.log(str)')
print('hello world') // hello world
复制代码
它就至关于:
const print = function (str) {console.log(str)}
复制代码
有了这个神奇的特性,咱们就在想,若是咱们把上述模板转化为合法的 JavaScript 代码的字符串,记做字符串 x 。
那咱们是否是就能够作一个模板引擎了呢?
new Funtion('data', x);
复制代码
答案是:是的,咱们就是要这么去作。
如今问题的关键就是咱们怎么把 content
的值转换为 JavaScript 代码的字符串。
<%for ( var i = 0; i < data.list.length; i++ ) { %>
<li>
<a data-id="<%= data.list[i].id %>">
<%=data.list[i].name %>
</a>
</li>
<% } %>
复制代码
咱们能够:
/<%=([\s\S]+?)%>/g
匹配到 <%= ... %>
格式的字符串/<%([\s\S]+?)%>/g
匹配到 <% ... %>
格式的字符串注意,第二个正则是包含第一个的,因此,咱们在正则替换的时候必定要先替换第一个。
若是咱们匹配到了 <%= ... %>
,咱们会把它变为:'+\n ... +\n'
content = content.replace(/<%=([\s\S]+?)%>/g, function(_, evaluate) {
return "'+\n" + evaluate + "+\n'"
})
复制代码
嗯... 结果有点奇怪?不要紧,先看下去。
接下来,咱们匹配 <% ... %>
。
把它变为:';\n ... \n_p +='
。
content = content.replace(/<%([\s\S]+?)%>/g, function(match,interpolate) {
return "';\n" + interpolate + "\n_p +='";
})
复制代码
如今是否是有点像样了呢?不过这个还不是合法的 JavaScript 代码。
咱们还须要在它的头尾加点东西。
在头部加上 let _p = '';\nwith (data){\n_p+='
,在尾部加上 '}return _p
,再来看一下效果:
这样才是差很少像样了,可是仍是有个问题,请看上图的第五行,由于行的最后有个 \n
字符,因此在 '
以后换行了。
可是在 JavaScript 中 '
是不容许换行的,若是咱们把这段代码拷贝到控制台执行,仍是会报错。
咱们能够考虑把 '
换成 ES6 的模板字符串语法,也能够考虑对此类特殊字符进行处理,咱们选择特殊处理一下。
若是咱们用编辑器在某个 JS 文件中写两行代码:
const a = 1;
const b = 2;
复制代码
它实际上是真正存储在文件是更像这样子的:const a = 1;\nconst b = 2;
。而咱们要在字符串里保留 \n
的原始模样,就要它作一层转义,当咱们在字符串写 'const a = 1;\\nconst b = 2;' 才真正表示了上面真正的存储结果。
和 \n
同样的还有下面几个,列一个统一的表:
转义字符 | 要转化为 |
---|---|
' | \` |
\ | \\ |
\r | \\r |
\n | \\n |
\u2028 | \\u2028 |
\u2029 | \\u2029 |
到代码层面的话,就会是下面这样子:
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g;
function escapeChar(match) {
if (match === "'") {
return "\'"
}
return '\\' + escapes[match];
}
复制代码
注意看 escapeChar
函数,咱们特别兼容了一下单引号,由于它和其余的不一样,对比咱们列的表,它的转化的结果前面只有一个 \
,可是咱们也能够去掉这个,那就是用单引号表示。由于 "\'"
等于 '\\''
,因此代码就能够去掉那个 if
语句,写成:
function escapeChar(match) {
return '\\' + escapes[match];
}
复制代码
鉴于 \
的做为转译序列的特殊性,咱们的 escapes
对象的第二项其实表明的是一个\
,而转换后的结果其实表明的两个 \
:
byte[] byteArray1 = "\\".getBytes();
byte[] byteArray2 = "\\\\".getBytes();
System.out.println(byteArray1) // [92]
System.out.println(byteArray2) // [92, 92]
复制代码
咱们在最开始获取到 content 后,加上这段处理转译序列的逻辑后,再看一下最后的结果:
content = content.replace(escapeRegExp, function(match) {
return escapeChar(match);
}
复制代码
这样就没什么问题了,咱们就能够放心的把它传给 new Function
的第二个参数了。
const render = new Function('data', content);
复制代码
后面调用咱们的 render
函数就能够这样:
render({
list: {name: 'Bob', id: 1}
})
复制代码
咱们能够获得下面这样的结果:
完美,逻辑咱们终于讲完了。
underscore 的思路也是这样子,只不过,它作的更简洁。
咱们的思路是先把 content 的特殊字符处理掉,再把 <%= ... %>
处理掉,再把 <% ... %>
处理掉,而后再把代码的头部尾部完善一下。
而它呢,它使用的正则和咱们不同,它是 /<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g
,关键点就在这里。
underscore 只须要遍历一遍,碰到 <%= ... %>
或者 <% ... %>
后,先把上一次匹配结果的结束到此次匹配结果以前的特殊字符处理掉,而后再判断当前匹配到的模板语法怎么处理,依次迭代,直到匹配到字符串结尾。
有些同窗可能会好奇,这样能匹配到最后嘛?若是咱们的模板最后面是一些纯字符串,而不是 <%= ... %>
或者 <% ... %>
,正则岂不是匹配不到最后了?这也就是 underscore 为了把正则最后加了 |$
的缘由,保证能够匹配到最后,这样就能把这一段的特殊字符也处理掉。
另外,underscore 在处理模板语法 <%= ... %>
的时候加了对 null
和 undefined
判断,若是是这二者,咱们最开始的写法会直接输出字符串 'undefined' 或者 'null'。可是 underscore 则让这些状况输出空字符串。
var interpolate = '123';
var __t;
(__t= (interpolate)) == null ? '' : __t
复制代码
写得人性化一点,就等同于:
interpolate == null ? interpolate : ''
复制代码
明白了上面这些点以后,再去看 _.template
的源码应该会轻松一些了。
可是,思路都是同样的,相信明白了最开始咱们分析过程的同窗,必定也能明白 underscore 的 _.template
函数的原理。比起直接讲解 _.template
的实现,拆解开来应该更容易理解吧 :)
为了方便各位调试,我把可执行代码都放在下面,须要的同窗自取~
我有一个当心愿,那就是总点赞数达到 100,如今已经有 91 个了,看到这里的同窗,若是本篇文章对你有帮助,能够帮我点个赞吗,不点也不要紧,阅读到这里,已是对我最大的支持了,感恩。
谢谢各位的阅读,撒花 ~
<!-- @filename: index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script id="templ" type="text/html"> <%for ( var i = 0; i < data.list.length; i++ ) { %> <li> <a data-id="<%= data.list[i].id %>"> <%=data.list[i].name %> </a> </li> <% } %> </script>
<script src="./index.js"></script>
</body>
</html>
复制代码
// @filename: main.js
let content = document.querySelector('#templ').innerHTML
var settings = {
evaluate: /<%([\s\S]+?)%>/g,
interpolate: /<%=([\s\S]+?)%>/g,
};
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g;
function escapeChar(match) {
if (match === "'") {
return '\\`'
}
return '\\' + escapes[match];
}
function template(text) {
var matcher = RegExp([
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
var index = 0;
var source = "__p+='";
text.replace(matcher, function (match, interpolate, evaluate, offset) {
source += text.slice(index, offset).replace(escapeRegExp, escapeChar);
index = offset + match.length;
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
} else if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
return match;
});
source += "';\n";
var argument = 'data';
source = 'with('+ argument + '||{}){\n' + source + '}\n';
source = "var __t,__p='';" +
source + 'return __p;\n';
var render;
try {
render = new Function(argument, source);
} catch (e) {
e.source = source;
throw e;
}
var template = function (data) {
return render.call(this, data);
};
return template;
}
const render = template(content);
var list = [
{name: 'Bob', id: 1},
{name: 'Jack', id: 2},
]
console.log(render({
list
}))
复制代码