在 create-react-app 项目中添加 less 支持的过程与思考

665 阅读6分钟

概述

这个问题在网上有许多解答,但都不外乎两种:一种方式是使用 craco ,另一种就是通过 npm run eject 的方式弹出 webpack 的配置信息,在其中进行修改。

本文主要描述后者,以及我在配置过程中的思考与收获。

具体做法

安装 less 支持

安装 lessless-loader ,以支持 webpack 中对 *.less 文件的解析。

npm install less less-loader

弹出配置信息

使用 npm run eject 弹出项目的默认配置信息。

注意该操作为不可逆操作,且弹出配置信息前不允许有 git 未提交或暂存的文件

修改配置文件

首先找到 /config/webpack.config.js ,其使用 module.exports 导出一个函数,该函数接收一个字符串 developmentproduction 表示当前环境为开发环境或生产环境。

(......)
// This is the production and development configuration.
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
module.exports = function (webpackEnv) {
  const isEnvDevelopment = webpackEnv === 'development';
  const isEnvProduction = webpackEnv === 'production';
(......)

该函数将返回一个配置对象,用于 webpack 的配置。找到该配置对象的 module.rules.oneOf 部分。这里需要解释一下,该配置对象中的 oneOf 数组,其官方文档中解释为:

An array of Rules from which only the first matching Rule is used when the Rule matches.

即不需要对一种文件,把所有的 loader 都尝试匹配,只需要匹配第一个符合条件的 loader 即可。提高了 loader 的匹配效率。

module.rules.oneOf 中,找到关于 sass 的 loader 配置信息(有两个,第一个是匹配 *.sass*.scss 文件的,一个是匹配 *.module.sass*.module.scss 文件的):

// Opt-in support for SASS (using .scss or .sass extensions).
// By default we support SASS Modules with the
// extensions .module.scss or .module.sass
{
  test: sassRegex,
  exclude: sassModuleRegex,
  use: getStyleLoaders(
    {
      importLoaders: 3,
      sourceMap: isEnvProduction
        ? shouldUseSourceMap
        : isEnvDevelopment,
      modules: {
        mode: 'icss',
      },
    },
    'sass-loader'
  ),
  // Don't consider CSS imports dead code even if the
  // containing package claims to have no side effects.
  // Remove this when webpack adds a warning or an error for this.
  // See https://github.com/webpack/webpack/issues/6571
  sideEffects: true,
},
// Adds support for CSS Modules, but using SASS
// using the extension .module.scss or .module.sass
{
  test: sassModuleRegex,
  use: getStyleLoaders(
    {
      importLoaders: 3,
      sourceMap: isEnvProduction
        ? shouldUseSourceMap
        : isEnvDevelopment,
      modules: {
        mode: 'local',
        getLocalIdent: getCSSModuleLocalIdent,
      },
    },
    'sass-loader'
  ),
},

在这段 Rules 下,添加我们关于 *.less 文件的 loader 配置:

{
  test: lessRegex,
  use: getStyleLoaders(
    {
      importLoaders: 3,
      sourceMap: isEnvProduction
        ? shouldUseSourceMap
        : isEnvDevelopment,
      modules: {
        getLocalIdent: getCSSModuleLocalIdent,
      },
    },
    'less-loader'
  ),
},

其中 lessRegex 需要被事先定义,其为匹配 *.less 文件名的正则表达式:

const lessRegex = /\.less$/;

说明

在添加的这段配置信息中, test 属性负责检查文件名是否符合 lessRegex ,即我们所定义的 /\.less$/,若匹配该正则表达式则使用 use 属性(其为一个数组)中定义的 loader 对该文件进行处理。在这里我们并没有直接定义其为

[
  { loader: 'style-loader' },
  { loader: 'css-loader' },
  { loader: 'postcss-loader' },
  { loader: 'resolve-url-loader' },
  { loader: 'less-loader' }
]

而是使用了 getStyleLoaders 这一函数。该函数接收两个参数,第一个参数是提交给 css-loaderoptions 对象(详见下文),第二个参数是我们希望使用的 loader。

该函数负责生成一个 loader 配置的数组交给配置对象中的 use 属性使用。在生产环境中,其第一个 loader 为 MiniCssExtractPlugin.loader ,在开发环境中则为 style-loader 。数组中其余的 loader 依次为 css-loaderpostcss-loaderresolve-url-loader ,最后才是我们在第二个参数中传入的 loader。因此我们可以通过向函数的第二个参数传入 'less-loader' ,指定我们需要的最后一个 loader 为 less-loader

关于这些 loader ,前两个不必赘述, postcss-loader 可以负责处理浏览器前缀,压缩 CSS 等, resolve-url-loader 则可以解决 CSS 中,因为使用 @import 造成的资源 url 链接错误问题。

将目光放回我们刚刚添加的配置信息,我们对 use 属性传入了这样的内容:

getStyleLoaders(
  {
    importLoaders: 3,
    sourceMap: isEnvProduction
      ? shouldUseSourceMap
      : isEnvDevelopment,
    modules: {
      getLocalIdent: getCSSModuleLocalIdent,
    },
  },
  'less-loader'
),

getStyleLoaders 的第二个参数 'less-loader' 的含义我们已经了然。对于第一个参数,我们该如何理解呢?通过查找该函数的源码(也在同一个文件,即 /config//webpack.config.js )下,可以看见:

// common function to get style loaders
const getStyleLoaders = (cssOptions, preProcessor) => {
  const loaders = [
    //......
    {
      loader: require.resolve('css-loader'),
      options: cssOptions,
    },
    //......

由此可知,第一个参数会被当做 css-loaderoptions 对象。而关于该对象,我们在其中配置的三个属性( importLoaderssourceMapmodules ),皆可以在官方文档中查得其含义.

对于 importLoaders

The option importLoaders allows you to configure how many loaders before css-loader should be applied to @imported resources and CSS modules/ICSS imports.

importLoaders 选项允许你配置在 css-loader 之前有多少 loader 应用于 @import 的资源与 CSS 模块/ICSS 导入。在这里我们配置为 3 ,因为在 css-loader 之前有 3 个 loader 需要被应用——在 getStyleLoaders 函数中添加的 postcss-loaderresolve-url-loader ,以及我们传入的 less-loader

对于 sourcemap

Default: depends on the compiler.devtool value

By default generation of source maps depends on the devtool option. All values enable source map generation except eval and false value.

即该选项控制是否启用 sourse map 。

对于 modules

Default: undefined

Allows to enable/disable CSS Modules or ICSS and setup configuration:

  • undefined - enable CSS modules for all files matching /.module.\w+$/i.test(filename) and /.icss.\w+$/i.test(filename) regexp.
  • true - enable CSS modules for all files.
  • false - disables CSS Modules for all files.
  • string - disables CSS Modules for all files and set the mode option, more information you can read here
  • object - enable CSS modules for all files, if modules.auto option is not specified, otherwise the modules.auto option will determine whether if it is CSS modules or not, more information you can read here

而我们刚刚设置的 modules{ getLocalIdent: getCSSModuleLocalIdent } ,继续查看官方文档,可查得:

getLocalIdent

Type:

type getLocalIdent = (
  context: LoaderContext,
  localIdentName: string,
  localName: string
) => string;

Default: undefined

Allows to specify a function to generate the classname. By default we use built-in function to generate a classname. If the custom function returns null or undefined, we fallback to the built-in function to generate the classname.

getLocalIdent 指定一个自定义函数,该函数用以生成独一无二的类名,以此实现 CSS 的模块化。这里我们传入了 getCSSModuleLocalIdent 。其声明于同一文件(还是 /config/webpack.config.js )下:

const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');

其定义位于 react-dev-utils/getCSSModuleLocalIdent 下,该函数生成类名的规则为:

  1. 若文件名符合 /index\.module\.(css|scss|sass)$/ ,则取其所在文件夹名字,否则取其文件名字,定义为 fileNameOrFolder ;(由于 React 尚未提供对 less 的支持,所以该函数此处的正则表达式仅考虑了 CSS 与 SASS 、 SCSS 三种后缀,即对于 *.less 文件,此处 fileNameOrFolder 永远为文件名)
  2. 根据文件路径和原本类名( className )生成一个 5 位的哈希值 hash
  3. 生成形如 ${fileNameOrFolder}_${className}__${hash}的独一无二的类名,生成过程中将所有 .module 字段去除,并将所有 . 替换为 _

举例来说,位于 Mycomponent.module.less 下的 MyClass 类生成的类名即为 MyComponent_MyClass_[hash] , 而位于 MyFolder/MyComponent.module.css 下的 MyClass 类则会生成 MyFolder_MyClass__[hash] 的类名。

因此,将目光放回刚刚的 modules 属性,我们将其指定为 { getLocalIdent: getCSSModuleLocalIdent } ,即意在使用 React 提供的 CSS 模块的类名生成工具函数,来生成 less 文件中的类名。同时,我们没有指定 modules.auto 属性,因此 css-loader 会对其所有应用的文件启用 CSS 模块特性。

总结

本文主要描述了对 create-react-app 项目进行修改,对其添加 less 支持的一种方式。

该方式首先使用 npm run eject 弹出配置信息后,更改 /config/webpack.config.js 文件,在其导出函数的返回对象中的 module.rules.oneOf 属性中添加一个 Rule 对象,以配置对 *.less 文件的解析以及需要对其应用的 loader 。