是什麼驅使我準備用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
基於這些需求,咱們將逐個解決完成這些需求所遇到的問題。react
首先要作的就是使用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
.error {
background-color: red;
}
複製代碼
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
本着能用現成的就別本身動手的宗旨🤦,Google到了React App Rewired這個神器,並且還有中文的說明:
此工具能夠在不 'eject' 也不建立額外 react-scripts 的狀況下修改 create-react-app 內置的 webpack 配置,而後你將擁有 create-react-app 的一切特性,且能夠根據你的須要去配置 webpack 的 plugins, loaders 等。
這正是咱們所須要的,依賴它們就能夠修改css-loader配置了。
yarn add react-app-rewired --dev
複製代碼
/* config-overrides.js */
module.exports = {
webpack: function(config, env) {
// 這裏修改config
// react-app-rewired攔截後修改配置,而後按照配置進行腳本構建
return config;
}
}
複製代碼
/* package.json */
"scripts": {
- "start": "react-scripts start",
+ "start": "react-app-rewired start",
}
複製代碼
查找react-app-rewired文檔,發現修改CSS Modules有對應的loader:
不過發現這兩個loader擴展貌似都不太適合如今版本的CRA了(現版本CRA已經支持CSS Modules,個人訴求是修改配置)。
不過咱們能夠借鑑代碼,借鑑代碼的同時咱們還能夠看看咱們劫持的react-scripts的webpack配置究竟是怎樣的,文件就在node_modules/react-scripts/config/webpack.config.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屬性。
/* 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;
}
};
複製代碼
.main {
border: 1px solid;
}
複製代碼
import styles from './Button.module.css'; // Import css modules
<button className={styles.main}>Button</button>
複製代碼
<button class="woo-button-main">Button</button>
複製代碼
第一步麼表達成!接下來應該是要各個組件的構建之路了,組件衆多,既要逐個展現還要羅列說明,若是循序漸進完成,那要消耗很多精力。有沒有方法簡化這個流程呢?下面就要祭出又一神器:
🐙React Styleguidist能夠幫助咱們輕鬆解決屬性自動生成、組件狀態展現、文檔說明等等問題,讓咱們能把精力徹底放到組件開發上。
yarn add react-styleguidist --dev
複製代碼
...
└── src
├── components
├── Button
├── Button.module.css //CSS
├── index.js //Button組件入口
├── Readme.md //示例說明
...
複製代碼
/* package.json */
"scripts": {
- "start": "react-app-rewired start",
+ "start": "styleguidist server",
}
複製代碼
命令行運行yarn start
,靜待‘奇蹟’發生...
(運行結果基於Button組件已經寫了部分代碼)
美如畫~ 不,等等,檢查元素的時候我剛配置的類名規則怎麼又變回來了?仔細想一想才發現Styleguidist加載的webpack配置是CRA提供的,那腫麼辦呢?咱們得想辦法讓Styleguidist調用Rewired來工做,這樣react-app-rewired start
發生的一切纔會在styleguidist server
上發生。能夠嗎?固然!
經過新建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這個CSS預編譯工具。一方面postcss面向將來的CSS標準,二來插件隨用隨裝,比一次裝個node-sass快了不知道多少。配置postcss的文件能夠有N種方式,往常的往項目根目錄新建個postcss.config.js
,postcss-loader讀取配置,按照插件順序完成編譯過程。因而配置個postcss-pxtorem。
/* 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
}
}
};
複製代碼
.main {
font-size: 16px;
}
複製代碼
.woo-button-main {
font-size: 16px;
}
複製代碼
預期結果並有發生,原來CRA也並無postcss-loader選項,看來仍是須要藉助Rewired
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;
}
};
複製代碼
.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
複製代碼
新建.eslintrc文件
{
"extends": ["react-app", "plugin:prettier/recommended"]
}
複製代碼
新建.prettierrc文件
{
"printWidth": 90,
"singleQuote": true,
"semi": true
}
複製代碼
接下來配置Husky 與 Lint 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環境變量
yarn add env-cmd --dev
複製代碼
REACT_APP_NODE_ENV = "library"
複製代碼
{
"scripts": {
"build:library": "rm -rf build && env-cmd -f .env.library react-app-rewired build"
}
}
複製代碼
/* src/index.js */
import Button from './components/Button';;
export { Button };
複製代碼
{
"module": "./src/index.js",
"main": "./build/wooui-react.js"
}
複製代碼
構建庫配置核心思路是將生產環境構建所作的諸如code splitting、md5文件名、修改模板html這些步驟所有省略,而後配置好output屬性參數。
/* 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;
}
};
複製代碼
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目標。
再仔細想一想,是否是還有不少東西能夠優化呢?好比單個組件文件的建立、整個入口文件的生成、單個組件的構建等等
這個問題如此,生活工做學習其餘許多,未嘗不是如此?
好了, 謝謝觀看,咱們下次見。
哦,對了,項目源碼放在這裏:
以後簡稱CRA ↩︎