普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月12日掘金 前端

为天地图 JavaScript API v4.0 提供 TypeScript 类型支持 —— tianditu-v4-types 正式发布!

作者 知了清语
2025年12月12日 14:15

如果你正在使用 天地图(Tianditu)JavaScript API v4.0 开发 Web 地图应用,并且希望获得更好的开发体验(比如自动补全、类型检查、减少运行时错误),那么你一定会喜欢这个新工具包 👉 tianditu-v4-types

简介

tianditu-v4-types 是一个 纯 TypeScript 类型定义包,专为 天地图官方 JavaScript API v4.0 设计。它完整覆盖了天地图 API 中的核心类、方法、事件和配置项,包括但不限于:

  • T.Map:地图实例
  • T.LngLat:经纬度坐标
  • T.Marker / T.Polyline / T.Polygon:覆盖物
  • T.InfoWindow:信息窗口
  • T.TileLayer / T.MapType:图层与底图类型
  • 事件监听器(如 clickzoomend 等)
  • 地理编码、行政区划查询等高级功能的回调结构

无需修改原有代码,只需安装类型包,即可在 TypeScript 项目中享受完整的类型提示!

快速开始

npm install tianditu-v4-types --save-dev

or

pnpm add tianditu-v4-types -D

 为什么需要它?

天地图官方 API 虽然功能强大,但仅提供 无类型的 JavaScript 库,在现代前端工程化开发中存在以下痛点:

  • ❌ 没有类型提示,开发效率低
  • ❌ 容易拼错方法名或传错参数
  • ❌ 团队协作时缺乏接口契约

tianditu-v4-types 正是为解决这些问题而生——零运行时开销,纯开发期增强

🌟 特点

  • ✅ 100% 覆盖天地图 JS API v4.0 官方文档
  • ✅ 支持主流框架(Vue、React、Angular、原生 TS)
  • ✅ 严格遵循 TypeScript 最佳实践
  • ✅ 开源免费,MIT 协议
  • ✅ 持续维护,欢迎 PR 和 Issue!

🔗 资源链接

React 性能优化(方向)

作者 之恒君
2025年12月12日 13:45

React 性能优化的核心目标是减少不必要的渲染降低渲染成本优化资源加载,最终提升应用响应速度和用户体验。以下从「渲染优化」「代码与资源优化」「运行时优化」「架构层优化」四个维度,系统梳理 React 性能优化方案,包含具体场景、实现方式及原理。

一、渲染优化:减少不必要的重渲染

React 中最常见的性能问题是「组件无意义重渲染」—— 父组件渲染时,子组件即使 props/state 未变化也被迫重新执行 render。需从「控制渲染触发条件」「隔离渲染上下文」两方面优化。

1. 优化组件渲染触发条件

通过控制 shouldComponentUpdate(类组件)或 React.memo(函数组件),判断组件是否需要重新渲染。

(1)类组件:shouldComponentUpdatePureComponent

  • shouldComponentUpdate(nextProps, nextState) :手动判断 props/state 是否变化,返回 false 可阻止重渲染。

示例:避免因父组件传递的不变 props(如函数、对象)导致子组件重渲染:

class Child extends React.Component {
  shouldComponentUpdate(nextProps) {
    // 仅当关键 props(如 id、name)变化时才渲染
    return nextProps.id !== this.props.id || nextProps.name !== this.props.name;
  }
  render() {
    return <div>{this.props.name}</div>;
  }
}
  • React.PureComponent:内置浅比较(shallow comparison)逻辑的类组件,自动对比 propsstate表层属性(基本类型直接比,引用类型比地址)。

✅ 适用场景:组件 props/state 均为基本类型(string/number/boolean),或引用类型(对象/数组)不频繁修改。

❌ 注意:若 props 包含引用类型(如 { age: 18 }),即使内容不变但地址变化(父组件每次渲染重新创建),PureComponent 仍会误判重渲染,需配合「不可变数据」或「缓存引用」优化。

(2)函数组件:React.memo

React.memo 是函数组件版的「浅比较优化」,本质是高阶组件(HOC),包裹函数组件后,仅当 props 表层变化时才重新渲染。

  • 基础用法:

    • // 仅当 props.name 或 props.id 变化时渲染
      const Child = React.memo(({ name, id }) => {
        return <div>{name}</div>;
      });
      
  • 自定义比较逻辑:若需深比较或自定义判断规则,可传递第二个参数(类似 shouldComponentUpdate):

    • const Child = React.memo(
        ({ user, id }) => <div>{user.name}</div>,
        // 自定义比较:仅当 user.id 或 id 变化时渲染
        (prevProps, nextProps) => {
          return prevProps.user.id === nextProps.user.id && prevProps.id === nextProps.id;
        }
      );
      

2. 缓存引用类型:避免浅比较误判

父组件渲染时,若传递给子组件的「引用类型 props」(函数、对象、数组)每次都是新创建的(即使内容不变),会导致 PureComponent/React.memo 误判为「props 变化」,触发不必要重渲染。需通过缓存引用解决。

(1)缓存函数:useCallback(函数组件)

useCallback 缓存函数引用,确保组件重渲染时,若依赖项未变化,返回的函数引用始终不变。

  • 问题场景:父组件每次渲染重新创建函数,导致子组件误渲染:

    • // 错误:每次 Parent 渲染,handleClick 都是新函数,Child(React.memo)会重渲染
      const Parent = () => {
        const handleClick = () => {
          console.log("点击");
        };
        return <Child onClick={handleClick} />;
      };
      
  • 优化方案:用 useCallback 缓存函数,依赖项为空数组时,函数引用永久不变:

    • const Parent = () => {
        // 正确:依赖项为空,handleClick 引用始终不变
        const handleClick = useCallback(() => {
          console.log("点击");
        }, []); 
        return <Child onClick={handleClick} />;
      };
      

(2)缓存对象/数组:useMemo(函数组件)

useMemo 缓存计算结果(如对象、数组、复杂计算值),确保依赖项未变化时,返回的引用不变。

  • 问题场景:父组件每次渲染重新创建对象,导致子组件误渲染:

    • // 错误:每次 Parent 渲染,user 都是新对象,Child(React.memo)会重渲染
      const Parent = () => {
        const user = { name: "张三", age: 20 }; 
        return <Child user={user} />;
      };
      
  • 优化方案:用 useMemo 缓存对象,依赖项为空时,对象引用不变:

    • const Parent = () => {
        // 正确:依赖项为空,user 引用始终不变
        const user = useMemo(() => ({ name: "张三", age: 20 }), []); 
        return <Child user={user} />;
      };
      
    • // 仅当 list 或 keyword 变化时,才重新过滤数据
      const filteredList = useMemo(() => {
        return list.filter(item => item.name.includes(keyword));
      }, [list, keyword]);
      

3. 隔离渲染上下文:避免父组件渲染影响子组件

若子组件与父组件状态完全无关,可通过「状态提升」「独立组件拆分」或「使用 React.memo 隔离」,避免父组件渲染时子组件被动重渲染。

典型场景:拆分「频繁更新组件」与「静态组件」

父组件包含「频繁更新的部分」(如计数器)和「静态部分」(如标题、说明),若不拆分,静态部分会随计数器更新而重渲染:

// 错误:Counter 更新时,Title 也会重渲染
const Parent = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <Title text="静态标题" /> {/* 无需更新 */}
      <Counter count={count} onIncrement={() => setCount(count + 1)} /> {/* 频繁更新 */}
    </div>
  );
};

优化方案:用 React.memo 包裹 Title,或拆分 Parent 为「状态组件」和「静态组件」:

// 正确:Title 被 React.memo 包裹,props 不变时不重渲染
const Title = React.memo(({ text }) => <h1>{text}</h1>);

const Parent = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <Title text="静态标题" /> {/* 不重渲染 */}
      <Counter count={count} onIncrement={() => setCount(count + 1)} /> {/* 正常渲染 */}
    </div>
  );
};

二、代码与资源优化:降低渲染成本

即使渲染触发合理,若代码逻辑复杂、资源体积大,仍会导致渲染缓慢。需从「代码精简」「资源加载」「DOM 优化」三方面入手。

1. 代码层面:精简逻辑与依赖

(1)避免渲染时执行高开销操作

渲染阶段(render 或函数组件主体)应仅做「UI 描述相关逻辑」,避免执行耗时操作(如 API 请求、大数据计算、DOM 操作)。

  • 错误示例:渲染时请求数据,导致每次渲染都触发请求:

    • const Child = () => {
        // 错误:每次渲染都会执行 fetch,且可能导致竞态问题
        fetch("/api/data").then(res => res.json()); 
        return <div>内容</div>;
      };
      
  • 正确方案:将高开销操作放在「副作用钩子」中(useEffect/componentDidMount),控制执行时机:

    • const Child = () => {
        useEffect(() => {
          // 正确:仅组件挂载时执行一次请求
          fetch("/api/data").then(res => res.json());
        }, []); 
        return <div>内容</div>;
      };
      

(2)按需引入依赖与组件

  • 第三方库按需引入:避免全量引入大体积库(如 Lodash、Ant Design),仅引入所需模块,减少打包体积。

示例:Lodash 按需引入:

// 错误:全量引入 Lodash(体积大)
import _ from "lodash";
// 正确:仅引入 debounce 模块
import debounce from "lodash/debounce";
  • 组件按需加载:通过「动态 import() + React.lazy + Suspense」,实现路由或组件级别的按需加载,减少首屏加载时间。

示例:路由按需加载(配合 React Router):

import { lazy, Suspense } from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";

// 动态引入组件(打包时拆分为独立 chunk)
const Home = lazy(() => import("./Home"));
const About = lazy(() => import("./About"));

const App = () => (
  <Router>
    {/* Suspense 提供加载 fallback(如骨架屏) */}
    <Suspense fallback={<div>加载中...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  </Router>
);

2. 资源层面:优化图片与静态资源

  • 图片优化

    • 使用「响应式图片」(srcset + sizes),根据设备分辨率加载合适尺寸的图片;
    • 采用现代图片格式(WebP、AVIF),比 JPG/PNG 体积小 25%-50%;
    • 图片懒加载:用 loading="lazy"(原生)或 React 懒加载库(如 react-lazyload),避免首屏加载非可视区域图片。
    •   示例:原生懒加载图片:
    • <img 
        src="image.webp" 
        alt="描述" 
        loading="lazy" // 可视区域外图片延迟加载
        srcset="image-480w.webp 480w, image-800w.webp 800w" 
        sizes="(max-width: 600px) 480px, 800px"
      />
      
  • 静态资源 CDN 分发:将 JS、CSS、图片等资源部署到 CDN,利用 CDN 节点缓存和就近访问,降低资源加载延迟。

3. DOM 层面:减少 DOM 操作与节点数量

React 最终会将虚拟 DOM 转换为真实 DOM,DOM 节点越多、操作越频繁,性能开销越大。

(1)减少不必要的 DOM 节点

  • 避免嵌套过深的 DOM 结构(如 div > div > div > span),尽量扁平化;

  • 用「碎片(Fragment)」代替无意义的容器 div,减少多余节点:

    • // 错误:多余的 div 容器
      const List = () => (
        <div>
          <Item1 />
          <Item2 />
        </div>
      );
      // 正确:用 Fragment 包裹,不生成额外 DOM 节点
      const List = () => (
        <>
          <Item1 />
          <Item2 />
        </>
      );
      

(2)优化列表渲染:key 与虚拟列表

列表是 React 中常见的高频渲染场景,需重点优化:

  • 设置唯一且稳定的 keykey 是 React 识别列表项身份的标识,需满足「唯一」「稳定」(不随渲染顺序变化)。

❌ 错误:用索引(index)作为 key(若列表删除/插入项,会导致 key 与项错位,引发 DOM 复用错误和重渲染);

✅ 正确:用列表项的唯一 ID(如后端返回的 id)作为 key:

const TodoList = ({ todos }) => (
  <ul>
    {todos.map(todo => (
      <li key={todo.id}>{todo.content}</li> // 用唯一 ID 作为 key
    ))}
  </ul>
);
  • 虚拟列表(Virtual List) :当列表数据量极大(如 1000+ 项)时,即使只渲染可视区域的项,隐藏非可视区域的项,大幅减少 DOM 节点数量。

常用库:react-window(轻量)、react-virtualized(功能全)。

示例(react-window):

import { FixedSizeList as List } from "react-window";

const BigList = ({ data }) => {
  // 渲染单个列表项
  const Row = ({ index, style }) => (
    <div style={style}>{data[index]}</div>
  );

  return (
    <List
      height={500} // 列表容器高度
      itemCount={data.length} // 总数据量
      itemSize={50} // 单个列表项高度
      width="100%" // 列表容器宽度
    >
      {Row}
    </List>
  );
};

三、运行时优化:提升交互响应速度

运行时优化聚焦于「用户交互」场景(如输入、点击、滚动),减少延迟,提升流畅度。

1. 防抖(Debounce)与节流(Throttle)

对于高频触发的事件(如输入框 onChange、滚动 onScroll、窗口 resize),需通过防抖或节流限制函数执行频率,避免频繁触发导致卡顿。

  • 防抖(Debounce) :事件触发后延迟 N 毫秒执行函数,若 N 毫秒内再次触发,则重新计时(适用于输入搜索、表单提交)。
  • 节流(Throttle) :每隔 N 毫秒仅执行一次函数,无论事件触发多少次(适用于滚动加载、窗口 resize)。

示例:输入框搜索防抖(用 Lodash 的 debounce):

import { useState, useCallback } from "react";
import debounce from "lodash/debounce";

const SearchInput = () => {
  const [value, setValue] = useState("");

  // 用 useCallback 缓存防抖函数,避免每次渲染重新创建
  const fetchSearchResult = useCallback(
    debounce((keyword) => {
      // 发送搜索请求
      fetch(`/api/search?keyword=${keyword}`).then(res => res.json());
    }, 300), // 300ms 防抖延迟
    []
  );

  const handleChange = (e) => {
    const keyword = e.target.value;
    setValue(keyword);
    fetchSearchResult(keyword); // 触发防抖函数
  };

  return <input type="text" value={value} onChange={handleChange} />;
};

2. 优化状态更新:批量更新与优先级

React 内部会对「同步状态更新」进行批量合并,减少渲染次数,但「异步场景」(如 setTimeout、Promise 回调)中,批量更新会失效,导致多次渲染。

(1)强制批量更新:unstable_batchedUpdates

若需在异步场景中批量更新状态,可使用 React 提供的 unstable_batchedUpdates(注意:虽带 unstable,但在实际项目中已广泛使用,未来可能转正)。

示例:Promise 回调中批量更新状态:

import { unstable_batchedUpdates } from "react-dom";

const Parent = () => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const handleClick = () => {
    fetch("/api/data")
      .then(res => res.json())
      .then(() => {
        // 未批量:会触发 2 次渲染
        // setCount1(count1 + 1);
        // setCount2(count2 + 1);

        // 批量更新:仅触发 1 次渲染
        unstable_batchedUpdates(() => {
          setCount1(count1 + 1);
          setCount2(count2 + 1);
        });
      });
  };

  return <button onClick={handleClick}>更新</button>;
};

(2)优先级调度:useDeferredValuestartTransition

React 18 引入「并发渲染」机制,允许将状态更新标记为「低优先级」,避免高优先级更新(如输入、点击)被阻塞。

  • useDeferredValue:延迟更新低优先级状态(如列表过滤结果),优先保证高优先级操作(如输入框输入)的响应速度。

示例:输入时优先更新输入框,延迟更新过滤后的列表:

import { useDeferredValue, useState } from "react";

const SearchList = ({ list }) => {
  const [keyword, setKeyword] = useState("");
  // 延迟更新过滤结果(低优先级)
  const deferredKeyword = useDeferredValue(keyword);
  // 仅当 deferredKeyword 变化时,才重新过滤(避免输入时频繁计算)
  const filteredList = list.filter(item => item.includes(deferredKeyword));

  return (
    <div>
      <input 
        type="text" 
        value={keyword} 
        onChange={(e) => setKeyword(e.target.value)} 
        placeholder="输入搜索"
      />
      <ul>
        {filteredList.map((item, idx) => (
          <li key={idx}>{item}</li>
        ))}
      </ul>
    </div>
  );
};
  • startTransition:将状态更新标记为「过渡任务」(低优先级),确保高优先级更新(如点击按钮)不被阻塞。

示例:点击按钮时,优先更新按钮状态,延迟更新大数据列表:

import { useState, startTransition } from "react";

const BigDataList = ({ data }) => {
  const [isLoading, setIsLoading] = useState(false);
  const [filteredData, setFilteredData] = useState([]);

  const handleFilter = () => {
    // 高优先级:立即更新加载状态
    setIsLoading(true);
    // 低优先级:标记为过渡任务,避免阻塞 UI
    startTransition(() => {
      // 耗时过滤操作
      const result = data.filter(item => item.value > 1000);
      setFilteredData(result);
      setIsLoading(false);
    });
  };

  return (
    <div>
      <button onClick={handleFilter} disabled={isLoading}>
        过滤数据
      </button>
      {isLoading ? <div>加载中...</div> : (
        <ul>{filteredData.map(item => <li key={item.id}>{item.name}</li>)}</ul>
      )}
    </div>
  );
};

四、架构层优化:从根源减少性能瓶颈

若应用规模较大,需从架构设计层面优化,避免后期性能问题难以修复。

1. 状态管理优化

  • 状态分层:将状态分为「全局状态」(如用户信息、主题)和「局部状态」(如组件内部弹窗显示/隐藏),避免局部状态上升到全局(如 Redux)导致不必要的全局重渲染。

    • 全局状态:用 Redux Toolkit(配合 createSelector 缓存计算结果)、Zustand、Jotai 等,减少全局状态更新时的组件重渲染;
    • 局部状态:优先用 useState/useReducer,避免过度依赖全局状态。
  • 缓存选择器(Selector) :在 Redux 中,用 reselect 库的 createSelector 缓存派生数据(如过滤、排序后的列表),避免每次全局状态更新时重复计算。

示例:

import { createSelector } from "@reduxjs/toolkit";

// 基础选择器:获取原始列表
const selectTodos = state => state.todos;
// 缓存选择器:仅当 todos 变化时,才重新过滤
export const selectCompletedTodos = createSelector(
  [selectTodos],
  (todos) => todos.filter(todo => todo.completed)
);

2. 避免过度使用 Context

Context 会导致「订阅 Context 的组件」在 Context 值变化时全部重渲染,即使组件未使用变化的部分。若 Context 包含频繁更新的数据(如计数器),会导致大量组件无意义重渲染。

优化方案:

  • 拆分 Context:将 Context 按「更新频率」拆分,如「主题 Context」(低频更新)和「用户 Context」(中频更新)分开,避免一个 Context 变化影响所有组件;

  • Context 与 useMemo 结合:确保 Context.Provider 的 value 引用稳定,避免父组件渲染时 value 重新创建导致所有订阅组件重渲染:

    • const ThemeContext = createContext();
      
      const ThemeProvider = ({ children }) => {
        const [theme, setTheme] = useState("light");
        // 用 useMemo 缓存 value,避免每次渲染重新创建
        const contextValue = useMemo(() => ({
          theme,
          toggleTheme: () => setTheme(prev => prev === "light" ? "dark" : "light")
        }), [theme]);
      
        return (
          <ThemeContext.Provider value={contextValue}>
            {children}
          </ThemeContext.Provider>
        );
      };
      

五、性能优化工具:定位瓶颈

优化前需先通过工具定位性能瓶颈,避免盲目优化。

  1. React DevTools Profiler:React 官方调试工具,可录制组件渲染过程,查看「重渲染次数」「渲染耗时」「触发渲染的原因」,精准定位无意义重渲染的组件。

    1. 使用方式:打开 Chrome 开发者工具 → React 标签 → Profiler 选项卡 → 点击录制按钮 → 操作应用 → 停止录制,查看渲染报告。
  2. Lighthouse:Chrome 内置工具,可评估应用的「性能得分」,并提供具体优化建议(如图片优化、代码分割、首次内容绘制(FCP)优化)。

    1. 使用方式:打开 Chrome 开发者工具 → Lighthouse 选项卡 → 勾选「Performance」→ 点击「Generate report」。
  3. Chrome Performance 面板:录制应用运行时的 CPU、内存、DOM 操作等数据,分析「长任务」(耗时 > 50ms 的任务),定位阻塞主线程的代码。

总结

React 性能优化需遵循「先定位瓶颈,再针对性优化」的原则,核心思路可归纳为:

  1. 减少渲染次数:用 React.memo/useCallback/useMemo 控制渲染触发条件,隔离渲染上下文;
  2. 降低渲染成本:精简代码逻辑,优化资源加载,减少 DOM 节点;
  3. 提升运行时流畅度:用防抖/节流限制高频事件,用并发渲染(useDeferredValue/startTransition)优化状态更新优先级;
  4. 架构层规避瓶颈:合理分层状态,避免过度使用 Context 和全局状态。

根据应用规模和场景,选择合适的优化方案(如小型应用侧重渲染优化,大型应用需结合架构优化),才能最大化提升 React 应用性能。

HTML标签 - 列表标签

作者 GinoWi
2025年12月12日 13:39

HTML标签 - 列表标签

首先需要知道,什么是列表标签?

列表标签的作用就是给一堆数据添加列表语义,也就是告诉浏览器这一堆数据是一个整体。

无序列表(unordered list)

  • 作用:给一堆数据添加列表语义,并且这一堆数据中的所有数据都没有先后之分

那么什么叫做有先后之分?什么叫做没有先后之分?

这一部分数据不能随意替换展示先后顺序的,就是有先后之分,例如:排行榜

这一部分数据可以随意替换展示先后顺序的,就是没有先后之分,例如:中国城市列表

  • 格式:
<ul>
  <li></li>
  <li></li>
</ul>
  • 注意点:

    • 无序列表是用来给一堆数据添加列表语义的,而不是用于给这一堆数据添加小圆点样式的。
    • ul标签和li标签是一个整体,所以一般情况下,ul标签和li标签都是一起出现,不会单独出现,也就是说不会单独使用一个ul标签或者单独使用一个li标签,都是结合在一起使用。
    • 由于ul标签和li标签是一个组合,所以ul标签中不推荐包含其他标签,也就是说以后在ul标签中一般情况下只会看到li标签。
    • 前面说过ul中最好只放li标签,但是在开发过程中,li标签中的内容可能会很复杂,所以可以继续在li标签中添加其他标签来丰富界面。
    • 在无序列表的li标签中,除了可以添加其他标签来丰富界面以外,还可以通过添加ul标签来丰富界面,也就是说ul中有lili中又可以有ul
  • 快捷键:

    • ul>li + tab键:生成一对ul标签,在ul标签中生成一对li标签。
    • ul>li*3 + tab键:生成一对ul标签,在ul标签中生成3对li标签。
  • 无序列表应用场景举例:

    • 新闻列表
    • 商品列表
    • 导航条

有序列表(ordered list)

  • 作用:给一堆数据添加语义,并且这一堆数据中所有的数据都有先后之分

  • 格式:

<ol>
  <li></li>
  <li></li>
</ol>
  • 有序列表的区别仅仅是是否有先后之分,其他的使用方法和注意点与无序列表ul标签都差不多。

定义列表(definition list)

  • 作用:给一堆数据添加列表语义,先通过dt标签定义列表中的所有标题,然后通过dd标签给每个标题添加描述信息

  • 格式:

<dl>
  <dt>北京</dt>
  <dd>中国的首都</dd>
  <dt>上海</dt>
  <dd>中国的经济中心</dd>
</dl>
  • 标签含义:dtdd都是英文缩写,dt是definition title的缩写,所以dt的含义就是用来定义列表中的标题dd是definition description的缩写,所以dd的含义就是用来定义标题对应的描述

  • 应用场景:

    • 做网站尾部的相关信息
    • 图文混排
  • 注意点:

    • ul/ol一样,dldt/dd是一个整体,一般情况下不会单独出现,都是一起出现。
    • ul/ol一样,由于dldt/dd是一个组合标签,所以dl中建议只放dtdd标签。
    • 一个dt可以没有对应的dd,也可以有多个对应的dd,但是无论有多个dd或者没有dd,都不推荐使用。推荐使用一个dt对应一个dd
    • li标签一样,当需要丰富界面的时候,可以在dt/dd标签中继续添加其他标签,但是建议不要再dl标签中添加。
  • 快捷键:

    • dl>dt+dd + tab键:生成一对dl标签,在dl标签当中生成一对dtdd标签。

参考链接:

W3School官方文档:www.w3school.com.cn

Flutter输入框TextField的属性与实战用法全面解析+示例

作者 鹏多多
2025年12月12日 13:33

在 Flutter 开发中,输入框是与用户交互的核心组件之一,无论是登录注册、搜索框还是表单填写,都离不开 TextField 或其封装组件 TextFormField。本文将详细介绍输入框的核心属性常用功能实战用法,预计阅读时间5分钟,下面开始吧~

1. 基础概念

Flutter 提供了两个主要的输入框组件:

  • TextField:基础输入框组件,提供基本的文本输入功能。
  • TextFormField:继承自 TextField,增加了表单验证、自动保存等功能,适合在 Form 组件中使用。

两者核心属性基本一致,本文以 TextField 为例讲解,差异部分会单独说明。

2. 核心属性详解

核心属性的详解,我会按照分类归纳,如下:

2.1. 基本配置

属性名 类型 说明
controller TextEditingController 控制输入框文本(获取、设置、监听文本变化)
keyboardType TextInputType 键盘类型(如数字、邮箱、手机号等)
textInputAction TextInputAction 键盘动作按钮(如完成、下一步、搜索)
obscureText bool 是否隐藏输入内容(密码框常用)
maxLines int 最大行数(单行输入设为 1,多行设为 null 或更大值)
minLines int 最小行数
maxLength int 最大输入长度(会显示计数器)
maxLengthEnforcement MaxLengthEnforcement 超出长度时的处理方式(截断/禁止输入)
scrollPhysics ScrollPhysics 输入框内容滚动时的物理效果(如禁止滚动、总是可滚动等)
textCapitalization TextCapitalization 自动大写规则(如句子首字母大写、所有字母大写等)

示例:基础配置

TextField(
  controller: _controller, // 绑定控制器
  keyboardType: TextInputType.number, // 数字键盘
  textInputAction: TextInputAction.done, // 键盘按钮为"完成"
  obscureText: false, // 不隐藏文本
  maxLines: 1, // 单行输入
  maxLength: 11, // 最大长度11(如手机号)
  // 多行输入时禁止滚动(内容超出时也不滚动)
  scrollPhysics: NeverScrollableScrollPhysics(),
  // 句子首字母自动大写(适合英文输入)
  textCapitalization: TextCapitalization.sentences,
  略......
)

2.2. 样式配置

属性名 类型 说明
style TextStyle 输入文本的样式(字体、大小、颜色等)
decoration InputDecoration 输入框装饰(边框、提示文本、图标等)
cursorColor Color 光标颜色
cursorWidth double 光标宽度
cursorHeight double 光标高度
textAlign TextAlign 文本对齐方式(左、中、右)
textDirection TextDirection 文本方向(从左到右/从右到左)
strutStyle StrutStyle 控制文本的基线对齐(解决不同字体大小导致的对齐问题)
toolbarOptions ToolbarOptions 长按文本时显示的工具栏选项(如复制、粘贴、剪切)
selectionControls TextSelectionControls 自定义文本选择控件(如替换系统默认的复制粘贴工具栏)

示例:自定义样式

TextField(
  style: TextStyle(
    fontSize: 16,
    color: Colors.black87,
  ),
  cursorColor: Colors.blue,
  cursorWidth: 2,
  textAlign: TextAlign.start,
  decoration: InputDecoration(
    hintText: "请输入用户名", // 提示文本
    hintStyle: TextStyle(color: Colors.grey), // 提示文本样式
    prefixIcon: Icon(Icons.person), // 左侧图标
    border: OutlineInputBorder( // 边框样式
      borderRadius: BorderRadius.circular(8),
    ),
    contentPadding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), // 内边距
  ),
  // 仅允许复制和粘贴操作(禁用剪切)
  toolbarOptions: ToolbarOptions(
    copy: true,
    paste: true,
    cut: false,
  ),
  // 自定义选择控件(需实现TextSelectionControls)
  // selectionControls: CustomSelectionControls(),
  略......
)

2.3. 交互控制

属性名 类型 说明
enabled bool 是否启用输入框(false 时禁用,不可输入)
readOnly bool 是否只读(可点击但不可编辑,区别于禁用)
autofocus bool 是否自动获取焦点
focusNode FocusNode 焦点控制器(手动管理焦点状态)
onChanged Function(String) 文本变化时触发(实时监听输入)
onSubmitted Function(String) 点击键盘动作按钮时触发(如点击"完成")
onTap Function() 点击输入框时触发
inputFormatters List<TextInputFormatter> 输入格式化(限制输入内容,如只允许数字)
onEditingComplete VoidCallback 编辑完成时触发(区别于onSubmitted,不接收文本参数)
onAppPrivateCommand Function(String, Map<String, dynamic>) 处理私有命令(通常用于原生与Flutter交互时传递指令)
enableIMEPersonalizedLearning bool 是否允许输入法个性化学习输入内容(默认true)

示例:

TextField(
  enabled: true, // 启用输入
  readOnly: false, // 可编辑
  autofocus: true, // 自动聚焦
  onChanged: (value) {
    print("输入变化:$value"); // 实时监听
  },
  onSubmitted: (value) {
    print("提交内容:$value"); // 点击完成时触发
  },
  // 编辑完成时触发(如手动调用_controller.text后)
  onEditingComplete: () {
    print("编辑已完成");
    // 通常配合焦点管理,如切换到下一个输入框
    FocusScope.of(context).nextFocus();
  },
  // 禁用输入法个性化学习(适用于敏感输入场景)
  enableIMEPersonalizedLearning: false,
  inputFormatters: [
    FilteringTextInputFormatter.digitsOnly, // 只允许输入数字
  ],
  略......
)

焦点管理

FocusNode 用于手动控制输入框的焦点状态,常见场景包括:切换输入框焦点、监听焦点变化、强制获取/失去焦点等。

示例:

// 1. 初始化焦点节点
final FocusNode _usernameFocus = FocusNode();
final FocusNode _passwordFocus = FocusNode();

@override
void dispose() {
  // 销毁焦点节点(避免内存泄漏)
  _usernameFocus.dispose();
  _passwordFocus.dispose();
  super.dispose();
}

// 2. 监听焦点变化
void initState() {
  super.initState();
  _usernameFocus.addListener(() {
    if (_usernameFocus.hasFocus) {
      print("用户名输入框获取焦点");
    } else {
      print("用户名输入框失去焦点");
    }
  });
}

// 3. 组件中使用
Column(
  children: [
    TextField(
      focusNode: _usernameFocus,
      decoration: InputDecoration(hintText: "用户名"),
      textInputAction: TextInputAction.next, // 键盘显示"下一步"
      onSubmitted: (_) {
        // 点击下一步时,将焦点切换到密码框
        FocusScope.of(context).requestFocus(_passwordFocus);
      },
    ),
    TextField(
      focusNode: _passwordFocus,
      decoration: InputDecoration(hintText: "密码"),
      obscureText: true,
    ),
    ElevatedButton(
      onPressed: () {
        // 手动让密码框获取焦点
        FocusScope.of(context).requestFocus(_passwordFocus);
        // 或手动失去所有焦点(收起键盘)
        // FocusScope.of(context).unfocus();
      },
      child: Text("操作焦点"),
    ),
  ],
)

2.4. 其他实用属性

属性名 类型 说明
autocorrect bool 是否启用自动纠错(默认 true)
autocompleteMode AutocompleteMode 自动完成模式(如关闭、用户名、邮箱等)
scrollPadding EdgeInsets 滚动时的内边距(避免被键盘遮挡)
enableInteractiveSelection bool 是否允许长按选中文本(默认 true)
mouseCursor MouseCursor 鼠标悬停时的光标样式(桌面端开发常用)
clipBehavior Clip 内容超出输入框时的裁剪方式(如Clip.none不裁剪)
restorationId String 用于状态恢复(配合RestorationMixin保存/恢复输入状态)
scribbleEnabled bool 是否启用手写输入(iPad等支持Apple Pencil的设备)
TextField(
  // 桌面端鼠标悬停时显示文本输入光标
  mouseCursor: SystemMouseCursors.text,
  // 内容超出时不裁剪(可能导致UI溢出,谨慎使用)
  clipBehavior: Clip.none,
  // 启用手写输入(iPad场景)
  scribbleEnabled: true,
  略......
)

3. 控制器TextEditingController的使用

TextEditingController 是控制输入框文本的核心工具,主要功能包括:

3.1. 获取输入文本

// 初始化控制器
final TextEditingController _controller = TextEditingController();

// 获取文本(通常在按钮点击等事件中调用)
ElevatedButton(
  onPressed: () {
    String inputText = _controller.text;
    print("用户输入:$inputText");
  },
  child: Text("获取输入"),
)

// 组件中绑定
TextField(controller: _controller)

3.2. 设置文本内容

ElevatedButton(
  onPressed: () {
    // 直接设置文本
    _controller.text = "默认文本";
  },
  child: Text("设置默认值"),
)

3.3. 清空文本

ElevatedButton(
  onPressed: () {
    // 清空输入框
    _controller.clear();
  },
  child: Text("清空输入"),
)

3.4. 监听文本变化

@override
void initState() {
  super.initState();
  // 监听文本变化(实时响应输入)
  _controller.addListener(() {
    String currentText = _controller.text;
    print("当前输入:$currentText");
    // 可在这里实现实时验证,如输入长度判断
    if (currentText.length > 5) {
      print("输入长度超过5个字符");
    }
  });
}

@override
void dispose() {
  // 销毁控制器(必须调用,避免内存泄漏)
  _controller.dispose();
  super.dispose();
}

3.5. 控制光标位置

ElevatedButton(
  onPressed: () {
    // 将光标移动到文本末尾
    _controller.selection = TextSelection.fromPosition(
      TextPosition(offset: _controller.text.length),
    );
  },
  child: Text("光标移至末尾"),
)

4. TextFormField与表单验证

TextFormField 用于 Form 组件中,支持表单验证,核心属性增加了 validator,例子如下:

Form(
  key: _formKey, // 表单key,用于触发验证
  child: Column(
    children: [
      TextFormField(
        decoration: InputDecoration(hintText: "请输入邮箱"),
        keyboardType: TextInputType.emailAddress,
        // 验证逻辑
        validator: (value) {
          if (value == null || value.isEmpty) {
            return "请输入邮箱";
          }
          if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
            return "请输入正确的邮箱格式";
          }
          return null; // 验证通过
        },
      ),
      ElevatedButton(
        onPressed: () {
          // 触发表单验证
          if (_formKey.currentState!.validate()) {
            // 验证通过,执行提交逻辑
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text("提交成功")),
            );
          }
        },
        child: Text("提交"),
      ),
    ],
  ),
)

5. 实战技巧

  1. 解决键盘遮挡问题

    • Scaffold 中设置 resizeToAvoidBottomInset: true(默认值),键盘弹出时自动调整布局。
    • 结合 SingleChildScrollView 等滑动组件包裹起来,使页面可滚动。
    • 解决键盘弹出会遮挡输入框的问题,可以参考我之前的文章:传送门
  2. 自定义输入框样式

    • 通过 InputDecorationborderfocusedBorderenabledBorder 区分不同状态的边框样式。
    • 示例:聚焦时显示蓝色边框,未聚焦时显示灰色边框。
  3. 密码框切换显示/隐藏

   bool _obscureText = true;

   TextField(
     obscureText: _obscureText,
     decoration: InputDecoration(
       suffixIcon: IconButton(
         icon: Icon(
           _obscureText ? Icons.visibility : Icons.visibility_off,
         ),
         onPressed: () {
           setState(() {
             _obscureText = !_obscureText;
           });
         },
       ),
     ),
   )
  1. 限制输入类型
    • 使用 inputFormatters 配合 FilteringTextInputFormatter 限制输入(如数字、字母、禁止表情等)。

6. 总结

TextFieldTextFormField 是 Flutter 中处理文本输入的核心组件,通过 TextEditingController 可灵活控制文本内容,通过 FocusNode 可精准管理焦点状态。掌握输入框的样式定制、文本控制、焦点管理和表单验证,能显著提升用户体验。实际开发中,需注意控制器和焦点节点的生命周期管理(及时销毁,避免内存泄漏),并根据具体场景选择合适的组件配置。


本次分享就到这儿啦,我是鹏多多,深耕前端的技术创作者,如果您看了觉得有帮助,欢迎评论,关注,点赞,转发,我们下次见~

PS:在本页按F12,在console中输入document.getElementsByClassName('panel-btn')[0].click();有惊喜哦~

往期文章

拒绝做 DOM 的“搬运工”:从 Vanilla JS 到 Vue 3 响应式思维的进化

作者 San30
2025年12月12日 13:20

在前端开发的漫长演进中,我们经常听到“数据驱动”这个词。但对于很多习惯了 jQuery 或者原生 JavaScript(Vanilla JS)的开发者来说,从“操作 DOM”到“操作数据”的思维转变,往往比学习新语法更难。

今天,我们将通过重构一个经典的 Todos 任务清单应用,来深度剖析 Vue 3 Composition API 是如何解放我们的双手,让我们专注于业务逻辑而非繁琐的页面渲染。

1. 痛点回顾:原生 JS 的“命令式”困境

在没有框架的时代,写一个简单的输入框回显功能,我们通常需要经历这几个步骤:寻找元素 -> 监听事件 -> 获取值 -> 修改 DOM。

让我们看看这个基于原生 JS 的实现片段:

// 先找到DOM元素, 命令式的, 机械的
const app = document.getElementById('app');
const todoInput = document.getElementById('todo-input');

todoInput.addEventListener('change', function(event) {
    const todo = event.target.value.trim();
    if (!todo) return;
    // 手动操作 DOM 更新
    app.innerHTML = todo; 
})

这种代码被称为命令式编程(Imperative Programming) 。正如在代码注释中所写,这是一种“机械”的过程。我们需要关注每一个步骤的实现细节。而且,频繁地操作 DOM 性能是低下的,因为这涉及到了 JS 引擎(V8)与渲染引擎之间的跨界通信。

随着应用变得复杂,大量的 getElementByIdinnerHTML 会让代码变成难以维护的“意大利面条”。

2. Vue 3 的破局:响应式数据与声明式渲染

Vue 的核心在于声明式编程(Declarative Programming) 。你只需要告诉 Vue “想要什么结果”,中间的 DOM 更新过程由 Vue 替你完成。

在 Vue 3 中,我们利用 setup 函数和 Composition API(组合式 API)来组织逻辑。

2.1 核心概念:ref 与数据驱动

App.vue 中,我们不再去查询 DOM 元素,而是定义了响应式数据

import { ref, computed } from 'vue'

// 响应式数据
const title = ref("");
const todos = ref([
  { id: 1, title: '睡觉', done: true },
  { id: 2, title: '吃饭', done: false }
]);

这里体现了 Vue 开发的核心思路: “不再需要思考页面的元素怎么操作,而是要思考数据是怎么变化的”

2.2 指令:连接数据与视图的桥梁

有了数据,我们通过 Vue 的指令将数据绑定到模板上:

  • 双向绑定 (v-model)<input type="text" v-model="title">。当用户输入时,title 变量自动更新;反之亦然。这比手动写 addEventListener 优雅得多。
  • 列表渲染 (v-for)<li v-for="todo in todos" :key="todo.id">。Vue 会根据 todos 数组的变化,智能地添加、删除或更新 <li> 元素。注意这里 :key 的使用,它是 Vue 识别节点的唯一标识,对性能至关重要。
  • 样式绑定 (:class)<span :class="{done: todo.done}">。我们不再需要手动 classList.add('done'),只需改变数据 todo.done,样式就会自动生效。

2.3 智能的条件渲染:v-if 与 v-else 的排他性逻辑

在实际应用中,用户体验细节至关重要。例如,当任务列表被清空时,我们不应该留给用户一片空白,而应该展示“暂无任务”的提示。在原生 JS 中,这通常需要我们在每次添加或删除操作后,手动检查数组长度并切换 DOM 的 display 属性。

而在 Vue 中,我们可以通过 v-ifv-else 指令,像写 if-else 代码块一样在模板中轻松处理这种逻辑分支:

<ul v-if="todos.length">
  <li v-for="todo in todos" :key="todo.id">
    ...
  </li>
</ul>
<div v-else>
  <span>暂无任务</span>
</div>

代码深度解析:

  1. 真实 DOM 的销毁与重建v-if 是真正的条件渲染。当 todos.length 为 0 时,Vue 不仅仅是隐藏了 <ul>(像 CSS 的 display: none 那样),而是直接从 DOM 中移除了整个列表元素。这意味着此时 DOM 中只有 <div>暂无任务</div>,减少了页面的 DOM 节点数量。
  2. 响应式切换:一旦我们向 todos 数组 push 了一条新数据,todos.length 变为 1。Vue 的响应式系统会立即感知,销毁 v-else 元素,并重新创建并插入 <ul> 列表。
  3. 逻辑互斥v-else 必须紧跟在 v-if 元素之后,它们构成了一个封闭的逻辑组,保证了同一时间页面上只会存在其中一种状态。

通过这两个指令,我们不仅实现了界面的动态交互,更重要的是,我们将“列表为空时显示什么”的业务逻辑直接通过模板表达了出来,不仅代码量减少了,意图也更加清晰。

3. 深度解析:Computed 计算属性 vs. 模板逻辑

在开发中,我们经常需要根据现有的数据计算出新的状态,比如统计“剩余未完成任务数”。

3.1 为什么要用 Computed?

初学者可能会直接在模板里写逻辑:

{{ todos.filter(todo => !todo.done).length }}

虽然这也能工作,但 Vue 官方更推荐使用 Computed(计算属性)

// 创建一个响应式的计算属性
const active = computed(() => {
  return todos.value.filter(todo => !todo.done).length
})

computed 的四大优势:

  1. 性能优化(带缓存) :这是最大的区别。模板内的表达式在每次组件重渲染时都会重新执行。而 computed 只有在它依赖的数据(这里是 todos)发生变化时才会重新计算。如果 todos 没变,多次访问 active 会直接返回缓存值。
  2. 可读性:将复杂的逻辑从 HTML 模板中剥离到 JS 中,让模板保持干净、语义化。
  3. 可复用性active 可以在模板中多处使用,也可以在 JS 逻辑中被引用。
  4. 调试与测试:单独测试一个 JS 函数远比测试模板中的一段逻辑要容易。

3.2 进阶技巧:Computed 的 Get 与 Set

计算属性通常是只读的,但 Vue 也允许我们定义 set 方法,这在处理“全选/全不选”功能时非常强大。

看看这段精妙的代码:

const allDone = computed({
  // 读取值:判断是否所有任务都已完成
  get() {
    return todos.value.every(todo => todo.done)
  },
  // 设置值:当点击全选框时,将所有任务状态同步修改
  set(value) {
    todos.value.forEach(todo => todo.done = value)
  }
})

在模板中,我们只需绑定 <input type="checkbox" v-model="allDone">

  • 当用户点击复选框,Vue 调用 set(value),我们遍历数组更新所有 todo.done
  • 当所有子任务被手动勾选,get() 返回 true,全选框自动被勾选。

这种双向的逻辑联动,如果用原生 JS 实现,需要编写大量的事件监听和状态判断代码,而在 Vue 中,它被封装成了一个优雅的属性。

4. 总结:Vue 开发方式的哲学

demo.htmlApp.vue,我们经历的不仅仅是语法的改变,更是思维模式的重构:

  • Focus on Business:我们不再是浏览器的“建筑工人”(搬运 DOM),而是“设计师”(定义数据状态)。
  • Composition APIsetuprefcomputed 让我们能够更灵活地组合逻辑,比 Vue 2 的 Options API 更利于代码复用和类型推断。
  • Best Practices:永远不要在模板中写复杂的逻辑,善用 computed 缓存机制。

Vue 3 通过响应式系统,替我们处理了脏活累活(DOM 更新),让我们能将精力集中在真正有价值的业务逻辑上。对于想要构建复杂交互系统(如粒子特效、数据可视化)的开发者来说,掌握这种“数据驱动”的思维是迈向高阶开发的第一步。

5.附录:完整App.vue代码

<template>
   <div>
    <h2>{{ title }}</h2>
    <input type="text" v-model="title" @keydown.enter="addTodo">
    <ul v-if="todos.length">
      <li v-for="todo in todos" :key="todo.id">
        <input type="checkbox" v-model="todo.done">
        <span :class="{done: todo.done}">{{ todo.title }}</span>
      </li>
    </ul>
    <div v-else>
      <span>暂无任务</span>
    </div>
    <div>
      全选<input type="checkbox" v-model="allDone">
      {{ active }}
      /
      {{ todos.length }}
    </div>
   </div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 响应式数据
const title = ref("");
const todos = ref([
  {
    id: 1,
    title: '睡觉',
    done: true
  },
  {
    id: 2,
    title: '吃饭',
    done: false
  }
]);

const active = computed(() => {
  return todos.value.filter(todo => !todo.done).length
})

const addTodo = () => {
  if(!title.value) return;
  todos.value.push({
    id: todos.value.length + 1,
    title: title.value,
    done: false
  });
  title.value = '';
}
const allDone = computed({
  get() {
    return todos.value.every(todo => todo.done)
  },
  set(value) {
    todos.value.forEach(todo => todo.done = value)
  }
})
</script>
<style>
  .done {
    color: gray;
    text-decoration: line-through;
  }
</style>

Vue 任务清单开发:数据驱动 vs 传统 DOM 操作

作者 冻梨政哥
2025年12月12日 13:04

Vue 任务清单开发:数据驱动 vs 传统 DOM 操作

在前端开发中,任务清单是一个常见的案例,通过这个案例我们可以清晰对比传统 DOM 操作与 Vue 数据驱动开发的差异。本文将结合具体代码,解析 Vue 的核心思想和常用 API。

传统开发方式的局限

传统 JavaScript 开发中,我们需要手动操作 DOM 元素来实现功能。以下代码为例:

<h2 id="app"></h2>
<input type="text" id="todo-input">
<script>
    // 传统方式需要先获取DOM元素
    const app = document.getElementById('app');
    const todoInput = document.getElementById('todo-input');
    
    // 手动绑定事件并操作DOM
    todoInput.addEventListener('change',function(event) {
        const todo = event.target.value.trim();
        if(!todo){
            console.log('请输入任务');
            return ;
        }else{
            // 直接修改DOM内容
            app.innerHTML = todo;
        }
    })
</script>

这种方式的特点是:

  • 需要手动获取 DOM 元素
  • 命令式地操作 DOM 进行更新
  • 业务逻辑与 DOM 操作混杂
  • 随着功能复杂,代码会变得难以维护

Vue 的数据驱动开发理念

Vue 采用了完全不同的思路:开发者只需关注数据本身,而非 DOM 操作。以任务清单为例:

<template>
  <div>
    <!-- 数据绑定 -->
    <h2>{{ title }}</h2>
    <!-- 双向数据绑定 -->
    <input type="text" v-model="title" @keydown.enter="addTodo">
    
    <!-- 条件渲染 -->
    <ul v-if="todos.length">
      <!-- 循环渲染 -->
      <li v-for="todo in todos" :key="todo.id">
        <input type="checkbox" v-model="todo.done">
        <span :class="{done: todo.done}">{{ todo.title }}</span>
      </li>
    </ul>
    <div v-else>暂无计划</div>
  </div>
</template>

Vue 的核心思想是:不再关心页面元素如何操作,只关注数据如何变化。当数据发生改变时,Vue 会自动更新 DOM,开发者无需手动操作。

Vue 常用 API 解析

  1. v-model 双向数据绑定

    <input type="text" v-model="title">
    

    实现表单输入与数据的双向绑定,输入框的变化会自动更新数据,数据的变化也会自动反映到输入框。

  2. v-for 循环渲染

    <li v-for="todo in todos" :key="todo.id">
    

    基于数组渲染列表,:key用于标识每个元素的唯一性,提高渲染性能。

  3. v-if/v-else 条件渲染

    <ul v-if="todos.length">
      ...
    </ul>
    <div v-else>暂无计划</div>
    

    根据条件动态渲染不同的内容,当todos数组为空时显示 "暂无计划"。

  4. :class 动态类绑定

    <span :class="{done: todo.done}">{{ todo.title }}</span>
    

    todo.donetrue时,自动为元素添加done类,实现完成状态的样式变化。

  5. @事件监听

    <input type="text" @keydown.enter="addTodo">
    

    监听键盘回车事件,触发addTodo方法,@v-bind:的缩写。

  6. computed 计算属性

    // 计算未完成的任务数量
    const active = computed(() => {
      return todos.value.filter(todo => !todo.done).length
    })
    
    // 全选功能的实现
    const allDone = computed({
      get(){
        return todos.value.every(todo => todo.done)
      },
      set(val){
        todos.value.forEach(todo => todo.done = val)
      }
    })
    

    计算属性具有缓存特性,只有依赖的数据变化时才会重新计算,相比方法调用更节省性能。全选功能展示了计算属性的高级用法,通过getset实现双向绑定。

  7. ref 响应式数据

    import { ref } from 'vue'
    const title = ref("");
    const todos = ref([...])
    

    创建响应式数据,当这些数据变化时,Vue 会自动更新相关的 DOM。

总结

Vue 通过数据驱动的方式,极大简化了前端开发流程:

  • 开发者可以专注于业务逻辑和数据处理
  • 减少了大量手动 DOM 操作的代码
  • 提供了简洁直观的 API,降低学习成本
  • 内置的性能优化(如计算属性缓存)让应用运行更高效

JavaScript常用设计模式完整指南

作者 1024肥宅
2025年12月12日 12:46

引言

设计模式是软件工程中解决常见问题的可复用方案。在JavaScript开发中,合理运用设计模式可以提高代码的可维护性、可扩展性和可读性。本文将详细介绍JavaScript中常用的设计模式及其实现。

一、设计模式分类

设计模式主要分为三大类:

  • 创建型模式: 处理对象创建机制
  • 结构型模式: 处理对象组合和关系
  • 行为型模式: 处理对象间通信和职责分配

二、创建型模式

2.1 单例模式 (Singleton Pattern)

确保一个类只有一个实例, 并提供全局访问点。

// ES6实现
class Singleton {
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    this.data = {};
    Singleton.instance = this;
    return this;
  }

  setData(key, value) {
    this.data[key] = value;
  }

  getData(key) {
    return this.data[key];
  }
}

// 使用示例
const instance1 = new Singleton();
const instance2 = new Singleton();

console.log(instance1 === instance2); // true
instance1.setData('name', 'Singleton');
console.log(instance2.getData('name')); // 'Singleton'

// 闭包实现
const SingletonClosure = (function() {
  let instance;
  
  function createInstance() {
    const object = { data: {} };
    return {
      setData: (key, value) => object.data[key] = value,
      getData: (key) => object.data[key]
    };
  }
  
  return {
    getInstance: function() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    }
  };
})();
2.2 工厂模式 (Factory Pattern)

创建对象而不暴露创建逻辑, 通过一个公共接口创建对象。

// 简单工厂模式
class Car {
  constructor(options) {
    this.type = options.type || 'sedan';
    this.color = options.color || 'white';
    this.price = options.price || 20000;
  }
}

class Truck {
  constructor(options) {
    this.type = options.type || 'truck';
    this.color = options.color || 'blue';
    this.price = options.price || 50000;
    this.capacity = options.capacity || '5t';
  }
}

class VehicleFactory {
  static createVehicle(type, options) {
    switch (type) {
      case 'car':
        return new Car(options);
      case 'truck':
        return new Truck(options);
      default:
        throw new Error('Unknown vehicle type');
    }
  }
}

// 使用示例
const myCar = VehicleFactory.createVehicle('car', {
  color: 'red',
  price: 25000
});

const myTruck = VehicleFactory.createVehicle('truck', {
  color: 'black',
  capacity: '10t'
});

// 工厂方法模式
class Vehicle {
  drive() {
    console.log(`${this.type} is driving`);
  }
}

class Car2 extends Vehicle {
  constructor() {
    super();
    this.type = 'Car';
  }
}

class Truck2 extends Vehicle {
  constructor() {
    super();
    this.type = 'Truck';
  }
}

class VehicleFactory2 {
  createVehicle() {
    throw new Error('This method must be overridden');
  }
}

class CarFactory extends VehicleFactory2 {
  createVehicle() {
    return new Car2();
  }
}

class TruckFactory extends VehicleFactory2 {
  createVehicle() {
    return new Truck2();
  }
}
2.3 建造者模式 (Builder Pattern)

将复杂对象的构建与其表示分离, 使同样的构建过程可以创建不同的表示。

class Pizza {
  constructor() {
    this.size = null;
    this.crust = null;
    this.cheese = false;
    this.pepperoni = false;
    this.mushrooms = false;
    this.onions = false;
  }

  describe() {
    console.log(`Pizza: Size-${this.size}, Crust-${this.crust}, 
      Cheese-${this.cheese}, Pepperoni-${this.pepperoni},
      Mushrooms-${this.mushrooms}, Onions-${this.onions}`);
  }
}

class PizzaBuilder {
  constructor() {
    this.pizza = new Pizza();
  }

  setSize(size) {
    this.pizza.size = size;
    return this;
  }

  setCrust(crust) {
    this.pizza.crust = crust;
    return this;
  }

  addCheese() {
    this.pizza.cheese = true;
    return this;
  }

  addPepperoni() {
    this.pizza.pepperoni = true;
    return this;
  }

  addMushrooms() {
    this.pizza.mushrooms = true;
    return this;
  }

  addOnions() {
    this.pizza.onions = true;
    return this;
  }

  build() {
    return this.pizza;
  }
}

// 使用示例
const pizza = new PizzaBuilder()
  .setSize('large')
  .setCrust('thin')
  .addCheese()
  .addPepperoni()
  .addMushrooms()
  .build();

pizza.describe();
2.4 原型模式 (Prototype Pattern)

通过复制现有对象来创建新对象, 而不是通过实例化类。

// 使用Object.create实现原型模式
const carPrototype = {
  wheels: 4,
  drive() {
    console.log(`${this.brand} is driving with ${this.wheels} wheels`);
  },
  clone() {
    return Object.create(this);
  }
};

// 创建新对象
const tesla = Object.create(carPrototype);
tesla.brand = 'Tesla';
tesla.model = 'Model 3';

const anotherTesla = Object.create(tesla);
anotherTesla.model = 'Model S';

tesla.drive(); // Tesla is driving with 4 wheels
anotherTesla.drive(); // Tesla is driving with 4 wheels

// ES6类实现原型模式
class VehiclePrototype {
  constructor(proto) {
    Object.assign(this, proto);
  }

  clone() {
    return new VehiclePrototype(this);
  }
}

const bikeProto = {
  wheels: 2,
  ride() {
    console.log(`Riding ${this.brand} with ${this.wheels} wheels`);
  }
};

const bike = new VehiclePrototype(bikeProto);
bike.brand = 'Giant';
const anotherBike = bike.clone();
anotherBike.brand = 'Trek';

三、结构型模式

3.1 装饰器模式 (Decorator Pattern)

动态地给对象添加额外职责, 而不改变其结构。

// ES7装饰器语法
function log(target, name, descriptor) {
  const original = descriptor.value;
  
  descriptor.value = function(...args) {
    console.log(`Calling ${name} with`, args);
    const result = original.apply(this, args);
    console.log(`Result: ${result}`);
    return result;
  };
  
  return descriptor;
}

class Calculator {
  @log
  add(a, b) {
    return a + b;
  }
}

// 传统JavaScript实现
class Coffee {
  cost() {
    return 5;
  }
}

class CoffeeDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }

  cost() {
    return this.coffee.cost();
  }
}

class MilkDecorator extends CoffeeDecorator {
  cost() {
    return this.coffee.cost() + 2;
  }
}

class SugarDecorator extends CoffeeDecorator {
  cost() {
    return this.coffee.cost() + 1;
  }
}

// 使用示例
let myCoffee = new Coffee();
console.log(`Basic coffee: $${myCoffee.cost()}`);

myCoffee = new MilkDecorator(myCoffee);
console.log(`Coffee with milk: $${myCoffee.cost()}`);

myCoffee = new SugarDecorator(myCoffee);
console.log(`Coffee with milk and sugar: $${myCoffee.cost()}`);
3.2 代理模式 (Proxy Pattern)

为其他对象提供一种代理以控制对这个对象的访问。

// ES6 Proxy实现
const target = {
  message: "Hello, World!",
  getMessage() {
    return this.message;
  }
};

const handler = {
  get: function(obj, prop) {
    if (prop === 'message') {
      console.log('Accessing message property');
      return obj[prop] + ' (via proxy)';
    }
    return obj[prop];
  },
  
  set: function(obj, prop, value) {
    if (prop === 'message') {
      console.log(`Setting message to: ${value}`);
      obj[prop] = value;
      return true;
    }
    return false;
  }
};

const proxy = new Proxy(target, handler);

console.log(proxy.message); // "Hello, World! (via proxy)"
proxy.message = "New Message"; // "Setting message to: New Message"

// 保护代理示例
const sensitiveData = {
  username: 'admin',
  password: 'secret123',
  creditCard: '1234-5678-9012-3456'
};

const protectionHandler = {
  get: function(obj, prop) {
    if (prop === 'password' || prop === 'creditCard') {
      return 'Access denied';
    }
    return obj[prop];
  },
  
  set: function(obj, prop, value) {
    if (prop === 'password' || prop === 'creditCard') {
      console.log('Cannot modify sensitive data directly');
      return false;
    }
    obj[prop] = value;
    return true;
  }
};

const protectedData = new Proxy(sensitiveData, protectionHandler);
3.3 适配器模式 (Adapter Pattern)

将一个类的接口转换成客户期望的另一个接口。

// 旧系统接口
class OldSystem {
  specificRequest() {
    return 'Old system response';
  }
}

// 新系统期望的接口
class NewSystem {
  request() {
    return 'New system response';
  }
}

// 适配器
class Adapter {
  constructor(oldSystem) {
    this.oldSystem = oldSystem;
  }

  request() {
    const result = this.oldSystem.specificRequest();
    return `Adapted: ${result}`;
  }
}

// 使用示例
const oldSystem = new OldSystem();
const adapter = new Adapter(oldSystem);

console.log(adapter.request()); // "Adapted: Old system response"

// 实际应用示例:数据格式适配
class JSONData {
  getData() {
    return '{"name": "John", "age": 30}';
  }
}

class XMLData {
  getData() {
    return '<user><name>John</name><age>30</age></user>';
  }
}

class DataAdapter {
  constructor(dataSource) {
    this.dataSource = dataSource;
  }

  getJSON() {
    const data = this.dataSource.getData();
    
    // 如果是XML,转换为JSON
    if (data.startsWith('<')) {
      // 简单转换逻辑
      const nameMatch = data.match(/<name>(.*?)<\/name>/);
      const ageMatch = data.match(/<age>(.*?)<\/age>/);
      
      return JSON.stringify({
        name: nameMatch ? nameMatch[1] : '',
        age: ageMatch ? parseInt(ageMatch[1]) : 0
      });
    }
    
    return data;
  }
}
3.4 外观模式 (Facade Pattern)

为复杂的子系统提供一个统一的简单接口。

// 复杂的子系统
class CPU {
  start() {
    console.log('CPU started');
  }
  
  execute() {
    console.log('CPU executing instructions');
  }
}

class Memory {
  load() {
    console.log('Memory loading data');
  }
}

class HardDrive {
  read() {
    console.log('Hard drive reading data');
  }
}

// 外观
class ComputerFacade {
  constructor() {
    this.cpu = new CPU();
    this.memory = new Memory();
    this.hardDrive = new HardDrive();
  }

  startComputer() {
    console.log('Starting computer...');
    this.cpu.start();
    this.memory.load();
    this.hardDrive.read();
    this.cpu.execute();
    console.log('Computer started successfully');
  }
}

// 使用示例
const computer = new ComputerFacade();
computer.startComputer();

// 另一个例子:DOM操作外观
class DOMFacade {
  constructor(elementId) {
    this.element = document.getElementById(elementId);
  }

  setText(text) {
    this.element.textContent = text;
    return this;
  }

  setStyle(styles) {
    Object.assign(this.element.style, styles);
    return this;
  }

  addClass(className) {
    this.element.classList.add(className);
    return this;
  }

  on(event, handler) {
    this.element.addEventListener(event, handler);
    return this;
  }
}
3.5 组合模式 (Composite Pattern)

将对象组合成树形结构以表示'部分-整体'的层次结构。

// 组件接口
class Component {
  constructor(name) {
    this.name = name;
  }

  add(component) {
    throw new Error('This method must be overridden');
  }

  remove(component) {
    throw new Error('This method must be overridden');
  }

  getChild(index) {
    throw new Error('This method must be overridden');
  }

  operation() {
    throw new Error('This method must be overridden');
  }
}

// 叶子节点
class Leaf extends Component {
  constructor(name) {
    super(name);
  }

  operation() {
    console.log(`Leaf ${this.name} operation`);
  }
}

// 复合节点
class Composite extends Component {
  constructor(name) {
    super(name);
    this.children = [];
  }

  add(component) {
    this.children.push(component);
  }

  remove(component) {
    const index = this.children.indexOf(component);
    if (index > -1) {
      this.children.splice(index, 1);
    }
  }

  getChild(index) {
    return this.children[index];
  }

  operation() {
    console.log(`Composite ${this.name} operation`);
    for (const child of this.children) {
      child.operation();
    }
  }
}

// 使用示例:文件系统
const root = new Composite('root');
const home = new Composite('home');
const user = new Composite('user');

const file1 = new Leaf('file1.txt');
const file2 = new Leaf('file2.txt');
const file3 = new Leaf('file3.txt');

root.add(home);
home.add(user);
user.add(file1);
user.add(file2);
root.add(file3);

root.operation();

四、行为型模式

4.1 策略模式 (Strategy Pattern)

定义一系列算法, 封装每个算法, 并使它们可以互相替换。

// 策略接口
class PaymentStrategy {
  pay(amount) {
    throw new Error('This method must be overridden');
  }
}

// 具体策略
class CreditCardStrategy extends PaymentStrategy {
  constructor(cardNumber, cvv) {
    super();
    this.cardNumber = cardNumber;
    this.cvv = cvv;
  }

  pay(amount) {
    console.log(`Paid $${amount} using Credit Card ${this.cardNumber.slice(-4)}`);
    return true;
  }
}

class PayPalStrategy extends PaymentStrategy {
  constructor(email) {
    super();
    this.email = email;
  }

  pay(amount) {
    console.log(`Paid $${amount} using PayPal (${this.email})`);
    return true;
  }
}

class CryptoStrategy extends PaymentStrategy {
  constructor(walletAddress) {
    super();
    this.walletAddress = walletAddress;
  }

  pay(amount) {
    console.log(`Paid $${amount} using Crypto Wallet ${this.walletAddress.slice(0, 8)}...`);
    return true;
  }
}

// 上下文
class ShoppingCart {
  constructor() {
    this.items = [];
    this.paymentStrategy = null;
  }

  addItem(item, price) {
    this.items.push({ item, price });
  }

  calculateTotal() {
    return this.items.reduce((total, item) => total + item.price, 0);
  }

  setPaymentStrategy(strategy) {
    this.paymentStrategy = strategy;
  }

  checkout() {
    const total = this.calculateTotal();
    if (!this.paymentStrategy) {
      console.log('Please select a payment method');
      return false;
    }
    return this.paymentStrategy.pay(total);
  }
}

// 使用示例
const cart = new ShoppingCart();
cart.addItem('Book', 25);
cart.addItem('Headphones', 100);

cart.setPaymentStrategy(new CreditCardStrategy('1234-5678-9012-3456', '123'));
cart.checkout();

cart.setPaymentStrategy(new PayPalStrategy('user@example.com'));
cart.checkout();
4.2 观察者模式 (Observer Pattern / 发布-订阅模式)

定义对象间的一对多依赖关系, 当一个对象状态改变时, 所有依赖它的对象都会得到通知。

// 发布-订阅实现
class EventEmitter {
  constructor() {
    this.events = {};
  }

  on(event, listener) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(listener);
    return () => this.off(event, listener);
  }

  off(event, listener) {
    if (!this.events[event]) return;
    
    const index = this.events[event].indexOf(listener);
    if (index > -1) {
      this.events[event].splice(index, 1);
    }
  }

  emit(event, ...args) {
    if (!this.events[event]) return;
    
    this.events[event].forEach(listener => {
      try {
        listener.apply(this, args);
      } catch (error) {
        console.error(`Error in event listener for ${event}:`, error);
      }
    });
  }

  once(event, listener) {
    const removeListener = this.on(event, (...args) => {
      listener.apply(this, args);
      removeListener();
    });
    return removeListener;
  }
}

// 使用示例
const emitter = new EventEmitter();

// 订阅事件
const unsubscribe = emitter.on('userLoggedIn', (user) => {
  console.log(`Welcome, ${user.name}!`);
});

emitter.on('userLoggedIn', (user) => {
  console.log(`Sending login notification to ${user.email}`);
});

// 发布事件
emitter.emit('userLoggedIn', { 
  name: 'John Doe', 
  email: 'john@example.com' 
});

// 取消订阅
unsubscribe();

// 观察者模式实现
class Subject {
  constructor() {
    this.observers = [];
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  removeObserver(observer) {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }

  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  constructor(name) {
    this.name = name;
  }

  update(data) {
    console.log(`${this.name} received:`, data);
  }
}
4.3 迭代器模式 (Iterator Pattern)

提供一种方法顺序访问聚合对象中的各个元素, 而又不暴露其内部表示。

// 自定义迭代器
class Range {
  constructor(start, end, step = 1) {
    this.start = start;
    this.end = end;
    this.step = step;
  }

  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    const step = this.step;
    
    return {
      next() {
        if (current <= end) {
          const value = current;
          current += step;
          return { value, done: false };
        }
        return { done: true };
      }
    };
  }
}

// 使用示例
for (const num of new Range(1, 5)) {
  console.log(num); // 1, 2, 3, 4, 5
}

// 自定义集合迭代器
class Collection {
  constructor() {
    this.items = [];
  }

  add(item) {
    this.items.push(item);
  }

  [Symbol.iterator]() {
    let index = 0;
    const items = this.items;
    
    return {
      next() {
        if (index < items.length) {
          return { value: items[index++], done: false };
        }
        return { done: true };
      },
      
      return() {
        console.log('Iteration stopped prematurely');
        return { done: true };
      }
    };
  }

  // 生成器实现
  *filter(predicate) {
    for (const item of this.items) {
      if (predicate(item)) {
        yield item;
      }
    }
  }

  *map(transform) {
    for (const item of this.items) {
      yield transform(item);
    }
  }
}
4.4 命令模式 (Command Pattern)

将请求封装为对象, 从而允许参数化客户、队列请求、记录日志以及支持可撤销操作。

// 命令接口
class Command {
  execute() {
    throw new Error('This method must be overridden');
  }

  undo() {
    throw new Error('This method must be overridden');
  }
}

// 具体命令
class LightOnCommand extends Command {
  constructor(light) {
    super();
    this.light = light;
  }

  execute() {
    this.light.turnOn();
  }

  undo() {
    this.light.turnOff();
  }
}

class LightOffCommand extends Command {
  constructor(light) {
    super();
    this.light = light;
  }

  execute() {
    this.light.turnOff();
  }

  undo() {
    this.light.turnOn();
  }
}

// 接收者
class Light {
  constructor(location) {
    this.location = location;
    this.isOn = false;
  }

  turnOn() {
    this.isOn = true;
    console.log(`${this.location} light is ON`);
  }

  turnOff() {
    this.isOn = false;
    console.log(`${this.location} light is OFF`);
  }
}

// 调用者
class RemoteControl {
  constructor() {
    this.commands = [];
    this.history = [];
  }

  setCommand(command) {
    this.commands.push(command);
  }

  executeCommands() {
    this.commands.forEach(command => {
      command.execute();
      this.history.push(command);
    });
    this.commands = [];
  }

  undoLast() {
    if (this.history.length > 0) {
      const lastCommand = this.history.pop();
      lastCommand.undo();
    }
  }
}

// 使用示例
const livingRoomLight = new Light('Living Room');
const kitchenLight = new Light('Kitchen');

const remote = new RemoteControl();

remote.setCommand(new LightOnCommand(livingRoomLight));
remote.setCommand(new LightOnCommand(kitchenLight));
remote.executeCommands();

remote.undoLast();

// 宏命令
class MacroCommand extends Command {
  constructor(commands) {
    super();
    this.commands = commands;
  }

  execute() {
    this.commands.forEach(command => command.execute());
  }

  undo() {
    // 逆序执行撤销
    this.commands.reverse().forEach(command => command.undo());
  }
}
4.5 状态模式 (State Pattern)

允许对象在其内部状态改变时改变其行为, 看起来像是修改了类。

// 状态接口
class TrafficLightState {
  constructor(context) {
    this.context = context;
  }

  change() {
    throw new Error('This method must be overridden');
  }
}

// 具体状态
class RedLightState extends TrafficLightState {
  change() {
    console.log('Red light - STOP');
    this.context.setState(new GreenLightState(this.context));
  }
}

class GreenLightState extends TrafficLightState {
  change() {
    console.log('Green light - GO');
    this.context.setState(new YellowLightState(this.context));
  }
}

class YellowLightState extends TrafficLightState {
  change() {
    console.log('Yellow light - CAUTION');
    this.context.setState(new RedLightState(this.context));
  }
}

// 上下文
class TrafficLight {
  constructor() {
    this.state = new RedLightState(this);
  }

  setState(state) {
    this.state = state;
  }

  change() {
    this.state.change();
  }
}

// 使用示例
const trafficLight = new TrafficLight();

trafficLight.change(); // Red light - STOP
trafficLight.change(); // Green light - GO
trafficLight.change(); // Yellow light - CAUTION
trafficLight.change(); // Red light - STOP

// 更复杂的例子:文档编辑器状态
class Document {
  constructor() {
    this.state = new DraftState(this);
    this.content = '';
  }

  setState(state) {
    this.state = state;
  }

  write(text) {
    this.state.write(text);
  }

  publish() {
    this.state.publish();
  }
}

class DraftState {
  constructor(document) {
    this.document = document;
  }

  write(text) {
    this.document.content += text;
    console.log(`Draft: Added "${text}"`);
  }

  publish() {
    console.log('Publishing draft...');
    this.document.setState(new PublishedState(this.document));
  }
}

class PublishedState {
  constructor(document) {
    this.document = document;
  }

  write(text) {
    console.log('Cannot write to published document. Create new draft first.');
  }

  publish() {
    console.log('Document is already published.');
  }
}
4.6 职责链模式 (Chain of Responsibility Pattern)

使多个对象都有机会处理请求, 从而避免请求发送者和接收者之间的耦合关系。

// 处理者接口
class Handler {
  constructor() {
    this.nextHandler = null;
  }

  setNext(handler) {
    this.nextHandler = handler;
    return handler;
  }

  handle(request) {
    if (this.nextHandler) {
      return this.nextHandler.handle(request);
    }
    console.log('No handler found for request:', request);
    return null;
  }
}

// 具体处理者
class AuthenticationHandler extends Handler {
  handle(request) {
    if (request.type === 'auth' && request.credentials === 'valid') {
      console.log('Authentication successful');
      return super.handle(request);
    } else if (request.type === 'auth') {
      console.log('Authentication failed');
      return null;
    }
    return super.handle(request);
  }
}

class AuthorizationHandler extends Handler {
  handle(request) {
    if (request.type === 'auth' && request.role === 'admin') {
      console.log('Authorization granted for admin');
      return super.handle(request);
    } else if (request.type === 'auth') {
      console.log('Authorization denied');
      return null;
    }
    return super.handle(request);
  }
}

class LoggingHandler extends Handler {
  handle(request) {
    console.log(`Logging request: ${JSON.stringify(request)}`);
    return super.handle(request);
  }
}

// 使用示例
const authHandler = new AuthenticationHandler();
const authzHandler = new AuthorizationHandler();
const logHandler = new LoggingHandler();

authHandler
  .setNext(authzHandler)
  .setNext(logHandler);

// 处理请求
const request1 = { type: 'auth', credentials: 'valid', role: 'admin' };
authHandler.handle(request1);

const request2 = { type: 'auth', credentials: 'invalid' };
authHandler.handle(request2);

// 实际应用:请求处理管道
class ValidationHandler extends Handler {
  handle(data) {
    if (!data.email || !data.email.includes('@')) {
      console.log('Validation failed: Invalid email');
      return null;
    }
    console.log('Validation passed');
    return super.handle(data);
  }
}

class SanitizationHandler extends Handler {
  handle(data) {
    data.email = data.email.trim().toLowerCase();
    console.log('Data sanitized');
    return super.handle(data);
  }
}

class SaveHandler extends Handler {
  handle(data) {
    console.log(`Saving data: ${data.email}`);
    return { success: true, id: Date.now() };
  }
}

五、其他重要模式

5.1 模块模式 (Module Pattern)
// 使用IIFE实现模块模式
const UserModule = (function() {
  // 私有变量
  let users = [];
  let userCount = 0;

  // 私有方法
  function generateId() {
    return Date.now().toString(36) + Math.random().toString(36).substr(2);
  }

  // 公共接口
  return {
    addUser: function(name, email) {
      const user = {
        id: generateId(),
        name,
        email,
        createdAt: new Date()
      };
      users.push(user);
      userCount++;
      return user.id;
    },

    getUser: function(id) {
      return users.find(user => user.id === id);
    },

    getUsers: function() {
      return [...users]; // 返回副本
    },

    getUserCount: function() {
      return userCount;
    },

    removeUser: function(id) {
      const index = users.findIndex(user => user.id === id);
      if (index > -1) {
        users.splice(index, 1);
        userCount--;
        return true;
      }
      return false;
    }
  };
})();

// ES6模块语法
export class Calculator {
  static add(a, b) {
    return a + b;
  }

  static multiply(a, b) {
    return a * b;
  }
}
5.2 混入模式 (Mixin Pattern)
// 混入函数
function mixin(target, ...sources) {
  Object.assign(target, ...sources);
  return target;
}

// 可复用的混入对象
const CanEat = {
  eat(food) {
    console.log(`${this.name} is eating ${food}`);
    this.energy += 10;
  }
};

const CanSleep = {
  sleep() {
    console.log(`${this.name} is sleeping`);
    this.energy += 20;
  }
};

const CanPlay = {
  play() {
    console.log(`${this.name} is playing`);
    this.energy -= 5;
  }
};

// 使用混入
class Animal {
  constructor(name) {
    this.name = name;
    this.energy = 100;
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name);
    mixin(this, CanEat, CanSleep, CanPlay);
  }

  bark() {
    console.log(`${this.name} is barking!`);
  }
}

// 使用示例
const dog = new Dog('Rex');
dog.eat('bone');
dog.play();
dog.sleep();
dog.bark();

// ES6类混入
const Flyable = BaseClass => class extends BaseClass {
  fly() {
    console.log(`${this.name} is flying!`);
  }
};

class Bird extends Flyable(Animal) {
  constructor(name) {
    super(name);
  }
}

const bird = new Bird('Tweety');
bird.fly();
5.3 中介者模式 (Mediator Pattern)
// 中介者
class ChatRoom {
  constructor() {
    this.users = new Map();
  }

  register(user) {
    this.users.set(user.name, user);
    user.chatRoom = this;
  }

  send(message, from, to) {
    if (to) {
      // 私聊
      const receiver = this.users.get(to);
      if (receiver) {
        receiver.receive(message, from);
      }
    } else {
      // 群聊
      this.users.forEach(user => {
        if (user.name !== from) {
          user.receive(message, from);
        }
      });
    }
  }
}

// 同事类
class User {
  constructor(name) {
    this.name = name;
    this.chatRoom = null;
  }

  send(message, to = null) {
    this.chatRoom.send(message, this.name, to);
  }

  receive(message, from) {
    console.log(`${from} to ${this.name}: ${message}`);
  }
}

// 使用示例
const chatRoom = new ChatRoom();

const alice = new User('Alice');
const bob = new User('Bob');
const charlie = new User('Charlie');

chatRoom.register(alice);
chatRoom.register(bob);
chatRoom.register(charlie);

alice.send('Hello everyone!');
bob.send('Hi Alice!', 'Alice');
charlie.send('Meeting at 3 PM', 'Alice');

六、总结与最佳实践

何时使用设计模式
  1. 单例模式: 全局配置、日志记录器、数据库连接池
  2. 工厂模式: 创建复杂对象、需要根据条件创建不同对象
  3. 观察者模式: 事件处理系统、实时数据更新
  4. 策略模式: 多种算法实现、需要动态切换行为
  5. 装饰器模式: 动态添加功能、AOP编程
JavaScript设计模式特点
  1. 灵活性: JavaScript的动态特性使得模式实现更加灵活
  2. 函数式特性: 可以利用高阶函数、闭包等特性简化模式实现
  3. 原型继承: 充分利用原型链实现继承和共享方法
  4. ES6+特性: 类语法、Proxy、Symbol、装饰器等增强了模式表达能力
最佳实践建议
  1. 不要过度设计: 只在必要时使用设计模式
  2. 保持简洁: JavaScript本身就很灵活,避免过度复杂的模式实现
  3. 结合语言特性: 充分利用JavaScript的函数式特性
  4. 考虑性能: 某些模式可能带来性能开销,在性能敏感场景要谨慎
  5. 团队共识: 确保团队成员理解所使用的设计模式

设计模式是解决特定问题的工具,而不是银弹。在实际开发中,应根据具体需求选择合适的模式,并灵活调整以适应JavaScript的语言特性。理解模式的核心思想比死记硬背实现方式更重要。

Vue 组件解耦实践:用回调函数模式替代枚举类型传递

作者 鹏北海
2025年12月12日 12:41

Vue 组件解耦实践:用回调函数模式替代枚举类型传递

前言

在 Vue 组件开发中,父子组件通信是一个常见场景。当子组件需要触发父组件的某个操作,而父组件又需要根据触发来源执行不同逻辑时,很容易写出耦合度较高的代码。本文通过一个真实的登录模块重构案例,介绍如何使用回调函数模式来解耦组件。

问题场景

业务背景

在登录页面中,验证码登录组件有两个操作入口:

  • 点击"获取验证码"按钮

  • 点击"登录"按钮

两个操作都需要检查用户是否同意服务协议。如果未同意,需要弹出协议确认弹窗。用户确认后,根据触发来源执行不同的后续操作。

原有实现

// codeLogin.enum.ts - 子组件定义枚举
export const CodeLoginEnum = {
  CODE_BTN: 'code-btn',    // 获取验证码按钮
  LOGIN_BTN: 'login-btn'   // 登录按钮
} as const;

// codeLogin.vue - 子组件
const getCode = () => {
  if (!isAgree.value) {
    emit('changeCodeAgreeDisplayType', CodeLoginEnum.CODE_BTN);  // 告诉父组件是哪个按钮
    emit('toggleAgreeDialog', true);
    return;
  }
  // ...
}

// login.vue - 父组件
const handleAgreementConfirm = () => {
  if (codeAgreeDisplayType.value === CodeLoginEnum.LOGIN_BTN) {
    // 登录按钮触发的,需要校验验证码
    if (!verifyKey.value) {
      ElMessage.warning('请先获取验证码');
      return;
    }
  }
  codeLoginInstance.value?.doGetCode();
}

问题分析

  1. 父组件依赖子组件内部细节:父组件需要导入并理解 CodeLoginEnum

  2. 违反开闭原则:子组件新增按钮时,父组件也需要修改

  3. 职责不清:子组件的业务逻辑分散在父子两个组件中

  4. 可测试性差:父组件的逻辑依赖子组件的枚举定义

解决方案:回调函数模式

核心思想

子组件不告诉父组件"我是谁",而是告诉父组件"确认后请通知我"

将"后续要执行的操作"封装为回调函数,保存在子组件内部。父组件只需要在适当时机通知子组件执行即可。

重构后的实现

// codeLogin.vue - 子组件
type PendingCallback = (() => void) | null;
const pendingCallback = ref<PendingCallback>(null);

const getCode = () => {
  if (!isAgree.value) {
    // 保存回调:协议确认后执行获取验证码
    pendingCallback.value = () => {
      executeGetCode();
    };
    emit('toggleAgreeDialog', true);
    return;
  }
  executeGetCode();
}

const codeLogin = () => {
  if (!isAgree.value) {
    // 保存回调:协议确认后执行登录
    pendingCallback.value = () => {
      emit('codeLogin', mobileValue.value, areaCodeValue.value, verifyCodeArg.value);
    };
    emit('toggleAgreeDialog', true);
    return;
  }
  emit('codeLogin', mobileValue.value, areaCodeValue.value, verifyCodeArg.value);
}

// 供父组件调用
const onAgreementConfirmed = () => {
  pendingCallback.value?.();
  pendingCallback.value = null;
}

defineExpose({ onAgreementConfirmed });
// login.vue - 父组件
const handleAgreementConfirm = () => {
  toggleIsAgree(true);
  toggleAgreeDialog(false);

  if (isAccount()) {
    doLoginFn(loginTempData);
  } else {
    // 简单通知子组件执行回调,无需知道具体是什么操作
    codeLoginInstance.value?.onAgreementConfirmed();
  }
}

数据流对比

重构前:

┌─────────┐  发送按钮类型   ┌─────────┐  根据类型判断   ┌─────────┐
│ 子组件  │ ─────────────→ │ 父组件  │ ─────────────→ │ 子组件  │
└─────────┘                └─────────┘                └─────────┘

重构后:

┌─────────┐  保存回调      ┌─────────┐  通知执行       ┌─────────┐
│ 子组件  │ ─────────────→ │ 父组件  │ ─────────────→ │ 子组件  │
└─────────┘  请求显示弹窗   └─────────┘  onConfirmed   └─────────┘
            (不传类型)                   (不传参数)

方案对比

维度 枚举类型传递 回调函数模式
耦合度 高,父组件依赖子组件枚举 低,父组件只调用方法
扩展性 差,新增类型需改两处 好,只改子组件
职责划分 模糊,逻辑分散 清晰,子组件自治
代码量 需要枚举文件 无额外文件
可测试性 差,依赖外部枚举 好,逻辑内聚

适用场景

回调函数模式适用于以下场景:

  1. 异步确认流程:如本文的协议确认、二次确认弹窗等

  2. 多入口单出口:多个触发点,但后续处理由同一个组件负责

  3. 子组件业务自治:子组件的业务逻辑不应该泄露给父组件

注意事项

  1. 回调清理:执行完回调后记得置空,避免重复执行

  2. 错误处理:回调执行可能失败,需要考虑异常情况

  3. 状态同步:确保回调执行时,相关状态(如 isAgree)已更新

总结

组件解耦的核心原则是让每个组件只关心自己的职责。当发现父组件需要了解子组件的内部实现细节时,就是重构的信号。

回调函数模式是一种简单有效的解耦手段,它将"做什么"的决策权留给子组件,父组件只负责"何时做"的协调。这种控制反转的思想,在很多设计模式中都有体现,值得在日常开发中灵活运用。

一次 React 项目 lock 文件冲突修复:从 Hook 报错到 Vite 配置优化

作者 eason_fan
2025年12月12日 11:37

一次 React 项目 lock 文件冲突修复:从 Hook 报错到 Vite 配置优化

在日常开发中,分支合并是高频操作,但稍有不慎就可能引发依赖相关的“连锁反应”。本文记录了一次 rebase main 后因 lock 文件冲突,导致 React Hook 报错的完整排查与解决过程,希望能为遇到类似问题的开发者提供参考。

一、背景:rebase main 引发的“意外”

最近在开发一个基于 React + Vite + Mobx 的项目,为了同步主分支的最新代码,我执行了 git rebase main 操作。过程中遇到了 package-lock.json 冲突,由于当时急于推进开发,我直接手动编辑了冲突文件,保留了双方的依赖配置后提交了代码。

本以为只是简单的文件合并,没想到启动项目后,浏览器控制台直接抛出了一连串报错:

img_v3_02sr_d84a55a3-57fb-4edb-b768-04c950fdd4hu.jpg

报错堆栈指向 mobx-react-lite 中的 useObserver 方法,提示 useRef 无法读取 null 属性。更奇怪的是,这些代码在 rebase 前完全正常,没有任何语法或逻辑修改。

二、问题分析:锁定核心矛盾

1. 排除代码逻辑问题

首先排查业务代码:近期未修改 Hook 调用逻辑,所有 useRefuseState 等 Hooks 均符合“顶层调用”规则,且未在条件、循环或事件处理函数中调用。排除代码本身的问题后,将目光聚焦到依赖和构建配置上。

2. 定位依赖层面问题

根据 React 官方文档提示,Hook 调用异常的三大常见原因:

  1. 违反 Hooks 使用规则(已排除);
  2. React 与渲染器(如 React DOM)版本不匹配;
  3. 项目中存在多个 React 实例。

结合“仅 lock 文件冲突后出现问题”的场景,重点排查后两点:

  • 执行 npm ls react react-dom 查看依赖树,
    • 发现输出中,Terminal#1-14 显示面板同时存在两版 mobx-react-lite :直接依赖 4.1.0 ,通过 mobx-react@9.2.1 间接带入 4.1.1 。这会让它们各自沿着不同的依赖解析路径去找 react ,在多入口/预打包的情况下,很容易把两份 React 打到同一页面。
  • 进一步验证:在打包文件中搜索package.json中的react版本号18.3.1,或者搜索react源码中的ReactCurrentDispatch。可以发现合了代码之后,构建产物两个chunk中都有react。
image.pngimg_v3_02sr_9d69706b-b957-48ec-8144-06036dc021hu.jpg

代码修改前的打包资源

img_v3_02sr_8a12fa19-d116-403a-9e4d-74a9914ce9hu.jpg

img_v3_02sr_9d2d7185-de0b-474a-8b81-b6169247b3hu.jpg

代码修改后的打包资源

3. 追溯问题根源

lock 文件的核心作用是锁定依赖的安装路径和版本。手动合并冲突时,错误保留了不同分支的依赖配置,导致 npm install 时出现依赖嵌套安装:

  • 项目和项目依赖的包都依赖了mobx-react-lite并且版本不同。
  • 打包产物中,两个chunk中各自有一个react
  • 运行时,就产生了两个react实例

React Hooks 的运行依赖单一的调度器实例,当 mobx-react-lite 中的 useObserver 调用嵌套依赖的 React 实例时,会因调度器不匹配导致 Hook 调用失效,进而抛出 useRef 读取 null 的错误。

三、尝试修改:从依赖到配置的逐步排查

1. 重置依赖(首次尝试失败)

首先想到的是修复依赖树,执行以下操作:

# 清除本地依赖和缓存
rm -rf node_modules package-lock.json
npm cache clean --force
# 重新安装依赖
npm install

但重新安装后,npm ls react 仍显示存在嵌套版本。推测是 mobx-react-lite 的依赖声明中未将 React 设为 peerDependency,导致 npm 自动安装兼容版本的嵌套依赖。

2. 强制统一依赖版本(部分缓解)

通过 npm install react@18.2.0 react-dom@18.2.0 --force 强制指定 React 版本,重新安装后嵌套依赖消失。但启动项目后,仍偶尔出现 Hook 报错,排查发现是 Vite 开发环境预构建时未正确识别依赖,导致部分代码仍引用旧版本缓存。

3. 优化 Vite 配置(最终突破)

结合之前对 Vite dedupeoptimizeDeps 的了解,意识到需要从构建层面确保依赖的唯一性和预构建的完整性:

  • resolve.dedupe:强制 Vite 将所有 React 相关依赖解析为根目录版本,杜绝多实例;
  • optimizeDeps.include:强制预构建核心依赖,避免预构建漏检导致的缓存问题。

四、解决问题:最终生效的配置方案

1. 固化 Vite 配置

修改 vite.config.js,添加依赖去重和预构建配置:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  resolve: {
    // 去重 React 相关核心依赖,确保单一实例
    dedupe: ['react', 'react-dom', 'mobx-react-lite'],
  },
  optimizeDeps: {
    // 强制预构建核心依赖,避免漏检
    include: ['react', 'react-dom', 'mobx-react-lite'],
    // 预构建阶段再次去重,双重保障
    dedupe: ['react', 'react-dom'],
  },
})

2. 清理缓存并验证

执行 vite --force 强制清除预构建缓存,重新启动项目后:

  • 浏览器控制台无任何 Hook 相关报错;
  • 执行 npm ls react react-dom 仅显示根目录单一版本;
  • 打印 React 实例对比结果为 true,确认多实例问题彻底解决。

五、总结与反思

这次问题的核心是“lock 文件冲突处理不当”,但背后暴露了对依赖管理和构建工具配置的认知缺口。总结几点关键经验:

  1. lock 文件冲突切勿手动修改:遇到 lock 文件冲突时,优先执行 git checkout -- package-lock.json 回滚,再通过 rm -rf node_modules && npm install 重新安装,避免依赖树混乱;
  2. 依赖声明需规范:第三方库应将 React 等核心依赖设为 peerDependency,而非直接依赖,避免嵌套安装;
  3. Vite 配置的“防护作用” :对于 React、Vue 等核心依赖,建议在 Vite 配置中提前设置 dedupeoptimizeDeps.include,从构建层面规避多实例和预构建问题;
  4. 报错排查要结合官方文档:React 官方明确列出了 Hook 调用异常的三大原因,排查时应先对照文档缩小范围,避免盲目尝试。

此次排查过程虽曲折,但也加深了对依赖管理、Vite 构建原理和 React Hooks 运行机制的理解。希望这篇记录能帮助大家在遇到类似问题时少走弯路~

从原生 JS 到 Vue3 Composition API:手把手教你用现代 Vue 写一个优雅的 Todos 任务清单

作者 不会js
2025年12月12日 11:35

从原生 JS 到 Vue3 Composition API:手把手教你用现代 Vue 写一个优雅的 Todos 任务清单

大家好,今天用一个最经典的 Todos 应用,来带大家彻底搞清楚:

「为什么我们不再手动操作 DOM?Vue 到底替我们做了什么?」

很多初学者看完 Vue 文档后,会觉得「好像很简单啊」,但真正自己写的时候,又会不自觉地回到原来的命令式写法:

document.getElementById('app').innerHTML = xxx

这篇文章将通过一个逐步演进的过程,让你从「机械式 DOM 操作」进化到「数据驱动」的现代 Vue3 开发思维,彻底领悟响应式编程的魅力。

一、原生 JS 写 Todos:痛并痛苦着

先来看看传统写法(很多人还在这么写):

<h2 id="app"></h2>
<input type="text" id="todo-input">

<script>
  const app = document.getElementById('app');
  const todoInput = document.getElementById('todo-input');
  
  todoInput.addEventListener('change', function(event) {
    const todo = event.target.value.trim();
    if (!todo) return;
    app.innerHTML = todo; // 只能显示最后一个!
  })
</script>

这代码能跑,但问题一大堆:

  • 只能显示一条任务(innerHTML 被覆盖)
  • 要实现多条任务、删除、完成状态……需要写几百行 DOM 操作
  • 一旦需求变动,改起来就是灾难

这就是典型的命令式编程:我们的大脑一直在想「我要先找到哪个元素,然后怎么改它」。

而 Vue 的核心思想是:别管 DOM,你只管数据就行。

二、Vue3 + Composition API 完整实现

03998dfb2be956b19c909a672ec27e78.jpg

<!-- App.vue -->
<script setup>
import { ref, computed } from 'vue'

// 1. 响应式数据(重点!)
const title = ref('') // 输入框内容
const todos = ref([
  { id: 1, title: '吃饭', done: false },
  { id: 2, title: '睡觉', done: true }
])

// 2. 计算属性:统计未完成任务数量(带缓存!)
const active = computed(() => {
  return todos.value.filter(todo => !todo.done).length
})

// 3. 添加任务
const addTodo = () => {
  if (!title.value.trim()) return
  
  todos.value.push({
    id: Date.now(), // 推荐用时间戳,比 Math.random() 更可靠
    title: title.value.trim(),
    done: false
  })
  title.value = '' // 清空输入框
}

// 4. 高级技巧:全选/全不选(computed 的 getter + setter)
const allDone = computed({
  get() {
    if (todos.value.length === 0) return false
    return todos.value.every(todo => todo.done)
  },
  set(value) {
    todos.value.forEach(todo => {
      todo.done = value
    })
  }
})
</script>

<template>
  <div class="todos">
    <h2>我的任务清单</h2>
    
    <input 
      type="text" 
      v-model="title" 
      @keydown.enter="addTodo"
      placeholder="今天要做什么?按回车添加"
      class="input"
    />

    <!-- 任务列表 -->
    <ul v-if="todos.length" class="todo-list">
      <li v-for="todo in todos" :key="todo.id" class="todo-item">
        <input type="checkbox" v-model="todo.done">
        <span :class="{ done: todo.done }">{{ todo.title }}</span>
      </li>
    </ul>
    
    <div v-else class="empty">
      🎉 暂无任务,休息一下吧~
    </div>

    <!-- 统计 + 全选 -->
    <div class="footer">
      <label>
        <input type="checkbox" v-model="allDone">
        全选
      </label>
      <span>未完成:{{ active }} / 总数:{{ todos.length }}</span>
    </div>
  </div>
</template>

<style scoped>
.done{
  color: gray;
  text-decoration: line-through;
}
</style>

三、核心知识点深度拆解(建议反复看)

1. ref() 是如何做到响应式的?

const title = ref('')

这句话背后发生了什么?

  • Vue 在内部为 title 创建了一个响应式对象
  • 真正的数据存在 title.value 中
  • 当你读取 title.value 时,Vue 会记录「当前组件依赖了这个数据」
  • 当你修改 title.value 时,Vue 知道「哪些组件需要重新渲染」,自动更新 DOM

这就叫「依赖收集 + 自动更新」,你完全不用管 DOM!

2. 为什么 computed 比普通函数香?

// 普通函数写法(每次都会计算!)
const activeCount = () => todos.value.filter(...).length

// computed 写法(只有依赖变化才重新计算)
const active = computed(() => todos.value.filter(...).length)

性能差异巨大!当你有 1000 条任务时,普通函数会在每次渲染都执行 1000 次过滤,而 computed 可能只执行一次。

3. computed 的 getter + setter 神技(90%的人不知道)

const allDone = computed({
  get() {
    // 如果todos为空,返回false
    if (todos.value.length === 0) return false;
    // 如果所有todo都完成,返回true
    return todos.value.every(todo => todo.done);
  },
  set(value) {
    // 设置所有todo的done状态
    todos.value.forEach(todo => {
      todo.done = value;
    });
  }
})

这才是真正的「双向计算属性」!点击全选框时,v-model 会自动调用 setter,把所有任务的 done 状态同步修改。

4. v-for 一定要写 :key!不然会出大问题

<li v-for="todo in todos" :key="todo.id">

不写 key 的后果:

  • Vue 无法准确判断哪条数据变了,会导致整张列表重绘
  • 输入框焦点丢失、动画错乱、状态错位

推荐 key 使用:

id: Date.now() + Math.random() // 更稳妥
// 或使用 uuid 库

5. v-model 本质是 :value + @input 的语法糖

Vue 的双向绑定(v-model) = 数据 → 视图 的绑定 + 视图 → 数据的绑定

它让「数据」和「表单元素的值」始终保持同步,你改数据,界面自动更新;你改输入框,数据也自动更新。

<input v-model="title">
<!-- 等价于 -->
<input :value="title" @input="title = $event.target.value">

拆解一下:

方向 对应指令 作用
数据 → 视图 :value="msg" 把 msg 的值渲染到 input 上
视图 → 数据 @input="msg = $event.target.value" 用户输入时,把值重新赋值给 msg

而 @keydown.enter 是 Vue 提供的键位修饰符,超级好用:

@keydown.enter="addTodo"
@keydown.ctrl.enter="addTodo"
@click.prevent="submit" <!-- 阻止默认行为 -->

四、常见坑位避雷指南(血泪经验)

场景 错误写法 正确写法 说明
添加任务后输入框不清空 没重置 title.value title.value = '' v-model 是双向绑定,必须手动清空
全选状态不同步 用普通变量控制 用 computed({get,set}) 普通变量无法响应所有任务的变化
key 使用 index :key="index" :key="todo.id" index 会导致状态错乱
id 使用 Math.random() id: Math.random() id: Date.now() 可能重复,尤其快速添加时
computed 忘记 .value return todos.filter(...) return todos.value.filter(...) script setup 中 ref 要加 .value

五、细节知识点

fc962ce0cd306c49bc54248e80437e81.jpg

computed 是如何做到「又快又省」的?

一句话结论:
computed 只有在它的「依赖」真正发生变化时,才会重新计算一次,其他所有时间直接返回缓存结果。

这才是它比普通方法快 10~100 倍的根本原因!

一、最直观的对比实验
<script setup>
import { ref, computed } from 'vue'

const a = ref(1)
const b = ref(10)

// 场景1:普通方法(每次渲染都重新算)
const sum1 = () => {
  console.log('普通方法被调用了') 
  return a.value + b.value
}

// 场景2:computed(只有依赖变了才算)
const sum2 = computed(() => {
  console.log('computed 被调用了')
  return a.value + b.value
})
</script>

<template>
  <p>普通方法:{{ sum1() }}</p>
  <p>computed:{{ sum2 }}</p>
  <button @click="a++">a + 1</button>
  <button @click="b++">b + 1</button>
</template>

你会看到:

操作 普通方法打印几次 computed 打印几次
页面首次渲染 1 次 1 次
点击 a++ 再次打印 再次打印
点击 b++ 再次打印 再次打印
页面任意地方触发渲染(比如父组件更新) 又打印! 不打印!(直接用缓存)

这就是「缓存」带来的性能飞跃!

Vue 内部到底是怎么实现这个缓存的?(底层逻辑)

Vue 用了一个经典的「脏检查 + 依赖收集」机制(Vue3 用 Proxy 更优雅,但原理一致):

步骤 发生了什么
1. 创建 computed Vue 创建一个「计算属性对象」,里面有个 value(缓存值)和 dirty(是否脏)标志」
2. 第一次读取 computed 执行计算函数 → 同时收集所有用到的响应式数据(a、b、todos.length 等)作为依赖
3. 把依赖和这个 computed 关联起来 a.effect.deps.push(computed)
4. 依赖变化时 Vue 把这个 computed 的 dirty 标志设为 true(表示缓存失效了)
5. 下一次读取时 发现 dirty = true → 重新执行计算函数 → 更新缓存 → dirty = false
6. 之后再读取 dirty = false → 直接返回缓存值,不执行函数

图解:

首次读取 computed
     ↓
执行计算函数 → 依赖收集(记录依赖了 a 和 b)
     ↓
把结果缓存起来,dirty = false

a.value = 999(依赖变化)
     ↓
Vue 自动把所有依赖了 a 的 computed 的 dirty 设为 true

下次读取 computed
     ↓
发现 dirty = true → 重新计算 → 更新缓存 → dirty = false
哪些情况会打破缓存?(常见坑)
情况 是否重新计算 说明
依赖的 ref/reactive 变了 正常触发
依赖的普通变量(let num = 1) 不是响应式的!永远只算一次(大坑!)
依赖了 props props 也是响应式的
依赖了 store.state(Pinia/Vuex) store 是响应式的
依赖了 route.params $route 是响应式的(Vue Router 注入)
依赖了 window.innerWidth 不是响应式!要配合 watchEffectScope 手动处理
实战避雷清单
错误写法 正确写法 后果
computed(() => Date.now()) 改成普通方法或用 ref(new Date()) + watch 每一次读取都重新计算,缓存失效
computed(() => Math.random()) 同上 永远不缓存,性能灾难
computed(() => props.list.length) 完全正确 推荐写法
computed(() => JSON.parse(JSON.stringify(todos.value))) 不要这么做,深拷贝太重 浪费性能
六、一句话记住

computed 的高性能秘诀只有 8 个字:
「依赖不变,绝不重新计算」

现在你再也不用担心「用 computed 会不会影响性能」了,反而应该大胆用!
因为它比你手写任何缓存逻辑都要聪明、都要快!

六、总结:从「操作 DOM」到「操作数据」的思维跃迁

传统 JS 思维 Vue 响应式思维
先找元素 → 再改 innerHTML 只改数据 → Vue 自动更新 DOM
手动 addEventListener 用 v-model / @event 声明式绑定
手动计算未完成数量 用 computed 自动计算 + 缓存
全选要遍历 DOM 用 computed setter 一行搞定

当你真正理解了「数据驱动视图」后,你会发现:

写 Vue 代码不再是「怎么操作页面」,而是「数据怎么变化。

这才是现代前端开发的正确姿势!

👊👊👊领导让我从vue转到react,我敲泥*

2025年12月12日 11:26

领导让我从vue转到react,怎么办尼,那就先看关键hooks,useRffect的使用吧🍭🍭🍭

掘金上关于 useEffect 的文章不少,
但真正把它 讲清楚、讲透、讲到你能写出可维护代码 的,其实不多😉😉😉。

今天哥们直接用几个你能马上复制运行的 Demo,
从最基础的依赖数组、清理副作用、到自定义 Hook,
再扩展到 TanStack Query 为什么几乎取代 useEffect

一句话:
看完这篇,你对所有 “useEffect 什么时候写 / 写什么 / 不写会怎样” 都有清晰答案。


① useEffect 的本质:依赖数组才是核心

先来看最经典的例子:

useEffect(() => {
  console.log("Effect 执行,依赖 count =", count);
}, [count]);

只要你理解下面这句话,你就能掌握 useEffect:

依赖变 → 执行 effect → 执行 cleanup清理函数(如果有)

来看看 Demo:

import { useEffect, useState } from "react";

export default function DemoUseEffect() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  useEffect(() => {
    console.log("Effect 执行,依赖 count =", count);
  }, [count]);

  return (
    <div className="p-6">
      <button onClick={() => setCount(count + 1)}>count + 1</button>

      <input value={text} onChange={(e) => setText(e.target.value)} />
    </div>
  );
}

重点:

  • 点按钮 → 触发 effect
  • 输入框 → 不会触发 effect(因为 text 没写进依赖数组)

那如果依赖数组省略?useEffect(()=>{},无依赖项)

不写依赖数组,等于依赖所有 state、props → 每次渲染都会执行。

所以这是 React 社区默认“不要做”的事 ——
性能差,还容易写出无限循环。

那依赖数组写成 [] 呢?

只执行一次,之后再也不会执行。
常用于初始化逻辑。

那为什么我在控制台看到 effect 执行两次?

因为从React18+之后, React.StrictMode(开发环境)会主动触发 “mount → unmount → mount” 两次
帮你提前暴露副作用 bug。

不是你写错,是 React 故意的。

开发两次 → 线上一次

//main.tsx
 <StrictMode>
    <APP>
  </StrictMode>

② 自定义 Hook 其实就是“把 useEffect 封装起来”

你写过下面这种做“localStorage 持久化”的逻辑吗?

function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState(
    () => JSON.parse(localStorage.getItem(key) || "{}") ?? initialValue
  );

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [value]);

  return [value, setValue];
}

页面使用:

const [name, setName] = useLocalStorage("demo-name", "sss");

本质是什么?
就是:

自定义 Hook = 包装 useState / useEffect,让你的页面更干净、逻辑复用。

你返回什么,外面就用什么。
完全等于“把 effect 写内部,外面只管拿结果”。

这也是为什么:
自定义 Hook 一般都带 use 开头,并且内部可能有多个 useEffect。

自定义hooks能不能不用use开头?
任何函数内部如果调用了 useState/useEffect,就必须以 use 开头,不然就是破坏 React 的 Hook 机制。其实我自己试了,不用use开头似乎也能正常运行(😕😕😕)


③ 为什么必须写 cleanup?(比如定时器)

下面这个 Demo 很多人面试必被问:

useEffect(() => {
  const timer = setInterval(() => {
    console.log("Interval,count=", count);
  }, 1000);

  return () => {
    console.log("清理旧定时器");
    clearInterval(timer);
  };
}, [count]);

流程是这样的:

  • count 第一次是 0 → 开启一个定时器
  • 点按钮 count = 1 → 清理旧定时器 → 开一个新的
  • 再点 → 一样清理 → 重建

如果你没有写 cleanup 会怎样?

  • 每次点都会创建一个新的定时器。
  • console 会变成“机关枪模式”。
  • 性能暴涨,浏览器发热,CPU 起飞 🔥
  • React 官方把这叫做 内存泄漏(memory leak)

所以:

任何产生订阅、定时器、事件监听、外部资源的 effect,都必须写 cleanup。

这是 useEffect 的最重要规则之一。


④ 防抖 / 节流输入框:useEffect 的神级用法

防抖逻辑:

function useDebounce(value: string, delay = 800) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debounced;
}

使用:

const debounced = useDebounce(text, 1000);

useEffect(() => {
  if (debounced) {
    console.log("防抖触发 →", debounced);
  }
}, [debounced]);

输入快,不触发;
停止 1s,才触发。

这是 effect 的另一个核心用法:
根据业务去做一些自定义的联动工具hooks


⑤ 重点扩展:为什么 TanStack Query (tanstack.com/query/lates…) 让你越来越少写 useEffect?

React 社区现在有一句话:

“越写越多 useEffect,代码越乱。useEffect在一个页面使用太多,太多的副作用域导致代码逻辑极度混乱,数据层形成干扰”

然后大家发现了更现代的做法:
用 TanStack Query(React Query),几乎不需要手写 useEffect 来请求数据了。

传统写法:

useEffect(() => {
  axios.get("/api/user").then(res => setUser(res.data));
}, []);

有:

  • loading 状态
  • error 状态
  • 缓存策略
  • 重试逻辑
  • refetch 机制

写成地狱。

而 React Query:

const { data, isLoading, error } = useQuery({
  queryKey: ["user"],
  queryFn: fetchUser,
});

🎉 不需要 useEffect:

  • 自动请求
  • 自动缓存
  • 自动失败重试
  • 自动并发控制
  • 自动后台刷新(Stale-While-Revalidate)
  • 自动依赖感知更新

这就是为什么:

React Query 正在让 useEffect 专注于“副作用”,不再用于“业务逻辑”。

这一点对项目复杂度提升巨大。


⑥ useEffect 的黄金法则(你能把这段贴到团队规范里)

1. useEffect 只处理副作用,不要处理业务

比如请求数据 → 用 React Query
比如格式化数据 → 用 useMemo
比如事件 → 封装成自定义 Hook

2. 依赖数组永远要写全(让 ESLint 帮你)

不写 / 漏写依赖 = bug 温床。

3. 如非必要,不要写空依赖数组 []

会导致数据永不更新。

4. cleanup 是必须的(定时器、事件、订阅)

否则内存泄漏 + 性能炸裂。

5. 自定义 Hook = 复用 useEffect 的最佳方式


总结

useEffect 已经不是你的业务逻辑中心,
而是“失控副作用的收容所”。

你应该:

- 把数据请求交给 React Query (非必要)

  • 把状态交给 state 管理库(Zustand / Jotai / Redux Toolkit)
  • 只在 effect 里写副作用(定时器、事件绑定、订阅等)
  • 用自定义 Hook 封装逻辑

写干净的组件,做干净react developers

一次 npm 更新强制2FA导致的发布失败的排查:403、2FA、Recovery Code、Granular Token 的混乱体验

作者 Toomey
2025年12月12日 11:18

最近在更新发布自己的一个 npm 包(branch-commit-compare)时踩了一堆坑。问题本身不复杂,但 npm 新旧验证体系在 UI 和流程上混杂一起,导致排查成本远高于必要水平。这里记录一下整个过程,避免别人重复踩坑。


背景

我本地执行:

npm publish

结果直接报:

npm ERR! 403 Forbidden - PUT https://registry.npmjs.org/xxx
Two-factor authentication or granular access token with bypass 2fa enabled is required to publish packages.

很明确:npm 要求 2FA 或具有 bypass-2FA 权限的 token 才允许发布。问题在于 —— 我当时的 2FA 状态看起来“不完整”。


一、UI 误导:npm 只给我一个「Security Key」选项

进入 npm 账户的 Security 页面 → Enable 2FA,看到的 2FA 方式只有:

Security Key(USB/NFC、生物识别等)

正常情况下,npm 应该同时提供:

  • Authenticator App(TOTP)
  • Security key(FIDO U2F)
  • Recovery codes

但在我这里,点击 Enable 2FA 直接跳到 Security Key,没有 TOTP 选项

这会带来一个明显问题:

npm CLI 的 --otp=xxxxxx 只能输入 TOTP 码,不能输入恢复码、不能输入 security-key 相关内容。

也就是说:只启用 Security Key = 无法在 CLI 完成 publish


二、我以为自己拿到的是「秘钥」,实际上那是 Recovery Codes

e496c18fe4b39b0e2e1d2d4c7958c561fd3a2526f31ce7aea8394413b1826576
061baf4f296ea7e94c416eb9be80ecd47843ae7b957916f7d788d7310cefc053
4c7feaaca38231ca76222bab37eea9ff1dc69ccf90398900025c4dd03226002b
1886537d1acf213f1ea27329bd7ecdc6e6934397a55a1b1d5882698617f738ed
8b6abe5645ffb5e09d6b6aad2955d8ac9c64bf971751845cbf2afd453476473d

UI 只显示一串普通文本,把它描述成“安全密钥”。但实际那是 Recovery Codes(一次性备份码),不是 TOTP Secret,也不是 token。

把换行删掉、合并成一行都没用,它压根不是 TOTP。

这导致我误以为 CLI 能用它 publish → 当然失败。


三、npm profile get 暴露核心真相

执行:

npm profile get

返回:

two-factor auth │ auth-and-writes

含义:

  • 你已经启用了 2FA,但只启用了 写操作保护
  • npm 要求:发布时必须提供 TOTP
  • Recovery Code 和 Security Key 都不能替代 TOTP

结论:你的 2FA 配置不完整,CLI 肯定 403


四、可行的解决路径:创建 Granular Token 并开启 bypass 2FA

既然 npm CLI 强制需要 TOTP,而 UI 又不给你配置 TOTP,那就只能走另一条路:

1. 创建 Granular Access Token

进入 npm → 头像 → Access Tokens → Create New Token

选择:

  • 类型:Granular / Automation Token
  • 权限:选择你要 publish 的包
  • 关键:勾选 “Bypass two-factor authentication”

创建后会出现真正的一次性 token(长字符串),这才是发布用的。

2. 写入本地 ~/.npmrc

echo "//registry.npmjs.org/:_authToken=你的token" >> ~/.npmrc

3. 再次发布

npm publish

→ 一次通过。


五、根因:npm 的验证系统正在剧烈切换

npm 现在要求:

  • 经典 token 全部废弃
  • granular token 成为唯一可用的 token
  • granular token 默认需要 2FA
  • 而 2FA 又分 TOTP / security key
  • 但 UI 渐进式上线,部分用户只能看到 security key
  • CLI 又要求 TOTP

这就造成一个现象:

你启用了“半套”2FA,UI 显示正常,但 CLI 无法完成发布。


六、最终解决方案总结

  1. npm profile get 显示 auth-and-writes → CLI 必须 OTP
  2. UI 只给你 Security Key,没有 TOTP → CLI 永远无法输入正确 OTP
  3. Recovery Code 不是 OTP → 无效
  4. 唯一可行方法 → 创建带 bypass-2FA 的 granular token
  5. 写入本地 .npmrc 后发布即可

七、给后来者的建议

如果你正在启用 npm 的 2FA:

  • 一定要启用 TOTP(Authenticator App)
    而不是只启用 Security Key (但是官方只给了这种方式)
  • Granular Token 是目前唯一不会踩坑的方式(特别是 CI/CD)
  • 不要被恢复码误导,它不是 OTP
  • 25年12月9号强制必须使用2FA或者绕过2FA的token 我今天12月12太多人在操作了,官网巨卡无比

结语

npm 的验证体系正在重构,UI 不统一、流程半旧半新,是这次踩坑的根本原因。如果你遇到类似的 403 + 要求 2FA,又找不到 TOTP,多半和我一样卡在了“只有 Security Key”这个界面。

解决方法就是:绕开 2FA,使用带 bypass 权限的 granular token 发布

【译】从零开始理解 JavaScript Promise:彻底搞懂异步编程

作者 Sherry007
2025年12月12日 10:52

🎯 从零开始理解 JavaScript Promise:彻底搞懂异步编程

🔗 原文链接:Promises From The Ground Up
👨‍💻 原作者:Josh W. Comeau
📅 发布时间:2024年6月3日
🕐 最后更新:2025年3月18日

⚠️ 关于本译文

本文基于 Josh W. Comeau 的原文进行忠实翻译,力求准确传达原作者的技术观点和逻辑结构。

🎨 特色亮点:

  • 保持原文的完整性和技术准确性
  • 采用自然流畅的中文表达,避免翻译腔
  • 添加画外音板块,提供译者的补充解读和实践心得
  • 使用生动比喻帮助理解复杂概念

💡 画外音说明: 文中标注为画外音的部分是译者基于实际开发经验添加的拓展解释,旨在帮助读者更好地理解和应用这些概念,不代表原作者观点。


在学习 JavaScript 的路上,有很多坎儿要过。其中最大、最让人头疼的,就是 Promise(承诺)

要想真正理解 Promise,咱们得对 JavaScript 的工作原理和它的局限性有相当深入的了解。没有这些背景知识,Promise 就像天书一样难懂。

这事儿确实挺让人抓狂的,因为 Promise API 在现代 JavaScript 开发中实在太重要了。它已经成为处理异步代码的标准方式。现代的 Web API 都是建立在 Promise 之上的。没办法绕过去:如果想用 JavaScript 高效工作,真的很有必要搞懂 Promise。

所以在这篇教程里,咱们要学习 Promise,但会从最基础的地方开始。我会分享那些花了我好几年才搞明白的关键知识点。希望读到最后,你能对 Promise 是什么、怎么有效使用它们有更深的理解。✨

💡 画外音:我刚开始学 Promise 的时候,总是记不住 .then().catch() 到底该怎么用,感觉像是硬记 API。后来才明白,一旦理解了 JavaScript 单线程的本质和异步编程的必要性,这些 API 设计就显得非常自然了。所以这篇文章真的是从根源讲起,强烈推荐耐心读完。

适合谁看

这篇文章适合初级到中级的 JavaScript 开发者。你需要懂一些基本的 JavaScript 语法。


🤔 为啥要这么设计?

假设咱们想做一个"新年倒计时",像这样的效果:

钉钉录屏_2025-12-11 195030.gif 如果 JavaScript 和大多数其他编程语言一样,咱们可以这样解决问题:

function newYearsCountdown() {
  print("3");
  sleep(1000);

  print("2");
  sleep(1000);

  print("1");
  sleep(1000);

  print("Happy New Year! 🎉");
}

在这段假想的代码里,程序会在遇到 sleep() 调用时暂停,等指定的时间过去后再继续执行。

可惜的是,JavaScript 里没有 sleep 函数,因为它是一门单线程语言。*

💡 画外音:这里的"线程"(thread)指的是执行代码的长时间运行进程。JavaScript 只有一个线程,所以它一次只能做一件事,不能同时处理多个任务。这是个问题,因为如果咱们唯一的 JavaScript 线程忙着管理倒计时,它就干不了别的事儿了。

*技术上讲,现代 JavaScript 可以通过 Web Workers 访问多线程,但这些额外的线程无法访问 DOM,所以在大多数场景下用不上。

当我刚学这些东西的时候,不太明白为什么这是个问题。如果倒计时是现在唯一发生的事情,那 JS 线程在这段时间被完全占用不是挺正常的吗?

嗯,虽然 JavaScript 没有 sleep 函数,但它确实有一些其他函数会长时间占用主线程。咱们可以用这些方法来体验一下,如果 JavaScript 真有 sleep 函数会是什么样子。

比如说,window.prompt()。这个函数用来从用户那里收集信息,它会暂停代码执行,就像咱们假想的 sleep() 函数一样。

点击下面这个示例中的按钮,然后在提示框打开时试着和页面交互

image.png

💡 提示:这里只放了截图。如果想亲自体验这个效果(强烈推荐!),可以去原文页面试试,点击按钮后你会发现整个页面真的卡住了,完全动不了。

注意到了吗?当提示框打开的时候,整个页面完全没反应!你没法滚动、点击任何链接,也没法选择任何文本!JavaScript 线程正忙着等咱们输入值,好让它能继续运行代码。在等待的过程中,它干不了别的任何事,所以浏览器就把整个 UI 都锁住了。

其他语言有多个线程,所以其中一个被占用一会儿也没啥大不了的。但在 JavaScript 里,咱们就这一个线程,而且它要用来干所有事情:处理事件、管理网络请求、更新 UI 等等。

如果想做一个倒计时,咱们得找个不阻塞线程的方法。

💡 画外音:这就是为什么你有时会看到有人说"不要在主线程做耗时操作"。比如复杂的计算、大数据处理,如果放在主线程,用户就会感觉页面卡死了。这也是为什么后来出现了 Web Workers,专门用来处理这类重活儿。

为什么整个 UI 都冻结了?

在上面 window.prompt() 的例子中,浏览器等待咱们输入值的时候,整个 UI 都变得没反应了。

这有点奇怪……浏览器滚动页面或选择文本又不依赖 JavaScript。那为什么这些操作也做不了呢?

我觉得浏览器这么做是为了防止 bug。比如滚动页面会触发 "scroll" 事件,这些事件可以被 JavaScript 捕获和处理。如果 JS 线程忙着的时候滚动事件发生了,那段代码就永远不会运行,如果开发者假设滚动事件总是会被处理,就可能导致 bug。

这也可能是出于用户体验的考虑;也许浏览器禁用 UI 是为了让用户不能忽略提示框。不管怎样,我估计原生的 sleep 函数也得这么工作才能防止 bug。


📞 回调函数(Callbacks)

咱们工具箱里解决这类问题的主要工具是 setTimeoutsetTimeout 是一个接受两个参数的函数:

  1. 未来某个时刻要做的一块工作
  2. 要等待的时间

来看个例子:

console.log('Start');

setTimeout(
  () => {
    console.log('After one second');
  },
  1000
);

这块工作通过一个函数传进去。这种模式叫做回调(callback)

前面假想的 sleep() 函数就像给公司打电话,然后一直等着接通下一个客服。而 setTimeout() 就像按 1 让他们在客服有空的时候给你回电。你可以挂掉电话,该干嘛干嘛。

setTimeout() 被称为异步函数。这意味着它不会阻塞线程。相比之下,window.prompt()同步的,因为 JavaScript 线程在等待的时候干不了别的。

异步代码的一个大坑是,它意味着咱们的代码不会总是按线性顺序运行。看看下面这个例子:

console.log('1. Before setTimeout');

setTimeout(() => {
  console.log('2. Inside setTimeout');
}, 500);

console.log('3. After setTimeout');

你可能期望这些日志按从上到下的顺序触发:1 > 2 > 3但记住,回调的核心思想就是"留个号,一会儿回你。 JavaScript 线程不会干坐着等,它会继续运行。

想象一下,如果咱们给 JavaScript 线程一本日记,让它记录运行这段代码时做的所有事情。运行完之后,日记会是这样:

  • 00:000:打印 "1. Before setTimeout"
  • 00:001:注册一个定时器
  • 00:002:打印 "3. After setTimeout"
  • 00:501:打印 "2. Inside setTimeout"

setTimeout() 注册了回调,就像在日历上安排一个会议。注册回调只需要极短的时间,一旦完成,它就继续往下走,执行程序的其余部分。

💡 画外音:这个"日记"的比喻特别好,帮我彻底理解了事件循环。很多新手(包括当年的我)觉得 setTimeout(fn, 0) 很神奇——明明延迟是 0,为什么还是异步的?就是因为它会被"注册"到日历上,即使时间到了,也得等当前同步代码都跑完才轮到它。

回调在 JavaScript 里到处都是,不只是用于定时器。比如,咱们这样监听指针事件(pointer events):

钉钉录屏_2025-12-11 201119.gif

💡 画外音:"pointer"(指针)是个统称,涵盖了所有涉及"指向"的 UI 输入方式,包括鼠标、手指在触摸屏上的点击、触控笔等。所以 pointer events 比 mouse events 的概念更广。

window.addEventListener() 注册了一个回调,每当检测到特定事件时就会被调用。在这个例子中,咱们监听鼠标移动。每当用户移动鼠标或在触摸屏上拖动手指,咱们就会运行一块代码作为响应。

就像 setTimeout 一样,JavaScript 线程不会专注于监视和等待这些事件。它告诉浏览器"嘿,用户移动指针的时候告诉我一声"。当事件触发时,JS 线程会回过头来运行咱们的回调。

好吧,咱们已经跑得有点远了。回到最初的问题:如果想做一个 3 秒倒计时,该怎么做?

在过去,最常见的解决方案是设置嵌套的回调,像这样:

console.log("3…");

setTimeout(() => {
  console.log("2…");

  setTimeout(() => {
    console.log("1…");

    setTimeout(() => {
      console.log("Happy New Year!!");
    }, 1000);
  }, 1000);
}, 1000);

这太疯狂了,对吧?咱们的 setTimeout 回调里又创建了新的 setTimeout 回调!

当我在 2000 年代早期开始折腾 JavaScript 的时候,这种模式挺常见的,虽然大家都觉得不太理想。咱们把这种模式叫做回调地狱(Callback Hell)

Promise 就是为了解决回调地狱的一些问题而开发的。

💡 画外音:回调地狱不仅仅是代码难看的问题。真正的痛点是:错误处理变得超级复杂,每层嵌套都要处理错误;代码的可读性和维护性极差,嵌套超过 3 层基本就看不懂了。我曾经维护过一个 7 层嵌套的回调,那酸爽,现在想起来还头疼。

等等,定时器怎么知道什么时候触发?

setTimeout API 接收一个回调函数和一个持续时间。过了指定时间后,回调函数就会被调用。

但怎么做到的?如果 JavaScript 线程没有看着定时器,像老鹰盯小鸡一样盯着它,它怎么知道该调用回调了?

这超出了本教程的范围,但 JavaScript 有个东西叫做事件循环(event loop)。当咱们调用 setTimeout 时,一条小消息会被添加到队列里。每当 JS 线程不在执行代码时,它就在监视事件循环,检查消息。

定时器到期时,事件循环里就会亮起一个提示灯,就像有新留言的答录机。如果 JS 线程当时没在忙,它会立刻跳过去执行传给 setTimeout() 的回调。

这确实意味着定时器不是 100% 精确的。JavaScript 只有一个线程,它可能正忙着干别的事儿,比如处理滚动事件或等待 window.prompt()。如果咱们指定了 1000ms 的定时器,可以确信至少过了 1000 毫秒,但可能会稍微长一点。

你可以在 MDN 上了解更多关于事件循环的内容。


🎁 Promise 登场

前面说过,咱们不能让 JavaScript 傻等着再执行下一行代码,因为那会把线程堵死。得想办法把工作拆成一块块异步执行。

不过嵌套太难看了,能不能换个思路?要是能把这些操作像串珠子一样连起来就好了——先做这个,做完了做那个,再做下一个。

就当好玩儿,咱们假设有根魔法棒,可以随意改变 setTimeout 函数的工作方式。如果咱们这样做会怎样:

console.log('3');

setTimeout(1000)
  .then(() => {
    console.log('2');

    return setTimeout(1000);
  })
  .then(() => {
    console.log('1');

    return setTimeout(1000);
  })
  .then(() => {
    console.log('Happy New Year!!');
  });

不直接把回调传给 setTimeout(那会导致嵌套和回调地狱),而是用一个特殊的 .then() 方法把它们串起来,是不是好多了?

这就是 Promise 的核心思想。Promise 是 JavaScript 在 2015 年一次大更新中加入的特殊结构。

可惜 setTimeout 还是老样子,用的是回调风格。因为 setTimeout 在 Promise 出现之前就已经存在很久了,要是改了它的工作方式,会导致很多老网站挂掉。向后兼容是好事,但也意味着有些东西没法那么优雅。

不过现代的 Web API 都是基于 Promise 构建的。咱们来看个例子。


🔧 使用 Promise

fetch() 函数允许咱们发起网络请求,通常是从服务器获取一些数据。

看看这段代码:

const fetchValue = fetch('/api/get-data');

console.log(fetchValue);
// -> Promise {<pending>}

当咱们调用 fetch() 时,它启动网络请求。这是一个异步操作,所以 JavaScript 线程不会停下来等待。代码继续运行。

fetch() 函数到底返回了啥?肯定不是服务器返回的真实数据,因为咱们才刚发起请求,数据还在路上呢。它返回的其实是一张"欠条"(IOU),就像浏览器给你打的一张白条,上面写着:"嘿,数据我还没拿到,但我保证马上就给你!"

💡 画外音:IOU 是 "I Owe You"(我欠你)的缩写,读音就像说"I Owe You"。它是一种表示欠债的凭据。用这个比喻特别贴切——Promise 就像浏览器给你打的一张欠条:"数据我现在还没拿到,但我欠你的,到时候一定给你"。

具体来说,Promise 就是个 JavaScript 对象。它内部永远只会处于三种状态之一:

  • pending(待定) — 工作正在进行中,还没完成
  • fulfilled(已完成) — 工作已成功完成
  • rejected(已拒绝) — 出了点问题,Promise 无法完成

只要 Promise 还在 pending 状态,就说它是未解决的(unresolved)。一旦工作完成了,它就变成已解决(resolved)。这里要注意:不管最后是成功(fulfilled)还是失败(rejected),都算是"解决了"。

💡 画外音:Promise 的这三种状态一开始可能有点绕。我喜欢这样理解:pending 就像快递在路上,fulfilled 就像快递送到了,rejected 就像快递丢了或地址错了。一旦快递状态确定(送到或丢失),就不会再变了。

一般来说,咱们会希望在 Promise 完成后做点什么。这时候就用 .then() 方法:

fetch('/api/get-data')
  .then((response) => {
    console.log(response);
    // Response { type: 'basic', status: 200, ...}
  });

fetch() 返回一个 Promise,咱们用 .then() 挂上一个回调函数。等浏览器收到响应了,这个回调就会被执行,响应对象也会作为参数传进来。

等待 JSON?

如果你用过 Fetch API,可能注意到需要第二步才能真正拿到咱们需要的 JSON 数据:

fetch('/api/get-data')
  .then((response) => {
    return response.json();
})
 .then((json) => {
   console.log(json);
   // { data: { ... } }
 });

response.json() 会返回一个全新的 Promise,等响应数据完全转成 JSON 格式后,这个 Promise 才算完成。

但等等,为啥 response.json() 还是异步的?咱们不是已经拿到响应了吗,数据不应该早就是 JSON 了吗?

还真不一定。Web 的一个核心特性是,服务器可以流式传输数据,一点点分批发送。这在传视频(比如 YouTube)的时候很常见,对于大一点的 JSON 数据也可以这么干。

fetch() 返回的 Promise,在浏览器收到第一个字节数据时就算完成了。而 response.json() 的 Promise,要等到收到最后一个字节才算完成。

实际上,JSON 数据很少分批发送,所以这两个 Promise 大多数时候会同时完成。但 Fetch API 在设计时就考虑到了流式响应的场景,所以才需要这么绕一下。

💡 画外音:新手常犯的一个错误是:拿到 response 后直接用,忘了调用 .json()。记住,fetch() 返回的第一个 Promise 只是给你一个"响应对象",里面的数据还是原始格式,需要再调用 .json() 才能解析成 JavaScript 对象。这也是为什么你经常看到两个 .then() 的原因。


🛠️ 创建自己的 Promise

用 Fetch API 的时候,Promise 是 fetch() 函数在背后帮咱们创建的。但要是咱们用的 API 不支持 Promise 呢?

比如 setTimeout,它是在 Promise 出现之前就有了。要想用定时器又不掉进回调地狱,就得自己动手包装一个 Promise。

语法是这样的:

const demoPromise = new Promise((resolve) => {
  // 做一些异步工作,然后
  // 调用 `resolve()` 来完成 Promise
});

demoPromise.then(() => {
  // 当 Promise 完成时,
  // 这个回调会被调用!
})

Promise 其实是个通用容器,它本身不干活儿。当咱们用 new Promise() 创建 Promise 时,得同时告诉它"你要干啥活儿"——通过传入一个函数来指定具体的异步任务。这个任务可以是任何东西:发网络请求、等个定时器、读个文件,啥都行。

等这个活儿干完了,咱们就调用 resolve(),告诉 Promise:"搞定了,一切顺利!"这样 Promise 就变成已解决状态了。

回到咱们一开始的问题——做个倒计时。在这个场景里,异步任务就是"等 setTimeout 跑完"。

那咱们可以自己动手,写一个基于 Promise 的小工具函数,把 setTimeout 包装一下:

function wait(duration) {
  return new Promise((resolve) => {
    setTimeout(resolve, duration);
  });
}

const timeoutPromise = wait(1000);

timeoutPromise.then(() => {
  console.log('1 second later!')
});

这段代码看起来超级吓人。咱们试着分解一下:

  • 咱们写了个新的工具函数 wait,它接收一个参数 duration(持续时间)。目标是把它当成 sleep 函数用,但是异步的、不阻塞线程的那种。
  • wait 函数里创建并返回了一个新的 Promise。Promise 自己啥也不干,得靠咱们在异步工作完成时调用 resolve
  • Promise 内部,咱们用 setTimeout 启动了一个定时器。把 Promise 给的 resolve 函数和用户传进来的 duration 都给它。
  • 定时器时间到了,就会执行回调。这就形成了连锁反应:setTimeout 执行了 resolveresolve 告诉 Promise "搞定了",然后 .then() 里的回调也跟着被触发。

这段代码要是还让你头疼,别担心😅。这里确实揉了好多高级概念在一起!能理解大概思路就行,细节慢慢消化。

有个点可能会帮你理清楚:上面代码里,咱们把 resolve 函数直接扔给了 setTimeout。其实也可以这样写,创建一个箭头函数来调用 resolve

function wait(duration) {
  return new Promise((resolve) => {
    setTimeout(
      () => resolve(),
      duration
    );
  });
}

JavaScript 里函数是"一等公民",意思是函数可以像字符串、数字那样随便传来传去。这特性挺厉害,但新手可能需要点时间才能习惯。上面这种写法不那么直接,但效果完全一样,哪种看着舒服就用哪种!

💡 画外音:这个 wait 函数是我在实际项目中常用的一个工具。很多人会把它加到工具函数库里。甚至有些库(比如 p-timeout)专门提供这类 Promise 工具。学会包装旧的回调式 API 成 Promise,这个技能超级有用,因为还有很多老代码和库用的是回调。


⛓️ 链式调用 Promise

关于 Promise,有一点很重要要理解:它们只能被解决一次。一旦 Promise 被完成或拒绝,它就永远保持那个状态了。

这意味着 Promise 并不真正适合某些场景。比如事件监听器:

window.addEventListener('mousemove', (event) => {
  console.log(event.clientX);
})

这个回调会在用户每次移动鼠标时触发,可能成百上千次。Promise 干不了这活儿。

那咱们的倒计时怎么办?虽然不能重复用同一个 wait Promise,但可以把多个 Promise 串成一条链:

wait(1000)
  .then(() => {
    console.log('2');
    return wait(1000);
  })
  .then(() => {
    console.log('1');
    return wait(1000);
  })
  .then(() => {
    console.log('Happy New Year!!');
  });

第一个 Promise 完成了,.then() 回调就被执行。这个回调又创建并返回一个新的 Promise,就这样一个接一个地串下去。

💡 画外音:Promise 链是个很强大的模式。关键点在于每个 .then() 都会返回一个新的 Promise,这样就能一直链下去。不过要注意:如果忘记 return,链就断了,后面的 .then() 不会等前面的异步操作完成。这是新手常犯的错误,我也踩过好几次坑。


📦 传递数据

前面的例子里,咱们调用 resolve 时都没传参数,只是用它来标记"活儿干完了"。但有时候,咱们还得把结果数据传出来!

来看个例子,假设有个用回调的数据库库:

function getUser(userId) {
  return new Promise((resolve) => {
    // 在这个例子中,异步工作是
    // 根据 ID 查找用户
    db.get({ id: userId }, (user) => {
      // 现在咱们有了完整的 user 对象,
      // 可以在这里传进去...
      resolve(user);
    });
  });
}

getUser('abc123').then((user) => {
  // ...然后在这里取出来!
  console.log(user);
  // { name: 'Josh', ... }
})

传给 resolve 的参数,会原封不动地传到 .then() 的回调函数里。这样就能把异步操作的结果一路传出去了。


❌ 被拒绝的 Promise

可惜,JavaScript 的世界里,Promise 不是总能兑现。有时候也会黄了。

比如用 Fetch API 发网络请求,不一定能成功啊!可能网络不稳定,也可能服务器挂了。这些情况下,Promise 就会被拒绝(rejected),而不是正常完成。

咱们可以用 .catch() 方法来处理:

fetch('/api/get-data')
  .then((response) => {
    // ...
  })
  .catch((error) => {
    console.error(error);
  });

Promise 成功了,就走 .then() 这条路。失败了,就走 .catch()。可以理解为两条岔路,看 Promise 最后是啥状态。

💡 画外音:错误处理是 Promise 相比回调的一大优势。在回调地狱里,每层嵌套都要单独处理错误。但用 Promise,你可以在链的末尾加一个 .catch(),它能捕获整个链中任何地方的错误。这大大简化了错误处理逻辑。

Fetch 的坑

假设服务器返回了个错误,比如 404 Not Found 或者 500 Internal Server Error。这应该会触发 Promise 被拒绝,对不对?

意外的是,并不会!这种情况下,Promise 还是会正常完成,只不过 Response 对象里会带着错误信息:

Response {
  ok: false,
  status: 404,
  statusText: 'Not Found',
}

这看着有点奇怪,但仔细想想也说得通:咱们的 Promise 确实完成了,也从服务器拿到响应了!虽然不是咱们想要的那种响应,但确实有响应。

至少按"许三个愿望的精灵"的逻辑,这没毛病。

自己写 Promise 的时候,可以用第二个参数 reject 来标记拒绝:

new Promise((resolve, reject) => {
  someAsynchronousWork((result, error) => {
    if (error) {
      reject(error);
      return;
    }

    resolve(result);
  });
});

Promise 里面要是出了问题,就调用 reject() 来标记失败。传给 reject() 的参数(通常是个错误对象)会被传到 .catch() 回调里。

令人困惑的名字

前面说过,Promise 有三种状态:pending(进行中)、fulfilled(成功)和 rejected(失败)。那为啥参数不叫 "fulfill" 和 "reject",而是叫 "resolve" 和 "reject" 呢?

原因是这样的:resolve() 大多数情况下确实会让 Promise 变成 fulfilled 状态。但有个特殊情况——如果你在 resolve() 里传入的不是普通值,而是另一个 Promise,事情就不一样了。

举个例子:

const promise1 = new Promise((resolve) => {
  const promise2 = fetch('/api/data');
  resolve(promise2); // 传入了另一个 Promise!
});

这时候,promise1 会"挂靠"到 promise2 上,等 promise2 的结果。虽然 promise1 技术上还在 pending 状态,但它已经算是 "resolved"(已交接)了——因为它已经把自己的命运交给 promise2 了,JavaScript 线程也已经去忙 promise2 的事儿了。

所以 "resolved" 不等于 "fulfilled",它更像是"已经有着落了"(不管最后成功还是失败)。

这个细节我也是发完博文后读者告诉我才知道的(感谢大家!)。老实说,99% 的开发者都不会碰到这种情况,不用纠结。如果你真的想深入研究,可以看这个文档:States and Fates

💡 画外音:说实话,这个"resolved vs fulfilled"的区别在日常开发中真的不太需要纠结,记住 resolve() 表示成功、reject() 表示失败就够了。不过如果你在面试或者读规范文档的时候碰到,至少知道是咋回事。


🎭 Async / Await

现代 JavaScript 最牛的一点就是 async / await 语法。用了这个语法,咱们终于能写出接近理想状态的倒计时代码了:

async function countdown() {
  console.log("5…");
  await wait(1000);

  console.log("4…");
  await wait(1000);

  console.log("3…");
  await wait(1000);

  console.log("2…");
  await wait(1000);

  console.log("1…");
  await wait(1000);

  console.log("Happy New Year!");
}

等等,这不是不可能吗! 函数执行到一半不能暂停啊,那会把线程堵死的!

其实这个新语法底层还是 Promise。咱们来扒开看看它是怎么运作的:

async function addNums(a, b) {
  return a + b;
}

const result = addNums(1, 1);

console.log(result);
// -> Promise {<fulfilled>: 2}

本以为返回值应该是数字 2,结果却是个 Promise,里面包着数字 2。只要给函数加上 async 关键字,它就一定会返回 Promise,哪怕函数里压根没干异步的活儿。

上面的代码其实是这样的语法糖:

function addNums(a, b) {
  return new Promise((resolve) => {
    resolve(a + b);
  });
}

同样的,await 关键字也是 .then() 回调的语法糖:

// 这段代码...
async function pingEndpoint(endpoint) {
  const response = await fetch(endpoint);
  return response.status;
}

// ...等价于这个:
function pingEndpoint(endpoint) {
  return fetch(endpoint)
    .then((response) => {
      return response.status;
    });
}

Promise 给 JavaScript 打好了底层基础,让咱们能写出看着像同步、实际是异步的代码。

这设计,真的绝了。

💡 画外音async/await 是我最喜欢的 JavaScript 特性之一。它让异步代码读起来就像同步代码一样自然。不过有个常见误区:很多人以为 async/await 是一种新的异步机制,其实它只是 Promise 的语法糖。理解这一点很重要,因为有时候你还是需要直接用 Promise(比如 Promise.all() 并发请求)。另外,别忘了用 try/catch 包裹 await,不然错误可能会悄悄溜走!


🚀 更多内容即将推出!

过去几年,我全职都在做教育内容,制作和分享像这篇博文这样的资源。我已经做了 CSS 课程和 React 课程。

学生们问得最多的就是:"能不能做个原生 JavaScript 的课程?"这事儿我一直在想。接下来几个月,应该会发更多关于原生 JavaScript 的文章。

想在我发布新内容时第一时间知道的话,最好是订阅我的邮件列表。有新博文或者课程更新,我都会发邮件通知你。❤️


📝 译者总结

💡 核心要点回顾

概念 关键理解
单线程本质 JavaScript 只有一个线程,不能像其他语言那样"停下来等"
回调地狱 嵌套回调难以维护,错误处理复杂,这是 Promise 要解决的核心问题
Promise 状态 pending(进行中)→ fulfilled(成功)或 rejected(失败)
链式调用 .then() 返回新 Promise,可以一直链下去,避免嵌套
async/await Promise 的语法糖,让异步代码看起来像同步,但本质还是 Promise

🎯 实用建议

  1. 包装旧 API:很多老 API 还在用回调,学会用 Promise 包装它们(像文中的 wait 函数)
  2. 错误处理:养成在 Promise 链末尾加 .catch() 的习惯,或者用 try/catch 包裹 await
  3. 别忘了 return:Promise 链中如果需要传递数据或继续链式调用,一定要 return
  4. 并发请求:需要同时发起多个请求时,用 Promise.all() 而不是多个 await
  5. Fetch 陷阱:记住 HTTP 错误状态码(404、500等)不会触发 .catch(),要检查 response.ok

Electron学习

作者 落幕__
2025年12月12日 10:50

Electron学习

一. Electron安装

自己从零开始,方便理解Electron的内部机制

// 项目初始化
npm init -y
//  下载Electron  不用 Electron Forge
npm  install  --save-dev  electron@latest

二. 创建一个窗口

添加 index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>hello world</h1>
</body>
</html>
添加 main.js
// 导入 Electron 的核心模块
// app: 控制应用程序的生命周期事件
// BrowserWindow: 创建和控制浏览器窗口
const { app, BrowserWindow } = require('electron')
// 当 Electron 完成初始化并准备创建浏览器窗口时触发此事件
app.on('ready', () => {
     // 创建一个新的浏览器窗口实例
  const mainWindow = new BrowserWindow({
    width: 800, // 设置窗口宽度为 800 像素
    height: 600, // 设置窗口高度为 600 像素
    // 配置网页功能选项
    webPreferences: {
      nodeIntegration: true  // 允许在渲染进程中使用 Node.js 功能
    }
  })
 // 加载应用程序的主页面
  // 这里加载当前目录下的 index.html 文件
  mainWindow.loadFile('index.html')
})
在package.json文件下scripts 添加运行命令

{
  "name": "lectron",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "dependencies": {
    "electron": "^39.2.6"
  },
  "devDependencies": {},
  "scripts": {
    "start": "electron ."
  },
 
  "keywords": [],
  "author": "",
  "license": "ISC"
}

注意: package.json文件下 需要修改为 "main": "main.js", 要不然会报错。

在这里插入图片描述

运行命令

npm run  start

在这里插入图片描述

三. 打开调试者工具

mainWindow.webContents.openDevTools() // 打开开发者工具

const { app, BrowserWindow } = require('electron')
// 当 Electron 完成初始化并准备创建浏览器窗口时触发此事件
app.on('ready', () => {
  const mainWindow = new BrowserWindow({
    width: 800, // 设置窗口宽度为 800 像素
    height: 600, // 设置窗口高度为 600 像素
    // 配置网页功能选项
    webPreferences: {
      nodeIntegration: true  // 允许在渲染进程中使用 Node.js 功能
    }
  })

  mainWindow.webContents.openDevTools() // 打开开发者工具
  // 这里加载当前目录下的 index.html 文件
  mainWindow.loadFile('index.html')
})

四.热更新mian.js文件

因为每次修改mian.js文件 。需要关闭程序,重新运行命令。mian修改过的内容才生效。所以我们需要热更。不需要关闭程序,重新运行命令

nodemon文件监视工具,检测文件变化并自动重启进程

安装 nodemon

npm install  nodemon --save-dev
// --watch main.js
监视 main.js文件的变化(Electron 主进程入口文件)
// --exec "electron ."
检测到变化时执行的命令:  electron .启动当前目录的 Electron 应用
"scripts": {
    "start": "electron .",
    "start:wacth": "nodemon --watch main.js --exec \"electron .\""
  },

五. Electron进程和线程

主进程Main Process

在 Electron应用都有一个单一的主进程,作为应用程序入口点。主进程在Node.js 运行。它具备所以Node.jsAPI的能力。

  • 窗口管理
  • 应用程序生命周期
  • 原生API
渲染进程Renderer Process

每个Electron应用会为每个打开的 BrowserWindow 生成一个单独的渲染进程。

渲染器无权直接访问require或其他Node.js API

进程间通信(IPC)

IPC通道名称

  • ipcMain
  • ipcRenderer

预加载脚本

为了将electron的不同类型的进程接在一起,我们需要使用被称为预加载preload的特殊脚本。

1.单向通信 - 从渲染器进程到主进程

使用ipcRenderer.send API发送消息,然后使用ipcMain.onAPI接收。

实现通过输入框修改应用标题

在这里插入图片描述

一. 添加输入框

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>hello world</h1>
    Title: <input id="title" />
    <button id="btn" type="button">Set</button>

    <h1 id="info"></h1>
  </body>
  <script src="./renderer.js"></script>
</html>

二. 添加renderer.js 编写 点击按钮事件和渲染Node版本号逻辑

// 获取页面上 ID 为 'info' 的元素
const info = document.getElementById('info')
console.log(window.versions,'window.versions')
// 设置该元素的内容为版本信息
info.innerHTML = `Chrome (v${window.versions.chrome}), Node.js (v${window.versions.node})`
// 获取页面上 ID 为 'btn' 的按钮元素
const btn = document.getElementById('btn')
// 获取页面上 ID 为 'title' 的输入框元素
const titleInput = document.getElementById('title')
// 给按钮添加点击事件监听器
btn.addEventListener('click', () => {
     // 从输入框中获取用户输入的值
    const title = titleInput.value
     // 调用预加载脚本暴露的 window.electron.setTitle 方法
    // 该方法会通过 IPC 通知主进程设置窗口标题
    window.electron.setTitle(title)
    
   
  })

三. 预加载脚本(preload.js)

它使用contextBridge来安全地暴露一些Node.js和Electron的功能给渲染进程(即网页)。

代码中使用了三个exposeInMainWorld调用,分别暴露了'versions'、'electron'和'require'三个全局变量。

// contextBridge: 安全地向渲染进程暴露 API 的桥梁
// ipcRenderer: 渲染进程与主进程通信的模块
const { contextBridge, ipcRenderer } = require("electron");
/**
 * 通过 contextBridge 向渲染进程暴露 Node.js 和 Chromium 版本信息
 * 安全地将只读数据暴露给 window.versions 对象
 */
contextBridge.exposeInMainWorld('versions', {
    node: process.versions.node, // Node.js 运行时版本号
    chrome: process.versions.chrome // Chromium 引擎版本号
  })
/**
 * 向渲染进程暴露自定义的 electron API
 * 创建一个安全的 window.electron 对象,包含 setTitle 方法
 * @param {string} title - 要设置的窗口标题
 */
contextBridge.exposeInMainWorld("electron", {
  setTitle: (title) => ipcRenderer.send("set-title", title),
});
/**
 * 危险操作!暴露 Node.js 的 require 函数到渲染进程
 * ⚠️ 严重安全警告:这会导致 XSS 漏洞和系统级攻击风险
 * 绝对不要在真实项目中这样做!
 */
contextBridge.exposeInMainWorld("require", require);

四.修改main.js

preload: path.join(__dirname, 'preload.js') // 预加载脚本路径 ipcMain.on('set-title',handlsSetTitle) // 注册 IPC 监听器:监听渲染进程发送的 'set-title' 事件 handlsSetTitle 修改应用标题

const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')

const createWindow = () => {
     // 创建一个新的浏览器窗口实例
   const mainWindow = new BrowserWindow({
    width: 800, // 设置窗口宽度为 800 像素
    height: 700, // 设置窗口高度为 600 像素
    // 配置网页功能选项
    webPreferences: {
      nodeIntegration: true , // 允许在渲染进程中使用 Node.js 功能
      preload:path.join(__dirname, 'preload.js') // 预加载脚本
    }
  })
  mainWindow.webContents.openDevTools() // 打开开发者工具
  //  加载应用程序的主页面  这里加载当前目录下的 index.html 文件
  mainWindow.loadFile('index.html')
}
/**
 * 处理设置窗口标题的 IPC 消息
 * @param {Electron.IpcMainEvent} event - IPC 事件对象
 * @param {string} title - 要设置的新窗口标题
 */

const handlsSetTitle = (event, title) => {
    console.log('title', event, title)
     // 获取发送消息的 WebContents 对象
    const webContents = event.sender
     // 通过 WebContents 找到对应的 BrowserWindow 实例
    const win = BrowserWindow.fromWebContents(webContents)
    // 设置窗口标题
    win.setTitle(title)
    
}

// 当 Electron 完成初始化并准备创建浏览器窗口时触发此事件
app.on('ready', () => {
    // 注册 IPC 监听器:监听渲染进程发送的 'set-title' 事件
    ipcMain.on('set-title',handlsSetTitle)
    // 创建主窗口
    createWindow()
})
2.双向通信 - 从渲染器进程到主进程,再从主进程到渲染器进程
  • 使用ipcRender.invoke 进行发送
  • 使用ipcMain.handle 来进行响应
  • 它的第二个函数被用一个回调。然后返回值将作为Promise返回到最初的invoker调用
实现本地保存输入框内容,并且页面显示文件大小

在这里插入图片描述

一. 添加输入框

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>hello world</h1>
    <!-- 标题输入框 -->
    Title: <input id="title" />
    <button id="btn" type="button">Set</button>
    <h1 id="info"></h1>
    <!-- 文本输入 -->
    Content: <input id="content"/>
    <button id="btn2" type="button">Write</button>
    <h1 id="counter"></h1>
  </body>
  <script src="./renderer.js"></script>
</html>

二.在preload.js,添加点击事件

const btn2 = document.getElementById('btn2')
const contentInput = document.getElementById('content')
const counter = document.getElementById('counter')
btn2.addEventListener('click', async () => {
    const content = contentInput.value
    const len = await window.electron.writeFile(content)
    console.log(len)
    counter.innerHTML = `文件大小: ${len}`
    const c = await fs.promises.readFile('test.txt', { encoding: 'utf-8' })
    counter.innerHTML += `文件内容: ${c}`
  })

三.在预设脚本preload.js ,使用ipcRender.invoke 进行发送

contextBridge.exposeInMainWorld("electron", {
  setTitle: (title) => ipcRenderer.send("set-title", title),
  // 调用主进程的 writeFile 方法,并将内容作为参数传递
  writeFile: (content) => ipcRenderer.invoke('write-file', content), 
});

四.修改mian.js

pcMain.handle('write-file', handleWriteFile) // 注册 IPC 处理程序:处理渲染进程发送的 'write-file' 请求

handleWriteFile //

// 导入 Electron 的核心模块
// app: 控制应用程序的生命周期事件
// BrowserWindow: 创建和控制浏览器窗口
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const fs = require('fs')
const createWindow = () => {
     // 创建一个新的浏览器窗口实例
   const mainWindow = new BrowserWindow({
    width: 800, // 设置窗口宽度为 800 像素
    height: 700, // 设置窗口高度为 600 像素
    // 配置网页功能选项
    webPreferences: {
      nodeIntegration: true , // 允许在渲染进程中使用 Node.js 功能
      preload:path.join(__dirname, 'preload.js') // 预加载脚本
    }
  })
  mainWindow.webContents.openDevTools() // 打开开发者工具
  //  加载应用程序的主页面  这里加载当前目录下的 index.html 文件
  mainWindow.loadFile('index.html')
}
/**
 * 处理设置窗口标题的 IPC 消息
 * @param {Electron.IpcMainEvent} event - IPC 事件对象
 * @param {string} title - 要设置的新窗口标题
 */

const handlsSetTitle = (event, title) => {
    console.log('title', event, title)
     // 获取发送消息的 WebContents 对象
    const webContents = event.sender
     // 通过 WebContents 找到对应的 BrowserWindow 实例
    const win = BrowserWindow.fromWebContents(webContents)
    // 设置窗口标题
    win.setTitle(title)
    
}

/**
 * 
 * @param {*} event 
 * @param {*} content 
 * @returns 
 */
async function handleWriteFile(event, content) {
    console.log('the content', content)
    await fs.promises.writeFile('test.txt', content)
    const stats = await fs.promises.stat('test.txt')
    return stats.size
  }
// 当 Electron 完成初始化并准备创建浏览器窗口时触发此事件
app.on('ready', () => {
    // 注册 IPC 监听器:监听渲染进程发送的 'set-title' 事件
    ipcMain.on('set-title',handlsSetTitle)
// 注册 IPC 处理程序:处理渲染进程发送的 'write-file' 请求
    ipcMain.handle('write-file', handleWriteFile) 
    // 创建主窗口
    createWindow()
})
3.单向通信- 从主进程到渲染进程
  • 使用win.webContets.send进行发送
  • 使用ipcRenderer.on接收

进入页面,页面开始每3秒加3

在这里插入图片描述

1.修改mian.js

const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const fs = require('fs')
const createWindow = () => {
     // 创建一个新的浏览器窗口实例
   const mainWindow = new BrowserWindow({
    width: 800, // 设置窗口宽度为 800 像素
    height: 700, // 设置窗口高度为 600 像素
    // 配置网页功能选项
    webPreferences: {
      nodeIntegration: true , // 允许在渲染进程中使用 Node.js 功能
      preload:path.join(__dirname, 'preload.js') // 预加载脚本
    }
  })
  mainWindow.webContents.openDevTools() // 打开开发者工具
  //  加载应用程序的主页面  这里加载当前目录下的 index.html 文件
  mainWindow.loadFile('index.html')

  return mainWindow
}
/**
 * 处理设置窗口标题的 IPC 消息
 * @param {Electron.IpcMainEvent} event - IPC 事件对象
 * @param {string} title - 要设置的新窗口标题
 */

const handlsSetTitle = (event, title) => {
    console.log('title', event, title)
     // 获取发送消息的 WebContents 对象
    const webContents = event.sender
     // 通过 WebContents 找到对应的 BrowserWindow 实例
    const win = BrowserWindow.fromWebContents(webContents)
    // 设置窗口标题
    win.setTitle(title)
    
}

/**
 * 
 * @param {*} event 
 * @param {*} content 
 * @returns 
 */
async function handleWriteFile(event, content) {
    console.log('the content', content)
    await fs.promises.writeFile('test.txt', content)
    const stats = await fs.promises.stat('test.txt')
    return stats.size
  }
// 当 Electron 完成初始化并准备创建浏览器窗口时触发此事件
app.on('ready', () => {
    // 注册 IPC 监听器:监听渲染进程发送的 'set-title' 事件
    ipcMain.on('set-title',handlsSetTitle)
// 注册 IPC 处理程序:处理渲染进程发送的 'write-file' 请求
    ipcMain.handle('write-file', handleWriteFile) 
    // 创建主窗口
    createWindow()



    let counter = 1
    const win = createWindow()
    // remote.enable(win.webContents)
    win.webContents.send('update-counter', counter)
    setInterval(() => {
      counter += 3
      win.webContents.send('update-counter', counter)
    }, 3000)
})
  1. index.html 添加显示数字元素

     <!-- 显示数字元素 -->
    
        <h1 id="time"></h1>
    
  2. renderer.js

    const time = document.getElementById('time')
    // 
      window.electron.onUpdateCounter((value) => {
        time.innerText = value.toString()
      })
    
  3. preload.js添加 onUpdateCounter

     */
    contextBridge.exposeInMainWorld("electron", {
      setTitle: (title) => ipcRenderer.send("set-title", title),
      // 调用主进程的 writeFile 方法,并将内容作为参数传递
      writeFile: (content) => ipcRenderer.invoke('write-file', content), 
      // 调用主进程的 readFile 方法,并将回调函数作为参数传递
      onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)),
    });
    

六. Electron Forge

Electron Forge 是一个用于打包和分发 Electron 应用程序的工具。 它将 Electron 的构建工具生态系统统一到一个可扩展的界面中,这样每个人都可以直接上手制作 Electron 应用。

它的亮点:

  • 📦 应用打包和代码签名
  • 🚚 Windows、macOS 和 Linux 上的可定制安装程序(DMG、deb、MSI、PKG、AppX 等)
  • ☁️ 云提供商(GitHub、S3、Bitbucket 等)的自动化发布流程
  • ⚡️ 易于使用的 Webpack 和 TypeScript 样板模板
  • ⚙️ 原生 Node.js 模块支持
  • 🔌 可扩展的 JavaScript 插件 API
初始化一个新的 Forge 项目
# my-app-electron 项目名称
#  --template=vite-typescript 模本用vite+typescript
npm init create-electron-app@latest my-app-electron1 -- --template=vite-typescript
# 进入项目
cd my-app-electron
#运行项目
npm start
结合vue3
# 安装vue
npm install vue
#安装vite识别vue插件
npm install --save-dev @vitejs/plugin-vue

七. 应用打包

macOS打包

需要安装 需通过Electron Forge@electron-forge/maker-dmg插件实现

npm install @electron-forge/maker-dmg

操作系统:仅能在macOS上打包DMG(因DMG是macOS专属格式)

forge.config.ts打包文件配置

import type { ForgeConfig } from '@electron-forge/shared-types';
// 每一种打包类型都设计一个单独的npm
import { MakerSquirrel } from '@electron-forge/maker-squirrel';
import { MakerZIP } from '@electron-forge/maker-zip';
import { MakerDeb } from '@electron-forge/maker-deb';
import { MakerRpm } from '@electron-forge/maker-rpm';
// macOS
import { MakerDMG } from '@electron-forge/maker-dmg';
import { VitePlugin } from '@electron-forge/plugin-vite';
import { FusesPlugin } from '@electron-forge/plugin-fuses';
import { FuseV1Options, FuseVersion } from '@electron/fuses';

const config: ForgeConfig = {
    // 基础打包配置
  packagerConfig: {
    asar: true,// 启用 ASAR 归档格式(将应用程序打包为单个文件)
  },
  rebuildConfig: {},
    // 制作包的工具
  makers: [
      // windows 安装包
    new MakerSquirrel({}),
         new MakerDMG({
      name: 'YourAppName', // DMG文件名(默认与packagerConfig.name一致)
      background: 'assets/dmg-background.png', // DMG窗口背景图(推荐1440x900像素)
      icon: 'assets/icon.icns', // DMG窗口中的应用图标(与packagerConfig.icon一致)
      iconSize: 80, // 图标大小(像素)
       format:'ULFO', // 使用ULFO格式,兼容性更好
      }
    })
    new MakerZIP({}, ['darwin']), // ZIP 压缩包生成器(排除 macOS 平台)
    new MakerRpm({}),// RPM 包生成器 (RedHat/CentOS/Fedora)
    new MakerDeb({}),// DEB 包生成器 (Debian/Ubuntu)
  ],
  plugins: [
    new VitePlugin({
      build: [
        {
          entry: 'src/main.ts', // 主进程入口文件
          config: 'vite.main.config.ts',// 主进程专用的 Vite 配置文件
          target: 'main',// 指定目标为 Electron 主进程
        },
        {
          entry: 'src/preload.ts', // 预加载脚本入口文件
          config: 'vite.preload.config.ts',
          target: 'preload',
        },
      ],
      renderer: [
        {
          name: 'main_window',
          config: 'vite.renderer.config.ts',
        },
      ],
    }),
    // Fuses are used to enable/disable various Electron functionality
    // at package time, before code signing the application
    new FusesPlugin({
      version: FuseVersion.V1,
      [FuseV1Options.RunAsNode]: false,
      [FuseV1Options.EnableCookieEncryption]: true,
      [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
      [FuseV1Options.EnableNodeCliInspectArguments]: false,
      [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
      [FuseV1Options.OnlyLoadAppFromAsar]: true,
    }),
  ],
};

export default config;

打包命令

# 1. package 命令
npm  run  package 
# 效果:生成可执行程序,但还不是安装包,直接双击就可以用
#2. make 命令
npm run make
# 效果: 生成完整的安装包,需要安装完毕以后使用

可执行程序和安装包区别

维度 可执行程序(package) 安装包(make)
运行方式 直接双击运行(无需安装) 需运行安装向导,将文件部署到系统目录
系统影响 不修改系统目录(或仅临时缓存) 写入系统目录、注册元数据、创建快捷方式
分发形式 单文件/文件夹(如 .exe.app 安装包(如 .msi.pkg.dmg
用户体验 适合快速测试或便携使用 适合正式发布,提供标准化安装/卸载流程

package打包文件结构

📂 输出目录结构(以 Windows 为例)

out/
└── your-app-name-win32-x64/
    ├── your-app-name.exe          # 主可执行文件(Electron 封装)
    ├── resources/
    │   ├── app.asar               # 应用代码(ASAR 归档格式)
    │   ├── electron.asar          # Electron 运行时(可选)
    │   └── ...                    # 其他资源文件
    ├── node_modules/              # 生产依赖(仅限被引用的模块)
    ├── locales/                   # 多语言文件(如 zh-CN.pak)
    ├── swiftshader/               # GPU 渲染备用库(无显卡驱动时)
    └── ...                        # 其他平台相关文件

什么ASAR

一、ASAR 的本质:为什么用它?

传统 Electron 应用打包时,会将代码直接放在 resources/app目录下(明文可见),存在两个问题:

  1. 源码暴露风险:用户可直接查看/修改 JS/CSS 代码;
  2. 文件系统性能差:大量小文件读取效率低(尤其 Windows)。

ASAR 解决了这些问题:

  • 归档整合:将分散的文件打包成单个 .asar文件,类似 ZIP 但更高效(无需解压即可随机访问);
  • 保护源码:虽然非加密(可用工具解压),但增加了直接阅读的门槛;
  • 提升性能:减少文件系统调用次数,加快应用启动速度。

二、ASAR 文件结构(以 app.asar为例)

ASAR 文件内部是一个虚拟文件系统,结构与你的项目目录一致(但排除了 devDependencies和无关文件)。例如,若你的项目结构如下:

your-project/
├── package.json       # 生产依赖声明(仅保留 dependencies)
├── src/
│   ├── main.js        # 主进程代码
│   └── renderer/      # 渲染进程代码
│       ├── index.html
│       └── app.js
├── static/            # 静态资源(图片、字体等)
│   └── logo.png
└── node_modules/      # 仅包含被引用的生产依赖(精简后)

app.asar解压后的虚拟结构完全一致(但实际存储为二进制归档):

app.asar (虚拟目录)
├── package.json
├── src/
│   ├── main.js
│   └── renderer/
│       ├── index.html
│       └── app.js
├── static/
│   └── logo.png
└── node_modules/      # 精简后的生产依赖

三、ASAR 的核心操作(开发必备)

  1. 解压 ASAR 文件(查看/修改源码)

若需调试或修改打包后的代码,可先将 app.asar解压为明文目录:

# 安装 asar 工具(Electron 内置,也可单独安装)
npm install -g @electron/asar

# 解压 app.asar 到 unpacked 目录
asar extract out/your-app-win32-x64/resources/app.asar unpacked/

解压后,unpacked/目录即为明文的项目结构,可直接编辑代码。

  1. 重新打包为 ASAR

修改完成后,将明文目录重新打包为 app.asar

asar pack unpacked/ new-app.asar  # 将 unpacked/ 打包为 new-app.asar
# 替换原文件:cp new-app.asar out/your-app-win32-x64/resources/app.asar

3. 禁用 ASAR(明文目录模式)

若需完全明文(如开发阶段调试),可在 forge.config.js中关闭 ASAR:

// forge.config.js
module.exports = {
  packagerConfig: {
    asar: false,  // 禁用 ASAR,生成明文 app 目录(而非 app.asar)
  },
};

此时,package命令会在 resources/下生成 app/明文目录(而非 app.asar)。

浅记录一下专家体系

作者 潜水豆
2025年12月12日 10:48

在扩展MCP的时候就遇到了上下文不够用的问题,目前最佳还是用RAG,这种方式抓取关键信息,节省字段。期待一手专家强化的表现

Expert RAG System 架构设计

版本: v2.0 日期: 2025-12-12


一、系统全景图

┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                                      │
│                              🧠 Expert RAG Workflow System                                           │
│                                                                                                      │
├──────────────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                                      │
│   ┌─────────────────────────────────────────────────────────────────────────────────────────────┐   │
│   │                                    User Layer (用户层)                                       │   │
│   │                                                                                              │   │
│   │    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐                │   │
│   │    │   Cursor    │    │    CLI      │    │    Web      │    │    MCP      │                │   │
│   │    │    IDE      │    │   Tools     │    │  Dashboard  │    │   Client    │                │   │
│   │    └──────┬──────┘    └──────┬──────┘    └──────┬──────┘    └──────┬──────┘                │   │
│   │           │                  │                  │                  │                        │   │
│   └───────────┼──────────────────┼──────────────────┼──────────────────┼────────────────────────┘   │
│               │                  │                  │                  │                            │
│               ▼                  ▼                  ▼                  ▼                            │
│   ┌─────────────────────────────────────────────────────────────────────────────────────────────┐   │
│   │                                   MCP Layer (MCP 服务层)                                     │   │
│   │                                                                                              │   │
│   │    ┌───────────────────────────────────────────────────────────────────────────────────┐    │   │
│   │    │                           cursor-workflow MCP Server                               │    │   │
│   │    │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐               │    │   │
│   │    │  │  Workflow   │  │   Expert    │  │    RAG      │  │   Status    │               │    │   │
│   │    │  │  Commands   │  │  Commands   │  │  Commands   │  │  Commands   │               │    │   │
│   │    │  │ /start      │  │ /activate   │  │ rag_query   │  │ rag_status  │               │    │   │
│   │    │  │ /iterate    │  │ /reinforce  │  │ rag_index   │  │ rag_inspect │               │    │   │
│   │    │  │ /confirm    │  │ /iterate    │  │             │  │             │               │    │   │
│   │    │  └─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘               │    │   │
│   │    └───────────────────────────────────────────────────────────────────────────────────┘    │   │
│   │                                              │                                               │   │
│   └──────────────────────────────────────────────┼───────────────────────────────────────────────┘   │
│                                                  │                                                   │
│                                                  ▼                                                   │
│   ┌─────────────────────────────────────────────────────────────────────────────────────────────┐   │
│   │                                  Core Layer (核心层)                                         │   │
│   │                                                                                              │   │
│   │    ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐                        │   │
│   │    │  Expert Manager │    │   RAG Engine    │    │ Session Manager │                        │   │
│   │    │                 │    │                 │    │                 │                        │   │
│   │    │ • 专家注册      │    │ • 文档分块      │    │ • 上下文缓存    │                        │   │
│   │    │ • 专家激活      │    │ • 向量化       │    │ • Session 管理  │                        │   │
│   │    │ • 强化记录      │    │ • 语义检索     │    │ • TTL 控制      │                        │   │
│   │    │ • 迭代升级      │    │ • 结果排序     │    │ • 缓存释放      │                        │   │
│   │    └────────┬────────┘    └────────┬────────┘    └────────┬────────┘                        │   │
│   │             │                      │                      │                                 │   │
│   │             ▼                      ▼                      ▼                                 │   │
│   │    ┌─────────────────────────────────────────────────────────────────┐                      │   │
│   │    │                     Retrieval Chain (检索链)                    │                      │   │
│   │    │                                                                 │                      │   │
│   │    │    RAG 检索 ───► 关键词匹配 ───► 文件加载 ───► 空响应          │                      │   │
│   │    │       │              │              │              │            │                      │   │
│   │    │       ▼              ▼              ▼              ▼            │                      │   │
│   │    │   [成功返回]    [成功返回]    [成功返回]    [无知识提示]        │                      │   │
│   │    └─────────────────────────────────────────────────────────────────┘                      │   │
│   │                                                                                              │   │
│   └──────────────────────────────────────────────────────────────────────────────────────────────┘   │
│                                                  │                                                   │
│                                                  ▼                                                   │
│   ┌─────────────────────────────────────────────────────────────────────────────────────────────┐   │
│   │                                 Storage Layer (存储层)                                       │   │
│   │                                                                                              │   │
│   │    ┌─────────────────────────────┐         ┌─────────────────────────────┐                  │   │
│   │    │      Global Storage         │         │      Project Storage        │                  │   │
│   │    │   ~/.cursor-workflow/       │         │   {project}/.cursor/        │                  │   │
│   │    │                             │         │                             │                  │   │
│   │    │  ├─ models/                 │         │  ├─ skills/                 │                  │   │
│   │    │  │  └─ all-MiniLM-L6-v2/   │         │  │  ├─ vue3-expert/         │                  │   │
│   │    │  │                          │         │  │  │  ├─ SKILL.md         │                  │   │
│   │    │  ├─ skills/                 │         │  │  │  ├─ LOCAL.md         │                  │   │
│   │    │  │  └─ (模板库)             │         │  │  │  └─ reinforcements/  │                  │   │
│   │    │  │                          │         │  │  └─ react-expert/       │                  │   │
│   │    │  ├─ vectors/                │         │  │                          │                  │   │
│   │    │  │  └─ global-index.json   │◄────────│  ├─ vectors/                │                  │   │
│   │    │  │                          │  引用   │  │  ├─ index.json          │                  │   │
│   │    │  └─ registry.json           │         │  │  └─ metadata.json       │                  │   │
│   │    │                             │         │  │                          │                  │   │
│   │    └─────────────────────────────┘         │  └─ workflow-state.json    │                  │   │
│   │                                             └─────────────────────────────┘                  │   │
│   │                                                                                              │   │
│   │    ┌─────────────────────────────────────────────────────────────────────────────────────┐  │   │
│   │    │                              Embedding Layer                                         │  │   │
│   │    │                                                                                      │  │   │
│   │    │    ┌─────────────────┐         ┌─────────────────┐         ┌─────────────────┐      │  │   │
│   │    │    │  Transformers.js │         │     Orama      │         │  JSON Storage   │      │  │   │
│   │    │    │  (Embedding)    │────────►│  (Vector DB)   │────────►│  (Persistence)  │      │  │   │
│   │    │    │  384 维向量     │         │  向量检索      │         │  文件存储       │      │  │   │
│   │    │    └─────────────────┘         └─────────────────┘         └─────────────────┘      │  │   │
│   │    └─────────────────────────────────────────────────────────────────────────────────────┘  │   │
│   │                                                                                              │   │
│   └──────────────────────────────────────────────────────────────────────────────────────────────┘   │
│                                                                                                      │
└──────────────────────────────────────────────────────────────────────────────────────────────────────┘

二、专家生命周期

┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    Expert Lifecycle (专家生命周期)                               │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                                  │
│   ┌──────────────────────────────────────────────────────────────────────────────────────────┐  │
│   │                                    1. 模板创建阶段                                        │  │
│   │                                                                                           │  │
│   │      开发者定义专家                                                                       │  │
│   │           │                                                                               │  │
│   │           ▼                                                                               │  │
│   │    ┌─────────────┐         ┌─────────────┐         ┌─────────────┐                       │  │
│   │    │  SKILL.md   │         │  commands/  │         │  resources/ │                       │  │
│   │    │  核心技能   │         │  命令定义   │         │  模板资源   │                       │  │
│   │    └─────────────┘         └─────────────┘         └─────────────┘                       │  │
│   │           │                       │                       │                               │  │
│   │           └───────────────────────┼───────────────────────┘                               │  │
│   │                                   ▼                                                       │  │
│   │                    ┌─────────────────────────────┐                                        │  │
│   │                    │   全局模板库                 │                                        │  │
│   │                    │   ~/.cursor-workflow/skills │                                        │  │
│   │                    └─────────────────────────────┘                                        │  │
│   └──────────────────────────────────────────────────────────────────────────────────────────┘  │
│                                           │                                                      │
│                                           ▼                                                      │
│   ┌──────────────────────────────────────────────────────────────────────────────────────────┐  │
│   │                                    2. 项目初始化阶段                                      │  │
│   │                                                                                           │  │
│   │    $ cursor-workflow init --project=/path/to/project --experts=vue3,react,architecture   │  │
│   │                                           │                                               │  │
│   │                                           ▼                                               │  │
│   │    ┌─────────────────────────────────────────────────────────────────────────────────┐   │  │
│   │    │                                                                                  │   │  │
│   │    │   1. 复制专家模板到项目                                                          │   │  │
│   │    │      ~/.cursor-workflow/skills/vue3-expert → .cursor/skills/vue3-expert         │   │  │
│   │    │                                                                                  │   │  │
│   │    │   2. 创建本地化配置文件                                                          │   │  │
│   │    │      .cursor/skills/vue3-expert/LOCAL.md (空模板)                               │   │  │
│   │    │                                                                                  │   │  │
│   │    │   3. 初始化向量索引                                                              │   │  │
│   │    │      引用全局索引 + 创建项目索引空间                                             │   │  │
│   │    │                                                                                  │   │  │
│   │    │   4. 创建工作流状态文件                                                          │   │  │
│   │    │      .cursor/workflow-state.json                                                │   │  │
│   │    │                                                                                  │   │  │
│   │    └─────────────────────────────────────────────────────────────────────────────────┘   │  │
│   └──────────────────────────────────────────────────────────────────────────────────────────┘  │
│                                           │                                                      │
│                                           ▼                                                      │
│   ┌──────────────────────────────────────────────────────────────────────────────────────────┐  │
│   │                                    3. 本地化阶段                                          │  │
│   │                                                                                           │  │
│   │    用户: "请根据这个 API 文档本地化 vue3 专家"                                           │  │
│   │           │                                                                               │  │
│   │           ▼                                                                               │  │
│   │    ┌─────────────────────────────────────────────────────────────────────────────────┐   │  │
│   │    │  workflow_localize(expertId, materials)                                          │   │  │
│   │    │                                                                                  │   │  │
│   │    │   1. 读取物料文件 (API.md, 规范文档等)                                           │   │  │
│   │    │   2. AI 总结提取知识点                                                           │   │  │
│   │    │   3. 更新 LOCAL.md                                                              │   │  │
│   │    │   4. 增量索引到向量库                                                            │   │  │
│   │    │                                                                                  │   │  │
│   │    └─────────────────────────────────────────────────────────────────────────────────┘   │  │
│   │                                                                                           │  │
│   │    结果:                                                                                  │  │
│   │    ┌─────────────────────────────────────────────────────────────────────────────────┐   │  │
│   │    │  LOCAL.md                                                                        │   │  │
│   │    │  ─────────────────────────────────────────────────────────────────               │   │  │
│   │    │  # vue3-expert 项目本地化配置                                                    │   │  │
│   │    │                                                                                  │   │  │
│   │    │  ## 项目 API 规范                                                                │   │  │
│   │    │  - 使用 /api/v1 作为 API 前缀                                                   │   │  │
│   │    │  - 响应格式: { code, data, message }                                            │   │  │
│   │    │                                                                                  │   │  │
│   │    │  ## 组件规范                                                                     │   │  │
│   │    │  - 强制使用 <script setup> 语法                                                 │   │  │
│   │    │  - 样式使用 SCSS + BEM 命名                                                     │   │  │
│   │    └─────────────────────────────────────────────────────────────────────────────────┘   │  │
│   └──────────────────────────────────────────────────────────────────────────────────────────┘  │
│                                           │                                                      │
│                                           ▼                                                      │
│   ┌──────────────────────────────────────────────────────────────────────────────────────────┐  │
│   │                                    4. 使用阶段                                            │  │
│   │                                                                                           │  │
│   │    用户: "/vue3-expert 帮我创建一个用户管理组件"                                         │  │
│   │           │                                                                               │  │
│   │           ▼                                                                               │  │
│   │    ┌─────────────────────────────────────────────────────────────────────────────────┐   │  │
│   │    │  workflow_activate(vue3-expert)                                                  │   │  │
│   │    │                                                                                  │   │  │
│   │    │   1. 检查向量索引状态                                                            │   │  │
│   │    │   2. RAG 检索相关知识                                                            │   │  │
│   │    │      - SKILL: 组件创建规范                                                       │   │  │
│   │    │      - LOCAL: 项目组件规范                                                       │   │  │
│   │    │      - REINFORCE: 历史踩坑经验                                                   │   │  │
│   │    │   3. 注入上下文,开始工作                                                        │   │  │
│   │    │                                                                                  │   │  │
│   │    └─────────────────────────────────────────────────────────────────────────────────┘   │  │
│   └──────────────────────────────────────────────────────────────────────────────────────────┘  │
│                                           │                                                      │
│                                           ▼                                                      │
│   ┌──────────────────────────────────────────────────────────────────────────────────────────┐  │
│   │                                    5. 强化阶段                                            │  │
│   │                                                                                           │  │
│   │    方案评审 < 80 分,或者用户指出问题                                                    │  │
│   │           │                                                                               │  │
│   │           ▼                                                                               │  │
│   │    ┌─────────────────────────────────────────────────────────────────────────────────┐   │  │
│   │    │  expert_reinforce(expertId, problem, solution)                                   │   │  │
│   │    │                                                                                  │   │  │
│   │    │   1. 记录问题和解决方案到 HISTORY.md                                             │   │  │
│   │    │   2. 增量索引强化内容                                                            │   │  │
│   │    │   3. 检查强化次数                                                                │   │  │
│   │    │      - < 5: 继续累积                                                             │   │  │
│   │    │      - >= 5: 触发专家迭代                                                        │   │  │
│   │    │                                                                                  │   │  │
│   │    └─────────────────────────────────────────────────────────────────────────────────┘   │  │
│   └──────────────────────────────────────────────────────────────────────────────────────────┘  │
│                                           │                                                      │
│                                           ▼                                                      │
│   ┌──────────────────────────────────────────────────────────────────────────────────────────┐  │
│   │                                    6. 迭代阶段                                            │  │
│   │                                                                                           │  │
│   │    强化次数 >= 5                                                                         │  │
│   │           │                                                                               │  │
│   │           ▼                                                                               │  │
│   │    ┌─────────────────────────────────────────────────────────────────────────────────┐   │  │
│   │    │  expert_iterate(expertId)                                                        │   │  │
│   │    │                                                                                  │   │  │
│   │    │   1. 读取所有强化记录                                                            │   │  │
│   │    │   2. AI 分析共性问题                                                             │   │  │
│   │    │   3. 更新 SKILL.md (融入强化经验)                                               │   │  │
│   │    │   4. 清空 HISTORY.md                                                            │   │  │
│   │    │   5. 重建向量索引                                                                │   │  │
│   │    │   6. 版本号 +1                                                                   │   │  │
│   │    │                                                                                  │   │  │
│   │    └─────────────────────────────────────────────────────────────────────────────────┘   │  │
│   │                                                                                           │  │
│   │    结果: 专家从 v1.0.0 升级到 v1.1.0,融合了 5 次强化经验                               │  │
│   └──────────────────────────────────────────────────────────────────────────────────────────┘  │
│                                                                                                  │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

三、工作流状态机

┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    Workflow State Machine (工作流状态机)                         │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                                  │
│                                         /start <任务>                                            │
│                                              │                                                   │
│                                              ▼                                                   │
│   ┌─────────────────────────────────────────────────────────────────────────────────────────┐   │
│   │                                                                                          │   │
│   │                                       ┌────────┐                                         │   │
│   │                                       │  INIT  │                                         │   │
│   │                                       └────┬───┘                                         │   │
│   │                                            │                                             │   │
│   │                            激活需求专家,开始分析需求                                    │   │
│   │                                            │                                             │   │
│   │                                            ▼                                             │   │
│   │                              ┌───────────────────────────┐                               │   │
│   │                              │       REQUIREMENT         │                               │   │
│   │                              │                           │                               │   │
│   │                              │  ┌─────────────────────┐  │                               │   │
│   │                              │  │ 迭代 1: 理解核心需求 │  │ ◄─── /iterate                │   │
│   │                              │  └──────────┬──────────┘  │                               │   │
│   │                              │             │              │                               │   │
│   │                              │  ┌──────────▼──────────┐  │                               │   │
│   │                              │  │ 迭代 2: 补充边界条件 │  │ ◄─── /iterate                │   │
│   │                              │  └──────────┬──────────┘  │                               │   │
│   │                              │             │              │                               │   │
│   │                              │  ┌──────────▼──────────┐  │                               │   │
│   │                              │  │ 迭代 3: 最终审查    │  │ ◄─── /iterate                │   │
│   │                              │  └─────────────────────┘  │                               │   │
│   │                              │                           │                               │   │
│   │                              └─────────────┬─────────────┘                               │   │
│   │                                            │                                             │   │
│   │                                      /confirm                                            │   │
│   │                                            │                                             │   │
│   │                                            ▼                                             │   │
│   │                              ┌───────────────────────────┐                               │   │
│   │                              │        SOLUTION           │                               │   │
│   │                              │                           │                               │   │
│   │                              │   ┌─────────────────────┐ │                               │   │
│   │                              │   │   并行生成方案      │ │                               │   │
│   │                              │   │                     │ │                               │   │
│   │                              │   │  ┌───────┐ ┌───────┐│ │                               │   │
│   │                              │   │  │Expert1│ │Expert2││ │                               │   │
│   │                              │   │  │方案 A │ │方案 B ││ │                               │   │
│   │                              │   │  └───────┘ └───────┘│ │                               │   │
│   │                              │   └─────────────────────┘ │                               │   │
│   │                              │                           │                               │   │
│   │                              └─────────────┬─────────────┘                               │   │
│   │                                            │                                             │   │
│   │                                       /review                                            │   │
│   │                                            │                                             │   │
│   │                                            ▼                                             │   │
│   │                              ┌───────────────────────────┐                               │   │
│   │                              │         REVIEW            │                               │   │
│   │                              │                           │                               │   │
│   │                              │   专家互评打分            │                               │   │
│   │                              │                           │                               │   │
│   │                              │   ┌─────────────────────┐ │                               │   │
│   │                              │   │ 评分维度 (各25分)   │ │                               │   │
│   │                              │   │ • 完整性            │ │                               │   │
│   │                              │   │ • 可行性            │ │                               │   │
│   │                              │   │ • 可维护性          │ │                               │   │
│   │                              │   │ • 性能考虑          │ │                               │   │
│   │                              │   └─────────────────────┘ │                               │   │
│   │                              │                           │                               │   │
│   │                              └─────────────┬─────────────┘                               │   │
│   │                                            │                                             │   │
│   │                          ┌─────────────────┼─────────────────┐                           │   │
│   │                          │                 │                 │                           │   │
│   │                     < 80 分           >= 80 分          最高分方案                       │   │
│   │                          │                 │                 │                           │   │
│   │                          ▼                 │                 │                           │   │
│   │            ┌─────────────────────┐         │                 │                           │   │
│   │            │    强化记录         │         │                 │                           │   │
│   │            │    记录不足点       │         │                 │                           │   │
│   │            │    重新生成方案     │         │                 │                           │   │
│   │            └──────────┬──────────┘         │                 │                           │   │
│   │                       │                    │                 │                           │   │
│   │                       └────────────────────┼─────────────────┘                           │   │
│   │                                            │                                             │   │
│   │                                            ▼                                             │   │
│   │                              ┌───────────────────────────┐                               │   │
│   │                              │       EXECUTION           │                               │   │
│   │                              │                           │                               │   │
│   │                              │   ┌─────────────────────┐ │                               │   │
│   │                              │   │ Task 1: xxx         │ │ ◄─── /next                   │   │
│   │                              │   │ [✓] 完成            │ │                               │   │
│   │                              │   ├─────────────────────┤ │                               │   │
│   │                              │   │ Task 2: xxx         │ │ ◄─── /next                   │   │
│   │                              │   │ [✓] 完成            │ │                               │   │
│   │                              │   ├─────────────────────┤ │                               │   │
│   │                              │   │ Task 3: xxx         │ │ ◄─── /next                   │   │
│   │                              │   │ [ ] 进行中          │ │                               │   │
│   │                              │   └─────────────────────┘ │                               │   │
│   │                              │                           │                               │   │
│   │                              └─────────────┬─────────────┘                               │   │
│   │                                            │                                             │   │
│   │                                        /done                                             │   │
│   │                                            │                                             │   │
│   │                                            ▼                                             │   │
│   │                                       ┌────────┐                                         │   │
│   │                                       │  DONE  │                                         │   │
│   │                                       └────────┘                                         │   │
│   │                                                                                          │   │
│   └──────────────────────────────────────────────────────────────────────────────────────────┘   │
│                                                                                                  │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

四、RAG 检索流程

┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    RAG Retrieval Flow (RAG 检索流程)                             │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                                  │
│    用户输入: "帮我创建一个带有表单验证的用户编辑组件"                                           │
│         │                                                                                        │
│         ▼                                                                                        │
│    ┌─────────────────────────────────────────────────────────────────────────────────────────┐  │
│    │                                   1. Query Processing                                    │  │
│    │                                                                                          │  │
│    │    ┌──────────────────┐         ┌──────────────────┐         ┌──────────────────┐       │  │
│    │    │   Query Parser   │────────►│   Transformers.js│────────►│  Query Vector    │       │  │
│    │    │   提取关键意图   │         │   文本向量化     │         │  [0.12, 0.34...] │       │  │
│    │    └──────────────────┘         └──────────────────┘         └──────────────────┘       │  │
│    │                                                                       │                  │  │
│    └───────────────────────────────────────────────────────────────────────┼──────────────────┘  │
│                                                                            │                     │
│                                                                            ▼                     │
│    ┌─────────────────────────────────────────────────────────────────────────────────────────┐  │
│    │                                   2. Session Check                                       │  │
│    │                                                                                          │  │
│    │    ┌──────────────────────────────────────────────────────────────────┐                 │  │
│    │    │  Context Budget Manager                                          │                 │  │
│    │    │                                                                   │                 │  │
│    │    │  ┌────────────────────┐    ┌────────────────────┐                │                 │  │
│    │    │  │ 首次激活?          │    │ 缓存命中?          │                │                 │  │
│    │    │  │                    │    │                    │                │                 │  │
│    │    │  │  是 → 完整检索     │    │  是 → 增量检索     │                │                 │  │
│    │    │  │  否 → 检查缓存     │    │  否 → 完整检索     │                │                 │  │
│    │    │  └────────────────────┘    └────────────────────┘                │                 │  │
│    │    └──────────────────────────────────────────────────────────────────┘                 │  │
│    │                                              │                                           │  │
│    └──────────────────────────────────────────────┼───────────────────────────────────────────┘  │
│                                                   │                                              │
│                                                   ▼                                              │
│    ┌─────────────────────────────────────────────────────────────────────────────────────────┐  │
│    │                                   3. Retrieval Chain                                     │  │
│    │                                                                                          │  │
│    │    ┌─────────────────────────────────────────────────────────────────────────────────┐  │  │
│    │    │                                                                                  │  │  │
│    │    │   ┌──────────────┐      ┌──────────────┐      ┌──────────────┐      ┌────────┐  │  │  │
│    │    │   │  RAG 检索    │─────►│  关键词匹配  │─────►│  文件加载    │─────►│  空    │  │  │  │
│    │    │   │  (Orama)     │ 失败 │  (BM25)      │ 失败 │  (降级)      │ 失败 │  响应  │  │  │  │
│    │    │   └──────┬───────┘      └──────┬───────┘      └──────┬───────┘      └────────┘  │  │  │
│    │    │          │ 成功                │ 成功                │ 成功                     │  │  │
│    │    │          ▼                     ▼                     ▼                          │  │  │
│    │    │   ┌──────────────────────────────────────────────────────┐                      │  │  │
│    │    │   │                    Results                           │                      │  │  │
│    │    │   └──────────────────────────────────────────────────────┘                      │  │  │
│    │    │                                                                                  │  │  │
│    │    └─────────────────────────────────────────────────────────────────────────────────┘  │  │
│    │                                              │                                           │  │
│    └──────────────────────────────────────────────┼───────────────────────────────────────────┘  │
│                                                   │                                              │
│                                                   ▼                                              │
│    ┌─────────────────────────────────────────────────────────────────────────────────────────┐  │
│    │                                   4. Priority & Merge                                    │  │
│    │                                                                                          │  │
│    │    ┌─────────────────────────────────────────────────────────────────────────────────┐  │  │
│    │    │                     知识优先级排序                                               │  │  │
│    │    │                                                                                  │  │  │
│    │    │   ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐                        │  │  │
│    │    │   │  LOCAL   │  │ REINFORCE│  │  SKILL   │  │  SHARED  │                        │  │  │
│    │    │   │ 优先级100│  │ 优先级80 │  │ 优先级60 │  │ 优先级40 │                        │  │  │
│    │    │   └──────────┘  └──────────┘  └──────────┘  └──────────┘                        │  │  │
│    │    │        │              │              │              │                            │  │  │
│    │    │        └──────────────┼──────────────┼──────────────┘                            │  │  │
│    │    │                       │              │                                           │  │  │
│    │    │                       ▼              ▼                                           │  │  │
│    │    │              ┌─────────────────────────────────┐                                 │  │  │
│    │    │              │   Conflict Resolver             │                                 │  │  │
│    │    │              │   相同主题取高优先级            │                                 │  │  │
│    │    │              └─────────────────────────────────┘                                 │  │  │
│    │    │                                                                                  │  │  │
│    │    └─────────────────────────────────────────────────────────────────────────────────┘  │  │
│    │                                              │                                           │  │
│    └──────────────────────────────────────────────┼───────────────────────────────────────────┘  │
│                                                   │                                              │
│                                                   ▼                                              │
│    ┌─────────────────────────────────────────────────────────────────────────────────────────┐  │
│    │                                   5. Context Injection                                   │  │
│    │                                                                                          │  │
│    │    ┌─────────────────────────────────────────────────────────────────────────────────┐  │  │
│    │    │                                                                                  │  │  │
│    │    │   ## 📚 相关知识 (置信度: 92%)                                                   │  │  │
│    │    │                                                                                  │  │  │
│    │    │   ### 来源: 项目规范 (LOCAL) [优先级: 100]                                       │  │  │
│    │    │   > 本项目使用 Element Plus 表单组件                                             │  │  │
│    │    │   > 表单验证使用 async-validator                                                 │  │  │
│    │    │                                                                                  │  │  │
│    │    │   ### 来源: 强化经验 (REINFORCE) [优先级: 80]                                    │  │  │
│    │    │   > 问题: 表单重置时验证状态未清除                                               │  │  │
│    │    │   > 解决: 调用 formRef.resetFields() 而非手动清空                                │  │  │
│    │    │                                                                                  │  │  │
│    │    │   ### 来源: 基础技能 (SKILL) [优先级: 60]                                        │  │  │
│    │    │   > Vue3 表单组件应使用 v-model 双向绑定                                         │  │  │
│    │    │   > 验证规则定义在 rules 对象中                                                  │  │  │
│    │    │                                                                                  │  │  │
│    │    │   ---                                                                            │  │  │
│    │    │   ℹ️ 已消耗 450 tokens,剩余预算 2550 tokens                                     │  │  │
│    │    │                                                                                  │  │  │
│    │    └─────────────────────────────────────────────────────────────────────────────────┘  │  │
│    │                                                                                          │  │
│    └──────────────────────────────────────────────────────────────────────────────────────────┘  │
│                                                                                                  │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

五、可视化管理

┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    Visualization & Management (可视化与管理)                     │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                                  │
│    ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│    │                              1. CLI Tools (命令行工具)                                    │ │
│    │                                                                                           │ │
│    │   $ cursor-rag status                          # 查看全局状态                            │ │
│    │   $ cursor-rag status --project=./             # 查看项目状态                            │ │
│    │   $ cursor-rag inspect vue3-expert             # 查看专家知识                            │ │
│    │   $ cursor-rag search "组件创建"               # 搜索知识库                              │ │
│    │   $ cursor-rag index --rebuild                 # 重建索引                                │ │
│    │   $ cursor-rag export vue3-expert              # 导出专家                                │ │
│    │                                                                                           │ │
│    └──────────────────────────────────────────────────────────────────────────────────────────┘ │
│                                                                                                  │
│    ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│    │                              2. MCP Tools (Cursor 内查询)                                 │ │
│    │                                                                                           │ │
│    │   用户: "查看当前知识库状态"                                                             │ │
│    │                                                                                           │ │
│    │   AI 调用: rag_status({ projectPath: "E:/vite7-vue-project" })                          │ │
│    │                                                                                           │ │
│    │   返回:                                                                                   │ │
│    │   ┌─────────────────────────────────────────────────────────────────────────────────┐    │ │
│    │   │  📊 RAG 知识库状态                                                               │    │ │
│    │   │                                                                                  │    │ │
│    │   │  项目: vite7-vue-project                                                        │    │ │
│    │   │  专家: 9| 知识块: 150| 总 Tokens: 22.0K                                 │    │ │
│    │   │                                                                                  │    │ │
│    │   │  专家详情:                                                                       │    │ │
│    │   │  ├─ vue3-expert    [25 块] SKILL:12 LOCAL:8 REINFORCE:5                        │    │ │
│    │   │  ├─ react-expert   [18 块] SKILL:10 LOCAL:6 REINFORCE:2                        │    │ │
│    │   │  └─ arch-expert    [28 块] SKILL:15 LOCAL:10 REINFORCE:3                       │    │ │
│    │   │                                                                                  │    │ │
│    │   │  Session: 活跃 (30分钟前激活 vue3-expert)                                       │    │ │
│    │   │  缓存: 命中率 80% | 节省 8000 tokens                                            │    │ │
│    │   └─────────────────────────────────────────────────────────────────────────────────┘    │ │
│    │                                                                                           │ │
│    └──────────────────────────────────────────────────────────────────────────────────────────┘ │
│                                                                                                  │
│    ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│    │                              3. Web Dashboard (可视化界面)                                │ │
│    │                                                                                           │ │
│    │   $ cursor-rag dashboard                       # 启动 Dashboard                         │ │
│    │   🌐 Dashboard 启动于 http://localhost:3721                                              │ │
│    │                                                                                           │ │
│    │   ┌─────────────────────────────────────────────────────────────────────────────────┐    │ │
│    │   │  ┌───────────────────────────────────────────────────────────────────────────┐  │    │ │
│    │   │  │  🧠 Expert RAG Dashboard                               [vite7-vue-project] │  │    │ │
│    │   │  ├───────────────────────────────────────────────────────────────────────────┤  │    │ │
│    │   │  │                                                                            │  │    │ │
│    │   │  │   ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐                  │  │    │ │
│    │   │  │   │ Projects │  │ Experts  │  │  Chunks  │  │  Tokens  │                  │  │    │ │
│    │   │  │   │    3     │  │    17    │  │   275    │  │  40.7K   │                  │  │    │ │
│    │   │  │   └──────────┘  └──────────┘  └──────────┘  └──────────┘                  │  │    │ │
│    │   │  │                                                                            │  │    │ │
│    │   │  │   ┌────────────────────────────────────────────────────────────────────┐  │  │    │ │
│    │   │  │   │  📈 知识分布                                                        │  │  │    │ │
│    │   │  │   │                                                                     │  │  │    │ │
│    │   │  │   │      SKILL ████████████████░░░░░░ 48%                              │  │  │    │ │
│    │   │  │   │      LOCAL ████████████░░░░░░░░░░ 32%                              │  │  │    │ │
│    │   │  │   │  REINFORCE ████████░░░░░░░░░░░░░░ 20%                              │  │  │    │ │
│    │   │  │   │                                                                     │  │  │    │ │
│    │   │  │   └────────────────────────────────────────────────────────────────────┘  │  │    │ │
│    │   │  │                                                                            │  │    │ │
│    │   │  │   ┌────────────────────────────────────────────────────────────────────┐  │  │    │ │
│    │   │  │   │  🔍 知识搜索                                       [ 搜索 ]        │  │  │    │ │
│    │   │  │   │  ┌──────────────────────────────────────────────────────────────┐  │  │  │    │ │
│    │   │  │   │  │ 表单验证                                                      │  │  │  │    │ │
│    │   │  │   │  └──────────────────────────────────────────────────────────────┘  │  │  │    │ │
│    │   │  │   │                                                                     │  │  │    │ │
│    │   │  │   │  Results: 5 matches                                                │  │  │    │ │
│    │   │  │   │  ┌────────────────────────────────────────────────────────────┐    │  │  │    │ │
│    │   │  │   │  │ 1. [LOCAL] 项目表单规范 (0.95)                             │    │  │  │    │ │
│    │   │  │   │  │ 2. [REINFORCE] 表单重置问题 (0.88)                         │    │  │  │    │ │
│    │   │  │   │  │ 3. [SKILL] Vue3 表单组件 (0.82)                            │    │  │  │    │ │
│    │   │  │   │  └────────────────────────────────────────────────────────────┘    │  │  │    │ │
│    │   │  │   └────────────────────────────────────────────────────────────────────┘  │  │    │ │
│    │   │  │                                                                            │  │    │ │
│    │   │  └───────────────────────────────────────────────────────────────────────────┘  │    │ │
│    │   └─────────────────────────────────────────────────────────────────────────────────┘    │ │
│    │                                                                                           │ │
│    └──────────────────────────────────────────────────────────────────────────────────────────┘ │
│                                                                                                  │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

六、数据流图

┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    Data Flow (数据流)                                            │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                                  │
│                                                                                                  │
│    ┌─────────────┐                                                      ┌─────────────┐         │
│    │   SKILL.md  │                                                      │   LOCAL.md  │         │
│    │   基础技能  │                                                      │  项目本地化 │         │
│    └──────┬──────┘                                                      └──────┬──────┘         │
│           │                                                                    │                │
│           │                        ┌─────────────┐                             │                │
│           │                        │ HISTORY.md  │                             │                │
│           │                        │  强化记录   │                             │                │
│           │                        └──────┬──────┘                             │                │
│           │                               │                                    │                │
│           ▼                               ▼                                    ▼                │
│    ┌─────────────────────────────────────────────────────────────────────────────────────┐     │
│    │                                Document Chunker                                      │     │
│    │                                   文档分块器                                         │     │
│    │                                                                                      │     │
│    │   输入: Markdown 文件                                                                │     │
│    │   输出: 知识块 (chunk_size=500, overlap=50)                                          │     │
│    │                                                                                      │     │
│    └────────────────────────────────────────┬────────────────────────────────────────────┘     │
│                                             │                                                   │
│                                             ▼                                                   │
│    ┌─────────────────────────────────────────────────────────────────────────────────────┐     │
│    │                              Transformers.js Embedding                               │     │
│    │                                    向量化引擎                                        │     │
│    │                                                                                      │     │
│    │   模型: all-MiniLM-L6-v2                                                             │     │
│    │   维度: 384                                                                          │     │
│    │   输出: [0.12, -0.34, 0.56, ...]                                                     │     │
│    │                                                                                      │     │
│    └────────────────────────────────────────┬────────────────────────────────────────────┘     │
│                                             │                                                   │
│                                             ▼                                                   │
│    ┌─────────────────────────────────────────────────────────────────────────────────────┐     │
│    │                                   Orama Database                                     │     │
│    │                                    向量数据库                                        │     │
│    │                                                                                      │     │
│    │   ┌─────────────────────────────────────────────────────────────────────────────┐   │     │
│    │   │  index.json                                                                  │   │     │
│    │   │  ─────────────────────────────────────────────────────────────────           │   │     │
│    │   │  {                                                                           │   │     │
│    │   │    "chunks": [                                                               │   │     │
│    │   │      {                                                                       │   │     │
│    │   │        "id": "vue3-expert:skill:001",                                        │   │     │
│    │   │        "expertId": "vue3-expert",                                            │   │     │
│    │   │        "docType": "skill",                                                   │   │     │
│    │   │        "priority": 60,                                                       │   │     │
│    │   │        "content": "Vue3 组件应使用 Composition API...",                      │   │     │
│    │   │        "embedding": [0.12, -0.34, 0.56, ...]                                 │   │     │
│    │   │      },                                                                      │   │     │
│    │   │      ...                                                                     │   │     │
│    │   │    ]                                                                         │   │     │
│    │   │  }                                                                           │   │     │
│    │   └─────────────────────────────────────────────────────────────────────────────┘   │     │
│    │                                                                                      │     │
│    │   ┌─────────────────────────────────────────────────────────────────────────────┐   │     │
│    │   │  metadata.json                                                               │   │     │
│    │   │  ─────────────────────────────────────────────────────────────────           │   │     │
│    │   │  {                                                                           │   │     │
│    │   │    "projectName": "vite7-vue-project",                                       │   │     │
│    │   │    "experts": {                                                              │   │     │
│    │   │      "vue3-expert": { "chunks": 25, "tokens": 3500, ... }                    │   │     │
│    │   │    },                                                                        │   │     │
│    │   │    "stats": { "totalChunks": 150, "totalTokens": 22000 }                     │   │     │
│    │   │  }                                                                           │   │     │
│    │   └─────────────────────────────────────────────────────────────────────────────┘   │     │
│    │                                                                                      │     │
│    └────────────────────────────────────────┬────────────────────────────────────────────┘     │
│                                             │                                                   │
│                                             ▼                                                   │
│    ┌─────────────────────────────────────────────────────────────────────────────────────┐     │
│    │                                workflow-state.json                                   │     │
│    │                                   工作流状态                                         │     │
│    │                                                                                      │     │
│    │   {                                                                                  │     │
│    │     "meta": { ... },                                                                 │     │
│    │     "state": { "current": "EXECUTION", ... },                                        │     │
│    │     "rag": {                                                                         │     │
│    │       "session": { "id": "...", "lastActiveAt": "..." },                             │     │
│    │       "activeExpert": { "id": "vue3-expert", "contextLoaded": true },                │     │
│    │       "contextCache": { ... },                                                       │     │
│    │       "stats": { "cacheHits": 12, "tokensSaved": 8000 }                              │     │
│    │     }                                                                                │     │
│    │   }                                                                                  │     │
│    │                                                                                      │     │
│    └─────────────────────────────────────────────────────────────────────────────────────┘     │
│                                                                                                  │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

七、实施计划(更新版)

┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    Implementation Plan (实施计划)                                │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                                  │
│   Phase 1: 基础设施 (3天)                                                                        │
│   ├─ 1.1 Orama 向量数据库集成                                    [2天]                          │
│   └─ 1.2 Transformers.js Embedding 集成                          [1天]                          │
│                                                                                                  │
│   Phase 2: 核心功能 (4天)                                                                        │
│   ├─ 2.1 Document Chunker 文档分块器                             [1天]                          │
│   ├─ 2.2 Session Manager 会话管理器                              [1天]                          │
│   ├─ 2.3 Retrieval Chain 检索链                                  [1天]                          │
│   └─ 2.4 Context Budget Manager 上下文预算                       [1天]                          │
│                                                                                                  │
│   Phase 3: MCP 集成 (3天)                                                                        │
│   ├─ 3.1 扩展 workflow_activate (内置 RAG)                       [1天]                          │
│   ├─ 3.2 新增 rag_status / rag_inspect 工具                      [1天]                          │
│   └─ 3.3 扩展 expert_reinforce (自动索引)                        [1天]                          │
│                                                                                                  │
│   Phase 4: 初始化与索引 (2天)                                                                    │
│   ├─ 4.1 冷启动处理                                               [1天]                          │
│   └─ 4.2 增量索引机制                                             [1天]                          │
│                                                                                                  │
│   Phase 5: 可视化 (3天)                                                                          │
│   ├─ 5.1 metadata.json 自动生成                                   [0.5天]                        │
│   ├─ 5.2 CLI 工具 (cursor-rag status/inspect)                    [1天]                          │
│   └─ 5.3 Web Dashboard 基础版                                     [1.5天]                        │
│                                                                                                  │
│   Phase 6: 测试与优化 (2天)                                                                      │
│   ├─ 6.1 集成测试                                                 [1天]                          │
│   └─ 6.2 性能优化                                                 [1天]                          │
│                                                                                                  │
│   ───────────────────────────────────────────────────────────────────────────                   │
│   总计: 17 个工作日                                                                              │
│                                                                                                  │
│   里程碑:                                                                                        │
│   ├─ M1 (Phase 1-2): 核心 RAG 引擎可用                           [7天]                          │
│   ├─ M2 (Phase 3-4): MCP 集成完成,可在 Cursor 中使用             [12天]                         │
│   └─ M3 (Phase 5-6): 可视化和优化完成                             [17天]                         │
│                                                                                                  │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

八、总结

本架构设计了一个完整的 Expert RAG Workflow System,包含:

模块 功能 技术选型
存储层 向量存储 + 持久化 Orama + JSON
Embedding 层 文本向量化 Transformers.js
核心层 专家管理 + RAG 检索 + Session 管理 TypeScript
MCP 层 Cursor 集成 MCP SDK
可视化层 CLI + Web Dashboard Node.js + Vue3

核心特点:

  • 零原生依赖 - 跨平台无编译问题
  • 上下文最优 - Session 缓存 + 增量检索
  • 知识优先级 - LOCAL > REINFORCE > SKILL
  • Fallback 机制 - RAG → 关键词 → 文件 → 空
  • 可视化管理 - CLI + Web Dashboard

npm发布报错急救手册:快速解决2FA与令牌问题

作者 登山者
2025年12月12日 10:42

npm发布报错急救手册:快速解决2FA与令牌问题

刚刚执行 npm publish 却看到 "Two-factor authentication or granular access token with bypass 2fa enabled is required" 的红色报错?别担心,5分钟内让你恢复发布能力。

快速诊断:为什么突然不能发布了?

如果你最近两天突然遇到这个报错,根本原因是:npm 在2025年12月9日永久撤销了所有“经典令牌”

以前能用的令牌现在全部失效,必须使用新的自动化令牌粒度访问令牌来发布包。


🚀 5分钟紧急解决方案

方案A:个人开发者快速恢复发布(推荐)

如果你在个人电脑上发布自己的包,这是最快的方法:

  1. 创建自动化令牌

    # 登录 npm 网站,进入令牌管理
    # 访问:https://www.npmjs.com/settings/你的用户名/tokens
    
    # 或使用 CLI(需要 npm v10+)
    npm token create --type automation --read-only false
    
  2. 复制生成的令牌字符串(形如 npm_xxxxxx

  3. 配置到本地环境

    # 一次性使用(仅本次终端会话有效)
    npm config set //registry.npmjs.org/:_authToken=你的令牌字符串
    
    # 或永久保存到项目(推荐)
    echo "//registry.npmjs.org/:_authToken=你的令牌字符串" >> .npmrc
    
  4. 立即测试发布

    npm publish --dry-run  # 先试运行
    npm publish            # 实际发布
    

方案B:团队项目配置(包管理员操作)

如果你管理一个团队包,需要为成员配置权限:

  1. 创建粒度访问令牌

    • 访问 npm 网站令牌页面
    • 点击 "Generate New Token" → 选择 "Granular Access Token"
    • 配置权限时必须勾选:"Bypass 2FA"
  2. 分配精确权限

    # 示例:创建仅能发布特定包的令牌
    npm token create --granular \
      --package "团队包名" \
      --permissions publish \
      --bypass-2fa \
      --expiry 30d  # 30天有效期
    
  3. 安全分享给团队成员

    • 通过密码管理器分享
    • 或配置到团队 CI/CD 环境变量

方案C:GitHub Actions 自动发布修复

如果你的 CI/CD 突然失败,更新工作流配置:

# .github/workflows/publish.yml
name: Publish to npm
on:
  push:
    branches: [main]
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          registry-url: 'https://registry.npmjs.org/'
          
      - run: npm ci
      
      - name: Publish Package
        run: npm publish
        env:
          # 关键:使用新的自动化令牌
          NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTOMATION_TOKEN }}

在 GitHub 仓库设置中:

  1. 进入 Settings → Secrets and variables → Actions
  2. 更新 NPM_AUTOMATION_TOKEN 值为新创建的自动化令牌

🔍 故障排查对照表

症状 可能原因 立即解决
E401/E403 错误 令牌过期或无效 创建全新的自动化令牌
EPUBLISHFORBIDDEN 令牌权限不足 检查令牌是否有写权限
仍要求2FA验证 令牌未配置 bypass-2fa 确保使用自动化令牌或勾选 bypass-2fa
仅 CI/CD 失败 环境变量未更新 更新 CI 的 NODE_AUTH_TOKEN 密钥
所有命令都失败 npm 登录会话过期 运行 npm login 重新认证

快速检查命令:

# 1. 检查当前认证状态
npm whoami

# 2. 验证令牌权限
npm access ls-collaborators 你的包名

# 3. 检查本地配置
npm config get //registry.npmjs.org/:_authToken

# 4. 查看令牌类型(前4字符)
# npm_ = 自动化/粒度令牌 | npm_org = 组织令牌 | 其他 = 已失效

📚 理解新令牌体系(原理简介)

为什么必须改变?

旧的“经典令牌”一旦泄露就永久有效,安全隐患极大。新的令牌体系提供:

  1. 自动化令牌:CI/CD 专用,自动绕过 2FA,最长 90 天
  2. 粒度访问令牌:精细权限控制,可选 bypass-2fa
  3. 会话令牌npm login 生成,仅 2 小时有效

令牌类型对比

场景 推荐令牌类型 有效期 关键优势
本地开发 自动化令牌 90天 自动绕过2FA,一次配置长期使用
CI/CD 流水线 自动化令牌 90天 专为自动化优化,无需交互
团队协作 粒度访问令牌 自定义 精确权限控制,可 bypass-2fa
临时发布 npm login 会话 2小时 无需配置令牌,简单快捷

🛡️ 长期最佳实践

1. 令牌安全管理

# 定期列出所有令牌
npm token list

# 撤销不再需要的令牌
npm token revoke 令牌ID前10字符

# 设置日历提醒,每80天轮换令牌

2. 项目标准化配置

# 项目 .npmrc 示例
@scope:registry=https://registry.npmjs.org/
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
always-auth=true

# .env 文件(不要提交到Git!)
NPM_TOKEN=npm_你的自动化令牌

3. 探索未来方案:可信发布

对于开源项目,考虑迁移到更安全的可信发布

  • 完全无需长期令牌
  • 通过 GitHub Actions OIDC 自动获取临时凭证
  • npm 官方推荐的新标准

总结与下一步

立即行动清单:

  1. ✅ 创建新的自动化令牌(个人)或粒度令牌(团队)
  2. ✅ 更新本地 .npmrc 或 CI/CD 环境变量
  3. ✅ 测试 npm publish --dry-run
  4. 🔄 设置令牌到期提醒(80天后)

长期策略:

  • 个人项目:使用自动化令牌 + 定期轮换
  • 团队项目:粒度访问令牌 + 权限审计
  • 开源项目:探索可信发布(Trusted Publishing)

这次变更虽然带来了短期的适配成本,但显著提升了整个 npm 生态系统的安全性。正确配置后,你的发布流程将更加健壮和安全。

本文基于 npm 2025年12月安全更新,适用于 npm v10+。如遇特殊问题,可查阅 npm 状态页面 获取最新信息。

【已解决】uni-textarea 无法绑定 v-model / 数据不回显?换原生 textarea 一招搞定!

作者 渔_
2025年12月12日 10:40

微信图片_20251212103855_623_10.jpg 在 uni-app 开发中,很多同学会遇到 <uni-textarea> 组件的坑:明明数据有值、v-model 绑定正确,但输入框就是不回显内容;甚至部分场景下 v-model 完全失效,只能输入却无法双向绑定。

试过重置样式、延迟渲染、更新 uni-ui 版本都没用?别折腾了!直接改用 uni-app 原生 <textarea> 组件,兼容性拉满,绑定和样式都能完全自定义。

问题复现

使用 <uni-textarea> 时,明明 value 有值、页面能打印出绑定的变量,但输入框内就是空白

<!-- 失效的 uni-textarea 写法 --> 
<uni-textarea v-model="content" placeholder="需求说明"></uni-textarea>
<text>数据存在:{{ content }}</text> <!-- 能打印,但输入框无内容 -->

解决方案:改用原生 textarea

直接替换为 uni-app 原生 <textarea>,样式完全自定义,v-model 绑定 100% 生效:

<template>
  <view class="textarea-container">
    <!-- 原生 textarea 替代 uni-textarea -->
    <textarea v-model="content" placeholder="请输入内容" class="native-textarea"
   adjust-position="true" maxlength="-1"></textarea>
    <!-- 验证数据绑定 -->
    <text class="verify-text">当前值:{{ content }}</text>
  </view>
</template>

<script>
export default {
  data() {
    return {
      // 支持大文本、换行、特殊字符,绑定即回显
      content: `需求文本`
    };
  }
};
</script>

<style scoped>
/* 容器:确保输入框有足够空间 */
.textarea-container {
  padding: 20rpx;
  box-sizing: border-box;
}

/* 原生 textarea 样式自定义 */
.native-textarea {
width: 70%;
height: 118rpx !important;
font-size: 20rpx;
border-radius: 8rpx;
resize: none;
box-sizing: border-box;
white-space: pre-wrap;
pointer-events: auto;
user-select: text;
position: relative;
z-index: 1;
}

/* 验证文本样式(可选) */
.verify-text {
  display: block;
  margin-top: 20rpx;
  font-size: 24rpx;
  color: #666;
}
</style>

核心优势

  1. 绑定 100% 生效:原生 <textarea> 完全兼容 uni-app 的 v-model 双向绑定,数据一改输入框立刻回显;
  2. 样式完全可控:摆脱 uni-textarea 内置样式的束缚,宽高、行高、边框等想怎么改就怎么改;
  3. 多端适配:兼容微信小程序、H5、App 等所有 uni-app 支持的端,无兼容性问题;
  4. 支持特殊格式white-space: pre-wrap 保留文本原有换行,大文本、特殊字符都能正常显示。

补充说明

1. 为什么 uni-textarea 会失效?

  • uni-ui 封装的 <uni-textarea> 底层用了绝对定位、多层嵌套,容易导致样式覆盖或渲染延迟;
  • 部分版本的 uni-ui 存在 v-model 语法糖兼容问题,即使数据绑定成功也无法渲染。

2. 原生 textarea 核心属性说明(解决赋值后无法编辑)

属性 作用
adjust-position 仅小程序端生效,软键盘弹出时自动调整输入框位置,修复赋值后聚焦失效、无法编辑的问题;
maxlength="-1" 解除小程序端默认 140 字长度限制,避免赋值内容超限时触发绑定校验异常,导致无法编辑;
pointer-events: auto 确保输入框可点击,避免全局样式污染导致的 “假不可编辑”;
user-select: text 允许文本选择,避免全局样式禁止选中文本导致的无法编辑;

3. 原生 textarea 进阶用法

(1)自定义 placeholder 样式

/* 自定义 placeholder 颜色 */
.native-textarea::placeholder {
  color: #999;
  font-size: 26rpx;
}

(2)限制输入长度

<textarea
  v-model="content"
  placeholder="需求说明"
  class="native-textarea"
  maxlength="2000" <!-- 限制最大输入长度,替换 -1 即可 -->
></textarea>

(3)Vue2 动态新增属性赋值(避免响应式丢失)

若绑定变量是对象属性(如 obj.content),新增属性时需用 Vue.set

import Vue from 'vue';
methods: {
  setObjContent() {
    // 给对象动态新增属性,保留响应式
    Vue.set(this.obj, 'content', '赋值内容');
  }
}

总结

如果你的 <uni-textarea> 遇到 v-model 绑定失效、数据不回显、样式无法修改,或改用原生 <textarea> 后赋值无法编辑等问题,按本文方案:

  1. 替换为原生 <textarea> 并添加 adjust-position="true" 和 maxlength="-1"
  2. 确保绑定变量提前初始化、赋值为字符串类型;
  3. 避免覆盖响应式对象、禁用全局样式干扰。

无需纠结 uni-ui 组件的兼容问题,简单直接又稳定!

vite+ts+monorepo从0搭建vue3组件库(五):vite打包组件库

作者 小胖霞
2025年12月12日 10:40

打包配置

vite 专门提供了库模式的打包方式,配置其实非常简单,首先全局安装 vite 以及@vitejs/plugin-vue

   pnpm add vite @vitejs/plugin-vue -D -w

在components下新建vite.config.ts。我们需要让打包后的结构和我们开发的结构一致,如下配置我们将打包后的文件放入dlx-ui 目录下,因为后续发布组件库的名字就是 dlx-ui,当然这个命名大家可以随意.具体代码在下方

然后在 components/package.json 添加打包命令scripts

 "scripts": {
    "build": "vite build"
  },

声明文件

到这里其实打包的组件库只能给 js 项目使用,在 ts 项目下运行会出现一些错误,而且使用的时候还会失去代码提示功能,这样的话我们就失去了用 ts 开发组件库的意义了。所以我们需要在打包的库里加入声明文件(.d.ts)。

全局安装vite-plugin-dts

pnpm add vite-plugin-dts -D -w

在vite.config.ts中引入,完整的配置文件如下:

// components/vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dts from 'vite-plugin-dts'
export default defineConfig({
  plugins: [
    vue(),
    dts({
      entryRoot: './src',
      outDir: ['../dlx-ui/es/src', '../dlx-ui/lib/src'],
      //指定使用的tsconfig.json为我们整个项目根目录下,如果不配置,你也可以在components下新建tsconfig.json
      tsconfigPath: '../../tsconfig.json',
    }),
  ],
  build: {
    //打包文件目录
    outDir: 'es',
    emptyOutDir: true,
    //压缩
    //minify: false,
    rollupOptions: {
      //忽略打包vue文件
      external: ['vue'],
      input: ['index.ts'],
      output: [
        {
          //打包格式
          format: 'es',
          //打包后文件名
          entryFileNames: '[name].mjs',
          //让打包目录和我们目录对应
          preserveModules: true,
          exports: 'named',
          //配置打包根目录
          dir: '../dlx-ui/es',
        },
        {
          //打包格式
          format: 'cjs',
          //打包后文件名
          entryFileNames: '[name].js',
          //让打包目录和我们目录对应
          preserveModules: true,
          exports: 'named',
          //配置打包根目录
          dir: '../dlx-ui/lib',
        },
      ],
    },
    lib: {
      entry: './index.ts',
    },
  },
})

执行pnpm run build打包,出现了我们需要的声明的文件

image.png

可以看到打包时打包了2种模式,一种是es模式,一种是cjs模式,当用户引入组件库时使用哪种呢?我们可以修改/components/package.json的代码:

  • main: 指向 lib/index.js,这是 CommonJS 模块的入口文件。Node.js 环境和不支持 ES 模块的工具会使用这个文件。
  • module: 指向 es/index.mjs,这是 ES 模块的入口文件。现代前端工具(如 Vite)会优先使用这个文件。
  "main": "lib/index.js", // CommonJS 入口文件
  "module": "es/index.mjs", // ES 模块入口文件

但是此时的所有样式文件还是会统一打包到 style.css 中,还是不能进行样式的按需加载,所以接下来我们将让 vite 不打包样式文件,样式文件后续单独进行打包。后面我们要做的则是让样式文件也支持按需引入,敬请期待。

vite+ts+monorepo从0搭建vue3组件库(四):button组件开发

作者 小胖霞
2025年12月12日 10:39

组件属性

button组件接收以下属性

  • type 类型
  • size 尺寸
  • plain 朴素按钮
  • round 圆角按钮
  • circle 圆形按钮
  • loading 加载
  • disabled禁用
  • text 文字

button组件全部代码如下:

// button.vue
<template>
  <button
    class="dlx-button"
    :class="[
      buttonSize ? `dlx-button--${buttonSize}` : '',
      buttonType ? `dlx-button--${buttonType}` : '',
      {
        'is-plain': plain,
        'is-round': round,
        'is-circle': circle,
        'is-disabled': disabled,
        'is-loading': loading,
        'is-text': text,
        'is-link': link,
      },
    ]"
    :disabled="disabled || loading"
    @click="handleClick"
  >
    <span v-if="loading" class="dlx-button__loading">
      <span class="dlx-button__loading-spinner"></span>
    </span>
    <span class="dlx-button__content">
      <slot></slot>
    </span>
  </button>
</template>

<script lang="ts" setup>
import { computed } from 'vue'

defineOptions({
  name: 'DlxButton',
})

const props = defineProps({
  // 按钮类型
  type: {
    type: String,
    values: ['primary', 'success', 'warning', 'danger', 'info'],
    default: '',
  },
  // 按钮尺寸
  size: {
    type: String,
    values: ['large', 'small'],
    default: '',
  },
  // 是否为朴素按钮
  plain: {
    type: Boolean,
    default: false,
  },
  // 是否为圆角按钮
  round: {
    type: Boolean,
    default: false,
  },
  // 是否为圆形按钮
  circle: {
    type: Boolean,
    default: false,
  },
  // 是否为加载中状态
  loading: {
    type: Boolean,
    default: false,
  },
  // 是否禁用
  disabled: {
    type: Boolean,
    default: false,
  },
  // 是否为文字按钮
  text: {
    type: Boolean,
    default: false,
  },
  // 是否为链接按钮
  link: {
    type: Boolean,
    default: false,
  },
})

const buttonSize = computed(() => props.size)
const buttonType = computed(() => props.type)

const handleClick = (evt: MouseEvent) => {
  if (props.disabled || props.loading) return
  emit('click', evt)
}

const emit = defineEmits(['click'])
</script>

<style lang="less" scoped>
.dlx-button {
  display: inline-flex;
  justify-content: center;
  align-items: center;
  line-height: 1;
  height: 32px;
  white-space: nowrap;
  cursor: pointer;
  color: #606266;
  text-align: center;
  box-sizing: border-box;
  outline: none;
  transition: 0.1s;
  font-weight: 500;
  padding: 8px 15px;
  font-size: 14px;
  border-radius: 4px;
  background-color: #fff;
  border: 1px solid #dcdfe6;

  &:hover,
  &:focus {
    color: #409eff;
    border-color: #c6e2ff;
    background-color: #ecf5ff;
  }

  &:active {
    color: #3a8ee6;
    border-color: #3a8ee6;
    outline: none;
  }

  // 主要按钮
  &--primary {
    color: #fff;
    background-color: #409eff;
    border-color: #409eff;

    &:hover,
    &:focus {
      background: #66b1ff;
      border-color: #66b1ff;
      color: #fff;
    }

    &:active {
      background: #3a8ee6;
      border-color: #3a8ee6;
      color: #fff;
    }
  }

  // 成功按钮
  &--success {
    color: #fff;
    background-color: #67c23a;
    border-color: #67c23a;

    &:hover,
    &:focus {
      background: #85ce61;
      border-color: #85ce61;
      color: #fff;
    }

    &:active {
      background: #5daf34;
      border-color: #5daf34;
      color: #fff;
    }
  }

  // 警告按钮
  &--warning {
    color: #fff;
    background-color: #e6a23c;
    border-color: #e6a23c;

    &:hover,
    &:focus {
      background: #ebb563;
      border-color: #ebb563;
      color: #fff;
    }

    &:active {
      background: #cf9236;
      border-color: #cf9236;
      color: #fff;
    }
  }

  // 危险按钮
  &--danger {
    color: #fff;
    background-color: #f56c6c;
    border-color: #f56c6c;

    &:hover,
    &:focus {
      background: #f78989;
      border-color: #f78989;
      color: #fff;
    }

    &:active {
      background: #dd6161;
      border-color: #dd6161;
      color: #fff;
    }
  }

  // 信息按钮
  &--info {
    color: #fff;
    background-color: #909399;
    border-color: #909399;

    &:hover,
    &:focus {
      background: #a6a9ad;
      border-color: #a6a9ad;
      color: #fff;
    }

    &:active {
      background: #82848a;
      border-color: #82848a;
      color: #fff;
    }
  }

  // 大尺寸
  &--large {
    height: 40px;
    padding: 12px 19px;
    font-size: 14px;
    border-radius: 4px;
  }

  // 小尺寸
  &--small {
    height: 24px;
    padding: 5px 11px;
    font-size: 12px;
    border-radius: 3px;
  }

  // 朴素按钮
  &.is-plain {
    background: #fff;

    // 不同类型按钮的默认状态
    &.dlx-button--primary {
      color: #409eff;
      border-color: #409eff;
    }

    &.dlx-button--success {
      color: #67c23a;
      border-color: #67c23a;
    }

    &.dlx-button--warning {
      color: #e6a23c;
      border-color: #e6a23c;
    }

    &.dlx-button--danger {
      color: #f56c6c;
      border-color: #f56c6c;
    }

    &.dlx-button--info {
      color: #909399;
      border-color: #909399;
    }

    &:hover,
    &:focus {
      background: #ecf5ff;
      border-color: #409eff;
      color: #409eff;
    }

    &:active {
      background: #ecf5ff;
      border-color: #3a8ee6;
      color: #3a8ee6;
    }

    // 为不同类型的朴素按钮添加对应的悬浮状态
    &.dlx-button--primary {
      &:hover,
      &:focus {
        background: #ecf5ff;
        border-color: #409eff;
        color: #409eff;
      }
      &:active {
        border-color: #3a8ee6;
        color: #3a8ee6;
      }
    }

    &.dlx-button--success {
      &:hover,
      &:focus {
        background: #f0f9eb;
        border-color: #67c23a;
        color: #67c23a;
      }
      &:active {
        border-color: #5daf34;
        color: #5daf34;
      }
    }

    &.dlx-button--warning {
      &:hover,
      &:focus {
        background: #fdf6ec;
        border-color: #e6a23c;
        color: #e6a23c;
      }
      &:active {
        border-color: #cf9236;
        color: #cf9236;
      }
    }

    &.dlx-button--danger {
      &:hover,
      &:focus {
        background: #fef0f0;
        border-color: #f56c6c;
        color: #f56c6c;
      }
      &:active {
        border-color: #dd6161;
        color: #dd6161;
      }
    }

    &.dlx-button--info {
      &:hover,
      &:focus {
        background: #f4f4f5;
        border-color: #909399;
        color: #909399;
      }
      &:active {
        border-color: #82848a;
        color: #82848a;
      }
    }
  }

  // 圆角按钮
  &.is-round {
    border-radius: 20px;
  }

  // 圆形按钮
  &.is-circle {
    border-radius: 50%;
    padding: 8px;
  }

  // 文字按钮
  &.is-text {
    border-color: transparent;
    background: transparent;
    padding-left: 0;
    padding-right: 0;

    &:not(.is-disabled) {
      // 默认文字按钮
      color: #409eff;

      &:hover,
      &:focus {
        color: #66b1ff;
        background-color: transparent;
        border-color: transparent;
      }

      &:active {
        color: #3a8ee6;
      }

      // 不同类型的文字按钮颜色
      &.dlx-button--primary {
        color: #409eff;
        &:hover,
        &:focus {
          color: #66b1ff;
        }
        &:active {
          color: #3a8ee6;
        }
      }

      &.dlx-button--success {
        color: #67c23a;
        &:hover,
        &:focus {
          color: #85ce61;
        }
        &:active {
          color: #5daf34;
        }
      }

      &.dlx-button--warning {
        color: #e6a23c;
        &:hover,
        &:focus {
          color: #ebb563;
        }
        &:active {
          color: #cf9236;
        }
      }

      &.dlx-button--danger {
        color: #f56c6c;
        &:hover,
        &:focus {
          color: #f78989;
        }
        &:active {
          color: #dd6161;
        }
      }

      &.dlx-button--info {
        color: #909399;
        &:hover,
        &:focus {
          color: #a6a9ad;
        }
        &:active {
          color: #82848a;
        }
      }
    }

    // 文字按钮的禁用状态
    &.is-disabled {
      color: #c0c4cc;
    }
  }

  // 链接按钮
  &.is-link {
    border-color: transparent;
    color: #409eff;
    background: transparent;
    padding-left: 0;
    padding-right: 0;

    &:hover,
    &:focus {
      color: #66b1ff;
    }

    &:active {
      color: #3a8ee6;
    }
  }

  // 禁用状态
  &.is-disabled {
    &,
    &:hover,
    &:focus,
    &:active {
      cursor: not-allowed;

      // 普通按钮的禁用样式
      &:not(.is-text):not(.is-link) {
        background-color: #fff;
        border-color: #dcdfe6;
        color: #c0c4cc;

        // 有颜色的按钮的禁用样式
        &.dlx-button--primary {
          background-color: #a0cfff;
          border-color: #a0cfff;
          color: #fff;
        }

        &.dlx-button--success {
          background-color: #b3e19d;
          border-color: #b3e19d;
          color: #fff;
        }

        &.dlx-button--warning {
          background-color: #f3d19e;
          border-color: #f3d19e;
          color: #fff;
        }

        &.dlx-button--danger {
          background-color: #fab6b6;
          border-color: #fab6b6;
          color: #fff;
        }

        &.dlx-button--info {
          background-color: #c8c9cc;
          border-color: #c8c9cc;
          color: #fff;
        }
      }
    }
  }

  // 有颜色的按钮禁用状态 - 直接选择器
  &.is-disabled.dlx-button--primary {
    background-color: #a0cfff;
    border-color: #a0cfff;
    color: #fff;
  }

  &.is-disabled.dlx-button--success {
    background-color: #b3e19d;
    border-color: #b3e19d;
    color: #fff;
  }

  &.is-disabled.dlx-button--warning {
    background-color: #f3d19e;
    border-color: #f3d19e;
    color: #fff;
  }

  &.is-disabled.dlx-button--danger {
    background-color: #fab6b6;
    border-color: #fab6b6;
    color: #fff;
  }

  &.is-disabled.dlx-button--info {
    background-color: #c8c9cc;
    border-color: #c8c9cc;
    color: #fff;
  }

  // 文字按钮禁用状态
  &.is-disabled.is-text {
    background-color: transparent;
    border-color: transparent;
    color: #c0c4cc;
  }

  // 链接按钮禁用状态
  &.is-disabled.is-link {
    background-color: transparent;
    border-color: transparent;
    color: #c0c4cc;
  }

  // 加载状态
  &.is-loading {
    position: relative;
    pointer-events: none;

    &:before {
      pointer-events: none;
      content: '';
      position: absolute;
      left: -1px;
      top: -1px;
      right: -1px;
      bottom: -1px;
      border-radius: inherit;
      background-color: rgba(255, 255, 255, 0.35);
    }
  }

  .dlx-button__loading {
    display: inline-flex;
    align-items: center;
    margin-right: 4px;
  }

  .dlx-button__loading-spinner {
    display: inline-block;
    width: 14px;
    height: 14px;
    border: 2px solid #fff;
    border-radius: 50%;
    border-top-color: transparent;
    animation: button-loading 1s infinite linear;
  }
}

@keyframes button-loading {
  0% {
    transform: rotate(0);
  }
  100% {
    transform: rotate(360deg);
  }
}
</style>

引用

在play的src下新建example,存放各个组件的代码,先在play下安装vue-router

pnpm i vue-router

目录结构如下

image.png

app.vue如下:

<template>
  <div class="app-container">
    <div class="sidebar">
      <h2 class="sidebar-title">组件列表</h2>
      <ul class="menu-list">
        <li
          v-for="item in menuItems"
          :key="item.path"
          :class="{ active: currentPath === item.path }"
          @click="handleMenuClick(item.path)"
        >
          {{ item.name }}
        </li>
      </ul>
    </div>
    <div class="content">
      <router-view></router-view>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'

const router = useRouter()
const currentPath = ref('/button')

const menuItems = [
  { name: 'Button 按钮', path: '/button' },
  // 后续添加其他组件...
]

const handleMenuClick = (path: string) => {
  currentPath.value = path
  router.push(path)
}
</script>

<style scoped>
.app-container {
  display: flex;
  min-height: 100vh;
}

.sidebar {
  width: 240px;
  background-color: #f5f7fa;
  border-right: 1px solid #e4e7ed;
  padding: 20px 0;
}

.sidebar-title {
  padding: 0 20px;
  margin: 0 0 20px;
  font-size: 18px;
  color: #303133;
}

.menu-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.menu-list li {
  padding: 12px 20px;
  cursor: pointer;
  color: #303133;
  font-size: 14px;
  transition: all 0.3s;
}

.menu-list li:hover {
  color: #409eff;
  background-color: #ecf5ff;
}

.menu-list li.active {
  color: #409eff;
  background-color: #ecf5ff;
}

.content {
  flex: 1;
  padding: 20px;
}
</style>

router/index.ts如下:

import { createRouter, createWebHistory } from 'vue-router'
import ButtonExample from '../example/button.vue'

const routes = [
  {
    path: '/',
    redirect: '/button',
  },
  {
    path: '/button',
    component: ButtonExample,
  },
]

const router = createRouter({
  history: createWebHistory(),
  routes,
})

export default router

play下执行pnpm run dev

运行效果:

image.png

❌
❌