webpack一文通

前提

首先我们得了解
在这里插入图片描述

Compiler 和 Compilation

compiler对象可以理解为一个和 webpack 环境整体绑定的一个对象,它包含了所有的环境配置,包括 options,loader 和 plugin,当 webpack 启动时,这个对象会被实例化,并且他是全局唯一的,上面我们说到的 apply 方法传入的参数就是它。
compilation在每次构建资源的过程中都会被创建出来,一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。它同样也提供了很多的 hook 。

生命周期函数

  1. beforeRun在 Webpack 开始读取配置之前,该钩子将被调用。
  2. run在 Webpack 开始编译时,该钩子将被调用。
  3. watchRun在使用 webpack-dev-server 进行开发时,该钩子将被调用。
  4. beforeCompile在 Webpack 开始编译之前,该钩子将被调用。
  5. compile在 Webpack 开始编译时,该钩子将被调用。
  6. thisCompilation在创建新的 Compilation 对象时,该钩子将被调用。
  7. compilation在编译时,每当 Webpack 生成一个新的 Compilation 对象时,该钩子将被调用。
  8. emit在生成资源之前,该钩子将被调用。
  9. afterEmit在生成资源之后,该钩子将被调用。
  10. done在 Webpack 编译完成时,该钩子将被调用。
  11. assetEmitted 生命周期钩子是在所有资源(如 JavaScript、CSS、图片等)都已经生成到输出目录中后,即 webpack 打包完毕后触发的。

打包流程

1、我们打包的时候,会先合并 Webpack config 文件和命令行参数,合并为 options
2、将 options 传入Compiler构造方法,生成 compiler 实例,并实例化了 Compiler 上的 Hooks
3、compiler 对象执行run方法,并自动触发 beforeRunrunbeforeCompilecompile 等关键 Hooks。
4、调用 Compilation 构造方法创建 compilation 对象,compilation 负责管理所有模块和对应的依赖,创建完成后触发 make Hook
5、执行 compilation.addEntry() 方法,addEntry 用于分析所有入口文件,逐级递归解析,调用 NormalModuleFactory 方法,为每个依赖生成一个 Module 实例,并在执行过程中触发 beforeResolveresolverafterResolvemodule 等关键 Hooks
6、将第 5 步中生成的 Module 实例作为入参,执行 Compilation.addModule()Compilation.buildModule() 方法递归创建模块对象和依赖模块对象。
7、调用seal方法生成代码,整理输出主文件和 chunk,并最终输出。
在这里插入图片描述

优化

通过speed-measure-webpack-plugin测试打包速度,通过webpack-bundle-analyzer 分析打包后的文件,判断哪些包还可以拆分和优化

主要从两个方面分析

  • webpack构建时间太久,影响开发效率
  • webpack打包的结果体积过大,影响网站的性能

构建优化

  1. 解析loader开启多进程,使用thread-loader
    将​​thread-loader​​放在比较费时间的loader之前,比如​​babel-loader​​
  2. 缓存资源,提高二次构建速度
    将​​cache-loader​​放在比较费时间的loader之前,比如​​babel-loader​​
  3. 开启热更新
    给开发环境下配置plugins
plugins: [
  newwebpack.HotModuleReplacementPlugin()
]
  1. exclude & include
    exclude​​:不需要处理的文件
    ​​include​​:需要处理的文件
//使用include来指定编译文件夹
include: path.resolve(__dirname, '../src'),
//使用exclude排除指定文件夹
exclude: /node_modules/,
  1. 构建区分环境
    开发环境​​:去除代码压缩、gzip、体积分析等优化的配置,大大提高构建速度
    ​​生产环境​​:需要代码压缩、gzip、体积分析等优化的配置,大大降低最终项目打包体积
  2. 并行构建HappyPack
    由于有大量文件需要解析和处理,所以构建是文件读写和计算密集型的操作, 特别是当章优化文件数量变多后, Webpack 构建慢的问题会显得更为严重。运行在 node. 之上的 Webpack 是单线程模型的,也就是说 Webpack 需要一个一个地处理任务,不能同时处理多个任务。
    happy能够让Webpack做到这一点,它将任务分解给多个子进程去并发执行,子进程处理后将结果返回给主进程。
    在这里插入图片描述

打包体积优化

  1. 代码分离
  • 它主要的目的是将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件;
  • 比如默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页 的加载速度;
  • 代码分离可以分出更小的bundle,以及控制资源加载优先级,提供代码的加载性能;

Webpack中常用的代码分离有三种:

  • 入口起点:使用entry配置手动分离代码;正则匹配规则写的更精准;除去无需打包的文件;配置module.noParse,例如JQuery或者lodash…已经是可以直接运行在浏览器的文件,就不必再搜索解析
  • 防止重复:使用Entry Dependencies或者SplitChunksPlugin去重和分离代码;
  • 动态导入:通过模块的内联函数调用来分离代码;import()语法
  1. CDN
  • 在打包的时候我们不再需要对类似于lodash或者axios这些库进行打包;
  • 在html模块中,我们需要自己加入对应的CDN服务器地址;
  1. 提取css文件
    MiniCssExtractPlugin可以帮助我们将css提取到一个独立的css文件中,然后link标签引入css。

  2. JS-CSS代码压缩
    js: terser-webpack-plugin
    css: css-minimizer-webpack-plugin​​

  3. Tree Shaking
    ​​tree-shaking​​简单说作用就是:只打包用到的代码,没用到的代码不打包,而​​webpack5​​默认开启​​tree-shaking​​,当打包的​​mode​​为​​production​​时,自动开启​​tree-shaking​​进行优化

mode: 'production'
  1. 文件压缩
    通过CompressionPlugin插件进行文件压缩
  2. source-map类型
// 开发环境
mode: 'development',
devtool: 'eval-cheap-module-source-map'
// 生产环境
mode: 'production',
devtool: 'nosources-source-map'
  1. 优化resolve.alias配置
    起别名来配置模块的入口地址,跳过递归解析文件的操作,比如react有两套代码,一个是已经打包好的react.min.js,另一个是common.js下所有的模块文件,以react.js为入口文件,在我们使用时,可以直接将文件入口配置到react.min.js,省去递归的过程。
    在这里插入图片描述

用户体验优化

  1. 模块懒加载
    使用​​模块懒加载​​之后,大js文件会分成多个小js文件,网页加载时会按需加载,大大提升首屏加载速度
// 路由懒加载
 {
    path: '/home',
    name: 'home',
    // 懒加载
    component: () => import('../views/home/home.vue'),
  },
  1. 合理配置hash
    改过的文件需要更新hash值,而没改过的文件依然保持原本的hash值,上线后对没有改变的文件依然使用缓存
output: {
  path: path.resolve(__dirname, '../dist'),
  // 给js文件加上 contenthash
  filename: 'js/chunk-[contenthash].js',
  clean: true,
},
  1. Gzip
const CompressionPlugin = require('compression-webpack-plugin')
  plugins: [
    // gzip
    new CompressionPlugin({
      algorithm: 'gzip',
      threshold: 10240,
      minRatio: 0.8
    })
  ]

原理

运行机制

下图是内部的运行机制
站位

模块解释:

Compiler:包含了所有webpack的所有配置信息,包含option|loaders|plugins,该对象有一个方法run,执行该方法开始编译,全局只有一个
Compilation:只要文件被修改,便有compilation被创建,包含当前模块的资源、编译生成资源、变化文件

流程:

在这里插入图片描述

为什么经webpack打包后的文件可以直接在浏览器中运行?

原来一个独立的模块文件被合并到了一个个单独的bundle.js文件中,浏览器不能像node快速加载本地文件,所以就将所有的模块放在一个数组中,执行一次网络请求。被加载过的文件不会执行第二次,执行结果会缓存在内存中。
还需要了解_webpack_require_函数,定义在浏览器中执行的加载函数(模拟node.jsrequire

为什么运行在node(遵循CommonJs)的环境,但webpack引入模块可以使用es6的import xxx from 'xxx'呢?

webpack内部的babel可以将es6的语法转换为CommonJs的语法。

具体webpack的配置,戳这里

webpack的异步模块加载的实现

对于异步方法(import(‘xxModule’)),webpack是怎么处理的呢?
单独打成一个包,采用动态加载的方式,具体过程:当用户触发其加载的动作时,会动态的在head标签中创建一个script标签,然后发送一个http请求,加载模块,模块加载完成以后自动执行其中的代码,主要的工作有两个,更改缓存中模块的状态,另一个就是执行模块代码
import()导入模块后是一个Promise对象,我们可以通过import().then()的方式来处理后续异步的工作

import moduleName from 'xxModule’和import(‘xxModule’)经过webpack编译打包后最终变成了什么?在浏览器中是怎么运行的?

  • import经过webpack打包以后变成一些Map对象,key为模块路径,value为模块的可执行函数;
  • 代码加载到浏览器以后从入口模块开始执行,其中执行的过程中,最重要的就是webpack定义的__webpack_require__函数,负责实际的模块加载并执行这些模块内容,返回执行结果,其实就是读取Map对象,然后执行相应的函数;
  • 当然其中的异步方法(import(‘xxModule’))比较特殊一些,它会单独打成一个包,采用动态加载的方式,具体过程:当用户触发其加载的动作时,会动态的在head标签中创建一个script标签,然后发送一个http请求,加载模块,模块加载完成以后自动执行其中的代码,主要的工作有两个,更改缓存中模块的状态,另一个就是执行模块代码。

以下代码webpack打包出来2个包,a.js因为是异步加载的,所以单独为一个包。
在b.html中访问了b.js,且b.js中异步加载了a.js,浏览器会先返回模块b-chunk.js,然后返回a-chunk.js

// b.html
<html>
	<script src="https://xxx.com/b.js"/>
</html>

// a.js
var a = 'a'
modules.exports = {
	name: 'a'
}

// b.js
var b = 'b'
import('./a.js').then(res => {
	console.log(res, '1s')
})

module.exports = {
	name: 'b'
}

webpack打包后产物分析

  • 总体的文件就是一个 IIFE——立即执行函数
  • webpack 会对加载过的文件进行缓存,从而优化性能
  • 主要是通过 __webpack_require__ 来模拟 import 一个模块,并在最后返回模块 export 的变量
  • 动态加载 import() 的实现主要是使用 JSONP 动态加载模块,并通过 webpackJsonpCallback 判断加载的结果
  • 对于模块的处理方式是,所有的js依赖,打包到一个文件中,然后自己实现一套require和module.exports,让浏览器可以执行源代码
(function(modules) { // webpackBootstrap
	// The module cache
	var installedModules = {};
	function __webpack_require__(moduleId) {
    // ...省略细节
	}
	// 入口文件
	return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({

 "./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {}),
  "./src/sayHello.js": (function(module, __webpack_exports__, __webpack_require__) {})
});
  • 对于commonJs打包产物分析
    • __webpack_modules__:保存文件路径和文件内容的映射关系
    • __webpack_modules_cache__:缓存已加载过的文件,key为文件路径,value为导出内容
    • __webpack_require__:加载文件,将导出内容添加到module.exports和exports对象中
  • ES modules
    • __webpack_require__.d 将传入的对象属性和值遍历到 exports 对象上
    • __webpack_require__.o 判断某属性是否存在于某对象中
    • __webpack_require__.r 给使用 ES Module 实现模块化的文件中 exports 对象里增加 __esModule 属性
  • commonJs和ES modules混合使用
    • __webpack_require__.n 定义遍历赋值为函数,并在该变量上添加a属性,值为函数

热更新原理

  • 通过webpack-dev-server创建两个服务器:提供静态资源的服务(express)和Socket服务
    express server 负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析)
  • socket server 是一个 websocket 的长连接,双方可以通信
  • 当 socket server 监听到对应的模块发生变化时,会生成两个文件.json(manifest文件)和.js文件(update chunk)
  • 通过长连接,socket server 可以直接将这两个文件主动发送给客户端(浏览器)
  • 浏览器拿到两个新的文件后,通过HMR runtime机制,加载这两个文件,并且针对修改的模块进行更新

在这里插入图片描述

  • Webpack Compile:将 JS 源代码编译成 bundle.js
  • HMR Server:用来将热更新的文件输出给 HMR Runtime
  • Bundle Server:静态资源文件服务器,提供文件访问路径
  • HMR Runtime:socket服务器,会被注入到浏览器,更新文件的变化
  • bundle.js:构建输出的文件
  • 在HMR Runtime 和 HMR Server之间建立 websocket,即图上4号线,用于实时更新文件变化

工具包的功能:

  • hot-module-replacement-plugin :提供 HMR 的 runtime,并且将 runtime 注入到 bundle.js 代码里面去。一旦磁盘里面的文件修改,那么 HMR server 会将有修改的 js module 信息发送给 HMR runtime,然后 HMR runtime 去局部更新页面的代码。因此这种方式可以不用刷新浏览器
  • webpack-dev-middleware:本地文件的编译和输出以及监听。
    1. 首先对本地文件代码进行编译打包
    2. 编译结束后,开启对本地文件的监听,调用compiler.watch()
    3. 当文件发生变化,重新编译,编译完成之后继续监听(通过文件的生成时间是否有变化)
    4. 执行setFs方法,这个方法主要目的就是将编译后的文件打包到内存(用的是memory-fs这个包)

tree shaking

Tree-Shaking 是一种基于 ES Module 规范的 Dead Code Elimination 技术,它会在运行过程中静态分析模块之间的导入导出,确定 ESM 模块中哪些导出值未曾其它模块使用,并将其删除,以此实现打包产物的优化。

  1. 如何开启
  • 使用 ESM 规范编写模块代码

  • 配置 optimization.usedExports 为 true,启动标记功能

  • 启动代码优化功能,可以通过如下方式实现:

    • 配置 mode = production
    • 配置 optimization.minimize = true
    • 提供 optimization.minimizer 数组

为什么require(‘./utils/index’)的方式引入就不会被删除掉呢?

Webpack 不支持使用 commonjs、AMD、CMD模块来完成 tree-shaking。仅支持ESM,原因是,旧的模块系统中,导入导出都是动态的,难以预测,但是ESM导入导出都在顶层,且模块名必须为字符串常量,模块之间的依赖关系是确定的,与运行状态无关,只需要对ESM做静态分析,就可以推断出来哪些模块未被使用,可以被优化掉。
Webpack的tree-shaking是代码在运行前完成的,而require(‘./utils/index’)的引入是代码运行的时候才会执行的,所以tree-shaking没有办法对它进行分析删除。
而es6的引入方式是静态的,在运行之前进行了引入,所以打包出来以后就是tree-shaking处理过的。

  1. 实现原理
    先标记模块导出中未被使用的值,使用 Terser 删掉这些没被用到的导出语句
    未被使用到的变量foo通过标记,不会被导出,但该变量foo对应的代码还是存在,通过Terser插件,分析永远不会被执行的代码就会被删除。
    在这里插入图片描述

懒加载实现与原理,如何配置懒加载

通过动态加载的方式

async function handlerUploadClick() {
  // 动态加载 upload 模块,该模块的 default 属性就是 upload 方法
  const { default: upload } = await import("./upload");
  // 调用 upload 方法
  upload();
}

webpack 的懒加载实现在打包时会将懒加载的代码切割出去单独打包,然后在主包中进行按需加载,最后执行调用。

在这里插入图片描述

webpack5

  1. 持久性缓存:更快的构建速度和优化
    通过使用文件内容的哈希来生成缓存文件名,Webpack 5能够在文件内容未发生变化时重用之前构建的结果。
module.exports = {
  // ...其他配置项
  cache: {
    type: 'filesystem', // 使用文件系统缓存
  },
};
  1. 模块联邦:拆分应用,共享模块
    使用模块联邦,每个应用块都应该是一个独立的构建,这些构建都将编译成容器,容器可以被其他应用或容器使用,引用模块的引用者成为host,一个被引用的容器成为remote。
// app1的webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
  // ...其他配置项
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      remotes: {
        app2: 'app2@http://localhost:3002/remoteEntry.js',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
};
  1. Tree Shaking优化
    确保在Webpack配置中启用了mode选项,通常选择production模式
    "sideEffects": false:告诉Webpack该包没有副作用,可以安全地删除未引用的代码
  2. 改进的代码生成:提高性能和加载速度
    Webpack 5通过生成更紧凑、高效的代码,提升了应用的性能和加载速度。这是Webpack的一个重要优化,不需要额外的配置即可生效。
  3. nodeJs的polyfill脚本被移除
    不再为 Node.js 模块自动引用 Polyfills

借鉴:webpack优化文章

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值