Webpack

1. 说说你对 webpack 的理解?解决了什么问题?

最初的 js 没有模块化,一个文件就是一个模块。这样一方面会带来全局变量污染的问题,还会因为引入顺序出现问题。后来有了 ESM 和 commonJs 模块化方案,js 开始进入模块化开发。这时候出现了新的问题,比如:

  • 模块化支持:浏览器并不支持 ESM 和 commonJs (现在可以通过设置 script 标签的 type=module 来支持 ESM)。
  • 代码兼容处理:jsx => js,ES 降级,sass/less => css 需要各类工具处理。

webpack 就解决了此类问题,主要做了以下工作:

  • 模块化开发支持:支持直接从 node_modules 引入代码,支持多重模块化

  • 处理代码的兼容性:比如 ES6 的代码降级,jsx 转换为 js, less/sass 转换为 css(不是构建工具做的,构建工具将这些工具集成进来自动化处理)

  • 提高项目性能:压缩代码,代码分割

  • 提高开发体验:提供开发服务器webpack-dev-server,能够解决服务跨域的问题(本地代理)。监听文件的变化,文件变化后能够自动调用相应的工具重新处理、打包,在浏览器重新运行(热更新)

本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图,然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。

2. 说说 webpack 的构建流程?

  • 读取配置:Webpack 会读取项目根目录下的配置文件(如 webpack.config.js),或者根据命令行参数获取配置信息。

  • 解析配置:Webpack 解析配置文件,获取入口文件、出口文件、Loader、Plugin 等配置信息。

  • 创建 Compiler 对象:Webpack 使用解析后的配置创建一个 Compiler 对象。Compiler 对象负责控制整个打包过程,是构建过程的主要实例。

  • 加载内置插件和配置的插件:Webpack 内置了一些常用插件,并根据配置加载用户配置的插件。

  • 构建 Compilation 对象:Compiler 创建一个 Compilation 对象表示当前的构建过程。Compilation 对象包含当前构建的所有模块、依赖图等信息。

  • 根据入口文件开始递归构建:Webpack 根据配置中的入口文件(entry)开始递归解析所有依赖的模块。

  • 解析模块:Webpack 使用 Parser 对象解析模块代码,找到模块之间的依赖关系。

  • 加载模块:根据模块的类型和配置的 Loader,Webpack 加载模块的源代码。

  • 转换模块:Webpack 使用配置的 Loader 对模块的源代码进行转换,将其转换为 Webpack 可以识别的代码。

  • 构建模块:将转换后的模块添加到 Compilation 对象中,并递归处理模块的依赖关系。

  • 构建 Chunk:根据模块之间的依赖关系,将相关联的模块组合成 Chunk。

  • 输出文件:将生成的 Chunk 输出到指定的输出目录中,生成打包后的静态文件。

  • 应用 Plugin:在特定的构建步骤中触发插件,插件可以在构建过程中执行自定义的操作。

3. 说说 webpack 中常见的 Loader?解决了什么问题?

webpack 是基于 node 的,默认只能处理 JS 和 JSON 文件,loader 的作用是用来处理其他类型的文件。 loader 用以对某个文件进行 import 或者 require,在此过程中可能会涉及到解析与编译,如 js 通过 babel 进行编译。

常见的 loader 如下:

  • style-loader: 将 css 添加到 DOM 的内联样式标签 style 里
  • css-loader :允许将 css 文件通过 require 的方式引入,并返回 css 代码
  • less-loader: 处理 less
  • sass-loader: 处理 sass
  • postcss-loader: 用 postcss 来处理 CSS
  • file-loader: 分发文件到 output 目录并返回相对路径 (webpack5 已经内置)
  • url-loader: 和 file-loader 类似,但是当文件小于设定的 limit 时可以返回一个 Data Url(webpack5 已经内置)
  • babel-loader: 用 babel 来转换 ES6 文件到 ES5

4. 说说 webpack 中常见的 Plugin?解决了什么问题?

loaders 的作用是转换其他类型的语言到 JS 语言, plugins 可以做其他所有 loaders 做不了的事情, 比如:

  • bundle optimization(bundle 优化)
  • assets management(assets 管理)
  • injection of environment variables(注入环境变量)

常见的 Plugin 有:

  • HTMLWebpackPlugin: ⾃动生成⼀个 html ⽂文件,并把打包生成的 js 模块引⼊到该 html 中
  • CopyWebpackPlugin: 复制文件或目录到 dist 目录
  • CleanWebpackPlugin: 清理构建目录(webpack5 已经内置)
  • MiniCssExtractPlugin: 提取 CSS 到一个单独的文件中
  • DefinePlugin: 允许在编译时创建配置的全局对象,是一个 webpack 内置的插件,不需要安装

5. 说说 Loader 和 Plugin 的区别?编写 Loader,Plugin 的思路?

从功能上来讲:

  • loader 主要用来处理 webpack 不能处理的文件,比如 less-loader 可以处理 less

  • plugin 可以让 webpack 实现不能实现的功能, 比如 html-webpack-plugin 可以创建一个 html 模板

从运行时间来讲:

  • loader 运行在打包文件之前

  • plugin 运行在整个编译周期

从本质来讲:

  • loader 就是一个函数。loader 能够链式调用。接收上一个 loader 的处理结果,返回下一个 loader 要接收的结果。

  • plugin 是一个类。我们需要关注 constructor 和 apply 方法。constructor 能够获取插件使用时传入的参数。apply 中能够调用编译各个阶段的 hooks,从而在对应的时机执行插件逻辑。

自定义 loader:

// 导出一个函数,source为webpack传递给loader的文件源内容
module.exports = function (source) {
  const content = doSomeThing2JsString(source);

  // 如果 loader 配置了 options 对象,那么this.query将指向 options
  const options = this.query;

  // 可以用作解析其他模块路径的上下文
  console.log('this.context');

  /*
   * this.callback 参数:
   * error:Error | null,当 loader 出错时向外抛出一个 error
   * content:String | Buffer,经过 loader 编译后需要导出的内容
   * sourceMap:为方便调试生成的编译后内容的 source map
   * ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程
   */
  this.callback(null, content); // 异步
  return content; // 同步
};

自定义 plugin:

class MyPlugin {
  // Webpack 会调用 MyPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler) {
    // 找到合适的事件钩子,实现自己的插件功能
    compiler.hooks.emit.tap('MyPlugin', (compilation) => {
      // compilation: 当前打包构建流程的上下文
      console.log(compilation);

      // do something...
    });
  }
}

6. 说说 webpack 的热更新是如何做到的?原理是什么?

HMR 全称 Hot Module Replacement,可以理解为模块热替换,在应用运行过程中修改了某个模块,通过自动刷新会导致整个应用的整体刷新,那页面中的状态信息都会丢失 如果使用的是 HMR,就可以实现只将修改的模块实时替换至应用中,不必完全刷新整个应用

关于 webpack 热模块更新的总结如下:

  • 监听文件变化:Webpack 开发服务器会在后台监听源代码文件的变化,当有文件发生修改时,Webpack 会感知到这些变化。

  • 打包更新的模块:当有文件变化时,Webpack 会将修改过的模块及其依赖重新打包,生成一个新的临时模块。

  • 构建补丁(Patch):Webpack 会生成表示当前模块状态的补丁(Patch),包含了需要替换的旧模块和新模块的差异信息。

  • 发送更新到浏览器:Webpack 将生成的补丁通过 WebSocket 或者其他实时连接机制发送到浏览器端。

  • 应用补丁:浏览器接收到补丁后,会根据补丁信息进行模块的替换,用新模块替换掉旧模块,从而实现实时更新。

需要注意的是,HMR 并不是完全取代整个模块,而是替换部分代码块或模块。这样做的好处是减少了页面的重新加载时间,只更新需要更新的部分,同时保持了应用的状态,避免了页面重置和状态丢失。

webpack 基于 express 启动的本地服务。webpack 会监听本地文件,一旦出现文件修改,则会对修改的文件进行重新打包,产生一个 manifest.json 文件和补丁文件。然后通过 websocket 把文件 hash 值发送到浏览器端。 然后浏览器通过 hash 值获取响应的资源更新页面。

7. 说说 webpack proxy 工作原理?为什么能解决跨域?

proxy 工作原理实质上是利用 http-proxy-middleware 这个 http 代理中间件,实现请求转发给其他服务器 举个例子: 在开发阶段,本地地址为 http://localhost:3000,该浏览器发送一个前缀带有/api 标识的请求到服务端获取数据,但响应这个请求的服务器只是将请求转发到另一台服务器中

const express = require('express');
const proxy = require('http-proxy-middleware');

const app = express();

app.use('/api', proxy({ target: 'http://www.example.org', changeOrigin: true }));
app.listen(3000);

// http://localhost:3000/api/foo/bar -> http://www.example.org/api/foo/bar

8. 说说如何借助 webpack 来优化前端性能?

通过 webpack 优化前端的手段有:

  • JS 代码压缩
  • CSS 代码压缩
  • Html 文件代码压缩
  • 文件大小压缩
  • 图片压缩
  • Tree Shaking
  • 代码分离: 将代码分离到不同的 bundle 中,之后我们可以按需加载,或者并行加载这些文件 默认情况下,所有的 JavaScript 代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度 代码分离可以分出出更小的 bundle,以及控制资源加载优先级,提供代码的加载性能 这里通过 splitChunksPlugin 来实现,该插件 webpack 已经默认安装和集成,只需要配置即可
  • 内联 chunk 可以通过 InlineChunkHtmlPlugin 插件将一些 chunk 的模块内联到 html,如 runtime 的代码(对模块进行解析、加载、模块信息相关的代码),代码量并不大,但是必须加载的

关于 webpack 对前端性能的优化,可以通过文件体积大小入手,其次还可通过分包的形式、减少 http 请求次数等方式,实现对前端性能的优化

9. 如何提高 webpack 的构建速度?

常见的提升构建速度的手段有如下:

  • 优化 loader 配置 通过 include、exclude 来缩减范围

  • 合理使用 resolve.extensions 当我们引入文件的时候,若没有文件后缀名,则会根据数组内的值依次查找 当我们配置的时候,则不要随便把所有后缀都写在里面,这会调用多次文件的查找,这样就会减慢打包速度

  • 优化 resolve.modules resolve.modules 用于配置 webpack 去哪些目录下寻找第三方模块。默认值为['node_modules'],所以默认会从 node_modules 中查找文件 当安装的第三方模块都放在项目根目录下的 ./node_modules 目录下时,所以可以指明存放第三方模块的绝对路径,以减少寻找,配置如下:

    module.exports = {
      resolve: {
        // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
        // 其中 __dirname 表示当前工作目录,也就是项目根目录
        modules: [path.resolve(__dirname, 'node_modules')]
      }
    };
    
  • 优化 resolve.alias

  • 使用 DLLPlugin 插件

  • 使用 thread-loader 对于比较耗时的 loader,比如 babel-loader。 可以启动一个新的进程处理响应文件

  • 合理使用 sourceMap 打包生成 sourceMap 的时候,如果信息越详细,打包速度就会越慢。

10. 与 webpack 类似的工具还有哪些?区别?

  • rollup Rollup 是一款 ES Modules 打包器,从作用上来看,Rollup 与 Webpack 非常类似。不过相比于 Webpack,Rollup 要小巧的多 可以看到 Rollup 的优点:

代码效率更简洁、效率更高 默认支持 Tree-shaking 但缺点也十分明显,加载其他类型的资源文件或者支持导入 CommonJS 模块,又或是编译 ES 新特性,这些额外的需求 Rollup 需要使用插件去完成

综合来看,rollup 并不适合开发应用使用,因为需要使用第三方模块,而目前第三方模块大多数使用 CommonJs 方式导出成员,并且 rollup 不支持 HMR,使开发效率降低

但是在用于打包 JavaScript 库时,rollup 比 webpack 更有优势,因为其打包出来的代码更小、更快,其存在的缺点可以忽略

  • vite vite ,是一种新型前端构建工具,能够显著提升前端开发体验

它主要由两部分组成:

一个开发服务器,它基于 原生 ES 模块 提供了丰富的内建功能,如速度快到惊人的 [模块热更新 HMR 一套构建指令,它使用 Rollup 打包你的代码,并且它是预配置的,可以输出用于生产环境的优化过的静态资源 其作用类似 webpack + webpack-dev-server,其特点如下:

快速的冷启动 即时的模块热更新 真正的按需编译 vite 会直接启动开发服务器,不需要进行打包操作,也就意味着不需要分析模块的依赖、不需要编译,因此启动速度非常快

利用现代浏览器支持 ES Module 的特性,当浏览器请求某个模块的时候,再根据需要对模块的内容进行编译,这种方式大大缩短了编译时间

vite 主要的提升还是针对于开发阶段,生产打包使用的是 rollup

  • webpack 相比上述的模块化工具,webpack 大而全,很多常用的功能做到开箱即用。有两大最核心的特点:一切皆模块和按需加载

与其他构建工具相比,有如下优势:

  • 智能解析:对 CommonJS 、 AMD 、ES6 的语法做了兼容
  • 万物模块:对 js、css、图片等资源文件都支持打包
  • 开箱即用:HRM、Tree-shaking 等功能
  • 代码分割:可以将代码切割成不同的 chunk,实现按需加载,降低了初始化时间
  • 插件系统,具有强大的 Plugin 接口,具有更好的灵活性和扩展性
  • 易于调试:支持 SourceUrls 和 SourceMaps
  • 快速运行:webpack 使用异步 IO 并具有多级缓存,这使得 webpack 很快且在增量编译上更加快
  • 生态环境好:社区更丰富,出现的问题更容易解决

21. 性能优化

一般说到性能优化,我们可能主要关注三个方面:
  • 打包速度和大小
  • 首屏渲染时间
  • 用户交互响应

一定程度上,包越小也就意味着首屏渲染越快。

对于 webpack 配置,我们可以做这些优化:

  • 压缩 JS,css,单独提取 css
  • 使用 loader 的时候,配上 include/exclude,减小范围
  • 编译 jsx 时候,使用 swc 替换 babel(swc 使用 rust 编写,速度比 babel 快很多倍,亲测有用)
  • 按需引入,antd
  • 替换一些包,比如 moment 换成 day.js,lodash 替换成 lodash-es(通过 alias),以便于 tree-shaking
  • 代码分割,使用动态导入配合 react.lazy 实现懒加载
  • splitChunk,将常用的依赖库分成一个包
  • cdn
  • 魔法注释实现预获取和预加载
  • dropConsole 去除 console
  • gzip 压缩,需要再 nginx 上配置
  • http2 替代 http1.1 nginx 配置
  • http 缓存,nginx 配置

性能工具:

  • webpack-bundle-analysis:来进行包大小分析,哪些包比较大替换掉
  • performance: api 可以获取 FP,FCP,LCP 等指标数据,来进行具体分析。还可以分析内存是否泄漏,网络是否阻塞,长任务等等
  • lighthouse:会给网站打个评分,推荐一些优化策略

代码方面:

  • 密集型计算使用 web worker
  • 函数,请求缓存
  • 使用缓存相关的 hooks 避免不必要的渲染, memo, useMemo, useCallback
  • 节流,防抖
  • 自定义 hook 复用逻辑
  • css 动画,替换 js 动画
  • 避免重绘和回流
  • 精灵图
  • 避免使用@import

本地服务启动,终极解决方案使用 vite

22. webpack 知识点

core-js:包含所有 ES6+的 polyfill, 并集成在 babel 中

Webpack 运行时(Webpack Runtime)指的是打包后的代码中所包含的用于模块解析、加载和执行的代码部分。

比如热更新,code split 的动态导入都依赖运行时。

代码分割:

  • 使用 splitChunk, 一般用来处理依赖的分包
  • 使用动态导入,配合 react.lazy 实现懒加载

tree-shaking:

  • webpack5 默认开启 tree-shaking
  • 使用 esModule 的依赖才会 tree-shaking,比如 lodash-es,而不是 lodash
  • 使用动态导入的组件,tree-shaking 会失效,因为动态导入的模块依赖在编译时无法确定。

Webpack 5 默认开启了持久缓存,并使用 chunkhash 作为哈希值来实现持久缓存。每次构建时,只有发生改变的文件的哈希值会发生变化,从而让浏览器能够正确地使用缓存。这样,在没有文件内容发生改变的情况下,Webpack 就可以直接使用之前的缓存结果,大大加快了构建速度。

DLL 插件最初是为了解决 Webpack 打包过程中长时间构建的问题。它的主要思想是将一些不经常改变的第三方依赖(如 React、lodash 等)单独打包成一个独立的动态链接库(DLL),并将其预先编译成静态文件。这样,在后续的构建过程中,可以跳过对这些不变的第三方依赖的重复打包,从而加快构建速度。

hash:一次打包,一个 hash chunkhush: 一个代码块,一个 hash contenthash: 一个文件,一个 hash。比如一个 js 里有 css。我们单独提取 css,而只改变了 js,这时候就用到 contenthash 了