KOA2 + TYPESCRIPT? 真香

起因

关于自测

自测是保证开发质量的第一关。如何在数据不足的情况下覆盖每个分支?一般情况下,大家会通过以下几种方式调试:

  1. 正常响应结果,根据结果手动修改页面代码展现逻辑(前后端联调阶段)
  2. 手动处理响应结果,对数据进行覆盖重写(前后端联调阶段)
  3. 引入mockjs,对接口进行mock(前端逻辑编写阶段)
  4. 走流程、造满足条件的数据 (前后端联调阶段)

前三种对项目的入侵非常严重,第四种走流程又比较复杂。有没有一种即对项目侵入性小,又可以在逻辑编写阶段尽可能的覆盖分支呢?

概述

提取上述几个关键字:mock、入侵性小、简单。通过这几个关键字,其实对要做的东西有了大概的轮廓:

  1. 对业务端调用改动要小,尽量能不动就不动
  2. 对于mock响应数据,简单、快速实现数据的添加修改
  3. 可以拦截接口请求,对于非mock的接口进行放行

基于此,hewa-front项目正式立项。目前完成功能:

  1. 路由代理
  2. mock数据代理
  3. 三方代理接口响应耗时日志
  4. swagger在线接口文档
  5. 接口注解
  6. Typescript版Mybatic插件

技术栈

  1. koa2:它是一个新的 web 框架。 由Express幕后的原班人马打造,致力于成为 web 应用和 API 开发领域中的一个更小,更富有表现力,更健壮的基石。通过利用 async 函数,Koa2 帮你丢弃回调函数,并有力地增强错误处理。Koa2 并没有捆绑任何中间件,而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。主要是它的 中间件机制洋葱模型 。使用中间件划分业务模块,使代码清晰。扩展性好,易添加,易删除。
  2. Typescript:TypeScript是微软开发的一个开源的编程语言,通过在JavaScript的基础上添加静态类型定义构建而成。TypeScript通过TypeScript编译器或Babel转译为JavaScript代码,可运行在任何浏览器,任何操作系统。这里选择它,其实是看重了它的类型注解和编译时类型检查功能。
  3. Gulp:Gulp.js 是一个自动化构建工具,开发者可以使用它在项目开发过程中自动执行常见任务。Gulp.js 是基于 Node.js 构建的,利用 Node.js 流的威力,你可以快速构建项目并减少频繁的 IO 操作。Gulp.js 源文件和你用来定义任务的 Gulp 文件都是通过 JavaScript(或者 CoffeeScript )源码来实现的。是为了监听Typescript文件变更以及mock文件快速创建功能
  4. Mockjs:生成随机数据,拦截Ajax 请求 开始 前后端分离 让前端攻城师独立于后端进行开发。
  5. Redis:一个高性能的 key-value 数据库。
  6. Mysql:一个关系型数据库管理系统。
  7. swagger-jsdoc:在线生成swagger接口文档

操作手册

项目安装

项目放置到了gitee上,可以通过以下命令进行安装:

1
git clone https://gitee.com/sunweipeng16/hewa-front.git

下载完成后,进入项目目录下,安装依赖:

1
npm install

npm的软件源是在国外服务器的,安装起来可能比较慢,你也可以使用 cnpm来安装依赖。首先要安装 cnpm,安装命令如下:

1
npm install cnpm -g --registry=https://r.npm.taobao.org

cnpm安装完后,在项目目录下执行以下命令完成依赖安装

1
cnpm install
配置代理地址

进入src/main/resources目录下,配置application_development.yml文件,修改/新增proxy.prefix和proxy.targets。

proxy.prefix:配置提供给前端的代理地址,项目根据这个地址进行代理分发。例如代理地址/hewa-front/program。hewa-front为我配置的项目名称,program为需要代理的项目域(项目域后续有用到)

proxy.targets:配置原地址与代理地址间的关系

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
# 代理相关
proxy:
prefix: '/hewa-front/agent,/hewa-front/data,/hewa-front/program' # 代理地址
targets:
/hewa-front/agent/hewa_test/(.*): # 需要匹配代理的URL地址
target: 'https://hewa-test.obs.cn-north-1.myhuaweicloud.com:443' # 目标地址
changeOrigin: true # 是否跨域
pathRewrite: # URL路径重写
'^/hewa-front/agent': ''
/hewa-front/agent/admin/(.*):
target: 'https://api-admin-uat.hewa.cn'
changeOrigin: true
pathRewrite:
'^/hewa-front/agent': ''
# /hewa-front/agent/hewaCorePosition/(.*):
# target: 'http://114.116.116.34:9007'
# changeOrigin: true
# pathRewrite:
# '^/agent': ''
# codeFormat: 'code' # 需要转化的key值
# escapeCode: # 需要转化的对照值
# '0': 200
# appendParams: # 追加参数
# 'userId': 9628
/hewa-front/agent/(.*):
target: 'https://api-pc-test4.hewa.cn'
changeOrigin: true
pathRewrite:
'^/hewa-front/agent': ''
/hewa-front/program/(.*):
target: 'https://api-pc-test4.hewa.cn'
changeOrigin: true
pathRewrite:
'^/hewa-front/program': ''
/hewa-front/pro/admin/(.*):
target: 'https://api-admin.hewa.cn'
changeOrigin: true
pathRewrite:
'^/hewa-front/pro': ''
/hewa-front/pro/(.*):
target: 'https://api.hewa.cn'
changeOrigin: true
pathRewrite:
'^/hewa-front/pro': ''
/hewa-front/data/(.*):
target: 'https://data-view.hewa.cn'
changeOrigin: true
pathRewrite:
'^/hewa-front/data': ''

1682321369751

创建mock文件

可以通过下方命令快速创建一个mock数据。其中:

URL:代表是需要mock数据的接口地址

PROJECT_DOMAIN:代表的是项目域,非必填。因为加了域的概念,即填写的话 当前的mock数据只有指定的代理代理可以访问。如果没有填写,则代表着只要匹配到,都可以访问。

1
2
3
4
创建MOCK数据步骤:
1. 修改请求URL 将指向接口的URL指向mock服务地址
2. 运行 npm run create <URL>[#<PROJECT_DOMAIN>]
3. 查询生成的文件并将定义的接口响应参数添加至文件即可
运行

可以通过以下命令进行启动,因为涉及到Typescript文件的监听,所以通过concurrently实现同时运行两个script命令:

1
npm run serve
打包/部署

通过以下打包命令进行打包,然后将target目录下的文件打包,上传服务器。解压、进入项目文件,运行启动命令(npm run prod)即可

1
2
3
4
# 打包命令
npm run build
# 生产启动命令
npm run prod

源码解析

安装脚手架:

1
2
3
4
5
6
7
8
9
10
 导入脚手架
npm install koa-generator -g
# 查看版本
koa2 -version
# 创建项目
koa2 <项目名>
# 进入项目
cd 项目
# npm
npm install

按上边的操作,简单的Koa2项目就创建完了.生成项目结构如图:

1662688013130

1
2
3
4
5
6
bin: www为项目入口,通过它引入app.js配置内容
public:公共文件夹,放一些样式,js和图片
routes:功能为分发请求
views:pug格式的文件,其内容还可以是我们常用的html

app.js和package.json是配置文件

按照上述操作就可以生成koa2项目了,接下来引入Typescript。

第一步:

1
2
# 安装typscript
npm install typescript -save-dev

第二步:

创建并配置tsconfig.json文件,可以通过tsc -init命令再当前目录创建。

第三步:

调整目录结构:

1662691173070

对,借鉴了spring boot的目录结构.至于为什么用yml文件,主要是觉得JSON格式不清晰明了,还要注意双引号的问题.

架子有了,开始填内容吧.

修改tsconfig.json文件:

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
{
"compilerOptions": {
"module": "commonjs", // 编译生成的模块系统代码
"target": "es2020", // 指定ecmascript的目标版本
"noImplicitAny": true, // 禁止隐式any类型
"outDir": "target",
"rootDir":"src/main/nodejs/cn/hewa/front",
"sourceMap": false,
"allowJs": true, // 是否允许出现js
"newLine": "LF",
"resolveJsonModule": true,
"skipLibCheck": true,
"experimentalDecorators":true, // 装饰器
"emitDecoratorMetadata":true, // 元数据
"esModuleInterop":true,
"removeComments": true,
"preserveConstEnums": true,
"typeRoots":["node_modules/@types","src/@types"],
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
]
}

这里有个取舍问题,我这里不想直接运行Typescript,所以就需要有编译动作。但每次都是先编译在启动,很烦。这里引入concurrently 来帮忙了

1
2
3
4
# 全局安装
npm install -g concurrently
# 或者项目安装
npm install concurrently --save

它支持并行执行多条命令 这样就可以保证编译和服务启动同时执行了.

参照上边的 tsconfig.json ,可以发现我是把编译后的文件放到了target目录下了。可是 resources 目录下的文件怎么copy过去呢? 肝稍微有些疼了~ 接着想办法吧。Gulp咋样?

可以通过Gulp将 resources 目录下的.yum文件/.xml文件等拷贝到target目录下.

因为只部署 target 目录下文件,需要将 package.json 拷贝到 target 目录下。 这时项目种就存在两份package.json,一个在根目录下,一个在 target 目录下.这就需要在拷贝 package.json 文件时,对文件就行修改。没错,这部分 Gulp也支持。生成好的 gulpfile.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
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import gulp from 'gulp'
import Clean from 'gulp-clean'
import Watch from 'gulp-watch'
import TS from 'gulp-typescript'
import Replace from 'gulp-replace'
import fs from 'fs'
import path from 'path'

const tsProject = TS.createProject('tsconfig.json')
let env = process.env.NODE_ENV
if (env === null || env === undefined || env === '') {
env = ''
}
tsProject.options.removeComments = env.trim() === 'production'

let rootPath = process.cwd() + '/src/main/nodejs/cn/hewa/front'
let resource = process.cwd() + '/src/main'

// 清理文件
gulp.task('clean', function () {
return gulp.src('target', { allowEmpty: true }).pipe(Clean())
})

// 拷贝package文件
gulp.task('package', function () {
// 替换根目录 及启动脚本

return gulp
.src('package.json')
.pipe(Replace(/"@ROOT": "target"/, '"@ROOT": "./"'))
.pipe(Replace(/"type": "module",/, ''))
.pipe(
Replace(
'cross-env NODE_ENV=development PORT=8084 node target/bin/www',
'cross-env NODE_ENV=development PORT=8084 node bin/www'
)
)
.pipe(
Replace(
'npm run build && cross-env NODE_ENV=development PORT=8084 ./node_modules/.bin/nodemon /bin/www',
'cross-env NODE_ENV=development PORT=8084 ./node_modules/.bin/nodemon /bin/www'
)
)
.pipe(gulp.dest('target'))
})
// 拷贝mock文件
gulp.task('mock', function () {
return gulp.src(`${rootPath}/mock/**`).pipe(gulp.dest('target/mock'))
})

// 拷贝配置文件
gulp.task('config', function () {
return gulp
.src(`${resource}/resources/**`)
.pipe(gulp.dest('target/resources'))
})

// TS
gulp.task('ts', function () {
return tsProject.src().pipe(tsProject()).js.pipe(gulp.dest('target'))
})

// 监听文件变更
gulp.task('watch', function (done) {
Watch('src/main/resources/**/*', gulp.series('config'))
Watch('package.json', gulp.series('package'))
Watch('src/main/nodejs/**/*', gulp.series('mock', 'ts'))
done()
})

/**
* 创建或更新文件
* @param {*} filePath
* @param {*} appendFilePath
* @param {*} options
*/
function createOrUpdateFile(filePath, appendFilePath, options) {
try {
// 文件存在
fs.accessSync(filePath, fs.constants.F_OK)
} catch (err) {
// 目录是否存在
let parentPath = filePath.substring(0, filePath.lastIndexOf(path.sep))
// 不存在 创建目录
if (!fs.existsSync(parentPath)) {
fs.mkdirSync(parentPath, { recursive: true })
}
// 创建文件
fs.writeFileSync(
filePath,
`{
"code": "200",
"message": "ok",
"data":""
}`
)
}
// 更新文件内容
fs.appendFileSync(
appendFilePath,
` - {
url: '${options}',
method: 'post',
disabled: false,
}
`
)
}

/**
* 创建文件
*/
gulp.task('create', function (cb) {
let [_classPath, _path, _taskName, _optionName, options] = process.argv
if (!options) {
cb()
return
}
// 解析参数
let temp_options = options.split('#')
const __dirname = path.resolve()
const basePath = path.join(
__dirname,
`src/main/nodejs/cn/hewa/front/mock${
temp_options[1] ? '/' + temp_options[1] : ''
}`
)
const filePath = path.join(basePath, `${temp_options[0]}.json`)

const appendFilePath = path.join(
__dirname,
'src/main/resources/mock_config.yml'
)
// 判断文件是否存在 存在则跳过 反之则创建
createOrUpdateFile(filePath, appendFilePath, temp_options[0])
// 发送结束信号
cb()
})

gulp.task(
'default',
gulp.series('clean', 'package', 'config', 'mock'),
function () {}
)



package.json
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
{
"name": "hewa-front",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"start": "cross-env NODE_ENV=development PORT=8084 node target/bin/www",
"dev": "npm run build && cross-env NODE_ENV=development PORT=8084 ./node_modules/.bin/nodemon /bin/www",
"prod": "cross-env NODE_ENV=development PORT=8084 pm2 start bin/www.js --name hewa-front",
"test": "echo \"Error: no test specified\" && exit 1",
"build": "set NODE_ENV=development && gulp && gulp ts",
"watch": "gulp watch",
"clean": "gulp clean",
"create": "gulp create --option ",
"serve": "concurrently \"npm:dev\" \"npm:watch\""
},
"_moduleAliases": {
"@ROOT": "target"
},
"dependencies": {
"concurrently": "^7.3.0",
"debug": "^4.1.1",
"gulp": "^4.0.2",
"gulp-typescript": "^6.0.0-alpha.1",
"html-parse-stringify": "^3.0.1",
"http-proxy-middleware": "^2.0.6",
"koa": "^2.7.0",
"koa-body": "^5.0.0",
"koa-bodyparser": "^4.2.1",
"koa-compress": "^5.1.0",
"koa-convert": "^1.2.0",
"koa-generic-session": "^2.3.0",
"koa-json": "^2.0.2",
"koa-log4": "^2.3.2",
"koa-logger": "^3.2.0",
"koa-onerror": "^4.1.0",
"koa-redis": "^4.0.1",
"koa-router": "^7.4.0",
"koa-static": "^5.0.0",
"koa-views": "^6.2.0",
"koa2-connect": "^1.0.2",
"koa2-cors": "^2.0.6",
"koa2-proxy-middleware": "0.0.4",
"koa2-request": "^1.0.4",
"koa2-swagger-ui": "^5.6.0",
"mockjs": "^1.1.0",
"module-alias": "^2.2.2",
"mysql2": "^2.3.3",
"public-ip": "^4.0.4",
"pug": "^2.0.3",
"redis": "^4.2.0",
"reflect-metadata": "^0.1.13",
"require-directory": "^2.1.1",
"sql-formatter": "^10.0.0",
"swagger-jsdoc": "^6.2.5",
"yaml": "^2.1.1"
},
"devDependencies": {
"@types/debug": "^4.1.7",
"@types/koa-bodyparser": "^4.3.7",
"@types/koa-compress": "^4.0.3",
"@types/koa-json": "^2.0.20",
"@types/koa-log4": "^2.3.3",
"@types/koa-router": "^7.4.4",
"@types/koa2-cors": "^2.0.2",
"@types/mockjs": "^1.0.6",
"@types/require-directory": "^2.1.2",
"chai": "^4.3.6",
"cross-env": "^7.0.3",
"externals-dependencies": "^1.0.4",
"gulp-clean": "^0.4.0",
"gulp-replace": "^1.1.3",
"gulp-watch": "^5.0.1",
"mocha": "^10.0.0",
"nodemon": "^1.19.1",
"supertest": "^6.2.4",
"ts-node": "^10.9.1",
"tslint": "^6.1.3",
"typescript": "^4.7.4"
}
}
下面正式进入源码解析

程序入口:app.ts

1
2
3
4
5
6
7
8
9
10
import Koa from 'koa'
import ApplicationManager from './framework/core/applicationManager'

const app: Koa = new Koa({
proxy: true,
proxyIpHeader: 'X-Real-IP',
})
// 核心管理
ApplicationManager(app)
export default app

程序初始化:applicationManager.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
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
import Koa from 'koa'
import StringUtil from '../../common/utils/stringUtil'
import middlewareConfig from '../config/middlewareConfig'
import routerConfig from '../config/routerConfig'
import mapperConfig from '../config/mapperConfig'
import annotationsConfig from '../config/annotationsConfig'

/**
* 初始化
*/
class ApplicationManager {
private _app: Koa
public static applicationManager: ApplicationManager

/**
* 构造函数
* @param app
*/
constructor(app: any) {
this._app = app
}

/**
* 单例模式
* @param app
* @returns
*/
public static getInstance(app: Koa) {
if (StringUtil.isEmpty(this.applicationManager)) {
this.applicationManager = new ApplicationManager(app)
}
return this.applicationManager
}

/**
* 加载初始配置
*/
public async register() {
// 初始化中间件
await middlewareConfig(this._app)
// 初始化Mapper.xml
await mapperConfig()
// 初始化Mapper/Service注解
await annotationsConfig()
// 初始化路由
await routerConfig(this._app)
}
}
export default async (app: Koa) => {
ApplicationManager.getInstance(app).register()
}

中间件初始化:middlewareConfig.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
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
import Koa from 'koa'
import json from 'koa-json'
import cors from 'koa2-cors'
import compress from 'koa-compress'
import koaBody from 'koa-body'
import bodyparser from 'koa-bodyparser'
import proxy from 'koa2-proxy-middleware'
import { koaSwagger } from 'koa2-swagger-ui'
import { responseInterceptor } from 'http-proxy-middleware'
import StringUtil from '../../common/utils/stringUtil'
import BasicService from '../../common/basic/basicService'
import BusiConstants from '../../common/constant/busiConstants'
import GlobalConfig from './globalConfig'
import MessagesConstants from '../../common/constant/messagesConstants'
/**
* 三方中间件初始化
*/
class MiddlewareConfig extends BasicService {
public static middlewareConfig: MiddlewareConfig
private _app: Koa
constructor(app: Koa) {
super()
this._app = app
}
/**
* 单例
* @param app
* @returns
*/
public static getInstance(app: Koa): MiddlewareConfig {
if (StringUtil.isEmpty(this.middlewareConfig)) {
this.middlewareConfig = new MiddlewareConfig(app)
}
return this.middlewareConfig
}

/**
* JSON
*/
public registerJson(): void {
this._app.use(json())
}

/**
* 跨域
*/
public registerCors(): void {
this._app.use(
cors({
origin: function (_ctx) {
return '*'
},
maxAge: 5, //指定本次预检请求的有效期,单位为秒。
credentials: true, //是否允许发送Cookie
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization', 'Accept'],
exposeHeaders: [],
})
)
}

/**
* 压缩
*/
public registerCompress(): void {
this._app.use(compress())
}

/**
* 解析POST body
*/
public registerBody(): void {
this._app.use(
bodyparser({
enableTypes: ['json', 'form', 'text'],
})
)
}

/**
* swagger
*/
public registerSwagger(): void {
this._app.use(
koaSwagger({
routePrefix: '/swagger/index.html', // 这里配置swagger的访问路径
swaggerOptions: {
url: '/swagger.json', // 这里配置swagger的文档配置URL,也就是说,我们展示的API都是通过这个接口生成的。
},
})
)
}

/**
* 添加流水号
*/
public registerTradeNo(): void {
this._app.use(async (ctx: any, next: any) => {
ctx.tradeNo = StringUtil.getTradeNo()
await next()
})
}

/**
* 解析formData数据
*/
public registerFileBody(): void {
this._app.use(async (ctx: any, next: any) => {
// 请求头
let contentType = ctx.headers[BusiConstants.CONTENT_TYPE.toLowerCase()]
// 为空 非三方代理 进行解码
if (
StringUtil.isEmpty(ctx.proxyURL) &&
StringUtil.isNotEmpty(contentType) &&
contentType.includes(BusiConstants.MULTIPART_FORM_DATA)
) {
await koaBody({
multipart: true,
})(ctx, next)
} else {
// 下一步
await next()
}
})
}

/**
* 注册异常信息
*/
public registerException(): void {
// 捕获异常
this._app.use(async (ctx: any, next: any) => {
try {
await next()
} catch (error) {
ctx.retCode = error.errorCode || error.code
ctx.error = error
ctx.body = {
message: this.getMsg(error.errorCode || error.code) || error.message,
code: error.errorCode || error.code,
}
}
})
}

/**
* 注册日志信息
*/
public registerLogger(): void {
this._app.use(async (ctx: any, next: any) => {
const start: any = new Date()
await next()
const end: any = new Date()
const ms: number = end - start
// 预检 日志不进行输出
if (ctx.method == BusiConstants.OPTIONS) {
return
}
// 简要日志 所有请求记录
this.access(ctx, ms)
// 错误日志
if (StringUtil.isNotEmpty(ctx.error)) {
this.error(ctx, ctx.error)
return
}
// 针对三方http代理
if (StringUtil.isNotEmpty(ctx.proxyURL)) {
this.http(ctx)
return
}
})
}

/**
* 注册三方代理
*/
public registerHttpProxy(): void {
this._app.use(async (ctx: any, next: any) => {
// 标志位
let flag = false
let prefix = GlobalConfig.getInstance().getConfigValue('proxy.prefix')
// 服务名称
let serverName = GlobalConfig.getInstance().getConfigValue('server.name')
// 环境判断 生产环境去掉mock数据
if (process.env.NODE_ENV !== BusiConstants.NODE_ENV_PRO) {
let mockList = GlobalConfig.getInstance().getConfigValue('mock') || []
let tempList: Array<string> = []
// 剔除disabled地址
mockList.filter((item: any) => {
if (!item.disabled) {
tempList.push(item.url)
}
})
flag = await StringUtil.isMockUrl(serverName, ctx.path, tempList)
}

// 请求地址为mock地址
if (flag) {
await next()
return
}

// 代理地址
const targets = GlobalConfig.getInstance().getConfigValue('proxy.targets')
if (StringUtil.isEmpty(targets)) {
await next()
return
}
// 循环
Object.values(targets).forEach((item: object) => {
// 默认参数
let DEFAULT_OPTIONS = this.getProxyDefaultOptions(ctx, item, prefix)
Object.assign(item, DEFAULT_OPTIONS)
})
await proxy({ targets: targets })(ctx, next)
})
}

/**
* 获取代理默认参数 获取代理参数
* @param ctx
* @param targets
* @param prefix
* @returns
*/
public getProxyDefaultOptions(
ctx: any,
targets: any,
prefix: string
): object {
let _this = this
const DEFAULT_OPTIONS = {
changeOrigin: true, // 是否支持跨域
selfHandleResponse: true, // 强制res返回
onError: function (_err: any, _req: any, res: any, _target: any) {
// 错误信息
res.writeHead(500, {
[BusiConstants.CONTENT_TYPE]: BusiConstants.APPLICATION_JSON,
})
let message = _this.getMsg(MessagesConstants.SYS_ERR_CODE)
// 返回错误信息
res.end(
JSON.stringify(
StringUtil.getResult(MessagesConstants.SYS_ERR_CODE, message)
)
)
// 错误信息
ctx.error = _err
},
onProxyReq: function (proxyReq: any, _req: any, _res: any) {
// 获取用户真实IP
proxyReq.setHeader('X-Real-IP', ctx.ip)
// 代理开始时间
ctx.proxyStart = new Date()
// 代理url地址
let prefixs = prefix.split(',')
prefixs.forEach((item: string) => {
if (ctx.href.indexOf(item) > -1) {
ctx.proxyURL = ctx.href.replace(
`${ctx.origin}${item}`,
targets['target']
)
}
})
// 内容类型
const contentType = proxyReq.getHeader(BusiConstants.CONTENT_TYPE)
ctx.contentType = contentType
// 重写请求体
const writeBody = (bodyData: any) => {
proxyReq.setHeader(
BusiConstants.CONTENT_LENGTH,
Buffer.byteLength(bodyData)
)
proxyReq.write(bodyData)
}
// 请求体
const requestBody = ctx.request.body || {}
// 是否追加参数 追加
let appendParams = targets['appendParams']
if (StringUtil.isNotEmpty(appendParams)) {
Object.assign(requestBody, appendParams)
}
// 请求体为空 不进行请求 如果传入空对象及空数组 内容长度及内容不一致 需要重写
if (
StringUtil.isEmpty(requestBody) &&
contentType &&
!contentType.includes(BusiConstants.MULTIPART_FORM_DATA)
) {
writeBody('')
return
}
// 非空判断
if (StringUtil.isEmpty(contentType)) {
return
}
//JSON 数据
if (contentType.includes(BusiConstants.APPLICATION_JSON)) {
writeBody(JSON.stringify(requestBody))
return
}
if (
contentType.includes(BusiConstants.APPLICATION_X_WWW_FORM_URLENCODED)
) {
// 表单数据
writeBody(new URLSearchParams(requestBody).toString)
return
}
},
onProxyRes: responseInterceptor(
async (responseBuffer, proxyRes, _req, res: any) => {
let result: any = responseBuffer
let proxyContentType = proxyRes.headers['content-type']
// JSON数据
if (
proxyContentType &&
(proxyContentType.indexOf('json') > -1 ||
proxyContentType.indexOf('text') > -1)
) {
const response = responseBuffer.toString(BusiConstants.UTF_8)
// 转义返回码
let escapeCode = targets['escapeCode']
// 业务结果字段
ctx.codeFormat = targets['codeFormat'] || 'code'
// 返回码
let retCode = StringUtil.getValues(ctx.codeFormat, response)
// 非空 按转义字段设置返回码
if (
StringUtil.isNotEmpty(retCode) &&
StringUtil.isNotEmpty(escapeCode) &&
StringUtil.isNotEmpty(escapeCode[retCode])
) {
result = StringUtil.setValues(
ctx.codeFormat,
escapeCode[retCode],
response
)
} else {
result = response
}
ctx.retCode = retCode || '200'
ctx.body = response
// 耗时
let end: any = new Date()
ctx.proxySpeedTime = end - ctx.proxyStart
} else {
ctx.retCode = '200'
// 耗时
let end: any = new Date()
ctx.proxySpeedTime = end - ctx.proxyStart
}
// 设置statusCode
res.statusCode = proxyRes.statusCode
// 设置statusMessage
res.statusMessage = proxyRes.statusMessage
return result
}
),
}
return DEFAULT_OPTIONS
}

/**
* 注册中间件
*/
public register(): void {
// logger
this.registerLogger()
// 捕获异常信息
this.registerException()
// 处理跨域
this.registerCors()
// 压缩
this.registerCompress()
// 添加流水号
this.registerTradeNo()
// body解压
this.registerBody()
// 三方代理
this.registerHttpProxy()
//解析formData
this.registerFileBody()
// JSON
this.registerJson()
// swagger文档 生产环境不展示
if (process.env.NODE_ENV !== BusiConstants.NODE_ENV_PRO) {
this.registerSwagger()
}
}
}

export default async (app: Koa) => {
MiddlewareConfig.getInstance(app).register()
}

annotations.ts : 注解(基于装饰器实现注解功能)

Typescript装饰器主要有类装饰器、类方法装饰器、类属性装饰器、自动访问器、getter装饰器、setter装饰器,下次详表。

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
import GlobalConfig from '../../framework/config/globalConfig'
import BusiConstants from '../constant/busiConstants'
import MessagesConstants from '../constant/messagesConstants'
import CustomException from '../exception/customException'
import MybatisMapperUtil from '../utils/mybatisMapperUtil'
import MySqlUtil from '../utils/mySqlUtil'
import StringUtil from '../utils/stringUtil'

// 接口
export interface IRoute {
method: 'get' | 'post' | 'delete' | 'put' | 'options' | 'head' | 'patch'
path: string
}

export interface IRouteConfig {
PathVariable?: {
[key: string]: string
}
RequestParam?: {
[key: string]: string
}
RequestBody?: {
index: number
}
Headers?: {
index: number
}
RequestContext?: {
index: number
}
Request?: {
index: number
}
MultipartFile?: {
index: number
}
route: IRoute
}

export interface IRouteFunction extends IRouteConfig {
(...args: Array<number | string | object>): Promise<object | string>
}

/**
* 路由工厂
* @param param
* @returns
*/
export function route({ method, path }: IRoute) {
return (target: object, propertyKey: string) => {
const routeFunction = Reflect.get(target, propertyKey)
Reflect.set(routeFunction, 'route', { path, method })
}
}
/**
* GET请求
* @param path 路径
* @returns
*/
export function GET(path: string) {
return route({ path, method: 'get' })
}

/**
* POST请求
* @param path 路径
* @returns
*/
export function POST(path: string) {
return route({ path, method: 'post' })
}
/**
* DELETE
* @param path
* @returns
*/
export function DELETE(path: string) {
return route({ path, method: 'delete' })
}

/**
* PUT
* @param path
* @returns
*/
export function PUT(path: string) {
return route({ path, method: 'put' })
}

/**
* HEAD请求
* @param path
* @returns
*/
export function HEAD(path: string) {
return route({ path, method: 'head' })
}

/**
* OPTIONS请求
* @param path
* @returns
*/
export function OPTIONS(path: string) {
return route({ path, method: 'options' })
}

/**
* PathVariable 注解 取URL内的参数
* @param key
* @returns
*/
export function PathVariable(key: string) {
return function (
target: object,
propertyKey: string,
parameterIndex: number
) {
const routeFunction = Reflect.get(target, propertyKey)
Reflect.set(routeFunction, 'PathVariable', {
...routeFunction.PathVariable,
[parameterIndex]: key,
})
}
}

/**
* RequestParam 注解
* @param key
* @returns
*/
export function RequestParam(key: string) {
return function (
target: object,
propertyKey: string,
parameterIndex: number
) {
const routeFunction = Reflect.get(target, propertyKey)
Reflect.set(routeFunction, 'RequestParam', {
...routeFunction.RequestParam,
[parameterIndex]: key,
})
}
}

/**
* RequestBody 注解
* @param key
* @returns
*/
export function RequestBody(
target: object,
propertyKey: string,
parameterIndex: number
) {
const routeFunction = Reflect.get(target, propertyKey)
Reflect.set(routeFunction, 'RequestBody', {
...routeFunction.RequestBody,
index: parameterIndex,
})
}

/**
* Request对象
* @param target
* @param propertyKey
* @param parameterIndex
*/
export function Request(
target: object,
propertyKey: string,
parameterIndex: number
) {
const routeFunction = Reflect.get(target, propertyKey)
Reflect.set(routeFunction, 'Request', {
...routeFunction.Request,
index: parameterIndex,
})
}

/**
* 请求头
* @param target
* @param propertyKey
* @param parameterIndex
*/
export function Headers(
target: object,
propertyKey: string,
parameterIndex: number
) {
const routeFunction = Reflect.get(target, propertyKey)
Reflect.set(routeFunction, 'Headers', {
...routeFunction.Headers,
index: parameterIndex,
})
}

/**
* 请求上下文
* @param target
* @param propertyKey
* @param parameterIndex
*/
export function RequestContext(
target: object,
propertyKey: string,
parameterIndex: number
) {
const routeFunction = Reflect.get(target, propertyKey)
Reflect.set(routeFunction, 'RequestContext', {
...routeFunction.RequestContext,
index: parameterIndex,
})
}

/**
* 文件注解
* @param target
* @param propertyKey
* @param parameterIndex
*/
export function MultipartFile(
target: object,
propertyKey: string,
parameterIndex: number
) {
const routeFunction = Reflect.get(target, propertyKey)
Reflect.set(routeFunction, 'MultipartFile', {
...routeFunction.MultipartFile,
index: parameterIndex,
})
}

/**
* 控制器 - 装饰器
* @param path 路径
* @returns
*/
export function Controller(path: string) {
return (target: Function) => {
Reflect.set(target, '_path', path)
}
}

/**
* Service - 装饰器
* @param name
* @returns
*/
export function Service(name: string) {
return (target: Function) => {
Reflect.set(target, '_name', name)
}
}

/**
* Autowired
* @param target
* @param propertyKey
*/
export function Autowired(target: object, propertyKey: string) {
// 资源文件
let resource = GlobalConfig.getInstance().getValues(
propertyKey,
BusiConstants.ANNOTATIONS
)
Object.defineProperty(target, propertyKey, {
get: () => {
return resource
},
})
}

/**
* Resource
* @param name
* @returns
*/
export function Resource(target: object, propertyKey: string) {
// 资源文件
let resource = GlobalConfig.getInstance().getValues(
propertyKey,
BusiConstants.ANNOTATIONS
)
Object.defineProperty(target, propertyKey, {
get: () => {
return resource
},
})
}

/**
* Mapper 装饰器
* @param name
* @returns
*/
export function Mapper(name: string) {
return (target: Function) => {
Reflect.set(target, '_name', name)
}
}
/**
* 包裹
* @param target
* @param propertyKey
* @param descriptor
*/
function warper(
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
let method = descriptor.value
descriptor.value = async function () {
let context = arguments[arguments.length - 1]
// 开启事务
if (StringUtil.isNotEmpty(context) && context.openTransaction) {
const sql = await MybatisMapperUtil.getInstance().getStatementSql(
`${target.constructor.name}.${propertyKey}`,
`${target.constructor.name}`,
arguments[0]
)
if (/^update|insert/gi.test(sql)) {
let sqls = context.sqls || []
sqls.push(sql)
Object.defineProperty(context, 'sqls', {
get: function () {
return sqls
},
})
} else {
const data = await MybatisMapperUtil.getInstance().getQueryData(
`${target.constructor.name}.${propertyKey}`,
`${target.constructor.name}`,
arguments[0]
)
let result = method.apply(this, [...arguments, data])
return result === undefined ? data : result
}
} else {
const data = await MybatisMapperUtil.getInstance().getQueryData(
`${target.constructor.name}.${propertyKey}`,
`${target.constructor.name}`,
arguments[0]
)
let result = method.apply(this, [...arguments, data])
return result === undefined ? data : result
}
}
}

function sqlWarper(
target: object,
propertyKey: string,
descriptor: PropertyDescriptor,
sql: string,
sqlType: string
) {
let method = descriptor.value
descriptor.value = async function () {
let context = arguments[arguments.length - 1]
// 生成sql
if (
StringUtil.isNotEmpty(context) &&
context.openTransaction &&
/^update|insert/gi.test(sql)
) {
const data = await MybatisMapperUtil.getInstance().formatSql(
sql,
arguments[0]
)

let sqls = context.sqls || []
sqls.push(data)
Object.defineProperty(context, 'sqls', {
value: sqls,
})
} else {
const data = await MybatisMapperUtil.getInstance().getQuerySQL(
sql,
sqlType,
arguments[0]
)
let result = method.apply(this, [...arguments, data])
return result === undefined ? data : result
}
}
}

/**
* SQL查询 不区分类型
* @param target
* @param propertyKey
* @param descriptor
*/
export function Query(
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
warper(target, propertyKey, descriptor)
}

/**
* 查询
* @param parameter
* @returns
*/
export function Select(parameter: string) {
return (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) => {
sqlWarper(target, propertyKey, descriptor, parameter, 'select')
}
}

/**
* 插入
* @param parameter
* @returns
*/
export function Insert(parameter: string) {
return (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) => {
sqlWarper(target, propertyKey, descriptor, parameter, 'insert')
}
}

/**
* 更新
* @param parameter
* @returns
*/
export function Update(parameter: string) {
return (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) => {
sqlWarper(target, propertyKey, descriptor, parameter, 'update')
}
}

/**
* 删除
* @param parameter
* @returns
*/
export function Delete(parameter: string) {
return (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) => {
sqlWarper(target, propertyKey, descriptor, parameter, 'delete')
}
}

/**
* 事务处理
* @param target
* @param propertyKey
* @param descriptor
*/
export function Transaction(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
let method = descriptor.value
descriptor.value = async function () {
// 读取最后一个参数 判断是否为对象
let context = arguments[arguments.length - 1]
// 对象 将openTransaction 设置为true
if (typeof context === 'object') {
// 事务
Object.defineProperty(context, 'openTransaction', {
get: function () {
return true
},
})
}

const data = await method.apply(this, arguments)
if (StringUtil.isNotEmpty(context) && StringUtil.isNotEmpty(context.sqls)) {
// 批量执行sql
await MySqlUtil.getInstance()
.transaction(context.sqls)
.catch((e) => {
throw new CustomException(
'sql is error',
MessagesConstants.DATABASE_ERR_CODE,
MessagesConstants.DATABASE_SQL_ERR_CODE,
e
)
})
}
return data
}
}

controllerRegister.ts Controller注册到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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
import fs from 'fs'
import path from 'path'
import Router from 'koa-router'
import StringUtil from '../utils/stringUtil'
import FileUtil from '../utils/fileUtil'
import { IRouteFunction, IRouteConfig } from './annotations'
import BusiConstants from '../constant/busiConstants'

/**
* Controller注册到Router
*/
class ControllerRegister {
// 私有方法
private _router: Router
private _path: any

public get router() {
return this._router
}

/**
* 构造函数
* @param router
*/
constructor(router: Router, path: any) {
this._router = router
this._path = path
}

/**
* 注册控制器到路由
* @returns
*/
public async register(): Promise<Router> {
// 解析导入controller
const controllers = await this.scanController()
if (StringUtil.isEmpty(controllers)) {
return
}
// 控制器工厂
const routeFactory = this.routeFactory(this._router)
controllers.forEach((item) => {
routeFactory(item)
})
return this._router
}

/**
* 实际函数执行
* @param fn
* @param routeConfig
* @returns
*/
private callBackFactory(
fn: IRouteFunction,
routeConfig: IRouteConfig,
baseUrl: string | undefined = undefined
) {
return async (ctx: Router.RouterContext) => {
// 参数列表
const args: Array<number | string | object> = []

// 获取参数
const requestBody = Object.assign({}, ctx.request.query, ctx.request.body)
// requestBody
const requestBodyConfig = routeConfig.RequestBody
if (StringUtil.isNotEmpty(requestBodyConfig)) {
this.setArgument(args, requestBodyConfig.index, requestBody)
}
// requestParam
const requestParamConfig = routeConfig.RequestParam
if (StringUtil.isNotEmpty(requestParamConfig)) {
const keys = Object.keys(requestParamConfig)
keys.forEach((key) => {
this.setArgument(
args,
parseInt(key, 10),
requestBody[requestParamConfig[key]]
)
})
}
// pathVariable
const pathVariableConfig = routeConfig.PathVariable
if (StringUtil.isNotEmpty(pathVariableConfig)) {
const keys = Object.keys(pathVariableConfig)
// 索引对象<索引位置,参数位置>
const sortKey = new Map<number, string>()
// url全路径
let url = StringUtil.join(baseUrl, routeConfig.route.path)
// 数据分割
let routePaths: string[] = url.split('/')
// 生成正则表达式
keys.forEach((key) => {
const item = pathVariableConfig[key]
const index = routePaths.indexOf(`:${item}`)
if (index > -1) {
sortKey.set(index, key)
}
})
// 请求URL
const paths: string[] = ctx.path.split('/')
// 设置参数
for (let [key, value] of sortKey) {
this.setArgument(args, parseInt(value, 10), paths[key])
}
}
// request
const requestConfig = routeConfig.Request
if (StringUtil.isNotEmpty(requestConfig)) {
this.setArgument(args, requestConfig.index, ctx.request)
}
// headers
const headersConfig = routeConfig.Headers
if (StringUtil.isNotEmpty(headersConfig)) {
this.setArgument(args, headersConfig.index, ctx.request.headers)
}
// MultipartFile
const multipartFileConfig = routeConfig.MultipartFile
if (StringUtil.isNotEmpty(multipartFileConfig)) {
this.setArgument(args, multipartFileConfig.index, ctx.request.files)
}
// RequestContext
const requestContextConfig = routeConfig.RequestContext
if (StringUtil.isNotEmpty(requestContextConfig)) {
this.setArgument(args, requestContextConfig.index, ctx)
} else {
this.setArgument(args, args.length, ctx)
}

ctx.body = (fn && (await fn(...args))) || StringUtil.getResult()
}
}

/**
*
* @param args
* @param key
* @param value
* @returns
*/
private setArgument(
args: Array<number | string | object>,
key: any,
value: any
): void {
// 为空
if (StringUtil.isEmpty(key)) {
return
}
// 设置对应索引位置的值
args[key] = value || null
}

/**
* 抽离函数工厂
* @param Controller
* @returns
*/
private routeFunctionFactory(
Controller: Function,
baseUrl: string | undefined = undefined
) {
const controller = Reflect.construct(Controller, [])
return (routeFunction: IRouteFunction) => {
// callback函数
const fn = routeFunction.bind(controller)
const routeCofnig: IRouteConfig = { ...routeFunction }
return this.callBackFactory(fn, routeCofnig, baseUrl)
}
}

/**
* 控制器生成路由工厂
* @returns
*/
private routeFactory(router: Router): Function {
return (Controller: Function) => {
if (StringUtil.isEmpty(Controller)) {
return
}
// 是否含有controller注解
const baseUrl = Reflect.get(Controller, '_path')
// 为空 返回
if (StringUtil.isEmpty(baseUrl)) {
return
}
// 读取所有属性名
const routeFunctionKeys: (string | number | symbol)[] = Reflect.ownKeys(
Controller.prototype
)
const routeFunctionFactory = this.routeFunctionFactory(
Controller,
baseUrl
)

routeFunctionKeys.forEach((item) => {
if (item === 'constructor') {
return
}
const routeFunction: IRouteFunction = Reflect.get(
Controller.prototype,
item
)

if (
StringUtil.isEmpty(routeFunction) ||
StringUtil.isEmpty(routeFunction.route)
) {
return
}

// 解构 获取路由信息
const { path, method } = routeFunction.route
// 组装URL地址 注解拼接
const url = StringUtil.join(baseUrl, path)
// 函数
const fn = routeFunctionFactory(routeFunction)
// 注入路由
router[method](url, fn)
})
}
}

/**
* 扫描所有的Controller,并解析导入
*/
private async scanController(): Promise<Array<Function>> {
let controllers: Array<Function> = []
// 默认路径
let address = StringUtil.trim(
this._path,
StringUtil.getAbsoluteAddress(BusiConstants.CONTROLLER_PATH)
)
// 递归查找所有文件
await this.recursionRequire(address, controllers)
return controllers
}

/**
* 递归导入所有Controller
* @param url
* @param controllers
*/
private async recursionRequire(
url: string,
controllers: Array<Function>
): Promise<void> {
const fileNames = fs.readdirSync(url)
// 空判断
if (StringUtil.isEmpty(fileNames)) {
return
}
// 遍历
for (const name of fileNames) {
// 拼接参数
const _url = path.join(url, name)
// 是否为文件夹
if (await FileUtil.isDirectory(_url)) {
// 递归
await this.recursionRequire(_url, controllers)
} else {
// 导入
const objClass = await require(_url)
if (
typeof objClass === 'object' &&
Object.prototype.toString.call(objClass).toLowerCase() ===
'[object object]' &&
objClass.default
) {
controllers.push(objClass.default)
}
}
}
}
}
export default async (router: Router, path: String | undefined = undefined) => {
return new ControllerRegister(router, path).register()
}

annotationsRegister.ts : 注解注册(@Service/@Autowired/@Resource/@Mapper/@Query/@Select/@Insert/@Update/@Delete/@Transaction)

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
import fs from 'fs'
import path from 'path'
import StringUtil from '../utils/stringUtil'
import FileUtil from '../utils/fileUtil'
import BusiConstants from '../constant/busiConstants'
import GlobalConfig from '../../framework/config/globalConfig'

/**
* Annotations注册
*/
class AnnotationsRegister {
// 私有方法
private _path: any

/**
* 构造函数
* @param path
*/
constructor(path: any) {
this._path = path
}

/**
* 挂载注解到全局变量
* @returns
*/
public async register(): Promise<void> {
// 解析导入annotations
const annotations = await this.scanAnnotations()
// 数据校验
if (StringUtil.isEmpty(annotations)) {
return
}
// 全局
GlobalConfig.getInstance().setGlobalValue(
BusiConstants.ANNOTATIONS,
annotations
)
}

/**
* 扫描所有注解,并解析实例化导入
* @returns
*/
private async scanAnnotations(): Promise<any> {
let annotations: any = {}
// 默认路径
let address = StringUtil.getAbsoluteAddress(this._path)
// 递归查找所有文件
await this.recursionRequire(address, annotations)
return annotations
}

/**
* 递归导入所有注解
* @param url
* @param annotations
*/
private async recursionRequire(url: string, annotations: any): Promise<void> {
const fileNames = fs.readdirSync(url)
// 空判断
if (StringUtil.isEmpty(fileNames)) {
return
}
// 遍历
for (const name of fileNames) {
// 拼接参数
const _url = path.join(url, name)
// 是否为文件夹
if (await FileUtil.isDirectory(_url)) {
// 递归
await this.recursionRequire(_url, annotations)
} else {
// 导入
const objClass = await require(_url)
if (
typeof objClass === 'object' &&
Object.prototype.toString.call(objClass).toLowerCase() ===
'[object object]' &&
objClass.default &&
StringUtil.isNotEmpty(objClass.default._name)
) {
annotations[objClass.default._name] = new objClass.default()
}
}
}
}
}
export default async (path: String) => {
return new AnnotationsRegister(path).register()
}

mapperRegister.ts : 扫描mapper.xml文件

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
import fs from 'fs'
import path from 'path'
import StringUtil from '../utils/stringUtil'
import FileUtil from '../utils/fileUtil'
import BusiConstants from '../constant/busiConstants'
import GlobalConfig from '../../framework/config/globalConfig'
import MybatisMapperUtil from '../utils/mybatisMapperUtil'

/**
* Mapper注册
*/
class MapperRegister {
// 私有方法
private _path: any

/**
* 构造函数
* @param path
*/
constructor(path: any) {
this._path = path
}

/**
* 挂载mapper.xml到全局变量
* @returns
*/
public async register(): Promise<void> {
// 解析导入xml
const mappers = await this.scanMapper()
// 数据校验
if (StringUtil.isEmpty(mappers)) {
return
}
// 全局
GlobalConfig.getInstance().setGlobalValue(BusiConstants.MAPPER, mappers)
}

/**
* 扫描所有mapper.xml
* @returns
*/
private async scanMapper(): Promise<any> {
let mappers: any = {}
// 默认路径
let address = StringUtil.getAbsoluteAddress(this._path)
// 递归查找所有文件
await this.recursionParse(address, mappers)
return mappers
}

/**
* 递归解析所有Mapper.xml
* @param url
* @param mappers
*/
private async recursionParse(url: string, mappers: any): Promise<void> {
const fileNames = fs.readdirSync(url)
// 空判断
if (StringUtil.isEmpty(fileNames)) {
return
}
// 遍历
for (const name of fileNames) {
// 拼接参数
const _url = path.join(url, name)
// 是否为文件夹
if (await FileUtil.isDirectory(_url)) {
// 递归
await this.recursionParse(_url, mappers)
} else {
// 解析
let mapper = await MybatisMapperUtil.getInstance().mapperConfig(_url)
if (
StringUtil.isNotEmpty(mapper) &&
StringUtil.isNotEmpty(Object.keys(mapper))
) {
let key = Object.keys(mapper)[0]
mappers[key] = mapper[key]
}
}
}
}
}
export default async (path: String) => {
return new MapperRegister(path).register()
}