个人知识总结持续更新中...

275 阅读27分钟

我是2023.3.1正式开启程序员职业生涯,不知不觉,从事前端开发已经两年半了[手动狗头],最近闲来无事,想着还是要为自己平时的积累做个总结,方便认清自身从而不断鞭策自己,敬请各位批评指正!

悟已往之不谏,知来者之可追

一、技术栈汇总

首先放一张思维导图,描述下自己具体掌握了哪些技术栈。掌握个蛋,能算熟悉就不错了!

bear-2025技术栈.png

二、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属性指的就是客人

无标题-2025-08-04-0955.png

2.2 dom diff对比

直接放一张我自己整理的diff算法的图 无标题-2025-08-04-0955.png

三、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 组合不同阶段的逻辑。
  • 自动 vs 手动

    • Vue 的生命周期钩子自动触发,无需手动管理。
    • React 的 useEffect 需要通过依赖数组手动控制执行时机。

3.1.3 设计模式

相似点

  1. 组件化: 两者都基于组件化思想,将 UI 拆分为独立、可复用的小组件,通过组合构建复杂应用。

  2. 状态管理模式: 都支持单向数据流和全局状态管理:

    • Vue:Vuex(官方)、Pinia(新一代状态管理库)。
    • React:Redux、Context API、Zustand 等。
  3. 高阶组件 / 函数组合

    • React:使用高阶组件(HOC)和自定义 Hooks 复用逻辑。
    • Vue:使用 mixins(Vue 2)、组合式 API(Vue 3)和自定义组合函数。
  4. 发布 - 订阅模式: 两者的响应式系统都基于发布 - 订阅模式

    • Vue:Dep/Watcher 实现依赖收集和通知。
    • React:事件系统和状态更新机制类似发布 - 订阅。

3.1.4 总结对比表

维度VueReact
响应式原理自动响应式(Proxy/Object.defineProperty)不可变数据 + 显式更新
虚拟 DOM有,内部实现核心设计理念之一
生命周期明确的钩子函数(mounted、updated 等)类组件(componentDidMount)和 Hooks(useEffect)
状态管理Vuex、PiniaRedux、Context API、Zustand
代码复用组合式 API、mixins自定义 Hooks、高阶组件
设计模式组件化、发布 - 订阅组件化、单向数据流、高阶组件

尽管有相似之处,两者的核心差异在于设计哲学

  • Vue:追求直观易用低学习曲线,提供明确的 API 和模板语法,适合快速上手。
  • React:强调灵活性函数式思维,通过 Hooks 等机制让开发者自由组合逻辑,适合复杂场景和大型应用。

3.2 响应式原理

Vue 的响应式系统主要通过 Object.defineProperty ()(Vue 2.x)或 Proxy(Vue 3.x)实现,核心流程可概括为:

  1. 初始化时:Vue 会遍历 data 选项中的所有属性,使用 Object.defineProperty () 将这些属性转换为 getter/setter(Vue 2.x),或通过 Proxy 代理对象(Vue 3.x)。这个过程中会收集依赖—— 即记录哪些 Watcher 依赖于当前属性。
  2. 依赖收集:当一个组件渲染时,它会读取数据属性,触发 getter。此时 Vue 会将正在执行的渲染 Watcher 添加到该属性的依赖列表中。
  3. 数据变更时:当属性值被修改,会触发 setter(或 Proxy 的 set 拦截器)。Vue 会通知所有依赖于此属性的 Watcher 进行更新,这就是派发更新
  4. Watcher:Watcher 是响应式系统的核心,负责订阅数据变化并执行副作用(如更新 DOM)。每个组件实例都有一个渲染 Watcher,当依赖的数据变化时,会重新渲染组件。

关键区别

  • Vue 2.x 的响应式是基于属性的,因此新增属性需要使用 Vue.set ();
  • Vue 3.x 使用 Proxy,支持深层响应式和动态属性添加,无需特殊 API。

React 采用单向数据流不可变数据的设计:

状态管理:使用useStatethis.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 节点。
特性ReactVue
数据流动单向数据流(自上而下)双向数据绑定(自动同步)
状态变更不可变数据,必须通过 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 = '张三'; // 自动触发页面更新

底层机制

  1. reactive使用 Proxy(Vue 3)拦截对象的所有属性访问。
  2. 当读取a.name(如在模板中)时,Vue 会记录这个依赖关系。
  3. 当修改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>
  );
}

关键步骤

  1. 使用useState声明对象状态a
  2. 更新时,通过setA创建新对象(保留其他属性)。
  3. 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 能通过对比新旧状态的引用快速判断 “是否需要更新”:如果新状态和旧状态的引用不同(比如新对象),就认为状态发生了变化,需要触发更新;如果引用相同,就跳过更新。

这种机制看似 “麻烦”,但带来了两个重要好处:

  1. 简化更新逻辑:React 不需要像 Vue 那样维护复杂的依赖追踪系统,通过 “引用对比” 就能快速判断是否需要更新。
  2. 可预测性:状态的每次变化都是显式的、可追溯的(通过 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 的修饰符

  1. .lazy:将绑定更新的时机从 input 事件改为 change 事件。

    <input v-model.lazy="message" />
    
  2. .number:自动将输入值转换为数字类型。

    <input v-model.number="age" />
    
  3. .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 的 stateDOM 自身
值的获取方式直接从 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 事件来模拟双向绑定:

  1. 原生表单元素的双向绑定(受控组件)
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 实现:

  1. 父组件
 function ParentComponent() {
      const [message, setMessage] = useState('');
      
      return (
        <ChildComponent 
          value={message} 
          onChange={(newValue) => setMessage(newValue)} 
        />
      );
    }
  1. 子组件
  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 显式传递

第三方库简化双向绑定

如果觉得手动绑定繁琐,可以使用第三方库:

  1. Formik:用于复杂表单管理,支持双向绑定
  2. React Hook Form:轻量级表单库,通过 ref 减少重新渲染
  3. MobX:状态管理库,支持自动双向绑定

React 虽然没有内置的 v-model,但通过以下方式可以实现类似功能:

  1. 受控组件:组合 value 和 onChange 实现基础双向绑定

  2. 自定义 Hook:封装复用逻辑,模拟 Vue 的修饰符

  3. 组件通信:通过 prop 和 callback 在组件间传递状态

相比 Vue 的隐式双向绑定,React 的方式更加显式和可控,符合 React 的单向数据流设计理念。

2.3 生命周期

React 有类似于 Vue 的生命周期钩子。React 的生命周期分为挂载、更新、卸载三个阶段,在类组件中通过特定方法实现,在函数式组件中可通过 Hooks 来模拟相关功能。具体如下:

  • 类组件中的生命周期方法

    • 挂载阶段:包括constructorstatic getDerivedStateFromPropsrendercomponentDidMount。其中componentDidMount与 Vue 中的mounted类似,常用于在组件挂载后执行一些操作,如发送网络请求、操作 DOM 等。
    • 更新阶段:包含static getDerivedStateFromPropsshouldComponentUpdaterendergetSnapshotBeforeUpdatecomponentDidUpdatecomponentDidUpdate可在组件更新后执行某些操作,比如对比更新前后的 props 或 state 来决定是否进行其他操作,类似于 Vue 中的updated钩子。
    • 卸载阶段:主要是componentWillUnmount方法,与 Vue 的beforeUnmount类似,用于在组件卸载和销毁之前执行清理操作,如清除定时器、取消网络请求等,以避免内存泄漏。
    • 异常处理:有static getDerivedStateFromErrorcomponentDidCatch,可用于捕获组件树中的错误,类似于 Vue 中的onErrorCaptured
  • 函数式组件中通过 Hooks 模拟生命周期功能:主要是利用useEffect钩子来模拟。当useEffect的依赖数组为空时,其回调函数仅在组件挂载后执行一次,相当于componentDidMount;若useEffect的回调函数返回一个清理函数,该清理函数会在组件卸载或依赖项变化前执行,可用于模拟componentWillUnmount;而不传入依赖数组或传入包含某些状态的依赖数组时,useEffect的回调函数会在组件挂载和相关状态更新后执行,能在一定程度上模拟componentDidUpdate的功能。

虽然 React 近年来大力推广函数式组件和 Hooks,但类组件并未被官方标记为 “过时”componentDidMount 等生命周期方法仍然是合法且有效的,只是在新的开发实践中,函数式组件 + Hooks 成为了更推荐的方式。

2.3.1 关于 “过时” 的误解澄清

  1. 类组件仍被支持
    React 官方明确表示,类组件不会被移除,目前所有类组件的生命周期方法(包括 componentDidMountcomponentDidUpdatecomponentWillUnmount 等)仍然完全可用,适用于维护旧项目或偏好类语法的场景。
  2. 被标记为 “过时” 的是部分不安全的生命周期
    早期 React 中,componentWillMountcomponentWillReceivePropscomponentWillUpdate 这三个方法因可能导致不可预测的副作用(如在挂载前请求数据、重复触发更新等),被官方标记为 “不安全”(Unsafe),并推荐用其他方法替代(如 getDerivedStateFromPropsuseEffect)。
    而 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个核心生命周期方法从未被标记为过时,至今仍是类组件中处理副作用的标准方式。

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(如 computedwatch)。

** 闭包在 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 组件内的状态管理(useStateuseReducer核心依赖 React 的内部调度机制,但闭包在其中扮演了关键辅助角色。

  1. 闭包的作用:保存状态快照

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)能访问到定义时的状态值(避免状态被每次渲染的函数作用域覆盖)。
  1. 核心原理:React 的状态更新与重新渲染

useState 的核心逻辑并非依赖闭包,而是:

  • 状态存储:React 将状态保存在组件对应的 Fiber 节点(虚拟 DOM 的底层结构)中,而非闭包本身。
  • 更新触发:调用 setCount 时,React 会标记组件为 “需要更新”,并在调度机制中触发重新渲染。
  • 快照机制:每次渲染时,count 是当前状态的 “快照”,闭包只是让这个快照在事件处理函数中可访问。

二、React 全局状态管理库的原理(以 Redux、Zustand 为例)

全局状态管理库的核心是状态的集中存储与订阅更新,闭包可能被用于简化 API,但并非核心原理。

  1. Redux:基于 “单一状态树” 和 “订阅 - 发布” 模式
  • 核心原理

    • 状态存储在单一的 store 对象中(非闭包,而是显式的对象)。
    • 通过 dispatch(action) 触发状态更新,reducer 函数根据 action 计算新状态(纯函数,无闭包依赖)。
    • 组件通过 useSelector 订阅状态,当状态变化时自动重新渲染(依赖 React 的上下文 Context 和订阅机制)。
  • 闭包的作用:几乎不用闭包,更依赖纯函数和显式的状态流转。

  1. 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 时,闭包确保组件能访问到最新的状态,且状态在所有组件间共享。
  • 但本质仍是状态共享:闭包只是封装了状态的访问方式,底层状态仍是一个全局共享的对象(类似单例)。
  1. 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为主,开发模式具有以下特点:

  1. 文件结构简单
    通常按照静态资源类型组织目录(如cssjsimages),项目规模较小时可以满足需求,但缺乏明确的模块化划分。
  2. 代码直接运行
    开发完成后直接在浏览器中打开 HTML 文件测试,没有复杂的构建流程。JavaScript 代码往往全局作用域污染严重,依赖管理混乱。
  3. 依赖手动管理
    通过<script><link>标签引入外部资源,需要手动处理依赖顺序,容易出现 "callback hell" 或资源加载错误。
  4. 兼容性处理复杂
    针对不同浏览器编写大量 hack 代码,测试和修复成本高。
  5. 部署流程原始
    通常通过 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):支持变量、嵌套、自动前缀等增强功能。

总结:工程化的核心价值:

  1. 提高效率:自动化工具减少重复劳动,热更新加速开发流程
  2. 增强可维护性:模块化和规范使代码结构清晰,降低维护成本
  3. 保障质量:自动化测试和代码检查提前发现问题
  4. 支持大规模协作:多人团队开发时避免冲突,统一技术栈和规范
  5. 优化用户体验:更小的包体积、更快的加载速度
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 initnpm run xxx都做了什么?

我们首先要搞清楚npm run xx和node xx.js的关系,直接用 node xx.js 运行 JS 文件,和通过 npm run xx 运行,本质上都是执行同一个文件,只是后者需要先在 package.json 中定义脚本。两者的执行效果一致,但 npm run 提供了更灵活的配置方式(比如添加参数、环境变量等)。

1. node xx.js步骤

  1. 查找文件:Node.js 首先在当前目录下查找 xx.js 文件

  2. 加载模块:找到文件后,Node.js 会加载该文件作为模块

  3. 编译执行

    • 对文件内容进行语法检查
    • 将 JavaScript 代码编译为字节码
    • 执行编译后的代码
  4. 事件循环:如果代码中有异步操作,进入 Node.js 事件循环

  5. 进程退出:当所有同步代码执行完毕且事件循环中没有待处理任务时,进程退出

2. npm run xx步骤

  1. 查找 package.json:npm 首先在当前目录及其父目录中查找 package.json 文件

  2. 解析 scripts 字段:找到 package.json 后,npm 查看其中的 scripts 字段

  3. 查找匹配命令:寻找与 xx 匹配的脚本命令

  4. 创建子进程:npm 创建一个子 shell 来执行该命令

  5. 环境变量注入:npm 会将 node_modules/.bin 添加到 PATH 环境变量中

  6. 执行命令

    • 如果是直接命令(如 node xx.js),则执行该命令
    • 如果是复杂命令,会按 shell 语法解析执行
  7. 返回结果:命令执行完毕后,将结果返回给主进程

具体实现步骤:假设你要运行的文件是 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.jsnpm 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等) │ │  (项目构建/库开发) │
└──────────────────────┘ └────────────────────┘ └────────────────────┘

图谱关系说明:

  1. 最底层:V8 引擎 + 操作系统

    • Chrome V8 是 JavaScript 解释器,负责执行 JS 代码。
    • 操作系统提供文件读写、网络等底层能力,Node.js 封装这些能力为 JS API。
  2. 核心层:Node.js

    • 所有工具(Webpack/Rollup/Vite)的运行依赖,提供:

      • 文件系统操作(fs 模块)
      • 路径处理(path 模块)
      • 模块系统(CommonJS/ES Modules)
    • 没有 Node.js,这些工具都无法运行。

  3. 包管理层:npm/yarn/pnpm

    • 负责下载工具(如 npm install webpack),并将其存储在 node_modules 中。
    • 工具的执行依赖包管理器解析路径(如 npx webpack 本质是调用 node_modules/.bin/webpack)。
  4. 构建工具层:Webpack/Rollup/esbuild

    • Webpack:全能型打包工具,支持各种资源(JS/CSS/ 图片),适合复杂应用。
    • Rollup:专注 JS 库打包,输出纯净代码,适合发布 npm 包。
    • esbuild:用 Go 语言编写,速度极快,作为预构建工具(如 Vite 开发时使用)。
  5. 上层工具:Vite

    • 不是从零实现打包,而是整合底层工具

      • 开发环境:用 esbuild 预构建依赖(快)。
      • 生产环境:用 Rollup 打包(兼容好)。
    • 依赖 Node.js 提供的服务能力(如开发服务器)。

  6. 外围生态

    • 框架(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原理差不多

维度RollupWebpack
诞生背景为 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、分割代码、热更新)。
    • 牺牲一些体积,换取更全面的兼容性和功能。

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

发生了什么

  1. 生成 package.json: 这就像给你的楼办了个「出生证明」,记录了项目的基本信息(名称、版本、作者等)。

    {
      "name": "my-project",
      "version": "1.0.0",
      "scripts": {}, // 这里是你以后写「施工指令」的地方
      "dependencies": {}, // 项目运行时需要的「材料」
      "devDependencies": {} // 开发时需要的「工具」
    }
    
    
  2. -y 参数: 相当于你跟 npm 说:「别问我一堆问题了(比如项目描述、作者),直接用默认值生成!」

二、安装 Webpack(npm install webpack)

场景:你需要买一堆「建筑工具」(Webpack 及其周边工具)。

命令

npm install webpack webpack-cli --save-dev

发生了什么

  1. 下载工具到仓库
    npm 把 Webpack 和 Webpack CLI 下载到你项目的node_modules文件夹(相当于你的「工地仓库」)。

  2. 记录工具清单
    package.jsondevDependencies里添加记录:

    {
     "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.jsonscripts里添加:

{
  "scripts": {
    "build": "webpack --config webpack.config.js"
  }
}

翻译成人话: 「当有人说npm run build时,就用node_modules/.bin目录下的webpack工具,按照webpack.config.js里的方案干活!」

五、执行 npm run build(真正的施工过程)

场景:你大喊一声:「开始施工!」(执行npm run build),然后发生了什么?

详细步骤

  1. npm 找到施工指令
    npm 打开package.json,看到scripts里的build命令:webpack --config webpack.config.js

  2. 找到 Webpack 工具
    npm 去node_modules/.bin目录(你的「工地仓库」)里找webpack工具(就像工人去仓库拿搅拌机)。

  3. Webpack 读取施工方案
    Webpack 打开webpack.config.js,看到:

    • 入口(entry) :从src/index.js开始拆楼(分析依赖)。
    • 出口(output) :把新楼盖在dist目录,名字叫bundle.js
  4. Webpack 开始拆楼(分析依赖)

    • src/index.js开始,Webpack 会像侦探一样,找出这个文件引用了哪些其他文件(比如import utils from './utils.js')。
    • 然后顺着这些引用,继续找utils.js又引用了谁,直到把整个项目的「依赖关系网」画出来。
  5. Webpack 重建新楼(打包)

    • 把所有找到的文件(JS、CSS、图片等)按照「依赖关系网」,合并成一个或多个新文件(比如bundle.js)。
    • 如果有 CSS 或图片,Webpack 会用对应的 Loader(比如css-loaderfile-loader)处理它们,就像工人用不同工具处理钢筋和水泥。
  6. 输出结果
    最终在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.jsformat(),导致index.js里调用format()时,结果和预期完全不一样(就像两户人家都叫「张三」,快递员送错东西)。

问题 4:新语法和工具用不了

你想写更简洁的 ES6 语法(import/export)或者Sass 写 CSS(嵌套语法更方便),浏览器都不认识。

总结:Webpack 怎么解决这些「乱」?

  1. 自动管理依赖
    Webpack 会从index.js开始,自动分析import语句,找出所有依赖(utils.jsapi.js→...),按正确顺序打包,你再也不用手动排<script>顺序了。
  2. 合并文件,减少请求
    把多个 JS/CSS 文件合并成 1-2 个文件(比如bundle.js),浏览器只需 1 次请求就能加载完,速度翻倍。
  3. 隔离作用域
    Webpack 会把每个模块的代码放在独立作用域里,utils.jsapi.js里的同名函数不会冲突(相当于给每户「张三」加了门牌号)。
  4. 支持新语法和工具
    通过 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 包,与普通业务库(如 axioslodash)的区别:

特性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 配置的生成流程

  1. Vue CLI 有一套内置的默认 Webpack 配置
  2. 启动时,Vue CLI 读取 vue.config.js,并根据其中的配置修改默认配置
  3. 最终生成完整的 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);
}

这个配置文件包含了以下核心部分:

  1. 路径配置publicPathoutputDirassetsDir
  2. 开发服务器devServer(端口、自动打开、代理)
  3. 生产环境优化productionSourceMap
  4. 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. 核心差异

特性ViteWebpack
开发服务器基于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' // 路径别名配置
    }
  }
})