V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
liux466713
V2EX  ›  Node.js

Node API 经验与种子项目分享 (二)功能详解

  •  1
     
  •   liux466713 · 2018-02-04 23:58:56 +08:00 · 4486 次点击
    这是一个创建于 2488 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    基于本人在现在公司的 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
            }
        },
    
    4 条回复    2018-02-09 13:58:02 +08:00
    liux466713
        1
    liux466713  
    OP
       2018-02-05 16:23:24 +08:00   ❤️ 1
    默默 star 又不来这里支持一下是什么鬼, 试试能不能顶上去
    wisetc
        2
    wisetc  
       2018-02-09 12:59:04 +08:00
    支持一下,希望能够顶上去
    strugglexiang
        3
    strugglexiang  
       2018-02-09 13:43:55 +08:00
    默默 star,不发言,说的就是我吗
    liux466713
        4
    liux466713  
    OP
       2018-02-09 13:58:02 +08:00
    不晓得 V2EX 的帖子排序规则, 貌似上不去啊, node 在 V2EX 确实是很尴尬, 很冷清啊
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3254 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 12:16 · PVG 20:16 · LAX 04:16 · JFK 07:16
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.