React + Storybook + Lerna 構建本身的前端UI組件庫

前言

本文意在幫助讀者快速搭建本身的前端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初始化,包管理

前端工程開發到必定階段之後你會發現大量的重複,這是全部開發人員須要面對的問題,組件複用提供了很好的解決思路,消除內部重複的同時還能解決跨團隊重複的問題。繼續以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
複製代碼

【文章的代碼和命令較多,但願有興趣的朋友耐心看完,若有不清楚的地方歡迎留言交流; 另外storybook和lerna都支持豐富的cli命令,功能強大,詳見各自的官方文檔; 本文未說起測試,css,圖片等靜態資源的處理,還請讀者本身添加】

相關文章
相關標籤/搜索