做者:崔靜javascript
雖然如今有各類前端框架來提升開發效率,可是在某些狀況下,原生 JavaScript 實現的組件也是不可或缺的。例如在咱們的項目中,須要給業務方提供一個通用的支付組件,可是業務方使用的技術棧多是 Vue、React 等,甚至是原生的 JavaScript。那麼爲了實現通用性,同時保證組件的可維護性,實現一個原生 JavaScript 的組件也就顯得頗有必要了。css
下面左圖爲咱們的 Panel 組件的大概樣子,右圖則爲咱們項目的大概目錄結構:html
咱們將一個組件拆分爲 .html
、.js
、.css
三種文件,例如 Panel 組件,包含 panel.html、panel.js、panel.css 三個文件,這樣能夠將視圖、邏輯和樣式拆解開來便於維護。爲了提高組件靈活性,咱們 Panel 中的標題,button 的文案,以及中間 item 的個數、內容等均由配置數據來控制,這樣,咱們就能夠根據配置數據動態渲染組件。這個過程當中,爲了使數據、事件流向更爲清晰,參考 Vue 的設計,咱們引入了數據處理中心 data center 的概念,組件須要的數據統一存放在 data center 中。data center 數據改變會觸發組件的更新,而這個更新的過程,就是根據不一樣的數據對視圖進行從新渲染。前端
panel.html 就是咱們常說的「字符串模板」,而對其進行解析變成可執行的 JavaScript 代碼的過程則是「模板引擎」所作的事情。目前有不少的模板引擎供選擇,且通常都提供了豐富的功能。可是在不少狀況下,咱們可能只是處理一個簡單的模板,沒有太複雜的邏輯,那麼簡單的字符串模板已足夠咱們使用。vue
主要分爲如下幾類:java
簡單粗暴——正則替換react
最簡單粗暴的方式,直接使用字符串進行正則替換。可是沒法處理循環語句和 if / else 判斷這些。git
a. 定義一個字符串變量的寫法,好比用 <%%>
包裹github
const template = (
'<div class="toast_wrap">' +
'<div class="msg"><%text%></div>' +
'<div class="tips_icon <%iconClass%>"></div>' +
'</div>'
)
複製代碼
b. 而後經過正則匹配,找出全部的 <%%>
, 對裏面的變量進行替換正則表達式
function templateEngine(source, data) {
if (!data) {
return source
}
return source.replace(/<%([^%>]+)?%>/g, function (match, key) {
return data[key] ? data[key] : ''
})
}
templateEngine(template, {
text: 'hello',
iconClass: 'warn'
})
複製代碼
簡單優雅——ES6 的模板語法
使用 ES6 語法中的模板字符串,上面的經過正則表達式實現的全局替換,咱們能夠簡單的寫成
const data = {
text: 'hello',
iconClass: 'warn'
}
const template = ` <div class="toast_wrap"> <div class="msg">${data.text}</div> <div class="tips_icon ${data.iconClass}"></div> </div> `
複製代碼
在模板字符串的 ${}
中能夠寫任意表達式,可是一樣的,對 if / else 判斷、循環語句沒法處理。
簡易模板引擎
不少狀況下,咱們渲染 HTML 模板時,尤爲是渲染 ul 元素時, 一個 for 循環顯得尤其必要。那麼就須要在上面簡單邏輯的基礎上加入邏輯處理語句。
例如咱們有以下一個模板:
var template = (
'I hava some menu lists:' +
'<% if (lists) { %>' +
'<ul>' +
'<% for (var index in lists) { %>' +
'<li><% lists[i].text %></li>' +
'<% } %>' +
'</ul>' +
'<% } else { %>' +
'<p>list is empty</p>' +
'<% } %>'
)
複製代碼
直觀的想,咱們但願模板能轉化成下面的樣子:
'I hava some menu lists:'
if (lists) {
'<ul>'
for (var index in lists) {
'<li>'
lists[i].text
'</li>'
}
'</ul>'
} else {
'<p>list is empty</p>'
}
複製代碼
爲了獲得最後的模板,咱們將散在各處的 HTML 片斷 push 到一個數組 html
中,最後經過 html.join('')
拼接成最終的模板。
const html = []
html.push('I hava some menu lists:')
if (lists) {
html.push('<ul>')
for (var index in lists) {
html.push('<li>')
html.push(lists[i].text)
html.push('</li>')
}
html.push('</ul>')
} else {
html.push('<p>list is empty</p>')
}
return html.join('')
複製代碼
如此,咱們就獲得了能夠執行的 JavaScript 代碼。對比一下,容易看出從模板到 JavaScript 代碼,經歷了幾個轉換:
<%%>
中若是是邏輯語句(if/else/for/switch/case/break),那麼中間的內容直接轉成 JavaScript 代碼。經過正則表達式 /(^( )?(var|if|for|else|switch|case|break|;))(.*)?/g
將要處理的邏輯表達式過濾出來。<% xxx %>
中若是是非邏輯語句,那麼咱們替換成 html.push(xxx)
的語句<%%>
以外的內容,咱們替換成 html.push(字符串)
const re = /<%(.+?)%>/g
const reExp = /(^( )?(var|if|for|else|switch|case|break|;))(.*)?/g
let code = 'var r=[];\n'
let cursor = 0
let result
let match
const add = (line, js) => {
if (js) { // 處理 `<%%>` 中的內容,
code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n'
} else { // 處理 `<%%>` 外的內容
code += line !== '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : ''
}
return add
}
while (match = re.exec(template)) { // 循環找出全部的 <%%>
add(template.slice(cursor, match.index))(match[1], true)
cursor = match.index + match[0].length
}
// 處理最後一個<%%>以後的內容
add(template.substr(cursor, template.length - cursor))
// 最後返回
code = (code + 'return r.join(""); }').replace(/[\r\t\n]/g, ' ')
複製代碼
到此咱們獲得了「文本」版本的 JavaScript 代碼,利用 new Function
能夠將「文本」代碼轉化爲真正的可執行代碼。
最後還剩一件事——傳入參數,執行該函數。
方式一:能夠把模板中全部的參數統一封裝在一個對象 (data) 中,而後利用 apply 綁定函數的 this
到這個對象。這樣在模板中,咱們即可經過 this.xx
獲取到數據。
new Function(code).apply(data)
複製代碼
方式二:老是寫 this.
會感受略麻煩。能夠把函數包裹在 with(obj)
中來運行,而後把模板用到的數據當作 obj 參數傳入函數。這樣一來,能夠像前文例子中的模板寫法同樣,直接在模板中使用變量。
let code = 'with (obj) { ...'
...
new Function('obj', code).apply(data, [data])
複製代碼
可是須要注意,with
語法自己是存在一些弊端的。
到此咱們就獲得了一個簡單的模板引擎。
在此基礎上,能夠進行一些包裝,拓展一下功能。好比能夠增長一個 i18n 多語言處理方法。這樣能夠把語言的文案從模板中單獨抽離出來,在全局進行一次語言設置以後,在後期的渲染中,直接使用便可。
基本思路:對傳入模板的數據進行包裝,在其中增長一個 $i18n 函數。而後當咱們在模板中寫 <p><%$i18n("something")%></p>
時,將會被解析爲 push($i18n("something"))
具體代碼以下:
// template-engine.js
import parse from './parse' // 前面實現的簡單的模板引擎
class TemplateEngine {
constructor() {
this.localeContent = {}
}
// 參數 parentEl, tpl, data = {} 或者 tpl, data = {}
renderI18nTpl(tpl, data) {
const html = this.render(tpl, data)
const el = createDom(`<div>${html}</div>`)
const childrenNode = children(el)
// 多個元素則用<div></div>包裹起來,單個元素則直接返回
const dom = childrenNode.length > 1 ? el : childrenNode[0]
return dom
}
setGlobalContent(content) {
this.localeContent = content
}
// 在傳入模板的數據中多增長一個$i18n的函數。
render(tpl, data = {}) {
return parse(tpl, {
...data,
$i18n: (key) => {
return this.i18n(key)
}
})
}
i18n(key) {
if (!this.localeContent) {
return ''
}
return this.localeContent[key]
}
}
export default new TemplateEngine()
複製代碼
經過 setGlobalContent
方法,設置全局的文案。而後在模板中能夠經過<%$i18n("contentKey")%>
來直接使用
import TemplateEngine from './template-engine'
const content = {
something: 'zh-CN'
}
TemplateEngine.setGlobalContent(content)
const template = '<p><%$i18n("something")%></p>'
const divDom = TemplateEngine.renderI18nTpl(template)
複製代碼
在咱們介紹的方法中使用 '<%%>' 的來包裹邏輯語塊和變量,此外還有一種更爲常見的方式——使用雙大括號 {{}}
,也叫 mustache 標記。在 Vue, Angular 以及微信小程序的模板語法中都使用了這種標記,通常也叫作插值表達式。下面咱們來看一個簡單的 mustache 語法模板引擎的實現。
模板引擎 mustache.js 的原理
有了方法3的基礎,咱們理解其餘的模板引擎原理就稍微容易點了。咱們來看一個使用普遍的輕量級模板 mustache 的原理。
簡單的例子以下:
var source = ` <div class="entry"> {{#author}} <h1>{{name.first}}</h1> {{/author}} </div> `
var rendered = Mustache.render(source, {
author: true,
name: {
first: 'ana'
}
})
複製代碼
模板解析
模板引擎首先要對模板進行解析。mustache 的模板解析大概流程以下:
tokens = []
while (!剩餘要處理的模板字符串是否爲空) {
value = scanner.scanUntil(openingTagRe);
value = 模板字符串中第一個 {{ 以前全部的內容
if (value) {
處理value,按字符拆分,存入tokens中。例如 <div class="entry">
tokens = [
{'text', "<", 0, 1},
{'text', "d"< 1, 2},
...
]
}
if (!匹配{{) break;
type = 匹配開始符 {{ 以後的第一個字符,獲得類型,如{{#tag}},{{/tag}}, {{tag}}, {{>tag}}等
value = 匹配結束符以前的內容 }},value中的內容則是 tag
匹配結束符 }}
token = [ type, value, start, end ]
tokens.push(token)
}
複製代碼
而後經過遍歷 tokens
,將連續的 text
類型的數組合並。
遍歷 tokens
,處理 section
類型(即模板中的 {{#tag}}{{/tag}}
,{{^tag}}{{/tag}}
)。section
在模板中是成對兒出現的,須要根據 section
進行嵌套,最後和咱們的模板嵌套類型達到一致。
渲染
解析完模板以後,就是進行渲染了:根據傳入的數據,獲得最終的 HTML 字符串。渲染的大體過程以下:
首先將渲染模板的數據存入一個變量 context
中。因爲在模板中,變量是字符串形式表示的,如 'name.first'
。在獲取的時候首先經過 .
來分割獲得 'name'
和 'first'
而後經過 trueValue = context['name']['first']
設值。爲了提升性能,能夠增長一個 cache
將該次獲取到的結果保存起來,cache['name.first'] = trueValue
以便於下次使用。
渲染的核心過程就是遍歷 tokens
,獲取到類型,和變量 (value
) 的正真的值,而後根據類型、值進行渲染,最後將獲得的結果拼接起來,即獲得了最終的結果。
衆多模板引擎中,如何鎖定哪一個是咱們所需的呢?下面提供幾個能夠考慮的方向,但願能夠幫助你們來選擇:
功能
選擇一個工具,最主要的是看它可否知足咱們所需。好比,是否支持變量、邏輯表達式,是否支持子模板,是否會對 HTML 標籤進行轉義等。下面表格僅僅作幾個模板引擎的簡單對比。 不一樣模板引擎除了基本功能外,還提供了本身的特有的功能,好比 artTemplate 支持在模板文件上打斷點,使用時方便調試,還有一些輔助方法;handlesbars 還提供一個 runtime 的版本,能夠對模板進行預編譯;ejs 邏輯表達式寫法和 JavaScript 相同;等等在此就不一一例舉了。
大小
對於一個輕量級組件來講,咱們會格外在乎組件最終的大小。功能豐富的模板引擎便會意味着體積較大,因此在功能和大小上咱們須要進行必定的衡量。artTemplate 和 doT 較小,壓縮後僅幾 KB,而 handlebars 就較大,4.0.11 版本壓縮後依然有 70+KB。 (注:上圖部分數據來源於 https://cdnjs.com/ 上 min.js 的大小,部分來源於 git 上大小。大小爲非 gzip 的大小)
性能
若是有很是多的頻繁 DOM 更新或者須要渲染的 DOM 數量不少,渲染時,咱們就須要關注一下模板引擎的性能了。
最後,以咱們的項目爲例子,咱們要實現的組件是一個輕量級的組件(主要爲一個浮層界面,兩個頁面級的全覆蓋界面)同時用戶的交互也很簡單,組件不會進行頻繁從新渲染。可是對組件的總體大小會很在乎,並且還有一點特殊的是,在組件的文案咱們須要支持多語言。因此最終咱們選定了上文介紹的第三種方案。