一、什么是Webpack?为什么要使用Webpack?

构建工具指能自动对代码执行检验、转换、压缩等功能的工具。常见功能包括:代码转换、代码打包、代码压缩、HMR、代码检验。构建工具也随着前端技术的发展,从Browserify、Gulp到Parcel,从Webpack到Rollup,一直到最近比较火的面向非打包的Snowpack和Vite。

想要理解为什么要使用 webpack,我们先回顾下历史,在打包工具出现之前,我们是如何在 web 中使用 JavaScript 的:

(1)那当然是直接在HTML文件中通过Script标签引用了,要么引用一个大型的.js文件,要么引用多个小的.js文件。无论引用一个大的或多个小的,都会出现可维护性、作用域等相关问题。

(2)随着前端的发展,项目大而复杂,仍然使用第一阶段的方式显然存在很大的局限性。这时出现了立即调用函数表达式(IIFE)- Immediately invoked function expressions。

IIFE解决大型项目的作用域问题,当脚本文件被封装在IIFE内部时,你可以安全地拼接或安全地组合所有文件而不必担心作用域冲突。

IIFE使用方式产生出了Make、Gulp、Grunt、Broccoli和Brunch等工具。这些工具被称为任务执行器,它们的作用就是将所有项目文件拼接在一起。拼接的好处就是可以跨文件重用脚本。

这些任务执行器无法判断代码是否被实际使用,比如你只用到lodash中的某个函数,但任务执行器会在构建结果中加入整个库。(都能做到跨文件重用脚本,这一点优化不了?奇怪)(webpack通过显性依赖,即就是每个模块明确表述它自身的依赖,避免了打包未使用的模块)

(3)JavaScript模块的概念真正诞生。

Node.js 是一个 JavaScript 运行时,可以在浏览器环境之外的计算机和服务器中使用。webpack 就运行在 Node.js 中。当 Node.js 发布时,一个新的时代开始了,它带来了新的挑战:既然不是在浏览器中运行 JavaScript,现在已经没有了可以添加到浏览器中的 html 文件和 script 标签。那么 Node.js 应用程序要如何加载新的代码 chunk 呢?(就是能在浏览器之外的环境运行js,但是多个js文件之间无法相互引用)

这时,CommonJS 问世并引入了 require 机制,它允许你在当前文件中加载和使用某个模块。标志着JavaScript模块的诞生。

(4)虽然CommonJS是NodeJS项目的绝佳解决方案,但浏览器不支持该模块,因而产生了Browserify、RequireJS和SystemJS等打包工具,允许我们编写能够在浏览器中运行的CommonJS模块。

(5)ESM模块(指的是ES6中引入的import、 export),至此模块正式成为ECMAScript标准的官方功能。

总结:

1、webpack用于编译模块化的JS文件,这是webpack开箱可用的自带功能。

回顾一路走来,随着前端项目工程化,所以代码需要模块化。一直发展出ESM,代码好写了,但是编译难了,不再像以前手动在顶部声明所有依赖,浏览器自动解析HTML中Script标签。现在出现了import、export等,这些肯定不能被浏览器直接解析,所以出现了webpack,它能对JavaScript应用程序进行依赖推断然后打包成最原始的,能被浏览器直接解析的结果。

注意,webpack不会更改代码中除了import和export语句之外的部分,如果你在使用其他ES6特性,要确保你在webpack loader系统中使用了一个像是babel的转译器。

2、使用loader实现新语法的转换(如TS、ES6+、SCSS等)

现今的很多网页其实可以看做是功能丰富的应用,它们拥有着复杂的JavaScript代码和一大堆依赖包。为了简化开发的复杂度,前端社区涌现出了很多好的实践方法:

(1)模块化,让我们可以把复杂的程序细化为小的文件;

(2)类似于TypeScript这种在JavaScript基础上拓展的开发语言:使我们能够实现目前版本的JavaScript不能直接使用的特性,并且之后还能转换为JavaScript文件使浏览器可以识别;

(3)Scss,less等CSS预处理器;

这些改进确实大大的提高了我们的开发效率,但是利用它们开发的文件往往需要进行额外的处理才能让浏览器识别,而手动处理又是非常繁琐的,这就为WebPack类的工具的出现提供了需求。

webpack 作为打包工具,通过入口文件递归构建一个依赖关系图,将所有模块引入整理后,再通过 loader 和 plugin 的处理(这句话有问题,依赖图的生成要先经过loader的处理),最终将这些模块打包成一个或多个bundle。

webpack 就像一条生产线,要经过一系列处理流程(loader)后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。

插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。

二、webpack和Grunt、Gulp的比较

1、webpack是基于Node.js开发出的一个前端项目构建工具。

2、Webpack的工作方式是:把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript文件。

3、Gulp/Grunt是一种能够优化前端的开发流程的工具。它们的工作方式是:在一个配置文件中,指明对某些文件进行类似编译,组合,压缩等任务的具体步骤,工具之后可以自动替你完成这些任务。

4、Gulp是基于task任务的构建工具,小巧灵活;webpack是基于整个项目的模块化的解决方案。借助于webpack,可以完美实现资源的合并、打包、压缩、混淆、处理依赖关系等诸多功能,在很多场景下可以替代Gulp/Grunt类的工具。

三、Webpack核心概念

webpack构建工具就是将源代码转换成可被浏览器编译执行的JavaScript、CSS、HTML代码。

Webpack有以下几个核心概念:

(1)Entry:入口,Webpack执行构建的第一步将从entry开始,可抽象成输入。

(2)Module:模块,配置处理模块的规则;在Webpack里一切皆模块,一个模块对应一个文件(包括源码组件、图片、各类文件格式的文件等);Webpack会从配置的Entry开始递归找出所有依赖的模块;

(3)Loader:模块转换器,用于将模块的原内容按照需求转换成新内容;(比如ts-loader打包编译TypeScript文件;css-loader将css文件变成commonjs模块加载到js中)

(4)Resolve:配置寻找模块的规则;

(5)Plugin扩展插件,在Webpack构建流程中的特定时机会广播对应的事件,插件可以监听这些事情的发生,在特定的时机做对应的事情;(Plugin功能比loader更强大,主要目的就是解决loader无法实现的事情,比如打包优化和代码压缩等)

(6)Output:输出结果,在Webpack经过一系列处理并得出最终想要的代码后输出结果;

(7)Chunk:代码块,一个Chunk由多个模块组合而成,用于代码合并与分割。

四、Webpack工作流程概述

命令行执行npx webpack打包命令开始:

1、初始化:从配置文件和Shell语句中读取和合并参数,根据参数初始化Compiler实例,加载Plugin,然后调用Compiler实例的run方法开始进行编译。

(Compiler编译对象掌控着webpack生命周期;Webpack会在特定的时间点广播特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑)

2、编译:从entry出发,调用所有的Loader对模块进行解析翻译,再找出该模块依赖的模块,再递归直到所有依赖的文件都经过了的处理。最终生成依赖关系图。

3、根据依赖关系图,组装成包含多个模块的chunk,最终根据配置确定输出的路径和文件名进行文件输出。

webpack 的运行流程是一个串行的过程,它的工作流程就是将各个插件串联起来。

其中loader运行在编译阶段,plugins在整个周期起作用。

五、webpack内部原理

1、项目中使用的每个文件都是一个模块。通过互相引用,这些模块会形成一个图数据结构。

(1)在打包过程中,模块会被合并成chunk。chunk合并成chunk组,并最终生成一个依赖图。

1
2
3
4
5
6
7
8
9
10
11
// ./webpack.config.js
module.exports = {
entry: './index.js',
};

// 是以下形式的简写:
// module.exports = {
// entry: {
// main: './path/to/my/entry/file.js',
// },
// };

这会创建出一个名为main的chunk组(main是入口起点的默认名称),此chunk组包含./index.js模块。随着parser处理./index.js内部的import时,新模块就会被添加到此chunk中。

(2)chunk有两种形式:

  • initial:是入口起点的main chunk。此chunk包含入口起点指定的所有模块及其依赖项。

  • non-initial:是可以延迟加载的块。可能会出现在使用动态导入时。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // webpack.config.js
    module.exports = {
    entry: './src/index.jsx',
    };

    // ./src/index.jsx
    import React from 'react';
    import ReactDOM from 'react-dom';

    import(
    /* webpackChunkName: "app" */ // 指定non-initial名称
    './app.jsx'
    ).then((App) => {
    ReactDOM.render(<App />, root);
    });

这会创建一个名为main的initial chunk。其中包括:./src/index.jsx、react、react-dom。然后会为./app.jsx创建non-initial chunk。

最终打包生成的就会是:

1
2
/dist/main.js  // 一个initial chunk
/dist/394.js // non-initial chunk。如果需要指定chunk名称,看上述代码注释

(3)output配置中out.filename用于配置initial chunk文件名;out.chunkFilename用于non-initial chunk文件名

2、manifest

在使用webpack构建的典型应用程序或站点中,有三种主要的代码类型:

  • 你或你的团队编写的代码
  • 你的源码会依赖的任何第三方的library代码
  • webpack的runtime和manifest,管理所有模块的交互

(1)runtime,以及伴随的manifest数据,主要是指:在浏览器运行过程中,webpack用来连接模块化应用程序所需的所有代码。它包括:在模块交互时,连接模块所需的加载和解析逻辑。包括:已经加载到浏览器中的连接模块逻辑,以及尚未加载模块的延迟加载逻辑。

(2)manifest

一旦你的应用在浏览器中以 index.html 文件的形式被打开,一些 bundle 和应用需要的各种资源都需要用某种方式被加载与链接起来。在经过打包、压缩、为延迟加载而拆分为细小的 chunk 这些 webpack 优化之后,你精心安排的 /src 目录的文件结构都已经不再存在。所以 webpack 如何管理所有所需模块之间的交互呢?这就是 manifest 数据用途的由来……

当 compiler 开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 “manifest”,当完成打包并发送到浏览器时,runtime 会通过 manifest 来解析和加载模块。无论你选择哪种模块语法,那些 import 或 require 语句现在都已经转换为 __webpack_require__ 方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够检索这些标识符,找出每个标识符背后对应的模块。

六、面试题

webpack的原理,loader和plugin是干什么的?

(1)原理:先编译,从entry出发,调用所有的Loader对模块进行解析翻译,再找出该模块依赖的模块,再递归直到所有依赖的文件都经过了的处理,最终生成依赖关系图。

根据依赖关系图,组装成包含多个模块的chunk,最终根据配置确定输出的路径和文件名进行文件输出。loader运行在编译阶段,plugins在整个周期起作用。

(2)Loader:由于webpack仅仅用于编译JS模块或者JSON模块,所以当遇到图片、css等资源时,就需要配置对应的loader。

(3)Plugin:扩展插件,在Webpack构建流程中的特定时机会广播对应的事件,插件可以监听这些事情的发生,在特定的时机做对应的事情;(Plugin功能比loader更强大,主要目的就是解决loader无法实现的事情,比如打包优化和代码压缩等)