参考
- pro git
- node api
- npm
- ECMAScript 6 入门
- express 4.x api
- 数据库设计规范(三大范式)
- MySQL数据库设计规范
- What is an Entity Relationship Diagram (ERD)?
- SQL 语法一览
- RESTful-API-and-GraphQL-API
- You-Dont-Need-Lodash-Underscore
- The Package Manager for Windows: choco and scoop
- development-environment-manual
- bluebird
- nodejs异步控制「co、async、Q 、『es6原生promise』、then.js、bluebird」有何优缺点?最爱哪个?哪个简单?
- ES next中async/await proposal实现原理是什么?
- TCP-IP-HTTP-QUIC-Axios
资源
- resources-api
- submissions-api
- challenge-api
- draw.io
- nodejs
- sequelize
- sqlite3
- typescript
- nestjs
- typeorm
- mysql
- docker
- scoop
- backpack
- postman
- cross-env
- bluebird
- joi
- cors
- config
- is
- lodash
- HttpStatus
- bodyParser
- express
前言
之前在 topcoder 写前端混过一段时间,对找真实世界的项目代码进行 Dissect 算是有点经验,
对着这个 ’蹭‘ 来的真实项目 Dissect 一番, 实践经验这都不是事。
这篇文章就是记录 Dissect 与 Refactor 过程的些许经验。
This is the way
首先要记录的是, topcoder 上的项目(以下简称: 项目)开发流程。
项目开发流程分为以下四个阶段:
- 设计阶段
- 开发阶段
- review 阶段
- 反馈阶段
具体流程是:
先是某些组织(公司)在 topcoder 上发布需求文档, 然后 topcoder 上的设计人员根据需求文档设计开发所需的设计文档,
这些设计文档一般是 ER Diagram.png (ER 图) 和 swagger.yaml(api 文档),(前端项目的设计文档会有所不同) 此为设计阶段。
然后开发人员再根据这些设计文档以及需求文档所要求的技术栈,完成项目的编码阶段,
编码完成后需要将使用的 postman 导出为 json 文档方便评审人员导入测试, 还需要编写 Verification.md (告知评审人员如何验证项目的文档) 此为开发阶段。
开发完成后再交由评审人员评审, 此为 review 阶段。
review 完成后, 如有要修改的地方则开发人员响应要求进行修改, 如此这般这个项目就算是完成。 此为反馈阶段。
这样一套流程下来, 才是真正的流水线工程, 职责分明, 分工明确, 所以效率很高。
这让我想起了 DDD (Domain-Driven Design 领域驱动设计), 说不得那些发布在 topcoder 上的需求文档就是领域层处理后的产物。
与那牢厂所谓的狼性文化一对比, 呵呵。
我之所以能了解的如此细节, 正是因为我 ‘蹭’ 到了一份源码, 由于保密协议不方便透露业务实现细节,不过框架的选型与应用都是开源的,
这个项目使用的是 nodejs + express + sequelize + sqlite3
我会对这份 ‘蹭’ 到源码进行一些 ‘优化’, 并使用 typescript + nestjs + typeorm + mysql + docker 进行重构,
以此来积累我的实践经验, 所以这篇文章就是我对其操作过程的一些笔记.
除了这个项目外, 我在 topcoder 团队的 github 帐号 下找到了以下几个开源的项目
除了 ORM 框架与数据库外, 项目架构与我手中的这个, 几乎是相同的。
前置知识
理解最重要, 切记切记
数据库设计规范
ER 图
ER 图设计工具
Dissect
Dissect 一个别人写的项目其实是比较枯燥的, 所以顺便做一些改动, 在证明自己真的读懂了之外, 添增些许乐趣, 让自己有兴趣持续做下去也算很重要的事情。
这里以 challenge-api 为例
Dissect 一个 node 项目的步骤如下:
- 读 package.json devDependencies 和 dependencies, 然后在 npm 依个检索, 搞清楚这些依赖包都是做什么的
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{
"name": "topcoder-challenges-api",
"version": "1.0.0",
"description": "TopCoder Challenges V5 API",
"main": "app.js",
"scripts": {
"start": "node app.js",
"lint": "standard",
"lint:fix": "standard --fix",
"init-es": "node src/init-es.js",
"init-db": "node src/init-db.js",
"sync-es": "node src/scripts/sync-es.js",
"drop-tables": "node src/scripts/drop-tables.js",
"create-tables": "node src/scripts/create-tables.js",
"seed-tables": "node src/scripts/seed-tables.js",
"view-data": "node src/scripts/view-data.js",
"view-es-data": "node src/scripts/view-es-data.js",
"test": "mocha --require test/prepare.js -t 20000 test/unit/*.test.js --exit",
"e2e": "mocha --require test/prepare.js -t 20000 test/e2e/*.test.js --exit",
"test:cov": "nyc --reporter=html --reporter=text npm test",
"e2e:cov": "nyc --reporter=html --reporter=text npm run e2e"
},
"author": "TCSCODER",
"license": "none",
"devDependencies": {
"chai": "^4.2.0",
"chai-http": "^4.2.1",
"mocha": "^6.1.4",
"mocha-prepare": "^0.1.0",
"nyc": "^14.0.0",
"standard": "^12.0.1"
},
"dependencies": {
"aws-sdk": "^2.466.0",
"axios": "^0.19.0",
"bluebird": "^3.5.1",
"body-parser": "^1.15.1",
"config": "^3.0.1",
"cors": "^2.7.1",
"dynamoose": "^1.8.0",
"elasticsearch": "^16.1.1",
"express": "^4.15.4",
"express-fileupload": "^1.1.4",
"express-interceptor": "^1.2.0",
"get-parameter-names": "^0.3.0",
"http-aws-es": "^6.0.0",
"http-status-codes": "^1.3.0",
"joi": "^14.0.0",
"jsonwebtoken": "^8.3.0",
"lodash": "^4.17.11",
"moment": "^2.24.0",
"tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.6.2",
"topcoder-bus-api-wrapper": "topcoder-platform/tc-bus-api-wrapper.git",
"uuid": "^3.3.2",
"winston": "^3.1.0"
},
"standard": {
"ignore": [
"mock-api"
],
"env": [
"mocha"
]
},
"engines": {
"node": "10.x"
}
}
从这一行 "standard": "^12.0.1" 就可以看出这个作者是很有品味的人, 这样的话, 代码规范就不用改了
看到这一行 "lodash": "^4.17.11", 我就知道有可以改动的地方了,原因在这里,You-Dont-Need-Lodash-Underscore
其实也不是什么大问题, 但是丑啊!
继续读 package.json, 这次是读 script 部分, 找到项目的入口文件, 然后读入口文件
1
2
3"scripts": {
"start": "node app.js",
},入口文件太长就不贴了, 贴个链接: https://sourcegraph.com/github.com/topcoder-platform/challenge-api/-/blob/app.js
可以看出这是一个原生 ES5 项目, 其实对于这样的项目 ES5 完全足够了, 但是没 ES6 好看 啊!!这一步是读 README.md 文档, 按照文档要求确保开发环境符合要求, 快速搭建 node 开发环境请参考: development-environment-manual
确保环境符合要后就把项目 git clone 到本地
然后先npm install安装依赖,再npm run start确保项目是可以运行起来的。这一步是看 Verification.md 文档, 假想自己是评审人员需要评审这个项目一样,
先把项目评审一下, 了解项目的功能有哪些, 这对接下来的深入源码很有帮助。
在这一步我发现需要将 postman json 文件导入 postman 来测试 api, 所以需要先下载一个 postmanscoop install postman然后打开 postman 将 docs 目录下的 postman json 文件导入进去
这里用到了 scoop 包管理器, 安装请参考: The Package Manager for Windows: choco and scoop 使用则请读 scoop 官网文档: scoop。上面阅读 package.json 确认了两个可以更改的地方
一个是要将其改成 ES6 项目, 一个是要去除 lodash 依赖。
ES5 -> ES6 lodash
我现在使用的 node 版本是最新的长期支持版本 12.14.0, node 在 13.2.0 版本开始支持不带标志的 ES modules,
但是,该实现仍是实验性的, 具体请看: https://nodejs.org/api/esm.html#esm_ecmascript_modules
因此我要使用 ES6 的话, 首先要下载一个 backpack 库来帮我编译 ES modules 到 CommonJS.
backpack 就是将前端的 webpack 应用到了后端。 先将 backpack README.MD 文档 读一遍 顺便点个 star
然后就可以开始使用了:
首先将其安装到开发时依赖
npm install backpack -D然后 touch 一个 backpack.config.js 配置文件
touch 是 liunx 系统下新建文件的命令, 在 windows 系统中需要使用 scoop 安装 touchscoop install touchtouch backpack.config.js如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21const paths = {
development: './app.js',
production: './app.js',
initDB: './src/init-db.js',
testData: './src/test-data.js',
viewData: './src/view-data.js'
}
const mainPath = paths[process.env.NODE_ENV_PRIVATE]
module.exports = {
webpack: (config, options, webpack) => {
// Perform customizations to config
// Important: return the modified config
// changes the name of the entry point from index -> main.js
config.entry.main = [mainPath]
return config
}
}修改 package.json script 如下
1
2
3
4
5
6"start": "npm run dev",
"dev": "cross-env NODE_ENV_PRIVATE=development backpack dev",
"build": "cross-env NODE_ENV_PRIVATE=production backpack build",
"init-db": "cross-env NODE_ENV_PRIVATE=initDB backpack dev",
"test-data": "cross-env NODE_ENV_PRIVATE=testData backpack dev",
"view-data": "cross-env NODE_ENV_PRIVATE=viewData backpack dev",这里我们需要使用环境变量来切换入口文件, 所以需要下载一个可以跨平台的设置环境变量的命令行工具: cross-env
添加一个开发依赖:npm install cross-env -D现在可以在项目中使用 ES6 Modules 了, 可以一边读代码一边修改
如: 在入口文件 app.js 中 将require('./app-bootstrap')改为import './app-bootstrap'
这是 app.js 第一行代码, 它执行了当前目录下的app-bootstrap.js, 顾名思义, 这是一个初始化项目的文件。
打开 app-bootstrap.js 内容如下:1
2
3
4
5import bluebird from 'bluebird'
import Joi from 'joi'
global.Promise = bluebird
Joi.id = () => Joi.string().required()可以看到这里声明了一个全局变量 Promise 并指向了导入的 bluebird 库
看样子这是一个实现了 Promise 的库, 一开始我准备将其删掉, 因为我已经使用 backpack 将这个项目改为 ES6 项目了
而 ES6 有原生实现的 Promise 了, 没必要再引入这个第三方 bluebird,
但是后来我查了下这个 bluebird
bluebird 作者说Bluebird is a fully featured promise library with focus on innovative features and performance
关键字 performance 然后我就去知乎查了下这个 bluebird 与 ES6 原生 Promise 的区别:
- nodejs异步控制「co、async、Q 、『es6原生promise』、then.js、bluebird」有何优缺点?最爱哪个?哪个简单?
- ES next中async/await proposal实现原理是什么?
于是 在 github 上点了个 star 把 bluebird 留了下来。
而 joi 这个库, 它的作者是这样说的:
The most powerful data validation library for JS
数据验证库,没什么好说的。 点个 star 然后开始下一步。
- 继续看 app.jsis 类型断言库, 用来替换 lodash 的 类型断言函数
1
2
3
4
5
6
7
8import * as _ from 'lodash'
import is from 'is'
import config from 'config'
import express from 'express'
import bodyParser from 'body-parser'
import cors from 'cors'
import logger from './src/common/logger'
import HttpStatus from 'http-status-codes'
lodash JavaScript 实用程序库
config 配置文件
express web 框架
bodyParser 请求体解析中间件
cors (Cross-Origin Resource Sharing) 跨域资源共享中间件
logger 日志
HttpStatus 状态码
没什么好说的 继续
1 | // setup express app |
同样没啥好说的, 生成一个 express 实体指向 app, 然后就是应用一些中间件, 设置了一个 express 变量 port.
继续看 app.js
1 | // Register routes |
这里导入了一个路由注册方法, 将 express 的实例 app 给传递了进去
- 现在可以看看 app-routes.js 搞清楚路由是如何被注册的可以看到这里,我将 lodash 的方法改写成了原生的 ES6 写法
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/**
* Configure all routes for express app
*/
import HttpStatus from 'http-status-codes'
import helper from './src/common/helper'
import auth from './src/common/auth'
import errors from './src/common/errors'
import routes from './src/routes'
import multer from 'multer'
import autoReap from 'multer-autoreap'
import * as Controllers from './src/controllers'
const upload = multer({ dest: './upload' })
/**
* Configure all routes for express app
* @param app the express app
*/
export default app => {
// auto remove uploaded files
app.use(autoReap)
// Load all routes
for (const [path, verbs] of Object.entries(routes)) {
for (const [verb, def] of Object.entries(verbs)) {
const method = Controllers[def.controller][def.method] // eslint-disable-line
if (!method) {
throw new Error(`${def.method} is undefined`)
}
const actions = []
actions.push((req, res, next) => {
req.signature = `${def.controller}#${def.method}`
next()
})
// Authentication
if (!def.public) {
actions.push(auth())
actions.push((req, res, next) => {
if (def.adminOnly && !req.authUser.isAdmin) {
next(
new errors.ForbiddenError(
"You don't have rights to perform this action!"
)
)
} else {
next()
}
})
}
if (def.file) {
actions.push(upload.single(def.file))
}
actions.push(method)
app[verb](path, helper.autoWrapExpress(actions))
}
}
// Check if the route is not found or HTTP method is not supported
app.use('*', (req, res) => {
const route = routes[req.baseUrl]
if (route) {
res.status(HttpStatus.METHOD_NOT_ALLOWED).json({
message: 'The requested HTTP method is not supported.'
})
} else {
res.status(HttpStatus.NOT_FOUND).json({
message: 'The requested resource cannot be found.'
})
}
})
}
这个 app-routes.js 的主要作用是将 ‘./src/controllers’ 中定义的 controllers 与 ‘./src/routes’ 中定义的 routes 给对应上
打开 ‘./src/routes’ 可以看到这就是一个中心化的 routes, 这里面定义了请求路径与请求方法以及其对应的 controller 和 对应 controller 中的 method, 还有一些作用域和权限相关的选项。
到这一步, 就可以知道这个项目所使用的架构就是大名鼎鼎的 MVC 架构。
知道 routes path 如何与 controller 中 method 绑定后, 就可以着手查看其他文件。
app-routes.js文件中使用了很多 ‘/src/common’ 下的文件, 可以从这里开始了解抽取的公用逻辑函数。
- challenge-api 的 ‘/src/common’ 下有三个文件
- error.js
- helper.js
- logger.js
error.js 处理程序抛出的异常, logger.js 纪录程序的运行日志, 而 helper.js 的用途就比较复杂了, 应用程序中所有频繁出现的,可抽取的公用逻辑都可以集中放在这个文件中, 以降低代码冗余.
这里我除了将 lodash 的方法用 ES6 原生方法替代外, 还对 error.js 进行了重构,你可以看到, 原先的error.js 中定义了一个创建各种 http 异常的工厂函数, 这个工厂函数创造一些继承自 Error 的 HTTP 异常类, 继承的实现是使用的 node 自带的 util.inherits 方法, ES6 有原生的 class 语法, 所以我用 class 语法重构了这一部分, 如下:可以看到 js 是真的非常简洁和灵活, 要是用 java 来写不知道要罗嗦成什么样, 还美名其曰: 设计模式。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/**
* This file defines application errors
/**
*
*
* @class ErrorFactory
*/
class ErrorFactory {
createCustomError (name, statusCode) {
/**
* Helper Class to create generic error object with http status code
* @param {String} name the error name
* @param {Number} statusCode the http status code
* @returns {Class} the error constructor
*/
class CustomError extends Error {
constructor (message = name, cause = statusCode) {
// Calling parent constructor of base Error class.
super(message, statusCode)
// Saving class name in the property of our custom error as a shortcut.
this.name = name
// Capturing stack trace, excluding constructor call from it.
Error.captureStackTrace(this, this.constructor)
this.message = message
this.cause = cause
this.httpStatus = statusCode
}
}
return CustomError
}
}
const createCustomError = new ErrorFactory().createCustomError
export default {
BadRequestError: createCustomError('BadRequestError', 400),
UnauthorizedError: createCustomError('UnauthorizedError', 401),
ForbiddenError: createCustomError('ForbiddenError', 403),
NotFoundError: createCustomError('NotFoundError', 404),
ConflictError: createCustomError('ConflictError', 409)
}
将 ‘/src/common’ 下的文件都读一遍后就可以回头继续读 app.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// The error handler
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
logger.logFullError(err, req.signature || `${req.method} ${req.url}`)
const errorResponse = {}
const status = err.isJoi
? HttpStatus.BAD_REQUEST
: err.httpStatus || HttpStatus.INTERNAL_SERVER_ERROR
if (is.array(err.details) && err.isJoi) {
err.details.forEach(e => {
if (e.message) {
if (is.undef(errorResponse.message)) {
errorResponse.message = e.message
} else {
errorResponse.message += `, ${e.message}`
}
}
})
}
if (is.undef(errorResponse.message)) {
if (err.message && status !== HttpStatus.INTERNAL_SERVER_ERROR) {
errorResponse.message = err.message
} else {
errorResponse.message = 'Internal server error'
}
}
res.status(status).json(errorResponse)
})
// if (!module.parent) {
app.listen(app.get('port'), () => {
logger.info(`Express server listening on port ${app.get('port')}`)
})
// }
export default {
expressApp: app
}这里自定义了一个集中处理错误的中间件, 没什么好说的。
然后就可以看下在 ‘src’ 根目录和 ‘src/script’ 目录下其他入口文件, 这里是一些初始化数据库 创建表 同步表 查看表数据的一些 script
由于这个 challenge-api 项目使用的是 AWS 云服务, 数据库是云数据库 Amazon DynamoDB, 而我手中这个是本地的 sqlite3,
所以没什么好说的, 读一遍 AWS 文档 就能看懂。最后一步就是看核心的 ‘src/models’ ‘src/controllers’ ‘src/services’
如果你看过 Verification.md 文档, 然后做过这一步: 假想自己是评审人员需要评审这个项目
你就能很容易的借助 postman 一边测试 api 一边理解这部分的业务逻辑了。
确保项目中的所有 lodash 依赖剔除后, 删除 lodash 依赖 npm uninstall lodash 然后重新 npm run start 确保项目没有因此改动而出现错误。
转型计划之初级后端上篇: Dissect 到此结束, 下一篇是使用 typescript + nestjs + typeorm + mysql + docker Refactor.
this is the way