從零開始搞後台管理系統(2)——shin-server
shin 的讀音是[ʃɪn],諧音就是行,寓意可行的後端系統服務,shin-server 的特點是:
- 站在巨人的肩膀上,依託KOA2、bunyan、Sequelize等優秀的框架和庫所搭建的訂製化後端系統服務。
- 一套完整的 Node.js 後端服務解決方案。
- 調試便捷,實時列印出各類請求、日誌和所有的查詢語句。
- 配合獨立的配置文件可連接 MongoDB、MySQL 以及 Redis。
- 已開闢腳本和定時任務目錄,可將相應文件補充進來。
- 容易擴展,可引入第三方庫,例如隊列、雲服務等。
與shin-admin配合使用的話,大致架構如下圖。
準備工作
1)安裝
在將項目下載下來後,來到其根目錄,運行安裝命令,自動將依賴包下載到本地。
$ npm install
2)啟動
在啟動伺服器之前,需要確保本地已經安裝並已開啟 MongoDB、MySQL 以及 Redis。
- mongo 啟動命令:mongod
- redis 啟動命令:redis-server
在 docs/SQL 中有個資料庫文件,可初始化所需的表,並且注意將 config/development.js 中資料庫的帳號和密碼修改成本機的。
執行項目啟動命令,成功後的終端如下圖所示,埠號默認是 6060,可在 config/development.js 中自定義埠號。
$ npm start
運行 //localhost:6060/user/init 可初始化超級管理員帳號,後台帳號和許可權都保存在 MongoDB 中,其他一些業務保存在 MySQL 中。
- 帳號:admin@shin.com
- 密碼:admin
3)運行流程
當向這套後端系統服務請求一個介面時,其大致流程如下圖所示。
目錄結構
├── shin-server │ ├── config --------------------------------- 全局配置文件 │ ├── db ------------------------------------- 資料庫連接 │ ├── docs ----------------------------------- 說明文檔 │ ├── middlewares ---------------------------- 自定義的中間件 │ ├── models --------------------------------- 數據表映射 │ ├── routers -------------------------------- api 路由層 │ ├── scripts -------------------------------- 腳本文件 │ ├── services ------------------------------- api 服務層 │ ├── static --------------------------------- 靜態資源 │ ├── test ----------------------------------- 單元測試 │ ├── utils ---------------------------------- 公用工具 │ ├── worker --------------------------------- 定時任務 │ ├── app.js --------------------------------- 啟動文件 │ ├── index.js ------------------------------- 入口文件 └───└── index-worker.js ------------------------ 任務的入口文件
1)app.js
在啟動文件中,初始化了 bunyan 日誌框架,並聲明了一個全局的 logger 變數(可調用的方法包括 info、error、warn、debug等) ,可隨時寫日誌,所有的請求資訊(引入了koa-bunyan-logger)、資料庫查詢語句、響應數據等,都會寫入到伺服器的日誌中。
JWT 認證 HTTP 請求,引入了 koa-jwt 中間件,會在 checkAuth.js 中間件(如下程式碼所示)中調用 ctx.state.user,以此來判斷許可權。而判斷當前是否是登錄會以 GET 方式向 」api/user「 介面發送一次請求。
還引入了 routers() 函數(位於 routers 目錄的 index.js 中),將 services 和 middlewares 兩個目錄下的文件作為參數傳入,這兩個目錄下都包含 index.js 文件,引用方式為 middlewares.checkAuth()、 services.android 等。
import requireIndex from 'es6-requireindex'; import services from '../services/'; import middlewares from '../middlewares'; export default (router) => { const dir = requireIndex(__dirname); Object.keys(dir).forEach((item) => { dir[item](router, services, middlewares); }); };
2)config
默認只包含 development.js,即開發環境的配置文件,可包含資料庫的地址、各類帳號密碼等。
使用node-config後,就能根據當前環境(NODE_ENV)調用相應名稱的配置文件,例如 production.js、test.js 等。
3)db
MySQL 資料庫 ORM 系統採用的是 Sequelize,MongoDB 資料庫 ORM系統採用的是 Mongoose,redis 庫採用的是 ioredis。
4)models
聲明各張表的結構,可用駝峰,也可用下劃線的命名方式,函數的參數為 mysql 或 mongodb,可通過 mysql.backend 來指定要使用的資料庫名稱。
export default ({ mysql }) => mysql.backend.define("AppGlobalConfig", { id: { type: Sequelize.INTEGER, field: "id", autoIncrement: true, primaryKey: true }, title: { type: Sequelize.STRING, field: "title" }, }, { tableName: "app_global_config", timestamps: false } );
models 目錄中的 index.js 文件會將當前所有的 model 文件映射到一個 models 對象中。
5)routers
前端訪問的介面,在此目錄下聲明,此處程式碼相當於 MVC 中的 Control 層。
在下面的示例中,完成了一次 GET 請求,middlewares.checkAuth()用於檢查許可權,其值就是在 authority.js 聲明的 id,ctx.body 會返迴響應。
router.get( "/tool/short/query", middlewares.checkAuth("backend.tool.shortChain"), async (ctx) => { const { curPage = 1, short, url } = ctx.query; const { rows, count } = await services.tool.getShortChainList({ curPage, short, url }); ctx.body = { code: 0, data: rows, count }; } );
6)services
處理數據,包括讀寫數據表、調用後端服務、讀寫快取等。
services 目錄中的 index.js 文件會初始化各個 service 文件,並將之前的 models 對象作為參數傳入。
注意,MySQL中查詢數據返回值中會包含各種資訊,如果只要表的數據需要在查詢條件中加 」raw:true「(如下所示)或將返回值調用 toJSON()。
async getConfigContent(where) { return this.models.AppGlobalConfig.findOne({ where, raw: true }); }
7)scripts
如果要跑腳本,首先修改 scripts/index.js 文件中的最後一行 require() 的參數,即修改 「./demo」。
global.env = process.env.NODE_ENV; global.logger = { trace: console.log, info: console.log, debug: console.log, error: console.log, warn: console.log, }; require('./demo');
當前位置如果與 scripts 目錄平級,則執行命令:
$ NODE_ENV=development node scripts/index.js
其中 NODE_ENV 為環境常量,test、pre 和 production。
8)static
上傳的文件默認會保存在 static/upload 目錄中,git 會忽略該文件,不會提交到倉庫中。
開發步驟
- 首先是在 models 目錄中新建對應的表(如果不是新表,該步驟可省略)。
- 然後是在 routes 目錄中新建或修改某個路由文件。
- 最後是在 services 目錄中新建或修改某個服務文件。
定時任務
本地調試全任務可執行:
$ npm run worker
本地調試單任務可執行下面的命令,其中 ? 代表任務名稱,即文件名,不用加後綴。
$ JOB_TYPES=? npm run worker
在 worker 目錄中還包含兩個目錄:cronJobs 和 triggerJobs。
前者是定時類任務 (指定時間點執行),使用了 node-schedule 庫。
module.exports = async () => { //每 30 秒執行一次定時任務 schedule.scheduleJob({ rule: "*/30 * * * * *" }, () => { test(result); }); };
後者是觸發類任務,在程式碼中輸入指令觸發執行,使用 agenda 庫。
module.exports = (agenda) => { // 例如滿足某種條件觸發郵件通知 agenda.define('send email report', (job, done) => { // 傳遞進來的數據 const data = job.attrs.data; console.log(data); // 觸發此任務,需要先引入 agenda.js,然後調用 now() 方法 // import agenda from '../worker/agenda'; // agenda.now('send email report', { // username: realName, // }); }); };
注意,寫好的任務記得添加進入口文件 index-worker.js。
require('./worker/cronJobs/demo')();
require('./worker/triggerJobs/demo')(agenda);
單元測試
運行下面的命令就會執行單元測試。
$ npm test
單元測試使用的框架是 mocha 3.4,採用的斷言是 chai 4.0,API測試庫是 supertest 3.0。
// routers 測試 describe('GET /user/list', () => { const url = '/user/list'; it('獲取用戶列表成功', (done) => { api .get(url) .set('Authorization', authToken) .expect(200, done); }); }); // serveices 測試 import backendUserRole from '../../services/backendUserRole'; describe('用戶角色', () => { it('獲取指定id的角色資訊', async () => { const service = new backendUserRole(models); const res = await service.getInfoById('584a4dc24c886205bd771afe'); // expect(2).toBe(2); // expect(res.rolePermisson).to.be.an('array'); }); });