普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月31日掘金 前端
昨天 — 2026年1月30日掘金 前端

React-Hooks逻辑复用艺术

2026年1月30日 17:38

前言

在 React 开发中,Hooks 的出现彻底改变了逻辑复用的方式。它让我们能够将复杂的、可复用的逻辑从 UI 组件中抽离,实现真正的“关注点分离”。本文将分享 Hooks 的核心原则,并提供 4 个在真实业务场景中封装的实战案例。

一、 Hooks 核心

1. 概念理解

Hooks 本质上是将组件间共享的逻辑抽离并封装成的特殊函数

2. 使用“红线”:规则与原理

  • 命名规范:必须以 use 开头(如 useChat),这不仅是约定,也是静态检查工具(ESLint)识别 Hook 的依据。
  • 调用位置严禁在循环、条件判断或嵌套函数中调用 Hook

底层原理: React 内部并不是通过“变量名”来记录 Hook 状态的,而是通过链表 。每次渲染时,React 严格依赖 Hook 的调用顺序来查找对应的状态。

注意: 如果在 if 语句中调用 Hook,一旦条件不成立导致某次渲染跳过了该 Hook,整个链表的指针就会错位,导致状态读取异常。

二、 实战:自定义 Hooks 封装

1. AI 场景:消息点赞/点踩逻辑 (useChatEvaluate)

在 AI 对话系统中,消息评价是通用功能。我们需要处理:状态切换(点赞 -> 取消点赞)、单选逻辑、以及异步接口调用。

import React, { useState } from 'react';

// 模拟接口
const public_evaluateMessage = async (params: any) => ({ data: true });

type EvaluateType = "GOOD" | "BAD" | "NONE";

export const useChatEvaluate = (initialType: EvaluateType = "NONE") => {
  const [ratingType, setRatingType] = useState<EvaluateType>(initialType);

  const evaluateMessage = async (contentId: number, type: "GOOD" | "BAD") => {
    let newEvaluateType: EvaluateType;

    // 逻辑:如果点击已选中的类型,则取消选中(NONE);否则切换到新类型
    if (type === "GOOD") {
      newEvaluateType = ratingType === "GOOD" ? "NONE" : "GOOD";
    } else {
      newEvaluateType = ratingType === "BAD" ? "NONE" : "BAD";
    }

    try {
      const res = await public_evaluateMessage({
        contentId,
        ratingType: newEvaluateType,
        content: "",
      });

      if (res.data === true) {
        setRatingType(newEvaluateType);
      }
    } catch (error) {
      console.error("评价失败:", error);
    }
  };

  return { ratingType, evaluateMessage };
};

// 使用示例
const ChatMessage: React.FC<{ id: number }> = ({ id }) => {
  const { ratingType, evaluateMessage } = useChatEvaluate();
  return (
    <button onClick={() => evaluateMessage(id, "GOOD")}>
      {ratingType === "GOOD" ? "👍 已点赞" : "👍 点赞"}
    </button>
  );
};

2. 响应式布局:屏幕尺寸监听 (useMediaSize)

在响应式系统中,封装一个能根据窗口宽度自动切换“设备类型”的 Hook,可以极大地简化响应式开发。

import { useState, useEffect, useMemo } from 'react';

export enum MediaType {
  mobile = 'mobile',
  tablet = 'tablet',
  pc = 'pc',
}

const useMediaSize = (): MediaType => {
  const [width, setWidth] = useState<number>(globalThis.innerWidth);

  useEffect(() => {
    const handleWindowResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleWindowResize);
    // 记得清理事件监听
    return () => window.removeEventListener('resize', handleWindowResize);
  }, []);

  // 使用 useMemo 避免每次渲染都重新运行计算逻辑
  const media = useMemo(() => {
    if (width <= 640) return MediaType.mobile;
    if (width <= 768) return MediaType.tablet;
    return MediaType.pc;
  }, [width]);

  return media;
};

export default useMediaSize;

3. 性能优化:防抖与节流 Hook

A. 防抖 Hook (useDebounce)

常用于搜索框,防止用户快速输入时频繁触发请求。

import { useState, useEffect } from 'react';

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // 关键:在下一次 useEffect 执行前清理上一次的定时器
    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}

export default useDebounce;

B. 节流 Hook (useThrottle)

常用于滚动加载、窗口缩放,确保在规定时间内只执行一次。

import { useState, useEffect, useRef } from 'react';

function useThrottle<T>(value: T, delay: number): T {
  const [throttledValue, setThrottledValue] = useState<T>(value);
  const lastExecuted = useRef<number>(Date.now());

  useEffect(() => {
    const now = Date.now();
    const remainingTime = delay - (now - lastExecuted.current);

    if (remainingTime <= 0) {
      // 立即执行
      setThrottledValue(value);
      lastExecuted.current = now;
    } else {
      // 设置定时器处理剩余时间
      const timer = setTimeout(() => {
        setThrottledValue(value);
        lastExecuted.current = Date.now();
      }, remainingTime);

      return () => clearTimeout(timer);
    }
  }, [value, delay]);

  return throttledValue;
}

export default useThrottle;

三、 总结:封装自定义 Hook 的心法

  1. 抽离状态而非仅逻辑:如果一段逻辑只涉及纯函数计算,不需要 Hook;只有涉及 useStateuseEffect 等状态管理时,才有必要封装 Hook。
  2. 保持纯净:自定义 Hook 应该只关心逻辑,而不应该直接操作 DOM。
  3. TS 类型保护:利用泛型 <T> 增强 Hook 的兼容性,让它能适配各种数据类型。

一个Vite插件实现PC到移动端项目的高效迁移方案

作者 浮幻云月
2026年1月30日 17:24

当PC端项目需要迁移到移动端时,你是否还在手动复制粘贴代码?今天分享一个我们团队自研的Vite插件,帮你实现跨仓库、跨技术栈的代码高效复用!

背景:从PC到移动端的迁移之痛

最近我们团队遇到了一个典型的企业级需求:将一个成熟的PC端医疗管理系统(MDM)迁移到移动端。听起来简单,但实际上却面临诸多挑战:

面临的挑战

  1. 技术栈差异:PC端使用Element Plus,移动端需要Vant
  2. 仓库隔离:PC端和移动端在不同的Git仓库
  3. 代码复用:希望复用80%的业务逻辑,但UI组件完全不同
  4. 维护同步:业务逻辑更新需要在两端同步

传统的解决方案要么是完全重写(成本高),要么是复制粘贴(维护噩梦),或者是Monorepo (代码和依赖放一起)。我们需要的是一种既能复用核心逻辑,代码在不同仓库维护,又能灵活定制UI的解决方案。

解决方案:vite-plugin-code-reuse

基于这个需求,我们开发了 vite-plugin-code-reuse 插件,它的核心思想是:多仓库代码复用、智能替换、无缝集成

插件核心能力

// 插件的核心配置结构
interface PluginConfig {
  repositories: RepositoryConfig[];     // 代码仓库配置
  repoImportMappings: RepoImportMappings[]; // 路径映射,主要是规避不同仓库路径别名冲突问题
  importReplacements: ImportReplacementConfig[]; // 导入替换
}

实战案例:医疗管理系统迁移

让我通过实际案例展示这个插件的效果。

项目背景

  • PC端项目xlian-web-mdm(Vue 3 + Element Plus + TypeScript)
  • 移动端项目:新建的H5项目(Vue 3 + Vant + TypeScript)
  • 目标:复用PC端80%的业务逻辑,100%替换UI组件

第一步:安装配置插件

npm install vite-plugin-code-reuse --save-dev
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import codeReusePlugin from 'vite-plugin-code-reuse'
import path from 'path'

export default defineConfig({
  plugins: [
    vue(),
    codeReusePlugin({
      // 1. 引入PC端代码仓库
      repositories: [
        {
          url: 'http://gitlab.xinglianhealth.com/web/mdm/xlian-web-mdm.git',
          branch: 'master',
          dir: 'mdm-pc'  // 本地存放目录
        }
      ],
      
      // 2. 修正路径别名映射,pc和移动都配置了“@”别名,需要各自引用正确的路径
      repoImportMappings: [
        {
          alias: '@',
          repoDir: 'mdm-pc',
          // PC端中的“@”别名还是引用pc端目录下的代码,但是需要排除路由、状态管理等路径,引用移动端的,因为路由和store不能存在两个
          ignorePatterns: ['src/router', 'src/store', 'src/axios']
        }
      ],
      
      // 3. 智能导入替换,pc端的部分组件无法复用时,可以做替换
      importReplacements: [
        // 场景1:组件替换(Element Plus → Vant)
        {
          pattern: /\/views\/MasterData\/components\/UploadFile\.vue$/,
          file: 'src/components/MbUpload.vue' // 移动端专用上传组件
        },
        {
          pattern: /\/views\/MasterData\/components\/TextInput\.vue$/,
          file: 'src/components/MbTextInput.vue' // 移动端输入框
        },
        
        // 场景2:复杂表单组件替换
        {
          pattern: /\/views\/MasterData\/components\/MasterForm\.vue$/,
          file: 'src/components/MbForm.vue' // 移动端表单封装
        },
        
        // 场景3:PC端特有功能在移动端隐藏
        {
          pattern: /\/views\/MasterData\/components\/HeaderFilter\/HeaderFilter\.vue$/,
          code: '<template></template>' // 移动端不需要复杂的表头筛选
        },
        
        // 场景4:UI库替换
        {
          pattern: /^@?element-plus/,
          resolve: path.resolve('src/components/ElementPlus.ts') // 重定向到兼容层
        }
      ]
    })
  ]
})

第二步:UI组件替换

// src/components/ElementPlus.ts - UI库兼容层
import { Button, Input, Select } from './ui-adaptor'

// 创建Element Plus到Vant的映射
export {
  ElButton: Button,
  ElInput: Input,
  ElSelect: Select,
  // ... 更多映射
}

// src/components/ui-adaptor.ts - UI库适配层,以Button组件为例

<template><van-button v-bind="$attrs" /></template>
<script>
const props = defineProps({
// 对element-plus Button Props做适配,比如size不支持"medium",可以处理成其他的,相同的属性不用处理
})
</script>

第三步:复用业务逻辑组件

<!-- src/views/MasterData/PatientList.vue -->
<template>
  <!-- 复用PC端的模板结构,但使用移动端组件 -->
  <div class="patient-list">
    <!-- 搜索框(PC端是ElInput,自动替换为Vant Field) -->
    <van-field 
      v-model="searchKey"
      placeholder="搜索患者"
      @input="handleSearch"
    />
    
    <!-- 患者列表 -->
    <van-list 
      v-model:loading="loading"
      :finished="finished"
      @load="loadPatients"
    >
      <patient-item 
        v-for="patient in patients"
        :key="patient.id"
        :patient="patient"
        @click="handlePatientClick"
      />
    </van-list>
  </div>
</template>

<script setup lang="ts">
// 这里直接复用PC端的业务逻辑!
import { usePatientList } from '@/mdm-pc/src/views/MasterData/composables/usePatientList'

// 完全复用PC端的逻辑,包括:
// 1. 数据获取逻辑
// 2. 搜索过滤逻辑  
// 3. 分页逻辑
// 4. 事件处理逻辑
const {
  patients,
  loading,
  finished,
  searchKey,
  loadPatients,
  handleSearch,
  handlePatientClick
} = usePatientList()
</script>

<style scoped>
/* 移动端特有的样式 */
.patient-list {
  padding: 12px;
}
</style>

插件的核心实现原理

1. 代码仓库管理

vite build start开始时,自动拉取PC端仓库代码,如无更新不会重复拉取,默认只拉取一个commit,速度极快。

{
  url: 'http://gitlab.xinglianhealth.com/web/mdm/xlian-web-mdm.git', //仓库地址
  branch: 'master', // 仓库分支
  dir: 'mdm-pc'  // 本地存放目录
}

image.png


### 2. 智能导入替换,支持三种替换方式,任意选择

```typescript
interface ImportReplacementConfig {
    /**
     * 匹配模式(正则表达式字符串)
     */
    pattern: RegExp;
    /**
     * 替换方式:代码字符串
     */
    code?: string;
    /**
     * 替换方式:文件路径
     */
    file?: string;
    /**
     * 替换方式:重定向路径
     */
    resolve?: string;
}

3. 别名路径冲突映射修正,将被引用的仓库内部别名指向仓库内部,规避和外层别名的冲突

interface RepoImportMappings {
    /**
     * 路径别名
     */
    alias: string;
    /**
     * 冲突的仓库目录
     */
    repoDir: string;
    /**
     * 忽略列表(路径匹配时跳过),使用外出仓库别名解析
     */
    ignorePatterns?: string[];
}

实际效果对比

迁移前(传统方式)

├── 移动端项目
│   ├── src
│   │   ├── views
│   │   │   ├── PatientList.vue    ← 需要重写
│   │   │   ├── DoctorList.vue     ← 需要重写
│   │   │   └── ... (20+个页面)
│   │   └── components
│   │       ├── MbForm.vue         ← 需要重写
│   │       └── ... (50+个组件)
└── 工时:3-4人月

迁移后(使用插件)

├── 移动端项目
│   ├── mdm-pc/                    ← 自动引入的PC端代码
│   ├── src
│   │   ├── views
│   │   │   ├── PatientList.vue    ← 直接引用PC端PatientList组件,只对样式做适配覆盖
│   │   │   ├── DoctorList.vue     ← 直接引用PC端DoctorList组件,只对样式做适配覆
│   │   │   └── ... (复用80%)
│   │   └── components
│   │       ├── MbUploader.vue     ← 无法复用的组件,用移动端专用文件组件做替换
│   │       └── ... (只需写30%的组件)
└── 工时:1-1.5人月(节省60%+)

高级应用场景

场景1:多项目共享工具库

// 多个项目共享工具函数
codeReusePlugin({
  repositories: [
    {
      url: 'git@github.com:company/shared-utils.git',
      branch: 'main',
      dir: 'shared-utils'
    },
    ...
  ],
  importReplacements: [
    {
      pattern: /^@shared\/utils/,
      resolve: path.resolve('shared-utils/src')
    },
    ...
  ]
})

场景2:主题系统切换

// 根据环境切换主题
codeReusePlugin({
  importReplacements: [
    {
      pattern: /\/themes\/default/,
      file: process.env.NODE_ENV === 'mobile' 
        ? 'src/themes/mobile.vue'
        : 'src/themes/pc.vue'
    }
  ]
})

场景3:A/B测试版本

// A/B测试不同版本的组件
codeReusePlugin({
  importReplacements: [
    {
      pattern: /\/components\/CheckoutButton\.vue$/,
      file: Math.random() > 0.5 
        ? 'src/components/CheckoutButtonVariantA.vue'
        : 'src/components/CheckoutButtonVariantB.vue'
    }
  ]
})

总结

vite-plugin-code-reuse 插件为我们解决了跨项目代码复用的核心痛点:

🎯 核心价值

  1. 大幅提升开发效率:节省60%+的开发时间
  2. 保证代码一致性:业务逻辑完全一致,减少BUG
  3. 降低维护成本:一处修改,多处生效
  4. 灵活定制:UI层完全可定制,不影响核心逻辑

🚀 适用场景

  • PC端到移动端的项目迁移
  • 多项目共享组件库
  • 微前端架构中的模块复用
  • A/B测试和实验性功能

如果你也面临类似的多项目代码复用问题,不妨试试这个插件。项目已在GitHub开源,欢迎Star和贡献代码!

GitHub地址vite-plugin-code-reuse

让代码复用变得简单,让开发效率飞起来! 🚀

React-Scheduler 调度器如何掌控主线程?

2026年1月30日 17:21

前言

在 React 18 的并发时代,Scheduler(调度器) 是实现非阻塞渲染的幕后英雄。它不只是 React 的一个模块,更是一个通用的、高性能的 JavaScript 任务调度库。它不仅让 React 任务可以“插队”,还让“长任务”不再阻塞浏览器 UI 渲染。

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

Scheduler 是一个独立的包,它通过与 React 协调过程(Reconciliation)的紧密配合,实现了任务的可中断、可恢复、带优先级执行。

主要职责

  1. 优先级管理:根据任务紧急程度(如用户点击 vs 数据预取)安排执行顺序。
  2. 空闲时间利用:在浏览器每一帧的空闲时间处理不紧急的任务。
  3. 防止主线程阻塞:通过“时间片(Time Slicing)”机制,避免长任务导致页面假死。

二、 Scheduler 的完整调度链路

当一个 setState 触发后,Scheduler 内部会经历以下精密流程:

1. 任务创建与通知

当状态更新时,React 不会立即执行 Render。它首先会创建一个 Update对象来记录这次变更,这个对象中包含这次更新所需的全部信息,例如更新后的状态值,Lane车道模型分配的任务优先级.

2. 优先级排序与队列维护

  • 任务优先级排序: 创建更新后,react会调用scheduleUpdateOnFiber函数通知scheduler调度器有个一个新的任务需要调度,这时scheduler会对该任务确定一个优先级,以及过期时间(优先级越高,过期时间越短,表示越紧急)

  • 队列维护: 接着scheduler会将该任务放入到循环调度中,scheduler对于任务循环调度在内部维护着两个队列,一个是立即执行队列taskQueue和延迟任务队列timeQueue,新任务会根据优先级进入到相应对列

    • timerQueue(延时任务队列) :存放还未到开始时间的任务,按开始时间排序。
    • taskQueue(立即任务队列) :存放已经就绪的任务,按过期时间排序。优先级越高,过期时间越短。

3. 时间片的开启:MessageChannel

将任务放入队列后,scheduler会调用requetHostCallback函数去请求浏览器在合适的时机去执行调度,该函数通过 MessageChannel对象中的port.postMessage 方法创建一个宏任务,浏览器在下一个宏任务时机触发 port.onmessage,并在这宏任务回调中启动 workLoop函数。

补充:Scheduler 会调用 requestHostCallback 请求浏览器调度。它没有选择 setTimeout,而是选择了 MessageChannel

为什么选 MessageChannel? setTimeout(fn, 0) 在浏览器中通常有 4ms 的最小延迟,且属于宏任务中执行时机较晚的。MessageChannelport.postMessage 产生的宏任务执行时机更早,且能更精准地在浏览器渲染帧之间切入。

4. 工作循环:workLoop

  • 在宏任务回调中,调度器会进入 workLoop。它会调用performUnitOfWork函数循环地处理Fiber节点,对比新旧节点的props、state,并从队列中取出最紧急的任务交给 React 执行。

  • workLopp中会包含一个shouldYield函数中断检查函数,用于检查当前时间片是否耗尽以及是否有更高优先级的任务执行,如果有的话则会将主线程控制权交还给浏览器,以保证高优先级任务(如用户输入、动画)能及时响应。


5. 中断与恢复:shouldYield 的魔力

workLoop 执行过程中,每一项单元工作完成后,都会调用 shouldYield() 函数进行“路况检查”。

  • 中断条件:如果当前时间片(通常为 5ms)耗尽,或者检测到有更紧急的用户交互(高优任务插队),shouldYield 返回 true
  • 状态保存:此时 React 会记录当前 workInProgress 树的位置,将控制权交还给浏览器。
  • 任务恢复:Scheduler 会在下一个时间片通过 MessageChannel 再次触发,从记录的位置继续执行,从而实现可恢复。

6. 任务插队

如果在执行一个低优先级任务时,有高优先级任务加入(如用户突然点击按钮),Scheduler会中断当前的低优任务并记录该位置,先执行高优任务。等高优任务完成后,再重新执行或继续之前的低优任务


三、 补充

  1. 执行时机对比MessageChannel 确实在宏任务中非常快,但在某些极其特殊的情况下(如没有 MessageChannel 的旧环境),它会回退到 setTimeout
  2. 饥饿现象防止:如果一个低优先级任务一直被插队怎么办?Scheduler 通过过期时间解决。一旦任务过期,它会从 taskQueue 中被提升为同步任务,强制执行。

深度解析 React Router v6:构建企业级单页应用(SPA)的全栈式指南

作者 San30
2026年1月30日 17:12
在 Web 开发的演进史中,从早期的多页应用(MPA)到现代的单页应用(SPA),我们见证了前端工程师角色的巨大转变。曾几何时,前端开发被戏称为“切图仔”,路由和页面跳转的控制权完全掌握在后端手中。每

vite+vue3+antd4项目兼容低版本chrome86+ && chrome68+

2026年1月30日 17:05

兼容性问题解决方案汇总

一、兼容到chrome86+

1.antd4 向下兼容方案

1、文档流中的样式和位置问题,主入口app.vue添加如下内容, 解决方案:

App组件容易漏,会导致message提示无法显示,需要注意~

import { ConfigProvider, App } from 'ant-design-vue';
import { StyleProvider, legacyLogicalPropertiesTransformer } from 'ant-design-vue/es/_util/cssinjs';
        <StyleProvider
          hash-priority="high"
          :prefix="configProviderPrefixCls"
          :transformers="[legacyLogicalPropertiesTransformer]"
        >
          <a-style-provider
            hash-priority="high"
            :prefix="configProviderPrefixCls"
            :transformers="[legacyLogicalPropertiesTransformer]"
          >
            <App>              
            <RouterView />
            </App>
          </a-style-provider>
        </StyleProvider>

2、  全局弹出、全局提示等脱离文档流的位置问题。 解决方案:

import { App } from 'ant-design-vue';
使用 const { modal } = App.useApp(); 替换Modal.confirm等,message alert 同理
 

 

3、  前缀是antd,没有被prefix前缀处理到的组件位置问题(如画布节点) 解决方案:采用问题一的解决方案,把画布自定义节点包裹一次

4、  使用creatApp或者creatVnode创建的模块,脱离了vue的上下文文档流

解决方案:采用问题一的解决方案,把节点包裹一次

样式及js兼容修改

1.建议初始化配置时在vite.config中加上如下内容:

build:{
    ... 原来的内容,
    // js最低兼容的浏览器版本
    target: ['chrome86', 'edge88', 'firefox78', 'safari14'],
    // 禁用 CSS 代码压缩,防止 top/right/bottom/left 被转换成 inset
     cssMinify: false
 }
 

本地启动报错

1.集成unocss出现报错问题 image.png

 

注意⚠️:   如果使用了unocss或taiwindcss覆盖antd原有的样式会失效,兼容后antd的样式等级会提高。我采取的方法比较笨但可靠,在覆盖的css类名后面添加‘!’,编译后会给unocss或taiwindcss的css后面添加!important。这样能解决!

position导致的样式失效

360极速版position导致的样式失效 如果同时有top:0;left:0,bottom:0;right:0;vite会打包成一个insert:0,但是360极速版不支持该属性加上width:100%和height:100%

二、兼容到chrome68+

在兼容chrome86的基础上添加如下配置

全程基于项目根目录执行操作,核心依赖为 Vite5 官方兼容插件@vitejs/plugin-legacy+ API 补全库core-js@3,所有配置可直接复制使用,按步骤执行即可完成兼容。

前提:Chrome68 支持 ES2017,缺失 ES2018+ 新语法(?.、?? 等)和部分全局 API,兼容核心是语法转译 + 自动 polyfill 注入 + 适配构建目标

步骤 1:安装核心兼容依赖

# npm 安装
npm install @vitejs/plugin-legacy core-js @babel/core -D

步骤 2:配置 browserslist(统一所有工具的兼容目标)

在项目根目录的package.json新增browserslist字段,统一 Vite、Babel、PostCSS 的浏览器兼容规则,仅指定Chrome 68即可,修改后如下:

{
  "name": "your-vue-project", // 你的项目名
  "version": "0.0.0",
  "browserslist": ["Chrome 68"], // 新增这行,统一兼容目标
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  // 保留项目原有dependencies、devDependencies等配置...
}

步骤 3:修改 Vite 核心配置(vite.config.js/ts)

这是兼容的核心步骤,修改项目根目录的vite.config.js(ts 项目为vite.config.ts,配置完全一致),完成插件注册 + 构建目标降级 + CSS 适配,直接替换原有配置即可:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import legacy from '@vitejs/plugin-legacy' // 1. 引入legacy兼容插件

// Vite5 核心配置
export default defineConfig({
  plugins: [
    vue(), // Vue3 插件
    // 2. 配置legacy插件,自动转译语法+注入polyfill
    legacy({
      targets: 'Chrome 68', // 明确指定兼容Chrome68(也可读取browserslist)
      polyfills: true, // 开启自动polyfill注入(基于core-js@3)
      renderLegacyChunks: true, // 生成旧浏览器兼容产物
      modernPolyfills: true // 现代浏览器兜底polyfill
    })
  ],
  build: {
    target: 'es2017', // 3. 构建目标降级为Chrome68支持的ES2017(Vite5默认es2020)
    cssTarget: 'chrome68', // 4. CSS适配Chrome68,避免生成不兼容CSS语法
    minify: 'terser', // 可选:用terser压缩,避免新压缩特性导致兼容问题
    terserOptions: {
      compress: { drop_console: false } // 可选:保留console,方便调试
    }
  }
})

步骤 4:Vue3.3.0 兼容兜底(无额外配置,仅 2 个避坑点)

Vue3 本身已放弃 IE 但完全兼容 Chrome68,无需修改任何 Vue 相关配置,只需注意 2 个细节即可:

  1. 代码中避免使用 Vue3 的实验性特性(如defineModel高级用法、props解构的新特性),若使用,legacy 插件会自动转译;
  2. Vue3 模板编译产物为 ES5 级别,Chrome68 可直接解析,无需修改@vitejs/plugin-vue的编译配置。

步骤 5:执行生产构建(兼容逻辑仅对构建生效)

Vite 的 legacy 兼容处理仅在生产构建时生效(开发环境 Chrome68 已支持 ES 模块,可直接运行npm run dev开发,无需额外处理,本地还是无法访问的!!!!!),执行构建命令生成兼容包:

# 生成兼容Chrome68的生产包,输出到项目根目录的dist文件夹
npm run build

构建完成后,dist目录会自动生成 2 类产物,且index.html内置浏览器嗅探逻辑:Chrome68 会自动加载兼容产物 + polyfill,高版本浏览器加载现代产物,无需手动判断。

注意: 本地无法访问,看不到页面呈现,只有部署后才能打开!!!!!!!

步骤 6:Chrome68 兼容性测试(2 种便捷方法,无需安装旧浏览器)

方法 1:Chrome 开发者工具模拟(推荐,最快)

  1. 打开新版 Chrome 浏览器,运行npm run preview启动预览服务,打开项目预览地址;
  2. F12打开开发者工具 → 点击右上角「⋮」→ 更多工具 → 设备模拟;
  3. 左上角设备下拉框选择「自定义」→ 输入Chrome 68,刷新页面即可模拟运行。

方法 2:本地启静态服务测试

  1. 全局安装静态服务工具servenpm install serve -g
  2. 项目根目录执行:serve dist,会生成本地访问地址(如http://localhost:3000);
  3. 在模拟的 Chrome68 中访问该地址,验证页面渲染、按钮点击、接口请求等交互是否正常。

步骤 7:常见兼容问题解决(按需处理)

若测试时出现报错 / 样式错乱,按以下场景针对性解决,均为 Chrome68 兼容的高频问题:

问题 1:第三方依赖未被转译(如 Element Plus/axios 用了 ES2018 + 语法)

Vite5 默认不转译node_modules,需在vite.config.js强制指定转译的依赖,修改后如下:

export default defineConfig({
  // 保留原有配置...
  optimizeDeps: {
    include: ['element-plus', 'axios'] // 按需添加需要转译的第三方依赖
  },
  build: {
    // 保留原有配置...
    commonjsOptions: {
      include: [/element-plus/, /axios/, /node_modules/] // 强制转为CommonJS格式
    }
  }
})

问题 2:个别全局 API 未被 polyfill(如 URLSearchParams/fetch)

Chrome68 已支持大部分 API,若遇到缺失,在 src/main.js/ts 中手动引入即可:

// src/main.js(Vue入口文件)
import 'core-js/es/url-search-params' // 手动注入URLSearchParams polyfill
import 'core-js/es/fetch' // 按需注入fetch polyfill
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

问题 3:CSS 样式错乱(如高级 CSS 选择器 / 特性)

Chrome68 支持基础 CSS 变量 / Flex 布局,样式问题多为第三方 CSS 用了高级特性,安装postcss-preset-env兜底:

  1. 安装依赖:pnpm add postcss-preset-env -D
  2. 项目根目录新建postcss.config.js,添加配置:
module.exports = {
  plugins: [
    require('postcss-preset-env')({
      browsers: 'Chrome 68', // 适配Chrome68
      features: { 'custom-properties': { preserve: true } } // 保留CSS变量
    })
  ]
}

核心步骤总结

  1. 安装@vitejs/plugin-legacy@5.x + core-js@3 + @babel/core三大核心依赖;
  2. package.json中配置browserslist: ["Chrome 68"]统一兼容目标;
  3. Vite 配置中注册 legacy 插件,降级build.target为 es2017、cssTarget为 chrome68;
  4. 执行npm run build生成兼容包,通过 Chrome 开发者工具模拟 Chrome68 测试;
  5. 按需解决第三方依赖转译、手动 polyfill、CSS 适配等问题。

 

公司低代码框架-列表个性化开发最佳实践

作者 王小菲
2026年1月30日 17:03

一、引言

当前低代码组件的功能框架已趋于稳定,而业务侧的需求设计却持续迭代、不断涌现。要落地各类个性化需求,正需要我们秉持‘人有多大胆,地有多大产’的探索精神,勇于构思、大胆尝试。比如低代码列表中,针对字段内容过多的问题,就需要自己开发部分展示的功能。

二、使用场景

1、列表只展示前三行,剩余的放在查看按钮内,弹框展示

image.png

实现思路:借助列表字段的自定义内容-复杂模式,实现设计稿里的展示效果,在页面加载事件里监听点击事件,实现弹窗效果

image.pngimage.png


let pileList = rowData.row.AccountList || [];
let resStrShow = '';
let resStrHide = '';
    for (let id = 0; id < pileList.length; id++) {
        let itemShow = "";
       ...
        if (rowData.row.AutoChargeType == '2') {//企业总账户
            if (pileList.length > 1) {
                itemShow += `${pileList[id].BusUnitAttrCompanyName}:`;
            }
            let BusUnitOrGroupBalance = Funcs.FormatDecimal(pileList[id]?.BusUnitOrGroupBalance, 2, "", ".").replace(/,/g, '');
            itemShow += `企业现金余额${BusUnitOrGroupBalance}元`;

        }
       ...
        }
        if (id < 3) {
            if (resStrShow) {
                resStrShow += '</br>';
            }
            resStrShow += itemShow;
        }
        if (resStrHide) {
            resStrHide += '</br>';
        }
        resStrHide += itemShow;

    }
if (pileList.length > 3) {

    let tipQuestion = `<div style="opacity: 1;    margin-top: 2px;" instancecode=""tabindex="1">`
        + `<span>`
        + `<span class="qiestionIcon" style="position: relative;display: flex;justify-content:  flex-start;">`
        + `<i class="material-icons" style="display:none" aria-hidden="true" role="presentation">help_outline</i>`
        + `<div  class="viewAll"
  style="
  padding:0px 7px;
  border-radius:10px;
  border:1px solid #D9D9D9;
  font-family:AlibabaPuHuiTi;
  font-size:12px;
  cursor: pointer;
  color:#3656FF;"
  >查看全部(${pileList.length})</div>`
        + `<div style="visibility:hidden;position: absolute; width: 560px; height: auto; left: 0px; top: -8px; box-sizing: content-box; padding-top: 8px;margin-left:20px;">`
        + `<span style="" class="resStrHide">${resStrHide}</span>`

        + `</div>`
        + `</span>`
        + `</span>`
        + `</div>`;

    return `<div   style="position: relative; display: inline-flex;flex-direction: column;">
          <div>${resStrShow}</div>          
          ${tipQuestion}
</div>`;

} else {
    return resStrShow;
}

js代码,点击实现弹框效果:


    var content = document.querySelector(`.UIControl_VehicleSettingList_Ecms_New`);
    content.addEventListener('click', function (event) {
        if (event.target.className == "viewAll") {
            const nextSiblingElement = event.target.nextElementSibling;
            if (nextSiblingElement) {
                const targetSpan = nextSiblingElement.querySelector('span.resStrHide');
                if (targetSpan) {
                    LocalVars.Variable_viewAll = targetSpan.innerHTML//把全部内容赋值给弹窗变量
                    Widget.fasr_dialog_viewALLNew.showDialog()//展示弹框
                
                }
            }
        }   
    });

2、列表只展示前三个,剩余的放在悬浮气泡里展示

image.png
实现思路:借助列表字段的自定义内容-复杂模式,实现设计稿里的展示效果,在页面加载事件里监听mousemove事件,实现气泡效果

image.pngimage.png

let PileRangeDesc = rowData.row.PileRangeDesc || '';
let pileList = PileRangeDesc.split(',') || [];

let resStrShow = '';
let resStrHide = '';

for (let id = 0; id < pileList.length; id++) {
    if (id < 3) {
        if (resStrShow) {
            resStrShow += '、';
        }
        resStrShow += pileList[id];
    } else {
        if (resStrHide) {
            resStrHide += '、';
        }
        resStrHide += pileList[id]
    }
}

if (pileList.length > 3) {

    let tipQuestion = `<div style="opacity: 1;margin-left:7px" instancecode=""tabindex="1">`
        + `<span>`
        + `<span class="qiestionIcon" style="position: relative;display: flex;justify-content: center;">`
        + `<i class="material-icons" style="display:none" aria-hidden="true" role="presentation">help_outline</i>`
        + `<div 
  style="
  padding:0px 7px;
  border-radius:10px;
  border:1px solid #D9D9D9;
  font-family:AlibabaPuHuiTi;
  font-size:12px;
  color:#3656FF;"
  >+${pileList.length - 3}</div>`
        + `<div style="visibility:hidden;position: absolute; width: 560px; height: auto; left: 0px; top: -8px; box-sizing: content-box; padding-top: 8px;margin-left:20px;">`
        + `<div class="q-tooltip--style q-position-engine arrow-top question-tooltip" style="--q-transition-duration: 30oms; --left: 27px; --top: false; -px;width:auto;
    padding: 16px 20px;
    background:#fff;
    font-family: AlibabaPuHuiTi;
    font-weight: 400;
    font-size: 14px;
    line-height: 20px;
    border-radius: 12px;
    color: rgba(0,0,0,0.8);
    max-width: 560px;
    box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
    margin-bottom:0;">`
        + `<span style="width:100%;
  display: inline-block;
  white-space: pre-wrap;">${resStrHide}</span>`
        + `<div style="width:12px;
  position:absolute;
  visibility:visible;
  bottom:-12px;
  height:12px;
  left:calc(50% - 6px);
  background-color:#fff;
  margin-top:0;
  clip-path: polygon(0 0,100% 0, 50% 100%);"></div>`
        + `</div>`
        + `</div>`
        + `</span>`
        + `</span>`
        + `</div>`;

    return `<div   style="position: relative; display: inline-flex;align-items:center">
          <div>${resStrShow}</div>          
          ${tipQuestion}
</div>`;

} else {
    return resStrShow;
}

js代码,实现气泡效果:

    var content = document.querySelector(`.${LocalVars.InParam_UISign}`);
    content.addEventListener('mousemove', function (event) {
        let table = content.querySelector(".fasr_mixview");
        if (table) {
            let tipElems = table.getElementsByClassName("question-tooltip");
            if (tipElems.length > 0) {
                for (let id in tipElems) {
                    if (tipElems[id].style) {
                        let parentNode = tipElems[id].parentNode;
                        // 清除TD提示框 
                        let signParentNode = parentNode;
                        for (let i = 0; i < 10; i++) {
                            if (signParentNode && signParentNode.tagName === 'TD') {
                                signParentNode.title = '';
                                break;
                            }
                            signParentNode = signParentNode.parentNode;
                        }

                        let position = parentNode.getBoundingClientRect()
                        let pX = position.left;
                        let pY = position.top;
                        let height = tipElems[id].offsetHeight
                        let width = tipElems[id].offsetWidth
                        // tipElems[id].style.left = pX - width / 2 - 14.3 + 'px';
                        tipElems[id].style.left = pX - width / 2 - 4 + 'px';
                        tipElems[id].style.top = pY - height - 7 + 'px';
                    }
                }
            }
        }
    });

3、列表格式化时间/字段内容

如果后台给我们的数据未经格式化,需要前端再次处理,我们除了在action里的返回值自定义扩展里面进行修改外,也可以利用列表字段的自定义设置,更加方便

image.png

例如,格式化时间:

return Funcs.FormatDateTime(rowData.row.LastModifyTime, 'yyyy-MM-dd HH:mm:ss');

格式化字段,没有值时展示‘--’

return `<div id="div">${rowData.value?rowData.value:'--'}</div>`

4、列表字段不固定,根据某个值动态展示部分字段,隐藏部分字段

实现思路:设置pc查询方案延迟加载,在onLoaded中,根据当前的模式处理当前方案下需要展示的字段,手动触发查询

 setTimeout(() => {
        let Scheme = Widget.fasr_mixed_view.getScheme()
            let ListViewSet = JSON.parse(Scheme.ListViewSet);
            if( LocalVars.Variable_mdoe=="1"){
               ListViewSet.displayColumns = ListViewSet.displayColumns.filter(item => (item.value != "AccountDesc"&&item.value != "AutoChargeTypeName"))
            }
            if( LocalVars.Variable_mdoe=="2"|| LocalVars.Variable_mdoe=="3"|| LocalVars.Variable_mdoe=="4"){
               ListViewSet.displayColumns = ListViewSet.displayColumns.filter(item => (item.value != "CarUseCustMobile"))
            }
                 
            Scheme.ListViewSet = JSON.stringify(ListViewSet)
            Widget.fasr_mixed_view.setScheme(Scheme)
     
    }, 0)

5、列表的查询条件不固定,根据某个值动态展示隐藏

实现思路:我们可以利用js操作原始dom,对条件进行显示隐藏,简单实现这个需求(低代码提供了硬写的方案,但是很繁琐)

 if(LocalVars.Variable_PVMismatchSwitch == "0"){
   document.querySelector(".Page_DriverManageListALLForNew .el-col-6:last-of-type").style.display = "none";
   }else{
    document.querySelector(".Page_DriverManageListALLForNew .el-col-6:last-of-type").style.display = "block";
   }

6、列表某些场景下列表每页的条数不允许切换,始终保持每页10条

image.png

实现思路:通过修改元素的行内样式,让这个元素彻底无法响应任何鼠标 / 触摸交互事件

 document.querySelector(".t-prefab-pagination__perfective-page-size").style.pointerEvents='none';

7、列表按钮点击触发服务响应缓慢,超过3秒,为防止白屏手动弹出loading

低代码提供了Funcs.ShowLoading(),但是不生效,考虑使用原生dom实现

    let eload1 = window.top.document.getElementById('tff_page_loading');
   eload1 && (eload1.style.display = "block");//显示
   eload1 && (eload1.style.display = "none");//隐藏

8、列表按钮触发后端服务每次只能校验一个,批量操作下需等待所有检查完成后才能进行下一步

实现思路:Promise.all()

  const checkPromises = [];
                let hasBoundVehicle = false;
                let CarLicenseArr = [];

                // 1. 检查所有车辆是否被绑定
                selectedRowDatas.forEach(item => {
                    if (item.VehiclesID) {
                        checkPromises.push(
                            Action.Action_CheckVehicleIfBindMembers_Ecms({ VehicleID: item.VehiclesID })
                                .then(result => {
                                    const hasCarObj = JSON.parse(result.Record.Data);
                                    if (hasCarObj.data) {
                                        CarLicenseArr.push(item.CarLicense);
                                        hasBoundVehicle = true;
                                    }
                                })
                        );
                    }
                });
                // 2. 等待所有检查完成
                Promise.all(checkPromises).then(() => {
                    debugger
                    var loadingMask = document.querySelector("#mvcFrameDiv")?.querySelector(".web-loading_mask");
                    loadingMask && (loadingMask.style.display = "none");
                    console.log(CarLicenseArr)
                    let tipI = ""
                    if (CarLicenseArr.length) {
                        CarLicenseArr.map(item => {
                            tipI += item + "、"
                        });
                        tipI = "当前车辆" + tipI.substring(0, tipI.length - 1) + "已被司机绑定,删除后司机将无法使用企业账户为该车辆充电,确认要删除吗?";
                        Funcs.Confirm(confirm, tipI, function () {
                            VehicleInformationNewUnbind()
                        })
                    } else {
                        Funcs.Confirm(confirm, confirmInfo, function () {
                            VehicleInformationNewUnbind()

                        })
                    }
                });

9、移动端列表没有数据时‘暂无数据’不展示

image.png

后端接口返回的count值不对,低代码模版根据count值决定是否展示暂无数据

10、移动端app框架内页面修改表头标题

需写在onloading方法里

 if(TFF.common.ctx.CLIENT_TYPE.TeldApp == TFF.common.ctx.getClientType()){

 window.pageNameTerm = ''
window.pageTitle = '添加司机'

if(LocalVars.InParam_FormState ==1){
window.pageTitle  ='编辑司机'
}
  TFF.jsdk.ready({ url: '', sgUrlPrefix: '' }, () => {
     window.envApi.setTitle({
            "title" : window.pageTitle ,
            success : function (res) {
             
            }
        })
            });

}

css代码规范

作者 hypnos_xy
2026年1月30日 16:54

css规范

使用属性简写

/* 使用属性简写 */
.container {
  margin: 10px 20px;
}

/* 不使用属性简写 */
.container {
  margin-top: 10px;
  margin-right: 20px;
  margin-bottom: 10px;
  margin-left: 20px;
}

统一样式格式

/* 统一使用双引号 */
.button {
  font-family: "Arial", sans-serif;
}

/* 统一使用分号结尾 */
.button {
  color: red;
}

避免使用全局样式

全局样式是指能够影响整个页面的样式,比如 body 元素或者 * 选择器。虽然在某些情况下使用全局样式会很方便,但是过度依赖全局样式会增加代码的复杂度,降低代码的可维护性。在编写 CSS 代码时,应该尽量避免使用全局样式,而是优先使用类名和 ID 来控制样式。

 !important

除公共样式之外,在业务代码中尽量不能使用 !important

z-index

建议将 z-index 进行分层,对文档流外绝对定位元素的视觉层级关系进行管理。

不要为 id、class 选择器添加类型选择器

/* good */
#error,.message {
  font-color: #c00;
}

/* bad */
input#error,p.message {
  font-color: #c00;
}

文字排版

  1. 字号 不要小于12px
  2. 字重使用数值
  3. 行高使用数字
 /* good */
h1 {
   font-weight: 700;
   line-height: 1.5;
   font-size: 12px
}

/* bad */
h1 {
   font-weight: bold;
   line-height: 15px;
   font-size: 10px
}

清楚浮动

.clearfix::before,
.clearfix::after {
  content: "";
  display: block;
  clear: both;
}

CSS代码命名规范

前端css代码规范:主要遵循BEM命名规范,BEM分别对应的是block,element和modifier,为的是结束混乱的命名方式,达到一个语义话的css命名方式

BEM命名规范

block

block 表示一个外层组件的意思,表示盒子的呈现的内容,如button,card,tabs等,在块被写为和class的名字一样 常见的block有:

布局类

.header      /* 页头 */
.footer      /* 页脚 */
.sidebar     /* 侧边栏 */
.container   /* 容器 */
.layout      /* 布局 */
.grid        /* 网格布局 */
.wrapper     /* 包装器 */
.frame       /* 框架 */
.holder      /* 容器 */
.box         /* 盒子 */
.panel       /* 面板 */
.segment     /* 分段 */
.section     /* 区块 */
.stack       /* 堆叠布局 */
.column      /* 列 */
.row         /* 行 */
.main        /* 主要内容区 */
.aside       /* 侧边内容 */

UI组件类

.button      /* 按钮 */
.modal       /* 模态框 */
.card        /* 卡片 */
.form        /* 表单 */
.input       /* 输入框 */
.select      /* 选择器 */
.checkbox    /* 复选框 */
.radio       /* 单选框 */
.toggle      /* 切换开关 */
.switch      /* 开关 */
.slider      /* 滑块 */
.range       /* 范围选择器 */
.stepper     /* 步进器 */
.rate        /* 评分组件 */

导航类

.menu          /* 菜单 */
.nav           /* 导航 */
.breadcrumb    /* 面包屑 */
.pagination    /* 分页 */
.tabs          /* 标签页 */
.dropdown      /* 下拉菜单 */
.pager         /* 分页器 */
.quick-nav     /* 快速导航 */
.action-sheet  /* 动作面板 */
.bottom-sheet  /* 底部面板 */
.drawer        /* 抽屉 */

表单控件

.search        /* 搜索框 */
.filter        /* 过滤器 */
.sort          /* 排序器 */
.upload        /* 上传组件 */
.date-picker   /* 日期选择器 */
.time-picker   /* 时间选择器 */
.color-picker  /* 颜色选择器 */
.uploader      /* 上传器 */
.search-bar    /* 搜索栏 */
.filter-bar    /* 筛选栏 */
.textarea      /* 文本域 */
.file-input    /* 文件输入 */

数据展示

.table         /* 表格 */
.list          /* 列表 */
.chart         /* 图表 */
.graph         /* 图形 */
.gauge         /* 仪表盘 */
.progress      /* 进度条 */
.timeline      /* 时间轴 */
.statistic     /* 统计数字 */
.counter       /* 计数器 */
.metric        /* 指标 */
.dashboard     /* 仪表板 */
.data-table    /* 数据表格 */
.pie-chart     /* 饼图 */
.bar-chart     /* 柱状图 */
.line-chart    /* 折线图 */

内容容器

.accordion     /* 手风琴 */
.carousel      /* 轮播图 */
.widget        /* 小部件 */
.tile          /* 磁贴 */
.collection    /* 集合 */
.feed          /* 信息流 */
.stream        /* 流 */
.grid-view     /* 网格视图 */
.list-view     /* 列表视图 */
.portfolio     /* 作品集 */
.gallery       /* 画廊 */

交互反馈

.alert         /* 警告提示 */
.toast         /* 轻量提示 */
.notification  /* 通知 */
.snackbar      /* 底部提示 */
.popover       /* 弹出框 */
.tooltip       /* 工具提示 */
.spinner       /* 加载器 */
.skeleton      /* 骨架屏 */
.placeholder   /* 占位符 */
.overlay       /* 遮罩层 */
.backdrop      /* 背景遮罩 */
.indicator     /* 指示器 */

多媒体

.player        /* 播放器 */
.audio-player  /* 音频播放器 */
.video-player  /* 视频播放器 */
.lightbox      /* 灯箱 */
.slideshow     /* 幻灯片 */
.image         /* 图片容器 */
.video         /* 视频容器 */
.audio         /* 音频容器 */

装饰元素

.avatar        /* 头像 */
.icon          /* 图标 */
.badge         /* 徽章 */
.label         /* 标签 */
.chip          /* 碎片标签 */
.tag           /* 标签 */
.mark          /* 标记 */
.highlight     /* 高亮 */
.divider       /* 分割线 */
.ornament      /* 装饰元素 */
.decoration    /* 装饰 */
.pattern       /* 图案 */

文本相关

.heading       /* 标题 */
.subheading    /* 副标题 */
.caption       /* 说明文字 */
.quote         /* 引用 */
.code-block    /* 代码块 */
.text          /* 文本容器 */
.article       /* 文章 */
.testimonial   /* 推荐语 */
.title         /* 标题 */
.subtitle      /* 副标题 */
.paragraph     /* 段落 */

功能模块

.wizard        /* 向导步骤 */
.tour          /* 引导漫游 */
.help          /* 帮助组件 */
.sorter        /* 排序器 */
.paginator     /* 分页器 */
.wishlist      /* 收藏夹 */
.cart          /* 购物车 */
.checkout      /* 结账流程 */
.settings      /* 设置面板 */
.profile       /* 个人资料 */
.account       /* 账户管理 */

移动端

.floating-action  /* 浮动按钮 */
.swipe            /* 滑动组件 */
.swipeable        /* 可滑动 */
.pull-to-refresh  /* 下拉刷新 */
.infinite-scroll  /* 无限滚动 */
.app-bar          /* 应用栏 */
.bottom-nav       /* 底部导航 */
.tab-bar          /* 标签栏 */

业务相关

.product-card     /* 商品卡片 */
.quick-view       /* 快速查看 */
.add-to-cart      /* 加入购物车 */
.review           /* 评论 */
.rating-stars     /* 星级评分 */
.price-display    /* 价格显示 */
.post             /* 帖子 */
.comment-thread   /* 评论线程 */
.like-button      /* 点赞按钮 */
.share-menu       /* 分享菜单 */
.notification-bell /* 通知铃 */
.profile-card     /* 个人资料卡片 */

营销展示

.banner       /* 横幅 */
.hero         /* 主视觉区 */
.feature      /* 特色区 */
.cta          /* 行动号召 */
.promo        /* 推广区块 */
.spotlight    /* 聚光灯区 */
.showcase     /* 展示区 */
.ad           /* 广告 */
.newsletter   /* 新闻订阅 */

形状组件

.circle       /* 圆形 */
.square       /* 方形 */
.triangle     /* 三角形 */
.diamond      /* 菱形 */
.hexagon      /* 六边形 */
.oval         /* 椭圆形 */
.polygon      /* 多边形 */

状态指示器

.status       /* 状态显示 */
.signal       /* 信号指示 */
.dot          /* 点状指示 */
.marker       /* 标记 */
.flag         /* 标志 */

链接相关

.link           /* 链接 */
.external-link  /* 外部链接 */
.internal-link  /* 内部链接 */
.nav-link       /* 导航链接 */
.button-link    /* 按钮样式链接 */

用户界面

.user-profile    /* 用户资料 */
.user-settings   /* 用户设置 */
.preferences     /* 偏好设置 */
.theme-switcher  /* 主题切换器 */

通知系统

.notification-center  /* 通知中心 */
.notification-bell    /* 通知铃 */
.message-center      /* 消息中心 */
.inbox              /* 收件箱 */

数据分析

.analytics       /* 分析 */
.report          /* 报告 */
.report-generator /* 报告生成器 */
.data-export     /* 数据导出 */
.statistics      /* 统计数据 */

系统管理

.admin-panel     /* 管理面板 */
.system-status   /* 系统状态 */
.backup          /* 备份 */
.restore         /* 恢复 */
.logs            /* 日志 */
.audit           /* 审计 */

时间相关

.timeline        /* 时间线 */
.schedule        /* 计划 */
.calendar        /* 日历 */
.time-range      /* 时间范围 */
.date-selector   /* 日期选择器 */

状态指示

.status-indicator /* 状态指示器 */
.health-status   /* 健康状态 */
.connection-status /* 连接状态 */
.battery-level   /* 电池电量 */

图表类型

.line-chart      /* 折线图 */
.bar-chart       /* 柱状图 */
.pie-chart       /* 饼图 */
.area-chart      /* 面积图 */
.scatter-plot    /* 散点图 */
.heatmap         /* 热力图 */
.gauge-chart     /* 仪表图 */

工具类组件

.toolbar         /* 工具栏 */
.context-menu    /* 上下文菜单 */
.shortcut        /* 快捷方式 */
.quick-action    /* 快速操作 */
.bulk-action     /* 批量操作 */
.wizard          /* 向导 */
.tour            /* 引导 */
.help-tooltip    /* 帮助提示 */

响应式组件

.mobile-view     /* 移动端视图 */
.tablet-view     /* 平板视图 */
.desktop-view    /* 桌面视图 */
.responsive-grid /* 响应式网格 */
.adaptive-layout /* 自适应布局 */

交互组件

.drag-drop       /* 拖放 */
.dropzone        /* 放置区域 */
.draggable       /* 可拖动 */
.sortable        /* 可排序 */
.resizable       /* 可调整大小 */
.collapsible     /* 可折叠 */
.accordion       /* 手风琴 */

存储相关

.storage         /* 存储 */
.storage-usage   /* 存储使用情况 */
.quota           /* 配额 */
.backup-status   /* 备份状态 */
.cloud-storage   /* 云存储 */
.local-storage   /* 本地存储 */

安全组件

.authentication  /* 身份验证 */
.login           /* 登录 */
.logout          /* 登出 */
.password        /* 密码 */
.two-factor      /* 双重认证 */
.encryption      /* 加密 */
.security-log    /* 安全日志 */

监控组件

.monitoring      /* 监控 */
.real-time       /* 实时 */
.historical      /* 历史 */
.trend-analysis  /* 趋势分析 */
.performance     /* 性能 */
.uptime          /* 运行时间 */
.downtime        /* 停机时间 */

同步组件

.sync            /* 同步 */
.sync-status     /* 同步状态 */
.offline         /* 离线 */
.online          /* 在线 */
.conflict        /* 冲突 */
.conflict-resolution /* 冲突解决 */

报告组件

.report          /* 报告 */
.report-builder  /* 报告构建器 */
.report-template /* 报告模板 */
.export-report   /* 导出报告 */
.print-report    /* 打印报告 */
.schedule-report /* 计划报告 */

Element

元素是块的子节点,元素表示的目的,而不是状态。块和元素之间用一个双下划线划开。

布局结构类

__header     /* 头部 */
__footer     /* 脚部 */
__body       /* 主体 */
__main       /* 主要区域 */
__aside      /* 侧边区域 */
__content    /* 内容区域 */
__container  /* 容器 */
__wrapper    /* 包装器 */
__inner      /* 内部容器 */
__section    /* 区块 */
__group      /* 分组 */
__panel      /* 面板 */
__frame      /* 框架 */

导航与交互

__item       /* 项目/项 */
__link       /* 链接 */
__button     /* 按钮 */
__icon       /* 图标 */
__toggle     /* 切换开关 */
__arrow      /* 箭头 */
__caret      /* 指示箭头 */
__handle     /* 手柄/把手 */
__drag       /* 拖拽手柄 */
__scroll     /* 滚动区域 */
__trigger    /* 触发器 */
__action     /* 操作按钮 */

内容显示

__title      /* 标题 */
__subtitle   /* 副标题 */
__heading    /* 标题(可细分 __heading-1, __heading-2) */
__text       /* 文本 */
__label      /* 标签文字 */
__caption    /* 说明文字 */
__description/* 描述 */
__summary    /* 摘要 */
__paragraph  /* 段落 */
__quote      /* 引用 */
__code       /* 代码 */
__image      /* 图片 */
__video      /* 视频 */
__audio      /* 音频 */
__media      /* 媒体内容 */

 表单元素

__input      /* 输入框 */
__textarea   /* 文本域 */
__select     /* 选择框 */
__option     /* 选项 */
__checkbox   /* 复选框 */
__radio      /* 单选按钮 */
__field      /* 表单字段 */
__label      /* 表单标签 */
__hint       /* 提示文字 */
__error      /* 错误信息 */
__success    /* 成功信息 */
__warning    /* 警告信息 */
__validation /* 验证信息 */

列表与集合

__list       /* 列表 */
__item       /* 列表项 */
__entry      /* 条目 */
__row        /* 行 */
__cell       /* 单元格 */
__col        /* 列 */
__grid       /* 网格项 */
__card       /* 卡片 */
__tile       /* 磁贴 */
__block      /* 块状项 */
__segment    /* 分段 */
__piece      /* 片段 */

信息与状态

__status     /* 状态显示 */
__badge      /* 徽章 */
__tag        /* 标签 */
__marker     /* 标记 */
__indicator  /* 指示器 */
__signal     /* 信号 */
__dot        /* 点状指示 */
__counter    /* 计数器 */
__number     /* 数字显示 */
__value      /* 数值 */
__percentage /* 百分比 */
__rating     /* 评分 */
__star       /* 星星 */

装饰与辅助

__icon       /* 图标 */
__avatar     /* 头像 */
__thumbnail  /* 缩略图 */
__preview    /* 预览图 */
__background /* 背景 */
__overlay    /* 遮罩层 */
__shadow     /* 阴影 */
__border     /* 边框 */
__divider    /* 分割线 */
__separator  /* 分隔符 */
__ornament   /* 装饰元素 */
__pattern    /* 图案 */
__gradient   /* 渐变 */

工具与控制

__toolbar    /* 工具栏 */
__tool       /* 工具按钮 */
__control    /* 控制元素 */
__settings   /* 设置按钮 */
__config     /* 配置 */
__option     /* 选项按钮 */
__switch     /* 开关 */
__slider     /* 滑块 */
__handle     /* 滑块手柄 */
__knob       /* 旋钮 */
__dial       /* 刻度盘 */

时间与进度

__time       /* 时间显示 */
__date       /* 日期显示 */
__timestamp  /* 时间戳 */
__duration   /* 时长 */
__progress   /* 进度条 */
__bar        /* 进度条条状部分 */
__track      /* 轨道 */
__step       /* 步骤 */
__milestone  /* 里程碑 */
__clock      /* 时钟 */
__calendar   /* 日历 */

交互反馈

__loading    /* 加载指示器 */
__spinner    /* 旋转器 */
__skeleton   /* 骨架屏元素 */
__placeholder/* 占位符 */
__hint       /* 提示 */
__tooltip    /* 工具提示内容 */
__popup      /* 弹出内容 */
__message    /* 消息内容 */
__alert      /* 警告内容 */
__toast      /* 轻提示内容 */
__notification /* 通知内容 */

导航组件 (Navigation)

/* .menu 的 Element */
__item       /* 菜单项 */
__link       /* 菜单链接 */
__icon       /* 菜单图标 */
__text       /* 菜单文字 */
__submenu    /* 子菜单 */
__dropdown   /* 下拉菜单 */

/* .tabs 的 Element */
__tab        /* 标签页 */
__content    /* 标签内容 */
__nav        /* 标签导航 */
__panel      /* 标签面板 */

/* .breadcrumb 的 Element */
__crumb      /* 面包屑项 */
__separator  /* 分隔符 */

表单组件 (Forms)

/* .form 的 Element */
__group      /* 表单组 */
__field      /* 表单字段 */
__label      /* 标签 */
__input      /* 输入框 */
__error      /* 错误信息 */
__help       /* 帮助文字 */
__submit     /* 提交按钮 */
__reset      /* 重置按钮 */

/* .input 的 Element */
__wrapper    /* 包装器 */
__field      /* 输入区域 */
__prefix     /* 前缀 */
__suffix     /* 后缀 */
__clear      /* 清除按钮 */

卡片组件 (Cards)

/* .card 的 Element */
__header     /* 卡片头部 */
__title      /* 卡片标题 */
__subtitle   /* 卡片副标题 */
__body       /* 卡片主体 */
__content    /* 卡片内容 */
__footer     /* 卡片脚部 */
__image      /* 卡片图片 */
__actions    /* 卡片操作区 */
__button     /* 卡片按钮 */

模态框 (Modals)

/* .modal 的 Element */
__overlay    /* 遮罩层 */
__dialog     /* 对话框 */
__header     /* 头部 */
__title      /* 标题 */
__close      /* 关闭按钮 */
__body       /* 主体 */
__content    /* 内容 */
__footer     /* 脚部 */
__actions    /* 操作区 */

数据表格 (Tables)

/* .table 的 Element */
__header     /* 表头 */
__head       /* 头部区域 */
__body       /* 表格主体 */
__footer     /* 表格脚部 */
__row        /* 行 */
__cell       /* 单元格 */
__col        /* 列 */
__sort       /* 排序按钮 */
__filter     /* 筛选按钮 */
__pagination /* 分页区域 */

工具栏 (Toolbars)

/* .toolbar 的 Element */
__left       /* 左侧区域 */
__center     /* 中间区域 */
__right      /* 右侧区域 */
__item       /* 工具项 */
__button     /* 工具按钮 */
__separator  /* 分隔符 */
__search     /* 搜索框 */
__filter     /* 筛选器 */
__sort       /* 排序器 */

轮播图 (Carousels)

/* .carousel 的 Element */
__slide      /* 幻灯片 */
__content    /* 幻灯片内容 */
__image      /* 幻灯片图片 */
__caption    /* 幻灯片说明 */
__prev       /* 上一个按钮 */
__next       /* 下一个按钮 */
__dots       /* 指示点区域 */
__dot        /* 单个指示点 */
__pagination /* 分页指示器 */

图表组件 (Charts)

/* .chart 的 Element */
__container  /* 容器 */
__canvas     /* 画布 */
__axis       /* 坐标轴 */
__axis-x     /* X轴 */
__axis-y     /* Y轴 */
__grid       /* 网格线 */
__legend     /* 图例 */
__tooltip    /* 工具提示 */
__data       /* 数据点 */
__bar        /* 柱状 */
__line       /* 线条 */
__area       /* 面积 */

通知组件 (Notifications)

/* .notification 的 Element */
__icon       /* 图标 */
__title      /* 标题 */
__message    /* 消息内容 */
__close      /* 关闭按钮 */
__actions    /* 操作按钮 */
__time       /* 时间显示 */
__progress   /* 进度条 */

位置关系

__top        /* 顶部 */
__bottom     /* 底部 */
__left       /* 左侧 */
__right      /* 右侧 */
__center     /* 中间 */
__side       /* 侧边 */
__edge       /* 边缘 */
__corner     /* 角落 */
__start      /* 起始端 */
__end        /* 结束端 */

 大小尺寸

__small      /* 小尺寸 */
__medium     /* 中尺寸 */
__large      /* 大尺寸 */
__mini       /* 迷你尺寸 */
__tiny       /* 超小尺寸 */
__compact    /* 紧凑型 */
__expanded   /* 展开型 */
__full       /* 全尺寸 */

状态指示

__active     /* 激活状态 */
__inactive   /* 非激活状态 */
__enabled    /* 启用状态 */
__disabled   /* 禁用状态 */
__selected   /* 选中状态 */
__checked    /* 勾选状态 */
__focused    /* 聚焦状态 */
__hover      /* 悬停状态 */
__pressed    /* 按下状态 */

数量关系

__single     /* 单个 */
__multiple   /* 多个 */
__first      /* 第一个 */
__last       /* 最后一个 */
__even       /* 偶数 */
__odd        /* 奇数 */
__nth        /* 第n个 */
__all        /* 所有 */
__none       /* 无 */

时间关系

__past       /* 过去 */
__present    /* 现在 */
__future     /* 未来 */
__old        /* 旧的 */
__new        /* 新的 */
__current    /* 当前的 */
__previous   /* 上一个 */
__next       /* 下一个 */

功能角色

__primary    /* 主要的 */
__secondary  /* 次要的 */
__tertiary   /* 第三级的 */
__auxiliary  /* 辅助的 */
__main       /* 主要的 */
__sub        /* 次要的 */
__support    /* 支持的 */
__detail     /* 详细的 */

Modifier

修饰符是改变某个块的外观的标志。要使用修饰符,用一个双短横线线隔开

通用状态

--active      /* 激活/活动状态 */
--inactive    /* 非激活状态 */
--enabled     /* 启用状态 */
--disabled    /* 禁用状态 */
--selected    /* 选中状态 */
--checked     /* 已勾选 */
--unchecked   /* 未勾选 */
--focused     /* 获得焦点 */
--blurred     /* 失去焦点 */
--hover       /* 悬停状态 */
--pressed     /* 按下状态 */
--dragging    /* 拖拽中 */
--loading     /* 加载中 */
--processing  /* 处理中 */
--waiting     /* 等待中 */

可见性状态

--visible     /* 可见 */
--hidden      /* 隐藏 */
--collapsed   /* 折叠 */
--expanded    /* 展开 */
--closed      /* 关闭 */
--open        /* 打开 */
--show        /* 显示 */
--hide        /* 隐藏 */

交互状态

--clickable   /* 可点击 */
--editable    /* 可编辑 */
--draggable   /* 可拖动 */
--droppable   /* 可放置 */
--sortable    /* 可排序 */
--resizable   /* 可调整大小 */
--selectable  /* 可选择 */

数据状态

--empty       /* 空状态 */
--filled      /* 已填充 */
--valid       /* 有效 */
--invalid     /* 无效 */
--verified    /* 已验证 */
--unverified  /* 未验证 */
--dirty       /* 已修改(表单) */
--clean       /* 未修改 */

文件/上传状态

--uploading   /* 上传中 */
--uploaded    /* 已上传 */
--failed      /* 失败 */
--success     /* 成功 */
--pending     /* 等待中 */
--completed   /* 已完成 */
--cancelled   /* 已取消 */

网络/连接状态

--online      /* 在线 */
--offline     /* 离线 */
--connected   /* 已连接 */
--disconnected /* 断开连接 */
--connecting  /* 连接中 */
--syncing     /* 同步中 */
--synced      /* 已同步 */

通用尺寸

/* 通用尺寸 */
--small       /* 小尺寸 */
--medium      /* 中等尺寸 */
--large       /* 大尺寸 */
--xl          /* 加大尺寸 */
--xxl         /* 特大尺寸 */
--tiny        /* 超小尺寸 */
--mini        /* 迷你尺寸 */
--compact     /* 紧凑型 */
/* 宽度尺寸 */
--narrow      /* 窄 */
--wide        /* 宽 */
--full        /* 全宽 */
--half        /* 半宽 */
--third       /* 三分之一宽 */
--quarter     /* 四分之一宽 */
--fluid       /* 流体宽度 */
--fixed       /* 固定宽度 */
/* 高度尺寸 */
--short       /* 矮 */
--tall        /* 高 */
--full-height /* 全高 */
--auto-height /* 自动高度 */
/* 间距尺寸 */
--dense       /* 密集间距 */
--loose       /* 宽松间距 */
--tight       /* 紧凑间距 */
--spacious    /* 宽敞间距 */

颜色主题

--primary     /* 主要颜色 */
--secondary   /* 次要颜色 */
--tertiary    /* 第三颜色 */
--accent      /* 强调色 */
--muted       /* 柔和色 */
--light       /* 浅色 */
--dark        /* 深色 */
--inverse     /* 反色 */

语义颜色

--success     /* 成功/绿色 */
--error       /* 错误/红色 */
--warning     /* 警告/黄色 */
--info        /* 信息/蓝色 */
--danger      /* 危险/红色 */
--safe        /* 安全/绿色 */
--critical    /* 严重/橙色 */

样式变体

--outline     /* 轮廓样式 */
--solid       /* 实心样式 */
--ghost       /* 幽灵样式(透明背景) */
--flat        /* 扁平样式 */
--raised      /* 凸起样式 */
--shadow      /* 有阴影 */
--borderless  /* 无边框 */
--rounded     /* 圆角 */
--square      /* 直角 */
--circle      /* 圆形 */

透明度

--transparent /* 透明 */
--semi-transparent /* 半透明 */
--opaque      /* 不透明 */
--translucent /* 半透明 */

布局方向

--vertical    /* 垂直排列 */
--horizontal  /* 水平排列 */
--row         /* 行方向 */
--column      /* 列方向 */

文本方向

--left        /* 左对齐 */
--center      /* 居中对齐 */
--right       /* 右对齐 */
--justify     /* 两端对齐 */
--start       /* 起始对齐 */
--end         /* 结束对齐 */

位置方向

--top         /* 顶部 */
--bottom      /* 底部 */
--left        /* 左侧 */
--right       /* 右侧 */
--middle      /* 中间 */
--center      /* 居中 */
--absolute    /* 绝对定位 */
--relative    /* 相对定位 */
--fixed       /* 固定定位 */
--sticky      /* 粘性定位 */

浮动方向

--float-left  /* 左浮动 */
--float-right /* 右浮动 */
--float-none  /* 不浮动 */

行为模式

--readonly    /* 只读 */
--editable    /* 可编辑 */
--required    /* 必填 */
--optional    /* 可选 */
--multiple    /* 多选 */
--single      /* 单选 */
--searchable  /* 可搜索 */
--filterable  /* 可筛选 */
--sortable    /* 可排序 */

交互模式

--interactive /* 可交互 */
--static      /* 静态 */
--dynamic     /* 动态 */
--animated    /* 有动画 */
--static      /* 静态 */
--fixed       /* 固定 */
--sticky      /* 粘性 */

数据模式

--empty       /* 空数据 */
--loading     /* 加载数据 */
--loaded      /* 数据已加载 */
--error       /* 数据错误 */
--no-data     /* 无数据 */
--has-data    /* 有数据 */

内容类型

--text        /* 文本类型 */
--number      /* 数字类型 */
--date        /* 日期类型 */
--file        /* 文件类型 */
--image       /* 图片类型 */
--video       /* 视频类型 */
--audio       /* 音频类型 */

文件类型

--pdf         /* PDF文件 */
--doc         /* Word文档 */
--xls         /* Excel文件 */
--ppt         /* PowerPoint文件 */
--image       /* 图片文件 */
--archive     /* 压缩文件 */
--code        /* 代码文件 */

设备类型

--mobile      /* 移动设备 */
--tablet      /* 平板设备 */
--desktop     /* 桌面设备 */
--phone       /* 手机 */
--watch       /* 手表 */
--tv          /* 电视 */

时间状态

--new         /* 新的 */
--old         /* 旧的 */
--recent      /* 最近的 */
--past        /* 过去的 */
--future      /* 未来的 */
--expired     /* 已过期 */
--upcoming    /* 即将到来 */

频率状态

--frequent    /* 频繁的 */
--rare        /* 罕见的 */
--once        /* 一次性的 */
--recurring   /* 重复的 */
--daily       /* 每日的 */
--weekly      /* 每周的 */
--monthly     /* 每月的 */

屏幕尺寸

--mobile      /* 移动端 */
--tablet      /* 平板端 */
--desktop     /* 桌面端 */
--sm          /* 小屏幕 */
--md          /* 中等屏幕 */
--lg          /* 大屏幕 */
--xl          /* 超大屏幕 */

设备方向

--portrait    /* 竖屏 */
--landscape   /* 横屏 */

断点相关

--below-md    /* 小于中等屏幕 */
--above-lg    /* 大于大屏幕 */
--only-mobile /* 仅移动端 */
--only-desktop /* 仅桌面端 */

数据质量

--valid       /* 数据有效 */
--invalid     /* 数据无效 */
--verified    /* 已验证 */
--pending     /* 待验证 */
--expired     /* 已过期 */
--fresh       /* 新鲜数据 */
--stale       /* 陈旧数据 */

数据量

--empty       /* 空数据 */
--few         /* 少量数据 */
--many        /* 大量数据 */
--full        /* 数据已满 */
--overflow    /* 数据溢出 */

安全状态 Modifier

--secured     /* 已保护 */
--unsecured   /* 未保护 */
--encrypted   /* 已加密 */
--decrypted   /* 已解密 */
--authenticated /* 已认证 */
--unauthenticated /* 未认证 */
--authorized  /* 已授权 */
--unauthorized /* 未授权 */

按钮 Button

--primary     /* 主要按钮 */
--secondary   /* 次要按钮 */
--danger      /* 危险操作按钮 */
--warning     /* 警告操作按钮 */
--success     /* 成功操作按钮 */
--link        /* 链接样式按钮 */
--icon        /* 图标按钮 */
--block       /* 块级按钮(宽度100%) */

输入框 Input

--filled      /* 已填充 */
--error       /* 错误状态 */
--success     /* 成功状态 */
--warning     /* 警告状态 */
--disabled    /* 禁用状态 */
--readonly    /* 只读状态 */
--search      /* 搜索框样式 */

表格 Table

--striped     /* 斑马纹 */
--bordered    /* 有边框 */
--hover       /* 悬停效果 */
--condensed   /* 紧凑型 */
--responsive  /* 响应式表格 */
--sortable    /* 可排序 */

卡片 Card

--shadow      /* 有阴影 */
--border      /* 有边框 */
--hoverable   /* 悬停效果 */
--selected    /* 选中状态 */
--clickable   /* 可点击 */
--draggable   /* 可拖动 */

模态框 Modal

--small       /* 小尺寸模态框 */
--medium      /* 中尺寸模态框 */
--large       /* 大尺寸模态框 */
--fullscreen  /* 全屏模态框 */
--centered    /* 居中显示 */

导航 Nav

--vertical    /* 垂直导航 */
--horizontal  /* 水平导航 */
--pills       /* 胶囊式导航 */
--tabs        /* 标签式导航 */
--underline   /* 下划线式导航 */

国际化 Modifier

--ltr         /* 从左到右 */
--rtl         /* 从右到左 */
--en          /* 英语 */
--zh          /* 中文 */
--ja          /* 日语 */
--ar          /* 阿拉伯语 */
--locale-en   /* 英语地区 */
--locale-zh   /* 中文地区 */

优先级 Modifier

--high        /* 高优先级 */
--medium      /* 中优先级 */
--low         /* 低优先级 */
--critical    /* 关键优先级 */
--normal      /* 普通优先级 */
--urgent      /* 紧急优先级 */

为什么在使用Vue的v-for时,一定要加上key字段?

2026年1月30日 16:53

假设我们要渲染一个简单的待办事项列表:

<div id="app">
  <div v-for="item in list">
    <input type="checkbox">
    <span>{{ item.text }}</span>
  </div>
</div>

当我们删除中间某个项目时,你可能会发现复选框的状态出现了错误。选中的项目被删除了,但其他项目的选中状态却乱了套。

问题的根源

Vue在更新DOM时,会尽量复用已有的元素。这是一种优化策略,可以减少DOM操作,提高性能。

但是,当数据顺序发生变化时,Vue需要知道哪些元素可以复用,哪些需要重新创建。如果没有key,Vue只能按照顺序进行对比。

没有key的情况:

  • • Vue按顺序对比新旧节点
  • • 删除第二个元素后,后面的元素会前移
  • • Vue认为这是同一个元素,只是内容变了
  • • 复用的元素保留了之前的状态

key的作用

key给每个节点一个唯一标识。Vue通过这个标识来跟踪每个节点的身份。

<!-- 正确的写法 -->
<div v-for="item in list" :key="item.id">
  <input type="checkbox">
  <span>{{ item.text }}</span>
</div>

加上key之后:

  • • Vue知道每个节点的唯一身份
  • • 删除某个节点时,其他节点身份不变
  • • 不会错误地复用其他节点的状态
  • • 列表更新更加准确

深入理解虚拟DOM

要理解key的重要性,我们需要了解Vue的虚拟DOM机制。

虚拟DOM是什么

虚拟DOM是真实DOM的JavaScript对象表示。Vue通过对比新旧虚拟DOM的差异,来决定如何更新真实DOM。

Diff算法

Vue使用Diff算法来比较虚拟DOM的差异。这个算法会找出最小的变更,然后应用到真实DOM上。

没有key的Diff过程:

  • • 按顺序逐个比较节点
  • • 发现长度变化,进行插入或删除
  • • 可能导致大量不必要的DOM操作

有key的Diff过程:

  • • 根据key建立映射关系
  • • 精确找到新增、删除、移动的节点
  • • 最小化DOM操作

key的选择

选择合适的key很重要。不合适的key可能带来问题。

好的key选择

  • • 数据中的唯一标识符
  • • 稳定的、不会改变的值
  • • 如:数据库ID、UUID等
// 好的例子
const list = [
  { id: 1, text: '学习Vue' },
  { id: 2, text: '写代码' },
  { id: 3, text: '阅读文档' }
]

不好的key选择

  • • 数组索引(在排序、过滤时会出问题)
  • • 随机数(每次渲染都会变化)
  • • 可能重复的值
// 不好的例子 - 使用索引作为key
<div v-for="(item, index) in list" :key="index">

// 不好的例子 - 使用随机数作为key  
<div v-for="item in list" :key="Math.random()">

实际开发中的场景

列表排序

当列表需要排序时,key的作用特别明显。

// 初始列表
[
  { id: 1, name: '苹果' },
  { id: 2, name: '香蕉' }, 
  { id: 3, name: '橙子' }
]

// 排序后
[
  { id: 3, name: '橙子' },
  { id: 1, name: '苹果' },
  { id: 2, name: '香蕉' }
]

有key时,Vue知道这是元素位置的移动,而不是内容的修改。

列表过滤

过滤列表时,key确保正确的元素被保留或移除。

动态组件

在动态组件中,key可以强制组件重新创建:

<component :is="currentComponent" :key="componentKey">

改变componentKey会触发组件的重新渲染。

性能考虑

什么时候可以不加key

在某些简单场景下,不加key可能不会立即发现问题:

  • • 静态列表,永远不会改变
  • • 列表项没有内部状态
  • • 列表项非常简单

但为了代码的健壮性,建议始终加上key。

错误的使用方式

有些开发者会这样使用key:

<!-- 错误:使用索引作为key -->
<div v-for="(item, index) in list" :key="index">

<!-- 错误:使用不稳定的值作为key -->
<div v-for="item in list" :key="Math.random()">

这些用法都会导致各种奇怪的问题。

常见问题解答

为什么不能用索引作为key?

当列表发生变化时,索引也会变化。原来索引为1的元素,在删除前面的元素后,可能变成索引0。这会导致Vue错误地复用元素。

key一定要全局唯一吗?

在同一个v-for中唯一即可,不需要全局唯一。

如果没有唯一标识怎么办?

如果数据源没有提供唯一标识,可以考虑:

    1. 在获取数据时生成唯一ID
    1. 使用多个字段组合作为key
    1. 使用第三方库生成UUID

记住:key是Vue跟踪节点身份的标识,不是普通的属性。

写在最后

理解key的作用,能帮助我们写出更稳定、性能更好的Vue应用。这个看似小的细节,在实际开发中却很重要。

下次使用v-for时,记得给它一个合适的key。

❌
❌