自从用单页应用(SPA)风靡以降,对于 web 前端项目来说 -- 无论是目前绝大多数的基于 webpack 的项目,还是既有的 grunt/gulp 项目来说,其基本开发流程大都如下:
- 用
npm start
等启动开发时环境,自动监控源文件改变并对浏览器热更新 - 依赖后端接口返回的数据渲染页面逻辑,或将结构化的数据提交给后端接口
- 完成阶段性或全部开发,以各种方式实现部署
这其中,最能体现“前后端分离”特征的就是第 2 步,即比之于传统上直接传输 HTML,代以轻量的局部 JSON 通信。
然而从另一个角度来看,在开发过程中,前端对后端的依赖某种程度上是更紧密更重了的。很多时候当后端接口服务出错,或尚未开发完成时,前端开发者立即就会面临无米下炊的窘境。
“切图仔” 永远在骚动,“CRUD boy” 却有恃无恐 🙄️
没错,本文要谈论的就是 mock 数据的问题。
几种常见的自建 mock 数据的方法如下:
- 由企业/组织自建一个专门的 mock 站点,开发者可以自动维护模拟接口并控制返回值
- 利用 web 上公开的 mock 网站
- 安装 mockjs 等第三方依赖包,在代码中按其约定编写假数据
- 直接在业务源码中硬编码自定义的假数据
以上方法一定程度上能暂时满足开发需求,但都需要在项目中硬编码,有些还要反复注释或删除,甚至还有可能泄露业务数据。因此除非是企业/团队的开发规范要求,否则都说不上是最方便的方法。
用 express 楔入本地 mock
在之前的一些项目中,我实践过这样的一种方案:
这种方案 A
用起来还不错,利用本地额外启动的一个 express 服务(可在 npm scripts 中和 dev 整合成一条命令),“拦截”住某些的异步请求,同时也能放过本地未实现的请求,实现针对性的 mock 开发,同时又不用重写所有接口,在自主控制和“正式”接口间取得平衡和灵活度。
但这样一来对项目的改动还是稍嫌麻烦,是对原有结构一种附着性的改造,若是利用相应的脚手架从头搭建的新项目还好,对改造既有项目、临时经手的各式项目来说,每次这样配置一番仍有些烧脑和麻烦;另一个小问题是,对于比较特殊复杂的请求,其转发步骤也难以保证完全的健壮性。
用 nginx 转发本地 mock
由此发展出的方案 B
是对方案 A
的改进,也是本文主要谈论的方法。其思路更简单直接,那就是借助 nginx,实现一种完全无侵入性的 mock 套壳开发:
该方案主要配置方法如下。
nginx.conf
确保本机安装了 nginx 后,在 nginx.conf
中配置相关本地请求路径的 location:
#user nobody; events { worker_connections 1024; } http { resolver 8.8.8.8; default_type application/octet-stream; sendfile on; keepalive_timeout 65; server { listen 8081; # hosts 中做相应的设置 127.0.0.1 localhost.foo.com server_name localhost.foo.com; charset UTF-8; location / { proxy_pass http://localhost.foo.com:8080; } # 要代理的路由 location /foo-api/bar { proxy_set_header Host $host; # express 服务地址 proxy_pass http://localhost:8090; } # ... 若干相似的代理配置 error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } }
mock.server.js
用 express 编写本地 mock 服务:
const express = require('express') const bodyParser = require('body-parser') const walk = require('klaw-sync') const ON_DEATH = require('death') const { execSync } = require('child_process') process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 // eslint-disable-line const app = express() app.use( bodyParser.urlencoded({ extended: false }) ) app.use(bodyParser.json()) app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', argsMap.mockorigin || '*') res.header( 'Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, hci-secret-key, x-api-key' ) res.header('Access-Control-Allow-Methods', 'POST, OPTIONS, GET') res.header('Access-Control-Allow-Credentials', true) next() }) // 遍历 xxx.api.js 文件 walk(__dirname) .filter(p => /\.api\.js$/.test(p.path)) .map(p => p.path) .forEach(part => require(part)(app)) const p1 = 8090 const h1 = 'localhost' const server = app.listen(p1, h1, () => { console.log(`\n\n Local server running at: http://${h1}:${p1} \n\n`) }) // 确保退出时停止所有服务 ON_DEATH((signal, err) => { server.close() try { execSync('nginx -s stop') } catch (ex) {} console.log(signal, 'nginx has stopped.') })
同目录下的若干 xxx.api.js:
module.exports = function(app) { app.post("/foo-api/bar/query", (req, res) => { res.json({ code: 1, msg: null, data: [0, 1, 3], }); }); };
package.json 和启动命令
编写 mock/run-nginx.js , 用以自动获取 nginx 绝对路径:
const {spawn} = require('child_process'); const {resolve} = require('path'); const conf = resolve(__dirname, './nginx.conf'); const child = spawn('nginx', ['-c', conf]); child.on('exit', code => { console.log(`ng process exited with code ${code}`); }); child.stdout.on('data', data => { console.log(`ng stdout: ${data}`); }); child.stderr.on('data', data => { console.log(`ng stderr: ${data}`); });
最后配置相应的 npm scripts 即可:
"express": "nodemon mock/mock.server.js", "startlocal": "shell-exec --colored-output \"node mock/run-nginx.js\" \"npm run start -- --local\" \"npm run express\"",
- 用
npm run "express": "nodemon mock/mock.server.js","startlocal": "shell-exec --colored-output \"node mock/run-nginx.js\" \"npm run start -- --local\" \"npm run express\"",startlocal
取代npm start
启动开发服务 - express 启动文件同目录下所有
<模块名>.api.js
的文件都会被自动加入 mock 服务中 - 在浏览器中,将自动打开的页面 url 中 8080 部分改为 8081 即可