ejs 淺析

ejs是一種歷史悠久的模版,具備簡單、性能好、使用普遍的特色。雖然沒有vuereact這些項目流行,但仍是有使用的場合和學習的價值。這裏會介紹ejs項目的源碼。使用方法詳見項目的readme,或者這裏html

哲學 ejs是字符串模版引擎,生成的是字符串,其實能夠被用到很是多的地方,只要是動態生成字符串,就均可以用到。它的思想是模版 + 數據 => 最終的字符串模版是字符串的格式,包含可變部分的和固定的部分,可變的部分經過數據來控制。經過使用include方法引用其餘模版。這個模型就比較符合前端開發的須要了。前端

一些概念vue

  • template:即模版,好比這個栗子👇。
<% if (user) { %>
  <h2><%= user.name %></h2>
<% } %>
複製代碼
  • data: 模版對應的數據,具備模版中使用的全部變量,好比上面這個栗子中必須具備user.name這個數據。
  • option:模版配置項。有這些👇:
    • cache Compiled functions are cached, requires filename
    • filename The name of the file being rendered. Not required if you are using renderFile(). Used by cache to key caches, and for includes.
    • root Set project root for includes with an absolute path (/file.ejs).
    • context Function execution context
    • compileDebug When false no debug instrumentation is compiled
    • client When true, compiles a function that can be rendered in the browser without needing to load the EJS Runtime (ejs.min.js).
    • delimiter Character to use with angle brackets for open/close
    • debug Output generated function body
    • strict When set to true, generated function is in strict mode
    • _with Whether or not to use with() {} constructs. If false then the locals will be stored in the locals object. Set to false in strict mode.
    • localsName Name to use for the object storing local variables when not using with Defaults to locals
    • rmWhitespace Remove all safe-to-remove whitespace, including leading and trailing whitespace. It also enables a safer version of -%> line slurping for all scriptlet tags (it does not strip new lines of tags in the middle of a line).
    • escape The escaping function used with <%= construct. It is used in rendering and is .toString()ed in the generation of client functions. (By default escapes XML).
    • outputFunctionName Set to a string (e.g., 'echo' or 'print') for a function to print output inside scriptlet tags.
    • async When true, EJS will use an async function for rendering. (Depends on async/await support in the JS runtime.
  • compile:編譯函數,把template和option轉化爲一個函數,往這個函數中注入數據,生成最終的字符串,不必定是html哦,還能夠是各類形式的字符串。
  • render:渲染函數,直接把template、data和option轉化爲最終的字符串。

主流程 ejs引擎的實現思路是把配置的模版轉化爲渲染的函數,再經過的數據生成字符串。把模版轉化爲渲染函數的這個過程就是compile。它的主要工做就是通生成函數輸入和函數體的字符串,再經過Function這個類來生成函數。執行流程分別爲:react

  1. 根據正則表達式切割模版,好比{ key1 = <%= key1 %>, 2key1 = <%= key1+key1 %> }會被切割成[ '{ key1 = ', '<%=', ' key1 ', '%>', ', 2key1 = ', '<%=', ' key1+key1 ', '%>', ' }' ]
  2. 根據切割後的數據,生成渲染函數中的執行步驟。上面這個栗子中,執行步驟爲
' ; __append("{ key1 = ")\n ; __append(escapeFn( key1 ))\n ; __append(", 2key1 = ")\n ; __append(escapeFn( key1+key1 ))\n ; __append(" }")\n'
複製代碼
  1. 組裝函數,經過prepend+執行步驟+append的模式生成函數體的字符串,最後生成這樣的函數頭、函數體,以及渲染函數。
opts.localsName + ', escapeFn, include, rethrow'
複製代碼
var __output = [], __append = __output.push.bind(__output);
  with (locals || {}) {
     ; __append("{ key1 = ")
    ; __append(escapeFn( key1 ))
    ; __append(", 2key1 = ")
    ; __append(escapeFn( key1+key1 ))
    ; __append(" }")
   }
  return __output.join("");
複製代碼
function (data) {
  var include = function (path, includeData) {
    var d = utils.shallowCopy({}, data);
    if (includeData) {
      d = utils.shallowCopy(d, includeData);
    }
    return includeFile(path, opts)(d);
  };
  return fn.apply(opts.context, [data || {}, escapeFn, include, rethrow]);
}
複製代碼

經過渲染函數和數據生成結果的方式和react比較像哈。但ejs在不直接支持嵌套,而是經過include方法調用子模版的渲染函數。git

一些細節 渲染函數的4個參數:dataescapeFnincluderethrowgithub

  • data: 傳入的數據。
  • escapeFn: 轉義函數。
  • include: 引入子模版函數。主要的邏輯是根據路徑獲取模版,而且編譯生成渲染函數進行緩存,最後進行渲染。
var include = function (path, includeData) {
    var d = utils.shallowCopy({}, data);
    if (includeData) {
      d = utils.shallowCopy(d, includeData);
    }
    return includeFile(path, opts)(d);
  };
複製代碼
  • rethrow: 拋出異常函數。

生成渲染函數的執行步驟。 這一步是在模版被切割以後進行的,首先模版遇到ejs的標籤時就會被切割,切割後的字符串中標籤是成對出現的,引用一下上面的栗子。正則表達式

[ '{ key1 = ', '<%=', ' key1 ', '%>', ', 2key1 = ', '<%=', ' key1+key1 ', '%>', ' }' ]
複製代碼

ejs會根據不一樣的標籤生成不一樣的執行步驟。執行過程當中會遍歷整個數組。因爲標籤不能嵌套,並且成對出現,正好能夠利用全局的的變量,保存當前標籤的類型,執行到夾在一對標籤中的內容時,能夠獲取到外層標籤信息。當執行到閉合標籤時,重置標籤信息。api

關於路徑 先了解一下include方法,ejs的語法不支持嵌套,只能經過這個方法來複用模版。下面是一個使用的栗子。數組

<ul>
  <% users.forEach(function(user){ %>
    <%- include('user/show', {user: user}) %>
  <% }); %>
</ul>
複製代碼

在使用include方法時,須要傳入複用template的路徑和data。路徑的邏輯先會看是不是絕對路徑,而後會拼接傳入的路徑參數和options.filename,若是不存在這個文件最後看views的目錄下是否存在這個文件,代碼請看👇緩存

function getIncludePath(path, options) {
  var includePath;
  var filePath;
  var views = options.views;

  // Abs path
  if (path.charAt(0) == '/') {
    includePath = exports.resolveInclude(path.replace(/^\/*/,''), options.root || '/', true);
  }
  // Relative paths
  else {
    // Look relative to a passed filename first
    if (options.filename) {
      filePath = exports.resolveInclude(path, options.filename);
      if (fs.existsSync(filePath)) {
        includePath = filePath;
      }
    }
    // Then look in any views directories
    if (!includePath) {
      if (Array.isArray(views) && views.some(function (v) {
        filePath = exports.resolveInclude(path, v, true);
        return fs.existsSync(filePath);
      })) {
        includePath = filePath;
      }
    }
    if (!includePath) {
      throw new Error('Could not find the include file "' +
          options.escapeFunction(path) + '"');
    }
  }
  return includePath;
}
複製代碼

這就意味着在使用include的時候,子template文件只能在views目錄下,後綴爲ejs的文件。或者設置options.filename變量,文件分佈在不一樣的目錄下。這個就比較坑了,使用起來很不方便。當嵌套層次比較高時,怎麼複用模版?貌似只能經過絕對路徑的方式了。

相關文章
相關標籤/搜索