本文是对之前 KOA2系列 文章的一次总结、升级、优化, 也是基于 TypeScript 对之前 Web 框架的重写.
本文介绍的 KOA Web 框架特点:
- 基于 JavaScript 超集 TypeScript 重写
- Web Session 存储于 redis 缓存
- 引入 ORM 框架 node-orm2 管理 models 对象, 本文采用 mysql 数据库
- 引入数据库迁移管理工具 migrate-orm2
- 参考 Django 路径设计思路, 重新设计文件目录
- 使用 PM2 管理守护 Web 进程
问题
环境要求
1、Node 环境 node >= 7.6 (需要原生的 async/await 语法支持)
2、推荐采用 yarn 管理依赖, 使用npm 也可以
3、TypeScript >= 2.x
4、PM2 安装配置
- 全局安装 PM2: npm install -g pm2
- 安装 PM2 的 TypeScript 解释器: pm2 install typescript (执行该命令建议翻墙)
5、获得 mysql 数据库配置参数
6、获得 redis 缓存配置参数
使用该框架
框架 git 仓库地址: https://github.com/FeifeiyuM/koa2-typescript
1、拉取框架代码 git clone git@github.com:FeifeiyuM/koa2-typescript.git
2、安装依赖: yarn install ( npm install )
3、添加 mysql & redis 配置参数, (如何修改在后面介绍)
4、启动服务:
- 开发环境启动服务: 在工程根目录下执行命令: npm start
- 生产环境部署服务: 在工程跟目录下执行命令: npm run deploy
5、数据库更新迁移:(需要编写更新文件)
- 执行数据库迁移命令: npm run migrate
6、删除开启的所有 PM2 进程
- 执行删除进程命令: npm run del
工程目录结构
1 | koats |
config 配置信息
1、env.config.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 isProdEnv:boolean = process.env.NODE_ENV === 'production'
//开发环境下的配置参数
//listen port
let listenPort:Number = 3000
//Redis Config
let redis = {
port: 6379,
host: '192.168.1.100',
db: 8
}
//Mysql Config
let mysql = {
host: '192.168.1.100',
database: 'nodeweb',
user: 'root',
password: 'mysql123',
protocol: 'mysql',
port: '3306'
}
//生产环境下的配置参数
if(isProdEnv) { //覆盖开发环境的参数
listenPort = 3000
redis = {
port: 6379,
host: '192.168.1.100',
db: 9
}
mysql = {
host: '192.168.1.100',
database: 'nodeweb',
user: 'root',
password: 'mysql123',
protocol: 'mysql',
port: '3306'
}
}
//模块导出
export default {
listenPort: listenPort,
redis: redis,
mysql: mysql
}
2、PM2 启动配置文件
PM2 启动文件分为开发、生产两个, 两个文件大致相同
name: PM2指进程名; script: 入口执行文件,即 app.ts;
log_file: 指日志文件,PM2 会自动搜集系统输出日志,并输出至 logs 目录下的 app.log 文件
error_file: error及以上级别日志, 输出至 logs 目录下的 err.log 文件
PM2 配置编写说明
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 {
"apps" : [{
"name": "devenv",
"script": "./src/app.ts",
"instances": 1,
"exec_mode": "fork",
"watch": true,
"ignore_watch" : ["node_modules", "public", "logs", "views", "package.json", "config", ".git/*"],
"out_file": "./logs/app.log",
"error_file": "./logs/err.log",
"log_date_format" : "YYYY-MM-DD HH:mm Z",
"combine_logs": true,
"listen_timeout": 8000,
"kill_timeout": 1600,
"env": {
"NODE_ENV": "development"
}
}]
}
{
"apps" : [{
"name": "prodenv",
"script": "./src/app.ts",
"instances": 1,
"exec_mode": "fork",
"watch": false,
"out_file": "./logs/app.log",
"error_file": "./logs/err.log",
"log_date_format" : "YYYY-MM-DD HH:mm Z",
"combine_logs": true,
"listen_timeout": 8000,
"kill_timeout": 1600,
"env": {
"NODE_ENV": "production"
}
}]
}
TypeScript 配置
TypeScript 配置文件 tsconfig.json tsconfig 配置1
2
3
4
5
6
7
8
9
10
11
12
13
14
15{
"compilerOptions": {
"module": "commonjs",
"target": "es2016",
"noImplicitAny": false,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true,
"outDir": "build",
"watch": false
},
"files": [
"src/app.ts"
]
}
系统入口文件 app.ts
src 目录下的 app.ts 文件与之前KOA2系列文章中介绍的大致相当, 不作详细介绍
注意: 模块引入时, 有些模块不能直接采用 import Koa from ‘koa’, 需要改成: import * as Koa from ‘koa’
utils 模块介绍
1、session.ts: 参考 koa2-web-Session
2、redis.ts: 参考 koa2-web-数据持久化
3、orm.ts: 参考 koa2-web-数据持久化, node-orm2 具体 model 配置将在后面介绍
4、auth.ts: 用户认证模块,这个是 koa-router 的中间件函数, 在路由中执行1
2
3
4
5
6
7
8
9
10
11
12
13enum UserType { //用户类型定义
super = 0,
normal = 1,
operator = 2
}
export const userAuth = async (ctx, next) => {
if(ctx.session.isLogin) { //判断 Session 中的状态判断登入状态
await next()
} else {
ctx.status = 403 //认证失败
ctx.body = {ERROR: 'user is not autherized'} //直接返回错误码
}
}
base 路径下文件介绍
1、该目录下需要介绍是 models.js 文件,这个文件主要是用来初始化其他业务的 model1
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//init models
import * as Log4js from 'koa-log4'
const logger = Log4js.getLogger('models')
import DbConn from '../utils/orm' //引入 utils 中的 orm 连接模块
import { User } from '../account/models' //引入 account 中的 user model 初始化函数
import { Pets } from '../pets/models' //引入 pets 中 pets model 初始化函数
//定义一个全局的 orm 对象, 防止调用该 InitModels 模块时反复连接数据库
let connection:any = null
//分别初始化每个model,
const setUp = (db):void => {
User(db)
Pets(db)
}
const InitModels = ():Promise<any> => {
if(connection && connection !== 'initing') {
logger.info('models has been inited') //如果已经初始化过,直接返回实例化的 orm 对象
return Promise.resolve(connection)
} else {
connection = 'initing'
return new Promise((resolve, reject) => { //如果未初始化, 连接数据库
DbConn(null).then(db => {
setUp(db)
logger.info('models inited successfuly')
connection = db //赋值全局变量
resolve(db) //返回 orm 实例化对象
}).catch(err => {
logger.info('models inited failed')
reject(err) //放回错误
})
})
}
}
export default InitModels
2、api.ts, router.ts, test.ts 用法与其他业务模块的类似,在后面介绍
业务模块 (Account为例)
1、数据建模( models.ts ), 将数据库中的某一张表,对应映射成一个数据对象。
这一块主要涉及 node-orm2 的使用, 详细文档可以参考 node-orm2 wiki
主要设计的知识点有 node-orm2 wiki 中的 Defining Models, Defining Associations
以用户表为例:
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 import * as orm from 'orm'
export const User = (db):void => { //以函数形式导出, 给上面介绍的 InitModels() 调用
const user = db.define('user', { // 'user' 为表名, 对应数据库中的 user 表
// id 字段, 对应 user 表中的 id 字段,
//{ }, 中描述的是该字段的属性, 具体查看 node-orm2 wiki 中的 Model Properties
id: {type: 'serial', key: true},
password: {type: 'text', size: 128, required: true}, // user 表中的 password 字段
type: {type: 'integer', size: 0|1|2, defaultValue: 0},
nick: {type: 'text', required: true},
mobile: {type: 'text', required: true},
last_login: {type: 'date', time: true},
create_time: {type: 'date', time: true, required: true},
update_time: {type: 'date', time: true, required: true}
}, {
validations: { //提交字段校验 具体查看 node-orm2 wiki 中的 Model Validations
nick: [
orm.enforce.unique('nick has been used'), //唯一性校验
orm.enforce.ranges.length(5, undefined, 'nick must be at least 5 letters long'),
orm.enforce.ranges.length(undefined, 30, 'nick can not be longer than 30 letters') //长度校验
],
mobile: [
orm.enforce.unique('this mobile has be registed'), //唯一性校验
orm.enforce.ranges.length(11, 11, 'mobile number length must be 11')
]
},
hooks: { //事件钩子, 定义了数据存取的各个阶段, 具体查看 node-orm2 wiki 中的 Model Hooks
beforeValidation(next) {
this.create_time = new Date().toISOString().slice(0, 19)
return next()
}
},
methods: { //序列化输出, 为 user 对象定义一个 baseInfo 方法,获取对应字段
baseInfo() {
return {
nick: this.nick,
mobile: this.mobile,
type: this.type
}
}
}
})
}
2、定义接口(api.ts), 将主要的业务逻辑放在 api.ts 中实现, 包括 数据存取,更新, 相关业务逻辑
以 Account 下 api.ts 为例:主要实现用户创建和用户信息读取的功能
需要注意的点:
- 在 api.ts 中导入的是 utils 中的 InitModels 方法
- api 类中必须定义一个init()方法, 用于初始化整个 model,
- api 中所有的方法返回的都为 Promise 对象
- api 中主要用到的是数据库 增,删,改,查功能, 涉及的知识点有 node-orm2 wiki 中的 Finding Items, Creating and Updating Items, Aggregation
1 | import * as Log4js from 'koa-log4' |
3、路由编写 (router.ts), 实现页面渲染 或 http 接口
路由编写同之前的 KOA2系列 中的 REST 接口编写类似
本文中的添加的内容是 koa-router 中间件, 以实现用户登入校验
注意: 每个 router.ts 都要添加 model 初始化的代码, 主要为了保证在接口调用前 model 都已初始化
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 import * as Router from 'koa-router'
import * as Log4js from 'koa-log4'
import Account from './api' //导入之前编写的 api 类
import { userAuth } from '../utils/auth' //导入用户认证函数
import { errAnal } from '../utils/error' //异常处理函数
const router = new Router()
let account:any = null // 全局存储 account 对象
const logger = Log4js.getLogger('account')
//该函数是初始化 model 中间件
const initAccount = async (ctx, next) => {
if(!account) { //如果 account 对象为定义, 保证该对象只会初始化一次
account = new Account()
try {
await account.init() //等待 account 初始化成功, 本质上是等待整个 model 初始化成功
} catch(err) {
logger.error('account init failed')
}
await next()
} else { //如果已经初始化的话,直接下一步
await next()
}
}
//当有请求进入时,先检查 account 是否已经初始化
//这部分代码同 api 中 init()方法配合使用, 时必须添加的
router.use(initAccount)
router.prefix('/account') //给当前文件中的 所有路由都加个 account 前缀
//如下 /account/info/:id 这个接口访问需要登入
//在进入到逻辑处理函数前,先添加 userAuth 函数验证用户
//如果验证通过 则进入到之后的逻辑处理函数,
//如果处理失败, userAuth 函数中会直接返回错误
router.get('/info/:id', userAuth, async(ctx) => {
let id = Number(ctx.params.id)
let userInfo:any = null
try {
userInfo = await account.getById(id)
} catch(err) {
logger.error(err)
}
ctx.body = JSON.stringify(userInfo)
})
// 如下: /account/register 这个接口不需要用户验证, 则不用添加 userAuth 方法
router.post('/register', async (ctx) => {
let nick:string = ctx.request.body.nick
let mobile:string = ctx.request.body.mobile
let password:string = ctx.request.body.password
let type:number = Number(ctx.request.body.type)
let user = {
nick: nick,
mobile: mobile,
password: password,
type: type
}
let result:any = null
try {
result = await account.createUser(user)
ctx.body = JSON.stringify(result)
} catch(err) {
logger.error('in error', err)
errAnal(ctx, err)
}
})
export default router
4、 test.ts 用于编写测试脚本
数据库迁移 migration
本框架中采用的数据库迁移工具是 migrate-orm2
执行命令: npm run migrate 会执行 migrations/index.ts 脚本, 根据脚本中的配置会相应调用那些数据库更新脚本,(例如: 001-test.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
37import * as MigrateTask from 'migrate-orm2'
import * as Log4js from 'koa-log4'
import DbConn from '../src/utils/orm'
const logger = Log4js.getLogger('migrate')
logger.info('to start migrate')
DbConn(null).then(conn => {
logger.info('orm connect successfully')
const task = new MigrateTask(conn.driver)
//第一步生成新的迁移脚本 001.test.js
//在 001-test.js 中会包含up, down 两个方法
//在up, down 方法中可以编写数据库迁移操作: 如: createTable, addColumn 等
// task.generate('test', (err, resp) => {
// if(err) {
// logger.error(err)
// } else {
// logger.debug('result' ,resp)
// }
// }
task.up('001-test.js', (err, resp) => { //执行 001-test.js 中的 up 方法
logger.info('----task up finished----')
if(err) {
logger.error(err)
} else {
logger.debug('result', resp)
}
})
task.down('001-test.js', (err, resp) => { //执行 001-test.js 中的 down 方法
logger.info('----task down finished----')
if(err) {
logger.debug(err)
} else {
logger.info('result', resp)
}
})
}).catch(err => {
logger.error('orm connect failed')
})
注意:
- migrate-orm2 工具目前(2017/03/07)无法实现修改字段名称
- dropColumn(或 drop相关操作)请谨慎使用, 有时会将整个表给删除
package.json 模块
package.json 中主要介绍系统的启动命令, 在 script 中编写
npm start: 按开发环境配置启动整个工程, 并输出日志
npm run deploy: 按生产环境配置启动整个工程,
npm run migrate: 执行数据库迁移脚本
npm run del: 删除所有 PM2 正在运行的进程
1
2
3
4
5
6 "scripts": {
"start": "pm2 start config/pm2.dev.config.json && pm2 logs",
"deploy": "pm2 start config/pm2.prod.config.json",
"migrate": "pm2 start migrations/index.ts && pm2 logs",
"del": "pm2 delete all"
}