koa2开发入门

一.koa2入门javascript

1.建立koa2工程

首先,咱们建立一个目录hello-koa并做为工程目录用VS Code打开。而后,咱们建立app.js,输入如下代码:css

// 导入koa,和koa 1.x不一样,在koa2中,咱们导入的是一个class,所以用大写的Koa表示:
const Koa = require('koa');

// 建立一个Koa对象表示web app自己:
const app = new Koa();

// 对于任何请求,app将调用该异步函数处理请求:
app.use(async (ctx, next) => {
    await next();
    ctx.response.type = 'text/html';
    ctx.response.body = '<h1>Hello, koa2!</h1>';
});

// 在端口3000监听:
app.listen(3000);
console.log('app started at port 3000...');

对于每个http请求,koa将调用咱们传入的异步函数来处理:html

async (ctx, next) => {
    await next();
    // 设置response的Content-Type:
    ctx.response.type = 'text/html';
    // 设置response的内容:
    ctx.response.body = '<h1>Hello, koa2!</h1>';
}

其中,参数ctx是由koa传入的封装了request和response的变量,咱们能够经过它访问request和response,next是koa传入的将要处理的下一个异步函数。java

上面的异步函数中,咱们首先用await next();处理下一个异步函数,而后,设置response的Content-Type和内容。node

async标记的函数称为异步函数,在异步函数中,能够用await调用另外一个异步函数,这两个关键字将在ES7中引入。git

如今咱们遇到第一个问题:koa这个包怎么装,app.js才能正常导入它?github

方法一:能够用npm命令直接安装koa。先打开命令提示符,务必把当前目录切换到hello-koa这个目录,而后执行命令:web

C:\...\hello-koa> npm install koa@2.0.0

npm会把koa2以及koa2依赖的全部包所有安装到当前目录的node_modules目录下。数据库

方法二:在hello-koa这个目录下建立一个package.json,这个文件描述了咱们的hello-koa工程会用到哪些包。完整的文件内容以下:npm

{
    "name": "hello-koa2",
    "version": "1.0.0",
    "description": "Hello Koa 2 example with async",
    "main": "app.js",
    "scripts": {
        "start": "node app.js"
    },
    "keywords": [
        "koa",
        "async"
    ],
    "author": "Michael Liao",
    "license": "Apache-2.0",
    "repository": {
        "type": "git",
        "url": "https://github.com/michaelliao/learn-javascript.git"
    },
    "dependencies": {
        "koa": "2.0.0"
    }
}

其中,dependencies描述了咱们的工程依赖的包以及版本号。其余字段均用来描述项目信息,可任意填写。

而后,咱们在hello-koa目录下执行npm install就能够把所需包以及依赖包一次性所有装好:

C:\...\hello-koa> npm install

很显然,第二个方法更靠谱,由于咱们只要在package.json正确设置了依赖,npm就会把全部用到的包都装好。

注意,任什么时候候均可以直接删除整个node_modules目录,由于用npm install命令能够完整地从新下载全部依赖。而且,这个目录不该该被放入版本控制中。

如今,咱们的工程结构以下:

hello-koa/
|
+- .vscode/
|  |
|  +- launch.json <-- VSCode 配置文件
|
+- app.js <-- 使用koa的js
|
+- package.json <-- 项目描述文件
|
+- node_modules/ <-- npm安装的全部依赖包

紧接着,咱们在package.json中添加依赖包:

"dependencies": {
    "koa": "2.0.0"
}

而后使用npm install命令安装后,在VS Code中执行app.js,调试控制台输出以下:

node --debug-brk=40645 --nolazy app.js 
Debugger listening on port 40645
app started at port 3000...

咱们打开浏览器,输入http://localhost:3000,便可看到效果:

koa-browser

还能够直接用命令node app.js在命令行启动程序,或者用npm start启动。npm start命令会让npm执行定义在package.json文件中的start对应命令:

"scripts": {
    "start": "node app.js"
}

2.koa middleware

让咱们再仔细看看koa的执行逻辑。核心代码是:

app.use(async (ctx, next) => {
    await next();
    ctx.response.type = 'text/html';
    ctx.response.body = '<h1>Hello, koa2!</h1>';
});

每收到一个http请求,koa就会调用经过app.use()注册的async函数,并传入ctxnext参数。

咱们能够对ctx操做,并设置返回内容。可是为何要调用await next()

缘由是koa把不少async函数组成一个处理链,每一个async函数均可以作一些本身的事情,而后用await next()来调用下一个async函数。咱们把每一个async函数称为middleware,这些middleware能够组合起来,完成不少有用的功能。

例如,能够用如下3个middleware组成处理链,依次打印日志,记录处理时间,输出HTML:

// 导入koa,和koa 1.x不一样,在koa2中,咱们导入的是一个class,所以用大写的Koa表示:
const Koa = require('koa');

// 建立一个Koa对象表示web app自己:
const app = new Koa();

app.use(async (ctx, next) => {
    console.log(`${ctx.request.method} ${ctx.request.url}`); // 打印URL
    await next(); // 调用下一个middleware
});

app.use(async (ctx, next) => {
    const start = new Date().getTime(); // 当前时间
    await next(); // 调用下一个middleware
    const ms = new Date().getTime() - start; // 耗费时间
    console.log(`Time: ${ms}ms`); // 打印耗费时间
});

app.use(async (ctx, next) => {
    await next();
    ctx.response.type = 'text/html';
    ctx.response.body = '<h1>Hello, koa2!</h1>';
});

// 在端口3000监听:
app.listen(3000);
console.log('app started at port 3000...');

middleware的顺序很重要,也就是调用app.use()的顺序决定了middleware的顺序。

此外,若是一个middleware没有调用await next(),会怎么办?答案是后续的middleware将再也不执行了。这种状况也很常见,例如,一个检测用户权限的middleware能够决定是否继续处理请求,仍是直接返回403错误:

app.use(async (ctx, next) => {
    if (await checkUserPermission(ctx)) {
        await next();
    } else {
        ctx.response.status = 403;
    }
});

理解了middleware,咱们就已经会用koa了!

最后注意ctx对象有一些简写的方法,例如ctx.url至关于ctx.request.urlctx.type至关于ctx.response.type

  

二.处理URL

 在hello-koa工程中,咱们处理http请求一概返回相同的HTML,这样虽然很是简单,可是用浏览器一测,随便输入任何URL都会返回相同的网页。

正常状况下,咱们应该对不一样的URL调用不一样的处理函数,这样才能返回不一样的结果。例如像这样写:

app.use(async (ctx, next) => {
    if (ctx.request.path === '/') {
        ctx.response.body = 'index page';
    } else {
        await next();
    }
});

app.use(async (ctx, next) => {
    if (ctx.request.path === '/test') {
        ctx.response.body = 'TEST page';
    } else {
        await next();
    }
});

app.use(async (ctx, next) => {
    if (ctx.request.path === '/error') {
        ctx.response.body = 'ERROR page';
    } else {
        await next();
    }
});

这么写是能够运行的,可是好像有点蠢。

应该有一个能集中处理URL的middleware,它根据不一样的URL调用不一样的处理函数,这样,咱们才能专心为每一个URL编写处理函数。

1.koa-router

为了处理URL,咱们须要引入koa-router这个middleware,让它负责处理URL映射。

咱们把上一节的hello-koa工程复制一份,重命名为url-koa

先在package.json中添加依赖项:

"koa-router": "7.0.0"

而后用npm install安装。

接下来,咱们修改app.js,使用koa-router来处理URL:

const Koa = require('koa');

// 注意require('koa-router')返回的是函数:
const router = require('koa-router')();

const app = new Koa();

// log request URL:
app.use(async (ctx, next) => {
    console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
    await next();
});

// add url-route:
router.get('/hello/:name', async (ctx, next) => {
    var name = ctx.params.name;
    ctx.response.body = `<h1>Hello, ${name}!</h1>`;
});

router.get('/', async (ctx, next) => {
    ctx.response.body = '<h1>Index</h1>';
});

// add router middleware:
app.use(router.routes());

app.listen(3000);
console.log('app started at port 3000...');

注意导入koa-router的语句最后的()是函数调用:

const router = require('koa-router')();

至关于:

const fn_router = require('koa-router');
const router = fn_router();

而后,咱们使用router.get('/path', async fn)来注册一个GET请求。能够在请求路径中使用带变量的/hello/:name,变量能够经过ctx.params.name访问。

再运行app.js,咱们就能够测试不一样的URL:

输入首页:http://localhost:3000/

url-index

输入:http://localhost:3000/hello/koa

url-hello

2.处理post请求

router.get('/path', async fn)处理的是get请求。若是要处理post请求,能够用router.post('/path', async fn)

用post请求处理URL时,咱们会遇到一个问题:post请求一般会发送一个表单,或者JSON,它做为request的body发送,但不管是Node.js提供的原始request对象,仍是koa提供的request对象,都不提供解析request的body的功能!

因此,咱们又须要引入另外一个middleware来解析原始request请求,而后,把解析后的参数,绑定到ctx.request.body中。

koa-bodyparser就是用来干这个活的。

咱们在package.json中添加依赖项:

"koa-bodyparser": "3.2.0"

而后使用npm install安装。

下面,修改app.js,引入koa-bodyparser

const bodyParser = require('koa-bodyparser');

在合适的位置加上:

app.use(bodyParser());

因为middleware的顺序很重要,这个koa-bodyparser必须在router以前被注册到app对象上。

如今咱们就能够处理post请求了。写一个简单的登陆表单:

const Koa = require('koa');

// 注意require('koa-router')返回的是函数:
const router = require('koa-router')();
const bodyParser = require('koa-bodyparser');

const app = new Koa();

// log request URL:
app.use(async (ctx, next) => {
    console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
    await next();
});

// add url-route:
router.get('/hello/:name', async (ctx, next) => {
    var name = ctx.params.name;
    ctx.response.body = `<h1>Hello, ${name}!</h1>`;
});

router.get('/', async (ctx, next) => {
    ctx.response.body = `<h1>Index</h1>
        <form action="/signin" method="post">
            <p>Name: <input name="name" value="koa"></p>
            <p>Password: <input name="password" type="password"></p>
            <p><input type="submit" value="Submit"></p>
        </form>`;
});

router.post('/signin', async (ctx, next) => {
    var
        name = ctx.request.body.name || '',
        password = ctx.request.body.password || '';
    console.log(`signin with name: ${name}, password: ${password}`);
    if (name === 'koa' && password === '12345') {
        ctx.response.body = `<h1>Welcome, ${name}!</h1>`;
    } else {
        ctx.response.body = `<h1>Login failed!</h1>
        <p><a href="/">Try again</a></p>`;
    }
});

router.get('/', async (ctx, next) => {
    ctx.response.body = '<h1>Index</h1>';
});

// add router middleware:
app.use(bodyParser());
app.use(router.routes());

app.listen(3000);
console.log('app started at port 3000...');

注意到咱们用var name = ctx.request.body.name || ''拿到表单的name字段,若是该字段不存在,默认值设置为''

相似的,put、delete、head请求也能够由router处理。

3.重构

如今,咱们已经能够处理不一样的URL了,可是看看app.js,总以为仍是有点不对劲。

全部的URL处理函数都放到app.js里显得很乱,并且,每加一个URL,就须要修改app.js。随着URL愈来愈多,app.js就会愈来愈长。

若是能把URL处理函数集中到某个js文件,或者某几个js文件中就行了,而后让app.js自动导入全部处理URL的函数。这样,代码一分离,逻辑就显得清楚了。最好是这样:

url2-koa/
|
+- .vscode/
|  |
|  +- launch.json <-- VSCode 配置文件
|
+- controllers/
|  |
|  +- login.js <-- 处理login相关URL
|  |
|  +- users.js <-- 处理用户管理相关URL
|
+- app.js <-- 使用koa的js
|
+- package.json <-- 项目描述文件
|
+- node_modules/ <-- npm安装的全部依赖包

因而咱们把url-koa复制一份,重命名为url2-koa,准备重构这个项目。

咱们先在controllers目录下编写index.js

var fn_index = async (ctx, next) => {
    ctx.response.body = `<h1>Index</h1>
        <form action="/signin" method="post">
            <p>Name: <input name="name" value="koa"></p>
            <p>Password: <input name="password" type="password"></p>
            <p><input type="submit" value="Submit"></p>
        </form>`;
};

var fn_signin = async (ctx, next) => {
    var
        name = ctx.request.body.name || '',
        password = ctx.request.body.password || '';
    console.log(`signin with name: ${name}, password: ${password}`);
    if (name === 'koa' && password === '12345') {
        ctx.response.body = `<h1>Welcome, ${name}!</h1>`;
    } else {
        ctx.response.body = `<h1>Login failed!</h1>
        <p><a href="/">Try again</a></p>`;
    }
};

module.exports = {
    'GET /': fn_index,
    'POST /signin': fn_signin
};

这个index.js经过module.exports把两个URL处理函数暴露出来。

相似的,hello.js把一个URL处理函数暴露出来:

var fn_hello = async (ctx, next) => {
    var name = ctx.params.name;
    ctx.response.body = `<h1>Hello, ${name}!</h1>`;
};

module.exports = {
    'GET /hello/:name': fn_hello
};

如今,咱们修改app.js,让它自动扫描controllers目录,找到全部js文件,导入,而后注册每一个URL:

// 先导入fs模块,而后用readdirSync列出文件
// 这里能够用sync是由于启动时只运行一次,不存在性能问题:
var files = fs.readdirSync(__dirname + '/controllers');

// 过滤出.js文件:
var js_files = files.filter((f)=>{
    return f.endsWith('.js');
});

// 处理每一个js文件:
for (var f of js_files) {
    console.log(`process controller: ${f}...`);
    // 导入js文件:
    let mapping = require(__dirname + '/controllers/' + f);
    for (var url in mapping) {
        if (url.startsWith('GET ')) {
            // 若是url相似"GET xxx":
            var path = url.substring(4);
            router.get(path, mapping[url]);
            console.log(`register URL mapping: GET ${path}`);
        } else if (url.startsWith('POST ')) {
            // 若是url相似"POST xxx":
            var path = url.substring(5);
            router.post(path, mapping[url]);
            console.log(`register URL mapping: POST ${path}`);
        } else {
            // 无效的URL:
            console.log(`invalid URL: ${url}`);
        }
    }
}

若是上面的大段代码看起来仍是有点费劲,那就把它拆成更小单元的函数:

function addMapping(router, mapping) {
    for (var url in mapping) {
        if (url.startsWith('GET ')) {
            var path = url.substring(4);
            router.get(path, mapping[url]);
            console.log(`register URL mapping: GET ${path}`);
        } else if (url.startsWith('POST ')) {
            var path = url.substring(5);
            router.post(path, mapping[url]);
            console.log(`register URL mapping: POST ${path}`);
        } else {
            console.log(`invalid URL: ${url}`);
        }
    }
}

function addControllers(router) {
    var files = fs.readdirSync(__dirname + '/controllers');
    var js_files = files.filter((f) => {
        return f.endsWith('.js');
    });

    for (var f of js_files) {
        console.log(`process controller: ${f}...`);
        let mapping = require(__dirname + '/controllers/' + f);
        addMapping(router, mapping);
    }
}

addControllers(router);

确保每一个函数功能很是简单,一眼能看明白,是代码可维护的关键。

4.Controller Middleware

最后,咱们把扫描controllers目录和建立router的代码从app.js中提取出来,做为一个简单的middleware使用,命名为controller.js

const fs = require('fs');

function addMapping(router, mapping) {
    ...
}

function addControllers(router, dir) {
    ...
}

module.exports = function (dir) {
    let
        controllers_dir = dir || 'controllers', // 若是不传参数,扫描目录默认为'controllers'
        router = require('koa-router')();
    addControllers(router, controllers_dir);
    return router.routes();
};

完整内容以下:

const fs = require('fs');

function addMapping(router, mapping) {
    for (var url in mapping) {
        if (url.startsWith('GET ')) {
            var path = url.substring(4);
            router.get(path, mapping[url]);
            console.log(`register URL mapping: GET ${path}`);
        } else if (url.startsWith('POST ')) {
            var path = url.substring(5);
            router.post(path, mapping[url]);
            console.log(`register URL mapping: POST ${path}`);
        } else {
            console.log(`invalid URL: ${url}`);
        }
    }
}

function addControllers(router) {
    var files = fs.readdirSync(__dirname + '/controllers');
    var js_files = files.filter((f) => {
        return f.endsWith('.js');
    });

    for (var f of js_files) {
        console.log(`process controller: ${f}...`);
        let mapping = require(__dirname + '/controllers/' + f);
        addMapping(router, mapping);
    }
}

module.exports = function (dir) {
    let
        controllers_dir = dir || 'controllers', // 若是不传参数,扫描目录默认为'controllers'
        router = require('koa-router')();
    addControllers(router, controllers_dir);
    return router.routes();
};

这样一来,咱们在app.js的代码又简化了:

...

// 导入controller middleware:
const controller = require('./controller');

...

// 使用middleware:
app.use(controller());

...

完整内容以下所示:

// 导入koa,和koa 1.x不一样,在koa2中,咱们导入的是一个class,所以用大写的Koa表示:
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');

// 建立一个Koa对象表示web app自己:
const app = new Koa();

// 导入controller middleware:
const controller = require('./controller');

// 使用middleware:
app.use(bodyParser());
app.use(controller());

// 在端口3000监听:
app.listen(3000);
console.log('app started at port 3000...');

通过从新整理后的工程url2-koa目前具有很是好的模块化,全部处理URL的函数按功能组存放在controllers目录,从此咱们也只须要不断往这个目录下加东西就能够了,app.js保持不变。

 

三.使用Nunjucks

Nunjucks是什么东东?其实它是一个模板引擎。

那什么是模板引擎?

模板引擎就是基于模板配合数据构造出字符串输出的一个组件。好比下面的函数就是一个模板引擎:

function examResult (data) {
    return `${data.name}同窗一年级期末考试语文${data.chinese}分,数学${data.math}分,位于年级第${data.ranking}名。`
}

若是咱们输入数据以下:

examResult({
    name: '小明',
    chinese: 78,
    math: 87,
    ranking: 999
});

该模板引擎把模板字符串里面对应的变量替换之后,就能够获得如下输出:

小明同窗一年级期末考试语文78分,数学87分,位于年级第999名。

模板引擎最多见的输出就是输出网页,也就是HTML文本。固然,也能够输出任意格式的文本,好比Text,XML,Markdown等等。

有同窗要问了:既然JavaScript的模板字符串能够实现模板功能,那为何咱们还须要另外的模板引擎?

由于JavaScript的模板字符串必须写在JavaScript代码中,要想写出新浪首页这样复杂的页面,是很是困难的。

输出HTML有几个特别重要的问题须要考虑:

a.转义

对特殊字符要转义,避免受到XSS攻击。好比,若是变量name的值不是小明,而是小明<script>...</script>,模板引擎输出的HTML到了浏览器,就会自动执行恶意JavaScript代码。

b.格式化

对不一样类型的变量要格式化,好比,货币须要变成12,345.00这样的格式,日期须要变成2016-01-01这样的格式。

c.简单逻辑

模板还须要能执行一些简单逻辑,好比,要按条件输出内容,须要if实现以下输出:

{{ name }}同窗,
{% if score >= 90 %}
    成绩优秀,应该奖励
{% elif score >=60 %}
    成绩良好,继续努力
{% else %}
    不及格,建议回家打屁股
{% endif %}

因此,咱们须要一个功能强大的模板引擎,来完成页面输出的功能。

1.Nunjucks

咱们选择Nunjucks做为模板引擎。Nunjucks是Mozilla开发的一个纯JavaScript编写的模板引擎,既能够用在Node环境下,又能够运行在浏览器端。可是,主要仍是运行在Node环境下,由于浏览器端有更好的模板解决方案,例如MVVM框架。

若是你使用过Python的模板引擎jinja2,那么使用Nunjucks就很是简单,二者的语法几乎是如出一辙的,由于Nunjucks就是用JavaScript从新实现了jinjia2。

从上面的例子咱们能够看到,虽然模板引擎内部可能很是复杂,可是使用一个模板引擎是很是简单的,由于本质上咱们只须要构造这样一个函数:

function render(view, model) {
    // TODO:...
}

其中,view是模板的名称(又称为视图),由于可能存在多个模板,须要选择其中一个。model就是数据,在JavaScript中,它就是一个简单的Object。render函数返回一个字符串,就是模板的输出。

下面咱们来使用Nunjucks这个模板引擎来编写几个HTML模板,而且用实际数据来渲染模板并得到最终的HTML输出。

咱们建立一个use-nunjucks的VS Code工程结构以下:

use-nunjucks/
|
+- .vscode/
|  |
|  +- launch.json <-- VSCode 配置文件
|
+- views/
|  |
|  +- hello.html <-- HTML模板文件
|
+- app.js <-- 入口js
|
+- package.json <-- 项目描述文件
|
+- node_modules/ <-- npm安装的全部依赖包

其中,模板文件存放在views目录中。

咱们先在package.json中添加nunjucks的依赖:

"nunjucks": "2.4.2"

注意,模板引擎是能够独立使用的,并不须要依赖koa。用npm install安装全部依赖包。

紧接着,咱们要编写使用Nunjucks的函数render。怎么写?方法是查看Nunjucks的官方文档,仔细阅读后,在app.js中编写代码以下:

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('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;
}

var env = createEnv('views', {
    watch: true,
    filters: {
        hex: function (n) {
            return '0x' + n.toString(16);
        }
    }
});

var s = env.render('hello.html', { name: '小明' });
console.log(s);

var s2 = env.render('hello.html', { name: '<script>alert("小明")</script>' });
console.log(s2);

var s3 = env.render('list.html', { fruits: ['apple', 'orange', 'banana'] });
console.log(s3);

console.log(env.render('extend.html', {
    header: 'Hello',
    body: 'bla bla bla...'
}));

变量env就表示Nunjucks模板引擎对象,它有一个render(view, model)方法,正好传入viewmodel两个参数,并返回字符串。

建立env须要的参数能够查看文档获知。咱们用autoescape = opts.autoescape && true这样的代码给每一个参数加上默认值,最后使用new nunjucks.FileSystemLoader('views')建立一个文件系统加载器,从views目录读取模板。

咱们编写一个hello.html模板文件,放到views目录下,内容以下:

<h1>Hello {{ name }}</h1>

而后,咱们就能够用下面的代码来渲染这个模板:

var s = env.render('hello.html', { name: '小明' });
console.log(s);

得到输出以下:

<h1>Hello 小明</h1>

咋一看,这和使用JavaScript模板字符串没啥区别嘛。不过,试试:

var s = env.render('hello.html', { name: '<script>alert("小明")</script>' });
console.log(s);

得到输出以下:

<h1>Hello &lt;script&gt;alert("小明")&lt;/script&gt;</h1>

这样就避免了输出恶意脚本。

此外,可使用Nunjucks提供的功能强大的tag,编写条件判断、循环等功能,例如:

<!-- 循环输出名字 -->
<body>
    <h3>Fruits List</h3>
    {% for f in fruits %}
    <p>{{ f }}</p>
    {% endfor %}
</body>

Nunjucks模板引擎最强大的功能在于模板的继承。仔细观察各类网站能够发现,网站的结构其实是相似的,头部、尾部都是固定格式,只有中间页面部份内容不一样。若是每一个模板都重复头尾,一旦要修改头部或尾部,那就须要改动全部模板。

更好的方式是使用继承。先定义一个基本的网页框架base.html

<html><body>
{% block header %} <h3>Unnamed</h3> {% endblock %}
{% block body %} <div>No body</div> {% endblock %}
{% block footer %} <div>copyright</div> {% endblock %}
</body>

base.html定义了三个可编辑的块,分别命名为headerbodyfooter。子模板能够有选择地对块进行从新定义:

{% extends 'base.html' %}

{% block header %}<h1>{{ header }}</h1>{% endblock %}

{% block body %}<p>{{ body }}</p>{% endblock %}

而后,咱们对子模板进行渲染:

console.log(env.render('extend.html', {
    header: 'Hello',
    body: 'bla bla bla...'
}));

输出HTML以下:

<html><body>
<h1>Hello</h1>
<p>bla bla bla...</p>
<div>copyright</div> <-- footer没有重定义,因此仍使用父模板的内容
</body>

2.性能

最后咱们要考虑一下Nunjucks的性能。

对于模板渲染自己来讲,速度是很是很是快的,由于就是拼字符串嘛,纯CPU操做。

性能问题主要出如今从文件读取模板内容这一步。这是一个IO操做,在Node.js环境中,咱们知道,单线程的JavaScript最不能忍受的就是同步IO,但Nunjucks默认就使用同步IO读取模板文件。

好消息是Nunjucks会缓存已读取的文件内容,也就是说,模板文件最多读取一次,就会放在内存中,后面的请求是不会再次读取文件的,只要咱们指定了noCache: false这个参数。

在开发环境下,能够关闭cache,这样每次从新加载模板,便于实时修改模板。在生产环境下,必定要打开cache,这样就不会有性能问题。

Nunjucks也提供了异步读取的方式,可是这样写起来很麻烦,有简单的写法咱们就不会考虑复杂的写法。保持代码简单是可维护性的关键。

 

四.使用MVC

1.MVC

咱们已经能够用koa处理不一样的URL,还能够用Nunjucks渲染模板。如今,是时候把这二者结合起来了!

当用户经过浏览器请求一个URL时,koa将调用某个异步函数处理该URL。在这个异步函数内部,咱们用一行代码:

ctx.render('home.html', { name: 'Michael' });

经过Nunjucks把数据用指定的模板渲染成HTML,而后输出给浏览器,用户就能够看到渲染后的页面了:

mvc

这就是传说中的MVC:Model-View-Controller,中文名“模型-视图-控制器”。

异步函数是C:Controller,Controller负责业务逻辑,好比检查用户名是否存在,取出用户信息等等;

包含变量{{ name }}的模板就是V:View,View负责显示逻辑,经过简单地替换一些变量,View最终输出的就是用户看到的HTML。

MVC中的Model在哪?Model是用来传给View的,这样View在替换变量的时候,就能够从Model中取出相应的数据。

上面的例子中,Model就是一个JavaScript对象:

{ name: 'Michael' }

下面,咱们根据原来的url2-koa建立工程view-koa,把koa二、Nunjucks整合起来,而后,把原来直接输出字符串的方式,改成ctx.render(view, model)的方式。

工程view-koa结构以下:

view-koa/
|
+- .vscode/
|  |
|  +- launch.json <-- VSCode 配置文件
|
+- controllers/ <-- Controller
|
+- views/ <-- html模板文件
|
+- static/ <-- 静态资源文件
|
+- controller.js <-- 扫描注册Controller
|
+- app.js <-- 使用koa的js
|
+- package.json <-- 项目描述文件
|
+- node_modules/ <-- npm安装的全部依赖包

package.json中,咱们将要用到的依赖包有:

"koa": "2.0.0",
"koa-bodyparser": "3.2.0",
"koa-router": "7.0.0",
"nunjucks": "2.4.2",
"mime": "1.3.4",
"mz": "2.4.0"

先用npm install安装依赖包。

而后,咱们准备编写如下两个Controller:

2.处理首页 GET /

咱们定义一个async函数处理首页URL/

async (ctx, next) => {
    ctx.render('index.html', {
        title: 'Welcome'
    });
}

注意到koa并无在ctx对象上提供render方法,这里咱们假设应该这么使用,这样,咱们在编写Controller的时候,最后一步调用ctx.render(view, model)就完成了页面输出。

3.处理登陆请求 POST /signin

咱们再定义一个async函数处理登陆请求/signin

async (ctx, next) => {
    var
        email = ctx.request.body.email || '',
        password = ctx.request.body.password || '';
    if (email === 'admin@example.com' && password === '123456') {
        // 登陆成功:
        ctx.render('signin-ok.html', {
            title: 'Sign In OK',
            name: 'Mr Node'
        });
    } else {
        // 登陆失败:
        ctx.render('signin-failed.html', {
            title: 'Sign In Failed'
        });
    }
}

因为登陆请求是一个POST,咱们就用ctx.request.body.<name>拿到POST请求的数据,并给一个默认值。

登陆成功时咱们用signin-ok.html渲染,登陆失败时咱们用signin-failed.html渲染,因此,咱们一共须要如下3个View:

  • index.html
  • signin-ok.html
  • signin-failed.html

4.编写View

在编写View的时候,咱们其实是在编写HTML页。为了让页面看起来美观大方,使用一个现成的CSS框架是很是有必要的。咱们用Bootstrap这个CSS框架。从首页下载zip包后解压,咱们把全部静态资源文件放到/static目录下:

view-koa/
|
+- static/
   |
   +- css/ <- 存放bootstrap.css等
   |
   +- fonts/ <- 存放字体文件
   |
   +- js/ <- 存放bootstrap.js等

这样咱们在编写HTML的时候,能够直接用Bootstrap的CSS,像这样:

<link rel="stylesheet" href="/static/css/bootstrap.css">

如今,在使用MVC以前,第一个问题来了,如何处理静态文件?

咱们把全部静态资源文件所有放入/static目录,目的就是能统一处理静态文件。在koa中,咱们须要编写一个middleware,处理以/static/开头的URL。

5.编写middleware

咱们来编写一个处理静态文件的middleware。编写middleware实际上一点也不复杂。咱们先建立一个static-files.js的文件,编写一个能处理静态文件的middleware:

const path = require('path');
const mime = require('mime');
const fs = require('mz/fs');

function staticFiles(url, dir) {
    return async (ctx, next) => {
        let rpath = ctx.request.path;
        console.log(rpath)
        console.log(url)
        console.log(dir)
        if (rpath.startsWith(url)) {
            let fp = path.join(dir, rpath.substring(url.length));
            console.log(fp)
            if (await fs.exists(fp)) {
                ctx.response.type = mime.lookup(rpath);
                ctx.response.body = await fs.readFile(fp);
            } else {
                ctx.response.status = 404;
            }
        } else {
            await next();
        }
    };
}

module.exports = staticFiles;

staticFiles是一个普通函数,它接收两个参数:URL前缀和一个目录,而后返回一个async函数。这个async函数会判断当前的URL是否以指定前缀开头,若是是,就把URL的路径视为文件,并发送文件内容。若是不是,这个async函数就不作任何事情,而是简单地调用await next()让下一个middleware去处理请求。

咱们使用了一个mz的包,并经过require('mz/fs');导入。mz提供的API和Node.js的fs模块彻底相同,但fs模块使用回调,而mz封装了fs对应的函数,并改成Promise。这样,咱们就能够很是简单的用await调用mz的函数,而不须要任何回调。

全部的第三方包均可以经过npm官网搜索并查看其文档:

https://www.npmjs.com/

最后,这个middleware使用起来也很简单,在app.js里加一行代码:

let staticFiles = require('./static-files');
app.use(staticFiles('/static/', __dirname + '/static'));

注意:也能够去npm搜索能用于koa2的处理静态文件的包并直接使用。

6.集成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, {
                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) {
    var env = createEnv(path, opts);
    return async (ctx, next) => {
        ctx.render = function (view, model) {
            ctx.response.body = env.render(view, Object.assign({}, ctx.state || {}, model || {}));
            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能顺带处理静态文件,不然,就必须手动配置一个反向代理服务器,这样会致使开发环境很是复杂。

7.编写View

在编写View的时候,很是有必要先编写一个base.html做为骨架,其余模板都继承自base.html,这样,才能大大减小重复工做。

编写HTML不在本教程的讨论范围以内。这里咱们参考Bootstrap的官网简单编写了base.html

8.运行

一切顺利的话,这个view-koa工程应该能够顺利运行。运行前,咱们再检查一下app.js里的middleware的顺序:

第一个middleware是记录URL以及页面执行时间:

app.use(async (ctx, next) => {
    console.log(`Process ${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/,能够看到首页内容:

koa-index

直接在首页登陆,若是输入正确的Email和Password,进入登陆成功的页面:

koa-signin-ok

若是输入的Email和Password不正确,进入登陆失败的页面:

koa-signin-failed

怎么判断正确的Email和Password?目前咱们在signin.js中是这么判断的:

if (email === 'admin@example.com' && password === '123456') {
    ...
}

固然,真实的网站会根据用户输入的Email和Password去数据库查询并判断登陆是否成功,不过这须要涉及到Node.js环境如何操做数据库,咱们后面再讨论。

9.扩展

注意到ctx.render内部渲染模板时,Model对象并非传入的model变量,而是:

Object.assign({}, ctx.state || {}, model || {})

这个小技巧是为了扩展。

首先,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;
    }
});

这样就没有必要在每一个Controller的async函数中都把user变量放入model中。

 

文章来源:廖雪峰的官方网站,全部的示例我都在本机运行过,能够到http://bijian1013.iteye.com/blog/2425085下载。

koa2的官方文档资料详见http://www.koacn.com/#contexthttps://koa.bootcss.com/#

相关文章
相关标签/搜索