2020 Create React App 開始一個UI組件庫

引子

是什麼驅使我準備用Create React App[1] (後文簡稱CRA)來開發一套UI Component Library呢?由於團隊選用了Vue做爲基礎技術棧,以前習慣了官方開箱即用的Vue-CLI很是便捷便可配置完成構建組件庫所需的生產環境,好比這套咱們內部使用的wooui-pro,基於CLI約定配置後便迅速產出了符合團隊標準的組件。那麼使用React官方提供的CRA,咱們是否也能快速打造出標準化的組件庫呢?帶着疑問開始了探索之旅。css

目標

以前總結過一個使用Vue技術棧的環境配置指南,你們感興趣能夠戳👉這裏html

咱們核心目標意在配置一個類Vue-CLI體驗的基於CRA的React UI Component Library。vue

需求

既然設定了目標,咱們應該明確一下咱們完成這個目標的需求點 (是的,人人都是產品經理,🐶保命)node

  • CRA做爲基礎腳手架且不eject
  • 使用CSS Modules管理CSS類名
  • 可配置postcss預編譯插件
  • 配置代碼校驗工具保證代碼標準化
  • 迅速生成組件示例以及文檔
  • 能夠Build出一個library包用於發佈

基於這些需求,咱們將逐個解決完成這些需求所遇到的問題。react

開始

CRA項目初始化

首先要作的就是使用CRA建立項目,一行代碼就完成了項目初始化webpack

npx create-react-app my-app
複製代碼

項目文件結構以下,那是至關簡潔,甚至都懷疑進錯了目錄...git

my-app
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
└── src
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── index.css
    ├── index.js
    ├── logo.svg
    └── serviceWorker.js
複製代碼

Create React App 顧名思義建立一個React應用,徹底標準化的腳手架。github

因而,試着引入CSS Modules,按照文檔web

Button.module.css

.error {
  background-color: red;
}
複製代碼

Button.js

import React, { Component } from 'react';
import styles from './Button.module.css'; // Import css modules
class Button extends Component {
  render() {
    // reference as a js object
    return <button className={styles.error}>Error Button</button>;
  }
}
複製代碼

結果

<button class="Button_error_ax7yz">Error Button</button>
複製代碼

Button_error_ax7yz 黑人問號.jpg! 不能忍受一個組件庫CSS類名帶着md5。找了半天文檔發現根本沒有給你改CSS Modules命名規則的地方啊。那麼要是想改這個規則的話怎麼辦呢?瞭解的人可能知道CSS Modules是css-loader提供支持的,那麼如今須要不eject CRA,還要把css-loader的配置項修改了,有招嗎?npm

React App Rewired配置Webpack

本着能用現成的就別本身動手的宗旨🤦,Google到了React App Rewired這個神器,並且還有中文的說明:

此工具能夠在不 'eject' 也不建立額外 react-scripts 的狀況下修改 create-react-app 內置的 webpack 配置,而後你將擁有 create-react-app 的一切特性,且能夠根據你的須要去配置 webpack 的 plugins, loaders 等。

這正是咱們所須要的,依賴它們就能夠修改css-loader配置了。

安裝react-app-rewired

yarn add react-app-rewired --dev
複製代碼

在項目根目錄中建立一個 config-overrides.js 文件

/* config-overrides.js */
module.exports = {
    webpack: function(config, env) {
        // 這裏修改config
        // react-app-rewired攔截後修改配置,而後按照配置進行腳本構建
        return config;
    }
}
複製代碼

修改package.json中的腳本指令

/* package.json */

  "scripts": {
-   "start": "react-scripts start",
+   "start": "react-app-rewired start",
  }
複製代碼

修改css-loader配置

查找react-app-rewired文檔,發現修改CSS Modules有對應的loader:

不過發現這兩個loader擴展貌似都不太適合如今版本的CRA了(現版本CRA已經支持CSS Modules,個人訴求是修改配置)。

不過咱們能夠借鑑代碼,借鑑代碼的同時咱們還能夠看看咱們劫持的react-scripts的webpack配置究竟是怎樣的,文件就在node_modules/react-scripts/config/webpack.config.js

  1. 項目根目錄新建個scripts目錄存放修改CSS Modules的腳本cssModuleConfig.js,直接貼出源碼:
/* scripts/cssModuleConfig.js */
const path = require('path');
const ruleChildren = loader =>
  loader.use || loader.oneOf || (Array.isArray(loader.loader) && loader.loader) || [];
const findIndexAndRules = (rulesSource, ruleMatcher) => {
  let result = undefined;
  const rules = Array.isArray(rulesSource) ? rulesSource : ruleChildren(rulesSource);
  rules.some(
    (rule, index) =>
      (result = ruleMatcher(rule)
        ? { index, rules }
        : findIndexAndRules(ruleChildren(rule), ruleMatcher))
  );
  return result;
};
const findRule = (rulesSource, ruleMatcher) => {
  const { index, rules } = findIndexAndRules(rulesSource, ruleMatcher);
  return rules[index];
};
const cssRuleMatcher = rule =>
  rule.test && String(rule.test) === String(/\.module\.css$/);
const sassRuleMatcher = rule =>
  rule.test && String(rule.test) === String(/\.module\.(scss|sass)$/);

const createLoaderMatcher = loader => rule =>
  rule.loader && rule.loader.indexOf(`${path.sep}${loader}${path.sep}`) !== -1;
const cssLoaderMatcher = createLoaderMatcher('css-loader');
const sassLoaderMatcher = createLoaderMatcher('sass-loader');

module.exports = function(config, env, options) {
  const cssRule = findRule(config.module.rules, cssRuleMatcher);
  let cssModulesRuleCssLoader = findRule(cssRule, cssLoaderMatcher);
  const sassRule = findRule(config.module.rules, sassRuleMatcher);
  let sassModulesRuleCssLoader = findRule(sassRule, sassLoaderMatcher);
  cssModulesRuleCssLoader.options = { ...cssModulesRuleCssLoader.options, ...options };
  sassModulesRuleCssLoader.options = { ...sassModulesRuleCssLoader.options, ...options };
  return config;
};
複製代碼

這麼一坨代碼其實就是找到對應loader,而後修改裏面的options屬性。

  1. 在config-overrides.js中修改CSS Modules的配置:
/* config-overrides.js */
const cssModuleConfig = require('./scripts/cssModuleConfig');
const loaderUtils = require('loader-utils');

module.exports = {
  webpack: function(config, env) {
    // 配置className按照namespace-folderName-localName的形式輸出
    config = cssModuleConfig(config, env, {
      modules: {
        getLocalIdent: (context, localIdentName, localName, options) => {
          const folderName = loaderUtils.interpolateName(context, '[folder]', options);
          const className =
            process.env.LIB_NAMESPACE + '-' + folderName + '-' + localName;
          return className.toLowerCase();
        }
      }
    });
    return config;
  }
};
複製代碼

結果驗收

Button.module.css
.main {
    border: 1px solid;
}
複製代碼
Button.js
import styles from './Button.module.css'; // Import css modules
<button className={styles.main}>Button</button>
複製代碼
結果
<button class="woo-button-main">Button</button>
複製代碼

第一步麼表達成!接下來應該是要各個組件的構建之路了,組件衆多,既要逐個展現還要羅列說明,若是循序漸進完成,那要消耗很多精力。有沒有方法簡化這個流程呢?下面就要祭出又一神器:

React Styleguidist生成組件示例

🐙React Styleguidist能夠幫助咱們輕鬆解決屬性自動生成、組件狀態展現、文檔說明等等問題,讓咱們能把精力徹底放到組件開發上。

安裝react-styleguidist

yarn add react-styleguidist --dev
複製代碼

src目錄創建components目錄

...
└── src
    ├── components
        ├── Button
            ├── Button.module.css //CSS
            ├── index.js          //Button組件入口
            ├── Readme.md         //示例說明
...
複製代碼

修改package.json中的指令

/* package.json */

  "scripts": {
-   "start": "react-app-rewired start",
+   "start": "styleguidist server",
  }
複製代碼

🚀發射

命令行運行yarn start,靜待‘奇蹟’發生...

(運行結果基於Button組件已經寫了部分代碼

React Styleguidist Button Component

美如畫~ 不,等等,檢查元素的時候我剛配置的類名規則怎麼又變回來了?仔細想一想才發現Styleguidist加載的webpack配置是CRA提供的,那腫麼辦呢?咱們得想辦法讓Styleguidist調用Rewired來工做,這樣react-app-rewired start發生的一切纔會在styleguidist server上發生。能夠嗎?固然!

配置Styleguidist

經過新建styleguide.config.js文件,完成調用react-app-rewired配置

/* styleguide.config.js */
const { paths } = require('react-app-rewired');
const overrides = require('react-app-rewired/config-overrides');
const config = require(paths.scriptVersion + '/config/webpack.config');

module.exports = {
  webpackConfig: overrides.webpack(config(process.env.NODE_ENV), process.env.NODE_ENV)
};
複製代碼

🚀再次發射

命令行運行yarn start,CSS Modules配置生效,美滋滋。

配置postcss

這兩年一直在用postcss這個CSS預編譯工具。一方面postcss面向將來的CSS標準,二來插件隨用隨裝,比一次裝個node-sass快了不知道多少。配置postcss的文件能夠有N種方式,往常的往項目根目錄新建個postcss.config.jspostcss-loader讀取配置,按照插件順序完成編譯過程。因而配置個postcss-pxtorem

postcss.config.js

/* postcss.config.js */
module.exports = {
  plugins: {
    'postcss-pxtorem': {
      rootValue: 16,
      propWhiteList: [
        '*',
        '!border',
        '!border-top',
        '!border-right',
        '!border-bottom',
        '!border-left',
        '!border-width'
      ],
      selectorBlackList: ['html'],
      mediaQuery: false
    }
  }
};
複製代碼

Button.module.css

.main {
    font-size: 16px;
}
複製代碼

結果

.woo-button-main {
    font-size: 16px;
}
複製代碼

預期結果並有發生,原來CRA也並無postcss-loader選項,看來仍是須要藉助Rewired

Rewired Postcss

安裝react-app-rewire-postcss

react-app-rewire-postcss試了一下能夠正常使用,咱們根據文檔配置一下config-override.js

/* config-override.js */
...
const rewirePostcss = require('react-app-rewire-postcss');

module.exports = {
  webpack: function(config, env) {
    ...
    config = rewirePostcss(config, true);
    return config;
  }
};
複製代碼

Button.module.css

.main {
    font-size: 16px;
}
複製代碼

結果

.woo-button-main {
    font-size: 1rem;
}
複製代碼

Done! 下面能夠繼續開始愉快Coding了~,爲了讓編碼標準規範,須要藉助工具來約束。

規範代碼

代碼檢查藉助Prettier以及ESLint的擴展,eslint-config-prettier將關閉全部沒必要要的或可能與Prettier衝突的規則。eslint-plugin-prettier則是添加Prettier格式設置規則的插件。

安裝

yarn add prettier eslint-config-prettier eslint-plugin-prettier --dev
複製代碼

ESLint配置

新建.eslintrc文件

{
  "extends": ["react-app", "plugin:prettier/recommended"]
}
複製代碼

Prettier配置

新建.prettierrc文件

{
  "printWidth": 90,
  "singleQuote": true,
  "semi": true
}
複製代碼

配置git提交校驗

接下來配置HuskyLint Staged來確保每次提交代碼的正確性

yarn add husky lint-staged --dev
複製代碼

修改package.json

{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "src/**/*.{js,jsx,json,css,md}": [
      "prettier --write",
      "git add"
    ]
  }
}
複製代碼

回頭看看開始制定的目標,只剩下最終最關鍵一步,將UI Components構建成爲一個Library。

構建庫

CRA只提供了開發與構建應用的功能,並無構建Library的能力。這時候又要祭出React App Rewired這個利器,在文檔裏面找到的react-app-rewire-create-react-library讓人眼前一亮,惋惜並很差用,因此又不得不改造一個本身的代碼來構建組件庫。

配置環境變量

建立一個自定義的Library環境變量

  1. 首先安裝 env-cmd
yarn add env-cmd --dev
複製代碼
  1. 建立環境變量文件.env.library
REACT_APP_NODE_ENV = "library"
複製代碼
  1. 修改package.json
{
    "scripts": {
        "build:library": "rm -rf build && env-cmd -f .env.library react-app-rewired build"
    }
}
複製代碼
  1. 配置入口文件
/* src/index.js */
import Button from './components/Button';;
export { Button };
複製代碼
  1. package.json指定es module入口與main入口
{
    "module": "./src/index.js",
    "main": "./build/wooui-react.js"
}
複製代碼

構建腳本

構建庫配置核心思路是將生產環境構建所作的諸如code splitting、md5文件名、修改模板html這些步驟所有省略,而後配置好output屬性參數。

  1. 在scripts目錄存新建打包腳本reactLibraryConfig.js:
/* scripts/reactLibraryConfig.js */
module.exports = function(config, env, options) {
  // 當值爲library的時候,修改配置
  if (env === 'library') {
    const srcFile = process.env.npm_package_module || options.module;
    const libName = process.env.npm_package_name || options.name;
    config.entry = srcFile;
    // 構件庫信息
    config.output = {
      path: path.resolve('./', 'build'),
      filename: libName + '.js',
      library: libName,
      libraryTarget: 'umd'
    };
    // 修改webpack optimization屬性,刪除代碼分割邏輯
    delete config.optimization.splitChunks;
    delete config.optimization.runtimeChunk;
    // 清空plugin只保留構建CSS命名
    config.plugins = [];
    config.plugins.push(
      new MiniCssExtractPlugin({
        filename: libName + '.css'
      })
    );
    // 代碼來自 react-app-rewire-create-react-library
    // 生成externals屬性值,排除外部擴展,好比React
    let externals = {};
    Object.keys(process.env).forEach(key => {
      if (key.includes('npm_package_dependencies_')) {
        let pkgName = key.replace('npm_package_dependencies_', '');
        pkgName = pkgName.replace(/_/g, '-');
        // below if condition addresses scoped packages : eg: @storybook/react
        if (pkgName.startsWith('-')) {
          const scopeName = pkgName.substr(1, pkgName.indexOf('-', 1) - 1);
          const remainingPackageName = pkgName.substr(
            pkgName.indexOf('-', 1) + 1,
            pkgName.length
          );
          pkgName = `@${scopeName}/${remainingPackageName}`;
        }
        externals[pkgName] = `${pkgName}`;
      }
    });
    config.externals = externals;
  }
  return config;
};
複製代碼

調用構建腳本

下面又要請出React App Rewired,使用剛剛完成reactLibraryConfig,取到修改後的config屬性。最後目前完整的代碼以下

/* config-overrides.js */

const cssModuleConfig = require('./scripts/cssModuleConfig');
const loaderUtils = require('loader-utils');
const reactLibraryConfig = require('./scripts/reactLibraryConfig');
const rewirePostcss = require('react-app-rewire-postcss');

module.exports = {
  webpack: function(config, env) {
    // 配置CSS Modules
    config = cssModuleConfig(config, env, {
      modules: {
        getLocalIdent: (context, localIdentName, localName, options) => {
          const folderName = loaderUtils.interpolateName(context, '[folder]', options);
          const className =
            process.env.LIB_NAMESPACE + '-' + folderName + '-' + localName;
          return className.toLowerCase();
        }
      }
    });
    // 配置Postcss
    config = rewirePostcss(config, true);
    // 配置構建信息
    // 當執行 yarn build:library時 process.env.REACT_APP_NODE_ENV值爲library
    config = reactLibraryConfig(config, process.env.REACT_APP_NODE_ENV);
    // 傳給 react-app-rewired 的最終配置清單
    return config;
  }
};
複製代碼

清理public目錄

CRA在生產構建時會將public目錄內容所有拷貝到build目錄,因此這個文件夾只保留index.html就能夠了。

🛰️👨‍🚀 順利着陸

yarn build:library
複製代碼
Creating an optimized production build...
Compiled successfully.

File sizes after gzip:

  2.83 KB  build/wooui-react.js
  684 B    build/wooui-react.css
複製代碼

build文件目錄,看到兩位小夥伴在向咱們招手~

總結

終於,按照既定目標,實現了動手前所提出的全部需求。由一個是可否按照Vue-CLI的構建流程快速搭建一個基於React的UI組件庫的想法。按照起初的需求,一步步的挖掘解決方案,遇到問題困難,明確本身要處理的核心問題,理清解決思路,找到解決方案,而後再進一步的豐滿需求,這樣最終實現了不eject CRA構建UI Component目標。

再仔細想一想,是否是還有不少東西能夠優化呢?好比單個組件文件的建立、整個入口文件的生成、單個組件的構建等等

這個問題如此,生活工做學習其餘許多,未嘗不是如此?

好了, 謝謝觀看,咱們下次見。

哦,對了,項目源碼放在這裏:

wooui-react


  1. 以後簡稱CRA ↩︎

相關文章
相關標籤/搜索