ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Storybook Links Addon 深度解析:用 linkTo、hrefTo 与 withLinks 构建 Story 间导航

Storybook Links Addon 深度解析:用 linkTo、hrefTo 与 withLinks 构建 Story 间导航 Storybook Links Addon 深度解析用 linkTo、hrefTo 与 withLinks 构建 Story 间导航【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybookStorybook 的storybook/addon-links解决的是一个很具体但高频的需求让一个 story 中的组件行为点击、选择等事件能够直接跳转到另一个 story从而把若干 UI 组件串成可点击的演示或原型流程。本文基于 Storybook 仓库中该 Addon 的 READMEcode/addons/links/README.md及其源码完整覆盖安装注册、linkTo、hrefTo、withLinks装饰器与 ReactLinkTo组件的全部用法并逐层拆解这些 API 在源码中的实现机制与真实模板示例帮助你在项目里正确、可控地构建 Story 间导航。定位与包结构从包的元数据 code/addons/links/package.json 看storybook/addon-links的自我定位是“Link stories together to build demos and prototypes with your UI components”——将 Story 串联起来构建演示与原型。几个值得注意的包级事实入口导出exports字段分为四部分.主入口导出linkTo/hrefTo/withLinks/navigate四个 API、./preview预览端注解、./manager管理端注册、./reactReact 专用LinkTo组件。react与types/react是可选的 peer 依赖React 16.8 19说明 Links 核心能力与框架无关只有LinkTo组件才需要 React。包的storybook.unsupportedFrameworks声明了react-native不被支持。主入口 code/addons/links/src/index.ts 只有寥寥几行但它揭示了该 Addon 的注册形态import { definePreviewAddon } from storybook/internal/csf; import * as addonAnnotations from ./preview.ts; export { linkTo, hrefTo, withLinks, navigate } from ./utils.ts; export default () definePreviewAddon(addonAnnotations);即默认导出通过definePreviewAddon把 preview.ts 中的“addon 注解”注册到预览端而四个 API 全部来自同一个 utils.ts。安装与注册按照 README安装只需添加依赖并在main.js或main.ts中注册yarn add -D storybook/addon-links// .storybook/main.js export default { addons: [storybook/addon-links], };从源码看这里有一个容易被忽略的细节preview.ts 的内容是import { withLinks } from ./index.ts; export const decorators [withLinks];也就是说一旦你在main.js中注册了storybook/addon-linkswithLinks会作为 addon 级装饰器自动作用于所有 story而不必在每个 story 里手动添加。这意味着data-sb-kind/data-sb-story属性的点击拦截在注册 Addon 后全局生效——这是理解withLinks生命周期行为的关键前提后面会展开。管理端manager侧则通过 manager.ts 注册事件处理器addons.register(ADDON_ID, (api) { api.on(EVENTS.REQUEST, ({ kind, name }) { const id api.storyId(kind, name); api.emit(EVENTS.RECEIVE, id); }); });其中ADDON_ID为storybook/links事件常量定义在 constants.ts 中NAVIGATE、REQUEST、RECEIVE均挂在storybook/links/前缀下参数键PARAM_KEY为links。manager 端负责把 kind/name 解析为最终 storyId 后再广播这是预览端与框架端之间的一次“解引用”。linkTo事件驱动的 Story 跳转linkTo是最常用的 API。README 给出的基本用法是把它作为事件处理函数挂到组件上第一个参数是组件的titlestory kind第二个可选参数是 story 的导出名import { linkTo } from storybook/addon-links; export default { title: Button, }; export const first () button onClick{linkTo(Button, second)}Go to Second/button; export const second () button onClick{linkTo(Button, first)}Go to First/button;参数规则与 README 一致第一个参数story kind 名即title字段命名的值第二个参数可选story 名即导出名省略时跳转到该 kind 下的第一个 story。三个典型形态import { linkTo } from storybook/addon-links; linkTo(Toggle, off); linkTo( () Toggle, () off ); linkTo(Toggle); // Links to the first story in the Toggle kind支持函数形式按事件动态决定目标linkTo的任意参数都可以传函数函数会接收事件触发的参数并返回字符串。例如用select动态跳转到任意 storyimport { linkTo } from storybook/addon-links; import LinkTo from storybook/addon-links/react; export default { title: Select, }; export const index () ( select valueIndex onChange{linkTo(Select, (e) e.currentTarget.value)} optionindex/option optionfirst/option optionsecond/option optionthird/option /select ); export const first () LinkTo storyindexGo back/LinkTo; export const second () LinkTo storyindexGo back/LinkTo; export const third () LinkTo storyindexGo back/LinkTo;从源码 utils.ts 看这个能力由内部辅助函数valueOrCall实现参数是函数时执行它并取返回值是字符串时原样使用。linkTo本身返回一个可多次调用的事件处理函数其分支逻辑是export const linkTo ( idOrTitle: string | ((...args: any[]) string), nameInput?: string | ((...args: any[]) string) ) (...args: any[]) { const resolver valueOrCall(args); const title resolver(idOrTitle); const name nameInput ? resolver(nameInput) : false; if (title?.match(/--/) !name) { navigate({ storyId: title }); } else if (name title) { navigate({ kind: title, story: name }); } else if (title) { navigate({ kind: title }); } else if (name) { navigate({ story: name }); } };由此可以确认几个实操要点支持直接传完整 storyId如果第一个参数含--且没有第二个参数会被直接当作storyId处理navigate({ storyId: title })。最终统一走navigate而navigate的实现只有一行——向 addon channel 发出SELECT_STORY事件export const navigate (params: ParamsId | ParamsCombo) addons.getChannel().emit(SELECT_STORY, params);也就是说linkTo并不改 URL而是通过 Storybook 的内部事件总线通知框架端切换 storyparams可以是{ storyId }也可以是{ kind, story }组合。模板故事中的真实用法仓库自带的模板故事 template/stories/linkto.stories.ts 覆盖了各种输入形态值得作为参数写法的参照export const Id { args: { onClick: linkTo(addons-links-linkto--target) }, // 完整 storyId }; export const TitleOnly { args: { onClick: linkTo(addons/links/linkTo) }, // 原始 title含 / }; export const NormalizedTitleOnly { args: { onClick: linkTo(addons-links-linkto) }, // 归一化后的 title }; export const TitleAndName { args: { onClick: linkTo(addons/links/linkTo, Target) }, // title story 名 }; export const Callback { args: { onClick: linkTo( (event: Event) addons-links-linkto, (event: Event) target ), }, };它还展示了跳转到文档页MDX 文档linkTo(Configure Your Project)与 autodocs 的Docsstory的场景说明linkTo的目标不限于组件 story也可以是文档页。hrefTo获取某个 story 的 URL如果需要的不是“跳转动作”而是“某个 story 的 URL 字符串”应使用hrefTo。它返回一个 Promiseresolve 为相对 URL。README 的示例是把结果打到 Actions 面板import { hrefTo } from storybook/addon-links; import { action } from storybook/actions; export default { title: Href, }; export const log () { hrefTo(Href, log).then(action(URL of this story)); return spanSee action logger/span; };模板故事 template/stories/hrefto.stories.ts 则展示了在play函数中使用hrefTo的方式export const Default { play: async () { const href await hrefTo(addons-links-hrefto, target); const content document.querySelector(#content); if (content) { content.textContent href; } }, };从源码 utils.ts 看hrefTo的 URL 构造规则是export const hrefTo (title: ComponentTitle, name: StoryName): Promisestring { return new Promise((resolve) { const { location } document; const query parseQuery(location.search); const existingId query.id; const titleToLink title || existingId.split(--, 2)[0]; const id toId(titleToLink, name); const path /story/${id}; // Drop the iframe.html from the preview path const sbPath location.pathname.replace(/iframe\.html$/, ); const url ${location.origin sbPath}?${...path...}; resolve(url); }); };目标 story 通过 CSF 的toId(title, name)归一化为标准 storyIdtitle 会被转成连字符形式若省略 title会从当前 URL 的?id查询参数中取当前 storyId 的--前缀作为 kind——即在某个 story 内部调用hrefTo(undefined, other)可以拿到同 kind 下另一个 story 的链接生成的 URL 形如originmanager 路径?path/story/storyId并会特意剥掉预览 iframe 路径里的iframe.html保证拿到的是指向 manager 入口、可在新标签页正常打开的地址。withLinks 装饰器用 data 属性声明式定义链接withLinks提供声明式的 story 链接方式在 DOM 元素上加data-sb-kind与data-sb-story属性即可无需为每个元素绑定 JS 事件。README 的示例import { withLinks } from storybook/addon-links; export default { title: Button, decorators: [withLinks], }; export const first () ( button>!-- kind story -- a>const linksListener (e: Event) { const { target } e; if (!(target instanceof HTMLElement)) return; const element target as HTMLElement; const { sbKind: kind, sbStory: story } element.dataset; if (kind || story) { e.preventDefault(); navigate({ kind, story }); } }; export const withLinks makeDecorator({ name: withLinks, parameterName: PARAM_KEY, wrapper: (getStory, context) { on(); addons.getChannel().once(STORY_CHANGED, off); return getStory(context); }, });几个实现层面的关键事实监听器是挂在document上的 click 事件代理通过element.dataset读取sbKind/sbStory对应 HTML 属性data-sb-kind/data-sb-story命中才preventDefault并调用navigate({ kind, story })因此对没有这两个属性的普通点击零干扰装饰器使用on()/off()成对管理监听器的挂载与卸载hasListener标志位防止重复注册并在STORY_CHANGED事件触发后移除监听器once语义由于预览端注册时withLinks已作为全局装饰器生效见上文 preview.ts 的分析实际效果是每个 story 挂载时确保监听器存在story 切换后清理避免旧 story 的监听残留parameterName: PARAM_KEY即links意味着该装饰器以parameters.links作为其参数命名空间——这也是 README 示例中显式写decorators: [withLinks]的场景所依赖的机制当你需要单独控制某个 story比如在不希望全局生效的框架里时可以在 meta 中手动挂载。LinkTo 组件ReactLinkTo是hrefTo的一种组件化封装它渲染原生a元素但对普通左键单击拦截默认行为、改为在 Storybook 内部跳转从而保留了“右键/⌘/CtrlClick 新标签页打开”等浏览器原生能力。从storybook/addon-links/react子路径导入import LinkTo from storybook/addon-links/react; export default { title: Link, }; export const first () LinkTo storysecondGo to Second/LinkTo; export const second () LinkTo storyfirstGo to First/LinkTo;它接受所有a元素的 props另加story与kindkind省略时保持当前 kind。README 还给出了带额外属性的 MDX 用法LinkTo kindToggle storyoff target_blank titlelink to second story style{{ color: #1474f3 }} Go to Second /LinkTo源码实现在 react/components/link.tsxREADME 中引用的RoutedLink在当前仓库中即此文件核心行为// Cmd/Ctrl/Shift/Alt Click should trigger default browser behaviour. const LEFT_BUTTON 0; const isPlainLeftClick (e) e.button LEFT_BUTTON !e.altKey !e.ctrlKey !e.metaKey !e.shiftKey; const cancelled (e, cb) { if (isPlainLeftClick(e)) { e.preventDefault(); cb(e); } };组件是PureComponent在componentDidMount/componentDidUpdatekind/title/story/name变化时异步调用hrefTo更新href点击时若命中“纯左键单击”无修饰键、非右键则preventDefault并执行navigate({ title, name })内部跳转否则放行走浏览器默认的链接行为。对kind与story的成对校验title name才更新 href/导航与linkTo的参数语义保持一致。README 同时提示如果要在其他框架实现等效组件需要自行处理原生a的click事件拦截可直接参考该组件源码。使用建议与限制结合源码与包配置实践中值得注意的边界适用前提linkTo/withLinks依赖 addon channel 与预览端事件机制在 Storybook 运行时内工作hrefTo依赖document.location因此生成的是相对当前 Storybook 实例的 URL适合展示、复制分享或构造可外链的地址。命名兼容从模板故事可见linkTo接受原始 title如addons/links/linkTo或归一化 title如addons-links-linkto也接受完整 storyId含--hrefTo内部经toId归一化对连字符化 title 与 story 名大小写同样宽容。框架支持核心 API 与框架无关仅LinkTo组件依赖 React可选 peer 依赖React 16.8 19react-native框架在 package.json 中被明确标记为不支持。与 withLinks 的全局性共存由于注册 addon 后withLinks默认全局挂载页面中任何带data-sb-kind/data-sb-story属性的元素点击都会被拦截跳转。如果某个 story 中存在不希望被劫持的此类属性元素需要意识到这一默认行为源码中监听器仅在这两个 dataset 字段存在时才干预因此常规属性不受影响。延伸阅读仓库内与 Links Addon 直接相关的源码与示例文件核心实现src/utils.tslinkTo/hrefTo/withLinks/navigate、src/index.ts、src/preview.ts、src/manager.ts、src/constants.tsReact 组件src/react/components/link.tsx官方模板故事各 API 形态的参考实现template/stories/linkto.stories.ts、template/stories/decorator.stories.ts、template/stories/hrefto.stories.ts包元数据与框架支持声明package.json掌握以上内容后你可以按需选择事件级跳转用linkTo支持函数式动态目标、需要外链地址用hrefTo、大批量静态链接用withLinks的 data 属性声明、React 项目里需要真实a语义新标签页、键盘可达性时用LinkTo组件从而在 Storybook 中把离散组件 story 组织成连贯的交互演示与原型。【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表