前端模块化
说说你对前端工程化的理解?为什么要工程化
一般的前端工程化就是:模块化拆分 + 规范化开发 + 自动化构建/测试/部署
| 维度 | 具体表现 |
|---|---|
| 模块化 | 代码按功能拆分(如 ES Module、组件库),降低耦合度 |
| 规范化 | 统一代码风格(ESLint)、提交规范(Commitizen)、目录结构 |
| 自动化 | 构建(Webpack)、测试(Jest)、部署(CI/CD)等流程无需手动干预 |
原因
| 指标 | 工程化影响 |
|---|---|
| 提升效率 | 减少重复劳动(封装组件) |
| 提高质量 | 静态检查 + 自动化测试提前拦截 Bug,类型系统减少运行时错误 |
| 提升性能 | 构建优化(Chunk 拆分、按需加载)、资源压缩(CSS/JS minify)、缓存策略 |
总结 :前端工程化本质是 用工具约束人,用流程保证质量
什么是模块?
模块(Module)是将程序代码按功能、逻辑拆分后的独立单元,每个模块封装了特定的功能(变量、函数、类、逻辑等),同时通过明确的规则暴露需要对外提供的内容,隐藏内部实现细节,是前端工程化中 “分而治之” 思想的核心体现。
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
- 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信
模块化的好处特点?为什么要模块化?
- 避免命名冲突
(独立性):模块内部的变量、函数不会污染全局作用域(解决传统 JS “全局变量冲突” 问题); - 高可复用性:一个模块可在项目多个地方引用(比如封装的 “日期格式化模块”,可在登录、订单页面复用);
- 高可维护性:功能拆分后,修改某个模块(如调整购物车逻辑)不会影响其他模块,便于迭代和 bug 修复;
- 更好的分离,按需引入:可只加载需要的模块(比如首屏只加载 “首页模块”,而非整个项目代码),优化性能。
模块化的进化过程
- 无模块化 →
- 手写隔离(IIFE / 命名空间) →
- 规范萌芽(CommonJS --> AMD --> CMD) →
- 工程化打包(Webpack) →
- 原生支持(ES6 Module)
核心目标始终是:让代码更易拆分、复用、维护,适配从「小脚本」到「大型应用」的开发需求
1. 早期无模块化(2000 年前后,JS 刚普及)
- 现状:没有任何模块化概念,所有代码写在
<script>标签里,全局变量满天飞。比如:多个<script>里的var a = 1会互相覆盖,函数名也容易冲突;引入顺序错了(比如先调用 jQuery 再引入 jQuery 文件)就报错。 - 临时方案:用「命名空间」(比如
var myApp = { util: {}, api: {} })或「立即执行函数(IIFE)」封装代码,勉强隔离作用域,但依赖管理还是靠手动调整<script>顺序,复杂项目完全扛不住。
2. 模块化规范萌芽(2010 年左右,Node.js/ 复杂前端项目兴起)
-
背景:Node.js 诞生(2009),前端项目规模变大(如电商、后台系统),急需统一的模块化规则。
-
核心规范:
- CommonJS:Node.js 的默认规范,用
require引入、module.exports导出,同步加载(适合服务端,因为文件读本地),但前端用不了(浏览器加载文件是异步的,同步require会阻塞页面)。 - AMD:专门为浏览器设计的异步模块化规范(代表:RequireJS),用
define定义模块、require加载,解决异步依赖问题,但语法繁琐(比如嵌套多层依赖),学习成本高。 - CMD:阿里 SeaJS 提出的折中方案,兼容同步 / 异步,语法更简洁,但生态不如 AMD,后来慢慢淡出。
- CommonJS:Node.js 的默认规范,用
3. 工程化工具适配(2015 年后,Webpack 等构建工具普及)
- 背景:CommonJS/AMD 语法不统一,浏览器原生不支持,构建工具(Webpack、Rollup)出现,解决「模块化语法兼容」问题。
- 核心逻辑:开发时用 CommonJS/ES6 Module 写代码,构建工具把所有模块打包成浏览器能识别的「全局变量形式」的单文件,既保留模块化优势,又兼容浏览器。
- 关键:Webpack 的出现让前端模块化落地成本大幅降低,成为行业标配。
4. 原生模块化支持(2017 年后,ES6 Module 普及)
- 核心:ES6 在语言层面定义了
import/export模块化规范,浏览器逐步原生支持(通过<script type="module">),Node.js 也在 v14 后全面支持。 - 变化:无需构建工具也能直接用模块化(简单项目),复杂项目仍用 Webpack/Vite 优化(按需加载、Tree-Shaking 等);模块化从「工具层面」回归「语言层面」,成为前端开发的基础。
总结
- CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
- AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
- CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重
- ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
CommonJS、AMD、CMD、UMD、ES Module 区别是什么?
| 方案 | 适用场景 | 核心特点 | 现状 |
|---|---|---|---|
| CommonJS | Node.js 服务端 | 同步、运行时加载、值拷贝 | 仍为 Node.js 主流(暂未完全被 ESM 取代) |
| AMD | 浏览器端 | 异步、依赖前置(提前加载,预先加载了所有依赖不管你是否用到) | 基本淘汰 |
| CMD | 浏览器端 | 异步、就近依赖(按需加载,用到了再引用依赖) | 已淘汰 |
| UMD | 跨环境兼容方案 | 自适应、 兼容 CommonJS/AMD/ 全局 | 已淘汰 |
| ES Module | 全场景(端 + 服务) | 静态、异步、值引用 | 现代前端 / Node.js 主流 |
AMD 与 CMD 的核心区别
AMD(异步模块定义)和 CMD(通用模块定义)是前端模块化规范,均用于解决 JavaScript 代码的依赖管理和异步加载问题。两者均支持异步加载模块,AMD 侧重预加载依赖,CMD 侧重按需加载,目前因 ES6 Module 的普及,两者使用场景已减少。
| 对比维度 | AMD(RequireJS) | CMD(SeaJS) |
|---|---|---|
| 依赖加载策略 | 依赖前置:声明模块时提前列出所有依赖,加载器先加载全部依赖再执行模块 | 就近依赖:无需提前声明,代码用到时才 require 加载(按需加载) |
| 模块执行时机 | 依赖加载完成后立即执行对应模块 | 所有依赖加载完成后,在代码执行到对应位置时才执行模块 |
| 语法风格 | 语法冗余,强调依赖前置声明 | 贴近 CommonJS,语法简洁,更符合同步书写习惯 |
| 核心设计理念 | 优先保证依赖全加载,执行顺序可控 | 按需加载,减少不必要的提前加载,更灵活 |
| 代表实现 | RequireJS | SeaJS(已停更) |
SeaJS 已停止维护,AMD/CMD 逐渐被 ES Module 取代
CommonJS与ES6 Module的区别 🌟🌟
| 对比维度 | CommonJS | ES6 Module (ESM) |
|---|---|---|
| 语法差异 | require 导入、module.exports/exports 导出 | import 导入、export 导出 |
| 加载机制 | 运行时加载(执行到 require 才加载,运行时确定依赖关系) | 编译时静态分析(编译阶段确定依赖) |
| 导出机制 | 导出「值拷贝」,浅拷贝:原始值拷贝后独立,引用类型共享地址,后续修改不影响导入方 | 导出「只读的变量映射」,导入方同步更新 |
| 树摇支持 | 无树摇(无法剔除未使用代码) | 支持树摇(静态分析实现代码优化) |
| 运行环境 | Node.js 原生支持,浏览器需打包(借助webpack等工具) | 浏览器 / Node.js 均原生支持(Node 需指定 type: module) |
| 顶层作用域 | 顶层 this 指向 module.exports | 顶层 this 为 undefined(严格模式 |
| 模块缓存 | 缓存模块,会缓存运行结果,导出的值副本 | 缓存模块本身,并且不会缓存运行结果,导出只读的变量映射,引用始终指向模块内部 |
说明文字
-
JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,在根据引用到被加载的那个模块里面去取值。ES6 模块是动态引用,并且不会缓存运行结果。
-
ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。
-
ES6 Module通过其静态特性可以进行编译过程中的优化,并且具备处理循环依赖的能力
理解UMD的概念,以及它如何使模块在不同环境下(如浏览器和Node.js)都能正常工作。
一、UMD 核心定义
UMD(Universal Module Definition)是跨环境的模块化规范,不是全新规范,而是封装了 CommonJS、AMD、浏览器全局变量等多种模块方式,让同一模块能在 Node.js、浏览器(含 AMD 加载器)、无模块加载器的全局环境下都能运行。
二、核心实现逻辑(适配不同环境)
UMD 通过环境检测判断当前运行环境,选择对应模块方式:
- Node.js 环境(CommonJS) :检测
module.exports/require,按 CommonJS 导出模块; - 浏览器 AMD 环境:检测
define.amd,按 AMD 的define方式定义模块; - 浏览器全局环境:无模块加载器时,将模块挂载到
window全局对象上。
三、简化示例(核心逻辑)
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
// Node.js/CommonJS 环境
module.exports = factory(require('依赖'));
} else if (typeof define === 'function' && define.amd) {
// 浏览器 AMD(RequireJS)
define(['依赖'], factory);
} else {
// 浏览器全局环境
root.模块名 = factory(root.依赖);
}
}(this, function (依赖) {
// 模块核心逻辑
return { /* 导出内容 */ };
}));
四、关键特点
- 兼容性:一站式适配 CommonJS、AMD、浏览器全局,无需为不同环境写多版代码;
- 无侵入:仅通过环境判断切换导出方式,不改变模块核心逻辑;
- 局限性:代码体积略增(多了环境判断),循环依赖处理需适配底层规范(如 Node 侧按 CommonJS,浏览器侧按 AMD)。
模块的动态导入:了解如何使用import()进行模块的动态加载,以及它在代码分割和懒加载中的应用
模块动态导入(import ())核心解析
1. 什么是 import ()
import() 是 ES Module 补充的动态加载语法,突破了 ESM 静态编译的限制,允许在运行时按需加载模块;它返回一个 Promise,加载成功后解析为模块对象,失败则触发 reject。
2. 基本使用方式
// 基础用法:加载单个模块
import('./utils.js')
.then((module) => {
// module 是完整模块对象,包含所有导出
console.log(module.default); // 访问默认导出
console.log(module.format); // 访问命名导出
})
.catch((err) => console.error('加载失败:', err));
// 结合 async/await(更简洁)
async function loadModule() {
try {
const utils = await import('./utils.js');
utils.doSomething();
} catch (err) {
// 处理加载错误
}
}
3. 核心应用场景
(1)代码分割(Code Splitting)
打包工具(Webpack/Rollup/Vite)会识别 import(),将动态加载的模块拆分为独立的 chunk(代码块),而非打包进主文件,减小首屏加载体积。
// 主文件 chunk:仅加载核心逻辑
// 点击按钮时才加载「编辑器模块」,拆分为独立 chunk
document.getElementById('editor-btn').addEventListener('click', async () => {
const Editor = await import('./editor.js');
Editor.init();
});
(2)懒加载(Lazy Loading)
- 路由懒加载(前端框架核心场景):Vue/React 中按需加载路由组件,只有访问对应路由时才加载组件代
- 组件 / 功能懒加载:非核心功能(如弹窗、图表、富文本)仅在用户触发操作时加载,避免首屏加载冗余代码。
(3)动态路径加载
根据运行时条件(如用户权限、环境变量)加载不同模块,适配灵活的业务场景:
// 根据环境加载不同配置
async function loadConfig(env) {
const config = await import(`./config.${env}.js`);
return config.default;
}
// 生产环境加载 prod 配置,开发环境加载 dev 配置
loadConfig(process.env.NODE_ENV);
4. 关键特点
- 异步非阻塞:加载过程不阻塞主线程,适配浏览器端性能要求;
- 兼容 ESM 特性:加载后的模块遵循 ESM 动态引用规则;
- 打包友好:被主流构建工具识别,支持 chunk 拆分和缓存优化;
- 全局可用:浏览器、Node.js 均支持(Node.js 14+ 无需额外配置)。
总结
import() 解决了 ESM 静态加载的灵活性不足问题,核心价值是按需加载—— 通过代码分割和懒加载减少首屏体积、提升加载速度,是现代前端性能优化的核心手段之一,尤其适用于大型应用的路由、组件、功能模块拆分。
模块化开发中如何处理循环依赖的问题,以及不同模块化规范下的解决方案
一、循环依赖核心
模块间互相引用,导致加载 / 执行顺序冲突,引发未定义、加载失败等问题。
二、通用思路
- 延迟引用:不在初始化阶段用依赖,在函数 / 执行阶段获取;
- 解耦拆分:抽离公共逻辑,减少交叉依赖;
- 利用各规范的模块执行特性。
三、各规范解决方案
| 规范 | 核心解决方式 |
|---|---|
| CommonJS | 延迟引用(函数内用依赖)+ 提前导出被依赖属性 |
| ESM | 导出引用型值(let/const)+ 动态 import () + 拆分模块 |
| AMD | 异步回调中延迟引用,避免初始化时调用依赖 |
| UMD | 适配底层规范(CommonJS/AMD)+ 内聚循环依赖逻辑 |
四、最佳实践
优先拆分模块从根源避免循环依赖;无法避免时,按对应规范延迟引用 / 动态加载,避开初始化阶段使用依赖。
模块化开发最佳实践
核心目标:降复杂度、提可维护性
一、核心设计原则
- 单一职责
-
定义:一个模块只负责完成一个独立的功能 / 业务逻辑,避免 “万能模块”。
-
落地:
- 按功能拆分:如将 “用户管理” 拆分为「用户数据请求」「用户信息渲染」「用户权限校验」3 个模块,而非混在一个文件中;
- 按职责分层:通用工具(utils)、业务逻辑(service)、UI 组件(components)严格分离,工具模块不掺杂业务逻辑;
- 反例:一个模块既处理接口请求,又操作 DOM,还包含常量定义,修改一处牵一发而动全身。
- 松耦合 + 高内聚
-
松耦合:模块间依赖尽可能少,且依赖通过 “接口”(导出的方法 / 变量)交互,而非直接修改对方内部状态;
- 落地:使用 “依赖注入” 替代模块内部硬编码依赖(如将请求实例传入业务模块,而非模块内直接引入 axios);通过导出纯函数(无副作用)降低模块间的状态依赖。
-
高内聚:模块内部的逻辑高度相关,无关代码不混入(如 “日期处理工具” 模块不包含字符串格式化逻辑)。
-
封闭 - 开放
- 对外暴露稳定接口,内部逻辑可修改不影响外部;新增功能扩展接口而非修改原有逻辑。
二、落地实践(从编码到工程化)
1. 模块拆分与命名规范
-
拆分维度:
- 按业务域:如电商项目拆分为「商品模块」「订单模块」「支付模块」,模块间通过公共接口交互;
- 按复用性:通用能力(如请求封装、日期工具)抽为独立模块,业务模块依赖通用模块,而非重复编写;
-
命名规范:
-
语义化命名:如
user-api.js(用户接口)、date-utils.js(日期工具),避免utils.js「func.js这类模糊命名; -
目录结构清晰:
src/ ├── utils/ // 通用工具(纯函数、无业务依赖) ├── api/ // 接口请求(仅封装请求,无业务逻辑) ├── business/ // 业务逻辑(依赖api和utils,不直接操作UI) ├── components/ // UI组件(仅负责渲染,依赖业务逻辑数据) └── config/ // 配置项(全局常量、环境变量)
-
2. 依赖管理最佳实践
-
最小化依赖:仅引入当前模块必需的依赖,避免 “全量导入”(如
import _ from 'lodash'改为import { debounce } from 'lodash'); -
避免循环依赖:
- 根源解决:拆分公共逻辑为独立模块(如 A 和 B 循环依赖,抽离公共逻辑为 C,A/B 均依赖 C);
- 应急方案:延迟引用(函数内动态导入)、依赖注入,而非在模块顶层直接引用循环依赖;
-
统一依赖版本:通过 lock 文件(package-lock.json/yarn.lock)固定依赖版本,避免不同环境依赖不一致;
-
区分依赖类型:开发依赖(devDependencies)和生产依赖(dependencies)严格区分(如 webpack、eslint 归为开发依赖)。
3. 模块导出 / 导入规范
-
导出精简:仅暴露外部需要的接口,内部变量 / 函数用
private(ES 模块)或闭包隐藏;- 反例:模块内定义 10 个函数,全部
export,外部仅需 1 个,其余均为冗余暴露;
- 反例:模块内定义 10 个函数,全部
-
导入明确:
- 避免通配符导入(
import * as utils from './utils'),改为按需导入(import { formatDate } from './utils'),提升可读性和打包体积; - 绝对路径导入:配置别名(如 webpack 的
alias),将import '../../utils'改为import '@/utils',避免路径层级混乱;
- 避免通配符导入(
-
ESM 优先:现代项目优先使用 ES 模块(import/export),而非 CommonJS(require),利用静态分析优势(如 tree-shaking、类型检查)。
4. 工程化保障
-
模块边界校验:通过 TypeScript 定义接口类型,约束模块输入 / 输出,避免类型不匹配;
-
按需加载:
- 前端:利用 ES 模块动态导入(
import())实现路由 / 组件懒加载,减少首屏体积; - 后端:Node.js 中通过
require()动态加载非核心模块(如按需加载插件);
- 前端:利用 ES 模块动态导入(
-
模块测试:每个模块独立编写单元测试,确保修改模块不影响其他功能(如 Jest 测试工具);
-
文档化:核心模块添加 README 或注释,说明模块功能、导出接口、依赖关系,降低协作成本。
三、避坑指南(常见问题与解决方案)
| 常见问题 | 解决方案 |
|---|---|
| 模块粒度过粗(万能模块) | 按单一职责拆分,拆到 “修改时无需考虑其他逻辑” |
| 模块粒度过细(碎片化) | 合并高频复用的小模块,避免 “文件爆炸” |
| 硬编码依赖 | 依赖注入 + 配置化,降低模块间耦合 |
| 循环依赖 | 拆分公共逻辑 + 延迟引用 |
| 冗余导出 / 导入 | 按需导出 / 导入 + 定期清理未使用的依赖 |
以单一职责拆分,以松耦合交互,以工程化保障,实现模块 “可理解、可复用、可测试、可替换”。
任务自动化
常见考点
-
任务自动化的概念:理解任务自动化的定义、目的和重要性。
-
npm scripts基础:
- 如何在
package.json中定义npm脚本。 - 使用
npm run <script-name>执行脚本的方法。 - npm脚本的生命周期钩子(如
pre和post)及其用法。
- 如何在
-
npm scripts进阶:
- 如何在npm脚本中使用环境变量。
- 如何通过npm脚本调用其他命令行工具或脚本。
- npm脚本的并行和串行执行策略。
-
其他任务自动化工具:
- 列举并比较Gulp、Grunt、Make等任务自动化工具的特点和适用场景。
- 了解这些工具的基本配置和使用方法。
-
任务自动化的实践:
- 如何根据项目需求选择合适的任务自动化工具。
- 如何编写和维护清晰、可复用的任务自动化脚本。
- 如何将任务自动化脚本集成到持续集成/持续部署(CI/CD)流程中。
-
性能优化:
- 如何通过任务自动化工具优化构建和部署过程。
- 如何利用缓存、并行处理等技术提高任务执行效率。
前端任务自动化全解析(核心问答版)
一、任务自动化的核心概念
1. 定义
任务自动化是通过脚本 / 工具替代人工执行重复、标准化的开发 / 部署任务(如代码编译、打包、测试、代码检查等)的过程。
2. 目的 & 重要性
- 减少人工操作,降低出错率;
- 统一团队执行标准,避免 “本地能跑、线上报错”;
- 提升研发效率,聚焦核心业务开发;
- 为 CI/CD 提供基础,支撑自动化部署。
二、npm scripts 核心用法
1. 基础使用
(1)定义脚本
在package.json的 scripts 段中声明,支持任意命令行指令:
{
"scripts": {
"dev": "vite", // 启动开发服务
"build": "vite build", // 生产打包
"test": "jest" // 执行测试
}
}
(2)执行脚本
npm run dev # 执行dev脚本
npm run build # 执行build脚本
# 简写:npm test / npm start(对应test/start脚本)
(3)生命周期钩子
脚本名以 pre/post为前缀,执行主脚本时自动触发:
{
"scripts": {
"prebuild": "eslint src", // 执行build前先做代码检查
"build": "vite build",
"postbuild": "echo '打包完成'" // build执行后输出提示
}
}
# 执行npm run build时,顺序:prebuild → build → postbuild
2. 进阶用法
(1)使用环境变量
-
方式 1:直接在脚本中拼接(跨平台需借助
cross-env):{ "scripts": { "dev": "cross-env NODE_ENV=development vite", // 跨平台设置环境变量 "build": "cross-env NODE_ENV=production vite build" } } -
方式 2:读取
.env文件(需dotenv):{ "scripts": { "dev": "node -r dotenv/config vite" } }
(2)调用其他工具 / 脚本
npm 脚本可直接调用node_modules/.bin下的工具(无需全局安装),也可执行自定义 JS/Shell 脚本:
{
"scripts": {
"lint": "eslint src --fix", // 调用eslint
"clean": "rm -rf dist", // 执行Shell命令(Windows需替换为rd/s/q dist)
"custom": "node scripts/custom.js" // 执行自定义JS脚本
}
}
(3)并行 / 串行执行
-
串行(按顺序执行):用
&&连接"scripts": { "deploy": "npm run build && scp dist/* server:/var/www" // 先打包再部署 } -
并行(同时执行):用
&(Windows 不兼容) 或npm-run-all:{ "scripts": { "dev:all": "npm-run-all --parallel dev:vite dev:mock", // 并行启动开发服务和mock "dev:vite": "vite", "dev:mock": "mock-server" } }
三、主流任务自动化工具对比
| 工具 | 核心特点 | 执行方式 | 适用场景 | 配置复杂度 |
|---|---|---|---|---|
| npm scripts | 无额外依赖,基于命令行 | 命令行指令 | 中小型项目、简单任务(打包 / 测试) | 低 |
| Gulp | 基于流(Stream),轻量 | 代码定义任务 | 前端工程化(编译 / 压缩 / 监听文件) | 中 |
| Grunt | 基于配置,插件生态丰富 | 配置文件定义 | 传统前端项目、复杂配置型任务 | 高 |
| Make | 跨平台(偏后端),依赖 Makefile | 声明式规则 | 后端 / 全栈项目、编译型任务 | 高 |
四、任务自动化实践
1. 工具选择原则
- 中小型前端项目:优先用
npm scripts(无额外依赖,维护成本低); - 复杂前端工程化(如多文件监听、流式处理):选 Gulp;
- 传统 / 后端项目:选 Make/Grunt;
- 跨团队 / 跨技术栈项目:优先标准化工具(如 npm scripts + 少量 Gulp)。
2. 脚本编写 & 维护规范
- 拆分粒度:单个脚本只做一件事(如
lint/build/deploy分离); - 复用性:抽离通用逻辑到单独 JS/Shell 脚本(如
scripts/utils.js); - 注释:复杂脚本添加说明(如环境变量含义、依赖条件);
- 跨平台:避免平台特定命令(用
cross-env/rimraf替代rm/set)。
3. 集成 CI/CD(以 GitHub Actions 为例)
将自动化脚本嵌入 CI/CD 流程,实现 “提交代码自动构建 / 测试 / 部署”:
# .github/workflows/ci.yml
name: CI/CD
on: [push]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 安装依赖
run: npm install
- name: 代码检查&打包
run: npm run lint && npm run build # 执行npm脚本
- name: 部署到服务器
run: npm run deploy # 执行自定义部署脚本
五、性能优化技巧
1. 构建 / 部署流程优化
- 增量构建:只处理变更文件(如 Gulp 的
gulp-watch、Vite 的依赖预构建缓存); - 缓存复用:缓存依赖安装结果(如 CI/CD 中缓存
node_modules)、构建产物(如 Vite 的node_modules/.vite); - 任务拆分:将耗时任务拆分为并行子任务(如
lint和test并行执行); - 资源压缩:构建阶段压缩代码 / 图片(如
terser/imagemin),减少传输耗时。
2. 执行效率提升
- 并行处理:用
npm-run-all/Gulp 的parallel并行执行无依赖的任务; - 跳过冗余步骤:通过条件判断跳过无需执行的任务(如仅变更文档时跳过 build);
- 轻量工具:替换重型工具(如用
esbuild替代babel做快速编译)。
关联面试题
前端构建工具详解
常见考点
前端构建工具是现代前端工程化体系中的核心内容之一,涉及模块打包、依赖管理、性能优化、开发体验提升等多个方向。面试中考察点通常会覆盖原理、使用、性能优化、以及工具演进。
一、基础认知类
| 考点 | 说明 |
|---|---|
| 构建工具的作用 | 为什么需要构建工具?解决哪些问题(如模块化、压缩、兼容性、自动化)? |
| 常见构建工具 | webpack、Vite、Rollup、esbuild、Parcel 各自特点与适用场景 |
| 构建流程概念 | 从源码到产物的完整流程(解析 → 转换 → 打包 → 优化) |
| 与脚手架区别 | 构建工具负责打包,脚手架(如 create-react-app、Umi)是上层封装 |
二、Webpack 方向(核心考点)
| 考点 | 说明 |
|---|---|
| 核心概念 | Entry、Output、Loader、Plugin、Module、Chunk、Bundle 区别 |
| Loader | 处理不同类型的文件,如 babel-loader、css-loader、file-loader |
| Plugin | 拓展构建流程的功能,如 HtmlWebpackPlugin、DefinePlugin、MiniCssExtractPlugin |
| HMR(热更新) | 原理、devServer 实现机制 |
| Tree Shaking | 原理(ES Module 静态分析)、副作用文件配置(sideEffects) |
| Code Splitting | 动态导入与分包策略 |
| 缓存优化 | hash、chunkhash、contenthash 区别与应用场景 |
| 性能优化 | 多进程构建(thread-loader)、持久化缓存(cache)、babel 编译优化 |
| webpack 与 Vite 的区别 | 构建速度、原理、依赖处理方式 |
三、Vite 方向(现代化考点)
| 考点 | 说明 |
|---|---|
| Vite 的核心原理 | 基于原生 ES 模块(ESM)的按需加载机制 |
| 构建阶段区别 | 开发时使用 esbuild 启动快;生产时用 Rollup 打包 |
| 热更新机制(HMR) | 基于 ESM 的模块热替换,无需重新构建全量 bundle |
| 插件系统 | Vite 插件与 Rollup 插件兼容性 |
| 环境变量 | .env 文件加载、import.meta.env 使用 |
| 常见优化方向 | 依赖预构建(Pre-Bundling)、SSR 支持、分包策略 |
四、Rollup 方向(库开发常考)
| 考点 | 说明 |
|---|---|
| 核心特性 | 面向库的打包,支持 ES、CJS、UMD 等格式 |
| 与 Webpack 区别 | 专注于打包 JS 模块,输出简洁、无多余运行时代码 |
| Tree Shaking | 更加彻底的静态分析能力 |
| 插件系统 | rollup-plugin-babel、rollup-plugin-node-resolve 等常用插件 |
| 使用场景 | 通常用于 npm 库、UI 组件库的打包方案 |
五、esbuild、swc(高性能方向)
| 考点 | 说明 |
|---|---|
| 编译速度快的原因 | 基于 Go(esbuild)或 Rust(swc)编写,利用多线程与增量编译 |
| 与 babel、webpack 的关系 | 可作为 babel-loader 替代品或底层构建加速器 |
| 在 Vite、Next.js 中的应用 | 被广泛用作底层依赖预构建或转译工具 |
六、构建性能优化方向(高级考点)
| 优化点 | 说明 |
|---|---|
| 构建体积优化 | Tree Shaking、Scope Hoisting、代码分割、压缩(Terser、ESBuild Minify) |
| 构建速度优化 | 缓存(babel-loader cacheDirectory)、多进程、预编译依赖 |
| 资源优化 | 图片压缩、字体拆分、懒加载、按需加载 |
| 开发体验优化 | HMR、SourceMap、错误提示优化 |
| CI/CD 构建加速 | 缓存 node_modules、分布式构建、增量编译 |
七、工程化与生态集成考点
| 考点 | 说明 |
|---|---|
| 与框架结合 | React、Vue、Svelte 各自生态的构建特点(CRA、Vite、Nuxt) |
| 环境变量与配置分离 | 区分 dev、test、prod 配置的方案 |
| 自动化构建 | 使用 npm scripts、gulp、vite-plugin、webpack-cli 实现构建自动化 |
| 前端监控 | sourcemap 上传(Sentry 集成) |
| 多页面应用(MPA)构建方案 | entry 配置、HTML 动态生成、资源路径管理 |
前端组件化开发
常见考点
一、组件化的认知与本质
考察重点:
- 是否理解组件化的核心目标:独立、复用、可维护、可组合
- 能否解释“组件”与“模块”的区别(UI + 状态 + 逻辑 vs 单纯逻辑或数据单元)
- 是否了解组件化发展脉络: 模板拼接时代 → Vue/React 的声明式组件 → 现代函数式组件设计
典型提问:
你如何理解前端组件化?
前端组件化是将页面拆分为独立、可复用的 “功能单元(组件)” ,每个组件封装自身的结构(HTML)、样式(CSS)、行为(JS)及状态逻辑,通过组件的组合 / 嵌套实现完整页面开发的范式
组件化开发解决了哪些传统前端开发痛点?
| 传统开发痛点 | 组件化解决方案 |
|---|---|
| 代码复用难 | 组件可跨页面复用(如通用按钮 / 弹窗),避免重复编写相同 UI / 逻辑 |
| 样式污染 | 组件封装独立样式(如 CSS Modules/Shadow DOM)、独立作用域,避免全局样式 / 变量冲突 |
| 维护成本高 | 需求变更仅需修改对应组件,无需改动整个页面代码,定位问题更精准 |
| 协作效率低 | 按组件拆分开发任务(如 A 开发表单、B 开发列表),避免代码冲突,并行开发 |
模块化(如 ES Module)与组件化有什么区别?
| 维度 | 模块化(ES Module) | 组件化 |
|---|---|---|
| 核心目标 | 解决 JS 逻辑的拆分 / 复用 | 解决 UI 单元的拆分 / 复用 |
| 封装范围 | 仅 JS 逻辑(工具函数、数据处理) | 结构 + 样式 + 逻辑 + 状态全维度 |
| 交互方式 | import/export 共享逻辑 | Props / 事件 / 插槽传递数据 / 交互 |
二、组件设计原则
考察重点:
- 单一职责原则(SRP) :一个组件只负责一件事
- 高内聚低耦合:组件内部逻辑完整,外部依赖最小化
- 可复用性与可组合性
- 通用组件 vs 业务组件 vs 页面组件 的分层设计
典型提问:
设计组件时,你如何判断逻辑是否应该拆分?
- 逻辑功能独立:若一段逻辑(如表单校验、数据格式化、状态管理)可脱离当前组件独立存在,且仅服务于某个特定功能(而非组件整体),就该拆分(比如抽成 hooks、子组件、工具函数);
- 代码体积阈值:单组件逻辑代码超过 200 行(或视觉上明显分块),且不同块无强关联,拆分;
- 复用需求:某段逻辑在多个地方被使用,必须拆分(避免复制粘贴);
- 复杂度:若逻辑包含多分支、多状态依赖,拆分后能降低单模块理解成本,就拆分。
如何避免组件间过度耦合?
-
数据通信规范化:仅通过 props/emit(Vue)、props / 回调(React)传值,不直接操作子组件实例、不共享全局变量(非必要不用 Vuex/Pinia/Redux);
-
抽象通用逻辑:把耦合的业务逻辑抽成独立 hooks / 工具类,组件只做 UI 渲染 + 调用通用逻辑;
-
避免硬编码依赖:组件配置通过 props 传入(如按钮文案、跳转链接),不写死;
你在项目中如何区分通用组件与业务组件?
- 通用组件:
- ① 无业务语义(如按钮、输入框、弹窗、表格);
- ② 高复用性,可跨项目使用;
- ③ 仅依赖基础 UI / 交互逻辑,
不绑定具体业务数据 / 规则; - ④ 通常放在
components/common/目录。 - 业务组件:
- ① 强业务语义(如订单卡片、商品列表、支付弹窗);
- ② 仅服务于特定业务场景,复用范围仅限当前项目;
- ③ 依赖业务数据模型、接口、业务规则(如订单状态判断);
- ④ 通常放在
components/business/[业务模块]/目录(如components/business/order/)。
四、组件状态与数据流
考察重点:
- 受控组件与非受控组件(React)
- 单向数据流思想
- 局部状态 vs 全局状态的划分
- 状态同步、缓存、懒加载、数据提升
典型提问:
- React 组件中,受控与非受控组件的区别是什么?
组件状态应该存在哪里?
-
仅当前组件使用:存在组件自身(Vue 的
data/ref/reactive,React 的useState); -
跨层级 / 多组件共享:用全局状态(Vue 的 Pinia/Vuex,React 的 Context+useReducer/Redux);
-
非响应式 / 临时状态:直接存在组件变量(如
let flag = false),无需响应式系统。状态存放原则:局部状态放组件内,共享状态放共同父组件,全局状态放 Context / 状态库,跨生命周期状态放外部存储 localstorage;
如何在多个组件间共享状态而不引起频繁重渲染?
- 复杂对象(如
userInfo)用shallowRef - 计算派生值(如
isAdult)用computed:缓存结果,仅依赖值变了才重新计算和渲 - 子组件接收 props 时,只取需要的字段,且关闭复杂对象的深度监听
- 拆分细粒度 store,只让组件监听「自己真正需要的状态」,不监听无关数据、不做无效计算,就能避免频繁重渲染
五、组件复用与抽象
考察重点:
- 高阶组件(HOC)
- Render Props
- 自定义 Hooks / Composition API
- Slot / children / 动态插槽
- 通用组件库设计(Button、Form、Table)
- 是否具备抽象复杂逻辑为可复用单元的能力
典型提问:
你在项目中是否写过自定义 Hook?
以 Vue 3 的 Composition API 为核心,自定义 Hook(也叫组合式函数) 本质是把复用的逻辑抽成独立函数,简单易懂的自定义 Hook 写法和思路如下:
一、核心原则
- 命名:以
use开头(如useRequest、useScroll),符合约定; - 功能:单一职责(一个 Hook 只做一件事),方便复用和维护;
- 返回:按需返回响应式数据、方法(让组件只拿需要的内容)。
二、极简示例(一步步写)
高频自定义 Hook:useRequest(Vue3)useRequest 是 Vue 项目中最常用的自定义 Hook,统一封装接口请求的通用逻辑,避免重复代码。
核心实现(精简版)
// src/hooks/useRequest.js
import { ref, onUnmounted } from 'vue'
export function useRequest(requestFn, immediate = true) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
let abortController = null
// 核心请求方法
const fetchData = async (...args) => {
loading.value = true
try {
abortController = new AbortController()
const res = await requestFn(...args, { signal: abortController.signal })
data.value = res.data
error.value = null
} catch (err) {
error.value = err
data.value = null
} finally {
loading.value = false
}
}
// 立即执行/清理副作用
immediate && fetchData()
const cancel = () => abortController?.abort()
onUnmounted(() => cancel())
return { data, loading, error, fetchData, cancel }
}
组件使用示例
<template>
<div v-if="loading">加载中</div>
<div v-else-if="error">请求失败</div>
<div v-else>{{ list }}</div>
<button @click="fetchData(2)">刷新</button>
</template>
<script setup>
import { useRequest } from '@/hooks/useRequest'
import axios from 'axios'
// 定义请求函数
const getList = (page = 1, { signal }) =>
axios.get('/api/list', { params: { page }, signal })
// 使用Hook
const { data: list, loading, error, fetchData } = useRequest(getList)
</script>
核心优势
- 复用:统一处理加载、错误、取消请求逻辑;
- 灵活:支持手动 / 自动请求、传参、取消请求;
- 安全:组件卸载自动清理,避免内存泄漏。
六、组件性能与渲染优化
考察重点:
- React:memo、useMemo、useCallback、虚拟化渲染、懒加载
- Vue:v-once、keep-alive、计算属性缓存、异步组件
- Diff 算法与重渲染机制理解
- key 的作用与渲染性能影响
- 大组件拆分、异步加载与按需渲染策略
典型提问:
- React 组件频繁重渲染的原因有哪些?
- 你如何设计高性能的长列表组件?
- Vue 的虚拟 DOM 如何帮助组件更新性能?
七、组件工程化与生态建设
考察重点:
- 组件库搭建与打包(rollup、vite、storybook)
- 按需引入与 Tree-shaking
- 样式隔离与命名规范(CSS Module、Scoped、BEM、Tailwind)
- 组件测试(Jest、Testing Library、Cypress)
- 版本管理与发布(npm publish、Monorepo 管理)
典型提问:
- 你有没有参与过组件库开发?
- 组件样式冲突如何处理?
- 如何让组件支持按需加载与主题定制?
八、可维护性与可扩展性
考察重点:
- 是否关注组件的 接口设计(props)清晰度
- 是否考虑 可测试性、可扩展性
- 是否遵循一致的命名规范与目录结构
- 是否关注团队协作下的组件复用体系(组件资产化)
典型提问:
如何设计一个长期可维护的组件?
封装组件时遵循单一职责、高内聚低耦合、参数可配置性、样式隔离、良好的错误处理和性能优化等原则,确保组件可复用、易维护、性能高并具备清晰的文档。
组件 Props 过多时该如何优化?
- 合并关联 props 为对象,利用 v-bind 批量传递
- 多层嵌套组件传大量 props(props 透传)改用 provide/inject
- 若 props 是共享状态(如全局配置、用户信息),不用通过 props 传递,直接用 Pinia/Vuex 管理,组件按需获取即可。
- 功能复杂就拆组件,把组件拆成更小的子组件,每个子组件只接收自己需要的 props,不接收无关属性。
如何让一个组件支持多主题或动态配置?
关联面试题
如果你有一个业务组件库,希望打包输出为 esm + cjs + dts,有什么思路?
如何封装一个 AI 图片生成组件,支持选择模型、输入 prompt、展示图像并支持本地保存?
单页面架构
常见考点
一、SPA 的核心概念与原理
考察重点:
-
SPA 与 MPA 的区别(单页应用 vs 多页应用)
-
SPA 的本质:前端接管路由 + 局部刷新渲染 + 状态持久
-
单页架构的优缺点:
- 优点:用户体验流畅、前后端分离、开发效率高
- 缺点:首屏加载慢、SEO 不友好、复杂状态管理
典型问题:
- 请解释一下什么是单页面应用?它和多页面应用有何区别?
- 为什么现代前端框架普遍采用 SPA 架构?
- SPA 的缺点有哪些?你在项目中如何应对?
二、前端路由机制(核心考点)
考察重点:
-
前端路由的两种实现方式:
- Hash 模式(依赖
window.onhashchange) - History 模式(依赖 HTML5 History API,如
pushState/replaceState)
- Hash 模式(依赖
-
路由与组件渲染的映射机制
-
浏览器刷新时的 404 问题与 Nginx 配置重定向
-
动态路由与懒加载机制
-
嵌套路由、路由守卫、导航守卫(beforeEach、onEnter)
典型问题:
- 你能解释一下前端路由是如何实现的吗?
- 在 React 或 Vue 中如何实现路由懒加载?
- 路由跳转时如何做权限校验?
- 为什么 History 模式在刷新时会 404?如何解决?
一、为什么 History 模式刷新会 404?
Vue/Router 的 History 模式依赖浏览器 history.pushState API 实现前端路由,URL 看起来像真实后端路径(如 /user/123),但实际是前端模拟的路由:
- 首次访问:浏览器请求域名根路径(如
www.xxx.com),后端返回 index.html,前端路由接管,匹配/user/123渲染对应组件; - 刷新页面:浏览器会直接向服务器请求
www.xxx.com/user/123,但后端没有/user/123这个物理路径 / 接口,因此返回 404。
(Hash 模式不会有这个问题,因为 # 后的内容不会传给服务器,刷新只会请求根路径)
二、解决方法(核心:让后端兜底返回 index.html)
1. 开发环境(Vue CLI/Vite)
-
Vue CLI:在
vue.config.js配置 devServer 重定向:module.exports = { devServer: { historyApiFallback: true // 所有404请求都重定向到index.html } }
2. 生产环境(后端配置)
核心逻辑:所有非接口请求都返回前端的 index.html,由前端路由匹配。
-
Nginx 配置:
location / { try_files $uri $uri/ /index.html; # 先找物理文件,找不到就返回index.html } -
Apache 配置:修改
.htaccessFallbackResource /index.html -
Node.js(Express):
const express = require('express') const app = express() app.use(express.static('dist')) // 前端打包目录 // 所有请求返回index.html app.get('*', (req, res) => { res.sendFile(__dirname + '/dist/index.html') })
核心总结
404 根源是「前端路由路径被浏览器当作后端请求」,解决关键是让后端把所有前端路由路径的请求,都兜底返回 index.html,交由前端路由处理。
三、首屏加载与性能优化
考察重点:
-
SPA 首屏性能瓶颈:JS 体积大、阻塞渲染、数据依赖多
-
优化手段:
- 代码分割、懒加载(
dynamic import) - 资源预加载(
<link rel="preload">) - 骨架屏 / loading 占位
- SSR / SSG / Prerender
- 缓存策略(localStorage、indexedDB、Service Worker)
- 代码分割、懒加载(
-
打包优化:Tree-shaking、动态 Polyfill、CDN 分发
典型问题:
- 为什么 SPA 首屏加载慢?如何优化?
- 你在项目中如何衡量并优化首屏渲染时间?
- 是否用过骨架屏?如何实现?
- SSR 与 CSR 的区别?哪种更适合你的业务?
四、状态管理与页面间数据共享
考察重点:
- 全局状态管理工具的选择与设计(Redux、Zustand、MobX、Pinia、Vuex)
- 单页多模块间数据同步
- 状态持久化(localStorage + redux-persist)
- 页面切换时的状态保留策略(keep-alive、缓存路由组件)
典型问题:
- SPA 中多个页面之间如何共享数据?
- 你如何在 React/Vue 中实现页面状态缓存?
- Redux / Vuex 为什么适合单页应用?
五、SEO 与可访问性问题
考察重点:
-
SPA 对 SEO 的影响(内容渲染在客户端)
-
解决方案:
- SSR(服务端渲染)
- 静态化预渲染(Prerender)
- 动态渲染(Dynamic Rendering)
-
是否理解 SSR 的原理与前后端同构机制
典型问题:
- 为什么单页面应用不利于 SEO?
- 你知道哪些改善 SEO 的方案?
- SSR、SSG、Prerender 各有什么适用场景?
六、页面切换与体验优化
考察重点:
- 路由切换动画实现方式(Transition / Framer Motion)
- 滚动位置还原、懒加载与预加载
- SPA 下的浏览器前进/后退行为管理
- 缓存页面状态与回退体验(如 keep-alive、React Router
useNavigate(-1))
典型问题:
- 单页应用如何实现页面切换动画?
- 如何在页面回退时保持滚动位置?
- React Router 中如何实现页面缓存?
七、工程架构与部署策略
考察重点:
- 前后端分离架构下的 SPA 部署流程
- 静态资源路径管理、Nginx 配置、路由重写
- 灰度发布、版本缓存控制
- 与后端接口的协作(API Gateway / BFF)
典型问题:
- SPA 应用如何配置 Nginx?
- 当路径直接访问子页面时如何避免 404?
- 如何在单页架构中支持国际化、多主题?
八、安全与容错
考察重点:
- 路由权限控制(前端路由守卫 + token 校验)
- 登录态维护与刷新机制(cookie / token / refresh token)
- 错误边界处理与白屏监控
- 版本更新提示与缓存失效策略(Service Worker + manifest 版本对比)
典型问题:
- SPA 如何实现路由级别的权限校验?
- 用户 token 过期时如何优雅地处理?
- 如何避免单页应用白屏问题?
九、进阶与扩展方向
考察重点:
- 微前端(Micro-Frontend) :将多个 SPA 组合成一个系统
- 多实例 SPA 与路由隔离
- 同构渲染与前后端协作模式(Next.js、Nuxt.js)
典型问题:
- 你对微前端架构的理解?
- 微前端与传统 SPA 的关系是什么?
- SSR 是如何解决 SPA 的首屏性能和 SEO 问题的?
关联面试题
为什么 SPA 应用都会提供一个 hash 路由,好处是什么?
前端模块化
babel概念及原理 🌟
babel 可以将代码转译为想要的目标代码,并且对目标环境不支持的api 自动 polyfill。而babel实现这些功能的流程是 解析(parse)-转换(transfrom)-生成(generator),接下来我们就看看每个流程都做了啥工作
-
解析:根据代码生成对应的AST结构 -
转换:遍历AST节点并生成新的AST节点:(将 AST 中的 ES6 特性转换为 ES5 兼容的代码)。 -
生成:根据新的AST生成目标代(生成对应的 ES5 代码字符串)
为什么pnpm比npm快
pnpm 用硬链+内容寻址,磁盘省 50%、装包快 2-3 倍
pnpm 比 npm 快的核心原因在于它的 存储机制和安装策略 更高效,主要体现在以下几点:
- 硬链接 + 符号链接(软链接)的存储方式pnpm 会将所有安装过的包统一存储在一个全局共享的 "store" 目录中,不同项目安装同一版本的包时,不会重复下载,而是通过 硬链接 直接复用全局存储的文件,仅用 符号链接 指向项目的
node_modules目录。而 npm 会为每个项目单独复制一份依赖文件,重复安装相同包时会浪费时间和磁盘空间。 - 避免冗余依赖安装pnpm 严格遵循 "扁平化依赖树" 的逻辑,通过符号链接构建依赖关系,不会像 npm(尤其是旧版本)那样在
node_modules中嵌套复制大量重复依赖,减少了文件复制和 I/O 操作的开销。 - 高效的依赖解析pnpm 在解析依赖时更直接,避免了 npm 中一些冗余的处理步骤(如旧版本的依赖树扁平化算法),进一步提升了速度。
简单说:pnpm 靠 "共享存储 + 链接复用" 减少了重复下载和文件复制,自然比 npm 快。
npm run start 的整个过程 🌟
以下是 npm run start 的简明执行过程:
1. 解析命令
- 查找脚本:在
package.json的scripts字段中查找start命令。{ "scripts": { "start": "react-scripts start" } }
2. 执行脚本
- 调用命令:运行
start对应的命令(如react-scripts start)。 - 环境变量注入:自动注入
NODE_ENV=development(开发环境)。
3. 启动开发服务器
- 初始化构建工具:调用 Webpack/Vite 等工具,加载配置文件。
- 编译代码:将源代码打包为浏览器可执行的文件(如 JS、CSS)。
- 启动本地服务器:通过 Express 或 Webpack Dev Server 托管静态资源。
4. 热更新(HMR)
- 监听文件变化:实时监控文件修改。
- 局部更新:通过 WebSocket 推送更新内容,浏览器局部刷新。
5. 打开浏览器
- 自动访问:默认打开
http://localhost:3000(端口可配置)。
npm install的执行过程 🌟
执行过程:
- 解析依赖
读取package.json中的dependencies和devDependencies,确定需安装的包及版本范围。 - 检查锁定文件
若存在package-lock.json,则按其中记录的精确版本安装;否则根据package.json的版本范围解析依赖树。 - 下载与缓存
优先从本地缓存(~/.npm)获取依赖包;若未命中则从 npm 仓库(或镜像)下载,并存入缓存。 - 扁平化安装
将依赖包解压至node_modules,通过依赖提升(Dedupe)减少嵌套:相同包且版本兼容时提升至顶层,冲突版本则嵌套安装。 - 生成锁文件
创建或更新package-lock.json,锁定所有依赖的确切版本及结构,确保环境一致性。
常见问题与优化
| 问题 | 原因与解决 |
|---|---|
| 安装速度慢 | 使用国内镜像(npm config set registry https://registry.npmmirror.com)或 pnpm |
node_modules 体积过大 | 使用 npm dedupe 减少冗余,或切换到 pnpm(硬链接节省空间) |
| 版本冲突(Peer Deps) | 根据警告手动调整版本,或使用 npm install --force 强制覆盖 |
| 锁文件冲突 | 禁止手动修改锁文件,始终通过 npm install 自动更新 |
总结
npm install 的核心流程是:解析依赖 → 检查锁定文件 → 下载包 → 扁平化安装 → 生成锁文件。
eslint概念以及原理 🌟
ESLint 概念
ESLint 是一个用于 JavaScript/TypeScript 的静态代码分析工具,用于检测代码中代码质量检查(如未定义变量) 和风格统一。
核心原理
1. 解析代码为 AST
ESLint 使用 Parser 将源代码转换为抽象语法树(AST)
2. 遍历 AST 并应用规则
ESLint 使用访问者模式(Visitor Pattern)遍历 AST 节点,对每个节点应用配置的规则:
// 规则示例:禁止使用 var
module.exports = {
create(context) {
return {
// 当遇到 VariableDeclaration 节点时触发
VariableDeclaration(node) {
if (node.kind === 'var') {
context.report({
node,
message: 'Unexpected var, use let or const instead.'
})
}
}
}
}
}
3. 生成并输出报告
遍历完成后,ESLint 收集所有违规信息,生成报告并输出:
/path/to/file.js
1:1 error Unexpected var, use let or const instead. no-var
5:3 warning Expected indentation of 2 spaces but found 4 indent
* 2 problems (1 error, 1 warning)
作用场景:
- 代码质量检查(如未定义变量)
- 风格统一(如引号、缩进)
- 团队协作规范强制执行。
package.json文件中的devDependdencies和dependdencies对象有什么区别
| 字段 | 核心含义 | 安装 / 打包行为 | 适用场景 |
|---|---|---|---|
dependencies | 项目运行时必需的依赖(生产环境也需要) | npm install 会安装;打包时(如 webpack)会被包含到产物中 | 业务功能核心依赖(如 React/Vue、axios、lodash、路由库、状态管理库等) |
devDependencies | 仅开发 / 构建阶段需要的依赖(生产环境不需要) | npm install 会安装;npm install --production 或生产打包时会忽略 | 构建 / 调试工具(如 webpack、babel、eslint、jest、rollup、测试库等) |
peerDependencies | 声明项目期望宿主环境提供的依赖(避免重复安装,保证版本兼容) | 不会自动安装,仅提示用户需手动安装指定版本;宿主需满足版本要求 | 插件 / 库开发(如 Vue 插件需宿主提供 Vue、ESLint 插件需宿主提供 ESLint) |
什么是CI/CD
- CI(持续集成,Continuous Integration)
开发者频繁将代码合并到主分支,自动触发构建和测试,确保代码质量。 - CD(持续交付/部署,Continuous Delivery/Deployent)
将通过测试的代码自动部署到测试或生产环境,快速交付新功能。
常见考点
-
触发:push / PR → GitHub Actions / GitLab CI / Jenkins 自动拉起流水线。
-
流程:安装依赖 → lint → test → build → 上传产物 → 部署。
-
产物:dist 包打 zip / Docker 镜像,版本号由 changesets 自动生成。
-
部署:
- 静态:CI 里
rsync/aws s3 sync到 CDN; - 容器:
docker build→ 推送仓库 → K8s 滚动更新。
- 静态:CI 里
-
回滚:保留最近 N 个版本,一键 revert;监控告警异常自动触发回滚脚本。
Mainifest 文件是什么,有什么用
Mainfest(更新清单),通常是一个 JSON 文件。需要配置 WebpackManifestPlugin 插件
在 Webpack 输出阶段生成,用于记录所有模块及其依赖关系的映射用来管理模块加载、优化浏览器缓存。 包含:
- 模块标识符: 每个模块都有一个唯一标识符,这些标识符用于在运行时查找和加载模块。
- Chunk 映射关系:包含 chunk 与包含的模块之间的映射关系,以及 chunk 之间的依赖关系。这有助于运行时确定哪些 chunk 需要被加载。
- Hash 值: 每个输出文件的 hash 值。有助于浏览器判断文件是否有更新,从而决定是加载缓存中的资源还是重新请求新的资源。
{
"main.js": "main.1a2b3c4d5e6f7g8h9i0j.js",
"vendor.js": "vendor.1a2b3c4d5e6f7g8h9i0j.js"
}
生成的 Manifest 文件可以用于以下场景:
- 服务端渲染: 在服务端渲染时,可以使用 Manifest 文件来生成正确的脚本标签,确保引用最新的资源。
- 缓存管理: 通过记录文件的哈希值,确保在文件内容变化时,客户端能够获取到最新的文件,而不是使用缓存的旧文件。
- 动态加载: 在需要按需加载模块时,可以使用 Manifest 文件来查找模块的路径。