博客 / 詳情

返回

基於 Webpack 插件體系的 Mock 服務

背景

圖片

在軟件研發流程中,對於前後端分離的架構體系而言,為了能夠更快速、高效的實現功能的開發,研發團隊通常來説會在產品原型階段對前後端聯調的數據接口進行結構設計及約定,進而可以分別同步進行對應功能的實現,提升研發速率。除了常見的研發流程提效之外,對於一些特殊的無法滿足前後端聯調場景下,也可在條件不允許的情況下進行 Mock 處理,等待條件滿足後再進行真實的接口聯調,如:網絡不通、多地協同等。本文從前端研發過程中的 Mock 需求場景出發,結合前端業界通用的 Webpack 工程化的方案來提供 Mock 服務,以期能夠給讀者提供一些 Mock 工程化的實現方案借鑑。

架構

圖片

對於絕大多數業務開發而言,在目前成熟的生產實踐中,前端開發團隊仍然是以Webpack作為前端工程打包構建的主流工具。因而,對於前端 Mock 服務的工程化方案而言,前端工程架構基建團隊提供適配Webpack體系的插件方案是一個不錯的工程基建選擇。雖然各方前端團隊都以各大框架或者框架生態的腳手架方案進行構建,但大部分現有生態工程打包器底層仍然是以Webpack為主,如:@vue/cliumicreate-react-app等。

圖片

對於Webpack插件,其本質是一個類(ps:更準確的説是函數,JavaScript 中沒有真正意義上的類),需要在類中定義apply方法,用於通過compiler對象掛載Webpack的事件鈎子,該回調中可以獲取到當前編譯的compilation對象以及異步的callbackWebpack提供了豐富的插件入口,並通過tapable鈎子事件系統,串聯起整個Webpack鈎子函數的生命週期流程。對於 Mock 服務而言,其實現的核心思路是在compiler的鈎子watchRun進行 Mock 服務器的啓動與監聽,其 Mock 服務器可以是基於koa或者express的 node 服務器。

注意:在自定義 Webpack 插件時,Webpack4 和 Webpack5 中的守護進程模式、異步加載、定義全局變量、訪問實例對象、事件監聽器等方面均有所變化,需要開發者進行相應的兼容處理。

目錄

├─ lib                                    // Mock服務的核心包
|   ├─ app.js
|   ├─ utils.js
├─ index.js                               // MockServiceWebpackPlugin插件導出

實踐

圖片

對於項目工期較緊且某一時間段內無法進行前後端聯調的場景下,業務開發下的實踐可通過引入mock-service-webpack-plugin的插件進行前端 Mock。由於團隊是基於 Vue 全家桶進行的業務開發,故而本實踐案例以@vue/cli腳手架方案作為工程基建的底座來對業務中的某一個接口聯調進行介紹。

在 Vue 腳手架配置中引入mock-service-webpack-plugin插件,對configureWebpack字段進行配置,代碼如下:

const path = require("path");
const resolve = (dir) => path.join(__dirname, dir);

const MockServiceWebpackPlugin = require("mock-service-webpack-plugin");

const fs = require("fs");

const mockUrl = "http://localhost:9009"; // 不要與proxy代理服務端口重合

const filterPort = (url) => parseFloat(url.split(":").pop());

const plugins = [],
  proxy = {
    "/api": {
      target: "http://localhost:8198", // 不要與mock服務端口重合
      ws: true,
      pathRewrite: {
        "^/api": "",
      },
    },
  };

if (process.env.VUE_APP_MOCK) {
  plugins.push(
    new MockServiceWebpackPlugin({
      source: path.resolve(process.cwd(), "./src/mock"),
      port: filterPort(mockUrl),
    })
  );
  proxy["/mock"] = {
    target: mockUrl,
    ws: true,
    pathRewrite: {
      "^/mock": "",
    },
  };
}

module.exports = {
  // webpack config
  configureWebpack: {
    plugins,
  },
  devServer: {
    // https: true,
    // 端口配置
    historyApiFallback: true,
    port: 8888,
    // 反向代理配置
    proxy,
  },
};

在項目結構中新建一個目錄用於放置相關的 Mock 數據接口,其需要和上述vue.config.js中的 Mock 設置目錄相同,結構如下:

├─ src
|   ├─ mock                               // mock目錄
|   |    ├─ screenConfig.js
|   ├─ api                                // 真實接口目錄
|   |    ├─ BigScreenConfig.js
├─ .env.dev                               // 環境配置
├─ vue.config.js                          // vue cli打包相關配置
注意:通常來説,為了能使用到Webpack的熱更新機制,可將 Mock 目錄放置到src下的某個目錄中

以其中一個大屏自配置的 Mock 接口為例,代碼如下:

// src/mock/screenConfig.js

module.exports = {
  path: "/sm/smJsonPnSetting/find",
  methods: "POST",
  data: {
    code: "0",
    success: true,
    msg: "成功",
    data: {
      settingId: "settingId-16943333",
      pnId: "pnId-12345678",
      title: "數字大屏",
      createTime: "2023-09-15 22:27:05",
      updateTime: "2023-09-15 22:27:05",
      isActived: "1",
      content: {
        charts: [
          {
            timeSize: "m15",
            edit: false,
            tabs: [
              {
                lineOptions: {
                  chartType: "area",
                  list: ["上傳速率(最小)", "下載速率(最小)"],
                },
                edit: false,
                staticTypes: [
                  {
                    kpiEnAlias: "userUprateAvr",
                    staticMethod: "Min",
                    neType: 5104,
                  },
                  {
                    kpiEnAlias: "userDownrateAvr",
                    staticMethod: "Min",
                    neType: 5104,
                  },
                ],
                title: "圖表名稱1",
              },
            ],
            id: 1,
            title: "圖表01",
          },
        ],
        materials: {
          MaterialResource: {
            top: 900,
            left: 1300,
          },
          MaterialTimeDimension: {
            top: 58,
            left: 1200,
          },
          MaterialChangeView: {
            top: 100,
            left: 1900,
          },
          MaterialTraffic: {
            top: 900,
            left: 1600,
          },
          MaterialAlarm: {
            top: 900,
            left: 700,
          },
          MaterialSelectPn: {
            top: 65,
            left: 1400,
          },
          MaterialCard: {
            top: 900,
            left: 1000,
          },
        },
        logo: "cdn/screen/selfScreen3/default_logo.svg",
        conf: "大屏自配置",
        title: "數字大屏",
        layouts: [
          {
            draggable: false,
            y0: 1,
            x0: 1,
            y1: 2,
            x1: 2,
            id: "1",
            matchId: 1,
            content: "LayoutPerformanceIndex",
          },
          {
            draggable: false,
            y0: 1,
            x0: 2,
            y1: 4,
            x1: 6,
            id: "2",
            matchId: "",
          },
          {
            draggable: false,
            y0: 2,
            x0: 1,
            y1: 3,
            x1: 2,
            id: "4",
            matchId: "",
          },
          {
            draggable: false,
            y0: 3,
            x0: 1,
            y1: 4,
            x1: 2,
            id: "6",
            matchId: "",
          },
        ],
        bottomTabs: [
          {
            itemid: 1,
            src: "img/traffic.png",
            checked: false,
            id: "MaterialTraffic",
            title: "本月流量",
            value: 0,
          },
          {
            itemid: 2,
            src: "img/resource.png",
            checked: false,
            id: "MaterialResource",
            title: "資源概況",
            value: 514,
          },
          {
            itemid: 6,
            src: "img/card.png",
            checked: true,
            id: "MaterialCard",
            title: "號卡詳情",
            value: 5,
          },
          {
            itemid: 5,
            src: "img/alarm.png",
            checked: false,
            id: "MaterialAlarm",
            title: "設備告警",
            value: 0,
          },
        ],
      },
      smVersion: "4",
    },
  },
};

對於是否開啓 Mock 服務,可藉助腳本通過.env變量進行控制,代碼如下:

VUE_APP_MODE=dev
VUE_APP_MOCK=false

而在頁面中對 Mock 與真實接口基於環境變量來進行切分,代碼如下:

// api.js
import axios from "axios";

let BaseAxios = axios.create({
  timeout: 60000,
});

let APIGetFind = async function (params) {
  return await BaseAxios.post("api" + "/sm/smJsonPnSetting/find", params);
};

let MockGetFind = async function (params) {
  return await BaseAxios.post("mock" + "/sm/smJsonPnSetting/find", params);
};

// main.js
Vue.prototype.$mock = process.env.VUE_APP_MOCK;
<script>
import { APIGetFind, MockGetFind } from "@/api/BigScreenConfig";

export default {
  data() {
    return {
      settingId: "",
    };
  },
  methods: {
    async useEffect() {
      console.log("this.settingId", this.settingId);

      const res = this.$mock
        ? await MockGetFind({
            settingId: this.settingId,
          })
        : await APIGetFind({
            settingId: this.settingId,
          });
    
      console.log('res', res);
    },
  },
};
</script>

對於package.json中的腳本設置,代碼如下:

{
  "scripts": {
    "serve": "vue-cli-service serve",
    "serve:dev": "cross-env VUE_APP_MODE=dev npm run serve",
    "serve:dev_mock": "cross-env VUE_APP_MODE=dev VUE_APP_MOCK=true npm run serve"
  }
}

源碼

index.js

Webpack插件mock-service-webpack-plugin的入口,導出MockServiceWebpackPlugin類,使用進程間通信對 Mock 服務器和本地 Web 開發服務器進行響應,代碼如下:

const path = require("path");
const fs = require("fs");
const { fork } = require("child_process");
class MockServiceWebpackPlugin {
  constructor(options) {
    this.options = options;
  }
  apply(compiler) {
    const { source, port = "9009" } = this.options;
    if (!source) {
      console.error(
        `Mock Directory did not exist. Please make sure your Mock Source Directory`
      );

      if (!fs.existsSync(source))
        console.error(
          `${source} did not exist. Please make sure your Source is Correct`
        );
    }

    let child;

    child = fork(path.resolve(__dirname, "./lib/app.js"), [], {
      encoding: "utf8",
      execArgv: process.execArgv,
    });

    child.send({ source, port });

    compiler.hooks.watchRun.tapAsync(
      "MockServiceWebpackPlugin",
      (compilation, callback) => {
        console.log("compiler watching...");

        fs.watch(source, { recursive: true }, (eventType, filename) => {
          console.log("eventType:", eventType, "filename:", filename);
          child.kill("SIGKILL");
          child = fork(path.resolve(__dirname, "./lib/app.js"), [], {
            encoding: "utf8",
            execArgv: process.execArgv,
          });
          child.send({ source, port });
        });
        callback();
      }
    );
  }
}

module.exports = MockServiceWebpackPlugin;

lib

app.js

app.js是基於express啓動的 Mock 服務器,也是實現 Mock 服務路由的核心,代碼如下:

const express = require("express");
const bodyParser = require("body-parser");
const fs = require("fs");
const path = require("path");
const app = express();
const router = express.Router();

const { createRoutes } = require("./utils");

app.use(bodyParser.json());
app.use(
  bodyParser.urlencoded({
    extended: false,
  })
);

process.on("message", ({ source, port }) => {
  console.log(`Options get From Parents`, source, port);

  createRoutes(router, source);

  app.use(router);

  app.listen(port, () => {
    console.log(`Mock Server Listen ${port} is Running`);
  });
});

utils.js

utils.js主要用於 HTTP 請求的相關處理,基於用户的 Mock 服務的 options 進行相應的路由動態生成,代碼如下:

const fs = require("fs");
const path = require("path");

const METHODS_MAP = {
  POST: "post",
  GET: "get",
  DELETE: "delete",
  PUT: "put",
};

const createRoutes = (router, p) => {
  const stats = fs.statSync(p);

  if (stats.isDirectory()) {
    fs.readdirSync(p).forEach((item) => {
      createRoutes(router, `${p}/${item}`);
    });
  } else if (stats.isFile()) {
    const { path, methods, data } = require(`${p}`);
    if (!methods)
      console.error(
        `Methods did not exist. Please make sure your method is one of ${Object.keys(
          METHODS_MAP
        ).join(" ")}`
      );

    if (!data)
      console.error(
        `Data did not exists. Please make sure your data is correct`
      );

    router[METHODS_MAP[`${methods}`]](path, (req, res) => {
      res.json(data);
    });
  }
};

module.exports = {
  createRoutes,
};

總結

除了基於 Webpack 的前端工程化構建,對於RollupVite以及Gulp等其他前端打包構建工具也是現代化前端工程團隊需要納入考慮的工程基建範疇。對於完整的 Mock 服務,也可提供平台服務、IDE 插件等形式來幫助業務團隊更好的提升效率及開發體驗,工具從來都是服務的承載形式,重要的不是功能本身,而是體驗帶來的效率優化。

對於前端工程化而言,Mock 服務僅僅是開發流程中的一環,面對日益增加的成本及業務壓力,如何有效的提升效率,實現工程效率才是前端工程師應該考慮的重中之重。不僅僅在於企業效益的間接貢獻,更重要的是前端工程化實踐也是平台工程乃至軟件工程方向的重要組成部分,所有的工程能力的提升都是工程師應該一直致力於培養的重要能力,共勉!!!

ps: 最後,對於mock-service-webpack-plugin的實現感到不錯的同學,歡迎點個小小的 star,您的 star,是我們最大的動力~~~

參考

  • Webpack 鈎子函數
  • webpack 的自定義插件學習
  • 學 webpack 前先看看 tapable 吧
  • Webpack HMR 原理解析
  • Webpack 原理淺析
  • 前端該如何優雅地 Mock 數據
  • 詳解如何優雅在 webpack 項目實現 mock 服務器
  • mock 服務搭建
  • Webpack 實戰:本地 mock 開發模式實踐
  • 基於 webpack-dev-server 搭建 mock 服務
  • 編寫 webpack 插件-webpack-mock-service-plugin
  • 【webpack 插件篇】webpack-plugin-mock 一款 mockjs 的 webpack 插件,配置簡單、易用
  • 從零開始搭建一個 mock 服務
user avatar susouth 頭像 yilezhiming 頭像 mofaboshi 頭像
3 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.