模拟服务工作者

使用模拟服务工作者在 Storybook 中模拟 API 请求。

在 Github 上查看

功能

  • 直接在你的故事中模拟 REST 和 GraphQL 请求。
  • 记录组件在各种场景下的行为。
  • 免费获得 a11y、快照和视觉测试,使用其他插件。

完整文档和实时演示

安装和设置

安装 MSW 和插件

使用 npm

npm i msw msw-storybook-addon -D

或使用 yarn

yarn add msw msw-storybook-addon -D

在你的 public 文件夹中为 MSW 生成服务工作者。

如果你已经在项目中使用 MSW,你可能之前已经做过这个步骤,所以你可以跳过此步骤。

npx msw init public/

如果你没有使用 public,请参考MSW 官方指南了解框架特定的路径。

配置插件

通过在 ./storybook/preview.js 中初始化 MSW 并提供 MSW 加载器来在 Storybook 中启用 MSW。

import { initialize, mswLoader } from 'msw-storybook-addon'

// Initialize MSW
initialize()

const preview = {
  parameters: {
    // your other code...
  },
  // Provide the MSW addon loader globally
  loaders: [mswLoader],
}

export default preview

启动 Storybook

运行 Storybook 时,你必须将 public 文件夹作为资产提供给 Storybook,以便包含 MSW,否则它将无法在浏览器中使用。

这意味着你应该在 Storybook 主配置文件中设置 staticDirs 字段。如有必要,请参考文档

npm run storybook

使用

你可以将请求处理程序(https://mswjs.io/docs/concepts/request-handler)传递给 msw 参数的 handlers 属性。这通常是一个处理程序数组。

import { http, HttpResponse } from 'msw'

export const SuccessBehavior = {
  parameters: {
    msw: {
      handlers: [
        http.get('/user', () => {
          return HttpResponse.json({
            firstName: 'Neil',
            lastName: 'Maverick',
          })
        }),
      ],
    },
  },
}

高级使用

组合请求处理程序

handlers 属性也可以是一个对象,其中键可以是处理程序数组或处理程序本身。这使你能够使用参数继承从 preview.js 继承(并选择性地覆盖/禁用)处理程序。

type MswParameter = {
  handlers: RequestHandler[] | Record<string, RequestHandler | RequestHandler[]>
}

假设你有一个应用程序,几乎每个组件都需要以相同的方式模拟对 /login/logout 的请求。你可以为这些请求在 preview.js 中设置全局 MSW 处理程序,并将它们捆绑到一个名为 auth 的属性中,例如

//preview.ts
import { http, HttpResponse } from 'msw'

// These handlers will be applied in every story
export const parameters = {
  msw: {
    handlers: {
      auth: [
        http.get('/login', () => {
          return HttpResponse.json({
            success: true,
          })
        }),
        http.get('/logout', () => {
          return HttpResponse.json({
            success: true,
          })
        }),
      ],
    },
  },
}

然后,你可以在你的单个故事中使用其他处理程序。Storybook 将合并全局处理程序和故事处理程序。

import { http, HttpResponse } from 'msw'

// This story will include the auth handlers from .storybook/preview.ts and profile handlers
export const SuccessBehavior = {
  parameters: {
    msw: {
      handlers: {
        profile: http.get('/profile', () => {
          return HttpResponse.json({
            firstName: 'Neil',
            lastName: 'Maverick',
          })
        }),
      },
    },
  },
}

现在假设你想覆盖 auth 的全局处理程序。你所要做的就是在你的故事中再次设置它们,这些值将优先使用。

import { http, HttpResponse } from 'msw'

// This story will overwrite the auth handlers from preview.ts
export const FailureBehavior = {
  parameters: {
    msw: {
      handlers: {
        auth: http.get('/login', () => {
          return HttpResponse.json(null, { status: 403 })
        }),
      },
    },
  },
}

如果你想禁用全局处理程序怎么办?你所要做的就是将它们设置为 null,它们将在你的故事中被忽略。

import { http, HttpResponse } from 'msw'

// This story will disable the auth handlers from preview.ts
export const NoAuthBehavior = {
  parameters: {
    msw: {
      handlers: {
        auth: null,
        others: [
          http.get('/numbers', () => {
            return HttpResponse.json([1, 2, 3])
          }),
          http.get('/strings', () => {
            return HttpResponse.json(['a', 'b', 'c'])
          }),
        ],
      },
    },
  },
}

配置 MSW

msw-storybook-addon 以默认配置启动 MSW。initialize 接受两个参数

  • options:当在浏览器中时,它传递给worker.start(),当在 Node 中时,它传递给server.listen(),因此预期相同的类型。
  • initialHandlers:一个 RequestHandler[] 类型,这个数组将扩展到在浏览器中时为setupWorker(),在 Node 中时为setupServer()

一个常见的例子是配置onUnhandledRequest 行为,因为 MSW 在存在未处理的请求时会记录警告。

如果你希望 MSW 绕过未处理的请求,什么也不做

// .storybook/preview.ts
import { initialize } from 'msw-storybook-addon'

initialize({
  onUnhandledRequest: 'bypass',
})

如果你希望在故事发出应该处理但没有处理的请求时发出有用的消息

// .storybook/preview.ts
import { initialize } from 'msw-storybook-addon'

initialize({
  onUnhandledRequest: ({ url, method }) => {
    const pathname = new URL(url).pathname
    if (pathname.startsWith('/my-specific-api-path')) {
      console.error(`Unhandled ${method} request to ${url}.

        This exception has been only logged in the console, however, it's strongly recommended to resolve this error as you don't want unmocked data in Storybook stories.

        If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses
      `)
    }
  },
})

虽然组合处理程序 是可能的,但这依赖于 Storybook 的合并逻辑,它只在你的故事参数中的处理程序是对象而不是数组时才有效。为了解决这个限制,你可以将初始请求处理程序直接作为第二个参数传递给 initialize 函数。

// .storybook/preview.ts
import { http, HttpResponse } from 'msw'
import { initialize } from 'msw-storybook-addon'

initialize({}, [
  http.get('/numbers', () => {
    return HttpResponse.json([1, 2, 3])
  }),
  http.get('/strings', () => {
    return HttpResponse.json(['a', 'b', 'c'])
  }),
])

在 Node.js 中使用便携式故事使用插件

如果你正在使用便携式故事,你需要确保 MSW 加载程序被正确应用。

Storybook 8.2 或更高版本

如果你通过调用故事的 play 函数来正确设置项目注释,MSW 加载程序将自动应用。

import { composeStories } from '@storybook/react'
import * as stories from './MyComponent.stories'

const { Success } = composeStories(stories)

test('<Success />', async() => {
  // The MSW loaders are applied automatically via the play function
  await Success.play()
})

Storybook < 8.2

你可以通过在渲染故事之前调用 applyRequestHandlers 帮助程序来做到这一点。

import { applyRequestHandlers } from 'msw-storybook-addon'
import { composeStories } from '@storybook/react'
import * as stories from './MyComponent.stories'

const { Success } = composeStories(stories)

test('<Success />', async() => {
  // 👇 Crucial step, so that the MSW loaders are applied
  await applyRequestHandlers(Success.parameters.msw)
  render(<Success />)
})

注意:applyRequestHandlers 实用程序应该是一个内部细节,由便携式故事自动调用,但由于它在 Storybook 7 中不可用,因此它由插件导出。它将在即将发布的版本中删除,因此建议你尽可能升级到 Storybook 8。

故障排除

MSW 干扰 HMR(热模块替换)

如果你在控制台中遇到 [MSW] Failed to mock a "GET" request to "https://127.0.0.1:6006/4cb31fa2eee22cf5b32f.hot-update.json" 等问题,MSW 可能正在干扰 HMR。这并不常见,似乎只发生在 Webpack 项目中,但如果你遇到了这个问题,你可以按照此问题中的步骤进行修复。

https://github.com/mswjs/msw-storybook-addon/issues/36#issuecomment-1496150729