CRA 很方便但不够灵活
现在写 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 的构建,就需要借助下面两个库:
第一步:先安装依赖:
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"
),
),
}
完整的使用方法,可以参考官方文档。
转载自:https://juejin.cn/post/7182899662364344378