前端工程化相关问题

web 前端模块化有哪些方式

  1. IIFE(立即执行函数表达式) 最早期的模块化方案,通过函数作用域隔离变量,避免全局污染。
js 复制代码
// 模块定义
const moduleA = (function() {
  const privateVar = '私有变量';
  function privateFn() { /* ... */ }
  return {
    publicMethod: () => { /* ... */ },
    publicVar: '公共变量'
  };
})();
// 使用
moduleA.publicMethod();

缺点:依赖手动管理,无法动态加载。

  1. CommonJS(Node.js 模块系统)

用于 Node.js 环境,采用同步加载方式,通过 require 导入、module.exports 导出。

js 复制代码
// 模块 a.js
module.exports = {
  foo: () => console.log('foo'),
  bar: 123
};
// 模块 b.js
const a = require('./a.js');
a.foo(); // 调用 a 模块的方法

缺点:同步加载不适合浏览器环境(会阻塞渲染)。

  1. AMD

专为浏览器设计,支持异步加载模块,代表库为 RequireJS。

js 复制代码
// 定义模块
define('moduleA', ['dependency'], function(dep) {
  return {
    method: () => dep.doSomething()
  };
});

// 加载模块
require(['moduleA'], function(moduleA) {
  moduleA.method();
});
  1. CMD
    结合 CommonJS 和 AMD 的特点,就近依赖、延迟执行,代表库为 SeaJS。
js 复制代码
// 模块定义
define(function(require, exports, module) {
  const dep = require('./dependency'); // 就近依赖
  exports.method = () => dep.doSomething();
});

目前已逐渐被 ES Modules 替代。

  1. UMD

通用模块格式,兼容 CommonJS、AMD 和全局变量,用于跨环境模块(如工具库)。

js 复制代码
(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    define(['dependency'], factory); // AMD
  } else if (typeof module === 'object' && module.exports) {
    module.exports = factory(require('dependency')); // CommonJS
  } else {
    root.moduleName = factory(root.dependency); // 全局变量
  }
})(this, function(dep) {
  return { /* 模块内容 */ };
});
  1. ES Modules
    ES6 官方标准化的模块化方案,浏览器和 Node.js 均支持,采用静态分析(编译时加载)。
js 复制代码
// 模块 a.js
export const name = 'moduleA';
export function func() { /* ... */ }

// 模块 b.js
import { name, func } from './a.js';
console.log(name);
func();

优点:静态检查、Tree-shaking 优化、原生支持;缺点:浏览器需通过 <script type="module"> 启用。

pnpm 有什么优势

  1. 节省磁盘空间,减少重复存储
    所有安装过的包会被存储在一个统一的全局存储目录 (如 ~/.pnpm-store)中,不同项目引用同一版本的包时,只会保留一份副本,通过硬链接符号链接 指向项目的 node_modules
  2. 安装速度更快
    由于依赖包无需重复下载和存储,pnpm 安装依赖时,优先从全局存储中复用已有的包,减少网络请求和磁盘写入操作。
  3. 严格的依赖隔离,避免 幽灵依赖
    npm/yarn 的 node_modules 采用 "扁平结构",可能导致项目间接依赖的包被意外访问(即 "幽灵依赖",如 import 'lodash' 但未在 package.json 中声明)。 pnpm 的 node_modules 结构为嵌套 + 链接 ,只有在 package.json 中显式声明的依赖才会被暴露,避免隐式依赖导致的潜在问题,更符合规范。
  4. 支持 monorepo 项目的高效管理
    对于多包管理的 monorepo 项目,pnpm 提供了原生支持(通过 pnpm workspace),能高效处理包之间的依赖关系,避免重复安装,提升构建效率。

webpack 有哪些常见的 Loader

js 复制代码
raw-loader: 允许你将文件作为字符串导入到 JavaScript 模块中
file-loader: 把文件输出到文件夹中,在代码中通过相对 url 去引用输出的文件 (图片 字体)
url-loader: 与 file-loader 类似,区别 可以设置阈值 大于 交给file-loader 处理,小于则 base64 编码 处理图片
image-loader: 加载并压缩图片文件
json-loader: 加载 json 文件
babel-loader: 将 es6 转换成 es5
ts-loader: 将 ts 语言转换成 js
sass-loader 转换 css
less-loader 转换 css
css-loader: 加载 css
style-loader: 把 css 注入到 js 中,通过 dom 操作加载 css
postcss-loader: 扩展 css 语法,使用下一代 css
eslint-loader 代码检查

有哪些常见的 Plugin

define-plugin: 定义环境变量

js 复制代码
ignore-plugin: 忽略部分文件
html-webpack-plugin: 简化 html 文件创建
uglifyjs-webpack-plugin: 压缩 js
mini-css-extract-plugin: 分离样式文件,css 提取为单独文件,支持按需加载
clean-webpack-plugin: 目录清理
webpack-bundle-analyzer: 可视化 webpack 输出文件的体积
speed-measure-webpack-plugin: 看到每个 loader plugin 执行耗时

Loader 和 Plugin 的区别

  1. Loader(加载器):处理文件转换
    核心功能 :将非 JavaScript 文件(如 CSS、图片、TypeScript 等)转换为 Webpack 可识别的模块(最终转为 JS 模块)。
    工作时机 :在 Webpack 的模块解析阶段 工作,当遇到特定类型的文件时,触发对应的 Loader 进行转换。
    使用方式 :在 webpack.config.jsmodule.rules 中配置,通过 test 匹配文件类型,use 指定使用的 Loader。

  2. Plugin(插件):扩展 Webpack 功能
    核心功能 :解决 Loader 无法完成的其他任务,如优化打包结果、资源管理、环境变量注入等,扩展 Webpack 的整个构建流程。
    工作时机 :贯穿 Webpack 的整个生命周期 (从初始化、编译、输出到结束),可在不同阶段介入处理。
    使用方式 :在 webpack.config.jsplugins 数组中配置,通常需要实例化(通过 new 关键字)。

维度 Loader Plugin
作用 转换特定类型的文件(如 CSS→JS) 扩展 Webpack 功能(如优化、生成文件)
处理对象 针对文件内容进行转换 针对整个构建流程进行扩展
工作阶段 模块解析阶段(加载时) 整个生命周期(从初始化到输出)
配置方式 module.rules 中配置 plugins 数组中实例化
本质 函数(接收文件内容,返回处理后内容) 类(需实现 apply 方法,注册钩子)

Webpack 构建流程

1. 初始化阶段

  • 读取并合并配置参数(webpack.config.js、CLI 参数、默认配置)
  • 创建 Compiler 实例,负责整个构建过程的统筹
  • 加载所有配置的插件,执行插件的 apply 方法

2. 编译阶段

  • 入口处理:从配置的 entry 出发,解析入口文件
  • 模块解析:对每个模块进行递归解析(调用相应的 loader 对不同类型的文件进行转换,分析模块依赖关系,形成依赖图谱)
  • 模块转换:将处理后的模块转换为可执行的代码块

3. 输出阶段

  • Chunk 优化:对生成的代码块进行优化(如代码分割、Tree-shaking 等)
  • 资源生成:根据输出配置(output)将 Chunk 写入到文件系统
  • 插件介入:在输出过程中,插件可以对最终产物进行二次处理(如压缩、添加哈希等)

4. 完成阶段

  • 完成所有文件的输出
  • 执行构建完成后的回调函数

Compiler 和 Compilation 区别

1. Compiler

  • 本质:全局唯一的编译器实例,贯穿整个 Webpack 生命周期。
  • 职责
    • 管理 Webpack 从启动到退出的全过程,是构建流程的总控制器。
    • 保存着 Webpack 的全局配置(如 entryoutputplugins 等)。
    • 提供生命周期钩子(如 runcompiledone 等),供插件在不同阶段介入。
  • 生命周期:从 Webpack 启动时创建,直到构建完全结束才被销毁。

2. Compilation

  • 本质:代表一次具体的编译过程(可能有多次),是构建过程中的 "工作单元"。
  • 职责
    • 负责模块的解析、依赖分析、代码转换和优化等具体编译工作。
    • 维护当前编译的所有模块(modules)、代码块(chunks)、资源(assets)等信息。
    • 提供编译阶段的钩子(如 buildModulesealoptimize 等),用于干预模块处理和代码优化。
  • 生命周期:每次触发编译时创建(如首次构建、watch 模式下文件变化时),编译完成后销毁。
维度 Compiler Compilation
生命周期 全局唯一,贯穿整个构建流程 每次编译创建,编译结束后销毁
核心作用 控制整体流程,管理全局配置 执行具体编译工作,处理模块和依赖
关联关系 可以创建多个 Compilation 实例 由 Compiler 实例创建和管理
钩子类型 全局流程钩子(如启动、完成) 编译阶段钩子(如模块解析、优化)

文件指纹

在 Webpack 中配置文件指纹(即给输出文件添加唯一标识),主要用于解决浏览器缓存问题,确保用户能获取到最新的文件。配置方式因文件类型(JS、CSS、图片等)略有不同,下面是具体实现方法:

  1. 核心配置:output.filename
    Webpack 通过 output.filename 配置出口文件的命名规则,通过内置的 占位符(placeholders) 实现文件指纹:

常用占位符:

  • [hash]:整个项目的哈希值,每次构建(内容有变化)都会改变
  • [chunkhash]:基于入口 chunk 的哈希值,不同入口的哈希独立
  • [contenthash]:基于文件内容的哈希值,内容不变则哈希不变(推荐)
  • [name]:文件名
  • [id]:chunk 的 id
  • [ext]:文件扩展名

(1)JS 文件指纹

直接在 output 中配置,推荐使用 [contenthash]

js 复制代码
module.exports = {
  output: {
    // 格式:文件名.内容哈希.扩展名
    filename: 'js/[name].[contenthash:8].js', 
    // 非入口的 chunk(如异步加载的模块)
    chunkFilename: 'js/[name].[contenthash:8].chunk.js'
  }
};

(2)CSS 文件指纹

CSS 通常通过 mini-css-extract-plugin 提取为单独文件,需在该插件中配置:

配置

js 复制代码
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  module: {
    rules: [
      {
        test: /.css$/,
        // 用 MiniCssExtractPlugin.loader 替代 style-loader
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      // CSS 文件指纹配置
      filename: 'css/[name].[contenthash:8].css',
      chunkFilename: 'css/[name].[contenthash:8].chunk.css'
    })
  ]
};

(3)图片 / 字体等资源文件

js 复制代码
module.exports = {
  module: {
    rules: [
      {
        test: /.(png|jpe?g|gif|svg)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10240, // 小于 10KB 的图片转 base64
              // 资源文件指纹配置
              name: 'img/[name].[contenthash:8].[ext]'
            }
          }
        ]
      }
    ]
  }
};

最佳实践

  • 优先使用 [contenthash] :只有文件内容变化时,哈希才会改变,最大化利用缓存
  • 按类型分目录 :如 js/css/img/,便于管理
  • 哈希长度 :通常取 8 位(如 [contenthash:8]),既保证唯一性又避免文件名过长
  • 配合 HtmlWebpackPlugin:自动引入带指纹的文件,无需手动修改 HTML 引用

Babel 原理

Babel 是一个广泛使用的 JavaScript 编译器,主要用于将 ES6+(ES2015 及以上)的代码转换为向后兼容的 JavaScript 语法,以便在旧版本浏览器或环境中运行。其核心原理可以概括为「解析 - 转换 - 生成」三个阶段的工作流程。

  1. 解析(Parse)
    将原始代码字符串转换为抽象语法树(AST,Abstract Syntax Tree),分为两个步骤:
  • 词法分析
    将代码字符串拆分为为不可再分的最小单元(称为 Token,如关键字、标识符、运算符等)。例如,const a = 1 会被拆分为 consta=1 等 Token。
  • 语法分析
    根据 Token 序列和语法规则,构建出描述代码结构的 AST。AST 是一种树形结构,每个节点代表代码中的一个语法结构(如变量声明、函数调用等)。
    Babel 使用 @babel/parser(基于 Acorn 实现)完成解析过程。
  1. 转换(Transform)
    对生成的 AST 进行遍历和修改,将高版本语法转换为低版本语法。这是 Babel 最核心的步骤: 通过 @babel/traverse 工具遍历 AST 的每个节点。例如,将 ES6 的箭头函数 () => {} 转换为 ES5 的 function() {},将 class 转换为原型链语法等。经过插件处理后,得到符合目标环境语法的新 AST。
  2. 生成(Generate)
    将转换后的 AST 重新转换为代码字符串:
    递归生成代码@babel/generator 会递归遍历新 AST 的每个节点,将其转换为对应的代码字符串,并自动添加分号、空格等格式。
    保留源码信息:在生成过程中,会尽量保留原始代码的注释、换行等格式(如果配置了相关选项)。

webpack 热更新

在开发时,修改代码后可以保留当前页面状态(如表单输入、页面滚动位置等),同时看到更新效果。

  1. 启动阶段
  • Webpack 构建时会在输出文件中注入 HMR 运行时代码(webpack/hot/dev-server
  • 开发服务器(如 webpack-dev-serverwebpack-dev-middleware)启动一个 WebSocket 连接,用于客户端和服务器的实时通信
  1. 文件变化时
  • Webpack 监听到文件修改,重新编译受影响的模块,生成 更新后的 chunk更新清单(manifest)
  • 服务器通过 WebSocket 将更新清单发送给客户端
  1. 客户端更新
  • 客户端根据清单请求获取更新的 chunk
  • HMR 运行时替换旧模块,执行模块的 module.hot.accept 回调(如果有)
  • 如果模块不支持热更新,会触发页面刷新

Webpack 5 显著提升和改进

  • 持久化缓存 :Webpack 5 引入了更强大的持久化缓存机制,可在多次构建间有效利用缓存,大幅减少重复计算和构建时间。

webpack 会缓存生成的 webpack 模块和 chunk, 来改善构建速度 缓存在 webpack5 中默认开启,缓存默认是在内存里,但可以对 cache 进行设置 webpack 追踪了每个模块的依赖,并创建了文件系统快照。此快照会与真实文件系统进行比较,当检测到差异时,将触发对应模块的重新构建

  • 模块联邦
    实现跨应用之间的模块共享,使用 npm 的缺点:npm 包升级要通知所有的项目升级,每个项目的升级都要重新打包构建。 案例:
js 复制代码
// 在 HOME 的入口文件或其他需要使用的文件中
// 动态导入远程模块中的组件
async function loadRemoteComponent() {
    const { Header } = await import('nav/./Header');
    // 这里假设 Header 是一个 React 组件,使用方式如下(以 React 为例)
    ReactDOM.render(<Header />, document.getElementById('root'));
}

loadRemoteComponent();
  • tree-shaking 在 Webpack5 打包之后,只会有function1 了,
  • 自动清除打包目录

代码分割

  1. 多入口起点配置
js 复制代码
module.exports = {
    entry: {
        main: './src/main.js',
        vendor: './src/vendor.js'
    }
}
  1. 动态导入 Dynamic import
js 复制代码
// main.js
import('./moduleA').then((module) => {
// 使用moduleA
})
  1. splitChunks 插件
js 复制代码
// webpack.config.js
module.exports = {
    optimization: {
        splitChunks: {
            chunks: 'all'
        }
    }
}

vite 中怎么修改 plugin 的执行顺序

● 1. enforce: 'pre'

设置此值的插件会在 Vite 核心插件之前执行。

● 2. 不设置 enforce

默认情况下,插件会在 Vite 核心插件之后、其他构建插件之前执行。

● 3. enforce: 'post'

设置此值的插件会在 Vite 构建插件之后执行。

1.首先执行带有 enforce: 'pre' 的用户插件

2.接着是 Vite 的核心插件

3.然后是没有设置 enforce 属性的用户插件和其他 Vite 构建插件

4.紧接着是带有 enforce: 'post' 的用户插件

5.最后是 Vite 的后置构建插件(如代码最小化、生成 manifest 文件等)

js 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import myPlugin from 'my-vite-plugin'

export default defineConfig({
    plugins: [
        // 在核心插件前执行的插件
        { ...myPlugin(), enforce: 'pre' },
        // 核心插件
        vue(),
        // 默认顺序的插件
        anotherPlugin(),
        // 在构建插件后执行的插件
        { ...postPlugin(), enforce: 'post' }
    ]
})

Vite为什么更快?

  • webpack 需要先打包,然后启动本地服务器。由于浏览器支持 ES module,vite 是先启动了服务器,然后按需编译构建。
  • vite 编译使用 esbuild ,编译速度比 babel 等工具快
  • 生产打包过程使用rollup,对 treeshaking 和代码拆分的支持度好,减少打包体积。
  • HRM:Vite 只会重新加载发生变化的模块,而不是重新打包整个应用

Vite 的依赖预加载机制

主要针对项目中不会频繁变动的第三方依赖(如node_modules中的库)进行优化

devServer启动 -> Vite 会先对依赖进行预构建 -> 通过esbuild将依赖转换为ES 模块(ESM) 格式(将 CommonJS 或 UMD 格式转换为ESM)

合并依赖:将多个关联的依赖模块合并为单个文件(如将lodash的多个子模块合并),减少浏览器请求次数。

预构建结果(转换后的 ESM 文件、依赖关系图谱等)会缓存到node_modules/.vite目录,后续启动时若依赖未变则直接复用,大幅提升启动速度。避免了重复执行耗时的依赖处理工作。

vite 中的插件

生命周期:

  • config:用于修改 Vite 配置,通常在构建或开发过程中使用。
  • configureServer:用于修改开发服务器的行为,如自定义请求处理。
  • transform:对文件内容进行转换,适用于文件类型转换或代码处理。
  • buildStart 和 buildEnd:在构建过程开始和结束时触发,适用于日志记录或优化操作。

webpack 的 bundle 和 chunk

chunk:在内存中处理的代码块 | Webpack 内部构建过程中的中间产物 | 用于 Webpack 内部的模块依赖管理和代码分割,帮助 Webpack 跟踪模块间的关系

bundle:输出到磁盘的文件 | 是最终供浏览器加载执行的文件,包含了经过处理(如压缩、转换)的代码。

相关推荐
Mintopia2 分钟前
⚔️ WebAI 推理效率优化:边缘计算 vs 云端部署的技术博弈
前端·javascript·aigc
爱学大树锯1 小时前
【Ruoyi 解密 - 09. 前端探秘2】------ 接口路径及联调实战指南
前端
老华带你飞1 小时前
校园二手书交易|基于SprinBoot+vue的校园二手书交易管理系统(源码+数据库+文档)
java·前端·数据库·vue.js·小程序·毕设·校园二手书交易管理系统
萌程序.1 小时前
创建Vue项目
前端·javascript·vue.js
VT.馒头2 小时前
【力扣】2704. 相等还是不相等
前端·javascript·算法·leetcode·udp
linweidong2 小时前
Vue前端国际化完全教程(企业内部实践教程)
前端·javascript·vue.js·多语言·vue-i18n·动态翻译·vue面经
lukeLiouu2 小时前
augment不能白嫖了?试试claude code + GLM4.5,十分钟搞定起飞🚀
前端
点正2 小时前
使用 Volta 管理 Node 版本和 chsrc 换源:提升开发效率的完整指南
前端
泉城老铁2 小时前
VUE2实现加载Unity3d
前端·vue.js·unity3d
chengqingyuan2 小时前
实现一个简易的代码AI Agent
前端