Docs
Storybook Docs

模拟网络请求

对于发出网络请求的组件(例如从 REST 或 GraphQL API 获取数据),你可以使用 模拟服务工作者 (MSW) 等工具模拟这些请求。MSW 是一个 API 模拟库,它依赖于服务工作者来捕获网络请求并在响应中提供模拟数据。

¥For components that make network requests (e.g. fetching data from a REST or GraphQL API), you can mock those requests using a tool like Mock Service Worker (MSW). MSW is an API mocking library, which relies on service workers to capture network requests and provides mocked data in response.

MSW 插件 将此功能带入 Storybook,允许你在故事中模拟 API 请求。下面是如何设置和使用插件的概述。

¥The MSW addon brings this functionality into Storybook, allowing you to mock API requests in your stories. Below is an overview of how to set up and use the addon.

设置 MSW 插件

¥Set up the MSW addon

首先,如有必要,运行此命令来安装 MSW 和 MSW 插件:

¥First, if necessary, run this command to install MSW and the MSW addon:

npm install msw msw-storybook-addon --save-dev

如果你尚未使用 MSW,请生成 MSW 运行所需的服务工作文件:

¥If you're not already using MSW, generate the service worker file necessary for MSW to work:

npx msw init public/

然后确保 Storybook 配置中的 staticDirs 属性将包含生成的服务工作文件(默认情况下在 /public 中):

¥Then ensure the staticDirs property in your Storybook configuration will include the generated service worker file (in /public, by default):

.storybook/main.ts
// Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite)
import type { StorybookConfig } from '@storybook/your-framework';
 
const config: StorybookConfig = {
  framework: '@storybook/your-framework',
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  staticDirs: ['../public', '../static'],
};
 
export default config;

最后,初始化插件并将其应用于所有带有 项目级别加载器 的故事:

¥Finally, initialize the addon and apply it to all stories with a project-level loader:

.storybook/preview.ts
// Replace your-renderer with the renderer you are using (e.g., react, vue, etc.)
import { Preview } from '@storybook/your-renderer';
 
import { initialize, mswLoader } from 'msw-storybook-addon';
 
/*
 * Initializes MSW
 * See https://github.com/mswjs/msw-storybook-addon#configuring-msw
 * to learn how to customize it
 */
initialize();
 
const preview: Preview = {
  // ... rest of preview configuration
  loaders: [mswLoader], // 👈 Add the MSW loader to all stories
};
 
export default preview;

模拟 REST 请求

¥Mocking REST requests

如果你的组件从 REST API 获取数据,则可以使用 MSW 在 Storybook 中模拟这些请求。作为示例,请考虑此文档屏幕组件:

¥If your component fetches data from a REST API, you can use MSW to mock those requests in Storybook. As an example, consider this document screen component:

YourPage.tsx
import React, { useState, useEffect } from 'react';
 
import { PageLayout } from './PageLayout';
import { DocumentHeader } from './DocumentHeader';
import { DocumentList } from './DocumentList';
 
// Example hook to retrieve data from an external endpoint
function useFetchData() {
  const [status, setStatus] = useState<string>('idle');
  const [data, setData] = useState<any[]>([]);
  useEffect(() => {
    setStatus('loading');
    fetch('https://your-restful-endpoint')
      .then((res) => {
        if (!res.ok) {
          throw new Error(res.statusText);
        }
        return res;
      })
      .then((res) => res.json())
      .then((data) => {
        setStatus('success');
        setData(data);
      })
      .catch(() => {
        setStatus('error');
      });
  }, []);
 
  return {
    status,
    data,
  };
}
 
export function DocumentScreen() {
  const { status, data } = useFetchData();
 
  const { user, document, subdocuments } = data;
 
  if (status === 'loading') {
    return <p>Loading...</p>;
  }
  if (status === 'error') {
    return <p>There was an error fetching the data!</p>;
  }
  return (
    <PageLayout user={user}>
      <DocumentHeader document={document} />
      <DocumentList documents={subdocuments} />
    </PageLayout>
  );
}

此示例使用 fetch API 发出网络请求。如果你使用的是不同的库(例如 axios),则可以将相同的原则应用于 Storybook 中的模拟网络请求。

¥This example uses the fetch API to make network requests. If you're using a different library (e.g. axios), you can apply the same principles to mock network requests in Storybook.

使用 MSW 插件,我们可以编写使用 MSW 模拟 REST 请求的故事。以下是文档屏幕组件的两个故事的示例:一个成功获取数据,另一个失败。

¥With the MSW addon, we can write stories that use MSW to mock the REST requests. Here's an example of two stories for the document screen component: one that fetches data successfully and another that fails.

YourPage.stories.ts|tsx
// Replace your-framework with the name of your framework (e.g. nextjs, vue3-vite)
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { http, HttpResponse, delay } from 'msw';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  component: DocumentScreen,
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
// 👇 The mocked data that will be used in the story
const TestData = {
  user: {
    userID: 1,
    name: 'Someone',
  },
  document: {
    id: 1,
    userID: 1,
    title: 'Something',
    brief: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
    status: 'approved',
  },
  subdocuments: [
    {
      id: 1,
      userID: 1,
      title: 'Something',
      content:
        'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
      status: 'approved',
    },
  ],
};
 
export const MockedSuccess: Story = {
  parameters: {
    msw: {
      handlers: [
        http.get('https://your-restful-endpoint/', () => {
          return HttpResponse.json(TestData);
        }),
      ],
    },
  },
};
 
export const MockedError: Story = {
  parameters: {
    msw: {
      handlers: [
        http.get('https://your-restful-endpoint', async () => {
          await delay(800);
          return new HttpResponse(null, {
            status: 403,
          });
        }),
      ],
    },
  },
};

模拟 GraphQL 请求

¥Mocking GraphQL requests

GraphQL 是另一种在组件中获取数据的常用方法。你可以使用 MSW 在 Storybook 中模拟 GraphQL 请求。以下是从 GraphQL API 获取数据的文档屏幕组件的示例:

¥GraphQL is another common way to fetch data in components. You can use MSW to mock GraphQL requests in Storybook. Here's an example of a document screen component that fetches data from a GraphQL API:

YourPage.ts|tsx
import { useQuery, gql } from '@apollo/client';
 
import { PageLayout } from './PageLayout';
import { DocumentHeader } from './DocumentHeader';
import { DocumentList } from './DocumentList';
 
const AllInfoQuery = gql`
  query AllInfo {
    user {
      userID
      name
    }
    document {
      id
      userID
      title
      brief
      status
    }
    subdocuments {
      id
      userID
      title
      content
      status
    }
  }
`;
 
interface Data {
  allInfo: {
    user: {
      userID: number;
      name: string;
      opening_crawl: boolean;
    };
    document: {
      id: number;
      userID: number;
      title: string;
      brief: string;
      status: string;
    };
    subdocuments: {
      id: number;
      userID: number;
      title: string;
      content: string;
      status: string;
    };
  };
}
 
function useFetchInfo() {
  const { loading, error, data } = useQuery<Data>(AllInfoQuery);
 
  return { loading, error, data };
}
 
export function DocumentScreen() {
  const { loading, error, data } = useFetchInfo();
 
  if (loading) {
    return <p>Loading...</p>;
  }
 
  if (error) {
    return <p>There was an error fetching the data!</p>;
  }
 
  return (
    <PageLayout user={data.user}>
      <DocumentHeader document={data.document} />
      <DocumentList documents={data.subdocuments} />
    </PageLayout>
  );
}

此示例使用 GraphQL 和 Apollo 客户端 发出网络请求。如果你使用的是不同的库(例如 URQLReact Query),则可以将相同的原则应用于 Storybook 中的模拟网络请求。

¥This example uses GraphQL with Apollo Client to make network requests. If you're using a different library (e.g. URQL or React Query), you can apply the same principles to mock network requests in Storybook.

MSW 插件允许你编写使用 MSW 模拟 GraphQL 请求的故事。这是一个演示文档屏幕组件的两个故事的示例。第一个故事成功获取数据,而第二个故事失败。

¥The MSW addon allows you to write stories that use MSW to mock the GraphQL requests. Here's an example demonstrating two stories for the document screen component. The first story fetches data successfully, while the second story fails.

YourPage.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
import { graphql, HttpResponse, delay } from 'msw';
 
import { DocumentScreen } from './YourPage';
 
const mockedClient = new ApolloClient({
  uri: 'https://your-graphql-endpoint',
  cache: new InMemoryCache(),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'no-cache',
      errorPolicy: 'all',
    },
    query: {
      fetchPolicy: 'no-cache',
      errorPolicy: 'all',
    },
  },
});
 
//👇The mocked data that will be used in the story
const TestData = {
  user: {
    userID: 1,
    name: 'Someone',
  },
  document: {
    id: 1,
    userID: 1,
    title: 'Something',
    brief: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
    status: 'approved',
  },
  subdocuments: [
    {
      id: 1,
      userID: 1,
      title: 'Something',
      content:
        'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
      status: 'approved',
    },
  ],
};
const meta: Meta<typeof DocumentScreen> = {
  component: DocumentScreen,
  decorators: [
    (Story) => (
      <ApolloProvider client={mockedClient}>
        <Story />
      </ApolloProvider>
    ),
  ],
};
 
export default meta;
type Story = StoryObj<typeof SampleComponent>;
 
export const MockedSuccess: Story = {
  parameters: {
    msw: {
      handlers: [
        graphql.query('AllInfoQuery', () => {
          return HttpResponse.json({
            data: {
              allInfo: {
                ...TestData,
              },
            }
          });
        }),
      ],
    },
  },
};
 
export const MockedError: Story = {
  parameters: {
    msw: {
      handlers: [
        graphql.query('AllInfoQuery', async () => {
          await delay(800);
          return HttpResponse.json({
            errors: [
              {
                message: 'Access denied',
              },
            ],
          });
        }),
      ],
    },
  },
};

为故事配置 MSW

¥Configuring MSW for stories

在上面的例子中,请注意每个故事如何配置 parameters.msw 来定义模拟服务器的请求处理程序。由于它以这种方式使用参数,因此它也可以在 component 甚至 project 级别进行配置,从而允许你在多个故事之间共享相同的模拟服务器配置。

¥In the examples above, note how each story is configured with parameters.msw to define the request handlers for the mock server. Because it uses parameters in this way, it can also be configured at the component or even project level, allowing you to share the same mock server configuration across multiple stories.