首先什麼是模板引擎?css
模板引擎
其實就是接收數據,把按照必定規則編寫模板字符串,轉換成 html 字符串。html
const TemplateEngine = (tpl, data) => { return xxx } var compiled = _.template('hello <%= user %>!'); compiled({ 'user': 'fred' }); // => 'hello fred!' 複製代碼
上面例子中 lodash
的 template
就實現了相似模板引擎的功能。git
Vue.js 的 compiler 就是包含了模板引擎的功能。github
// 來自 Vue.js 官網的例子 Vue.component('todo-item', { // todo-item 組件如今接受一個 // "prop",相似於一個自定義 attribute。 // 這個 prop 名爲 todo。 props: ['todo'], template: '<li>{{ todo.text }}</li>' }) 複製代碼
上面的 <li>{{ todo.text }}</li>
就是模板字符串。數組
首先模板引擎的動態區域都有特定的規則,好比上面的 <% %>
。bash
說明一下,下面的文字中的動態區域
都是特指 <% %>
。markdown
針對這個規則能夠用正在匹配。app
const tplStr = `hello <%user%>!` const re = /<%([^%>]+)?%>/g; 複製代碼
re.exec(tplStr)
可以匹配出 user
ide
這裏說明一下正則式的 [^%>]
。這是一個反向字符集,說明是不能匹配到中括號裏面的 %>
。這個正是咱們上面寫的模板字符串動態區域的。函數
(xx)?
是非貪婪匹配,這樣就不會出現匹配到 <%foo%> barzzz <%bar%>
使用 re.exec
可以匹配動態區域了,可是實際狀況是動態區域不止一個。所以要執行 re.exec
屢次。
const re = /<%([^%>]+)?%>/g; const tplStr = `<%foo%> barzzz <%bar%>` let match while(match = re.exec(tplStr)) { console.log(match) // <%foo%> // <%bar%> } 複製代碼
這樣咱們的模板引擎就能夠按照最開始的寫法
const TemplateEngine = (tpl, data) => { const re = /<%([^%>]+)?%>/g; let match; while(match = re.exec(tpl)) { tpl = tpl.replace(match[0], data[match[1]]) } return tpl; } 複製代碼
按照上面的方式,已經能夠替換模板字符串成功了。
可是在咱們實際開發過程當中,好比 Vue.js
的模板支持 {{ todo.text }}
因此單純替換是不夠的。這時候須要語法運行。
接下來咱們須要用到 new Function
var a = 1 var b = 2 var fn = new Function('return a+b') fn() // 3 複製代碼
這就是 Vue.js 的模板 {{ a + b }}
支持 JavaScript 表達式的緣由
假設如今咱們須要處理的模板字符串是下面這樣
var template = 'My skills:' + '<%for(var index in this.skills) {%>' + '<%this.skills[index]%>' + '<%}%>'; 複製代碼
替換掉 <%
和 %>
並拼接起來,成爲下面的形式。這樣的字符串不是合法的語句,會報錯。
return 'My skills:' + for(var index in this.skills) { + '' + this.skills[index] + '' + } 複製代碼
咱們須要將 for...in
的產出用其它的形式拼接。
定義一個數組來存儲代碼每一行。而後把存儲代碼信息的數組拼接起來。
var r = []; r.push('My skills:'); for(var index in this.skills) { r.push(''); r.push(this.skills[index]); r.push(''); } return r.join(''); 複製代碼
new Function(body)
body 就是上面代碼字符串
省略干擾的代碼就是 new Function("var r = []; return r;")
這裏代碼字符串就是引號裏面的內容: var r= []; return r;
我把 r.push
for in
}
這些都省略了。
所以模板字符串轉換成代碼字符串的規則是:
普通字符: 'My skills:', 變成了代碼 r.push('xxx')
普通的動態區域,便是沒有 for...in
, 變成了 r.push(this.skills[index])
特殊的動態區域,直接就是字符串 for (var index in this.skills) {
將上面的規則寫成代碼就是
let code = 'var r=[];\\n'; const reExp = /(^( )?(for|if|{|}|;))(.*)?/g; var add = function (line, js) { js ? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') : (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : ''); return add; } 複製代碼
add 函數的參數 js
就是一個標示,用來判斷是否是動態區域。
reExp
判斷是否是有關鍵字的動態區域
完整版 reExp
reExp = /(^( )?(var|if|for|else|switch|case|break|{|}|;))(.*)?/g 複製代碼
爲了生成代碼字符串,咱們須要定義一個索引 cursor
。用來記錄匹配和處理過的原始模板字符串的位置。
let match; let cursor = 0; while(match = re.exec(html)) { add(html.slice(cursor, match.index))(match[1], true); cursor = match.index + match[0].length; } // 處理剩餘未被匹配的模板字符串 add(html.slice(cursor, html.length)); 複製代碼
var TemplateEngine = function (html, options) { var re = /<%([^%>]+)?%>/g, reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g, code = 'var r=[];\n', cursor = 0, match; var add = function (line, js) { js ? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') : (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : ''); return add; } while (match = re.exec(html)) { add(html.slice(cursor, match.index))(match[1], true); cursor = match.index + match[0].length; } add(html.slice(cursor, html.length)); code = (code + 'return r.join("");').replace(/[\r\t\n]/g, ' '); return new Function(code).apply(options) } var template = 'My skills:' + '<%if(this.showSkills) {%>' + '<%for(var index in this.skills) {%>' + '<%this.skills[index]%>' + '<%}%>' + '<%} else {%>' + 'none' + '<%}%>'; console.log(TemplateEngine(template, { skills: ["js", "html", "css"], showSkills: true })); 複製代碼
注意:參考文章中的轉義存在問題會致使 r.push
替換成了 .push
上面的版本是可以正常運行的
lodash/lodash.js at 4.17.15 · lodash/lodash