「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,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
}))
複製代碼