阅读视图
JSyncQueue——一个开箱即用的鸿蒙异步任务同步队列
零、JSyncQueue
JSyncQueue 是一个开箱即用的鸿蒙异步任务同步队列。
一、JSyncQueue 有什么作用
在鸿蒙应用开发中,有时需要让多个异步任务按顺序执行,例如状态的转换处理,如果不加控制,会因为执行顺序混乱而产生一些莫名其妙的问题。 所以 JSyncQueue 提供了一个简洁的解决方案:
- 保证顺序执行:所有任务严格按照入队顺序执行,即使任务内部有异步操作也能保证顺序
- 两种执行模式:支持 "立即执行" 和 "延时执行" 两种模式,可以满足不同场景需求
- 两种任务类型:支持向同步队列添加 "Message 类型任务" 和 "Runnable 类型任务"
- 任务取消和管理:可随时取消指定任务或清空整个队列
-
获取任务结果:通过任务的
getResult()获取执行结果
项目架构如下图所示:
二、如何安装 JSyncQueue
第一种方式: 在需要使用 JSyncQueue 的模块中运行以下命令
ohpm install jsyncqueue
第二种方式: 在需要使用 JSyncQueue 的模块 oh-package.json5 中添加以下依赖
{
"name": "sample",
"version": "1.0.0",
"description": "Please describe the basic information.",
"main": "",
"author": "",
"license": "",
"dependencies": {
"jsyncqueue": "1.0.0" // 添加这一行,请根据需要修改版本号
}
}
三、JSyncQueue API 介绍
3-1、JSyncQueue 类
构造函数
constructor(queueName: string)
创建一个同步队列实例。
-
queueName: 队列名称,用于标识和调试
方法
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
post(runnable) |
runnable: (taskId: number) => Promise<Any> |
Task |
立即执行闭包 |
postDelay(runnable, delay) |
runnable: (taskId: number) => Promise<Any>, delay: number
|
Task |
延时 delay 毫秒执行闭包 |
sendMessage(message) |
message: Message |
Task |
立即发送消息 |
sendMessageDelay(message, delay) |
message: Message, delay: number
|
Task |
延时 delay 毫秒发送消息 |
cancel(taskId) |
taskId: number |
void |
取消指定任务 |
clear() |
- | void |
清空队列中所有等待的任务 |
dumpInfo() |
- | string |
获取队列调试信息 |
onHandleMessage(message, taskId) |
message: Message, taskId: number
|
Promise<Any> |
消息处理方法,子类可重写 |
属性
| 属性 | 类型 | 说明 |
|---|---|---|
queueName |
string |
队列名称(只读) |
length |
number |
当前队列中的任务数量(只读) |
3-2、Message 接口
interface Message {
what: string // 消息类型
data: Any // 消息数据
}
3-3、Task 接口
所有添加的任务,包括“Message 类型任务”和“Runnable 类型任务”,均会返回该类型实例,通过该实例可以“取消任务”、“获取任务结果”、“任务 Id”。
interface Task {
cancel(): void // 取消任务
getResult(): Promise<Any> // 获取任务结果
getTaskId(): number // 获取任务 ID
}
3-4、异常类型
JSyncQueueCancelException
当任务被取消时,会抛出该类型的异常。
interface JSyncQueueCancelException {
message: string
}
JSyncQueueException
当 JSyncQueue 内部发生异常时,会抛出该类型的异常。
值得注意:使用者编写的逻辑中抛出的异常会原封不动的抛到
Task.getResult().catch中,而不是以JSyncQueueException类型抛出。
interface JSyncQueueException {
message: string
}
四、如何使用 JSyncQueue
4-1、使用 JSyncQueue 创建同步队列
如果你处理的场景均是简单的一次性任务,那么直接使用 JSyncQueue 创建一个同步队列,并压入 Runnable 闭包即可。
以下代码展示的逻辑细节:
- 代码中使用了 delay 函数模拟了两次耗时操作,并且返回结果
- 外部通过
Task类型实例接收返回结果,并且打印 - 在第四次循环(即 i 为 3)的时候,会模拟抛出异常,异常内容会原封不动的抛到
catch中
值得注意:
- 立即执行任务会严格按入队顺序执行
- 任务结果的接收处理(即对
Task.getResult()的处理)和JSyncQueue对任务的处理是不保证顺序的,因为Task.getResult()的处理已不在队列范围内
immediatelyJSyncQueue: JSyncQueue = new JSyncQueue("ImmediatelyJSyncQueue")
for (let i = 0; i < 5; ++i) {
const task = this.immediatelyJSyncQueue.post(async () => {
const delayTime1 = Math.round(Math.random() * 500)
Log.i(TAG, `【添加5个Runnable】执行逻辑 i=${i} 第一段 将会模拟耗时=${delayTime1}`)
await this.delay(delayTime1)
if (i == 3) {
throw { message: "模拟异常" } as Error
}
const delayTime2 = Math.round(Math.random() * 500)
Log.i(TAG, `【添加5个Runnable】执行逻辑 i=${i} 第二段 将会模拟耗时=${delayTime2}`)
await this.delay(delayTime2)
return `jiangpengyong-添加5个Runnable ${i}`
})
task.getResult()
.then((result) => {
Log.i(TAG, `【添加5个Runnable-执行成功】i=${i} result=${result}`)
})
.catch((e: Error) => {
Log.e(TAG, `【添加5个Runnable-执行异常】i=${i} e=${JSON.stringify(e)}`)
})
.finally(() => {
Log.i(TAG, `【添加5个Runnable-执行结束】i=${i}`)
})
}
// ========================================= 输出日志 =========================================
// 【添加5个Runnable】执行逻辑 i=0 第一段 将会模拟耗时=239
// 【添加5个Runnable】执行逻辑 i=0 第二段 将会模拟耗时=315
// 【添加5个Runnable】执行逻辑 i=1 第一段 将会模拟耗时=379
// 【添加5个Runnable-执行成功】i=0 result=jiangpengyong-添加5个Runnable 0
// 【添加5个Runnable-执行结束】i=0
// 【添加5个Runnable】执行逻辑 i=1 第二段 将会模拟耗时=391
// 【添加5个Runnable】执行逻辑 i=2 第一段 将会模拟耗时=499
// 【添加5个Runnable-执行成功】i=1 result=jiangpengyong-添加5个Runnable 1
// 【添加5个Runnable-执行结束】i=1
// 【添加5个Runnable】执行逻辑 i=2 第二段 将会模拟耗时=395
// 【添加5个Runnable】执行逻辑 i=3 第一段 将会模拟耗时=478
// 【添加5个Runnable-执行成功】i=2 result=jiangpengyong-添加5个Runnable 2
// 【添加5个Runnable-执行结束】i=2
// 【添加5个Runnable】执行逻辑 i=4 第一段 将会模拟耗时=166
// 【添加5个Runnable-执行异常】i=3 e={"message":"模拟异常"}
// 【添加5个Runnable-执行结束】i=3
// 【添加5个Runnable】执行逻辑 i=4 第二段 将会模拟耗时=33
// 【添加5个Runnable-执行成功】i=4 result=jiangpengyong-添加5个Runnable 4
// 【添加5个Runnable-执行结束】i=4
取消同步任务
通过返回的 Task 类型实例调用 cancel 方法可以进行取消任务。
下面的代码会取消第四次任务,所以在日志中会看到对应的取消异常,并且不会执行该任务。
let task: Task | undefined
for (let i = 0; i < 5; ++i) {
const tempTask = this.immediatelyJSyncQueue.post(async () => {
const delayTime1 = Math.round(Math.random() * 500)
Log.i(TAG, `【移除Runnable】执行逻辑 i=${i} 第一段 将会模拟耗时=${delayTime1}`)
await this.delay(delayTime1)
const delayTime2 = Math.round(Math.random() * 500)
Log.i(TAG, `【移除Runnable】执行逻辑 i=${i} 第二段 将会模拟耗时=${delayTime2}`)
await this.delay(delayTime2)
if (i == 2) {
throw { message: "模拟异常" } as Error
}
return `jiangpengyong-移除Runnable ${i}`
})
tempTask.getResult().then((result) => {
Log.i(TAG, `【移除Runnable】执行成功 i=${i} result=${result}`)
}).catch((e: Any) => {
Log.e(TAG, `【移除Runnable】执行异常 i=${i} e=${JSON.stringify(e)}`)
}).finally(() => {
Log.i(TAG, `【移除Runnable】执行完成 i=${i}`)
})
if (i == 3) {
task = tempTask
}
}
Log.i(TAG, `【移除Runnable】取消任务 task=${JSON.stringify(task)}`)
task?.cancel()
// ========================================= 输出日志 =========================================
// 【移除Runnable】执行逻辑 i=0 第一段 将会模拟耗时=263
// 【移除Runnable】取消任务 task={"taskId":13,"queue":{},"promise":{}}
// 【移除Runnable】执行异常 i=3 e={"message":"Cancel task by cancel function."}
// 【移除Runnable】执行完成 i=3
// 【移除Runnable】执行逻辑 i=0 第二段 将会模拟耗时=474
// 【移除Runnable】执行逻辑 i=1 第一段 将会模拟耗时=318
// 【移除Runnable】执行成功 i=0 result=jiangpengyong-移除Runnable 0
// 【移除Runnable】执行完成 i=0
// 【移除Runnable】执行逻辑 i=1 第二段 将会模拟耗时=6
// 【移除Runnable】执行逻辑 i=2 第一段 将会模拟耗时=406
// 【移除Runnable】执行成功 i=1 result=jiangpengyong-移除Runnable 1
// 【移除Runnable】执行完成 i=1
// 【移除Runnable】执行逻辑 i=2 第二段 将会模拟耗时=212
// 【移除Runnable】执行逻辑 i=4 第一段 将会模拟耗时=226
// 【移除Runnable】执行异常 i=2 e={"message":"模拟异常"}
// 【移除Runnable】执行完成 i=2
// 【移除Runnable】执行逻辑 i=4 第二段 将会模拟耗时=439
// 【移除Runnable】执行成功 i=4 result=jiangpengyong-移除Runnable 4
// 【移除Runnable】执行完成 i=4
延时执行 Runnable 类型任务
添加延时任务只需改用 postDelay 方法并传入延时参数
- 下面代码记录了添加任务到真正执行的延时,通过
realDelay参数可以查看 - 使用了
delay函数模拟了两次耗时操作,并模拟返回了处理结果 - 第四次任务抛出了异常,异常消息会原封不动的在
catch的日志展示 - 因为延时任务的添加是按索引进行累加的,所以添加顺序其实并没变化,从最后的日志输出可以看到保证了执行顺序
for (let i = 0; i < 5; ++i) {
const startTime = systemDateTime.getTime(false)
const delayTime = i * 100
const task = this.delayJSyncQueue.postDelay(async () => {
const endTime = systemDateTime.getTime(false)
const realDelay = endTime - startTime
const delayTime1 = Math.round(Math.random() * 500)
Log.i(TAG, `【添加5个Runnable】执行逻辑 delay=${delayTime} realDelay=${realDelay} i=${i} 第一段 将会模拟耗时=${delayTime1}`)
await this.delay(delayTime1)
const delayTime2 = Math.round(Math.random() * 500)
Log.i(TAG, `【添加5个Runnable】执行逻辑 i=${i} 第二段 将会模拟耗时=${delayTime2}`)
await this.delay(delayTime2)
if (i == 3) {
throw { message: "模拟异常" } as Error
}
return `jiangpengyong-添加5个Runnable ${i}`
}, delayTime)
task.getResult()
.then((result) => {
Log.i(TAG, `【添加5个Runnable】执行成功 i=${i} result=${result}`)
})
.catch((e: Error) => {
Log.e(TAG, `【添加5个Runnable】执行异常 i=${i} e=${JSON.stringify(e)}`)
})
.finally(() => {
Log.i(TAG, `【添加5个Runnable】执行结束 i=${i}`)
})
}
// ========================================= 输出日志 =========================================
// 【添加5个Runnable】执行逻辑 delay=0 realDelay=1 i=0 第一段 将会模拟耗时=473
// 【添加5个Runnable】执行逻辑 i=0 第二段 将会模拟耗时=410
// 【添加5个Runnable】执行逻辑 delay=100 realDelay=888 i=1 第一段 将会模拟耗时=178
// 【添加5个Runnable】执行成功 i=0 result=jiangpengyong-添加5个Runnable 0
// 【添加5个Runnable】执行结束 i=0
// 【添加5个Runnable】执行逻辑 i=1 第二段 将会模拟耗时=204
// 【添加5个Runnable】执行逻辑 delay=200 realDelay=1272 i=2 第一段 将会模拟耗时=410
// 【添加5个Runnable】执行成功 i=1 result=jiangpengyong-添加5个Runnable 1
// 【添加5个Runnable】执行结束 i=1
// 【添加5个Runnable】执行逻辑 i=2 第二段 将会模拟耗时=36
// 【添加5个Runnable】执行逻辑 delay=300 realDelay=1721 i=3 第一段 将会模拟耗时=475
// 【添加5个Runnable】执行成功 i=2 result=jiangpengyong-添加5个Runnable 2
// 【添加5个Runnable】执行结束 i=2
// 【添加5个Runnable】执行逻辑 i=3 第二段 将会模拟耗时=483
// 【添加5个Runnable】执行逻辑 delay=400 realDelay=2686 i=4 第一段 将会模拟耗时=9
// 【添加5个Runnable】执行异常 i=3 e={"message":"模拟异常"}
// 【添加5个Runnable】执行结束 i=3
// 【添加5个Runnable】执行逻辑 i=4 第二段 将会模拟耗时=395
// 【添加5个Runnable】执行成功 i=4 result=jiangpengyong-添加5个Runnable 4
// 【添加5个Runnable】执行结束 i=4
取消延时任务
延时任务的取消操作和立即执行的取消操作是完全一样的,都是通过返回的 Task 实例调用 cancel 方法,这里就不再赘述。
4-2、继承 JSyncQueue 创建同步队列
如果你的同步逻辑需要集中管理或进行复用,可以考虑 Message 类型任务。
处理 Message 类型任务,需要继承 JSyncQueue 实现 onHandleMessage 方法,在该方法中会按入队顺序接收到 Message :
- 通过
Message.what属性区分不同类别消息实现不同处理逻辑 - 通过
Message.data属性可以获取外部传入的数据,数据类型是Any可以是任意类型数据,使用者自行转换为真实类型进行逻辑处理
具体操作如下:
- 定义一个
ImmediatelyQueue类继承JSyncQueue,实现onHandleMessage方法 - 创建一个
ImmediatelyQueue实例,并通过这个实例进行发送 Message 消息,同步队列会按入队顺序一个个进行分发给该实例的onHandleMessage方法进行处理
// 自定义 JSyncQueue
export class ImmediatelyQueue extends JSyncQueue {
private count = 0
async onHandleMessage(message: Message): Promise<Any> {
switch (message.what) {
case "say_hello": {
const name = message.data["name"]
this.count += 1
const delayTime1 = Math.round(Math.random() * 500)
Log.i("ImmediatelyQueue", `【say_hello】执行逻辑 第一段 将会模拟耗时=${delayTime1}`)
await this.delay(delayTime1)
const delayTime2 = Math.round(Math.random() * 500)
Log.i("ImmediatelyQueue", `【say_hello】执行逻辑 第二段 将会模拟耗时=${delayTime2}`)
await this.delay(delayTime2)
if (this.count % 10 == 5) {
throw { message: "模拟异常" }
}
return `你好,${name}。这是第${this.count}次打招呼。`
}
// ... 其他 what 处理逻辑
}
return undefined
}
private async delay(ms: number) {
return new Promise<Any>(resolve => setTimeout(resolve, ms))
}
}
// 使用逻辑
immediatelyQueue: JSyncQueue = new ImmediatelyQueue("ImmediatelyQueue")
for (let i = 0; i < 5; ++i) {
const tempTask = this.immediatelyQueue.sendMessage({
what: `say_hello`,
data: { name: '江澎涌', age: 20 + i },
})
tempTask.getResult()
.then((result) => {
Log.i(TAG, `【添加5个Message】执行成功 i=${i} result=${result}`)
})
.catch((e: Error) => {
Log.e(TAG, `【添加5个Message】执行异常 i=${i} e=${JSON.stringify(e)}`)
})
.finally(() => {
Log.i(TAG, `【添加5个Message】执行结束i=${i}`)
})
}
// ========================================= 输出日志 =========================================
// onHandleMessage message={"what":"say_hello","data":{"name":"江澎涌","age":20}}
// 【say_hello】执行逻辑 第一段 将会模拟耗时=92
// 【say_hello】执行逻辑 第二段 将会模拟耗时=143
// onHandleMessage message={"what":"say_hello","data":{"name":"江澎涌","age":21}}
// 【say_hello】执行逻辑 第一段 将会模拟耗时=276
// 【say_hello】执行逻辑 第二段 将会模拟耗时=377
// onHandleMessage message={"what":"say_hello","data":{"name":"江澎涌","age":22}}
// 【say_hello】执行逻辑 第一段 将会模拟耗时=120
// 【say_hello】执行逻辑 第二段 将会模拟耗时=223
// onHandleMessage message={"what":"say_hello","data":{"name":"江澎涌","age":23}}
// 【say_hello】执行逻辑 第一段 将会模拟耗时=424
// 【say_hello】执行逻辑 第二段 将会模拟耗时=444
// onHandleMessage message={"what":"say_hello","data":{"name":"江澎涌","age":24}}
// 【say_hello】执行逻辑 第一段 将会模拟耗时=181
// 【say_hello】执行逻辑 第二段 将会模拟耗时=402
移除 Message 消息
使用 sendMessage 方法压入 “Message 类型任务” 同样会返回 Task 类型实例,调用该实例的 cancel 方法就可以取消该任务。
下列代码会取消第二个任务,所以不会看到 "age":11 的消息。
let task: Task | undefined
for (let i = 0; i < 5; ++i) {
const tempTask = this.immediatelyQueue.sendMessage({
what: `remove_message`,
data: { name: 'jiang peng yong', age: 10 + i },
})
tempTask.getResult().then((result) => {
Log.i(TAG, `【移除Message】执行成功 i=${i} result=${result}`)
}).catch((e: Any) => {
Log.e(TAG, `【移除Message】执行异常 i=${i} e=${JSON.stringify(e)}`)
}).finally(() => {
Log.i(TAG, `【移除Message】执行完成 i=${i}`)
})
if (i == 1) {
task = tempTask
}
}
Log.i(TAG, `【移除Message】取消任务 task=${JSON.stringify(task)}`)
task?.cancel()
// ========================================= 输出日志 =========================================
// onHandleMessage message={"what":"remove_message","data":{"name":"jiang peng yong","age":10}}
// 【remove_message】执行逻辑 第一段 将会模拟耗时=497
// 【remove_message】执行逻辑 第二段 将会模拟耗时=397
// onHandleMessage message={"what":"remove_message","data":{"name":"jiang peng yong","age":12}}
// 【remove_message】执行逻辑 第一段 将会模拟耗时=162
// 【remove_message】执行逻辑 第二段 将会模拟耗时=283
// onHandleMessage message={"what":"remove_message","data":{"name":"jiang peng yong","age":13}}
// 【remove_message】执行逻辑 第一段 将会模拟耗时=193
// 【remove_message】执行逻辑 第二段 将会模拟耗时=93
// onHandleMessage message={"what":"remove_message","data":{"name":"jiang peng yong","age":14}}
// 【remove_message】执行逻辑 第一段 将会模拟耗时=359
// 【remove_message】执行逻辑 第二段 将会模拟耗时=145
延时执行 Message 类型任务
- 定义一个
DelayQueue类继承JSyncQueue,主要重写onHandleMessage方法,用于接收处理Message - 创建
DelayQueue实例,通过这个实例调用sendMessageDelay方法即可达到相应的延时效果
export class DelayQueue extends JSyncQueue {
private count = 0
async onHandleMessage(message: Message): Promise<Any> {
Log.i("DelayQueue", `onHandleMessage message=${JSON.stringify(message)}`)
switch (message.what) {
case "say_hello": {
const name = message.data["name"]
this.count += 1
const delayTime1 = Math.round(Math.random() * 500)
Log.i("DelayQueue", `【say_hello】执行逻辑 第一段 将会模拟耗时=${delayTime1}`)
await this.delay(delayTime1)
const delayTime2 = Math.round(Math.random() * 500)
Log.i("DelayQueue", `【say_hello】执行逻辑 第二段 将会模拟耗时=${delayTime2}`)
await this.delay(delayTime2)
if (this.count % 10 == 5) {
throw { message: "模拟异常" }
}
return `Hello,${name}. This is the ${this.count} th greeting.`
}
}
return undefined
}
private async delay(ms: number) {
return new Promise<Any>(resolve => setTimeout(resolve, ms))
}
}
delayQueue: JSyncQueue = new DelayQueue("DelayQueue")
for (let i = 0; i < 5; ++i) {
const delayTime = i * 100
const task = this.delayQueue.sendMessageDelay({
what: `say_hello`,
data: { name: '江澎涌', age: 20 + i },
}, delayTime)
task.getResult()
.then((result) => {
Log.i(TAG, `【添加5个Message】执行成功 i=${i} result=${result}`)
})
.catch((e: Error) => {
Log.e(TAG, `【添加5个Message】执行异常 i=${i} e=${JSON.stringify(e)}`)
})
.finally(() => {
Log.i(TAG, `【添加5个Message】执行结束i=${i}`)
})
}
// ========================================= 输出日志 =========================================
// onHandleMessage message={"what":"say_hello","data":{"name":"江澎涌","age":20}}
// 【say_hello】执行逻辑 第一段 将会模拟耗时=356
// 【say_hello】执行逻辑 第二段 将会模拟耗时=302
// onHandleMessage message={"what":"say_hello","data":{"name":"江澎涌","age":21}}
// 【say_hello】执行逻辑 第一段 将会模拟耗时=67
// 【say_hello】执行逻辑 第二段 将会模拟耗时=344
// onHandleMessage message={"what":"say_hello","data":{"name":"江澎涌","age":22}}
// 【say_hello】执行逻辑 第一段 将会模拟耗时=339
// 【say_hello】执行逻辑 第二段 将会模拟耗时=384
// onHandleMessage message={"what":"say_hello","data":{"name":"江澎涌","age":23}}
// 【say_hello】执行逻辑 第一段 将会模拟耗时=442
// 【say_hello】执行逻辑 第二段 将会模拟耗时=392
// onHandleMessage message={"what":"say_hello","data":{"name":"江澎涌","age":24}}
// 【say_hello】执行逻辑 第一段 将会模拟耗时=443
// 【say_hello】执行逻辑 第二段 将会模拟耗时=199
取消延时的 Message 类型任务
延时任务的取消操作和立即执行的取消操作是完全一样的,都是通过返回的 Task 实例调用 cancel 方法,这里就不再赘述。
同一队列压入 Message 类型任务和 Runnable 类型任务
对 JSyncQueue 同一实例压入 Message 和 Runnable 两种类型任务是支持的,会按照压入顺序进行执行和分发。
// ImmediatelyQueue 源码就不再展示,需要可以移步 Github 上查阅
immediatelyQueue: JSyncQueue = new ImmediatelyQueue("ImmediatelyQueue")
for (let i = 0; i < 10; ++i) {
if (i % 2 == 0) {
this.immediatelyQueue.post(async () => {
const delayTime1 = Math.round(Math.random() * 500)
Log.i(TAG, `【添加10个Message和Runnable】执行逻辑 i=${i} 第一段 将会模拟耗时=${delayTime1}`)
await this.delay(delayTime1)
const delayTime2 = Math.round(Math.random() * 500)
Log.i(TAG, `【添加10个Message和Runnable】执行逻辑 i=${i} 第二段 将会模拟耗时=${delayTime2}`)
await this.delay(delayTime2)
if (i / 2 == 3) {
throw { message: "模拟异常" } as Error
}
return `小朋友-添加10个Message和Runnable ${i}`
})
} else {
this.immediatelyQueue.sendMessage({
what: `say_hello`,
data: { name: '小朋友', age: i },
})
}
}
// ========================================= 输出日志 =========================================
// 【添加10个Message和Runnable】执行逻辑 i=0 第一段 将会模拟耗时=416
// 【添加10个Message和Runnable】执行逻辑 i=0 第二段 将会模拟耗时=41
// onHandleMessage message={"what":"say_hello","data":{"name":"小朋友","age":1}}
// 【say_hello】执行逻辑 第一段 将会模拟耗时=184
// 【say_hello】执行逻辑 第二段 将会模拟耗时=63
// 【添加10个Message和Runnable】执行逻辑 i=2 第一段 将会模拟耗时=451
// 【添加10个Message和Runnable】执行逻辑 i=2 第二段 将会模拟耗时=223
// onHandleMessage message={"what":"say_hello","data":{"name":"小朋友","age":3}}
// 【say_hello】执行逻辑 第一段 将会模拟耗时=99
// 【say_hello】执行逻辑 第二段 将会模拟耗时=27
// 【添加10个Message和Runnable】执行逻辑 i=4 第一段 将会模拟耗时=273
// 【添加10个Message和Runnable】执行逻辑 i=4 第二段 将会模拟耗时=193
// onHandleMessage message={"what":"say_hello","data":{"name":"小朋友","age":5}}
// 【say_hello】执行逻辑 第一段 将会模拟耗时=20
// 【say_hello】执行逻辑 第二段 将会模拟耗时=231
// 【添加10个Message和Runnable】执行逻辑 i=6 第一段 将会模拟耗时=46
// 【添加10个Message和Runnable】执行逻辑 i=6 第二段 将会模拟耗时=198
// onHandleMessage message={"what":"say_hello","data":{"name":"小朋友","age":7}}
// 【say_hello】执行逻辑 第一段 将会模拟耗时=179
// 【say_hello】执行逻辑 第二段 将会模拟耗时=0
// 【添加10个Message和Runnable】执行逻辑 i=8 第一段 将会模拟耗时=131
// 【添加10个Message和Runnable】执行逻辑 i=8 第二段 将会模拟耗时=401
// onHandleMessage message={"what":"say_hello","data":{"name":"小朋友","age":9}}
// 【say_hello】执行逻辑 第一段 将会模拟耗时=452
// 【say_hello】执行逻辑 第二段 将会模拟耗时=40
4-3、取消队列中所有任务
对 JSyncQueue 实例调用 clear 方法,就会把队列中等待执行的任务,包括延时执行和立即执行的任务,全都取消。同时会抛出 JSyncQueueCancelException 类型异常。
for (let i = 0; i < 5; ++i) {
const task = this.immediatelyQueue.post(async () => {
const delayTime1 = Math.round(Math.random() * 500)
Log.i(TAG, `【清空队列】执行逻辑 i=${i} 第一段 将会模拟耗时=${delayTime1}`)
await this.delay(delayTime1)
const delayTime2 = Math.round(Math.random() * 500)
Log.i(TAG, `【清空队列】执行逻辑 i=${i} 第二段 将会模拟耗时=${delayTime2}`)
await this.delay(delayTime2)
return `小朋友-清空队列 ${i}`
})
task.getResult()
.then((result) => {
Log.i(TAG, `【清空队列】执行成功 i=${i} result=${result}`)
})
.catch((e: Error) => {
Log.e(TAG, `【清空队列】执行异常 i=${i} e=${JSON.stringify(e)}`)
})
.finally(() => {
Log.i(TAG, `【清空队列】执行结束 i=${i}`)
})
}
this.immediatelyQueue.clear()
// ========================================= 输出日志 =========================================
// 【清空队列】执行逻辑 i=0 第一段 将会模拟耗时=14
// 【清空队列】执行异常 i=1 e={"message":"Cancel task by clear function."}
// 【清空队列】执行异常 i=2 e={"message":"Cancel task by clear function."}
// 【清空队列】执行异常 i=3 e={"message":"Cancel task by clear function."}
// 【清空队列】执行异常 i=4 e={"message":"Cancel task by clear function."}
// 【清空队列】执行结束 i=1
// 【清空队列】执行结束 i=2
// 【清空队列】执行结束 i=3
// 【清空队列】执行结束 i=4
// 【清空队列】执行逻辑 i=0 第二段 将会模拟耗时=125
// 【清空队列】执行成功 i=0 result=小朋友-清空队列 0
// 【清空队列】执行结束 i=0
五、作者博客
csdn:blog.csdn.net/weixin_3762…
公众号:微信搜索 "江澎涌"
Vue-Data 属性避坑指南
前言
在 Vue 开发中,我们经常会遇到“明明修改了数据,视图却不更新”的尴尬场景。这通常与 Vue 的初始化顺序及响应式实现原理有关。本文将从 Data 属性的本质出发,解析响应式“丢失”的根本原因及解决方案。
一、 组件中的 Data 为什么必须是函数?
在 Vue 2 中,根实例的 data 可以是对象,但组件中的 data 必须是函数。
核心原因:数据隔离
-
对象形式:JavaScript 中的对象是引用类型。如果
data是对象,所有组件实例将共享同一个内存地址。修改实例 A 的数据,实例 B 也会跟着变。 -
函数形式:当
data是一个函数时,每次创建新实例,Vue 都会调用该函数,返回一个全新的数据对象拷贝。这保证了每个组件实例数据的独立性。
二、 Props 与 Data 的优先级之争
在组件初始化时,Vue 会按照特定的顺序处理选项。
初始化顺序
Props → Methods → Data → Computed → Watch
因为 Props 最先被初始化,所以我们可以在 data 中直接引用 props 传来的值:
// Vue 3 + TS 示例
const props = defineProps<{ initialCount: number }>();
const count = ref(props.initialCount); // 合法,因为 props 优先初始化
三、 Vue2动态添加新属性的“失效”困局
1. 故障场景
vue2中当我们直接给对象添加一个原本不存在的属性时,视图不会产生任何变化。
<p v-for="(value,key)in item" :key="key">
{{ value }}
</p>
<button@click="addProperty">动态添加新属性</button>
const app = new Vue({
el: '#app',
data: {
item: {
oldProperty: 'l日属性'
}
},
methods: {
addProperty() {
this.items.newProperty = '新属性'; // 为items添加新属性
console.log(this.items); // 输出带有newProperty的items
}
}
})
2. 原因剖析
-
Vue 2 局限性:使用
Object.defineProperty实现响应式。它只能劫持对象已有的属性。对于后来新增的属性,Vue 无法感知其getter/setter,因此无法触发视图更新。 -
Vue 3 的进化:改用
Proxy代理整个对象。Proxy可以拦截到属性的新增与删除,因此 Vue 3 不再有这个问题。
四、 解决方案(Vue 2 必备技巧)
如果你仍在使用 Vue 2,可以通过以下三种方式解决:
1. 推荐方案:Vue.set / this.$set
这是最正统的方法,它会手动将新属性转为响应式,并触发依赖更新。
语法:
this.$set(target, propertyName/index, value)
-
target:data中要修改的对象或者数组 -
propertyName/index:要添加或修改的属性名称(对于对象)或索引(对于数组) -
value:要设置的值
addProperty() {
this.$set(this.item, 'newProperty', '新属性');
}
2. 对象整体替换:Object.assign
通过创建一个包含新属性的新对象,并将这个新对象赋值给原有对象,触发 Vue 对原对象引用的变更感知。
addProperty() {
this.item = Object.assign({}, this.item, { newProperty: '新属性' });
// 或者使用展开运算符
this.item = { ...this.item, newProperty: '新属性' };
}
3. 暴力方案:$forceUpdate
迫使 Vue 重新渲染组件。
-
注意:这只是“治标”。虽然视图刷新了,但该属性依然不是响应式的。后续再次修改
newProperty时,视图依然不会动。
五、 Vue 3 + TS 最佳实践
在 Vue 3 中,借助 TypeScript 的类型定义,我们可以规避大部分因“动态添加”导致的逻辑混乱。
<script setup lang="ts">
import { reactive } from 'vue';
// 定义接口,提前声明可选属性
interface Item {
oldProperty: string;
newProperty?: string; // 声明可选属性
}
const item = reactive<Item>({
oldProperty: '旧属性'
});
const addProperty = () => {
// Vue 3 Proxy 自动处理响应式,无需 $set
item.newProperty = '新属性';
};
</script>
FossFLOW:开源等距图表工具,为技术文档注入立体活力!
文章简介:FossFLOW是一款创新的开源等距图表工具,专为技术文档设计。它通过立体视角将复杂的系统架构转化为直观的3D图表,支持拖放式操作和离线使用,让技术图表变得生动易懂。无需注册,数据安全存储在本地,并提供JSON导入导出功能。无论是Docker快速部署还是在线体验,FossFLOW都能为架构图、流程图注入立体活力,是提升技术文档表现力的得力助手。
你是否曾经为了绘制清晰的技术架构图或系统流程图而烦恼?是否觉得传统的平面图表难以表达复杂的层次关系?今天,我要向大家介绍一款令人惊艳的开源工具——FossFLOW,它能让你的技术图表瞬间变得立体、生动!
🌟 什么是FossFLOW?
FossFLOW 是一款功能强大的、开源的渐进式 Web 应用(PWA),专为创建精美的等距图表而设计。它基于 React 和 Isoflow(现已 fork 并以 fossflow 名称发布到 NPM)库构建,完全在浏览器中运行,并支持离线使用,让你随时随地都能创作出专业级的技术图表!
github地址:github.com/stan-smith/…
在线地址:stan-smith.github.io/FossFLOW/
该项目目前在github上已有17k ⭐️star
✨ 主要特性
🎨 立体图表,视觉升级
- • 创建令人惊叹的3D风格技术图表
- • 等距视角让复杂的系统架构一目了然
- • 拖放式操作,简单直观
🔒 隐私优先,安全可靠
- • 所有数据都存储在您的浏览器中
- • 无需注册,无需上传
- • 完全控制你的数据
🔄 导入导出,轻松分享
- • JSON格式导入导出
- • 快速分享你的设计
- • 完整备份功能
🚀 快速上手
🐳Docker部署
创建docker-compose.yml文件,内容如下:
services:
fossflow:
image: stnsmith/fossflow:latest
container_name: fossflow
ports:
- "5010:80"
volumes:
# 如果要禁用服务端存储,可以注释掉这行
- ./diagrams:/data/diagrams
environment:
- TZ=Asia/Shanghai
# 如果要启用服务端存储,注释掉下面这行
# - ENABLE_SERVER_STORAGE="false"
restart: unless-stopped
在docker-compose.yml 同级命令下使用以下命令启动
docker-compose up -d
到此,我们就部署完了,在浏览器中输入地址就可以访问了
🌐在线体验
直接访问:stan-smith.github.io/FossFLOW/
📱本地启动
# 克隆仓库
git clone https://github.com/stan-smith/FossFLOW
cd FossFLOW
# 安装依赖
npm install
# 启动开发服务器
npm start
🛠️ 使用指南
📈1. 创建图表
- • 点击右上角"+"按钮打开组件库
- • 从左侧拖放组件到画布
- • 或右键网格选择"Add node"
🧩2. 连接组件
- • 使用连接器显示组件关系
- • 智能对齐,保持图表整洁
- • 多层连接,表达复杂关系
✏️3. 自定义样式
- • 更改颜色、标签和属性
- • 调整位置和大小
- • 添加说明文字
🎨4. 导航操作
- • 鼠标滚轮放大缩小
- • 点击拖动平移画布
- • Ctrl+Z撤销,Ctrl+Y重做
🏗️ 技术栈
- • React - 现代化的UI框架
- • TypeScript - 类型安全的开发体验
- • Isoflow - 强大的等距图表引擎
- • PWA - 离线优先的Web应用架构
🚨缺点与不足
虽然该工具在基础功能方面表现良好,但在实际使用过程中仍存在一些明显的局限性与不足之处:
-
• 3D节点资源严重匮乏
官方提供的3D节点类型极为有限,仅包含基础的几何形状和少数预设模型,无法满足复杂三维场景的构建需求。
-
• 第三方节点生态发展不完善
第三方插件多为2D节点,在构建复杂三维场景时可能面临节点素材不足的问题。
- • 快捷操作方式还有待改进
📝 最后的话
在技术文档越来越重要的今天,一个清晰、直观的图表往往胜过千言万语。FossFLOW以其独特的等距视角,为技术图表带来了全新的可能性。无论你是架构师、开发者、技术作家还是项目经理,这款工具都值得一试。
最重要的是,它是完全免费和开源的!你可以在GitHub上找到所有源代码,自由使用、学习和改进。
一天一个开源项目(第8篇):UI/UX Pro Max Skill - AI设计智能助手,让AI帮你构建专业UI/UX
引言
"当AI能够理解设计系统、色彩理论、用户体验原则时,它就不再是简单的代码生成器,而是真正的设计伙伴。"
这是"一天一个开源项目"系列的第8篇文章。今天带你了解的项目是 UI/UX Pro Max Skill(GitHub)。
如果你正在使用Claude Code、Cursor、Windsurf等AI编程助手,并且希望它们能够生成专业、美观、符合设计规范的UI/UX代码,那么UI/UX Pro Max Skill绝对值得你深入了解。它通过24.7k+ Stars的社区认可,证明了AI设计智能的巨大价值。
你将学到什么
- UI/UX Pro Max Skill的核心架构和工作原理
- 如何为不同AI助手安装和配置这个技能
- 设计系统自动生成的机制和原理
- 智能推荐系统如何工作
- 如何构建分层设计系统(Master + Overrides)
- 与手动设计相比的优势和局限性
- 如何在实际项目中应用这个技能
前置知识
- 对AI编程助手有基本了解(Claude Code、Cursor等)
- 熟悉UI/UX设计的基本概念
- 了解设计系统的基本组成
- 对命令行工具有基本使用经验
项目背景
项目简介
UI/UX Pro Max Skill 是一个为AI编程助手提供设计智能的开源技能,通过内置的设计系统生成引擎和智能推荐系统,让AI助手能够生成专业、美观、符合设计规范的UI/UX代码。它不仅仅是一个代码生成工具,而是一个完整的设计智能系统。
项目解决的核心问题:
- AI生成的UI代码缺乏设计规范,样式混乱
- 缺乏对色彩理论、排版、间距等设计原则的理解
- 无法根据产品类型推荐合适的设计风格
- 缺少设计系统的概念,代码难以维护
- 不同AI助手平台需要重复配置设计知识
面向的用户群体:
- 使用AI编程助手的开发者
- 需要快速生成专业UI的前端开发者
- 缺乏设计背景但需要构建美观界面的开发者
- 希望提升AI生成代码质量的团队
- 需要统一设计系统的项目
作者/团队介绍
团队:nextlevelbuilder
- 背景:专注于AI工具和设计智能的开源团队
- 理念:让AI真正理解设计,而不仅仅是生成代码
- 项目定位:AI设计智能的标准解决方案
- 官网:ui-ux-pro-max-skill.nextlevelbuilder.io
项目创建时间:2024年(从GitHub活动来看是持续活跃的项目)
项目数据
- ⭐ GitHub Stars: 24.7k+(持续快速增长)
- 🍴 Forks: 2.5k+
- 📦 版本: v2.2.1(最新版本,2026年1月26日发布)
- 📄 License: MIT(完全开源,自由使用)
- 🌐 官网: ui-ux-pro-max-skill.nextlevelbuilder.io
- 📚 文档: 包含完整的使用指南和API文档
- 💬 社区: GitHub Issues和Discussions活跃
- 👥 贡献者: 27位贡献者,活跃的社区参与
项目发展历程:
- 2024年:项目创建,开始构建核心设计智能引擎
- 2024年中:添加多平台支持,扩展到20+AI助手
- 2024年底:引入分层设计系统(Master + Overrides)
- 2025年:完善CLI工具,优化安装体验
- 2026年:持续优化,社区活跃度持续提升
主要功能
核心作用
UI/UX Pro Max Skill的核心作用是为AI编程助手注入设计智能,主要功能包括:
- 自动生成设计系统:根据产品类型自动生成完整设计系统(色彩、字体、间距、组件规范)
- 智能样式推荐:基于产品领域推荐最佳样式方案
- 代码质量保障:生成符合UI/UX最佳实践的代码,自动检测设计反模式
- 多平台支持:支持Web(HTML+Tailwind、React、Vue)、移动端(SwiftUI、Jetpack Compose)、跨平台(React Native、Flutter)
- 分层设计系统:支持Master设计系统和页面级覆盖
快速开始
安装方式
UI/UX Pro Max Skill支持多种安装方式:
方式1:通过Claude Marketplace(Claude Code)
# 在Claude Code中直接安装
/plugin marketplace add nextlevelbuilder/ui-ux-pro-max-skill
/plugin install ui-ux-pro-max@ui-ux-pro-max-skill
方式2:使用CLI工具(推荐)
# 安装CLI工具
npm install -g uipro-cli
# 为你的AI助手安装技能
cd /path/to/your/project
# 支持多种AI助手
uipro init --ai claude # Claude Code
uipro init --ai cursor # Cursor
uipro init --ai windsurf # Windsurf
uipro init --ai antigravity # Antigravity
uipro init --ai copilot # GitHub Copilot
uipro init --ai codex # Codex CLI
uipro init --ai gemini # Gemini CLI
uipro init --ai opencode # OpenCode
uipro init --ai all # 所有支持的助手
其他CLI命令:
uipro versions # 查看可用版本
uipro update # 更新到最新版本
uipro init --offline # 离线安装(使用本地资源)
前置要求
Python 3.x是必需的(用于搜索脚本):
# 检查Python版本
python3 --version
# macOS安装
brew install python3
# Ubuntu/Debian安装
sudo apt update && sudo apt install python3
# Windows安装
winget install Python.Python.3.12
最简单的使用示例
Skill模式(自动激活)
支持的平台:Claude Code, Windsurf, Antigravity, Codex CLI, Continue, Gemini CLI, OpenCode, Qoder, CodeBuddy
# 直接对话,技能会自动激活
Build a landing page for my SaaS product
Workflow模式(斜杠命令)
支持的平台:Cursor, Kiro, GitHub Copilot, Roo Code
# 使用斜杠命令
/ui-ux-pro-max Build a landing page for my SaaS product
示例提示词:
Build a landing page for my SaaS product
Create a dashboard for healthcare analytics
Design a portfolio website with dark mode
Make a mobile app UI for e-commerce
Build a fintech banking app with dark theme
我通过一行提示词生成的项目管理网站首页UI界面
核心特性
- 自动设计系统生成:根据产品类型生成完整设计系统,支持10+个专业领域知识库
- 智能推荐引擎:基于产品类型推荐最佳样式,考虑用户体验和可访问性
- 多技术栈支持:Web(HTML+Tailwind、React、Vue等)、iOS(SwiftUI)、Android(Jetpack Compose)、跨平台(React Native、Flutter)
- 分层设计系统:Master设计系统 + 页面级覆盖,智能检索和优先级管理
- 设计规范检查:自动检测UI/UX反模式,提供改进建议
- CLI工具:统一安装管理,支持离线安装和版本管理
- 模板系统:基于模板的代码生成,平台特定模板支持
项目优势
| 对比项 | UI/UX Pro Max Skill | 手动设计 | 其他AI设计工具 |
|---|---|---|---|
| 设计智能 | ✅ 内置完整设计系统 | ❌ 需要手动设计 | ⚠️ 有限设计知识 |
| 多平台支持 | ✅ 20+AI助手 | ❌ 无 | ⚠️ 单一平台 |
| 自动推荐 | ✅ 智能样式推荐 | ❌ 需要经验 | ⚠️ 基础推荐 |
| 设计系统 | ✅ 自动生成 | ❌ 手动构建 | ⚠️ 部分支持 |
| 代码质量 | ✅ 符合最佳实践 | ⚠️ 依赖经验 | ⚠️ 不一致 |
| 学习曲线 | ✅ 开箱即用 | ❌ 需要学习 | ⚠️ 需要配置 |
| 社区支持 | ✅ 24.7k+ Stars | ❌ 无 | ⚠️ 有限 |
为什么选择UI/UX Pro Max Skill?
相比手动设计和其他AI设计工具,UI/UX Pro Max Skill提供完整的设计系统生成引擎、智能推荐、20+AI助手支持、10+领域知识库,开箱即用,社区活跃(24.7k+ Stars)。
项目详细剖析
架构设计
UI/UX Pro Max Skill采用模块化、可扩展的架构,主要包含以下几个核心模块:
核心架构
UI/UX Pro Max Skill
├── CLI工具层
│ ├── 安装器(uipro-cli)
│ ├── 模板生成系统
│ └── 版本管理
├── 技能层(.claude/skills/ui-ux-pro-max)
│ ├── 技能定义文件
│ ├── 提示词模板
│ └── 平台适配器
├── 数据层(data/)
│ ├── 设计知识库(CSV格式)
│ ├── 样式数据库
│ ├── 色彩方案库
│ └── 字体库
├── 脚本层(scripts/)
│ ├── search.py(搜索和推理引擎)
│ ├── 设计系统生成器
│ └── 推荐算法
└── 模板层(templates/)
├── 平台特定模板
├── 组件模板
└── 页面模板
设计系统生成引擎
核心是Python脚本search.py,实现设计系统的自动生成。流程包括:1) 分析产品类型和需求;2) 从知识库检索设计知识(色彩、字体、样式、组件、领域特定知识);3) 应用推理引擎生成设计系统;4) 验证和优化设计系统。
智能推荐系统
推荐系统基于产品类型和领域知识进行智能匹配,推荐维度包括:色彩方案(基于产品类型、目标用户、品牌调性、可访问性)、字体方案(基于可读性和品牌)、间距系统(基于平台和内容)、组件风格(基于交互需求)。通过相似度计算返回Top 3推荐方案。
分层设计系统(Master + Overrides)
支持分层设计系统,Master系统包含全局色彩、字体、间距、组件规范,页面覆盖只包含与Master不同的规则。检索逻辑:如果指定页面,先检查页面覆盖文件;如果存在,合并Master和页面规则;否则只使用Master规则。
知识库系统
UI/UX Pro Max Skill的核心是设计知识库,包含10+个领域的专业设计知识:
知识库结构
data/
├── colors.csv # 色彩方案库
├── typography.csv # 字体方案库
├── styles.csv # 样式方案库
├── components.csv # 组件规范库
└── domain/ # 领域特定知识
├── saas.csv # SaaS产品设计
├── ecommerce.csv # 电商设计
├── fintech.csv # 金融科技设计
├── healthcare.csv # 医疗健康设计
├── education.csv # 教育设计
└── ...
知识库内容示例
色彩方案库(colors.csv):
product_type,industry,tone,primary_color,secondary_color,accent_color,background_color,text_color
SaaS,B2B,professional,#2563EB,#1E40AF,#3B82F6,#FFFFFF,#1F2937
SaaS,B2C,friendly,#10B981,#059669,#34D399,#F9FAFB,#111827
Ecommerce,Retail,vibrant,#EC4899,#DB2777,#F472B6,#FFFFFF,#1F2937
Fintech,Finance,trustworthy,#1E40AF,#1E3A8A,#3B82F6,#F8FAFC,#0F172A
Healthcare,Medical,calm,#059669,#047857,#10B981,#FFFFFF,#064E3B
字体方案库(typography.csv):
product_type,heading_font,body_font,font_size_scale,line_height_scale
SaaS,Inter,Inter,1.25,1.5
Ecommerce,Poppins,Inter,1.2,1.6
Fintech,Roboto,Roboto,1.15,1.5
Healthcare,Open Sans,Open Sans,1.3,1.7
领域特定知识
每个领域都有专门的设计知识库,包含:
- 行业最佳实践:该领域的UI/UX最佳实践
- 用户行为模式:目标用户的使用习惯
- 设计趋势:当前流行的设计风格
- 可访问性要求:行业特定的可访问性标准
- 合规要求:法律法规要求(如金融、医疗)
多平台适配系统
UI/UX Pro Max Skill通过模板系统支持多种AI助手平台:
平台适配架构
通过PlatformAdapter接口实现平台适配,每个AI助手(Claude Code、Cursor等)都有独立的适配器。Claude Code支持自动激活,Cursor使用斜杠命令。CLI工具使用模板系统动态生成平台特定文件,从templates/目录加载模板并生成技能文件、配置文件和脚本。
设计规范检查系统
UI/UX Pro Max Skill内置设计规范检查,自动检测常见反模式:
反模式检测
设计规范检查系统自动检测常见反模式:色彩对比度不足(WCAG AA标准)、字体大小过小、间距不一致、组件使用不当、可访问性问题。检测器解析代码中的设计元素,对照设计系统规范,返回问题列表和改进建议。
工作流程
UI/UX Pro Max Skill的完整工作流程:
用户请求UI/UX任务
↓
技能自动激活(Skill模式)或通过命令激活(Workflow模式)
↓
分析产品类型和需求
↓
从知识库检索相关设计知识
↓
应用推理引擎生成设计系统
↓
智能推荐最佳样式方案
↓
生成符合设计系统的代码
↓
执行设计规范检查
↓
返回生成的代码和改进建议
项目地址与资源
官方资源
- 🌟 GitHub: github.com/nextlevelbu…
- 🌐 官网: ui-ux-pro-max-skill.nextlevelbuilder.io
- 📚 文档: GitHub README
- 💬 社区: GitHub Discussions
- 🐛 Issue Tracker: GitHub Issues
- 📦 最新版本: v2.2.1(2026年1月26日发布)
适用人群
UI/UX Pro Max Skill特别适合:需要快速生成专业UI代码的前端开发者、缺乏设计背景的全栈开发者、需要建立设计系统的项目、使用Claude Code/Cursor等AI助手的开发者、需要统一设计规范的团队。
不适合:不需要AI辅助的资深设计师、只需要简单代码生成的用户、不使用AI编程助手的开发者。
欢迎来我中的个人主页找到更多有用的知识和有趣的产品
财政部、税务总局发布《关于增值税征税具体范围有关事项的公告》
深圳罗湖区:水贝黄金平台杰我睿公司已启动兑付,网传金额明显夸大
世界黄金协会:投资者应保持风险意识,避免盲目“all in”
【节点】[VertexID节点]原理解析与实际应用
在Unity的可编程渲染管线中,Shader Graph为开发者提供了可视化编写着色器的能力,而Vertex ID节点则是其中一个功能强大但常被忽视的重要工具。Vertex ID节点允许着色器访问当前处理的顶点或片元的唯一标识符,为各种高级渲染技术提供了基础支持。
Vertex ID节点概述
![]()
Vertex ID节点的核心功能是输出当前正在处理的顶点或片元在网格中的索引值。这个索引值从0开始,按照网格顶点缓冲区的顺序递增。在顶点着色器阶段,它代表顶点的索引;在片元着色器阶段,它代表生成该片元的顶点的索引。
工作原理与底层机制
Vertex ID的实现依赖于GPU的顶点着色器输入语义。在HLSL中,这通常对应着SV_VertexID系统值语义。当Unity提交绘制调用时,GPU会为每个处理的顶点分配一个唯一的ID,这个ID基于顶点在顶点缓冲区中的位置。
在传统的编写着色器代码方式中,开发者会这样声明和使用Vertex ID:
HLSL
truct appdata
{
uint vertexID : SV_VertexID;
};
而在Shader Graph中,这个过程被简化为简单地添加和连接Vertex ID节点,大大降低了使用门槛。
节点特性与限制
Vertex ID节点有几个重要特性需要注意:
- 输出值为浮点数类型,范围从0到网格顶点数减1
- 在顶点着色器和片元着色器中均可使用
- 值在单个绘制调用中保持唯一性和连续性
- 不受网格变形或动画影响,始终反映原始网格的顶点顺序
同时也有一些使用限制:
- 不能用于计算着色器
- 在某些移动设备上可能有限制或性能考虑
- 对于动态批处理的物体,Vertex ID可能不会按预期工作
Vertex ID节点的应用场景
Vertex ID节点在Shader Graph中有着广泛的应用场景,从简单的效果到复杂的渲染技术都能发挥作用。
顶点级动画与变形
利用Vertex ID可以实现基于顶点索引的动画效果,比如波浪效果、随机偏移等。由于每个顶点都有唯一的ID,可以基于ID计算不同的变换参数。
HLSL
// 伪代码示例:基于Vertex ID的波浪动画
float wave = sin(_Time.y * _WaveSpeed + vertexID * _WaveDensity);
float3 offset = float3(0, wave * _WaveHeight, 0);
position.xyz += offset;
程序化纹理坐标生成
当网格缺乏合适的UV坐标时,可以使用Vertex ID来生成程序化的纹理映射。这在处理程序化生成的几何体时特别有用。
HLSL
// 伪代码示例:基于Vertex ID生成UV
float2 uv = float2(frac(vertexID * _UVScale), floor(vertexID * _UVScale) / _GridSize);
实例化与批量渲染优化
在GPU实例化场景中,Vertex ID可以与其他系统值(如Instance ID)结合使用,实现高效的批量渲染和数据索引。
调试与可视化工具
Vertex ID是强大的调试工具,可以用于:
- 可视化顶点分布和顺序
- 检测顶点缓冲区问题
- 理解网格拓扑结构
实际应用示例
下面通过几个具体的Shader Graph设置示例,展示Vertex ID节点的实际应用。
波浪地形效果
创建一个基于Vertex ID的波浪地形效果:
- 首先在Shader Graph中创建Vertex ID节点
- 将输出连接到Custom Function节点进行波浪计算
- 使用Time节点提供动画参数
- 将计算结果连接到Position节点的偏移量
关键节点设置:
- Vertex ID → Custom Function (波浪计算) → Add to Position
- Time → Multiply (控制速度) → Custom Function
- 参数输入:波浪幅度、频率、传播速度
这种设置可以实现流畅的波浪动画,每个顶点基于其ID产生相位偏移,形成自然的波浪传播效果。
顶点颜色渐变
使用Vertex ID创建沿着顶点顺序的颜色渐变:
- Vertex ID节点输出除以网格顶点总数,归一化到[0,1]范围
- 将归一化值输入到Gradient节点
- 将Gradient输出连接到Base Color
这种方法特别适合线框渲染或几何可视化,可以清晰展示顶点的顺序和分布。
程序化网格变形
结合Vertex ID和数学节点创建复杂的网格变形:
- 使用Vertex ID作为噪声函数的输入种子
- 通过不同的数学运算(sin、cos、fract等)创建各种变形模式
- 将变形结果应用到顶点位置
这种技术可以创建有机的、程序化的形状变化,无需额外的纹理或顶点数据。
性能优化与最佳实践
正确使用Vertex ID节点需要考虑性能因素和最佳实践。
性能考虑
- 在移动平台上,尽量减少基于Vertex ID的复杂计算
- 避免在片元着色器中使用Vertex ID进行每帧重计算
- 考虑使用顶点着色器计算并将结果传递给片元着色器
兼容性处理
- 使用Shader Graph的节点功能检查目标平台的兼容性
- 为不支持Vertex ID的平台提供fallback方案
- 测试在不同图形API下的行为一致性
调试技巧
- 使用Vertex ID可视化来理解网格结构
- 结合RenderDoc等工具分析实际的Vertex ID分布
- 创建调试着色器来验证Vertex ID的预期行为
高级应用技巧
与其他系统值的结合
Vertex ID可以与其他系统值结合使用,创造更复杂的效果:
- 结合Instance ID实现每实例的顶点变形
- 与Primitive ID配合实现基于图元的特效
- 和Screen Position结合创建屏幕相关的顶点动画
自定义函数封装
对于复杂的Vertex ID应用,可以创建自定义HLSL函数节点:
HLSL
void VertexIDAnimation_float(float VertexID, float Time, float Amplitude, float Frequency, out float3 Offset)
{
float phase = VertexID * Frequency + Time;
Offset = float3(0, sin(phase) * Amplitude, 0);
}
这样可以在多个Shader Graph中重用复杂的Vertex ID逻辑。
数据驱动的方法
将Vertex ID与外部数据结合:
- 使用Compute Buffer存储每顶点的动画参数
- 通过MaterialPropertyBlock传递顶点级别的数据
- 结合Scriptable Renderer Features实现更高级的渲染管线集成
故障排除与常见问题
Vertex ID输出异常
当Vertex ID不按预期工作时,可能的原因包括:
- 网格被动态批处理,改变了顶点顺序
- 使用了不支持的渲染路径
- 图形API限制
解决方案:
- 禁用动态批处理
- 检查目标平台的图形API支持
- 使用Shader Variant收集器确保所有需要的变体都被编译
性能问题
基于Vertex ID的效果导致性能下降时的优化策略:
- 将计算从片元着色器移到顶点着色器
- 使用LOD系统在远距离简化效果
- 预计算静态效果到顶点颜色或纹理中
平台兼容性
处理不同平台的兼容性问题:
- 为OpenGL ES 2.0等老旧平台提供简化版本
- 使用Shader Graph的Keyword系统管理平台特定代码
- 进行充分的跨平台测试
【Unity Shader Graph 使用与特效实现】专栏-直达 (欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)
京东航空A330宽体货机正式投入商业运营
Nest 的中间件 Middleware ?
新建项目
nest new middleware-demo
创建一个中间件
nest g middleware aaa --no-spec --flat
加下打印
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
@Injectable()
export class AaaMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => void) {
console.log('brefore');
next();
console.log('after');
}
}
在 Module 里这样使用
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AaaMiddleware } from './aaa.middleware';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(AaaMiddleware).forRoutes('*');
}
}
跑起来看看
可以指定更精确的路由,添加几个 handler
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('hello')
getHello(): string {
console.log('hello');
return this.appService.getHello();
}
@Get('hello2')
getHello2(): string {
console.log('hello2');
return this.appService.getHello();
}
@Get('hi')
getHi(): string {
console.log('hi');
return this.appService.getHello();
}
@Get('hi1')
getHi1(): string {
console.log('hi1');
return this.appService.getHello();
}
}
module 匹配更新下
import { AaaMiddleware } from './aaa.middleware';
import {
MiddlewareConsumer,
Module,
NestModule,
RequestMethod,
} from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AaaMiddleware)
.forRoutes({ path: 'hello/*path', method: RequestMethod.GET });
consumer
.apply(AaaMiddleware)
.forRoutes({ path: 'hi1', method: RequestMethod.GET });
}
}
Nest 为什么要把 Middleware 做成 class 呢?
为了依赖注入!
通过 @Inject 注入 AppService 到 middleware 里
import { AppService } from './app.service';
import { Inject, Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
@Injectable()
export class AaaMiddleware implements NestMiddleware {
@Inject(AppService)
private readonly appService: AppService;
use(req: Request, res: Response, next: () => void) {
console.log('brefore');
console.log('-------' + this.appService.getHello());
next();
console.log('after');
}
}
这就是 Nest 注入的依赖
国轩高科与科大讯飞签署战略合作协议
Vue2(三)——模板语法
Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。所有 Vue.js 的模板都是合法的 HTML,所以能被遵循规范的浏览器和 HTML 解析器解析。
在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合响应系统,Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少。
如果你熟悉虚拟 DOM 并且偏爱 JavaScript 的原始力量,你也可以不用模板,直接写渲染 (render) 函数,使用可选的 JSX 语法。
上面这段原封不动,来自官方,我比较菜,看不懂,下面AI的分析
第一层:写什么?—— 声明式模板 (Declarative Templates)
原文: “Vue.js 使用了基于 HTML 的模板语法... 声明式地将 DOM 绑定至数据... 所有模板都是合法的 HTML。”
这讲的是 Vue 的**“门面”**。
- 声明式 (Declarative) vs 命令式 (Imperative):
-
- 命令式(如 jQuery): 你需要一步步告诉浏览器怎么做(“找到 id 为 app 的 div,清空内容,插入一个 span,设置 span 的文字...”)。
-
声明式(Vue): 你只告诉 Vue 你想要什么结果(“这里要显示
{{ message }}”),具体的脏活累活(DOM 操作)交给 Vue 去处理。
- 基于 HTML:
-
- 这大大降低了学习门槛。设计师或后端开发人员也能看懂 Vue 代码,因为它长得就像普通的 HTML。
- 这也意味着现有的 HTML 解析器都能处理它,不会像某些非标语法那样导致编辑器报错。
第二层:怎么变?—— 编译与虚拟 DOM (Compilation & VDOM)
原文: “在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。”
这讲的是 Vue 的**“转换过程”**。浏览器其实看不懂 v-if 或 {{ }},所以 Vue 在代码运行前(或运行时)做了一次“翻译”。
- 编译 (Compile):
Vue 有一个编译器,它会把你的 HTML 模板字符串“翻译”成一段 JavaScript 代码。这段代码就叫 渲染函数 (Render Function) 。
- 虚拟 DOM (Virtual DOM):
渲染函数执行后,不会直接去动真的 DOM(因为操作真 DOM 很慢),而是生成一个 JavaScript 对象树,这个对象树就是虚拟 DOM。它就像是真实 DOM 的一份“轻量级蓝图”。
举个例子:
- 你的模板:
HTML
<div :id="dynamicId">Hello</div>
- Vue 编译后的渲染函数 (伪代码):
JavaScript
function render() {
return h('div', { id: this.dynamicId }, 'Hello')
}
第三层:怎么跑?—— 响应式与智能更新 (Reactivity & Diffing)
原文: “结合响应系统,Vue 能够智能地计算出... 把 DOM 操作次数减到最少。”
这讲的是 Vue 的**“超能力”**。
- 响应系统 (Reactivity):
Vue 会“监视”你的数据。当数据变化时,它不仅知道数据变了,还精确地知道哪个组件依赖了这个数据。
- Diff 算法 (最小化更新):
当数据变化,渲染函数会重新执行,生成新的虚拟 DOM 树。
Vue 会拿着 新树 和 旧树 做对比(Diff)。
-
- Vue 发现: “哦,只有这个
div的class变了,其他都没变。” - 结果:Vue 只去更新真实 DOM 里那个
div的class,其他不动。
- Vue 发现: “哦,只有这个
这就是为什么 Vue 即使在处理庞大页面时依然很快的原因。
第四层:给高手的“后门” —— Render 函数 & JSX
原文: “如果你熟悉虚拟 DOM 并且偏爱 JavaScript 的原始力量... 直接写渲染 (render) 函数...”
这段话是说,模板虽然好用,但有时候不够灵活。
-
模板的局限: 比如你要写一个组件,根据 props 动态生成
h1到h6标签。用模板写,你可能得写 6 个v-if。 -
JS 的力量: 如果用渲染函数,你只需要写一行 JS:
return h('h' + this.level, ...)。
Vue 并不强迫你用模板,它完全支持你像 React 那样写代码(JSX),这给了高级开发者极大的灵活性。
总结分析
这段话其实揭示了 Vue 的架构分层:
| 层次 | 作用 | 对应原文 |
|---|---|---|
| 顶层 (API) | 易用性 | 基于 HTML 的模板,声明式绑定 |
| 中间层 (Compiler) | 转化 | 模板 渲染函数 虚拟 DOM |
| 底层 (Engine) | 性能 | 响应式系统 + 智能 Diff 计算 |
| 扩展层 (Flexibility) | 灵活性 | 可选 JSX / Render 函数 |
插值
文本
双大括号 Mustache
<span>Message: {{ msg }}</span>
Mustache 标签将会被替代为对应数据对象上 msg property 的值。无论何时,绑定的数据对象上 msg property 发生了改变,插值处的内容都会更新。
可以使用v-once ,只执行一次插值,后面值变化不会更新
<span v-once>这个将不会改变: {{ msg }}</span>
原始HTML
v-html
<p>Using mustaches: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
整个span 的内容 会被替换成 property 值 rawHtml直接作为 HTML
注意:XSS 攻击,并且只支持常规html,不支持解析自定义组件
Attribute
v-bind 执行 绑定 Attribute
<div v-bind:id="dynamicId"></div>
注意与 HTML的不同 ( https://juejin.cn/post/7598447519823101993 里面的布尔属性提到了)
对于布尔 attribute (它们只要存在就意味着值为 true),v-bind 有不同,在这个例子中:
<button v-bind:disabled="isButtonDisabled">Button</button>
如果 isButtonDisabled 的值是 null、undefined 或 false,则 disabled attribute 甚至不会被包含在渲染出来的 <button> 元素中。
使用 JavaScript 表达式
插值模板还支持三目运算符和数学表达式,以及库函数操作
{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
<div v-bind:id="'list-' + id"></div>
错误例子
<!-- 这是语句,不是表达式 -->
{{ var a = 1 }}
<!-- 流控制也不会生效,请使用三元表达式 -->
{{ if (ok) { return message } }}
模板表达式都被放在沙盒中,只能访问全局变量的一个白名单,如 Math 和 Date 。你不应该在模板表达式中试图访问用户定义的全局变量。
大白话就是指定访问 new Vue({}) 时传入的对象里面的 data,methods computed等,以及库函数,外部定义的变量,只要没传入 new Vue({}) 里面就不能访问
指令
指令 (Directives) 是带有 v- 前缀的特殊 attribute。
指令 attribute 的值预期是单个 JavaScript 表达式 (v-for 是例外情况,稍后我们再讨论)。
当表达式的值改变时,将影响DOM
即 Model 影响 -> DOM
<p v-if="seen">现在你看到我了</p>
v-if 指令将根据表达式 seen 的值的真假来插入/移除 <p> 元素
参数
一些指令能够接收一个“参数”,在指令名称之后以冒号表示。
例如,v-bind 指令可以用于响应式地更新 HTML attribute:
<a v-bind:href="url">...</a>
在这里 href 是参数,告知 v-bind 指令将该元素的 href attribute 与表达式 url 的值绑定。
另一个例子是 v-on 指令,它用于监听 DOM 事件:
<a v-on:click="doSomething">...</a>
在这里参数是监听的事件名。我们也会更详细地讨论事件处理。
动态参数
用方括号括起来的 JavaScript 表达式作为一个指令的参数:
<!--
注意,参数表达式的写法存在一些约束,如之后的“对动态参数表达式的约束”章节所述。
-->
<a v-bind:[attributeName]="url"> ... </a>
这里的 attributeName 会被作为一个 JavaScript 表达式进行动态求值,求得的值将会作为最终的参数来使用。例如,如果你的 Vue 实例有一个 data property attributeName,其值为 "href",那么这个绑定将等价于 v-bind:href。
使用动态参数为一个动态的事件名绑定处理函数:
<a v-on:[eventName]="doSomething"> ... </a>
在这个示例中,当 eventName 的值为 "focus" 时,v-on:[eventName] 将等价于 v-on:focus。
动态参数值的约束
- 预期是求出一个字符串
- 异常情况为null,null表示移除该绑定
- 动态参数表达式有一些语法约束,因为某些字符,如空格和引号,放在 HTML attribute 名里是无效的。
<!-- 这会触发一个编译警告 -->
<a v-bind:['foo' + bar]="value"> ... </a>
使用没有空格或引号的表达式,或者计算属性替代。
在 DOM 中使用模板时 (直接在一个 HTML 文件里撰写模板),还需要避免使用大写字符来命名键名,因为浏览器会把 attribute 名全部强制转为小写:
<!--
在 DOM 中使用模板时这段代码会被转换为 `v-bind:[someattr]`。
除非在实例中有一个名为“someattr”的 property,否则代码不会工作。
-->
<a v-bind:[someAttr]="value"> ... </a>
例子如下
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<!-- 开发环境版本,包含了有帮助的命令行警告 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
<div id="app">
<a v-bind:[someAttr]="value">掘金</a>
</div>
</body>
<script>
var app = new Vue({
el: "#app",
data: {
value: "https://https://juejin.cn/",
someAttr: "href",
},
});
</script>
</html>
修改之后的代码
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<!-- 开发环境版本,包含了有帮助的命令行警告 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
<div id="app">
<a v-bind:[someAttr]="value">掘金</a>
</div>
</body>
<script>
var app = new Vue({
el: "#app",
data: {
value: "https://https://juejin.cn/",
someattr: "href",
},
});
</script>
</html>
修饰符
修饰符 (modifier) 是以半角句号 . 指明的特殊后缀
可以用于阻止事件冒泡或者阻止表单提交
例如,.prevent 修饰符告诉 v-on 指令对于触发的事件调用 event.preventDefault():
<form v-on:submit.prevent="onSubmit">...</form>
缩写
缩写不是必须的
v-bind 缩写
<!-- 完整语法 -->
<a v-bind:href="url">...</a>
<!-- 缩写 -->
<a :href="url">...</a>
<!-- 动态参数的缩写 (2.6.0+) -->
<a :[key]="url"> ... </a>
v-on 缩写
<!-- 完整语法 -->
<a v-on:click="doSomething">...</a>
<!-- 缩写 -->
<a @click="doSomething">...</a>
<!-- 动态参数的缩写 (2.6.0+) -->
<a @[event]="doSomething"> ... </a>
CSS动画的灵动之吻:从代码到情感的生动演绎
CSS动画的灵动之吻:从代码到情感的生动演绎
在Web前端的世界里,CSS不仅仅是定义颜色和布局的工具,它更是一门能够赋予静态元素生命与情感的艺术。今天,我们将通过一个名为“双球亲吻”的生动案例,深入剖析如何利用CSS关键帧动画(@keyframes)和精细的类名设计,让两个简单的圆形化身为含情脉脉的男女主角,上演一场温馨的互动。
一、 动画蓝图:整体构思与HTML结构
任何精彩的动画都始于一个清晰的故事板。我们的故事很简单:画面中央,代表“女主”的白色圆球(#l-ball)在原地微微靠近又远离,而代表“男主”的圆球(#r-ball)则会主动“亲吻”女主。
HTML结构(文档1)体现了“面向对象CSS(OOCSS)”的思想,这是代码严谨性的基石:
<div class="container">
<!-- 女主 -->
<div class="ball" id="l-ball">...</div>
<!-- 男主 -->
<div class="ball" id="r-ball">...</div>
</div>
设计解析:
-
容器居中:
.container使用了经典的“绝对定位 +transform: translate”方法实现水平垂直居中,这是一种与元素尺寸无关的优雅居中方案。 -
类名复用:两个球都使用了
.ball基类,定义了它们共有的样式:尺寸、圆形边框、白色背景等。这遵循了“不要重复自己(DRY)”原则,避免了代码冗余。 -
ID与角色:通过
#l-ball和#r-ball这两个ID,我们为两个角色赋予了独立的“身份”,以便后续为它们定制不同的动画行为。
二、 角色塑造:面相与情绪的CSS实现
角色的“面部”细节(眼睛、嘴巴、腮红)是传递情绪的关键。这里大量运用了CSS伪元素和“多态”类名。
1. 面部基类与“腮红”特效
.face类是所有面部元素的定位上下文。其最巧妙的设计在于使用 ::before和 ::after伪元素来创建腮红。
.face::after, .face::before {
content: ""; /* 必须存在 */
position: absolute;
width: 18px;
height: 8px;
background-color: #badc58; /* 腮红颜色 */
top: 20px;
border-radius: 50%;
}
- 为什么用伪元素? 腮红是纯粹的装饰性内容,不应污染HTML结构。伪元素完美解决了这个问题,使得HTML保持简洁。
-
定位:通过
right: -8px和left: -5px将两个腮红定位在面部元素之外,模拟出球体上的红晕。
2. 眼睛与嘴巴的“多态”
眼睛(.eye)通过边框(border-bottom)巧妙地画出了下半圆,营造出可爱的感觉。而男主的眼睛需要表现出与女主不同的神态(比如挑眉),这里通过“多态”类名 .eye-r-p来实现。
.eye {
border-bottom: 5px solid; /* 默认向下看 */
}
.eye-r-p {
border-top: 5px solid; /* 改为向上看 */
border-bottom: 0px solid;
}
设计解析:.eye是基类,定义了眼睛的基本形状。.eye-r-p是一个修饰类,通过覆盖边框样式,改变了眼睛的方向。这种设计使得我们只需在HTML中为男主的眼睛添加这个类,就能轻松改变其神态,这正是“多态”的威力。
三、 生命注入:核心动画的时序与协作逻辑
整个动画的精髓在于四个核心动画的精密配合,它们共享一个4秒(4s)的周期并无限循环(infinite),但各自的节奏(关键帧百分比)不同。
1. 女主的矜持:靠近与停留(#l-ball与 .face-l)
-
身体动画 (
#l-ball-close) :-
0%-20%: 从原点平滑向右移动20px(向男主靠近)。 -
20%-35%: 在右侧停留。这个停留至关重要,它为男主的亲吻动作创造了“目标”和“时机”。 -
35%-55%: 平滑返回原点。 -
55%-100%: 在原点长时间停留,等待下一个周期。
-
-
面部动画 (
.face-l-face) :这个动画让女主的头部在移动时伴有轻微的转动(
rotate),增加了拟人化的生动感。其关键帧与身体动画同步,在20%和35%(即身体停留时)触发头部微调,仿佛在害羞地调整姿态。
为什么这样设计? 将身体和面部的动画分离,符合“单一职责原则”。#l-ball控制宏观位移,.face-l控制细微表情。两者协同,共同塑造出一个完整、生动的角色。
2. 男主的主动:亲吻的爆发(#r-ball及其组件)
男主的动画是故事的高潮,由三个部分精密协作完成:
-
身体冲刺 (
#r-ball-kiss) :-
40%-50%: 在女主停留的期间(20%-35%之后),男主迅速向右移动并旋转,做出“探头亲吻”的动作。 -
50%-60%: 快速向左回弹(translate(-33px)),这个回弹的幅度甚至超过了初始位置,模拟出亲吻的冲击力。 -
67%-77%: 缓慢移回原点。
-
-
嘴巴消失与爱心出现(
.mouth-r和.kiss-m) :这是最精彩的部分,通过透明度(
opacity) 的切换实现“变脸”。-
嘴巴 (.mouth-r -
mouth-m) : 在55%关键帧瞬间变为透明(opacity: 0),在66%关键帧瞬间恢复。这意味着在亲吻动作发生时,男主的嘴巴“消失”了。 -
爱心 (.kiss-m -
kiss-m) : 它的显示(opacity: 1)时间被精确地设置在66%,仅持续0.1%的时间(约0.004秒)。同时,嘴巴在66%恢复。
-
嘴巴 (.mouth-r -
为什么这样设计? 这创造了一个视觉魔术:在 55%到 66%之间,嘴巴消失,爱心出现。虽然爱心只闪现了一瞬间,但由于人眼的“视觉暂留”效应,我们会感觉在亲吻的刹那,男主的嘴巴变成了一颗爱心。这种通过极短时间显示替代元素来制造特效的手法,在CSS动画中非常经典且高效。
四、 深度优化:Z轴与时序的严谨性
-
层级控制 (Z-index) : 为
#l-ball设置了较大的z-index: 100,确保女主始终在前景。这样在男主亲吻回弹时,不会出现不合理的重叠穿帮,保证了视觉逻辑的正确。 -
时序的严谨性: 所有动画的
animation-timing-function都设置为ease,这使得动作的开始和结束更平滑自然,符合真实物体的运动规律。关键帧百分比的设定需要反复调试,以达到动作间无缝衔接的效果。
总结与复习指南
这个“双球亲吻”动画是一个绝佳的CSS动画综合练习案例。要复习此项目,您可以遵循以下步骤:
-
重构HTML: 根据文档1,仅凭记忆写出结构清晰的HTML,注意基类与修饰类的应用。
-
还原静态样式: 先实现两个球的静态样式,包括居中、基本形状、面部五官。重点练习伪元素(腮红)和边框画图法(眼睛)。
-
分步添加动画:
-
第一步:实现女主球的水平移动动画(
close)。 -
第二步:为女主添加面部微动画(
face),注意与身体动画的同步。 -
第三步:实现男主球的冲刺与回弹动画(
kiss)。 -
第四步(关键) :实现嘴巴和爱心的透明度切换动画,仔细体会
mouth-m和kiss-m中关键帧百分比的设计意图,理解“视觉暂留”特效的实现原理。
-
第一步:实现女主球的水平移动动画(
通过这样的分解与重构,您不仅能牢固掌握这个动画的制作过程,更能深刻理解CSS动画设计的核心思想:将复杂动作拆解为独立的、可复用的属性变化,并通过精确的时序控制将它们组合起来,最终赋予元素生命。
国产RPU人工智能芯片公司清微智能完成股改
从远程组件到极致性能:一次低代码架构的再思考
前段时间突然回顾了一下之前做过的一件事:上一份工作的核心任务之一,其实就是一个可视化 / 低代码平台。
当时受限于时间和复杂度,整体方案基本是基于 vue-sfc-playground 这一套思路,通过 iframe + 浏览器端编译的方式来实现远程组件的扩展能力。虽然最终把功能跑通了,但在真实使用过程中,这套方案逐渐暴露出不少问题。
比较典型的有:
- iframe 性能较差,通信成本高,调试也不友好
- 引入的模块必须支持 ESM,且兼容性受限
- 模块之间存在前置依赖,需要人工维护依赖关系
- 代码补全、Lint、插件能力受限,开发体验远不如本地 IDE
- 以及一系列零散但很消耗心智的问题
站在现在这个时间点回看,我觉得这个问题本身并不复杂,只是当时的实现方式并不优雅——它其实是有更好的解法的。
需求假设与目标拆解
为了方便后续讨论,我们先假定一个明确的需求:
低代码平台需要支持挂载任意自定义组件,用来组合实现业务功能,而不是只能使用平台内置的物料。
在这个前提下,我给自己定了几个明确的目标:
- 开发阶段:本地 IDE 编写组件,修改后可以快速预览
- 生产环境:极致的运行性能和尽可能小的包体积
- 整体策略:放弃“纯在线编写组件”,全部基于本地开发来解决问题
原因也很现实:
在线写组件这条路,优化成本太高了,一旦引入复杂依赖、真实业务代码,维护难度会指数级上升。
开发阶段:追求极致的反馈速度
开发阶段整体的数据流和职责关系如下:
flowchart LR
IDE[本地 IDE]
IDE -->|文件变更| Rsbuild
Rsbuild -->|HMR / WS| Renderer
Renderer -->|defineAsyncComponent| VueRuntime
开发阶段的核心诉求其实很简单:
我在本地改代码,页面要立刻有反馈。
如果你看过 Vue 3 的官方文档,会发现它提供了一个非常关键的能力:defineAsyncComponent,用于异步加载组件。
import { defineAsyncComponent } from "vue";
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// 从服务端获取组件
resolve(/* 组件对象 */);
});
});
推荐阅读: 给我 5 分钟,保证教会你在 Vue3 中动态加载远程组件 这篇文章可以帮助理解如何结合本地 Node 服务来挂载远程组件。
需要注意的一点是:
resolve 返回的并不是字符串形式的源码,而是经过编译后的组件对象,本质上类似于 vue-loader 处理后的产物。
到这里,背景铺垫就结束了。
开发态整体架构设计
在开发阶段,我选择 Rsbuild 作为构建核心,整体思路大致如下:
- 维护一个模板工程(template) 用于提前约定好组件开发所需的基础配置。
-
编写 Rsbuild 插件
- 插件会结合用户的
API Key和Project ID - 在本地开发阶段,通过 HMR / WebSocket 将组件的最新编译结果推送到平台面板
- 在生产阶段,则负责将源码和构建产物上传到 OSS
- 插件会结合用户的
-
渲染器(低代码核心)
- 渲染器内部会固定一个 Vue 版本,作为所有组件的公共依赖
- 接收由 Rsbuild 编译后的 JS 模块
- 再通过
defineAsyncComponent动态注入组件
约定与约束
除此之外,还需要提前定义一些结构和规范,例如:
-
global.css:用于声明全局样式 -
composes/:用于存放多个自定义组件
在 dev 模式启动后,插件会:
- 开启 CORS
- 向渲染器推送当前可用的组件列表及版本号
- 文件变更后重新计算版本,并通知渲染器刷新
两个关键限制
这里有两个非常重要的点:
-
样式约束
在 Vue SFC 中禁止书写全局
style,一旦检测到直接报错,防止出现难以审查和回滚的样式污染。 -
依赖处理策略
在开发阶段,仅将
vue本身 external 掉。 其他依赖(如dayjs、UI 框架等)直接打进包里。
虽然这样会导致 JS 体积偏大,但这是开发态,为了效率和稳定性,这个代价是完全可以接受的。
为什么不用 vue3-sfc-loader?
一个常见的问题是:
为什么不直接使用 vue3-sfc-loader 在浏览器端加载 SFC?
核心原因在于依赖管理。
vue3-sfc-loader 需要手动维护 moduleCache,而一旦涉及真实项目,就必然要引入大量第三方包。这就意味着你需要结合 importmap 来管理依赖关系和版本冲突。
例如:
<script type="importmap">
{
"imports": {
"vue": "https://play.vuejs.org/vue.runtime.esm-browser.js",
"vue/server-renderer": "https://play.vuejs.org/server-renderer.esm-browser.js"
}
}
</script>
这种方式在 Demo 场景下还可以接受,但在真实低代码平台中,维护成本会非常高,几乎不可控。
Build 阶段:为极致性能服务
flowchart TB
subgraph Dev[开发阶段]
IDE --> Rsbuild
Rsbuild --> Renderer
Renderer --> Browser
end
subgraph Prod[生产阶段]
Config[JSON / DSL]
Config --> Monorepo
Monorepo --> Build
Build --> OSS
OSS --> Browser
end
生产环境的目标只有一个:性能。
整体思路是结合 Monorepo,将低代码平台中的配置还原为一个真实可构建的工程。
工程还原策略
flowchart TB
Root[monorepo]
Root --> Composes[composes/* 组件包]
Root --> Apps[apps/platform]
Apps --> Pages[页面还原]
- 每一个自定义组件,都被放置在
components/目录下,作为 Monorepo 的子包 - 在
apps/platform中,根据平台生成的 JSON 配置:- 还原页面结构
- 将对应的子包注册到
package.json依赖中
随后直接执行 build。
由于使用的是基于 Rust 的构建工具(如 Rsbuild / Rspack),即使是全量构建,耗时也基本控制在 30 秒以内。
带来的收益
- Tree Shaking:未使用代码会被自动移除
- 更小的包体积
- 更好的缓存命中率:结合分包策略,可最大化利用 HTTP 缓存
多页面与路由加载
对于支持多页面的低代码平台来说,结合这种拆分方式,每一个页面本质上只包含自己的业务代码。
const routes = [
{
path: "/remote-js",
component: () => import("https://my-server.com/assets/RemoteComponent.js"),
},
];
浏览器原生已经支持通过 import() 加载远程 ESM 模块,只要返回的是一个 Promise,Vue Router 就可以正常工作。
参考文档: Vue Router 路由懒加载
最后
这套方案更多是一次架构思路的延展,以及一些关键落地点的总结。
真正落地时,依然会有很多细节需要打磨,例如:
- 沙箱与安全隔离
- 组件版本管理
- 发布与回滚策略
不过整体主线是清晰的:
开发态为体验让路,生产态为性能让路。
如果你对其中某些设计有不同的想法,或者有类似的实践经验,也欢迎交流一波。
