是什么驱使我准备用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 ↩︎