實現一個簡單的模板引擎

對如今的前端來講,模板是很是熟悉的概念。畢竟如今三大框架那麼火,不會用框架還能叫前端嗎🐶,而框架是一定有模板的。那咱們寫的模板是如何轉換成 HTML 顯示在網頁上的呢?前端

咱們先從簡單的提及,靜態模板通常用於須要 SEO 且頁面數據是動態的網頁。由前端編寫好靜態模板,後端負責將動態的數據和靜態模板交給模板引擎,最終編譯成 HTML 字符串返回給瀏覽器。這種時候咱們用到的模板引擎多是遠古的 jsp,或是如今用的比較多的 pug(原來叫 jade)、ejs。後端

模板引擎作的就是編譯模板的工做。它說白了就是一個函數:將模板字符串轉換成 HTML 字符串。數組

咱們先寫一個最簡單的靜態模板編譯函數:瀏覽器

正則替換

咱們的模板和數據以下:框架

const tpl = '<p>hello,我是{{name}},職業:{{job}}<p>'

const data = {
  name: 'hugo',
  job: 'FE'
}

那咱們想到的最簡單的辦法就是正則替換,固然咱們別忘了要把前綴加上,name 要轉換成 data.namejsp

function compile(tpl, data) {
  const regex = /\{\{([^}]*)\}\}/g
  const string = tpl.trim().replace(regex, function(match, $1) {
    if ($1) {
      return data[$1]
    } else {
      return ''
    }
  })
  console.log(string) // <p>hello,我是hugo,職業:FE<p>
}

compile(tpl, data)

上面的編譯函數在例子中是能夠工做的,但要是我把模板和數據改一下呢?函數

const tpl = '<p>hello,我是{{name}},年齡:{{info.age}}<p>'

const data = {
  name: 'hugo',
  info: {
    age: 26
  }
}

這個時候控制檯打印的就是:rest

<p>hello,我是hugo,年齡:undefined<p>

由於 data["info.age"] 的值是 undefined 。因此咱們還要處理正則匹配到的字符串,這個時候再用正則已經很是很差作了。既然這樣,不如就直接全改用字符串匹配:code

字符串解析

function compile(tpl) {
  let string = ''
  tpl = tpl.trim()
  while (tpl) {
    const start = tpl.indexOf('{{')
    const end = tpl.indexOf('}}')
    if (start > -1 && end > -1) {
      if (start > 0) {
        string += JSON.stringify(tpl.slice(0, start))
      }
      string += '+ data.' + tpl.slice(start + 2, end).trim() + ' +'
      tpl = tpl.slice(end + 2)
    } else {
      string += JSON.stringify(tpl)
      tpl = ''
    }
  }
  console.log(string)
  // "<p>hello,我是"+ data.name +",年齡:"+ data.info.age +"<p>"

  return new Function('data', 'return ' + string)
}

compile(tpl)(data) // <p>hello,我是hugo,年齡:26<p>

這樣咱們新的編譯函數就能夠處理 {{info.age}} 這種嵌套屬性的狀況了。上面的 JSON.stringify 做用是給字符串的兩端加上 ",而後轉義字符串中的特殊字符。模板引擎

雖然咱們解決了嵌套屬性的問題,但又面臨更困難的問題,就是怎樣讓模板裏插值支持像 {{ '名字是: ' + name }} 這樣表達式。在這種狀況下,咱們是很難在每一個正確的地方加 data. 前綴的,由於前綴只能加上變量前,而表達式裏可能還有字符串。

使用 with 語句

咱們考慮最簡單的處理方式,也就是不加前綴了,使用 with 語句指定變量的做用域。因此咱們只要編譯後返回一個函數,在這個函數內使用 with 語句指定做用域,函數再返回 HTML 字符串。在下面的例子中,我使用的是 ejs 模板的語法:

const tpl = `<p>hello,個人<%= '名字是: ' + name %>,年齡:<%= info.age %><p>`

const data = {
  name: 'hugo',
  info: {
    age: 26
  }
}
function compile(tpl) {
  const ret = []
  tpl = tpl.trim()
  ret.push('var _data_ = [];')
  ret.push('with(data) {')
  while (tpl) {
    let start = tpl.indexOf('<%=')
    const end = tpl.indexOf('%>')
    if (start > -1 && end > -1) {
      if (start > 0) {
        ret.push('_data_.push(' + JSON.stringify(tpl.slice(0, start)) + ');')
      }
      ret.push('_data_.push(' + tpl.slice(start + 3, end) + ');')
      tpl = tpl.slice(end + 2)
    } else {
      ret.push('_data_.push(' + JSON.stringify(tpl) + ');')
      tpl = ''
    }
  }
  ret.push('}')
  ret.push('return _data_.join("")')
  return new Function('data', ret.join('\n'))
}

const fn = compile(tpl)
fn(data)
// <p>hello,個人名字是: hugo,年齡:26<p>

上面的編譯函數將模板根據模板語法 <%=%> 分割成各個部分放入數組中,再將數組中的元素由換行符鏈接,成爲 new Function 的函數體,生成的函數以下:

function(data/*``*/) {
  var _data_ = [];
  with(data) {
    _data_.push("<p>hello,個人");
    _data_.push('名字是: ' + name);
    _data_.push(",年齡:");
    _data_.push(info.age);
    _data_.push("<p>");
  }
  return _data_.join("")
}

咱們再將 data 做爲參數傳入這個函數就能夠獲得指望的 HTML 字符串。

如今咱們已經實現了可以編譯插值是表達式的模板引擎。但咱們還差一個很是重要的功能,那就是編譯模板中的語句,如:for 循環和 if 語句。要實現編譯語句的功能,咱們必須將語句和插值區分開,所以要使用不一樣的模板語法:語句用 <% %>,插值則用<%= %>。那咱們就能夠將上面的編譯函數稍微修改下,根據不一樣的語法分別處理,就能夠支持模板語句了:

const tpl = `
<p>hello,我是<%= name + '-seth' %>,年齡:<%= info.age %><p>
<% if (info.age > 18 && info.age < 28){ %>
  <p>是個九零後中年人</p>
<% } %>
<h3>興趣</h3>
<ul>
  <% for (var i = 0; i < interests.length; i++) { %>
    <li><%= interests[i] %></li>
  <% } %>
</ul>
`
const data = {
  name: 'hugo',
  info: {
    age: 26
  },
  interests: ['movie']
}
function compile(tpl) {
  const ret = []
  tpl = tpl.trim()
  ret.push('var _data_ = [];')
  ret.push('with(data) {')
  while (tpl) {
    let start = tpl.indexOf('<%')
    const end = tpl.indexOf('%>')
    if (start > -1 && end > -1) {
      if (start > 0) {
        ret.push('_data_.push(' + JSON.stringify(tpl.slice(0, start)) + ');')
      }
      if (tpl.charAt(start + 2) === '=') {
        ret.push('_data_.push(' + tpl.slice(start + 3, end) + ');')
      } else {
        ret.push(tpl.slice(start + 2, end))
      }
      tpl = tpl.slice(end + 2)
    } else {
      ret.push('_data_.push(' + JSON.stringify(tpl) + ');')
      tpl = ''
    }
  }
  ret.push('}')
  ret.push('return _data_.join("")')
  return new Function('data', ret.join('\n'))
}
const fn = compile(tpl)
fn(data)
// <p>hello,個人名字是: hugo,年齡:26<p>

//   <p>是個九零後中年人</p>

// <h3>興趣</h3>
// <ul>
  
//     <li>movie</li>
  
// </ul>

這個修改後的編譯函數沒什麼好解釋的,就是根據不一樣的模板語法作不一樣的處理,最終返回的函數以下:

function(data /*``*/ ) {
  var _data_ = [];
  with(data) {
    _data_.push("<p>hello,個人");
    _data_.push('名字是: ' + name);
    _data_.push(",年齡:");
    _data_.push(info.age);
    _data_.push("<p>\n");
    if (info.age > 18 && info.age < 28) {
      _data_.push("\n  <p>是個九零後中年人</p>\n");
    }
    _data_.push("\n<h3>興趣</h3>\n<ul>\n  ");
    for (var i = 0; i < interests.length; i++) {
      _data_.push("\n    <li>");
      _data_.push(interests[i]);
      _data_.push("</li>\n  ");
    }
    _data_.push("\n</ul>");
  }
  return _data_.join("")
}

這樣咱們就已經完成了一個功能簡單的模板引擎。

相關文章
相關標籤/搜索