1、模块打包运行原理
Webpack是如何把这些模块合并到一起,并且保证其正常工作的,你是否了解呢?
webpack的整个打包流程:
a、读取webpack的配置参数;
b、启动webpack,创建Compiler对象并开始解析项目;
c、从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树;
d、对不同文件类型的依赖模块文件使用对应的Loader进行编译,最终转为Javascript文件;
e、整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。
其中文件的解析与构建是一个比较复杂的过程,在webpack源码中主要依赖于compiler和compilation两个核心对象实现。
compiler对象是一个全局单例,他负责把控整个webpack打包的构建流程。compilation对象是每一次构建的上下文对象,它包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler都会重新生成一个新的compilation对象,负责此次更新的构建过程。
而每个模块间的依赖关系,则依赖于AST语法树。每个模块文件在通过Loader解析完成之后,会通过acorn库生成模块代码的AST语法树,通过语法树就可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。
最终Webpack打包出来的bundle文件是一个IIFE的执行函数。(IIFE(Immediately Invoked Function Expressions) 叫做立即执行表达式,顾名思义,该表达式一被创建就立即执行)。
2、缓存
webpack 是如何解决两次引入的?他想问的是webpack是怎么处理模块的,就是引入了一次就会"缓存起来",再次引入会去判断是否引入过
1)babel缓存
设置cacheDirectory: true,可以 让第二次打包构建速度更快。
2)文件资源缓存
a、hash:每次wepack构建时会生成一个唯一的hash值。
存在的问题:因为js和css同时使用一个hash值。如果重新打包,会导致所有缓存失效。(可能我却只改动一个文件)
b、chunkhash:根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样
存在的问题:js和css的hash值还是一样的,因为css是在js中被引入的,所以同属于一个chunk
c、contenthash: 根据文件的内容生成hash值。不同文件hash值一定不一样
3、webpack 热更新
基于 WDS (Webpack-dev-server) 的模块热替换,只需要局部刷新页面上发生变化的模块,同时可以保留当前的页面状态。
热更新的过程:
1)启动阶段:文件经过 Webpack-complier 编译好后传输给 Bundle Server,Bundle Server 可以让浏览器访问到我们打包出来的文件。
2)文件热更新阶段:同时文件经过 Webpack-complier 编译好后传输给 HMR Server,HMR Server 知道哪个资源 (模块) 发生了改变,并通知 HMR Runtime 有哪些变化(也就是上面我们看到的两个请求),HMR Runtime 就会更新我们的代码,这样我们浏览器就会更新并且不需要刷新。
4、webpack的性能优化
1)缩小文件范围 Loader
优化loader配置,使用test、include、exclude三个配置项来缩⼩loader的处理范围,推荐include。
2)优化resolve.modules
resolve.modules用于配置webpack去哪些目录下寻找第三方模块,默认是
['node_modules']。寻找第三方,默认是在当前项目目录下的node_modules里面去找,如果没有找到,就会去上一级目录../node_modules找,再没有会去../../node_modules中找,以此类推,和Node.js的模块寻找机制很类似。
如果我们的第三⽅模块都安装在了项⽬根⽬录下,就可以直接指明这个路径。
3)使用静态资源路径publicPath(CDN)
CDN通过将资源部署到世界各地,使得⽤户可以就近访问资源,加快访问速度。要接⼊CDN,需要把网页的静态资源上传到CDN服务上,在访问这些资源时,使⽤CDN服务提供的URL。
4)借助MiniCssExtractPlugin完成抽离css
5)压缩css和HTML
6)抽取公共代码
在多页应用中,抽离各页面模块公共代码。
5、Source Map
我们知道 Webpack 通过模块之间的引用关系,构建一个依赖树,并生成相应的结果文件。但这个结果文件是存在一定的缺陷的:代码有可能压缩并混淆;代码文件可能是由一个或者多个组成。
Source Map, 顾名思义,是保存源代码映射关系的文件。因为代码是压缩混淆的,我们找不到报错的文件的相关信息,那有没有一个拥有源文件与打包后文件的映射关系的文件,让它来告诉我们呢?这个文件就是 Source Map 文件。
官方文档列出了很多种组合,在这之前,我们可以先好好看看以下的关键字,不管是什么组合都是下面的一个或者多个拼接而成的。
source map:产生 .map 文件(配合 eval 或者 inline 使用的时候,会不生成 source map 文件,具体要看哪个模式)。
eval:使用 eval 包裹块代码。
cheap:不生成列信息。
inline:将 .map 作为 DataURI 嵌入,不单独生成一个 .map 文件。
module:包含 loader 的 source map。
6、webpack异步加载原理及分包策略
webpack ensure 有人称它为异步加载,也有人称为代码切割,他其实就是将 js 模块给独立导出一个.js 文件,然后使用这个模块的时候,再创建一个 script 对象,加入到 document.head 对象中,浏览器会自动帮我们发起请求,去请求这个 js 文件,然后写个回调函数,让请求到的 js 文件做一些业务操作。
需求:main.js 依赖两个 js 文件:A.js 是点击 aBtn 按钮后,才执行的逻辑,B.js 是点击 bBtn 按钮后,才执行的逻辑。
正常情况下,webpack 把 main.js 依赖的两个文件都同时打包到同一个 js 文件,并在 index.html 中引入。但是 A.js 和 B.js 都是点击相应按钮才会执行的逻辑,如果用户并没有点击相应按钮,而且这两个文件又是比较大的话,这样是不是就导致首页默认加载的 js 文件太大,从而导致首页渲染较慢。
1)require.ensure 异步加载
能否实现当用户点击按钮的时候再加载相应的依赖文件呢?require.ensure 异步加载可以解决这个问题。
我们再进行一下打包,发现多了 1.index.js 和 2.index.js 两个文件。而我们打开页面时只引入了 index.js 一个文件,当点击按钮 A 的时候才引入 1.index.js 文件,点击按钮 B 的时候才引入 2.index.js 文件。这样就满足了我们按需加载的需求。
require.ensure 这个函数是一个代码分离的分割线,表示回调里面的 require 是我们想要进行分割出去的,即 require('./A.js'),把 A.js 分割出去,形成一个 webpack 打包的单独 js 文件。它的语法如下:
打开 1.index.js 文件,发现它的代码如下:
a、异步加载的代码,会保存在一个全局的webpackJsonp中。
b、webpackJsonp.push的的值,两个参数分别为异步加载的文件中存放的需要安装的模块对应的 id 和异步加载的文件中存放的需要安装的模块列表。
c、在满足某种情况下,会执行具体模块中的代码。
2)import() 按需加载
webpack4 官方文档提供了模块按需切割加载,配合 es6 的按需加载 import() 方法,可以做到减少首页包体积,加快首页的请求速度,只有其他模块,只有当需要的时候才会加载对应 js。
该函数只接受一个参数,就是引用包的地址,并且使用了 promise 式的回调,获取加载的包。在代码中所有被 import()的模块,都将打成一个单独的包,放在 chunk 存储的目录下。在浏览器运行到这一行代码时,就会自动请求这个资源,实现异步加载。