­

從零開始搞後台管理系統(2)——shin-server

  shin 的讀音是[ʃɪn],諧音就是行,寓意可行的後端系統服務,shin-server 的特點是:

  • 站在巨人的肩膀上,依託KOA2bunyanSequelize等優秀的框架和庫所搭建的訂製化後端系統服務。
  • 一套完整的 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 會忽略該文件,不會提交到倉庫中。

開發步驟

  1. 首先是在 models 目錄中新建對應的表(如果不是新表,該步驟可省略)。
  2. 然後是在 routes 目錄中新建或修改某個路由文件。
  3. 最後是在 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');
  });
});