普通视图

发现新文章,点击刷新页面。
昨天 — 2026年3月24日首页

深入浅出 AST:解密 Vite、Babel编译的底层“黑盒”

2026年3月24日 19:48

前言

在前端开发中,我们每天都在写 JSX、TypeScript、Vue SFC,但浏览器其实根本看不懂这些。是谁把这些高级语法翻译成了浏览器能执行的 JS?答案就是 AST(Abstract Syntax Tree,抽象语法树) 。它是所有前端构建工具(Vite、Webpack、ESBuild、Babel)的灵魂。

一、 核心概念:什么是 AST?

AST(Abstract Syntax Tree,抽象语法树) ,是代码的结构化数据表示。简单来说,就是把原本一行行纯文本形式的代码,剥离无关的格式、空格、注释等冗余信息,转换成一棵有层级、有嵌套、有明确语法逻辑的树状对象。

  • 转换的核心意义:让计算机能够真正读懂代码的含义,而不是把代码当成普通字符串处理。有了AST,机器才能精准分析代码结构、修改代码逻辑、实现各类编译构建功能。

  • 例子const a = 1 在 AST 中会被拆解为:一个变量声明节点、一个标识符 a 和一个数字字面量 1


二、 AST的编译与生成流程

代码转换通常经历以下四个标准阶段:

  1. 词法分析 (Tokenization) :将长字符串拆解为最小语法单元(Tokens)。例如把 const a = 1 拆成 consta=1

  2. 语法分析 (Parsing) :在通过词法分析得到零散的Tokens后,语法分析会根据对应的语言规范(JS规范、Vue模板规范等),将这些无序的Tokens按照语法规则,组装成具有嵌套依赖关系的节点树,也就是最终的AST。这一步会确立代码的语法结构,比如声明语句、赋值语句、函数定义等节点的层级关系。

  3. 转换 (Transformation) :这是各类编译工具的核心工作区,比如Babel、ESBuild、Rollup的关键逻辑都在这一步。工具会深度遍历AST上的每一个节点,根据需求对节点进行修改、新增、删除操作,比如语法降级、代码替换、依赖处理等,改造出符合目标要求的新AST。

  4. 代码生成 (Code Generation) :完成AST的修改后,最后一步就是逆向操作:把改造后的树状AST,重新转换回纯文本形式的可执行代码,完成整个编译构建流程。


三、AST的核心应用场景

AST是前端工程化的底层基石,几乎所有主流的构建、转译、优化工具,都是基于AST实现的,核心应用场景包括:

  • 代码转译(ES6+转ES5、TS转JS、Vue/React编译)
  • 依赖预构建与依赖分析
  • Tree Shaking(无用代码剔除)
  • 代码压缩、混淆、格式化
  • 静态代码检查(ESLint)
  • 框架单文件组件编译(Vue SFC、React JSX)

四、 AST 在 Vite 中的降维打击

Vite作为新一代前端构建工具,凭借超快的启动速度和构建效率出圈,而这一切高效能力的底层,都离不开AST的支撑。下面详解AST在Vite四大核心场景中的具体作用。

1. 依赖预构建 (Pre-bundling)

依赖预构建是Vite启动速度远超Webpack的核心秘诀,而AST则是依赖预构建的核心底层支撑,具体执行流程:

  1. Vite会深度解析第三方依赖包代码(比如lodash-es、axios等),先将代码文本转换为AST;
  2. 遍历AST节点,精准识别出所有 import/export 语句(或CommonJS的 require 语句),梳理清楚第三方包的内部依赖关系;
  3. 修改AST节点:将不兼容浏览器的CommonJS语法,转换成浏览器原生支持的ESM模块化语法;
  4. 继续优化AST,把零散的多个依赖文件,合并成少数几个文件,减少网络请求;
  5. 将修改后的AST重新生成代码文本,缓存到 node_modules/.vite 目录下,供浏览器直接加载。

2. ESBuild 转译

Vite在开发阶段选用Go 编写的 ESBuild 进行快如闪电的转译,实现TS转JS、ES6+语法降级等能力,而ESBuild的核心工作原理就是基于AST处理

  1. ESBuild读取TS/TSX源码,将其解析生成标准AST;
  2. 遍历AST节点,剔除TS特有的语法节点(比如类型注解const a: number = 1),保留纯JS逻辑;
  3. 对ES6+高阶语法节点(箭头函数、解构赋值、可选链等)进行转换,替换为ES5兼容的AST节点;
  4. 将转换后的AST生成纯JS代码文本,返回给浏览器加载执行。

3. 按需导入与 Tree Shaking

Vite生产环境打包底层基于Rollup,而Tree Shaking(剔除无用代码、实现按需引入)完全依赖AST实现:

  1. Rollup解析项目源码,生成完整的AST;
  2. 深度遍历AST,跟踪代码的引用关系,精准识别出未被调用、未被引用的无用代码节点(比如未使用的函数、变量、模块);
  3. 从AST中直接删除这些无用节点,精简AST结构;
  4. 将精简后的AST重新生成代码文本,大幅减少打包体积,实现代码瘦身。

4. Vue SFC 单文件组件编译

在Vite+Vue项目中,@vitejs/plugin-vue 插件负责解析.vue单文件组件,AST是整个编译流程的核心:

  1. 插件先将.vue文件拆分为 <template><script><style> 三大核心模块;
  2. 针对 <template> 模板:生成专属的Vue模板AST(结构类似JS AST,针对模板语法优化),再将模板AST进一步转换成渲染函数(render函数)对应的JS AST;
  3. 针对 <script setup> 脚本:解析JS AST,处理 definePropsdefineEmitsdefineExpose 等Vue语法糖,将其转换为浏览器可识别的普通JS代码;
  4. 最后合并所有模块的AST,生成浏览器可直接运行的完整JS代码,完成Vue组件编译。

📝 总结与启发

AST 是前端工程化的“上帝视角”。掌握了它,你就掌握了编写 Lint 工具、代码加密、自动重构脚本 以及 自定义 Babel/Vite 插件 的能力。

前端模块化:CommonJS、AMD、ES Module三大规范全解析

2026年3月24日 19:01

前言

在前端工程化日益庞大的今天,模块化已成为基石。从最初的“全局变量污染”到如今的“万物皆可模块”,前端社区经历了漫长的探索。本文将深度解析业界主流的三大模块规范:CommonJSAMDES Module

一、 CommonJS:服务端的先行者

CommonJS 是最早正式提出的 JavaScript 模块规范,伴随着 Node.js 的诞生而风靡。

1. 核心语法

  • 导出:使用 module.exportsexports
  • 导入:使用 require
// a.js
const add = (a, b) => a + b;
module.exports = { add };

// main.js
const { add } = require('./a.js');
console.log(add(1, 2));

2. 局限性与挑战

  • 环境依赖:模块加载器由 Node.js 提供,高度依赖运行时环境。
  • 同步阻塞:CommonJS 规定模块加载是同步的。在服务端(磁盘读取)这没问题,但在浏览器端(网络请求),同步加载会导致 JS 解析阻塞,造成页面假死。

二、 AMD:浏览器的异步解法

为了解决 CommonJS 在浏览器端的同步阻塞问题,AMD (Asynchronous Module Definition) 应运而生。

1. 核心语法

AMD规范依赖第三方库(如RequireJS)实现,通过 define() 函数定义模块:第一个参数声明依赖模块数组,第二个参数为回调函数,依赖加载完成后执行;模块导出通过return实现。

// print.js 定义无依赖的模块
define(function () {
  // 模块内部逻辑
  function print(msg) {
    console.log("print " + msg);
  }
  // return 导出模块成员
  return {
    print
  };
});

// main.js 定义有依赖的模块
// 第一个参数:依赖模块列表;第二个参数:依赖加载完成后的回调
define(["./print"], function (printModule) {
  // 使用依赖模块的方法
  printModule.print("main");
});

2. 存在的不足

  • 非原生支持:需要引入第三方的 loader(如著名的 RequireJS)。
  • 开发成本:书写格式相对复杂,代码逻辑被包裹在回调函数中,阅读和维护成本较高。

三、 ES Module (ESM):终极统一方案

ES Module(ESM) 是ECMAScript官方推出的模块化标准,也是目前现代前端工程化的唯一标准,浏览器和Node.js均已原生支持,完美解决了前两种规范的缺陷。

1. 核心语法

  • 导出exportexport default
  • 导入import
// lib.js
export const version = '1.0.0';
export default function MyFunc() {}

// main.js
import MyFunc, { version } from './lib.js';

2. 为什么它是最优解?

  • 编译时加载(静态分析) :ESM 在代码执行前就能确定模块依赖关系,这使得 Tree-shaking(摇树优化) 成为可能。
  • 原生支持:现代浏览器通过 <script type="module"> 即可直接运行,无需转换。
  • 异步加载:天然支持异步,不会阻塞页面渲染。

四、 核心对比:CommonJS vs AMD vs ESM

维度 CommonJS AMD ES Module
加载方式 同步加载 异步加载 静态编译/异步加载
运行环境 主要用于服务端 (Node.js) 浏览器端 (需 Loader) 浏览器/服务端通用
典型代表 Node.js RequireJS Vite, Webpack, 现代浏览器

五、 总结与趋势

  1. CommonJS 依然是 Node.js 生态的基石,但在向 ESM 过渡。
  2. AMD 已逐渐退出历史舞台,基本被打包工具(如 Webpack)内部处理。
  3. ESM 是未来,无论是前端框架(Vue3/React)还是构建工具(Vite),都在全面拥抱 ESM。

React-彻底搞懂 Redux:从单向数据流到 useReducer 的终极抉择

2026年3月23日 21:42

前言

在 React 生态中,状态管理一直是开发者绕不开的话题。Redux 以其严谨的“单向数据流”闻名,虽然有一定的学习成本,但它为大型项目带来的可预测性和可调试性是无可替代的。本文将带你深度复盘 Redux 的核心逻辑。

一、 Redux 的核心

Redux 的核心思想是将应用的所有状态(State)集中存储在一个唯一的 Store 中,并遵循严格的规则进行更新,让状态变化可追踪、可调试,大幅降低复杂应用的状态维护成本。

1. 三大核心概念

Redux 的运行逻辑完全围绕三大核心模块展开,各司其职、互不干扰,构建了清晰的单向数据流:

1. Store(数据仓库)

  • 定位:应用状态的唯一存储容器,整个应用有且仅有一个 Store
  • 作用:承载全局状态、派发 Action、监听状态变化、整合 Reducer,是连接视图和数据的核心枢纽
  • 特性:独立于组件生命周期,不会随组件销毁而消失,状态持久稳定

2. Action(动作描述)

  • 定位改变 State 的唯一途径,是一个普通的 JavaScript 对象
  • 结构:必须包含 type 属性(字符串类型,描述动作类型),可选携带 payload 属性(传递更新状态所需的数据)
  • 示例{ type: 'UPDATE_USER_NAME', payload: '李四' }
  • 本质:只描述“要做什么”,不负责“怎么做”,属于指令载体

3. Reducer(状态处理器)

  • 定位纯函数(固定输入必然得到固定输出,无副作用、不修改入参)
  • 参数:接收两个参数——当前旧 State(prevState)、派发的 Action 对象
  • 逻辑:根据 Action 的 type 类型,匹配对应的更新逻辑,绝不直接修改旧 State
  • 规则:Redux 强制要求 State 不可变(Immutable),必须返回全新的 State 对象,保证状态变化可回溯、可调试

二、 Redux 的工作流程:闭环的单向流

Redux 的数据流转遵循严格的循环路径,确保了状态变化的可追踪性:

  1. 用户触发操作:用户在页面执行交互行为(点击按钮、输入内容、路由跳转等),组件内触发状态更新需求

  2. 派发 Action:通过 Redux 提供的 dispatch 方法,将封装好的 Action 对象派发出去

  3. Reducer 处理:Store 自动将当前旧 State 和派发的 Action 传递给 Reducer,Reducer 根据 type 执行对应逻辑,返回新 State

  4. Store 更新状态:Store 接收 Reducer 返回的新 State,替换内部旧状态

  5. 组件同步数据:所有订阅了 Store 状态的组件,会自动感知状态变化,重新渲染视图,完成数据同步

注意:禁止直接修改 Store 中的 State,必须通过 dispatch 派发 Action → Reducer 生成新 State 的方式更新,这是 Redux 可预测性的核心保障。


三、 Redux vs useReducer:我该选哪个?

很多开发者会混淆这两者,虽然它们都使用了 action/reducer 模式,但在应用范围上有本质区别。

维度 useReducer Redux
存储位置 组件内部(Local State) 独立的全局 Store(Global State)
作用域 仅限当前组件及其子组件 整个应用,任意组件均可访问
生命周期 随组件销毁而消失 独立于组件,持久存在
跨组件通信 需配合 useContext 并提升组件层级 天然支持,无需透传 Props

场景选择:

  • 使用 useReducer:逻辑复杂(有很多 if/else 或 switch),但只在单个组件或其嵌套子组件中使用。
  • 使用 Redux:数据需要全局共享。例如:用户信息需要同步更新导航栏、侧边栏和个人中心;或者需要将状态持久化到 localStorage 并在刷新后恢复。

四、 总结

Redux 的本质是牺牲了一定的代码简便性,换取了极致的状态可预测性。在处理跨页面同步、复杂业务逻辑以及需要状态回溯的场景下,Redux 依然是前端状态管理的王者。

React-路由监听 / 跳转 / 守卫全攻略(附实战代码)

2026年3月23日 20:44

前言

React Router 是 React 单页应用的核心路由库,除了基础的路由配置,日常开发中还会高频用到路由监听、编程式跳转、路由守卫等进阶功能。本文从实战角度拆解这三大核心能力,涵盖实现方式、场景对比、避坑要点,基于 React Router v6+ 版本(主流稳定版)讲解,新手也能快速落地!

一、 路由监听:如何捕捉 URL 的变化?

在 React 中,React 监听路由变化的本质是监听 URL 相关属性(pathname/search/params 等)的变化,触发自定义回调函数。以下是 3 种常用实现方式:

1. 核心方案:useLocation + useEffect

这是最通用的监听方式。通过 useLocation 获取当前路由完整信息(pathname/search/state 等),结合 useEffect 监听 location 对象变化,触发回调函数。

import { useLocation } from 'react-router-dom';
import { useEffect } from 'react';

const App = () => {
  const location = useLocation();

  useEffect(() => {
    // 每次路由切换时执行
    console.log('当前路径:', location.pathname);
    console.log('搜索参数:', location.search);
  }, [location]); 
};

2. 精准监听:useParamsuseSearchParams

如果你只关心某个特定的动态参数(如 id),直接监听参数对象会更高效。

  • useParams:监听动态路由参数(如 /user/:id 中的 id);

  • useSearchParams:监听 URL 搜索参数(如 ?page=1&size=10);

  • 适用场景:

    • 仅关注动态路由参数或搜索参数变化的场景(如详情页 ID 切换、列表页分页参数变化)。

3.监听原生路由事件(不推荐)

  • 通过 window.addEventListener 监听 popstate(History 模式)或 hashchange(Hash 模式)事件,直接捕获 URL 变化。

  • 缺点:React Router 已封装原生事件,手动监听易出现重复触发、状态不一致问题,仅建议特殊场景(如兼容老代码)使用。


二、 路由跳转

React Router 提供多种跳转方式,适配「点击跳转」「编程式跳转」「导航栏高亮」等不同场景:

1. 声明式导航:<Link><NavLink>(最常用)

  • <Link> :基础跳转,React Router 核心跳转组件,替代原生 <a> 标签(避免页面刷新),核心属性如下:

    • to:必传,目标路径(支持字符串 / 对象格式);

    • replace:默认 false(新增历史记录),true 则替换当前记录(跳转后无法回退);

    • state:传递自定义状态(不显示在 URL 中,通过 useLocation().state 获取)。

  • <NavLink> :专为导航栏设计,新增激活状态相关配置,适合导航栏场景:

    • isActive:可根据激活状态动态设置样式类名。

    • end:精准匹配模式,防止 / 匹配到所有子路由(如 /home 不匹配 /home/detail)。

2. 编程式导航:useNavigate

适用于点击按钮后的逻辑处理或异步请求后的跳转,通过 useNavigate Hook 获取导航函数,实现非点击触发的跳转(如接口请求后、条件判断后)。

const navigate = useNavigate();

// 基础跳转,带状态
navigate('/profile', { replace: true, state: { from: 'home' } });

// 历史记录操作
navigate(-1); // 后退一步

三、 路由守卫:在 React 中如何“拦截”?

Vue 有原生的 beforeEach/afterEach 路由守卫,但 React Router 无专属 API,核心通过监听路由 + 条件判断实现,分为 3 类场景:

1. 全局路由守卫

实现逻辑

在路由根组件(如 App.jsx)中监听 location 变化,执行全局校验(如登录状态、白名单),拦截非法跳转。

实战代码

import { useLocation, useNavigate, useEffect } from 'react-router-dom';
import { isLogin } from '@/utils/auth'; // 自定义登录校验函数

// 全局路由守卫组件
const GlobalRouterGuard = () => {
  const location = useLocation();
  const navigate = useNavigate();
  // 无需登录的白名单路由
  const whiteList = ['/login', '/register'];

  useEffect(() => {
    // 未登录且不在白名单 → 跳转到登录页
    if (!isLogin() && !whiteList.includes(location.pathname)) {
      navigate('/login', { 
        replace: true,
        state: { from: location.pathname } // 记录来源路径,登录后跳转回去
      });
    }
  }, [location.pathname, navigate]);

  return null; // 守卫组件无需渲染 DOM
};

// 在根路由中引入
// <BrowserRouter>
//   <GlobalRouterGuard />
//   <Routes>...</Routes>
// </BrowserRouter>

2. 组件内路由守卫

实现逻辑

在组件内通过 useEffect 实现进入守卫(组件挂载时校验),通过 useEffect 的返回函数实现离开守卫(组件卸载时执行)。

实战代码

import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { hasPermission } from '@/utils/permission'; // 自定义权限校验

const UserDetail = () => {
  const navigate = useNavigate();

  // 进入守卫:组件挂载时校验权限
  useEffect(() => {
    if (!hasPermission('user:view')) {
      navigate('/403', { replace: true });
    }
  }, [navigate]);

  // 离开守卫:组件卸载时执行(如保存表单、提示未提交内容)
  useEffect(() => {
    return () => {
      console.log('离开用户详情页,执行清理逻辑');
      // 业务逻辑:保存草稿、关闭WebSocket等
    };
  }, []);

  return <div>用户详情页</div>;
};

3. 路由独享守卫

  • 概念: 不影响全局其他路由,只针对这一个页面(或这一组页面)进行特殊的准入检查。 例如整个应用都可以访问,但只有/admin页面需要检查管理员权限

  • 实现”先创建一个守卫组件AdminGuard,这个组件专门负责检查当前用户是不是管理员,然后将需要单独检查的Admin路由套在这个守卫组件AdminGuard里面

实战代码:

// 守卫组件:AdminGuard.jsx
import { useNavigate } from 'react-router-dom';
import { isAdmin } from '@/utils/auth';

const AdminGuard = ({ children }) => {
  const navigate = useNavigate();
  
  // 管理员权限校验
  if (!isAdmin()) {
    navigate('/403', { replace: true });
    return null;
  }

  // 权限通过,渲染子组件
  return children;
};

// 路由配置中使用
import { Routes, Route } from 'react-router-dom';
import SettingsPage from '@/pages/Settings';

const RouterConfig = () => {
  return (
    <Routes>
      {/* 独享守卫:仅 /settings 路由触发管理员校验 */}
      <Route 
        path="/settings" 
        element={
          <AdminGuard>
            <SettingsPage />
          </AdminGuard>
        } 
      />
    </Routes>
  );
};

4.扩展:离开时的路由拦截(useBlocker)

useBlocker 是 React Router v6 新增 Hook,用于拦截所有路由跳转行为(包括 <Link>navigate、浏览器前进 / 后退)。

import { useBlocker } from 'react-router-dom';

// blockerFn:返回 true 拦截跳转,false 放行
// when:是否启用拦截(可选,默认 true)
useBlocker((tx) => {
  console.log('即将跳转到:', tx.location.pathname);
  return true; // 拦截跳转
}, when);

实战场景:表单未提交拦截

import { useBlocker } from 'react-router-dom';

const FormPage = () => {
  const [formDirty, setFormDirty] = useState(false); // 表单是否修改

  // 表单未提交时拦截跳转
  useBlocker((tx) => {
    if (formDirty) {
      const confirm = window.confirm('表单内容未保存,是否确认离开?');
      return !confirm; // 点击取消 → 拦截(返回 true)
    }
    return false; // 放行
  }, formDirty); // 仅表单修改时启用拦截

  return (
    <form onChange={() => setFormDirty(true)}>
      <input type="text" placeholder="输入内容..." />
    </form>
  );
};

四、 总结与最佳实践

  1. 优先使用原生 Link:对于简单的跳转,<Link> 的性能和 SEO 优于 useNavigate
  2. 善用 State 传参:如果不想 URL 变得太长,利用 location.state 传递对象是最佳选择。
  3. 守卫逻辑模块化:不要在 App.js 里写一堆 if-else,将权限校验封装成独立的 Guard 组件。
昨天以前首页

React-深度拆解 React路由:从实战进阶到底层原理

2026年3月22日 14:48

前言

在单页面应用(SPA)开发中,前端路由是核心基石。它让我们可以根据 URL 的变化,在不刷新页面的情况下切换组件。本文将带你从 React Router 的基本使用出发,深入其底层的浏览器实现机制。

一、 React 路由配置实战

1. 基础环境搭建

首先,我们需要安装 React 路由的核心库:

npm install react-router-dom

2. 注入路由容器

需要在应用最顶层(如 main.jsx/App.jsx)包裹路由核心容器,决定使用 Hash 模式 还是 History 模式,二者的核心区别是 URL 是否带 #,以及服务端配置要求。

两种模式核心区别:

特性 History 模式 Hash 模式
URL 形式 https://xxx.com/home(无 #) https://xxx.com/#/home(带 #)
服务端配置 需要 Nginx配置兜底 无需服务端配置
兼容性 依赖 HTML5 History API 兼容所有浏览器
场景 =现代浏览器 老项目兼容
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter, HashRouter } from 'react-router-dom';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));

// 方式1:History 模式(推荐,URL 无 #,美观)
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

// 方式2:Hash 模式(兼容老浏览器/无需后端配置,URL 带 #)
// root.render(
//   <HashRouter>
//     <App />
//   </HashRouter>
// );

3. 定义路由映射

通过 <Routes><Route> 定义「URL 路径 → 组件」的映射关系,用 <Link> 替代原生 <a> 标签实现无刷新导航。

import { Routes, Route, Link } from 'react-router-dom';
// 导入页面组件(需提前创建)
import Home from './pages/Home';
import About from './pages/About';
import User from './pages/User';
import NotFound from './pages/NotFound';

function App() {
  return (
    <div className="App">
      <nav style={{ margin: '10px 0' }}>
        <Link to="/" style={{ marginRight: '10px' }}>首页</Link>
        <Link to="/about" style={{ marginRight: '10px' }}>关于我们</Link>
        <Link to="/user/123">用户123</Link>
      </nav>

      {/* 路由匹配规则:Routes 是 v6 新增的容器(替代旧版 Switch) */}
      <Routes>
        {/* 基础路由:路径完全匹配时渲染对应组件 */}
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        {/* 动态路由:匹配 /user/任意值,通过 params 传参 */}
        <Route path="/user/:id" element={<User />} />
        {/* 404 路由:匹配所有未定义的路径(必须放最后) */}
        <Route path="*" element={<NotFound />} />
      </Routes>
    </div>
  );
}

export default App;

二、 深度解析路由两种模式底层原理

React Router 的本质是基于浏览器原生能力的封装,核心作用是根据 URL 路径匹配对应的组件并渲染,实现单页面应用(SPA)的无刷新页面切换。。

2. Hash 模式实现原理

Hash 模式依赖 URL 中 # 后的哈希值,核心逻辑如下:

  • URL 变化监听:基于 window.hashchange 事件监听哈希值变化;

  • 设置 URL 值:通过 location.hash = '新路径' 修改哈希值;

  • 获取当前 URL 值:通过 location.hreflocation.hash 获取;

  • URL 页面跳转:通过 location.assign('#/新路径') 实现跳转;

  • 核心优势# 后的内容不会发送到服务器,所有请求都指向 域名/index.html,服务端无需额外配置。

3. History 模式实现原理

History 模式依赖 HTML5 新增的 History API,核心逻辑如下:

  • URL 变化监听:基于 window.popstate 事件监听浏览器前进 / 后退导致的 URL 变化;

  • 新增 / 替换历史记录

    • history.pushState(stateObj, title, url):新增一条历史记录(无刷新跳转);
    • history.replaceState(stateObj, title, url):替换当前历史记录(不新增记录);
  • 获取当前 URL 值:通过 window.location.pathname 获取;

  • 页面前进 / 后退:通过 history.go(number)(前进 / 后退指定步数)、history.back()(后退一步)实现;

  • 获取自定义状态:通过 history.state 获取 pushState 传入的自定义状态对象;

  • 核心注意:URL 无 #,但刷新页面时浏览器会向服务器发送对应路径的请求,需服务端配置兜底,否则会 404。


四、 注意:History 模式的服务器配置

当你使用 History 模式 部署到 Nginx 时,最常见的问题是:点击页面内的 Link 没问题,但一按刷新就 404

原因:History 模式下,浏览器会向服务器请求 /home 这个物理路径。服务器上并没有这个文件,因此报错。

解决方案:在 Nginx 配置中增加 try_files 指令,让所有找不到的请求都重定向到 index.html,交给 React Router 去处理。

location / {
     root   /usr/share/nginx/html;   # 前端打包文件的存放目录
     index  index.html index.htm;    # 默认访问文件
     try_files $uri $uri/ /index.html;  # 核心配置:如果找不到资源,统统指向 index.html
}

📝 总结

  • React 路由需先安装 react-router-dom,顶层包裹 BrowserRouter(History 模式)或 HashRouter(Hash 模式);

  • 核心使用 <Routes>/<Route> 定义路由规则,<Link> 实现导航;

  • Hash 模式无需服务端配置,History 模式需在 Nginx 中配置 try_files 兜底,避免刷新 404;

  • React Router 本质是监听 URL 变化(Hash 监听 hashchange,History 监听 popstate),匹配并渲染对应组件。


React-手把手带你实现 Keep-Alive 效果

2026年3月22日 14:20

前言

在 Vue 中,我们可以通过 <keep-alive> 轻松缓存组件实例。但在 React 中,组件卸载(Unmount)意味着状态和 DOM 的彻底销毁。如何在 React 中实现“切换页面不丢失滚动位置、不重置表单”?本文将为你拆解三种主流方案。

一、 Keep-Alive 的本质是什么?

在实现 React 版缓存组件前,先明确 Vue keep-alive 的核心逻辑,才能精准复刻:

  • 核心本质:缓存组件实例,保留组件内部状态(如输入框内容、滚动位置);
  • 行为特征:组件切换时不销毁 / 重建,仅通过「隐藏 / 显示」控制渲染状态;
  • React 目标:实现和 Vue 一致的效果 —— 组件切走不丢状态,切回来能恢复。

二、 React 实现 keep-alive 的 3 种方案

方案 1:CSS 隐藏 + 不卸载组件(最简单)

核心思路:

通过 display: none 隐藏不活跃的组件,保留组件的 DOM 节点和内部状态,仅切换 display 属性控制显示 / 隐藏,不触发组件的卸载 / 重新挂载生命周期。

适用场景

  • 少量组件切换(2-3 个,如 tab 标签页);
  • 简单业务场景(如表单页、列表页切换)。

优缺点

✅ 优点:实现简单,无额外依赖,状态保留完整;

❌ 缺点:所有组件都会挂载在 DOM 树中,组件数量多(如 5 个以上)会增加 DOM 节点数量,可能影响页面渲染性能。

import { useState } from 'react';

// 模拟 Tab 切换场景
const KeepAliveByCSS = () => {
  // 控制当前激活的标签
  const [activeKey, setActiveKey] = useState('tab1');

  return (
    <div>
      <div className="tab-header">
        <button onClick={() => setActiveKey('tab1')}>标签1</button>
        <button onClick={() => setActiveKey('tab2')}>标签2</button>
      </div>
      <div className="tab-content">
        {/* 始终挂载,仅通过 CSS 隐藏 */}
        <div style={{ display: activeKey === 'tab1' ? 'block' : 'none' }}>
          <Tab1 />
        </div>
        <div style={{ display: activeKey === 'tab2' ? 'block' : 'none' }}>
          <Tab2 />
        </div>
      </div>
    </div>
  );
};

// 带状态的子组件
const Tab1 = () => {
  // 切换标签后,输入框内容不会丢失
  const [inputVal, setInputVal] = useState('');
  return <input value={inputVal} onChange={(e) => setInputVal(e.target.value)} placeholder="标签1输入框" />;
};

const Tab2 = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
};

方案 2:使用 react-activation 第三方库(推荐)

这是目前社区内最成熟的方案,它通过将组件渲染到“外部容器”再动态挂载回来的方式,模拟了 Vue 的行为。

核心功能

  • 提供 <KeepAlive> 容器组件,包裹需要缓存的组件即可生效;

  • 内置 useActivate/useDeactivate 钩子函数,分别在组件「激活」和「失活」时触发;

  • 支持缓存控制(如指定缓存 Key、条件缓存);

  • 能保留 DOM 状态(如滚动条位置、输入框焦点)。

使用示例

import { KeepAlive, useActivate, useDeactivate } from 'react-activation';
import { useState } from 'react';

const KeepAliveByLib = () => {
  const [show, setShow] = useState(true);

  return (
    <div>
      <button onClick={() => setShow(!show)}>
        {show ? '隐藏组件' : '显示组件'}
      </button>
      {/* 用 KeepAlive 包裹需要缓存的组件 */}
      {show && (
        <KeepAlive id="cached-component">
          <CachedComponent />
        </KeepAlive>
      )}
    </div>
  );
};

// 被缓存的组件
const CachedComponent = () => {
  const [inputVal, setInputVal] = useState('');
  const [scrollTop, setScrollTop] = useState(0);

  // 组件激活时触发(显示时)
  useActivate(() => {
    console.log('组件被激活');
  });

  // 组件失活时触发(隐藏时)
  useDeactivate(() => {
    console.log('组件被失活');
  });

  // 模拟滚动条状态保留
  const handleScroll = (e) => {
    setScrollTop(e.target.scrollTop);
  };

  return (
    <div>
      <input 
        value={inputVal} 
        onChange={(e) => setInputVal(e.target.value)} 
        placeholder="缓存的输入框" 
      />
      <div 
        style={{ height: '200px', overflow: 'auto', marginTop: '10px' }}
        onScroll={handleScroll}
      >
        {Array.from({ length: 50 }).map((_, index) => (
          <p key={index}>滚动测试行 {index + 1}</p>
        ))}
      </div>
      <p>当前滚动位置:{scrollTop}</p>
    </div>
  );
};

方案 3:全局状态管理 + 状态回显(Redux)

如果不想引入第三方库,也可以通过全局状态缓存实现核心效果,本质是组件卸载前存状态,重新挂载时取状态。

核心思路

在组件 useEffect 的清理函数中,将关键数据(输入框、计数、滚动位置等)保存到全局 Store;重新挂载时再读取初始化。

  • 优点:符合 React 数据流规范,内存占用可控。

  • 缺点

    1. 无法恢复 DOM 状态:如页面的滚动位置、输入框的焦点、已播放的视频进度,需单独编写逻辑处理。
    2. 开发成本高:每个需要缓存的组件都要手动编写保存/恢复逻辑。
  • 补救措施:若要恢复滚动位置,需手动在卸载前记录 scrollTop,并在渲染后通过 window.scrollTo 还原。


三、缓存 React Router 路由组件

实际开发中,最常见的场景是切换路由不丢失页面状态(如列表页滚动位置、表单输入内容),可结合 React Router + react-activation 实现。

核心代码如下:

import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
import { KeepAlive } from 'react-activation';
import Profile from './pages/Profile';
import Home from './pages/Home';
import Settings from './pages/Settings';

// 路由容器组件(获取当前路由路径)
const RouterContainer = () => {
  const location = useLocation();
  const currentPath = location.pathname;

  return (
    <Routes>
      {/* 普通路由(不缓存) */}
      <Route path="/" element={<Home />} />
      <Route path="/settings" element={<Settings />} />
      
      {/* 缓存路由组件:when 控制是否缓存,id 为缓存标识 */}
      <Route
        path="/profile"
        element={
          <KeepAlive id="profile" when={currentPath === "/profile"}>
            <Profile />
          </KeepAlive>
        }
      />
    </Routes>
  );
};

// 根组件
const App = () => {
  return (
    <BrowserRouter>
      <RouterContainer />
    </BrowserRouter>
  );
};

四、 方案选择建议

方案 优点 缺点 适用场景
CSS 隐藏 实现简单、无依赖 DOM 节点冗余、性能一般 少量组件(2-3 个)、简单 tab 切换
react-activation 功能完整、支持 DOM 状态缓存 新增第三方依赖 中大型项目、需完整 keep-alive 效果
全局状态缓存 无额外依赖、贴合状态管理 仅恢复数据、需手动处理 DOM 已用全局状态库、仅需数据缓存

五、总结

  1. React 无原生 keep-alive,但可通过CSS 隐藏、react-activation 库、全局状态缓存3 种方案模拟核心效果;
  2. 简单场景用 CSS 隐藏,中大型项目优先选 react-activation(兼顾易用性和完整性);
  3. 路由组件缓存可结合 React Router + react-activation 实现,核心是通过 KeepAlive 包裹路由元素并指定缓存标识。

Vue - @ 事件指南:原生 / 内置 / 自定义事件全解析

2026年3月22日 13:36

前言

在 Vue 开发中,@v-on 指令的简写,是绑定事件监听的核心语法。很多新手容易混淆不同类型的 @ 事件用法,本文整理了 Vue 中所有常用的 @ 事件类型,包括原生 DOM 事件、内置组件事件、自定义事件,以及提升开发效率的事件修饰符,看完就能直接上手用!

一、 Vue @ 事件的核心分类

Vue 中的 @ 事件本质是对 DOM 事件 / 组件事件的封装,核心分为三大类:

  • 原生 DOM 事件:浏览器自带的基础交互事件
  • Vue 内置组件事件:Vue 官方组件专属的状态监听事件
  • 自定义事件:组件间通信的核心自定义事件

二、原生 DOM 事件

这类事件是浏览器原生支持的 DOM 事件,Vue 可直接通过 @ 绑定,覆盖绝大部分交互场景,按类型整理如下:

1. 鼠标事件

事件语法 说明 常用场景
@click 点击事件(最常用) 按钮点击、卡片跳转
@dblclick 双击事件 列表项编辑、文件重命名
@mouseenter 鼠标进入(不冒泡) 悬浮提示、菜单展开
@mouseleave 鼠标离开(不冒泡) 悬浮提示隐藏、菜单收起
@mousemove 鼠标移动 拖拽跟随、坐标监听
@mousedown 鼠标按下 拖拽开始、按住触发
@mouseup 鼠标松开 拖拽结束、松开停止
@contextmenu 右键菜单事件 自定义右键菜单

2. 键盘事件

事件语法 说明 注意点
@keydown 键盘按下时触发 可监听组合键(如 @keydown.ctrl.s
@keyup 键盘松开时触发 常用 @keyup.enter 监听回车
@keypress 键盘按压时触发 已逐步废弃,推荐用 keydown 替代

3. 表单事件

事件语法 说明 触发时机对比
@input 输入框内容变化 实时触发(每输入一个字符都触发)
@change 表单值变化 失去焦点 / 选择完成后触发(如下拉框选值)
@submit 表单提交事件 点击提交按钮 / 按回车触发
@focus 元素获取焦点 输入框激活、下拉框展开
@blur 元素失去焦点 输入框失活、表单校验

4. 移动端触摸事件

事件语法 说明 适用场景
@touchstart 触摸开始 移动端点击、滑动开始
@touchend 触摸结束 移动端点击完成、滑动结束

5. 页面 / 窗口事件

事件语法 说明 优化建议
@scroll 滚动事件 监听页面滚动加载、导航栏吸顶
@resize 窗口大小变化 响应式布局适配、画布重绘

6.使用示例

<template>
  <div>
    <!-- 点击事件 -->
    <button @click="handleClick">普通点击</button>
    <!-- 键盘事件(监听回车) -->
    <input @keyup.enter="handleSearch" placeholder="按回车搜索" />
    <!-- 表单输入事件 -->
    <input @input="handleInput" @blur="handleBlur" placeholder="实时输入监听" />
  </div>
</template>

<script setup>
const handleClick = () => console.log('按钮被点击');
const handleSearch = () => console.log('执行搜索');
const handleInput = (e) => console.log('实时输入:', e.target.value);
const handleBlur = () => console.log('输入框失活,可做校验');
</script>


三、 Vue 内置组件事件:监听生命周期

Vue 的内置组件(如动画、路由)拥有自己独特的“生命周期事件”,让我们能精准控制交互细节。

内置组件 常用事件 触发时机
<transition> @before-enter / @enter 进入动画开始前与执行中
@after-enter 动画完全结束,常用于清理工作
@leave / @after-leave 离开动画的相关节点
<router-link> @click 点击跳转(Vue Router 内部处理)
@navigate (Vue Router 4+) 导航正式开始时触发

四、 自定义事件:父子通信核心

自定义事件是 Vue 父子组件通信的重要方式,子组件通过 emit 触发事件,父组件通过 @ 监听事件并接收参数。

  1. 子组件触发:使用 emit 抛出事件和数据。

  2. 父组件监听:通过 @ 绑定回调。

<!-- 子组件 Child.vue -->
<template>
  <button @click="sendData">向父组件传值</button>
</template>

<script setup>
  // 定义可触发的自定义事件
  const emit = defineEmits(['custom-event']);

  const sendData = () => {
    // 触发事件并传递参数
    emit('custom-event', { name: 'Vue', version: '3.x' });
  };
</script>

<!-- 父组件 Parent.vue -->
<template>
  <!-- 监听子组件自定义事件 -->
  <Child @custom-event="handleCustomEvent" />
</template>

<script setup>
  import Child from './Child.vue';

  const handleCustomEvent = (data) => {
    console.log('接收子组件数据:', data); // 输出:{ name: 'Vue', version: '3.x' }
  };
</script>


五、 扩展:事件修饰符

Vue 提供事件修饰符简化事件处理逻辑,无需手动调用 e.preventDefault()/e.stopPropagation(),常用修饰符如下:

1. 流程控制

  • .stop阻止冒泡。相当于 e.stopPropagation()
  • .prevent阻止默认行为。常用于 <a> 标签和 <form> 提交。
  • .capture:使用捕获模式触发事件。

2. 触发频率与性能

  • .once只触发一次。之后再点击将失效。

  • .passive提升性能(移动端必用)

3. 按键与鼠标修饰符

  • .enter / .esc / .space:特定按键触发。
  • .left / .right / .middle:限制特定的鼠标按键。
❌
❌