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 ↩︎

相关文章
相关标签/搜索