前後端分離之更好的mock你的後端api

阿輝funkyLover發表於2019-05-17

注意! 廣告警告! 廣告警告! 廣告警告!

在一個web應用的開發週期中, 一般前端與後端都是並行開發的, 各自完成自己的開發工作後進行聯調, 聯調通過再進行提測/釋出.

開發過程中, 前端都會以後端提供的 api 文件作為標準, mock 模擬 api 返回資料, 以確保在開發中就保證功能的完整性.

而關於如何更好的進行 mock, 業界/開源社群可謂有相當多質量上乘的解決方案, 如easy-mock, yapi等.

但是越是大而全的工具很多時候功能會超越需求非常多, 要簡單實現 mock api 的需求其實也有非常多小而美工具庫可以使用.

而本文主要介紹mock-server這個工具的使用

選用mock-server的主要原因除了是我開發的使用比較簡單之外, 更多的是滿足了下文提到的一些開發需求, 如果你也有同樣的需求而還沒找到解決方案的話, 不妨試用一下.

安裝 & 基本配置

可選全域性安裝, 安裝完成過後, 就可以通過mock命令啟動來 mock server

npm install -g mock-server-local

mock -h # 即安裝成功

# 用 -d 指定mock資料配置目錄, 為空時預設為當前目錄 `.`
mock -d ./mock

# 用 -p 指定server的埠, 預設為8888, 如果8888被佔用會埠號+1, 直至埠可用
# 注意如果指定了埠號, 但是埠號被佔用的話, 會丟擲錯誤
mock -d ./mock -p 8080
複製程式碼

個人比較習慣在專案中進行安裝, 並通過npm script啟動, 而 mock 資料也存放在專案當中, 通過 git 等版本管理工具在專案成員當中共享, 假設專案目錄為proj

// proj/package.json
{
  // ...
  "script": {
    "mock": "mock -d ./mock -p 8080"
  }
  // ...
}
複製程式碼
# 本地安裝
npm install mock-server-local --save-dev

# 啟動mock server
npm run mock

> mock@1.8.9 mock /path/to/proj
> mock -d ./mock -p 8080

you can access mock server:
http://127.0.0.1:8080
http://ww.xx.yy.zz:8080

you can access mock server view:
http://127.0.0.1:8080/view
http://ww.xx.yy.zz:8080/view
複製程式碼

就這樣 mock server 就已經啟動了, 訪問127.0.0.1:8080/view即可看到 mock server 的控制頁面

就下來就是調整代理, 把應用的請求轉發到 mock server 進行處理

如果你使用webpack來構建你的專案, 那你只需要改動一下webpack.devServer的配置即可

假設我們的業務域名為target.mock.com, 而介面基本都是target.mock.com/api/**, 可以這樣進行配置

devServer: {
  proxy: {
    '/api': {
      target: 'http://127.0.0.1:8080', // mock server
      headers: {
        host: 'target.mock.com' // 業務域名
      },
      onProxyReq: function(proxyReq, req, res) {
        proxyReq.setheader('host', 'target.mock.com'); // 業務域名
      }
    }
  }
}
複製程式碼

接著在開發中, 啟動 webpack 之後, 發出的請求/api/**都會被轉發到

而如果應用本身不使用 webpack 或其他帶 server 功能的打包工具, 可以使用代理工具進行請求轉發

如果是用 Chrome 瀏覽器除錯應用, 可以下載SwitchyOmega一類可配置, 把特定域名的請求進行轉發

使用微信開發者工具的話, 可直接設定代理, 設定 -> 代理設定 -> 勾選手動設定代理 -> 填寫代理配置

推薦使用 whistle, fiddler 一類功能完整代理工具, 類似配置如下

target.mock.com/api 127.0.0.1:8080 # 介面請求轉發到mock server

target.mock.com www.xx.yy.zz # 頁面從正常的開發測試機ip中獲取, 或本地除錯伺服器
複製程式碼

mock 資料配置

mock server 的配置是根據 mock 目錄的目錄結構生成的, 假設需要進行 mock 的 api 介面完整的 url 為target.mock.com/api/login

而且需要模擬以下三種情況的資料返回

  • 登入失敗, 返回錯誤碼-1 及錯誤資訊
  • 登入成功, 返回錯誤碼 0 和使用者資訊, 且要帶上登入態 cookie
  • 請求時間超過 8 秒, 導致前端請求超時

那麼目錄結構與資料配置檔案應該如下所示

|- proj
  |- mock
    |- target.mock.com
      |- api
        |- login
          |- 登入成功
          |- 登入失敗
          |- 請求超時

// 登入失敗
// proj/mock/target.mock.com/api/login/登入失敗/data.js
module.exports = {
  code: -1,
  msg: '登入失敗!'
};

// 登入成功
// proj/mock/target.mock.com/api/login/登入成功/data.js
module.exports = {
  code: 0,
  msg: '登入成功',
  username: 'ahui'
};
// proj/mock/target.mock.com/api/login/登入成功/http.js
module.exports = {
  header: {
    'Set-Cookie': 'token=123456789;'
  }
};

// 請求超時
// proj/mock/target.mock.com/api/login/請求超時/data.js
module.exports = {};
// proj/mock/target.mock.com/api/login/請求超時/http.js
module.exports = {
  delay: 8
};
複製程式碼

根據上面目錄配置, 訪問 mock server 頁面的 mock 皮膚127.0.0.1:8080/view/mocks, 就可以看到以下頁面

mock皮膚
訪問mock server頁面, 開啟mock皮膚

現在我們只需要勾選其中一個狀態, 然後發出請求即可

請求mock api
訪問mock api, 返回所選狀態的資料

mock完成之後就可以愉快編寫業務邏輯了

mock 配置詳解

看了上面的例子應該也就大致可以瞭解到, data.js裡面定義介面返回的資料. 而http.js, 顧名思義就是定義 http 請求相關行為的, 例如可以定義響應頭, http 狀態碼, 請求耗時等.

data 同時也支援使用 json 檔案, 非 js/json 檔案的一律當 json 格式處理, 而 http 則只支援通過 js 檔案定義

http.js可選配置如下

module.exports = {
  delay: 8, // 耗時8秒
  status: 200 // http狀態碼
  header: {
    // ... http響應頭
  }
}
複製程式碼

data.js除了可以簡單定義返回資料之外, 還可以直接返回模板, 如

module.exports = `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  這個是由mock server返回的模板
</body>
</html>
`;
複製程式碼

並且可以返回一個方法, 通過指令碼邏輯來處理最終要返回的資料

// ctx: https://koajs.com/#context
module.exports = function(ctx) {
  // 邏輯處理

  return {
    data: {}, // 響應資料
    header: {}, // 如有需要可以配置, 同http.js#header
    delay: 0.1, // 如有需要可以配置, 同http.js#delay
    status: 200 // 如有需要可以配置, 同http.js#status
  };
};

// 如果當中有非同步邏輯, 請返回promise, 或直接使用async方法
module.exports = async function(ctx) {
  // 非同步邏輯處理
  await asyncFn();

  return {
    data: {}, // 響應資料
    header: {}, // 如有需要可以配置, 同http.js#header
    delay: 0.1, // 如有需要可以配置, 同http.js#delay
    status: 200 // 如有需要可以配置, 同http.js#status
  };
};
複製程式碼

代理線上資料

在開發過程可能出現這樣的場景, 一期專案已經開發完了, 現在進行二期迭代的開發工作, 這個時候由於之前的介面後臺已經都實現, 二期開發中只想對新增的 api 進行 mock

這個時候可以修改一下代理工具的配置, 把不同介面的請求轉發到不同的伺服器

# 新增介面轉發至mock server
target.mock.com/api/new1 127.0.0.1:8080

target.mock.com/api/new2 127.0.0.1:8080

# 其餘介面直接使用線上/測試機資料
target.mock.com ww.xx.yy.zz
複製程式碼

又或者可以直接使用 mock server 提供的簡單的代理功能, 只需要在 mock 目錄個目錄下新建proxy.js檔案

|- proj
  |- mock
    |- proxy.js

// proj/mock/proxy.js
module.exports = {
  target.mock.com: 'https://ww.xx.yy.zz' // 這裡可以指定ip也可以指定域名, 但是需要注意協議型別是必須要帶上的
}
複製程式碼

這樣配置之後, 在代理工具中就可以直接把所有的target.mock.com的請求都直接轉發到 mock server

當對應請求的 url 並沒有勾選任何一個返回狀態, 或根本沒有配置對應的 url 時, mock server 都會幫助我們把請求轉發到目標 ip

假設沒有配置proxy.js的話, 對於沒有命中的 url 請求, 會根據 host 直接請求線上的資源或介面

模板介面除錯 & 微信登入支援

在非前後端分離的架構中, 很常會出現這樣的需求, 應用的入口即是後端介面, 後端會進行鑑權, 拼接模板內容和資料, 然後直接返回頁面給到前端進行展示.

這樣的場景 mock server 可以很簡單通過data.js中匯出方法的方式來處理

const fs = require('fs');

module.exports = async ctx => {
    let html = '';

    let template = fs.readFileSync('path/to/html/template/index.ejs'); // 舉例ejs, 實際可以處理任何模板引擎

    // 這裡處理模板的語法
    // 1. 處理類似include的拼接模板的語法
    // 2. 處理類似<%= =>插入變數/資料的語法
    // 3. 等等等等....

    html = processedHtml;

    return {
        data: html,
        header: {
          'Set-Cookie': 'SESSIONID=123123123123;'
        };
    };
};
複製程式碼

這樣子我們就可以進行模板介面的除錯了. 再回到我們的上一個例子

我們希望可以使用線上已有介面和資料狀態(如開戶資料)

也希望使用後端的登入態(這樣後續的介面呼叫也能通過鑑權), 但也同時希望可以除錯本地模板呢?

比較直觀的方式是, 本地修改模板然後把模板改動上傳到開發伺服器, 然後直接請求開發伺服器進行除錯

但是改動比較多, 需要頻繁除錯的話, 或許使用 mock server 也是一個不錯的選擇. 更進一步, 如果是微信 h5 且後端的登入鑑權接入了微信登入呢?

我們來分析一下如何使用 mock server 滿足這樣的除錯述求, h5 微信登入基本的流程如下

  1. 請求線上/開發測試伺服器介面
  2. 介面返回 http 狀態碼 302 並帶上 Location 頭, 跳轉到微信 url
  3. 請求微信 url 會返回 301 再回跳我們的業務域名
  4. 回跳我們的業務域名時, 即再次請求伺服器介面, 獲取微信登入 code 進行業務登入
  5. 返回登入態及 html 頁面

上面的流程中, 其實需要介入只有最後一步而已, 就是獲取到登入態並返回需要除錯的 html 模板內容即可

而前面的步驟, 完全可以通過在data.js中實現簡單的代理完成

// 微信登入/data.js
const httpProxy = require("http-proxy");
const fs = require("fs");
const path = require("path");

proxy = httpProxy.createServer({
  secure: false
});

async function req({ req, res }) {
  proxy.web(req, res, {
    target: {
      protocol: "https:",
      host: "ww.xx.yy.zz", // 目標伺服器
      port: 443,
      pfx: fs.readFileSync(path.resolve(process.cwd(), "cert/cert.p12")), // 如果伺服器是https需要生成證書
      passphrase: "password"
    },
    selfHandleResponse: true
  });

  return new Promise((resolve, reject) => {
    proxy.once("proxyRes", function(proxyRes, req, res) {
      let body = [];
      let size = 0;
      function onData(chunk) {
        body.push(chunk);
        size += chunk.length;
      }

      proxyRes.on("data", onData).once("end", () => {
        proxyRes.off("data", onData);
        body = Buffer.concat(body, size);
        resolve({
          header: proxyRes.headers,
          data: body,
          status: proxyRes.statusCode
        });
      });
    });
  });
}

module.exports = async function(ctx) {
  // 登入態
  const res = await req(ctx);
  const header = res.header;
  res.header = Object.keys(header).reduce((c, k) => {
    let nk = k
      .split("-")
      .map(v => v.charAt(0).toUpperCase() + v.slice(1))
      .join("-");
    c[nk] = header[k];
    return c;
  }, {});

  if (res.header["Set-Cookie"]) {
    // 如果有Set-Cookie header, 則要處理返回本地模板
    // 這裡處理模板的語法
    // 1. 處理類似include的拼接模板的語法
    // 2. 處理類似<%= =>插入變數/資料的語法
    // 3. 等等等等....
    res.data = template;

    // 這裡需要注意, 目標伺服器可能會返回gzip過後的資料
    // 如果不對Content-Encoding和Content-Length進行處理的話
    // 會導致響應中Content-Length和實際內榮長度不一致而出錯
    res.header["Content-Encoding"] = "identity";
    delete res.header["Content-Length"];
  }
  return res;
};
複製程式碼

這樣我們就可以對具體的介面模板進行除錯了

寫在最後

重複造輪子不易, 且造且珍惜

如果大家有mock api的需求的話, 不妨也試用一下mock-server

如果覺得mock-server還不錯, 或者解決了mock的一些痛點, 不妨賞個star

最後, 用得不爽或發現bug, 懇請提issue!

相關文章