普通视图

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

十多道啫啫煲和砂锅菜,让你的冬日吃出汗

作者 潘誉晗
2025年12月20日 20:17
冬日,我常用的锅进入冬天后,我喜欢一锅端。冒着热气的菜肴就这么端上桌,满满一大盆,特别好。有天清点家里的锅具,发现为了做这些菜,家里竟然准备了17口锅——并且我又看中了一款新的土锅……珐琅锅我有8口珐 ...查看全文

本文为会员文章,出自《单篇文章》,订阅后可阅读全文。

今日头条发布深度内容扶持计划

2025年12月20日 19:37
36氪获悉,12月20日,在今日头条2025优质深度创作者大会上,今日头条发布“专项创作基金”“深一度工作室”“薪火计划”三大扶持计划,持续加码深度内容创作。今日头条作者与内容生态负责人李黎在大会主题演讲中指出,今日头条选择回归初心,将“优质深度”确立为2025年核心平台内容战略。

重庆市国资委:要进一步加强市属重点国企及其子企业参股股权管理

2025年12月20日 17:37
36氪获悉,据“重庆国资”公众号消息,12月19日下午,重庆市国资委党委召开会议。会议强调,要全面降低企业融资成本,常态化开展债务债权清偿清收,加大力度除险清患,推动各类风险实现常态化管控化解。要进一步加强市属重点国企及其子企业参股股权管理,切实维护国有资产安全,提高国有资本配置效率。要提质扩面推进清廉国企建设,认真总结提炼最佳案例和优秀实践案例做法,加强推广应用,切实提升建设质效。

【Gemini简直无敌了】掌间星河:通过MediaPipe实现手势控制粒子

2025年12月20日 17:15

受掘金大佬“不如摸鱼去”的启发,我也试试 Gemini 3做一下手势+粒子交互, 确实学到了不少东西,在这里简单的分享下。 github地址:掌间星河:github.com/huijieya/Ge…

在这里插入图片描述

基于原生h5、浏览器、PC摄像头实现手势控制粒子特效交互的逻辑,粒子默认离散,类似银河系分布缓慢移动,同时有5种手势:

手势1: 握拳,握拳后粒子聚拢显示爱心的形状

手势2: 展开手掌并挥手,展开手掌挥手后粒子从当前状态恢复到离散状态

手势3: 👆 比 1 :只有食指伸直,其他 3 根弯曲,此时粒子聚拢显示第一句话:春来夏往

手势4: ✌ 比 2 :只有食指和中指伸直,其他 2 根弯曲手此时粒子聚拢显示第二句话: 秋收冬藏

手势5: 👌 比 3 :只有中指、无名指、小指伸直,食指弯曲,此时粒子聚拢显示第三句话:我们来日方长

效果展示

在这里插入图片描述

源码地址

掌间星河:github.com/huijieya/Ge…

源码分析

手势识别流程

1. 手部检测初始化

// HandTracker.tsx 中初始化 MediaPipe Hands
const hands = new (window as any).Hands({
  locateFile: (file: string) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`
});

hands.setOptions({
  maxNumHands: 1,           // 最多检测一只手
  modelComplexity: 1,       // 模型复杂度
  minDetectionConfidence: 0.7,  // 最小检测置信度
  minTrackingConfidence: 0.7    // 最小跟踪置信度
});

2. 手部关键点获取

MediaPipe 会检测手部的21个关键点(landmarks),每个关键点包含 x, y, z 坐标:

  • 指尖: 4(拇指), 8(食指), 12(中指), 16(无名指), 20(小指)
  • 指关节: 用于判断手指是否伸直

3. 手势分类逻辑

// gestureLogic.ts 中的 classifyGesture 函数
const isExtended = (tipIdx: number, mcpIdx: number) => landmarks[tipIdx].y < landmarks[mcpIdx].y;

// 判断各手指是否伸直
const indexExt = isExtended(8, 5);   // 食指
const middleExt = isExtended(12, 9); // 中指
const ringExt = isExtended(16, 13);  // 无名指
const pinkyExt = isExtended(20, 17); // 小指

// 根据手指状态识别不同手势
if (!indexExt && !middleExt && !ringExt && !pinkyExt) {
  return GestureType.HEART; // 握拳 - 显示爱心
}
if (indexExt && middleExt && ringExt && pinkyExt) {
  return GestureType.GALAXY; // 手掌展开 - 银河状态
}
// ... 其他手势判断

粒子绘制机制

1. 粒子系统初始化

// ParticleCanvas.tsx 中初始化粒子
useEffect(() => {
  const particles: Particle[] = [];
  for (let i = 0; i < PARTICLE_COUNT; i++) {
    particles.push({
      x: Math.random() * window.innerWidth,     // 随机初始位置
      y: Math.random() * window.innerHeight,
      targetX: Math.random() * window.innerWidth, // 目标位置
      targetY: Math.random() * window.innerHeight,
      vx: 0,                                    // 速度
      vy: 0,
      size: Math.random() * 1.5 + 0.5,          // 大小
      color: COLORS[Math.floor(Math.random() * COLORS.length)], // 颜色
      alpha: Math.random() * 0.4 + 0.4,         // 透明度
    });
  }
  particlesRef.current = particles;
}, []);

2. 形状生成算法

const getShapePoints = (type: GestureType, width: number, height: number): Point[] => {
  const centerX = width / 2;
  const centerY = height / 2;
  
  switch (type) {
    case GestureType.HEART: 
      // 心形方程参数化生成点
      // x = 16sin³(t)
      // y = 13cos(t) - 5cos(2t) - 2cos(3t) - cos(4t)
      
    case GestureType.TEXT_1/2/3:
      // 使用 Canvas 绘制文字并提取像素点
      
    case GestureType.GALAXY:
    default:
      // 螺旋银河形状
      const angle = Math.random() * Math.PI * 2;
      const r = Math.pow(Math.random(), 0.7) * maxRadius;
      const spiralFactor = 2.0;
      const offset = r * (spiralFactor / maxRadius) * 5;
      points.push({ 
        x: centerX + Math.cos(angle + offset) * r, 
        y: centerY + Math.sin(angle + offset) * r 
      });
  }
}

3. 粒子动画更新

// 粒子运动和渲染循环
const render = () => {
  // 半透明背景覆盖产生拖尾效果
  ctx.fillStyle = 'rgba(0, 0, 0, 0.18)';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  
  particlesRef.current.forEach((p) => {
    // 平滑插值移动到目标位置
    p.x += (tx - p.x) * LERP_FACTOR;
    p.y += (ty - p.y) * LERP_FACTOR;
    
    // 手势交互影响粒子位置
    if (hPos && canInteract) {
      const dx = p.x - hPos.x;
      const dy = p.y - hPos.y;
      const distSq = dx * dx + dy * dy;
      if (distSq < INTERACTION_RADIUS * INTERACTION_RADIUS) {
        // 排斥力计算
        const dist = Math.sqrt(distSq);
        const force = (1 - dist / INTERACTION_RADIUS) * INTERACTION_STRENGTH;
        p.x += dx * force;
        p.y += dy * force;
      }
    }
    
    // 添加随机扰动使粒子更生动
    p.x += (Math.random() - 0.5) * 0.6;
    p.y += (Math.random() - 0.5) * 0.6;
    
    // 绘制粒子
    ctx.fillStyle = p.color;
    ctx.globalAlpha = p.alpha;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
    ctx.fill();
  });
  
  animationRef.current = requestAnimationFrame(render);
};

4. 银河旋转效果

// 银河状态下的旋转动画
galaxyAngleRef.current += GALAXY_ROTATION_SPEED;
const cosA = Math.cos(galaxyAngleRef.current);
const sinA = Math.sin(galaxyAngleRef.current);

// 对每个粒子应用旋转变换
const dx = p.targetX - cx;
const dy = p.targetY - cy;
tx = cx + dx * cosA - dy * sinA;
ty = cy + dx * sinA + dy * cosA;

脚手架开发工具——dotenv

作者 code_YuJun
2025年12月20日 16:58

简介

dotenv 是一个轻量级的 Node.js 环境变量管理工具,其核心作用是:从项目根目录的 .env 文件中加载自定义的环境变量,并将它们注入到 Node.js 的 process.env 对象中,使得我们可以在项目代码中统一通过 process.env.XXX 的方式获取这些环境配置,无需手动在系统环境中配置临时变量或永久变量。

核心工作原理

  1. 当在项目中引入并执行 dotenv 时,它会自动查找项目根目录下的 .env 文件(该文件为纯文本格式,采用键值对配置);
  2. 它会解析 .env 文件中的每一行配置(格式通常为 KEY=VALUE);
  3. 将解析后的键值对逐一挂载到 Node.js 内置的 process.env 对象上(process.env 原本用于存储系统级环境变量,dotenv 为其扩展了项目自定义环境变量);
  4. 之后在项目的任意代码文件中,都可以通过 process.env.KEY 的形式获取对应的值。

使用示例

npm install dotenv --save
# .env 文件内容 当前用户路径下创建 `.env` 文件
PORT=3000
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=123456
API_KEY=abcdefg123456
    const dotenv = require('dotenv');
    const dotenvPath = path.resolve(userHome, '.env'); // /Users/***/.env
    if (pathExists(dotenvPath)) {
        dotenv.config({
            path: dotenvPath,
        });
    }

美国一法院放行马斯克原560亿美元薪酬方案

2025年12月20日 16:56
美国特拉华州最高法院19日裁定,特斯拉公司首席执行官埃隆·马斯克原总值560亿美元的薪酬方案遭下级法院取消的决定太过极端,该方案必须恢复。 特拉华州最高法院指出,下级法院、特拉华州衡平法院并未给特斯拉机会解释公正薪酬理应如何。(新华社)

付鹏:2026年将是AI应用的“证伪之年”,巨头们需证明自己

2025年12月20日 16:48
知名经济学家付鹏表示,当前AI领域的关键拷问在于“高速公路修好了(算力基建),有没有车跑(应用落地)”。明年市场的核心在于验证AI能否从生产力向生产关系传导,其中特斯拉将是重要观察对象——它究竟是被估值为一家车企,还是能通过FSD证明自己是真正的重AI应用。(华尔街见闻)

vscode没有js提示:配置jsconfig配置

作者 Mr_Swilder
2025年12月20日 16:44

jsconfig.json是一个配置文件,它的核心作用是告诉 Visual Studio Code(VSCode)当前目录是一个 JavaScript 项目的根目录,从而为你的代码提供更强大的智能感知(IntelliSense)和语言支持。

下面这个表格概括了它的主要作用:

核心作用 解决的问题 简单示例
定义项目上下文 没有它时,VSCode 将每个 JS 文件视为独立单元,文件间缺乏关联性。有了它,VSCode 能将整个项目作为一个整体理解。 {}(一个空文件即可定义项目)
配置路径别名映射 当项目使用像 @这样的别名来代表 src目录时,VSCode 默认无法识别。配置后,可以实现路径的自动补全和点击跳转 "paths": { "@/*": ["src/*"] }
提升 IDE 性能 通过排除不必要的文件(如 node_modules, dist),让语言服务专注于源代码,避免 IntelliSense 变慢 "exclude": ["node_modules", "dist"]
调整语言服务选项 配置 JavaScript 的语言检查标准,例如启用实验性语法支持(如装饰器)或指定 ECMAScript 目标版本。 "experimentalDecorators": true

💡 详细解读与配置

  • 定义项目上下文:在没有 jsconfig.json的“文件范围(File Scope)”模式下,VSCode 虽然能为单个文件提供基础语法高亮,但难以准确分析文件之间的模块引用关系。创建 jsconfig.json后,项目进入“显式项目(Explicit Project)”模式,VSCode 的语言服务能理解项目的整体结构,从而提供更精确的代码补全、类型推断和错误检查。

  • 配置路径映射(Paths Mapping) :这是在前端项目中非常实用的功能。许多项目使用 Webpack 或 Vite 等构建工具配置了路径别名,但在代码编辑器中,这些别名默认无法被识别。通过在 jsconfig.json中配置 paths,即可让 VSCode 理解这些别名。

    {
      "compilerOptions": {
        "baseUrl": "./", // 设置基础目录
        "paths": {
          "@/*": ["src/*"],    // 将 @ 映射到 src 目录
          "components/*": ["src/components/*"] // 配置其他别名
        }
      }
    }
    

    配置后,当你输入 import App from '@/App',VSCode 就能知道 @指向 src目录,并提供自动补全和跳转功能。

  • 优化性能(Exclude) :JavaScript 语言服务会分析项目中的文件来提供 IntelliSense。如果它去解析庞大的 node_modules或构建输出的 dist目录,会严重拖慢速度。使用 exclude属性可以告诉语言服务忽略这些目录。

    {
      "exclude": ["node_modules", "dist", "build", "*.min.js"]
    }
    

🛠️ 创建与配置示例

你可以在项目的根目录下创建一个名为 jsconfig.json的文件。一个适用于现代前端项目(如 Vue、React)的常见配置如下:

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "node",
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    },
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "lib": ["esnext", "dom", "dom.iterable"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "build"]
}

配置项说明

  • compilerOptions:虽然名字叫“编译选项”,但它主要用于配置 VSCode 的 JavaScript 语言服务行为,因为 jsconfig.json源自 TypeScript 的 tsconfig.json
  • include:明确指定哪些文件属于项目。如果未设置,则默认包含所有子目录下的文件。
  • exclude:指定要排除的文件和文件夹。

💎 总结

总而言之,jsconfig.json就像是你在 VSCode 中的项目地图和说明书。它通过定义项目根目录、映射路径别名、排除无关文件等方式,显著提升了代码编辑的智能体验、导航效率和整体性能。对于任何有一定规模的 JavaScript/TypeScript 项目,配置一个 jsconfig.json都是非常值得的。

希望这些信息能帮助你更好地理解和使用 jsconfig.json。如果你在配置过程中遇到具体问题,例如如何为特定框架进行优化,我很乐意提供进一步的帮助。

Koa 源码深度解析:带你理解 Koa 的设计哲学和核心实现原理

2025年12月20日 16:42

Koa 源码深度解析:带你理解 Koa 的设计哲学和核心实现原理

注:本文只讲koa源码与核心实现,无应用层相关知识

一、Koa 的设计哲学

1.1 什么是koa

Koa 是由 Express 原班人马打造的下一代 Node.js Web 框架。相比于 Express,Koa 利用 ES2017 的 async/await 特性,让异步代码的编写变得更加优雅和可维护。本文将深入解析 Koa 的核心源码,帮助你理解其设计哲学和实现原理。

1.2 核心设计理念

Koa 的设计理念可以概括为:

// Koa 应用本质上是一个包含中间件函数数组的对象
// 这些中间件以类似栈的方式组合和执行
const Koa = require('koa');
const app = new Koa();

// 中间件以"洋葱模型"方式执行
app.use(async (ctx, next) => {
  // 请求阶段(向下)
  await next();
  // 响应阶段(向上)
});

官方文档这样描述:

A Koa application is an object containing an array of middleware functions which are composed and executed in a stack-like manner upon request.

二、源码核心流程解析

在深入源码之前,我们先理解一下 Koa 应用从创建到处理请求的完整生命周期。一个典型的 Koa 应用会经历以下几个阶段:

  1. 创建应用实例 - new Koa() 初始化应用对象
  2. 注册中间件 - app.use() 将中间件函数添加到数组
  3. 启动监听 - app.listen() 创建 HTTP 服务并开始监听
  4. 处理请求 - 当请求到来时,组合中间件并执行

接下来我们逐步剖析每个阶段的源码实现。

2.1 创建Application

当我们执行 const app = new Koa() 时,Koa 内部做了哪些初始化工作呢?让我们看看 Application 类的构造函数:

class Application {

  constructor (options) {
    ......
    options = options || {}
    this.compose = options.compose || compose // 组合中间件的函数,这是实现洋葱模型的关键
    this.middleware = []                      // 中间件数组,所有通过 use() 注册的中间件都会存储在这里
    this.context = Object.create(context)     // 上下文对象的原型,每个请求会基于它创建独立的 ctx
    this.request = Object.create(request)     // 请求对象的原型,封装了对 Node.js 原生 req 的访问
    this.response = Object.create(response);  // 响应对象的原型,封装了对 Node.js 原生 res 的访问
    ......
   }

为什么使用 Object.create() 而不是直接赋值?

这是一个非常巧妙的设计。使用 Object.create() 创建原型链,意味着:

  • 每个应用实例都有自己独立的 contextrequestresponse 对象
  • 这些对象继承自共享的原型,既节省内存又保证了隔离性
  • 可以在不同应用实例上挂载不同的扩展属性,互不影响

2.2 注册中间件

中间件是 Koa 的核心概念。通过 app.use() 方法,我们可以注册各种中间件来处理请求。让我们看一个实际例子:

// 请求日志中间件:记录请求的 URL 和响应时间
const logMiddleware = async (ctx, next) => {
  const start = Date.now();  // 记录开始时间
  await next();              // 等待后续中间件执行完毕
  const end = Date.now();    // 记录结束时间
  console.log(`${ctx.method} ${ctx.url} - ${end - start}ms`);
};

app.use(logMiddleware);

class Application {
  use(fn) {
    // 注册中间件:将中间件函数添加到数组末尾
    this.middleware.push(fn);
    return this; // 返回 this 支持链式调用
  }
}

use() 方法的设计亮点:

  1. 简单直接 - 只是将中间件函数 push 到数组,没有复杂的逻辑
  2. 链式调用 - 返回 this 使得可以连续调用 app.use().use().use()
  3. 顺序敏感 - 中间件的执行顺序取决于注册顺序,这对理解洋葱模型很重要

注意上面的日志中间件示例:await next() 是一个分水岭,它将中间件分为"请求阶段"和"响应阶段"。这正是洋葱模型的精髓所在。

2.3 创建context

每当有新的 HTTP 请求到来时,Koa 都会为这个请求创建一个全新的 context 对象。这个对象是 Koa 最重要的创新之一,它封装了 Node.js 原生的 reqres,提供了更加便捷的 API。

createContext(req, res) {
  // 基于应用的 context 原型创建新的 context 实例
  const context = Object.create(this.context);
  // 基于应用的 request 和 response 原型创建新的实例
  const request = context.request = Object.create(this.request);
  const response = context.response = Object.create(this.response);

  // 建立各对象之间的引用关系
  context.app = request.app = response.app = this;        // 都持有 app 实例的引用
  context.req = request.req = response.req = req;         // 都持有 Node.js 原生 req 的引用
  context.res = request.res = response.res = res;         // 都持有 Node.js 原生 res 的引用

  // 建立 context、request、response 之间的相互引用
  request.ctx = response.ctx = context;                   // request 和 response 都能访问 context
  request.response = response;                            // request 能访问 response
  response.request = request;                             // response 能访问 request

  return context;
}

这个方法的精妙之处:

  1. 原型继承 - 使用 Object.create() 确保每个请求都有独立的 context,但共享原型上的方法
  2. 四层封装 - contextrequest/responsereq/res,逐层抽象,提供更优雅的 API
  3. 相互引用 - 建立了复杂但合理的引用关系,使得在任何层级都能方便地访问其他对象
  4. 内存优化 - 通过原型链共享方法,避免每个请求都创建重复的方法副本

这样设计的好处是,在中间件中我们可以灵活地访问:

  • ctx.req / ctx.res - 访问 Node.js 原生对象
  • ctx.request / ctx.response - 访问 Koa 封装的对象
  • ctx.body / ctx.status - 使用 Koa 的便捷属性(代理到 response)

2.4 启动监听服务

当所有中间件注册完成后,我们需要启动 HTTP 服务器开始监听请求。

// 用户代码:启动服务器
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

// Koa 内部实现
class Application {
  listen (...args) {
    debug('listen')
    // 创建 Node.js HTTP 服务器,传入 callback 作为请求处理函数
    const server = http.createServer(this.callback())
    return server.listen(...args) // 返回 Node.js 的 http.Server 实例
  }

  // callback 方法返回一个符合 Node.js http.createServer 要求的请求处理函数
  callback() {
    // ⭐️ 核心:使用 compose 将所有中间件组合成一个函数
    // 这就是洋葱模型的实现入口!
    const fn = compose(this.middleware);

    // 如果没有监听 error 事件,添加默认的错误处理器
    if (!this.listenerCount('error')) this.on('error', this.onerror);

    // 返回请求处理函数,Node.js 会在每次请求到来时调用它
    return (req, res) => {
      // 为这个请求创建独立的 context
      const ctx = this.createContext(req, res);
      // 执行组合后的中间件函数,传入 context
      return this.handleRequest(ctx, fn);
    };
  }
}

流程分解:

  1. app.listen() - 这只是对 Node.js 原生 API 的薄封装
  2. this.callback() - 这里是魔法发生的地方:
    • 调用 compose(this.middleware) 将所有中间件组合成一个函数
    • 返回一个闭包函数,每次请求时被调用
  3. 请求处理 - 当请求到来时:
    • 创建本次请求专属的 ctx 对象
    • 执行组合后的中间件函数 fn(ctx)
    • 所有中间件共享同一个 ctx

关键点:compose(this.middleware)

这行代码是理解 Koa 的关键。它将一个中间件数组:

[middleware1, middleware2, middleware3]

转换成一个嵌套的调用链:

middleware1(ctx, () => {
  middleware2(ctx, () => {
    middleware3(ctx, () => {
      // 最内层
    })
  })
})

这就是著名的"洋葱模型"的实现基础。接下来我们将深入剖析 compose 函数的源码。

三、洋葱模型:中间件的优雅编排

3.1 什么是洋葱模型?

Koa 的中间件执行机制被形象地称为"洋葱模型"。中间件的执行过程类似于剥洋葱:

  1. 请求阶段(外层到内层):从第一个中间件开始,遇到 await next() 就进入下一个中间件
  2. 响应阶段(内层到外层):最内层中间件执行完毕后,依次返回到外层中间件

3.2 compose 源码解析与实现

compose 函数是 koa-compose 包提供的,它是实现洋葱模型的核心。让我们先看看官方源码:

function compose(middleware) {
  // compose 返回一个函数,这个函数接收 context 和一个可选的 next
  return function (context, next) {
    let index = -1;  // 用于记录当前执行到第几个中间件

    // dispatch 函数负责执行第 i 个中间件
    function dispatch(i) {
      // 防止在同一个中间件中多次调用 next()
      // 如果 i <= index,说明 next() 被调用了多次
      if (i <= index) {
        return Promise.reject(new Error('next() 被多次调用'));
      }

      index = i; // 更新当前中间件索引,用于防止 next 被多次调用
      let fn = middleware[i];  // 获取当前要执行的中间件

      // 如果已经是最后一个中间件,fn 设为传入的 next(通常为 undefined)
      if (i === middleware.length) fn = next;
      // 如果 fn 不存在,说明已经到达末尾,返回一个 resolved 的 Promise
      if (!fn) return Promise.resolve();

      try {
        // ⭐️ 核心逻辑:执行当前中间件,并将 dispatch(i + 1) 作为 next 参数传入
        // 这样当中间件调用 await next() 时,实际上是在调用 dispatch(i + 1)
        // 从而递归地执行下一个中间件
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }

    // 从第一个中间件开始执行
    return dispatch(0);
  };
}

这个函数的精妙之处:

  1. 闭包保存状态 - 通过闭包保存 index,防止 next() 被重复调用,这是一个重要的安全检查
  2. 递归调用链 - dispatch(i) 执行当前中间件,并将 dispatch(i + 1) 作为 next 传入
  3. Promise 包装 - 所有中间件都被包装成 Promise,支持 async/await 语法
  4. 懒执行 - 只有当中间件调用 await next() 时,下一个中间件才会执行

执行流程可视化:

假设有三个中间件 [m1, m2, m3]

dispatch(0) 执行 m1(ctx, dispatch(1))
  ↓
  m1 执行到 await next()
  ↓
  dispatch(1) 执行 m2(ctx, dispatch(2))
    ↓
    m2 执行到 await next()
    ↓
    dispatch(2) 执行 m3(ctx, dispatch(3))
      ↓
      m3 执行完毕
    ↓
    m2 的 next() 后的代码执行
  ↓
  m1 的 next() 后的代码执行

源码使用递归实现,初看可能有些难懂。没关系,下面我们来实现一个简化版本,帮助理解核心思想。

3.3 手写简易版 compose

核心思想:在当前中间件执行过程中,让 next() 函数能够自动执行下一个中间件,直到最后一个。

const compose = (middleware) => {
  const ctx = {};  // 创建一个上下文对象

  if (middleware.length === 0) {
    return;
  }

  let index = 0;
  const fn = middleware[index]; // 获取第一个中间件
  fn(ctx, next);                // 手动执行第一个中间件

  // 实现 next() 函数
  // 核心是在当前中间件执行过程中,获取下一个中间件函数并自动执行,直到最后一个
  async function next() {
    index++;  // 移动到下一个中间件

    // 如果已经是最后一个中间件,直接返回
    if (index >= middleware.length) {
      return;
    }

    const fn = middleware[index];  // 获取下一个中间件
    return await fn(ctx, next);    // 执行下一个中间件,并传入 next
  }
};

// 定义三个测试中间件
const middleware1 = (ctx, next) => {
  console.log(">> one");
  next();                 // 调用 next(),执行 middleware2
  console.log("<< one");
};

const middleware2 = (ctx, next) => {
  console.log(">> two");
  next();                 // 调用 next(),执行 middleware3
  console.log("<< two");
};

const middleware3 = (ctx, next) => {
  console.log(">> three");
  next();                 // 已经是最后一个,next() 直接返回
  console.log("<< three");
};

// 执行组合后的中间件
compose([middleware1, middleware2, middleware3]);

// 输出:
// >> one
// >> two
// >> three
// << three
// << two
// << one

关键理解点:

  1. 同步执行 - 当 middleware1 调用 next() 时,middleware2 会立即开始执行
  2. 栈式回溯 - 当 middleware3 执行完毕后,控制权会依次返回到 middleware2middleware1next() 之后
  3. 洋葱结构 - 这就形成了"进入"和"退出"两个阶段,像剥洋葱一样

执行顺序详解:

1. middleware1 开始执行 → 打印 ">> one"
2. middleware1 调用 next() → 暂停,进入 middleware2
3. middleware2 开始执行 → 打印 ">> two"
4. middleware2 调用 next() → 暂停,进入 middleware3
5. middleware3 开始执行 → 打印 ">> three"
6. middleware3 调用 next() → 返回(已是最后一个)
7. middleware3 继续执行 → 打印 "<< three"
8. middleware3 执行完毕 → 返回到 middleware2
9. middleware2 继续执行 → 打印 "<< two"
10. middleware2 执行完毕 → 返回到 middleware1
11. middleware1 继续执行 → 打印 "<< one"

建议: 使用 VSCode 的断点调试功能,在每个中间件的 next() 前后打断点,单步执行体会代码的具体执行过程。这样能够更直观地理解洋葱模型的运作机制。

image.png

四、总结

通过对 Koa 源码的深入分析,我们可以看到它的设计哲学:极简、优雅、灵活

参考资源


如果觉得有帮助,欢迎点赞收藏,也欢迎在评论区交流讨论!

Promise :从基础原理到高级实践

作者 Isenberg
2025年12月20日 16:35

Promise 是 JavaScript 中处理异步操作的核心机制,它解决了传统回调函数(Callback)带来的“回调地狱”(Callback Hell)问题,使异步代码更清晰、可读、可维护。自 ES6(ECMAScript 2015)正式引入以来,Promise 已成为现代前端开发的基石,并为 async/await 语法提供了底层支持。


一、为什么需要 Promise?

1.1 回调函数的局限性

在 Promise 出现之前,异步操作主要通过回调函数实现:

// 嵌套回调(回调地狱)
getData(function(a) {
  getMoreData(a, function(b) {
    getEvenMoreData(b, function(c) {
      console.log(c);
    });
  });
});

问题

  • 代码横向扩展,难以阅读和维护
  • 错误处理分散,需在每个回调中重复写 try/catch
  • 无法使用 returnthrow 控制流程
  • 多个异步操作的组合(如并行、竞态)实现复杂

1.2 Promise 的优势

  • 链式调用:通过 .then() 实现线性流程
  • 统一错误处理:通过 .catch() 捕获整个链中的错误
  • 组合能力:支持 Promise.allPromise.race 等高级模式
  • 与 async/await 无缝集成

二、Promise 基础概念

2.1 什么是 Promise?

Promise 是一个表示异步操作最终完成或失败的对象。

它有三种状态(State):

  • pending(待定) :初始状态,既不是成功也不是失败
  • fulfilled(已成功) :操作成功完成
  • rejected(已失败) :操作失败

⚠️ 状态不可逆
一旦 Promise 从 pending 变为 fulfilledrejected,状态将永久固定,不能再改变。

2.2 创建 Promise

使用 new Promise(executor) 构造函数:

const promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    const success = Math.random() > 0.5;
    if (success) {
      resolve('操作成功!'); // 将状态变为 fulfilled
    } else {
      reject(new Error('操作失败!')); // 将状态变为 rejected
    }
  }, 1000);
});
  • resolve(value):标记 Promise 成功,传递结果值
  • reject(reason):标记 Promise 失败,传递错误原因(通常为 Error 对象)

三、Promise 的基本用法

3.1 链式调用(Chaining)

通过 .then(onFulfilled, onRejected) 处理结果:

promise
  .then(
    result => {
      console.log('成功:', result); // '操作成功!'
      return result.toUpperCase(); // 返回新值,传递给下一个 then
    },
    error => {
      console.error('失败:', error); // 不会执行(除非上一步 reject)
    }
  )
  .then(transformedResult => {
    console.log('转换后:', transformedResult); // '操作成功!'
  })
  .catch(error => {
    // 捕获链中任何未处理的 reject
    console.error('捕获错误:', error);
  });

关键规则

  • .then() 总是返回一个新的 Promise
  • onFulfilled 返回普通值 → 新 Promise 状态为 fulfilled
  • onFulfilled 抛出异常 → 新 Promise 状态为 rejected
  • onFulfilled 返回另一个 Promise → 新 Promise 跟随该 Promise 的状态

3.2 错误处理:.catch()

.catch(onRejected).then(null, onRejected) 的语法糖:

fetchUserData()
  .then(user => processUser(user))
  .then(data => saveToCache(data))
  .catch(error => {
    // 捕获 fetchUserData、processUser 或 saveToCache 中的任何错误
    console.error('操作失败:', error.message);
    showErrorMessage();
  });

📌 最佳实践
在链的末尾使用 .catch() 统一处理错误,避免在每个 .then() 中写错误回调。


四、Promise 的高级特性

4.1 静态方法

Promise.resolve(value)

将值转为已成功的 Promise:

Promise.resolve(42).then(v => console.log(v)); // 42
Promise.resolve(Promise.resolve('hello')).then(v => console.log(v)); // 'hello'

Promise.reject(reason)

创建一个已失败的 Promise:

Promise.reject(new Error('Oops!')).catch(e => console.error(e.message));

Promise.all(iterable)

并行执行多个 Promise,全部成功才成功

const promises = [
  fetch('/api/users'),
  fetch('/api/posts'),
  fetch('/api/comments')
];

Promise.all(promises)
  .then(results => {
    const [users, posts, comments] = results;
    renderPage(users, posts, comments);
  })
  .catch(error => {
    // 任一请求失败,立即 reject
    console.error('加载失败:', error);
  });

⚠️ 注意:若任一 Promise reject,all 立即 reject,其余 Promise 仍会执行但结果被忽略。

Promise.allSettled(iterable)

等待所有 Promise 完成(无论成功或失败):

Promise.allSettled(promises)
  .then(results => {
    results.forEach((result, i) => {
      if (result.status === 'fulfilled') {
        console.log(`请求 ${i} 成功:`, result.value);
      } else {
        console.error(`请求 ${i} 失败:`, result.reason);
      }
    });
  });

Promise.race(iterable)

返回第一个完成的 Promise(无论成功或失败)

const timeout = new Promise((_, reject) =>
  setTimeout(() => reject(new Error('超时')), 5000)
);

Promise.race([fetch('/api/data'), timeout])
  .then(data => console.log('数据:', data))
  .catch(error => console.error('失败或超时:', error));

Promise.any(iterable)(ES2021)

返回第一个成功的 Promise(忽略失败):

Promise.any([
  Promise.reject('A 失败'),
  Promise.resolve('B 成功'),
  Promise.reject('C 失败')
]).then(value => console.log(value)); // 'B 成功'

❗ 若全部失败,则 reject 一个 AggregateError


五、Promise 与 async/await

async/await 是 Promise 的语法糖,使异步代码看起来像同步代码。

5.1 基本用法

async function fetchData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) throw new Error('请求失败');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('错误:', error);
    throw error; // 可选择重新抛出
  }
}

// 调用
fetchData().then(data => console.log(data));

5.2 关键规则

  • async 函数总是返回 Promise
  • await 只能在 async 函数内使用
  • await 后可跟 Promise 或普通值
  • 错误可通过 try/catch 捕获

5.3 并行 vs 串行

// ❌ 串行(慢)
async function slow() {
  const a = await fetch('/a');
  const b = await fetch('/b');
  const c = await fetch('/c');
}

// ✅ 并行(快)
async function fast() {
  const [a, b, c] = await Promise.all([
    fetch('/a'),
    fetch('/b'),
    fetch('/c')
  ]);
}

六、常见陷阱与最佳实践

6.1 陷阱 1:忘记返回 Promise

// ❌ 错误:第二个 then 无法获取数据
fetch('/api')
  .then(res => res.json())
  .then(data => {
    processData(data); // 忘记 return
  })
  .then(result => {
    console.log(result); // undefined!
  });

// ✅ 正确
fetch('/api')
  .then(res => res.json())
  .then(data => {
    return processData(data); // 显式 return
  });

6.2 陷阱 2:未处理拒绝(Uncaught Rejection)

// ❌ 危险:可能被忽略,导致静默失败
somePromise.then(result => {
  // ...
});

// ✅ 安全:始终处理错误
somePromise
  .then(result => { /* ... */ })
  .catch(error => { /* 处理错误 */ });

🔔 Node.js 提示:未处理的 Promise rejection 会导致进程警告(未来可能终止进程)。

6.3 陷阱 3:在循环中使用 await(串行而非并行)

// ❌ 串行执行(总耗时 = 所有请求时间之和)
for (const url of urls) {
  const data = await fetch(url);
  results.push(data);
}

// ✅ 并行执行(总耗时 ≈ 最长请求时间)
const promises = urls.map(url => fetch(url));
const results = await Promise.all(promises);

6.4 最佳实践

  1. 始终处理错误:使用 .catch()try/catch
  2. 避免嵌套 Promise:使用链式调用或 async/await
  3. 明确返回值:在 .then() 中显式 return
  4. 合理使用组合方法allraceallSettled
  5. 不要混合回调与 Promise:统一异步风格

七、Promise 的内部原理(简要)

虽然开发者通常无需实现 Promise,但理解其机制有助于调试:

// 极简 Promise 实现(仅演示思路)
class SimplePromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.callbacks = [];

    const resolve = value => {
      if (this.state !== 'pending') return;
      this.state = 'fulfilled';
      this.value = value;
      this.callbacks.forEach(cb => cb());
    };

    const reject = reason => {
      if (this.state !== 'pending') return;
      this.state = 'rejected';
      this.value = reason;
      this.callbacks.forEach(cb => cb());
    };

    executor(resolve, reject);
  }

  then(onFulfilled) {
    return new SimplePromise((resolve) => {
      const callback = () => {
        if (this.state === 'fulfilled') {
          const result = onFulfilled(this.value);
          resolve(result);
        }
      };
      if (this.state === 'pending') {
        this.callbacks.push(callback);
      } else {
        callback();
      }
    });
  }
}

📚 真实 Promise 更复杂:需处理微任务队列(Microtask Queue)、thenable 对象、递归解析等。


八、在 Vue 3 中的实践

Vue 3 的组合式 API 与 Promise 天然契合:

// composables/useApi.js
import { ref } from 'vue';

export function useApi(url) {
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);

  const execute = async () => {
    loading.value = true;
    error.value = null;
    try {
      const res = await fetch(url);
      if (!res.ok) throw new Error(res.statusText);
      data.value = await res.json();
    } catch (err) {
      error.value = err;
    } finally {
      loading.value = false;
    }
  };

  return { data, loading, error, execute };
}
<script setup>
import { useApi } from '@/composables/useApi';

const { data, loading, error, execute } = useApi('/api/users');
execute();
</script>

<template>
  <div v-if="loading">加载中...</div>
  <div v-else-if="error">错误: {{ error.message }}</div>
  <ul v-else>
    <li v-for="user in data" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

优势:逻辑复用、状态管理、错误处理一体化。


结语

Promise 是 JavaScript 异步编程的里程碑,它不仅解决了回调地狱问题,还为现代异步语法(async/await)奠定了基础。掌握 Promise 的核心概念、链式调用、错误处理和组合方法,是成为高效前端开发者的必经之路。

记住:

  • Promise 是状态机:pending → fulfilled/rejected
  • 链式调用是核心:每个 .then() 返回新 Promise
  • 错误必须处理:避免静默失败
  • 组合优于嵌套:善用 Promise.all 等静态方法

随着 Web 应用日益复杂,异步操作无处不在。

智元江苏具身智能产业基地战略合作项目在无锡签约落地

2025年12月20日 16:35
据无锡发布消息,12月19日,智元江苏具身智能产业基地战略合作项目签约落地无锡市惠山区。市长蒋锋与智元创新(上海)科技股份有限公司董事长邓泰华一行工作会谈,并共同见证签约。蒋锋说,希望双方以此次签约为契机,强强联合、优势互补,合力推动合作项目早投产、早见效,更好促进人工智能与实体经济深度融合发展。(界面新闻)

脚手架开发工具——判断文件是否存在 path-exists

作者 code_YuJun
2025年12月20日 16:23

简介

path-exists 是一个轻量级的 Node.js npm 包,其核心作用是简便、高效地检查文件系统中指定的路径(文件或目录)是否存在,无需开发者手动封装原生文件操作的回调逻辑或错误处理,简化了 Node.js 中的路径存在性校验场景。

核心用法

npm install path-exists --save
  • 异步用法(推荐,非阻塞 I/O)
const pathExists = require('path-exists');

// 异步检查文件路径是否存在
async function checkFilePath() {
  // 传入要检查的文件/目录路径(相对路径或绝对路径均可)
  const isExist = await pathExists('./test.txt');
  console.log('文件是否存在:', isExist); // 返回 true 或 false
}

checkFilePath();

// 也可使用 Promise 链式调用(兼容旧版语法)
pathExists('./dist/').then((exists) => {
  console.log('目录是否存在:', exists);
});
  • 同步用法(适用于简单脚本,阻塞 I/O)
const pathExists = require('path-exists');

// 同步检查目录路径是否存在
const dirExists = pathExists.sync('./node_modules/');
console.log('node_modules 目录是否存在:', dirExists); // 返回 true 或 false

XMLHttpRequest、AJAX、Fetch 与 Axios

作者 Isenberg
2025年12月20日 16:20

在现代 Web 开发中,前端与后端的数据交互是构建动态应用的核心。围绕这一需求,诞生了多个关键技术与工具:XMLHttpRequest(XHR)AJAXAxiosFetch API。它们之间既有历史演进关系,也有功能重叠与互补。本文将系统梳理四者的关系,深入剖析 XHR 的工作机制与 Fetch 的底层原理,并结合 Vue 3 开发实践,提供一套完整的前端网络通信知识体系。


一、核心概念与层级关系

1.1 AJAX:一种编程范式(不是技术)

  • 全称:Asynchronous JavaScript and XML
  • 本质一种开发模式,指在不刷新页面的情况下,通过 JavaScript 异步与服务器交换数据并更新部分网页内容。
  • 核心思想:解耦 UI 更新与数据获取,提升用户体验。

关键点
AJAX 不是某个具体 API,而是一种使用现有技术实现异步通信的策略
实现 AJAX 的核心技术就是 XMLHttpRequest

1.2 XMLHttpRequest(XHR):浏览器原生 API

  • 角色实现 AJAX 的底层工具

  • 功能:提供浏览器与服务器进行 HTTP 通信的能力

  • 特点

    • 基于回调(事件驱动)
    • 支持进度监控、取消请求、上传/下载
    • 兼容性极好(IE7+)

📌 关系
XHR 是 AJAX 的“引擎” 。没有 XHR,就没有现代意义上的 AJAX。

1.3 Axios:基于 Promise 的 HTTP 客户端库

  • 定位对 XHR 的封装与增强

  • 核心特性

    • 返回 Promise,支持 async/await
    • 自动转换 JSON 数据
    • 拦截器(请求/响应)
    • 客户端支持 XSRF 防护
    • 浏览器 + Node.js 双端支持
  • 底层实现:在浏览器中默认使用 XHR,在 Node.js 中使用 http 模块

// Axios 内部简化逻辑
function axios(config) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(config.method, config.url);
    xhr.send(config.data);
    xhr.onload = () => resolve(xhr.response);
    xhr.onerror = () => reject(xhr.statusText);
  });
}

关系
Axios 是 XHR 的现代化封装,让开发者用更简洁的语法享受 XHR 的全部能力。

1.4 Fetch API:浏览器新一代原生 API

  • 定位XHR 的官方继任者

  • 设计目标

    • 基于 Promise,符合现代 JS 编程习惯
    • 更简洁的 API 设计
    • 更好的流(Stream)支持
    • 统一请求/响应模型(Request/Response 对象)
  • 底层实现并非基于 XHR,而是直接调用浏览器的网络层(如 Chromium 的 blink::WebURLLoader

⚠️ 重要区别
Fetch 不是 XHR 的封装,而是全新的底层实现


二、四者关系图谱

                          ┌──────────────┐
                          │    AJAX      │ ←── 编程范式(异步通信思想)
                          └──────┬───────┘
                                 │
         ┌───────────────────────┼───────────────────────┐
         │                       │                       │
┌────────▼────────┐   ┌──────────▼──────────┐   ┌────────▼────────┐
│ XMLHttpRequest  │   │       Fetch API     │   │      Axios      │
│ (原生, 回调式)   │   │ (原生, Promise式)   │   │ (第三方库, Promise)│
└────────┬────────┘   └─────────────────────┘   └────────┬────────┘
         │                                               │
         └───────────────────────┬───────────────────────┘
                                 │
                   ┌─────────────▼─────────────┐
                   │   现代 Web 应用数据通信    │
                   └───────────────────────────┘

🔑 总结关系

  • AJAX 是思想,XHR/Fetch 是实现该思想的原生工具
  • Axios 是对 XHR(浏览器端)的高级封装
  • Fetch 是浏览器提供的、与 XHR 并列的新一代原生 API

三、XMLHttpRequest 详解

3.1 基本使用流程

const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/users', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseText);
  }
};
xhr.send();

3.2 核心属性

属性 说明
readyState 请求状态(0–4)
status / statusText HTTP 状态码与描述
responseText 字符串响应体
response 根据 responseType 解析后的数据
responseType 响应类型(jsonblobarraybuffer 等)

3.3 事件模型

  • 传统方式onreadystatechange(需手动判断 readyState

  • 现代方式(推荐):

    • onload:请求完成
    • onerror:网络错误
    • ontimeout:超时
    • onabort:被中止

3.4 高级功能

  • 超时控制xhr.timeout = 5000
  • 跨域凭据xhr.withCredentials = true
  • 上传进度xhr.upload.onprogress
  • 中止请求xhr.abort()

3.5 实际应用场景

文件上传(带进度)

function uploadFile(file) {
  const formData = new FormData();
  formData.append('file', file);
  
  const xhr = new XMLHttpRequest();
  xhr.open('POST', '/upload');
  
  xhr.upload.onprogress = (e) => {
    if (e.lengthComputable) {
      const percent = (e.loaded / e.total) * 100;
      updateProgress(percent);
    }
  };
  
  xhr.onload = () => {
    if (xhr.status === 200) showSuccess();
  };
  
  xhr.send(formData);
}

四、Fetch API 原理深度解析

4.1 核心设计:基于 Stream 的请求/响应模型

Fetch 的核心是两个构造函数:

  • Request:表示 HTTP 请求
  • Response:表示 HTTP 响应

两者都实现了 Body mixin,包含可读流(ReadableStream):

fetch('/api/data')
  .then(response => {
    console.log(response.body instanceof ReadableStream); // true
    return response.json(); // 内部读取 body 流并解析
  });

💡 关键机制
Fetch 将响应体视为流(Stream) ,支持边下载边处理,适合大文件或实时数据。

4.2 执行流程(浏览器内部)

以 Chromium 为例:

  1. 调用 fetch(url) → 创建 Request 对象
  2. 浏览器主线程 → 网络服务线程(Network Service)
  3. 网络线程发起 HTTP 请求(复用连接池、DNS 缓存等)
  4. 收到响应头 → 立即 resolve Promise(返回 Response 对象)
  5. 响应体通过 ReadableStream 逐步传输到 JS 主线程
  6. 调用 .json() / .text() 等方法 → 消费流并解析

4.3 与 XHR 的关键差异

特性 XHR Fetch
错误处理 网络错误 → onerror;HTTP 错误(404/500)→ onload 仅网络错误 reject;HTTP 错误仍 resolve(需手动检查 response.ok
Cookie 发送 同域自动发送 需显式设置 credentials: 'same-origin'
取消请求 xhr.abort() AbortController
上传进度 原生 upload.onprogress 不支持(需自定义 ReadableStream,复杂)
超时控制 xhr.timeout 需配合 AbortController + setTimeout

错误处理对比示例:

// Fetch:HTTP 404 仍 resolve
fetch('/not-found')
  .then(res => {
    if (!res.ok) { // 必须手动检查
      throw new Error(`HTTP ${res.status}`);
    }
  })
  .catch(err => {
    // 只有网络断开才会进入这里
  });

4.4 Fetch 的局限性与解决方案

问题 1:无法监控下载进度

解决方案:手动读取流并计算进度:

const response = await fetch('/large-file');
const contentLength = +response.headers.get('Content-Length');
let loaded = 0;

const reader = response.body.getReader();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  loaded += value.length;
  const progress = (loaded / contentLength) * 100;
  updateProgress(progress);
}

问题 2:无内置超时

解决方案:结合 AbortController

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);

fetch('/api/data', { signal: controller.signal })
  .finally(() => clearTimeout(timeoutId));

五、Vue 3 中的网络通信实践

虽然 Vue 本身不强制使用特定 HTTP 客户端,但其组合式 API 与现代请求库天然契合。

5.1 使用 Axios(推荐用于复杂项目)

// composables/useApi.js
import axios from 'axios';

const api = axios.create({
  baseURL: '/api',
  timeout: 10000,
  withCredentials: true
});

// 请求拦截器
api.interceptors.request.use(config => {
  const token = localStorage.getItem('token');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// 响应拦截器
api.interceptors.response.use(
  response => response.data,
  error => {
    if (error.response?.status === 401) {
      // 处理未授权
      router.push('/login');
    }
    return Promise.reject(error);
  }
);

export default api;
<!-- 在组件中使用 -->
<script setup>
import { ref } from 'vue';
import api from '@/composables/useApi';

const users = ref([]);
const loading = ref(false);

const fetchUsers = async () => {
  loading.value = true;
  try {
    users.value = await api.get('/users');
  } finally {
    loading.value = false;
  }
};

fetchUsers();
</script>

5.2 使用 Fetch(轻量级项目)

// utils/request.js
async function request(url, options = {}) {
  const config = {
    credentials: 'include',
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...options.headers
    }
  };

  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 10000);
  
  try {
    const response = await fetch(url, {
      ...config,
      signal: controller.signal
    });
    
    clearTimeout(timeoutId);
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    
    return await response.json();
  } catch (error) {
    clearTimeout(timeoutId);
    throw error;
  }
}

export { request };

5.3 封装为 Composable(最佳实践)

// composables/useFetch.js
import { ref } from 'vue';

export function useFetch(url) {
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);

  const execute = async () => {
    loading.value = true;
    error.value = null;
    
    try {
      const res = await fetch(url);
      if (!res.ok) throw new Error(res.statusText);
      data.value = await res.json();
    } catch (err) {
      error.value = err;
    } finally {
      loading.value = false;
    }
  };

  return { data, loading, error, execute };
}
<script setup>
import { useFetch } from '@/composables/useFetch';

const { data: users, loading, execute } = useFetch('/api/users');
execute();
</script>

六、如何选择?—— 使用场景建议

场景 推荐方案 理由
新项目(现代浏览器) fetch() + 工具函数封装 原生支持,无依赖,符合标准
需要上传/下载进度 XMLHttpRequest 或 Axios 原生支持 onprogress,简单可靠
复杂拦截、转换、兼容 Node.js Axios 功能全面,生态成熟
维护旧项目(IE11+) XMLHttpRequest 或 Axios(带 polyfill) 最大兼容性
轻量级应用,避免打包体积 fetch() 无需引入第三方库
Vue 3 项目 Axios(复杂)Fetch + Composable(简单) 与组合式 API 完美契合

📌 现代最佳实践

  • 优先使用 fetch() 或 Axios
  • 将网络逻辑封装为 Composable,实现逻辑复用
  • 避免直接使用裸 XHR(除非特殊需求)

七、安全与性能注意事项

7.1 安全

  • XSS 防护:永远不要将响应直接插入 innerHTML
  • CSRF 防护:使用 anti-CSRF token,重要操作用非 GET 方法
  • CORS 策略:服务器严格限制 Access-Control-Allow-Origin
  • 敏感数据:使用 HTTPS,避免客户端存储密码/token

7.2 性能

  • 缓存策略:合理设置 Cache-Control
  • 请求合并:避免频繁小请求
  • 懒加载:非关键数据延迟请求
  • 取消冗余请求:组件销毁时中止未完成的请求
// Vue 3 中取消请求
import { onUnmounted } from 'vue';

export function useFetch(url) {
  const controller = new AbortController();
  
  onUnmounted(() => {
    controller.abort(); // 组件卸载时取消请求
  });
  
  const execute = () => {
    return fetch(url, { signal: controller.signal });
  };
  
  return { execute };
}

结语

理解 XHR、AJAX、Axios 与 Fetch 的关系,本质上是理解 Web 异步通信技术的演进史:

  • AJAX 提出了“异步更新”的思想
  • XHR 提供了首个标准化实现
  • Axios 在 XHR 基础上构建了开发者友好的抽象
  • Fetch 则代表了浏览器厂商对下一代网络 API 的重新设计

作为开发者,我们不必拘泥于某一种工具,而应根据项目需求、浏览器支持和功能复杂度做出合理选择。但无论使用哪种方式,其背后的核心原理——HTTP 协议、CORS 安全模型、异步编程范式——始终不变。

在 Vue 3 的组合式 API 时代,将网络逻辑封装为可复用的 Composable,不仅能提升代码可维护性,更能充分发挥现代 JavaScript 的表达力。掌握这些底层逻辑,才能在技术变迁中游刃有余,构建出高性能、高安全性的现代 Web 应用。

2025-12-20 vue3中 eslint9+和prettier配置

作者 Mr_Swilder
2025年12月20日 16:19

eslint 9+相较8版本使用eslint.config.js和扁平化配置方式。

1.在项目根目录下安装所需的开发依赖包

# 核心代码检查与格式化工具
pnpm add -D eslint prettier

# Vue.js 语法支持
pnpm add -D eslint-plugin-vue

# Prettier 与 ESLint 集成
pnpm add -D eslint-config-prettier eslint-plugin-prettier

💅 2. prettier配置

vscode中安装插件

image.png

根目录下新建配置文件 .prettierrc

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100,
  "arrowParens": "avoid",
  "htmlWhitespaceSensitivity": "ignore"
}

3.eslint配置并将prettier规则作为eslint一部分,对不符合要求的报错

// eslint.config.js
import eslintPluginVue from 'eslint-plugin-vue'
import vueEslintParser from 'vue-eslint-parser'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
// console.log(eslintPluginPrettierRecommended)
export default [
  // 全局配置:指定环境、解析器选项
  {
    files: ['**/*.js', '**/*.vue'],
    ignores: ['vite.config.js', 'node_modules/', 'dist/', 'public/'],
    languageOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      globals: {
        browser: true, // 浏览器环境全局变量
        node: true, // Node.js 环境全局变量
        es2021: true // ES2021 语法支持
      }
    },
    rules: {
      'no-console': 'warn',
      'no-debugger': 'error',
      'no-unused-vars': ['warn', { varsIgnorePattern: '^_' }]
    }
  },

  // Vue 单文件组件专属配置
  {
    files: ['**/*.vue'],
    // 使用 vue-eslint-parser 解析 .vue 文件
    languageOptions: {
      parser: vueEslintParser,
      parserOptions: {
        parser: 'espree', // 解析 <script> 块内的 JavaScript
        ecmaVersion: 'latest',
        sourceType: 'module'
      }
    },
    plugins: {
      vue: eslintPluginVue
    },
    rules: {
      // 启用 Vue 官方推荐规则
      ...eslintPluginVue.configs['flat/recommended'].rules,
      // 自定义 Vue 规则
      'vue/multi-word-component-names': 'off' // 关闭组件名必须多单词的要求
    }
  },
  //prettier配置
  eslintPluginPrettierRecommended
]

此时运行

npm run lint

可以对不符合规则的代码检查,包含不符合eslint规则的

image.png

4.配置vscode插件eslintPrettier ESlint,设置默认格式化工具为Prettier ESlint,让eslint直接报错prettier中的配置,有时修改了.prettierrc中的配置需要重启Prettier ESlint插件才能生效。

image.png

image.png

对于.eslintignore文件缺失时eslint会使用.gitignore文件

Vue3条件渲染中v-if系列指令如何合理使用与规避错误?

作者 kknone
2025年12月20日 16:13

一、Vue3条件渲染的核心概念

在Vue3中,条件渲染是指根据响应式数据的真假,决定是否在页面上渲染某个元素或组件。而v-ifv-elsev-else-if 这组指令,就是实现条件渲染的“核心工具”——它们像一套“逻辑开关”,帮你精准控制DOM的显示与隐藏。

二、v-if系列指令的语法与用法

我们先逐个拆解每个指令的作用,再通过实际案例串起来用。

2.1 v-if:基础条件判断

v-if是最基础的条件指令,它的语法很简单:


<元素 v-if="条件表达式">要渲染的内容</元素>
  • 条件表达式:可以是任何返回truefalse的JavaScript表达式(比如isLoginscore >= 90)。
  • 惰性渲染v-if是“懒”的——如果初始条件不满足(比如isLogin = false),元素不会被渲染到DOM中(连DOM节点都不会生成);只有当条件变为 true时,才会“从零开始”创建元素及其子组件。

举个例子:判断用户是否登录,显示不同的内容:


<script setup>
  import {ref} from 'vue'

  const isLogin = ref(false) // 初始未登录
</script>

<template>
  <div>
    <!-- 登录后显示 -->
    <p v-if="isLogin">欢迎回来,用户!</p>
    <!-- 未登录显示 -->
    <button v-if="!isLogin" @click="isLogin = true">点击登录</button>
  </div>
</template>

当点击按钮时,isLogin变为true,未登录的按钮会被销毁,同时渲染“欢迎”文本——这就是v-if的“销毁/重建”逻辑。

往期文章归档
免费好用的热门在线工具

2.2 v-else:补充默认分支

如果v-if的条件不满足,你可以用v-else添加一个“默认选项”。但要注意:
v-else必须紧跟在v-ifv-else-if的后面,中间不能有其他兄弟元素(否则Vue无法识别它属于哪个条件)。

修改上面的例子,用v-else简化未登录的情况:


<template>
    <div>
        <p v-if="isLogin">欢迎回来,用户!</p>
        <!-- 直接跟在v-if后面,无需写条件 -->
        <button v-else @click="isLogin = true">点击登录</button>
    </div>
</template>

2.3 v-else-if:多分支条件判断

当需要判断多个条件时,用v-else-if连接。它的语法是:


<元素 v-if="条件1">内容1</元素>
<元素 v-else-if="条件2">内容2</元素>
<元素 v-else-if="条件3">内容3</元素>
<元素 v-else>默认内容</元素>

关键规则:Vue会按指令的顺序依次判断,满足第一个条件就停止(所以条件的顺序很重要!)。

比如根据分数显示等级(最常见的多分支场景):


<script setup>
  import {ref} from 'vue'

  const score = ref(85) // 响应式分数,初始85分
</script>

<template>
  <div class="score-level">
    <h3>你的分数:{{ score }}</h3>
    <!-- 顺序:从高到低 -->
    <p v-if="score >= 90" class="excellent">等级:优秀(≥90)</p>
    <p v-else-if="score >= 80" class="good">等级:良好(80-89)</p>
    <p v-else-if="score >= 60" class="pass">等级:及格(60-79)</p>
    <p v-else class="fail">等级:不及格(<60)</p>
  </div>
</template>

<style scoped>
  .excellent {
    color: #4CAF50;
  }

  .good {
    color: #2196F3;
  }

  .pass {
    color: #FFC107;
  }

  .fail {
    color: #F44336;
  }
</style>

运行这个组件,修改score的值(比如改成9550),会看到等级自动切换——这就是多分支条件渲染的实际效果。

三、条件渲染的流程逻辑(附流程图)

为了更直观理解v-if系列的执行顺序,我们画一个判断流程图

flowchart TD
    A[开始] --> B{检查v-if条件}
    B -->|满足| C[渲染v-if内容,结束]
    B -->|不满足| D{检查下一个v-else-if条件}
    D -->|满足| E[渲染对应内容,结束]
    D -->|不满足| F{还有v-else-if吗?}
    F -->|是| D
    F -->|否| G[渲染v-else内容,结束]

简单来说:按顺序“闯关”,满足条件就“通关”,否则继续,直到最后一个v-else

四、课后Quiz:巩固你的理解

Quiz 1:v-if和v-show有什么区别?

问题:同样是“隐藏元素”,v-ifv-show的核心差异是什么?分别适合什么场景?

答案解析(参考Vue官网):

  • v-if销毁/重建DOM——条件不满足时,元素从DOM中消失;条件满足时,重新创建。适合条件很少变化的场景(比如权限判断),因为初始渲染更省性能。
  • v-show修改CSS显示——无论条件如何,元素都会渲染到DOM,只是用display: none隐藏。适合条件频繁切换的场景(比如 tabs 切换),因为切换时无需销毁重建,更高效。

Quiz 2:为什么v-else必须紧跟v-if?

问题:如果写v-if之后隔了一个div再写v-else,会报错吗?为什么?

答案解析
会报错!错误信息是“v-else has no adjacent v-if”。
原因:Vue需要明确v-else对应的“上级条件”——它必须与最近的v-if/v-else-if直接相邻(中间不能有其他兄弟元素)。如果隔开,Vue无法识别两者的关联。

五、常见报错与解决方案

在使用v-if系列时,新手常遇到以下问题,我们逐一解决:

1. 报错:“v-else/v-else-if has no adjacent v-if”

  • 原因v-elsev-else-if没有紧跟对应的v-if(中间有其他元素)。
    比如:
    <div v-if="isShow"></div>
    <p>无关内容</p> <!-- 中间的p元素导致错误 -->
    <div v-else></div>
    
  • 解决:删除中间的无关元素,让v-else直接紧跟v-if
  • 预防:写v-else前,先检查前面的元素是否是v-ifv-else-if

2. 报错:“条件变化时,v-if内容不更新”

  • 原因:条件变量不是响应式的(比如用let isShow = false而不是ref(false)),Vue无法追踪其变化。
  • 解决:用ref(基本类型)或reactive(对象/数组)包裹条件变量:
    // 错误写法
    let isShow = false 
    // 正确写法
    const isShow = ref(false)
    
  • 预防:所有需要“随数据变化而更新”的变量,都用Vue的响应式API(ref/reactive)定义。

3. 逻辑错误:v-else-if顺序导致条件失效

  • 例子:先写v-else-if="score >= 60",再写v-else-if="score >= 80"——此时80分会被第一个条件拦截,永远到不了第二个。
  • 原因:Vue按指令顺序判断,满足第一个条件就停止。
  • 解决:将更严格的条件放在前面(比如先>=90,再>=80,最后>=60)。

参考链接

Vue官网条件渲染文档:vuejs.org/guide/essen…

脚手架开发工具——root-check

作者 code_YuJun
2025年12月20日 16:02

简介

root-check 是一个 Node.js 工具包,核心作用是检测当前 Node.js 进程是否以 root(超级管理员)权限运行,并在检测到 root 权限时,自动降级为指定的普通用户权限运行,以此提升应用的安全性。

检测 root 权限

它会判断当前进程的 uid(用户 ID)是否为 0(Unix/Linux 系统中 root 用户的 uid 固定为 0),以此识别是否为 root 权限运行。

自动降级权限

如果检测到当前是 root 权限,它会尝试切换到一个非 root 普通用户的权限来运行后续代码。

  • 默认会尝试切换到 nobody 用户(Unix/Linux 系统中内置的无特权用户)。
  • 也可以手动指定要切换的用户(通过用户名或 uid)。

解决的核心问题

在 Unix/Linux 系统中,以 root 权限运行 Node.js 应用存在极高的安全风险:一旦应用存在漏洞被攻击,攻击者将直接获得系统的最高权限,可能导致服务器被完全控制。root-check 的存在就是为了避免这种风险,强制应用以低权限运行,即使被攻击,影响范围也会被大幅限制。

适用场景

  • 命令行工具(CLI)开发:很多 Node.js 命令行工具(如前端的构建工具、脚手架)会用到这个包,防止用户以 root 权限执行命令带来风险。
  • 服务端应用:Node.js 编写的后端服务,部署时需要避免 root 权限运行,可通过此包自动降级。

基本使用示例

npm install root-check --save
const rootCheck = require('root-check').default;
// 自动降级为 nobody 用户(如果当前是 root 权限)
rootCheck();
// 或者手动指定要切换的用户
// rootCheck('www-data'); // 切换到 www-data 用户

注意事项

  • 仅支持 Unix/Linux 系统:Windows 系统没有 root/uid 的概念,该包在 Windows 上会直接失效(无副作用)。
  • 降级失败会抛出错误:如果当前是 root 权限,但无法切换到目标用户(比如目标用户不存在),rootCheck 会抛出异常,需要手动捕获处理。

解决Tailwind任意值滥用:规范化CSS开发体验

作者 远山无期
2025年12月20日 15:15

背景 eslint-plugin-tailwindcss插件的no-unnecessary-arbitrary-value无法对所有的任意值进行校验,比如h-[48px]text-[#f5f5f5]无法校验出来。但tailwindcss的预设值太多了,一个不小心可能就又写了一个没有必要的任意值。为了避免这种情况,我们需要自己实现一个检测任意值的eslint插件。

插件地址:eslint-plugin-tailwind-no-preset-class

首先来看下效果

no-unnecessary-arbitrary-value 无法检测的情况

image.png

使用自定义的:eslint-plugin-tailwind-no-preset-class插件,完美完成了校验

image.png

创建eslint插件标准目录结构

  • 安装Yeoman
npm install -g yo
  • 安装Yeoman generator-eslint
npm install -g generator-eslint
  • 创建项目
mkdir eslint-plugin-my-plugin
yo eslint:plugin

生成目录结构如下:

eslint-plugin-my-plugin/
├── lib/                    # 核心源代码目录
│   ├── index.js           # 插件的入口文件,在这里导出所有规则
│   └── rules/             # 存放所有自定义规则的目录
│       └── my-rule.js     # 生成器为你创建的一条示例规则文件
├── tests/                 # 测试文件目录
│   └── lib/
│       └── rules/
│           └── my-rule.js # 示例规则对应的测试文件
├── package.json           # 项目的 npm 配置文件,依赖和元信息都在这里
└── README.md              # 项目说明文档

根据实际项目的tailwindcss配置文件和tailwindcss默认配置生成全量定制化配置,用于后续eslint插件的校验依据

实现配置文件生成并加载方法:

// lib/tailwind-config-loader.js
// 配置文件生成
...
...
// 动态加载 Tailwind 预设配置
let tailwindPresetConfig = null;
...
async function generateTailwindConfig(projectRootPath) {
  try {
    // 动态导入tailwindcss
    const resolveConfigModule = await import('tailwindcss/lib/public/resolve-config.js');
    const resolveConfig = resolveConfigModule.default.default
    // 尝试加载项目配置
    let projectConfig = {};
    try {
      const projectConfigPath = join(projectRootPath||process.cwd(), 'tailwind.config.js');
      const projectConfigModule = await import(projectConfigPath);
      projectConfig = projectConfigModule.default || projectConfigModule;
    } catch (error) {
      console.log('⚠️ 未找到项目 tailwind.config.js,使用默认配置');
      throw error;
    }

    // 使用tailwindcss的resolveConfig函数
    const finalConfig = resolveConfig(projectConfig);

    console.log('✅ Tailwind preset config generated successfully!');
    
    return finalConfig;
  } catch (error) {
    console.error('❌ 生成Tailwind配置失败:', error.message);
    throw error;
  }
}


// 加载配置到内存中
async function loadTailwindPresetConfig(projectRootPath) {
  if (configLoading) {
    console.log('⏳ 配置正在加载中,跳过重复请求');
    return;
  }

  configLoading = true;

  try {
    // 直接动态生成配置
    tailwindPresetConfig = await generateTailwindConfig(projectRootPath);
    console.log('✅ Tailwind 预设配置已动态生成并加载');
    onConfigLoaded();
  } catch (error) {
    console.error('❌ 动态生成 Tailwind 预设配置失败:', error.message);
    onConfigLoadFailed(error);
    throw error;
  }
}


...
// 导出配置
export const TailwindConfigLoader = {
  getConfig: () => tailwindPresetConfig,
  isLoaded: () => configLoaded,
  ensureLoaded: ensureConfigLoaded,
  reload: loadTailwindPresetConfig,
  generateConfig: generateTailwindConfig
};
...
...

创建校验规则函数

  • 实现校验规则函数checkAndReport
...
// 使用 WeakMap 来跟踪每个文件的已报告类名,避免重复报告
const reportedClassesMap = new WeakMap();
...
// 检查并报告
async function checkAndReport(context, node, className) {
  // 如果配置尚未加载,尝试等待加载
  if (!TailwindConfigLoader.isLoaded()) {
    try {
        const projectRootPath = context.getCwd();
        console.log(`正在等待加载配置文件 ${projectRootPath}...`);
      const loaded = await TailwindConfigLoader.ensureLoaded(projectRootPath);
      if (!loaded) {
        console.warn('⚠️ Tailwind 预设配置尚未加载,跳过检查');
        return;
      }
    } catch (error) {
      console.warn('⚠️ 配置加载失败,跳过检查');
      return;
    }
  }

  const filePath = context.getFilename();
  const filePathWrapper = new FilePathWrapper(filePath);

  if (!reportedClassesMap.has(filePathWrapper)) {
    reportedClassesMap.set(filePathWrapper, new Set());
  }
  const reportedClasses = reportedClassesMap.get(filePathWrapper);

  if (reportedClasses.has(className)) {
    return;
  }

  const propertyInfo = extractProperty(className);
  if (!propertyInfo) {
    return;
  }

  const { property, value, originalPrefix } = propertyInfo;

  // 只检查任意值
  if (isArbitraryValue(value)) {
    const arbitraryValue = value.slice(1, -1);
    const presetClass = findPresetClass(property, arbitraryValue);

    if (presetClass) {
      reportedClasses.add(className);
      // 使用原始前缀显示正确的类名格式(如 h-14 而不是 height-14)
      const suggestedClass = `${originalPrefix}${presetClass}`;
      context.report({
        node,
        message: `类名 "${className}" 使用了任意值,但存在对应的预设类名 "${suggestedClass}"。请使用预设类名替代。`,
      });
    }
  }
}

  • 实现属性提取,将classname解析为tailwindcss的property和value
// 提取属性值
function extractProperty(className) {
  // 处理响应式前缀(如 max-md:, md:, lg: 等)
  const responsivePrefixes = [
    'max-sm:',
    'max-md:',
    'max-lg:',
    'max-xl:',
    'max-2xl:',
    'max-',
    'min-',
    'sm:',
    'md:',
    'lg:',
    'xl:',
    '2xl:',
  ];

  // 移除响应式前缀,保留核心类名
  let coreClassName = className;
  let responsivePrefix = '';

  for (const prefix of responsivePrefixes) {
    if (className.startsWith(prefix)) {
      responsivePrefix = prefix;
      coreClassName = className.slice(prefix.length);
      break;
    }
  }

  // 按前缀长度降序排序,优先匹配更长的前缀
  const sortedPrefixes = Object.keys(prefixToProperty).sort(
    (a, b) => b.length - a.length
  );

  for (const prefix of sortedPrefixes) {
    if (coreClassName.startsWith(prefix)) {
      return {
        property: prefixToProperty[prefix],
        value: coreClassName.slice(prefix.length),
        originalPrefix: responsivePrefix + prefix, // 包含响应式前缀
      };
    }
  }

  return null;
}
  • 将提取的property和前面生成的全量的tailwindcss进行映射
// 简化属性映射,只保留常用的属性
const prefixToProperty = {
  // 尺寸相关
  "w-": "width",
  "h-": "height",
  "min-w-": "minWidth",
  "min-h-": "minHeight",
  "max-w-": "maxWidth",
  "max-h-": "maxHeight",

  // 间距相关
  "m-": "margin",
  "mt-": "marginTop",
  "mr-": "marginRight",
  "mb-": "marginBottom",
  "ml-": "marginLeft",
  "mx-": "margin",
  "my-": "margin",
  "p-": "padding",
  "pt-": "paddingTop",
  "pr-": "paddingRight",
  "pb-": "paddingBottom",
  "pl-": "paddingLeft",
  "px-": "padding",
  "py-": "padding",

  // 边框相关(新增)
  "border-": "borderWidth;borderColor",
  "border-t-": "borderWidth;borderColor",
  "border-r-": "borderWidth;borderColor",
  "border-b-": "borderWidth;borderColor",
  "border-l-": "borderWidth;borderColor",
  "border-x-": "borderWidth;borderColor",
  "border-y-": "borderWidth;borderColor",

  // 圆角相关(新增)
  "rounded-": "borderRadius",
  "rounded-t-": "borderRadius",
  "rounded-r-": "borderRadius",
  "rounded-b-": "borderRadius",
  "rounded-l-": "borderRadius",
  "rounded-tl-": "borderRadius",
  "rounded-tr-": "borderRadius",
  "rounded-br-": "borderRadius",
  "rounded-bl-": "borderRadius",

  // 文字相关
  "text-": "fontSize;color",
  "leading-": "lineHeight",
  "tracking-": "letterSpacing",
  "font-": "fontWeight",

  // 背景相关
  "bg-": "backgroundColor",

  // SVG相关
  "fill-": "fill",
  "stroke-": "stroke",
  "stroke-w-": "strokeWidth",

  // 定位相关
  "z-": "zIndex",
  "inset-": "inset",
  "top-": "top",
  "right-": "right",
  "bottom-": "bottom",
  "left-": "left",

  // 布局相关(新增)
  "gap-": "gap",
  "gap-x-": "gap",
  "gap-y-": "gap",
  "space-x-": "gap",
  "space-y-": "gap",

  // 透明度
  "opacity-": "opacity",

  // 变换相关(新增)
  "scale-": "scale",
  "scale-x-": "scale",
  "scale-y-": "scale",
  "rotate-": "rotate",
  "translate-x-": "translate",
  "translate-y-": "translate",
  "skew-x-": "skew",
  "skew-y-": "skew",

  // 阴影相关(新增)
  "shadow-": "boxShadow",

  // 网格相关(新增)
  "grid-cols-": "gridTemplateColumns",
  "grid-rows-": "gridTemplateRows",
  "col-": "gridColumn",
  "row-": "gridRow",
  "col-start-": "gridColumnStart",
  "col-end-": "gridColumnEnd",
  "row-start-": "gridRowStart",
  "row-end-": "gridRowEnd",

  // Flexbox相关(新增)
  "flex-": "flex",
  "basis-": "flexBasis",
  "grow-": "flexGrow",
  "shrink-": "flexShrink",
  "order-": "order",

  // 动画相关(新增)
  "duration-": "transitionDuration",
  "delay-": "transitionDelay",
  "ease-": "transitionTimingFunction",

  // 其他(新增)
  "aspect-": "aspectRatio",
  "cursor-": "cursor",
};

// 动态构建支持的 Tailwind 属性映射
function getSupportedProperties() {
  const config = TailwindConfigLoader.getConfig();
  if (!config) {
    return {};
  }

  return {
    width: config.theme.width,
    height: config.theme.height,
    minWidth: config.theme.minWidth,
    minHeight: config.theme.minHeight,
    maxWidth: config.theme.maxWidth,
    maxHeight: config.theme.maxHeight,
    margin: config.theme.margin,
    marginTop: config.theme.margin,
    marginRight: config.theme.margin,
    marginBottom: config.theme.margin,
    marginLeft: config.theme.margin,
    padding: config.theme.padding,
    paddingTop: config.theme.padding,
    paddingRight: config.theme.padding,
    paddingBottom: config.theme.padding,
    paddingLeft: config.theme.padding,
    fontSize: config.theme.fontSize,
    lineHeight: config.theme.lineHeight,
    borderRadius: config.theme.borderRadius,
    color: config.theme.colors,
    backgroundColor: config.theme.backgroundColor,
    borderColor: config.theme.borderColor,
    fill: config.theme.fill,
    stroke: config.theme.stroke,
    borderWidth: config.theme.borderWidth,
    zIndex: config.theme.zIndex,
    gap: config.theme.gap,
    inset: config.theme.inset,
    top: config.theme.spacing,
    right: config.theme.spacing,
    bottom: config.theme.spacing,
    left: config.theme.spacing,
    opacity: config.theme.opacity,
  };
}

整体实现流程

graph TD
    A[ESLint 执行插件] --> B[遍历代码中的类名]
    B --> C{是否为 Tailwind 类名?}
    C -->|否| D[跳过检查]
    C -->|是| E{是否包含任意值?}
    E -->|否| F[使用预设值 通过检查]
    E -->|是| G[提取类名前缀和任意值]
    
    G --> H[通过 prefixToProperty 映射到CSS属性]
    H --> I[检查Tailwind配置是否已加载]
    I -->|已加载| J[获取支持的属性预设值]
    I -->|未加载| K[加载项目Tailwind配置]
    
    K --> L[读取项目tailwind.config.js]
    L --> M{配置是否存在?}
    M -->|不存在| N[使用Tailwind默认配置]
    M -->|存在| O[解析项目配置]
    
    O --> P[合并默认配置和项目配置]
    N --> P
    
    P --> Q[生成全量Tailwind配置]
    Q --> R[缓存配置到内存]
    R --> J
    
    J --> S{判断属性类型}
    S -->|颜色相关| T[调用 findColorPreset]
    S -->|数值相关| U[调用 findNumericPreset]
    
    T --> V{是否匹配预设?}
    U --> V
    
    V -->|是| W[找到对应预设类名]
    V -->|否| X[未找到预设类名]
    
    W --> Y[生成建议消息]
    X --> Z[通过检查 无匹配预设]
    
    Y --> AA[报告建议]
    Z --> BB[检查完成]
    
    AA --> BB
❌
❌