前端在开发的过程中可能会调用到后端的 API,可能因为 API 未完成、或者返回数据不稳定,所以在前端的开发过程中当然不能依赖真实的 API。所以需要用到 Mock 技术来模拟 API 的请求和响应。这里演示如何使用 msw 来使用 mock。msw 官网 msw

什么是 Mock

Mock 技术,是用于拦截请求,并可以使用 mock 定义的响应数据来返回,常用在开发、测试环境中:用于模拟稳定的响应数据,方便开发调试。

安装

运行下面的命令安装 msw:

npm install msw --save-dev

注意,因为只需要在开发中使用到 msw,所以要加上 --save-dev

配置

接下来要配置 msw,哪些请求是可以拦截的: 在 src 文件夹下新建一个 mocks 文件夹,我们有关 mock 的代码都会写在这个文件夹里。我们在这个文件夹里新建一个名为 handlers.ts 的文件,在这个文件里我们将告诉 msw 要 mock 哪些接口

// src/mocks/handlers.js
import { http } from "msw";

export const handlers = [
  // Handles a POST /login request
  http.post("/login", null),

  // Handles a GET /user request
  http.get("/user", null),
];

在上面的示例代码中,我们首先从 msw 中导入了 http 模块,关于 RESTAPI 规范的 mock 功能都在这个 http 模块里。可以使用 http.[Method] 语法来定义你的 mock 请求,如示例中使用 http.post(...) 来 mock 了一个 post 请求。这个方法需要两个参数,第一个参数 mock 的 api 地址,第二个参数是一个返回假数据的函数。

上面的示例中,我们定义了两个 mock,且返回假数据的函数是 null。为了方便演示,我们只留一个。

// src/mocks/handlers.js
import { http, HttpResponse } from "msw";

export const handlers = [
  http.get("/getSth", () => {
    return HttpResponse.json({
      data: 'data'
    });
  }),
];

们看上面的代码,我们定义了一个 get 请求的 mock,api 路由为 /getSth,使用 HttpResponse.json() 函数返回假数据,默认的状态码是 200,使用 HttpResponse.json() 的 Content-Type: "application/json",并且传入了响应内容。 如果希望返回的 Content-Type"text/plain" 可以使用 HttpResponse.text('body')

我们将 mock 放入了一个名为 handlers 的数组,记得 export 导出,需要在外部用到。

浏览器拦截

我们已经写好了 mock 的数据,接下来要拦截浏览器的请求。幸运的是,msw 帮我们做好了大部分的工作,我们只需要在终端里执行 npx msw init <PUBLIC_DIR> --save,这个命令里,要把 <PUBLIC_DIR> 替换为你项目的公开目录,我们的项目是使用 vite 构建的,它的公开目录就是 ./public。后面的参数 --save 将我们指定的公开目录保存到 package.json 里。 在我们使用 vite 构建的 react-ts 项目里,完整的命令是 npx msw init public/ --save。

执行后,你将会看到这个文件:./public\mockServiceWorker.js。文件内的代码不需要理会,我们只需要知道它拦截了浏览器的请求即可。

src/mocks/ 文件夹下新建文件 browser.ts,写入如下代码:

import { setupWorker } from "msw";
import { handlers } from "./handlers";

export const worker = setupWorker(...handlers);

我们导入了 setupWorker,这个方法用于设置 mock,你可以看到我们导入了 handlers,先前写的 mock 就在这里,将它解构传入 setupWorker,返回了一个 worker。我们将使用这个 worker 来启动 mock 服务。

启动 Mock

我们要在项目启动时启动 mock 服务,所以回到我们的启动文件里,如果你用的是 vite,那么启动文件应该是 main:

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'

async function enableMocking() {
  if (process.env.NODE_ENV !== 'development') {
    return
  }

  const { worker } = await import('./mocks/browser')

  // `worker.start()` returns a Promise that resolves
  // once the Service Worker is up and ready to intercept requests.
  return worker.start()
}

enableMocking().then(() => {
  ReactDOM.createRoot(document.getElementById('root')!).render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
  )
});

在上面的代码中我们将 ReactDOM.createRoot 放在 enableMocking() 成功执行后再执行,这是因为要确保 mock 服务启动成功后再执行 React 的代码。可以看到 enableMocking() 的实现,显示判断了当前是否为开发环境,是的话再启用 mock。

到这里,简单的 mock 就已经可以使用了,我们在开发中启动项目时就会顺道启动 mock 服务,如果启动 mock 成功,你会在浏览器控制台中看到:

[MSW] Mocking enabled.

测试一下

直接写一个按钮,点击后发起请求就可以了,要确保发起请求的 url 是在 handlers 里面有配置到的:

import { useState } from "react";

export default function App() {
  const [result, setResult] = useState("");

  async function handleRequest() {
	  //  handlers 里有配置到 '/getSth',所以会拦截这个请求
    const res = await fetch("/getSth").then((resp) => {
      return resp.json();
    });
    setResult(res);
  }

  return (
    <div className="App">
      <button onClick={handleRequest}>发送请求</button>
      <div>请求结果:</div>
      <div>{result}</div>
    </div>
  );
}

如上,我们发起请求后,预期中会被 mock 拦截,返回 mock 的数据。由于我们没有真实的接口,所以如果 mock 启动失败,那么请求是会报错。