Compiler 对象包含了当前运行Webpack的配置,包括entry、output、loaders等配置,这个对象在启动Webpack时被实例化,而且是全局唯一的。
Compilation对象代表了一次资源版本构建
一、webpack怎么拆包
webpack 会根据模块依赖图的内容组织分包 —— Chunk 对象,默认的分包规则有:
-
同一个 entry 下触达到的模块组织成一个 chunk -
异步模块单独组织为一个 chunk -
entry.runtime 单独组织成一个 chunk默认规则集中在 compilation.seal 函数实现,seal 核心逻辑运行结束后会生成一系列的 Chunk、ChunkGroup、ChunkGraph 对象,后续如 SplitChunksPlugin 插件会在 Chunk 系列对象上做进一步的拆解、优化,最终反映到输出上才会表现出复杂的分包结果。
拆包的方式:
- entry设置多入口文件
重点:seal 阶段遍历 entry 对象,为每一个 entry 单独生成 chunk,之后再根据模块依赖图将 entry 触达到的所有模块打包进 chunk 中。
初始化完毕后,Webpack 会读取 ModuleDependencyGraph 的内容,将 entry 所对应的内容塞入对应的 chunk (发生在 webpack/lib/buildChunkGrap.js 文件);
Main.js 以同步方式直接或间接引用了 a/b/c/d 四个文件,分析 ModuleDependencyGraph 过程会逐步将 a/b/c/d 模块逐步添加到 chunk[main] 中,
- 按需加载
重点:分析 ModuleDependencyGraph 时,每次遇到异步模块都会为之创建单独的 Chunk 对象,单独打包异步模块。
Webpack 4 之后,只需要用异步语句 require.ensure("./xx.js") 或 import("./xx.js") 方式引入模块,就可以实现模块的动态加载,这种能力本质也是基于 Chunk 实现的。
- runtime分包
重点: Webpack 5 之后还能根据 entry.runtime 配置单独打包运行时代码。
webpack 5之后还支持基于 runtime 的分包规则
编译时,Webpack 会根据业务代码决定输出那些支撑特性的运行时代码(基于 Dependency 子类),例如:
-
需要 __webpack_require__.f、__webpack_require__.r 等功能实现最起码的模块化支持 -
如果用到动态加载特性,则需要写入 __webpack_require__.e 函数 -
如果用到 Module Federation 特性,则需要写入 __webpack_require__.o 函数 -
等等
module.exports = {
entry: {
index: { import: "./src/index", runtime: "solid-runtime" },
}
};
Webpack 执行完 entry、异步模块分包后,开始遍历 entry 配置判断是否带有 runtime 属性,如果有则创建以 runtime 值为名的 Chunk,因此,上例配置将生成两个chunk:chunk[index.js] 、chunk[solid-runtime],
-
Bundle splitting: 为了更好的缓存,可以将一个大文件分割成更多,更小的文件。
通过设置optimization中的splitchunks来实现
module.exports = { entry: path.resolve(__dirname, 'src/index.js'), output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash].js' }, optimization: { splitChunks: { chunks: 'all', } } };增加的optimization.splitChunks.chunks = 'all',会把引用的第三方模块(node_modules)全部打包到vendor.js。
vendors.js同样会遇到与main.js文件相同的问题,对其一部分进行更改意味着重新下载整个vendor.js。
const path = require('path'); const webpack = require('webpack'); module.exports = { entry: path.resolve(__dirname, 'src/index.js'), plugins: [ new webpack.HashedModuleIdsPlugin(), // so that file hashes don't change unexpectedly ], output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash].js', }, optimization: { runtimeChunk: 'single', splitChunks: { chunks: 'all', maxInitialRequests: Infinity, minSize: 0, cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name(module) { //获取每个npm包的名称 const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]; //对npm的包名子添加前缀,并去掉@ return `npm.${packageName.replace('@', '')}`; } } } } } }一个vue-cli生成的项目的打包结果将会是:
dist/npm.vue.44c71c1a.js dist/npm.vue-router.0290a1da.js dist/npm.core-js.028dc51e.js dist/npm.vuex.6946f3d5.js dist/app.e76cff0a.js dist/runtime.4e174d8a.js dist/npm.vue-loader.611518c6.js dist/about.16c4e81c.js dist/npm.webpack.034f3d3d.js dist/css/app.ab586865.css下面重点介绍一下cacheGroups
cacheGroups是splitChunks里面最核心的配置。splitChunks根据cacheGroups拆分模块,之前说的chunks以及其他属性都是对缓存组进行配置的。splitChunks默认有两个缓存组,vendor-加载内容来源node_modules,另一个是default。
二、webpack的动态加载原理
感谢: https://zhuanlan.zhihu.com/p/159216534 https://juejin.cn/post/6952703369135800350#heading-1
三、webpack的tree-shaking
理解:tree-shaking指的是消除没被引用的模块代码,减少代码体积大小,以提高页面的性能。
原理: ES Module 最大的特点就是静态化,在编译时就能确定模块的依赖关系,以及输入和输出的值,这意味着什么?意味着模块的依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,正是基于这个基础,才使得 Tree-Shaking 成为可能
条件:
-
模块系统必须为ESmodule。原因:
ESModule是静态的,依赖关系在编译时就确定了。而commonjs是node环境默认的模块系统,是动态的。而webpack只会在编译时做处理,所以只有es模块才行
但是,在实际项目中,在进入webpack优化前,我们一般会用一系列loader对源码进行处理,其中包括神一样的babel-loader。我们按照以下的babel配置: 同样是上面的源码,webpack打包后,我们可以看到tree-shaking又失效了
问题是preset为env时,模块转换默认为cjs。我们把babel配置为以下即可:
{
"presets": [
[ "env",{modules:false}]
]
}
对于ts项目,一般我们都会使用ts-loader来处理ts文件,对于ts,同样,我们需要在tsconfig.json中的module设置为'esnext'或者'es2015':
{
"compilerOptions": {
module:'esnext'
}
}
-
在package.json文件标识sideEffect字段。webpack对有副作用的模块,都不会进行tree-shaking,甚至是代码混淆! 。
那么在webpack中,还有哪些模块会被认为是有副作用的呢?
1. 含有eval也被认为是不纯的
2. 在顶级作用域中对用一个属性赋值两次
-
把webpack设置为生产环境生产环境下,webpack默认开启了tree-shaking.有关tree-shaking的配置如下:
optimization: { providedExports: false,//会在webpack编译过程中标识模块的哪些方法是被导出的 usedExports: true,//会在webpack编译过程中标识模块的哪些方法是被导出的 sideEffects: true,//允许在package.json中标志没有作用的文件 minimizer: [ new TerserPlugin({ //把无用的代码remove掉。 cache: true, parallel: true }) ] }, -
存在的问题:1、tree-shaking对第三方库不管用
2、无法shake掉class中没被引用的方法
感谢: https://juejin.cn/post/6956522989810614308 https://juejin.cn/post/6955383260759195678
四、webpack的性能优化
五、怎么写loader
六、怎么写plugin
https://juejin.cn/post/6955421936373465118#heading-31
七、webpack5 VS webpack4
八、webpack的热替换原理
https://juejin.cn/post/6973825927708934174
九、webpack的打包原理
感谢:
https://juejin.cn/post/6960924869307400200#heading-10
十、webpack的模块化原理
- 通过一个webpack_module对象来存储模块化代码
1. key为文件名称
2. value为文件代码
- 通过webpack_module_cache来缓存模块化代码
- 通过webpack_require来从webpack_module_cache或webpack_module中读取并从引入代码
十一、source-map的原理
定义:保存源代码映射关系的文件
source Map 文件大概长:
{
"version": 3,
"sources": [//转换前的文件
"webpack://webpack5-template/./src/index.js"
],
"names": [],
"mappings": ";;;;;AAAA;AACA;AACA;AACA;AACA,eAAe;AACf;AACA;AACA,mB",//记录位置信息的字符串。这个的话,用到了 VLQ 编码相关
"file": "main.bundle.js",
"sourcesContent": [
"console.log('Interesting!!!')\n// Create heading node\nconst heading = document.createElement('h1')\nheading.textContent = 'Interesting!'\nconsole.log(a); // 这一行会报错\n// Append heading node to the DOM\nconst app = document.querySelector('#root')\napp.append(heading)"
],
"sourceRoot": ""
}
重要的字段:version,sources,mappings
- source map。产生 .map 文件(配合 eval 或者 inline 使用的时候,会不生成 source map 文件,具体要看哪个模式)
- eval。使用 eval 包裹块代码
- cheap。不生成列信息
- inline。将 .map 作为 DataURI 嵌入,不单独生成一个 .map 文件
- module。包含 loader 的 source map
对于开发环境,eval, eval-source-map, eval-cheap-source-map, eval-cheap-module-source-map 等都是可以的。个人推荐使用 eval-cheap-module-source-map
开发环境:
将你的服务器配置为,不允许普通用户访问 source map 文件!你不应将 source map 文件部署到 web 服务器。而是只将其用于错误报告工具。
大致流程:
- webpack 构建的时候将原始 JS 和 source map 文件都上传到我们的监控平台
- js 错误堆栈收集,通过 window.onerror 来捕获 js 报错,然后上报到服务器,以用来收集用户使用时候发生的 bug
- 解析 JS 错误,映射源文件堆栈
- 通过 sourcemap 查找原始报错信息,可以使用source-map
- 监控平台展示
十一、webpack5(Module Federation)实现微前端
十二、webpack打包的babel处理
感谢: https://juejin.cn/post/6968027732504477726