在之前的一篇文章中,介绍过组件化搭建工具 storybook 在 vue 项目中的安装和配置。 相比于其成文的时间,vue 项目依赖的工具多有发展;并且在实际应用中,多种历史版本的项目并存的状况比比皆是,用官方提供的 npx sb init
往往会出现配置失败的情况,而较新或过旧的资料都在网上难觅 --
所以在此特别补充一篇,记录 新、旧 两种典型配置下,storybook 可用的手动配置方法:
1. babel7 + webpack5
1.1 安装过程
diff --git a/.babelrc b/.babelrc index e1f4817..71b5fe8 100644 --- a/.babelrc +++ b/.babelrc "modules": false, "targets": "last 2 years, ie > 8" } - ] + ], + "@vue/babel-preset-jsx" ], "plugins": [ "@babel/plugin-transform-runtime", diff --git a/.eslintignore b/.eslintignore index e2192c5..0858135 100644 --- a/.eslintignore +++ b/.eslintignore +/test/* +*.spec.js diff --git a/.storybook/demo.css b/.storybook/demo.css new file mode 100644 index 0000000..820f051 --- /dev/null +++ b/.storybook/demo.css +.demo { + padding: 50px; + display: inline-block; +} +.demo .block { + margin-bottom: 30px; +} diff --git a/.storybook/main.js b/.storybook/main.js new file mode 100644 index 0000000..ceca416 --- /dev/null +++ b/.storybook/main.js +const path = require("path"); +const pathResolve = p => path.resolve(__dirname, "../", p); + +module.exports = { + stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], + addons: ["@storybook/addon-links", "@storybook/addon-essentials"], + core: { + builder: "webpack5" + }, + webpackFinal: config => { + config.resolve.alias = { + ...config.resolve.alias, + "@": pathResolve("src"), + "~": pathResolve("node_modules") + }; + config.module.rules.push({ + test: /\.scss$/, + use: ["vue-style-loader", "css-loader", "sass-loader"] + }); + return config; + } +}; diff --git a/.storybook/manager-head.html b/.storybook/manager-head.html new file mode 100644 index 0000000..c5c2955 --- /dev/null +++ b/.storybook/manager-head.html +<style> + .sidebar-header { + text-align: center; + } + .sidebar-header > div > a { + font-size: 28px; + } +</style> diff --git a/.storybook/manager.js b/.storybook/manager.js new file mode 100644 index 0000000..c2321a4 --- /dev/null +++ b/.storybook/manager.js +import { addons } from "@storybook/addons"; +import { create } from "@storybook/theming"; + +addons.setConfig({ + theme: create({ + base: "light", + brandImage: null, + brandTitle: "前端组件" + }) +}); diff --git a/.storybook/preview.js b/.storybook/preview.js new file mode 100644 index 0000000..55d75d7 --- /dev/null +++ b/.storybook/preview.js +import "./demo.css"; + +export const parameters = { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/ + } + }, + // https://github.com/storybookjs/storybook/pull/9090/commits/2db3faa727fed585fb5d9e6db23cc3835fc88829 + viewMode: "docs" +}; diff --git a/package.json b/package.json index 673f3a9..c0b7b27 100644 --- a/package.json +++ b/package.json + "storybook": "start-storybook -p 6006 --no-manager-cache", + "build-storybook": "build-storybook" }, "dependencies": { + "@babel/plugin-syntax-jsx": "^7.12.13", + "@storybook/addon-actions": "^6.2.7", + "@storybook/addon-essentials": "^6.2.7", + "@storybook/addon-links": "^6.2.7", + "@storybook/builder-webpack5": "^6.2.7", + "@storybook/theming": "^6.2.8", + "@storybook/vue": "^6.2.7", + "@vue/babel-helper-vue-jsx-merge-props": "^1.2.1", + "@vue/babel-preset-jsx": "^1.2.4", "babel-plugin-transform-vue-jsx": "~3.7.0", diff --git a/src/utils/utils.js b/src/utils/utils.js new file mode 100644 index 0000000..3e8413d --- /dev/null +++ b/src/utils/utils.js +export const formatCode = jsCode => + JSON.stringify( + jsCode, + // eslint-disable-next-line + (key, value) => { + if (typeof value === 'function') { + return value.toString(); + } else { + return value; + } + }, + 2 + ).replace(/\\n/g, '\n'); diff --git a/src/utils/storyUtils.js b/src/utils/storyUtils.js new file mode 100644 index 0000000..4ad5149 --- /dev/null +++ b/src/utils/storyUtils.js +import {formatCode} from './utils'; + +/** + * 生成 storybook 用例辅助函数 + * @param {String} path - 组件的层级式路径,由 `/` 分割 + * @param {Object} storyComponent - 用于展示的用例 + * @param {String} markdown - 文档 + * @param {Object} [originComponent] - 可选,如果需要从原始组件内部的 jsdoc 自动生成 API,则传入原始组件 + * @return {Object} story - {metadata, named} + * @see https://github.com/storybookjs/storybook/issues/8527 + */ +export const storyOf = (path, storyComponent, markdown, originComponent) => { + const pathArr = path.split('/').map(p => { + // https://storybook.js.org/docs/vue/writing-stories/args#setting-args-through-the-url + if (!/^[0-9a-zA-Z]/.test(p)) { + return '#' + p; + } + return p; + }); + const storyName = pathArr[pathArr.length - 1]; + + const metadata = { + component: originComponent || void 0, + title: pathArr.join('/'), + decorators: [ + () => ({template: '<div class="demo"><story></story></div>'}) + ] + }; + + const named = () => storyComponent; + named.storyName = storyName; + named.parameters = { + docs: { + description: { + component: markdown || ' ' + }, + source: { + // type: 'dynamic' + type: 'code', + code: formatCode(storyComponent) + } + } + }; + + return { + metadata, + named + }; +};
1.2 组件和用例
用例1:
mdx
代码解读
复制代码
import { Meta, Preview, Description } from '@storybook/addon-docs/blocks' import readme from './README.md' import pathPrefix from './pathPrefix.js' <Meta title={pathPrefix + "/API"} /> <Description markdown={readme} />
组件2:
<template functional> <x-table v-bind="{ ...data.attrs, ...props }" v-on="listeners" > <!-- ... --> </x-table> </template> <script> export default { props: { /** * 配置中增加了 __自定义 component__ 的能力,避免了在 template 中再分别写 slot; * 自定义组件对象会默认接收 `row` 和 `column-config` 两个属性 */ columns: { type: Array, default: () => [] } }, emits: [ /** * 自定义组件中如果发出同名事件,会被 table 的容器监听到; * 参数为 `key, ...values` */ 'column-action' ] }; </script>
用例2:
import {storyOf} from '@/utils/storyUtils'; import Comp from '@/components/SimpleTable'; const title = 'Example/SimpleTable|全配置化表格'; const markdown = ` > 基于 \`x-table\` 的扩展,可以接收其原有属性和事件 `; const comp = { template: ` <x-simple-table :width="600" :columns="columns" :data="list" @column-action="onColAction" /> `, data() { return { //... }; }, methods: { onColAction(act, value) { alert([act, value]); } } }; const {metadata, named} = storyOf(title, comp, markdown, Comp); export default metadata; export const story = named;
2. babel6 + webpack4
2.1 安装过程
diff --git a/.storybook/addons.js b/.storybook/addons.js new file mode 100644 index 0000000..7106272 --- /dev/null +++ b/.storybook/addons.js +import 'storybook-addon-vue-info/lib/register'; +import '@storybook/addon-backgrounds/register'; +import '@storybook/addon-viewport/register'; +import '@storybook/addon-storysource/register'; diff --git a/.storybook/config.js b/.storybook/config.js new file mode 100644 index 0000000..f84ef78 --- /dev/null +++ b/.storybook/config.js +import {configure, addDecorator, addParameters} from '@storybook/vue'; +import {create} from '@storybook/theming'; +import {withInfo, setDefaults} from 'storybook-addon-vue-info'; +import './demo.css'; + +setDefaults({ + header: false, + useDocgen: false +}); +addDecorator(withInfo); +addParameters({ + options: { + theme: create({ + base: 'light', + brandImage: null, + brandTitle: '前端组件' + }) + } +}); +const req = require.context('../src', true, /\.stories\.js$/); +function loadStories() { + req.keys().forEach(filename => { + try { + req(filename); + } catch (ex) { + console.log('storybook-req', ex); + } + }); +} +configure(loadStories, module); diff --git a/.storybook/manager-head.html b/.storybook/manager-head.html new file mode 100644 index 0000000..c5c2955 --- /dev/null +++ b/.storybook/manager-head.html +<style> + .sidebar-header { + text-align: center; + } + .sidebar-header > div > a { + font-size: 28px; + } +</style> diff --git a/.storybook/demo.css b/.storybook/demo.css new file mode 100644 index 0000000..820f051 --- /dev/null +++ b/.storybook/demo.css +.demo { + padding: 50px; + display: inline-block; +} + +.demo .block { + margin-bottom: 30px; +} diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 0000000..e69de29 diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js new file mode 100644 index 0000000..3186d15 --- /dev/null +++ b/.storybook/webpack.config.js +const path = require('path'); +const pathResolve = p => path.resolve(__dirname, '../', p); +module.exports = ({config, mode}) => { + config.resolve.alias = { + ...config.resolve.alias, + '@': pathResolve('src'), + '~': pathResolve('node_modules') + }; + config.module.rules.push({ + test: /\.scss$/, + use: ['style-loader', 'css-loader', 'sass-loader'] + }); + config.module.rules.push({ + test: /\.vue$/, + loader: 'storybook-addon-vue-info/loader', + enforce: 'post' + }); + config.module.rules.push({ + test: /\.stories\.js$/, + loaders: [require.resolve('@storybook/addon-storysource/loader')], + enforce: 'pre' + }); + if (process.env.NODE_ENV === 'production') { + config.output.filename = 'bundle.[name].js'; + config.optimization.splitChunks.automaticNameDelimiter = '.'; + config.optimization.runtimeChunk = { + name: entrypoint => `runtime.${entrypoint.name}` + }; + } + // console.log(config); + return config; +}; diff --git a/package.json b/package.json index d83bbba..6027c58 100644 --- a/package.json +++ b/package.json + "build-storybook": "build-storybook", + "storybook": "start-storybook -p 6006" "devDependencies": { + "@storybook/addon-backgrounds": "^5.3.18", + "@storybook/addon-info": "^5.3.18", + "@storybook/addon-storysource": "^5.3.18", + "@storybook/addon-viewport": "^5.3.18", + "@storybook/addons": "^5.3.18", + "@storybook/theming": "^5.3.18", + "@storybook/vue": "^5.3.18", + "storybook-addon-vue-info": "^1.4.2",
2.2 用例
import {storiesOf} from '@storybook/vue'; import CustomCols from './index'; const totalColumns = [ // ... ]; const description = { CustomCols: { props: { choose: '用于 v-model 的值', totalColumns: '可选择的列', storageName: '本地存储的key', }, events: { 'on-change': '选择的列改变时触发' } } }; const info = { useDocgen: false, summary: '基于 xxx' }; storiesOf('增强的自定义表格列', module) .add( '本机存储上次选择结果', () => ({ components: { CustomCols }, template: ` <div class="demo"> <div id="demo-result"></div> <custom-cols :storage-name="selColsStoName" :total-columns="totalColumns" v-model="selectedCols" @on-change="onChange" /> </div> `, data() { return { totalColumns, selColsStoName: `${window.location.hostname}_overview_custom_cols`, selectedCols: ['impression', 'click'] }; }, methods: { onChange(v) { //... } }, description }), { info } );