2025前端面试 —— 工程化篇

184 阅读22分钟

一、package.json 中 devDependencies 和 dependencies 的区别

1. dependencies (生产依赖)

  • 作用:项目运行时(生产环境)必须的依赖包。

  • 安装方式
    通过 npm install <package-name> 或 yarn add <package-name> 默认安装到 dependencies

  • 典型场景

    • 框架或库的核心依赖(如 reactexpresslodash)。
    • 项目实际运行时会调用的模块(如 axiosmoment)。
  • 示例

    "dependencies": {
      "react": "^18.2.0",
      "express": "^4.17.1"
    }
    

2. devDependencies (开发依赖)

  • 作用:仅在开发阶段需要的工具(测试、构建、格式化等),不会打包到生产环境。

  • 安装方式
    通过 npm install <package-name> --save-dev 或 yarn add <package-name> -D

  • 典型场景

    • 构建工具(如 webpackvitebabel)。
    • 测试库(如 jestmocha)。
    • 代码规范工具(如 eslintprettier)。
  • 示例

    "devDependencies": {
      "eslint": "^8.0.0",
      "webpack": "^5.75.0"
    }
    

关键区别

特性dependenciesdevDependencies
环境生产环境开发环境
打包部署时包含不包含(除非显式安装)
安装命令npm install <包名>npm install <包名> --save-dev

二、 Webpack5 升级点

性能优化

  • 持久化缓存:支持将编译过程中的中间结果存储在磁盘上,当再次构建时,如果文件没有发生变化,就可以直接使用缓存结果,从而显著提高构建速度。可以通过配置 cache 选项来启用持久化缓存,示例如下:
module.exports = {
    // ...其他配置
    cache: {
        type: 'filesystem', // 使用文件系统缓存
    },
};
  • 更好的 Tree Shaking:优化了 Tree Shaking ,能够更准确地识别和移除未使用的代码。它支持 ES 模块的静态分析,即使在复杂的模块结构中,也能更好地进行代码优化。

  • 模块联邦:允许在不同的构建之间共享代码,实现模块的动态加载。通过模块联邦,可以将一个大型项目拆分成多个独立的构建,每个构建可以作为一个独立的微前端应用,同时又能共享代码和资源。

功能增强

  • 内置的资源模块:不再需要使用 file-loaderurl-loader 等加载器来处理图片、字体等资源。可以通过 asset/resourceasset/inline 等类型来处理不同类型的资源,示例如下:
module.exports = {
    // ...其他配置
    module: {
        rules: [
            {
                test: /.(png|jpg|gif)$/i,
                type: 'asset/resource',
            },
        ],
    },
};
  • 自动拆分共享模块:支持自动识别和拆分共享模块,将它们提取到单独的文件中,避免代码重复。可以通过配置 optimization.splitChunks 选项来实现更精细的代码分割。

  • 更好的长期缓存:改进了文件哈希的生成方式,通过使用 [contenthash] 等占位符,可以确保只有当文件内容发生变化时,哈希值才会改变,从而实现更好的长期缓存。

兼容性改进

  • 移除旧的 API 和特性:移除了一些旧的、不再推荐使用的 API 和特性,同时对一些 API 进行了更新和改进,以提高代码的可维护性和兼容性。

  • 支持最新的 JavaScript 特性:支持更好地处理 ES6+ 模块和动态导入等特性。

三、 Vite 原理

Vite 是一种现代化的前端构建工具,其核心原理基于 原生 ES Modules(ESM) 和 按需编译,显著提升了开发环境下的启动速度和热更新效率。以下是 Vite 的核心原理和关键设计:

1. 基于原生 ESM 的开发服务器

传统打包工具的问题(如 Webpack)

  • 启动慢:需要先打包所有文件,生成 Bundle 后才能启动开发服务器。
  • HMR 慢:修改文件后需重新构建整个依赖图。

Vite 的解决方案

  • 直接利用浏览器原生 ESM

    • 开发阶段不打包,让浏览器直接加载 ES Modules。
    • 通过 <script type="module"> 引入入口文件(如 main.js)。
  • 按需编译

    • 浏览器请求文件时,Vite 实时编译当前文件(如 .vue.ts),并返回编译后的 ESM 代码。
    • 依赖的第三方库(node_modules)通过 预构建 优化为 ESM 格式。
示例流程
  1. 浏览器请求:http://localhost:3000/src/main.js
  2. Vite 拦截请求,实时编译 main.js 并返回。
  3. 浏览器解析 main.js 中的 import 语句,继续发起对依赖文件的请求(如 import React from 'react')。
  4. Vite 对每个请求按需处理,动态返回编译后的代码。

2. 依赖预构建(Dependency Pre-Bundling)

为什么需要预构建?

  • 第三方库可能非 ESM 格式:如 CommonJS 的包(React、Lodash)需转换为 ESM。
  • 减少请求数量:将多个小文件合并为单个文件(如 lodash 包含数百个文件)。

实现方式

  1. 首次启动时,Vite 使用 esbuild(Go 语言编写,速度极快)将 node_modules 中的依赖预构建为 ESM 格式。
  2. 预构建结果缓存到 node_modules/.vite 目录,后续启动直接复用。
配置示例
// vite.config.js
export default {
  optimizeDeps: {
    include: ['react', 'lodash'], // 强制预构建的依赖
  },
};

3. 快速热更新(HMR)

传统 HMR 的问题

  • 需要重新构建整个 Bundle,项目越大更新越慢。

Vite 的 HMR 优化

  • 基于 ESM 的精准更新

    • 修改文件后,仅重新编译该文件及其直接依赖。
    • 通过 WebSocket 通知浏览器重新加载对应模块。
  • 框架无关的 HMR API:支持 Vue、React 等框架的组件级热更新。

HMR 流程
  1. 修改 Foo.vue 文件。
  2. Vite 检测到变化,编译 Foo.vue
  3. 通过 WebSocket 向浏览器发送更新消息。
  4. 浏览器替换 Foo.vue 模块,保持应用状态。

4. 生产环境构建

开发环境基于 ESM,但生产环境仍需打包(为了兼容性和性能优化):

  • 使用 Rollup:Vite 在生产模式下调用 Rollup 进行 Tree Shaking、代码分割等优化。
  • 配置一致性:开发和生产共享同一套配置(如 vite.config.js)。
生产构建命令
vite build

5. 核心优势

特性Vite传统打包工具(Webpack)
启动速度毫秒级(无需打包)秒级(依赖项目规模)
HMR 速度即时更新(单个文件)较慢(需重建依赖图)
构建工具开发用 esbuild,生产用 Rollup全程使用 Webpack/Babel
语法支持默认支持 TS、JSX、CSS Modules需额外配置 Loader

四、前端性能优化指标有哪些,如何进行性能检测

前端性能优化指标

1. 首次内容绘制(First Contentful Paint,FCP)

  • 定义:从页面开始加载到页面的任意部分在屏幕上完成渲染的时间,这里的 “部分” 可以是文本、图片、canvas 等元素。

  • 意义:FCP 反映了页面的初始加载速度,是用户对页面加载速度的第一印象,数值越小,用户能越快看到页面内容。

2. 最大内容绘制(Largest Contentful Paint,LCP)

  • 定义:从页面开始加载到最大的可见内容元素在屏幕上完成渲染的时间,最大的可见内容元素通常是图片、视频、大块文本等。

  • 意义:LCP 更关注页面中主要内容的加载情况,能更准确地反映用户看到页面主要内容的时间。

3. 首次输入延迟(First Input Delay,FID)

  • 定义:从用户首次与页面进行交互(如点击按钮、输入文本等)到浏览器实际能够响应该交互的时间。

  • 意义:FID 衡量了页面的响应能力,数值越小,用户在与页面交互时的等待时间越短,交互体验越好。

4. 累积布局偏移(Cumulative Layout Shift,CLS)

  • 定义:测量页面在加载过程中,可视元素的位置发生意外变化的累积分数。当页面元素突然移动或重新布局时,就会产生布局偏移。

  • 意义:CLS 反映了页面的稳定性,数值越小,页面在加载过程中越稳定,用户体验越好。

5. 完全加载时间(Time to Interactive,TTI)

  • 定义:从页面开始加载到页面完全可交互的时间,即页面所有资源加载完成,并且能够快速响应用户交互的时间。

  • 意义:TTI 是衡量页面可用性的重要指标,用户在 TTI 之后才能流畅地与页面进行交互。

性能检测方法

1. Chrome DevTools

  • 操作步骤:打开 Chrome 浏览器,访问目标页面,右键点击页面并选择 “检查”(或使用快捷键 Ctrl + Shift + I 或 Cmd + Opt + I)打开开发者工具。切换到 “Performance” 面板,点击 “Record” 按钮开始录制,然后在页面上进行一些操作模拟用户行为,最后点击 “Stop” 按钮停止录制。

  • 功能特点:Chrome DevTools 会生成详细的性能报告,包含上述各项性能指标的数值,以及页面加载过程中各个阶段的时间分布、资源加载情况等信息。通过分析这些信息,可以找出性能瓶颈并进行优化。

2. Lighthouse

  • 操作步骤:同样在 Chrome 开发者工具中,切换到 “Lighthouse” 面板,选择要检测的类别(如性能、可访问性、最佳实践等),然后点击 “Generate report” 按钮,Lighthouse 会对页面进行全面的性能检测,并生成详细的报告。

  • 功能特点:Lighthouse 不仅会给出各项性能指标的得分和数值,还会提供具体的优化建议,帮助开发者快速定位和解决性能问题。

五、Webpack 优化前端性能的方法

构建速度优化

  • 开启持久化缓存:Webpack 5 引入了持久化缓存机制,能将编译过程中的中间结果存储在磁盘上。再次构建时,若文件未变化,可直接使用缓存结果,提高构建速度。配置示例如下:
module.exports = {
    // ...其他配置
    cache: {
        type: 'filesystem', // 使用文件系统缓存
        buildDependencies: {
            config: [__filename] // 当配置文件变化时,缓存失效
        }
    }
};
  • 多线程打包:使用 thread-loader 可以将耗时的 loader 放到单独的 worker 池里运行,利用多核 CPU 加速打包。例如,在处理 Babel 转换时:
module.exports = {
    module: {
        rules: [
            {
                test: /.js$/,
                use: [
                    'thread-loader',
                    'babel-loader'
                ]
            }
        ]
    }
};
  • 排除不必要的文件:使用 exclude 和 include 选项来指定 loader 应用的文件范围,避免对不必要的文件进行处理,减少构建时间。示例如下:
module.exports = {
    module: {
        rules: [
            {
                test: /.js$/,
                include: path.resolve(__dirname, 'src'), // 只处理 src 目录下的文件
                exclude: /node_modules/, // 排除 node_modules 目录
                use: 'babel-loader'
            }
        ]
    }
};

打包体积优化

  • Tree Shaking:Tree Shaking 可以去除代码中未使用的部分,减少打包体积。Webpack 支持 ES 模块的 Tree Shaking,确保只打包实际使用的代码。确保代码使用 ES 模块语法,并且在生产环境下开启压缩插件,如 terser-webpack-plugin
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
    mode: 'production',
    optimization: {
        minimizer: [
            new TerserPlugin({
                terserOptions: {
                    compress: {
                        unused: true // 开启 Tree Shaking
                    }
                }
            })
        ]
    }
};
  • 代码分割:通过代码分割将大的文件拆分成多个小的文件,实现按需加载,减少首屏加载时间。可以使用 splitChunks 配置进行公共模块的分割,以及动态导入实现按需加载:
module.exports = {
    optimization: {
        splitChunks: {
            chunks: 'all'
        }
    }
};
  • 压缩文件:使用 optimize-css-assets-webpack-plugin 压缩 CSS 文件,使用 terser-webpack-plugin 压缩 JavaScript 文件,减少文件体积。示例如下:
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
    optimization: {
        minimizer: [
            new TerserPlugin(),
            new OptimizeCSSAssetsPlugin()
        ]
    }
};

运行时性能优化

  • 懒加载:使用动态导入(import())实现代码的懒加载,只有在需要时才加载相应的模块,提高应用的响应速度。例如:
// 动态导入模块
button.addEventListener('click', async () => {
    const { someFunction } = await import('./module.js');
    someFunction();
});
  • 预加载和预取:使用 <link rel="preload"> 和 <link rel="prefetch"> 来提前加载资源,提高后续资源的加载速度。Webpack 可以通过 html-webpack-preload-plugin 实现预加载和预取:
const HtmlWebpackPreloadPlugin = require('html-webpack-preload-plugin');

module.exports = {
    plugins: [
        new HtmlWebpackPreloadPlugin({
            rel: 'preload',
            include: 'initial',
            fileBlacklist: [/.map$/]
        })
    ]
};

六、Webpack Proxy 工作原理以及为什么能解决跨域

Webpack Proxy 工作原理

Webpack Proxy 主要借助 webpack-dev-server 来实现,它的核心工作原理是在本地开发服务器和目标服务器之间充当中间代理。下面是其详细的工作步骤:

  1. 启动本地开发服务器:当你使用 webpack-dev-server 启动开发环境时,它会在本地开启一个服务器,通常监听在 localhost 的某个端口(如 8080)。这个本地服务器负责处理浏览器的请求,并将其转发到目标服务器。
  2. 拦截请求:当浏览器向本地开发服务器发送请求时,webpack-dev-server 会拦截这些请求。它会根据你在配置文件中设置的代理规则,判断该请求是否需要转发到目标服务器。
  3. 转发请求:如果请求匹配到了代理规则,webpack-dev-server 会将该请求转发到目标服务器。在转发过程中,它会保持请求的原始信息(如请求方法、请求头、请求体等)不变,就好像是浏览器直接向目标服务器发送请求一样。
  4. 接收响应:目标服务器接收到请求后,会处理请求并返回响应。webpack-dev-server 会接收目标服务器的响应,并将其返回给浏览器。

以下是一个简单的 Webpack 配置示例,展示了如何设置代理:

const path = require('path');
const { WebpackDevServer } = require('webpack-dev-server');
const webpack = require('webpack');

const config = {
    // 其他配置...
    devServer: {
        proxy: {
            '/api': {
                target: 'http://example.com', // 目标服务器地址
                changeOrigin: true, // 是否改变请求的源
                pathRewrite: { '^/api': '' } // 路径重写
            }
        }
    }
};

const compiler = webpack(config);
const server = new WebpackDevServer(config.devServer, compiler);

const runServer = async () => {
    console.log('Starting server...');
    await server.start();
};

runServer();

为什么能解决跨域问题

跨域问题是由于浏览器的同源策略(Same-Origin Policy)导致的。同源策略规定,浏览器在访问一个页面时,只允许访问与该页面具有相同协议、域名和端口的资源。当你在开发环境中,前端代码运行在本地服务器(如 http://localhost:8080),而后端接口可能部署在另一个域名(如 http://example.com)上,此时浏览器会阻止跨域请求。

Webpack Proxy 能够解决跨域问题的原因在于:

  • 服务器端请求不受同源策略限制:虽然浏览器有同源策略的限制,但服务器之间的请求不受此限制。当使用 Webpack Proxy 时,请求实际上是由本地开发服务器(服务器端)发送到目标服务器的,因此不会受到同源策略的影响。
  • 伪装请求源:通过设置 changeOrigin 为 true,可以让 webpack-dev-server 在转发请求时修改请求头中的 Origin 字段,使其与目标服务器的域名一致。这样,目标服务器会认为请求是从自己的域名发出的,从而允许请求通过。
  • 路径重写:使用 pathRewrite 可以对请求的路径进行重写,确保请求能够正确地转发到目标服务器的接口。例如,将 /api 开头的请求路径重写为空字符串,使得请求能够正确匹配目标服务器的接口路径。

七、Webpack 热更新原理

整体架构

Webpack 热更新主要涉及三个核心部分:Webpack 编译器、HMR 服务器和浏览器客户端。

  • Webpack 编译器:负责监听文件系统的变化,当检测到文件发生更改时,重新编译受影响的模块,并生成更新后的模块信息。
  • HMR 服务器:作为 Webpack 编译器和浏览器客户端之间的桥梁,通过 WebSocket 与浏览器建立实时连接。当 Webpack 编译器生成新的模块更新信息时,HMR 服务器会将这些信息发送给浏览器客户端。
  • 浏览器客户端:接收 HMR 服务器发送的模块更新信息,并根据这些信息更新浏览器中运行的模块。

工作流程

1. 初始化阶段

  • 启动开发服务器:在开发环境中,启动 Webpack 开发服务器,同时开启 HMR 功能。Webpack 会对项目进行初始编译,生成初始的模块代码和 HMR 相关的运行时代码。

  • 建立 WebSocket 连接:浏览器客户端加载页面时,会与 HMR 服务器建立 WebSocket 连接,以便实时接收模块更新信息。

2. 文件变更检测阶段

  • 监听文件变化:Webpack 编译器会监听项目文件系统的变化,当检测到某个模块文件发生更改时,会重新编译该模块。

  • 生成更新信息:编译完成后,Webpack 会生成包含更新模块信息的更新包,这些信息包括更新模块的 ID、更新后的代码等。

3. 模块更新阶段

  • 发送更新信息:HMR 服务器接收到 Webpack 编译器生成的更新包后,会通过 WebSocket 连接将更新信息发送给浏览器客户端

  • 检查更新:浏览器客户端接收到更新信息后,会检查是否有对应的模块需要更新。如果有,则会根据更新信息下载更新后的模块代码。

  • 更新模块:浏览器客户端使用新的模块代码替换旧的模块,并重新执行相关代码。在更新过程中,会尽量保持应用程序的状态(如表单输入、滚动位置等)不变。

关键组件和技术

  • HotModuleReplacementPlugin:这是 Webpack 提供的一个插件,用于启用 HMR 功能。在 Webpack 配置文件中引入该插件后,Webpack 会在编译过程中注入 HMR 相关的代码。

  • module.hot API:在模块代码中,可以使用 module.hot API 来处理模块的热更新逻辑。例如,可以通过 module.hot.accept 方法指定当模块更新时需要执行的回调函数。示例代码如下:

if (module.hot) {
    module.hot.accept('./module', () => {
        // 当 './module' 模块更新时,执行这里的代码
        console.log('Module updated!');
    });
}
  • WebSocket:用于实现 HMR 服务器和浏览器客户端之间的实时通信。通过 WebSocket 连接,HMR 服务器可以及时将模块更新信息发送给浏览器客户端,实现实时更新。

八、Webpack loader 和 plugin 的区别

1. 功能定位

  • loader:主要用于对模块的源代码进行转换。在 Webpack 里,它只能处理单个模块,把不同类型的文件(如 CSS、图片、TypeScript 等)转换为 Webpack 能够识别和处理的模块。例如,babel-loader 能把 ES6+ 代码转换为 ES5 代码,css-loader 可以处理 CSS 文件中的 @import 和 url() 等语句。

  • plugin:功能更为广泛,它能在 Webpack 构建流程的各个阶段执行特定的任务。插件可以影响整个构建过程,包括打包优化、资源管理、环境变量注入等。例如,HtmlWebpackPlugin 可以自动生成 HTML 文件并插入打包后的资源链接,CleanWebpackPlugin 能在每次构建前清空输出目录。

2. 工作时机

  • loader:在模块解析阶段工作。当 Webpack 遇到不同类型的模块时,会按照配置的规则使用相应的 loader 对模块进行处理,处理完成后再将其纳入到打包流程中。

  • plugin:在 Webpack 整个构建生命周期的各个钩子(hook)中执行。Webpack 在构建过程的不同阶段会触发各种钩子,插件可以监听这些钩子并在相应的时机执行特定的操作。

3. 使用方式

  • loader:在 Webpack 配置文件的 module.rules 中配置。rules 是一个数组,每个元素是一个对象,用于定义匹配规则和使用的 loader。例如:
module.exports = {
    module: {
        rules: [
            {
                test: /.js$/,
                use: 'babel-loader'
            }
        ]
    }
};
  • plugin:在 Webpack 配置文件的 plugins 数组中引入和使用。需要先创建插件的实例,然后将其添加到 plugins 数组中。例如:
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html'
        })
    ]
};

九、Webpack 常见的 plugin 和 loader 有哪些,解决了什么问题

常见的 plugin

1. HtmlWebpackPlugin

  • 解决的问题:在前端项目中,通常需要手动在 HTML 文件里引入打包后的 JavaScript 和 CSS 文件。当项目规模变大或者文件名因为哈希等原因动态变化时,手动管理这些引用会变得繁琐且容易出错。HtmlWebpackPlugin 可以自动生成 HTML 文件,并将打包后的资源(如 JavaScript、CSS)自动注入到 HTML 文件中,简化了资源引用的管理。

  • 示例配置

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html' // 指定 HTML 模板文件
        })
    ]
};

2. CleanWebpackPlugin

  • 解决的问题:每次重新构建项目时,旧的打包文件可能会残留在输出目录中,造成文件冗余。CleanWebpackPlugin 可以在每次构建前自动清空输出目录,确保输出目录只包含最新的打包文件,避免文件冗余。

  • 示例配置

const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
    plugins: [
        new CleanWebpackPlugin()
    ]
};

3. MiniCssExtractPlugin

  • 解决的问题:在 Webpack 中,默认情况下 CSS 会被打包到 JavaScript 文件中,这可能会导致页面加载时出现无样式内容闪烁(FOUC)的问题。MiniCssExtractPlugin 可以将 CSS 提取到单独的文件中,减少 JavaScript 文件的体积,并且可以通过 <link> 标签引入 CSS 文件,避免 FOUC 问题,提高页面加载性能。

  • 示例配置

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader'
                ]
            }
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].[contenthash].css'
        })
    ]
};

4. TerserPlugin

  • 解决的问题:打包后的 JavaScript 文件通常包含大量的空格、注释和不必要的代码,文件体积较大,会影响页面的加载速度。TerserPlugin 可以对 JavaScript 文件进行压缩和混淆,去除不必要的空格、注释和代码,减少文件体积,提高页面加载性能。

  • 示例配置

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
    optimization: {
        minimizer: [
            new TerserPlugin()
        ]
    }
};

5. CopyWebpackPlugin

  • 解决的问题:项目中可能存在一些不需要经过 Webpack 处理的静态文件(如图片、字体等),但需要将它们复制到输出目录中。CopyWebpackPlugin 可以将指定的文件或目录复制到输出目录,确保这些静态文件在打包后仍然可用。

  • 示例配置

const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
    plugins: [
        new CopyWebpackPlugin({
            patterns: [
                { from: 'public', to: '.' }
            ]
        })
    ]
};

6. HotModuleReplacementPlugin

  • 解决的问题:在开发过程中,每次修改代码后都需要刷新整个页面才能看到修改后的效果,这会导致开发效率低下。HotModuleReplacementPlugin 可以实现热模块替换(HMR)功能,在不刷新整个页面的情况下更新修改的模块,保持应用的状态,提高开发效率。

  • 示例配置

const webpack = require('webpack');

module.exports = {
    mode: 'development',
    devServer: {
        hot: true
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ]
};

常见的 loader

1. Babel-loader

  • 解决的问题:JavaScript 语言标准不断发展,新的特性如 ES6+ 带来了更简洁高效的语法,但并非所有浏览器都能支持这些新特性。babel-loader 结合 Babel 工具,能将 ES6+ 代码转换为向后兼容的 JavaScript 代码,确保代码在旧版浏览器中也能正常运行。

  • 示例配置

module.exports = {
    module: {
        rules: [
            {
                test: /.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                }
            }
        ]
    }
};

2. CSS-loader 和 Style-loader

  • 解决的问题:Webpack 默认无法处理 CSS 文件,css-loader 用于解析 CSS 文件中的 @import 和 url() 语句,处理 CSS 文件之间的依赖关系;style-loader 则将 CSS 代码以 <style> 标签的形式插入到 HTML 文件的 <head> 中,使样式能够在页面中生效。

  • 示例配置

module.exports = {
    module: {
        rules: [
            {
                test: /.css$/,
                use: ['style-loader', 'css-loader']
            }
        ]
    }
};

3. SASS-loader 和 LESS-loader

  • 解决的问题:Sass 和 Less 是 CSS 预处理器,它们扩展了 CSS 的语法,提供了变量、嵌套、混合等功能,能提高样式代码的可维护性和复用性。但浏览器无法直接识别 Sass 和 Less 代码,sass-loader 和 less-loader 分别将 Sass 和 Less 文件编译为普通的 CSS 文件,再结合 css-loader 和 style-loader 让样式在页面中生效。

  • 示例配置(以 Sass 为例)

module.exports = {
    module: {
        rules: [
            {
                test: /.scss$/,
                use: ['style-loader', 'css-loader', 'sass-loader']
            }
        ]
    }
};

4. File-loader 和 URL-loader

  • 解决的问题:项目中会有图片、字体等静态文件,Webpack 默认无法处理这些文件。file-loader 可以将这些文件复制到输出目录,并返回文件的公共 URL,方便在代码中引用;url-loader 功能与 file-loader 类似,但它可以根据文件大小将文件转换为 Base64 编码的 Data URL,减少 HTTP 请求。

  • 示例配置(以图片为例)

module.exports = {
    module: {
        rules: [
            {
                test: /.(png|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 8192 // 文件大小小于 8KB 时使用 Data URL
                        }
                    }
                ]
            }
        ]
    }
};

5. TS-loader

  • 解决的问题:TypeScript 是 JavaScript 的超集,增加了静态类型检查,有助于提高代码的可靠性和可维护性。但浏览器无法直接运行 TypeScript 代码,ts-loader 可以将 TypeScript 文件编译为 JavaScript 文件,让 Webpack 能够处理 TypeScript 模块。

  • 示例配置

module.exports = {
    module: {
        rules: [
            {
                test: /.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/
            }
        ]
    },
    resolve: {
        extensions: ['.tsx', '.ts', '.js']
    }
};

6. Vue-loader

  • 解决的问题:在 Vue.js 项目中,组件通常以 .vue 文件的形式存在,它包含了模板(template)、脚本(script)和样式(style)三部分。vue-loader 可以将 .vue 文件解析为 JavaScript 模块,让 Webpack 能够处理 Vue 组件。

  • 示例配置

const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
    module: {
        rules: [
            {
                test: /.vue$/,
                use: 'vue-loader'
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin()
    ]
};

十、Webpack 构建流程及解决的问题

工作原理

Webpack 的工作过程主要包含以下几个步骤:

  1. 解析配置:Webpack 启动时会读取并解析配置文件(通常是 webpack.config.js),获取入口、输出、loader、plugin 等配置信息。

  2. 构建模块依赖图:从入口文件开始,Webpack 递归分析文件中的依赖关系,找出所有依赖的模块。在这个过程中,会使用配置好的 loader 对不同类型的模块进行处理。

  3. 打包模块:根据模块依赖图,Webpack 将所有模块打包成一个或多个文件。在打包过程中,会对代码进行优化,如去除重复代码、压缩代码等。

  4. 生成输出文件:最后,Webpack 将打包好的文件输出到指定的目录。

解决的问题

  • 模块管理:随着前端项目规模的不断增大,代码会被拆分成多个模块。Webpack 可以帮助开发者管理这些模块之间的依赖关系,确保模块按照正确的顺序加载。它支持多种模块规范,如 CommonJS、ES6 模块等,让开发者可以自由选择合适的模块规范进行开发。

  • 资源处理:现代前端项目中包含各种类型的资源,如 CSS、图片、字体等。Webpack 可以通过 loader 对这些资源进行处理,将它们转换为模块并打包到最终的文件中。例如,使用 css-loader 和 style-loader 处理 CSS 文件,使用 file-loader 或 url-loader 处理图片文件。

  • 代码分割:对于大型项目,将所有代码打包到一个文件中会导致文件体积过大,影响页面加载速度。Webpack 支持代码分割功能,可以将代码拆分成多个小文件,实现按需加载。例如,使用动态导入(import())可以实现懒加载,只有在需要时才加载相应的模块。

  • 性能优化:Webpack 提供了多种性能优化手段,如代码压缩、Tree Shaking(去除未使用的代码)、资源缓存等。通过这些优化措施,可以减少打包文件的体积,提高页面的加载速度。

  • 开发体验提升:Webpack 结合 webpack-dev-server 可以提供热更新(Hot Module Replacement)功能,在开发过程中修改代码后,无需刷新整个页面就能看到修改后的效果,大大提高了开发效率。同时,Webpack 还支持 Source Map,方便开发者在调试时定位代码问题。

十一、module chunk bundle 分别指的是什么

module 是代码的基本单元,chunk 是打包过程中的中间产物,而 bundle 是最终输出的文件。。

module(模块)

  • 定义:在 Webpack 的体系里,一切皆模块。无论是 JavaScript 文件、CSS 文件、图片、字体,还是 JSON 文件等,都能被当作模块。模块可以相互依赖,一个模块能够引入其他模块,形成复杂的依赖关系。

  • 作用:模块是代码组织和复用的基本单位。通过将代码拆分成多个模块,可以提高代码的可维护性和可测试性。Webpack 会从入口模块开始,递归分析模块之间的依赖关系,进而构建出整个项目的依赖图。

  • 示例

// math.js 模块
export const add = (a, b) => a + b;

// main.js 模块引入 math.js 模块
import { add } from './math.js';
const result = add(1, 2);
console.log(result);

chunk(代码块)

  • 定义:chunk 是 Webpack 在打包过程中生成的中间产物,它是一系列模块的集合。在打包过程中,Webpack 会根据配置和模块之间的关系,将模块分割成不同的 chunk。chunk 可以理解为是构建过程中的逻辑分组,最终会被合并成一个或多个 bundle。

  • 作用:chunk 的存在有助于实现代码分割和按需加载。通过将代码分割成多个 chunk,可以减少初始加载的代码量,提高页面的加载速度。例如,对于一些不常用的模块,可以将其分割成单独的 chunk,在需要时再进行加载。

  • 示例:使用动态导入(import())可以实现代码分割,生成新的 chunk:

// main.js
const loadModule = async () => {
    const { add } = await import('./math.js');
    const result = add(1, 2);
    console.log(result);
};

loadModule();

在这个例子中,math.js 模块会被分割成一个单独的 chunk,在 loadModule 函数被调用时才会加载。

bundle(捆绑包)

  • 定义:bundle 是 Webpack 打包后的最终输出文件。Webpack 根据模块之间的依赖关系,把所有模块合并、打包成一个或多个文件,这些文件就是 bundle。在生产环境中,通常希望将代码打包成尽可能少的文件,以减少浏览器的请求次数,提高页面加载速度。

  • 作用:bundle 文件是可以直接部署到生产环境并被浏览器加载的文件。它们包含了项目中所有模块的代码,经过了压缩、优化等处理,以适应生产环境的需求。

  • 示例:在 Webpack 配置中,通过 output 选项可以指定 bundle 的输出路径和文件名:

const path = require('path');

module.exports = {
    entry: './src/main.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    }
};

十二、Lighthouse 与 Performance 的区别

1. 功能覆盖范围

  • Lighthouse:是一个综合性工具,不仅关注页面的性能指标(如首次内容绘制(FCP)、最大内容绘制(LCP)、首次输入延迟(FID)、累积布局偏移(CLS)、完全加载时间(TTI)等),还涵盖了页面的可访问性(确保残障人士能够无障碍使用页面)、最佳实践(检查代码是否遵循现代前端开发的最佳实践,如 HTTPS 使用、安全头设置等)、搜索引擎优化(SEO,检查页面是否有利于搜索引擎索引和排名)以及渐进式 Web 应用(PWA)相关的评估。它可以对页面进行全面的分析和诊断。

  • Performance:主要专注于页面性能方面分析。通过记录页面加载过程中的各种事件(如资源加载、脚本执行、渲染过程等),生成详细的性能时间线。它能精确展示每个阶段的耗时情况,帮助开发者深入了解页面性能瓶颈的具体位置,比如是网络请求时间过长、脚本执行时间太久还是渲染过程出现了问题等。

2. 数据呈现方式

  • Lighthouse:以报告的形式呈现结果,会给出每个评估类别(性能、可访问性等)的得分,以及具体的改进建议和优化提示。报告中还会对一些关键性能指标进行总结和分析,方便开发者快速了解页面的整体情况和需要改进的方向。例如,在性能方面,会指出哪些资源可以进行压缩、哪些脚本可以进行延迟加载等。

  • Performance:主要通过可视化的时间线展示数据。在时间线上,可以看到各种事件(如资源加载、脚本执行、渲染帧等)的发生时间和持续时间,并且可以进行放大、缩小等操作,以便更细致地观察每个事件的细节。开发者可以通过时间线分析事件之间的先后顺序和依赖关系,从而找出性能问题的根源。

3. 使用场景

  • Lighthouse:适合在项目的整体评估阶段使用,例如在开发完成后对页面进行全面的审核,或者在优化过程中定期检查页面的各项指标是否达到预期。它可以帮助开发者从多个维度了解页面的质量,发现潜在的问题并制定综合的优化策略。另外,对于需要满足特定标准(如 SEO 要求、可访问性标准等)的项目,Lighthouse 是一个非常有用的工具。

  • Performance:更常用于开发和调试阶段。当开发者发现页面存在性能问题(如加载缓慢、卡顿等)时,可以使用 Performance 面板进行详细的性能分析。通过分析时间线,开发者可以精确地定位到问题所在,比如某个脚本的执行时间过长导致页面卡顿,然后针对性地进行优化。

十三、什么是 CI / CD

CI/CD 是现代软件开发流程中的关键实践,分别代表持续集成(Continuous Integration)和持续交付 / 部署(Continuous Delivery/Deployment)。

  • 持续集成(CI) :是一种软件开发实践,团队成员频繁地将代码集成到共享代码库中,每次集成都通过自动化的构建(包括编译、测试等)来验证,从而尽早发现集成过程中出现的问题。通过这种方式,可以减少代码冲突和集成错误,确保代码的可维护性和稳定性。

  • 持续交付(CD - Delivery) :是在持续集成的基础上,将经过测试的代码自动部署到预生产环境或其他测试环境中,以便进行进一步的验证和测试。持续交付强调代码始终处于可部署的状态,但部署到生产环境仍然需要人工手动触发。

  • 持续部署(CD - Deployment) :是持续交付的更进一步,它不仅要求代码始终处于可部署的状态,而且在经过自动化测试后,会自动将代码部署到生产环境中,无需人工干预。这可以大大缩短从代码提交到上线的时间,提高软件的交付速度。

十四、Tree Shaking 工作原理

1. 静态分析

Tree Shaking 依赖于静态代码分析,即不执行代码,仅通过分析代码的语法结构来确定哪些代码是被使用的,哪些是未被使用的。在 JavaScript 中,ES6 模块系统的静态导入和导出特性为 Tree Shaking 提供了良好的支持。ES6 模块采用 import 和 export 语句来导入和导出模块,这些语句在编译时就可以确定模块之间的依赖关系,而不像 CommonJS 模块的 require 语句是动态的,难以进行静态分析。

示例代码如下:

// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// main.js
import { add } from './math.js';
const result = add(1, 2);
console.log(result);

在这个例子中,math.js 模块导出了 add 和 subtract 两个函数,但在 main.js 中只导入并使用了 add 函数,subtract 函数就属于未使用的代码。

2. 标记未使用的代码

构建工具在进行静态分析后,会标记出那些未被使用的代码。以 Webpack 为例,它会在打包过程中,根据模块之间的依赖关系和代码的引用情况,找出未被引用的导出项,并将其标记为可移除的。

3. 去除未使用的代码

在打包的最后阶段,构建工具会根据标记,将未使用的代码从最终的打包文件中去除。这个过程通常结合 UglifyJS、Terser 等代码压缩工具来完成,这些工具会在压缩代码时,删除那些被标记为未使用的代码。

十五、Webpack 常见的配置及作用

入口(entry)

  • 作用:指定 Webpack 开始打包的入口文件,Webpack 会从入口文件开始递归地构建依赖图。
  • 示例
module.exports = {
    entry: './src/index.js'
};

这里指定了 ./src/index.js 作为入口文件。

输出(output)

  • 作用:配置 Webpack 打包后的文件输出位置和文件名。
  • 示例
const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    }
};

上述配置将打包后的文件输出到 dist 目录下,文件名为 bundle.js

加载器(loader)

  • 作用:Webpack 本身只能处理 JavaScript 文件,loader 用于处理其他类型的文件,如 CSS、图片、字体等,将它们转换为 Webpack 可以处理的模块。

  • 常见 loader

    • style-loader:将 CSS 插入到 DOM 中。

    • css-loader:解析 CSS 文件。

    • file-loader:处理图片、字体等文件。

插件(plugin)

  • 作用:插件可以在 Webpack 构建过程的不同阶段执行特定的任务,如压缩代码、生成 HTML 文件等。

  • 常见插件

    • HtmlWebpackPlugin:用于生成 HTML 文件,并自动将打包后的 JavaScript 文件引入到 HTML 中。

    • CleanWebpackPlugin:用于在每次打包前清理输出目录(dist 目录)。

模式(mode)

  • 作用:指定 Webpack 的构建模式,有 developmentproduction 和 none 三种模式。
  • 示例
module.exports = {
    mode: 'production'
};

development 模式会启用一些有助于开发的特性,如更详细的错误信息和更快的构建速度;production 模式会对代码进行压缩和优化,以减小文件体积。

解析(resolve)

  • 作用:配置 Webpack 如何解析模块路径。
  • 示例
module.exports = {
    resolve: {
        extensions: ['.js', '.json'],
        alias: {
            '@': path.resolve(__dirname, 'src')
        }
    }
};

extensions 配置可以让你在导入模块时省略文件扩展名;alias 配置可以为模块路径设置别名,方便导入模块。