Open shaozj opened 5 years ago
组件开发的流程一般是需求确定、开发、测试、编译、发布。如果我们只是逐个地开发组件,每个组件独立发布,这样不利于沉淀出一个完整的组件库,形成一个完整的组件系统,以支撑某个领域的业务。开发组件库,除了需要在工程上完成开发、测试、编译和发布的支持,还需要有一个组件展示平台,能以网站的形式将所有组件的介绍、使用方式、使用示例、API 等展现出来。可以说,一个好的组件展示平台,对组件的使用和推广能起到很大的作用。 当前有两个组件开发展示平台是较为常见的,即 storybook 和 bisheng。storybook 自身定位为 ui 组件的开发环境,它提供了丰富的插件功能,可以交互式地开发和测试组件。bisheng 是蚂蚁金服前端开发的基于 markdown 和 react 生成静态网站的工具。虽然其定位是生成静态网站,不过主要用处还是用于生成组件展示平台。 下文将对这两个工具作为组件库开发展示平台进行技术上的对比和分析。
src/index.js 是 bisheng 程序的实际入口文件。start、build 等操作都是执行该文件中的函数。这个程序主要的功能是,读取 bisheng 配置文件 => 生成页面入口js文件 => 生成页面路由js文件 => 配置 webpack/webpackDevServer(开发环境) => 启动开发服务器 => webpack 编译完成后生成 html 文件。
src/index.js
`entry.${configEntryName}.js`,根据模板 entry.nunjucks.jsx 生成入口文件于 tmp 目录下。下文中,我们默认将 configEntryName 设为 index。
`entry.${configEntryName}.js`
entry.nunjucks.jsx
tmp
以routes.nunjucks.jsx 为模板,传入 themePath, themeRoutes 等参数,在 tmp 目录下生成 routes.index.js 文件。这个文件将被入口文件引入,用于生成最终的页面路由:
routes.nunjucks.jsx
routes.index.js
// tmp/entry.index.js var routes = require('./routes.index.js')(data);
其中,data 是我们处理后的有关页面的所有数据。 在生成 routes.index.js 时,我们会传入 themeRoutes,这个是我们在项目的 theme 配置文件中配置的路由。在 antd-fincore 中,配置 routes 如下:
themeRoutes
// site/theme/index.js const homeTmpl = "./template/Home/index"; const contentTmpl = "./template/Content/index"; routes: { path: "/", component: "./template/Layout/index", indexRoute: { component: homeTmpl }, childRoutes: [{ path: "index-cn", component: homeTmpl }, { path: "/docs/:children", component: contentTmpl }, { path: "/components", component: contentTmpl }, { path: "/components/:children", component: contentTmpl } ] }
theme 中配置的 routes 会在 routes.index.js 中经 getRoutes 函数做进一步处理,使之成为真正能被渲染出来的 routes。处理后返回的 routes 如下:
getRoutes
从上图可以看到,要渲染出路由对应的组件,最关键的是 getComponent 函数,这个 getComponent 函数将在后面分析。
config/updateWebpackConfig
webpackConfig.module.rules.push({ test(filename) { return filename === path.join(bishengLib, 'utils', 'data.js') || filename === path.join(bishengLib, 'utils', 'ssr-data.js'); }, loader: path.join(bishengLibLoaders, 'bisheng-data-loader'), });
// entry.nunjucks.jsx const data = require('../lib/utils/data.js');
data.js 是个空文件,通过以上处理,其实是为了在 webpack 处理 data.js 时,加载 bisheng-data-loader
bisheng-data-loader
// components-AfcCodeEditor-demo.index.js module.exports = { 'basic': require('bisheng/lib/loaders/source-loader!antd-fincore/components/AfcCodeEditor/demo/basic.md'), }
对所有 Markdown 文件处理后 bisheng-data-loader 返回如下形式的结果:
// bisheng-data-loader // @return data { markdown: { components: { AfcCodeEditor: { demo: function() { return new Promise(function(resolve) { require.ensure( [], function(require) { resolve( require('tmp/components-AfcCodeEditor-demo.index.js') ); }, 'components/AfcCodeEditor/demo' ); }); }, index: function() { return new Promise(function(resolve) { require.ensure( [], function(require) { resolve( require('bisheng/lib/loaders/source-loader!antd-fincore/components/AfcCodeEditor/index.md') ); }, 'components/AfcCodeEditor/index.md' ); }); }, }, }, }, picked: { // 用于生成 menu components: [{ meta: [Object] }, { meta: [Object] }], }, plugins: '[require("/bisheng-plugin-highlight/lib/browser.js"), {}]', }
source-loader
mark-twain
collector
jsonml-to-react-element
// bisheng-data-loader collector(nextProps) .then((collectedValue) => { try { const Comp = Template.default || Template; Comp[dynamicPropsKey] = { ...nextProps, ...collectedValue }; callback(null, Comp); } catch (e) { console.error(e) } }) .catch((err) => { const Comp = NotFound.default || NotFound; Comp[dynamicPropsKey] = nextProps; callback(err === 404 ? null : err, Comp); });
// create-element.jsx /* eslint-disable no-unused-vars */ const React = require('react'); /* eslint-enable no-unused-vars */ const NProgress = require('nprogress'); module.exports = function createElement(Component, props) { NProgress.done(); const dynamicPropsKey = props.location.pathname; return <Component {...props} {...Component[dynamicPropsKey]} />; };
// entry.index.js const router = ( <ReactRouter.Router history={ReactRouter.useRouterHistory(history.createHistory)({ basename })} routes={routes} createElement={createElement} /> ); ReactDOM.render( router, document.getElementById('react-content'), );
插件主要分为 node 端的处理和 browser 端的处理。插件的 node 部分,主要是对 markdownData 做进一步的处理。
// utils/source-data.js const markdown = require(transformer.use)(filename, fileContent); const parsedMarkdown = plugins.reduce( (markdownData, plugin) => require(plugin[0])(markdownData, plugin[1], isBuild === true), markdown, ); return parsedMarkdown;
上个插件输出的结果是下个插件的输入,一个处理结果的例子如下:
{ content: [], meta: { order: 1, title: '搜索多个员工', filename: 'components/AfcUserSearch/demo/multiple.md', id: 'components-AfcUserSearch-demo-multiple' }, toc: [ 'ul' ], highlightedCode: [], }
上面的 meta 信息在页面渲染 menu 的时候会被用到。 插件的 browser 部分, 会放入 utils 中,会作为页面组件的属性传入页面中:
// routes.index.js function generateUtils(data, props) { const plugins = data.plugins.map(pluginTupple => pluginTupple[0](pluginTupple[1], props)); const converters = chain(plugin => plugin.converters || [], plugins); const utils = { get: exist.get, toReactComponent(jsonml) { return toReactElement(jsonml, converters); }, }; plugins.map(plugin => plugin.utils || {}) .forEach(u => Object.assign(utils, u)); return utils; }
插件 browser 部分的 converters 和 utils 分别传入 toReactElement 和 utils 中。最终 utils 将被传入页面 react 组件中,在渲染页面时可以调用使用。例如:
// antd-fincore // site/theme/template/Content/ComponentDoc.jsx {props.utils.toReactComponent( ['section', { className: 'markdown' }].concat(getChildren(content)) )}
storybook 本身支持 react、vue、angular 等多种框架,我们主要分析 react 框架。storybook-react,react 部分分为 client 和 server 两部分。其依赖于 storybook-core,core 分为 server 和 client 两部分。
开发环境,启动开发服务器实际入口文件为 app/react/src/server/index.js。启动开发服务器的代码十分简洁,调用 core 的 buildDev 函数,传入相应配置:
app/react/src/server/index.js
// app/react/src/server/index.js import { buildDev } from '@storybook/core/server'; import options from './options'; buildDev(options);
在 core 的 buildDev 函数中,启动一个 express 服务器,使用 storybook middleware:
// core/src/server/build-dev.js const storybookMiddleware = await storybook(options); app.use(storybookMiddleware);
在 storybook middleware 中,用 getBaseConfig,自定义的 .babelrc 文件和 webpack.config.js 配置文件生成最终的 webpackConfig。new 一个 express Router,router 使用 webpackDevMiddlewareInstance 和 webpackHotMiddleware。配置相应的路由。
getBaseConfig
.babelrc
webpack.config.js
webpackDevMiddlewareInstance
webpackHotMiddleware
// lib/core/src/server/middleware.js router.use(webpackDevMiddlewareInstance); router.use(webpackHotMiddleware(compiler)); ... router.get('/', (req, res) => { res.set('Content-Type', 'text/html'); res.sendFile(path.join(`${__dirname}/public/index.html`)); }); router.get('/iframe.html', (req, res) => { res.set('Content-Type', 'text/html'); res.sendFile(path.join(`${__dirname}/public/iframe.html`)); });
整个流程就是启动一个 express 服务器,添加 webpackDevMiddleware 和 webpackHotMiddleware,其中需要 webpackConfig,这个 webpackConfig 是用 storybook 中的默认配置和用户自定义配置结合生成的。
storybook 的页面结构是外层为框架页面 manager,内部为 iframe。查看 webpackConfig 中的 entry 配置:
manager 的入口为 storybook 框架中写的 lib/core/src/client/manager/index.js。
lib/core/src/client/manager/index.js
// lib/core/src/client/manager/index.js import { document } from 'global'; import renderStorybookUI from '@storybook/ui'; import Provider from './provider'; const rootEl = document.getElementById('root'); renderStorybookUI(rootEl, new Provider());
manager 页面的渲染基于 @storybook/mantra-core 框架,状态管理用了 @storybook/podda,这些我们都不熟悉,暂时只要理解基于他们来渲染外层框架页面 manager。
@storybook/mantra-core
@storybook/podda
iframe 页面的入口为用户项目下 storybook 配置目录下的 config.js 文件。在我创建的工程中,其配置如下:
// .storybook/config.js import { configure, addDecorator } from '@storybook/react'; import { withOptions } from '@storybook/addon-options'; addDecorator( withOptions({ hierarchySeparator: /\/|\./, hierarchyRootSeparator: /\|/, }) ); function loadStories() { require('../stories/'); } configure(loadStories, module);
可见,configure 函数是渲染 iframe 页面的入口函数。
下图展示了 storybook 整个页面的架构:
子页面加载所有的 stories,做处理,生成目录结构数据,通知主页面渲染:
// lib/core/src/client/preview/config_api.js _renderMain(loaders) { if (loaders) loaders(); const stories = this._storyStore.dumpStoryBook(); // send to the parent frame. this._channel.emit(Events.SET_STORIES, { stories }); // clear the error if exists. this._reduxStore.dispatch(clearError()); this._reduxStore.dispatch(setInitialStory(stories)); }
这个 stories 是如何得到的呢?在 _renderMain 中,首先执行了 loaders(); 也就是我们在 config.js 中配置的 require('../stories/');。这样其实也就会执行我们所有编写的 stories。每个 story 编写的格式都是如下所示:
loaders();
require('../stories/');
// stories/AvatarList/index.js import React from 'react'; import { storiesOf } from '@storybook/react'; storiesOf('AvatarList', module) .add('example1', () => { return ( <div> AvatarList </div> ); });
这里关键的是 storiesOf 这个函数。
// lib/core/src/client/preview/client_api.js storiesOf = (kind, m) => { ... const localDecorators = []; let localParameters = {}; const api = { kind, }; // apply addons Object.keys(this._addons).forEach(name => { const addon = this._addons[name]; api[name] = (...args) => { addon.apply(api, args); return api; }; }); api.add = (storyName, getStory, parameters) => { if (typeof storyName !== 'string') { throw new Error(`Invalid or missing storyName provided for a "${kind}" story.`); } if (this._storyStore.hasStory(kind, storyName)) { logger.warn(`Story of "${kind}" named "${storyName}" already exists`); } // Wrap the getStory function with each decorator. The first // decorator will wrap the story function. The second will // wrap the first decorator and so on. const decorators = [...localDecorators, ...this._globalDecorators, withSubscriptionTracking]; const fileName = m ? m.id : null; const allParam = { fileName }; [this._globalParameters, localParameters, parameters].forEach(params => { if (params) { Object.keys(params).forEach(key => { if (Array.isArray(params[key])) { allParam[key] = params[key]; } else if (typeof params[key] === 'object') { allParam[key] = { ...allParam[key], ...params[key] }; } else { allParam[key] = params[key]; } }); } }); // Add the fully decorated getStory function. this._storyStore.addStory( kind, storyName, this._decorateStory(getStory, decorators), allParam ); return api; }; api.addDecorator = decorator => { localDecorators.push(decorator); return api; }; api.addParameters = parameters => { localParameters = { ...localParameters, ...parameters }; return api; }; return api; };
它的主要工作是应用 addons,应用 decorators 然后将处理后的 story 添加入 storyStore 中。之后,storybook 会调用 render 函数,利用之前存入 storyStore 中的数据,渲染出当前 story 的 iframe 页面。
通过 channel 通信,channel 基于 post-message 实现。
channel
// lib/core/src/client/manager/provider.js ... handleAPI(api) { api.onStory((kind, story) => { this.channel.emit(Events.SET_CURRENT_STORY, { kind, story }); }); this.channel.on(Events.SET_STORIES, data => { api.setStories(data.stories); }); this.channel.on(Events.SELECT_STORY, data => { api.selectStory(data.kind, data.story); }); this.channel.on(Events.APPLY_SHORTCUT, data => { api.handleShortcut(data.event); }); addons.loadAddons(api); }
storybook 的默认配置是基于 create-react-app 的。用户可以自定义修改 webpack 配置。如果我们是基于 bigfish 的项目,那么可以按如下方式来修改 webpack 配置:
// .storybook/webpack.config.js const path = require('path'); module.exports = (baseConfig, env, defaultConfig) => { defaultConfig.module.rules.push({ test: /\.jsx?$/, include: [path.resolve(__dirname, '../src'), path.resolve(__dirname, '../stories')], exclude: /node_modules/, use: [ { loader: 'babel-loader', options: { cacheDirectory: true, babelrc: false, presets: [ [ '@babel/env', { targets: { browsers: ['Chrome>=59'], }, modules: false, loose: true, }, ], '@babel/react', ], plugins: [ [ 'import', { libraryName: '@alipay/bigfish/antd', libraryDirectory: 'es', style: true }, ], [require("@babel/plugin-proposal-class-properties"), { "loose": false }], [require("@babel/plugin-proposal-decorators"), { "legacy": true }], ], }, }, ], }); defaultConfig.module.rules.push({ test: /\.less$/, include: [path.resolve(__dirname, '../src'), path.resolve(__dirname, '../stories')], use: [ 'style-loader', { loader: 'css-loader', options: { importLoaders: 1, sourceMap: false, modules: true, localIdentName: '[local]___[hash:base64:5]', }, }, { loader: 'postcss-loader', options: { plugins: () => [require('autoprefixer')], }, }, { loader: 'less-loader', options: { modifyVars: {}, javascriptEnabled: true, }, }, ], }); defaultConfig.module.rules.push({ test: /\.less$/, include: path.resolve(__dirname, '../node_modules'), use: [ 'style-loader', 'css-loader', { loader: 'less-loader', options: { modifyVars: {}, javascriptEnabled: true, }, }, ], }); defaultConfig.resolve.alias = defaultConfig.resolve.alias || {}; defaultConfig.resolve.alias['@alipay/bigfish/react'] = 'react'; defaultConfig.resolve.alias['@alipay/bigfish/antd'] = 'antd'; defaultConfig.resolve.alias['@alipay/bigfish/util/prop-types'] = 'prop-types'; defaultConfig.resolve.alias['@alipay/bigfish/util/classnames'] = 'classnames'; defaultConfig.resolve.alias['@alipay/bigfish/sdk/fetch'] = 'isomorphic-fetch'; defaultConfig.resolve.alias['@alipay/bigfish/sdk/router'] = 'react-router-dom'; defaultConfig.resolve.alias['@alipay/bigfish/sdk/history'] = 'history'; defaultConfig.resolve.alias['~'] = path.resolve(__dirname, '../src'); return defaultConfig; };
这里我们采用了 full-control mode,完全使用我们返回的修改了的 webpackConfig;
在 storybook 的 webpack 配置中,默认给 markdown 文本有如下配置 :
{ test: /\.md$/, use: [ { loader: require.resolve('raw-loader'), }, ], },
用 raw-loader 直接加载 markdown 文件,也就是直接得到了字符串。为了构建我们自己的组件库,我们需要将 markdown 字符串渲染为页面元素,同时,我们需要模仿 antd 插入组件示例和源码展示:
// stories/AvatarList/index.js import React from 'react'; import { storiesOf } from '@storybook/react'; import CodeExample from '../CodeExample'; import Demo1 from './demo1'; import Demo1Raw from '!raw-loader!./demo1'; import MarkView from '../MarkView'; import readme from '~/component/AvatarList/index.zh-CN.md'; storiesOf('AvatarList', module) .add('avatar list', () => { return ( <MarkView readme={readme} name="AvatarList"> <CodeExample title="基本用法" code={Demo1Raw}> <Demo1 /> </CodeExample> </MarkView> ); });
得到如下的效果:
对 markdown 的处理,完全放在 browser 端,通过 MarkView 组件来处理。这里只做了最简单的处理,将 Markdown 字符串解析为 ast,从 ast 中取出 yaml 用于渲染标题和子标题,将 ast 分为两部分,头部是组件介绍,尾部是 API 文档,中间插入我们编写的各个 demo 示例。
MarkView
bigfish 的插件可以分为两种,Decorators 和 Addons。其实在上文分析 storiesOf 函数时,已经讲到了 Decorators 和 Addons。
// lib/core/src/client/preview/client_api.js export const defaultDecorateStory = (getStory, decorators) => decorators.reduce( (decorated, decorator) => context => decorator(() => decorated(context), context), getStory ); const decorators = [...localDecorators, ...this._globalDecorators, withSubscriptionTracking];
收集完所有的 decorators 后,逐个执行 decorators,第一个 decorator 会包裹原始 story,第二个会包裹第一个,以此类推。
例子,让 story 居中:
import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import Button from './button'; const styles = { textAlign: 'center', }; const CenterDecorator = (storyFn) => ( <div style={styles}> { storyFn() } </div> ); storiesOf('Button', module) .addDecorator(CenterDecorator) .add('with text', () => ( <Button onClick={action('clicked')}>Hello Button</Button> )) .add('with some emojies', () => ( <Button onClick={action('clicked')}><span role="img" aria-label="so cool">😀 😎 👍 💯</span></Button> ));
Addons 更为强大,除了包裹 story 的处理,Addons 还提供其他特性的支持。
github 项目地址:https://github.com/shaozj/stories
前端组件库开发展示平台对比bisheng-vs-storybook
组件开发的流程一般是需求确定、开发、测试、编译、发布。如果我们只是逐个地开发组件,每个组件独立发布,这样不利于沉淀出一个完整的组件库,形成一个完整的组件系统,以支撑某个领域的业务。开发组件库,除了需要在工程上完成开发、测试、编译和发布的支持,还需要有一个组件展示平台,能以网站的形式将所有组件的介绍、使用方式、使用示例、API 等展现出来。可以说,一个好的组件展示平台,对组件的使用和推广能起到很大的作用。
当前有两个组件开发展示平台是较为常见的,即 storybook 和 bisheng。storybook 自身定位为 ui 组件的开发环境,它提供了丰富的插件功能,可以交互式地开发和测试组件。bisheng 是蚂蚁金服前端开发的基于 markdown 和 react 生成静态网站的工具。虽然其定位是生成静态网站,不过主要用处还是用于生成组件展示平台。
下文将对这两个工具作为组件库开发展示平台进行技术上的对比和分析。
bisheng
路由生成
bisheng 入口文件
src/index.js
是 bisheng 程序的实际入口文件。start、build 等操作都是执行该文件中的函数。这个程序主要的功能是,读取 bisheng 配置文件 => 生成页面入口js文件 => 生成页面路由js文件 => 配置 webpack/webpackDevServer(开发环境) => 启动开发服务器 => webpack 编译完成后生成 html 文件。页面入口文件
`entry.${configEntryName}.js`
,根据模板entry.nunjucks.jsx
生成入口文件于tmp
目录下。下文中,我们默认将 configEntryName 设为 index。路由生成
以
routes.nunjucks.jsx
为模板,传入 themePath, themeRoutes 等参数,在 tmp 目录下生成routes.index.js
文件。这个文件将被入口文件引入,用于生成最终的页面路由:其中,data 是我们处理后的有关页面的所有数据。
在生成
routes.index.js
时,我们会传入themeRoutes
,这个是我们在项目的 theme 配置文件中配置的路由。在 antd-fincore 中,配置 routes 如下:theme 中配置的 routes 会在
routes.index.js
中经getRoutes
函数做进一步处理,使之成为真正能被渲染出来的 routes。处理后返回的 routes 如下:从上图可以看到,要渲染出路由对应的组件,最关键的是 getComponent 函数,这个 getComponent 函数将在后面分析。
Markdown 解析
config/updateWebpackConfig
函数中,添加了如下配置entry.nunjucks.jsx
中data.js 是个空文件,通过以上处理,其实是为了在 webpack 处理 data.js 时,加载
bisheng-data-loader
bisheng-data-loader
根据 config 中的配置,加载到所有 markdown 文件,在 tmp 目录下为每个 markdown demo 文件生成一个对应的 js 文件,用于异步加载,其形式如下:对所有 Markdown 文件处理后
bisheng-data-loader
返回如下形式的结果:routes.index.js
,在routes.index.js
中的getRoutes
函数中,将生成页面的路由。路由对应的组件通过用户在 theme 中的配置取到模板,然后将模板和 data 数据结合最终渲染出页面。source-loader
处理 markdown 文件,其中,用mark-twain
将 markdown 转换为 JsonML。routes.index.js
中的collector
函数中将 utils(包括 plugins 如jsonml-to-react-element
)传入到浏览器端的组件中(通过 props),在各个页面中,通过调用 props.utils.toReactComponent 将 JsonML 转换为 react element,渲染出页面。在 antd-fincore 中,经collector
函数处理后的数据示例如下:问题:如何将 props 传入route组件中的?
插件机制
插件主要分为 node 端的处理和 browser 端的处理。插件的 node 部分,主要是对 markdownData 做进一步的处理。
上个插件输出的结果是下个插件的输入,一个处理结果的例子如下:
上面的 meta 信息在页面渲染 menu 的时候会被用到。
插件的 browser 部分, 会放入 utils 中,会作为页面组件的属性传入页面中:
插件 browser 部分的 converters 和 utils 分别传入 toReactElement 和 utils 中。最终 utils 将被传入页面 react 组件中,在渲染页面时可以调用使用。例如:
bisheng 小结
stroybook
storybook 本身支持 react、vue、angular 等多种框架,我们主要分析 react 框架。storybook-react,react 部分分为 client 和 server 两部分。其依赖于 storybook-core,core 分为 server 和 client 两部分。
工具入口
开发环境,启动开发服务器实际入口文件为
app/react/src/server/index.js
。启动开发服务器的代码十分简洁,调用 core 的 buildDev 函数,传入相应配置:在 core 的 buildDev 函数中,启动一个 express 服务器,使用 storybook middleware:
在 storybook middleware 中,用
getBaseConfig
,自定义的.babelrc
文件和webpack.config.js
配置文件生成最终的 webpackConfig。new 一个 express Router,router 使用webpackDevMiddlewareInstance
和webpackHotMiddleware
。配置相应的路由。整个流程就是启动一个 express 服务器,添加 webpackDevMiddleware 和 webpackHotMiddleware,其中需要 webpackConfig,这个 webpackConfig 是用 storybook 中的默认配置和用户自定义配置结合生成的。
页面入口
storybook 的页面结构是外层为框架页面 manager,内部为 iframe。查看 webpackConfig 中的 entry 配置:![](https://img.alicdn.com/tfs/TB141OGkNYaK1RjSZFnXXa80pXa-1518-544.png)
manager 的入口为 storybook 框架中写的
lib/core/src/client/manager/index.js
。manager 页面的渲染基于
@storybook/mantra-core
框架,状态管理用了@storybook/podda
,这些我们都不熟悉,暂时只要理解基于他们来渲染外层框架页面 manager。iframe 页面的入口为用户项目下 storybook 配置目录下的 config.js 文件。在我创建的工程中,其配置如下:
可见,configure 函数是渲染 iframe 页面的入口函数。
下图展示了 storybook 整个页面的架构:
路由生成和页面渲染
子页面加载所有的 stories,做处理,生成目录结构数据,通知主页面渲染:
这个 stories 是如何得到的呢?在 _renderMain 中,首先执行了
loaders();
也就是我们在 config.js 中配置的require('../stories/');
。这样其实也就会执行我们所有编写的 stories。每个 story 编写的格式都是如下所示:这里关键的是 storiesOf 这个函数。
它的主要工作是应用 addons,应用 decorators 然后将处理后的 story 添加入 storyStore 中。之后,storybook 会调用 render 函数,利用之前存入 storyStore 中的数据,渲染出当前 story 的 iframe 页面。
主页面和子页面之间的通信
通过
channel
通信,channel
基于 post-message 实现。webpack 配置
storybook 的默认配置是基于 create-react-app 的。用户可以自定义修改 webpack 配置。如果我们是基于 bigfish 的项目,那么可以按如下方式来修改 webpack 配置:
这里我们采用了 full-control mode,完全使用我们返回的修改了的 webpackConfig;
markdown 处理
在 storybook 的 webpack 配置中,默认给 markdown 文本有如下配置 :
用 raw-loader 直接加载 markdown 文件,也就是直接得到了字符串。为了构建我们自己的组件库,我们需要将 markdown 字符串渲染为页面元素,同时,我们需要模仿 antd 插入组件示例和源码展示:
得到如下的效果:
对 markdown 的处理,完全放在 browser 端,通过
MarkView
组件来处理。这里只做了最简单的处理,将 Markdown 字符串解析为 ast,从 ast 中取出 yaml 用于渲染标题和子标题,将 ast 分为两部分,头部是组件介绍,尾部是 API 文档,中间插入我们编写的各个 demo 示例。插件机制
bigfish 的插件可以分为两种,Decorators 和 Addons。其实在上文分析 storiesOf 函数时,已经讲到了 Decorators 和 Addons。
Decorators
收集完所有的 decorators 后,逐个执行 decorators,第一个 decorator 会包裹原始 story,第二个会包裹第一个,以此类推。
例子,让 story 居中:
Addons
Addons 更为强大,除了包裹 story 的处理,Addons 还提供其他特性的支持。
总结