添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
  • 基于 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 [email protected]: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
    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
    koats
    ├──config # 工程需要的相关配置文件,存放在该路径下
    | ├──env.config.ts # 用于存放系统需要的相关配置参数, 例如:数据库,缓存,端口
    | ├──pm2.dev.config.json # 开发环境下,PM2 启动的配置文件
    | └──pm2.prod.config.json # 生产环境下, PM2 启动的配置文件
    |
    ├──migrations # msyql 更新迁移相关脚本目录
    | ├──index.ts # migrate 执行的脚本入口文件
    | ├──001-test.js # 这样的文件会有很多,是数据库更新的相关配置脚本
    | └──...
    |
    ├──src #开发文件路径
    | ├──utils # 公共模块的存放路径,例如下面本工程中添加的:
    | | ├──auth.ts # 用户验证模块
    | | ├──orm.ts # 建立 orm 模块与数据库连接的公共模块
    | | ├──redis.ts # 连接 redis 的公共模块
    | | ├──session.ts # session 存取的公共模块
    | | └──test.ts # 测试脚本
    | |
    | ├──base #与业务无关的相关逻辑代码
    | | ├──api.ts # api 接口文件
    | | ├──models.ts # 该 models.ts 用于初始化其他业务相关的 models
    | | ├──router.ts # 用于设置与业务无关的相关路由, 例如 首页等
    | | └──test.ts # 测试脚本
    | |
    | ├──account # 业务模块,用路径名区分业务,每个路径下都包含如下四个文件, 例如 account 表示用户管理相关业务
    | | ├──api.ts # api 接口文件
    | | ├──models.ts # orm 数据模型文件, 将数据库字段转换成相应的数据对象
    | | ├──router.ts # 与该业务相关的路由配置
    | | └──test.ts # 测试脚本
    | |
    | ├──pets # 另一个业务
    | |
    | ├──app.ts # 整个 Web 工程的入口文件
    | └──views #view层文件放置路径, 模板、js、css/less等文件
    | ├──layout # 公共模板文件
    ├──views #展示层代码,即 html 文件路径
    | ├──layout # nujunks 模板文件的公共模板路径
    | | ├──base-h5.html # 移动端的公共模板
    | | └──...
    | ├──base # 与业务无关相关页面
    | | ├──home.html #首页
    | | └──...
    | ├──account # 业务相关页面,推荐此类路径命名与 src 路径下的业务命名一致
    | | └──...
    | └──... # 如果需要加入打包相关工具的话,推荐加在该路径下
    |
    ├──public # 静态文件路径
    ├──logs # 日志文件输出路径
    ├──node_modules # node依赖路径,node引入依赖自动生成
    ├──package.json # 工程管理文件,系统依赖管理,启动命令、环境变量配置,
    ├──tsconfig.json # TypeScript 配置文件
    ├──.gitignore
    └──README.md

    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
    13
    enum 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 文件,这个文件主要是用来初始化其他业务的 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
    //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
    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
    import * as Log4js from 'koa-log4'
    //这边要特别注意,在此导入的是 base 目录下的 model 初始化函数
    // 而不是在当前路径下,定义的 models.ts
    import InitModels from '../base/models'
    const logger = Log4js.getLogger('account')
    class Account {
    userM: any = null //用于存储实例话后的 user Model
    models: any = null //存储整个 model 对象
    constructor() {}
    //初始化 model, 这个每个 api 类中必须定义的一个方法
    //用于初始化 model 对象
    init():Promise<any> {
    return new Promise((resolve, reject) => {
    InitModels().then(db => {
    this.userM = db.models.user //提取 user model
    this.models = db.models //实例化后的整个 model
    resolve(true)
    }).catch(err => {
    reject(false)
    })
    })
    }
    getById(id:number):Promise<any> { //根据用户id, 拉取用户信息
    return new Promise((resolve, reject) => {
    this.userM.get(id, (err, result) => {
    if(err) {
    reject(err)
    }
    //调用 user model 中 method 中定义的 baseInfo 方法
    //输出有效信息
    resolve(result.baseInfo)
    })
    })
    }
    createUser(userInfo):Promise<any> { //实现业务逻辑 创建用户
    return new Promise((resolve, reject) => {
    const createTime = new Date().toISOString().slice(0, 19)
    userInfo.update_time = createTime
    logger.debug('to create user', userInfo)
    //采用 user model 对象中的 create 方法,创建一条数据库数据
    this.userM.create(userInfo, (err, result) => {
    if(err) {
    reject(err)
    }
    resolve(result)
    })
    })
    }
    }
    export default Account

    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
    37
    import * 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"
    }