本文意在幫助讀者快速搭建本身的前端UI組件庫,構建-打包-發佈,幫你解決大型web前端應用中組件重用的問題.css
Reacthtml
自2014年以來,react不斷地發展壯大,時至今日已經發展成爲最受歡迎的前端框架,若是你還不太瞭解react,請看這裏。前端
Storybooknode
storybook是一套UI組件的開發環境,它容許你瀏覽組件庫,查看每一個組件的不一樣狀態,以及交互式開發和測試組件。 storybook容許你獨立於你的app來開發你的UI組件,你能夠先不關心應用層級的組件依賴,快速的着手組件的開發,然後再將之應用於本身的app中。尤爲在大型應用,跨團隊合做過程當中,良好的組件抽象,使用storybook封裝管理,能夠極大的提升的組件的重用性,可測試性,和開發速度。你能夠點擊這裏查看storybook是如何工做的。react
Lernawebpack
lerna幫你管理你的包集合,當你本身的library變多時,你的版本控制,跟蹤管理,測試就會變得愈加複雜,lerna正是幫你解決這個問題,它使用npm和git來幫助你優化你的多包管理流程。git
本文假設你已經熟悉發佈本身的npm包,若是不熟悉,能夠先查看相關文章,例如《怎麼開發一個npm包》;github
接下來咱們就一步一步來搭建本身的UI組件庫。web
一. 初始化react app;npm
有不少教程幫助咱們如何搭建一個前端react app,本文重點不在react的原理,生命週期函數等使用上,這裏選擇facebook官方提供的腳手架create-react-app來快速構建一個react app,注意你的node版本(推薦>=6, 你可使用nvm來幫助你管理node版本,npx comes with npm 5.2+ and higher)。
npx create-react-app my-app
複製代碼
初始話成功後你會獲得一個以下的工程目錄:
my-app
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│ └── favicon.ico
│ └── index.html
│ └── manifest.json
└── src
└── App.css
└── App.js
└── App.test.js
└── index.css
└── index.js
└── logo.svg
└── registerServiceWorker.js
複製代碼
而後執行:
cd my-app
yarn start
複製代碼
此時你就能夠經過訪問你的http://localhost:3000/ 來查看你初始化好的app了;
二. 初始化storybook
若是你是第一次安裝storybook,嘗試如下命令:
npm i -g @storybook/cli
cd my-app #(the app above)
getstorybook
複製代碼
此時你會獲得一個以下的工程目錄:
my-app
├── .storybook
│ └── addons.js #(storybook的包依賴)
│ └── config.js #(配置文件,告訴storybook去加載哪些定義好的組件集合)
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│ └── favicon.ico
│ └── index.html
│ └── manifest.json
└── src
├── stories
│ └── index.js #(storybook的組件集合,你須要在這裏添加你建立好的UI組件)
└── App.css
└── App.js
└── App.test.js
└── index.css
└── index.js
└── logo.svg
└── registerServiceWorker.js
複製代碼
一旦你安裝好,此後能夠執行yarn run storybook
來起本地storybook開發環境server,訪問相應的url, 如http://localhost:9009/你會看到一個包含簡單示例的storybook交互界面:
三. 開發本身的組件
接下來讓咱們來開發本身的兩個button組件而且加入到storybook中:
在src
目錄下新建 StateFulReactButton.js
import React, { Component } from 'react';
class StateFulReactButton extends Component {
render() {
const { handleOnclick } = this.props;
return (
<button onClick={handleOnclick}>react stateful button</button>
);
}
}
export default StateFulReactButton;
複製代碼
同時新建 StatelessReactButton.js
import React from 'react';
const StatelessReactButton = ({ handleOnclick }) => {
return <button onClick={handleOnclick}>react stateless button</button>
};
export default StatelessReactButton;
複製代碼
將組件引入到storybook中:
在src/story/index.js
文件中添加入下代碼:
import StateFulReactButton from './../StatefullReactButton';
import StatelessReactButton from './../StatelessReactButton';
複製代碼
.add('StateFulReactButton', () => <StateFulReactButton handleOnclick={action('clicked')} />)
.add('StatelessReactButton', () => <StatelessReactButton handleOnclick={action('clicked')} />);
複製代碼
訪問本地storybook server,是否是看到了以下畫面:
好啦,至此咱們的兩個react組件就開發好了。
固然,配合其餘插件storybook能夠作不少事情,好比knobs
查看示例
,你能夠在你的storybook server界面上直接與你的定製的組件交互,直觀的驗證你的組件行爲,而這一切徹底從你的app中剝離出來了。
四. 應用你的組件
上述組件的開發驗證過程完成後,你就能夠把你的組價加入到你的app 生產代碼中去了。 好比在本例中,在你的src/App.js
中加入以下代碼:
import StateFulReactButton from './../StatefullReactButton';
import StatelessReactButton from './../StatelessReactButton';
複製代碼
<StateFulReactButton handleOnclick={() => alert("I am StateFulReactButton")} />
<StatelessReactButton handleOnclick={() => alert("I am StatelessReactButton")} />
複製代碼
打開你的本地app server (http://localhost:3000),看咱們的button已經完美的工做了:
五. lerna初始化,包管理
前端工程開發到必定階段之後你會發現大量的重複,這是全部開發人員須要面對的問題,組件複用提供了很好的解決思路,消除內部重複的同時還能解決跨團隊重複的問題。繼續以StateFulReactButton
和StatelessReactButton
爲例,咱們來把它們拆成兩個獨立的包,使用lerna管理起來。
安裝lerna:
npm install --global lerna
複製代碼
初始化lerna:
cd my-app #(the app above)
lerna init
複製代碼
lerna 會幫你初始化git作版本管理,此時你的工程目錄應該是這個樣子:
my-app
├── .storybook
│ └── addons.js
│ └── config.js
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│ └── favicon.ico
│ └── index.html
│ └── manifest.json
└── src
├── stories
│ └── index.js
└── App.css
└── App.js
└── App.test.js
└── index.css
└── index.js
└── logo.svg
└── registerServiceWorker.js
└── StateFulReactButton.js
└── StatelessReactButton.js
└── packages #(lerna包管理目錄,在這裏定義並測試你的組件)
└── lerna.json #(lerna配置文件)
複製代碼
packages
目錄裏新建StateFulReactButton/src
, StatelessReactButton/src
目錄,咱們把StateFulReactButton.js
和StatelessReactButton.js
分別遷移過來,再分別在兩個src
目錄下新建本身的index.js
文件,像這樣:
#StatefullReactButton/src/index.js
import StateFullReactButton from './StatefullReactButton';
export default StateFullReactButton;
複製代碼
#StatelessReactButton/src/index.js
import StatelessReactButton from './StatelessReactButton';
export default StatelessReactButton;
複製代碼
多包一層便於後面打包自動化配置;
在各自的根目錄下分別初始化npm包:
cd packages/StateFulReactButton
npm init
複製代碼
cd packages/StatelessReactButton
npm init
複製代碼
初始化過程npm會詢問並初始話一些配置給你,這裏注意entry point
,咱們的兩個組件是基於react和ES6語法寫的,須要打包工具幫咱們打包成通用的js纔可以使用,這裏暫時用默認配置,後面咱們打好包後會來手動修改這個配置。
注意: 這個時候咱們要從新組織一下storybook了,新建StateFulReactButton/src/stories
和StatelessReactButton/src/stories
目錄,各自新建index.js文件(一樣你須要從新修改一下你根目錄src/stories下的storybook):
#StateFulReactButton/src/stories/index.js
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import StatefullReactButton from '../StatefullReactButton';
storiesOf('Stateful Button', module)
.add('stateful react Button', () => <StatefullReactButton handleOnclick={action('clicked')}/>);
複製代碼
#StatelessReactButton/src/stories/index.js
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import StatelessReactButton from '../StatelessReactButton';
storiesOf('Stateless Button', module)
.add('stateless react Button', () => <StatelessReactButton handleOnclick={action('clicked')}/>);
複製代碼
修改.storybook/config.js
storybook配置文件:
import { configure } from '@storybook/react';
const req = require.context('../packages/', true, /stories\/.+.js$/);
const loadStories = () => {
require('../src/stories'); #(加載根目錄下的storybook)
req.keys().forEach(module => req(module)); #(加載各個組件目錄下的storybook)
};
configure(loadStories, module);
複製代碼
用瀏覽器打開你的storybook server,看看是否工做正常;
說到打包工具,webpack
和rollup
不得不提,在構建複雜的前端應用時,他們幫助咱們拆分代碼,管理靜態資源,是前端工程化必備的工具,二者類似又有不一樣,在什麼場景下如何使用你們能夠參考下這篇文章,一言以蔽之,對於應用開發,使用 webpack;對於類庫開發,使用 Rollup。
咱們分離出的兩個button組件,更像是類庫,這裏咱們選擇rollup,如何使用rollup打包具體細節咱們不詳細說了你們能夠自行搜索。這裏提供幾個配置文件,說明如何把rollup打包引入到咱們的工程中來;
首先安裝rollup:yarn add rollup
;
還有一些打包須要用到的插件(有些可能在你的工程裏用不到):
yarn add rollup-plugin-babel
yarn add rollup-plugin-node-resolve
yarn add rollup-plugin-filesize
yarn add rollup-plugin-sass
yarn add rollup-plugin-react-svg
複製代碼
根目錄下新建文件rollup.config.js
, 加入下列代碼:
import babel from 'rollup-plugin-babel';
import resolve from 'rollup-plugin-node-resolve';
import filesize from 'rollup-plugin-filesize';
import sass from 'rollup-plugin-sass';
import svg from 'rollup-plugin-react-svg';
import { writeFileSync } from 'fs';
import path from 'path';
const external = ['react', 'prop-types'];
const outputTypes = [
{ file: './dist/es/index.js', format: 'es' }, #(ES Modules)
];
const tasks = outputTypes.map(output => ({
input: './src/index.js', #(組件主入口,相對路徑)
external,
output,
name: 'my-library',
plugins: [
resolve(),
filesize(),
sass({
output: styles => writeFileSync(path.resolve('./dist', 'index.css'), styles),
options: {
importer(url) {
return url.startsWith('~') && ({
file: `${process.cwd()}/node_modules/${url.slice(1)}`
})
}
}
}),
babel({
exclude: 'node_modules/**',
plugins: ['external-helpers'], #(你須要安裝babel插件來解析ES6)
}),
svg()
],
}));
export default tasks;
複製代碼
而後安裝babel插件來解析ES6(有些可能在你的工程裏用不到):
yarn add babel-core
yarn add babel-cli
yarn add babel-loader
yarn add babel-plugin-external-helpers
yarn add babel-plugin-transform-object-rest-spread
yarn add babel-preset-env
yarn add babel-preset-react
複製代碼
根目錄下新建.babelrc
babel配置文件, 寫入:
{
"presets": [
[
"env",
{ "modules": false }
],
"react"
],
"env": {
"test": {
"presets": [["env"], "react"]
}
},
"plugins": [
"transform-object-rest-spread"
]
}
複製代碼
接下來咱們回頭修改前面提到的兩個包的package.json
配置文件:
StatefulReactButton/package.json
{
"name": "statefull-react-button",
"version": "1.0.0", #(組件版本)
"description": "this is my StatefullReactButton",
"main": "dist/es/index.js", #(打包後組件主函數入口)
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"build": "rollup -c ../../rollup.config.js" #(組件打包,這裏使用同一個rollup.config.js,此處爲相對路徑)
},
"dependencies": {
"classnames": "^2.2.5" #(另外單獨給每一個組件添加本身的依賴庫,以作比較)
},
"publishConfig": {
"access": "public" #(組件庫發佈地址,默認爲你的npm帳戶倉庫)
}
}
複製代碼
StatelessReactButton/package.json
{
"name": "stateless-react-button",
"version": "1.0.0", #(組件版本)
"description": "this is my StatelessReactButton",
"main": "dist/es/index.js", #(打包後組件主函數入口)
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rollup -c ../../rollup.config.js" #(組件打包,這裏使用同一個rollup.config.js,此處爲相對路徑)
},
"dependencies": {
"lodash": "^4.4.0" #(另外單獨給每一個組件添加本身的依賴庫,以作比較)
},
"publishConfig": {
"access": "public" #(組件庫發佈地址,默認爲你的npm帳戶倉庫)
}
}
複製代碼
至此,咱們的工程化就基本完成了,執行下面命令:
lerna bootstrap #(安裝各個組件的包依賴)
lerna run build #(使用lerna和rollup爲各個組件打包)
複製代碼
你會在你的兩個組件根目錄裏看到dist
文件夾,裏面有打包好的可用於發佈的index.js
文件。
你的工程目錄應該是這個樣子:
my-app
├── .storybook
│ └── addons.js
│ └── config.js
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│ └── favicon.ico
│ └── index.html
│ └── manifest.json
└── src
├── stories
│ └── index.js
└── App.css
└── App.js
└── App.test.js
└── index.css
└── index.js
└── logo.svg
└── registerServiceWorker.js
└── StatelessReactButton.js
└── packages #(lerna包管理目錄,在這裏定義並測試你的組件)
├── StatefulReactButton
├── node_modules
├── dist
└── es
└── index.js
└── src
└── stories
└── index.js
├── index.js
└── StatefulReactButton.js
└── StatelessReactButton
├── node_modules
├── dist
└── es
└── index.js
└── src
└── stories
└── index.js
├── index.js
└── StatelessReactButton.js
└── lerna.json #(lerna配置文件)
└── .babelrc
└── rollup.config.js
└── yarn.lock
複製代碼
一條命令,你的包就上線啦:
lerna publish
複製代碼
打開你的npm帳戶倉庫,看到你剛剛發佈的組件了吧, 接下來你就能夠像安裝其餘前端庫同樣使用你本身的組件了~~~
yarn add statefull-react-button
yarn add stateless-react-button
複製代碼
【文章的代碼和命令較多,但願有興趣的朋友耐心看完,若有不清楚的地方歡迎留言交流; 另外storybook和lerna都支持豐富的cli命令,功能強大,詳見各自的官方文檔; 本文未說起測試,css,圖片等靜態資源的處理,還請讀者本身添加】