React + Storybook + Lerna 構建自己的前端UI元件庫

IOException發表於2018-02-19

前言

本文意在幫助讀者快速搭建自己的前端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互動介面:

React + Storybook + Lerna 構建自己的前端UI元件庫

三. 開發自己的元件

接下來讓我們來開發自己的兩個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 + Lerna 構建自己的前端UI元件庫

好啦,至此我們的兩個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已經完美的工作了:

React + Storybook + Lerna 構建自己的前端UI元件庫

五. lerna初始化,包管理

前端工程開發到一定階段以後你會發現大量的重複,這是所有開發人員需要面對的問題,元件複用提供了很好的解決思路,消除內部重複的同時還能解決跨團隊重複的問題。繼續以StateFulReactButtonStatelessReactButton為例,我們來把它們拆成兩個獨立的包,使用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.jsStatelessReactButton.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/storiesStatelessReactButton/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.jsstorybook配置檔案:

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,看看是否工作正常;

打包

說到打包工具,webpackrollup不得不提,在構建複雜的前端應用時,他們幫助我們拆分程式碼,管理靜態資源,是前端工程化必備的工具,兩者相似又有不同,在什麼場景下如何使用大家可以參考下這篇文章一言以蔽之,對於應用開發,使用 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
複製程式碼

React + Storybook + Lerna 構建自己的前端UI元件庫
React + Storybook + Lerna 構建自己的前端UI元件庫
React + Storybook + Lerna 構建自己的前端UI元件庫

【文章的程式碼和命令較多,希望有興趣的朋友耐心看完,如有不清楚的地方歡迎留言交流; 另外storybook和lerna都支援豐富的cli命令,功能強大,詳見各自的官方文件; 本文未提及測試,css,圖片等靜態資源的處理,還請讀者自己新增】

相關文章