Fork me on GitHub

细说koa2框架

文章概述

本篇文章介绍koa2框架的使用。

koa历史

  • 基于2.x

参考资料

安装koa

  1. npm安装koa到node_modules:
1
$ npm install koa --save
  1. js文件中引入:
1
const Koa = require('koa');

koa概述

koa是Express的下一代基于Node.js的web框架,目前有1.x和2.0两个版本,此处基于2.x版本;

异步概念

koa2完全使用Promise并配合async来实现异步, koa可以把很多async函数组成一个处理链;

异步执行流程

每收到一个http请求,koa就会调用通过app.use()注册的async函数,并传入ctx和next参数,async函数内部,用await next()来调用下一个async函数,async函数按顺序用app.use()注册,next函数也是按注册顺序调用,如果一个async函数内没有调用await next(),则后续的async函数将不再执行。

1
2
3
4
5
6
app.use(async (ctx, next) => {
await next();
var data = await doReadFile();
ctx.response.type = 'text/plain';
ctx.response.body = data;
});
1
2
3
4
5
6
7
8
9
【async异步函数参数】
- ctx:是由koa传入的封装了request和response的变量,可对ctx操作,设置返回内容;
----------------------------------
ctx.url相当于ctx.request.url;
ctx.type相当于ctx.response.type;
----------------------------------
- next:是koa传入的将要处理的下一个异步函数;
【wait】
- wait作用是等待出入的promise执行结束;
中间件

每个async函数称为middleware,这些middleware可以组合起来,完成很多有用的功能。

koa-bodyparser模块

详见:HTTP服务章节;

  • koa-bodyparser库用来解析原始request请求,然后,把解析后的参数,绑定到ctx.request.body中。
  • 在koa中,我们只需要给ctx.response.body赋值一个JavaScript对象,koa会自动把该对象序列化为JSON并输出到客户端。
  • 非常适合编写rest-api;

处理url

使用koa-router路由来集中处理url.

koa原生处理

koa原生api可以处理url,来响应不同的请求,如下,但是当支持的请求越多就会越混乱;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 导入koa
const Koa = require('koa');

// 创建一个Koa对象表示web app本身:
const app = new Koa();

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

// 在端口3000监听:
app.listen(3000);
console.log('server on http://localhost:3000/');

koa-router

为了处理URL,我们需要引入koa-router这个middleware,让它负责处理URL映射,这样开发者不用每次都去调用await next(),而只需要关心请求的处理。

get请求

使用router.get(‘/path’, async fn)来注册一个GET请求。

参数
  • 可以在请求路径中使用带变量的/hello/:name,变量可以通过ctx.params.name访问。
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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('server on http://localhost:3000/');
post请求
请求参数

用post请求处理URL时,post请求通常会发送一个表单,或者JSON,它作为request的body发送,但是Node.js原始request对象、koa提供的request对象,都不提供解析request的body的功能。

koa-bodyparser库用来解析原始request请求,然后,把解析后的参数,绑定到ctx.request.body中。

  1. 安装koa-bodyparser库:
1
$ npm install --save koa-bodyparser
  1. 导入,在router.routes()之前调用:
1
2
3
4
5
6
const bodyParser = require('koa-bodyparser');
//...
const app = new Koa();
//...
app.use(bodyParser());
app.use(router.routes());
示例

一个post请求登录的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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();
});

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>`;
}
});

app.use(bodyParser());
app.use(router.routes());

app.listen(3000);
console.log('server on http://localhost:3000/');

HTTP-API封装

  1. 创建controller文件夹,用来处理请求业务,包含hell.js和index.js文件;
  • hello.js文件,处理hello页面相关请求:
1
2
3
4
5
6
7
8
let fn_hello = async (ctx, next) => {
let name = ctx.params.name;
ctx.response.body = `<h1>Hello, ${name}!</h1>`;
};

module.exports = {
'GET /hello/:name': fn_hello
};
  • index.js登录页面处理登录相关请求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let 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>`;
};

let fn_signin = async (ctx, next) => {
let
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
};
  1. controller.js文件,用于扫描controller目录里面的js请求处理文件;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const fs = require('fs');

// add url-route in /controllers:

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

function addControllers(router, dir) {
fs.readdirSync(__dirname + '/' + dir).filter((f) => {
return f.endsWith('.js');
}).forEach((f) => {
console.log(`process controller: ${f}...`);
let mapping = require(__dirname + '/' + dir + '/' + f);
addMapping(router, mapping);
});
}

module.exports = function (dir) {
let
controllers_dir = dir || 'controllers',
router = require('koa-router')();
addControllers(router, controllers_dir);
return router.routes();
};
  1. app.js作为程序的入口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const controller = require('./controller');

const app = new Koa();

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


app.use(bodyParser());
app.use(controller());

app.listen(3000);
console.log('http://localhost:3000');

REST-API

规范

编写REST API,实际上就是编写处理HTTP请求的async函数,不过,REST请求和普通的HTTP请求有几个特殊的地方:

  1. REST请求仍然是标准的HTTP请求,但是,除了GET请求外,POST、PUT等请求的body是JSON数据格式,请求的Content-Type为application/json;
  2. REST响应返回的结果是JSON数据格式,因此,响应的Content-Type也是application/json。
  3. REST请求只是一种请求类型和响应类型均为JSON的HTTP请求;

rest-url示例

  • GET请求:获取所有Product的URL:
1
GET /api/products
  • GET请求:id为123的Product的URL:
1
GET /api/products/123
  • GET请求:资源还可以按层次组织。例如,获取某个Product的所有评论,使用:
1
GET /api/products/123/reviews
  • GET请求:当我们只需要获取部分数据时,可通过参数限制返回的结果集,例如,返回第2页评论,每页10项,按时间排序:
1
GET /api/products/123/reviews?page=2&size=10&sort=time
  • POST请求: 新建一个Product,JSON数据参数包含在body中,URL如下:
1
POST /api/products
  • PUT请求: 更新一个Product使用PUT请求,例如,更新id为123的Product,其URL如下:
1
PUT /api/products/123
  • DELETE请求:删除一个Product使用DELETE请求,例如,删除id为123的Product,其URL如下:
1
DELETE /api/products/123

编写rest-api

这里使用koa-bodyparser模块库来让ctx.request.body直接访问解析后的JavaScript对象。

在koa中处理REST请求是非常简单的。bodyParser()这个middleware可以解析请求的JSON数据并绑定到ctx.request.body上,输出JSON时我们先指定ctx.response.type = ‘application/json’,然后把JavaScript对象赋值给ctx.response.body就完成了REST请求的处理。

示例

创建Product.js处理product相关的api请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 存储Product列表,相当于模拟数据库:
let products = [{
name: 'iPhone',
price: 6999
}, {
name: 'Kindle',
price: 999
}];


module.exports = {
// localhost:3000/api/products
'GET /api/products': async (ctx, next) => {
// 设置Content-Type:
ctx.response.type = 'application/json';
// 设置Response Body:
// json object
// ctx.response.body = {
// products: products
// };

// json array
ctx.response.body = products;
},
// url:localhost:3000/api/products
// Content-Type:application/json
// params-style-body: {"name":"XBox","price":3999}
'POST /api/products': async (ctx, next) => {
let p = {
name: ctx.request.body.name,
price: ctx.request.body.price
};
products.push(p);
ctx.response.type = 'application/json';
ctx.response.body = p;
}
};

Nunjucks

官方文档

Nunjucks官方文档自带中文版翻译;

模板引擎

Nunjucks是一个模板引擎,模板引擎就是基于模板配合数据构造出字符串输出的一个组件。

简介

Nunjucks是Mozilla开发的一个纯JavaScript编写的模板引擎,既可以用在Node环境下,又可以运行在浏览器端。但是,主要还是运行在Node环境下,因为浏览器端有更好的模板解决方案,例如MVVM框架。

安装

1
$ npm install nunjucks --save

API

Nunjucks支持在html模板中使用一些NunjucksAPI的逻辑语法;

block区块

模板可以通过block拆分成许多区块;

1
2
3
{% block css %}
<link rel="stylesheet" href="app.css" />
{% endblock %}
模板继承
  1. 先定义一个基本的网页框架base.html:
1
2
3
4
5
<html><body>
{% block header %} <h3>Unnamed</h3> {% endblock %}
{% block body %} <div>No body</div> {% endblock %}
{% block footer %} <div>copyright</div> {% endblock %}
</body>
  1. base.html定义了三个可编辑的块,分别命名为header、body和footer。子模板可以有选择地对块进行重新定义:
1
2
3
4
5
{% extends 'base.html' %}

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

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

koa2中使用

  1. 封装nunjucks,为koa异步函数的ctx添加render属性,用于渲染模板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 模板引擎nunjucks封装,目的是给异步函数的ctx添加render方法,来调用模板引擎渲染视图
*/
const nunjucks = require('nunjucks');

function createEnv(path, opts) {
let
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 (let f in opts.filters) {
env.addFilter(f, opts.filters[f]);
}
}
return env;
}

/**
* ctx添加render方法,调用时会调用nunjucks模板引擎渲染模板
* @param path
* @param opts
* @returns {function(*, *)}
*/
function templating(path, opts) {
let 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;
  1. koa中,加载controller网络请求前加载模板引擎配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const controller = require('./controller');
const templating = require('./templating');

const app = new Koa();

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

// request解析body
app.use(bodyParser());

// nunjucks模板引擎
app.use(templating('views', {
noCache: !isProduction,
watch: !isProduction
}));

// 加载所有逻辑处理controller的请求函数
app.use(controller());

app.listen(3000);
console.log('server on http://localhost:3000/');
  1. http请求响应调用模板引擎渲染:
1
2
3
4
5
6
7
module.exports = {
'GET /': async (ctx, next) => {
ctx.render('index.html', {
title: 'Welcome'
});
}
};
坚持原创技术分享,您的支持将鼓励我继续创作!