自己動手:實現Dustjs中間件
Dustjs是我個人比較喜歡的一個JS模版引擎,原因有兩個,一是,同時支持客戶端和服務端渲染,模版編譯成JS后使用,性能好;二是,有大公司的支持,Linkedin有專門的Dustjs版本(本文所說的都是該版本),而且經過線上考驗。
關于Dustjs本文不再贅述(可參看文檔),直接進入正題。
1. 為什么要寫一個中間件
Dustjs 官方支持作為Express的View Engine使用,但個人傾向用于客戶端渲染,能減少服務端的性能損耗,充分利用客戶端的機器性能。目前Dustjs沒有類似于less- middleware的插件,能夠在按需的對模版進行編譯,供客戶端引用,因此才有了這個Dustjs中間件。
2. Show Me The Code
2.1. 中間件
中間件代碼很簡單,只有幾十行,無非是攔截HTTP請求,如發現是獲取模版,則按需的進行編譯。
- // 依賴模塊的引入
- 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();
- }
- });
- });
- };
- };
需要注意的是中間件以文件名作為模版的名字,使用模版時,需要指定該模版名,示例如下。
- <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做區分。
2.2. 編碼問題
Dustjs的編碼問題相對簡單,先來看一個編譯后的Dust模版。
- (function() {
- dust.register("hello", body_0);
- function body_0(chk, ctx) {
- return chk.write("Hello world!");
- }
- return body_0;
- })();
所有的Dust模版在加載時都會注冊到dust 全局對象中,模版間的互相引用都是通過該全局對象完成,不像Less那樣需要把組件的代碼合并到一起。因此解決Dustjs的編碼問題只要保證單個文件的編碼正確即可(詳見代碼)。
2.3. Node模塊定義
除了代碼,還需要補充Node模塊的定義,才能被正常的依賴和使用。
- {
- // 作者信息
- "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(); ,這個可以有效的避免多次調用的問題。
本文來自:http://home4j.duapp.com/index.php/2014/06/01/diy-writing-a-dust-middleware.html