likes
comments
collection
share

一文带你梳理Webpack面试题(2024年版)又是一年金九银十,虽然市场一般,如何才能抓住来之不易的机会呢?自然是砥砺

作者站长头像
站长
· 阅读数 34

又是一年金九银十,虽然市场一般,如何才能抓住来之不易的机会呢?自然是砥砺自身能力,完善知识体系。 挖下深坑擒虎豹,挂上长线钓金鳌。

本文基于笔者的个人面试经历 + 线上他人面经,筛选整理 webpack 常见面试题,并主观标记重点,各位酌情参考;

评论区有抽奖🥇哦


一、为什么要工程化

因为在浏览器端,开发时态、运行时态的侧重点不一样:

开发时态: devtime

  1. 模块划分越细越好
  2. 支持多种模块化标准
  3. 不考虑兼容性,怎么方便怎么写
  4. 支持 npm 或其他包管理器下载的模块

运行时态: runtime

  1. 文件越少越好
  2. 文件体积越小越好
  3. 代码内容越乱越好(安全性)
  4. 所有浏览器都要兼容
  5. 能够解决其他运行时的问题,主要是执行效率问题

这种差异在小项目中表现的并不明显,可是一旦项目形成规模,就越来越明显,如果不解决这些问题,前端项目形成规模只能是空谈。

解决方法

既然两种时态面临的侧重点不同,那么我们需要一个工具,能够将开发时态写的代码转换为运行时态的代码。

此时开发者只用专注开发左边的代码结构就好,一切问题,都由构建工具抹平。

一文带你梳理Webpack面试题(2024年版)又是一年金九银十,虽然市场一般,如何才能抓住来之不易的机会呢?自然是砥砺

常见的构建工具:webpack、vite、grunt、gulp、browserify ...


二、webpack 基础

一文带你梳理Webpack面试题(2024年版)又是一年金九银十,虽然市场一般,如何才能抓住来之不易的机会呢?自然是砥砺

webpack 是基于模块化的打包(构建)工具,它把一切视为模块。

它通过一个开发时态的入口模块为起点,分析出所有的依赖关系,然后经过一系列的过程(压缩、合并),最终生成运行时态的文件。

01|特性

  • 为前端工程化而生:webpack 致力于解决前端工程化,特别是浏览器端工程化中遇到的问题,让开发者集中注意力编写业务代码,而把工程化过程中的问题全部交给 webpack 来处理
  • 简单易用:支持零配置,可以不用写任何一行额外的代码就使用 webpack
  • 强大的生态:非常灵活、可以扩展,webpack 本身的功能并不多,但它提供了一些可以扩展其功能的机制,使得一些第三方库可以融于到 webpack 中
  • 基于nodejs: 由于 webpack 在构建的过程中需要读取文件,因此它是运行在 node 环境中的
  • 基于模块化:webpack 在构建过程中要分析依赖关系,方式是通过模块化导入语句进行分析的,它支持各种模块化标准,包括但不限于 CommonJS、ES6 Module

02|安装与使用

步骤 1:初始化项目

创建项目目录:

mkdir my-webpack-project
cd my-webpack-project

初始化项目: npm init -y

步骤 2:安装 Webpack 和 Webpack CLI

npm install --save-dev webpack webpack-cli

步骤 3:创建 Webpack 配置文件

在项目根目录下创建一个名为 webpack.config.js 的文件,并添加以下基本配置:

const path = require('path');

module.exports = {
  entry: './src/index.js', // 入口文件
  output: {
    filename: 'bundle.js', // 输出文件名
    path: path.resolve(__dirname, 'dist') // 输出路径
  },
  mode: 'development' // 模式,可以是 'development' 或 'production'
};

步骤 4:创建项目结构

创建必要的目录和文件:

mkdir src
touch src/index.js

src/index.js 文件中添加一些示例代码:

console.log('Hello, Webpack!');

步骤 5:更新 package.json 脚本

package.json 文件中添加一个脚本来运行 Webpack:

{
  "name": "my-webpack-project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "^5.0.0",
    "webpack-cli": "^4.0.0"
  }
}

步骤 6:运行 Webpack

使用以下命令运行 Webpack 构建项目:npm run build

运行成功后,你将在项目根目录下看到一个 dist 目录,里面包含了 bundle.js 文件。


三、市面常见面试题

00|减少代码体积方案

减少代码体积是优化前端性能的重要步骤,主要通过以下几种方法来实现:

1、代码压缩

通过删除代码中的空格、注释、换行符以及缩短变量名等方式来减少代码体积。

2、样式和脚本的合并

将多个 CSS 文件和 JavaScript 文件合并成一个文件,以减少 HTTP 请求的数量,从而提高加载速度。

3、移除未使用的代码(Tree Shaking)

Tree Shaking 是一种通过静态分析模块依赖关系,移除未使用代码的技术。常用的工具包括:

  • Webpack:支持 Tree Shaking 的打包工具。
  • Rollup:专注于 ES6 模块的打包工具,支持 Tree Shaking。

4、延迟加载

指在需要时才加载某些资源,以减少初始加载的代码体积,常用的方法包括:

  • 动态导入:在需要时才加载模块。
  • 按需加载:将代码分割成多个小块,按需加载。

5、图片和资源优化

虽然这不直接减少 JavaScript 或 CSS 代码的体积,但优化图片和其他资源可以显著减少整体页面的加载时间。常用的方法包括:

  • 图片压缩:使用工具如 ImageOptimTinyPNG 等。
  • 使用矢量图:如 SVG 格式的图像。
  • 使用 WebP 格式:比传统的 JPEGPNG 更小。

01|为什么要用 Webpack

没用 webpack 之前有什么问题:

  1. 全局变量污染: 传统的 JavaScript 开发中,所有变量和函数默认都是全局的;
  2. 手动管理依赖顺序麻烦:<script />里引入外部 JS 难度随着项目体量越来越难;
  3. 手动进行性能优化繁琐: 如文件合并、压缩、减少 HTTP 请求,繁琐且易出错;
  4. 要确保代码在不同浏览器中的兼容性: 需要手动编写或引入 polyfills、babel 等第三方工具;

对于小型项目来说,这些问题不太明显。但随之项目规模逐渐增大,再去解决这些问题就很吃力,这个时候我们就需要一种工具来帮我们把这些机械性问题,自动化的解决掉,让开发者更加专注在业务层面

02|webpack 有哪些常见配置

有两种方式可以来控制 webpack 的行为。

  • 配置文件(默认) : webpack 会读取 webpack.config.js
  • 命令行:或者通过 cli 参数 --config 来指定其他配置文件;

配置文件通过 CommonJS 导出一个对象module.exports = {},对象中的各种属性对应不同的 webpack 配置。当命令行参数与配置文件出现冲突时,以命令行参数为准。常见配置见下文代码:

  1. 入口(entry): 指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。
  2. 输出(output): 告诉 webpack 在哪输出它所创建的 bundles,以及如何命名这些文件。
  3. 转换器(module): 声明 loader 用法。将某个源码字符串转换成另一个源码字符串返
    • test:正则表达式,用于标识 Loader 转换哪类文件。
    • use:字符串,标识使用哪些 Loader。调用规则从后向前,如下文处理 css 的 loader 会先使用 css-loader 处理,得到的结果再传给 style-loader
  4. 插件(plugins): 扩展了 webpack 的功能,并提供其编译过程中的一些事件钩子,plugin 去监听这些内容然后进行操作。
  5. ...
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  entry: './src/index.js',	// 打包的入口文件

  // 指定打包后文件的输出位置和文件名
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },

  // 指定 Webpack 模式,可以是 development、production 或 none。
  mode: 'development',

  // 配置 Loader,用于处理不同类型的文件
  module: {
    rules: [
      {
        test: /.js$/,
        exclude: /node_modules/,
        use: 'babel-loader',
      },
      {
        test: /.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /.(png|svg|jpg|gif)$/,
        use: ['file-loader'],
      },
    ],
  },

  // 配置插件,用于执行各种任务,如打包优化、资源管理等
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
    // 使用 DefinePlugin 插件定义环境变量
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production'),
    }),
    new WebpackManifestPlugin({
      fileName: 'manifest.json', // 生成的 Manifest 文件名
      publicPath: '/', // 公共路径
    }),
  ],

  // 配置开发服务器,用于本地开发和热更新
  devServer: {
    contentBase: './dist',
    hot: true,
    proxy: {
      '/api': 'http://localhost:3000',
    }, // 配置代理,用于将特定 URL 路径代理到另一个服务器
  },

  // 配置模块解析选项
  resolve: {
    // 自动补全文件扩展名,这样在导入模块时,可以省略这些扩展名
    extensions: ['.js', '.jsx', '.json'],
    // 创建模块别名,以便更方便地导入模块
    alias: {
      '@components': path.resolve(__dirname, 'src/components/'),
      '@utils': path.resolve(__dirname, 'src/utils/'),
    },
  },

  // 配置优化选项,如代码分割和压缩
  optimization: {
    splitChunks: {
      chunks: 'all',
    },
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,
          },
        },
      }),
    ],
  },

  // 配置 SourceMap 选项,用于调试,默认没 SourceMap
  devtool: 'source-map',
};

03|webpack 的构建/打包流程是什么(高频)

一文带你梳理Webpack面试题(2024年版)又是一年金九银十,虽然市场一般,如何才能抓住来之不易的机会呢?自然是砥砺

  1. 初始化参数:从配置文件 和 Shell 语句中读取与合并参数,得出最终的参数;

  2. 开始编译:用上一步得到的参数

    1. 初始化 Compiler 对象
    2. 加载所有配置的 Plugin 插件
    3. 执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置项的 entry,找出所有的入口文件;

  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,递归此步骤直到所有依赖的文件也都处理过;

  5. 输出资源:根据入口和模块之间的依赖关系,组装成一个包含多个模块的 Chunk。再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;

  6. 输出完成:在确定好输出内容后,根据output确定输出的路径和文件名,输出文件夹。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

总的来说,Webpack 的模块打包原理就是通过递归解析模块之间的依赖关系,将所有的模块打包成为一个或多个文件,并通过一系列的插件和 loader 对代码进行处理和优化。

这样可以减少 HTTP 请求次数、提高页面加载速度,并大大提高了前端开发的效率和可维护性。

04|webpack 如何确定依赖引用顺序

依赖图的构建过程

  1. 入口点:Webpack 从配置的入口点entry开始,从入口文件开始解析。
  2. 递归解析:递归解析每个模块的依赖,找到所有被引用的模块。
  3. 构建依赖图:根据模块之间的依赖关系构建一个依赖图。
  4. 确定顺序:根据依赖图确定模块的引用顺序,确保被依赖的模块先于依赖它们的模块打包。

05|Module/Chunk/Bundle 是什么

  • Module:webpack 里一个概念性内容,每个文件都可以看为一个 module。 js、css、图片等都可以看作 module。
  • Chunk:代码块,webpack 处理代码时候的一个中间态,它表示有一组功能相关的模块的集合。一个 Chunk 可以由多个模块(module)组成
  • Bundle:是 Webpack 构建结果的输出,由一个或多个 Chunk 的合并优化后的结果,最终以文件形式输出,用于在浏览器中加载和执行。

06|Loader 和 Plugin有什么区别(高频)

Loader 转换器,用于转换模块的源代码。可以将不同类型的文件(如 CSS、图像、TypeScript 等)转换为 JavaScript 模块,从而使它们能够被 Webpack 处理。以下是 CSS 转换的例子:

// css-loader 转换后的 JavaScript 模块
module.exports = {
  // CSS 内容被转换为字符串
  css: "body { background-color: lightblue; } h1 { color: navy; }"
};

// style-loader 会将这些样式注入到 DOM 中
const style = document.createElement('style');
style.textContent = module.exports.css;
document.head.appendChild(style);

Plugin 插件是 Webpack 的扩展,执行范围更广,可以在构建过程的各个阶段进行操作和自定义功能。Webpack 会提供一些 API 和 生命周期钩子方便开发者触达到除了编译之外的一些环节来执行操作。

使用场景

  • 压缩输出的 JavaScript 文件(如使用 TerserPlugin)。
  • 提取 CSS 到单独的文件(如使用 MiniCssExtractPlugin)。
  • 生成 HTML 文件并自动注入打包后的资源(如使用 HtmlWebpackPlugin)。
  • 清理输出目录(如使用 CleanWebpackPlugin)。

07|写 Loader 的步骤和思路

一文带你梳理Webpack面试题(2024年版)又是一年金九银十,虽然市场一般,如何才能抓住来之不易的机会呢?自然是砥砺

Loader 本质上是一个函数,作用是将某个源码字符串转换成另一个源码字符串返回。接收源文件代码字符串为参数,经过处理转换,然后 return 目标代码字符串,构建步骤如下:

module.exports = function(source) {
  // 对源代码进行处理
  const result = source.replace(/\b(foo)\b/g, 'bar');
  // 返回更新后的代码
  return result;
};
  1. 新建一个 JS 文件
  2. 写 Loader 函数:
    1. 接收 source 参数
    2. 内容转换
    3. 结果返回
  3. 导出这个函数
  4. webpack.config.js 文件中配置使用
  5. 如果想发布 NPM 就走发布流程,然后写份清晰的文档

注意:如果 Loader 有异步操作需要通过 this.async() 处理,不然可能会出现 Loader 函数在异步操作完成前返回,导致转换结果不正确。

this.async()方法返回一个回调函数,你将通过这个回调函数来返回处理结果或错误。回调接收三个参数

  • 错误:Loader 执行过程中出错,则返回给这个参数。没错就传 null/undefined
  • 结果:Loader 执行成功后的结果
  • SourceMap(可选):如果转换过程中能产生 SourceMap 可以通过这个传参帮助定位错误位置
module.exports = function(source) {
  const callback = this.async();

  someAsyncOperation(source, (err, transformedSource, sourceMap) => {
    if (err) {
      // 如果有错误发生,传递错误对象
      callback(err);
      return;
    }
    // 成功处理,传递处理后的结果和source map(如果有的话)
    callback(null, transformedSource, sourceMap);
  });
};

08|写 Plugin 的步骤和思路

在 Webpack 中,Plugin 是一个具有 apply 方法的对象。

apply 方法会被 Compiler 对象调用,并且在整个编译生命周期可以访问 Compiler 对象。所以步骤如下:

  1. 编写插件类:创建一个类,实现 apply 方法。Webpack 在启动编译过程时,会调用每个插件实例的 apply 方法
  2. 注册钩子回调:在 apply 方法中,使用编译器 Compiler 对象注册你需要的钩子回调。
  3. 实现功能逻辑:在回调函数中实现具体的插件逻辑。
// 通过 tap 方法注册钩子,第一个参数是插件名称,第二个参数是回调函数
module.exports = class MyPlugin {
    apply(compiler) {
        // 注册事件,类似于window.onload = function() {}
        compiler.hooks.done.tap('MyPlugin', (Compilation) => {
            console.log('MyPlugin: Compilation finished!');
        });
    }
}

在这个例子中,MyPlugin 类定义了一个 apply 方法,这个方法接收一个 compiler 参数。

我们在 compiler.hooks.done 上注册了一个回调,这个回调会在编译完成后执行,输出一条消息。

要将插件应用到 webpack,需要把插件对象配置到 webpack 的 plugins 数组中,如下:

const MyPlugin = require('./MyPlugin');

module.exports = {
    plugins: [
        new MyPlugin(),
    ]
}

09|用过哪些常见 Loader

在Webpack中,loader用于将各种类型的文件转换为模块。以下是一些常见的loader及其用途:

  1. babel-loader
  • 用途:将 ES6 及以上版本的 JavaScript 代码转换为向后兼容的 JavaScript 代码。
  1. css-loader/style-loader 这俩一般一起用
  • css-loader 解析 CSS 文件中的@importurl(),将 CSS 转为 Webpack 可管理的模块。
  • style-loader 将处理后的 CSS 注入页面,通过在<head>中添加<style>标签应用样式。
module: {
  rules: [
    {
      test: /.css$/,
      use: [
        'style-loader', // Step 2: 将CSS注入到DOM中
        'css-loader' // Step 1: 处理CSS文件
      ]
    }
  ]
}
  1. sass-loader/less-loader
  • 用途:将 Sass/SCSS/Less 文件编译为CSS。
{
  test: /.s[ac]ss$/,
  use: ['style-loader', 'css-loader', 'sass-loader']
}
  1. file-loader
  • 用途:处理文件导入,将文件输出到输出目录,并返回 URL。
  1. url-loader
  • 用途:用于将文件(如图片、字体等)转换为 base64 编码的 Data URL
  • 当文件较小时,这可以减少 HTTP 请求的数量,从而提高性能。
  • 文件较大时,url-loader 会自动回退到 file-loader,将文件复制到输出目录并返回文件的 URL。
  1. ts-loader
  • 用途:将TypeScript代码编译为JavaScript代码。

Base64 编码的 Data URL 是一种将文件数据直接嵌入到网页中的方式,而不需要单独的文件请求。

它将文件内容转换为 Base64 编码的字符串,并将其作为 URL 的一部分。这种方法可以减少 HTTP 请求的数量,特别适用于小文件(如小图片、图标等)。

一、Babel 原理

Babel 是 JS 的转换器,主要用来将新 JS 语法转为向后兼容版本

原理很简单,就三部分:解析/转换/生成

  • 解析:通过词法分析把代码变 token、语法分析把 token 解析成抽象语法树(AST)
  • 转换:接收到 AST 并遍历,对树上节点增删改实现兼容性转换
  • 生成:被转换的新 AST 被生成为新 JS 代码字符串

二、less-loader 的底层原理

这个问题别想太多,直接秒

就是接收 less 代码为入参,内部转换为 css 代码,然后输出

一般后面会跟着执行 style-loader/css-loader 继续处理转换后的 css 代码。

10|用过哪些常见 Plugin

  1. HtmlWebpackPlugin:自动生成 HTML 文件,并自动引入打包后的 JS 文件。
  2. MiniCssExtractPlugin:将 CSS 提取为独立的文件,支持按需加载和缓存。
  3. HotModuleReplacementPlugin:模块热替换(HMR),实现页面实时预览更新。
  4. TerserWebpackPlugin:压缩 JavaScript,Webpack 4+ 默认内置。
  5. OptimizeCSSAssetsPlugin:优化和压缩 CSS 资产。
  6. BundleAnalyzerPlugin:可视化 Webpack 输出文件的大小,帮助分析和优化。

11|SourceMap 原理(高频)

配置 devtool: 'source-map'后,

在编译过程中,会生成一个 .map 文件,一般用于代码调试和错误监控。

  • 包含了源代码、编译后的代码、以及它们之间的映射关系。
  • 编译后的文件通常会在文件末尾添加一个注释,指向 SourceMap文件的位置。
    • // # sourceMappingURL=example.js.map
  • 当在浏览器开发者工具调试时,浏览器会读取这行注释并加载对应的 SourceMap 文件

报错时,点击跳转。即使运行的是编译后的代码,也能够追溯到原始源代码的具体位置,而不是处理经过转换或压缩后的代码,从而提高了调试效率。

跟 Mainfest 的区别

  • SourceMap 主要用于调试目的,让开发者能够在压缩或转译后的代码中追踪到原始代码
  • Manifest 文件用于资源管理,用于优化资源的加载和缓存

12|Mainfest 文件是什么,有什么用

Mainfest(更新清单),通常是一个 JSON 文件。需要配置 WebpackManifestPlugin 插件

在 Webpack 输出阶段生成,用于记录所有模块及其依赖关系的映射用来管理模块加载、优化浏览器缓存。 包含:

  • 模块标识符: 每个模块都有一个唯一标识符,这些标识符用于在运行时查找和加载模块。
  • Chunk 映射关系:包含 chunk 与包含的模块之间的映射关系,以及 chunk 之间的依赖关系。这有助于运行时确定哪些 chunk 需要被加载。
  • Hash 值: 每个输出文件的 hash 值。有助于浏览器判断文件是否有更新,从而决定是加载缓存中的资源还是重新请求新的资源。
{
  "main.js": "main.1a2b3c4d5e6f7g8h9i0j.js",
  "vendor.js": "vendor.1a2b3c4d5e6f7g8h9i0j.js"
}

生成的 Manifest 文件可以用于以下场景:

  • 服务端渲染: 在服务端渲染时,可以使用 Manifest 文件来生成正确的脚本标签,确保引用最新的资源。
  • 缓存管理: 通过记录文件的哈希值,确保在文件内容变化时,客户端能够获取到最新的文件,而不是使用缓存的旧文件。
  • 动态加载: 在需要按需加载模块时,可以使用 Manifest 文件来查找模块的路径。

13|Webpack 热替换 HMR 原理(高频)

没热替换之前,每次改代码之后要重新刷新页面才能看到新的页面。

热替换可以让我们不用刷新浏览器,通过增删改模块来实时更新页面。HMR 的实现依赖于 Webpack Dev Server 启动一个 WebSocket 服务器,跟浏览器进行全双工通信。

  1. 服务器和客户端的通信:Webpack Dev Server 在服务端启动一个 WebSocket 服务器,浏览器通过 WebSocket 连接此服务器。
  2. 监听文件变化: Webpack 使用 watch 模式监听项目中的文件变化。一旦文件发生变化,Webpack 就会重新编译改变的模块,并生成更新后的模块代码及一个更新清单( Mainfest
  3. 通知客户端:更新清单会通过已建立的 WebSocket 连接发送给客户端(浏览器) 。
  4. 热替换:浏览器接收到更新信息后,通过 HMR API 获取更新的模块,并替换旧的模块。
    • 如果模块热替换失败,会触发整页刷新。

Webpack 的 HMR 功能极大地提高了开发效率,使得开发者可以即时看到代码变更的效果,而无需进行完整的页面刷新。这不仅加快了开发流程,也使得调试更加方便。

用法

  1. 通过配置项devServer.hot: true,启用 HMR 功能。
  2. 或者使用HotModuleReplacementPlugin HMR 插件。

14| Webpack 性能优化方法(高频)

性能优化方案总的来说,无非就是追求几点:减少打包体积、多线程并行打包、利用缓存提效

一、减小打包体积

1、压缩资源

  • JavaScript:使用 TerserPlugin 等工具压缩 JS 代码。
  • CSS:使用 cssnano 等工具压缩 CSS 代码。
  • HTML:使用 html-webpack-plugin 时配置压缩选项。
  • 图片:使用 image-webpack-loader 等工具减小图片体积。

2、引入外部库的 CDN

  • 对于 React、Vue、Lodash 这种库不会经常变化,所以就没必要打包。这种方式可以减少打包体积,并利用 CDN 的缓存优势加快页面加载速度。
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  // 配置 externals,说明哪些模块是外部引入的,不打包到 bundle 中
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
  },
};
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
    <!-- 引入 React 和 ReactDOM 的 CDN -->
    <script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
    <!-- Webpack 会自动注入 bundle.js -->
  </body>
</html>

3、 代码分割

  • SplitChunks 自动提取公共模块和第三方库,可以减少代码重复和减少编译时间。
  • 提取公共模块:将多个 chunk 共享的模块提取到一个单独 chunk 中,减少代码重复和生成文件的大小。
  • 分割大模块:将大的模块拆分成更小的块,提高加载速度和并行下载的效率。
  • 按需加载:创建按需加载的代码块,提高应用的启动速度。

二、多进程打包

使用 thread-loaderparallel-webpack 可以将打包任务分配到多个进程,提高打包速度。

三、利用缓存提效

  1. 使用 babel-loader 的 cacheDirectory 选项开启缓存,减少重复编译时间。
{
  loader: 'babel-loader',
  options: {
    cacheDirectory: true
  }
}
  1. 开启持久化缓存:提高二次构建速度。

Webpack 5 引入了持久化缓存,通过配置cache.type属性缓存生成的 chunk

cache: {
  type: 'filesystem', // 使用文件系统级别的缓存
}

15|Tree Shaking 是什么,原理是什么,如何实现

Tree Shaking 是一种用于移除 JavaScript 中未使用代码的优化技术。可以减小打包文件的体积,提高加载性能。

它依赖于 ES6 模块的静态结构特性(importexport),使得构建工具能够在编译时确定哪些代码是未使用的,并将其移除。

工作原理

  • 静态分析:编译时可以确定模块之间的依赖关系,哪些被使用了。
  • 标记未使用:通过分析,标记所有未被引用的代码。
  • 移除未使用:在最终生成的代码中移除那些未被标记为使用的代码。

实现步骤

  1. 使用 ES6 模块语法 (importexport)。
  2. 在 Webpack 配置中启用生产模式和 usedExports 选项。
// webpack.config.js
const path = require('path');

module.exports = {
  mode: 'production', // Tree Shaking 仅在生产模式下启用
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  optimization: {
    usedExports: true, // 启用 Tree Shaking
  },
};
  1. 确保模块是纯函数,没有副作用。
// 副作用:在模块加载时发起网络请求
export const data = fetch('https://api.example.com/data'); // module.js

// 即使 data 在 main.js 中未被使用,构建工具也不能安全地移除它
// 因为移除它会导致网络请求不再发生,从而改变程序的行为。
import { data } from './module'; // main.js


// fetchData 是一个纯函数
// 如果未被使用,构建工具可以安全地移除它,而不会影响程序的其他部分。
export function fetchData() {
  return fetch('https://api.example.com/data');
}
import { fetchData } from './module';

16|如何在 WebPack 中代码分割/提取一个公共模块

这里方案我们使用 SplitChunksPlugin,这是 Webpack 的内置插件,用于将公共的依赖模块提取到单独的 chunk 中,减少代码重复、提高加载速度

在 webpack.config.js 文件中,你可以在配置 optimization.splitChunks 选项来指定如何提取公共模块

基本配置

module.exports = {
  // 其他配置...
  optimization: {
    splitChunks: {
      chunks: 'all', // 对所有模块进行优化
    }
  }
};

高级配置: 通过cacheGroups自定义分割策略

module.exports = {
  // 其他配置...
  optimization: {
    splitChunks: {
      chunks: 'all', // 对所有模块进行优化
      minSize: 20000, // 生成chunk的最小大小(以字节为单位)
      minChunks: 1, // 分割前必须共享模块的最小块数
      maxAsyncRequests: 30, // 按需加载时的最大并行请求数
      maxInitialRequests: 30, // 入口点的最大并行请求数
      automaticNameDelimiter: '~', // 默认情况下,webpack将使用块的来源和名称生成名称(例如vendors~main.js)
      
      cacheGroups: { // 缓存组可以继承或覆盖splitChunks.*的任何选项
        vendors: {
          test: /[\/]node_modules[\/]/, // 控制哪些模块被这个缓存组选中
          priority: -10 // 一个模块可以属于多个缓存组。优化将优先考虑具有更高优先级的缓存组
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true // 如果当前块包含已经从主束分离出的模块,则将重用它而不是生成新的块
        }
      }
    }
  }
};
  • 提取第三方库为chunk:通过 vendors 缓存组

    • 可以将 node_modules 的模块提取到单独的文件中,这对于提取大型的第三方库(如React, Vue等)特别有用。
  • 提取公共模块为chunk:通过 default 缓存组

    • Webpack 会自动提取,被多个入口共享的模块到一个或多个公共块中。

17|webpack5 的新特性

  1. 持久化缓存:通过将编译结果缓存到磁盘上,可以显著提高构建速度.
    1. 配置中设置 cache.type:'filesystem' ,可以启用持久化缓存
    2. webpack4 每次编译都需要重新执行构建流程,即使文件没变化也重新构建,所以导致速度慢。
    3. 社区提供 HardSourceWebpackPlugin 实现持久化缓存,5 代是对这功能进行官方支持与优化。
cache: {
  type: 'filesystem', // 使用文件系统级别的缓存
}
  1. 长缓存优化: 通过文件名哈希和缓存控制头来缓存静态资源,减少服务器负载和加快页面加载速度。
  2. Tree Shaking 优化:提高了对未使用模块的检测能力,从而在打包时排除更多未使用的代码。
  3. 输出文件名优化ContentHash,基于内容粒度变化来判断是否更新文件名
  4. 资产模块:是一种新的模块类型,处理字体、图标、图片等资源。
    • Webpack 4 中需通过 file-loaderurl-loader 处理的文件,5 代不用额外的 Loader 就能用
  5. 模块联邦:通过插件实现不同前端应用见资源共享与集成,跟微前端相关,细节没用过。

18|长缓存、持久化缓存

是 webpack5 的新特性,通过确保浏览器只在文件内容变化时才下载新版本文件,来减少不必要的网络请求。减少资源的重复加载,从而提高页面加载速度和用户体验

关键点

  1. 文件名哈希, 每次文件内容变化时,文件名也会变化
module.exports = {
  output: {
    filename: '[name].[contenthash].js',
    path: __dirname + '/dist'
  },
};
  1. 合理配置缓存头: 设置 HTTP 缓存头(如Cache-ControlExpires),控制资源的缓存时间和策略。
  2. 分离动态和静态资源: 将动态资源和静态资源分开管理,动态资源设置较短的缓存时间,静态资源设置较长的缓存时间。

PS:长缓存主要用于生产环境中的资源缓存,持久化缓存主要用于开发环境中的构建优化。

长缓存:通过文件名哈希和缓存控制头来缓存静态资源,减少服务器负载和加快页面加载速度。

持久化缓存:通过将构建过程中生成的缓存数据存储在磁盘上,加快开发过程中的构建速度。

19|Chunkhash和Contenthash区别

在问文件指纹相关内容,哈希值好理解,就是文件变了,hash值就会改变,

区别是二者计算粒度不同,前面的 chunk/content 就是粒度级别

  • chunkhash 适用于识别 整个 chunk 的变化,不同的 entey 会生成不同的 chunkhash 值
  • contenthash 适用于识别 文件内容的变化,文件内容不变,则 contenthash 不变。

20|多页面打包是什么,如何实现

SPA打包:只有一个 HTML 页面和一个 JS 入口文件

MPA打包:是指在一个项目中,通过配置,构建多个独立的 HTML 页面,每个页面有自己的 JS 入口和依赖。更适合页面间相互独立。实现步骤如下:

  1. 定义入口配置:为每个页面配置一个入口文件,例如 page1 和 page2;
  2. 定义出口配置:使用 [name].bundle.js 模板字符串,为每个入口文件生成独立的输出文件。
  3. HTML插件配置HtmlWebpackPlugin 插件能为每个页面生成一个 HTML 文件,并将构建后的资源自动注入到这个 HTML 文件中。
  4. 优化配置(可选) :根据需要配置代码分割、压缩等优化功能。
  5. 输出结果:dist 目录中将包含 page1.html、page2.html 以及对应的 page1.bundle.js 和 page2.bundle.js 文件。
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'production', // 或 'development'
  entry: {
    page1: './src/page1/index.js',
    page2: './src/page2/index.js',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  // 为每个页面生成独立的 HTML 文件
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'page1.html',
      template: './src/page1/index.html',
      chunks: ['page1'],
    }),
    new HtmlWebpackPlugin({
      filename: 'page2.html',
      template: './src/page2/index.html',
      chunks: ['page2'],
    }),
  ],
};

21|Webpack 怎么做错误上报

细节不清楚,思路主要分两步:错误捕获、错误上报

  • 捕获:本质还是在 webpack 生命周期的钩子函数中获取到 webpack 打包构建过程中的问题
    • 除了自己写, 也可以使用:webpack-fail-plugin 插件,在使构建失败时抛出错误
  • 上报:单纯的数据发送,但需要注意不要用户敏感信息

22|Webpack 代理怎么做

代理通常是通过 devServer.proxy 来实现的,这是在开发环境下常用的一种方式

用于解决开发中的跨域请求问题,模拟生产环境中的 API 请求,并简化前端代码的配置

  1. 安装 webpack-dev-servernpm install webpack-dev-server --save-dev
  2. 配置 devServer.proxy
module.exports = {
  // 其他配置...
  devServer: {
    proxy: {
      // 需要代理的请求路径前缀。这里是'/api'
      '/api': {
          target: 'http://example.com', // 目标服务器地址
          pathRewrite: {'^/api' : ''}, // 重写路径:去掉路径中开头的'/api'
          changeOrigin: true, // 是否更改请求的源
          secure: false, // 如果是 https 接口,需要配置为false
      },
    },
  },
};

23|按需加载如何实现,原理是什么(高频)

按需加载是基于动态导入和代码分割实现的,允许应用将代码分割成多个 chunk,并在运行时按需动态加载这些chunk。按需加载可以减少应用的初始加载时间,提升用户体验。具体实现方式如下:

  1. 使用import() 动态导入模块
    • import 将模块内容转换为 ESM 标准的数据结构后,通过 Promise 形式返回,加载完成后获取 Module 并在 then 中注册回调函数。
  2. Webpack 自动代码分割
    • 当 webpack 检测到 import()存在时,将会自动进行代码分割,将动态import的模块打到一个新 bundle 中
    • 此时这部分代码不包含在初始包中,而是在需要的时候动态加载。
  3. 网络请求
    • import()被执行时,浏览器会发起一个网络请求来加载对应的 chunk 文件。
    • 加载完成后,模块中的代码就可以被执行了。

24|文件监听是什么,怎么用,原理是什么

文件监听是在源代码发生变化时,自动重新编译代码的功能。

一、如何使用

  1. 命令行启动:webpack --watch
  2. 或者,配置文件设置
module.exports = {
  watch: true,
};

二、配置优化

功能很有用,但是有些优化手段也应该了解

  1. 排除不需要监听的文件:watchOptions.ignored
  2. 设置轮训间隔:watchOptions.poll
module.exports = {
  watch: true,
  watchOptions: {
    ignored: /node_modules/,
    poll: 1000, // 每 1 秒检查一次变化
  },
};

三、原理

基于 文件系统事件轮询 实现的,具体方式取决于操作系统和配置

  • 文件系统事件:在支持文件系统事件的操作系统上(Linux、macOS,Windows),Webpack 会注册这些事件来直接获取文件变化通知。
  • 轮询:在不支持文件系统事件或文件系统事件不可靠的环境中,Webpack 可能会退回到轮询模式。在轮询模式下,Webpack 定期检查文件的最后修改时间来判断文件是否发生变化

四、跟热更新的区别

  • 文件监听:监视文件变化,自动重新编译代码,会重新加载整个页面,导致应用状态丢失。实现简单。
  • 热更新(HMR):在应用程序运行时替换、添加或删除模块,无需重新加载整个页面,保留应用状态。实现相对复杂,但显著提高开发效率。

25|webpack 能动态加载 require 引入的模块吗?

可以,虽然动态加载模块的主要方式是使用 import() 语法,Webpack 会将这种动态导入转换为代码分割,从而实现按需加载模块。但require引入的模块也能动态加载

动态加载单个模块

require.ensure(dependencies, callback, chunkName);

  • 适用于 Webpack 2 及更高版本。
  • dependencies:包含所有需要加载的模块的数组。通常可以传递一个空数组 []
  • callback:在所有依赖模块加载完成后执行的函数。require 动态加载在这实现
  • chunkName(可选):一个字符串,用于指定生成的代码块的名称。这有助于调试和缓存。

动态加载一组模块

const context = require.context(directory, useSubdirectories, regExp);

  • 适用于需要在运行时动态引入多个模块的场景。
  • directory:要搜索的目录路径。
  • useSubdirectories:一个布尔值,表示是否搜索子目录。
  • regExp:一个正则表达式,用于匹配文件名。
// require.ensure()
function loadModuleA() {
  require.ensure([], function(require) {
    const moduleA = require('./moduleA');
    moduleA.greet();
  }, 'moduleA');
}

const context = require.context('./modules', false, /.js$/);
function loadModule(moduleName) {
  const module = context(`./${moduleName}.js`);
  module.greet();
}

// 在某个条件下调用 loadModuleA or loadModule
if (someCondition) {
  loadModuleA();
  loadModule('moduleA');
}

26|为什么 Vite 速度比 Webpack 快?

一、开发模式的差异

  • 当使用 Webpack 时,所有的模块都需要在开发前进行打包 会增加启动时间和构建时间。
  • Vite 则是直接启动,它会在请求模块时再进行实时编译,这种按需动态编译的模式极大地缩短了编译时间,特别是在大型项目中,文件数量众多,Vite 的优势更为明显。

二、底层语言的差异

  • Webpack 是基于 Node.js 构建的,毫秒级别的
  • Vite 则是基于 esbuild 进行预构建依赖。esbuild 是采用 Go 语言编写的,纳秒级别的

因此,Vite 在打包速度上相比Webpack 有 10-100 倍的提升。

三、热更新的处理

  • Webpack 中,当一个模块或其依赖的模块内容改变时,需要重新编译这些模块。
  • Vite 中,当某个模块内容改变时,只需要让浏览器重新请求该模块即可,这大大减少了热更新的时间。

四、参考文档

转载自:https://juejin.cn/post/7414371017253568539
评论
请登录