【1.1w字】webpack学习笔记--深入了解原理

某个技术的出现都是为了解决某些方面的问题。

# 遇到了什么问题呢?

-   **模块化**的出现,我们写代码的时候,尽可能地细分模块,提高代码的可维护性。但是细分模块的结果导致js文件变得更多,给浏览器带来了更多的请求,降低了访问效率。我们期望写代码时模块化分更细,但是浏览器请求的时候如何减少请求。
-   浏览器只能识别ES6模块化,如果某个模块使用commonJS实现,浏览器如何兼容。我们期望**多种模块化标准**我们都可以直接支持。
-   浏览器如何导出npm下载的第三方包呢?node端可以通过node找取文件规则,找到node_modules文件夹下导出的包,ES6如何能找到呢?

```
// jquery
import $ from 'jquery';

//直接放在浏览器运行会报错。 
// 第一:ESM导入文件必须以./或 ../开头 
// 第二:jquery导出方式是commonJs导出。
```

-   浏览器只认识html js css ,那给我们开发中提供便利的如ts less这一类文件如何识别呢?
-   开发出的代码,如何让各大浏览器厂商兼容。
-   我们写代码的时候,希望代码结构,代码质量越清晰越好,便于维护。但是又不期望别人在浏览器访问到我们的文件之后,轻松的看到我们的代码。(知识产权)

这些问题的本质是,**开发时**和**运行时**的侧重点不同。所以能不能出现一种工具,既能让我们开发方便,又能保证运行时的兼容性和代码安全。

所以出现了**构建工具**。社区很多构建工具,先学习webpack:致力于解决前端工程化问题,让开发者集中注意编写代码,其他问题交给webpack。

# webpack 2012年

## 简介

用于现代javeScript应用程序的静态模块打包工具。他会再内部一个或者多个**入口**为起点构建一个**依赖图**,然后将项目中所需要的**所有模块**组合成一个或者多个浏览器兼容的bundles,来展现你的内容。

一切皆模块

## 安装

两个核心包

-   webpack:包含了webpack构建中所有的api
-   webpack-cli:提供一些cli命令,调用api

## 使用

命令行:npx webpack

默认情况下,webpack会./src/index.js 为入口进行分析文件的依赖关系,打包到./dist/main.js。

添加 --mode 判断环境。 webpack会进行进一步处理。 比如dev会有一些注释,生成的文件较为好看。但是pro就是完全压缩的文件内容了。

```
npx webpack --mode=production
npx webpack --mode=development
```

# 核心概念

-   Entry:指定webpack开始构建的入口模块,
-   output:告诉webpack输出的文件名以及输出目录
-   Loaders:由于webpack只能处理js文件,所以提供需要对一些非js的文件进行处理的能力,用来转换
-   Plugins:webpack构建的过程中,会在特定的时机广播事件,插件可以监听这些行为,再特点给的时候介入编译过程,例如:打包优化,压缩,资源管理

# 构建的核心流程

打包的基本机制:

1.  利用babel完成代码转换,并生成单个文件的依赖
1.  从入口开始递归分析,生成依赖图谱
1.  将各个引用模块打包为一个立即执行函数
1.  输出

根据这个基本机制,可以分为更详细的基本步骤,可以分为三个阶段:初始化阶段,构建阶段,生成阶段

## 初始化阶段(**Compiler对象**)

1.  初始化参数:从配置文件,配置对象,shell参数中读取,校验参数,合并出最终的参数。
1.  初始化编译器对象:使用上一步的参数创建**Compiler**对象。
1.  初始化编译环境:遍历用户定义的所有plugins集合,执行插件的apply方法。加载内部插件。
1.  开始编译:执行Compiler对象的run方法
1.  确定入口:找到配置文件的Entry找到所有文件的入口。

## 构建阶段(compilation对象)

6.  从入口文件出发,根据**文件类型**构建**module**对象。
6.  将module对象通过runLoader转换为js文本
6.  将js文本通过解析为AST(babel),
6.  遍历AST(babel)触发各种钩子:解析js文本的资源依赖(识别require/import之类的关键字),并将依赖加入到依赖列表。
6.  使用babel转换代码(ES6转换成ES5)
6.  递归以上步骤,直到所有文件都经过处理,都得到每个模块被编译后的最终内容和他们之间的依赖关系(构建依赖图谱)。

## 生成阶段

经过构建阶段之后,webpack得到了所有的模块内容和模块之间的关系,接下来生成最终资源。

12. 输出资源:根据入口和模块之间的依赖关系(分析module和chunk,将module分配给各自的chunk),组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表,可以在这一步修改输出内容。
12. 根据配置的输出路径和文件(遍历chunk根据规则生成assets集合)写入到文件系统

chunk是输出的基本单位,默认情况下,一个entry会对应打包出一个资源。

**优化chunk:**

多入口打包生成的文件,可能有对相同文件的依赖,使用`SplitChunksPlugin` 可以优化,避免重复打包。

在输出资源的步骤中,先分析modul和chunk的关系;触发各种优化的钩子;遍历module构建chunk;再触发优化钩子。

`SplitChunksPlugin` 就是再构建完chunk之后的钩子中通过分析chunk的内容,生成通用的chunk;

## 资源的历程

先找到文件----根据类型生成module ---- 再进行loader转换---通过babel一系列操作生成依赖图谱(对module操作完毕) ----生成chunk --- 生成assrts集合(写入到系统文件)

# 核心模块

## Loader

可以理解为翻译员。一个Loader的职责是单一的,只需要完成一种转换。

在构建阶段,runLoader会调用用户配置的loader集合读取转义资源,将千奇百怪的内容转换成标准的js文本或者AST对象, webpack才能继续的处理。

本质就是一个Node.js的模块,这个模块需要导出一个函数。这个函数会在加载文件时执行。

### Loader的两种配置

-   config配置(从右往左)

```
module.exports = {
  module: {
    rules: [
      { test: /.css$/, use: 'css-loader',enforce: 'post' },
      { test: /.ts$/, use: 'ts-loader' },
    ],
  },
}

// test正则进行匹配文件。
// use 将匹配到的文件使用对应的loader
// enforce 配置loader的执行顺序, pre:前置loader post:后置loader

作者:19组清风
链接:https://juejin.cn/post/7036379350710616078
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
```

-   行内 inline loader

```
import a from 'raw-loader!../../utils.js'
```

### loader的执行顺序

-   执行 pre loader
-   nomal loader
-   inline loader
-   post loader

### loader的pitch阶段和normal阶段

loader的执行阶段实际上分两个阶段 patch阶段和normal阶段。 平时说的loader的执行顺序就是normal阶段(从右往左)。pitch是倒序调用(从左往右)。

每个 Loader 都可以有一个 `pitch` 方法,这个方法会在正常的 Loader 处理流程之前执行。`pitch` 方法的执行顺序与正常 Loader 执行顺序相反,正常情况下 Loader 是从右到左(或者说从下到上,取决于配置方式)执行,而 `pitch` 方法是从左到右执行。

加入有 a b c 三个loader。 解析这三个loader的顺序是 先 pitch阶段:a -> B ->C 后normal阶段 C ->B ->A

可以给loader导出的函数上添加一个pitch属性, 当解析loader的时候, pitch loader返回非undefined值是,就会发生熔断效果。后续的loader不执行了, 返回上一个loader。

通过pitch方法 可以在这里读取到一些想要的信息 必要时可以提前终止loader链,或者改变后续loader的处理逻辑

### style-loader了解pitch阶段

style-loader做的事情:获取对应的样式文件内容,然后在页面创建style节点, 将内容赋值给style节点,再插入head标签

这个loader的所有逻辑都设计在pitch阶段, normal函数就是一个空函数。

**假如将style-loader设计为normal loader**, 根据平时我们处理css文件时,都会配合css-loader处理。 根据执行顺序, 会先经过css-loader处理,将处理结果再交给style-loader处理。

这样子理解是没错的对吧, 但是我们尝试打印出css-loader的返回结果会发现,返回的是一段js脚本。那style-loader拿到这个脚本,直接插入到style节点中, 显然是不能做到正确显示的。 想要显示,需要将读取这段js脚本的导出内容,才能生效。 这就意味这需要再normal阶段实现一系列js方法。 聪明的你会发现, 这种事情不就是webpack的能力吗?

**那如果设计再pitch阶段有什么好处呢**? 首先pitch阶段如果返回非空内容会进行熔断效果, 后续的pitch loader不再执行, 直接掉头执行 normal loader。

如果再pitch函数中, 通过import/require引入css loader处理文件,返回js脚本,作为这个pitch 的返回值,发生熔断, 掉头执行normal loader ,这样的好处是, 直接将css loader返回的js 脚本交给webpack执行, 不再需要我们去编写处理这段脚本的逻辑。

执行过程:

1.  style-loader的pitch阶段,返回了一段脚本,产生熔断,掉头返还给webpack。
1.  webpack读取这段脚本, 发现了 import/require,编译成一个module,省略(webpack打包步骤)
1.  然后将处理结果,给到pitch阶段进行执行,获取到它的导出内容。

如果loader开发中需要的依赖其他loader,但上一个loader的函数返回结果不是处理好之后的资源文件内容,而是一段js脚本,那么将loader设计在pitch阶段是更合理的。

### 同步Loader or 异步loader

```
// 同步loader

function loader(content){
  return content
}

module.exports = loader;

// 异步loader
function loader(content){
  return  Promise((resolve)=>{
     setTimeout(() => {
            resolve('')
        },3000)
  })
}

// 异步loader
function loader(content){
  const callback = this.async();
  callback()
}

module.exports = loader;
```

### 缓存加速

webpack默认情况下会将loader的处理结果标记为可缓存的, 如果被处理的文件或者依赖的文件没有变化,会使用缓存的结果,可以禁用缓存

```
module.exports = function(source) {
  // 强制不缓存
  this.cacheable(false);
  return source;
};
```

### 实现一个Loader需要注意什么?

-   确保单一职责,让loader的维护变得简单。
-   链式组合,每一个loader接受前一个loader的执行结果。
-   统一原则:遵循webpack指定的设计规定和结构,输入和输出均为字符串

```
function loader(content){
  // 一系列操作
  return content
}

module.exports = loader;
```

### 如何测试自己写的loader

-   直接配绝对路径

```

module.exports = {
    ...
    module: {
        rules: [
            {
                test:/.js$/,
                loader: path.resolve(__dirname,'../loaders/babel-loader.js')
            }
        ]
    }
}
```

-   在webpack的resolveLoader中进行配置。

## Plugin

专注处理webpack在编译过程中的某个特定任务的功能和模块。

是一个独立的模块, 对外暴露一个js函数,需要提供一个apply方法(用于注入comliler对象)通过这个对象可以读取webpack的事件钩子。 钩子的回调中可以拿到编译时的compilation对象,然后对webpack进行操作,在不同的周期触发不同的Hook从而影响最终的打包结果。

1.  初始化阶段会遍历plugin,执行apply方法, 这时候就注册了一系列的监听事件。
1.  当webpack广播事件时,就会触发注册事件的回调函数。
1.  回调函数中通过compiler/compilation提供的api 可以添加module、添加入口,添加编译信息等操作。

```
class SomePlugin {
    apply(compiler) {
    }
}
```

# 打包结果分析

再复习下webpack的流程。 找到入口文件,分析依赖,生成兼容性高的js文件(js降级)。

那是否能按住这个流程我们尝试的写一个打包后的结果呢?我们可以进行反推一波。

1.  生成原生js文件。 原生js代码说明就没有了模块化这一说。不再有commonJS和ESM。再模块化出来之前,我们是如何实现一个模块的呢?---立即执行函数。

```
(function(){
  
})()
```

2.  分析依赖;想要分析依赖,就要先找到文件和它对应的函数,这是不是相当于字典的数据结构。可以将这个对象传入到立即执行函数中。 每个模块的函数中可能会使用到的方法和对象:require, module,export,所以要作为参数传入

```
(function(map){
  
})({
  './src/a.js':function(module,exports,require){
    // ...函数
  },
  './src/inde.js':function(module,exports,require){
    // ...函数
  }
})
```

3.  现在就差从入口文件开始执行,一直将每个所依赖的弄块都执行完,并且创建函数中所需要的对象和方法就ok了。

```
(function (map) {
  //执行函数
  function require(id) {
    //找到对应的模块函数
    var func = map[id];
    //创建module,用来保存函数执行结果,也就是导出的东西。
    var module = {
      export: {},
    };
    //创建export
    var myexport = module.export;
    //执行函数
    func(module, myexport, require);
    // 导出模块结果
    return module.export;
  }

  // 执行入口文件
  require("./src/index.js");
})({
  "./src/a.js": function (module, exports, require) {
    console.log("a");
    module.export = "a";
  },
  "./src/index.js": function (module, exports, require) {
    console.log("index");
    console.log(require("./src/a.js"));
  },
});
```

ok我们成功了。 现在只需要知道, 如何找到入口文件,如何生成依赖分析的map结构。 我们的mini-webpack就成功了。(直接看神三元大佬的文章 [实现一个简单的Webpack](https://juejin.cn/post/6844903858179670030?searchId=202501252311112C587074722AAA776068))

# 文中提到的提到的几个对象

## Compiler

webpack的编译对象,包含了所有webpack的操作配置,例如loader,plugin等等完整配置信息。

该对象会在启动webpack的时候初始化,一直存活到结束退出。

这个对象中包含属性options(webpack 的完整配置信息)、获取文件的API对象、输出文件的相关API对象等

## Compilation

代表一次资源的构建,每次文件重新编译都会创建一个compilation对象。

包含属性有 modules(可以认为一个文件就是一个模块,每个模块可以理解为一个module),chunk(多个modules组合成的代码快,从入口开始分析依赖关系,将多个modules组合成一个chunk),assrts(记录了本次打包的结果)

## Tapable

webpack本质上是一种事件流的机制,实现整个工作流的核心就是Tapable对象。 负责编译的compiler和负责构建的compilation都是Tapable对象的实例(所以在这两个对象中也有很多Hook,可以利用这个Hook做一些事情)。 专注于自定义事件的触发和操作。tapable暴露出了很多钩子,插件可以使用这些钩子创建函数向webpack中注入自定义构建的步骤。

注册hooks的时候可以同步钩子(tap)可以注册异步钩子(tapAsync和tapPromise)。

```
class MyPlugin {
  apply(compiler) {
    // 在done钩子上执行一些操作
    compiler.hooks.done.tap("MyPlugin", (compilation) => {
      console.log("compilation done");
    });
    // 在emit钩子上执行一些操作
    compiler.hooks.emit.tapPromise("MyPlugin", (compilation) => {
      return new Promise((resolve, reject) => {
        setTimeout(()=>{
          console.log("compilation emit");
          resolve();
        }, 1000)
      });
    });
  }
}
```

# 其他

## 如何优化打包速度

1.  缩小文件查找范围:配置include exclude较少范围、配置resolve指定范围。
1.  使用缓存:避免重复打包没有变化的模块
1.  开启多线程loader转换:HappyPack
1.  抽离第三方模块(dllPlugin):webpack只需要打包我们项目本身的文件代码
1.  splitChunks:将公共依赖快提取到chunk中减少代码重复。也可以自定义分割chunk

## 如何减小打包体积

1.  tree-shaking:移除没有使用到的模块,在编译分析依赖的时候, 会对每一个文件进行标记有没有使用, 没有使用最后会被剔除。(必须是ES6模块,依赖于ES6模块的静态结构特性(commonJS可以动态加载模块,分析变得困难),分析模块间的依赖)
1.  压缩代码:uglifyJS
1.  代码层面按需引入
1.  图片优化:可以使用 `image-webpack-loader` 对图片进行压缩。

## webpack热更新

webpack dev server启动一个websocket服务器,进行全双工通信

webpack监听项目中的文件变化, 文件变化之后会重新编译模块,将文件清单Manifest发送给浏览器, 浏览器接收到消息之后,进行替换旧的模块,触发页面刷新。

## source map

通过以上的学习,知道运行代码的是打包之后的代码,并不是我们写的源代码。

如果报错,看到报错的发生的文件是我们打包之后的文件,并且可能是压缩丑化之后的,这就给我们的调试带来了困难。

那么如何能找到报错文件的源地址呢? 配置dectool 开启source map

首先需要明白:source map 是用来调试用的,所以应该再开发环境中使用。 生成的source map文件,也不需要上传到服务器。 如果上传到服务器, 导致额外的网络传输,也会造成自己的代码暴露。

如果上传到服务器, 也需要进行服务器配置,不能让普通用户也能请求到。

## webpack5

2.  长缓存优化:优化了hash算法,尤其是contenthash的生成更加准确。只有文件内容本身未改变,hash就不会改变。 webpack5还引入了模块标识,根据内容生成稳定的ID,而不是依赖模块的索引或者路径。
2.  Tree shaking优化:改进了模块依赖分析算法,更准确的识别哪些模块实际使用,哪些未被使用。

```
// utils.js
export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}

// main.js
import { add } from './utils.js';
console.log(add(1, 2));

// Webpack 5 能准确识别出 subtract 函数未被使用,从而在打包时将其移除。
```

4.  资产模块:新的模块类型,处理字体图标图片等资源(webpack4中使用loader)

```
module.exports = {
    module: {
        rules: [
            {
                test: /.(png|jpg|gif)$/i,
                type: 'asset/resource'
            }
        ]
    }
};
```

5.  模块联邦:多个独立的构建可以组成一个应用程序。 之间不存在依赖关系,可以独立开发和部署(微前端?)。可以让跨应用间真正做到模块共享

```
// 主应用配置
module.exports = {
    plugins: [
        new ModuleFederationPlugin({
            remotes: {
                app2: 'app2@http://localhost:3002/remoteEntry.js'
            }
        })
    ]
};

// 子应用配置
module.exports = {
    plugins: [
        new ModuleFederationPlugin({
            name: 'app2',
            filename: 'remoteEntry.js',
            exposes: {
                './Button': './src/Button'
            }
        })
    ]
};
```

# 参考文章

-   [[万字总结] 一文吃透 Webpack 核心原理 -- 范文杰](https://juejin.cn/post/6949040393165996040#heading-7)
-   [webpack详解 -- 腾讯IVWEB团队](https://juejin.cn/post/6844903573675835400?searchId=202501202221286FA465C0E1BF916D88E2#heading-1)
-   [Webpack 深入浅出之公司级分享总结 -- winty](https://juejin.cn/post/6844904022567043080?searchId=2025012022363420E41B2E862E346E52AD#heading-17)
-   [多角度解析Webpack5之Loader核心原理 -- 19组清风](https://juejin.cn/post/7036379350710616078#heading-32)
-   [Webpack手写loader和plugin -- 谢小飞](https://juejin.cn/post/6888936770692448270?from=search-suggest#heading-8)
-   [实现一个简单的Webpack -- 神三元](https://juejin.cn/post/6844903858179670030?searchId=202501252311112C587074722AAA776068)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值