普通视图
今日头条发布深度内容扶持计划
重庆市国资委:要进一步加强市属重点国企及其子企业参股股权管理
微光股份:公司和较多机器人公司建立了合作关系
【Gemini简直无敌了】掌间星河:通过MediaPipe实现手势控制粒子
受掘金大佬“不如摸鱼去”的启发,我也试试 Gemini 3做一下手势+粒子交互, 确实学到了不少东西,在这里简单的分享下。 github地址:掌间星河:github.com/huijieya/Ge…
![]()
基于原生h5、浏览器、PC摄像头实现手势控制粒子特效交互的逻辑,粒子默认离散,类似银河系分布缓慢移动,同时有5种手势:
手势1: 握拳,握拳后粒子聚拢显示爱心的形状
手势2: 展开手掌并挥手,展开手掌挥手后粒子从当前状态恢复到离散状态
手势3: 👆 比 1 :只有食指伸直,其他 3 根弯曲,此时粒子聚拢显示第一句话:春来夏往
手势4: ✌ 比 2 :只有食指和中指伸直,其他 2 根弯曲手此时粒子聚拢显示第二句话: 秋收冬藏
手势5: 👌 比 3 :只有中指、无名指、小指伸直,食指弯曲,此时粒子聚拢显示第三句话:我们来日方长
效果展示
![]()
源码地址
源码分析
手势识别流程
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
简介
dotenv 是一个轻量级的 Node.js 环境变量管理工具,其核心作用是:从项目根目录的 .env 文件中加载自定义的环境变量,并将它们注入到 Node.js 的 process.env 对象中,使得我们可以在项目代码中统一通过 process.env.XXX 的方式获取这些环境配置,无需手动在系统环境中配置临时变量或永久变量。
核心工作原理
- 当在项目中引入并执行 dotenv 时,它会自动查找项目根目录下的
.env文件(该文件为纯文本格式,采用键值对配置); - 它会解析
.env文件中的每一行配置(格式通常为KEY=VALUE); - 将解析后的键值对逐一挂载到 Node.js 内置的
process.env对象上(process.env原本用于存储系统级环境变量,dotenv 为其扩展了项目自定义环境变量); - 之后在项目的任意代码文件中,都可以通过
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亿美元薪酬方案
付鹏:2026年将是AI应用的“证伪之年”,巨头们需证明自己
vscode没有js提示:配置jsconfig配置
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 的设计哲学和核心实现原理
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 应用会经历以下几个阶段:
-
创建应用实例 -
new Koa()初始化应用对象 -
注册中间件 -
app.use()将中间件函数添加到数组 -
启动监听 -
app.listen()创建 HTTP 服务并开始监听 - 处理请求 - 当请求到来时,组合中间件并执行
接下来我们逐步剖析每个阶段的源码实现。
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() 创建原型链,意味着:
- 每个应用实例都有自己独立的
context、request、response对象 - 这些对象继承自共享的原型,既节省内存又保证了隔离性
- 可以在不同应用实例上挂载不同的扩展属性,互不影响
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() 方法的设计亮点:
- 简单直接 - 只是将中间件函数 push 到数组,没有复杂的逻辑
-
链式调用 - 返回
this使得可以连续调用app.use().use().use() - 顺序敏感 - 中间件的执行顺序取决于注册顺序,这对理解洋葱模型很重要
注意上面的日志中间件示例:await next() 是一个分水岭,它将中间件分为"请求阶段"和"响应阶段"。这正是洋葱模型的精髓所在。
2.3 创建context
每当有新的 HTTP 请求到来时,Koa 都会为这个请求创建一个全新的 context 对象。这个对象是 Koa 最重要的创新之一,它封装了 Node.js 原生的 req 和 res,提供了更加便捷的 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;
}
这个方法的精妙之处:
-
原型继承 - 使用
Object.create()确保每个请求都有独立的 context,但共享原型上的方法 -
四层封装 -
context→request/response→req/res,逐层抽象,提供更优雅的 API - 相互引用 - 建立了复杂但合理的引用关系,使得在任何层级都能方便地访问其他对象
- 内存优化 - 通过原型链共享方法,避免每个请求都创建重复的方法副本
这样设计的好处是,在中间件中我们可以灵活地访问:
-
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);
};
}
}
流程分解:
-
app.listen()- 这只是对 Node.js 原生 API 的薄封装 -
this.callback()- 这里是魔法发生的地方:- 调用
compose(this.middleware)将所有中间件组合成一个函数 - 返回一个闭包函数,每次请求时被调用
- 调用
-
请求处理 - 当请求到来时:
- 创建本次请求专属的
ctx对象 - 执行组合后的中间件函数
fn(ctx) - 所有中间件共享同一个
ctx
- 创建本次请求专属的
关键点:compose(this.middleware)
这行代码是理解 Koa 的关键。它将一个中间件数组:
[middleware1, middleware2, middleware3]
转换成一个嵌套的调用链:
middleware1(ctx, () => {
middleware2(ctx, () => {
middleware3(ctx, () => {
// 最内层
})
})
})
这就是著名的"洋葱模型"的实现基础。接下来我们将深入剖析 compose 函数的源码。
三、洋葱模型:中间件的优雅编排
3.1 什么是洋葱模型?
Koa 的中间件执行机制被形象地称为"洋葱模型"。中间件的执行过程类似于剥洋葱:
-
请求阶段(外层到内层):从第一个中间件开始,遇到
await next()就进入下一个中间件 - 响应阶段(内层到外层):最内层中间件执行完毕后,依次返回到外层中间件
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);
};
}
这个函数的精妙之处:
-
闭包保存状态 - 通过闭包保存
index,防止next()被重复调用,这是一个重要的安全检查 -
递归调用链 -
dispatch(i)执行当前中间件,并将dispatch(i + 1)作为next传入 - Promise 包装 - 所有中间件都被包装成 Promise,支持 async/await 语法
-
懒执行 - 只有当中间件调用
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
关键理解点:
-
同步执行 - 当
middleware1调用next()时,middleware2会立即开始执行 -
栈式回溯 - 当
middleware3执行完毕后,控制权会依次返回到middleware2和middleware1的next()之后 - 洋葱结构 - 这就形成了"进入"和"退出"两个阶段,像剥洋葱一样
执行顺序详解:
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() 前后打断点,单步执行体会代码的具体执行过程。这样能够更直观地理解洋葱模型的运作机制。
![]()
四、总结
通过对 Koa 源码的深入分析,我们可以看到它的设计哲学:极简、优雅、灵活。
参考资源
如果觉得有帮助,欢迎点赞收藏,也欢迎在评论区交流讨论!
国际银价突破67美元/盎司,4只白银概念股跑出翻倍行情
Promise :从基础原理到高级实践
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 - 无法使用
return或throw控制流程 - 多个异步操作的组合(如并行、竞态)实现复杂
1.2 Promise 的优势
-
链式调用:通过
.then()实现线性流程 -
统一错误处理:通过
.catch()捕获整个链中的错误 -
组合能力:支持
Promise.all、Promise.race等高级模式 - 与 async/await 无缝集成
二、Promise 基础概念
2.1 什么是 Promise?
Promise 是一个表示异步操作最终完成或失败的对象。
它有三种状态(State):
- pending(待定) :初始状态,既不是成功也不是失败
- fulfilled(已成功) :操作成功完成
- rejected(已失败) :操作失败
⚠️ 状态不可逆:
一旦 Promise 从pending变为fulfilled或rejected,状态将永久固定,不能再改变。
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 最佳实践
-
始终处理错误:使用
.catch()或try/catch - 避免嵌套 Promise:使用链式调用或 async/await
-
明确返回值:在
.then()中显式return -
合理使用组合方法:
all、race、allSettled - 不要混合回调与 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 应用日益复杂,异步操作无处不在。
智元江苏具身智能产业基地战略合作项目在无锡签约落地
脚手架开发工具——判断文件是否存在 path-exists
简介
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
在现代 Web 开发中,前端与后端的数据交互是构建动态应用的核心。围绕这一需求,诞生了多个关键技术与工具:XMLHttpRequest(XHR) 、AJAX、Axios 和 Fetch 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 |
响应类型(json、blob、arraybuffer 等) |
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 为例:
- 调用
fetch(url)→ 创建Request对象 - 浏览器主线程 → 网络服务线程(Network Service)
- 网络线程发起 HTTP 请求(复用连接池、DNS 缓存等)
- 收到响应头 → 立即 resolve Promise(返回
Response对象) - 响应体通过 ReadableStream 逐步传输到 JS 主线程
- 调用
.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配置
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中安装插件
![]()
根目录下新建配置文件 .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规则的
![]()
4.配置vscode插件eslint和Prettier ESlint,设置默认格式化工具为Prettier ESlint,让eslint直接报错prettier中的配置,有时修改了.prettierrc中的配置需要重启Prettier ESlint插件才能生效。
![]()
![]()
对于
.eslintignore文件缺失时eslint会使用.gitignore文件
Vue3条件渲染中v-if系列指令如何合理使用与规避错误?
一、Vue3条件渲染的核心概念
在Vue3中,条件渲染是指根据响应式数据的真假,决定是否在页面上渲染某个元素或组件。而v-if、v-else、v-else-if
这组指令,就是实现条件渲染的“核心工具”——它们像一套“逻辑开关”,帮你精准控制DOM的显示与隐藏。
二、v-if系列指令的语法与用法
我们先逐个拆解每个指令的作用,再通过实际案例串起来用。
2.1 v-if:基础条件判断
v-if是最基础的条件指令,它的语法很简单:
<元素 v-if="条件表达式">要渲染的内容</元素>
-
条件表达式:可以是任何返回
true或false的JavaScript表达式(比如isLogin、score >= 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的“销毁/重建”逻辑。
往期文章归档
- Vue3动态样式控制:ref、reactive、watch与computed的应用场景与区别是什么?
- Vue3中动态样式数组的后项覆盖规则如何与计算属性结合实现复杂状态样式管理?
- Vue浅响应式如何解决深层响应式的性能问题?适用场景有哪些? - cmdragon's Blog
- Vue 3组合式API中ref与reactive的核心响应式差异及使用最佳实践是什么? - cmdragon's Blog
- Vue 3组合式API中ref与reactive的核心响应式差异及使用最佳实践是什么? - cmdragon's Blog
- Vue3响应式系统中,对象新增属性、数组改索引、原始值代理的问题如何解决? - cmdragon's Blog
- Vue 3中watch侦听器的正确使用姿势你掌握了吗?深度监听、与watchEffect的差异及常见报错解析 - cmdragon's Blog
- Vue响应式声明的API差异、底层原理与常见陷阱你都搞懂了吗 - cmdragon's Blog
- Vue响应式声明的API差异、底层原理与常见陷阱你都搞懂了吗 - cmdragon's Blog
- 为什么Vue 3需要ref函数?它的响应式原理与正确用法是什么? - cmdragon's Blog
- Vue 3中reactive函数如何通过Proxy实现响应式?使用时要避开哪些误区? - cmdragon's Blog
- Vue3响应式系统的底层原理与实践要点你真的懂吗? - cmdragon's Blog
- Vue 3模板如何通过编译三阶段实现从声明式语法到高效渲染的跨越 - cmdragon's Blog
- 快速入门Vue模板引用:从收DOM“快递”到调子组件方法,你玩明白了吗? - cmdragon's Blog
- 快速入门Vue模板里的JS表达式有啥不能碰?计算属性为啥比方法更能打? - cmdragon's Blog
- 快速入门Vue的v-model表单绑定:语法糖、动态值、修饰符的小技巧你都掌握了吗? - cmdragon's Blog
- 快速入门Vue3事件处理的挑战题:v-on、修饰符、自定义事件你能通关吗? - cmdragon's Blog
- 快速入门Vue3的v-指令:数据和DOM的“翻译官”到底有多少本事? - cmdragon's Blog
- 快速入门Vue3,插值、动态绑定和避坑技巧你都搞懂了吗? - cmdragon's Blog
- 想让PostgreSQL快到飞起?先找健康密码还是先换引擎? - cmdragon's Blog
- 想让PostgreSQL查询快到飞起?分区表、物化视图、并行查询这三招灵不灵? - cmdragon's Blog
- 子查询总拖慢查询?把它变成连接就能解决? - cmdragon's Blog
- PostgreSQL全表扫描慢到崩溃?建索引+改查询+更统计信息三招能破? - cmdragon's Blog
- 复杂查询总拖后腿?PostgreSQL多列索引+覆盖索引的神仙技巧你get没? - cmdragon's Blog
- 只给表子集建索引?用函数结果建索引?PostgreSQL这俩操作凭啥能省空间又加速? - cmdragon's Blog
- B-tree索引像字典查词一样工作?那哪些数据库查询它能加速,哪些不能? - cmdragon's Blog
- 想抓PostgreSQL里的慢SQL?pg_stat_statements基础黑匣子和pg_stat_monitor时间窗,谁能帮你更准揪出性能小偷? - cmdragon's Blog
- PostgreSQL的“时光机”MVCC和锁机制是怎么搞定高并发的? - cmdragon's Blog
- PostgreSQL性能暴涨的关键?内存IO并发参数居然要这么设置? - cmdragon's Blog
- 大表查询慢到翻遍整个书架?PostgreSQL分区表教你怎么“分类”才高效
- PostgreSQL 查询慢?是不是忘了优化 GROUP BY、ORDER BY 和窗口函数? - cmdragon's Blog
- PostgreSQL里的子查询和CTE居然在性能上“掐架”?到底该站哪边? - cmdragon's Blog
- PostgreSQL选Join策略有啥小九九?Nested Loop/Merge/Hash谁是它的菜? - cmdragon's Blog
- PostgreSQL新手SQL总翻车?这7个性能陷阱你踩过没? - cmdragon's Blog
- PostgreSQL索引选B-Tree还是GiST?“瑞士军刀”和“多面手”的差别你居然还不知道? - cmdragon's Blog
- 想知道数据库怎么给查询“算成本选路线”?EXPLAIN能帮你看明白? - cmdragon's Blog
- PostgreSQL处理SQL居然像做蛋糕?解析到执行的4步里藏着多少查询优化的小心机? - cmdragon's Blog
- PostgreSQL备份不是复制文件?物理vs逻辑咋选?误删还能精准恢复到1分钟前? - cmdragon's Blog
- 转账不翻车、并发不干扰,PostgreSQL的ACID特性到底有啥魔法? - cmdragon's Blog
- 银行转账不白扣钱、电商下单不超卖,PostgreSQL事务的诀窍是啥? - cmdragon's Blog
- PostgreSQL里的PL/pgSQL到底是啥?能让SQL从“说目标”变“讲步骤”? - cmdragon's Blog
- PostgreSQL视图不存数据?那它怎么简化查询还能递归生成序列和控制权限? - cmdragon's Blog
- PostgreSQL索引这么玩,才能让你的查询真的“飞”起来? - cmdragon's Blog
- PostgreSQL的表关系和约束,咋帮你搞定用户订单不混乱、学生选课不重复? - cmdragon's Blog
- PostgreSQL查询的筛子、排序、聚合、分组?你会用它们搞定数据吗? - cmdragon's Blog
- PostgreSQL数据类型怎么选才高效不踩坑? - cmdragon's Blog
- 想解锁PostgreSQL查询从基础到进阶的核心知识点?你都get了吗? - cmdragon's Blog
- PostgreSQL DELETE居然有这些操作?返回数据、连表删你试过没? - cmdragon's Blog
- PostgreSQL UPDATE语句怎么玩?从改邮箱到批量更新的避坑技巧你都会吗? - cmdragon's Blog
- PostgreSQL插入数据还在逐条敲?批量、冲突处理、返回自增ID的技巧你会吗? - cmdragon's Blog
- PostgreSQL的“仓库-房间-货架”游戏,你能建出电商数据库和表吗? - cmdragon's Blog
- PostgreSQL 17安装总翻车?Windows/macOS/Linux避坑指南帮你搞定? - cmdragon's Blog
- 能当关系型数据库还能玩对象特性,能拆复杂查询还能自动管库存,PostgreSQL凭什么这么香? - cmdragon's Blog
- 给接口加新字段又不搞崩老客户端?FastAPI的多版本API靠哪三招实现? - cmdragon's Blog
- 流量突增要搞崩FastAPI?熔断测试是怎么防系统雪崩的? - cmdragon's Blog
- FastAPI秒杀库存总变负数?Redis分布式锁能帮你守住底线吗 - cmdragon's Blog
- FastAPI的CI流水线怎么自动测端点,还能让Allure报告美到犯规? - cmdragon's Blog
- 如何用GitHub Actions为FastAPI项目打造自动化测试流水线? - cmdragon's Blog
- 如何用Git Hook和CI流水线为FastAPI项目保驾护航? - cmdragon's Blog
免费好用的热门在线工具
- RAID 计算器 - 应用商店 | By cmdragon
- 在线PS - 应用商店 | By cmdragon
- Mermaid 在线编辑器 - 应用商店 | By cmdragon
- 数学求解计算器 - 应用商店 | By cmdragon
- 智能提词器 - 应用商店 | By cmdragon
- 魔法简历 - 应用商店 | By cmdragon
- Image Puzzle Tool - 图片拼图工具 | By cmdragon
- 字幕下载工具 - 应用商店 | By cmdragon
- 歌词生成工具 - 应用商店 | By cmdragon
- 网盘资源聚合搜索 - 应用商店 | By cmdragon
- ASCII字符画生成器 - 应用商店 | By cmdragon
- JSON Web Tokens 工具 - 应用商店 | By cmdragon
- Bcrypt 密码工具 - 应用商店 | By cmdragon
- GIF 合成器 - 应用商店 | By cmdragon
- GIF 分解器 - 应用商店 | By cmdragon
- 文本隐写术 - 应用商店 | By cmdragon
- CMDragon 在线工具 - 高级AI工具箱与开发者套件 | 免费好用的在线工具
- 应用商店 - 发现1000+提升效率与开发的AI工具和实用程序 | 免费好用的在线工具
- CMDragon 更新日志 - 最新更新、功能与改进 | 免费好用的在线工具
- 支持我们 - 成为赞助者 | 免费好用的在线工具
- AI文本生成图像 - 应用商店 | 免费好用的在线工具
- 临时邮箱 - 应用商店 | 免费好用的在线工具
- 二维码解析器 - 应用商店 | 免费好用的在线工具
- 文本转思维导图 - 应用商店 | 免费好用的在线工具
- 正则表达式可视化工具 - 应用商店 | 免费好用的在线工具
- 文件隐写工具 - 应用商店 | 免费好用的在线工具
- IPTV 频道探索器 - 应用商店 | 免费好用的在线工具
- 快传 - 应用商店 | 免费好用的在线工具
- 随机抽奖工具 - 应用商店 | 免费好用的在线工具
- 动漫场景查找器 - 应用商店 | 免费好用的在线工具
- 时间工具箱 - 应用商店 | 免费好用的在线工具
- 网速测试 - 应用商店 | 免费好用的在线工具
- AI 智能抠图工具 - 应用商店 | 免费好用的在线工具
- 背景替换工具 - 应用商店 | 免费好用的在线工具
- 艺术二维码生成器 - 应用商店 | 免费好用的在线工具
- Open Graph 元标签生成器 - 应用商店 | 免费好用的在线工具
- 图像对比工具 - 应用商店 | 免费好用的在线工具
- 图片压缩专业版 - 应用商店 | 免费好用的在线工具
- 密码生成器 - 应用商店 | 免费好用的在线工具
- SVG优化器 - 应用商店 | 免费好用的在线工具
- 调色板生成器 - 应用商店 | 免费好用的在线工具
- 在线节拍器 - 应用商店 | 免费好用的在线工具
- IP归属地查询 - 应用商店 | 免费好用的在线工具
- CSS网格布局生成器 - 应用商店 | 免费好用的在线工具
- 邮箱验证工具 - 应用商店 | 免费好用的在线工具
- 书法练习字帖 - 应用商店 | 免费好用的在线工具
- 金融计算器套件 - 应用商店 | 免费好用的在线工具
- 中国亲戚关系计算器 - 应用商店 | 免费好用的在线工具
- Protocol Buffer 工具箱 - 应用商店 | 免费好用的在线工具
- IP归属地查询 - 应用商店 | 免费好用的在线工具
- 图片无损放大 - 应用商店 | 免费好用的在线工具
- 文本比较工具 - 应用商店 | 免费好用的在线工具
- IP批量查询工具 - 应用商店 | 免费好用的在线工具
- 域名查询工具 - 应用商店 | 免费好用的在线工具
- DNS工具箱 - 应用商店 | 免费好用的在线工具
- 网站图标生成器 - 应用商店 | 免费好用的在线工具
- XML Sitemap
2.2 v-else:补充默认分支
如果v-if的条件不满足,你可以用v-else添加一个“默认选项”。但要注意:v-else必须紧跟在v-if或v-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的值(比如改成95或50),会看到等级自动切换——这就是多分支条件渲染的实际效果。
三、条件渲染的流程逻辑(附流程图)
为了更直观理解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-if和v-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-else或v-else-if没有紧跟对应的v-if(中间有其他元素)。
比如:<div v-if="isShow"></div> <p>无关内容</p> <!-- 中间的p元素导致错误 --> <div v-else></div> -
解决:删除中间的无关元素,让
v-else直接紧跟v-if。 -
预防:写
v-else前,先检查前面的元素是否是v-if或v-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
简介
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开发体验
背景
eslint-plugin-tailwindcss插件的no-unnecessary-arbitrary-value无法对所有的任意值进行校验,比如h-[48px]、text-[#f5f5f5]无法校验出来。但tailwindcss的预设值太多了,一个不小心可能就又写了一个没有必要的任意值。为了避免这种情况,我们需要自己实现一个检测任意值的eslint插件。
插件地址:eslint-plugin-tailwind-no-preset-class
首先来看下效果
no-unnecessary-arbitrary-value 无法检测的情况
![]()
使用自定义的:eslint-plugin-tailwind-no-preset-class插件,完美完成了校验
![]()
创建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