引子
是什么驱使我准备用Create React App[1] (后文简称CRA)来开发一套UI Component Library呢?因为团队选用了Vue作为基础技术栈,之前习惯了官方开箱即用的Vue-CLI非常便捷即可配置完成构建组件库所需的生产环境,比如这套我们内部使用的wooui-pro,基于CLI约定配置后便迅速产出了符合团队标准的组件。那么使用React官方提供的CRA,我们是否也能快速打造出标准化的组件库呢?带着疑问开始了探索之旅。
目标
之前总结过一个使用Vue技术栈的环境配置指南,大家感兴趣可以戳👉这里
我们核心目标意在配置一个类Vue-CLI体验的基于CRA的React UI Component Library。
需求
既然设定了目标,我们应该明确一下我们完成这个目标的需求点 (是的,人人都是产品经理,🐶保命)
- CRA作为基础脚手架且不eject
- 使用CSS Modules管理CSS类名
- 可配置postcss预编译插件
- 配置代码校验工具保证代码标准化
- 迅速生成组件示例以及文档
- 可以Build出一个library包用于发布
基于这些需求,我们将逐个解决完成这些需求所遇到的问题。
开始
CRA项目初始化
首先要做的就是使用CRA创建项目,一行代码就完成了项目初始化
npx create-react-app my-app
项目文件结构如下,那是相当简洁,甚至都怀疑进错了目录...
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应用,完全标准化的脚手架。
于是,试着引入CSS Modules,按照文档
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的配置项修改了,有招吗?
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
。
- 项目根目录新建个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属性。
- 在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组件已经写了部分代码)
美如画~ 不,等等,检查元素的时候我刚配置的类名规则怎么又变回来了?仔细想想才发现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.js
,postcss-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提交校验
接下来配置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环境变量
- 首先安装 env-cmd
yarn add env-cmd --dev
- 创建环境变量文件.env.library
REACT_APP_NODE_ENV = "library"
- 修改package.json
{
"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 };
- package.json指定es module入口与main入口
{
"module": "./src/index.js",
"main": "./build/wooui-react.js"
}
构建脚本
构建库配置核心思路是将生产环境构建所做的诸如code splitting、md5文件名、修改模板html这些步骤全部省略,然后配置好output属性参数。
- 在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目标。
再仔细想想,是不是还有很多东西可以优化呢?比如单个组件文件的创建、整个入口文件的生成、单个组件的构建等等
这个问题如此,生活工作学习其他许多,何尝不是如此?
好了, 谢谢观看,我们下次见。
哦,对了,项目源码放在这里:
之后简称CRA ↩︎