世界上没有免费的早餐。
前言
波斯有一位国王,他勤政爱民,仁慈又聪明。 在他的领导下,全国人民过上了安居乐业的生活。
但随着国王一天天老去,他开始害怕一件事,那就是他死后,人民将无法再像现在一样过上幸福的生活。 我们应该做什么? 有一天,他召集全省所有有识之士,要求他们找出一条能够保证人民生活幸福的永恒法则。
三个月后,学者们向国王献上三本六寸厚的帛书,并说道:“这三本书,涵盖了天下的知识,只要老百姓能读懂它们,就可以世世代代无忧无虑。” 。 向上”。
国王看着这三本厚厚的书,不以为然,因为他知道人民不会花那么多时间去读那些书。 他让那些学者继续研究。
两个月后,学者们向国王呈上一张纸。 国王读完纸条后,非常满意,说道:“很好,我就带着这句话死去。按照纸条上写的去做,我相信他们一定能够过上幸福健康的生活。”
纸上写的是什么?
以上内容是一个简单的鱼汤故事,与本文内容无关,但蕴含着人生哲理。
今天的话题是关于webpack的性能优化,以及webpack5正式版的新更新。
Webpack性能优化
之前在拉勾教育买了一课《webpack原理与实践》。 本课程从独特的角度探讨了webpack的工作原理和运行机制,传达了一种“带着疑问看源码”的思想,明确“复习”而不是“死扣”。
文章提到了生产环境中的一个优化插件DefinePlugin。 DefinePlugin 用于将全局成员注入到我们的代码中。
// ./webpack.config.js
const webpack = require('webpack')
module.exports = {
// ... 其他配置
plugins: [
new webpack.DefinePlugin({
// 值要求的是一个代码片段
API_BASE_URL: JSON.stringify('https://api.example.com')
})
]
}
本文将解释一些插件的一些技巧和常见陷阱。
比较有趣的是我问了一个问题:“程序如何识别rc类型文件,webpack如何用.webpackrc.js替换webpack.config.js?”
老师的回复是一个库:“cosmiconfig”这个库会手动搜索配置文件。
除了拉勾教育之外,我还在凯科吧买了一个小课程《Web后端架构师中级高级技能》。 这门课有很多东西。 其中一课是《webpack原理分析》,从源码讲解webpack如何打包编译,自动实现一个简单的webpack。
除了上面这些之外,我还去B站找到了一个比较清晰的“weebpack实用教程”。 本课程来自尚硅谷。 从使用层面出发,讲解了webpack可以解决哪些问题,在项目中如何优化,如何配置。
本文下面的内容大部分是上述课程的笔记。 如果你说的不明白,可以自行查看实战,或者留言说明。
关于webpack的性能优化,我们从两个方面入手:开发环境和生产环境。
开发环境性能优化
在开发环境中,程序员会关注两点,一是实时编译,二是方便调试。
说到热更新,大家就会想到HMR功能。 在webpack配置中开启devServer的hot属性,开启HMR功能。 HMR将大大提高构建速度,并且如果某个模块发生变化,则只会重新打包一个模块而不是所有模块。
与样式文件相比,style-loader内部已经实现了对HMR的支持。
与HTML文件相比,更改不会手动更新(默认不支持HMR),我们需要做一些配置。 (引文html很少变动,一般我们不做处理)
// 修改entry入口,将html文件引入
module.exports = {
entry: ["./src/index.js", "./src/index.html"],
// ... 其他配置
}
与JS文件相比,每次更改都会全局刷新。 我们想要的效果是每次更改模块时都更新该模块,而无需刷新浏览器。以下示例
//a.js
function add(x,y){
console.log("add加载了")
return x+y;
}
export default add
//index.js
import add from "./a.js"
console.log("index.js被加载了")
if(module.hot){
module.hot.accept("./a.js",function(){
//监听a.js的变化,一旦发生变化,其他模块不会重新打包构建
add()
})
}
使用module.hot判断是否开启HMR功能,使用accept监听模块是否发生变化。
source-map是一种在源代码创建后提供代码映射的技术。 如果创建的代码有问题,可以通过映射追踪源代码的错误位置。
源映射有两种类型:内联和外部。 内联是直接在打包的js内部生成的,内联速度比较快。 external是在外部生成source-map文件。
//[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
//source-map:外部
//错误代码的准确位置
//inline-source-map:内联 只生成一个内联的source-map
//错误代码的准确位置
//hidden-source-map:外部 在外部生成一个source-map文件
//错误代码的错误原因,不能追踪到源代码的错误位置,只能提示构建后的代码错误位置。
//eval-source-map:内联 每一个文件对应的source-map都在eval里
//错误代码的准确位置
//nosource-source-map:外部
//错误代码的准确位置,没有任何源代码信息
//cheap-source-map
//错误代码的准确位置,只精确到行
//cheap-module-source-map
//module会将loader的source-map加入
//开发环境:速度快,调试友好
//速度 eval>inline>cheap>...
//eval-cheap-source-map会变得更快
//调试更友好的是
//source-map>cheap-module-source-map>cheap-source-map
//折中化方案(脚手架默认配置方案)
//eval-source-map
//生产环境:源代码要不要隐藏?
//内联会让体积非常大,所以一般不考虑内联
//如果需要隐藏源代码可以考虑
//nosource-source-map 全部隐藏
//hidden-source-map 只隐藏源代码
生产环境性能优化
通常,一个文件只能由一个加载器处理。 当一个文件要被多个加载器处理时,需要指定加载器的执行顺序。
oneOf是让一个加载器处理一类文件,这会让加载器执行得更好。 需要注意的是,两个配置不能处理同一个文件,比如eslint-loader,所以我们需要把eslint-loader放在外面。
module: {
rules: [
{
test: /.js$/,
exclude:/node_modules/,
enforce:"pre",
loader:"eslint-loader",
options:{
fix:true,
}
},
{
oneOf: [
{
test: /.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /.(png|jpg|gif|svg)$/,
use: ["file-loader"],
},
//...
]
}
]
}
除了oneOf等优化速度的解决方案之外,我们还可以从缓存入手。 babel-loader 提供了一个 cacheDirectory 属性来帮助我们实现 babel 缓存,并且在第二次创建时会读取之前的缓存。
module: {
rules: [
{
test: /.js$/,
exclude:/node_modules/,
loader:"babel-loader",
options:{
cacheDirection:true,
}
}
//...
]
}
除了babel缓存之外,还有一种缓存就是文件资源缓存,它是通过hash来实现的。
webpack中有3种hash,每次创建webpack时,都会生成一个唯一的hash值。
//因为css和js同时使用一个hash值
//如果重新打包,会导致所有缓存失效。
output: {
filename: "bundle.[hash:10].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new MiniCssExtractPlugin({
filename:"css/main.[hash:10].css"
}),
new HtmlWebpackPlugin({
template: "./index.html",
}),
],
chunkhash是根据chunk生成的hash值。 如果包装来自同一个块,则哈希值将相同。
另一种哈希是contenthash,它根据文件的内容生成哈希值。 不同文件的hash值一定是不同的。
所以我们经常使用contenthash作为文件资源缓存webpack相关,这样可以在代码在线运行时更好的利用缓存。
接下来我们来说说tree-shaking,它是用来消除无用代码的。 它的前提是必须使用ES6模块化并且环境是生产环境。
举个反例,比如,untils.js 中导入的方式有很多种,但我们只使用其中一种。 那么untils.js中其他无用的代码将不会被webpack打包编译。
//untils.js
export const add=()=>{
//...
}
export const muilt=()=>{
//...
}
//index.js
import { add } from "../../untils"
//tree-shaking 就是通过import将没用使用过的代码去除掉,比如muilt方法
但是如果代码中引入了css模块呢? css模块也会被tree-shaking处理吗?
这里我们需要在package.json中设置:
//...
//"sideEffects":false,//所有代码都没有副作用(都可以进行tree-shaking)
"sideEffects":["*.css"],//css等文件不会进行tree-shaking。
如果我们的生产环境中需要第三方依赖,并且我们想将它们单独打包,我们该怎么做呢?
这里会用到优化:
//...
optimization:{
splitChunks:{
chunk:"all"
}
}
他会手动分析多入口chunk,看看是否有共同的文件,如果有,就会打包成单独的chunk。
另一种方式是在动态导出的时候添加,这在路由中比较常见。
//...
component:()=>import(/*webpackChunkName: "text"*/ "./../views/text.vue")
import可以动态导出句型,这个文件可以单独打包。
如果我们想要按需加载,即当风暴触发时,就会加载需要的文件,那么如何设置呢?
这就是典型的懒加载,用的时候就加载。 除了延迟加载和预加载之外,看起来效果和延迟加载是一样的,但毕竟已经偷偷加载了,点击的时候就会从显存中获取。
//如果把import放在头部是正常加载,也是并行加载。
//把import语法放在异步的回掉中处理。
document.getElementById("btn").onclick=function(){
//懒加载
import(/*webpackChunkName:'test'*/"./test").then(
(res)=>{
//....
}
)
//预加载:会提前加载(空闲的时候偷偷加载,不是并行加载)
//等其他资源加载完毕的时候,偷偷加载,兼容行不是很好
import(/*webpackPreFetch:true */"./test").then(
(res)=>{
//....
}
)
}
如果项目比较大,js文件较多,另外一个优化打包率的方案就是多进程打包,通过thread-loader来实现。 比如babel-loader需要编译转换大量的js。
{
test: /.js$/,
exclude: /node_modules/,
use: [
{
//多进程打包
loader: "thread-loader",
options: {
workers: 2,
},
},
{
loader: "babel-loader",
options: {
cacheDirection:true,
}
}
]
}
在一些项目中webpack相关,我们可能会通过CDN引入一些第三方库或者资源。 我们不希望 webpack 在编译时将它们打包。 如何设置它们?
只有这个时候才会用到externals,它的主要作用是排除运行和编译时的外部依赖。 官网给出了一个非常恰当的反例:
//index.html
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous">
</script>
//webpack.config.js
module.exports = {
//...
externals: {
jquery: 'jQuery'
}
};
//打包编译后仍然工作
import $ from 'jquery';
$('.my-element').animate(/* ... */);
外部根本不需要打包,但是如果我们不想使用CDN,想在本地打包,又不想重复打包,怎么办?
这里用到了dll技术,需要分为几个步骤。 第一步:将第三方库单独打包到node_modules中。 首先,我们创建一个webpack.dll.js(名称可以任意,这里我们使用dll)文件。
//webpack.dll.js
const path = require("path");
const webpack = require("webpack");
module.exports = {
mode: "production",
entry: {
//打包输出的名字为键,值中的jquery为要打包的库
jquery: ["jquery"],
},
output: {
path: path.resolve(__dirname, "dll"),
filename: "[name].js",
//打包的库里面向外暴露出去的内容叫什么
library: "[name]_[hash]",
},
plugins: [
new webpack.DllPlugin({
//映射库暴露的内容名字
name: "[name]_[hash]",
path: path.resolve(__dirname, "dll/manifest.json"),
}),
],
};
通过 webpack --config webpack.dll.js 命令打包后,会生成一个dll文件目录,该目录下有两个文件,一个是jquery,另一个是manifest.json映射文件。
第二步是告诉webpack哪些库不需要打包,并将之前打包的dll库映射到项目中(使用时名称会改变)。
第三步,借助add-asset-html-webpack-plugin插件将其引入到html中。
//webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const webpack = require("webpack");
const AddAssetHtmlWebpackPlugin = require("add-asset-html-webpack-plugin");
module.exports = {
mode: "production",
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
},
plugins: [
new HtmlWebpackPlugin({
title: "Hello",
template: "src/index.html",
// minify: {
// //移除空格
// collapseWhitespace: true,
// //移除注释
// removeComments: true,
// },
// filename: "html/admin.html",
}),
//告诉webpack哪些库不需要打包,同时使用时名称也的变。
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, "dll/manifest.json"),
}),
//将某个文件打包输出去,并在html中自动引入该资源
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, "dll/jquery.js"),
}),
],
}
输出HTML:
<body>
<div id="title">Hello World</div>
<p>say you hello-</p>
<script src="jquery.js"></script>
<script src="bundle.js"></script>
</body>
性能优化总结
上述性能优化从开发环境和生产环境两个方面入手。
主要优化点是代码重构率、代码调试性能、代码运行效率等。
webpack5展望
webpack 5 尚未发布,但提供了一个实验版本。 webpack5的github的文档地址是:
文章顶部的原文链接是github地址,可以点击查看webpack5的详细文档。
我们还可以下载 webpack5 的实验版本并尝试新的更新。
npm i webpack@next webpack-cli -D