Docs
Storybook Docs

编写插件

Storybook 插件是扩展 Storybook 功能和定制开发体验的强大方法。它们可用于添加新功能、自定义 UI 或与第三方工具集成。

¥Storybook addons are a powerful way to extend Storybook's functionality and customize the development experience. They can be used to add new features, customize the UI, or integrate with third-party tools.

我们要构建什么?

¥What are we going to build?

本参考指南旨在通过基于流行的 大纲插件 构建一个简单的插件来帮助你开发 Storybook 插件如何工作的心理模型。通过本指南,你将了解插件的结构、Storybook 的 API、如何在本地测试插件以及如何发布它。

¥This reference guide is to help you develop a mental model for how Storybook addons work by building a simple addon based on the popular Outline addon. Throughout this guide, you'll learn how addons are structured, Storybook's APIs, how to test your addon locally, and how to publish it.

插件剖析

¥Addon anatomy

插件主要分为两类,每类都有其作用:

¥There are two main categories of addons, each with its role:

  • 基于 UI:这些插件负责自定义界面、启用常见任务的快捷方式或在 UI 中显示其他信息。

    ¥UI-based: These addons are responsible for customizing the interface, enabling shortcuts for common tasks, or displaying additional information in the UI.

  • 预设:这些 是预先配置的设置或配置,使开发者能够使用一组特定的特性、功能或技术快速设置和自定义他们的环境。

    ¥Presets: These are pre-configured settings or configurations that enable developers to quickly set up and customize their environment with a specific set of features, functionality, or technology.

基于 UI 插件

¥UI-based addons

本指南中内置的插件是一个基于 UI 的插件,特别是 toolbar 插件,使用户能够通过快捷方式或单击按钮在故事中的每个元素周围绘制轮廓。UI 插件可以创建其他类型的 UI 元素,每个元素都有其功能:panelstabs,为用户提供与 UI 交互的各种方式。

¥The addon built in this guide is a UI-based addon, specifically a toolbar addon, enabling users to draw outlines around each element in the story through a shortcut or click of a button. UI addons can create other types of UI elements, each with its function: panels and tabs, providing users with various ways to interact with the UI.

src/Tool.tsx
import React, { memo, useCallback, useEffect } from 'react';
 
import { useGlobals, useStorybookApi } from '@storybook/manager-api';
import { IconButton } from '@storybook/components';
import { LightningIcon } from '@storybook/icons';
 
import { ADDON_ID, PARAM_KEY, TOOL_ID } from './constants';
 
export const Tool = memo(function MyAddonSelector() {
  const [globals, updateGlobals] = useGlobals();
  const api = useStorybookApi();
 
  const isActive = [true, 'true'].includes(globals[PARAM_KEY]);
 
  const toggleMyTool = useCallback(() => {
    updateGlobals({
      [PARAM_KEY]: !isActive,
    });
  }, [isActive]);
 
  useEffect(() => {
    api.setAddonShortcut(ADDON_ID, {
      label: 'Toggle Measure [O]',
      defaultShortcut: ['O'],
      actionName: 'outline',
      showInMenu: false,
      action: toggleMyTool,
    });
  }, [toggleMyTool, api]);
 
  return (
    <IconButton key={TOOL_ID} active={isActive} title="Enable my addon" onClick={toggleMyTool}>
      <LightningIcon />
    </IconButton>
  );
});

设置

¥Setup

要创建你的第一个插件,你将使用 插件套件,这是一个现成的模板,具有所有必需的构建块、依赖和配置,可帮助你开始构建插件。在 Addon Kit 存储库中,单击使用此模板按钮根据 Addon Kit 的代码创建一个新的存储库。

¥To create your first addon, you're going to use the Addon Kit, a ready-to-use template featuring all the required building blocks, dependencies and configurations to help you get started building your addon. In the Addon Kit repository, click the Use this template button to create a new repository based on the Addon Kit's code.

克隆你刚刚创建的存储库并安装其依赖。安装过程完成后,系统会提示你配置插件的问题。回答这些问题,当你准备好开始构建插件时,运行以下命令以在开发模式下启动 Storybook 并在监视模式下开发插件:

¥Clone the repository you just created and install its dependencies. When the installation process finishes, you will be prompted with questions to configure your addon. Answer them, and when you're ready to start building your addon, run the following command to start Storybook in development mode and develop your addon in watch mode:

npm run start

Addon Kit 默认使用 Typescript。如果你想改用 JavaScript,可以运行 eject-ts 命令将项目转换为 JavaScript。

¥The Addon Kit uses Typescript by default. If you want to use JavaScript instead, you can run the eject-ts command to convert the project to JavaScript.

了解构建系统

¥Understanding the build system

Storybook 生态系统中构建的插件依赖于 tsup,这是一个由 esbuild 提供支持的快速、零配置打包器,可将你的插件代码转换为可以在浏览器中运行的现代 JavaScript。开箱即用,Addon Kit 附带一个预配置的 tsup 配置文件,你可以使用它来自定义插件的构建过程。

¥Addons built in the Storybook ecosystem rely on tsup, a fast, zero-config bundler powered by esbuild to transpile your addon's code into modern JavaScript that can run in the browser. Out of the box, the Addon Kit comes with a pre-configured tsup configuration file that you can use to customize the build process of your addon.

当构建脚本运行时,它将查找配置文件并根据提供的配置预打包插件的代码。插件可以通过各种方式与 Storybook 交互。它们可以定义预设来修改配置、向管理器 UI 添加行为或向预览 iframe 添加行为。这些不同的用例需要不同的包输出,因为它们针对不同的运行时和环境。预设在 Node 环境中执行。Storybook 的管理器和预览环境在全局范围内提供某些包,因此插件不需要将它们打包在一起或将它们作为依赖包含在其 package.json 文件中。

¥When the build scripts run, it will look for the configuration file and pre-bundle the addon's code based on the configuration provided. Addons can interact with Storybook in various ways. They can define presets to modify the configuration, add behavior to the manager UI, or add behavior to the preview iframe. These different use cases require different bundle outputs because they target different runtimes and environments. Presets are executed in a Node environment. Storybook's manager and preview environments provide certain packages in the global scope, so addons don't need to bundle them or include them as dependencies in their package.json file.

tsup 配置默认处理这些复杂性,但你可以根据他们的要求进行自定义。有关所使用的打包技术的详细说明,请参阅 插件套件的自述文件,并查看默认的 tsup 配置 此处

¥The tsup configuration handles these complexities by default, but you can customize it according to their requirements. For a detailed explanation of the bundling techniques used, please refer to the README of the addon-kit, and check out the default tsup configuration here.

注册插件

¥Register the addon

默认情况下,基于 UI 的插件的代码位于以下文件之一中,具体取决于构建的插件类型:src/Tool.tsxsrc/Panel.tsxsrc/Tab.tsx。由于我们正在构建工具栏插件,我们可以安全地删除 PanelTab 文件并将剩余文件更新为以下内容:

¥By default, code for the UI-based addons is located in one of the following files, depending on the type of addon built: src/Tool.tsx, src/Panel.tsx, or src/Tab.tsx. Since we're building a toolbar addon, we can safely remove the Panel and Tab files and update the remaining file to the following:

src/Tool.tsx
import React, { memo, useCallback, useEffect } from 'react';
 
import { useGlobals, useStorybookApi } from '@storybook/manager-api';
import { IconButton } from '@storybook/components';
import { LightningIcon } from '@storybook/icons';
 
import { ADDON_ID, PARAM_KEY, TOOL_ID } from './constants';
 
export const Tool = memo(function MyAddonSelector() {
  const [globals, updateGlobals] = useGlobals();
  const api = useStorybookApi();
 
  const isActive = [true, 'true'].includes(globals[PARAM_KEY]);
 
  const toggleMyTool = useCallback(() => {
    updateGlobals({
      [PARAM_KEY]: !isActive,
    });
  }, [isActive]);
 
  useEffect(() => {
    api.setAddonShortcut(ADDON_ID, {
      label: 'Toggle Addon [8]',
      defaultShortcut: ['8'],
      actionName: 'myaddon',
      showInMenu: false,
      action: toggleMyTool,
    });
  }, [toggleMyTool, api]);
 
  return (
    <IconButton key={TOOL_ID} active={isActive} title="Enable my addon" onClick={toggleMyTool}>
      <LightningIcon />
    </IconButton>
  );
});

按顺序浏览代码块:

¥Going through the code blocks in sequence:

// src/Tool.tsx
 
import { useGlobals, useStorybookApi } from '@storybook/manager-api';
import { IconButton } from '@storybook/components';
import { LightningIcon } from '@storybook/icons';

manager-api 包中的 useGlobalsuseStorybookApi 钩子用于访问 Storybook 的 API,允许用户与插件交互,例如启用或禁用它。

¥The useGlobals and useStorybookApi hooks from the manager-api package are used to access the Storybook's APIs, allowing users to interact with the addon, such as enabling or disabling it.

@storybook/components 包中的 IconButtonButton 组件可用于渲染工具栏中的按钮。@storybook/icons 包提供了大量大小和样式合适的图标可供选择。

¥The IconButton or Button component from the @storybook/components package can be used to render the buttons in the toolbar. The @storybook/icons package provides a large set of appropriately sized and styled icons to choose from.

// src/Tool.tsx
 
export const Tool = memo(function MyAddonSelector() {
  const [globals, updateGlobals] = useGlobals();
  const api = useStorybookApi();
 
  const isActive = [true, 'true'].includes(globals[PARAM_KEY]);
 
  const toggleMyTool = useCallback(() => {
    updateGlobals({
      [PARAM_KEY]: !isActive,
    });
  }, [isActive]);
 
  useEffect(() => {
    api.setAddonShortcut(ADDON_ID, {
      label: 'Toggle Addon [8]',
      defaultShortcut: ['8'],
      actionName: 'myaddon',
      showInMenu: false,
      action: toggleMyTool,
    });
  }, [toggleMyTool, api]);
 
  return (
    <IconButton key={TOOL_ID} active={isActive} title="Enable my addon" onClick={toggleMyTool}>
      <LightningIcon />
    </IconButton>
  );
});

Tool 组件是插件的入口点。它渲染工具栏中的 UI 元素,注册键盘快捷键,并处理启用和禁用插件的逻辑。

¥The Tool component is the entry point of the addon. It renders the UI elements in the toolbar, registers a keyboard shortcut, and handles the logic to enable and disable the addon.

转到管理器,在这里我们使用唯一的名称和标识符向 Storybook 注册插件。由于我们已经删除了 PanelTab 文件,我们需要调整文件以仅引用我们正在构建的插件。

¥Moving onto the manager, here we register the addon with Storybook using a unique name and identifier. Since we've removed the Panel and Tab files, we'll need to adjust the file to only reference the addon we're building.

src/manager.ts
import { addons, types } from '@storybook/manager-api';
import { ADDON_ID, TOOL_ID } from './constants';
import { Tool } from './Tool';
 
// Register the addon
addons.register(ADDON_ID, () => {
  // Register the tool
  addons.add(TOOL_ID, {
    type: types.TOOL,
    title: 'My addon',
    match: ({ tabId, viewMode }) => !tabId && viewMode === 'story',
    render: Tool,
  });
});

有条件地渲染插件

¥Conditionally render the addon

注意 match 属性。它允许你控制工具栏插件可见的视图模式(故事或文档)和选项卡(故事画布或 自定义选项卡)。例如:

¥Notice the match property. It allows you to control the view mode (story or docs) and tab (the story canvas or custom tabs) where the toolbar addon is visible. For example:

  • ({ tabId }) => tabId === 'my-addon/tab' 将在查看 ID 为 my-addon/tab 的选项卡时显示你的插件。

    ¥({ tabId }) => tabId === 'my-addon/tab' will show your addon when viewing the tab with the ID my-addon/tab.

  • ({ viewMode }) => viewMode === 'story' 将在画布中查看故事时显示你的插件。

    ¥({ viewMode }) => viewMode === 'story' will show your addon when viewing a story in the canvas.

  • ({ viewMode }) => viewMode === 'docs' 将在查看组件文档时显示你的插件。

    ¥({ viewMode }) => viewMode === 'docs' will show your addon when viewing the documentation for a component.

  • ({ tabId, viewMode }) => !tabId && viewMode === 'story' 将在画布中查看故事时显示你的插件,而不是在自定义选项卡中(即 tabId === undefined 时)。

    ¥({ tabId, viewMode }) => !tabId && viewMode === 'story' will show your addon when viewing a story in the canvas and not in a custom tab (i.e. when tabId === undefined).

运行 start 脚本来构建并启动 Storybook,并验证插件是否已正确注册并显示在 UI 中。

¥Run the start script to build and start Storybook and verify that the addon is registered correctly and showing in the UI.

Addon registered in the toolbar

设置插件样式

¥Style the addon

在 Storybook 中,为插件应用样式被视为副作用。因此,我们需要对我们的插件进行一些更改,以允许它在活动时使用样式,并在禁用时删除它们。我们将依靠 Storybook 的两个功能来处理这个问题:decoratorsglobals。为了处理 CSS 逻辑,我们必须包含一些辅助函数来从 DOM 中注入和删除样式表。首先创建包含以下内容的帮助文件:

¥In Storybook, applying styles for addons is considered a side-effect. Therefore, we'll need to make some changes to our addon to allow it to use the styles when it is active and remove them when it's disabled. We're going to rely on two of Storybook's features to handle this: decorators and globals. To handle the CSS logic, we must include some helper functions to inject and remove the stylesheets from the DOM. Start by creating the helper file with the following content:

src/helpers.ts
import { global } from '@storybook/global';
 
export const clearStyles = (selector: string | string[]) => {
  const selectors = Array.isArray(selector) ? selector : [selector];
  selectors.forEach(clearStyle);
};
 
const clearStyle = (input: string | string[]) => {
  const selector = typeof input === 'string' ? input : input.join('');
  const element = global.document.getElementById(selector);
  if (element && element.parentElement) {
    element.parentElement.removeChild(element);
  }
};
 
export const addOutlineStyles = (selector: string, css: string) => {
  const existingStyle = global.document.getElementById(selector);
  if (existingStyle) {
    if (existingStyle.innerHTML !== css) {
      existingStyle.innerHTML = css;
    }
  } else {
    const style = global.document.createElement('style');
    style.setAttribute('id', selector);
    style.innerHTML = css;
    global.document.head.appendChild(style);
  }
};

接下来,使用我们要注入的样式创建文件,内容如下:

¥Next, create the file with the styles we want to inject with the following content:

src/OutlineCSS.ts
import { dedent } from 'ts-dedent';
 
export default function outlineCSS(selector: string) {
  return dedent/* css */ `
    ${selector} body {
      outline: 1px solid #2980b9 !important;
    }
 
    ${selector} article {
      outline: 1px solid #3498db !important;
    }
 
    ${selector} nav {
      outline: 1px solid #0088c3 !important;
    }
 
    ${selector} aside {
      outline: 1px solid #33a0ce !important;
    }
 
    ${selector} section {
      outline: 1px solid #66b8da !important;
    }
 
    ${selector} header {
      outline: 1px solid #99cfe7 !important;
    }
 
    ${selector} footer {
      outline: 1px solid #cce7f3 !important;
    }
 
    ${selector} h1 {
      outline: 1px solid #162544 !important;
    }
 
    ${selector} h2 {
      outline: 1px solid #314e6e !important;
    }
 
    ${selector} h3 {
      outline: 1px solid #3e5e85 !important;
    }
 
    ${selector} h4 {
      outline: 1px solid #449baf !important;
    }
 
    ${selector} h5 {
      outline: 1px solid #c7d1cb !important;
    }
 
    ${selector} h6 {
      outline: 1px solid #4371d0 !important;
    }
 
    ${selector} main {
      outline: 1px solid #2f4f90 !important;
    }
 
    ${selector} address {
      outline: 1px solid #1a2c51 !important;
    }
 
    ${selector} div {
      outline: 1px solid #036cdb !important;
    }
 
    ${selector} p {
      outline: 1px solid #ac050b !important;
    }
 
    ${selector} hr {
      outline: 1px solid #ff063f !important;
    }
 
    ${selector} pre {
      outline: 1px solid #850440 !important;
    }
 
    ${selector} blockquote {
      outline: 1px solid #f1b8e7 !important;
    }
 
    ${selector} ol {
      outline: 1px solid #ff050c !important;
    }
 
    ${selector} ul {
      outline: 1px solid #d90416 !important;
    }
 
    ${selector} li {
      outline: 1px solid #d90416 !important;
    }
 
    ${selector} dl {
      outline: 1px solid #fd3427 !important;
    }
 
    ${selector} dt {
      outline: 1px solid #ff0043 !important;
    }
 
    ${selector} dd {
      outline: 1px solid #e80174 !important;
    }
 
    ${selector} figure {
      outline: 1px solid #ff00bb !important;
    }
 
    ${selector} figcaption {
      outline: 1px solid #bf0032 !important;
    }
 
    ${selector} table {
      outline: 1px solid #00cc99 !important;
    }
 
    ${selector} caption {
      outline: 1px solid #37ffc4 !important;
    }
 
    ${selector} thead {
      outline: 1px solid #98daca !important;
    }
 
    ${selector} tbody {
      outline: 1px solid #64a7a0 !important;
    }
 
    ${selector} tfoot {
      outline: 1px solid #22746b !important;
    }
 
    ${selector} tr {
      outline: 1px solid #86c0b2 !important;
    }
 
    ${selector} th {
      outline: 1px solid #a1e7d6 !important;
    }
 
    ${selector} td {
      outline: 1px solid #3f5a54 !important;
    }
 
    ${selector} col {
      outline: 1px solid #6c9a8f !important;
    }
 
    ${selector} colgroup {
      outline: 1px solid #6c9a9d !important;
    }
 
    ${selector} button {
      outline: 1px solid #da8301 !important;
    }
 
    ${selector} datalist {
      outline: 1px solid #c06000 !important;
    }
 
    ${selector} fieldset {
      outline: 1px solid #d95100 !important;
    }
 
    ${selector} form {
      outline: 1px solid #d23600 !important;
    }
 
    ${selector} input {
      outline: 1px solid #fca600 !important;
    }
 
    ${selector} keygen {
      outline: 1px solid #b31e00 !important;
    }
 
    ${selector} label {
      outline: 1px solid #ee8900 !important;
    }
 
    ${selector} legend {
      outline: 1px solid #de6d00 !important;
    }
 
    ${selector} meter {
      outline: 1px solid #e8630c !important;
    }
 
    ${selector} optgroup {
      outline: 1px solid #b33600 !important;
    }
 
    ${selector} option {
      outline: 1px solid #ff8a00 !important;
    }
 
    ${selector} output {
      outline: 1px solid #ff9619 !important;
    }
 
    ${selector} progress {
      outline: 1px solid #e57c00 !important;
    }
 
    ${selector} select {
      outline: 1px solid #e26e0f !important;
    }
 
    ${selector} textarea {
      outline: 1px solid #cc5400 !important;
    }
 
    ${selector} details {
      outline: 1px solid #33848f !important;
    }
 
    ${selector} summary {
      outline: 1px solid #60a1a6 !important;
    }
 
    ${selector} command {
      outline: 1px solid #438da1 !important;
    }
 
    ${selector} menu {
      outline: 1px solid #449da6 !important;
    }
 
    ${selector} del {
      outline: 1px solid #bf0000 !important;
    }
 
    ${selector} ins {
      outline: 1px solid #400000 !important;
    }
 
    ${selector} img {
      outline: 1px solid #22746b !important;
    }
 
    ${selector} iframe {
      outline: 1px solid #64a7a0 !important;
    }
 
    ${selector} embed {
      outline: 1px solid #98daca !important;
    }
 
    ${selector} object {
      outline: 1px solid #00cc99 !important;
    }
 
    ${selector} param {
      outline: 1px solid #37ffc4 !important;
    }
 
    ${selector} video {
      outline: 1px solid #6ee866 !important;
    }
 
    ${selector} audio {
      outline: 1px solid #027353 !important;
    }
 
    ${selector} source {
      outline: 1px solid #012426 !important;
    }
 
    ${selector} canvas {
      outline: 1px solid #a2f570 !important;
    }
 
    ${selector} track {
      outline: 1px solid #59a600 !important;
    }
 
    ${selector} map {
      outline: 1px solid #7be500 !important;
    }
 
    ${selector} area {
      outline: 1px solid #305900 !important;
    }
 
    ${selector} a {
      outline: 1px solid #ff62ab !important;
    }
 
    ${selector} em {
      outline: 1px solid #800b41 !important;
    }
 
    ${selector} strong {
      outline: 1px solid #ff1583 !important;
    }
 
    ${selector} i {
      outline: 1px solid #803156 !important;
    }
 
    ${selector} b {
      outline: 1px solid #cc1169 !important;
    }
 
    ${selector} u {
      outline: 1px solid #ff0430 !important;
    }
 
    ${selector} s {
      outline: 1px solid #f805e3 !important;
    }
 
    ${selector} small {
      outline: 1px solid #d107b2 !important;
    }
 
    ${selector} abbr {
      outline: 1px solid #4a0263 !important;
    }
 
    ${selector} q {
      outline: 1px solid #240018 !important;
    }
 
    ${selector} cite {
      outline: 1px solid #64003c !important;
    }
 
    ${selector} dfn {
      outline: 1px solid #b4005a !important;
    }
 
    ${selector} sub {
      outline: 1px solid #dba0c8 !important;
    }
 
    ${selector} sup {
      outline: 1px solid #cc0256 !important;
    }
 
    ${selector} time {
      outline: 1px solid #d6606d !important;
    }
 
    ${selector} code {
      outline: 1px solid #e04251 !important;
    }
 
    ${selector} kbd {
      outline: 1px solid #5e001f !important;
    }
 
    ${selector} samp {
      outline: 1px solid #9c0033 !important;
    }
 
    ${selector} var {
      outline: 1px solid #d90047 !important;
    }
 
    ${selector} mark {
      outline: 1px solid #ff0053 !important;
    }
 
    ${selector} bdi {
      outline: 1px solid #bf3668 !important;
    }
 
    ${selector} bdo {
      outline: 1px solid #6f1400 !important;
    }
 
    ${selector} ruby {
      outline: 1px solid #ff7b93 !important;
    }
 
    ${selector} rt {
      outline: 1px solid #ff2f54 !important;
    }
 
    ${selector} rp {
      outline: 1px solid #803e49 !important;
    }
 
    ${selector} span {
      outline: 1px solid #cc2643 !important;
    }
 
    ${selector} br {
      outline: 1px solid #db687d !important;
    }
 
    ${selector} wbr {
      outline: 1px solid #db175b !important;
    }`;
}

由于插件可以在故事和文档模式下处于活动状态,因此 Storybook 预览 iframe 的 DOM 节点在这两种模式下是不同的。事实上,Storybook 在文档模式下会在一页上渲染多个故事预览。因此,我们需要为将注入样式的 DOM 节点选择正确的选择器,并确保 CSS 的范围在该特定选择器内。该机制作为 src/withGlobals.ts 文件中的示例提供,我们将使用它将样式和辅助函数连接到插件逻辑。将文件更新为以下内容:

¥Since the addon can be active in both the story and documentation modes, the DOM node for Storybook's preview iframe is different in these two modes. In fact, Storybook renders multiple story previews on one page when in documentation mode. Therefore, we'll need to choose the correct selector for the DOM node where the styles will be injected and ensure the CSS is scoped to that particular selector. That mechanism is provided as an example within the src/withGlobals.ts file, which we'll use to connect the styling and helper functions to the addon logic. Update the file to the following:

src/withGlobals.ts
import type { Renderer, PartialStoryFn as StoryFunction, StoryContext } from '@storybook/types';
 
import { useEffect, useMemo, useGlobals } from '@storybook/preview-api';
import { PARAM_KEY } from './constants';
 
import { clearStyles, addOutlineStyles } from './helpers';
 
import outlineCSS from './outlineCSS';
 
export const withGlobals = (StoryFn: StoryFunction<Renderer>, context: StoryContext<Renderer>) => {
  const [globals] = useGlobals();
 
  const isActive = [true, 'true'].includes(globals[PARAM_KEY]);
 
  // Is the addon being used in the docs panel
  const isInDocs = context.viewMode === 'docs';
 
  const outlineStyles = useMemo(() => {
    const selector = isInDocs ? `#anchor--${context.id} .docs-story` : '.sb-show-main';
 
    return outlineCSS(selector);
  }, [context.id]);
  useEffect(() => {
    const selectorId = isInDocs ? `my-addon-docs-${context.id}` : `my-addon`;
 
    if (!isActive) {
      clearStyles(selectorId);
      return;
    }
 
    addOutlineStyles(selectorId, outlineStyles);
 
    return () => {
      clearStyles(selectorId);
    };
  }, [isActive, outlineStyles, context.id]);
 
  return StoryFn();
};

打包和发布

¥Packaging and publishing

Storybook 插件与 JavaScript 生态系统中的大多数软件包类似,以 NPM 软件包的形式分发。但是,它们有特定的标准需要满足才能发布到 NPM 并被集成目录抓取:

¥Storybook addons, similar to most packages in the JavaScript ecosystem, are distributed as NPM packages. However, they have specific criteria that need to be met to be published to NPM and crawled by the integration catalog:

  1. 有一个包含转译代码的 dist 文件夹。

    ¥Have a dist folder with the transpiled code.

  2. 一个 package.json 文件声明:

    ¥A package.json file declaring:

    • 模块相关信息

      ¥Module-related information

    • 集成目录元数据

      ¥Integration catalog metadata

模块元数据

¥Module Metadata

第一类元数据与插件本身有关。这包括模块的条目,即发布插件时要包含哪些文件。将插件与 Storybook 集成所需的配置,允许其消费者使用它。

¥The first category of metadata is related to the addon itself. This includes the entry for the module, which files to include when the addon is published. And the required configuration to integrate the addon with Storybook, allowing it to be used by its consumers.

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "node": "./dist/index.js",
      "require": "./dist/index.js",
      "import": "./dist/index.mjs"
    },
    "./manager": "./dist/manager.mjs",
    "./preview": "./dist/preview.mjs",
    "./package.json": "./package.json"
  },
  "main": "dist/index.js",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "files": ["dist/**/*", "README.md", "*.js", "*.d.ts"],
  "devDependencies": {
    "@storybook/blocks": "^7.0.0",
    "@storybook/components": "^7.0.0",
    "@storybook/core-events": "^7.0.0",
    "@storybook/manager-api": "^7.0.0",
    "@storybook/preview-api": "^7.0.0",
    "@storybook/theming": "^7.0.0",
    "@storybook/types": "^7.0.0"
  },
  "bundler": {
    "exportEntries": ["src/index.ts"],
    "managerEntries": ["src/manager.ts"],
    "previewEntries": ["src/preview.ts"]
  }
}

集成目录元数据

¥Integration Catalog Metadata

第二个元数据类别与 集成目录 相关。这些信息中的大部分已由 Addon Kit 预先配置。但是,显示名称、图标和框架等项目必须通过 storybook 属性进行配置才能显示在目录中。

¥The second metadata category is related to the integration catalog. Most of this information is already pre-configured by the Addon Kit. However, items like the display name, icon, and frameworks must be configured via the storybook property to be displayed in the catalog.

{
  "name": "my-storybook-addon",
  "version": "1.0.0",
  "description": "My first storybook addon",
  "author": "Your Name",
  "storybook": {
    "displayName": "My Storybook Addon",
    "unsupportedFrameworks": ["react-native"],
    "icon": "https://yoursite.com/link-to-your-icon.png"
  },
  "keywords": ["storybook-addons", "appearance", "style", "css", "layout", "debug"]
}

storybook 配置元素包含其他属性,可帮助自定义插件的可搜索性和索引。有关更多信息,请参阅 集成目录文档

¥The storybook configuration element includes additional properties that help customize the addon's searchability and indexing. For more information, see the Integration catalog documentation.

需要注意的一项重要内容是 keywords 属性,因为它映射到目录的标签系统。添加 storybook-addons 可确保在搜索插件时可以在目录中发现插件。其余关键字有助于插件的可搜索性和分类。

¥One essential item to note is the keywords property as it maps to the catalog's tag system. Adding the storybook-addons ensures that the addon is discoverable in the catalog when searching for addons. The remaining keywords help with the searchability and categorization of the addon.

发布到 NPM

¥Publishing to NPM

准备好将插件发布到 NPM 后,Addon Kit 预先配置了 Auto 包以进行发布管理。它会生成一个变更日志并自动将包上传到 NPM 和 GitHub。因此,你需要配置对两者的访问权限。

¥Once you're ready to publish your addon to NPM, the Addon Kit comes pre-configured with the Auto package for release management. It generates a changelog and uploads the package to NPM and GitHub automatically. Therefore, you need to configure access to both.

  1. 使用 npm adduser 进行身份验证

    ¥Authenticate using npm adduser

  2. 生成具有 readpublish 权限的 访问令牌

    ¥Generate a access token with both read and publish permissions.

  3. 创建具有 repoworkflow 范围权限的 个人访问令牌

    ¥Create a personal access token with repo and workflow scoped permissions.

  4. 在项目的根目录中创建一个 .env 文件并添加以下内容:

    ¥Create a .env file in the root of your project and add the following:

GH_TOKEN=value_you_just_got_from_github
NPM_TOKEN=value_you_just_got_from_npm

接下来,运行以下命令在 GitHub 上创建标签。你将使用这些标签对软件包的更改进行分类。

¥Next, run the following command to create labels on GitHub. You'll use these labels to categorize changes to the package.

npx auto create-labels

最后,运行以下命令为你的插件创建一个版本。这将构建和打包插件代码,提升版本,将版本推送到 GitHub 和 npm,并生成更改日志。

¥Finally, run the following command to create a release for your addon. This will build and package the addon code, bump the version, push the release into GitHub and npm, and generate a changelog.

npm run release

CI 自动化

¥CI automation

默认情况下,Addon Kit 预先配置了 GitHub Actions 工作流,使你能够自动化发布管理流程。这可确保包始终保持最新更改,并且更改日志会相应更新。但是,你需要进行额外的配置才能使用 NPM 和 GitHub 令牌成功发布包。在你的存储库中,单击“设置”选项卡,然后单击“机密和变量”下拉菜单,最后单击“操作”项。你应该看到以下屏幕:

¥By default, the Addon Kit comes pre-configured with a GitHub Actions workflow, enabling you to automate the release management process. This ensures that the package is always up to date with the latest changes and that the changelog is updated accordingly. However, you'll need additional configuration to use your NPM and GitHub tokens to publish the package successfully. In your repository, click the Settings tab, then the Secrets and variables dropdown, followed by the Actions item. You should see the following screen:

GitHub secrets page

然后,单击新存储库机密,将其命名为 NPM_TOKEN,然后粘贴你之前生成的令牌。每当你将拉取请求合并到默认分支时,工作流都会运行并发布新版本,自动增加版本号并更新更改日志。

¥Then, click the New repository secret, name it NPM_TOKEN, and paste the token you generated earlier. Whenever you merge a pull request to the default branch, the workflow will run and publish a new release, automatically incrementing the version number and updating the changelog.

了解有关 Storybook 插件生态系统的更多信息

¥Learn more about the Storybook addon ecosystem