create-react-app(以下简称cra)作为react官方提供的脚手架工具,是目前生成react项目一个非常常用和主流的工具。很多企业级的应用搭建也是基于这个脚手架工具上二次开发。最近这段正好最近学习了webpack打包配置工程化的一些内容,索性就以cra的配置为例,对这段时间的学习做一个总结。
准备工作
首先,我们要用cra创建一个项目。这个没啥好说,有手就行。
create-react-app cra-config-project
这样初始化后创建出来项目的配置信息是隐藏在node_modules中的react-scripts中的。为了更直观的看到配置信息和修改,使用
eject命令将配置弹射出来。
yarn eject
完成后,我们项目配置的目录结构变成这样。
启动命令
打开package.json 文件,在scripts中看到以下三条命令
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"test": "node scripts/test.js"
},
很明显,这分别是项目的启动开发环境,构建,测试的命令。我们重点看一下scripts中开发和构建的脚本。
start.js
在大概115行的位置,我们看到这样一段代码
const devServer = new WebpackDevServer(serverConfig, compiler);
// Launch WebpackDevServer.
devServer.startCallback(() => {
...
很明显,这就是启动开发服务器的关键代码。在开发环境的时候,我们通过webpack-dev-server来启动一个本地的服务器,然后把随时构建出来的项目放在这个服务器下面运行。实例化这个devServer对象时候传的第一个参数是服务器的配置项,包括端口号,代理,静态资源目录等,具体见https://webpack.docschina.org/configuration/dev-server/;第二个参数是webpack的相关配置。如下所示:
compiler = webpack(config);
build.js
构建脚本直接输出打包结果,自然不再需要启动本地服务。因此在获取了编译结果后,直接运行即可。因此在140行中
compiler.run((err, stats) => {
//...
}
在代码中我们可以看到构建时,编译过程通过promise封装,对各种错误情况进行了处理。
目录结构
在看具体的配置之前,让我们回到这张图,看一下eject命令都弹射出了哪些配置放到了项目目录中来。
比起初使状态,现在的项目目录中除了装有启动脚本文件的目录scripts外,另外增加的就是config目录。打开config目录,webpack.config.js和webpackDevServer.config.js赫然在目,根据这个文件名我们可以很明显得知,这两个文件一个是webpack的配置,一个是开发服务器devServer 的配置。接下来,我们就可以从这两个文件按图索骥,学习cra的基本配置了。
配置解析
weback.config.js
在webpack.config.js中,默认导出了一个接受一个环境变量作为返回一个配置对象的方法。那传这个环境变量的目的不言而喻,一定有很多配置开发和生产环境是不同的。接下来重头戏来了,让我们来一条一条地学习下react官方对react开发环境是怎么配置的吧。
1.entry
entry: paths.appIndexJs,
也就是 src目录下的index.js,因为cra构建的是单页应用,只有一个入口文件
- output
output: {
path: paths.appBuild, // 打包后文件目录 在config目录中path.js中配置
pathinfo: isEnvDevelopment, // webpack 在 bundle 中是否引入「所包含模块信息」的相关注释 开发环境打开 生产环境关闭
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/bundle.js',//打包后文件名,生产环境根据name放在不同文件,开发环境放在一个bundle.js文件中
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',//chunk文件名称,生产环境和开发环境的区别是文件名中加上了hash
assetModuleFilename: 'static/media/[name].[hash][ext]',//打包后的静态资源目录和文件名规则,如不指定直接放在打包后的根目录中
publicPath: paths.publicUrlOrPath,//打包后的文件部署的url地址
devtoolModuleFilenameTemplate: isEnvProduction
? info =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),//自定义source-map文件数组使用名称
}
- target
target: ['browserslist'],
构建目标,从 browserslist-config 中推断出平台和 ES 特性 默认是browserslist 如果browserslist不存在,为web(cra项目中browserslist在package.json中)
- bail
bail: isEnvProduction,
错误出现时是否立即退出,生产环境下打开
- devtool
devtool: isEnvProduction
? shouldUseSourceMap
? 'source-map'
: false
: isEnvDevelopment && 'cheap-module-source-map',
生成sourceMap方式,cra配置为生产环境source-map,开发环境为cheap-module-source-map。这两者的区别source-map调试时会显示列信息。devtool的配置有很多种,具体见https://webpack.docschina.org/configuration/devtool/#root
- cache
cache: {
type: 'filesystem',//缓存生成的 webpack 模块和 chunk,来改善构建速度 开发环境下默认为type:'memory' 生产环境下关闭
version: createEnvironmentHash(env.raw),
cacheDirectory: paths.appWebpackCache,//缓存目录
store: 'pack',
buildDependencies: {
defaultWebpack: ['webpack/lib/'],
config: [__filename],
tsconfig: [paths.appTsConfig, paths.appJsConfig].filter(f =>
fs.existsSync(f)
),
},
},
- optimization 优化项
optimization: {
minimize: isEnvProduction, //只在生产环境下开启
minimizer: [
//js TerserPlugin开启代码压缩
new TerserPlugin({
terserOptions: {
parse: {
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
comparisons: false,
inline: 2,
},
mangle: {
safari10: true,
},
keep_classnames: isEnvProductionProfile,
keep_fnames: isEnvProductionProfile,
output: {
ecma: 5,
comments: false,
ascii_only: true,
},
},
}),
//css 代码压缩
new CssMinimizerPlugin(),
],
},
- resolve 解析
resolve: {
modules: ['node_modules', paths.appNodeModules].concat(
modules.additionalModulePaths || []
),//解析模块时应该搜索的目录
extensions: paths.moduleFileExtensions
.map(ext => `.${ext}`)
.filter(ext => useTypeScript || !ext.includes('ts')),//如果有多个文件有相同的名字,但后缀名不同时webpack按顺序解析这些后缀名,使用户在引入模块时不带扩展名
alias: {
'react-native': 'react-native-web',
...(isEnvProductionProfile && {
'react-dom$': 'react-dom/profiling',
'scheduler/tracing': 'scheduler/tracing-profiling',
}),
...(modules.webpackAliases || {}),
},//创建 import 或 require 的别名,来确保模块引入变得更简单。可以给utils之类的文件色之后
plugins: [
new ModuleScopePlugin(paths.appSrc, [
paths.appPackageJson,
reactRefreshRuntimeEntry,
reactRefreshWebpackPluginRuntimeEntry,
babelRuntimeEntry,
babelRuntimeEntryHelpers,
babelRuntimeRegenerator,
]),
],
},//应该使用的额外的解析插件列表
- performance
performance: false,
关闭了webpack本身的性能提示,cra本身提供了FileSizeReporter来计算和报告文件大小
10. module
终于进入到我们这个比较重要的module配置项,module配置决定了webpack如何解析非js的模块,项目中的各种静态资源,样式文件,乃至于ts tsx jsx等loader配置都是在这个模块中配置。
- source-map loader
shouldUseSourceMap && {
enforce: 'pre',
exclude: /@babel(?:\/|\\{1,2})runtime/,
test: /\.(js|mjs|jsx|ts|tsx|css)$/,
loader: require.resolve('source-map-loader'),
},
- 静态资源loader
{
test: [/\.avif$/],
type: 'asset',
mimetype: 'image/avif',
parser: {
dataUrlCondition: {
maxSize: imageInlineSizeLimit,
},
},
},
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: imageInlineSizeLimit,
},
},
},
{
test: /\.svg$/,
use: [
{
loader: require.resolve('@svgr/webpack'),
options: {
prettier: false,
svgo: false,
svgoConfig: {
plugins: [{ removeViewBox: false }],
},
titleProp: true,
ref: true,
},
},
{
loader: require.resolve('file-loader'),
options: {
name: 'static/media/[name].[hash].[ext]',
},
},
],
issuer: {
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
},
},
对各种格式的图片,svg文件的处理
- 样式文件loader
{
test: cssRegex,
exclude: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
mode: 'icss',
},
}),
sideEffects: true,
},
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
mode: 'local',
getLocalIdent: getCSSModuleLocalIdent,
},
}),
},
{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
mode: 'icss',
},
},
'sass-loader'
),
sideEffects: true,
},
{
test: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
mode: 'local',
getLocalIdent: getCSSModuleLocalIdent,
},
},
'sass-loader'
),
},
对样式文件的处理, 主要是scss和css,cra 为什么没有配置less文件loader呢?开发环境下直接将所有样式注入head中style中,生产环境下结合下面要介绍的miniCssExtractPlugin插件抽出后放入不同css文件。另外,这里cra还对以.module.css 和 .module.sass后缀结尾的文件进行了css module处理,如果开发者需要对样式文件要用modules规则,可以将文件的后缀写成这两种。
11. 插件
- htmlWebpackPlugin
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
没啥好说的,地球人都知道的一个插件,把打包好的js文件注入到html中去,要注意的是在生产环境了开启了移除注释,合并空格一系列优化配置
- InlineChunkHtmlPlugin
isEnvProduction &&
shouldInlineRuntimeChunk &&
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
这个插件辅助将一些chunk出来的模块内联到html中,比如runtime的代码,代码量不大。生产环境下开启
- InterpolateHtmlPlugin
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
HtmlWebpackPlugin的辅助插件,可以在html文件中加入变量
- ModuleNotFoundPlugin
- ReactRefreshWebpackPlugin
isEnvDevelopment &&
shouldUseReactRefresh &&
new ReactRefreshWebpackPlugin({
overlay: false,
}),
热更新 react 组件,开发环境下开启
- MiniCssExtractPlugin
isEnvProduction &&
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
}),
抽离css文件插件,生产环境下开启
- WebpackManifestPlugin
- ForkTsCheckerWebpackPlugin
useTypeScript &&
new ForkTsCheckerWebpackPlugin({
async: isEnvDevelopment,
typescript: {
typescriptPath: resolve.sync('typescript', {
basedir: paths.appNodeModules,
}),
configOverwrite: {
compilerOptions: {
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
skipLibCheck: true,
inlineSourceMap: false,
declarationMap: false,
noEmit: true,
incremental: true,
tsBuildInfoFile: paths.appTsBuildInfoFile,
},
},
context: paths.appPath,
diagnosticOptions: {
syntactic: true,
},
mode: 'write-references',
},
issue: {
include: [
{ file: '../**/src/**/*.{ts,tsx}' },
{ file: '**/src/**/*.{ts,tsx}' },
],
exclude: [
{ file: '**/src/**/__tests__/**' },
{ file: '**/src/**/?(*.){spec|test}.*' },
{ file: '**/src/setupProxy.*' },
{ file: '**/src/setupTests.*' },
],
},
logger: {
infrastructure: 'silent',
},
}),
强制ts类型检查,如果项目使用了typescript编写的话使用
- webpack.definePlugin
new webpack.DefinePlugin(env.stringified),
wepack内置插件,在浏览器环境中定义环境变量
- webpack.ignorePlugin
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
}),
wepack内置插件,可以在打包时有选择的忽略一些内容,这里的配置是在打包moment的时候忽略moment的本地化内容
isEnvDevelopment && new CaseSensitivePathsPlugin(),
解决为了解决mac系统中文件名大小写不敏感导致的打包不报错的问题,详见https://github.com/facebook/create-react-app/issues/240
结语
对于工程化经验特别少的开发者来说,webpack的配置浩如烟海,宛如一本百科全书让人望而兴叹。但是掌握webpack可以说是前端开发者进阶的必经之路。在学习的过程中,可以自己多搞一些demo,多去尝试和实践,就会渐渐的对它熟悉起来。之后,笔者计划对webpack打包的性能优化从配置项的各个维度做一个总结,请拭目以待。