基於本人在如今公司的Node微服務實踐, 不斷維護升級着一個Node Restful API種子項目, 特此共享出來以供借鑑和討論. 項目中幾乎全部的東西都使用了node/javascript及相應模塊的最新功能, 語法, 和實踐. javascript
接上一篇帖子, 本次分享將會對此項目提供的各個主要功能不分前後作下詳細介紹.
項目github倉庫地址, 歡迎star: https://github.com/xiaozhongliu/node-api-seedhtml
.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來實驗提供的接口: java
代碼文件: router.js
自動判斷有沒有控制器對應的接口數據校驗規則集合, 若有則採用.
包裝控制器來統一捕捉拋出的非預期錯誤, 並將在app.js中最後一箇中間件發送告警郵件.
提供基礎健康檢查接口.node
代碼文件: midware/validate.js & util/validator.js
按約定聲明與控制器名稱相同的接口數據校驗規則集合, 便可在請求時進行驗證. 例如:mysql
/** * validate api: login */ login: [ // 參數名 參數類型 是否必傳 ['sysType', Type.Number, true], ['username', Type.String, true], ['password', Type.String, true], ],
類型校驗方法大可能是express-validator模塊提供的, 能夠自定義類型及其校驗方法. 例如:git
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:github
const ts = new Date().getTime(); const TOKEN = "08fbf466b37a924a8b3d3b2e6d190ef3"; postman.setGlobalVariable("ts", ts); postman.setGlobalVariable("token", CryptoJS.MD5(TOKEN+ts));
代碼文件: util/extender.js
給express的response添加擴展方法, 簡化使用. 例如:web
// 無需返回數據 res.success() // 須要返回數據 res.success(payload) res.success({ accessToken, sysType: getRes.sysType, username: getRes.username, avatar: getRes.avatar, redirectUrl, })
代碼文件: midware/httplog.js
記錄請求地址, 請求數據, 響應數據, 響應狀態碼及處理時長. 例如:redis
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方法增長一個切面來實現非侵入記錄響應數據:sql
// 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
代碼文件: 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() }
代碼文件: 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 } },