如何在Storybook中根据所选视口展示不同故事标记并合并配置?
合并Storybook桌面/移动端配置,实现视口切换加载对应资源
核心解决思路
不用维护两套独立配置,而是通过Storybook的全局变量结合视口插件的状态监听,在切换视口时动态加载对应平台的CSS和标记文件,实现同一配置下的跨视口预览。
具体实现步骤
1. 统一主配置文件
把原两套配置的main.js内容合并,确保所有组件故事能被统一扫描,保留必要的插件:
// .storybook/main.js module.exports = { stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'], addons: [ '@storybook/addon-viewport', // 其他你用到的addons都放这 ], };
2. 在preview.js中实现动态资源加载
用Storybook的useGlobals钩子监听视口变化,根据当前视口加载对应的CSS和标记:
// .storybook/preview.js import { useEffect, useGlobals } from '@storybook/preview-api'; import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport'; // 先定义好视口和对应资源的映射关系 const viewportResourceMap = { // 对应桌面端视口(用你实际的视口key) desktop: { css: '../src/styles/desktop.css', markup: '../src/markup/desktop-base.html' }, // 对应移动端视口 mobile1: { css: '../src/styles/mobile.css', markup: '../src/markup/mobile-base.html' } }; export const decorators = [ (Story) => { const [{ viewport }] = useGlobals(); // 默认用桌面端视口 const currentViewport = viewport || 'desktop'; const { css, markup } = viewportResourceMap[currentViewport]; useEffect(() => { // 先移除之前加载的平台专属CSS document.querySelectorAll('.platform-css').forEach(el => el.remove()); // 加载当前视口对应的CSS const cssLink = document.createElement('link'); cssLink.rel = 'stylesheet'; cssLink.href = css; cssLink.className = 'platform-css'; document.head.appendChild(cssLink); // 替换平台专属标记(假设页面里有个容器放这些标记) const markupContainer = document.getElementById('platform-markup'); if (markupContainer) { fetch(markup) .then(res => res.text()) .then(html => markupContainer.innerHTML = html); } // 组件卸载时清理CSS return () => cssLink.remove(); }, [currentViewport]); return ( <> {/* 专门放平台标记的容器 */} <div id="platform-markup"></div> <Story /> </> ); } ]; // 配置视口选项,用默认的或者自定义你的视口 export const parameters = { viewport: { viewports: INITIAL_VIEWPORTS, defaultViewport: 'desktop' } };
3. 可选:预加载资源解决延迟问题
如果切换视口时资源加载慢,可以提前预加载所有平台的资源,切换时直接用缓存好的内容:
// 在preview.js开头加这段预加载逻辑 const preloaded = {}; // 提前拉取所有资源并存起来 Object.values(viewportResourceMap).forEach(async (res) => { if (!preloaded[res.css]) { const cssRes = await fetch(res.css); preloaded[res.css] = await cssRes.text(); } if (!preloaded[res.markup]) { const markupRes = await fetch(res.markup); preloaded[res.markup] = await markupRes.text(); } }); // 然后把useEffect里的加载逻辑改成用预加载内容 useEffect(() => { document.querySelectorAll('.platform-css').forEach(el => el.remove()); // 用style标签插入预加载的CSS文本 const cssStyle = document.createElement('style'); cssStyle.className = 'platform-css'; cssStyle.textContent = preloaded[css]; document.head.appendChild(cssStyle); const markupContainer = document.getElementById('platform-markup'); if (markupContainer) { markupContainer.innerHTML = preloaded[markup]; } return () => cssStyle.remove(); }, [currentViewport]);
4. 组件故事的适配(按需)
如果个别组件需要针对不同视口调整,直接在故事里通过全局变量判断即可:
// Button.stories.jsx export const Primary = { render: (args, { globals }) => { const isMobile = globals.viewport === 'mobile1'; return <Button {...args} variant={isMobile ? 'mobile-primary' : 'desktop-primary'} />; } };
内容的提问来源于stack exchange,提问作者Sergiu Statache
相关产品推荐
相关产品推荐

