用Express搭建Api演示

本文演示用Express搭建Api的过程。内容涉及:模块化路由,中间件,Promise,用bluebird访问MySQL,用Express实现非RESTful风格和RESTful风格api。如有错误,欢迎指正。

本文包含一个demo项目源码,地址:express-demo。运行项目需要本地安装Node.js,MySQL,一个你习惯的数据库管理工具(我用的是Sequel Pro),postman。

可用这个命令克隆到本地:

1
git clone https://github.com/morrxy/express-demo.git

demo代码里包含了文章里的步骤:

1
2
3
4
5
6
7
step1-npm-init
step2-hello
step3-module-route
step4-middleware
step5-db-access
step6-db-more
step7-restful-todos

查看某步骤的源码:

1
git checkout step1-npm-init

建立项目目录

查看源码

1
git checkout step1-npm-init

步骤

1
2
3
mkdir express-todolist
cd express-todolist
npm init

npm init后一路回车,生成一个package.json文件,用与保存项目依赖,项目描述等信息。

安装Express

1
npm install --save express

启动App并返回JSON

查看源码

1
git checkout step2-hello

步骤

在根目录下新建文件app.js,作为项目启动文件。并编辑文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
'use strict';

const express = require('express');
const app = express();

app.get('/', function (req, res) {
res.send({
data: 'Hello World!'
});
});

app.listen(3000, function () {
console.log('Example app listening on port 3000!');
});

这里用express()了一个app,并对其设置了根目录的get设置了路由,最后通过app.listen()启动app。

然后在命令行运行,启动Node进程。之后用浏览器访问http://localhost:3000/就可以看到响应结果。

1
node app.js

响应结果

备注:想看到整齐的json结果可以安装这个chrom插件

模块化路由

查看源码

1
git checkout step3-module-route

步骤

实际项目会有很多路由,一般不把路由直接放在启动文件里,而是根据需要划分成路由模块,每个模块的所有具体路由放在各自的索引文件里,最后从启动文件里加载所有路由模块的索引文件。

假设需要两个模块usermission。每个模块下各自需要r1,r2两个路由,即需要这4个路由/user/r1/user/r2,/mission/r1,/mission/r2

下面用user当例子,说明建立模块过程:

  • 在根目录下新建目录route用于存放s所有路由模块索引文件和索引各自的目录
  • 在route目录下新建文件user.js,作为user模块的索引文件
  • 在route目录下新建目录user,用于存放所有user模块每个路由文件
  • 在/route/user目录下新建需要的文件,这里是r1.jsr2.js
  • 在app.js引入user模块

user.js:

1
2
3
4
5
6
7
8
'use strict';

const express = require("express");
const router = express.Router();
module.exports = router;

router.get('/r1', require('./user/r1'));
router.get('/r2', require('./user/r2'));

上面的代码,用Express.Router()建立一个子路由,用于容纳所有/user下的路由,然后往这个路由里添加了两个get路由/user/r1/user/r2

/route/user/r1.js,定义具体业务逻辑:

1
2
3
4
5
6
7
'use strict';

module.exports = (req, res) => {
res.send({
data: 'GET /user/r1 result'
})
};

app.js添加如下代码,用于引入user路由模块:

1
app.use('/user', require('./route/user'));

按上述步骤建立mission模块,另外顺手把前面对/的请求也放进一个单独的文件里。app.js改成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
'use strict';

const express = require('express');
const app = express();

app.use('/', require('./route/index'));
app.use('/user', require('./route/user'));
app.use('/mission', require('./route/mission'));

app.listen(3000, function () {
console.log('Example app listening on port 3000!');
});

重启app,就可以在浏览器请求/user/r1,/mission/r1等4个新路由了:

user/r1

中间件

简介

官方介绍: 使用中间件,写中间件

每个具体的路由都处理特定的业务逻辑,但有时候需要对所有路由,或某个路由模块下的所有路由都进行某些业务处理,比如身份验证。这时候就可以使用中间件。

中间件是函数,之所以叫中间件,是因为其执行时间处于接受请求至返回响应周期中间。并且能访问请求对象,响应对象,和下一个中间件。中间件能执行任何代码;能修改请求和响应对象;能结束请求响应周期;能调用下一个中间件。Express是一个路由和中间件web框架,Express app基本上就是一系列中间件函数调用。

假设这个app有些路由模块需要进行身份验证,其中包括/secret,其他路由不需要。用两个中间件实现这个需求,第一个用于检测路由,第二个用于验证身份。如果路由不需要身份验证,跳过后续身份验证中间件,交出控制权,对应的路由程序继续处理。否则执行后续的身份验证中间件,验证成功则交出控制权,secret路由继续处理。否则结束请求响应周期,返回验证失败信息。

查看源码

1
git checkout step4-middleware

步骤

  • 步骤1:按上面模块化路由的方法,新建一个secret路由模块
  • 步骤2:在根目录新建一个middleware目录,用于存放所有中间件
  • 步骤3:在middleware目录下新建两个文件,route-test.jssession-test.js,用于检测路由和验证身份
  • 步骤4:在app.js里引入中间件

步骤3的route-test.js:

1
2
3
4
5
6
7
8
9
10
11
12
'use strict';

const url = require('url');

module.exports = function(req, res, next) {
const pathname = url.parse(req.originalUrl).pathname;
if (pathname === '/secret') {
next();
} else {
next('route');
}
};

可以看到,这个中间件从请求对象获得请求路径,与需要验证的路径对比,如果匹配,则用next()把控制权转交给后面的验证程序。否则通过next('route')跳过后面的验证中间件,转交其他路由继续处理。

步骤3的session-test.js:

1
2
3
4
5
6
7
8
9
10
'use strict';

module.exports = function (req, res, next) {
const key = req.query.key;
if (key === 'secretkey') return next();

res.send({
data: 'access deny!'
});
};

可以看到,这个中间件从请求对象获取用户提交的key,并与系统的key对比,如果成功,则用next()交出控制权,转交/secret继续处理。否则中止请求响应周期,返回验证失败信息’access deny!’。

步骤4之后的app.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'use strict';

const express = require('express');
const app = express();

app.all('*', require('./middleware/route-test'), require('./middleware/session-test'));

app.use('/', require('./route/index'));
app.use('/user', require('./route/user'));
app.use('/mission', require('./route/mission'));
app.use('/secret', require('./route/secret'));

app.listen(3000, function () {
console.log('Example app listening on port 3000!');
});

app.all()app.get()类似,但匹配所有得http动词。*匹配所有路由。把这行放在其他路由模块上面,所有请求就会先被这两个中间件处理。

重启app,在浏览器测试/secret路由:

fail

success

Promise

Promise是一种处理异步io的方法,可以用来解决Node的回调地狱问题,使代码编写,修改都容易很多。这里有个介绍Promises and Asynchronous Programming。bluebird是一个Promise库,提供了很多方便的工具,之前我写过一篇使用感受,bluebird带给我的惊喜,这个demo就用bluebird演示数据库访问。

数据库配置与测试

假设这个app是个todolist管理app,用mysql存数据,需要提供对todo表的读取,新增,修改,删除。这节配置数据库连接,并用/todo/list测试。

查看源码

1
git checkout step5-db-access

步骤

  • 1.在本地安装mysql,并创建好todo表
  • 2.安装bluebird和mysql,bluebird用于实现Promise语法,mysql用于连接mysql
  • 3.配置数据库连接,并用/todo/list测试

步骤1:

本地安装好mysql,并用如下命令启动:

1
mysql.server start

启动成功会看到

1
2
Starting MySQL
. SUCCESS!

之后使用任何你习惯的数据库管理工具(我用的是Sequel Pro),连上本地mysql。

conncet to local db

创建一个数据库,叫todolit

add db

然后再建一个表,叫todo。表结构如下:

1
2
3
4
5
6
CREATE TABLE `todo` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(50) NOT NULL DEFAULT '' COMMENT '标题',
`done` tinyint(1) NOT NULL DEFAULT '0' COMMENT '1完成 0未完成',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

往里面手动插入两条数据:

todo table

步骤2:

在项目根目录执行如下命令:

1
npm install --save bluebird mysql

步骤3-配置数据库连接:

在根目录新建目录config,用于存放配置文件。在config目录下新建db.js用于连接mysql。代码如下:

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
'use strict';

const mysql = require('mysql');
const Promise = require('bluebird');

Promise.promisifyAll(mysql);
Promise.promisifyAll(require('mysql/lib/Connection').prototype);
Promise.promisifyAll(require('mysql/lib/Pool').prototype);

const local = {
host: '127.0.0.1',
user: 'root',
password: '',
database: 'todolist',
port: 3306,
charset: 'utf8mb4'
};

const pool = mysql.createPool(local);

module.exports = () => {
return pool.getConnectionAsync().disposer(function(connection) {
connection.release();
});
};

mysql自带的接口是回调式的,并且每个连接用完以后都有手动释放。可去官网了解一下,并与下述的db访问方式对比。

这里有一些用bluebird库连接mysql的细节,并使用了bluebird提供的Promisify功能,把mysql包自带的callback方式转成了promise方式,使数据库访问可以接入路由的Promise链。另外使用了bluebird资源自动释放,最后导出了一个连接mysql的函数,供所有db访问使用。使用时候只需要使用Promise.using()即可以连库,并且不用管释放连接,连接会自动释放,非常方便。

细节可参考bluebird官方参考:promisifyAll, 连库与自动释放, 资源使用

步骤3-用/todo/list测试db访问:

用前面的方式新建路由模块todo,和/todo/list路由,用于返回所有todo。代码如下:

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
'use strict';

const Promise = require('bluebird');

const todoDao = require('../../model/todo');
const sendError = require('../../helper/sendError');

module.exports = (req, res) => {
Promise.resolve()
.then(() => {
return getTodoList();
})
.then((data) => {
res.send({
data: data,
status: {
code: 0,
msg: 'ok'
}
})
})
.catch(err => {
sendError(res, err);
});
};

function getTodoList() {
return todoDao.list();
}

这里使用Promise链控制程序流,使路由过程一目了然,方便开发和修改。其中访问数据的部分独立到单独的model目录下,结构与路由模块类似,以便重复使用。

接着看一下数据访问,/model/todo/list.js:

1
2
3
4
5
6
7
8
9
10
11
12
'use strict';

const Promise = require('bluebird');

const db = require('../../config/db');

module.exports = () => {
return Promise.using(db(), conn => {
const sql = `select * from todo`;
return conn.queryAsync(sql, []);
});
};

这里使用了上述的bluebird提供的资源使用api:using(),这里注意,整个函数返回了一个访问数据库的Promise(using会返回一个promise),使访问数据库的过程融入上面的路由Promise链,否则查库数据无法返回到路由里。另外,如果需要多个db访问,可在using后增加.then()继续其他io操作,比如:

1
2
3
4
5
6
7
8
9
module.exports = () => {
return Promise.using(db(), conn => {
const sql = `select * from todo`;
return conn.queryAsync(sql, []);
})
.then(data => {
//用data继续其他io操作,并返回结果
})
};

Promise的另一个好处是,Promise链中的所有错误都能传给后面的异常处理函数,这样一个Promise链里只需要一个最后的异常处理函数就行了。这里是sendError函数,把它放进根目录下的helper目录(用于存放所有得辅助函数),避免重复。

最后重启app,在浏览器访问http://localhost:3000/todo/list:

todo list

增加,修改,删除

这步需要用POST向服务器提交数据,所以服务端要安装npm包body-parser用来解析数据,在项目根目录执行:

1
npm install --save body-parser

查看源码

1
git checkout step6-db-more

步骤

按照上一步完成的/todo/list添加todo新增,修改,删除api,分别是/todo/add,/todo/update,/todo/remove,因为这些接口的method不是get,所以用postman代替浏览器测试接口,参数和测试结果分别如下图:

add
update
remove

这样就完成了所有todo的api。

RESTful风格的todo接口

上述todo接口也可以用RESTful风格实现:

列表:GET /todos, 显示所有todo
增加:POST /todos, 新增todo
单个:GET /todos/:todo_id, 显示某个todo
修改:PUT /todos/:todo_id, 修改某个todo
删除:DELETE /todos/:todo_id, 删除某个todo

查看源码

1
git checkout step7-restful-todos

步骤

与上面非restful实现相比,model里的数据访问都可以重复使用。不同之处:路由模块索引;路由参数获取。

路由模块索引todos.js:

1
2
3
4
5
6
7
8
9
10
11
'use strict';

const express = require("express");
const router = express.Router();
module.exports = router;

router.route('/').get(require('./todos/list'));
router.route('/').post(require('./todos/add'));
router.route('/:todo_id').get(require('./todos/single'));
router.route('/:todo_id').put(require('./todos/update'));
router.route('/:todo_id').delete(require('./todos/remove'));

route是Express提供的api,参数里可以加上冒号形式的动态路由参数,比如上面的/:todo_id,后面再跟着动词和对应的处理函数。动态路由参数的获取可以通过req.params.todo_id的形式:

1
const todoId = req.params.todo_id;

最后看下RESTful风格的postman参数形式和测试结果:

list
add
single
update
delete

全文完。