Node API经验与种子项目分享 (二)功能详解
发布于 6 个月前 作者 xiaozhongliu 1338 次浏览 来自 分享

前言

基于本人在现在公司的Node微服务实践, 不断维护升级着一个Node Restful API种子项目, 特此共享出来以供借鉴和讨论. 项目中几乎所有的东西都使用了node/javascript及相应模块的最新功能, 语法, 和实践.

上一篇帖子, 本次分享将会对此项目提供的各个主要功能不分先后做下详细介绍. 项目github仓库地址, 欢迎star: https://github.com/xiaozhongliu/node-api-seed

详解

项目目录结构

    .vscode          VSC服务调试/测试调试配置  
    config           多环境服务配置, 不依赖外部逻辑  
    ctrl             控制器, 基本与路由对应  
    log              服务请求日志, 自动生成  
    midware          express服务中间件  
    model            数据库模型: mongo, postgres/mysql  
    service          服务层, 供控制器/中间件调用  
    test             API测试, 运行命令npm t  
    util             各种工具库, 仅依赖系统配置  
    .eslintrc.js     eslint规则配置  
    app.js           应用服务入口文件  
    global-helper.js 挂载少许全局helper  
    message.js       集中管理接口/系统消息  
    package.json     应用服务包配置文件  
    pm2.config.js    多环境pm2配置文件  
    router.js        集中管理服务路由  

项目首次运行

首次运行项目进行测试, 先脚本建表或执行User.sync()将表结构同步到数据库. 服务运行起来之后, 直接使用postman来实验提供的接口:
Run in Postman

路由注册扩展

代码文件: router.js 自动判断有没有控制器对应的接口数据校验规则集合, 如有则采用. 包装控制器来统一捕捉抛出的非预期错误, 并将在app.js中最后一个中间件发送告警邮件. 提供基础健康检查接口.

接口数据校验

代码文件: midware/validate.js & util/validator.js 按约定声明与控制器名称相同的接口数据校验规则集合, 即可在请求时进行验证. 例如:

/**
* validate api: login
*/
login: [
    // 参数名     参数类型     是否必传
    ['sysType', Type.Number, true],
    ['username', Type.String, true],
    ['password', Type.String, true],
],

类型校验方法大多是express-validator模块提供的, 可以自定义类型及其校验方法. 例如:

isHash(value) {
    return /^[a-f0-9]{32}$/i.test(value)
},

isUnixStamp(value) {
    return /^[0-9]{10}$/.test(value)
},

无效请求过滤

代码文件: midware/auth.js 此中间件做的无效请求过滤, 和认证没关系. 具体通过header中传来的ts和token校验请求有效性. ts或token未传则会直接回绝请求, 这个可以过滤掉95%以上的无效请求了. ts和token对校验失败回绝请求, 不会执行后续业务逻辑. ts和token的计算规则参考中间件代码, 客户端要以相同的规则计算后传入, 参考postman中Pre-request Script:

const ts = new Date().getTime();
const TOKEN = "08fbf466b37a924a8b3d3b2e6d190ef3";

postman.setGlobalVariable("ts", ts);
postman.setGlobalVariable("token", CryptoJS.MD5(TOKEN+ts));

结果处理扩展

代码文件: util/extender.js 给express的response添加扩展方法, 简化使用. 例如:

// 无需返回数据
res.success()

// 需要返回数据
res.success(payload)

res.success({
    accessToken,
    sysType: getRes.sysType,
    username: getRes.username,
    avatar: getRes.avatar,
    redirectUrl,
})

接口请求日志

代码文件: midware/httplog.js 记录请求地址, 请求数据, 响应数据, 响应状态码及处理时长. 例如:

2018-02-02 13:23:46 - [B1qkId-Lf] Start  POST /login
2018-02-02 13:23:46 - [B1qkId-Lf] Data   {"sysType":1,"username":"unittest","password":"e10adc3949ba59abbe56e057f20f883e"}
2018-02-02 13:23:46 - [B1qkId-Lf] Resp   {"code":1,"msg":"success","data":{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVuaXR0ZXN0IiwiaWF0IjoxNTE3NTQ5MDI2LCJleHAiOjE1MTg0MTMwMjZ9.-U4P6ksOUN6WsmI3ZEWow9npYDmO-QI020eVY5Mg2bQ","sysType":1,"username":"unittest","avatar":"https://nodejs.org/static/images/logo.svg"}}
2018-02-02 13:23:46 - [B1qkId-Lf] Done   200 (134ms)

2018-02-02 13:23:49 - [SJCJUuZLM] Start  GET /verify
2018-02-02 13:23:49 - [SJCJUuZLM] Resp   {"code":1,"msg":"success","data":{"username":"unittest"}}
2018-02-02 13:23:49 - [SJCJUuZLM] Done   200 (7ms)

高并发时可通过请求ID来找到同一次请求的多行日志记录. 通过给原生res.json方法增加一个切面来实现非侵入记录响应数据:

// add a logging aspect to the primary res.json function
const origin = express.response.json
express.response.json = function (json) {
    logger.info(`[${this.reqId}] Resp  `, JSON.stringify(json))
    return origin.call(this, json)
}

支持日志在线预览, 可在浏览器查看日志文件内容(首次会有http auth认证):

当然如果使用的ELK(或者Elastic Stack), 则对于一次请求最好就输出一行json, 以方便logstash或者filebeat抓取.

服务监控面板

代码文件: midware/monitor.js 可以打开这个地址查看服务监控面板(首次会有http auth认证): /dashboard

Jest接口测试

代码文件: test/base.test.js 已经集成VSC Jest测试配置, 选择Jest All这个profile, 加断点并F5即可开始调试. 或者对当前打开的文件选择Jest File这个profile. 我开始用Jest的时候它才8000多star, 和ava差不多并列第三, 但现在已经排第一了, 不得不服自己的眼光, 啊哈哈哈哈…嗝. 样例:

describe('base ctrl tests', () => {

    test('login succeeds    ', async () => {
        const data = {
            sysType: 1,
            username: 'unittest',
            password: 'e10adc3949ba59abbe56e057f20f883e'
        }

        const res = await client.POST(`${host}/login`, data)
        expect(res.code).toBe(1)
        expect(res.data.username).toBe('unittest')
    })

    test('login fails       ', async () => {
        const data = {
            sysType: 1,
            username: 'unittest',
            password: 'invalid password'
        }

        const res = await client.POST(`${host}/login`, data)
        expect(res.code).toBe(message.LoginFail.code)
    })
})

执行npm t, 测试结果如下:

接口示例说明

提供了3个基于jsonwebtoken (jwt) 的接口示例: 注册, 登录, 验证. 验证接口仅供参考, 实际使用时应在中间件中验证jwt, 这样的中间件类似:

module.exports = async (req, res, next) => {
    if (
        ![
            '/path/needs/jwt/verification' // TODO: 考虑放到配置
        ].includes(req.path)
    ) {
        return next()
    }


    // // test generating a jwt token
    // const jwtToken = await jwtSvc.sign({
    //     foo: 'bar'
    // })
    // console.log(jwtToken)


    // verify
    const { authorization } = req.headers
    if (!authorization) {
        return next(new Error('verify fail')) // TODO: 修改错误处理, 下同
    }
    const jwtToken = authorization.substr(7)

    let payload
    try {
        payload = await jwtSvc.verify(jwtToken)
    } catch (e) {
        return next(new Error('verify fail'))
    }
    if (!payload) {
        return next(new Error('verify fail'))
    }


    console.log(payload) // TODO: 设置到req上, 后续就能拿到


    next()
}

thunk函数包装

代码文件: service/*.js node进化到今天, 用原生async/await做代码异步流程控制也已经好久了. 很多库提供了基于promise的API, 但难免还有很多基于thunk的库, 或者同时提供了promise的API但还不完善的库. 对于thunk函数我们可以使用node提供的util.promisify来包装为promise. 例如:

   /**
    * set value of a hash field
    * @param {string} key      hash key
    * @param {string} field    field name
    * @param {string} value    field value
    */
    async hset(key, field, value) {
        if (typeof value === 'object') {
            value = JSON.stringify(value)
        }
        return promisify(redis.hset)(key, field, value)
    },

   /**
    * get value of a hash field
    * @param {string} key      hash key
    * @param {string} field    field name
    */
    async hget(key, field) {
        const value = await promisify(redis.hget)(key, field)
        try {
            return JSON.parse(value)
        } catch (e) {
            return value
        }
    },
5 回复

@heweicong 终于有人评论了, 只看到默默去star的

nice work! 监控可以考虑用alinode 做成docker 镜像 image.png

如果是api服务考虑集成swagger,也方便测试

sequelize的用法,我们这国更多的情况是先改库表,再根据库表自动生成 model, 生产环境下是没有权限修改表节构的。

@virtoolswebplayer alinode看起来挺好的, 可以考虑. 一般公司内部有做私有云的话直接接入.

至于swagger这个看具体情况, 团队觉得什么好用就用什么. 我们所有文档都在confluence管理的, 然后都很喜欢Postman辅助, 这样不管用哪个技术都是那样做. 如果是swagger首先每个人得花点时间熟悉它, 还得对不同技术的项目做适配, 并且要侵入项目, swagger的UI个人感觉用起来不太顺手.

sequelize由表sync生成model有个库, 这个我也用过, 但毕竟生成的model代码跟我想要的有差距. 我们习惯于先编写model, 开发环境sync表结构到数据库, 然后再数据库导出init脚本稍作修改放入项目. 别说生产环境了, 从测试环境往上走都是按正规的部署流程走的, 也便于提前发现问题. 情况不同措施就不太一样, 比如spring里面, 先由init.sql编写建库开始, 然后选表由mybatis直接生成mapper, DO, VO, dao, service, controller等文件.

不错,已Star

回到顶部