使用文件路径组织 MSW handlers

271 阅读2分钟

原文发表在我的博客 doma.land/blog/msw-ha…

当我们使用 Mock Service Worker (MSW) 进行 API Mocking 时,随着接口数量的增长,我们常会遇到这些问题:

其一,REST API 常常同一个路径支持多个请求方法,我们需要重复书写相同的 API 路径。

// mocks/handlers.js
const handlers = [
  rest.get("/api/todos", "..."),
  rest.post("/api/todos", "..."),
  rest.get("/api/todos/:id", "..."),
  rest.put("/api/todos/:id", "..."),
  rest.delete("/api/todos/:id", "..."),

  // 如果路径有嵌套,则会更糟
  rest.get("/api/todos/:id/attachments", "..."),
  rest.post("/api/todos/:id/attachments", "..."),
  rest.get("/api/todos/:id/attachments/:name", "..."),
  rest.put("/api/todos/:id/attachments/:name", "..."),
  rest.delete("/api/todos/:id/attachments/:name", "..."),
]

其二,将这么多 Mock 接口定义在同一个文件中很笨重,因此我们往往会将它们拆分到多个模块中。 通常,我们会根据业务功能来拆分模块,而对这些模块的命名常常恰与 REST API 的路径是重复的。

作为程序员,我们既不喜欢命名,也不喜欢重复。 那么,有没有一种方法,可以直接通过模块文件的路径来拼接出 API 路径,就像 Next.js 的 API Routes 那样?例如,能否创建一个 api/todos.js 文件并导出一个 POSTGET 常量,然后自动为我们生成 POST /api/todosGET /api/todos 两个 MSW handlers?

// mocks/api/todos.js
export const GET = (req, res, { json }) => res(json("..."));

export const POST = (req, res, { status, json }) => res(status(201), json("..."));

幸运的是,如果你使用 webpack 进行构建,你可以通过 require.context 做到这一点。

require.context 允许我们动态地去 require 一个目录下的各个文件,例如:

// 搜索 ./api 目录下所有以 ".js" 结尾的文件(包括子目录下的文件)
require.context("./api", true, /\.js$/)

这一特性使得我们可以实现类似 API Routes 的功能:

// mocks/handlers.js
const requireMockHandler = require.context("./api", true, /\.js$/)

export const handlers = requireMockHandler
  .keys()
  .reduce((allHandlers, path) => {
    const handlers = requireMockHandler(path)

    // 将 require.context 读取到的文件路径 "./path/[to]/handlers.js"
    // 转换为 MSW 接收的 API 路径 "/api/path/:to/handlers"
    const apiPath =
      "/api/" +
      path
        // 移除开头的 "./"
        .replace(/^\.\//, "")
        // 移除结尾的 ".js"
        .replace(/\.[^/.]+$/, "")
        // 将方括号标记的变量 "[param]" 改为冒号标记 ":param"
        .replace(/\[([^/]*)\]/g, ":$1")

    const pathHandlers = Object.getOwnPropertyNames(handlers).reduce((moduleHandlers, exportName) => {
      const handler = handlers[exportName]
      const method = exportName.toLowerCase()

      // 对于模块中导出的每个字段,注册一个 msw handler
      if (method in rest) {
        moduleHandlers.push(rest[method](apiPath, handler))
      }

      return moduleHandlers
    }, [])

    return [...allHandlers, ...pathHandlers]
  }, [])

最终,我们只需要根据 API 路径在 ./api 目录下创建文件即可获得所需的 msw handlers,而不再需要反复在代码中书写 API 路径。

src/mocks/
├── api/
|   ├── todos.js
|   └── todos/
|       ├── [id].js
|       └── [id]/
|           ├── attachments.js
|           └── attachments/
|               └── [name].js
└── handlers.js

文中展示的示例代码片段可在此仓库中找到 github.com/SevenOutman…