我是2023.3.1正式开启程序员职业生涯,不知不觉,从事前端开发已经两年半了[手动狗头],最近闲来无事,想着还是要为自己平时的积累做个总结,方便认清自身从而不断鞭策自己,敬请各位批评指正!
悟已往之不谏,知来者之可追
一、技术栈汇总
首先放一张思维导图,描述下自己具体掌握了哪些技术栈。掌握个蛋,能算熟悉就不错了!
二、vue
2.1 响应式原理
2.1.1 sub和dep的关系
sub和dep依赖于link完成联结,dep是依赖,如ref,reactive;sub是订阅者,即effect函数
举个比较生动的例子:
你去ktv玩,你是sub,dep是你叫的小姐姐 你可能叫很多小姐姐, 所以你身上有个deps,里面有很多dep 小姐姐也不可能只服务于你,所以小姐姐身上有个subs,里面有很多客人 客人和小姐姐通过link链接,link上的dep属性指的就是小姐姐,sub属性指的就是客人
2.2 dom diff对比
直接放一张我自己整理的diff算法的图
三、react
三、vue和react的区别
3.1 设计思路
Vue 和 React 作为当前最流行的两大前端框架,虽然设计理念和实现细节存在差异,但在核心功能和解决的问题上有很多相似之处。以下从响应式系统、生命周期、设计模式三个维度进行对比分析:
3.1.1 响应式系统
相似点:
数据驱动视图:
两者都采用 “数据驱动视图” 的理念,通过状态变化自动更新 DOM,避免手动操作 DOM。
虚拟 DOM:
Vue 3 和 React 都使用虚拟 DOM 来优化渲染性能,通过对比新旧虚拟 DOM 的差异,最小化真实 DOM 操作单向数据流:
两者都推荐单向数据流(父组件向子组件传递数据),尽管 Vue 提供了 v-model 等双向绑定语法糖,但底层仍是单向数据流。
差异点:
-
响应式原理:
- Vue 3 使用 Proxy(Vue 2 使用 Object.defineProperty)实现自动响应式,直接修改对象属性即可触发更新。
- React 使用 不可变数据 + 显式 setState/useState,必须通过更新函数创建新对象来触发更新。
3.1.2 生命周期
相似点:
组件生命周期阶段: 两者都包含挂载、更新、卸载三个主要阶段,并且提供了对应的钩子函数。
副作用管理: 都提供了机制处理副作用(如数据获取、定时器、订阅等):
- Vue:`mounted`、`updated`、`beforeUnmount` 等钩子。
- React:`useEffect`(通过依赖数组控制执行时机,返回清理函数模拟 `componentWillUnmount`)。
差异点:
-
语法和组织方式:
- Vue:生命周期钩子是组件选项的一部分,按阶段定义(如
mounted(){...})。 - React:类组件使用
componentDidMount等方法,函数组件使用useEffect组合不同阶段的逻辑。
- Vue:生命周期钩子是组件选项的一部分,按阶段定义(如
-
自动 vs 手动:
- Vue 的生命周期钩子自动触发,无需手动管理。
- React 的
useEffect需要通过依赖数组手动控制执行时机。
3.1.3 设计模式
相似点:
-
组件化: 两者都基于组件化思想,将 UI 拆分为独立、可复用的小组件,通过组合构建复杂应用。
-
状态管理模式: 都支持单向数据流和全局状态管理:
- Vue:Vuex(官方)、Pinia(新一代状态管理库)。
- React:Redux、Context API、Zustand 等。
-
高阶组件 / 函数组合:
- React:使用高阶组件(HOC)和自定义 Hooks 复用逻辑。
- Vue:使用 mixins(Vue 2)、组合式 API(Vue 3)和自定义组合函数。
-
发布 - 订阅模式: 两者的响应式系统都基于发布 - 订阅模式:
- Vue:Dep/Watcher 实现依赖收集和通知。
- React:事件系统和状态更新机制类似发布 - 订阅。
3.1.4 总结对比表
| 维度 | Vue | React |
|---|---|---|
| 响应式原理 | 自动响应式(Proxy/Object.defineProperty) | 不可变数据 + 显式更新 |
| 虚拟 DOM | 有,内部实现 | 核心设计理念之一 |
| 生命周期 | 明确的钩子函数(mounted、updated 等) | 类组件(componentDidMount)和 Hooks(useEffect) |
| 状态管理 | Vuex、Pinia | Redux、Context API、Zustand |
| 代码复用 | 组合式 API、mixins | 自定义 Hooks、高阶组件 |
| 设计模式 | 组件化、发布 - 订阅 | 组件化、单向数据流、高阶组件 |
尽管有相似之处,两者的核心差异在于设计哲学:
- Vue:追求直观易用和低学习曲线,提供明确的 API 和模板语法,适合快速上手。
- React:强调灵活性和函数式思维,通过 Hooks 等机制让开发者自由组合逻辑,适合复杂场景和大型应用。
3.2 响应式原理
Vue 的响应式系统主要通过 Object.defineProperty ()(Vue 2.x)或 Proxy(Vue 3.x)实现,核心流程可概括为:
- 初始化时:Vue 会遍历 data 选项中的所有属性,使用 Object.defineProperty () 将这些属性转换为 getter/setter(Vue 2.x),或通过 Proxy 代理对象(Vue 3.x)。这个过程中会收集依赖—— 即记录哪些 Watcher 依赖于当前属性。
- 依赖收集:当一个组件渲染时,它会读取数据属性,触发 getter。此时 Vue 会将正在执行的渲染 Watcher 添加到该属性的依赖列表中。
- 数据变更时:当属性值被修改,会触发 setter(或 Proxy 的 set 拦截器)。Vue 会通知所有依赖于此属性的 Watcher 进行更新,这就是派发更新。
- Watcher:Watcher 是响应式系统的核心,负责订阅数据变化并执行副作用(如更新 DOM)。每个组件实例都有一个渲染 Watcher,当依赖的数据变化时,会重新渲染组件。
关键区别:
- Vue 2.x 的响应式是基于属性的,因此新增属性需要使用 Vue.set ();
- Vue 3.x 使用 Proxy,支持深层响应式和动态属性添加,无需特殊 API。
React 采用单向数据流和不可变数据的设计:
状态管理:使用useState或this.state(类组件)声明状态变量,例如:
const [name, setName] = useState('初始值');
- 更新机制:状态是不可变的,必须通过显式调用更新函数(如
setName)来触发变更。React 会创建一个新的状态对象,替换旧状态。 - 渲染触发:当状态更新时,React 会重新调用组件函数(或
render方法),生成新的虚拟 DOM,然后通过 Diff 算法对比差异,最小化真实 DOM 的操作。
区别
Vue 采用双向数据绑定和自动追踪依赖的设计:
- 更新机制:Vue 通过 Object.defineProperty ()(Vue 2)或 Proxy(Vue 3)拦截属性的 getter/setter,当属性值变化时自动触发更新,无需显式调用更新函数。
- 渲染触发:Vue 会自动追踪哪些 DOM 节点依赖于哪些数据,当数据变化时,直接更新对应的 DOM 节点。
| 特性 | React | Vue |
|---|---|---|
| 数据流动 | 单向数据流(自上而下) | 双向数据绑定(自动同步) |
| 状态变更 | 不可变数据,必须通过 setter 修改 | 可变数据,直接修改属性 |
| 依赖追踪 | 手动声明依赖(useEffect 依赖数组) | 自动追踪依赖(getter/setter 拦截) |
| 更新触发 | 显式调用更新函数(如 setState) | 隐式触发(属性值变化自动更新) |
| 虚拟 DOM | 必须(所有更新都通过 VDOM Diff) | 可选(Vue 3 的 SFC 可直接编译为 DOM 操作) |
3.2.1 响应式小demo
当你在 Vue 中使用reactive定义对象时:
javascript
const a = reactive({ name: '', age: '' });
// 更新数据
a.name = '张三'; // 自动触发页面更新
底层机制:
reactive使用 Proxy(Vue 3)拦截对象的所有属性访问。- 当读取
a.name(如在模板中)时,Vue 会记录这个依赖关系。 - 当修改
a.name时,Proxy 的set拦截器会通知所有依赖该属性的 Watcher 更新 DOM。
3.2.2 React 的等价实现流程
在 React 中,类似的功能需要显式管理状态和更新:
1. 使用 useState(基础方式)
jsx
import { useState } from 'react';
function MyComponent() {
const [a, setA] = useState({ name: '', age: '' });
const updateName = () => {
// 不可变更新:创建新对象
setA(prev => ({ ...prev, name: '张三' }));
};
return (
<div>
<p>姓名: {a.name}</p>
<button onClick={updateName}>更新姓名</button>
</div>
);
}
关键步骤:
- 使用
useState声明对象状态a。 - 更新时,通过
setA创建新对象(保留其他属性)。 - React 比较新旧状态,发现差异后重新渲染组件。
2. 使用 useReducer(复杂对象更新)
对于嵌套较深的对象,useReducer更方便:
jsx
import { useReducer } from 'react';
const initialState = { name: '', age: '' };
function reducer(state, action) {
switch (action.type) {
case 'UPDATE_NAME':
return { ...state, name: action.payload };
default:
return state;
}
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>姓名: {state.name}</p>
<button onClick={() => dispatch({ type: 'UPDATE_NAME', payload: '张三' })}>
更新姓名
</button>
</div>
);
}
优势:集中管理状态更新逻辑,避免分散的setState调用。
3. 使用 immer 简化不可变更新
手动展开对象很繁琐,可使用immer库:
import { useState } from 'react';
import produce from 'immer';
function MyComponent() {
const [a, setA] = useState({ name: '', age: '' });
const updateName = () => {
setA(
produce(draft => {
draft.name = '张三'; // 直接修改草稿对象
})
);
};
return (
<div>
<p>姓名: {a.name}</p>
<button onClick={updateName}>更新姓名</button>
</div>
);
}
原理:produce内部使用 Proxy 拦截修改,生成新对象,保持不可变性。
为什么 React 需要 生成新对象?
对于vue来说,比如定义一个对象const a=reactive({name:'',age:''}),当使用a.name=‘张三’的时候,即我其实只更新了一个对象中的某个属性,此时页面会自动更新,那对于react来说,必须一下子更新整个对象吗,那这是不是有点太鸡肋了
先明确核心区别:两种框架的 “更新触发逻辑” 不同
- Vue:基于 Proxy/Object.defineProperty 的自动追踪机制。当你修改
a.name时,Vue 的响应式系统能精准感知到 “这个具体属性被修改了”,从而只通知依赖该属性的组件部分更新。 - React:基于 状态不可变 + 显式触发更新 的机制。React 本身不追踪具体属性的变化,而是通过
setXxx函数 “标记” 状态需要更新,然后通过对比新旧状态来决定如何更新 UI。
2.2.3 React中如何更新对象的某个属性?
对于 React 中的对象状态(比如 const [user, setUser] = useState({ name: '', age: '' })),如果你只想更新 name 属性,完全可以只修改这一个属性,无需覆盖整个对象。具体做法是:创建一个新的对象,只修改需要更新的属性,其余属性保持不变,然后通过 setUser 传入这个新对象。
示例代码:
// 初始化状态
const [user, setUser] = useState({ name: '', age: 0 });
// 更新时,只修改 name 属性,其余属性保留
const handleUpdateName = () => {
// 方式1:使用对象展开运算符(推荐)
setUser({ ...user, name: '张三' });
// 方式2:使用 Object.assign
// setUser(Object.assign({}, user, { name: '张三' }));
};
此时,setUser 传入的新对象中,只有 name 被修改,age 仍然是原来的值。React 会对比新旧 user 对象(通过浅比较),发现 name 变化后,只会更新依赖 user.name 的 UI 部分,而不会重新渲染整个组件(除非组件设计不合理)。
2.2.4 为什么 React 要 “创建新对象” 而不是 “直接修改属性”?
这是因为 React 基于 “状态不可变” 的设计理念:
- React 认为 “状态应该是只读的”,修改状态时必须返回新的状态对象,而不是直接修改原对象(比如
user.name = '张三'这种直接修改在 React 中是无效的,不会触发更新)。 - 这种设计的核心是为了让 React 能通过对比新旧状态的引用快速判断 “是否需要更新”:如果新状态和旧状态的引用不同(比如新对象),就认为状态发生了变化,需要触发更新;如果引用相同,就跳过更新。
这种机制看似 “麻烦”,但带来了两个重要好处:
- 简化更新逻辑:React 不需要像 Vue 那样维护复杂的依赖追踪系统,通过 “引用对比” 就能快速判断是否需要更新。
- 可预测性:状态的每次变化都是显式的、可追溯的(通过
setXxx记录),便于调试和理解组件行为。
2.2.5 React 如何避免 “全量更新”?
浅比较机制:React 在更新状态时,只会对新旧状态进行浅比较(比较引用是否相同),而不会深遍历对象的每个属性。只要新对象的引用和旧对象不同,就会触发更新,但更新时只会重新渲染依赖变化属性的部分(通过虚拟 DOM 的 diff 算法实现)。
例如,如果你修改了 `user.name`,React 的 diff 算法会对比新旧虚拟 DOM,只更新渲染 `user.name` 的那部分 UI,而不会重新渲染 `user.age` 相关的内容。
函数式更新:如果新状态依赖旧状态,可以通过函数式更新确保获取最新状态,同时避免覆盖其他属性:
jsx
```
// 函数式更新:prev 是最新的旧状态
setUser(prev => ({ ...prev, name: '张三' }));
```
useReducer 处理复杂状态:对于更复杂的对象 / 数组状态,可以用 useReducer 集中管理更新逻辑,进一步避免手动展开对象的繁琐:
jsx
```
const reducer = (state, action) => {
switch (action.type) {
case 'UPDATE_NAME':
return { ...state, name: action.payload }; // 只更新 name
case 'UPDATE_AGE':
return { ...state, age: action.payload }; // 只更新 age
default:
return state;
}
};
// 调用时只需指定要更新的属性
dispatch({ type: 'UPDATE_NAME', payload: '张三' });
```
总结:React 的设计并不 “鸡肋”,只是理念不同
React 的状态更新机制看似 “需要手动处理对象展开”,但这是其 “不可变状态 + 显式更新” 设计的必然结果,而非 “必须全量更新”。这种设计让 React 在复杂应用中更易于维护和调试,同时通过虚拟 DOM 的 diff 算法保证了更新效率。 简单说:
-
Vue 是 “自动追踪变化,隐式更新”。
-
React 是 “显式触发更新,通过不可变保证可预测性,再通过 diff 算法优化更新范围”。
两种方式各有优劣,但 React 绝不是 “必须更新整个对象”,实际开发中完全可以高效地进行局部更新。
2.2.5 React 如何实现v-model?
在 Vue 里,
v-model主要用于实现表单元素和组件之间的双向数据绑定。它的核心原理是把属性绑定(:value)和事件监听(@input)进行了语法糖封装,下面为你详细剖析其实现机制: 原生表单元素(以 input 为例)
<!-- 以下两种写法是等价的 -->
<input v-model="message" />
<input :value="message" @input="message = $event.target.value" />
- 绑定属性:通过
:value将数据渲染到视图。 - 监听事件:通过
@input监听用户输入,并更新数据。
自定义组件
<!-- 父组件 -->
<CustomInput v-model="message" />
<!-- 等价于 -->
<CustomInput :value="message" @input="message = $event" />
<!-- 子组件(CustomInput.vue) -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script>
export default {
props: {
modelValue: String // 默认 prop 名是 modelValue
},
emits: ['update:modelValue']
}
</script>
- 接收 prop:子组件通过
modelValue接收父组件的数据。 - 触发事件:子组件通过
update:modelValue事件通知父组件更新数据。
组件中自定义 v-model
你可以通过 modelOptions 选项来自定义 prop 和事件的名称:
<!-- 父组件 -->
<CustomInput v-model:title="pageTitle" />
<!-- 子组件 -->
<template>
<input :value="title" @input="$emit('update:title', $event.target.value)" />
</template>
<script>
export default {
props: {
title: String // 自定义 prop 名
},
emits: ['update:title']
}
</script>
v-model 的修饰符
-
.lazy:将绑定更新的时机从input事件改为change事件。<input v-model.lazy="message" /> -
.number:自动将输入值转换为数字类型。<input v-model.number="age" /> -
.trim:自动过滤输入的首尾空格。<input v-model.trim="username" />
总结
-
数据流向:
- 父组件 → 子组件:通过 prop 传递数据。
- 子组件 → 父组件:通过自定义事件通知数据更新。
-
核心机制:利用了 Vue 的响应式原理和事件系统,实现了数据和视图的双向联动。
在 React 中,虽然没有像 Vue 的
v-model这样的内置指令,但可以通过受控组件和自定义 Hook 来实现类似的双向数据绑定功能。下面详细介绍其实现方式和原理:
在 React 中,“受控组件” 和 “非受控组件” 是处理表单元素状态的两种核心模式,它们的区别主要在于状态由谁来管理。下面用具体例子解释两者的差异和使用场景:
1、受控组件(Controlled Components)
核心特点:表单元素的值由 React 的 state 管理,用户输入会触发 onChange 事件更新 state,进而重新渲染表单元素。
简单说:状态由 React 控制,表单值和 state 始终保持同步。
** 示例:受控组件的输入框**
jsx
import React, { useState } from 'react';
function ControlledInput() {
// 1. 用 state 存储输入框的值
const [value, setValue] = useState('');
// 2. 定义事件处理函数:更新 state
const handleChange = (e) => {
setValue(e.target.value); // e.target.value 是输入框的当前值
};
return (
<div>
<input
type="text"
value={value} // 3. 绑定 state 到输入框的 value 属性
onChange={handleChange} // 4. 监听输入变化,触发 state 更新
/>
<p>你输入的是:{value}</p>
</div>
);
}
工作流程:
用户输入 → 触发 onChange → 调用 setValue 更新 state → 组件重新渲染 → 输入框的 value 被更新为新的 state
此时输入框的值完全由 state 控制,这就是 “受控” 的含义。
非受控组件(Uncontrolled Components)
核心特点:表单元素的值由 DOM 自身管理(类似原生 HTML),React 不通过 state 跟踪它,而是通过 ref 直接访问 DOM 元素获取值。
简单说:状态由 DOM 控制,React 只是 “查询” 它的值。
** 示例:非受控组件的输入框**
import React, { useRef } from 'react';
function UncontrolledInput() {
// 1. 创建 ref 用于访问 DOM 元素
const inputRef = useRef(null);
// 2. 点击按钮时,通过 ref 获取输入框的值
const handleClick = () => {
const value = inputRef.current.value; // 直接从 DOM 中取值
alert(`你输入的是:${value}`);
};
return (
<div>
<input
type="text"
ref={inputRef} // 3. 将 ref 绑定到输入框
defaultValue="默认值" // 非受控组件用 defaultValue 而不是 value
/>
<button onClick={handleClick}>获取值</button>
</div>
);
}
注意:
- 非受控组件不能用
value绑定(否则会变成受控组件),而是用defaultValue设置初始值。 - 复选框 / 单选框用
defaultChecked代替defaultValue。
受控组件 vs 非受控组件:关键区别
| 对比维度 | 受控组件 | 非受控组件 |
|---|---|---|
| 状态管理方 | React 的 state | DOM 自身 |
| 值的获取方式 | 直接从 state 读取 | 通过 ref 访问 DOM 元素获取 |
| 实时校验 | 支持(onChange 时即可校验) | 不支持(需手动触发获取值后校验) |
| 重置表单 | 直接修改 state 即可 | 需要手动操作 DOM(如 inputRef.current.value = '') |
| 适用场景 | 表单验证、实时反馈、复杂表单逻辑 | 简单表单、文件上传(input type="file" 只能是非受控) |
受控组件是 React 推荐的方式,尤其适合需要实时处理用户输入的场景(如表单验证、搜索联想、实时预览等),因为它能实时响应状态变化。
例如:注册表单中,输入密码时实时提示 “密码强度”,必须用受控组件在 onChange 中处理。
非受控组件更接近原生 HTML,适合简单场景(如一个单纯的搜索框、不需要实时处理的表单),代码更简洁。
特别注意:<input type="file" /> 只能是非受控组件,因为它的 value 是只读的,无法通过 React 状态修改。
2.2.6 实现方式
在 React 中,虽然没有像 Vue 的
v-model这样的内置指令,但可以通过受控组件和自定义 Hook 来实现类似的双向数据绑定功能。下面详细介绍其实现方式和原理:
- React 中的双向数据绑定基础
React 的数据流动是单向的,但可以通过组合 state 和 onChange 事件来模拟双向绑定:
- 原生表单元素的双向绑定(受控组件)
import React, { useState } from 'react';
function InputDemo() {
const [value, setValue] = useState('');
return (
<input
type="text"
value={value} // 绑定 state 到 value
onChange={(e) => setValue(e.target.value)} // 监听变化更新 state
/>
);
}
这是最基本的双向绑定实现:
- 数据流向:
state → value → DOM - 事件流向:
DOM 变化 → onChange → 更新 state
- 自定义 Hook 封装双向绑定逻辑
对于复杂场景或需要复用的情况,可以封装自定义 Hook:
import React, { useState } from 'react';
// 自定义 Hook 封装双向绑定逻辑
function useModelValue(initialValue = '') {
const [value, setValue] = useState(initialValue);
// 返回一个对象,包含 value 和 onChange 处理函数
return {
value,
onChange: (e) => setValue(e.target.value),
// 支持 .number 和 .trim 修饰符
number: {
value: Number(value) || 0,
onChange: (e) => setValue(e.target.value)
},
trim: {
value: value.trim(),
onChange: (e) => setValue(e.target.value)
}
};
}
// 使用自定义 Hook 的组件
function FormComponent() {
const name = useModelValue('');
const age = useModelValue('20');
return (
<div>
<input {...name} placeholder="姓名" />
<input type="number" {...age.number} placeholder="年龄" />
</div>
);
}
- 组件间的双向绑定(类似 Vue 自定义组件的 v-model)
在 React 中,父组件向子组件传递值并接收更新,通常通过 prop 和 callback 实现:
- 父组件
function ParentComponent() {
const [message, setMessage] = useState('');
return (
<ChildComponent
value={message}
onChange={(newValue) => setMessage(newValue)}
/>
);
}
- 子组件
function ChildComponent({ value, onChange }) {
return (
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
);
}
React 与 Vue 双向绑定的差异
| 特性 | Vue (v-model) | React |
|---|---|---|
| 语法 | v-model="value" | value={value} onChange={setValue} |
| 数据流向 | 双向(语法糖封装) | 单向(显式控制) |
| 实现方式 | 自动处理 prop 和 event | 需要手动绑定 value 和 onChange |
| 修饰符 | .lazy、.number、.trim 等 | 通过自定义逻辑实现(如上面的 Hook) |
| 组件间双向绑定 | 通过 modelOptions 自定义 prop/event | 通过 prop 和 callback 显式传递 |
第三方库简化双向绑定
如果觉得手动绑定繁琐,可以使用第三方库:
- Formik:用于复杂表单管理,支持双向绑定
- React Hook Form:轻量级表单库,通过 ref 减少重新渲染
- MobX:状态管理库,支持自动双向绑定
React 虽然没有内置的
v-model,但通过以下方式可以实现类似功能:
受控组件:组合
value和onChange实现基础双向绑定自定义 Hook:封装复用逻辑,模拟 Vue 的修饰符
组件通信:通过
prop和callback在组件间传递状态相比 Vue 的隐式双向绑定,React 的方式更加显式和可控,符合 React 的单向数据流设计理念。
2.3 生命周期
React 有类似于 Vue 的生命周期钩子。React 的生命周期分为挂载、更新、卸载三个阶段,在类组件中通过特定方法实现,在函数式组件中可通过 Hooks 来模拟相关功能。具体如下:
-
类组件中的生命周期方法:
- 挂载阶段:包括
constructor、static getDerivedStateFromProps、render和componentDidMount。其中componentDidMount与 Vue 中的mounted类似,常用于在组件挂载后执行一些操作,如发送网络请求、操作 DOM 等。 - 更新阶段:包含
static getDerivedStateFromProps、shouldComponentUpdate、render、getSnapshotBeforeUpdate和componentDidUpdate。componentDidUpdate可在组件更新后执行某些操作,比如对比更新前后的 props 或 state 来决定是否进行其他操作,类似于 Vue 中的updated钩子。 - 卸载阶段:主要是
componentWillUnmount方法,与 Vue 的beforeUnmount类似,用于在组件卸载和销毁之前执行清理操作,如清除定时器、取消网络请求等,以避免内存泄漏。 - 异常处理:有
static getDerivedStateFromError和componentDidCatch,可用于捕获组件树中的错误,类似于 Vue 中的onErrorCaptured。
- 挂载阶段:包括
-
函数式组件中通过 Hooks 模拟生命周期功能:主要是利用
useEffect钩子来模拟。当useEffect的依赖数组为空时,其回调函数仅在组件挂载后执行一次,相当于componentDidMount;若useEffect的回调函数返回一个清理函数,该清理函数会在组件卸载或依赖项变化前执行,可用于模拟componentWillUnmount;而不传入依赖数组或传入包含某些状态的依赖数组时,useEffect的回调函数会在组件挂载和相关状态更新后执行,能在一定程度上模拟componentDidUpdate的功能。
虽然 React 近年来大力推广函数式组件和 Hooks,但类组件并未被官方标记为 “过时” ,componentDidMount 等生命周期方法仍然是合法且有效的,只是在新的开发实践中,函数式组件 + Hooks 成为了更推荐的方式。
2.3.1 关于 “过时” 的误解澄清
- 类组件仍被支持:
React 官方明确表示,类组件不会被移除,目前所有类组件的生命周期方法(包括componentDidMount、componentDidUpdate、componentWillUnmount等)仍然完全可用,适用于维护旧项目或偏好类语法的场景。 - 被标记为 “过时” 的是部分不安全的生命周期:
早期 React 中,componentWillMount、componentWillReceiveProps、componentWillUpdate这三个方法因可能导致不可预测的副作用(如在挂载前请求数据、重复触发更新等),被官方标记为 “不安全”(Unsafe),并推荐用其他方法替代(如getDerivedStateFromProps、useEffect)。
而componentDidMount、componentDidUpdate、componentWillUnmount这三个核心生命周期方法从未被标记为过时,至今仍是类组件中处理副作用的标准方式。
2.3.2 为什么现在更推荐函数式组件 + useEffect?
- 更简洁的逻辑组织:
useEffect可以将 “挂载时执行 + 更新时执行 + 卸载时清理” 的逻辑整合到一个函数中,避免类组件中生命周期方法分散的问题。
例如,componentDidMount+componentWillUnmount的组合,用 Hooks 可以写成:
useEffect(() => {
// 相当于 componentDidMount:挂载时执行
const timer = setInterval(() => {}, 1000);
// 相当于 componentWillUnmount:卸载时清理
return () => clearInterval(timer);
}, []); // 空依赖数组确保只执行一次
- 更好的代码复用:Hooks 可以将组件逻辑抽离为自定义 Hooks,而类组件的生命周期逻辑难以复用。
componentDidMount本身没有过时,只是在新开发中,更推荐用函数式组件的useEffect来实现类似功能。- 类组件和生命周期方法仍然是 React 生态的一部分,适用于特定场景(如维护旧项目),不存在 “过时不可用” 的情况。
2.4 状态管理
vue一般是使用vuex pinia来管理状态,React 生态中没有官方内置的状态管理库,但社区提供了丰富的方案,可根据项目规模和复杂度选择。
2.4.1 Redux(最经典,适合大型项目)
-
核心思想:基于 “单一状态树”“不可变数据”“纯函数 reducer”,遵循严格的单向数据流。
-
特点:
- 生态成熟,有大量中间件(如
redux-thunk处理异步、redux-saga管理复杂副作用)。 - 适合多人协作的大型项目,状态变更可追踪(配合
redux-devtools调试)。
- 生态成熟,有大量中间件(如
-
缺点:模板代码较多,学习成本较高。
-
使用示例:
// 定义 reducer
const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT': return state + 1;
default: return state;
}
};
// 创建 store
const store = createStore(counterReducer);
// 组件中使用(需配合 react-redux)
function Counter() {
const count = useSelector(state => state);
const dispatch = useDispatch();
return <button onClick={() => dispatch({ type: 'INCREMENT' })}>{count}</button>;
}
2.4.2. Redux Toolkit(Redux 官方推荐的简化方案)
- 解决 Redux 模板代码冗余的问题,内置
createSlice(自动生成 action 和 reducer)、configureStore(简化 store 配置)等工具。 - 示例:
import { createSlice, configureStore } from '@reduxjs/toolkit';
// 创建 slice(自动生成 action 和 reducer)
const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: (state) => state + 1, // 内部使用 Immer 支持直接修改状态
},
});
const store = configureStore({ reducer: counterSlice.reducer });
2.4.3 Zustand(轻量简洁,适合中小型项目)
-
特点:
- 无需 Provider 包裹,直接创建 store 并在组件中调用,API 极简。
- 支持中间件(如 Redux 开发者工具)和不可变更新。
-
示例:
import create from 'zustand';
// 创建 store
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
// 组件中使用
function Counter() {
const { count, increment } = useStore();
return <button onClick={increment}>{count}</button>;
}
2.4.4 Recoil(Facebook 推出,适合 React 生态深度集成)
-
特点:
- 专为 React 设计,支持派生状态(类似 Vue 的计算属性)和异步状态。
- 状态更新粒度更细,避免不必要的重渲染。
-
示例:
import { atom, useRecoilState } from 'recoil';
// 定义原子状态
const countState = atom({ key: 'count', default: 0 });
// 组件中使用
function Counter() {
const [count, setCount] = useRecoilState(countState);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
2.4.5 MobX(响应式状态管理,类似 Vue 的 reactive)
-
特点:
- 基于 “观察者模式”,通过
observable定义响应式状态,observer包裹组件实现自动更新。 - 学习成本低,写法灵活,适合习惯 Vue 响应式的开发者。
- 基于 “观察者模式”,通过
-
示例:
jsx
import { makeAutoObservable } from 'mobx'; import { observer } from 'mobx-react-lite'; // 创建 store class CounterStore { count = 0; constructor() { makeAutoObservable(this); // 自动使状态响应式 } increment = () => this.count++; } const counterStore = new CounterStore(); // 用 observer 包裹组件,使其响应状态变化 const Counter = observer(() => ( <button onClick={counterStore.increment}>{counterStore.count}</button> ));
2.4.6 vue的状态管理(vuex/pinia)
响应式系统 + 闭包
Pinia(Vue 3 的官方状态管理库)的核心设计确实涉及闭包,但它的实现机制更复杂,主要基于 Vue 3 的响应式系统(Proxy) 和 组合式 API(Composition API) 。
Pinia 的核心原理
Pinia 的状态管理基于以下几点:
-
Vue 3 的响应式系统:
Pinia 直接使用 Vue 3 的reactive和ref创建响应式状态,依赖 Proxy 实现自动追踪属性变化。 -
闭包的作用:
在 Pinia 中,闭包主要用于隔离不同 store 的状态和实现插件机制,但不是状态响应式的核心。每个 store 是一个函数返回的对象,函数内部的变量形成闭包:import { defineStore } from 'pinia'; const useCounterStore = defineStore('counter', () => { const count = ref(0); // 闭包中的状态 const increment = () => count.value++; return { count, increment }; });
这里的 count 和 increment 被闭包捕获,确保每个组件使用 store 时获得独立的状态实例。
- 组合式 API 的设计:
Pinia 的defineStore本质上是一个组合函数,允许在 store 中使用 Vue 3 的所有组合式 API(如computed、watch)。
** 闭包在 Pinia 中的具体应用**
- 状态隔离
每个组件调用 useCounterStore() 时,Pinia 会创建一个新的 store 实例(通过闭包保存状态),确保不同组件间的状态隔离:
// 组件 A
const storeA = useCounterStore();
storeA.count++; // 只影响组件 A 的状态
// 组件 B
const storeB = useCounterStore();
console.log(storeB.count); // 仍然是 0
- 插件机制 Pinia 的插件可以通过闭包访问和修改 store 的状态:
const myPlugin = () => {
return (context) => {
// 闭包中保存插件状态
const pluginState = ref(0);
// 可以修改 store
context.store.someNewProperty = 'xxx';
return { pluginState }; // 注入到 store 中
};
};
Pinia 与闭包的关系 vs Vuex
- Pinia: 基于组合式 API,使用闭包隔离状态,更灵活、更接近 Vue 3 的响应式理念。
- Vuex(Vue 2 时代的状态管理):
基于类和模块,使用单一状态树,依赖
mutations修改状态,不依赖闭包实现核心功能。
为什么说 “Pinia 利用闭包实现状态管理” 不完全准确?
Pinia 的核心响应式能力来自 Vue 3 的 reactive 和 ref(基于 Proxy),闭包只是辅助工具,用于:
- 隔离不同 store 实例的状态。
- 实现插件的私有状态。
- 支持组合式 API 的函数式写法。 如果没有 Vue 3 的响应式系统,单纯闭包无法实现 “数据变化自动更新 UI” 的功能。
Pinia 的状态管理依赖 Vue 3 的响应式系统(Proxy) 和 闭包 的组合:
- 响应式系统:负责追踪数据变化并触发 UI 更新。
- 闭包:负责隔离状态和实现插件机制。
2.4.7 React 的状态管理
React 的状态管理原理与 Pinia 有相似之处(闭包确实会被用到),但核心机制差异较大,主要围绕 React 的状态更新机制 和 函数组件的特性 展开。以下从不同状态管理场景(内置状态、全局状态库)详细说明:
一、React 内置状态(useState/useReducer)的原理
React 组件内的状态管理(useState、useReducer)核心依赖 React 的内部调度机制,但闭包在其中扮演了关键辅助角色。
- 闭包的作用:保存状态快照
useState 的实现利用了闭包来保存状态的 “快照” ,确保每次组件渲染时能访问到当前作用域内的状态:
function Counter() {
const [count, setCount] = useState(0); // 状态被闭包保存
const handleClick = () => {
setCount(count + 1); // 访问的是当前闭包中的 count
};
return <button onClick={handleClick}>{count}</button>;
}
- 每次组件渲染时,
Counter函数会重新执行,但useState内部通过闭包将状态保存在 React 的内部纤维节点(Fiber Node)中,而非函数自身的作用域。 - 闭包在这里的作用是:让事件处理函数(如
handleClick)能访问到定义时的状态值(避免状态被每次渲染的函数作用域覆盖)。
- 核心原理:React 的状态更新与重新渲染
useState 的核心逻辑并非依赖闭包,而是:
- 状态存储:React 将状态保存在组件对应的 Fiber 节点(虚拟 DOM 的底层结构)中,而非闭包本身。
- 更新触发:调用
setCount时,React 会标记组件为 “需要更新”,并在调度机制中触发重新渲染。 - 快照机制:每次渲染时,
count是当前状态的 “快照”,闭包只是让这个快照在事件处理函数中可访问。
二、React 全局状态管理库的原理(以 Redux、Zustand 为例)
全局状态管理库的核心是状态的集中存储与订阅更新,闭包可能被用于简化 API,但并非核心原理。
- Redux:基于 “单一状态树” 和 “订阅 - 发布” 模式
-
核心原理:
- 状态存储在单一的
store对象中(非闭包,而是显式的对象)。 - 通过
dispatch(action)触发状态更新,reducer函数根据 action 计算新状态(纯函数,无闭包依赖)。 - 组件通过
useSelector订阅状态,当状态变化时自动重新渲染(依赖 React 的上下文 Context 和订阅机制)。
- 状态存储在单一的
-
闭包的作用:几乎不用闭包,更依赖纯函数和显式的状态流转。
- Zustand:轻量全局状态,依赖闭包简化 API
Zustand 是更简洁的状态库,大量使用闭包来简化状态定义和访问:
import { create } from 'zustand';
// 用闭包定义状态和方法
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
// 组件中使用(闭包确保访问到最新状态)
function Counter() {
const { count, increment } = useStore();
return <button onClick={increment}>{count}</button>;
}
- 闭包的核心作用:
create函数返回的useStore是一个闭包,内部保存了全局状态的引用。每次调用useStore时,闭包确保组件能访问到最新的状态,且状态在所有组件间共享。 - 但本质仍是状态共享:闭包只是封装了状态的访问方式,底层状态仍是一个全局共享的对象(类似单例)。
- Recoil/Jotai:原子化状态,依赖上下文和订阅
这类库将状态拆分为 “原子(atom)”,核心原理是:
- 每个原子是独立的状态单元,通过 React Context 传递。
- 组件订阅原子时,状态变化会触发组件更新(依赖订阅机制,类似 Redux)。
- 闭包的使用较少,更多依赖 Context 和内部的状态追踪逻辑。
三、React 状态管理与闭包的关系总结 内置状态(useState) :
- 核心是 React 的内部状态调度(Fiber 节点存储状态)。
- 闭包用于保存状态快照,确保事件处理函数能访问到定义时的状态。
全局状态库:
- **Redux 类**:几乎不依赖闭包,依赖纯函数、单一状态树和订阅机制。
- **Zustand 类**:大量使用闭包简化 API,封装全局状态的访问和更新。
- **Recoil 类**:依赖 Context 和原子订阅,闭包作用有限。
与 Pinia 的区别:
- Pinia 依赖 Vue 的响应式系统(Proxy)实现自动更新,闭包仅用于隔离状态。
- React 没有内置响应式系统,状态更新依赖显式的 `setState` 或 `dispatch`,闭包更多用于简化状态的保存和访问(而非响应式)。
React 的状态管理并非核心依赖闭包,但闭包在部分场景(如
useState的状态快照、Zustand 的 API 设计)中是重要的辅助工具。其核心原理是:
- 组件内状态:React 内部的状态调度和 Fiber 节点存储。
- 全局状态:通过单一存储对象、订阅机制或 Context 实现跨组件共享,闭包可用于简化 API 但非必需。
2.4.8 选择建议
| 项目规模 | 推荐方案 |
|---|---|
| 小型项目 / 简单状态 | Context API + useReducer 或 Zustand(轻量、无额外依赖) |
| 中型项目 | Redux Toolkit(平衡功能与简洁性)或 Recoil(React 深度集成) |
| 大型项目 / 复杂状态 | Redux Toolkit(生态完善、可追踪性强)或 MobX(灵活、适合复杂业务逻辑) |
| 偏好响应式语法 | MobX(类似 Vue 的 reactive 体验) |
三、前端工程化
3.1 序言
3.1.1 背景
在前端工程化概念普及之前,前端开发主要以手工编写 HTML、CSS 和 JavaScript为主,开发模式具有以下特点:
- 文件结构简单
通常按照静态资源类型组织目录(如css、js、images),项目规模较小时可以满足需求,但缺乏明确的模块化划分。 - 代码直接运行
开发完成后直接在浏览器中打开 HTML 文件测试,没有复杂的构建流程。JavaScript 代码往往全局作用域污染严重,依赖管理混乱。 - 依赖手动管理
通过<script>和<link>标签引入外部资源,需要手动处理依赖顺序,容易出现 "callback hell" 或资源加载错误。 - 兼容性处理复杂
针对不同浏览器编写大量 hack 代码,测试和修复成本高。 - 部署流程原始
通常通过 FTP 手动上传文件到服务器,缺乏自动化测试和版本控制机制。 前端工程化是将软件工程的原理和方法应用于前端开发领域,通过工具链、自动化流程和规范化方法,解决前端开发中面临的复杂度、协作效率、可维护性等问题。它不是某一个工具或技术,而是一套涵盖代码编写、测试、构建、部署等全生命周期的解决方案。
示例(工程化前的典型项目结构) :
project/
├── index.html
├── css/
│ ├── style.css
│ └── reset.css
├── js/
│ ├── utils.js
│ ├── main.js
│ └── jquery.min.js
└── images/
└── logo.png
3.1.2 工程化前的模块化实践(黑暗时代)
在 ES6 Modules、CommonJS 或 AMD 规范出现前(2009 年以前),JavaScript 没有官方模块化机制,开发者主要通过以下方式模拟:
1.全局变量法(命名空间模式)
将所有功能挂载到一个全局对象上:
// utils.js
var MyUtils = {
version: '1.0',
add: function(a, b) { return a + b; }
};
// main.js
console.log(MyUtils.add(1, 2)); // 3
问题:
- 仍存在全局变量(
MyUtils) - 依赖关系不明确(需手动确保 utils.js 在 main.js 前加载)
2.立即执行函数表达式(IIFE)
通过闭包隔离作用域:
// math.js
var MathModule = (function() {
const privateValue = 10; // 私有变量
const add = (a, b) => a + b;
return { add }; // 只暴露公共接口
})();
// main.js
console.log(MathModule.add(1, 2)); // 3
问题:
- 依赖管理仍需手动处理
- 代码嵌套层级深,可读性差
3.文件级依赖(灾难级)
通过 <script> 标签按顺序引入多个 JS 文件:
<!-- index.html -->
<script src="lib/jquery.js"></script>
<script src="lib/underscore.js"></script>
<script src="utils.js"></script>
<script src="app.js"></script>
问题:
- 依赖顺序错误会导致运行时错误
- 全局变量污染严重
- 维护成本高(新增 / 删除依赖需手动调整所有 HTML 文件)
也就是说传统的前端项目中,在a.js和b.js中分别定义一个变量 name,在html中后引入的会把先前引入的覆盖,因为在传统前端项目(没有使用模块化规范)中,如果你在不同的 JavaScript 文件中定义同名的全局变量,后加载的文件会覆盖先加载的文件中的变量。这是因为所有变量都挂载在全局作用域(浏览器中是
window对象)下。
3.1.3 工程化带来的变革
工程化通过引入系统化的工具和流程,解决了传统开发模式的痛点。 1. 模块化开发
- JavaScript 模块化:通过 ES6 Modules、CommonJS 或 AMD 规范,将代码分割为独立的模块,解决命名冲突和依赖管理问题。
(示例:使用
import/export替代全局变量) - CSS 模块化:通过 CSS Modules、CSS-in-JS 等方案,实现样式的局部作用域,避免全局污染。
2. 自动化构建工具
- 打包工具(Webpack、Rollup、Vite):将分散的模块打包成浏览器可直接运行的文件,支持代码分割、懒加载。
- 编译工具(Babel、TypeScript):将高级语法(如 ES6+、TS)转换为兼容性更好的代码。
- 样式处理(Sass/Less、PostCSS):支持变量、嵌套、自动前缀等增强功能。
总结:工程化的核心价值:
- 提高效率:自动化工具减少重复劳动,热更新加速开发流程
- 增强可维护性:模块化和规范使代码结构清晰,降低维护成本
- 保障质量:自动化测试和代码检查提前发现问题
- 支持大规模协作:多人团队开发时避免冲突,统一技术栈和规范
- 优化用户体验:更小的包体积、更快的加载速度
project/
├── src/
│ ├── components/
│ ├── pages/
│ ├── styles/
│ ├── utils/
│ └── index.js
├── public/
│ └── index.html
├── package.json
├── webpack.config.js
├── .babelrc
├── .eslintrc
└── .gitignore
3.1.4 工程化的前置知识
3.1.4.1 nodejs是什么?
- JavaScript 由 Brendan Eich 为 Netscape 浏览器开发,最初的用途是增强网页交互性
- 典型场景:表单验证、动态效果(如弹窗、菜单)、DOM 操作
- 运行环境:完全依赖浏览器,代码通过
<script>标签嵌入 HTML
浏览器环境的限制
- 无法访问底层系统:不能直接读写文件、操作网络端口、创建进程
- 沙箱安全机制:代码被限制在当前网页的上下文,无法跨域访问资源
- 单线程执行:长时间运算会导致页面卡顿(如大型循环)
- Node.js出现使JS可以脱离浏览器的限制,是一个基于 Chrome V8 引擎的 JavaScript 运行环境,让 JS 可以在服务器端运行可访问文件系统、网络等底层 API;
- 提供包管理生态系统(npm)【官网(nodejs.org)下载安装包,会同时安装 Node.js 和 npm】
3.1.4.2 npm是什么?
-
定义:Node.js 的包管理工具,用于安装、分享和管理代码包
-
核心功能:
- 包安装与版本管理
- 依赖自动解析
- 脚本执行(如启动开发服务器、构建项目)
-
与 Node.js 的关系:
- 随 Node.js 一起安装(版本对应关系:Node 14.x → npm 6.x,Node 16.x+ → npm 7.x+)
-
npm init:
- 命令:
npm init(或带默认值的npm init -y) - 作用:创建
package.json文件,记录项目元信息和依赖配置
- 命令:
3.1.4.3 npm init及npm run xxx都做了什么?
我们首先要搞清楚npm run xx和node xx.js的关系,直接用
node xx.js运行 JS 文件,和通过npm run xx运行,本质上都是执行同一个文件,只是后者需要先在package.json中定义脚本。两者的执行效果一致,但npm run提供了更灵活的配置方式(比如添加参数、环境变量等)。
1. node xx.js步骤
-
查找文件:Node.js 首先在当前目录下查找
xx.js文件 -
加载模块:找到文件后,Node.js 会加载该文件作为模块
-
编译执行:
- 对文件内容进行语法检查
- 将 JavaScript 代码编译为字节码
- 执行编译后的代码
-
事件循环:如果代码中有异步操作,进入 Node.js 事件循环
-
进程退出:当所有同步代码执行完毕且事件循环中没有待处理任务时,进程退出
2. npm run xx步骤
-
查找 package.json:npm 首先在当前目录及其父目录中查找 package.json 文件
-
解析 scripts 字段:找到 package.json 后,npm 查看其中的
scripts字段 -
查找匹配命令:寻找与
xx匹配的脚本命令 -
创建子进程:npm 创建一个子 shell 来执行该命令
-
环境变量注入:npm 会将 node_modules/.bin 添加到 PATH 环境变量中
-
执行命令:
- 如果是直接命令(如
node xx.js),则执行该命令 - 如果是复杂命令,会按 shell 语法解析执行
- 如果是直接命令(如
-
返回结果:命令执行完毕后,将结果返回给主进程
具体实现步骤:假设你要运行的文件是 app.js,可以在 scripts 中添加一个自定义脚本(比如叫 start 或 run-app):
{
"scripts": {
"start": "node app.js", // 直接执行 app.js
"dev": "node app.js --mode dev" // 带参数执行
}
}
- 运行
npm run start→ 等同于直接在终端执行node app.js - 运行
npm run dev→ 等价于直接执行node app.js --mode dev
两者的主要区别如下:
| 方面 | node xx.js | npm run xx |
|---|---|---|
| 执行环境 | 直接由 Node.js 执行 | 通过 npm 创建子 shell 执行 |
| PATH 设置 | 系统默认 PATH | 包含 node_modules/.bin 的 PATH |
| 错误处理 | 直接显示 Node.js 错误 | 通过 npm 包装显示错误 |
| 适用场景 | 直接执行 JS 文件 | 执行 package.json 中定义的脚本 |
| 预处理 | 无 | 可以执行复杂的预处理命令 |
3.1.4.5 what is Rollup?
1.Rollup 是 Node.js 工具
- Rollup 本身用 JavaScript 编写,必须在 Node.js 环境 中运行(类似 Webpack)
- 其 API 和配置文件(
rollup.config.js)完全基于 Node.js
// rollup.config.js
import { nodeResolve } from '@rollup/plugin-node-resolve'; // Node.js 模块
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'es'
},
plugins: [nodeResolve()] // 使用 Node.js API 解析模块
};
2.依赖 Node.js API
- 文件操作(
fs):读取 / 写入打包文件 - 路径处理(
path):解析模块路径 - 模块系统(CommonJS):Rollup 配置文件使用
import/export,但依赖 Node.js 的require加载插件
3.为什么需要两个Rollup和webpack两个工具?
| 场景 | 选择 Webpack | 选择 Rollup |
|---|---|---|
| 构建 React 应用 | ✅ | ❌ |
| 开发 UI 组件库 | ✅(用 Webpack 构建示例) | ✅(用 Rollup 打包组件) |
| 打包工具函数库 | ❌ | ✅ |
以下是 Node.js 及其生态中主流前端工具的关系图谱,清晰展示底层依赖、工具定位及关联关系:
┌─────────────────────────────────────────────────────────────────────┐
│ 底层基础环境 │
│ │
│ ┌─────────────────┐ ┌─────────────────────────────────────┐ │
│ │ Chrome V8 │ │ 操作系统 │ │
│ │ (JS 引擎) │◄──────┤ (提供文件系统、网络等系统调用) │ │
│ └────────┬────────┘ └─────────────────────────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Node.js │ (基于 V8 引擎,封装系统 API,提供 JS 运行环境) │
│ └────────┬────────┘ │
└───────────┼─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 包管理与工程化基础设施 │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ npm │ │ yarn/pnpm │ │ node_modules │ │
│ │ (包管理) │ │ (替代包管理) │ │ (依赖包存储目录) │ │
│ └──────────────┘ └──────────────┘ └──────────────────────────┘ │
└────────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 核心构建工具(基于 Node.js) │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌──────────────────────┐ │
│ │ Webpack │ │ Rollup │ │ esbuild │ │
│ │ (全能打包) │ │ (库打包) │ │ (Go 写的极速打包) │ │
│ └────────┬───────┘ └────────┬───────┘ └───────────┬──────────┘ │
│ │ │ │ │
│ └───────────┬───────┴──────────────┬───────┘ │
│ │ │ │
│ ┌────────────────────▼──────────────────────▼──────────────────┐ │
│ │ Vite (开发工具) │ │
│ │ (开发时用 esbuild 预构建,生产环境用 Rollup 打包) │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
▲ ▲ ▲
│ │ │
┌───────────┴──────────┐ ┌───────┴────────────┐ ┌────┴───────────────┐
│ 框架集成 │ │ 辅助工具 │ │ 应用场景 │
│ (React/Vue/Angular) │ │ (Babel/ESLint等) │ │ (项目构建/库开发) │
└──────────────────────┘ └────────────────────┘ └────────────────────┘
图谱关系说明:
-
最底层:V8 引擎 + 操作系统
- Chrome V8 是 JavaScript 解释器,负责执行 JS 代码。
- 操作系统提供文件读写、网络等底层能力,Node.js 封装这些能力为 JS API。
-
核心层:Node.js
-
所有工具(Webpack/Rollup/Vite)的运行依赖,提供:
- 文件系统操作(
fs模块) - 路径处理(
path模块) - 模块系统(CommonJS/ES Modules)
- 文件系统操作(
-
没有 Node.js,这些工具都无法运行。
-
-
包管理层:npm/yarn/pnpm
- 负责下载工具(如
npm install webpack),并将其存储在node_modules中。 - 工具的执行依赖包管理器解析路径(如
npx webpack本质是调用node_modules/.bin/webpack)。
- 负责下载工具(如
-
构建工具层:Webpack/Rollup/esbuild
- Webpack:全能型打包工具,支持各种资源(JS/CSS/ 图片),适合复杂应用。
- Rollup:专注 JS 库打包,输出纯净代码,适合发布 npm 包。
- esbuild:用 Go 语言编写,速度极快,作为预构建工具(如 Vite 开发时使用)。
-
上层工具:Vite
-
不是从零实现打包,而是整合底层工具:
- 开发环境:用 esbuild 预构建依赖(快)。
- 生产环境:用 Rollup 打包(兼容好)。
-
依赖 Node.js 提供的服务能力(如开发服务器)。
-
-
外围生态
- 框架(React/Vue)通过工具链构建最终产物。
- 辅助工具(Babel 转译、ESLint 检查)集成到构建流程中,依赖 Node.js 运行。
核心依赖链:
操作系统 → V8 引擎 → Node.js → 包管理器(npm) → 构建工具(Webpack/Rollup/esbuild) → 上层工具(Vite)
所有工具最终都依赖 Node.js 提供的运行环境,而 Node.js 又依赖 V8 引擎和操作系统的底层能力。
3.1.5 总结
-
Node.js 以 V8 引擎为核心,将其从浏览器环境中剥离,结合自身封装的系统 API(如文件操作、网络通信),使 JavaScript 能脱离浏览器运行在服务器 / 本地环境中。
-
Node.js 的核心能力是运行 JavaScript 代码,而 npm 作为配套工具,用于管理项目依赖(通过
npm init创建package.json记录依赖配置,npm install下载第三方库)。这些第三方库需在 Node.js 环境中运行(如通过require引入)。 -
Node.js 通过内置的模块化系统(
require/module.exports)实现文件隔离(每个 JS 文件是独立模块,变量默认局部作用域),减少全局污染;而 Webpack 等工具基于模块化规范,将多个模块打包为少量文件(便于浏览器加载),打包过程依赖 Node.js 环境运行,但打包本身是工具的功能。 -
node xx.js是 Node.js 运行 JS 文件的基础命令,支持通过模块化加载多个文件;npm run <脚本名>用于执行预设命令(如npm run build调用 Webpack),借助工具链实现多文件的打包、转换等复杂操作。
更通俗易懂的解释
1. Node.js 是怎么让 JS 离开浏览器的?
- V8 引擎:就像是 JS 代码的 “翻译官”,能把我们写的 JS 代码变成电脑能听懂的指令。原来这个 “翻译官” 只能在浏览器里工作(比如 Chrome)。
- Node.js:把这个 “翻译官”(V8 引擎)挖过来,开了个 “新办公室”,还配了一堆浏览器没有的 “工具”(比如读写文件、联网的工具),让 JS 代码不用待在浏览器里,也能干活了。
2. Node.js 和 npm 是啥关系?
- Node.js:就像是一台 “JS 电脑”,能运行 JS 代码。
- npm:就像是一个 “超市”,里面有各种各样别人写好的 JS 工具(比如 Webpack、React)。你可以用
npm install把这些工具 “买回家”(下载到项目里),用npm init列个 “购物清单”(生成package.json记录买了啥)。
3. Node.js 的模块化和 Webpack 打包是怎么回事?
- Node.js 的模块化:
就像一个 “文件隔离区”,每个 JS 文件都是独立的小房间,里面的变量和函数不会随便跑出来影响别人(除非你主动用module.exports把它们 “放出来”,再用require拿到其他文件里)。 - Webpack 打包:
就像一个 “快递打包员”。如果你的项目有很多 JS 文件(比如 100 个),浏览器一次加载这么多文件会很慢。Webpack 会把这些文件 “打包” 成 1 - 2 个大文件(就像把很多小包裹捆成一个大包裹),这样浏览器加载就快多了。
4. node xx.js 和 npm run 有啥区别?
node xx.js:
就像 “直接启动一个 JS 程序”。比如你写了个app.js文件,里面有require('./utils.js')加载其他文件,用node app.js就能直接运行这个程序(Node.js 会自动找到并加载所有依赖的文件)。npm run:
就像 “执行一个预设好的命令”。比如你在package.json里写了"build": "webpack",当你运行npm run build时,就相当于自动执行webpack命令,让 Webpack 帮你打包文件。
3.2 Rollup
3.2.1 背景
Rollup 为啥会出现?—— 简单说,就是为了 “更纯粹的打包”。
早期 Webpack 为了兼容 CommonJS(Node.js 的模块规范),打包时会加入很多适配代码。而 Rollup 从一开始就只认 ES6 的 import/export,所以打包出来的代码几乎没有多余的 “胶水代码”(比如 Webpack 里的 __webpack_require__ 这种辅助函数),非常简洁。
如果你写了两个 ES6 模块:
// math.js
export const add = (a, b) => a + b;
// index.js
import { add } from './math.js';
export const sum = add(1, 2);
Rollup 打包后可能直接是:几乎和手写的一样干净,没有多余代码。
const add = (a, b) => a + b;
const sum = add(1, 2);
export { sum };
Webpack 更适合打包 “应用”(比如一个完整的网站),因为它能处理图片、CSS 等各种资源,还能兼容复杂的依赖关系。
而 Rollup 更适合打包 “库”(比如一个工具函数库、UI 组件库),因为它输出的代码简洁、体积小,而且默认支持输出 ES6 模块格式(方便其他工具进一步处理)。
像 Vue、React、D3 这些知名库,早期都是用 Rollup 打包的。
3.2.2 和其他工具的差别
我们拿Rollup和webpack对比,这里就不带vite玩了,因为webpack和vite原理差不多
| 维度 | Rollup | Webpack |
|---|---|---|
| 诞生背景 | 为 JavaScript 库开发 而生(2012 年) | 为 大型应用 构建而生(2012 年) |
| 核心优势 | 打包后的代码 简洁、体积小 | 功能全面,支持各种资源和复杂场景 |
| 模块处理 | 仅支持 ES6 模块(ESM) | 兼容 所有模块规范(CommonJS、AMD、ESM) |
| 输出格式 | 专注于生成 库格式(ES、UMD、CJS) | 专注于生成 应用格式(浏览器直接加载) |
| 资源处理 | 原生仅处理 JS,需插件支持其他资源 | 原生支持 一切资源(CSS、图片、字体等) |
| 代码分割 | 基础支持(需插件增强) | 全面支持(动态导入、懒加载) |
| 构建速度 | 快(仅处理 JS,逻辑简单) | 慢(处理各种资源,依赖分析复杂) |
| 典型场景 | 开发库(如 React、Vue 早期用 Rollup) | 开发应用(如 React/Vue 项目) |
为什么会有这些差异?—— 设计目标决定的
1.Rollup:专注库开发,追求极致简洁
- 场景:开发一个工具库(比如
lodash),希望打包后代码干净、体积小,且保留 ES6 模块格式(方便用户按需引入)。 - 怎么做:
- 只处理 JS(其他资源交给用户自己处理)。
- 利用 ES6 模块的静态分析能力,直接删除未使用的代码(Tree Shaking)。
- 输出格式纯净,几乎没有多余的 “运行时代码”。
2.Webpack:全能选手,适合复杂应用
- 场景:开发一个大型网站(比如淘宝),需要处理 CSS、图片、懒加载、多页面等复杂需求。
- 怎么做:
- 通过 Loader 支持一切资源类型(CSS 可以
import,图片可以require)。 - 通过 插件 实现各种高级功能(如自动生成 HTML、分割代码、热更新)。
- 牺牲一些体积,换取更全面的兼容性和功能。
- 通过 Loader 支持一切资源类型(CSS 可以
3.3 webpack
3.3.1 序言
推荐几篇关于webpack的优质文章
看完这篇还搞不懂webpack,求你打我webpack是一个打包工具,他的宗旨是一切静态资源皆可打包。有人就会问为什么要 - 掘金
🔥【万字】透过分析 webpack 面试题,构建 webpack5.x 知识体系1. 基础 -- 会配置 2. 进阶 - 掘金
一文带你读懂webpack的知识和原理,附带常见面试题!在前端技术发展日新月异的今天,前端工程化已经成了每个前端工作/学 - 掘金
以前写网页,直接在 HTML 里用<script>标签引入 JS 文件:
1. JS
<!-- index.html -->
<script src="utils/math.js"></script>
<script src="utils/format.js"></script>
<script src="components/header.js"></script>
<script src="components/footer.js"></script>
<script src="pages/home.js"></script>
问题:
- 加载顺序混乱:如果
home.js依赖math.js,但你把home.js放在前面,就会报错。 - 全局变量污染:所有 JS 文件共享一个全局作用域,变量名容易冲突(比如两个文件都有
let count = 0)。 - HTTP 请求过多:每个 JS 文件都要单独请求,浏览器加载慢。
2. CSS
直接用<link>引入,图片直接写死路径:
<link rel="stylesheet" href="style/main.css">
<img src="images/logo.png" alt="Logo">
问题:
- 路径管理麻烦:如果图片文件夹改名,所有引用路径都要改。
- 样式冲突:不同组件的 CSS 类名可能重复,导致样式错乱。
- 无法优化资源:图片不会自动压缩,CSS 不会自动合并。 3. 模块化
后来出现了CommonJS(Node.js 用的模块规范)和AMD(RequireJS 实现的异步模块规范):
// math.js
exports.add = (a, b) => a + b;
// home.js
const math = require('./utils/math.js');
console.log(math.add(1, 2));
问题:
- CommonJS 只能在 Node 环境运行,浏览器不支持(因为它是同步加载模块)。
Webpack 出现后
1. 单一化
Webpack 把所有 JS 模块打包成一个或多个文件
// webpack.config.js
module.exports = {
entry: './src/index.js', // 入口文件
output: {
filename: 'bundle.js', // 输出文件名
path: path.resolve(__dirname, 'dist')
}
};
好处:
- 只需引入一个 JS 文件:
- 自动处理依赖关系:Webpack 会分析
index.js引用了哪些模块,然后把它们都打包进去。 - 支持各种模块规范:CommonJS、ES Modules(import/export)都能混用。
2. 处理CSS
Webpack 用 Loader 处理非 JS 资源,比如 CSS 和图片:
module.exports = {
module: {
rules: [
{
test: /.css$/,
use: ['style-loader', 'css-loader']
}
]
}
};
现在你可以这样写 CSS:
// index.js
import './style/main.css'; // 直接在JS里引入CSS
3. 处理Img
module.exports = {
module: {
rules: [
{
test: /.(png|jpg|gif)$/,
use: ['file-loader']
}
]
}
};
现在你可以这样用图片:
// home.js
import logo from '../images/logo.png';
document.querySelector('img').src = logo;
4.代码分割
Webpack 支持把代码拆分成多个小块,按需加载:这样可以不用加载初期就加载所有js文件,提升网页渲染速度
// 当用户点击按钮时,才加载购物车模块
document.getElementById('cart-button').addEventListener('click', () => {
import('./cart.js').then(cart => {
cart.show();
});
});
5.插件集成
Webpack 的插件可以做各种高级优化:如自动生成html
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
};
因此我们可以总结如下:
| 场景 | 没有 Webpack | 有 Webpack |
|---|---|---|
| JS 文件管理 | 手动维护<script>顺序,容易出错 | 自动分析依赖,打包成一个或多个文件 |
| CSS 处理 | 全局样式,容易冲突 | 模块化 CSS(如 CSS Modules),自动提取或内联 |
| 图片管理 | 手动处理路径,无法自动优化 | 自动处理路径,支持压缩、Base64 内联等优化 |
| 模块规范 | 浏览器不支持 CommonJS,需用复杂的 AMD | 支持 CommonJS、ES Modules 等各种规范 |
| 代码分割 | 无法实现,所有代码必须一次性加载 | 支持按需加载,提升首屏速度 |
| 构建流程 | 手动压缩代码、合并文件、处理版本号 | 自动化构建,插件系统灵活扩展 |
3.3.2 大白话说webpack
一、初始化项目(npm init)
场景:你想盖一栋楼(开发一个项目),首先要画一张「地基图纸」(项目配置文件)。
命令:
npm init -y
发生了什么:
-
生成 package.json: 这就像给你的楼办了个「出生证明」,记录了项目的基本信息(名称、版本、作者等)。
{ "name": "my-project", "version": "1.0.0", "scripts": {}, // 这里是你以后写「施工指令」的地方 "dependencies": {}, // 项目运行时需要的「材料」 "devDependencies": {} // 开发时需要的「工具」 } -
-y 参数: 相当于你跟 npm 说:「别问我一堆问题了(比如项目描述、作者),直接用默认值生成!」
二、安装 Webpack(npm install webpack)
场景:你需要买一堆「建筑工具」(Webpack 及其周边工具)。
命令:
npm install webpack webpack-cli --save-dev
发生了什么:
-
下载工具到仓库:
npm 把 Webpack 和 Webpack CLI 下载到你项目的node_modules文件夹(相当于你的「工地仓库」)。 -
记录工具清单:
在package.json的devDependencies里添加记录:{ "devDependencies": { "webpack": "^5.88.2", "webpack-cli": "^5.1.4" } }这就像在你的「材料清单」上写了:「我买了 Webpack 牌搅拌机(版本 5.88.2)和 Webpack CLI 牌铲子(版本 5.1.4)」。
三、创建 Webpack 配置文件(webpack.config.js)
场景:你要写一份「施工方案」,告诉工人(Webpack)怎么盖楼。
在操作:项目根目录创建webpack.config.js,内容可能长这样:
const path = require('path');
module.exports = {
entry: './src/index.js', // 从哪个文件开始「拆楼」(分析依赖)
output: {
path: path.resolve(__dirname, 'dist'), // 把「新楼」盖在哪里
filename: 'bundle.js' // 「新楼」的名字叫啥
},
mode: 'development' // 施工模式:开发模式(不压缩代码)
};
四、配置 package.json 脚本(npm run build 的前身)
场景:你要在「地基图纸」上写一条「施工指令」,告诉工人什么时候用什么工具干活。
操作:
在package.json的scripts里添加:
{
"scripts": {
"build": "webpack --config webpack.config.js"
}
}
翻译成人话:
「当有人说npm run build时,就用node_modules/.bin目录下的webpack工具,按照webpack.config.js里的方案干活!」
五、执行 npm run build(真正的施工过程)
场景:你大喊一声:「开始施工!」(执行npm run build),然后发生了什么?
详细步骤:
-
npm 找到施工指令:
npm 打开package.json,看到scripts里的build命令:webpack --config webpack.config.js。 -
找到 Webpack 工具:
npm 去node_modules/.bin目录(你的「工地仓库」)里找webpack工具(就像工人去仓库拿搅拌机)。 -
Webpack 读取施工方案:
Webpack 打开webpack.config.js,看到:- 入口(entry) :从
src/index.js开始拆楼(分析依赖)。 - 出口(output) :把新楼盖在
dist目录,名字叫bundle.js。
- 入口(entry) :从
-
Webpack 开始拆楼(分析依赖) :
- 从
src/index.js开始,Webpack 会像侦探一样,找出这个文件引用了哪些其他文件(比如import utils from './utils.js')。 - 然后顺着这些引用,继续找
utils.js又引用了谁,直到把整个项目的「依赖关系网」画出来。
- 从
-
Webpack 重建新楼(打包) :
- 把所有找到的文件(JS、CSS、图片等)按照「依赖关系网」,合并成一个或多个新文件(比如
bundle.js)。 - 如果有 CSS 或图片,Webpack 会用对应的 Loader(比如
css-loader、file-loader)处理它们,就像工人用不同工具处理钢筋和水泥。
- 把所有找到的文件(JS、CSS、图片等)按照「依赖关系网」,合并成一个或多个新文件(比如
-
输出结果:
最终在dist目录生成bundle.js,这就是 Webpack 打包后的成果(你的「新楼」)。
六、为什么要这么麻烦?直接写 HTML+JS 不好吗?
问题 1:依赖关系靠「人工记忆」,容易出错
在index.html里需要手动写<link>和<script>引入所有文件,而且必须按「依赖顺序」排列:
<!-- index.html -->
<!-- 先引重置样式,再引主样式 -->
<link rel="stylesheet" href="./css/reset.css">
<link rel="stylesheet" href="./css/main.css">
<!-- 先引工具函数,再引接口请求,最后引主逻辑 -->
<script src="./js/utils.js"></script>
<script src="./js/api.js"></script>
<script src="./js/index.js"></script>
隐患:
- 如果你不小心把
api.js放在utils.js前面,而api.js里又用到了utils.js的函数,就会报错(utils is undefined)。 - 项目变大后(比如有 20 个 JS 文件),谁记得清谁依赖谁?每次加新文件都要手动调整顺序,累死个人。 问题 2:浏览器加载效率低,用户等得久
浏览器加载资源时,每一个<link>和<script>都是一个单独的 HTTP 请求。上面的例子已经有 5 个请求了,实际项目可能有几十上百个:
- 请求越多,浏览器等待时间越长(就像你网购,分开下单 10 个包裹,比一个大包裹到货慢)。
- 每个请求都有网络延迟,叠加起来可能让页面加载时间从「1 秒」变成「5 秒」,用户早就跑了。
问题 3:全局变量污染,代码冲突
假设utils.js里有个format()函数,api.js里也有个format()函数:浏览器运行时,后加载的api.js会覆盖utils.js的format(),导致index.js里调用format()时,结果和预期完全不一样(就像两户人家都叫「张三」,快递员送错东西)。
问题 4:新语法和工具用不了
你想写更简洁的 ES6 语法(import/export)或者Sass 写 CSS(嵌套语法更方便),浏览器都不认识。
总结:Webpack 怎么解决这些「乱」?
- 自动管理依赖:
Webpack 会从index.js开始,自动分析import语句,找出所有依赖(utils.js→api.js→...),按正确顺序打包,你再也不用手动排<script>顺序了。- 合并文件,减少请求:
把多个 JS/CSS 文件合并成 1-2 个文件(比如bundle.js),浏览器只需 1 次请求就能加载完,速度翻倍。- 隔离作用域:
Webpack 会把每个模块的代码放在独立作用域里,utils.js和api.js里的同名函数不会冲突(相当于给每户「张三」加了门牌号)。- 支持新语法和工具:
通过 Loader(比如babel-loader转 ES6,sass-loader转 Sass),让浏览器能识别各种新语法,你写代码时不用束手束脚。
3.3.3 webpack.config.js和vue.config.js的区别
webpack.config.js 是 Webpack 的配置文件,你可以在这个文件中设置入口(entry)、出口(output)以及其他各种 Webpack 选项。下面是一个简单的示例,展示了如何配置入口和出口:
const path = require('path');
module.exports = {
// 入口:指定 Webpack 开始打包的文件
entry: './src/index.js',
// 出口:指定打包后的文件输出位置和文件名
output: {
path: path.resolve(__dirname, 'dist'), // 输出目录的绝对路径
filename: 'bundle.js' // 输出文件的名称
}
};
我们在做vue项目的时候,会配置一个vue.config.js文件,大家都说其实这个和webpack.config.js是一个东西,原理是什么呢?
3.3.3.1 vue Cli是什么?
Vue CLI(Command Line Interface)是一个基于 Node.js 的脚手架工具,用于快速搭建 Vue.js 项目。
以下是用文字图谱形式呈现的 Vue CLI 核心信息(按层级和关联关系梳理):
Vue CLI 核心图谱
├─ 本质定位:Vue.js 项目脚手架工具
│ └─ 核心目标:简化 Vue 项目初始化、配置与构建流程
│
├─ 技术栈基础
│ ├─ 开发语言:JavaScript(运行于 Node.js 环境)
│ ├─ 核心依赖
│ │ ├─ 运行时:Node.js(提供文件操作、命令行交互能力)
│ │ ├─ 包管理:npm / yarn(处理依赖安装)
│ │ ├─ 构建工具:Webpack(模块打包核心)
│ │ └─ 辅助工具:Babel(ES6+ 转译)、ESLint(代码校验)等
│ └─ 扩展机制:插件系统(@vue/cli-plugin-* 形式,如 @vue/cli-plugin-router)
│
├─ 初始化流程(核心步骤)
│ ├─ 1. 环境准备
│ │ ├─ 检查 Node.js / npm 版本兼容性
│ │ └─ 确认全局已安装 Vue CLI(未安装则提示安装)
│ │
│ ├─ 2. 项目配置选择
│ │ ├─ 预设模式(default:基础配置;自定义:手动选择特性)
│ │ └─ 特性选择(交互式)
│ │ ├─ 核心功能:Babel、TypeScript、Router、Vuex 等
│ │ ├─ 开发工具:ESLint、Prettier、Unit Testing(Jest)等
│ │ └─ 样式处理:CSS 预处理器(Sass/Scss、Less)、CSS Modules 等
│ │
│ ├─ 3. 项目生成
│ │ ├─ 模板渲染:基于 Handlebars 引擎生成文件结构
│ │ └─ 目录构建
│ │ ├─ 基础文件:package.json、.gitignore、README.md
│ │ ├─ 配置文件:vue.config.js(Webpack 配置入口)、.babelrc 等
│ │ └─ 源码目录:src/(components、assets、main.js 等)
│ │
│ ├─ 4. 依赖安装
│ │ ├─ 自动执行 npm install / yarn install
│ │ └─ 安装内容:Vue 核心库、构建工具依赖、开发辅助工具
│ │
│ └─ 5. 收尾与提示
│ ├─ 可选:Git 初始化(git init + 首次提交)
│ └─ 输出启动命令(cd 项目名 + npm run serve)
│
└─ 核心特性
├─ 开箱即用:内置最佳实践配置(无需手动写 Webpack 复杂配置)
├─ 可扩展性:通过 vue.config.js 覆盖默认配置
└─ 模板化:基于 Handlebars 动态生成个性化项目结构
Vue CLI 属于 CLI 工具类 npm 包,与普通业务库(如 axios、lodash)的区别:
| 特性 | Vue CLI | 普通 npm 包 |
|---|---|---|
| 安装方式 | 通常全局安装(-g) | 局部安装(项目依赖) |
| 核心用途 | 提供命令行工具(如 vue create) | 作为代码库被引入(如 import axios) |
| 执行方式 | 通过命令行调用(如 vue --help) | 通过代码调用(如 axios.get()) |
| 依赖结构 | 复杂(包含多个子插件包) | 相对简单(专注单一功能) |
3.3.3.2 本质区别
webpack.config.js:是 Webpack 的原生配置文件,用于直接配置 Webpack 的各种功能(如入口、出口、loader、插件等)。vue.config.js:是 Vue CLI 提供的高级配置文件,用于间接配置 Webpack。它本身不是 Webpack 配置,而是 Vue CLI 对 Webpack 配置的一层抽象。
3.3.3.3 为什么需要 vue.config.js?
Vue CLI 为了简化 Webpack 配置,提供了一套默认配置(基于最佳实践)。但开发者可能需要自定义配置,此时可以通过 vue.config.js 来覆盖默认设置,而不必直接修改复杂的 Webpack 配置。
核心原理:Vue CLI 在启动时会读取 vue.config.js,并根据其中的配置动态生成最终的 Webpack 配置。
vue.config.js 支持两种配置方式:
方式一:直接覆盖默认选项
// vue.config.js
module.exports = {
outputDir: 'dist', // 覆盖输出目录
publicPath: '/', // 覆盖公共路径
productionSourceMap: false // 关闭生产环境的 source map
};
这些选项会直接修改 Vue CLI 内置的 Webpack 配置。
方式二:链式操作(ChainWebpack)
通过 chainWebpack 可以精细调整 Webpack 配置:
// vue.config.js
module.exports = {
chainWebpack: config => {
// 修改图片 loader,限制内联文件大小
config.module
.rule('images')
.use('url-loader')
.tap(options => {
options.limit = 4096; // 4KB
return options;
});
}
};
方式三:直接修改 Webpack 配置(configureWebpack)
// vue.config.js
module.exports = {
configureWebpack: {
plugins: [
// 添加自定义插件
new MyAwesomeWebpackPlugin()
],
resolve: {
alias: {
// 添加路径别名
'@utils': path.resolve(__dirname, 'src/utils')
}
}
}
};
4. 最终 Webpack 配置的生成流程
- Vue CLI 有一套内置的默认 Webpack 配置。
- 启动时,Vue CLI 读取
vue.config.js,并根据其中的配置修改默认配置。 - 最终生成完整的 Webpack 配置,并传递给 Webpack 执行。
5. 何时使用哪种配置文件?
-
使用
vue.config.js:- 项目基于 Vue CLI 创建。
- 需要修改 Vue CLI 内置的配置(如输出路径、代理、loader 等)。
- 不想直接操作复杂的 Webpack 配置。
-
使用
webpack.config.js:- 项目没有使用 Vue CLI,而是手动搭建的 Webpack 环境。
- 需要完全自定义 Webpack 配置,不依赖 Vue CLI 的默认设置。
-
vue.config.js是 Vue CLI 提供的简化配置方式,本质上是对 Webpack 配置的封装。 -
最终的 Webpack 配置由 Vue CLI 根据
vue.config.js和内置默认配置动态生成。 -
两者的关系是:
vue.config.js→ Vue CLI → Webpack 配置。
3.3.3.4 webpack小demo
下面是一个最基本的 vue.config.js 文件结构,包含了常用的配置选项:
// vue.config.js
module.exports = {
// 基本路径(开发环境下通常不需要修改)
publicPath: '/',
// 构建输出目录(默认dist)
outputDir: 'dist',
// 静态资源存放路径(相对于outputDir)
assetsDir: 'static',
// 开发服务器配置
devServer: {
port: 8080, // 端口号
open: true, // 自动打开浏览器
proxy: null, // 代理配置(用于跨域请求)
},
// 生产环境是否生成 sourceMap 文件
productionSourceMap: false,
// 配置Webpack(方式一:简单对象合并)
configureWebpack: {
resolve: {
alias: {
// 路径别名配置
'@': resolve('src'),
}
}
},
// 配置Webpack(方式二:链式操作,更细粒度)
chainWebpack: config => {
// 这里可以对loader、插件等进行更详细的配置
}
};
// 辅助函数:用于解析路径
function resolve(dir) {
return require('path').join(__dirname, dir);
}
这个配置文件包含了以下核心部分:
- 路径配置:
publicPath、outputDir、assetsDir - 开发服务器:
devServer(端口、自动打开、代理) - 生产环境优化:
productionSourceMap - Webpack 配置:
configureWebpack和chainWebpack两种方式
3.4 vite
3.4.1 序言
Vite(法语意为"快")是由尤雨溪开发的新一代前端构建工具,它基于原生ES模块导入特性,结合Rollup打包能力,为现代Web项目提供极速的开发体验。
传统构建工具(如Webpack)在处理大型项目时存在明显性能瓶颈:
- 冷启动时间长:需预编译整个项目依赖树
- 热更新缓慢:修改文件后需重新构建相关模块链
- 内存占用高:随着项目增大,构建过程消耗资源呈指数级增长
Vite的出现正是为了解决这些痛点,它利用现代浏览器原生支持的ES模块特性,实现了无需打包的开发服务器。
3.4.2 vite vs webpack
1. 相同点
- 均为前端构建工具,支持模块打包、资源处理
- 支持多种文件类型(JS、CSS、图片等)
- 生产环境均生成优化后的静态资源
2. 核心差异
| 特性 | Vite | Webpack |
|---|---|---|
| 开发服务器 | 基于ES模块按需加载,无需打包 | 全量预打包所有模块 |
| 冷启动时间 | 毫秒级(通常<1秒) | 秒级到分钟级(取决于项目规模) |
| 热更新机制 | 仅更新修改的模块,与项目大小无关 | 依赖链越长,更新越慢 |
| 配置复杂度 | 极简(vite.config.js) | 复杂(webpack.config.js) |
| 生产环境打包 | 使用Rollup | 使用自身打包引擎 |
3. vite优势
- 极速冷启动
由于无需预打包,Vite开发服务器启动时间通常在1秒以内,即使是大型项目也能瞬间启动。
- 即时热更新(HMR)
Vite的HMR基于模块级别,无论项目规模多大,更新都能保持在毫秒级响应。
- 简化配置
Vite提供了开箱即用的配置,常见功能(如TypeScript、CSS预处理器)无需额外配置:
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [vue()], // 仅需引入Vue插件
resolve: {
alias: {
'@': '/src' // 路径别名配置
}
}
})