Node——構建Web應用

基礎功能

之前我們通過http模塊創建了一個簡單的服務器,但是對於一個網絡應用來說肯定是遠遠不夠的,在聚義的業務中我們至少有如下要求:

  • 請求方法的判斷
  • URL的路徑解析
  • URL中查詢字符串的解析
  • Cookie的解析
  • Basic認證
  • 表單數據的解析
  • 任意格式的上傳處理
  • Session管理

一切的開始都是這個函數:

var server = http.createServer(function (req, res) {  
    res.writeHead(200, {'Content-Type': 'text/plain'});   
    res.end('Hello World\n'); 
}).listen(1337, '127.0.0.1'); 

請求方法

最常見的請求方法就是GET和POST。我們可以根據這個來決定響應的行爲。

function (req, res) {   
    switch (req.method) {   
        case 'POST':     
            update(req, res);     
            break;   
        case 'DELETE':     
            remove(req, res);     
            break;   
        case 'PUT':     
            create(req, res);     
            break;   
        case 'GET':   
        default:     
            get(req, res);   
    } 
}

路徑解析

比較常見的場景是根據路徑來選擇控制器
我們假裝配置一個簡單的控制器:

var handles = {};
handles.index = {}; 
handles.index.index = function (req, res, foo, bar) {   
    res.writeHead(200);   
    res.end("Rabbit&Lion"+foo+bar); 
}; 

並在get方法裏訪問:

function get(req, res){
    var pathname = url.parse(req.url).pathname;   
    var paths = pathname.split('/');   
    var controller = paths[1] || 'index';   
    var action = paths[2] || 'index';   
    var args = paths.slice(3);   
    if (handles[controller] && handles[controller][action]) {     
        handles[controller][action].apply(null, [req, res].concat(args));   
    } else {     
        res.writeHead(500);     
        res.end('no such controller');   
    }
}

查詢字符串

可以使用現成的方法將url中的query字符串轉爲對象:

var query = url.parse(req.url, true).query; 

注意如果相同的鍵在查詢中出現兩次,值會是一個數組而不是字符串。

Cookie的處理分爲以下這幾個步驟:

  • 服務器向客戶端發送Cookie
  • 瀏覽器儲存住Cookie
  • 之後每次訪問這個域名時瀏覽器都會將這個Cookie發向服務器

服務器向客戶端發送Cookie時是在響應頭部加入:

Set-Cookie: name=value; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com; 

客戶端向服務器發送時,cookie就在header裏,所以通過req.headers.cookie就可以獲取到,可以給處理成對象:

var parseCookie = function (cookie) {   
    var cookies = {};   
    if (!cookie) {     
        return cookies;   
    } 
    var list = cookie.split(';');   
    for (var i = 0; i < list.length; i++) {     
        var pair = list[i].split('=');     
        cookies[pair[0].trim()] = pair[1];   
    }   
    return cookies; 
};

這裏要注意的是Cookie對性能的影響,Cookie對設置路徑的所有子路徑都有效,所以在太高的根URL上設置就很不妥。

Session

Cookie有個問題,它是可以自己改的,所以在cookie中放一些敏感信息或權限什麼的是不合適的。
Session的數據只保留在服務器端,並通過客戶端發來的一些身份標識和用戶對應起來。
基於Cookie
雖然不能在Cookie裏放這些數據,但是將口令放在Cookie裏是可以的。因爲口令一旦被篡改,就丟失了映射關係。
使用查詢字符串
這種直接放在URL裏也是個很方便的辦法,但是這不太安全,只要別人獲得了這個URL就擁有與你相同的身份。

緩存

條件請求:使用If-Modified-Since來詢問服務器是否有最新的改動,服務器端做如下處理:

var catchControl = function (req, res) {   
    fs.stat(filename, function (err, stat) {     
        var lastModified = stat.mtime.toUTCString();     
        if (lastModified === req.headers['if-modified-since']) {       
            res.writeHead(304, "Not Modified");       
            res.end();     
        } else {       
            fs.readFile(filename, function(err, file) {         
                var lastModified = stat.mtime.toUTCString();         
                res.setHeader("Last-Modified", lastModified);         
                res.writeHead(200, "Ok");         
                res.end(file);       
            });     
        } 
    });
}

這樣的處理有些缺陷:只精確到秒級別,時間戳變內容不一定變。
ETag
這個是一個由服務器端生成的值,服務器可以決定它的生成規則,如果根據文件內容生成散列值那麼就可以根據這個值精確的判定文件是否有改動。ETag的請求和響應是:If-None-Match/ETag。

var getHash = function (str) {   
    var shasum = crypto.createHash('sha1');   
    return shasum.update(str).digest('base64'); 
};
var ETag = function (req, res) {   
    fs.readFile(filename, function(err, file) {     
        var hash = getHash(file);     
        var noneMatch = req.headers['if-none-match'];     
        if (hash === noneMatch) {       
            res.writeHead(304, "Not Modified");       
            res.end();     
        } else {       
            res.setHeader("ETag", hash);       
            res.writeHead(200, "Ok");       
            res.end(file);     
        }   
    }); 
}; 

不過儘管這個方法很靈活,並且在文件沒有改動的時候很節省帶寬,但他依然會發起一個HTTP請求。
Expires、Cache-Control
這個方法直接告訴瀏覽器該資源會過期的時間。

數據上傳

之前的內容都發生在頭部,但是表單提交,文件提交等動作是不能放在頭部的。
你可以通過頭部中的字段值判斷請求中是否包含內容,然後通過Buffer把它存起來:

var hasBody = function(req) {   
    return 'transfer-encoding' in req.headers || 'content-length' in req.headers; 
}; 
if (hasBody(req)) {     
    var buffers = [];     
    req.on('data', function (chunk) {       
        buffers.push(chunk);     
    });     
    req.on('end', function () {       
        req.rawBody = Buffer.concat(buffers).toString();       
        handle(req, res);     
    });   
} else {     
    handle(req, res);   
} 

表單

對於表單數據,非常好解析,它的報文體內容和查詢字符串相同:

var mime = function (req) {   
    var str = req.headers['content-type'] || '';   
    return str.split(';')[0]; 
};
if (mime(req) === 'application/x-www-form-urlencoded') {     
    req.body = querystring.parse(req.rawBody);  
}

JSON文件

if (mime(req) === 'application/json') {     
    try {       
        req.body = JSON.parse(req.rawBody);     
    } catch (e) {       // 異常內容響應Bad request   
        res.writeHead(400);       
        res.end('Invalid JSON');       
        return;     
    }   
}

XML

這個有點複雜,不過有XML轉json的模塊。

附件上傳

這裏當用戶需要直接提交文件時,表單屬性enctype需要指定爲multipart/form-data。瀏覽器遇到了這樣的表單,就會構造一種特殊的報文,頭部中:

Content-Type: multipart/form-data; boundary=AaB03x Content-Length: 18231 

可以看到這裏type代表本次提交的內容是由多部分構成的,其中boundary這個字段裏的值代表了每部分之間的分隔符。

--AaB03x\r\n 
Content-Disposition: form-data; name="username"\r\n 
\r\n 
Jackson Tian\r\n 
--AaB03x\r\n 
Content-Disposition: form-data; name="file"; filename="diveintonode.js"\r\n 
Content-Type: application/javascript\r\n 
\r\n  
... contents of diveintonode.js ... 
--AaB03x--

根據這些你就可以解析它了。
你可以使用formidable模塊來處理:

if (mime(req) === 'multipart/form-data') { 
    var form = new formidable.IncomingForm();  
    form.parse(req, function(err, fields, files) {         
        req.body = fields;         
        req.files = files; 
        console.log(err);
        console.log(files);
        console.log(fields);
    });
}

數據上傳與安全

內存限制
在解析表單,JSON等小數據的時候,我們常常採用的策略是先保存用戶提交的所有數據,解析,傳遞給業務邏輯。但是如果數據量很大,這樣做一下就會把內存佔光。
可以從兩個方面來解決這個問題:

  • 限制上傳內容的大小,一旦超過限制,停止接收數據
  • 通過流式解析將數據流導向到磁盤中

CSRF
跨站請求僞造,這個利用的是瀏覽器會根據域名發送cookie,來發送未經用戶允許的請求。
假設你登陸了a站沒有退出,這時你登陸了有惡意的b站,b站上有這麼一段會自動提交的表單:

<form id="test" method="POST" action="http://a.com/sell">
    <input type="hidden" name="content" value="把我的錢都轉給b" />
</form>
<script type="text/javascript">     
    $(function () {     
        $("#test").submit();   
    }); 
</script>

那瀏覽器就會在這段表單提交的同時發送你的cookie到a站,a站會誤以爲真的是你發起的請求。
要防止這樣的危機發生,現在通行的解決方式是服務器在渲染前端頁面時在表單中加一個隨機值。在提交時檢測這個隨機值,這樣就可以保證請求來自自己服務器的頁面了。

<form id="test" method="POST" action="http://a.com/sell">
    <input type="hidden" name="content" value="把我的錢都轉給b" />
    <input type="hidden" name="_csrf" value="< =_csrf >" />
</form>

路由解析

以前路由基本是按照服務器文件結構來的。

MVC

這是目前應用最廣泛的模型。工作模式爲:

  • 路由解析,根據URL尋找響應的控制器
  • 控制器中調用相應的模型進行數據操作
  • 數據操作結束後,調用視圖和相關數據進行頁面渲染

我們將之前直接寫的handles控制器改進一下:
將每個controller提出來放在一個模塊中,這邊解析URL的時候就直接將模塊讀出來:

var module; 
try {        
    module = require('./controllers/' + controller);   
} catch (ex) {     
    res.writeHead(500);     
    res.end('no such controller');      
    return;   
}   
var method = module[action];   
if (method) {     
    method.apply(null, [req, res].concat(args));   
} else {     
    res.writeHead(500);     
    res.end('no such controller');
    return;   
}

RESTful

這個是最近流行的起來的URL格式,其目的就是將服務器端提供的內容實體看做一個資源,對於同一個資源使用同樣的URL,利用正確的HTTP方法執行增刪改查的操作,比如,原來的URL:

POST /user/add?username=jacksontian 
GET /user/remove?username=jacksontian 
POST /user/update?username=jacksontian 
GET /user/get?username=jacksontian 

現在就變成了:

POST /user/jacksontian 
DELETE /user/jacksontian 
PUT /user/jacksontian 
GET /user/jacksontian 

我們改進下路由:

var routes = {'all': []}; 
var app = {}; 
app.use = function (path, action) {   
    routes.all.push([pathRegexp(path), action]); 
};  
['get', 'put', 'delete', 'post'].forEach(function (method) {   
    routes[method] = [];   
    app[method] = function (path, action) {     
        routes[method].push([pathRegexp(path), action]);   
    }; 
}); 
app.post('/user/:username', addUser); 
app.delete('/user/:username', removeUser); 
app.put('/user/:username', updateUser); 
app.get('/user/:username', getUser); 

分發方法:

function (req, res) {   
    var pathname = url.parse(req.url).pathname;   
    // 將請求方法爲小寫   
    var method = req.method.toLowerCase();   
    var match = function (pathname, routes) {   
        for (var i = 0; i < routes.length; i++) {     
            var route = routes[i];     // 正則配     
            var reg = route[0].regexp;     
            var keys = route[0].keys;     
            var matched = reg.exec(pathname);
            console.log(pathname);
            console.log(matched);  
            console.log(keys);
            console.log(reg);
            if (matched) {       // 具體       
                var params = {};       
                for (var i = 0, l = keys.length; i < l; i++) {         
                    var value = matched[i + 1];         
                    if (value) {           
                        params[keys[i]] = value;         
                    }       
                }       
                req.params = params;  
                var action = route[1];       
                action(req, res);       
                return true;     
            }   
        }  
        return false; 
    };
    if (routes.hasOwnProperty(method)) {     
        // 據請求方法分發    
        if (match(pathname, routes[method])) {       
            return;     
        } else {       
            // 如路徑有配不成功試all()處理       
            if (match(pathname, routes.all)) {         
                return;       
            }     
        }   
    } else {     
        // 接all()處理     
        if (match(pathname, routes.all)) {       
            return;     
        } 
    }   
    // 處理404請求   
    res.writeHead(404);     
    res.end('no such controller');
}

中間件

經過上面的過程我們驚奇的發現做了這麼多事情我們其實並沒有開始搭建我們的Web應用。我們希望可以不用接觸到這麼多細節性的處理,爲此我們引入中間件來簡化和和隔離這些基礎設施與業務邏輯之間的細節。
採用尾觸發的方法來執行中間件,每一箇中間件執行完就通知下一個中間件執行,那麼一個基本的中間件的形式如下所示:

初級版

var middleware = function (req, res, next) {
    // TODO
    next(); 
} 

最後實現完我們希望可以直接這樣添加中間件:

app.use('/user/:username', querystring, cookie, session, function (req, res) {   
    // TODO 
}); 

這裏把我們之前的2個功能改寫爲中間件的形式:

// querystring解析中間件 
var querystring = function (req, res, next) {   
    req.query = url.parse(req.url, true).query;   
    next(); 
}; 
// cookie解析中間件 
var cookie = function (req, res, next) {   
    var cookie = req.headers.cookie;   
    var cookies = {};   
    if (cookie) {     
        var list = cookie.split(';');     
        for (var i = 0; i < list.length; i++) {       
            var pair = list[i].split('=');       
            cookies[pair[0].trim()] = pair[1];     
        }   
    }  
    req.cookies = cookies;   
    next(); 
}; 

改進use和各HTTP方法對應的綁定函數,將所有的中間件和業務方法與路徑一起存在路由中。

app.use = function (path) {   
    var handle = {     
        // 第一個參數作爲路徑     
        path: pathRegexp(path),     
        // 其他的是處理單元     
        stack: Array.prototype.slice.call(arguments, 1)   
    };   
    routes.all.push(handle);
};  
['get', 'put', 'delete', 'post'].forEach(function (method) {   
    routes[method] = [];   
    app[method] = function (path, action) {     
        var handle = {     
            // 第一個參數作爲路徑     
            path: pathRegexp(path),     
            // 其他的是處理單元     
            stack: Array.prototype.slice.call(arguments, 1)   
        };   
        routes[method].push(handle);   
    }; 
}); 

由於路由的結構變了,匹配部分也需要修改一下,在完成匹配後,我們把所有的中間件和方法取出來,傳給handle方法:

var match = function (pathname, routes) {   
    for (var i = 0; i < routes.length; i++) {     
        var route = routes[i];     // 正則配     
            var reg = route[0].regexp;     
            var keys = route[0].keys;     
            var matched = reg.exec(pathname);
            if (matched) {       // 具體       
                var params = {};       
                for (var i = 0, l = keys.length; i < l; i++) {         
                    var value = matched[i + 1];         
                    if (value) {           
                        params[keys[i]] = value;         
                    }       
                }       
                req.params = params;       
                handle(req, res, route.stack);       
                return true;     
            }   
        }  
        return false; 
    };

handle方法通過遞歸實現尾觸發,逐個執行中間件和業務邏輯:

var handle = function (req, res, stack) {  
    var quene = stack.slice(0);
    var next = function () {     
        // 從stack數組中出中間件執行    
        var middleware = quene.shift();  
        if (middleware) {       
            // 傳入next()函數自使中間件能執行結遞
            middleware(req, res, next);     
        }   
    };  
    // 啓動執行   
    next(); 
};

這樣一來只要這樣配置就好了:

app.get('/user/:username/:ID', querystring, cookie , getUser);

不過我們不想每次設置每個路由的時候都把所有的中間件打上,我們希望有個配置公有中間件的地方,上面這種就只配置特殊的。

改進版

現在我們是找到匹配的就不往下匹配了,現在我們要把所有匹配的都存下來,最後一起執行。
在use方法中,如果我們只傳入一箇中間件,就將path指定爲“/”,並添加到相應的路由裏:

app.use = function (path) {   
    var handle;   
    if (typeof path === 'string') {     
        handle = {           
            path: pathRegexp(path),            
            stack: Array.prototype.slice.call(arguments, 1)     
        };   
    } else {     
        handle = {    
            path: pathRegexp('/'),    
            stack: Array.prototype.slice.call(arguments, 0)     
        };   
    }   
    routes.all.push(handle); 
};  
['get', 'put', 'delete', 'post'].forEach(function (method) {   
    routes[method] = [];   
    app[method] = function (path) {     
        var handle;   
        if (typeof path === 'string') {     
            handle = {           
                path: pathRegexp(path),            
                stack: Array.prototype.slice.call(arguments, 1)     
            };   
        } else {     
            handle = {    
                path: pathRegexp('/'),    
                stack: Array.prototype.slice.call(arguments, 0)     
            };   
        }   
        routes[method].push(handle);   
    }; 
}); 

在匹配的方法中,我們不直接執行handle,而是先把所有要執行的中間件和方法存起來:

var match = function (pathname, routes) {   
    var stacks = []; 
    for (var i = 0; i < routes.length; i++) {     
        var route = routes[i];     // 正則配     
        var reg = route.path.regexp;     
        var keys = route.path.keys;    
        console.log(reg);
        console.log(pathname);
        if (reg.toString()==='/^\\/$/') {
            stacks = stacks.concat(route.stack);
        }
        var matched = reg.exec(pathname);
        console.log(matched);
        if (matched) {       // 具體       
            var params = {};       
            for (var i = 0, l = keys.length; i < l; i++) {         
                var value = matched[i + 1];         
                if (value) {           
                    params[keys[i]] = value;         
                }       
            }       
            req.params = params;       
            stacks = stacks.concat(route.stack);      
        }   
    }  
    return stacks; 
};

在分發方法中,我們將所有的方法拿出來並執行:

var stacks = match(pathname, routes.all); 
if (routes.hasOwnProperty(method)) {     
    stacks = stacks.concat(match(pathname, routes[method])); 
} 
if (stacks.length) {     
    handle(req, res, stacks);   
} else {     
    // 處理404請求   
    res.writeHead(404);     
    res.end('no such controller'); 
}

異常處理

如果中間件發生了異常,我們需要獲取到異常並處理,那我們將handle更改一下來處理中間件拋出來的同步異常。

var handle = function (req, res, stack) {  
    var quene = stack.slice(0);
    var next = function (err) {     
        // 從stack數組中出中間件執行    
        if (err) {       
            return handle500(err, req, res, quene);     
        } 
        var middleware = quene.shift();  
        if (middleware) {       
            // 傳入next()函數自使中間件能執行結遞    
            try {         
                middleware(req, res, next);       
            } catch (ex) {         
                next(err);       
            }     
        }   
    };  
    // 啓動執行   
    next(); 
};

對於異步中間件的異常這樣捕獲不到,可以在回調裏直接返回

next(err); 

異常處理也可能有好多種,我們可以將其也做爲中間件來處理,用參數的數量來進行篩選,異常處理的中間件長這個樣子:

var middleware = function (err, req, res, next) {
    // TODO   
    next(); 
};

我們把所有的錯誤交給了handle500這個函數,這個函數把錯誤接過來,傳給每個錯誤中間件,遞歸執行。

var handle = function (req, res, stack) {  
    var quene = stack.filter(function (middleware) {     
        return middleware.length !== 4;   
    });
    var errorQuene = stack.filter(function (middleware) {     
        return middleware.length === 4;   
    });
    var next = function (err) {     
        // 從stack數組中出中間件執行    
        if (err) {       
            return handle500(err, req, res, errorQuene);     
        } 
        console.log(quene);
        var middleware = quene.shift();  
        console.log(middleware);
        if (middleware) {       
            // 傳入next()函數自使中間件能執行結遞    
            try {         
                middleware(req, res, next);       
            } catch (ex) { 
                console.log('catch error');        
                next(ex);       
            }     
        }   
    };  
    // 啓動執行   
    next(); 
};

handle500就是一個簡化版的handle,只負責處理錯誤的隊列:

var handle500 = function (err, req, res, stack) {   
    // 異常處理中間件   
    console.log(stack);
    console.log(err);
    var next = function () {     
    // 從stack數組中出中間件執行     
        var middleware = stack.shift();     
        if (middleware) {       
        // 傳遞異常對象       
            middleware(err, req, res, next);     
        }   
    };  
    // 啓動執行   
    next(); 
};

中間件與性能

我們的業務邏輯往往是在所有的中間件執行完再執行的,中間件的性能至關重要。
對於中間件本身來說,效率要儘量的高,算法效率要高,要儘量緩存重複計算的結果,避免不必要的計算,比如對於GET方法,HTTP報文就不用解析了。
還要合理的使用路由,不必要執行的中間件一定不要執行。

頁面渲染

在前面的中間件完成對請求的預處理後,我們通過數據庫,文件操作等取得數據,接下來我們就要將響應發送給客戶端了。響應會包括HTML,CSS,JS或多媒體文件等。對於過去流行的動態網頁技術,比如JSP等,它們自帶頁面渲染功能,Node並不帶這樣的功能,但也正是因爲這樣,我們可以更加靈活的創建自己的渲染技術。

內容響應

響應過程中,響應頭部中Content-*字段非常重要,它指示了客戶端如何處理這段響應,比如:

Content-Encoding: gzip 
Content-Length: 21170 
Content-Type: text/javascript; charset=utf-8 

MIME
這個指的就是Type部分,如果設置的不正確,會影響到客戶端的處理,比如你返回的是HTML,這個值卻設置的是text/plain,那客戶端就會直接把所有的文本顯示出來而不加載DOM。
附件下載
有時不需要客戶端打開文件,只需要下載就好,這時可以設置這個字段:

Content-Disposition: attachment; filename="filename.ext" 

不是附件時把值設置爲inline就好。

function download(req,res){
    res.sendfile = function (filepath) {   
        fs.stat(filepath, function(err, stat) {     
            var stream = fs.createReadStream(filepath); // 設置內容     
            res.setHeader('Content-Type', mime.lookup(filepath));     // 設置長     
            res.setHeader('Content-Length', stat.size);     // 設置爲附件     
            res.setHeader('Content-Disposition',' attachment; filename="' + pathUtil.basename(filepath) + '"');     
            res.writeHead(200);  
            //由於res也是個可讀可寫流,這裏我們直接使用pipe方法。
            //這個方法監聽stream的data事件和end事件,分別會調用res的write方法和end方法   
            stream.pipe(res);   
        }); 
    };
    res.sendfile('test.js');
}

響應JSON
爲了快速處理某些格式的數據,可以在響應上封裝響應的方法:

res.json = function (json) {   
    res.setHeader('Content-Type', 'application/json');   
    res.writeHead(200);   
    res.end(JSON.stringify(json)); 
}; 

響應跳轉
當出現什麼問題的時候,可以手動跳轉:

res.redirect = function (url) {   
    res.setHeader('Location', url);   
    res.writeHead(302);   
    res.end('Redirect to ' + url); 
};  
res.redirect('www.baidu.com');

視圖渲染

在動態頁面技術中,最終的視圖是通過模板和動態的數據生成出來的。模板是帶有特殊標籤的HTML片段,我們可以設計這樣一個方法:

res.render = function (view, data) {   
    res.setHeader('Content-Type', 'text/html');   
    res.writeHead(200);   
    // 實渲染   
    var html = render(view, data);   
    res.end(html); 
};

這裏的view就是模板,data就是傳進去的數據啦

模板

模板技術看起來多種多樣,他們的本質是一樣的,都是將模板文件和數據通過模板引擎生成最終的HTML代碼,其本質上乾的是拼接字符串這樣很底層的活。
模板引擎
我們來實現一個簡單的模板,使用<%==%>來作爲模板標籤。

Hello <%=username%>

這個模板,對於數據:

{username: "JacksonTian"}

會輸出Hello JacksonTian。

var complied = function(str) {
    var tpl = str.replace(/<%=([\s\S]+?)%>/g, function(match, code) { 
        return "' + obj." + code + ";";   
    }); 
    tpl = "var tpl = '" + tpl + "\nreturn tpl;";   
    return new Function('obj', tpl);
}
var render = function (complied, data) {   
    return complied(data); 
};

這裏complied函數會根據模板生成一個函數,這個過程稱爲模板編譯。接下來只要將數據傳給這個函數,這個函數就可以渲染出最後的頁面。這個函數只與模板本身有關係,與數據無關,所以不需要每次請求都生成這個函數,我們把這個過程提出來以便緩存。
with的應用
上面的模板引擎有個可以改進的地方,因爲使用了obj.code的方式來獲取數據,那如果我的模板中想寫一個:

Hello <%="exialym"%>

這樣固定的數據,由於obj.”exialym”是有語法錯誤的,應用就會報錯。我們對complied做一點修改:

var complied = function (str, data) {   // 模板是換的   
    var tpl = str.replace(/<%=([\s\S]+?)%>/g, function (match, code) {      
        return "' + " + code + ";";   
    });  
    tpl = "tpl = '" + tpl + "";   
    tpl = 'var tpl = "";\nwith (obj) {' + tpl + '}\nreturn tpl;'; 
    return new Function('obj', tpl); 
};

這回如果是字符串就直接輸出了,因爲在obj中找不到,如果是要傳入動態的也沒問題。
模板安全
爲了防止XSS漏洞,需要對數據值進行轉義。

var escape = function (html) {   
    return String(html)
        .replace(/&(?!\w+;)/g, '&amp;')     
        .replace(/</g, '&lt;')     
        .replace(/>/g, '&gt;')     
        .replace(/"/g, '&quot;')     
        .replace(/'/g, '&#039;'); 
};
var complie = function (str) {   // 模板是換的   
    var tpl = str.replace(/<%=([\s\S]+?)%>/g, function (match, code) {      
        return "' +  escape(" + code + ");";   
    });  
    tpl = "tpl = '" + tpl + "";   
    tpl = 'var tpl = "";\nwith (obj) {' + tpl + '}\nreturn tpl;'; 
    return new Function('obj','escape', tpl); 
};

模板邏輯
在視圖上我們還是會有一些邏輯來控制頁面最終的渲染,我們希望在模版中有一些簡單的JS可以使用:

<% if (user) { %>
    <h2><%= user.name %></h2>
<%} else {%> 
    <h2> 名用戶</h2>
<% } %>

它編譯成函數應該是這樣的:

function (obj, escape) { 
    var tpl = "";
    with (obj) {
        if (user) {
            tpl += "<h2>" + escape(user.name) + "</h2>";
        } else {
            tpl += "<h2> 名用戶</h2>";
        } 
    }
    return tpl; 
}

改進一下我們的模版引擎:

var complie = function (str) {
    var tpl = str.replace(/\n/g, '\\n') // 將換行符 換 
        .replace(/<%=([\s\S]+?)%>/g, function (match, code) {
            // 轉義
            return "' + escape(" + code + ");+'"; 
        })
        .replace(/<%([\s\S]+?)%>/g, function (match, code) {
            return "';\n" + code + "\ntpl += '"; 
        })
        .replace(/\'\n/g, '\'')
        .replace(/\n\'/gm, '\'');
    tpl = "tpl = '" + tpl + "';";
    // 轉換空行 3 
    tpl = tpl.replace(/''/g, '\'\\n\'');
    tpl = 'var tpl = "";\nwith (obj || {}) {\n' + tpl + '\n}\nreturn tpl;';
    return new Function('obj', 'escape', tpl);
};

模板:

<% if (obj.user) { %>
    <h2><%= user.name %></h2>
<%} else {%> 
    <h2>anonymou</h2>
<% } %>
<% for (var i = 0; i < items.length; i++) { %>
    <%var item = items[i];%>
    <%console.log(item)%>
    <p><%=item.name%></p>
<% } %>

緩存
之前我們提到了從模板編譯過來的函數應該緩存:

res.render = function (view, data) {   
    if (!cache[view]) {
        var text;
        try {
            text = fs.readFileSync(view, 'utf-8'); 
        } catch (e) {
            res.writeHead(500, {'Content-Type': 'text/html'}); 
            res.end('模板文件錯誤');
            return;
        }
        cache[view] = complie(text,escape);
    }
    var complied = cache[view];
    res.writeHead(200, {'Content-Type': 'text/html'});
    var html = render(complied, data);   
    res.end(html);  
}; 

我們每次都檢查一下是不是在緩存裏已經有請求視圖對應的函數了,如果有就直接取出來。
子模版
有時有的模板裏會有重複的部分,所以子模板是比較好的解決辦法:

<ul>
    <% users.forEach(function(user){ %>
        <% include user/show %> <% }) %>
</ul>

我們需要將include後面的路徑讀出來,將模板拼好,然後再生成函數。這樣我們就需要一個預編譯函數:

var files = {};
var preComplie = function (str) {
    var replaced = str.replace(/<%\s+(include.*)\s+%>/g, function (match, code) {
        var partial = code.split(/\s/)[1]; 
        console.log(partial);
        if (!files[partial]) {
            files[partial] = fs.readFileSync(__dirname+partial, 'utf-8'); 
        }
        return files[partial]; 
    });
    //嵌套替換
    if (str.match(/<%\s+(include.*)\s+%>/)) {
        return preComplie(replaced); 
    } else {
        return replaced; 
    }
};

這個函數就把子模板讀出來放在主模板裏咯。
模板性能
首先一定要做的就是緩存模板文件和模板函數。這兩部做好了除了第一次訪問,性能就只和你的模板函數的效率有關了。所以模板引擎的優化標準應該是最終生成的函數的執行效率最高,而不是生成函數的效率最高。

Bigpipe

當頁面中的數據較多時,我們以前都是等待所有數據讀完,拼好頁面,返回給瀏覽器。但是這樣做的問題就是在所有數據返回前,瀏覽器會是一片空白。
Bigpipe的解決方案是,先向用戶輸出沒有數據的佈局,再將每個部分逐漸輸出到前端補空白。現在的京東,淘寶的首頁都是這樣做的。
這是一個需要前後端配合的優化技術。在這裏就不介紹具體實現了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章