模板引擎的原理:實現一個簡單的模板引擎

首先什麼是模板引擎?css

模板引擎 其實就是接收數據,把按照必定規則編寫模板字符串,轉換成 html 字符串。html

const TemplateEngine = (tpl, data) => {
    return xxx
}

var compiled = _.template('hello <%= user %>!');
compiled({ 'user': 'fred' });
// => 'hello fred!'
複製代碼

上面例子中 lodashtemplate 就實現了相似模板引擎的功能。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) 可以匹配出 useride

這裏說明一下正則式的 [^%>]。這是一個反向字符集,說明是不能匹配到中括號裏面的 %>。這個正是咱們上面寫的模板字符串動態區域的。函數

(xx)? 是非貪婪匹配,這樣就不會出現匹配到 <%foo%> barzzz <%bar%>

special-negated-character-set - JavaScript | MDN

使用 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

Function - JavaScript | MDN

var a = 1
var b = 2
var fn  = new Function('return a+b')
fn() // 3
複製代碼

這就是 Vue.js 的模板 {{ a + b }} 支持 JavaScript 表達式的緣由

支持 for...in 語法

假設如今咱們須要處理的模板字符串是下面這樣

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

absurd/TemplateEngine.js at master · krasimir/absurd

JavaScript template engine in just 20 lines

相關文章
相關標籤/搜索