模拟服务工作者

使用模拟服务工作者在 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