前言
最近掐指一算髮現本月還有篇技術博文沒寫~,雖然隨便拿一篇日常積累的文章,或者把最近重構的一些點拿出來講都可以糊弄過去,但是我決定還是搞一點事情。。。
近期就有一個需求是這樣的,我手裡進行的一個重構專案裡,有一些元件我想抽離,給未來其它專案使用,然後我還需要開發兩個前端專案,他們有一些共同的元件需求。純靜態頁面是不適合的,因為我現在技術棧上了React + Typescipt。我想做到即插即用。順便把props,state這些東西定義好,以後改一改就能上專案。原本是立了個flag準備自己搞一點東西出來,但是在微信群裡,有人扔了一個連結出來storybook,
粗略一看好像就說我需要的。因此今天目標就是搗鼓一份開發環境。
確定需求
Storybook是UI元件的開發環境。它允許您瀏覽元件庫,檢視每個元件的不同狀態,以及互動式開發和測試元件。
但是官方github的介紹非常貧瘠,因此建議大家看Introduction to Storybook 來了解更多。
以及guide
我們明確一下我們的需求:
- 支援載入ant-design等UI庫
- 支援Typescript
- 支援redux
- 支援引數除錯
正式開始
根據思路先建立一個支援ts的react專案
create-react-app my-app --scripts-version=react-scripts-ts
然後更新依賴包
yarn upgrade
然後按照 storybook
npm i -g @storybook/cli
cd my-app
getstorybook
複製程式碼
之後直接執行yarn run storybook
就可以看到介面
然而事實並沒有那麼簡單。因為支援ts的是專案本身,而storybook是獨立出來的。因此你需要按照配置進行各種修改。
首先在.storybook
目錄下建立webpack.config.js
裡面載入typescript-loader
// load the default config generator.
const genDefaultConfig = require('@storybook/react/dist/server/config/defaults/webpack.config.js');
module.exports = (baseConfig, env) => {
const config = genDefaultConfig(baseConfig, env);
// Extend it as you need.
// For example, add typescript loader:
config.module.rules.push({
test: /\.(ts|tsx)$/,
loader: require.resolve('awesome-typescript-loader')
});
config.resolve.extensions.push('.ts', '.tsx');
return config;
};
複製程式碼
然後對package.json
進行改造
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-scripts-ts": "2.7.0"
},
"scripts": {
"start": "react-scripts-ts start",
"build": "react-scripts-ts build",
"test": "react-scripts-ts test --env=jsdom",
"eject": "react-scripts-ts eject",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
"devDependencies": {
"@storybook/addon-actions": "^3.2.12",
"@storybook/addon-links": "^3.2.12",
"@storybook/react": "^3.2.12",
"@types/jest": "^21.1.2",
"@types/node": "^8.0.39",
"@types/react": "^16.0.12",
"@types/react-dom": "^16.0.1",
"@types/storybook__react": "^3.0.5",
"awesome-typescript-loader": "^3.2.3"
}
}
複製程式碼
之後最重要的一點是將根目錄下的stories
目錄移到src
目錄之下.
裡面寫入一個index.tsx
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { linkTo } from '@storybook/addon-links';
import { Button, Welcome } from '@storybook/react/demo';
storiesOf('Welcome', module).add('to Storybook', () => <Welcome showApp={linkTo('Button')} />);
storiesOf('Button', module)
.add('with text', () => <Button onClick={action('clicked')}>Hello Button</Button>)
.add('with some emoji', () => <Button onClick={action('clicked')}>? ? ? ?</Button>);
複製程式碼
之後再執行yarn run storybook
就可以實現支援ts語法了。
之後我們需要考慮我們的ui元件該如何組織,通過大量翻看gitHub上的原始碼,大體上兩種方式。
一種是在同名元件下直接新增.stories.ts
的檔案
./Button.jsx
./Button.stories.ts
複製程式碼
一種是stories
目錄下建立index.ts,引用其他元件內容。
我們採取後一種,這是為了方便管理。而且直接在我們現有程式碼基礎上就可以進行。
我們考慮做個demo,現在react
+ redux
的demo都是用todolist
來完成。但是我們這裡直接代入一個成熟的redux
方案。
首先我們看src
目錄下現在的結構:
.
├── App.css
├── App.test.tsx
├── App.tsx
├── index.css
├── index.tsx
├── logo.svg
├── registerServiceWorker.ts
├── stories
│ └── index.jsx
├── webpack.config.1.js
└── webpack.config.js
複製程式碼
很明顯 典型create-app
的結構。
然後我們直接看加入redux之後的專案結構:
── src
│ ├── actions
│ │ └── index.ts
│ ├── components
│ ├── constants
│ │ └── index.ts
│ ├── containers
│ │ └── App
│ ├── index.tsx
│ ├── logo.svg
│ ├── reducers
│ │ ├── index.ts
│ │ └── info.ts
│ ├── registerServiceWorker.ts
│ ├── store
│ │ └── index.ts
│ ├── stories
│ │ └── index.jsx
│ ├── typing.d.ts
│ ├── webpack.config.1.js
│ └── webpack.config.js
複製程式碼
這裡還需要注意的是你需要對tsconfig
做一些整個,而且為了支援Less,我對webpack也做了一些修改。
之後我們寫一段簡單的action to reducer
action:
import { INFO_LIST } from '../constants/index'
const saveList = (data: Object) => ({
type: INFO_LIST,
data: data,
})
export function infoListRemote () {
const info = {
data: {
item: 'Hello LinShuiZhaoYing',
cnItem: '你好, 臨水照影'
}
}
return (dispatch: any) => {
dispatch(saveList(info))
return info
}
}
複製程式碼
reducer:
import { INFO_LIST } from '../constants';
const initialState = {
info:''
}
const info = (state = initialState, action: any) => {
// console.log(action)
switch (action.type) {
case INFO_LIST:
return {
...state,
info:action.data.data
}
default:
return state
}
}
export default info;
複製程式碼
App:
index.tsx:
import * as React from 'react';
import { Button, Icon } from 'antd';
import { connect } from 'react-redux';
import { infoListRemote } from '../../actions/index';
import './index.css';
class App extends React.Component<any, any> {
constructor (props: any) {
super(props)
this.state = {
infoList: '',
}
}
componentWillMount() {
}
componentDidMount() {
// console.log(this.props)
}
componentWillReceiveProps(nextProps: any) {
// console.log(nextProps)
if (nextProps.info) {
this.setState({
infoList: nextProps.info.item
})
}
}
getInfo = () => {
const { dispatch } = this.props;
dispatch(infoListRemote())
}
render() {
return (
<div className="App">
<div className="test"> {this.state.infoList} </div>
<Button type="danger" onClick={this.getInfo}> Click Me</Button>
<Icon type="play-circle-o" />
</div>
);
}
}
const mapStateToProps = (state: any) => ({
info: state.info.info,
})
let AppWrapper = App
AppWrapper = connect(mapStateToProps)(App);
export default AppWrapper;
複製程式碼
然後來看下效果圖:
可以看到資料已經傳遞成功。
接下來就是寫stories
,因為我們用了redux
所以我們需要用addDecorator
來包裝我們的元件
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { linkTo } from '@storybook/addon-links';
import { Provider } from 'react-redux';
import { Button, Welcome } from '@storybook/react/demo';
import { createBrowserHistory } from 'history';
import configureStore from '../store';
import AppWrapper from '../containers/App'
const store = configureStore(createBrowserHistory);
storiesOf('AppWrapper', module)
.addDecorator(story => <Provider store={store}>{story()}</Provider>)
.add('empty App', () => <AppWrapper />);
storiesOf('Button', module)
.add('with text', () => <Button onClick={action('clicked')}>Hello Button</Button>)
.add('with some emoji', () => <Button onClick={action('clicked')}>? ? ? ?</Button>);
複製程式碼
最後一步就是加入引數除錯,這裡我們需要加在一些addon
來增強體驗。
我們可以在More addons這裡看到addons列表。根據需求來增加。然後到對應的git網站上去看用法加入即可。
首先加入addon-notes
,它用來寫元件描述,而且經過測試,它是可以加入Html程式碼,因此可以先自己定義統一格式,然後加入內容。
還可以自定義一些資訊,比如使用引數,暴露出來的介面等等。載入Info Addon
就可以實現。
接下來的核心就是增加引數除錯功能
這裡給段示例程式碼:
storiesOf('AppWrapper', module)
.addDecorator(withKnobs)
.addDecorator(story => <Provider store={store}>{story()}</Provider>)
.add('knobs App', () =><AppWrapper text={text('Label', 'Hello World')}></AppWrapper>)
.add('with all knobs', () => {
const name = text('Name', 'Tom Cary');
const dob = date('DOB', new Date('January 20 1887'));
const bold = boolean('Bold', false);
const selectedColor = color('Color', 'black');
const favoriteNumber = number('Favorite Number', 42);
const comfortTemp = number('Comfort Temp', 72, { range: true, min: 60, max: 90, step: 1 });
const passions = array('Passions', ['Fishing', 'Skiing']);
const customStyle = object('Style', {
fontFamily: 'Arial',
padding: 20,
});
const style = {
...customStyle,
fontWeight: bold ? 800 : 400,
favoriteNumber,
color: selectedColor,
};
return (
<div style={style}>
I like: <ul>{passions.map((p, i) => <li key={i}>{p}</li>)}</ul>
<p>My favorite number is {favoriteNumber}.</p>
<p>My most comfortable room temperature is {comfortTemp} degrees Fahrenheit.</p>
</div>
);
});
複製程式碼
需要在開發的時候把動態傳遞的引數給設定好。這樣才能即時顯示。效果圖如下:
然後呼叫addon options
在config.js里加入
setOptions({
downPanelInRight: true,
})
複製程式碼
把橫軸的顯示板變成豎著的。
還有非常多有意思的addons
,比如Info的提升版本readme
.以及一鍵換背景的backgrounds
。還有現成的Material-UI
。還有直接顯示你Jsx原始碼的Storybook-addon-jsx
.以及控制版本顯示的storybook-addon-versions
,讓你直接對比多個版本的區別。一鍵生成所有截圖的Storybook Chrome Screenshot Addon
。這些社群的addons
都非常實用。感興趣可以自己增加。
結尾
最後我們已經完美完成了之前的需求,還有了一些意外的驚喜。
根據我這份環境配置,可以自行進行擴充套件,雖然我現在基於React開發。但是StoryBook
也是支援Vue的。
最後照慣例放出該工程的github地址