关于webpack零零散散总结了很长一段时间了,今天才来梳理成文,里面主要包含了如下知识点:
- webpack怎么配置(区分打包环境、启动热更新、动态导入、模块解析等)
- source map包含哪些?开发环境和生产环境怎么选择?
- tree-shaking怎么配置
- 缓存机制设置等
- webpack4和webpack5的区别
- 热更新HRM原理
- webpack构建流程
- webpack打包文件详解
集中其他相关篇幅:
- plugin和loader的区别;
- 怎么写plugin?其原理;
- 怎么写loader?其原理;
- 常用的plugin和loader;
希望对大家有帮助,同时发现错误欢迎指正,谢谢!!!
一、webpack进阶配置
细节可参考官网,以下是一些重要点提取出来 概念 | webpack 中文网 (webpackjs.com)
资源模块(Asset Modules,webpack5新引入的的)
功能:
- 是一种 模块类型,它允许使用资源文件,而无需配置额外loader。
- 资源文件:字体、图片、图标、HTML。。。
- 不用file-loader、url-loader也能加载图片和字体。
webpack4操作:
- raw-loader :将文件导入为字符串
- file-loader: 将文件发送到输出目录
- url-loader:将文件发送到输出目录,或转为Data URI(base64)内联到bundle中
webpack5操作:
- asset/resource:发送一个单独的文件并导出URL(之前通过使用file-loader实现)
- asset/inline:导出一个资源的data URI (之前通过使用url-loader实现)
- asset/source:导出资源的源代码(之前通过使用raw-loader 实现)
- asset:在导出一个data URI 和发送一个单独的文件之间自动选择(url-loader)
Webpack Dev Serve
作用:发布web服务,提高开发效率
webpack4:webpack-dev-server ...
webpack5:webpack server...
webpack4热更新:
hot:true
webpack5热更新:
webpack5新加的
liveReload:true (不能再使用hot)
target: 'web' (热更新只适用于web相关的targets)
proxy配置接口代理
changeOrigin:true
区分打包环境
- 通过环境变量区分
- webpack --env.production
- webpack.config.js中判断env
- 通过配置文件区分
- webpack.dev.conf.js
- webpack.prod.conf.js
- webpack.base.conf.js (公共配置)
webpack-merge 将多个配置合并在一起
命令行中设置环境变量
- webpack4:webpack --env.production
- webpack5:webpack --env production (没有点)
webpack.config.js
- 读取环境变量env.production
- 根据环境变量指定不同的配置
webpack.config.js
module.exports = (env, argv) => {
cosnt config = {mode:'development'}
if(env production){
config.mode = 'production'
...
}
return config
}
提取公共模块
optimization: {
splitChunks: {
chunks:'all'
}
}
动态导入
懒加载:默认不加载,事件触发后才加载。
webpackChunkName: '加载名称'
document.getElementId('btn').onclick = function(){
//import 启动懒加载
//webpackChunkName: 'desc' 指定懒加载的文件名称
//webpackPrefetch: true 启动预加载
import(/*webpackChunkName: 'desc', webpackPrefetch: true */'test').then(()=>{
console.lo('此处才加载了test文件,才能调用它文件中的东西')
})
}
预加载:先等待其他资源加载,浏览器空闲时,再加载 webpackPrefetch: true 缺点:在移动端有兼容性问题
源码映射(source map)
映射模式(devtool的值)
- 不同映射模式的报错定位效果和打包执行速度不同
- webpack4中,一共有13种不同的映射模式
- webpack5中,一共有26种不同的映射模式
webpack5中的命名更新严格
^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$
模式 | 描述 |
---|---|
cheap | 只定位错误,不定位报错列 |
hidden | 生成.map文件,但是.js的末尾没有关联.map,报错后需手动关联,然后定位报错 |
inline | 不生成.map文件,映射以base64-VLQs的形式添加到.js最后 |
eval | 不生成.map文件,映射信息追加到eval函数的最后,来关联处理前后的对应关系 |
module | 不但映射工程师自己写的代码,还支持对loader和第三方模块的映射 |
nosources | 生成.map中不包含sourceContent,定位错误时看不到源码(更安全) |
webpack4的13个常见模式
webpack5 有26种模式虽然更丰富,但是关键词不变,多了几个排列组合, 虽然模式多,但是很多模式现在还没效果,webpack5更新内容多,是为了以后做铺垫的。
如何选择合适的映射模式(这是个人建议,但是不绝对)
- 开发环境:eval-cheap-module-source-map
- 生产环境:none | nosources-source-map
如果想在生成环境也想看到错误定位,也可以在生成环境使用 cheap-module-source-map
这样选择的原因:
- eval的rebuild速度快,因此我们可以在本地环境中增加eval属性。
- 使用eval-source-map会使打包后的文件太大,因此在生产环境中不会使用
tree-shaking
Tree-shaking 较早由 Rich_Harris 的 rollup 实现,后来,webpack2 也引入了tree-shaking 的功能,本质是消除无用的JavaScript代码,(支持CSS消除吗?我觉得不支持,CSS又不是ES Modules规范)。
tree-shaking原理:
将所有代码打包到一个作用域下,然后遍历所有作用域,去除没使用的作用域(webpack原理:遍历所有引入模块,把它们打包成一个文件,在这个过程中,就知道哪些export的模块被使用到)
基于ES6的静态引用,treeshaking通过扫描所有ES6的export,找出被import的内容并添加到最终代码中。
注意点:
- 使用ES Modules规范的模块,才能执行tree-shaking。因为tree-shaking依赖于ES Modules的静态语法分析
- 不能删除立即执行函数,避免使用IFEE
- 如果使用第三方的模块,可以尝试直接从文件路劲引用的方式使用
Tree shaking为什么用ES6:
因为ES6模块的出现,ES6模块依赖关系是确定的,和运行时的状态无关
,可以进行可靠的静态分析
,这样方便甩掉重复代码,这就是Tree shaking的基础。
如何使用:
- 生产模式:tree-shaking会自动开启
- 开发模式:
- usedExports
- sideEffects
1. usedExports
optimization: {
//标记未被使用的代码,打包后的未使用的代码会带上注释/*unused harmony export xxxx*/
usedExports:true,
//删除unused harmony export xxxx标记的代码
minimize:true,
//terser-webpack-plugin压缩插件:webpack4需要单独安装,webpack5无需安装,但需要引入
minimizer: [new TerserPlugin()]
}
optimization.usedExports (标记没用的代码) ,打包后的未使用的代码会带上注释/*unused harmony export xxxx*/
optimization.minimize:true (删除unused harmony export xxxx标记的代码)
terser-webpack-plugin (去掉项目多余的debuger)
webpack4需要单独安装terser-webpack-plugin (webpack5无需安装,但需要引入)
Tree Shaking与 Source Map存在兼容性问题:
Tree Shaking仅仅支持
devtool: source-map | inline-source-map | hidden-source-map | nosources-source-map;
因为eval模式,将JS输出为字符串 (不是ES module规范),导致Tree Shaking失效
2. sideEffects 副作用
无副作用:如果一个模块单纯的导入导出变量,那它就无副作用
有副作用:如果一个模块还修改其他模块或者全局的一些东西,就有副作用
- 修改全局变量
- 在原型上扩展方法
- css的引入(比如作用于html)
sideEffects的作用:把未使用但无副作用的模块一并删除
对于没有副作用的模块,未使用代码不会被打包(相当于压缩了输出内容)
开启副作用:
optimization: {
sideEffects:true
}
标识代码是否有副作用(在package.json中设置sideEffects):
- true: 所有代码都有副作用
- false: 所有代码都没有副作用(告诉webpack可以安全地删除未用的exports)
- 数组:(告诉webpack哪些模块有副作用,不删除)
//比如 为true
sideEffects:true
//比如 为数组
sideEffects: ['.src/test.js','*.css']
Webpack Tree shaking 深入探究 (juejin.cn)
缓存机制
babel缓存:
- cacheDirectory:true 第二次构建时,会读取之前的缓存
文件资源缓存:
- 如果代码在缓存期内,代码更新后看不到实时效果
- 方案:将代码文件名称,设置为哈希名称,名称发生变化时,就加载最新内容
webpack哈希值:
- hash 每次webpack打包生成的hash值
- chunkhash 不同chunk的hash值不同,同一次打包可能生成不同的chunk
- contenthash 不同内容的hash值不同,同一个chunk中可能有不同的内容
//8代表hash名称位数
[name].[contenthash:8].js
[name].[contenthash:8].css
模块解析resolve
- 配置模块解析的规则
- alias:配置模块加载的路径别名
alias:{'@':resolve('src')}
- extensions:引入模块时,可以省略哪些后缀
extensions:['js','json']
还有其他,可看官网
排除打包externals
- 排除打包依赖,防止对某个依赖项进行打包
- 一般,一些成熟的第三方库,是不需要打包的,比如jquery,可以直接引入CDN
模块联邦
Webpack5 模块联邦让 Webpack 达到了线上 Runtime 的效果,让代码直接在项目间利用 CDN 直接共享,不再需要本地安装 Npm 包、构建再发布。
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
// other webpack configs...
plugins: [
new ModuleFederationPlugin({
name: "app_one_remote",
remotes: {
app_two: "app_two_remote",
app_three: "app_three_remote"
},
exposes: {
AppContainer: "./src/App"
},
shared: ["react", "react-dom", "react-router-dom"]
}),
new HtmlWebpackPlugin({
template: "./public/index.html",
chunks: ["main"]
})
]
};
调用
import { Search } from "app_two/Search";
ModuleFederationPlugin插件的几个重要参数:
- name当前应用名称,需要全局唯一
- remotes可以将其他项目的name映射到当前项目中
- exposes表示导出的模块,只有在此申明的模块才可以作为远程依赖被使用
- shared是非常重要的参数,可以让远程加载的模块对应依赖改为使用本地项目的React或ReactDOM
二、webpack高阶详解知识
webpack4和webpack5的区别
- 热更新
webpack4:webpack-dev-server ...
webpack5:webpack server...
webpack4热更新:
hot:true
webpack5热更新:
webpack5新加的
liveReload:true (不能再使用hot)
target: 'web' (热更新只适用于web相关的targets)
- source map模式写法不一样
webpack4的13个常见模式
webpack5的26个常见模式
具体查看官网
- sideEffects 使用 usedExports
optimization: {
//标记未被使用的代码,打包后的未使用的代码会带上注释/*unused harmony export xxxx*/
usedExports:true,
//删除unused harmony export xxxx标记的代码
minimize:true,
//terser-webpack-plugin压缩插件:webpack4需要单独安装,webpack5无需安装,但需要引入
minimizer: [new TerserPlugin()]
}
webpack4需要单独安装terser-webpack-plugin (webpack5无需安装,但需要引入使用)
- webpack5内置缓存功能 Webpack5 内置缓存方案探索_追逐丶的博客-CSDN博客_webpack5 缓存
webpack4的缓存方案:cache-loader
、dll
webpack5的缓存方案:`
IdleFileCachePlugin
:持久化到本地磁盘MemoryCachePlugin
:持久化到内存
webpack5的内置缓存方案无论从性能上还是安全性上都要好于cache-loader
:
性能上
:由于所以被webpack处理的模块都会被缓存,缓存的覆盖率要高的多安全上
:由于cache-loader使用了基于mtime的缓存验证机制,导致在CI环境中缓存经常会失效,但是Webpack5改用了基于文件内容etag的缓存验证机制,解决了这个问题。具体使用的Webpack5配置官网已经给出了。
-
webpack5增加了模块联邦
-
关闭url-loader默认的ES Modules规范,强制url-loader使用CommonJS规范进行打包
webpack4中只需要url-loader配置esModule:false
webpack5需要html-loader和url-loader都配置esModule:false
热更新(HRM)原理
Webpack HMR 原理解析 - 知乎 (zhihu.com)
总结主要几点:
- 浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端
- 通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块
再次通过 jsonp 请求,获取到最新的模块代码
优化 Webpack 的构建速度?
Webpack5 性能优化 - 优化构建速度 - 云+社区 - 腾讯云 (tencent.com)
如何对bundle体积进行监控和分析?
-
VSCode 中有一个插件 import cost 可以帮助我们对引入模块的大小进行实时监测
-
webpack-bundle-analyzer生成 bundle 的模块组成图,显示所占体积
webpack构建流程(打包原理终版)
webpack打包后产生的文件是个立即自执行函数,自执行函数的入参是个数组,这个数组包含了所有的模块,包裹在函数中。
Webpack 实际上为每个模块创造了一个可以导出和导入的环境,本质上并没有修改 代码的执行逻辑,代码执行顺序与模块加载顺序也完全一致。
从webpack配置文件中配置的entry入口文件开始解析文件构建AST语法树,找出每个文件所依赖的文件,递归下去,最终打包成一个文件。
简单webpack打包原理:
- 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
- 编译:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
- 输出:将编译后的 Module 组合成 Chunk,将 Chunk转换成文件,输出到文件系统中
/**
* 初始化准备工作,获取模块内容,分析模块,收集依赖,通过babel把es6转化为es5形成更AST,递归获取所有依赖
*
* 初始化准备工作,获取模块内容
* 分析模块:将获取到的模块内容 解析成AST语法树( @babel/parser),AST是个引用路劲
* 收集依赖:将用import语句引入的文件路径收集起来。我们将收集起来的路径放到deps里,将file目录路径跟获得的value值拼接成相对路劲放在deps里(@babel/traverse)
* ES6转成ES5(AST): 把获得的ES6的AST转化成ES5,这样获取到代码 (@babel/core @babel/preset-env)
* 递归获取所有依赖:这个对象包括该模块的路径(file),该模块的依赖(deps),该模块转化成es5的代码;
* 并且得处理成:以文件的路径为key,{code,deps}为值的形式存储
* 整合代码:目的就是要生成一个bundle.js文件,也就是打包后的一个文件。其实思路很简单,就是把index.js的内容和它的依赖模块整合起来。然后把代码写到一个新建的js文件。
* 形成可执行的文件:放到立即执行函数里
*
初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
确定入口:根据配置中的 entry 找出所有的入口文件
编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
最终表达:
webpack的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:根据配置文件初始化参数,加载所有配置的插件,执行对象的 run 方法开始执行编译;
* 与此同时,根据配置确定该入口文件,从入口文件出发,调用所有配置的 Loader 对模块进行翻译(翻译包括css转化成js、es6转化成es5),并找出该模块依赖的模块,递归处理;
* 再根据输出的源码、文件依赖关系,最终打包成一个文件
*/
webpack打包文件详解
如果该模块是es6,会在导出对象中加入__esModule = true CommonJS加载ES Module
__webpack_require__
是引入输入模块,并返回模块的导出对象,最开始是引入入口文件,后面怎么引入其他依赖文件的呢?
是一个立即执行函数,参数是依赖文件
异步加载的chunk最终还是同步加载,它被添加到 modules 对象中
,这样就可通过 modules[moduleId].call(module.exports, module, module.exports, webpack_require) 来同步加载chunk,也就是 foo.bundle.js(异步加载的模块) 中第一个 then 执行的内容,传入模块的路径,使用 webpack_require 进行同步加载。
懒加载代码块:原理使用script标签(可以理解为jsonp原理?),返回Promise.all(promises),再在then中使用它
异步就是:只有import的时候,会加载它的内容
ES6模块:会块标识为 ES Module,并且将函数内容定义挂在 default 上
打包最终生成的文件:最终打包出的是一个自执行函数;
自执行函数入参是一个对象modules,其key为打包的模块文件的路径,对应的value为一个函数,其内部为模块文件定义的内容
;
自执行函数体返回 __webpack_require__(__webpack_require__.s = "./src/index.js")
这段代码,此处为加载入口模块并返回模块的导出对象。
function __webpack_require__(moduleId) {
}
其中包含缓存机制
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
模块间的引用,webpack怎么打包的
- CommonJS 加载 CommonJS
({
"./src/foo.js":
(function(module, exports) {
module.exports = 'foo';
}),
"./src/index.js":
(function(module, exports, __webpack_require__) {
const foo = __webpack_require__("./src/foo.js");
console.log(foo)
})
})
- CommonJS 加载 ES module
({
"./src/foo.js":
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__); //将传入的对象标识上__esModule=true,即表明该模块为es6模块
__webpack_exports__["default"] = ('foo'); //将模块的内容挂在__webpack_exports__的default属性上
}),
"./src/index.js":
(function(module, exports, __webpack_require__) {
const foo = __webpack_require__("./src/foo.js");
console.log(foo)
})
})
- ES module 加载 ES module
({
"./src/foo.js":
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
__webpack_exports__["default"] = ('foo');
}),
"./src/index.js":
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
var _foo_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/foo.js");
console.log(_foo_js__WEBPACK_IMPORTED_MODULE_0__["default"])
//_foo_js__WEBPACK_IMPORTED_MODULE_0__用来接收导入的文件,并通过default属性获取到文件的默认导出内容
})
})
webpack_require.n则是用于获取模块的默认导出对象,兼容 CommonJS 和 ES module 两种方式。
- ES module 加载 CommonJS
({
"./src/foo.js":
(function(module, exports) {
module.exports = 'foo';
}),
"./src/index.js":
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
var _foo_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/foo.js");
var _foo_js__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_foo_js__WEBPACK_IMPORTED_MODULE_0__);
console.log(_foo_js__WEBPACK_IMPORTED_MODULE_0___default.a)
})
})
当入口文件index.js以es module的方式加载遵循commonjs规范的foo.js时,通过__webpack_require__加载传入的模块,将得到的模块_foo_js__WEBPACK_IMPORTED_MODULE_0__再传入__webpack_require__.n方法获取到该模块的默认导出对象。因为foo.js中的内容是通过export导出,而非export default导出。因此foo被挂在了default的一个a属性上。
异步按需加载
懒加载代码块:原理使用script标签(可以理解为jsonp原理?),返回Promise.all(promises),再在then中使用它
异步就是:只有import的时候,会加载它的内容
/**
* 该对象用于存储已经加载和正在加载中的chunks
* undefined:表示chunk未加载
* null:表示chunk预加载 / 预获取
* Promise:表示chunk正在加载中
* 0: 表示chunk已经加载了
*/
var installedChunks = {
"index": 0, // 默认入口模块已经加载完毕
};
AST
从代码生成AST的关键:词法分析和语法分析
github.com/CodeLittleP…
github.com/jamiebuilds…
我开发过的plugin
- 很多需求涉及到时间,但是因为时间有兼容性,写了plugin统一处理date格式。
- 图片资源路径统一处理:提测后(因为本地图片调试方便,也方便更改),上传本地图片到阿里云,本地图片替换成cdn链接图片,上传完之后进行删除,防止项目文件太多,减少项目总体积。
思路:获取本地文件,取出文件路径,读出文件流,上传到oss,得到链接,然后进行替换
可参考loader和plugin专题 (juejin.cn)
我开发过的loader
- 兼容以前自适应方式,改了之前的计算方式
我的总结
webpack 是一个模块打包工具
根据业务需要需要进行不同的得配置,当然配置好它,可以进行一些优化,比如减少包的大小、提高打包速度、减少代码中重复写代码、分析文件大小 (具体等着面试官来问对吧?)
webpack构建流程最终表达:
webpack的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:根据配置文件初始化参数,加载所有配置的插件,执行对象的 run 方法开始执行编译;
与此同时,根据配置确定该入口文件,从入口文件出发,调用所有配置的 Loader 对模块进行翻译(翻译包括css转化成js、es6转化成es5),并找出该模块依赖的模块,递归处理;
再根据输出的源码、文件依赖关系,最终打包成一个文件。
juejin.im/post/684490… (webpack)
juejin.im/entry/68449… (webpack配置)
www.cnblogs.com/HYZhou2018/… (优化)
juejin.im/post/685457… (源码分析)
juejin.im/post/685457… (源码分析)
www.yuque.com/yijiangxili… (面试总结)