「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战」。
webpack原理解析
5个阶段: 初始化参数—— 编译准备—— 模块编译—— 完成编译 —— 输出文件
1、初始化参数
从 webpack.config.js
中读取对应配置参数,还有shell命令传入的参数,进行合并最终打包配置参数
webpack.config.js传参
shell命令传参
- 通过process.argv.slice(2)来获得
合并参数
- index.js文件作为核心入口文件
- webpack.js文件作为webpack()方法的实现文件
2、编译准备
调用webpack()方法返回compiler方法,创建compiler对象; 注册各个webpack plugin,找到配置入口中的entry代码,调用compiler.run()方法进行编译。
创建compiler对象
编写plugin
通过hooks.tap注册webpack插件
任何一个webpack插件都是一个类(当然类本质上都是funciton的语法糖,一般plugin插件都是一个类),每个插件都必须存在一个apply方法。
关于webpack插件本质上就是通过发布订阅的模式,通过compiler上监听事件。然后再打包编译过程中触发监听的事件从而添加一定的逻辑影响打包结果。
寻找entry入口
getEntry方法获得各个入口的对象
通过options.entry处理获得入口文件的绝对路径
-
this.hooks.run.call()
通过this.hooks.run.call()执行关于run的所有tap监听方法,从而触发对应的plugin逻辑
-
this.rootPath
-
entry处理
通过getEntry方法获得了一个key为entryName,value为entryAbsolutePath的对象了,接下来就从入口文件出发进行编译流程
3、模块编译
从入口出发,读取入口文件内容调用匹配loader处理入口文件。 通过babel分析依赖,并且同时将所有依赖的路径更换为相对于项目启动目录options.context的路径。 入口文件中如果存在依赖的话,递归上述步骤编译依赖模块。 将每个依赖的模块编译后的对象加入this.modules。 将每个入口文件编译后的对象加入this.entries。
分析入口文件
- buildEntryModule方法
- 读取文件内容
- 调用loader处理匹配后缀文件(loader本质上就是一个函数)
- 使用loader处理文件
递归处理
4、完成编译
递归进行模块编译工作完成之后,每个引用模块通过loaders处理得到模块之间的相互依赖关系,组合最终输出的chunk模块
得到chunk模块
- name
- entryModules
- modules
5、输出文件
整理模块依赖关系,将处理后的文件输出到output的目录中
配置文件
Webpack配置文件
webpack.config.js
学习地址: [万字长文,带你从零学习 Webpack] mp.weixin.qq.com/s/2rUjUM6Zf…
entry和output
-
entry
可以是字符串、数组或者对象类型
- 单入口单输出:字符串
- 多入口单输出:数组
- 多入口多输出:对象
// 多入口:
entry: {
index: "./src/index.js", // chunkName为index
main: "./src/main.js" // chunkName为main
}
// 多输出:
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js' // [name]占位符会自动替换为chunkName
},
- output
必须是对象类型
① path(导出路径,绝对路径) ② filename(bundle文件名字)
mode
如果不在webpack.config.js 设置,也可以在命令行中设置
// package.json
"scripts": {
"dev": "webpack --mode development",
"build": "webpack --mode production"
}
-
none
-
production(生产)
会开启代码压缩和代码性能优化的插件,从而打包出来的文件也相对none和development小很多
-
development(开发)
会默认开启一些有利于开发调试的选项,比如NamedModulesPlugin和NamedChunksPlugin,分别是给module和chunk命名的
devtool
// 开启source-map
devtool: "source-map"
打包后,你的dist中就会多了一个main.js.map文件
-
none
-
eval (不建议生产环境中使用)
-
cheap-source-map
生成sourceMap,但是没有列映射,则只会提醒是在代码的第几行,不会提示到第几列
-
inline-source-map
会生成sourceMap,但不会生成map文件,而是将sourceMap放在打包文件中
-
source-map
module
webpack的入口文件只能处理JS文件,json文件,对于其他类型的文件需要通过loaders处理。
loaders配置就是写在module选项里面,rules属性(对象数组)
- test和use
module: {
rules: [
{
test: /\.css$/, // 识别css文件
use: ['style-loader', 'css-loader'] // 对css文件使用的三个loader 从后往前即从右到左执行
}
]
}
plugins
使用第三方插件来实现打包优化、资源管理、注入环境变量等任务。 数组形式
plugins: [
new htmlWebpackPlugin(),
new CleanWebpackPlugin(),
new miniCssExtractPlugin(),
new TxtWebpackPlugin()
]
- html-webpack-plugin (HTML模板)
plugins: [
new htmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
chunks: ["index"] // 只引入index chunk
})
]
- clean-webpack-plugin (清理打包路径)
plugins: [
new CleanWebpackPlugin({
// dry: true // 打开可测试,不会真正执行删除动作
cleanOnceBeforeBuildPatterns: [
'**/*', // 删除dist路径下所有文件
`!package.json`, // 不删除dist/package.json文件
],
}),
]
- webpack-dev-server (webpack本地服务)
// webpack会默认开启http://localhost:8080/服务
// 在webpack.config.js 中devserver选项中配置
devServer:{
port: 8888, // 自定义端口号
open:true, // 自动打开浏览器
proxy:{
'/api': 'http://localhost:3000'
}, // 设置跨域代理
// 或者
proxy:{
'/api': {
target: 'http://localhost:3000', // 代理地址
pathRewrite: { '^/api': '' }, // 重写路径
secure: false, // 使用https
changeOrigin: true // 覆盖主机源
},
},
- mini-css-extract-plugin (单独导出css文件)
plugins: [
// 使用miniCssExtractPlugin插件
new miniCssExtractPlugin({
filename: "[name].css" // 设置导出css名称,[name]占位符对应chunkName
})
]
// 此时我们module中rules设置也需要更改:
module: {
rules: [
{
test: /\.css$/,
// 使用miniCssExtractPlugin.loader替换style-loader
use: [miniCssExtractPlugin.loader,'css-loader']
}
]
},
-
autoprefixer (兼容浏览器)
-
cssnano (压缩css代码)
-
html-withimg-loader (打包后图片引入问题)
// 首先在rules添加:
{ test: /\.html$/,loader: 'html-withimg-loader' }
//因为html-loader使用的是commonjs进行解析的,而url-loader默认是使用esmodule解析的,所以在url-loader还需要设置:
esModule: false, // 不适用esmodule解析
loaders
-
css预处理器
sass和sass-loader
-
静态资源处理
url-loader和file-loader
其实url-loader默认会将静态资源转成base64,提供了一个属性 ‘limit’, 当文件超过设置的大小就会打包成文件输出。
module: {
rules: [
{
test: /\.(png|je?pg|gif|webp)$/,
use: [{
loader: 'url-loader',
options: {
name: '[name].[ext]', // 使用占位符设置导出名称
limit: 1024 * 10 // 设置转成base64阈值,大于10k不转成base64
}
}]
}
]
}
- 静态资源直接打包成文件
{
test: /\.(png|jpe?g|gif|svg|eot|ttf|woff|woff2)$/i,
type: "asset/resource",
}
type设置: asset/resource: 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。 asset/inline : 导出一个资源的 data URI。之前通过使用 url-loader 实现。 asset/source:导出资源的源代码。之前通过使用 raw-loader 实现。 asset: 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现
- JavaScript转义 —babel
// webpack.config.js文件
{
test: /\.js$/,
use: ['babel-loader']
}
// .babelrc (babel配置文件)
{
"presets": [
[
"@babel/preset-env",
{
// 浏览器版本
"targets": {
"edge": "17",
"chrome": "67"
},
// 配置corejs版本,但需要额外安装corejs
"corejs": 3,
// 加载情况
// entry: 需要在入口文件进入@babel/polyfill,然后babel根据使用情况按需载入
// usage: 无需引入,自动按需加载
// false: 入口文件引入,全部载入
"useBuiltIns": "usage"
}
]
]
}
哈希值
作用:我们打包文件的文件名都需要带上一个哈希值,这会给我们的好处就是避免缓存
-
hash策略
项目文件变化,css,js文件都会变化; 静态文件(图片...)变化,其他的js,css也会变化
-
chunkhash策略
而chunkhash策略的话,是以chunk为单位的,如果一个文件发生变化,只有那条chunk相关的文件的打包文件文件名才会发生变化
注意:chunkhash不适用于静态文件,因此静态文件依旧使用hash。
-
contenthash策略
以自己内容为单位,自己变化或者引用了自己的文件变化了才会发生变化
多个打包配置
- webpack.conf.js
// 安装
webpack --config webpack.conf.js
// package.json
"scripts": {
"dev": "webpack --config webpack.dev.js",
"build": "webpack --config webpack.prod.js",
}
- mode设置
// argv.mode可以获取到配置的mode选项
module.exports = (env, argv) => {
if (argv.mode === 'development') {
// 返回开发环境的配置选项
return { ... }
}else if (argv.mode === 'production') {
// 返回生产环境的配置选项
return { ... }
}
};
优化一下Webpack配置
合理的配置mode选项和devtool选项
缩小文件搜索范围
- alias选项
// webpack.config.js
const path = require('path');
module.exports = {
...
resolve: {
alias: {
// 配置style路径的别名
style: path.resolve(__dirname, 'src/style/')
},
}
};
include、exclude选项
当我们使用loader的时候,我们可以配置include来指定只解析该路径下的对应文件,同时我们可以配置exclude来指定不解析该路径下的对应文件
noParse选项
我们可以在module.noParse选项中,只配置不需要解析的文件。通常我们会忽略一些大型插件从而来提高构建性能
使用HappyPack开启多进程Loader
使用webpack-parallel-uglify-plugin 增强代码压缩
配置缓存
- cache
// 开发环境
module.exports = {
cache: {
type: 'memory' // 默认配置
}
}
// 生产环境
module.exports = {
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
}
}
分析打包文件大小
- webpack-bundle-analyzer插件
手写系列
官方文档:webpack.js.org/contribute/…
loaders
本质是函数
loader的编写原则: 单一原则:每个loader只做一件事情;
链式调用:webpack会按照顺序链式去调用每个loader;
统一原则:遵循webpack定制的设计规则和结构,输入和输入均为字符串,每个loader完全独立,即插即用。
※ loader的实现就不能使用箭头函数
注意
需要在webpack引入,并且需要配置一下resolveLoader选项,因为webpack默认只会去node_modules搜索loader
module.exports = {
......
resolveLoader: {
// 添加loader查询路径
modules: ['node_modules', './myLoaders']
},
module: {
rules: [{
test: /\.(scss|sass)$/,
// 使用自己的loader
use: ['ou-style-loader','ou-css-loader','ou-sass-loader']
}]
}
};
plugins
本质是类(类是语法糖)
plugin是可以实现在webpack生命周期中的合适时机通过Webpack提供的API去实现一些动作。
一个plugin是一个类,并且里面会有一个apply函数,而在apply函数中会接收到一个compiler参数,里面包含了关于webpack环境所有的配置信息
module.exports = class MyPlugin {
apply (compiler) {
compiler.hooks.compile.tap("MyPlugin", (compilation) => {
console.log(compilation)
})
}
}
webpack
Webpack是一个类
webpack打包的一个逻辑: webpack是直接拿到js文件的代码,即字符串。然后通过eval()函数执行代码; webpack会从入口文件开始,不断递归遍历引入模块,然后保持在一个对象里面,key值为moduleId,即模块路径,而value是模块的相关代码。 webpack会将代码转换为commonJS,即使用require去引入模块,同时它自身会去封装一个require函数,去执行入口文件代码。