前言
React 项目的构建,是基于 Node.js 的。而 webpack 作为一个静态模块打包工具,在构建过程中发挥着至关重要的优化作用:模块化打包、性能优化、资源管理等。这大大简化了项目的构建流程,提高了开发效率和用户体验。
这次我们来看看 React 脚手架项目(通过 create-react-app
创建)中的 webpack 究竟做了哪些工作?
接上篇文章内容,这篇文章我们来了解一下 webpack 配置内容 以及项目构建时 webpack 是如何工作的。
上一篇文章指路:简单看看 React 脚手架中的 Webpack 做了什么?(1)
四、执行 start 命令
当我们启动项目时,会在终端执行 yarn start
或 npm run start
命令。实际上,就是通过 Node.js 执行 scripts
文件夹中的 start.js
文件。
这一章节,我们来看看 start.js
中做了什么?
首先,设置项目环境为开发环境。
js
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';
接着,设置浏览器的兼容性。
js
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
checkBrowsers(paths.appPath, isInteractive)......
checkBrowsers
方法中做了什么事?
js
function checkBrowsers(dir, isInteractive, retry = true) {
const current = browserslist.loadConfig({ path: dir });
if (current != null) {
return Promise.resolve(current);
}
if (!retry) {
return Promise.reject(
new Error(
chalk.red(
'As of react-scripts >=2 you must specify targeted browsers.'
) +
os.EOL +
`Please add a ${chalk.underline(
'browserslist'
)} key to your ${chalk.bold('package.json')}.`
)
);
}
return shouldSetBrowsers(isInteractive).then(shouldSetBrowsers => {
if (!shouldSetBrowsers) {
return checkBrowsers(dir, isInteractive, false);
}
return (
pkgUp({ cwd: dir })
.then(filePath => {
if (filePath == null) {
return Promise.reject();
}
const pkg = JSON.parse(fs.readFileSync(filePath));
pkg['browserslist'] = defaultBrowsers;
fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2) + os.EOL);
browserslist.clearCaches();
console.log(
`${chalk.green('Set target browsers:')} ${chalk.cyan(
defaultBrowsers.join(', ')
)}`
);
})
.catch(() => {})
.then(() => checkBrowsers(dir, isInteractive, false))
);
});
}
1、获取 package.json
中的浏览器配置,若不存在相关配置则继续执行;
2、判断是否需要设置浏览器配置?此处判断是根据 process.stdout.isTTY
进行的,这个变量是用来判断当前环境是否是一个支持交互式的命令行界面;
3、如果需要配置,就将默认的浏览器配置写入 package.json
中的 broswerslist
字段中;
4、如果不需要配置,则抛出一个错误。
默认的浏览器配置大致如下:
json
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
browserslist
配置会告诉 webpack 在编译和打包过程中需要考虑的目标浏览器范围,使得 webpack 能够在构建过程中自动处理兼容性问题 。webpack 通常会使用这些信息来自动地为你的代码生成兼容目标浏览器的代码。
具体来说,当 webpack 在编译过程中遇到 CSS、JavaScript 或其他需要兼容性处理的代码时,它会根据 browserslist
配置自动应用相应的 polyfills
、autoprefixer
或其他兼容性处理工具,以确保最终生成的代码在指定的目标浏览器中能够正常运行。
在完成浏览器的相关配置后,下一步进行开发服务器的启动工作:
js
checkBrowsers(paths.appPath, isInteractive)
.then(() => {
return choosePort(HOST, DEFAULT_PORT);
})
.then(port => {
if (port == null) {
return;
}
const config = configFactory('development');
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const appName = require(paths.appPackageJson).name;
const useTypeScript = fs.existsSync(paths.appTsConfig);
const urls = prepareUrls(
protocol,
HOST,
port,
paths.publicUrlOrPath.slice(0, -1)
);
// Create a webpack compiler that is configured with custom messages.
const compiler = createCompiler({
appName,
config,
urls,
useYarn,
useTypeScript,
webpack,
});
// Load proxy config
const proxySetting = require(paths.appPackageJson).proxy;
const proxyConfig = prepareProxy(
proxySetting,
paths.appPublic,
paths.publicUrlOrPath
);
// Serve webpack assets generated by the compiler over a web server.
const serverConfig = {
...createDevServerConfig(proxyConfig, urls.lanUrlForConfig),
host: HOST,
port,
};
const devServer = new WebpackDevServer(serverConfig, compiler);
// Launch WebpackDevServer.
devServer.startCallback(() => {
if (isInteractive) {
clearConsole();
}
if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) {
console.log(
chalk.yellow(
`Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.`
)
);
}
console.log(chalk.cyan('Starting the development server...\n'));
openBrowser(urls.localUrlForBrowser);
});
['SIGINT', 'SIGTERM'].forEach(function (sig) {
process.on(sig, function () {
devServer.close();
process.exit();
});
});
if (process.env.CI !== 'true') {
// Gracefully exit when stdin ends
process.stdin.on('end', function () {
devServer.close();
process.exit();
});
}
})
.catch(err => {
if (err && err.message) {
console.log(err.message);
}
process.exit(1);
});
首先,根据项目的一些基本信息,例如通信协议、项目名称、主机地址、端口号 等以及 webpack 配置 创建一个 webpack 编译器。
接着,根据代理配置 和上面创建的编译器创建一个 webpack 开发服务器。
那么 WebpackDevServer
是如何工作的呢?
WebpackDevServer
会启动一个用于开发环境的轻量级 Node.js Express 服务器 ,并通过配置指定主机地址 和端口号。
这个服务器并不像生产环境使用的服务器,而是一个虚拟的服务器 ,它运行于本机的内存中,模拟生产环境中服务器的行为。
服务器监听到代码文件发生变化时,会进行重新编译,生成最新的静态资源文件。静态资源文件将会存储在内存中,浏览器也将从内存中进行静态资源文件的加载。
五、webpack 的 config 文件
这一章节我们来了解一下 webpack.config.js
文件,简单介绍一下其中配置的含义,看看脚手架中是如何处理这些配置的。
target
由于 webpack 可以为多种环境构建编译,所以需要配置一下 target
属性,为编译结果指定一个环境。
js
target: ['browserslist']
target
的默认值是 web
,意为"编译为类浏览器环境里可用 "。在这里我们设置为了 browserslist
,webpack 将会从最近的 package.json
或 BROWSERSLIST
环境变量中读取浏览器配置,相较于 web
更加的具体。
stats
webpack 在构建的过程中可以在终端或浏览器控制台看到构建的详细信息,而 stats
可以设置输出哪些信息。
js
stats: 'errors-warnings'
这里的 stats
设置为了 errors-warnings
,意为"只在发生错误或有警告时输出"。
mode
设置项目编译的环境,取值有 development
, production
, none
。
mode
会告知 webpack 在编译时启用对应的优化配置 。例如 production
环境下,为模块和 chunk 启用确定性的混淆名称,会启用对应的插件。
另外,mode
会将值赋给 process.env.NODE_ENV
,通过 DefinePlugin
注入程序中。
bail
当打包过程出现错误时迫使 webpack 退出打包过程。默认值为 false
。
devtool
此选项控制是否生成,以及如何生成 source map。
source map 是一种映射关系,它将编译后的代码映射回原始源代码,以方便开发者在调试时定位错误和问题。
webpack 支持多种 source map 类型,每种类型都有各自的优缺点和适应场景。常见的选项包括:
-
eval :通过
eval()
函数来执行代码,并生成 DataUrl 形式的 source map 。这是最快的 source map 类型,但也是最不完整的,不支持逐行调试。 -
cheap-eval-source-map :生成精简版的 source map,可以逐行调试,但无法显示列信息。
-
cheap-module-eval-source-map :与
cheap-eval-source-map
类似,但支持显示模块名和路径。 -
eval-source-map :生成完整版的 source map,可以逐行调试,但会降低构建速度。
-
cheap-source-map :生成精简版的 source map,可以显示列信息,但不支持逐行调试。
-
cheap-module-source-map :与
cheap-source-map
类似,但支持显示模块名和路径。 -
source-map :生成完整版的 source map,包含所有源文件信息,但会明显降低构建速度。
根据不同的配置的特点分别设置开发环境和生产环境生成 source map 的类型。最重要的一点,不要让普通用户能够访问 source map 文件!
你可以直接使用
SourceMapDevToolPlugin
/EvalSourceMapDevToolPlugin
来替代使用devtool
选项,因为它有更多的选项。切勿 同时使用devtool
选项和SourceMapDevToolPlugin
/EvalSourceMapDevToolPlugin
插件。devtool
选项在内部添加过这些插件,所以你最终将应用两次插件。
entry
webpack 打包的结果就是一个依赖关系图,entry
则是指示 webpack 用哪个模块作为依赖图的起点。
通常来说,每个 HTML 页面都需要一个 entry
,单页面应用(SPA) 只需要一个 entry
,多页面应用(MPA) 则需要配置多个 entry
。
entry
除了接收字符串,也接收对象,在对象中可以配置多个 entry
,每个 entry
也支持很多配置项。例如:
js
entry: {
home: './home.js',
catalog: {
import: './catalog.js',
filename: 'pages/catalog.js',
dependOn: 'home',
chunkLoading: false
}
home 入口文件就是最普通的写法。
catalog 入口配置:
import:指定入口文件路径。
filename:执行生成文件的文件名和路径。
dependOn :表示该入口依赖于 home 入口。
chunkLoading :禁用按需加载的 chunks ,并将所有内容放在主 chunk 中。
output
output
包括了一组选项,指示 webpack 如何去输出 、以及在哪里输出 你的「bundle
、asset
和其他你所打包或使用 webpack 载入的任何内容」。
js
output: {
// The build folder.
path: paths.appBuild,
// Add /* filename */ comments to generated require()s in the output.
pathinfo: isEnvDevelopment,
// There will be one main bundle, and one file per asynchronous chunk.
// In development, it does not produce real files.
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/bundle.js',
// There are also additional JS chunk files if you use code splitting.
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
assetModuleFilename: 'static/media/[name].[hash][ext]',
// webpack uses `publicPath` to determine where the app is being served from.
// It requires a trailing slash, or the file assets will get an incorrect path.
// We inferred the "public path" (such as / or /my-project) from homepage.
publicPath: paths.publicUrlOrPath,
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: isEnvProduction
? info =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
}
来看看脚手架中的 output
是如何配置的。
path :指定 webpack 打包结果输出的路径,该值是一个绝对路径。
pathinfo :在 bundle
中引入 所包含模块信息 的相关注释。该值在开发环境下为 true
,在生产环境下为 false
。
filename :指定每个 bundle
文件的名称。
其中,[name]
表示入口的名称,[contenthash:8]
表示根据内容生成的哈希值的前8位。
chunkFilename :指定每个 chunk
文件的名称,另外的命名规则同 filename
。
assetModuleFilename :指定静态资源文件的名称,命名规则同上。
publicPath :publicPath
是一个很重要的配置项,网页在浏览器中加载时便会根据 publicPath
的值去服务器请求资源。
在单页面应用 中,所有的页面都是通过 JavaScript 动态加载 的。因此,当用户访问一个特定的 URL 时,服务器只会返回一个 HTML 文件,而不是每个页面的完整 HTML 文件 。像是 js 脚本文件、 css 样式文件以及图片、视频等资源文件则是通过 publicPath
提供的路径去进行加载的。
举个例子:前端页面编译后的包放在了服务器的 data 文件夹下,经过代理后,对应的域名为 "https:// xxx. project. com/" 。此时 publicPath
的值为 /
,即从项目的根目录 data
文件下加载资源。如果相关资源放在了 data/home
文件夹下,那么 publicPath
的值就应该为 /home/
,这样才能正确地加载资源。
devtoolModuleFilenameTemplate :用于指定生成 Source Map 时每个模块的文件名。它的值是一个字符串,可以包含多个占位符,这些占位符将根据实际情况替换成相应的值。
具体来说,当 webpack 构建时,每个 JavaScript 模块都会被转换成对应的 bundle 文件 ,同时 webpack 也会生成一个 Source Map 文件,用于将编译后代码映射回原始的源代码。在这个过程中,webpack 需要为每个模块生成一个文件名,以便在 Source Map 中正确地标识出每个模块的位置。
cache
缓存生成的 webpack 模块和 chunk,来改善构建速度。
js
cache: {
type: 'filesystem',
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)
),
},
}
type :设置缓存的类型,可以设置为 memory
或 filesystem
。
memory
意为缓存在内存中,不支持更多的配置。
与计算机的内存和硬盘有关,缓存在内存 中访问速度较快 ,但每次构建都会重新生成,无法实现持久化 ;缓存在文件系统中,也即为硬盘 中,访问速度较慢 ,但可以持久化缓存。
version :设置缓存的版本。这里用环境变量的哈希值作为缓存的版本,使得环境变量没有发生变化的情况下就会读取缓存中的数据。
cacheDirectory:设置缓存数据文件的路径。
store :拥有唯一且默认的值 pack
。意为当编译器空闲时,将缓存数据都存放在一个文件中。
buildDependencies :编译构建的依赖项,当依赖项发生变化时,缓存数据就会失效,webpack 将重新进行编译。
defaultWebpack
默认引入 webpack 内部模块作为依赖项,当模块发生变化时,会触发重新编译。
config
设置为 [__filename]
意为获取最新的 webpack 配置,当配置发生变化时会触发重新编译。
tsconfig
指定了 tsconfig.json
和 jsconfig.json
发生变化时会触发重新编译。
infrastructureLogging
控制基础设施的日志输出。
和上文 stats
选项类似,但仅仅是对于基础设施而言。
webpack 中的基础设施,通常指的是构建工具本身所提供的核心功能和基本架构,包括但不限于模块解析、资源管理、代码转换、插件系统等。基础设施是构建工具的基础框架,用于支持项目的构建和打包过程。
optimization
优化。从 webpack 4 开始,会根据你选择的 mode
来执行不同的优化, 不过所有的优化还是可以手动配置和重写。
js
optimization: {
minimize: isEnvProduction,
minimizer: [
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,
},
},
}),
new CssMinimizerPlugin(),
],
},
minimize :告知 webpack 使用 TerserPlugin
或其它在 optimization.minimizer
定义的插件压缩 bundle
。
minimizer :提供一个或多个定制过的 TerserPlugin
实例,覆盖默认压缩工具。
在配置中,ecma
配置项表示 JavaScript 的语法标准,例如 ES5, ES8。
TerserPlugin
的作用是将 js 代码进行压缩和混淆。
CssMinimizerPlugin
用于压缩 CSS 代码,它可以将重复的样式合并,并删除未使用的样式。
resolve
resolve
选项用于如何解析模块的路径。
js
resolve: {
modules: ['node_modules', paths.appNodeModules].concat(
modules.additionalModulePaths || []
),
extensions: paths.moduleFileExtensions
.map(ext => `.${ext}`)
.filter(ext => useTypeScript || !ext.includes('ts')),
alias: {
'react-native': 'react-native-web',
...(isEnvProductionProfile && {
'react-dom$': 'react-dom/profiling',
'scheduler/tracing': 'scheduler/tracing-profiling',
}),
...(modules.webpackAliases || {}),
},
plugins: [
new ModuleScopePlugin(paths.appSrc, [
paths.appPackageJson,
reactRefreshRuntimeEntry,
reactRefreshWebpackPluginRuntimeEntry,
babelRuntimeEntry,
babelRuntimeEntryHelpers,
babelRuntimeRegenerator,
]),
],
},
modules:告知 webpack 在解析模块时从什么地方搜索。
这里可以接收绝对路径或相对路径。接收绝对路径 时,会将搜索范围限定在传入的目录中;接收相对路径时,会依次从当前目录、父级目录...文件系统、环境变量路径 NODE_PATH 进行搜索。
extensions:接收一个后缀名数组,webpack 会按照数组的顺序对文件进行解析。这使得我们在项目中导入一个模块时可以省略其后缀名。
如果导入文件时有多个名字相同但后缀不同的文件,webpack 将会解析位于首位的后缀名的文件并忽略其它文件。
alias:用于创建别名,可以将模块引入时的路径映射到指定的目录,简化模块引入的路径。
例如,可以将 src
目录 设置为 @
,就变成了如下所示:
js
import pic_a from "../../src/image/pic_a.png"; // 设置别名前
import pic_a from "@/image/pic_a.png"; // 设置别名后
plugins:设置额外的用于模块解析的插件。
我们在这里使用了 ModuleScopePlugin
插件,它的作用是限制模块的查找路径,只允许从指定目录中导入文件。在 webpack 解析模块路径时,如果路径不符合要求,则会抛出错误,从而防止开发者在代码中导入一些不应该被导入的文件。
module
module
用于设置如何加载不同的模块。
上面的 resolve
是用于解析模块路径,解析完之后的下一步便是加载模块。
rules
设置模块加载时的规则,接收一个数组。这些规则能够对模块应用 loader 或修改解析器(parser),决定了模块的加载方式。
每个规则包含三个要素:条件 (condition)、结果 (result)和嵌套规则(nested rules)。
条件(condition)
条件分为两大类:一是需要匹配的资源文件 resource ,二是需要匹配的请求文件 issuer。
例如:从 app.js 导入 './style.css',resource 是 /path/to/style.css, issuer 是 /path/to/app.js。
像 test, include, exclude, resource
属性是用于匹配资源文件的。issuer
属性是用于匹配请求文件的。这些属性通常接收正则表达式作为参数。
结果(result)
结果的含义是:对模块的处理结果。所以其实这个部分是决定如何处理模块的。
处理类型有两种:一是 loader,二是 parser。
loader 将各种资源文件转换成 webpack 可以处理的模块,转换为可以导入应用程序的模块。
parser 负责分析和解析 模块的源代码 ,将其转换为抽象语法树(AST),然后 webpack 使用这个 AST 来执行各种操作,比如代码压缩、依赖分析等。
嵌套规则(nested rules)
嵌套规则表示在 Rule 中可以包含子 Rule。用于对特定条件下的模块应用不同的 loader 和 parser,提高构建的灵活性和效率。
具体的配置代码由于篇幅的原因就不放了,大致上就是对图片文件、js 文件和样式文件做处理,应用了一些具体的 loader 和 parser 配置就不一一展开说明了。
plugins
plugins
用于自定义 webpack 构建的过程。
webpack 本身有一些内置的插件,可以通过 webpack.[plugin-name]
访问这些插件。更多的是一些第三方的插件,像安装依赖一样安装使用它们。
下面介绍一些常见的插件:
HtmlWebpackPlugin
生成一个 index.html
作为项目的入口文件,webpack 构建生成的资源文件(js、css文件)将会被注入这个 html 文件。
同时可以指定一个 html 文件的模板,并对生成的 html 文件进行压缩、优化等操作。
webpack.DefinePlugin
webpack 内置的一个插件,用于在编译过程中创建全局常量。它可以将用户定义的全局常量注入到代码中,以在运行时进行替换。
我们可以根据项目环境的不同注入对应的全局常量,例如前文提到的 process.env.NODE_ENV
便需要在此处进行注入。
ReactRefreshWebpackPlugin
用于在开发环境下提供 React 组件的热重载(Hot Module Replacement)功能。可以在修改 React 组件代码后,实现无需完全刷新页面即可更新组件的变化。
MiniCssExtractPlugin
用于将 CSS 文件从 JavaScript 文件中提取出来,单独生成一个独立的 CSS 文件 。这样做的好处是可以将样式和脚本分离,使得代码更易于维护、加载更快,并且可以利用浏览器的并行加载机制提高页面性能。
WebpackManifestPlugin
用于生成 manifest 文件,其中包含了编译后文件的映射关系,如文件名、路径等信息。开发者可以轻松地查看每个模块对应的输出文件,并且在构建过程中保持这种映射关系。
webpack.IgnorePlugin
用于忽略特定的模块或目录,以减少打包后的文件体积。
通常情况下,webpack 会将项目中引入的所有模块都打包到最终的输出文件中,通过配置 IgnorePlugin
,可以告诉 webpack 在构建过程中忽略某些特定的模块。
ESLintPlugin
用于在构建过程中集成 ESLint 静态代码检查工具。通过使用 ESLintPlugin,可以在每次构建时自动运行 ESLint 检查,并将检查结果输出到终端或者以文件形式保存。
总结
在这篇文章中,我们了解了在项目构建时,webpack 是如何工作的,以及相关的配置代码。