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发向服务器

服务器向客户端发送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的解决方案是,先向用户输出没有数据的布局,再将每个部分逐渐输出到前端补空白。现在的京东,淘宝的首页都是这样做的。
这是一个需要前后端配合的优化技术。在这里就不介绍具体实现了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容