文档
Storybook 文档

模拟网络请求

对于发出网络请求的组件(例如,从 REST 或 GraphQL API 获取数据),您可以使用像 Mock Service Worker (MSW) 这样的工具来模拟这些请求。MSW 是一个 API 模拟库,它依赖于 Service Worker 来捕获网络请求,并提供模拟数据作为响应。

MSW 插件 将此功能引入 Storybook,允许您在故事中模拟 API 请求。以下是有关如何设置和使用该插件的概述。

设置 MSW 插件

首先,如果需要,运行以下命令来安装 MSW 和 MSW 插件

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

如果您尚未使用 MSW,请生成 MSW 工作所需的 Service Worker 文件

npx msw init public/

然后确保 Storybook 配置中的 staticDirs 属性包含生成的 Service Worker 文件(默认情况下位于 /public 中)

.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;

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

.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 请求

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

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 中的网络请求。

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

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 请求

GraphQL 是另一种在组件中获取数据的常用方式。您可以使用 MSW 在 Storybook 中模拟 GraphQL 请求。以下是如何模拟从 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>
  );
}

此示例使用 Apollo Client 与 GraphQL 发出网络请求。如果您使用的是其他库(例如 URQLReact Query),则可以应用相同的原理来模拟 Storybook 中的网络请求。

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

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

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