【学习笔记】webpack个人学习笔记

最近开始学习webpack,所谓好记性不如烂笔头,现在开始日常笔记。

这次打算换个方式记笔记,我会以各种webpack问题为切入点进行记录。

1. 什么是webpack

webpack是一个对JS模块进行打包的开源工具,其最核心的功能是解决模块之间的依赖,把各个模块按照特定的规则和顺序组织在一起,最终合并为一个或多个JS文件。这个过程就叫模块打包

2.模块打包原理是什么

2.1 webpack打包结果

回答这个问题之前,首先看一个简单的webpack打包结果(bundle),看看它是如何将有依赖关系的模块串联一起的:

// 最外层立即执行匿名函数。用来包裹整个bundle,并构成自己的作用域
 (function(modules) {
    
     
 	// 用于模块缓存。被加载过的模块存储到这个对象里面
 	var installedModules = {
    
    };
 	// 执行入口模块的加载
 	function __webpack_require__(moduleId) {
    
    
 		// 代码部分
 }
 	// 执行入口模块加载 ./src/index.js是我们入口文件
 	return __webpack_require__(__webpack_require__.s = "./src/index.js");
 })
 ({
    
    
 // 以下为打包的模块,它是以Key-value的形式存储被打包的模块,Key是模块id,value是匿名函数包裹的模块实体
 "./src/calcute.js":
 (function(module, exports) {
    
    
    // 模块的内容
}),

 "./src/index.js":
 (function(module, exports, __webpack_require__) {
    
    
   // 打包入口
    const add = __webpack_require__(/*! ./calcute */ "./src/calcute.js").add
    const count = __webpack_require__(/*! ./calcute */ "./src/calcute.js").count
})
});

2.2 bundle在浏览器中执行的顺序

接着了解一下webpack打包结果(bundle)是如何在浏览器中执行的

1.在最外层的匿名函数会初始化浏览器执行的环境,为模块的加载和执行做一些准备工作。

2.加载入口模块,浏览器会从入口模块开始,一个bundle只有一个入口模块。

3.执行模块代码,如果执行到了module.exports则记录下模块的导出值;如果中间有_webpack_require_函数,需要引入其他模块,则会暂时交出执行权,进入加载其他模块的逻辑。

4.在_webpack_require_函数中会判断即将加载的模块是否
加载过,如果加载过,这直接去installedModules 取值,否则回到第三步,执行该模块的代码来获取导出值。

5.所有依赖的模块执行完毕之后,最后执行权回到入口模块。当入口模块的代码执行到结果,也就代表整个bundle运行结束。

2.3 webpack打包原理

通过了解整个流程之后,就可以开始回答这个小结的问题,webpack打包原理是什么?

答:webpack为每个模块创造了一个可以导入和导出模块的环境,但本质上没有修改代码的执行逻辑,因此代码执行的顺序和模块加载的顺序完全一致。

3.CommonJS(cjs)和ES6Module(esm)的区别

3.1 CommonJS是什么

CommonJS是包含模块、文件、IO、控制台在内的一系列标准
CommonJS中规定每个文件是一个模块,会形成一个只属于模块自身的作用域,所有的变量及函数只能自己访问,对外不可见。

3.1.1 导出方式

导出是一个模块向外暴露自身的唯一方式。使用module.exports方式导出模块中的内容,如:

// calcute.js
module.exports = {
    
    
    name:'commonJS',
    add(a,b){
    
    return a+b}
}

或者为了书写方便,也可用另一种写法:

// calcute.js
exports.name = 'commonJS'
exports.add = function(a,b)

3.1.2 导入方式

在CJS中使用require的方式进行模块导入,如:

// index.js
const add = require('./calcute').add
const sum = add(2,3)
console.log(sum)

3.2 ES6 Module是什么

ES6 Module也是将每个文件作为一个模块,不同区别在于导入和导出的方式。

3.2.1 导出方式

esm用export的命令导出模块。export有两种形式:

  • 命名导出

  • 默认导出

  • 1.命名导出

命名有两种写法,如下:
第一种:将变量声明和导出写在一行

// demo.js
export const a = 1
export const b = {
    
    
    name: '1'
}

第二种:先进行变量声明,然后再用同一个export语句导出

//demo.js
const a = 1
const b = {
    
    name:'1'}
export {
    
    a,b}
  • 2.默认导出
    命名导出可导出多个模块,但是默认的只能有一个
//demo.js
export default {
    
    
  a:1,
  b:{
    
    name:'1'}
}

3.2.2 导入方式

esm使用import的方式进行引入,如:

import {
    
     a, b } from './demo1'
console.log(a, b)

3.3 cjs和esm区别

3.3.1 动态与静态

cjs:模块依赖关系的建立发生在代码运行阶段(动态);
esm:模块依赖关系的建立发生在代码编译阶段(静态);

这也是二者最本质的区别。
cjs中require的模块路径可以动态指定,支持传入一个表达式,甚至可以用if语句判断是否加载某个模块;

但是esm却不行,它的导入和导出语句必须为声明式,而且得位于模块的顶层作用域,不能像cjs那样放在if中。

3.3.2 值拷贝和动态映射

cjs:导入一个模块时,cjs获取的是一份导出值的拷贝;
esm:是值的动态映射,并且映射为只读的;

可以看一个例子:

// calcute.js
var count = 0
module.exports = {
    
    
    name:'commonJS',
    count: count,
    add(a,b){
    
    
        count +=1
        return a+b
    }
}
//index.js
const add = require('./calcute').add
const count = require('./calcute').count
console.log(count) //0
add(2,3)
console.log(count) //0

使用cjs的方式,输出count的两次结果都是0,从这个例子可以看出cjs是值拷贝,当导入的模块(calcute.js)发生变化时,使用这个模块的文件(index.js)是不会有任何影响,但这个情况有时并不是我们想要的。

接着看一下esm的引用:

// calcute.js
const count = 0
const add = function(a,b){
    
    
    count +=1
    return a+b
}
export default {
    
    count,add}
// index.js
import {
    
     add, count } from './calcute6'
console.log(count) //0
add(2,3)
console.log(count) //1

使用esm方法的两次输出分别为0,1,从这个例子中可以看出esm导入的变量其实是对原有值的动态映射。index中的count对calcute中的count是实时反映。

另外,从webpack编译出来的结果也可以看出来他们的区别:

再看另一个简单的例子
首先是cjs的写法:

// demo.js
exports.a = 1
exports.b = {
    
    name:'haha'}
// index.js
const {
    
    a,b} = require('./demo') 
console.log(a,b)

这就是一个很简单的写法,这时候看webpack打包出来的结果(下面的结果是提取出来的部分,重点关注使用的两个模块):

 (function(modules) {
    
     
 	// 执行入口的文件
 	return __webpack_require__(__webpack_require__.s = "./src/index.js");
 })
 // 模块打包部分
({
    
    
 "./src/demo.js": (function(module, exports) {
    
    
    exports.a = 1
    exports.b = {
    
    name:'haha'}
 }),

 "./src/index.js":
 (function(module, exports, __webpack_require__) {
    
    
   const {
    
    a,b} = __webpack_require__(/*! ./demo */ "./src/demo.js") 
   console.log(a,b)
 })
});

从这个模块打包模块可以看出来,模块打包的内容就是原文件的内容,基本就是将内容进行的赋值,也就是上面说的一份导出值的拷贝

接着再看esm的写法:

// demo6.js
const a = 1;
const b = {
    
    name:'haha'};
export {
    
    a,b}
// index6.js
import {
    
    a,b} from './demo6'
console.log(a,b)

采用ES6写法的代码和上面的CommonJS的内容近乎一致,接下来看看打包出来的结果:

 (function(modules) {
    
     // webpackBootstrap
 	// Load entry module and return exports
 	return __webpack_require__(__webpack_require__.s = "./src/index6.js");
 })
({
    
    
 "./src/demo6.js":
 (function(module, __webpack_exports__, __webpack_require__) {
    
    

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() {
    
     return a; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "b", function() {
    
     return b; });
  const a = 1;
  const b = {
    
    name:'haha'};
}),

"./src/index6.js":
(function(module, __webpack_exports__, __webpack_require__) {
    
    

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _demo6__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./demo6 */ "./src/demo6.js");

console.log(_demo6__WEBPACK_IMPORTED_MODULE_0__["a"],_demo6__WEBPACK_IMPORTED_MODULE_0__["b"])

})
});

这个打包的结果可以明显看出来,在处理模块时候就有不同了,在esm中模块不在是将内容进行的赋值,而是使用了_demo6__WEBPACK_IMPORTED_MODULE_0__这种对象来获取值,这样就和原模块建立了一种映射接,原先模块的内容变换,_demo6__WEBPACK_IMPORTED_MODULE_0__就会进行变换,这样引入该模块文件中的内容也就会同样进行改变,实现了动态映射。

3.3.3 循环依赖

循环依赖是指模块A依赖模块B,同时模块B依赖模块A,如下例子:

// a.js
import {
    
    foo} from './b.js'
foo()

// b.js
import {
    
    bar} from 'a.js'
bar()

对于这种情况首先要说肯定不支持这么写法,这会带来很大的麻烦。
另外对于这种情况,cjs是没有办法得到预想中的结果,但是使用esm,利用它的动态映射的特性可以支持循环依赖,但是开发者必须确保导入的值被使用时已经设置好正确的导出值。

4.webpack如何确保被加载过的模块不会再次执行模块代码

在加载模块过程中:

  • 如果该模块第一次被加载,webpack首先会执行该模块,然后导出内容;
  • 如果该模块曾被加载过,这时该模块的代码不会再次执行,而是直接导出上次执行后得到的结果。

接着看一下打包的结果,就会了解它的缓存原理:

// 最外层立即执行匿名函数
 (function(modules) {
    
     
 	// 用于模块缓存。被加载过的模块存储到这个对象里面
 	var installedModules = {
    
    };

 	// The require function
 	function __webpack_require__(moduleId) {
    
    

 		// 检查缓存中是否有这个模块
 		if(installedModules[moduleId]) {
    
    
 			return installedModules[moduleId].exports;
 		}
 		// Create a new module (and put it into the cache)
 		var module = installedModules[moduleId] = {
    
    
 			i: moduleId,
 			l: false,
 			exports: {
    
    }
 		};

 		// Execute the module function
 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

 		// Flag the module as loaded
 		module.l = true;

 		// Return the exports of the module
 		return module.exports;
 	}
 }

从上面代码可以看到,在模块中会有一个installedModules 对象用来存放模块的信息,在这个对象中有个属性为loaded即为l,用于记录该模块是否被加载过。它的默认值为false,当这个模块第一次被加载和执行过后会置为true,后面再次加载时候检查到installedModules有这个模块信息时,就不会再次执行模块代码了。

5.webpack常见的loader

5.1 loader的定义

预处理器(loader),它赋予了webpack可处理不同资源类型的能力。

为了更好说明loader是如何工作的,看一下loader的源码结构:

module.exports = function loader (content,map,meta) {
    
    
    var callback = this.async()
    var result = handler(content,map,meta)
    callback(
        null,                //error
        result.content,     //转换后的内容
        result.map,         //转换后的source-map
        result.meta         //转换后的AST
    )
}

从代码上可以看出,每个loader的本质都是一个函数,在该函数中对接受到的内容进行转换,然后返回转换后的结果,用公式表达loader的本质为以下形式:
output = loader(input)

5.2 常用的loader

  1. babel-loader:用来处理ES6,将它编译为ES5
  2. vue-loader:用来加载 Vue.js 单文件组件
  3. file-loader:处理png,jpg,gif这类图片资源时使用,把文件输出到一个文件夹中,并返回pubilcPath,在代码中通过相对 URL 去引用输出的文件
  4. url-loader:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值时返回其 publicPath,小于阈值时返回文件 base64 形式编码 (处理图片和字体)
  5. css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
  6. style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS
  7. eslint-loader:通过 ESLint 检查 JavaScript 代码
  8. i18n-loader: 国际化
  9. ts-loader: 将 TypeScript 转换成 JavaScript
  10. html-loader: 将HTML文件转化为字符串并进行格式化
  11. handlebars-loader: 将 Handlebars 模版编译成函数并返回

6.webpack常见的plugin

6.1 Plugin的定义

webpack的plugins是用于接收一个插件数组,我们可以使用webpack内部提供的一些插件,也可以加载外部插件。

6.2 常用plugin

  1. mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载 (替代extract-text-webpack-plugin)
  2. ignore-plugin:忽略部分文件
  3. html-webpack-plugin:简化 HTML 文件创建 (依赖于 html-loader)
  4. web-webpack-plugin:可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用
  5. webpack-parallel-uglify-plugin: 多进程执行代码压缩,提升构建速度
  6. webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)

7.loader和plugin的区别

loader的存在是做翻译官使用的,因为webpack只认JS,所以需要预处理器对其他类型的资源进行转译。loader的本质就是一个函数,在该函数中对接收的内容进行转换。

plugin就是插件,用来扩展webpack的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

Loader 在 module.rules 中配置,也就是说他作为模块的解析规则而存在,类型为数组

Plugin 在 plugins 中单独配置,类型为数组,每一项是一个 plugin 的实例,参数都通过构造函数传入

8.source map是什么?生产环境怎么用?

8.1 source map是什么

source map 是将编译、打包、压缩后的代码映射回源代码的过程。
经过webpack打包压缩后的代码不具备良好的可读性,这样代码出现了问题就不好排查,而有了source map,再加上dev-tools,想要调试源码就非常容易了。

map文件只要不打开开发者工具,浏览器是不会加载的。

8.2 生产环境怎么用?

为了他提升source map的安全性,线上环境一般有三种处理方案:

hidden-source-map:不会再bundel文件中添加对于Map文件的引用,可以借助第三方错误监控平台 Sentry 使用map文件

nosources-source-map:只会显示具体行数以及查看源代码的错误栈。安全性比 sourcemap 高

sourcemap:通过 nginx 设置将 .map 文件只对白名单开放(公司内网)

9. Webpack 的热更新原理(必考点)

9.1 什么是HMR

webpack热更新又称为模块热替换(Hot Module Replacement, HMR),这个机制可以让代码在网页不刷新的前提下得到最新的改动,甚至不需要重新发起请求就能看到更新后的效果。

使用HMR是有些前提的,webpack本身的命令行不支持HMR,我们需要确保项目是基于webpack-dev-server或者webpack-dev-middle进行开发的。

9.2 HMR的原理

在本地开发环境下,浏览器是客户端,webpack-dev-server(WDS)相当于我们的服务端。

HMR的核心就是客户端从服务端拉取更新后的文件,更准确说是拉取的不是整个资源文件,而是chunk diff(即chunk需要更新的部分)

接下来说一下HMR如何知道更改什么部分:

第一步就是浏览器什么时候去拉取这些更新。这需要WDS对本地源文件进行监听。实际上WDS与浏览器之间维护了一个websocket,当本地资源发生变化时WDS会向浏览器推送更新事件,并带上这个构建的hash,让客户端与上一次资源进行对比。通过hash的比对可以防止冗余更新的出现。

第二步知道拉取什么。现在客户端已经知道新的构建结果和当前的有了差别,就会向WDS发起Ajax请求来获取更改内容(文件列表、hash),该结果返回客户端,这样客户端就可再借助这些信息继续向WDS发起jsonp请求获取该chunk的增量更新。

客户端获取到了chunk的更新之后如何处理?哪些状态该保留?哪些又需要更新?这个就不属于webpack的工作了,但它提供了相关 API 以供开发者针对自身场景进行处理。

10.如何优化 Webpack 的构建速度?

优化构建速度的方法有很多,就看要讲多久,简单说几种方法:

  1. 使用高版本的webpack: 高版本支持的功能更多,进行了更多优化

  2. 使用多进程构建:HappyPack(不维护了)、thread-loader

  3. 压缩代码: 通过 mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件

  4. 缩小打包作用域:

    • 使用include或者exclude指定打包的内容
    • 使用noParse 对完全不需要解析的库进行忽略,虽然不会解析,但仍会打包到bundle中
    • 使用IgnorePlugin 完全排除一些模块,被排除的模块即使被引用了,也不会被打包进资源文件中
  5. 使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中

  6. 使用 SplitChunksPlugin(webpack4+)进行代码分割

  7. 使用DllPlugin:借鉴动态链接库的思路,对于第三方模块或者一些不常变化的模块,将他们预先编译和打包,然后在项目实际构建中直接取用。

  8. 利用tree shaking:
    1 tree shaking功能可以在打包过程中检测“死代码”,对这部分代码进行标记,并在资源压缩时将他们从最终的bundle中去掉。
    2 tree shaking只对ES6 Module生效,所以要多用ES6 Module模块
    3 禁用babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking
    4 使用压缩工具去除死代码:如 terser-webpack-plugin

11.什么是代码分割(或者代码分片)

11.1 代码分片意义

代码分片是webpack作为打包工具所特有的一项技术,通过这项技术我们可以把代码按照特定的形式进行拆分,使用户不必一次全部加载,而是按需加载。

11.2 分片方法

webpack提供了插件进行分片操作,CommonChunkPlugin是webpack 4之前的内部插件,SplitChunks是webpack 4之后的插件,比CommonChunkPlugin功能更加强大,还简单易用。

除了使用插件,还可以进行资源的异步加载。当模块数量过多,资源体积过大时,可以把一些暂时用不到的模块延迟加载,这样页面初次渲染的时候用户下载的资源也会尽可能的小,后续的模块等到适当实际再去出发加载。

使用这些方法进行代码分片,可以有效地缩小资源体积,同时更好的利用缓存,提高用户体验。

12.webpack与grunt、gulp的不同

三者都是前端构建工具
grunt 和 gulp 是基于任务和流的。找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程.其缺点是集成度不高,要写很多配置后才可以用,无法做到开箱即用。

webpack 是基于入口的。webpack 会自动地递归解析入口所需要加载的所有资源文件,然后用不同的 Loader 来处理不同的文件,用 Plugin 来扩展 webpack 功能

从构建思路来说 gulp和grunt需要开发者将整个前端构建过程拆分成多个Task,并合理控制所有Task的调用关系

webpack需要开发者找到入口,并需要清楚对于不同的资源应该使用什么Loader做何种解析和加工

猜你喜欢

转载自blog.csdn.net/baidu_33438652/article/details/107436658