开发 Chrome 插件 + Mockjs 自动生成服务器,在也不用手动 mock 了

567 阅读4分钟

开发 Chrome 插件 + Mockjs 自动生成服务器,解决前端手动 mock 的痛苦

1. 背景

最近笔者开发一个新项目,需求评审后准备开发,后端拆解了产品需求并提供了接口文档,然后笔者在开始时要自己在代码中mock数据或者借助构建工具提供的相关插件去mock数据很不爽,因为还要自己去写mock的返回值并copy后端定义的返回值,降低了开发效率,不就相当于减少了moyu 时间么

不用现有的开源 mock 平台是因为安全,如果要进行私有化部署会收费,所以综合考虑下笔者就花了几天时间开发了此款chrome 插件

2. 效果图

效果图.gif

3. 插件功能流程

为了尽可能的让其他的前端小伙伴使用无负担,整体功能流程就是

  1. 打开后端提供的接口文档地址
  2. 右键打开控制台
  3. 选择笔者开发的插件名称Kg
  4. 生成服务器代码
  5. 打开新标签页,测试后端提供的接口地址

4. chrome 插件开发-前端代码

网上有很多的 chrome 插件开发教程,这里笔者就不一一赘述了,有兴趣的小伙伴可以自行搜索相关资料,这里只列举一下 chrome 插件所需的 相关文件

4.1 manifest.json

{
  "manifest_version": 3, // v3 版本
  "name": "auto-mock", // 插件名称
  "version": "1.0.0", // 插件版本
  "description": "读取 confluence 中的接口,自动写入本地nodejs服务器,生成接口,供前端模拟", // 插件描述
  "action": {
    "default_icon": "icon.png", // chrome 插件浏览器右上角插件图标
    "default_title": "解放双手" // 输入移入插件时的提示
  },
  "content_scripts": [
    {
      "matches": ["*://*/*"], // 匹配任意网站
      "js": ["./content-script.js"], // 网站打开时运行的 js 文件
      "css": ["./css/index.css"], // 依赖的 css 文件
      "run_at": "document_end" // 网站打开后什么时候注入 js 文件,这里是 文档加载完成后注入
    }
  ],
  "devtools_page": "./devtool.html", // 控制台插件打开时的 html
  "permissions": [
    // chrome 插件所需要的相关权限
    "tabs",
    "notifications",
    "activeTab",
    "downloads",
    "history",
    "storage"
  ]
}

4.2 devtool.js

// 创建一个Panel
chrome.devtools.panels.create("Kg", "./icon.png", "./index.html");

4.3 index.html

笔者是用 vue3 + typescript + naive-ui 开发的,最后会被打包到一个单独的 js 文件中,在 html 文件中引用该文件即可

4.4 爬取页面数据格式化成 json

  1. 监听devtool页面发来的消息
// 当首次进入此页面时,默认会向 content-scripts 文件发送一个消息,content-scripts 文件中会监听事件,代码如下
chrome.runtime.onMessage.addListener((action, sender, sendResponse) => {
  try {
    const response = handleMatchPageData(params); // 爬取页面数据
    return sendResponse(response);
  } catch (error) {
    console.log(error, "error");
    return sendResponse(error);
  }
});
  1. 爬取页面数据
// 这里其实就是通过获取dom,然后用正则提取相关字段信息进行组装了,以下代码仅供参考,实际不同的接口文档爬取的规则是不一样的

const handleMatchPageData = (params) => {
  const rules = {
    defaultApiName: "",
    defaultApiURI: "",
    defaultApiMethod: "",
    defaultApiQuery: {},
    defaultApiBody: {},
    defaultReponse: {},
    // 匹配开始的dom
    matchStartDoms() {
      const list = document.querySelectorAll("h4");
      return list ? Array.from(list) : [];
    },
    // 匹配 Api name
    matchApiName(parentNodeHTML) {
      const matched = parentNodeHTML.match(/\>\d+(?:、|\.)(.*?)\</);
      return matched ? matched[1] : rules.defaultApiName;
    },
    // 匹配 Api uri
    matchApiURI(parentNodeHTML) {
      const matched = parentNodeHTML.match(/uri(?:\:|:)(?:&nbsp;)?(.*?)\</i);
      return matched ? matched[1] : rules.defaultApiURI;
    },
    // 匹配 Api method
    matchApiMethod(parentNodeHTML) {
      const matched = parentNodeHTML.match(
        /method(?:\s)?(?:\:|:)(?:\s+)?(.*?)\</i
      );
      return matched ? matched[1].toLowerCase() : rules.defaultApiMethod;
    },
    // 匹配get请求的query参数
    matchApiQueryByGet(method, index) {
      if (!method || method !== "get") return rules.defaultApiQuery;
      return matchApiParams(index);
    },
    // 匹配post请求的body参数
    matchApiBodyByPost(method, index) {
      if (!method || method !== "post") return rules.defaultApiBody;
      return matchApiParams(index);
    },
    // 匹配返回值,格式化json数据,因为后端写的json可能会有很多错,这里是把后端写的json格式化为尽可能正确的json
    matchApiResponse(element) {
      const table = element.querySelector(".code table");
      if (!table) return rules.defaultReponse;
      const text = table.innerText;
      const response = JSON.parse(
        text
          .replace(/\/\/.*?\n/g, "") // 干掉注释
          .replace(/\'/g, '"') // 替换 ' 为 "
          .replace(/\'/g, '"') // 替换 ' 为 "
          .replace(/\“/g, '"') // 替换 “ 为 "
          .replace(/\”/g, '"') // 替换 ” 为 "
          .replace(/,/g, ",") // 替换 ,为 ,
          .replace(/:/g, ":") // 替换 :为 :
          .replace(/\n/g, "") // 干掉换行符
          .replace(/\s/g, "") // 干掉空格
          .replace(/\,(?=\})/g, "") // 干掉不符合json规范的逗号
          .replace(/\,(?=\])/g, "") // 干掉不符合json规范的逗号
      );
      return response;
    },
  };

  const startDoms = matchStartDoms();

  return startDoms.reduce((p, startDom, index) => {
    let element = startDom;
    let HTML = "";

    while (element.outerHTML.match(/div class="code/) === null) {
      HTML += element.outerHTML;
      element = element.nextElementSibling;
    }
    HTML += element.outerHTML;

    const name = matchApiName(HTML);
    const uri = matchApiURI(HTML);
    const method = matchApiMethod(HTML);
    const response = matchApiResponse(element);

    p.push({
      name,
      uri,
      method,
      response,
    });

    return p;
  }, []);
};
  1. 拿到数据后展示成表格即可

  2. 生成服务器代码

// 前端将爬取到的数据通过请求全部统一发送给后端
function request(params, url = "/gen-api") {
  // 这里的 params 就是爬取到的数据进行了整合
  const baseUrl = "http://localhost:3004"; // 后端服务器接口地址
  const requestUrl = `${baseUrl}${url}`;
  const instance = new Request(requestUrl, {
    method: "post",
    headers: {
      "Content-Type": "application/json;charset=utf-8",
    },
    body: JSON.stringify(params),
  });
}

5. chrome 插件开发-后端代码

后端是通过 express框架 搭建的

  1. 接口中生成路由模板
function genExpressRouter({ method, uri, response }) {
  method = method.toLowerCase();
  return `
      app.${method.replace(/&nbsp;/g, "").replace(/\s+/, "")}('${uri
    .replace(/&nbsp;/g, "")
    .replace(/\s+/, "")}',async (req,res) =>{
        try{
          return res.send(Mock.mock(${JSON.stringify(response)}))
        }catch(e){
          console.log(e,'e');
          return res.send({
            code:201,
            codeMsg:e,
            success:false
          })
        }
      })
    `;
}

app.post("/gen-api", (req, res) => {
  try {
    // mock = 前端匹配到的数据
    const { mock } = req.body;
    ...
    const expressRoutes = mock.reduce((p, info) => {
      p += genExpressRouter(info);
      return p;
    }, "");
    ...
  } catch (error) {
    return res.send({
      code: 201,
      message: "生成代码失败",
    });
  }
});
  1. 将生成后的模板写入到单独的 js 文件
fs.writeFileSync("auto-gen-server.js", template, "utf-8");
  1. 使用 pm2 启动服务器
// package.json 文件
{
  "name": "auto-mock-server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "del:mock-server": "pm2 del mock-server",
    "start:mock-server": "pm2 start auto-gen-server.js --name mock-server"
  },
  "license": "ISC",
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.18.1",
    "mockjs": "^1.1.0"
  }
}
const { execSync } = require("child_process");
app.post("/gen-api", (req, res) => {
  try {
    ...
    try {
      execSync("npm run del:mock-server");
      execSync("npm run start:mock-server");
    } catch (error) {
      execSync("npm run start:mock-server");
    }
  } catch (error) {
    return res.send({
      code: 201,
      message: "生成代码失败",
    });
  }
});


6. 实现思路总结

  1. 控制面板打开后,devtool 页面发送一个消息给 content-scripts 文件
  2. content-scripts 文件中接受到消息,利用正则匹配页面中的内容
  3. content-scripts 中匹配页面内容后,在发送消息给 devtool 页面
  4. devtool 文件中接受到消息,将数据展示成表格
  5. 生成服务器代码功能会发送一个请求给express 服务器
  6. express 服务器接受到请求,会生成对应的模板代码
  7. 模板代码写入到一个单独的文件中
  8. 利用execSync + pm2 启动这个单独的文件
  9. 完成