该面试题只是为了记录我自己的面试笔记,大多数摘自行内有关大佬总结,本人只是搬运工,有关链接已放置相关笔记的下面
webpack
bundle,chunk,Module
bundle
bundle由许多不同的模块生成,包含已经经过加载和编译过程的源文件的最终版本
chunk
chunk主要是在内部用于管理捆绑过程。输出是由bundle由chunk组成,其中有几种类型entry child等。通常,chunk直接与bundle对应,但是有些配置不会产生一对一的关系,例如MiniCssExtractPlugin可从chunk中抽离出css文件,单独生成bundle。生成chunk有三种方式,entry、动态加载、splitChunks抽取共有代码
Module
module是离散功能块,相对于完整程序提供了更小的接触面。一般是module可提供抽象和封装界限,使得应用程序中每个模块都具有明确的目的
Webpack 分包原理
手动分包
原理
顾名思义,就是要先手动的将公共文件先单独打包出来,成为动态链接库dll(Dynamic Link Library)
,生成一个资源清单(manifest.json)
。至于什么是dll
还是在上学时接触.net网站开发了解的,在分包中简单的理解为一个代码仓库
,你要哪些东西直接去里面拿,没了解过的同学自行百度哈,这里就不过多解释了。
怎么做?该如何配置?在实践中会详细讲解,有了动态链接库
和资源清单
后,我们就可以正常进行打包了,在正常打包的过程中,如果发现导入的路径
跟资源清单
中记录的模块名称相同,那么就会使用动态链接库
中的文件,就不会将依赖打包进自己的文件中。
这里有一个小问题,这里为什么打包出来的jquery
和lodash
要直接使用var
暴露全局变量出来?
我们直接来分析下webpack
的打包结果就一目了然了,使用mode=dev
开发模式打包,看的更清楚一些。
// dist/dll/main.6321.js
// ....其他代码....
"dll-reference jquery":
(function (module, exports) {
eval("module.exports = jquery;\n\n//# sourceURL=webpack:///external_%22jquery%22?");
}),
"dll-reference lodash":
(function (module, exports) {
eval("module.exports = lodash;\n\n//# sourceURL=webpack:///external_%22lodash%22?");
})
可以发现并没有将jquery
和lodash
一大堆源码打入到main.js
中。而是使用了module.exports
的方式导出一个全局变量。
为什么? 因为在前面生成的资源清单中有关于jquery或lodash的描述
。打包过程中分析依赖凡是看到依赖的名称为jquery
的,都会去资源清单的content
的路径进行匹配,也就是./node_modules/jquery/dist/jquery.js
。
// 我们平时写的引入路径
import $ from "jquery" === import $ from "node_modules/jquery/dist/jquery.js"
node_modules/jquery/dist/jquery.js
正好匹配到了jquery.manifest.json
资源清单文件的content
路径,所以在打包结果中jquery
的源码变成了:module.exports = 该资源清单的name
。
// jquery清单文件
// jquery.manifest.json
{
"name": "jquery",
"content": {
"./node_modules/jquery/dist/jquery.js": {
"id": 1,
"buildMeta": {
"providedExports": true
}
}
}
}
之后直接在模版index.html
中直接引入公共代码dist/dll/jquery.js
就可以完成手动分包的目的。有些懵?没事,走一遍实践就明白了。
实践
前面提到的资源清单
和动态链接库
如何生成呢?新建webpack.dll.config.js
,配置如下:
// webpack.dll.config.js
const webpack = require("webpack");
const path = require("path");
module.exports = {
entry: {
//有几个公共文件就写几个入口
jquery: ["jquery"],
lodash: ["lodash"]
},
output: {
//打包到dist/dll目录下
filename: "dll/[name].js",
library: "[name]"
},
plugins: [
new webpack.DllPlugin({
//资源清单的保存位置
path: path.resolve(__dirname, "dll", "[name].manifest.json"),
//资源清单中,暴露的变量名
name: "[name]"
}),
]
};
然后在packages.json
中添加script
脚本dll: "webpack --mode=production --config webpack.dll.config.js"
指明配置文件。运行npm run dll
。
可以看到我们想要的文件都被打包出来了,用来描述清单的manifest
文件。以及打包结果中的dll
文件夹。我们在来看一下打包后的jquery.js
和lodash.js
是不是导出了一个全局变量。
哦♂ yeah~ 我们的配置生效了,公共代码分出来了,前置工作已经做好了,接下来就可以正常进行打包了。进一步完善webpack.config.js
。
这里安装两个新插件html-webpack-plugin
和clean-webpack-plugin
(5.x版本已内置),在plugins
中新增两个webpack.DllReferencePlugin,配置一下manifest
指明清单文件。
//webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const webpack = require("webpack");
module.exports = {
mode: "production",
entry: {
main: "./src/index.js",
handler: "./src/handler.js",
},
output: {
filename: "[name].[hash:4].js"
},
plugins: [
// 打包分析工具
new BundleAnalyzerPlugin(),
new CleanWebpackPlugin({
// 不删除手动打包出来dll文件,这样每次打包就不会删除dist/dll文件了
cleanOnceBeforeBuildPatterns: ["**/*", "!dll", "!dll/*"],
}),
new HtmlWebpackPlugin({
template: "./index.html"
}),
// 指明清单文件
new webpack.DllReferencePlugin({
manifest: require("./dll/jquery.manifest.json"),
}),
// 指明清单文件
new webpack.DllReferencePlugin({
manifest: require("./dll/lodash.manifest.json")
})
]
}
然后在模版文件index.html
中手动引入dist/dll
下的公共代码。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>webpack sep packages</title>
</head>
<body>
<script src="./dll/jquery.js"></script>
<script src="./dll/lodash.js"></script>
</body>
</html>
然后敲下npm run prod
,可以看到打包分析工具启动端口打开页面,呈现在我们眼前的就是文章前面提到的优化后打包
的样子啦。至此,手动分包
的原理及使用就全部ok了。但是有没有觉得有一丝繁琐
?还有一个问题就是如果第三方库依赖其他的第三方库,那我是不是要先把依赖的依赖先打包,然后再打包依赖?这些就是手动分包
的一些缺点,如何解决?先看看自动分包
怎么做。
自动分包
原理
与手动分包
不同的是,自动分包
无需每次都要手动先将公共代码先打包一次,它不针对某个具体的包分出去,我们只需要配置好分包策略
后webpack
每次都会自动的完成分包的流程,更符合我们的开发方式,无需关注以后会新增哪些公共代码,所以我们一般优化基本上都用的是自动分包
,一次配置,永久畅享~
事实上,webpack
内部完成分包依赖的是SplitChunksPlugin来实现的,可以在官方文档中看见过去是使用CommonsChunkPlugin
来实现的分包,从v4
版本后就换成了SplitChunksPlugin
。所以自动分包的策略实际上是对配置文件webpack.config.js
中optimization.splitChunks
配置项的修改。
从图中可以看到,经过分包策略后webpack
开启了一个新的chunk
,对公共代码进行打包,并且在输出文件的时被提取出来形成了一个单独的文件,它是新chunk
打包出来的产物。
最后在公共代码
打包出来的文件内挂载一个全局变量window.webpackJsonp = [common1、common2、...]
,然后使用到公共代码
的chunk
从这个数组中拿,今后有再分出来的包继续添加到数组中。最后把打包出去的模块
从原始包中删除,并且修正原始包的代码。
实践
了解到大概原理其实根据分包策略开启若干个新chunk
并打包形成一个单独的文件,并且挂载一个全局变量webpackJsonp
来存放公共代码。如何配置?最优解是什么?那么就进入到实践环节。
基本配置
这里就不新开一个单独的项目来演示了,在原有项目中新建webpack.auto.config.js
专门用来做自动分包
(正常项目中直接在默认的config
文件中配置就行,不用专门新建文件)。
//webpack.auto.config.js
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin")
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
mode: "production",
entry: {
main: "./src/index.js",
handler: "./src/handler.js",
},
output: {
filename: "[name].[hash:4].js"
},
optimization: {
splitChunks: {
//优化配置项...
}
},
plugins: [
new BundleAnalyzerPlugin(),
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template:"./index.html",
})
],
};
除了最基本的配置外,主要就是对webpack
提供optimization
的优化配置项中的splitChunks进行修改。那么可以写哪些配置呢,我单独列了自动分包可能用到的配置项,想要拓展的朋友可以去英文文档上自行翻阅。
名称 | 作用 | 默认值 |
---|---|---|
chunks | 哪些chunk需要应用分包策略 | async(异步chunk) |
maxSize | 分出来的包超过了多少字节需要继续进行拆分 | 0 |
minChunks | 模块被chunk引用了多少次才会进行分包 | 1 |
minSize | 模块超过多少字节才会进行拆分 | 20000 |
automaticNameDelimiter | 公共代码的新chunk名称的分隔符 | ~ |
1. chunks
chunks
有三个取值,默认值为async
,表示只对异步的chunk
开启分包,就是懒加载
的模块。所以如果没有懒加载
的模块并不配置的话直接打包是看不什么效果的,所以一般使用all
来表示对所有的chunk
都要应用分包策略。
optimization: {
splitChunks: {
//优化配置项...
chunks: "all",
}
}
2. maxSize
maxSize
表示如果某个包(包括经过分包后的包)体积大于了设定的值,那么webpack
会尽最大努力继续再次分离,什么意思呢?比如开启了chunks: "all"
后的打包结果是这样的。
可以看到开启了all
后,jquery
和lodash
被合并打包成了一个新的文件,而不是像手动分包
那样将每个源码都映射一份出来。那么这个新的文件vendors~handler~main.2fca.js
是公共代码对应的新chunk
的产物。但是如果觉得160kb
还是有点大,能不能再继续分一分?这就要用到maxSize
配置项了。
optimization: {
splitChunks: {
//优化配置项...
chunks: "all",
maxSize: 50 * 1024,
}
},
表示如果包大于了50kb
那么webpack
还会去进一步的继续尝试着分离包。运行npm run auto
。
能发现比刚刚多了一个文件,都是vendors
开头的。点开进去之后可以看到分别是jquery
和lodash
的源码。同上所说,打包出来的文件都去定义一个全局变量webpackJsonp
,那如果存在多个文件不就覆盖了吗?
其实不然,真正每个verdors
文件第一行是(window.webpackJsonp = window.webpackJsonp || []).push(序号索引)
,按照分出来的顺序,序号索引就是0、1、...,这样就无论分几个包,最后其实还都是会push
到同一个window.webpackJsonp
数组中。
细心的同学应该会发现一个问题就是明明设置的是50kb
呀,为什么拆分出来的两个文件都还是大于设置的值呢?仔细看前面说的如果超过了设定值,webpack
会尽最大努力
继续分离,但是,是以模块
为基础的,再怎么分,都不可能把原来的一个整体的模块
直接打乱掉代码分割出去,最小的单元
就是一个模块
。所以最小也只能jquery
和lodash
完整的代码再单独各分出去。
走到这大家就会发现有点不对劲,再怎么分总体积是不变的,只不过是拆分了很多份,有些时候反而对性能是负提升
。但是如果某个浏览器
支持多线程请求的话,可能会对性能有帮助。大家对这个属性仁者见仁,智者见智就好,谨慎使用
。
3. minChunks
表示一个chunk
被引用了多少次,才会进行分包。默认值是1
,只要被引用过就要进行分包优化,这个配置就很简单,可以试一试设置为一个大点的值,那么就不会进行分包了。
optimization: {
splitChunks: {
//优化配置项...
chunks: "all",
maxSize: 60 * 1024,
minChunks: 20,
}
},
注意: 这里设置的minChunks
是针对于我们自己写的一些公共模块
想要进行分包处理的最小引用数,针对于引用依赖node_modules
中的文件是不生效
的,因为存在缓存组
单独针对node_modules
的规则,下面会说到,所以这个配置对第三方库是不生效
的!
4. minSize
此配置表示当一个包
达到多大体积才会进行分包,默认值为20000
。对第三方库
同样生效。如果设置一个很大的值,那么就都不会进行分包。
optimization: {
splitChunks: {
//优化配置项...
chunks: "all",
maxSize: 60 * 1024,
minChunks: 20,
minSize: 1000000,
}
},
5. automaticNameDelimiter
这个配置项十分简单,只是修改新chunk
生成的文件名中的分隔符,默认值~
。比如现在改成---
,那么新chunk
的文件名分隔符就是vendors---handler---main.b457.js
。
optimization: {
splitChunks: {
//优化配置项...
chunks: "all",
// maxSize: 60 * 1024,
// minChunks: 20,
minSize: 20000,
automaticNameDelimiter: "---"
}
},
缓存组
前面提到了minChunks
配置对第三方库不生效,是因为有缓存组
的存在,那么缓存组
到底是什么?在之前设置的配置都是基于全局的,实际上,分包策略
是基于缓存组
的,每一个缓存组
都是一套单独的分包策略
,可以设置不同的缓存组
来针对不同的文件进行分包
。webpack
默认开启了两个缓存组
,即cacheGroups。
optimization: {
splitChunks: {
//优化配置项...
chunks: "all",
cacheGroups: {
vendors: { //属性名即是缓存的名称,改成common看看
test: /[\/]node_modules[\/]/,
priority: 2
},
default: { //默认缓存组设置,会覆盖掉全局设置
minChunks: 2,
priority: 1,
reuseExistingChunk: true
}
}
}
},
在cacheGroups
中,每一个对象就是一个缓存组
分包策略,属性名
便是缓存组
的名称,对象中设置的值可以继承
全局设置的属性(如minChunks
等),也存在只有缓存组
独特的属性,比如test
(匹配模块名称规则)、priority
(缓存组的优先级),reuseExistingChunk
(重用已经被分离出的chunk)。
webpack
会根据缓存组
的优先级(priority)来依次处理每个缓存组
,被缓存组
处理过的模块不需要再次分包。所以前面为什么minChunks
对第三方库没有生效,是因为有默认缓存组
的存在,已经针对node_modules
定了一套独有的分包策略。
可以把默认的vendors
的属性名改成其他的,比如common
,那么打包结果中,经过缓存组
处理过的node_modules
分出来的包就是以common
开头了。
正常情况下,缓存组
对我们来说没有太多意义,webpack
提供的默认缓存组
就已经够用了。但是大家可以想一想,它其实还可以用来做对公共样式
的抽离,比如两个css
文件有相同的样式,那么我们可以用test
匹配css
文件来设置公共样式
的打包。这个就属于拓展了,有兴趣的同学可以试一试找我交流~
总结
两者分包的区别在于手动分包
可以极大的提升编译构建速度,但是使用起来比较繁琐
,一旦今后有新增的公共代码
都需要手动去处理。自动分包
的话可以极大的提高开发效率,只要配置好分包策略
后就一劳永逸了。深入了解两者分包
的原理及优缺点,还有一些比较冷门的点后,就已经基本拿捏了,斯国一!遇到相同的业务场景时,可以选择合适的分包手段来进行优化。
但是分包也是有局限性
的,比如已经分到不能再分的时候,就只能通过代码压缩
、tree shaking
、懒加载
等等手段来继续优化了,本文就不过多赘述了,以后可以出个续集。最后,谢谢大家的观看,对本文内容有异议或交流欢迎评论~
webpack中的hash、chunkhash和contenthash
hash一般是结合CDN缓存来使用,通过webpack构建之后,生成对应文件名自动带上对应的MD5值。如果文件内容发生改变的话,那么对应文件hash值也会改变,对应的HTML引用的URL地址也会改变,触发CDN服务器从原服务器上拉取对应数据,进而更新本地缓存。但是实际使用时,这三种hash计算还是有一定区别。
hash
hash是对webpack整个一次构建而言,在webpack构建中,文件都会带上对应的MD5值,构建生成的文件hash值都是一样的。如果出口是hash,那么一旦针对项目中任何一个文件的修改,都会构建整个项目,重新获取hash值。如果有目的性的缓存就会失败。
chunkhash(js)
chunkhash的范围可以针对某个模块而言,它会从入口出发,对依赖文件进行解析,构建对应的chunk和hash值。一般的使用是在生产环境对公共库和程序入口文件单独抽离开,单独打包构建,用chunkhash的方式对这些打包后的文件带上相应hash值。在线上,只要公共库和入口没变,其hash值就不会改变,从而达到缓存的目的。
contenthash(css)
contenthash表示由文件内容产生的hash值,内容不同产生的contenthash值也不一样。在项目中,通常做法是把项目中css都抽离出对应的css文件来加以引用。
原文链接:webpack4中hash、chunkhash和contenthash三者的区别_bubbling_coding的博客-CSDN博客
原文链接:webpack中的hash、chunkhash和contenthash_Lawliet_ZMZ的博客-CSDN博客
webpack loader
loader是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中
- 处理一个文件可以使用多个loader,loader的执行顺序和配置中的顺序是相反的,即最后一个loader最先执行,第一个loader最后执行
- 第一个执行的loader接收源文件内容作为参数,其它loader接收前一个执行的loader的返回值作为参数,最后执行的loader会返回此模块的JavaScript源码
一、webpack的打包原理
- 识别入口文件
- 通过逐层识别模块依赖(Commonjs、amd或者es6的import,webpack都会对其进行分析,来获取代码的依赖)
- webpack做的就是分析代码,转换代码,编译代码,输出代码
- 最终形成打包后的代码
二、什么是loader
loader是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中
- 处理一个文件可以使用多个loader,loader的执行顺序和配置中的顺序是相反的,即最后一个loader最先执行,第一个loader最后执行
- 第一个执行的loader接收源文件内容作为参数,其它loader接收前一个执行的loader的返回值作为参数,最后执行的loader会返回此模块的JavaScript源码
三、什么是plugin
在webpack运行的生命周期中会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果。
四、loader和plugin的区别
对于loader,它是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss转换为A.css,单纯的文件转换过程
plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务
webpack loader
webpack 本身只能处理 JavaScript 和 JSON 文件,而 loader 为 webpack 添加了处理其他类型文件的能力。loader 将其他类型的文件转换成有效的 webpack modules(如 ESmodule、CommonJS、AMD),webpack 能消费这些模块,并将其添加到依赖关系图中。
loader 本质上是一个函数,该函数对接收到的内容进行转换,返回转换后的结果。
常见的 loader 有:
- raw-loader:加载文件原始内容。
- file-loader:将引用文件输出到目标文件夹中,在代码中通过相对路径引用输出的文件。
- url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式将文件内容注入到代码中。
- babel-loader:将 ES 较新的语法转换为浏览器可以兼容的语法。
- style-loader:将 CSS 代码注入到 JavaScript 中,通过 DOM 操作加载 CSS。
- css-loader:加载 CSS,支持模块化、压缩、文件导入等特性。
使用 loader 的方式主要有两种:
- 在 webpack.config.js 文件中配置,通过在 module.rules 中使用 test 匹配要转换的文件类型,使用 use 指定要使用的 loader。
module.exports = {
module: {
rules: [{ test: /.ts$/, use: "ts-loader" }],
},
};
- 内联使用
import Styles from "style-loader!css-loader?modules!./styles.css";
参考链接 谈下 webpack loader 的机制 | HZFE - 剑指前端 Offer
Tree-shaking 核心原理
一、treeshaking原理
Tree-shaking的原理是利用ES6模块化规范的特性,在编译时通过静态分析代码,识别出未被使用的代码(dead code)并在打包时去除。具体来说,比如在代码中引入了一个模块,但实际上只使用了其中的一部分代码,通过静态分析可以识别出未被使用的代码,删掉这部分代码从而减小bundle的大小。
Tree-shaking的实现借助了ES6模块化的特性,ES6模块化规范是静态的,也就是说,在编译时就可以确定模块的依赖关系,因此可以通过静态分析来判断哪些代码没有被使用。
二、谢可寅shaking
treeshaking的发明人是谢可寅,tree-shaking这个词的由来其实是源于webpack社区的。webpack的开发者认为把未使用的代码从打包结果中摇掉很像树上的果实,因此用tree-shaking来形容这个过程。
三、treeshaking配置
对于webpack用户来说,使用tree-shaking非常方便,只需要在webpack配置文件中开启optimization.minimize选项就可以了。optimization.minimize选项默认会开启tree-shaking,并使用内置的UglifyJsPlugin压缩代码,从而生成一个更小的bundle。
// webpack.config.js
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
optimization: {
minimize: true,
},
};
需要注意的是,只有引入ES6 module的代码才能启用tree-shaking。对于CommonJS或AMD模块化的代码,由于不带有静态分析的特性,无法利用tree-shaking功能。
四、treeshaking怎么读
对于英文不太好的开发者来说,"tree-shaking"这个词还是挺难理解的。它到底是什么意思呢?
实际上,tree-shaking这个词的意思可以通过拆分词汇来理解。Tree是树的意思,是一种数据结构。Shake是摇动的意思,可以引申为“震动”。因此,treeshaking可以理解为“震动树”(摇动树的果实掉落下来的意思)。
五、treeshaking不生效
虽然tree-shaking看上去很美好,但实际上开发者们会发现有些时候它并不会生效。有以下一些情况可能导致tree-shaking不生效。
- 在代码中使用了process.env.NODE_ENV变量,会导致webpack将整个模块打包进去。
- 有些库会使用类似于全局注册的方式注册组件,比如Ant Design Vue的组件,这会导致tree-shaking失效,因为在编译时无法知道哪些组件被使用。
- 使用动态导入(如import())时,由于要在运行时决定使用哪个模块,编译时不会对这部分代码进行分析。
- 代码中使用了webpack的require.ensure()或require.include()等动态加载模块的方式。
需要注意的是,尽管使用tree-shaking会减小bundle的大小,但并不一定会提升应用程序的性能。这是因为虽然tree-shaking会减小bundle的大小,但整个应用程序的总体积可能并没有得到明显的减少,因为一些库的体积可能还是非常大。
六、treeshaking副作用
虽然tree-shaking在很多情况下可以减小bundle的大小,但使用不当也会带来一些副作用。
- 可读性差。优化过度的代码可能会失去可读性,这会给维护和代码优化带来困难。
- 可能会破坏代码的正确性。对一些代码进行tree-shaking可能会破坏代码的正确性,导致应用程序无法正常运行。
- 代码冗余。有时候对代码进行tree-shaking会导致生成更多的代码,这可能会导致bundle的大小反而更大。
七、treeshaking对怎样的包不生效
treeshaking并不是万能的,对某些类型的包并不会起作用。比如:
- 对于只有一个入口文件的包或库,tree-shaking会对整个文件进行编译,而不是只编译其中被使用的部分。
- 对于内置模块(比如fs、http等),由于它们没有使用ES6的模块化规范,所以tree-shaking并不会起作用。
最后,需要注意一点的是,虽然tree-shaking非常方便,但也不是解决所有性能问题的银弹。代码优化应该是一个综合性的过程,需要综合考虑代码的质量、代码的体积、代码的可读性以及代码的运行效率等多个方面。
完整代码示例
// index.js
import { sum } from './math';
console.log(sum(1, 2));
// math.js
export function sum(a, b) {
return a + b;
}
export function minus(a, b) {
return a - b;
}
常见loader
thread-loader配置参数
use: [
{
loader: "thread-loader",
// 有同样配置的 loader 会共享一个 worker 池(worker pool)
options: {
// 产生的 worker 的数量,默认是 cpu 的核心数
workers: 2,
// 一个 worker 进程中并行执行工作的数量
// 默认为 20
workerParallelJobs: 50,
// 额外的 node.js 参数
workerNodeArgs: ['--max-old-space-size', '1024'],
// 闲置时定时删除 worker 进程
// 默认为 500ms
// 可以设置为无穷大, 这样在监视模式(--watch)下可以保持 worker 持续存在
poolTimeout: 2000,
// 池(pool)分配给 worker 的工作数量
// 默认为 200
// 降低这个数值会降低总体的效率,但是会提升工作分布更均一
poolParallelJobs: 50,
// 池(pool)的名称
// 可以修改名称来创建其余选项都一样的池(pool)
name: "my-pool"
}
},
"expensive-loader"
]
image-webpack-loader
chainWebpack: config => {
if(IS_PROD){
config.module
.rule('images')
.exclude.add(resolve('src/assets/icons')) // 排除icons目录,这些图标已用 svg-sprite-loader 处理,打包成 svg-sprite 了
.end()
.use('url-loader')
.tap(options => ({
limit: 10240, // 稍微改大了点
fallback: {
loader: require.resolve('file-loader'),
options: {
// 在这里修改file-loader的配置
// 直接把outputPath的目录加上,虽然语义没分开清晰但比较简洁
name: 'static/img/[name].[hash:8].[ext]'
// 从生成的资源覆写 filename 或 chunkFilename 时,assetsDir 会被忽略。
// 因此别忘了在前面加上静态资源目录,即assetsDir指定的目录,不然会直接在dist文件夹下
// outputPath: 'static/img'
}
}
}))
.end()
.use('image-webpack-loader')
.loader('image-webpack-loader')
.options({
mozjpeg: { progressive: true, quality: 50 }, // 压缩JPEG图像
optipng: { enabled: true }, // 压缩PNG图像
pngquant: { quality: [0.5, 0.65], speed: 4 }, // 压缩PNG图像
gifsicle: { interlaced: false } // 压缩GIF图像
})
.end()
.enforce('post') // 表示先执行配置在下面那个loader,即image-webpack-loader
}
}
vite
Vite 和 webpack
-
基础概念不同
- webpack是一个模块打包器,它可以把许多不同类型的模块和资源文件打包为静态资源。它具有高度的可配置性,可以通过插件和loader扩展其功能。
- vite,由Vue.js作者尤雨溪开发并维护,是一个基于浏览器原生 ES imports 的开发服务器。它能够提供丰富的功能,如快速冷启动、即时热更新和真正的按需编译等。
-
编译方式不同
- webpack在编译过程中,会将所有模块打包为一个bundle.js文件,然后再运行这个文件。
- 而vite在开发模式下,没有打包的步骤,它利用了浏览器的ES Module Imports特性,只有在真正需要时才编译文件。在生产模式下,vite使用Rollup进行打包,提供更好的tree-shaking,代码压缩和性能优化。
-
开发效率不同
- webpack的热更新是全量更新,即使修改一个小文件,也会重新编译整个应用,这在大型应用中可能会导致编译速度变慢。
- vite的热更新是增量更新,只更新修改的文件,所以即使在大型应用中也能保持极快的编译速度。
-
扩展性不同
- webpack有着成熟的插件生态,几乎可以实现任何你想要的功能,扩展性非常强。
- vite虽然也支持插件,但相比webpack的生态,还有一些距离。
-
应用场景不同
-
webpack由于其丰富的功能和扩展性,适合于大型、复杂的项目。
-
而vite凭借其轻量和速度,更适合于中小型项目和快速原型开发。
-
参考链接 vite和webpack的区别 • Worktile社区
Vite 相关(兼容性)
Antd 与 vite 兼容性问题
兼容低版本
- 如果要支持低版本浏览器可以使用官方提供的插件 @vitejs/plugin-legacy,plugin-legacy 会将代码打包两套
- 如果浏览器支持
- 如果浏览器不支持ESM
Vite 性能篇:掌握这些优化策略,一起纵享丝滑! - 掘金
babel
polyfill兼容
打包
Npm 打包原理
1. npm 模块安装机制:
-
发出
npm install
命令 -
查询node_modules目录之中是否已经存在指定模块
-
若存在,不再重新安装
-
若不存在
- npm 向 registry 查询模块压缩包的网址
- 下载压缩包,存放在根目录下的
.npm
目录里 - 解压压缩包到当前项目的
node_modules
目录
-
2. npm 实现原理
输入 npm install 命令并敲下回车后,会经历如下几个阶段(以 npm 5.5.1 为例):
执行工程自身 preinstall
当前 npm 工程如果定义了 preinstall 钩子此时会被执行。
确定首层依赖模块
首先需要做的是确定工程中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。
工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。
获取模块
获取模块是一个递归的过程,分为以下几步:
- 获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic version(semver,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接拿即可,如果没有则从仓库获取。如 packaeg.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。
- 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。
- 查找该模块依赖,如果有依赖则回到第1步,如果没有则停止。
模块扁平化(dedupe)
上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。比如 A 模块依赖于 loadsh,B 模块同样依赖于 lodash。在 npm3 以前会严格按照依赖树的结构进行安装,因此会造成模块冗余。
从 npm3 开始默认加入了一个 dedupe 的过程。它会遍历所有节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有重复模块时,则将其丢弃。
这里需要对重复模块进行一个定义,它指的是模块名相同且 semver 兼容。每个 semver 都对应一段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到一个兼容版本,而不必版本号完全一致,这可以使更多冗余模块在 dedupe 过程中被去掉。
比如 node-modules 下 foo 模块依赖 lodash@^1.0.0,bar 模块依赖 lodash@^1.1.0,则 ^1.1.0 为兼容版本。
而当 foo 依赖 lodash@^2.0.0,bar 依赖 lodash@^1.1.0,则依据 semver 的规则,二者不存在兼容版本。会将一个版本放在 node_modules 中,另一个仍保留在依赖树里。
举个例子,假设一个依赖树原本是这样:
node_modules
-- foo
---- lodash@version1
-- bar
---- lodash@version2
假设 version1 和 version2 是兼容版本,则经过 dedupe 会成为下面的形式:
node_modules
-- foo
-- bar
-- lodash(保留的版本为兼容版本)
假设 version1 和 version2 为非兼容版本,则后面的版本保留在依赖树中:
node_modules
-- foo
-- lodash@version1
-- bar
---- lodash@version2
安装模块
这一步将会更新工程中的 node_modules,并执行模块中的生命周期函数(按照 preinstall、install、postinstall 的顺序)。
执行工程自身生命周期
当前 npm 工程如果定义了钩子此时会被执行(按照 install、postinstall、prepublish、prepare 的顺序)。
最后一步是生成或更新版本描述文件,npm install 过程完成。
参考 npm 模块安装机制简介
参考链接:npm/yarn lock真香
参考链接:第 20 题:介绍下 npm 模块安装机制,为什么输入 npm install 就可以自动安装对应的模块? · Issue #22 · Advanced-Frontend/Daily-Intervie
Npm 版本锁定原理
package-lock.json相当于本次install的一个快照,它不仅记录了package.json指明的直接依赖的版本,也记录了间接依赖的版本。
semver通配符
符号^:表示安装不低于该版本的应用,但是大版本号需相同,例如:vuex: "^3.1.3",3.1.3及其以上的3.x.x都是满足的。
符号~:表示安装不低于该版本的应用,但是大版本号和小版本号需相同,例如:vuex: "^3.1.3",3.1.3及其以上的3.1.x都是满足的。
无符号:无符号表示固定版本号,例如:vuex: "3.1.3",此时一定是安装3.1.3版本
npm 源
项目根目录下创建一个 .npmrc
的文件,在其中指定 npm 源
,以保证团队的成员使用的是统一的 npm 源
npm ci
是根据 package-lock.json 去安装确定的依赖,package.json 只是用来验证是不是有不匹配的版本,假设 package-lock.json 中存在一个确定版本的依赖 A,如果 package.json 中不存在依赖 A 或者依赖 A 版本和 lock 中不兼容,npm ci 就会报错。
原文链接:npm的package.json和package-lock.json更新策略_semver-range version_码飞飞的博客-CSDN博客
Yarn 打包原理
yarn
的出现主要目标是解决上面描述的由于语义版本控制而导致的 npm
安装的不确定性问题。虽然可以使用 npm shrinkwrap
来实现可预测的依赖关系树,但它并不是默认选项,而是取决于所有的开发人员知道并且启用这个选项。 yarn
采取了不同的做法。每个 yarn
安装都会生成一个类似于npm-shrinkwrap.json
的 yarn.lock
文件,而且它是默认创建的。除了常规信息之外,yarn.lock
文件还包含要安装的内容的校验和,以确保使用的库的版本相同。
yarn 的主要优化
yarn
的出现主要做了如下优化:
- 并行安装:无论
npm
还是yarn
在执行包的安装时,都会执行一系列任务。npm
是按照队列执行每个package
,也就是说必须要等到当前package
安装完成之后,才能继续后面的安装。而yarn
是同步执行所有任务,提高了性能。 - 离线模式:如果之前已经安装过一个软件包,用
yarn
再次安装时之间从缓存中获取,就不用像npm
那样再从网络下载了。 - 安装版本统一:为了防止拉取到不同的版本,
yarn
有一个锁定文件 (lock file
) 记录了被确切安装上的模块的版本号。每次只要新增了一个模块,yarn
就会创建(或更新)yarn.lock
这个文件。这么做就保证了,每一次拉取同一个项目依赖时,使用的都是一样的模块版本。 - 更好的语义化:
yarn
改变了一些npm
命令的名称,比如yarn add/remove
,比npm
原本的install/uninstall
要更清晰。
安装依赖树流程
-
执行工程自身 preinstall。 当前
npm
工程如果定义了preinstall
钩子此时会被执行。 -
确定首层依赖。 模块首先需要做的是确定工程中的首层依赖,也就是
dependencies
和devDependencies
属性中直接指定的模块(假设此时没有添加npm install
参数)。工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm
会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。 -
获取模块。 获取模块是一个递归的过程,分为以下几步:
- 获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为
package.json
中往往是semantic version
(semver
,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.json
或package-lock.json
)中有该模块信息直接拿即可,如果没有则从仓库获取。如package.json
中某个包的版本是^1.1.0
,npm
就会去仓库中获取符合1.x.x
形式的最新版本。 - 获取模块实体。上一步会获取到模块的压缩包地址(
resolved
字段),npm
会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。 - 查找该模块依赖,如果有依赖则回到第
1
步,如果没有则停止。
- 获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为
-
模块扁平化(dedupe)。
上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。比如A
模块依赖于loadsh
,B
模块同样依赖于lodash
。在npm3
以前会严格按照依赖树的结构进行安装,因此会造成模块冗余。yarn
和从npm5
开始默认加入了一个dedupe
的过程。它会遍历所有节点,逐个将模块放在根节点下面,也就是node-modules
的第一层。当发现有重复模块时,则将其丢弃。这里需要对重复模块进行一个定义,它指的是模块名相同且semver
兼容。每个semver
都对应一段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到一个兼容版本,而不必版本号完全一致,这可以使更多冗余模块在dedupe
过程中被去掉。 -
安装模块。 这一步将会更新工程中的
node_modules
,并执行模块中的生命周期函数(按照preinstall
、install
、postinstall
的顺序)。 -
执行工程自身生命周期。 当前
npm
工程如果定义了钩子此时会被执行(按照install
、postinstall
、prepublish
、prepare
的顺序)。
举例说明
插件 htmlparser2@^3.10.1
和 dom-serializer@^0.2.2
都有使用了 entities
依赖包,不过使用的版本不同,同时我们自己安装一个版本的 entities
包。具体如下:
--htmlparser2@^3.10.1
|--entities@^1.1.1
--dom-serializer@^0.2.2
|--entities@^2.0.0
--entities@^2.1.0
参考链接:npm/yarn lock真香