Background

开发没有mock还是比较痛苦的,最开始接入mock-server需要在业务里新开一个 mock 手动写 json,维护成本很高,也不利于共享。一直以来,都想着接入现在开源的mock产品,但是,一方面需要收钱,一方面比较复杂需要自己搭建服务&数据,一致拖到现在还没有顺手的mock方案。

最近决定自己写个简单的,可能有人说你又在创建轮子,我的想法是,

  • 一方面我只开发mock功能,不支持其他比如api自动化测试的功能,我要的功能就是mock
  • 另一方面mock通常只是在开发阶段才用到,就那么2天时间,后面进入联调就不会使用了
  • 再有就是用来测试接口超时等异常情况

综上,一个简单的mock平台足以满足我的需求,实际上,做这么个平台也就需要1天时间而已。

Mock平台

创建项目

每个业务需要创建对应的项目,我们就简单点

const MockProjectSchema = new mongoose.Schema({
  id: String,
  name: String,
  desc: String,
})

这里 id 才是最关键的,后续业务要使用

创建 API

api 层只要几个功能

  • 支持 GET/POST

  • 支持超时 timeout

  • 一个 json editor

至于 api 的匹配,一期做个模糊匹配即可,也就是只关心 method/url,不考虑入参

const MockAPISchema = new mongoose.Schema({
  projectId: String,
  url: String,
  desc: String,
  method: String,
  mocked: Boolean,
  timeout: Number, // 超时时间
  json: String, // 出参
})

然后就是对应的 crud 接口实现,比较简单,这里就简单举个例子,不再赘述。

class MockMeController {
  prefix = '/mockme'

  async queryProjectById(ctx: any) {
    const { id } = ctx.request.params
    const data = await MockProjectModel.findOne({ id }, { __v: 0, _id: 0 })
    return data
  }
}

业务接入

重点看看,业务怎么接入平台

mock option

从开发者的角度思考,他只需要在 api 这个文件对需要 mock 的 api,加一个参数即可

async queryList() {
  await request.get({
    url: 'test/list',
  }, {
    mock: true, // 开启 mock
  })
}

要想拿到 mock 数据,必须和 mock 平台关联,也就是要拦截请求,代理到平台的地址。以 axios 为例,我们封装如下方法

async get(confg, option) {
  return await request({ ...conf, method: 'get', }, option)
}
async request(conf, option) {
  // 判断接口是否开启了 mock
  config.url = option.mock ? `${mockUrl}${config.url}` : conf.url
  await axios.request(conf)
}

这段代码判断是否开启了 mock,如果开启,则把 mockUrl 和业务的 url 拼接起来,这里的 mockUrl 是代理到平台的地址,这样就实现了请求的拦截。

不过,这里会有跨域的问题,后面会解决这个问题

mock url

mockUrlmock 平台提供的,前面我们说过每个业务对应一个mock项目,也就会有对应唯一的项目id,平台根据这个 id会自动生成一个地址,格式如下

https://test.com/mockme/proxy/:projectId
  • test.com 是平台服务器地址

  • /mockme/proxy是路由

  • projectId 就是每个业务在 mock 平台的项目 id

举个例子

// 项目一
https://test.com/mockme/proxy/elem

// 项目二
https://test.com/mockme/proxy/ant-design

请求打到平台后,平台会有对应的接口处理

class MockMeController {
  ...
  async proxyAPI(ctx: any) {
    const urlRegResult = ctx.request.path?.match(/\/mockme\/proxy(.*)/)
    const [, regUrl] = urlRegResult
    const pathNode = pathToRegexp('/:projectId/:proxyURL*').exec(regUrl)
    const [, projectId, proxyURL] = pathNode

    const doc = await MockAPIModel.findOne({ projectId, url: '/' + proxyURL })
  }
}

router.all(`/mockme/proxy/:id/(.*)`, proxyAPI)

这里有几点注意

  • 我们使用 router.all 来覆盖所有的 http method,毕竟不知道业务会有哪些方法

  • 通过 path-to-regexp 转换为 url 为正则,方便匹配需要的数据

跨域

Q:前面说过代理地址会有跨域问题,那该怎么解决呢?

A:mock 平台配置 cors

Q:但是怎么识别请求是真的来自业务呢?cors 只能对白名单列表的 url 开启,

A:业务连接到 mock 平台,可以带个请求头,表明身份

axios 为例,只要添加一个 requestInterceptor 即可

this.axiosInstance.interceptors.request.use((config) => {
    // 加签校验,配置 mock 请求头
    if (options?.serverMode && options?.mock) {
      config.headers['x-test-mock'] = 'mock-me'
    }
    return config
}, undefined)

服务端则检测请求头,进行白名单校验,以 koa 为例

app.use(
    cors({
      origin: (ctx: any) => {
        // cors 预检
        if (ctx.header['access-control-request-headers']?.includes('x-test-mock')) {
          return url
        }

        if (ctx.header['x-test-mock'] == 'mock-me') {
          return url
        }
      },
      allowHeaders: ['Content-Type', 'x-test-mock'],
    })
  )

综上,一个简单的 api mock 平台就开发好了,还有更多功能可以扩展,比如 api 自动化测试、实现网页版本 postman 等。