尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Storybook 插件开发指南:用 Preset 的 webpackFinal 钩子深度定制构建配置

Storybook 插件开发指南:用 Preset 的 webpackFinal 钩子深度定制构建配置 Storybook 插件开发指南用 Preset 的 webpackFinal 钩子深度定制构建配置本文基于当前仓库中的插件编写参考文档与源码系统讲解 Storybook 插件addon通过 Preset 提供的webpackFinalAPI 扩展默认 Webpack 构建配置的原理、签名、执行时机与完整代码示例。读完你将掌握在本地插件 preset 中给 Storybook 的 Webpack5 构建器追加 loader/plugin 的写法理解它与.storybook/main.js|ts中端侧webpackFinal配置的关系并能结合构建器源码排查配置不生效、rules 被覆盖等典型问题。webpackFinal 在 Preset 体系中的定位在 Storybook 中Preset 是一组预置配置它让插件作者通过一组标准 API 把参数、插件、全局配置组合进 Storybook从而扩展其行为。官方插件编写指南 docs/addons/writing-presets.mdx 把插件 preset 划分为两类本地 PresetLocal presets负责封装并组织插件自身的配置包括构建器builder支持、Babel 或第三方集成。根级 PresetRoot-level presets面向最终用户负责把插件注册进 Storybook例如通过previewAnnotations注入 decorator/parameters通过managerEntries注册 UI 侧功能。按 docs/addons/writing-presets.mdx 中 Presets API 一节的归纳编写 preset 可用的 API 覆盖babelDefaultBabel 配置、viteFinal/webpackFinal两大构建器的配置扩展、managerEntries、previewAnnotations以及 UI 相关的previewHead、previewBody、managerHead、previewMainTemplate等。本文讨论的webpackFinal正是Builders类别中面向 Webpack 构建器的那个钩子对应参考片段为 docs/_snippets/storybook-addons-preset-webpackFinal.md。官方文档对它的用途给出了非常精确的定义To customize the Webpack configuration in Storybook to add support for additional file types, apply specific loaders, configure plugins, or make any other necessary modifications, you can use thewebpackFinalAPI. Once invoked, it will extend the default Webpack configuration with the provided configuration.即为 Storybook 增加对额外文件类型的支持、挂载特定 loader、配置插件或做其它必要改动时使用webpackFinal钩子它会在默认 Webpack 配置之上叠加你的扩展。函数签名与执行时机结合 docs/_snippets/storybook-addons-preset-webpackFinal.md 与 docs/api/main-config/main-config-webpack-final.mdx钩子的标准形态是async (config: Configuration, options: Options) Configuration两个参数分别代表config当前累计的 WebpackConfiguration来自 webpack 的类型定义。它是已经经过 Storybook 默认配置处理的半成品你可以在其上增删 rules、plugins、resolve 等最后必须原样返回可携带你的修改。optionsStorybook 运行时选项。类型定义中明确可用的字段为{ configType?: DEVELOPMENT | PRODUCTION }用于区分开发态与生产构建除此之外还包含大量难以逐一枚举的字段官方建议直接查看类型定义。关于执行时机code/builders/builder-webpack5/src/presets/custom-webpack-preset.ts 的webpack()函数L79-L102揭示了完整调用链export async function webpack(config: Configuration, options: Options) { const { configDir, configType, presets } options; const coreOptions await presets.apply(core); let defaultConfig config; if (!coreOptions?.disableWebpackDefaults) { defaultConfig await createDefaultWebpackConfig(config, options); } const finalDefaultConfig await presets.apply(webpackFinal, defaultConfig, options); // ... }关键一步在第 89 行presets.apply(webpackFinal, defaultConfig, options)。presets.apply会把 preset 链上所有注册方导出的webpackFinal依次执行——前一个的返回值会作为后一个的入参最终产物才是构建真正使用的配置。因此先由createDefaultWebpackConfig或直接使用传入 config生成 Storybook 默认 Webpack 配置再串行应用所有 preset含各插件 preset 与根 preset导出的webpackFinal得到最终配置后再交给 webpack 实例构建。对端侧用户而言code/builders/builder-webpack5/src/types.tsL19-L33中StorybookConfigWebpack类型对两个近似字段的注释区分了顺序语义webpack是Storybook 默认配置运行之后即可修改主要由插件使用而webpackFinal则是所有插件都运行完毕后进行最终修改。这正是插件作者与最终用户在修改时机的关键差异点。代码拆解追加自定义 loader 的最小示例关联文档 给出了 JS 与 TS 两版完整示例展示最常见的用法——为新的文件扩展名绑定自定义 loader。JS 版本import { fileURLToPath } from node:url; export function webpackFinal(config, options {}) { const rules [ ...(config.module?.rules || []), { test: /\.custom-file-extension$/, loader: fileURLToPath(import.meta.resolve(custom-loader)), }, ]; config.module.rules rules; return config; }TS 版本带 Webpack 官方类型标注import { fileURLToPath } from node:url; import type { Configuration as WebpackConfig } from webpack; export function webpackFinal(config: WebpackConfig, options: any {}) { const rules [ ...(config.module?.rules || []), { test: /\.custom-file$/, loader: fileURLToPath(import.meta.resolve(custom-loader)), }, ]; config.module.rules rules; return config; }提示node:url中实际的导出名为驼峰写法的fileURLToPath使用时注意大小写。这段代码的每一处都值得拆开讲解...(config.module?.rules || [])先把已有 rules 展开保留再追加新 rule。这是最重要的习惯——如果直接用新数组整体覆盖config.module.rules而不保留旧值会丢掉 Storybook 自身处理 JS/TS、CSS、静态资源所需的全部 loader直接导致构建崩溃。可选链?.与|| []则是对配置里可能不存在的module.rules做防御。test: /\.custom-file-extension$//test: /\.custom-file$/匹配需要交给自定义 loader 处理的文件后缀。正则中的.需要转义\.$表示锚定到结尾。fileURLToPath(import.meta.resolve(custom-loader))import.meta.resolve把模块名解析成file://形式的 URLfileURLToPath再把它转成 loader 所需的绝对路径。这种写法保证了即便custom-loader是安装在 node_modules 中的第三方包也能被精确解析到真实磁盘位置避免了相对路径解析问题。它要求运行环境支持import.meta.resolve即 Node.js 的 ESM 环境较新版本 Node 无需 flag 即可使用。config.module.rules rules; return config;先做原地修改再把整个 config 返回。返回值会被presets.apply传给链上的下一个 preset不返回返回undefined会让下游直接拿到空配置这是最常见的错误之一。仓库自身的 builder-webpack5L47-L77就是一个活生生的用webpackFinal扩展构建的例子它向config.plugins追加WebpackMockPlugin、WebpackInjectMockerRuntimePlugin并向config.module.rules推入一个专门处理preview.(t|j)sx?文件的 mock 转换 loader从而支撑sb.mock()模块级 mock 功能。这印证了钩子文档里追加 loader、配置插件、修改 module rules的三大典型用途。在插件内接线把 webpackFinal 导出为本地 preset单独写出webpackFinal函数还不够——它需要被插件 preset 入口文件导出Storybook 才能识别。官方插件目录约定中这个函数放在插件的本地 preset 中例如example-addon/src/webpack/webpackFinal.js|ts随后在 preset 入口汇总导出。参考 docs/_snippets/storybook-addons-local-preset.mdimport { webpackFinal as webpack } from ./webpack/webpackFinal; import { viteFinal as vite } from ./vite/viteFinal; import { babelDefault as babel } from ./babel/babelDefault; export const webpackFinal webpack; export const viteFinal vite; export const babelDefault babel;要点钩子函数与入口文件分离存放如src/webpack/webpackFinal.*、src/vite/viteFinal.*职责清晰、便于维护也与 docs/_snippets/storybook-addons-preset-viteFinal.md 展示的viteFinal保持对仗结构。建议同时导出viteFinal等姊妹钩子让插件无论用户使用 Webpack 还是 Vite 构建器都能正常工作否则应明确告知仅支持 Webpack。根级 preset 面向用户注册见 docs/addons/writing-presets.mdx 中的previewAnnotations、managerEntries用法而webpackFinal属于本地 preset 的职责范畴它不直接暴露给用户配置而是在插件被加入addons数组后自动生效。实际在仓库中的映射是所有插件通过.storybook/main.js|ts的addons字段注册后其 preset 导出的webpackFinal都会进入 custom-webpack-preset.ts 第 89 行presets.apply(webpackFinal, defaultConfig, options)的串行链。与 .storybook/main.js|ts 中 webpackFinal 的关系webpackFinal既可以是插件 preset 的导出项也可以作为端侧main.js|ts的顶层配置键出现最终用户模式。docs/api/main-config/main-config-webpack-final.mdx 明确说明它是main.js|ts配置的合法字段类型async (config: Config, options: WebpackOptions) Config适用条件仅在项目使用 webpack 构建器时生效对应 code/builders/builder-webpack5框架示例如react-webpack5、nextjs等。官方还指出.storybook/main.js|ts本身就是一个私有 preset见 docs/addons/writing-presets.mdx 的 Advanced configuration 一节——这解释了两者为何共享同一套机制端侧写在 main 配置里的webpackFinal本质上是根 preset 上的同一个钩子属性与插件 preset 的webpackFinal走同一条presets.apply管线。main 配置更完整的参考示例可见配套片段 docs/_snippets/main-config-webpack-final.md。插件作者与端侧用户的差异主要在时机语义按 types.ts 的类型注释端侧webpack别名语义发生在默认配置之后、偏向插件内部阶段而端侧webpackFinal设计为所有插件之后的最终修改点。因此在写插件时应避免在webpackFinal中做覆盖性的激进改动如清空 rules因为你永远不知道后面是否还有别的 preset 或用户配置需要读取这些信息保持增量追加 返回完整 config是最稳妥的契约。常见误区与排障结合 docs/addons/writing-presets.mdx 的 Troubleshooting 一节与源码列出高频踩坑点必须返回 config钩子类型要求(config) config。遗漏return config会导致后续 preset 与最终构建拿到undefined报错往往晦涩难懂。不要整体覆盖已有 rules务必...(config.module?.rules || [])展开旧值后再追加否则 Storybook 自身 loader 全部丢失。只在 Webpack 构建器下生效使用 Vite 的项目走viteFinal参考 docs/_snippets/storybook-addons-preset-viteFinal.md若项目基于非 Babel/SWC 的其它编译器babelDefault类配置也不会生效需按构建器分别适配。Manager 端不再支持managerWebpackStorybook 已改用 esbuild 构建 UImanager部分依赖managerWebpackAPI 加载非 CSS/图片文件的旧插件会失效。官方建议移除该 API并把额外文件预先转换为 JavaScript。相关预设仍在使用此 API 的应及时迁移。正则与 loader 路径文件扩展名正则需正确转义/\.custom-file$/loader 通过fileURLToPath(import.meta.resolve(custom-loader))解析为绝对路径避免相对解析误差。ESM 前提示例使用import.meta.resolve要求 preset 以 ESM/支持它的环境运行否则可退化为基于require.resolve/显式绝对路径的方案。扩展阅读插件 preset 完整指南docs/addons/writing-presets.mdx插件本地 preset 接线示例docs/_snippets/storybook-addons-local-preset.mdVite 构建器对应钩子docs/_snippets/storybook-addons-preset-viteFinal.md端侧 main 配置中的 webpackFinaldocs/api/main-config/main-config-webpack-final.mdx 与其示例 docs/_snippets/main-config-webpack-final.md构建器内webpackFinal的执行链源码code/builders/builder-webpack5/src/presets/custom-webpack-preset.ts端侧配置类型定义code/builders/builder-webpack5/src/types.ts创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表