CRA 很方便但不够灵活

lxf2023-05-04 00:25:01

现在写 React 项目基本上都是用 create-react-app 脚手架来创建,非常方便,省去一大堆的打包和构建配置。然而方便的代价就是牺牲了一些灵活性,像是一个黑盒,开发者没办法在构建流程中添加额外的逻辑,以一个简单的 CRA 项目为例,当 npm run build 之后,会生成 build 目录,里面的文件结构为:

build
├── asset-manifest.json
├── index.html
└── static
    ├── css
    │   └── main.05c4b3d4.css
    └── js
        ├── main.76756c14.js
        └── main.76756c14.js.LICENSE.txt

看上去乱七八糟、花里胡哨,我只想要一个 index.html 和 app.js 两个文件:

build
├── app.js
└── index.html

你给我整那么多有的没的干啥??

CRA 很方便但不够灵活

搭建 CRA 定制环境

开发者如果想要介入 CRA 的构建,就需要借助下面两个库:

  • customize-cra
  • react-app-rewired

第一步:先安装依赖:

yarn add customize-cra react-app-rewired --dev

第二步:在项目根目录创建 config-overrides.js 文件,里面的内容为:

const { override, adjustStyleLoaders } = require('customize-cra')

module.exports = {
  webpack: override(
    (config) => {
      // 这里拿到的 config 就是 webpack 的配置,可以在这里进行定制
      return config
    }
  )
}

第三步:把 package.json 中的 scripts 脚本改成:

"scripts": {
  "start": "react-app-rewired start",
  "build": "GENERATE_SOURCEMAP=false react-app-rewired build",
  "test": "react-app-rewired test",
  "eject": "react-scripts eject"
},

这样 npm start 和 npm build 的时候,就会读取 config-overrides.js 里面修改过的配置了。

修改 webpack 配置

从技术的视角来看,上述大需求可以分解为下面几个具体的构建需求:

  • 能够指定产物名称,而不是默认的 [name].[hash].js这种格式
  • 不要把 css 拆分出来,而是直接打到 js 里面
  • 把 LICENSE 的文件给去掉,不要自动生成了
  • 把 manifest 文件给去掉,不要自动生成了

指定 js 文件名

这个很简单,用过 webpack 的同学都知道,就是修改 output.filename 而已,代码如下:

const { override, adjustStyleLoaders } = require('customize-cra')

module.exports = {
  webpack: override(
    (config) => {
      const { output } = config
      output.filename = 'app.js'
      return config
    },
  ),
}

再打包就会发现产物发生了变化:

build
├── app.js
├── app.js.LICENSE.txt
├── asset-manifest.json
├── index.html
└── static
    └── css
        └── main.05c4b3d4.css

去掉 css 独立文件

这个就略麻烦一些了,要借助 adjustStyleLoaders 这个函数来修改配置中样式相关的 loader,我们先看代码:

const { override, adjustStyleLoaders } = require('customize-cra')

module.exports = {
  webpack: override(
    adjustStyleLoaders(({ use }) => {
      const lastStyleLoader = use[0]
      if (typeof lastStyleLoader === 'string') return // 开发模式下就是 style-loader
      const styleLoader = lastStyleLoader.loader.replace('mini-css-extract-plugin/dist/loader.js', 'style-loader/dist/cjs.js')
      use[0] = styleLoader // 不提取独立 CSS 文件
    }),
  ),
}

其实也只是 4 行代码而已:

const lastStyleLoader = use[0]
if (typeof lastStyleLoader === 'string') return

首先从 adjustStyleLoaders 的参数中解构出 use 数组,拿到最后一个处理样式的 loader,然后判断一下它的类型是不是 string,因为 CRA 的配置在开发环境(npm start)和生产环境(npm run build)是不一样的。

const styleLoader = lastStyleLoader.loader.replace('mini-css-extract-plugin/dist/loader.js', 'style-loader/dist/cjs.js')
use[0] = styleLoader

在开发环境下,最后一个处理样式的 loader 是 style-loader,而在生产环境下,最后一个 loader 是 mini-css-extract-plugin 带的专门用于将 css 提取成独立文件的 loader,我们只要再把它替换成 style-loader 不就行了么!

打包后的结果为:

build
├── app.js
├── app.js.LICENSE.txt
├── asset-manifest.json
└── index.html

是不是一下子清爽多了,大功即将告成!

去掉 manifest 文件

asset-manifest.json 是从哪儿来的呢?其实是 WebpackManifestPlugin 创建的,我们只需要把它从 plugins 中干掉即可:

const { override, adjustStyleLoaders } = require('customize-cra')

module.exports = {
  webpack: override(
    (config) => {
      config.plugins = config.plugins.filter((plugin) => !['WebpackManifestPlugin'].includes(plugin.constructor.name)) // 移除 manifest
      return config
    },
  ),
}

这个也好简单的对不对?打包之后发现 manifest 没有了:

build
├── app.js
├── app.js.LICENSE.txt
└── index.html

去掉 license 文件

LICENSE.txt 是哪来的呢?其实是 optimization.minimizer 中的 terserPlugin 在对代码进行压缩时提取出来的,我们只需要在其配置项中将 extractComments 设置为 false 即可:

const { override, adjustStyleLoaders } = require('customize-cra')

module.exports = {
  webpack: override(
    (config) => {
      const terserPlugin = optimization.minimizer.find((it) => it.constructor.name === 'TerserPlugin')
      terserPlugin.options.extractComments = false // 移除 license
      return config
    },
  ),
}

最终大功告成!打包之后只剩 index.html 和 app.js 啦:

build
├── app.js
└── index.html

完整代码为:

const { override, adjustStyleLoaders } = require('customize-cra')

module.exports = {
  webpack: override(
    (config) => {
      const { output, plugins, optimization } = config
      output.filename = 'app.js' // 指定文件名
      config.plugins = plugins.filter((plugin) => !['WebpackManifestPlugin'].includes(plugin.constructor.name)) // 移除 manifest
      const terserPlugin = optimization.minimizer.find((it) => it.constructor.name === 'TerserPlugin')
      terserPlugin.options.extractComments = false // 移除 license
      return config
    },
    adjustStyleLoaders(({ use }) => {
      const lastStyleLoader = use[0]
      if (typeof lastStyleLoader === 'string') return // 开发模式下就是 style-loader
      const styleLoader = lastStyleLoader.loader.replace('mini-css-extract-plugin/dist/loader.js', 'style-loader/dist/cjs.js')
      use[0] = styleLoader // 不提取独立 CSS 文件
    }),
  ),
}

扩展学习:高级用法

在一些特殊的场景下,我们可能会添加自定义的 loader 或 plugin,这个时候应该怎么办呢?下面做了整理,方便快速查阅:

添加自定义 loader

module.exports = {
  webpack: override(
    addRules([
      {
        test: /.ts$/,
        exclude: [/node_modules/],
        use: [
          {
            loader: require.resolve('./your-awesome-loader'), // 这里是自己的 loader
            options: {},
          },
        ],
      },
    ]),
  ),
}

添加自定义 plugin

const { override, addPostcssPlugins, addWebpackPlugin } = require('customize-cra')
module.exports = {
  webpack: override(
    addWebpackPlugin(
      new MyAwesomePlugin({
        baseUrl: `https://xxx`,
        filename: 'awesome-app',
      }),
    ),
  ),
}

添加 postcss 的 plugin

const { override, addPostcssPlugins } = require('customize-cra')
module.exports = {
  webpack: override(
    addPostcssPlugins([
      require('postcss-pxtorem')({
        rootValue: 16,
        propList: ['*'],
        exclude: (file) => {
          if (file.includes('node_modules')) return true
          return false
        },
      }),
      require('postcss-url')({
        filter: /.(svg|png)$/,
        url: (asset) => {
          const { url } = asset
          const srcName = url.split('/').pop()
          const obj = imageMap[srcName]
          return (obj && obj.tps) || url
        },
      }),
    ]),
  ),
}

添加 babel 的 plugin

const { override, addBabelPlugin, addBabelPlugins } = require('customize-cra')
module.exports = {
  webpack: override(
    addBabelPlugin(),
    ...addBabelPlugins(
      "polished",
      "emotion",
      "babel-plugin-transform-do-expressions"
    ),
  ),
}

完整的使用方法,可以参考官方文档