云音乐前端专栏

使用 svrx 实现更优雅的接口 Mock

2019-11-20

导言

目前 Web 开发普遍都切换到了前后端分离的开发模式。虽然在工程和职能上已经分离了,但在实际工作中,前后端在开发进度上往往会出现不一致的情况,此时就会极大地影响开发效率。 接口 mock 在此时就发挥出了巨大价值,它磨平了这个时间差,最终实现高效的前后端分离开发。

具体到接口 mock 方案就多种多样了,但大体不外乎 「硬编码」 、 「前端拦截」和「后端拦截」这三种。 本文会尝试简单分析这三种常见方案的优劣,然后引出主要议题:基于 svrx 的接口 mock 方案。

硬编码方案

硬编码即在前端代码中直接写入 mock 数据,比如:

function getUser(id) {
  return { username: 'mock username', id: 100 }; //接口mock
  return ajax('/user', { id });
}

提交时移除或注释掉即可:

function getUser(id) {
  // return {username: 'mock username', id: 100}
  return ajax(`/user/${id}`);
}

后端硬编码的 mock 方式亦是如此,不过它的侵入性保留在了后端逻辑中,前端的业务代码可以保持干净:

router.get('/user/:id', async ctx => {
  ctx.body = { username: 'mock username', id: 100 };
  // ctx.body = await userService.get(ctx.params.id);
});

注:上述范例基于 Koa 框架

硬编码的优点

  • 简单灵活,不需要任何工具和框架支持,就地使用。
  • 如果是前端硬编码,支持修改生效,不需要重启 server。

硬编码缺点

  • 接口 mock 和业务代码耦合,挖坑一时爽,填坑火葬场

这种骚操作估计很多人年轻时都干过,提交时忘记删除导致夹带私货上线的车祸现场历历在目。 无论是否用一些专业的 mock 框架(比如 mock.js ),这种在业务逻辑中耦合的方式显然是下下策,线上事故通报中可能往往因此就有了你的名字。

稍严谨的同学可能会配合构建工具(如 webpack )来实现本地 mock 代码和业务代码的隔离,但并未在本质上解决这种耦合关系,随着项目的迭代,项目同样也会变得难以维护。

更好的做法其实是将 mock 逻辑与业务逻辑完全解耦,并放到独立的切面中管理, 这样就可以避免将非业务代码提交到仓库。

这种切面分为前端拦截和后端拦截两种方式,如下图所示,数据响应直接在对应的切面中被拦截返回:

前端拦截

前端拦截即在请求真正发送前做的拦截返回,这种切面通常可以通过 「Webview 容器定制」 和 「浏览器插件」 两种方式来实现。

Webview 容器定制

Webview 容器定制一般可以通过「网络拦截」和「脚本注入」两种方式,这也是一般混合应用中前端和 Native 交互的主要方式。

网络拦截

网络拦截经常会用在类似离线包的功能场景中,配合 mock 管理工具当然也可以用来接口模拟。 参考 Android,一般会使用下面的方法进行拦截来替换响应

public WebResourceResponse shouldInterceptRequest(final WebView view, final String urlstr)

此内容不是本文主要议题,不再深入展开

脚本注入

Android 和 iOS 都有能力向 Webview 直接注入 JS 逻辑,这也是 Hybrid 应用中 Bridge 通信层的实现方式。

如果在注入脚本中通过魔改 fetch 或 XMLHttpRequest 等原生对象,就可以达到对响应的拦截改写。

iOS 关键 API 举例

[self.webView stringByEvaluatingJavaScriptFromString:injectjs];

Android 关键代码片段

webView.loadUrl("javascript:" + injectjs);

但无论是网络拦截还是脚本注入,基于 Webview 容器的拦截很少会用在真实场景中,因为定制和使用成本都太高,而且只在本 App 中可以被使用。

浏览器插件

相较于定制 Webview 容器,浏览器插件显然是一个成本更低的前端容器劫持方案。 以 code-mancers/interceptor 这个项目为例:

通过 Interceptor 插件,可以很容易以 GUI 的方式配置我们的 mock 数据,简单直观,且完全不侵入工程代码。

前端拦截分析

前端拦截有个两个天然优势:

  • 可提供配置界面:由于是在浏览器端拦截,可使用 DOM API 提供例如 Interceptor 插件的可配置界面。
  • 就地生效:修改后无需重启服务。

但无论是浏览器插件还是定制 Webview 容器,实际上我们都忽略了一个重要事实:浏览器环境其实是多种多样的。 这导致了前端拦截的一个典型缺陷:无法跨浏览器使用,如上例的Intercepror插件就无法在微信浏览器中使用。

如果是通过服务端拦截的话就可以避免这种情况。

服务端拦截方案

服务端拦截实现接口 mock,主要通过一个单独的 dev server 层来实现,它一般在访问真实接口前拦截请求并返回模拟数据。

裸奔的 dev server

方便起见,以 Koa 为例,裸奔一个 dev server:

const proxy = require('koa-proxy');
const Koa = require('koa');

const app = new Koa();
app.use((ctx, next) => {
  switch (ctx.path) {
    case '/api/blog':
      ctx.body = { type: 'blog' };
      break;
    case '/api/user':
      ctx.body = { type: 'user' };
      break;
    default:
      return next();
  }
});
app.use(
  proxy({
    host: 'http://api.yoursite.com'
  })
);

app.listen(8000, () => {
  console.log(`server start at http://localhost:8000`);
});

如上例所见, 默认会将接口代理到 api.yoursite.com(你的目标 API 或后端基友的服务器)。 mock 数据的优先级大于真实的代理接口,比如我们访问https://localhost:8000/api/user,返回的就是我们的 mock 数据,后续如果需要增加 mock 接口,则需要不断添加 case 分支。

这种裸奔的方式很不直观,因为它将 mock 规则和其他 dev server 的配置逻辑杂糅了,且对于非 Node 选手有较高的学习成本。

专业的 dev server

由于裸奔 server 的明显痛点,一些聚焦于 dev server 领域的解决方案就开始大行其道,比如开发者耳熟能详的 webpack-dev-server

它集成了一些通用服务配置,例如端口、host、代理等等,并且设计为被集成在 webpack 的构建流程中以实现构建产物的 serve。 这样我们就可以将 mock 逻辑比较独立的嵌入其中,以下述 webpack 配置为例:

module.exports = {
  //...
  devServer: {
    port: 9000,
    headers: {
      'X-Custom-Foo': 'bar'
    },
    proxy: {
      '/api': 'http://localhost:3000'
    },
    before(app) {
      // 配置mock逻辑
      app.get('/api/blog', function(req, res) {
        res.json({ custom: 'response' });
      });
    }
  }
};

(专业的 dev server 用预设的配置代替了手工的代码逻辑,显著提高了开发效率)

但无论是裸起还是使用专业的 dev server,本质上还是存在以下问题:

  • 不支持热重载: 每次修改 mock 规则,都需要重新启动服务器。
  • 不直观: mock 规则和其他 server 配置杂糅,且对于非 Node 选手有较高的学习成本。
  • 无法提供界面支持,相较于前端拦截, 它无法提供 GUI 的界面配置能力。

使用 svrx 实现高效的接口 mock

从以上分析可以得出:前端拦截与后端拦截,都存在一些本质缺陷。 那是否有一种方式是同时拥有前后端接口 mock 的优势呢?答案就是 svrx

广告高能预警,看到这一步了,相信你已经是 svrx 的潜在客户了

svrx 简介

svrx(音:Server-X) 是一个微内核架构、插件化的前端开发服务器,内部功能模块主要包含三个部分:

  • 前端注入模块: svrx 劫持所有 html 响应注入种子脚本,此脚本会集成管理所注册的前端资源(JS、CSS)。
  • 后端注入模块: svrx 内置一个带有优先级的中间件注册模块。
  • 前后端通信模块: 实现前端与后端注入的通信方式统一(基于 websocket),可以以同构的方式完成事件或消息通信。

如上图所示,通过清晰的模块划分,插件可以以统一的方式来完成插件注册,灵活使用前端和后端注入功能。

svrx 也抽离了 dev-server 的通用功能,作为内置插件集成(包括 livereload、proxy、https 等等),其他专有领域的功能(如 markdown、qrcode 等)则以外部插件的方式提供,最大化实现便捷和灵活的平衡。

其中细分到接口 mock 领域,目前也有一系列开箱即用的配套满足开发者的需求。让我们来试一试吧!

安装

npm install @svrx/cli -g

注: 后续所有插件能力都不需要再显式安装了

使用

切换到你的工作目录并运行svrx,你会发现一个通用的 dev-server 已经运行起来了。

svrx

svrx Routing DSL 实现接口 mock

具体到接口 mock 的需求,我们可以直接使用内置的动态路由功能

touch route.js
svrx --route route.js

以上就是成功启动的界面, 在route.js加入以下代码:

get('/api/user/:id').to.json({ name: 'svrx' });

浏览器打开/api/user/1,可以看到对应的 JSON 响应。所有在route.js的改动都是支持 hot reload 的,我们无需重启服务器。

更多 svrx Routing DSL 的使用指南请点击这里

如果你使用 svrx 路由来代替上面的其他 dev-server,除了路由写法更直观高效外,还有一个作用就是可以更细粒度地管理路由的优先级,比如 mock 和 proxy 的优先级:

get('/api/user/:id').to.json({ name: 'svrx' });
post('/api/blog(.*)').to.proxy('http://path.to.api.com');
get('/(.*)').to.send('404 PAGE IS NOT FOUND');

注:路由规则越前置,优先级越高

使用 mock 插件来快速模拟接口

直接裸用 svrx 路由能解决 mock 的功能性问题,但无法解决 mock 的效率问题。

基于此,svrx 官方提供了svrx-plugin-mock, 它内置了好用的 mock.js ,帮助我们实现快速数据模拟:

svrx --mock --route route.js

直接使用 -p mock 或简写--mock 来激活这个插件。

如上图红框所示,svrx 的插件体系有首次即安装的特性,被安装插件会自动进入 svrx 全局管理,后续激活插件无需重复下载,更重要的是不会污染你的工作目录(包括node_modules)。

route.js中加入以下代码:

get('/api/user/:id').to.mock({
  name: '@name',
  email: '@email'
});

mock 插件注册了一个名为 mock 的路由 Action,可在 Routing DSL 中被使用

再次访问/api/user/1,你会得到以下满足一定模式的随机响应,比如:

{
  "user": "Linda Thomas",
  "email": "g.ykyiexto@toaloso.cc"
}

除此之外,mock 插件也能快速模拟一些列表循环的逻辑, 比如:

get('/api/user/:id').to.mock({
  name: '@name',
  email: '@email',
  'region|1-3': ['@region']
});

对应的响应中region将会是一个长度是 1 到 3 的地区数组,比如:

{
  "name": "Nancy Allen",
  "email": "aopao@qpo.scm",
  "region": ["西北", "华中"]
}

可以看到使用 mock 插件可以大大提高我们的 mock 效率,并且阅读仍然很直观。

使用 json-server 创建基于一定规则的批量接口

svrx 的 mock 插件加上内置的动态路由功能基本上能高效的处理 90% 的本地 mock 需求了。

但如果你的服务是基于 json-server 规范的,你也可以利用 svrx-plugin-json-server 来快速实现海量接口,让我们一起来试下吧。

首先在当前目录创建如下内容的 db.json 文件:

{
  "posts": [{ "id": 1, "title": "json-server", "author": "typicode" }],
  "comments": [{ "id": 1, "body": "some comment", "postId": 1 }]
}

启动 svrx 并激活 json-server 插件:

svrx -p json-server --route route.js

与 mock 类似,json-server 插件会注册一个名为 jsonServer路由 Action

route.js 加入以下配置:

route('/(.*)').to.jsonServer();

以上语句会将所有请求直接代理到内部的 json-server 模块。

访问 /posts, 将看到如下响应:

[
  {
    id: 1,
    title: 'json-server',
    author: 'typicode'
  }
];

值得一提的是,其实 json-server 内置了全部 crud 操作,以posts为例:

POST /posts        => Create 即创建操作
UPDATE /posts/:id  => UPDATE 即更新操作
GET /posts/:id     => READ 即读操作
GET /posts         => READ LIST 即列表读操作
DELETE /posts/:id  => DELETE 即删除操作

举个栗子,当你发起一个创建请求(以前端 fetch 为例):

fetch('/posts', {
  method: 'POST',
  body: JSON.stringify({ title: 'svrx', author: 'x-orpheus' }),
  headers: {
    'content-type': 'application/json'
  }
});

你再访问 /posts 列表,会发现多了一条记录,且这条记录会同步持久化到db.json:

[
  {
    id: 1,
    title: 'json-server',
    author: 'typicode'
  },
  {
    title: 'svrx',
    author: 'x-orpheus',
    id: 2
  }
];

请求改写

通过串连路由的 rewrite 指令,我们可以做到只引导部分流量到 json-server 服务,例如:

route('/api/(.*)')
  .rewrite('/{0}')
  .to.jsonServer(); // /api/posts => /posts

这样只有/api开头的请求会代理到 json-server,其他请求可以走其他的 mock 逻辑。

使用接口管理平台

以上所有的 mock 方式其实都有一个较大的问题,就是 mock 规则都是在本地的,我们无法共享配置。

而实际上较大的团队都应该有 API 接口管理平台来统一管理接口定义,在网易我们使用NEI:接口管理平台来管理 API(由云音乐前端团队维护,欢迎免费试用)。 一般这类平台都有接口模拟功能,代理到这类平台,我们可以轻松实现规范化的接口 mock:

搭配这种接口管理平台,云音乐团队也封装了 svrx-plugin-nei (即将开源)来实现代理到 NEI 平台的数据模拟,如下图所示:

基于接口管理平台的接口模拟是与真实接口规范匹配的,所以前后端规范性会更一致,并且它的平台属性也方便开发者共享配置。 但这种方式也有巨大的劣势,就是灵活度远低于本地接口模拟

值得一提的是此插件利用 svrx 的前端注入能力实现了跨浏览器的前端配置界面, svrx 通过内部 injector 模块自动为响应是 html 类型的资源注入种子脚本,种子脚本会集成所有 plugin 注册的脚本内容,从而实现了前端逻辑在 dev-server 侧的注入。

通过 mock 解析 svrx 的核心价值

我们可以看到,以上所有特性在数据 Mock 领域都是功能互补的,没有所谓的万金油方案。

所以 svrx 带给我们其实并不是 svrx-plugin-mocksvrx-plugin-json-server亦或是svrx-plugin-nei 等等这些隔离的单一功能, 而是基于 svrx 这个平台,我们可以很容易的将这些围绕在dev-server领域的功能以一种统一的方式集成起来使用,避免重复的安装和配置工作

举个栗子 🌰,当开发者希望 JSON 响应的格式输出更好看时,可以直接使用-p json-viewer来激活对应插件:

svrx --route router.js \
  -p json-viewer \
  -p json-server \
  -p mock

响应视图立刻从下面的无序纯文本:

无缝切换为直观的下图:

再举个栗子 🌰,当我们想将我们的本地服务暴露到外网使用时,可以使用 -p localtunnel 激活 localtunnel 的反向隧道网关服务。

svrx --route route.js \
  -p json-viewer \
  -p json-server \
  -p mock \
  -p "localtunnel?host=https://tunnel.svrx.io"
  • 参数过长时可以使用 svrx 配置文件
  • tunnel.svrx.io 是属于福利性设施,不确保稳定性,请大家悠着点使用以避免服务因为各种原因不可用。

上图类似 https://fast-dragon-86.tunnel.svrx.io 的随机地址即可用于外网访问的域名了,这种即开即走的使用体验是碎片化的各种 dev server 平台无法提供给你的。

更重要的是,接口 mock 其实仅仅只是我们日常开发中的一环,svrx 的定位是一个通用开发服务器,它内置集成了serveproxylivereloadroute等等日常前端开发中必不可少的功能, 并且可以通过社区不断增加的插件池来进行自由组合使用,这个我们从上述接口 mock 这一场景的描述中应该已经看到。

完全可以这么说,围绕 dev-server 的设施越多,svrx 存在的价值就越大

写在最后

除了完全不推荐的「硬编码方案」之外,做到与业务代码解耦的「纯前端拦截」和「纯后端拦截」的接口 mock 方案也都存在一些无法规避的本质性问题。

而使用 svrx 以及它配套的社区插件,我们除了可以整合前端和后端拦截的优势,还可以将各种 mock 功能集成在一个服务中运行,解决了工具的碎片化问题,从而高效的实现接口 mock 需求。

Links

本文发布自 网易云音乐前端团队,可自由转载,转载请在标题标明转载并在显著位置保留出处。我们一直在招人,如果你恰好准备换工作,又恰好喜欢云音乐,那就 加入我们