本身動手——實現Dustjs中間件

原文地址:http://home4j.duapp.com/index.php/2014/06/01/diy-writing-a-dust-middleware.htmlphp

Dustjs是我我的比較喜歡的一個JS模版引擎,緣由有兩個,一是,同時支持客戶端和服務端渲染,模版編譯成JS後使用,性能好;二是,有大公司的支持,Linkedin有專門的Dustjs版本(本文所說的都是該版本),並且通過線上考驗。html

關於Dustjs本文再也不贅述(可參看文檔),直接進入正題。node

1. 爲何要寫一箇中間件

Dustjs 官方支持做爲Express的View Engine使用,但我的傾向用於客戶端渲染,能減小服務端的性能損耗,充分利用客戶端的機器性能。目前Dustjs沒有相似於less- middleware的插件,可以在按需的對模版進行編譯,供客戶端引用,所以纔有了這個Dustjs中間件。jquery

2. Show Me The Code

2.1. 中間件

中間件代碼很簡單,只有幾十行,無非是攔截HTTP請求,如發現是獲取模版,則按需的進行編譯。git

// 依賴模塊的引入
var url = require('url'),
  fs = require('fs'),
  extend = require('node.extend'),
  dust = require('dustjs-linkedin'),
  beautify = require('js-beautify').js_beautify,
  iconv = require('iconv-lite'),
  path = require('path');

// 遵循模塊定義,把模塊暴露給使用方
module.exports = function(source, options) {

  // 使用node.extend模塊來提供默認值
  options = extend(true, {
    format: false, // 是否格式化代碼,便於閱讀
    encoding: 'utf-8' // 代碼的編碼格式,支持中文
  }, options || {});

  // source參數用於指定模版代碼的存放路徑,編譯後的JS代碼和模版源碼放在一塊兒
  if (!source) {
    throw new Error('dustjs-middleware requires `source` directory');
  }
  
  return function(req, res, next) {
    if ('GET' != req.method.toUpperCase() && 'HEAD' != req.method.toUpperCase()) {
      // 只處理Get和Head請求
      return next();
    }
    
    var pathname = url.parse(req.url).pathname;
    if (!/^\/dust\/[\S]+\.js$/.test(pathname)) {
      // 不是對JS文件的請求這裏不處理
      return next();
    }
    
    var jsPath = source + pathname;
    var dustPath = jsPath.replace(/\.js$/, '.dust');
    
    var error = function(err) {
      return next('ENOENT' == err.code ? null : err);
    };
    
    // 編譯模版的函數
    var compile = function() {
      fs.readFile(dustPath, function(err, buf){
        if (err) {
          return error(err);
        }
        
        // 用指定的編碼解析出模版源碼
        var data = iconv.decode(buf, options.encoding);

        // 編譯模版,以文件名做爲模版名
        var name = path.basename(dustPath, '.dust');
        var template = dust.compile(data, name);

        if (options.format) {
          // 有須要則進行代碼格式化,基於js-beautify
          template = beautify(template, { indent_size: 2 });
        }
        
        // 以指定的編碼寫入編譯後的JS代碼
        buf = iconv.encode(template, options.encoding);
        fs.writeFile(jsPath, buf, next);
      });
    };
    
    fs.stat(dustPath, function(dustErr, dustStats) {
      // 判斷模版代碼是否存在,不存在則不處理請求
      if (dustErr) {
        if ('ENOENT' == dustErr.code) {
          return next();
        } else {
          return next(dustErr);
        }
      }
      
      if (dustStats.isDirectory()) {
        // 模版代碼是個文件,也不處理
        return next();
      }
      
      fs.stat(jsPath, function(jsErr, jsStats) {
        if (jsErr) {
          if ('ENOENT' == jsErr.code) {
            // JS文件不存在,直接編譯
            return compile();
          } else {
            return next(jsErr);
          }
        } else if (dustStats.mtime > jsStats.ctime) {
          // 模版有變更,從新編譯
          return compile();
        }
      });
    });
  };
};

須要注意的是中間件以文件名做爲模版的名字,使用模版時,須要指定該模版名,示例以下。github

<div id="demo"></div>
<script src="https://home4j.duapp.com/share/jquery/jquery-2.min.js"></script>
<!-- 引入dust -->
<script src="https://home4j.duapp.com/share/linkedin-dustjs/dist/dust-core.min.js"></script>
<!-- 引入編譯後的模版 -->
<script src="context.js"></script>
<script>
  $(function() {
    // 準備數據
    var data = {
      ...
    };
    // 調用模版,模版名爲文件名
    dust.render("context", data, function(err, out) {
      $('#demo').replaceWith(out);
    });
  });
</script>

這裏隱含的一個約束是同一個頁面不能引入同名的模版,這會致使衝突,有必要時能夠在模版文件命名時加上Namespace作區分。express

2.2. 編碼問題

Dustjs的編碼問題相對簡單,先來看一個編譯後的Dust模版。npm

(function() {
  dust.register("hello", body_0);

  function body_0(chk, ctx) {
    return chk.write("Hello world!");
  }
  return body_0;
})();

全部的Dust模版在加載時都會註冊到dust 全局對象中,模版間的互相引用都是經過該全局對象完成,不像Less那樣須要把組件的代碼合併到一塊兒。所以解決Dustjs的編碼問題只要保證單個文件的編碼正確便可(詳見代碼)。app

2.3. Node模塊定義

除了代碼,還須要補充Node模塊的定義,才能被正常的依賴和使用。less

{
  // 做者信息
  "author": {
    "name": "Joshua Zhan",
    "email": "daonan.zhan@gmail.com",
    "url": "http://home4j.duapp.com/"
  },
  // 模塊信息
  "name": "dustjs-middleware",
  "description": "Dustjs middleware for express.",
  "version": "0.0.1",
  "repository": {
    "type": "git",
    "url": "http://git.oschina.net/joshuazhan/dustjs-middleware.git"
  },
  // 模塊代碼入口
  "main": "index.js",
  // 依賴
  "dependencies": {
    "dustjs-linkedin": "~2.3.4",
    "node.extend": "~1.0.8",
    "iconv-lite": "~0.2.11",
    "js-beautify": "~1.5.1"
  }
  ...
}

其中最重要的是指定模塊的入口,不然模塊將沒法被加載。

同時由於沒有加入npm倉庫,現階段還沒法直接使用,須要經過git來引入,示例"dustjs-middleware": "git+http://git.oschina.net/joshuazhan/dustjs-middleware.git" 。

3. 一些感想

3.1. Callback

得益於事件驅動和非阻塞的IO接口,Nodejs有着很好的性能,同時也帶來了編碼方式的變動。隨處可見的匿名函數和回調函數看起來讓人不太舒服,慶幸的是有一些有效的方法能在很大程度上緩解這個問題,推薦一篇文章給你們(http://callbackhell.com/),該文章對此作了很好的整理總結,但願能有所幫助。

3.2. Express

和Java Web的Filter相似,Express中間件也是鏈式的處理請求,一個典型的中間件以下:

function(request, response, next) {
  ...
  return next();
}

銜接各個中間件的則是next() 回調函數,若是中間件沒有把內容輸出到response 中,則必經過回調把請求交給下一個中間件處理。通常而言回調函數只能調用一次,屢次調用可能產生異常;不調用則請求得不到響應,佔用寶貴的連接資源和內存空間。

麻煩之處在於,Node中充斥着各類回調和匿名函數,使得next() 很是容易被遺忘或是錯誤的調用。這個目前貌似沒有很好的解決辦法,只能靠開發的經驗和測試,一個好的習慣是儘量的在調用回調後就當即返回return next(); ,這個能夠有效的避免屢次調用的問題。

相關文章
相關標籤/搜索