普通视图

发现新文章,点击刷新页面。
昨天 — 2025年12月16日首页

Vue 3 动态菜单渲染优化实战:从白屏到“零延迟”体验

2025年12月16日 18:10

背景与问题

在构建中后台管理系统时,动态菜单(Permission Menu)是标准功能。通常的实现流程是:

  1. 用户登录,获取 Token。
  2. 进入主页,调用用户信息接口(/api/user/permissions)。
  3. 后端返回权限列表和菜单数据。
  4. 前端根据数据动态生成路由和菜单树。
  5. 渲染侧边栏组件。

痛点: 在这个流程中,步骤 2 和 3 是异步网络请求。在请求完成前,菜单数据为空,导致侧边栏会出现短暂的空白Loading 状态。 对于用户体验来说,每次刷新页面或重新进入系统,都要忍受 200ms - 1s 的菜单“闪烁”或“白屏”,这极大地影响了系统的流畅感。

优化方案:Cache-First + 增量更新

为了解决这个问题,我们采用了类似 PWA 的 Cache-First(缓存优先) 策略,结合 增量更新(Incremental Update) 机制。

核心策略

  1. 缓存优先渲染 (Cache-First Rendering)

    • 页面初始化时,不等待网络请求,直接从 localStorage 读取上一次缓存的菜单数据进行渲染。
    • 结果:用户看到的菜单是“瞬间”加载的,零延迟。
  2. 静默后台更新 (Silent Background Update)

    • 在缓存渲染完成后,立即在后台发起用户信息请求。
    • 这是一个“静默”操作,用户无感知。
  3. 增量更新检测 (Incremental Update Check)

    • 实现细节:实现 extractMenuEssential 函数提取影响渲染的关键字段(如 id, path, name, icon, children 等),再结合 lodash-esisEqual 进行深度比对。
    • 比对原理
      • 字段筛选:只比对前端关注的 UI 字段,过滤掉后端返回的无关元数据(如 createTime, updateTime 等)。这不仅减少了数据量,也避免了因无关字段变化导致的无效渲染。
      • 结构比较:对筛选后的精简对象树进行深度递归比较。
    • 效率与准确性分析
      • 更高效:相比全量对象比对,剔除无关字段后,比对的数据量大幅减少,性能提升显著。
      • 更精准:只响应业务相关的变更。
      • 对比 Vue 3 Diff:虽然 Vue 的 Diff 已经很快,但在数据层拦截变更(Data-Level Diff)可以完全跳过组件实例的更新流程(Update Cycle),即连 VNode 都不生成,是最高效的优化手段。
    • 执行策略
      • 如果关键指纹一致:直接 return,不执行 webCache.set,也不更新响应式变量 menuList。这彻底切断了后续的 Vue 响应式链路,实现了零重绘
      • 如果指纹变更:更新缓存,并触发 Vue 的响应式更新,视图随之刷新。

技术实现

1. 增量更新逻辑 (CachePermissions.ts)

利用 extractMenuEssential 提取关键特征,结合 lodash-es/isEqual 实现高效的增量检测。

// src/composables/cache/CachePermissions.ts
import { isEqual } from 'lodash-es'

/**
 * 提取菜单关键字段用于比对
 * @description 只保留影响渲染的关键字段,忽略 createTime 等无关字段
 */
function extractMenuEssential(menu: MenuItem): Partial<MenuItem> {
  // 只提取 id, path, name, icon, children 等 UI 相关字段
  const { id, path, name, icon, children, meta, type, enabled, sort } = menu
  const result: Partial<MenuItem> = { id, path, name, icon, meta, type, enabled, sort }

  if (children && children.length > 0) {
    result.children = children.map(extractMenuEssential) as MenuItem[]
  }
  return result
}

export function setCachePermissions(userInfo: UserInfoWithPermissions): void {
  // ... 数据预处理 ...

  // 1. 构建菜单树
  const sortedMenuTree = sortMenuTree(menuTree)

  // 2. 菜单树增量更新检测
  const cachedMenuTree = webCache.get(CACHE_KEY.ROLE_ROUTERS)
  
  // 使用关键特征比对,而非全量比对
  if (isMenuTreeChanged(sortedMenuTree, cachedMenuTree)) {
    webCache.set(CACHE_KEY.ROLE_ROUTERS, sortedMenuTree)
    console.log('[Permission] 菜单数据已更新')
  } else {
    console.log('[Permission] 菜单数据无变更,跳过更新')
  }
}

2. 组件侧渲染策略 (MainMenu.vue)

组件初始化时采用同步读取缓存 + 异步更新的模式。

// src/layout/components/MainMenu/src/MainMenu.vue

/**
 * 从缓存加载并构建菜单
 */
function loadMenusFromCache() {
  const localRouters = webCache.get(CACHE_KEY.ROLE_ROUTERS)
  // ... 构建菜单 ViewModel ...
  
  // Vue 的响应式系统会自动处理 Diff,但这里我们只在数据变动时赋值更好
  // 或者依赖 Vue 3 高效的 Virtual DOM Diff
  menuList.value = finalMenuList
}

/**
 * 初始化用户数据和菜单
 * @description 采用"优先缓存,后台更新"策略
 */
async function initUserStoreAndMenus(): Promise<void> {
  // 1. 【关键】立即从缓存加载菜单,消除白屏
  loadMenusFromCache()

  // 2. 异步获取最新数据 (静默更新)
  try {
    await userStore.setUserInfoAction()
    
    // 3. 数据更新后,重新加载
    // 由于 setCachePermissions 做了增量检测,如果数据没变,
    // webCache.get 获取的引用可能没变(取决于 storage 实现),
    // 即使变了,Vue 的 diff 也能处理,但最重要的是避免了数据抖动
    loadMenusFromCache()
  } catch (e) {
    console.warn('用户信息同步失败,降级使用缓存')
  }
}

// 立即执行
initUserStoreAndMenus()

优化效果与收益分析

1. 核心指标对比

关键指标 (KPI) 优化前 (Baseline) 优化后 (Optimized) 收益 (Gain) 备注
首屏菜单可见耗时 (FMP) 300ms - 1000ms 0ms (即时) ∞ (无限提升) 彻底消除白屏等待
视觉稳定性 (CLS) 存在抖动 (Layout Shift) 极其稳定 100% 无 Loading -> Content 突变
Vue 重绘频率 (Re-render) 100% (每次刷新必重绘) < 1% (仅数据变更时) 降低 99% 节省客户端 CPU/Memory
网络容错率 0% (接口挂=菜单挂) 99.9% (接口挂=用旧菜单) 高可用 离线/弱网可用

2. 流程对比 (Mermaid)

优化前:串行阻塞渲染

sequenceDiagram
    participant U as 用户
    participant P as 页面(Vue)
    participant A as API
    
    U->>P: 进入页面
    P->>P: 渲染框架(无菜单)
    Note right of P: ❌ 此时菜单区域空白
    P->>A: 请求权限接口
    A-->>P: 返回数据 (300ms)
    P->>P: 生成菜单树
    P->>P: 渲染菜单DOM
    Note right of P: ✅ 此时才显示菜单

优化后:并行非阻塞渲染

sequenceDiagram
    participant U as 用户
    participant P as 页面(Vue)
    participant C as 本地缓存
    participant A as API
    
    U->>P: 进入页面
    P->>C: 读取缓存菜单
    C-->>P: 返回旧数据
    P->>P: **立即渲染菜单**
    Note right of P: ✅ 菜单瞬间可见 (0ms)
    
    par 静默更新
        P->>A: 请求最新权限
        A-->>P: 返回新数据
        P->>P: **增量比对(Diff)**
        alt 数据有变更
            P->>C: 更新缓存
            P->>P: 触发Vue更新视图
        else 数据无变更
            P->>P: ⛔ 拦截更新(无重绘)
        end
    end

总结

对于读多写少(Read-heavy, Write-rarely)的数据,如菜单、字典、配置项,“缓存优先 + 增量更新” 是提升用户体验的黄金法则。它将网络延迟从用户感知的关键路径(Critical Path)中移除,让 Web 应用拥有了原生应用般的流畅度。

Vue自定义拖拽指令架构解析:从零到一实现元素自由拖拽

作者 青青很轻_
2025年12月16日 18:07

在Vue项目开发中,拖拽功能是提升用户体验的重要交互方式。本文将详细介绍如何基于Vue自定义指令实现一个功能完整的拖拽组件,让你轻松为元素添加拖拽能力。

Tips: 如果时间急迫,可以忽略文中介绍,直接划到代码处,CV代码即可实现拖拽功能

一、拖拽指令的核心原理

拖拽功能的本质是通过监听鼠标事件(mousedownmousemovemouseup)或触摸事件来控制元素的位置变化。在Vue中,我们可以通过自定义指令的方式将这些逻辑封装起来,实现可复用的拖拽功能。 自定义指令的优势在于可以将拖拽逻辑与组件业务逻辑分离,使代码更加清晰和易于维护。Vue指令的生命周期钩子(如bindinsertedunbind)为我们提供了完美的集成点。

二、环境准备与项目设置

在开始之前,确保你已经创建了一个Vue项目。可以使用Vue CLI快速搭建:

# 创建Vue项目
vue create my-drag-project

# 进入项目目录
cd my-drag-project

# 启动开发服务器
npm run serve

三、基础拖拽指令实现

我们先创建一个基本的拖拽指令文件drag.js,实现最核心的拖拽功能:

import Vue from "vue";

// 自定义元素实现弹框拖拽[重点]
Vue.directive("draw", {
  inserted: function (el, bindding, vNode) {
    let left, top, width, height;
    const resetButton = el.querySelector('.reset-button'); // 获取重置按钮
    
    // 设置元素基础样式
    el.setAttribute('style', 'position: fixed; z-index: 9999;');
    el.setAttribute('draggable', true);

    // 记录初始位置
    const styleOrigin = window.getComputedStyle(el, null);
    const defaultPosition = {
      left: el.offsetLeft - parseInt(styleOrigin.marginLeft),
      top: el.offsetTop - parseInt(styleOrigin.marginTop),
    };
    
    // 拖拽开始事件处理
    el._dragstart = function (event) {
      console.log("_dragstart");
      event.stopPropagation();
      left = event.clientX - el.offsetLeft;
      top = event.clientY - el.offsetTop;
      width = el.offsetWidth;
      height = el.offsetHeight;
      
      // 显示重置按钮
      resetButton.style.display = "block";
    };
    
    // 检查位置,防止被拖出边界
    el._checkPosition = function () {
      let width = el.offsetWidth;
      let height = el.offsetHeight;
      let left = Math.min(el.offsetLeft, document.body.clientWidth - width);
      left = Math.max(0, left);
      let top = Math.min(el.offsetTop, document.body.clientHeight - height);
      top = Math.max(0, top);
      
      el.style.left = left + 'px';
      el.style.top = top + 'px';
    };
    
    // 拖拽结束事件处理
    el._dragEnd = function (event) {
      event.stopPropagation();
      left = event.clientX - left;
      top = event.clientY - top;
      
      el.style.left = left + 'px';
      el.style.top = top + 'px';
      el.style.marginTop = '0px';
      
      el._checkPosition(); // 最终位置检查
    };

    // 窗口大小变化时重置位置
    el._resetPosition = function () {
      el.style.marginTop = '0px';
    };

    // 重置按钮点击事件
    resetButton.addEventListener("click", function () {
      // 重置元素到默认位置
      el.style.left = defaultPosition.left + "px";
      el.style.top = defaultPosition.top + "px";
      resetButton.style.display = "none";
    });

    // 允许拖拽放置
    document.body.addEventListener("dragover", function (event) {
      event.preventDefault();
    });

    // 绑定事件监听器
    el.addEventListener('dragstart', el._dragstart);
    el.addEventListener('dragend', el._dragEnd);
    window.addEventListener('resize', el._resetPosition);
  },

  // 指令解绑时清理资源
  unbind: function (el, bindding, vNode) {
    el.removeEventListener("dragstart", el._dragstart);
    el.removeEventListener("dragend", el._dragEnd);
    window.removeEventListener("resize", el._checkPosition);
  },
});

四、注册和使用拖拽指令

1. 全局注册指令

main.js中全局注册指令,使其在整个项目中可用:

import Vue from 'vue';
import App from './App.vue';

// 导入拖拽指令
import './directives/drag';

new Vue({
  render: h => h(App)
}).$mount('#app');

2. 在组件中使用

在Vue组件中,只需简单添加v-draw指令即可使元素可拖拽:

<template>
  <div id="app">
    <div v-draw class="draggable-box">
      <h3>可拖拽的盒子</h3>
      <p>按住并拖动我可以移动位置</p>
      <button class="reset-button" style="display:none;">重置位置</button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
.draggable-box {
  width: 300px;
  padding: 20px;
  background-color: #f5f5f5;
  border: 1px solid #ddd;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  cursor: move;
}

.reset-button {
  margin-top: 10px;
  padding: 5px 10px;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.reset-button:hover {
  background-color: #45a049;
}
</style>

五、自定义指令实现拖拽的优势

使用Vue自定义指令实现拖拽功能相比传统方法或第三方库具有多重优势:

1. 代码复用性与维护性

自定义指令允许你将重复的DOM操作封装成一个独立的指令,从而实现代码的复用。这样可以避免同样的代码在多个组件中重复出现,提高代码的可读性和可维护性。

2. 逻辑分离与关注点分离

将DOM操作逻辑从组件的业务逻辑中分离出来,使组件更专注于数据和状态的管理,而不是处理DOM事件。这样可以使组件的代码更加简洁和清晰,同时也使得DOM操作逻辑更容易被测试和调试。

3. 轻量级解决方案

相比于引入完整的拖拽库,自定义指令只需几十行代码就能实现核心功能,减少了项目体积。指令可以应用到任意元素上,不需要改变原有组件结构,维护起来更加方便。

4. 灵活性与可定制性

通过自定义指令,你可以完全控制拖拽的每个细节,包括拖拽手柄、边界限制、动画效果等。这种灵活性是使用第三方库难以达到的。

5. 性能优化

指令内部会正确管理事件监听器的绑定和解绑,避免内存泄漏问题。同时,由于不需要加载外部库,减少了资源请求,提升了页面加载速度。

六、实际应用场景

自定义拖拽指令在实际项目中有广泛的应用场景:

  1. 弹窗拖拽:最常见的应用场景,让用户可以自由移动对话框位置。
  2. 看板应用:在任务看板中拖动卡片到不同列。
  3. 设计工具:让用户可以自由调整设计元素的位置。
  4. 游戏开发:简单游戏中的可拖动元素实现。
  5. 仪表盘定制:允许用户自由排列仪表盘中的各个组件。

七、总结

通过本文的详细介绍,我们实现了一个功能完整的Vue拖拽指令,并深入探讨了其优势和使用注意事项。自定义指令的方式实现拖拽功能,不仅代码简洁、性能优异,而且具有极高的灵活性和可维护性。 相比引入第三方拖拽库,自定义指令方案具有明显的轻量级优势,减少了项目依赖和打包体积。同时,由于代码完全可控,可以根据具体业务需求进行定制化开发,不受第三方API限制。 在实际项目中,建议根据具体需求对示例代码进行扩展,例如添加拖拽手柄、动画效果、拖拽边界限制等高级功能。通过合理利用Vue自定义指令的特性,可以大大提升开发效率和用户体验。

教你快速从Vue 开发者 → React开发者转变!

作者 叁两
2025年12月15日 16:43

前言

工作这么多年,一直用的都是vue,对vue框架也最熟悉,但最近想深入学习react,之前也学过,只懂一点皮毛,对很多写法还是不理解,我就在想既然我比较熟悉vue,那能不能设计一份react和vue的转化总结,这样用理解vue的方式来学习react那就事半功倍了。

现在AI这么方便,我就把我的需求说给chatGPT了,他帮我设计了一份Vue 开发者 → React 转化总结与对照表和一份学习计划。

真的一目了然,特别好理解,在上周末我根据这两份计划很快的上手了react框架,也顺利完成一个小型的react项目,这次没有一知半解!

虽然AI可以帮我写代码,但我们自己还是得掌握,不然可能连代码都看不懂,怎么去进行调试呢?

AI帮我们写90%,那剩下10%就要自己来了!

AI真的很好用,它可以帮助我们快速学习任何我们想学习的,还是使用最简单易上手的方式!

以下就是对照表以及学习计划了,希望对想快速上手react的小伙伴有点借鉴意义!

核心思想对比

Vue

  • 使用模板(HTML-like template)
  • 自动依赖追踪的响应式系统
  • 指令系统(v-if / v-for / v-model)
  • 双向绑定常见
  • 更“框架化”,封装度高

React

  • UI = f(state) 的函数式思想
  • 使用 JSX(模板+JS融合)
  • 没有自动依赖追踪,需要手动声明(useEffect)
  • 单向数据流
  • 更灵活、更偏底层

API 对照表

核心 API 对照

Vue React 说明
data() useState 声明组件状态
computed useMemo 计算属性
watch useEffect 监听值变化或模拟生命周期
方法(methods) 普通函数 不需要特殊 API
ref() useRef DOM 或变量引用
provide / inject createContext + useContext 跨组件传递数据
v-model onChange + useState 双向绑定自己实现
v-if JS 表达式(条件渲染) { condition && <Component /> }
v-for arr.map() 列表渲染
组件通信(props) props 完全一致
子组件事件(emit) 父传入回调函数 回调作为 props

生命周期迁移

Vue → React 生命周期对照表

Vue React
onMounted useEffect(() => {}, [])
onUpdated useEffect(() => {})
onUnmounted useEffect(() => return cleanup, [])
onActivated 无,需要自定义
onDeactivated 无,需要自定义
beforeMount / beforeUpdate 无(通常不需要)

示例:Vue → React 生命周期对比

Vue
<script setup>
onMounted(() => console.log("mounted"))
onUnmounted(() => console.log("unmounted"))
</script>
React
useEffect(() => {
  console.log("mounted");
  return () => console.log("unmounted");
}, []);

状态管理迁移

Vue → React 状态管理选择

Vue React 等价方案 特点
Pinia Zustand 写法最像,轻量好用(推荐)
Vuex Redux Toolkit 官方推荐、企业级
composables 自定义 Hooks 逻辑复用方式几乎一样
reactive() useState/useReducer 响应式状态

Zustand(最适合 Vue 开发者)

Zustand 使用方式类似 Pinia,推荐入门 React 状态管理就用它。

示例:Zustand vs Pinia

Pinia

export const useUserStore = defineStore('user', {
  state: () => ({ name: 'Echo' }),
  actions: { setName(name) { this.name = name } }
})

Zustand

const useUserStore = create((set) => ({
  name: "Echo",
  setName: (name) => set({ name }),
}));

几乎一模一样。


路由迁移

Vue Router → React Router 对照表

Vue Router React Router
routes 数组 <Routes><Route/></Routes>
router.push() useNavigate()
获取参数 useParams()
路由守卫 自定义鉴权组件包裹 Route

Vue 路由

const routes = [
  { path: '/user/:id', component: User }
]

React 路由

<Routes>
  <Route path="/user/:id" element={<User />} />
</Routes>

读取参数:

const { id } = useParams()

项目结构对照

Vue 项目结构

src/
  components/
  views/
  store/
  router/
  composables/
  assets/

React 推荐结构

src/
  components/     # 公共组件
  pages/          # 页面级组件
  hooks/          # 自定义 Hooks
  store/          # 状态管理(Zustand / Redux)
  router/         # 路由定义
  services/       # API 请求封装
  assets/

思维模型转化总结

1. 模板 → JSX(最大的差异)

Vue:

<v-if="ok">hello</v-if>

React:

{ok && <div>hello</div>}

2. 自动响应式 → 显式依赖声明

Vue 自动追踪
React 必须写依赖数组

3. 逻辑复用方式变化

Vue compositions → React 自定义 Hooks

4. 组件通信完全一致(props)

但事件改为父传回调函数

5. React 更纯粹、更 JavaScript 化

少魔法、少黑盒、更多掌控权。


学习计划(适合 Vue 开发者)

📘 阶段 1:核心理念与基础 JSX

⭐ 核心目标

  • 理解 React 哲学(UI = f(state))
  • 能用 JSX 编写组件、列表、事件

✅ 实战 1:Hello React + 计数器

1. 新建项目(Vite)

npm create vite@latest react-basic --template react
cd react-basic
npm install
npm run dev

2. 编写一个计数器(App.jsx)

import { useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <div style={{ padding: 20 }}>
      <h1>计数器</h1>
      <p>当前值:{count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
}

📝 Vue 对照:

<script setup>
const count = ref(0)
</script>

<button @click="count++">{{ count }}</button>

🌱 实践任务

  • 写一个 从 1 增加到 100 的进度条动画
  • 写一个 列表渲染组件(todoList)

📘 阶段 2:Hooks 深入

⭐ 核心要点

  • useState
  • useEffect(替代 watch / 生命周期)
  • useMemo / useCallback(性能)
  • useRef(替代 Vue的模板 ref)
  • 自定义 hooks(对 Vue 用户最重要)

✅ 实战 2:useEffect 生命周期模拟

Vue → React 生命周期对照

Vue React
onMounted useEffect(() => {}, [])
onUpdated useEffect(() => {})
onUnmounted return () => {}

例子:监听窗口大小

import { useState, useEffect } from "react";

export default function WindowSize() {
  const [size, setSize] = useState({ w: window.innerWidth, h: window.innerHeight });

  useEffect(() => {
    const onResize = () => {
      setSize({ w: window.innerWidth, h: window.innerHeight });
    };
    window.addEventListener("resize", onResize);

    return () => window.removeEventListener("resize", onResize);
  }, []);

  return <div>窗口大小:{size.w} x {size.h}</div>;
}

✅ 实战 3:自定义 Hook(重要)

Vue 组合式 API 与 React 自定义 hook 是一样的概念。

useFetch.js

import { useState, useEffect } from "react";

export function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(url)
      .then((r) => r.json())
      .then((json) => {
        setData(json);
        setLoading(false);
      });
  }, [url]);

  return { data, loading };
}

使用它

import { useFetch } from "./useFetch";

export default function UserList() {
  const { data, loading } = useFetch("https://jsonplaceholder.typicode.com/users");

  if (loading) return <p>加载中...</p>;
  return (
    <ul>
      {data.map((u) => <li key={u.id}>{u.name}</li>)}
    </ul>
  );
}

🌱 实践任务

  • 写一个 useLocalStorage(key, defaultValue)
  • 写一个 useCountdown(秒)
  • 写一个 useDebounce(value, delay)

📘 阶段 3:React Router


✅ 项目结构

src/
  pages/
    Home.jsx
    Detail.jsx
  main.jsx
  App.jsx

安装

npm install react-router-dom

✅ 实战 4:配置基础路由

import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import Detail from "./pages/Detail";

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/detail/:id" element={<Detail />} />
      </Routes>
    </BrowserRouter>
  );
}

Detail.jsx

import { useParams } from "react-router-dom";

export default function Detail() {
  const { id } = useParams();
  return <h2>详情页面 - ID: {id}</h2>;
}

🌱 实践任务

  • 写一个 商品列表页 + 商品详情页
  • 点击列表项跳转到详情页

📘 阶段 4:状态管理

为了更像 Vue 的 Pinia,你会更喜欢 Zustand


👉 为什么不用 Redux?

  • Redux 心智负担大(action/reducer/immutable)
  • Zustand 更像 Pinia:简单、轻巧、API优雅

📦 安装 Zustand

npm install zustand

✅ 实战 5:创建用户 store(类似 Pinia)

src/store/user.js

import { create } from "zustand";

export const useUserStore = create((set) => ({
  name: "Echo",
  setName: (name) => set({ name }),
}));

在组件里使用

import { useUserStore } from "../store/user";

export default function Profile() {
  const { name, setName } = useUserStore();

  return (
    <div>
      <p>用户名:{name}</p>
      <input onChange={(e) => setName(e.target.value)} />
    </div>
  );
}

🌱 实践任务

  • 给 store 增加:token / userInfo / logout
  • 全局 Layout 读取用户信息

📘 阶段 5:接口请求与工程化


推荐:axios 封装 + useRequest Hook


✅ 实战 6:axios 封装

/src/api/request.js

import axios from "axios";

const request = axios.create({
  baseURL: "https://api.example.com",
  timeout: 8000,
});

request.interceptors.response.use((res) => res.data);

export default request;

使用 useRequest Hook(可复用)

import { useEffect, useState } from "react";
import request from "../api/request";

export function useRequest(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    request(url).then((res) => {
      setData(res);
      setLoading(false);
    });
  }, [url]);

  return { data, loading };
}

📘 阶段 6:综合项目

做一个 后台管理系统模版(React)


功能组成

  • 登录页
  • Layout 布局(侧边栏 + Header)
  • react-router 路由守卫
  • 表格 CRUD(增删改查)
  • Zustand 全局状态(保存用户)
❌
❌