前言
本文意在幫助讀者快速搭建自己的前端UI元件庫,構建-打包-釋出,幫你解決大型web前端應用中元件重用的問題.
React
自2014年以來,react不斷地發展壯大,時至今日已經發展成為最受歡迎的前端框架,如果你還不太瞭解react,請看這裡。
Storybook
storybook是一套UI元件的開發環境,它允許你瀏覽元件庫,檢視每個元件的不同狀態,以及互動式開發和測試元件。 storybook允許你獨立於你的app來開發你的UI元件,你可以先不關心應用層級的元件依賴,快速的著手元件的開發,而後再將之應用於自己的app中。尤其在大型應用,跨團隊合作過程中,良好的元件抽象,使用storybook封裝管理,可以極大的提高的元件的重用性,可測試性,和開發速度。你可以點選這裡檢視storybook是如何工作的。
Lerna
lerna幫你管理你的包集合,當你自己的library變多時,你的版本控制,跟蹤管理,測試就會變得越發複雜,lerna正是幫你解決這個問題,它使用npm和git來幫助你優化你的多包管理流程。
本文假設你已經熟悉釋出自己的npm包,如果不熟悉,可以先檢視相關文章,例如《怎麼開發一個npm包》;
接下來我們就一步一步來搭建自己的UI元件庫。
構建
一. 初始化react app;
有很多教程幫助我們如何搭建一個前端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,圖片等靜態資源的處理,還請讀者自己新增】