# 实战(三) node中间层
# 基础环境
运行node端的ts需要全局安装 ts-node,对应着自动刷新的是 ts-node-dev
安装 koa
yarn add koa
安装 @types/koa
yarn add @types/koa --dev
node端我们采用 MVC 的模式进行建立
在src下创建server文件夹,具体目录结构如图
先写一个简单的入口文件 app.ts
import * as Koa from "koa";
const app = new Koa();
app.use(ctx => {
ctx.body = "hello koa";
});
app.listen(8888, () => {
console.log("数据监控系统🍺,server is running on port8888");
});
2
3
4
5
6
7
8
9
10
11
12
- 在ts中可以直接使用import
- 用koa启动一个最基本的服务,可以在 localhost:8888 下看到
hello koa
这里我们可以把端口号提出来,放在config中
config/index.ts
import { extend } from "lodash";
import { join } from "path";
interface configIn {
viewDir: string;
staticDir: string;
env: string;
port?: string;
}
let config: configIn = {
viewDir: join(__dirname, "..", "views"),
staticDir: join(__dirname, "..", "assets"),
env: process.env.NODE_ENV
};
if (process.env.NODE_ENV == "development") {
config = extend(config, {
port: 8081
});
}
if (process.env.NODE_ENV == "production") {
config = extend(config, {
port: 80
});
}
export default config;
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
- 这里我们安装
lodash
库做为工具库,使用extend
方法来合并port
- 分别设置静态资源和页面的路径
- 按环境变量不同分别设置开发环境和上线环境的端口
这样我们的app.ts就可以改成下面这样
app.ts
import * as Koa from "koa";
import config from "./config";
// 创建服务实例
const app = new Koa();
app.use(ctx => {
ctx.body = "hello koa";
});
app.listen(config.port, () => {
console.log(`数据监控系统🍺,server is running on port${config.port}`);
});
2
3
4
5
6
7
8
9
10
11
12
13
koa是一个非常精简的框架,不同于express,koa把所有的功能都拿了出去,通过插入不同的中间件来实现功能。
安装 koa-bodyparser
和 @types/koa-bodyparser
来接收并解析请求
yarn add koa-bodyparser
yarn add @types/koa-bodyparser --dev
在 app.ts 中引入
import * as bodyParser from "koa-bodyparser";
app.use(bodyParser());
2
安装 koa-static
和 @types/koa-static
用来绑定静态资源
yarn add koa-static
yarn add @types/koa-static --dev
这里直接使用IOC的方式来进行路由管理,就不使用 koa-route 了
安装 awilix
和 awilix-koa
用来做容器管理
yarn add awilix awilix-koa
# 创建IOC容器
okk,接下来先创建容器吧
import { resolve } from 'path';
const { createContainer, Lifetime } = require("awilix");
const {loadControllers, scopePerRequest} = require('awilix-koa');
import historyApiFallback from "koa2-connect-history-api-fallback";
// 创建容器
const container = createContainer();
// 每次请求都是一个 new model
app.use(scopePerRequest(container));
// 装载所有的service(models),并将 services代码注入到controllers
container.loadModules(
[
resolve(__dirname, './service/*.ts'),
resolve(__dirname, './util/SafeRequest.ts')
],
{
formatName: 'camelCase',
resolveOptions: {
lifetime: Lifetime.SCOPED
}
}
)
//注册所有路由
app.use(loadControllers(resolve(__dirname, "../controller/*.ts"), {
cwd: __dirname
})
);
app.use(historyApiFallback({ index: "/", whiteList: ["/api"] }));
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
- 通过
createContainer
创建容器 - 用
scopePerRequest
把容器和路由最终合并到一起,每次请求都去容器里去取 loadModules
把所有的service
注入到容器中loadControllers
注册所有路由- historyApiFallback 用于让koa2支持SPA应用程序。
# interface
先在 interface
下写几个接口文件,别管干啥的,先放着 ╮(╯▽╰)╭
interface/IApi.ts
export interface IApi {
getInfo(url: string, arg?: Object, callback?: Function): Promise<Object>
}
2
3
interface/IIndex.ts
import User from '../model/user'
export interface IIdex {
getUser(id: string): User
}
2
3
4
5
model/user
export default interface User {
email: string;
name: string;
}
2
3
4
5
interface/ISafeRequest.ts
export interface ISafeRequest {
fetch(url: string, arg?: Object, callback?: Function): Promise<Object>
}
2
3
interface/ISocketHandler.ts
export interface ISocketHandler {
init(): void
}
2
3
# service
接下来写两个 service
service/IndexService.ts
import { IIdex } from '../interface/IIndex'
import User from '../model/User'
export default class IndexService implements IIdex {
constructor() {}
private userStorage: User[] = [
{
email: "yuanzhijia@yidengfe.com",
name: "zhijia"
},
{
email: "Copyright © 2016 yidengfe.com All Rights Reversed.京ICP备16022242号-1",
name: "laowang"
}
]
public getUser(id: string): User {
let result: User
result = this.userStorage[id]
return result
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- 注意在写
service
时的命名要用大驼峰,后面在controller
中要对应好,使用小驼峰 - 在这里定义用户信息
service/ApiService.ts
// import safeRequest from '../util/SafeRequest'
import { IApi } from '../interface/IApi'
export default class ApiService implements IApi {
constructor({ safeRequest }) {
this.safeRequest = safeRequest
}
public getInfo(url: string, arg?: Object, callback?: Function): Promise<Object> {
return this.safeRequest.fetch(url, arg, callback)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
- 前面在注入的时候,已经将
SafeRequest
方法注入到容器中了,在这里直接结构出来,使用getInfo
方法调用
SafeRequest.ts
import * as fetch from 'node-fetch'
import { ISafeRequest } from '../interface/ISafeRequest'
export default class SafeRequest implements ISafeRequest {
public async fetch(
url: string,
arg?: Object,
callback?: Function
): Promise<Object> {
let result = { code: "error" };
await fetch(url)
.then(res => res.json())
.then(json => (result = json));
return result
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# controller
接下来就是重头戏 controller
写两个 controller
controller/IndexController.ts
import * as Router from "koa-router";
import { route, GET } from "awilix-koa";
import User from "../model/User";
@route("/")
@route("/index")
export default class IndexController {
private indexService
constructor({ indexService }) {
this.indexService = indexService;
}
@route("/")
@GET()
private async index(
ctx: Router.IRouterContext,
next: () => Promise<any>
): Promise<any> {
const result: User = await this.indexService.getUser("0");
console.log(result.email);
ctx.body = await ctx.render("index", { data: result.email });
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 引入了 koa-router 要记得安装一下,为了给 ctx 添加类型检测
- 在构造函数中结构出
indexService
,记得名字要跟 service 文件名对上,大小写注意 - 然后要绑定路由,从
awilix-koa
引入的route
和GET
方法直接通过装饰器的方式将路由注入进去 - 同一个类或方法上面可以用多个装饰器,@route("/") 和 @GET() 表示在 '/' 路由时通过 get 请求接口
controller/ApiController.ts
import * as Router from 'koa-router'
import { route, GET } from 'awilix-koa'
import { IApi } from '../interface/IApi'
@route('/api')
export default class ApiController {
private apiService
constructor({ apiService }) {
this.apiService = apiService
}
@route('/test')
@GET()
private async test(
ctx: Router.IRouterContext,
next: () => Promise<any>
): Promise<any> {
const result: Promise<Object> = await this.apiService.getInfo('https://api.github.com/users/github')
ctx.body = result
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 定义几个数据接口
- 接收前端的接口请求,然后去后端地址请求数据回来,再返回给前端
至此,一个基本的已经搭起来了。
启动服务,先打开 localhost:8081/api/test,可以看到拿到了请求的数据
# 渲染页面
通过 swig 模板引擎进行渲染页面。当然实际上我们是用react进行页面的绘制,这里先通过后端渲染上来,后面会做ssr同构。
安装 koa-swig koa-static co @types/co @types/koa-static
yarn add koa-swig koa-static
yarn add @types/koa-static @types/co --dev
import * as render from "koa-swig";
import * as serve from "koa-static";
import co from "co";
//配置swig(前端模板)
app.context.render = co.wrap(
render({
root: config.viewDir,
autoescape: true,
cache: "memory",
ext: "html",
writeBody: false
})
);
//配置静态文件目录
app.use(serve(config.staticDir));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 在app上绑定
render
方法 - root 指定根路径
- 通过 serve 配置静态文件目录
# 容错处理和日志
接下来该添加日志了和做一些容错处理了。
首先安装 log4js
yarn add log4js
在 app 上挂载 logger
import { configure, getLogger } from "log4js";
configure({
appenders: {
cheese: { type: "file", filename: resolve(__dirname, "../logs/yd.log") }
},
categories: { default: { appenders: ["cheese"], level: "error" } }
});
const logger = getLogger("cheese");
app.context.logger = logger;
2
3
4
5
6
7
8
9
10
11
- 在 app 中加入 logger
- 指定日志的输出位置
在 middleware 下创建 errorHandler.ts,分别处理 500 错误和 404 错误
middleware/errorHandler.ts
// 容错处理
import { Context } from 'koa'
import { Logger } from 'log4js'
const errorHandler = {
error(app) {
interface KOAContext extends Context {
logger: Logger
}
// 500
app.use(async (ctx: KOAContext, next: () => Promise<any>) => {
try {
await next()
} catch (error) {
// error logs pm2 logs
ctx.logger.error(error)
console.log(error)
ctx.status = error.status || 500
ctx.body = error || '请求出错'
}
})
// 404
app.use(async (ctx: KOAContext, next: () => Promise<any>) => {
await next()
if (404 !== ctx.status) return
ctx.logger.error(ctx)
ctx.status = 404
ctx.body = '<script type="text/javascript" src="//qzonestyle.gtimg.cn/qzone/hybrid/app/404/search_children.js" charset="utf-8" homePageUrl="http://yoursite.com/yourPage.html" homePageName="回到我的主页"></script>'
})
}
}
export default errorHandler
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
- 定义接口
KOAContext
- 定义监听 500 的服务器错误,把错误信息写到log里去,同时把status设置成500,通知页面请求出错
- 定义监听 404 的错误,写入log,同时把页面转到404页
# 抽离中间件
上面写了一大坨,都写在了 app.ts 中,非常臃肿,其实可以把他们都抽离出来到一个中间件中,依次加载
app.ts
import * as Koa from "koa";
import config from "./config";
// 创建服务实例
const app = new Koa();
app.listen(config.port, () => {
console.log(`数据监控系统🍺,server is running on port${config.port}`);
});
2
3
4
5
6
7
8
9
middleware/loadMiddleware.ts
import * as bodyParser from "koa-bodyparser";
const render = require("koa-swig");
import * as serve from "koa-static";
import co from "co";
import { configure, getLogger } from "log4js";
import { resolve } from "path";
import historyApiFallback from "koa2-connect-history-api-fallback";
const { createContainer, Lifetime, asClass } = require("awilix"); // IOC
const { loadControllers, scopePerRequest } = require("awilix-koa"); // IOC
import config from "../config";
import errorHandler from "./errorHandler";
// 初始化IOC容器
const initIOC = app => {
// 创建IOC的容器
const container = createContainer();
// 每一次请求都是一个new model
app.use(scopePerRequest(container));
// 装载所有的service(models), 并将services代码注入到controllers
container.loadModules(
[
resolve(__dirname, "../service/*.ts"),
resolve(__dirname, "../util/SafeRequest.ts")
],
{
// we want `TodosService` to be registered as `todosService`.
formatName: "camelCase",
resolverOptions: {
lifetime: Lifetime.SCOPED
}
}
);
};
// 配置log
const initLog = app => {
configure({
appenders: {
cheese: { type: "file", filename: resolve(__dirname, "../logs/yd.log") }
},
categories: { default: { appenders: ["cheese"], level: "error" } }
});
const logger = getLogger("cheese");
app.context.logger = logger;
// 错误处理
errorHandler.error(app);
};
// 配置渲染
const initRender = app => {
// 配置swig(前端模板)
app.context.render = co.wrap(
render({
root: config.viewDir,
autoescape: true,
cache: "memory",
ext: "html",
varControls: ["[[", "]]"], // 默认动态数据是{{}},但是为了与vue区分开来,改为[[xxx]]
writeBody: false
})
);
// 配置静态文件目录
app.use(serve(config.staticDir));
};
// 配置路由
const initController = app => {
// 注册所有路由
app.use(
loadControllers(resolve(__dirname, "../controller/*.ts"), {
cwd: __dirname
})
);
app.use(historyApiFallback({ index: "/", whiteList: ["/api"] }));
};
export default function load(app) {
app.use(bodyParser());
initIOC(app);
initLog(app);
initController(app);
initRender(app);
}
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
- 定义了初始化IOC容器 initIOC
- 定义了配置log方法
- 定义了配置渲染方法
- 定义了配置路由方法
- 按顺序执行
至此一个基本的 MVC node 端已经搭完,下一章进行 SSR 同构