搭建node + typescript搭建服务

搭建node + typescript搭建服务

  • npm init 生成 package.json 文件

  • 安装 node 模块的 ts 声明文件

    1
    2
    npm install --save typescript
    npm i --save @types/node
  • 目录结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    .
    ├── config.js // 配置文件,保存不同环境的全局变量(如数据库相关参数)
    ├── index.ts // 入口文件
    ├── package.json
    ├── src
    │ ├── auth.js // 登录中间库
    │ ├── controller // 接口相关
    │ │ ├── comment.controller.ts
    │ ├── service // 服务相关
    │ │ ├── comment.service.ts
    │ ├── types // 类型
    │ │ └── BaseContest.ts
    │ └── utils // 通用函数
    │ ├── http.ts
    │ ├── redis.ts
    │ ├── statusCode.ts
    │ └── token.ts
    └── tsconfig.json // ts相关配置

入口文件

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
import Koa from 'koa';
const bodyParser = require('koa-bodyparser');
const cors = require('koa2-cors');
import errorHandler from './utils/error_handler';
const app = new Koa();
// 路由目录
const auth = require('./src/auth');
import loginRoute from './src/controller/login.controller'

async function start() {
// 解决跨域
app.use(cors());
// 解析post的body内容
app.use(bodyParser());
// 错误处理
app.use(errorHandler);
// 获取用户信息
app.use(auth());
// 注册路由
app.use(loginRoute.routes()).use(loginRoute.allowedMethods());
app.listen(3200);
console.log('Server start on 3200');
}

start();

中间件

koa-body-parser 解析请求体

koa-body-parser 中间件用来解析http请求体,把koa2上下文的formData数据解析到ctx.request.body

Koa2-cors 解决跨域

由于浏览器同源策略,凡是发送请求url的协议、域名、端口三者之间任意一个与当前页面地址不同即为跨域。

koa2直接设置跨域

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
app.use(async (ctx, next) => {
// 允许来自所有域名请求
ctx.set("Access-Control-Allow-Origin", "*");
// 这样就能只允许 http://localhost:8080 这个域名的请求了
// ctx.set("Access-Control-Allow-Origin", "http://localhost:8080");

// 设置所允许的HTTP请求方法
ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST, DELETE");

// 字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段.
ctx.set("Access-Control-Allow-Headers", "x-requested-with, accept, origin, content-type");

// 服务器收到请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

// Content-Type表示具体请求中的媒体类型信息
ctx.set("Content-Type", "application/json;charset=utf-8");

// 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。
// 当设置成允许请求携带cookie时,需要保证"Access-Control-Allow-Origin"是服务器有的域名,而不能是"*";
ctx.set("Access-Control-Allow-Credentials", true);

// 该字段可选,用来指定本次预检请求的有效期,单位为秒。
// 当请求方法是PUT或DELETE等特殊方法或者Content-Type字段的类型是application/json时,服务器会提前发送一次请求进行验证
// 下面的的设置只本次验证的有效时间,即在该时间段内服务端可以不用进行验证
ctx.set("Access-Control-Max-Age", 300);

/*
CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:
Cache-Control、
Content-Language、
Content-Type、
Expires、
Last-Modified、
Pragma。
*/
// 需要获取其他字段时,使用Access-Control-Expose-Headers,
// getResponseHeader('myData')可以返回我们所需的值
//https://www.rails365.net/articles/cors-jin-jie-expose-headers-wu
ctx.set("Access-Control-Expose-Headers", "myData");

await next();
})

使用中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.....
const cors = require('koa2-cors');

// 具体参数我们在后面进行解释
app.use(cors({
origin: function (ctx) {
if (ctx.url === '/test') {
return "*"; // 允许来自所有域名请求
}
return 'http://localhost:8080'; / 这样就能只允许 http://localhost:8080 这个域名的请求了
},
exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
maxAge: 5,
credentials: true,
allowMethods: ['GET', 'POST', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization', 'Accept'],
}))
.....

自定义中间件

error_handler 错误处理中间件

处理所有内部函数抛出的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import json from './json'
import Koa from 'koa';

// 在最为外层包一层,处理所有未被catch的异常
export default async (ctx: Koa.BaseContext, next: () => Promise<any>) => {
try {
await next()
} catch (e) {
// Unknown Exception
console.warn(e);
json(ctx, {
code: STATUS.ERROR,
msg: e.msg || 'Unknown Internal Server Error'
});
}
}

./json 文件处理返回的函数

保证所有接口返回统一的结构

1
2
3
4
5
6
7
8
9
10
import Koa from 'koa';
export default (ctx: Koa.BaseContext, returnData: Object = {}) => {
ctx.type = 'application/json';
const newReturn = Object.assign({
code: STATUS.ERROR
msg: 'ok',
data: {}
}, returnData);
ctx.body = JSON.stringify(newReturn)
}

自定义中间件 Auth 获取当前用户数据

可以自定义已些全局的中间件,此处以获取当前用户信息为例:

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
// 自定义的type,继承自Koa.ExtendableContext,添加了userInfo
import { MvpBaseContext } from './types/BaseContest';
const STATUS = require('./utils/statusCode');
const tokenUtil = require('./utils/token');

// 从所有请求中解析cookie有效性,传递用户id
module.exports = function () {
return async (ctx: MvpBaseContext, next: any) => {
try {
const cookie: string = ctx.cookies.get('p-auth');
if (cookie) {
// 校验token并获取当然用户,赋值给ctx,以便在后面的环节使用
const token = await tokenUtil.checkToken(cookie);
if (token.code == STATUS.SUCCESS) {
ctx.userInfo = {
code: STATUS.SUCCESS,
username: token.username,
uid: token.uid,
cookie: cookie,
id: token.id,
};
} else {
ctx.userInfo = { code: token.code };
}
} else {
ctx.userInfo = { code: STATUS.TOKEN_ERROR };
}
} catch (error) {
console.log('auth------error:', error);
ctx.userInfo = { code: STATUS.TOKEN_ERROR };
}
await next();
};
};

🍃 下面看下tokenUtils里做了什么

这里使用了redis来缓存token和用户信息的映射,从而避免平凡获取用户信息

给每个检制度对设定了清空时间,以保证token的安全性

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
const { setRedis, getRedis } = require('./redis');
const STATUS = require('./statusCode');
const loginService = require('../service/login.services');
const userService = require('../service/user.service');

type UserType = { uid: string; username: string; id: string };
type userInfoType = null | { data: null | { data: null | UserType } };

/**
* 向redis中插入token
* 逻辑是返回给用户的token是由用户id+唯一的随机key组成,redis中存储的是用户id和key的关联,当账号再另一个地方登录,redis中的key会发生改变,之前用户token中解码出来的key就跟redis中的不相同了,redise绘制在2小时后过期自动清理
*/
async function insertUserToken(cookie: string, value: UserType) {
await setRedis(cookie, JSON.stringify(value), 7200);
}

/**
* 验证token有效性
* @param {*} token
* @returns {code, uid}
*/
async function checkToken(token: string) {
// 从redis缓存中获取
const temp: string = await getRedis(token);
if (temp) {
const re: { uid: string; username: string; id: string } | null = JSON.parse(
temp
);
if (re && re.uid && re.username) {
return {
code: STATUS.SUCCESS,
uid: re.uid,
username: re.username,
id: re.id,
};
} else {
return { code: STATUS.TOKEN_INVALID, uid: '' };
}
} else {
// redis中不存在是,重新调用service获取userInfo,并缓存到redis
const userInfo: userInfoType = await loginService.checkCookie(token);
if (
userInfo &&
userInfo.data &&
userInfo.data.data &&
userInfo.data.data.username
) {
// 调用service获取userInfo
const user = await userService.getUserInfoByUserName(
userInfo.data.data.username,
{
username: userInfo.data.data.username,
cookie: token,
}
);
if (
user &&
user.data &&
user.data.data &&
user.data.data.loginName == userInfo.data.data.username
) {
// 缓存到redis
await insertUserToken(token, {
id: user.data.data.id,
uid: user.data.data.uid,
username: user.data.data.loginName,
});
return {
code: STATUS.SUCCESS,
id: user.data.data.id,
uid: user.data.data.uid,
username: user.data.data.loginName,
};
} else {
return { code: STATUS.TOKEN_EXPIRE };
}
} else {
return { code: STATUS.TOKEN_EXPIRE };
}
}
}

koa-router 处理url映射

安装

1
npm install koa-router --save

以login为例🌰:

1
2
3
4
5
6
7
8
9
10
11
12
import Koa from 'koa';
const Router = require('koa-router');
const STATUS = require('../utils/statusCode');
// 定义router的前缀为'/login'
const router = new Router({ prefix: '/login' });

router.get('/', async (ctx: Koa.BaseContext) => {
// 重定向到第三方登录
ctx.redirect(
ssoURL + '/login?redirect_url=' + getBackUrl(ctx.query.redirect_url)
);
});

注册到app,index.ts

1
2
3
4
5
........
import loginRoute from './src/controller/login.controller';
.......
app.use(loginRoute.routes()).use(loginRoute.allowedMethods());
.......

封装http

当node需要以http的方式调用其他服务,且需要对这些请求做同意处理时,可以封装一个http的函数

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 axios = require('axios');

const http = axios.create();

// 当前请求为跨域类型时是否在请求中协带cookie。
http.defaults.withCredentials = true;
// 请求超时时间
http.defaults.timeout = 100000;

http.interceptors.request.use((config) => {
return config;
}, (error) => {
return Promise.reject(error);
});

http.interceptors.response.use((response) => {
// 在这里你可以判断后台返回数据携带的请求码
return response;
},
(error) => {
// 对错误的请求返回做处理
console.log("------http error:", error);
return error;
}
)
// export default http
module.exports = {
http
}

Redis使用

Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库

apiDoc 生成接口文档

安装依赖

1
npm install apidoc

在你的项目根目录下新建apidoc.json文件,该文件描述了项目对外提供接口的概要信息如名称、版本、描述、文档打开时浏览器显示标题和接口缺省访问地址。

apidoc.json :

1
2
3
4
5
6
7
{
"name": "XXX API",
"version": "0.0.1",
"description": "接口文档",
"title": "XXX API",
"url" : "http://localhost:8080/api"
}

使用说明

  • HTTP接口调用方法、路径及名称 @api {method} path [title]

  • 名称 @apiName {name}

  • 分组 @apiGroup {name}

  • 请求头参数 @apiHeader [(group)][{type}] [field=defaultValue][description]

  • 请求参数 @apiParam [(group)][{type}] [field=defaultValue][description]

  • 返回数据描述 @apiSuccess [(group)][{type}] field [description]

  • 接口成功返回样例 @apiSuccessExample [{type}][title] {example}

  • 接口失败描述 @apiError [(group)][{type}] field [description]

  • 接口失败返回样例 @apiErrorExample [{type}][title] {example}

  • 使用模板 @apiUse {name} name->模板名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
    * @apiDefine MySuccess
    * @apiSuccess {string} firstname The users firstname.
    * @apiSuccess {number} age The users age.
    */

    /**
    * @api {get} /user/:id
    * @apiUse MySuccess
    */

文档生成

生成api文档,在package.json中添加生成api的指令

1
2
3
4
5
6
7
8
{
....
"scripts":{
...
"doc": "apidoc -i ./src/controller -o ./api-docs"
}
....
}