利用Swagger UI介面文件同步本地Mock資料

朱子彥小朋友發表於2018-06-22

什麼是Mock

Mock顧名思義是一種模擬。通常利用相同的介面來模擬出一個物件以代替真實物件,這樣能有效隔離外部依賴,便於測試。對於前端開發,Mock作為重要一環,能帶來很多好處:

  • 前後端並行開發
  • 模擬各種響應值,便於測試
  • 可及早發現一些極端響應值下的頁面佈局問題等

背景

前端開發可簡單分為三個階段:並行開發階段、聯調階段和測試階段。現在的前端專案大多為前後端分離,在開發、聯調階段不可避免要面對資料來源的問題。

前端開發階段

在聯調階段,各個環境已有真實資料,方便本地除錯,我們一般會將介面指向真實資料來源。如果有跨域限制的話,可利用Charles、Fiddler等除錯代理工具來解決,也可以起一個本地Server:

const express = require('express');
const proxy = require('http-proxy-middleware');
const app = express();

app.use('/api', proxy({ target: 'your-api-url', changeOrigin: true }));
app.listen(3000);

複製程式碼

如果還處在並行開發階段,那我們就需要Mock資料,一般有以下幾種常用方式:

1、攔截Ajax、Fetch請求
缺點:前端混入髒程式碼;無法有效模擬網路情況。

2、本地Mock Server
缺點:介面眾多,建立和修改成本高。

3、YApi、Easy Mock的介面管理平臺
缺點:靈活性不夠。比如一些配置資訊分散在各個介面,沒法集中管理,修改成本高。

本文以筆者接觸較多的Swagger為例,從一個側面改善本地Mock Server需要不斷建立介面的缺點。開啟後端提供的Swagger UI地址的Network,發現有個api-docs檔案。

介面文件

這個JSON檔案包含介面、請求方法、響應格式等資訊。可以想見解析這個檔案並不難,唯一比較麻煩的可能就是響應值的解析和型別轉換。如果能適時同步資料到本地Mock Server,能省去不少乏味的體力活。

Talk is cheap

1、目標

  • 介面路徑和Mock目錄相對應,便於查詢、修改
  • 以請求方法為檔名,一個方法對應一個檔案,減少多人編輯衝突
  • 使用Mock.js包裝響應值,易於模擬一些極端狀況

2、解析JSON檔案

前面我們提到解析JSON檔案的難點主要在響應值型別的轉換,這邊我們利用Easy Mock的一個解析模組來做這件事情。

const swaggerParserMock = require('swagger-parser-mock');

const synchronizeSwagger = {
  init({ url, blacklist, outputPath }) {
    this.url = url;
    this.blacklist = blacklist;
    this.outputPath = outputPath;
    this.parse();
  },
  async parse() {
    const { paths } = await swaggerParserMock(this.url);
    this.generate(paths);
    console.log(paths);
  }
}

synchronizeSwagger.init({
  // Swagger api-docs地址
  "url": "your-api-docs-url",
  // 輸出目錄
  "outputPath": "./routes",
  // 黑名單,跳過一些不需要同步的api
  "blacklist": []
});
複製程式碼

列印paths資訊,格式大致如下:

"/path/foo": {
  "get": {
    "summary": "bar",
    "responses": {
      "200": {
        "example": "'@string'" // 模組為我們做的型別轉化和Mock.js包裝。
      }
    }
  },
  "post": {
    "summary": "baz",
    "responses": {
      "200": {
        "example": "'@string'"
      }
    }
  }
}
複製程式碼

3、遍歷介面。我們可以加入黑名單,過濾掉一些對前端沒有用處的介面。減少干擾,提高可維護性。

const fs = require('fs');
const { join } = require('path');
const { promisify } = require('util');
const mkdirp = require('mkdirp');

const writeFile = promisify(fs.writeFile);
const mkdir = promisify(mkdirp);

const synchronizeSwagger = {
  // 遍歷api path資訊
  traverse(paths) {
    for (let path in paths) {
      if (this.blacklist.includes(path)) {
        continue;
      }

      for (let method in paths[path]) {
        const pathInfo = paths[path][method];

        if (!pathInfo['responses']['200']) {
          continue;
        }
        this.generate(path, method, pathInfo);
      }
    }
  }
}
複製程式碼

4、生成Mock檔案,可以新增註釋等資訊。

const synchronizeSwagger = {
  // 生成mock檔案
  async generate(path, method, pathInfo) {
    const outputPath = join(__dirname, this.outputPath, path);
    const {
      summary,
      responses: { 200: responseOK },
    } = pathInfo;

    try {
      // 生成目錄
      await mkdir(outputPath);

      const example = responseOK['example'];
      // 生成檔案內容
      const template = this.generateTemplate({
        summary,
        example,
        method,
        path,
      });

      // 生成檔案, 已存在的跳過,避免覆蓋本地以及編輯的檔案
      const fPath = join(outputPath, `${method}.js`);
      await writeFile(fPath, template, { flag: 'wx' });
      console.log(`增加Mock檔案:${fPath}`);
    } catch (error) {
      /* eslint-disable no-empty */
    }
  },

  generateTemplate({ summary, example, method, path }) {
  // prettier-ignore
  // api path中的{petId}形式改為:petId
  return `/**
${summary}
**/
const Mock = require("mockjs");
module.exports = function (app) {
  app.${method}('/api${path.replace(/\{([^}]*)\}/g, ":$1")}', (req, res) => {
    res.json(Mock.mock(${example}));
  });
};`;
  },
}
複製程式碼

5、啟動Mock Server

以express為例,利用require動態特徵我們來建立路由,對映到剛才建立的介面檔案。

const fs = require('fs');
const join = require('path').join;
const express = require('express');

const app = express();
const port = process.env.PORT || 3000;

app.listen(port, function() {
  console.log(`server is listening ${port}`);
});

function scan(path, app) {
  const files = fs.readdirSync(path);

  for (let i = 0; i < files.length; i++) {
    const fpath = join(path, files[i]);
    const stats = fs.statSync(fpath);

    if (stats.isDirectory()) {
      scan(fpath, app);
    }
    if (stats.isFile()) {
      require(fpath)(app);
    }
  }
}

scan(join(__dirname, './routes'), app);
複製程式碼

寫在最後

至此我們就利用Swagger UI同步Mock資料,如果再加上cors、body-parser等Middleware,一個本地Mock Server基本成形。方便同步,我們將它加入npm scripts。

  "scripts": {
    "ss": "node ./synchronizeSwagger.js"
  },
複製程式碼

執行npm run ss,就能生成相應的Mock資料和訪問介面了。

route

api

附件:示例程式碼

相關文章