咱們的前端模版引擎更新總結

最近花了些時間更新了下咱們的模版引擎。就像構建工具同樣,模版引擎也基本是你們玩爛的內容,什麼運行速度啊,編譯速度啊,你們也談了不少。讓咱們講些不一樣的東東^ ^html

原文地址:https://app.yinxiang.com/shard/s30/sh/83bcf109-aee4-44df-ab01-4990d384d8f7/8fafd8df4dd428bbc6f76fadd6febb59git

項目地址:https://github.com/QQEDU/micro-tplgithub

需求

沒需求也不會有更新,首先咱們看看此次咱們有哪些需求:gulp

  • 隨着構建工具的發展,特別是gulp這種基於流(Stream)的構建工具的出現,之前的插件已經很難知足將來的需求,咱們須要將模版線下編譯核心抽出來,並知足:模版字符串通過核心後編譯成Javascript字符串。這樣才能使同一份代碼在多個構建工具複用。
  • include功能的開發,只有interpolationevaluation雖然解決了大部分問題,可是無法優雅的將模版分塊複用。誠然若是咱們基於AMD規範,也能夠利用r.js的打包解決,但畢竟仍是有些項目沒有基於AMD規範的。
  • 更早的發現錯誤,而不是運行時再提示。在編譯階段解決一些問題,那麼實際運行過程當中問題就少了。

怎樣include比較好?

空間的糖餅的artTemplate使用:app

{{include '../public/header'}}

也就是直接把一個模版插入到另外一個新的模版。和數據沒啥關係的模版(好比頁面的header)還好,對於有數據的模版就略微蛋疼了。兩個模版共享同一個上下文,給模版複用帶來較大的困難。咱們先想一想,若是對於使用了AMD規範的項目中的include是怎樣的?函數

<%=require('../public/header')(data, opts)%>

很好,爲了對齊體驗又便於區分,咱們引入include方法:工具

<%=include('../public/header')(data, opts)%>

這樣咱們就解決了上面提到的問題。測試

如何更早的發現問題

在編譯構成中,先檢查代碼,保證代碼沒有問題。ui

沒法分析的html問題

在模版中,html問題是難以分析的,由於咱們不清楚Javascript會輸出什麼,例如:this

<a href=<%=it.href ? '"'  + it.href + '"' : ''%>>

該模版是有問題的,好比當it.href不存在是就變成了<a href=>,但很難分析出來。因此咱們忽略html的錯誤,把它放在渲染時處理。

Javascript問題

閉合問題

模版閉合問題時一般遇到的問題,好比

noclose<%

這是比較好分析的,再好比說:

<% if (true) { %>
<a href="javscirpt:void(0);">hello</a>
<% { %>

這也較好分析。

變量問題

變量問題很差分析,由於咱們沒法知道傳進來的變量可能有什麼。

那麼咱們換一種思路,好比在Strict Mode時候的變量分析,除去模版內聲明的變量和函數以及it和opt外,其餘變量引用應當都是不合法的,例如考慮下面例子:

<a href="<%=$.render(it.item.url)%>">hello</>

這個例子是不合法的,由於內部沒有明顯的$的申明,嚴格模式下,不該當容許使用這種隱性引用。

字面量沒法多行

因爲實現緣由,字面量是沒法多行的,考慮下面例子:

<p><%=
it.say
%></p>

字面量沒法使用分號

例如:

<p><%=it.say;%></p>

Javascript解析錯誤

例如:

<p><%={say, ,'error'}%></p>

實現

簡單的說,只是利用語法分析找到這些問題。但咱們仍是有點難點的:

  • 如何從終端直觀的找到代碼問題
  • Javascirpt AST Parse固然能解決不少問題,可是模版語言她不懂,仍是要處理下再丟給她的

比較好看的UI

首先咱們先去jscs扒了段代碼,只要告訴該函數錯誤的行和列,以及原文件內容他就能雖然出一個不錯的UI:

// Inspired from jscs
var colors = require('colors');

function explainError(error, colorize) {
  // 錯誤的行號
  var lineNumber = error.line - 1
    // 原文件的內容,並拆成一行一行
    , lines = error.lines
    // 輸出結果
    , result = [
        renderLine(lineNumber, lines[lineNumber], colorize),
        renderPointer(error.column, colorize)
  ] , i = lineNumber - 1
    // 顯示錯誤行上下2行,即最多顯示5行內容
    , linesAround = 2;
  // 從錯誤行向前找行數並渲染
  while (i >= 0 && i >= (lineNumber - linesAround)) {
      result.unshift(renderLine(i, lines[i], colorize));
      i--;
  }
  i = lineNumber + 1;
  // 從錯誤行向後找行數並渲染
  while (i < lines.length && i <= (lineNumber + linesAround)) {
      result.push(renderLine(i, lines[i], colorize));
      i++;
  }
  result.unshift(formatErrorMessage(error.message, error.filename, colorize));
  return result.join('\n');
}

// 生成錯誤提示
function formatErrorMessage(message, filename, colorize) {
  return (colorize ? colors.bold(message) : message) +
    ' at ' +
    (colorize ? colors.green(filename) : filename) + ' :';
}

// 生成制定空格
function prependSpaces(s, len) {
  while (s.length < len) {
    s = ' ' + s;
  }
  return s;
}

// 渲染一行內容
function renderLine(n, line, colorize) {
  line = line.replace(/\t/g, ' ');

  var lineNumber = prependSpaces((n + 1).toString(), 5) + ' |';
  // 渲染出行號 + 內容,例如:
  // 1 | alert('hello world');
  return ' ' + (colorize ? colors.grey(lineNumber) : lineNumber) + line;
}

// 渲染出指針,用於指示出錯誤位置
function renderPointer(column, colorize) {
  var res = (new Array(column + 9)).join('-') + '^';
  return colorize ? colors.grey(res) : res;
}

module.exports = explainError;

分析步驟

首先咱們先解決<%%>的閉合問題:

analyse: function () {
  // 找到打開標記
  var i = find(this.tmpl, '<%' , this.i);
  // 若是存在
  if (~i) {
    this.i = i;
    // 找下一個閉合標記
    i = find(this.tmpl, '%>', this.i);
    // 存在,則辨認是interpolation仍是evaluation
    if (~i) {
      this.tmpl.charAt(this.i + 2) === '=' ?
        // check interpolation
        this.check(this.tmpl.substring(this.i + 3, i), i + 3) :
        // prepare for ast builder
        this.script.push([this.tmpl.substring(this.i + 2, i), this.i + 2]);
      this.i = i;
      // 遞歸遍歷
      this.analyse();
    // 不存在,顯然沒閉合,拋出錯誤
    } else {
      throw (explainError(merge({
        message: 'it need %> after <%',
        lines: this.lines,
        filename: this.filename
      }, getPos(this.tmpl, this.i)), true));
    }
  // 不然,即閉合問題檢查完畢,檢查腳本
  } else {
    this.checkEval();
  }
}

解決interpolation錯誤問題

直接將其丟入AST進行分析,可是因爲實現緣由,不能使用分號和換行,因此若是使用,則拋錯:

check: function (str, i) {
  // 使用換行,則拋錯
  if (~str.indexOf('\n')) {
    throw (explainError(merge({
      message: 'Should not use multi-lines in interpolation',
      lines: this.lines,
      filename: this.filename
    }, getPos(this.tmpl, this.i + str.indexOf('\n') + 2)), true));
  }
  // 創建ast,若是有錯誤,則包裝後拋出
  try {
    var ast = acorn.parse(str);
  } catch (e) {
    throw (explainError(merge({
      message: e.toString().replace(/\(.+?\)/, ''),
      lines: this.lines,
      filename: this.filename
    }, getPos(this.tmpl, this.i + e.pos + 3)), true));
  }
  // 若是發現ast末尾爲分號(目前還未實現辨認全非字符串分號,簡單處理末尾的分號),則拋錯
  if (str.charAt(ast.end - 1) === ';') {
    throw (explainError(merge({
      message: 'Should not use ";" in interpolation',
      lines: this.lines,
      filename: this.filename
    }, getPos(this.tmpl, this.i + ast.end + 2)), true));
  } 
}

解決evaluation問題

咱們將前面過程當中獲得的代碼組裝起來,丟進ast,若是找到問題,從新定位並拋錯:

checkEval: function () {
  // 若是有腳本則檢查,沒有不須要進行
  if (this.script.length) {
    var script = '';
    // 將腳本組合起來
    this.script.forEach(function (arr) {
      script += arr[0];
    });
    // 丟進ast看看有沒有錯誤
    try {
      var ast = acorn.parse(script);
    } catch (e) {
      // 有錯誤則經過錯誤,回溯源碼位置,包裝後拋出
      var pos = e.pos, num, l;
      this.script.every(function (arr, i) {
        l = arr[0].length;
        if (l < pos) {
          pos -= l;
          return true;
        } else {
          num = i;
          return false;
        }
      });
      pos = this.script[num][1] + pos;
      throw (explainError(merge({
        message: e.toString().replace(/\(.+?\)/, ''),
        lines: this.lines,
        filename: this.filename
      }, getPos(this.tmpl, pos)), true));
    }
  }
}

這樣咱們就基本解決了編譯階段提早找出錯誤的需求,將來看看如何改進了^ ^

效果

 

Alt text
 

 

咱們能夠看到對於測試用例noclose.html:

no<%close

模版引擎提示本模版有錯誤,並指出問題處在哪裏,開發者能夠很是方便的定位問題。其餘用例類推。

相關文章
相關標籤/搜索