Fork me on GitHub

koa学习-实现路由及对外的API服务-5

实现一个最小统计的 API 服务器

之前我们已经将整个网站的数据库写完了之后,现在我们需要构建一些API层面的东
西,来写一些http请求的东西.

koa-learnning

这一节我们来做一下关于网站路由的东西。

1
npm install koa-router -S

先从一个最简单的做起,我们在上面引用了 koa-router,然后我们在server下面新建一个文件夹routes来专门存放我们的路由文件.

在routes里面新建一个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
27
28
29
30
31
32
33
const Router = require('koa-router');
const mongoose = require('mongoose');
const router = new Router();
const Router = require('koa-router');
const mongoose = require('mongoose');
const router = new Router();
// 这个router来处理不同的http请求(例如get和post)
router.get('/movies/all', async (ctx, next) => {
// 先拿到 Movie 的数据库里面所有的数据,然后再按照创建时间来进行一个排序
const Movie = mongoose.model('Movie');
const movies = await Movie.find({}).sort({
'meta-createdAt': -1
})
// 然后只要访问对应的这个路径我们就返回对应的数据
ctx.body = {
movies
}
})
// 通过一个具体的样例来进行测试
router.get('/movies/detail/:id', async (ctx, next) => {
const Movie = mongoose.model('Movie');
const id = ctx.params.id;
const movie = await Movie.findOne({_id:id})
// 这里只要我们访问到对应id的数据,就返回具体到某个细节的数据
ctx.body = {
movie
}
})
module.exports = router

我们在server文件夹里面的入口文件里面引用一下这个routes文件里面的路由规则来将其激活.

server/index.js

1
2
3
4
5
6
7
8
9
10
11
/*
some codes
*/
const Koa = require('koa');
const router = require('./routes');
const app = new Koa();
// 这里还是使用调用中间价的方式来操作
// 在routes里面使用引用一下
// allowedMethods()里面表示允许使用router里面所有的方法
app.use(router.routes()).use(router.allowedMethods());

然后使用npm start 启动一下我们的koa服务,打开对应的本地链接,使用localhost:4440/movies/all 去访问我们对应的路径,这个时候页面会返回之前我们已经抓取成功的所有的数据。

我们在这些数据里面找一条对应的id值,然后拼接出一个对应的路由地址,就可以访问到对应的详细的数据了。

这杨我们就完成了一个简单的路由服务了.

koa-router的使用

在上面的koa-router里面,我们可以通过添加多个中间件的形式来完成对数据的过滤操作,例如在上面的例子里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 我们可以在路由的get请求里面接一个中间件的回调方式
router.get('/movies', async (ctx,next)=>{
const Category = mongoose.model('Catagory');
const cats = await Category.find({});
ctx.body = cats;
return next();
// 通过在这个中间件里面做一些数据过滤之后然后再进入下一步
},async (ctx, next) => {
// 先拿到 Movie 的数据库里面所有的数据,然后再按照创建时间来进行一个排序
const Movie = mongoose.model('Movie');
const movies = await Movie.find({}).sort({
'meta-createdAt': -1
})
ctx.body = {
movies
}
})

我们可以在里面添加很多的中间价的来完完成这种过滤操作。

如果我们想使用koa-router来完成添加一个路由前缀的操作的话,可以这样来写:

1
2
3
4
5
6
7
8
9
10
const Router = require('koa-router');
// const router = new Router();
// 通过在声明的时候接一些参数来完成
const router = new Router({
prefix:'/movies'
})
router.get('trailer/:tid',async(ctx,next)=>{
// some codes...
})

这样真正get到的路由地址是(/movies/trailer/:tid)

在router里面也是可以使用中间件的:

1
2
3
4
5
6
7
const Router = require('koa-router');
const router = new Router();
router.use(mid1()).use(mid2()).get('/movies',async(ctx,next)=>{
// ... some codes...
})

使用的顺序和use()的顺序是有关系的(先调用mid1,再调用mid2).

关于 koa-router 还想学习更多的话,可以去参考一下github上面的仓库源代码去看一下文档说明 koa-router地址

装饰器简介

我们来使用装饰器来完成路由的拆分和改造。
我们可以构造一个装饰器类来对类里面的方法去进行装饰:

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
const Router = require('koa-router');
const mongoose = require('mongoose');
const router = new Router();
@controller('/api/v0/movies')
export class movieController{
@get('/')
// 如果我们进入这个页面之后,需要检验一些权限(利于登录人的权限)
// 可以直接在这里加装饰器进行检测
// @login
// @admin(['developer'])
// @log
async getMovies(ctx,next){
const Movie = mongoose.model('Movie');
const movies = await Movie.find({}).sort({
'meta.createdAt':-1
})
ctx.body = {
movies
}
}
@get('/:id')
async getMovieDetail(ctx,next){
const Movie = mongoose.model('Movie');
const id = ctx.params.id;
const movie = await Movie.findOne({_id:id});
ctx.body = {
movie
}
}
}
module.exports = router;

用一个简单的例子解释一下装饰器,大概就是下面这些代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Boy {
@speak
run(){
console.log('I can run');
}
}
function speak(target){
console.log(target);
}
const wd = new Boy();
wd.run();

但是一般 node 里面并没有支持装饰器的操作,所以为了使上面的代码运行起来我们需要自己在.babelrc里面去自己配置一波。

找一下babel的插件 babel-plugin-transform-decorators-legacy 这个插件在.babelrc里面的plugins里面有添加就可以使用了.

然后还需要添加几个babel的插件:

1
npm install babel-core babel-polyfill -D

然后在根目录下新建一个叫做 start.js 的文件来支持装饰器的运行.

1
2
3
4
// 来注册一下
require('babel-core/register')();
require('babel-polyfill');
require('./test/dec.js');

然后通过node start.js 去启动

这个时候就会输出:

1
2
Boy {}
I can run

相当于装饰器就是装饰了Boy这个类.
装饰器里面一共有三个参数是这样的:

1
2
3
4
5
function speak(target,key,descriptor){
console.log(target);
console.log(key);
console.log(descriptor);
}

第一个参数target我们已经知道是被装饰器装饰的对象的,通常就是@符号上面所在的第一个类,或者是speak下面紧跟的class,但是无论是在类的内部还是紧贴着类,这个装饰器的key就是装饰器所修饰的方法,在这个地方修饰的方法就是run(),descriptor就是这个方法的一些日志情况。

我们也可以修改往装饰器里面传一些参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Boy{
@speack('中文')
run (){
console.log('I can say' + this.language);
console.log('I can run!');
}
}
function speack(language){
return function(target,key,descriptor){
console.log(target);
console.log(key);
console.log(descriptor);
target.language = language;
return descriptor;
}
}
const wd = new Boy();
wd.run();

上面通过传递参数,输出的结果就是:

1
2
3
4
5
6
7
8
Boy {}
run
{ value: [Function: run],
writable: true,
enumerable: false,
configurable: true }
I can say中文
I can run!

使用装饰器来改造 koa-router

我们前面已经基本知道的装饰器的作用(装饰器对类以及类的装饰作用)。

现在我们使用装饰器来改造一下koa-router

在server目录下新建一个lib文件件,里面放上一个叫做decorator.js的文件,在里面完成对koa-router的改造

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
const Router = require('koa-router');
const { resolve } = require('path');
const _ = require('lodash')
// Symbol一旦被创建就不能被修改,并且值也不能改变
const symbolPrefix = Symbol('prefix');
const routerMap = new Map();
const isArray = c => _.isArray(c) ? c : [c];
export class Route {
// 先往里面传入两个属性
constructor(app, apiPath) {
this.app = app;
this.apiPath = apiPath;
// 这个router是koa-router的实例
this.router = new Router();
}
// 对路由进行一个初始化
init(){
// 使用glob对路由文件进行一个加载,这在之前对数据库的schema初始化也有介绍,把path里面任意的一个js文件全部require进来
glob.sync(resolve(this.apiPath,'./**/*.js')).forEach(require);
// routerMap建立完之后,通过枚举拿到里面的conf和controller
for(let [conf,controller] of routerMap){
// 首先拿到里面所有的 controllers,通过isArray将其搞成数组
const controllers = isArray(controller);
const prefixPath = conf.target[symbolPrefix];
// 如果存在prefixPath就对路由进行一层拼接
if(prefixPath){
// prefixPath要在前面加上一个/
prefixPath = normalizePath(prefixPath);
}
// routerPath就是使用前缀path拼接上当前的path
const routerPath = prefixPath + conf.path;
// 拼接上之后使用koa-router里面的方法去调用,可以参考上面的使用方法
this.router[conf.method](routerPath,...controllers);
}
// 使用完成之后封装一下所有请求的方法
this.app.use(this.router.routes());
// 运用一下所使用的请求方法
this.app.use(this.router.allowedMethods());
}
}
// 声明一个controller用于对路由的头部prefix的装修
// 这是是相当于是在装饰器函数里面返回一个函数
// 在函数的原型上面添加一个prefix属性
// 这个属性可以使用ES6里面的Symbol数据类型来表示
// 这里的path参数就是请求那边传递过来的
export const controller = path => target => (target.prototype[symbolPrefix] = path)
// normalizePath处理的路径是根路径开头直接返回path,如果不是用根路径拼接一下再返回(认为它是下一层路径)
const normalizePath = path => path.startWith('/') ? path : `/${path}`
// 这个router其实也是个装饰器
const router = conf => (target,path,descriptor) => {
// 对路径进行根路径化
conf.path = normalizePath(conf.path);
// 使用map来进行键值的访问,键就是前面的对象,值就是target[key]
routerMap.set({
target:target,
...conf,
},target[key])
}
// 有了上面的集合我们可以对请求方法全部装饰一遍
export const get = path => router({
method:'get',
path: path
})
export const post = path => router({
method:'post',
path: path
})
export const put = path => router({
method:'put',
path:path
})
export const del = path => router({
method:'del',
path:path
})
// router上面可以装饰use来使用中间件
export const use = path => router({
method:'use',
path:path
})
export const all = path => router({
method:'all',
path:path
})

router的使用在server下面新建一个叫做midlleware的文件夹,里面放上一个叫做router.js的文件

1
2
3
4
5
6
7
8
const { resolve } = require('path');
const { Route } = require('../lib/decorator');
export const router = app => {
const apiPath = resolve(__dirname,'../routes');
const router = new Route(app,apiPath);
router.init();
}

如果我们要使用这些装饰器的话,直接使用require的方法去引入即可。

代码位于routes文件夹下面的movie.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
// const Router = require('koa-router');
const mongoose = require('mongoose');
// const router = new Router();
const { get,post,put,controller } = require('../lib/decorator');
// 这个router来处理不同的http请求(例如get和post)
@controller('/api/v0/movies')
export class movieController{
@get('/')
async getMovies(ctx,next) {
const Movie = mongoose.model('Movie');
const movies = await Movie.find({}).sort({
'meta.createdAt': -1
})
ctx.body = {
movies
}
}
@get('/:id')
async getMovieDetail(ctx,next){
const Movie = mongoose.model('Movie');
const id = ctx.params.id;
const movie = await Movie.findOne({_id:id});
ctx.body = {
movie
}
}
}
// module.exports = router

这里我们就不再需要之前写使用的koa-router了,直接去外部引用方法即可。

拆分路由组件以及对外暴露API

我们现在来拆分一下服务层,在server目录下面新建一个service的目录,在里面新建一个movie.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
const mongoose = require('mongoose');
const Movie = mongoose.model('Movie');
// 用这个文件来完成和数据库的交互
export const getAllMovies = async (type, year) => {
let query = {};
if (type) {
query.movieTypes = {
$in: [type]
}
}
if (year) {
query.year = year;
}
const movies = await Movie.find(query);
return movies;
}
export const getMovieDetail = async (id) => {
const movie = await Movie.findOne({
_id: id
});
return movie;
}
export const getRelativeMovies = async (movie) => {
const movies = await Movie.find({
// 通过in 去数据库里面找一个数组值,这个数组的值就是传递过来的movie.movieTypes
movieTypes: {
$in: movie.movieTypes
}
})
return movies;
}

这些方法写完并导出之后我们直接在 routes/movie.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
const {
get,
post,
put,
controller
} = require('../lib/decorator');
const {
getAllMovies,
getMovieDetail,
getRelativeMovies
} = require('../service/movie');
// 这个router来处理不同的http请求(例如get和post)
@controller('/api/v0/movies')
export class movieController {
@get('/')
async getMovies(ctx, next) {
const {
type,
year
} = ctx.query;
// 查询所有电影的方法
const movies = await getAllMovies(type, year)
ctx.body = {
movies
}
}
@get('/:id')
async getMovieDetail(ctx, next) {
const id = ctx.params.id;
const movie = await getMovieDetail(id);
const relativeMovies = await getRelativeMovies(movie);
ctx.body = {
data: {
movie,
relativeMovies
},
success: true
}
}
}

然后我们在service下新建一个user.js来处理和登录相关的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 处理登录和后台相关的操作
const mongoose = require('mongoose');
const User = mongoose.model('User');
// 用来校验登录的用户名和密码是否正确
export const checkPassword = async (email, password) => {
let match = false;
const user = await User.findOne({
email
})
// 首先去数据库里面查找登录用户的邮箱,如果邮箱是存在的,那么就去比对用户的密码
if (user) {
// 这里直接用user实体里面的方法去进行密码的比对
match = await user.comparePassword(password,user.password);
}
return {
match,
user
}
}

这个拿去出之后在routes里面新增一个路由文件(user.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
const {
controller,
post,
get,
put
} = require('../lib/decorator');
const {
checkPassword
} = require('../service/user');
@controller('/api/v0/user')
export class userController {
@post('/')
async login(ctx, next) {
// 通过 request 里面的 body 拿到 email 和 password
const {
email,
password
} = ctx.request.body;
const matchData = await checkPassword(email, password);
// 如果用户信息压根不存在
if (!matchData.user) {
return (ctx.body = {
success: false,
err: '用户不存在'
})
}
// 如果返回了true
if (matchData.match) {
return (ctx.body = {
success: true
})
}
return (ctx.body = {
success: true,
err:'密码不正确'
})
}
}

这样我们的routes下面的路由文件就已经全部都写好了,我们在入口文件里面可以使用app.use()将他们一个个的都导入进去,也可以使用一种简单的遍历方法去进行导入。

这里我们使用一种函数式编程的写法来逐步引入routes,

首先安装一个库:

1
npm i ramda -S

然后在 server/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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const Koa = require('koa');
const {
resolve
} = require('path');
const {
connect,
initSchemas,
initAdmin
} = require('./database/init');
const app = new Koa();
// 使用ramda的一个库
const R = require('ramda');
// 声明一个中间件的数组
const MIDDLEWARES = ['router'];
const useMiddlewares = (app) => {
// 使用 R.map来遍历所有中间件
R.map(R.compose(
// 使用R.compose来
R.forEachObjIndexed(
initWith => initWith(app)
),
// require上面通过R.forEachObjIndexed
require,
// name 在拿到middlewares下面的中间件之后会向上传给require
name => resolve(__dirname, `./middlewares/${name}`)
))(MIDDLEWARES)
};
;(async () => {
await connect();
initSchemas();
await initAdmin();
await useMiddlewares(app);
app.listen(4550);
// 把它 require 进来,他就会启动子进程去爬取数据,然后把爬取到的数据存在数据库里面
// require('./tasks/movie');
// require('./tasks/api');
// require('./tasks/trailer');
// require('./tasks/qiniu');
})();

这样修改一下我们的start.js 里面的代码,让脚本在启动的时候直接去访问server/index.js即可了。

start.js

1
2
3
4
// 来注册一下
require('babel-core/register')();
require('babel-polyfill');
require('./server/index');

然后使用nodemon start.js 即可启动我们的脚本了.去访问localhost:port/api/v0/movies能够得到数据则证明我们的成功了。

这样就完成了我们的对外的API服务操作.

-------------本文结束感谢您的阅读-------------