前言
Create React App(以下简称 CRA)是创建 React 应用的一个脚手架,它与其他脚手架不同的一个地方就是将一些复杂工具(比如 webpack)的配置封装了起来,让使用者不用关心这些工具的具体配置,从而降低了工具的使用难度。
使用 create-react-app
生成项目,不会有 webpack
的配置项,要导出 webpack
,必须使用 react-script eject
,但这是一个单向操作,eject
后,就无法恢复了。如果只是修改一些简单的配置,eject
是没有必要的。
但是对于一些熟悉 webpack 的开发者来说,他们可能想对 webpack 配置做一些修改,这个时候应该怎么办呢?
其实我们可以通过以下几种方式来修改 webpack 的配置:
- 项目 eject
- 替换 react-scripts 包
- 使用 react-app-rewired
- scripts 包 + override 组合
如果你正在为如何处理react配置项而烦恼,以下内容可以带你走出一片蓝天。接下来对这几种方式分别进行介绍下,配置是一个痛苦的过程,认真品味,如果对你有帮助请点赞转发哦!!使用插件的方法见官方文档,
项目 eject
使用 CRA 创建完项目以后,项目在package.json
里面提供了这样一个命令:
{
...
"scripts": {
"eject": "react-scripts eject"
},
...
}
复制代码
执行完这个命令——yarn run eject
后会将封装在 CRA 中的配置全部反编译
到当前项目,这样用户就可以完全取得 webpack 文件的控制权,想怎么修改就怎么修改了。
# eject 后项目根目录下会出现 config 文件夹,里面就包含了 webpack 配置
config
├── env.js
├── jest
│ ├── cssTransform.js
│ └── fileTransform.js
├── paths.js
├── polyfills.js
├── webpack.config.dev.js // 开发环境配置
├── webpack.config.prod.js // 生产环境配置
└── webpackDevServer.config.js
复制代码
CRA 与其他脚手架不同的另一个地方,就是可以通过升级其中的react-scripts
包来升级 CRA 的特性。比如用老版本 CRA 创建了一个项目,这个项目不具备 PWA 功能,但只要项目升级了react-scripts
包的版本就可以具备 PWA 的功能,项目本身的代码不需要做任何修改。
但如果我们使用了eject
命令,就再也享受不到 CRA 升级带来的好处了,因为react-scripts
已经是以文件的形式存在于你的项目,而不是以包的形式,所以无法对其升级。
替换 react-scripts 包
react-scripts 是 CRA 的一个核心包,一些脚本和工具的默认配置都集成在里面,使用 CRA 创建项目默认就是使用这个包,但是 CRA 还提供了另外一种方式来创建 CRA 项目,即使用自定义 scripts 包的方式。
# 默认方式
$ create-react-app foo
# 自定义 scripts 包方式
$ create-react-app foo --scripts-version 自定义包
复制代码
自定义包
可以是下面几种形式:
react-scripts
包的版本号,比如0.8.2
,这种形式可以用来安装低版本的react-scripts
包。- 一个已经发布到 npm 仓库上的包的名字,比如
your-scripts
,里面包含了修改过的 webpack 配置。 - 一个 tgz 格式的压缩文件,比如
/your/local/scripts.tgz
,通常是未发布到 npm 仓库的自定义 scripts 包,可以用npm pack
命令生成。
这种方式相对于之前的eject
是一种更灵活地修改 webpack 配置的方式,而且可以做到和 CRA 一样,通过升级 scrips 包来升级项目特性。
自定义 scripts 包的结构可以参照react-scripts
包的结构,只要修改对应的 webpack 配置文件,并安装上所需的 webpack loader 或 plugin 包就可以了。
使用 react-app-rewired 自定义配置
注意
react-app-rewired 1.x 配合 create-react-app 1.x
react-app-rewired 2.x 配合 create-react-app 2.x
版本升级导致互不兼容,另外,react-app-rewired 2.x 应该是社区维护了。
react-app-rewired@^2.0.0+
版本需要搭配customize-cra
使用
在 react-app-rewired 1.x 的版本中,它除了提供覆盖配置的方法,还体用了一些 helpers
,例如 rewireLess、rewirePreact 等,2.x 版本只保留了核心功能。
From README:
Version 2.0 removes the rewire helper functions
All helper functions:
- injectBabelPlugin
- compose
- getBabelLoader
- getLoader
- babelLoaderMatcher
- loaderNameMatches
have been removed with commit 0848602
另外一个工具帮我们实现了这些,customize-cra,这次我们将使用 react-app-rewired
和 customize-cra
一起倒腾。
虽然有这两种方式可以扩展 webpack 配置,但是很多开发者还是觉得太麻烦,有没有一种方式可以既不用eject
项目又不用创建自己的 scripts 包呢?答案是肯定的,react-app-rewired 是 react 社区开源的一个修改 CRA 配置的工具。
纯 react-app-rewired
的方式自定义配置,参考 Extended Configuration Options 文档。
这次我们使用 customize-cra
协助自定义,参考 Using the plugins 文档。
customize-cra提供了一些简遍的api(customize-cra/api.md),通常就可以满足大部分的开发需求
源码(customize-cra/src/customizes/webpack.js)(比如当前版本github.com/arackaf/cus…)
在 CRA 创建的项目中安装了react-app-rewired
后,可以通过创建一个config-overrides.js
文件来对 webpack 配置进行扩展。具体操作如下:
config-overrides.js配置
1、修改webpack配置文件,需要安装 react-app-rewired customize-cra
yarn add react-app-rewired customize-cra -D
2、修改package.json文件
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test --env=jsdom",
"eject": "react-scripts eject"
},
3、在项目根目录新建config-overrides.js
const { override } = require('customize-cra');
module.exports = {};
4、添加配置,跨域设置、增加less支持、px转rem、ant-design按需加载、打包压缩js和css
// 安装less less-loader
yarn add less less-loader -D
// 安装compression-webpack-plugin 压缩js为gzip
yarn add compression-webpack-plugin -D
const { override, overrideDevServer, addLessLoader, addPostcssPlugins, fixBabelImports } = require('customize-cra');
const CompressionWebpackPlugin = require('compression-webpack-plugin');
// 打包配置
const addCustomize = () => config => {
if (process.env.NODE_ENV === 'production') {
// 关闭sourceMap
config.devtool = false;
// 配置打包后的文件位置
config.output.path = __dirname + '../dist/demo/';
config.output.publicPath = './demo';
// 添加js打包gzip配置
config.plugins.push(
new CompressionWebpackPlugin({
test: /\.js$|\.css$/,
threshold: 1024,
}),
)
}
return config;
}
// 跨域配置
const devServerConfig = () => config => {
return {
...config,
// 服务开启gzip
compress: true,
proxy: {
'/api': {
target: 'xxx',
changeOrigin: true,
pathRewrite: {
'^/api': '/api',
},
}
}
}
}
module.exports = {
webpack: override(
fixBabelImports('import', {
libraryName: 'antd-mobile',
style: 'css',
}),
addLessLoader(),
addPostcssPlugins([require('postcss-pxtorem')({ rootValue: 75, propList: ['*'], minPixelValue: 2, selectorBlackList: ['am-'] })]),
addCustomize(),
),
devServer: overrideDevServer(
devServerConfig()
)
}
5、antd
假设我们要使用 antd,参考 高级配置 文档。
npm i --save-dev babel-plugin-import
npm i --save antd
const { override, fixBabelImports, addLessLoader } = require('customize-cra');
module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
}),
addLessLoader({
javascriptEnabled: true,
modifyVars: { '@primary-color': '#1DA57A' },
localIdentName: '[local]--[hash:base64:5]' // 自定义 CSS Modules 的 localIdentName
}),
);
6、decorators
在 create-react-app 的 Can I Use Decorators 文档中说,当前它并不是一个文档的规范,默认不推荐使用,如果要使用,需要自己手动开启。
npm i --save-dev @babel/plugin-proposal-decorators
const { override, fixBabelImports, addLessLoader, addDecoratorsLegacy } = require('customize-cra');
module.exports = override(
addDecoratorsLegacy(),
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
}),
addLessLoader({
javascriptEnabled: true,
modifyVars: { '@primary-color': '#1DA57A' },
localIdentName: '[local]--[hash:base64:5]' // 自定义 CSS Modules 的 localIdentName
}),
);
7、添加别名
const { override, fixBabelImports, addLessLoader, addDecoratorsLegacy, addWebpackAlias } = require('customize-cra');
module.exports = override(
addDecoratorsLegacy(),
addWebpackAlias({
["ag-grid-react$"]: path.resolve(__dirname, "src/shared/agGridWrapper.js")
}),
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
}),
addLessLoader({
javascriptEnabled: true,
modifyVars: { '@primary-color': '#1DA57A' },
localIdentName: '[local]--[hash:base64:5]' // 自定义 CSS Modules 的 localIdentName
}),
);
8、添加 react-hot-reloader、在根组件处开启 react-hot-reloader
# https://www.npmjs.com/package/react-hot-loader
# https://github.com/cdharris/react-app-rewire-hot-loader
$ npm i react-hot-loader -D
$ npm i react-app-rewire-hot-loader @hot-loader/react-dom -D
随后在App.js中做如下设置
import React, { Component } from 'react'
import { hot } from 'react-hot-loader/root'
class App extends Component {
render() {
return (
<>测试</>
)
}
}
const AppHot = process.env.NODE_ENV === 'development' ? hot(App) : App
export default AppHot
9、关闭sourceMap
-
方案一:修改package中scripts里的build
“build”: “GENERATE_SOURCEMAP=false react-app-rewired build”
-
方案二:
const rewiredMap = () => config => { config.devtool = config.mode === ‘development’ ? ‘cheap-module-source-map’ : false
return config }
10、按需加载、lodash优化
ant-design-mobile、ant-design按需加载
亲试有如下几个方案:
方法一:
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true
}),
fixBabelImports('babel-plugin-import', {
libraryName: 'antd-mobile',
libraryDirectory: 'es',
style: true
}),
方法二:
fixBabelImports('antd', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true
}),
fixBabelImports('antd-mobile', {
libraryName: 'antd-mobile',
libraryDirectory: 'es',
style: true
}),
使用插件优化
lodash
作为一个比较常用的前端开发工具集,在使用webpack进行vendor
分离的实践中,会遇到将整个lodash
文件分离到vendor.js
的问题。这样会使vendor.js
文件变得特别大。
根据babel-plugin-lodash
参考文档介绍,使用lodash-webpack-plugin
可以进一步压缩lodash
。
安装lodash-webpack-plugin
依赖:
yarn add lodash-webpack-plugin --dev
增加配置文件如下:
const LodashWebpackPlugin = require('lodash-webpack-plugin')
addWebpackPlugin({
new LodashWebpackPlugin({
collections: true,
paths: true
}),
})
实际上还需要这一步支持,首先新建自己的.bable文件或者直接在package.json里面修改,下面方法采用的是前者
{
"presets": ["react-app"],
// 开发环境下配置项
"env": {
"development": {
"plugins": ["dynamic-import-node"]
}
},
"plugins": [
[
"module-resolver",
{
"alias": {
"src": ["./src/"]
}
}
],
[
"import",
{
"libraryName": "lodash",
"libraryDirectory": "",
"camel2DashComponentName": false
}
],
[
"@babel/plugin-transform-modules-commonjs",
{
"allowTopLevelThis": true
}
]
]
}
**注意:**一般情况下,不使用lodash-webpack-plugin
就可以满足开发的需要,但是文件特别大的情况下,建议还是使用它。
使用 lodash-es
tree-shaking 作为 rollup 的一个杀手级特性,能够利用 ES6 的静态引入规范,减少包的体积,避免不必要的代码引入,webpack2 也很快引入了这个特性。
要用到 tree-shaking,必然要保证引用的模块都是 ES6 规范的。lodash-es 是着具备 ES6 模块化的版本,只需要直接引入就可以。
import {isEmpty, isObject, cloneDeep} from 'lodash-es';
可以看到,使用优化后,lodash 从原来的 70.7kb 减少到了20.2kb效果还是很明显的。
可以参考以下链接:
以上是一些简单的配置已经好了。
整体配置文件如下:
const {
override,
fixBabelImports,
addLessLoader,
addWebpackAlias,
addBabelPlugins,
addWebpackPlugin,
useBabelRc,
disableChunk,
adjustWorkbox,
setWebpackPublicPath,
addBundleVisualizer,
disableEsLint,
addWebpackExternals
// addBundleVisualizer
} = require('customize-cra')
const path = require('path')
const paths = require('react-scripts/config/paths')
const rewireReactHotLoader = require('react-app-rewire-hot-loader')
const CompressionWebpackPlugin = require('compression-webpack-plugin')
// const rewireCompressionPlugin = require('react-app-rewire-compression-plugin')
const rewireUglifyjs = require('react-app-rewire-uglifyjs')
const FilterWarningsPlugin = require('webpack-filter-warnings-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const LodashWebpackPlugin = require('lodash-webpack-plugin')// 补充:对开发友好,打包完成桌面提醒
const WebpackBuildNotifierPlugin = require('webpack-build-notifier')
const webpackConfig = require('./webpack.config.js')
// const ProgressBarPlugin = require('progress-bar-webpack-plugin')
// const Dashboard = require('webpack-dashboard')
// const DashboardPlugin = require('webpack-dashboard/plugin')
// const dashboard = new Dashboard()
const theme = require('./theme')
// SKIP_PREFLIGHT_CHECK = true
/** * 生产环境是否打包 Source Map 两种方法 * */
const rewiredMap = () => config => {
config.devtool = config.mode === 'development' ? 'cheap-module-source-map' : false
return config
}
process.env.PORT = 3006
process.env.GENERATE_SOURCEMAP !== 'false'
console.log(process.env.NODE_ENV)
// const addWebpackModules = () => config => {
// const loaders = config.module.rules.find(rule => Array.isArray(rule.oneOf)).oneOf
// loaders[loaders.length - 4] = Object.assign(
// loaders[loaders.length - 4],
// webpackConfig.module.rules[0]
// )
// return config
// }
// path
const resolveAlias = dir => path.join(__dirname, '.', dir)
// 热跟新
const hotLoader = () => (config, env) => {
config = rewireReactHotLoader(config, env)
return config
}
// build--->prod --->文件设置
const appBuildPathFile = () => config => {
if (config.mode === 'development') {
console.log('evn is development, skip build path change...')
} else if (config.mode === 'production') {
console.log('evn is production, change build path...')
// 关闭sourceMap
config.devtool = false
// // 配置打包后的文件位置修改path目录
paths.appBuild = path.join(path.dirname(paths.appBuild), 'dist')
config.output.path = path.join(path.dirname(config.output.path), 'dist')
// 添加js打包gzip配置
// config.plugins.push(
// new CompressionWebpackPlugin({
// test: /\.js$|\.css$/,
// threshold: 1024
// })
// )
// 更改生产模式输出的文件名
// config.output.filename = 'static/js/[name].js?_v=[chunkhash:8]'
// config.output.chunkFilename = 'static/js/[name].chunk.js?_v=[chunkhash:8]'
}
return config
}
//生产环境去除console.* functions
const dropConsole = () => {
return config => {
if (config.optimization.minimizer) {
config.optimization.minimizer.forEach(minimizer => {
if (minimizer.constructor.name === 'TerserPlugin') {
minimizer.options.terserOptions.compress.drop_console = true
}
})
}
return config
}
}
/** * * @description 解决打包的时候如下报错 * @url{https://github.com/ant-design/ant-design/issues/15696} * https://blog.csdn.net/peade/article/details/84890399 chunk 3 [mini-css-extract-plugin] Conflicting order between: * css ./node_modules/css-loader/dist/cjs.js??ref--6-oneOf-7-1!./node_modules/postcss-loader/src??postcss!./node_modules/less-loader/dist/cjs.js??ref--6-oneOf-7-3!./node_modules/antd/es/input/style/index.less * css ./node_modules/css-loader/dist/cjs.js??ref--6-oneOf-7-1!./node_modules/postcss-loader/src??postcss!./node_modules/less-loader/dist/cjs.js??ref--6-oneOf-7-3!./node_modules/antd/es/message/style/index.less */
const delConflictingOrder = () => {
return config => {
for (let i = 0; i < config.plugins.length; i++) {
const p = config.plugins[i]
if (!!p.constructor && p.constructor.name === MiniCssExtractPlugin.name) {
const miniCssExtractOptions = { ...p.options, ignoreOrder: true }
config.plugins[i] = new MiniCssExtractPlugin(miniCssExtractOptions)
break
}
}
}
}
const addMiniCssExtractPlugin = () => {
return config => {
config.plugins.unshift(
new FilterWarningsPlugin({
// exclude: /any-warnings-matching-this-will-be-hidden/
// exclude: /mini-css-extract-plugin[^]*Conflicting order between:/
exclude: /\[mini-css-extract-plugin\][^]*Conflicting order between:/
})
)
}
}
const proxyApi = {
'/api': {
// target: '', // prod
changeOrigin: true,
secure: false,
xfwd: false,
pathRewrite: {
'^/api': '/'
}
},
'/store': {
// target: '', // staging
changeOrigin: true,
secure: false,
xfwd: false,
pathRewrite: {
'^/store': '/'
}
}
}
module.exports = {
webpack: override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true
}),
addLessLoader({
// strictMath: true,
noIeCompat: true,
javascriptEnabled: true,
modifyVars: { ...theme }
// localIdentName: '[local]--[hash:base64:5]', // 自定义 CSS Modules 的 localIdentName
}),
setWebpackPublicPath('/hostsec'), // 修改 publicPath
addWebpackExternals({
React: 'React',
lodash: 'Lodash'
}),
// addWebpackModules(),
addWebpackAlias({
'@': resolveAlias('src'),
lib: resolveAlias('src/lib'),
components: resolveAlias('src/components'),
images: resolveAlias('src/assets/images'),
styled: resolveAlias('src/assets/styled'),
views: resolveAlias('src/views'),
store: resolveAlias('src/store'),
router: resolveAlias('src/router'),
locale: resolveAlias('src/locale'),
// 处理警告 React-Hot-Loader: react-🔥-dom patch is not detected. React 16.6+ features may not work.
'react-dom': '@hot-loader/react-dom'
// 解决antd 的icon图标打包体积大
// '@ant-design/icons': 'purched-antd-icons'
}),
disableEsLint(),
appBuildPathFile(),
disableChunk(),
dropConsole(),
// 关闭mapSource
rewiredMap(),
// 热跟新
hotLoader(),
// 配置babel解析器
addBabelPlugins(['@babel/plugin-proposal-decorators', { legacy: true }]),
//启用ES7的修改器语法(babel 7)
// ['@babel/plugin-proposal-decorators', {legacy: true}],
// ['@babel/plugin-proposal-class-properties', {loose: true}],
// 打包编译完成提醒
addWebpackPlugin(
new WebpackBuildNotifierPlugin({
title: '',
logo: path.resolve('./public/logo.svg'),
suppressSuccess: true
}),
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash].css',
chunkFilename: 'static/css/[id].[contenthash].css',
ignoreOrder: false
// moduleFilename: ({ name }) => `${name.replace('/js/', '/css/')}.css`
}),
new LodashWebpackPlugin({ collections: true, paths: true }), // 美化控制台
// new DashboardPlugin(dashboard.setData),
// 进度条
// new ProgressBarPlugin(),
delConflictingOrder(),
addMiniCssExtractPlugin()
),
rewireUglifyjs,
// rewireCompressionPlugin,
// 允许使用.babelrc文件进行Babel配置。
useBabelRc(),
// add webpack bundle visualizer if BUNDLE_VISUALIZE flag is enabled
process.env.BUNDLE_VISUALIZE == 1 && addBundleVisualizer(),
adjustWorkbox(wb =>
Object.assign(wb, {
skipWaiting: true,
exclude: (wb.exclude || []).concat('index.html')
})
)
// addDecoratorsLegacy() // 解析器,
),
// 配置devServer
// devServer: overrideDevServer(
// // dev server plugin
// watchAll(),
// ),
// 配置devServer
devServer: configFunction => (proxy, allowedHost) => {
proxy = process.env.NODE_ENV === 'development' ? proxyApi : null
// allowedHost: 添加额外的地址
const config = configFunction(proxy, allowedHost)
return config
}
}
㊗️注意:如果还要增加配置可以自行插入,具体支持的方法如图:
传送门**api.md**
尾声:配置代码解析
到了这里,估计有部分同学还不满意为什么以上代码能够解决问题,我按我的理解解释下。
const path = require('path');
这行代码是基于require.js
,是一个js进行引入包的工具。通过这行代码,拿到path,才能够对路径做处理。
const paths = require('react-scripts/config/paths');
react-scripts
里面有React项目用于打包的命令,以及配置文件,如果你进行了eject,会发现config和script目录里的内容与react-scripts
里的同名目录惊人的相似。可以认为eject是把这里的配置暴露出来了。这里这行代码是为了获取项目的路径配置。
paths.appBuild = path.join(path.dirname(paths.appBuild), 'dist');
这行代码是修改配置里的appBuild目录,React项目在进行build的时候,都是根据这里配置的目录做的操作(例如检查打包后的代码大小,计算Gzip等),必须要修改,不然打包会失败。
config.output.path = path.join(path.dirname(config.output.path), 'dist');
这行代码就是实现我们目的的根源了,修改项目打包地址。
总结
CRA 是一个非常棒的 React 脚手架工具,但你如果不满足于它的 webpack 默认配置,你可以通过上述几种方式来扩展自己项目的 webpack 配置,这几种方式各有优缺点,可以结合具体的使用场景来选择合适自己的方式。
好了,到这里本文结束。如果本文对你有帮助,欢迎关注、点赞、收藏一波。
同时,欢迎小伙伴们加微信群一起探讨:
微信号
文档参考 :
- create-react-app facebook.github.io/create-reac…
- customize-cra github.com/arackaf/cus…
- ant-design在create-react-app中使用 ant.design/docs/react/…
可以实现基本需求,包含:按需引入,加入Less - 本地开发跨域添加Proxy, create-react-app官网: facebook.github.io/create-reac…
- http-proxy-middleware github.com/chimurai/ht…
- react-hot-loader github.com/gaearon/rea…
- react-hot-loader without eject [react-app-rewire-hot-loader] github.com/cdharris/re…
- create-react-app默认webpack配置解析及自定义
- juejin.cn/post/684490…
今天的文章Create React App无eject配置(react-app-rewired 和 customize-cra)分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/16715.html