storybook-addon-fetch-mock
这个 Storybook.js 插件使用 fetch-mock
库添加了 fetch()
模拟功能。
为什么要使用 storybook-addon-fetch-mock?
如果你已经在使用 Storybook.js,你可能有一些组件会调用 API 接口。为了确保你的组件 Storybook 文档不依赖于这些 API 接口的可用性,你会希望模拟任何对这些 API 接口的调用。如果你的任何组件会修改接口上的数据,这一点尤其重要。
幸运的是,Storybook 生态系统有许多插件可以让你更容易地模拟 Fetch API。这些插件中的任何一个都可以让你拦截组件中的真实 API 调用,并返回你想要的任何模拟数据响应。
Storybook 插件 | 完整的 Fetch 模拟 | 模拟函数 | 模拟对象 |
---|---|---|---|
Mock Service Worker 插件 | ✅ | ✅ 2 | ❌ |
Mock API Request 插件 | ❌ 1 | ✅ | ✅ |
storybook-addon-fetch-mock | ✅ | ✅ | ✅ 3 |
1 如果你使用 XMLHttpRequest (XHR),Mock API Request 插件会很好地为你服务,但我们不推荐它用于 Fetch API 模拟。它的功能非常基础,有些 Fetch API 请求无法通过此插件模拟。
2 如果你想模拟一个你正在编写的 Fetch API,使用 Mock Service Worker 插件编写模拟解析器函数可能是最简单的方法。
3 如果你想模拟一个你 不是 正在编写的 Fetch API,编写简单的 JavaScript 对象可能是最简单的模拟方法。这个项目,storybook-addon-fetch-mock,是对 fetch-mock
库的轻量级封装。fetch-mock
是一个维护良好、高度可配置的模拟库,自 2015 年以来一直可用。它允许你将模拟数据编写为简单的 JavaScript 对象、解析器函数,或两者的组合。
一个快速示例
想象一个 UnicornSearch
组件使用 fetch()
调用一个接口来搜索独角兽列表。你可以使用 storybook-addon-fetch-mock
绕过实际的 API 接口并返回一个模拟的响应。在按照下面的“安装”说明操作后,你可以像这样配置 UnicornSearch.stories.js
import UnicornSearch from './UnicornSearch';
export default {
title: 'Unicorn Search',
component: UnicornSearch,
};
// We define the story here using CSF 3.0.
export const ShowMeTheUnicorns = {
args: {
search: '',
},
parameters: {
fetchMock: {
// "fetchMock.mocks" is a list of mocked
// API endpoints.
mocks: [
{
// The "matcher" determines if this
// mock should respond to the current
// call to fetch().
matcher: {
name: 'searchSuccess',
url: 'path:/unicorn/list',
query: {
search: 'Charlie',
},
},
// If the "matcher" matches the current
// fetch() call, the fetch response is
// built using this "response".
response: {
status: 200,
body: {
count: 1,
unicorns: [
{
name: 'Charlie',
location: 'magical Candy Mountain',
},
],
},
},
},
{
matcher: {
name: 'searchFail',
url: 'path:/unicorn/list',
},
response: {
status: 200,
body: {
count: 0,
unicorns: [],
},
},
},
],
},
},
};
如果在 Storybook 中打开“Show Me The Unicorns”的故事,我们可以在“search”字段中填写“Charlie”,并且假设 UnicornSearch
调用 fetch()
到 https://example.com/unicorn/list?search=charlie
,我们的 Storybook 插件将比较 parameters.fetchMock.mocks
中的每个模拟直到找到匹配项,并返回第一个匹配模拟的响应。
如果我们在“search”字段中填写不同的值,我们的 Storybook 插件将返回第二个模拟的响应。
安装
-
作为开发依赖安装插件
npm i -D storybook-addon-fetch-mock
-
通过将插件名称添加到
.storybook/main.js
中的addons
数组来注册 Storybook 插件module.exports = { addons: ['storybook-addon-fetch-mock'], };
-
可选地,通过向
.storybook/preview.js
中的parameters
对象添加fetchMock
条目来配置插件。详情请参见下面的“为所有故事配置全局参数”部分。 -
将模拟数据添加到你的故事中。详情请参见下面的“配置模拟数据”部分。
配置模拟数据
要拦截对你的 API 接口的 fetch
调用,添加一个 parameters.fetchMock.mocks
数组,其中包含一个或多个接口模拟。
参数放在哪里?
如果你将 parameters.fetchMock.mocks
数组放在单个故事的导出中,模拟将只应用于该故事
export const MyStory = {
parameters: {
fetchMock: {
mocks: [
// ...mocks go here
],
},
},
};
如果你将 parameters.fetchMock.mocks
数组放在 Storybook 文件的 default
导出中,模拟将应用于该文件中的所有故事。但如果需要,你仍然可以按故事覆盖模拟。
export default {
title: 'Components/Unicorn Search',
component: UnicornSearch,
parameters: {
fetchMock: {
mocks: [
// ...mocks go here
],
},
},
};
你也可以将 parameters.fetchMock.mocks
数组放在 Storybook 的 preview.js
配置文件中,但不推荐这样做。有关更好的替代方案,请参阅下面的“为所有故事配置全局参数”部分。
parameters.fetchMock.mocks
数组
当进行 fetch()
调用时,会将 parameters.fetchMock.mocks
数组中的每个模拟与 fetch()
请求进行比较,直到找到匹配项。
每个模拟应该是一个对象,包含以下可能的键
matcher
(必需):每个模拟的matcher
对象包含一个或多个用于匹配的条件。如果matcher
中包含多个条件,则所有条件都必须匹配才能使用该模拟。response
(可选):一旦找到匹配项,将使用匹配模拟的response
来配置fetch()
的响应。- 如果模拟没有指定
response
,fetch()
响应将使用 HTTP 200 状态,且没有主体数据。 - 如果
response
是一个对象,则使用这些值来创建fetch()
响应。 - 如果
response
是一个函数,该函数应返回一个对象,其值用于创建fetch()
响应。
- 如果模拟没有指定
options
(可选):配置模拟行为的更多选项。
以下是 matcher
、response
和 options
可能包含的全部键列表
const exampleMock = {
// Criteria for deciding which requests should match this
// mock. If multiple criteria are included, all of the
// criteria must match in order for the mock to be used.
matcher: {
// Match only requests where the endpoint "url" is matched
// using any one of these formats:
// - "url" - Match an exact url.
// e.g. "http://www.site.com/page.html"
// - "*" - Match any url
// - "begin:..." - Match a url beginning with a string,
// e.g. "begin:http://www.site.com"
// - "end:..." - Match a url ending with a string
// e.g. "end:.jpg"
// - "path:..." - Match a url which has a given path
// e.g. "path:/posts/2018/7/3"
// - "glob:..." - Match a url using a glob pattern
// e.g. "glob:http://*.*"
// - "express:..." - Match a url that satisfies an express
// style path. e.g. "express:/user/:user"
// - RegExp - Match a url that satisfies a regular
// expression. e.g. /(article|post)\/\d+/
url: 'https://example.com/endpoint/search',
// If you have multiple mocks that use the same "url",
// a unique "name" is required.
name: 'searchSuccess',
// Match only requests using this HTTP method. Not
// case-sensitive.
method: 'POST',
// Match only requests that have these headers set.
headers: {
Authorization: 'Basic 123',
},
// Match only requests that send a JSON body with the
// exact structure and properties as the one provided.
// See matcher.matchPartialBody below to override this.
body: {
unicornName: 'Charlie',
},
// Match calls that only partially match the specified
// matcher.body JSON.
matchPartialBody: true,
// Match only requests that have these query parameters
// set (in any order).
query: {
q: 'cute+kittenz',
},
// When the express: keyword is used in the "url"
// matcher, match only requests with these express
// parameters.
params: {
user: 'charlie',
},
// Match if the function returns something truthy. The
// function will be passed the url and options fetch was
// called with. If fetch was called with a Request
// instance, it will be passed url and options inferred
// from the Request instance, with the original Request
// will be passed as a third argument.
functionMatcher: (url, options, request) => {
return !!options.headers.Authorization;
},
// Limits the number of times the mock can be matched.
// If the mock has already been used "repeat" times,
// the call to fetch() will fall through to be handled
// by any other mocks.
repeat: 1,
},
// Configures the HTTP response returned by the mock.
response: {
// The mock response’s "statusText" is automatically set
// based on this "status" number. Defaults to 200.
status: 200,
// By default, the optional "body" object will be converted
// into a JSON string. See options.sendAsJson to override.
body: {
unicorns: true,
},
// Set the mock response’s headers.
headers: {
'Content-Type': 'text/html',
},
// The url from which the mocked response should claim
// to originate from (to imitate followed directs).
// Will also set `redirected: true` on the response.
redirectUrl: 'https://example.com/search',
// Force fetch to return a Promise rejected with the
// value of "throws".
throws: new TypeError('Failed to fetch'),
},
// Alternatively, the `response` can be a function that
// returns an object with any of the keys above. The
// function will be passed the url and options fetch was
// called with. If fetch was called with a Request
// instance, it will be passed url and options inferred
// from the Request instance, with the original Request
// will be passed as a third argument.
response: (url, options, request) => {
return {
status: options.headers.Authorization ? 200 : 403,
};
},
// An object containing further options for configuring
// mocking behaviour.
options: {
// If set, the mocked response is delayed for the
// specified number of milliseconds.
delay: 500,
// By default, the "body" object is converted to a JSON
// string and the "Content-Type: application/json"
// header will be set on the mock response. If this
// option is set to false, the "body" object can be any
// of the other types that fetch() supports, e.g. Blob,
// ArrayBuffer, TypedArray, DataView, FormData,
// URLSearchParams, string or ReadableStream.
sendAsJson: false,
// By default, a Content-Length header is set on each
// mock response. This can be disabled when this option
// is set to false.
includeContentLength: false,
},
};
为所有故事配置全局参数
以下选项旨在用于 Storybook 的 preview.js
配置文件。
// .storybook/preview.js
export const parameters = {
fetchMock: {
// When the story is reloaded (or you navigate to a new
// story, this addon will be reset and a list of
// previous mock matches will be sent to the browser’s
// console if "debug" is true.
debug: true,
// Do any additional configuration of fetch-mock, e.g.
// setting fetchMock.config or calling other fetch-mock
// API methods. This function is given the fetchMock
// instance as its only parameter and is called after
// mocks are added but before catchAllMocks are added.
useFetchMock: (fetchMock) => {
fetchMock.config.overwriteRoutes = false;
},
// After each story’s `mocks` are added, these catch-all
// mocks are added.
catchAllMocks: [
{ matcher: { url: 'path:/endpoint1' }, response: 200 },
{ matcher: { url: 'path:/endpoint2' }, response: 200 },
],
// A simple list of URLs to ensure that calls to
// `fetch( [url] )` don’t go to the network. The mocked
// fetch response will use HTTP status 404 to make it
// easy to determine one of the catchAllURLs was matched.
// These mocks are added after any catchAllMocks.
catchAllURLs: [
// This is equivalent to the mock object:
// {
// matcher: { url: 'begin:http://example.com/' },
// response: { status: 404 },
// }
'http://example.com/',
],
},
};