集成Nunjucks
集成Nunjucks实际上也是编写一个middleware,这个middleware的作用是给ctx对象绑定一个render(view, model)的方法,这样,后面的Controller就可以调用这个方法来渲染模板了。
我们创建一个templating.js来实现这个middleware:
const nunjucks = require('nunjucks'); function createEnv(path, opts) { var autoescape = opts.autoescape === undefined ? true : opts.autoescape, noCache = opts.noCache || false, watch = opts.watch || false, throwOnUndefined = opts.throwOnUndefined || false, env = new nunjucks.Environment( new nunjucks.FileSystemLoader(path || 'views', { noCache: noCache, watch: watch, }), { autoescape: autoescape, throwOnUndefined: throwOnUndefined }); if (opts.filters) { for (var f in opts.filters) { env.addFilter(f, opts.filters[f]); } } return env; } function templating(path, opts) { // 创建Nunjucks的env对象: var env = createEnv(path, opts); return async (ctx, next) => { // 给ctx绑定render函数: ctx.render = function (view, model) { // 把render后的内容赋值给response.body: ctx.response.body = env.render(view, Object.assign({}, ctx.state || {}, model || {})); // 设置Content-Type: ctx.response.type = 'text/html'; }; // 继续处理请求: await next(); }; } module.exports = templating;
注意到createEnv()函数和前面使用Nunjucks时编写的函数是一模一样的。我们主要关心tempating()函数,它会返回一个middleware,在这个middleware中,我们只给ctx“安装”了一个render()函数,其他什么事情也没干,就继续调用下一个middleware。
使用的时候,我们在app.js添加如下代码:
const isProduction = process.env.NODE_ENV === 'production'; app.use(templating('views', { noCache: !isProduction, watch: !isProduction }));
这里我们定义了一个常量isProduction,它判断当前环境是否是production环境。如果是,就使用缓存,如果不是,就关闭缓存。在开发环境下,关闭缓存后,我们修改View,可以直接刷新浏览器看到效果,否则,每次修改都必须重启Node程序,会极大地降低开发效率。
Node.js在全局变量process中定义了一个环境变量env.NODE_ENV,为什么要使用该环境变量?因为我们在开发的时候,环境变量应该设置为'development',而部署到服务器时,环境变量应该设置为'production'。在编写代码的时候,要根据当前环境作不同的判断。
注意:生产环境上必须配置环境变量NODE_ENV = 'production',而开发环境不需要配置,实际上NODE_ENV可能是undefined,所以判断的时候,不要用NODE_ENV === 'development'。
类似的,我们在使用上面编写的处理静态文件的middleware时,也可以根据环境变量判断:
if (! isProduction) { let staticFiles = require('./static-files'); app.use(staticFiles('/static/', __dirname + '/static')); }
这是因为在生产环境下,静态文件是由部署在最前面的反向代理服务器(如Nginx)处理的,Node程序不需要处理静态文件。而在开发环境下,我们希望koa能顺带处理静态文件,否则,就必须手动配置一个反向代理服务器,这样会导致开发环境非常复杂。
编写View
在编写View的时候,非常有必要先编写一个base.html作为骨架,其他模板都继承自base.html,这样,才能大大减少重复工作。
编写HTML不在本教程的讨论范围之内。这里我们参考Bootstrap的官网简单编写了base.html。
运行
一切顺利的话,这个view-koa工程应该可以顺利运行。运行前,我们再检查一下app.js里的middleware的顺序:
第一个middleware是记录URL以及页面执行时间:
app.use(async (ctx, next) => { console.log(`Process ctx.request.method{ctx.request.method} {ctx.request.url}...`); var start = new Date().getTime(), execTime; await next(); execTime = new Date().getTime() - start; ctx.response.set('X-Response-Time', `${execTime}ms`); });
第二个middleware处理静态文件:
if (! isProduction) { let staticFiles = require('./static-files'); app.use(staticFiles('/static/', __dirname + '/static')); }
第三个middleware解析POST请求:
app.use(bodyParser());
第四个middleware负责给ctx加上render()来使用Nunjucks:
app.use(templating('view', { noCache: !isProduction, watch: !isProduction }));
最后一个middleware处理URL路由:
app.use(controller());
现在,在VS Code中运行代码,不出意外的话,在浏览器输入localhost:3000/,可以看到首页内容:
直接在首页登录,如果输入正确的Email和Password,进入登录成功的页面:
如果输入的Email和Password不正确,进入登录失败的页面:
怎么判断正确的Email和Password?目前我们在signin.js中是这么判断的:
if (email === 'admin@example.com' && password === '123456') { ... }
当然,真实的网站会根据用户输入的Email和Password去数据库查询并判断登录是否成功,不过这需要涉及到Node.js环境如何操作数据库,我们后面再讨论。
扩展
注意到ctx.render内部渲染模板时,Model对象并不是传入的model变量,而是:
if (email === 'admin@example.com' && password === '123456') { ... }
这个小技巧是为了扩展。
首先,model || {}确保了即使传入undefined,model也会变为默认值{}。Object.assign()会把除第一个参数外的其他参数的所有属性复制到第一个参数中。第二个参数是ctx.state || {},这个目的是为了能把一些公共的变量放入ctx.state并传给View。
例如,某个middleware负责检查用户权限,它可以把当前用户放入ctx.state中:
app.use(async (ctx, next) => { var user = tryGetUserFromCookie(ctx.request); if (user) { ctx.state.user = user; await next(); } else { ctx.response.status = 403; } });