本文为会员文章,出自《单篇文章》,订阅后可阅读全文。
阅读视图
财政部、税务总局发布《关于增值税征税具体范围有关事项的公告》
深圳罗湖区:水贝黄金平台杰我睿公司已启动兑付,网传金额明显夸大
世界黄金协会:投资者应保持风险意识,避免盲目“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 路由懒加载
最后
这套方案更多是一次架构思路的延展,以及一些关键落地点的总结。
真正落地时,依然会有很多细节需要打磨,例如:
- 沙箱与安全隔离
- 组件版本管理
- 发布与回滚策略
不过整体主线是清晰的:
开发态为体验让路,生产态为性能让路。
如果你对其中某些设计有不同的想法,或者有类似的实践经验,也欢迎交流一波。
用数据分析项目,带你走进成语里的数字世界
构建无障碍组件之Breadcrumb Pattern
Breadcrumb Pattern 详解:构建无障碍面包屑导航
面包屑导航是 Web 页面中不可或缺的导航组件,它以层级链接的形式展示当前页面在网站结构中的位置,帮助用户快速了解自己所处的位置并轻松返回上级页面。根据 W3C WAI-ARIA Breadcrumb Pattern 规范,正确实现的面包屑导航不仅要提供清晰的层级导航路径,更要确保所有用户都能顺利使用,包括依赖屏幕阅读器等辅助技术的用户。本文将深入探讨 Breadcrumb Pattern 的核心概念、实现要点以及最佳实践。
一、面包屑导航的定义与核心功能
面包屑导航(Breadcrumb Trail)是由一系列指向父级页面的链接组成的导航路径,按照层级顺序展示当前页面在网站架构中的位置。它的主要功能是帮助用户了解自己在网站中的位置,并在需要时快速返回上级页面。面包屑导航通常水平放置在页面主体内容之前,为用户提供清晰的导航参考。
在实际应用中,面包屑导航广泛应用于内容层级较深的网站,例如电商平台的产品分类页、博客文章的分类归档页、企业官网的产品介绍页等。一个设计良好的面包屑导航能够显著提升用户体验,降低用户的迷失感,同时也有利于搜索引擎更好地理解网站的结构层次。
二、键盘交互规范
面包屑导航的键盘交互具有特殊性。由于面包屑导航仅由静态链接组成,用户不需要进行任何特殊的键盘操作来与之交互。链接本身已经支持标准的键盘导航行为,用户可以通过 Tab 键在各个链接之间切换,通过 Enter 键激活链接进行页面跳转。这种标准的行为已经满足了键盘可访问性的要求,无需额外的键盘事件处理。
因此,Breadcrumb Pattern 规范明确指出,面包屑导航不需要特殊的键盘交互支持。开发者只需要确保链接元素是标准的 HTML 元素,即可获得完整的键盘可访问性支持。这一特点使得面包屑导航的实现相对简单,重点应放在正确的语义标记和 ARIA 属性使用上。
三、WAI-ARIA 角色、状态和属性
正确使用 WAI-ARIA 属性是构建无障碍面包屑导航的技术基础。虽然面包屑导航不涉及复杂的交互逻辑,但正确的语义标记对于屏幕阅读器用户理解导航结构至关重要。
3.1 导航容器标记
面包屑导航必须放置在导航地标区域(Navigation Landmark)内。这可以通过使用 nav 元素或为其他元素添加 role="navigation" 来实现。导航地标区域需要通过 aria-label 或 aria-labelledby 进行标记,以便屏幕阅读器向用户描述这个导航区域的用途。
示例:使用 nav 元素包裹面包屑导航:
<nav aria-label="面包屑导航">
<ol>
<li><a href="/">首页</a></li>
<li><a href="/products/">产品</a></li>
<li><a href="/products/electronics/">电子产品</a></li>
<li aria-current="page">笔记本电脑</li>
</ol>
</nav>
示例:使用 role 属性定义导航地标:
<div
role="navigation"
aria-label="面包屑导航">
<ul>
<li><a href="/">首页</a></li>
<li><a href="/blog/">博客</a></li>
<li aria-current="page">2025 年技术趋势</li>
</ul>
</div>
3.2 当前页面状态标记
面包屑导航中的当前页面链接需要使用 aria-current 属性来标识。aria-current="page" 明确告诉辅助技术当前元素代表的是用户正在浏览的页面。这一属性对于屏幕阅读器用户理解面包屑导航的结构非常重要,使他们能够区分可导航的父级页面和当前所在页面。
值得注意的是,如果代表当前页面的元素不是链接(例如使用 span 或其他元素呈现),那么 aria-current 属性是可选的。但为了保持一致性,建议始终为当前页面元素添加 aria-current="page"。
示例:当前页面为链接时的标记:
<nav aria-label="面包屑导航">
<ol>
<li><a href="/">首页</a></li>
<li><a href="/docs/">文档</a></li>
<li><a href="/docs/guides/">指南</a></li>
<li>
<a
href="/docs/guides/getting-started/"
aria-current="page"
>快速入门</a
>
</li>
</ol>
</nav>
示例:当前页面为非链接元素时的标记:
<nav aria-label="面包屑导航">
<ol>
<li><a href="/">首页</a></li>
<li><a href="/shop/">商店</a></li>
<li><a href="/shop/clothing/">服装</a></li>
<li aria-current="page">春季新品</li>
</ol>
</nav>
四、完整示例
以下是使用不同方式实现面包屑导航的完整示例,展示了标准的 HTML 结构、ARIA 属性应用以及样式设计:
4.1 基础面包屑导航实现
<nav aria-label="面包屑导航">
<ol class="breadcrumb">
<li><a href="/">首页</a></li>
<li><a href="/products/">所有产品</a></li>
<li><a href="/products/electronics/">电子产品</a></li>
<li aria-current="page">智能手表</li>
</ol>
</nav>
4.2 带分隔符的面包屑导航
<nav
aria-label="面包屑导航"
class="breadcrumb-nav">
<ol class="breadcrumb-list">
<li class="breadcrumb-item">
<a
href="/"
class="breadcrumb-link"
>首页</a
>
</li>
<li
class="breadcrumb-separator"
aria-hidden="true">
/
</li>
<li class="breadcrumb-item">
<a
href="/categories/"
class="breadcrumb-link"
>分类</a
>
</li>
<li
class="breadcrumb-separator"
aria-hidden="true">
/
</li>
<li class="breadcrumb-item">
<a
href="/categories/books/"
class="breadcrumb-link"
>图书</a
>
</li>
<li
class="breadcrumb-separator"
aria-hidden="true">
/
</li>
<li
class="breadcrumb-item"
aria-current="page">
<span class="breadcrumb-current">编程指南</span>
</li>
</ol>
</nav>
4.3 电商网站产品页面包屑导航
<nav
aria-label="商品位置"
class="product-breadcrumb">
<ol>
<li><a href="https://www.example.com/">首页</a></li>
<li><a href="https://www.example.com/electronics/">家用电器</a></li>
<li><a href="https://www.example.com/electronics/kitchen/">厨房电器</a></li>
<li>
<a href="https://www.example.com/electronics/kitchen/coffee/">咖啡机</a>
</li>
<li aria-current="page">全自动咖啡机 X2000</li>
</ol>
</nav>
五、最佳实践
5.1 语义化 HTML 结构
面包屑导航应使用语义化的 HTML 元素构建。使用 nav 元素定义导航区域,使用 ol 或 ul 元素创建有序或无序列表,使用 li 元素包含各个导航项。这种结构不仅对搜索引擎友好,也便于屏幕阅读器向用户传达导航的层级关系。
在列表的选择上,ol 元素更适合面包屑导航,因为它能够传达各个项之间的顺序关系。但如果网站没有强制的层级顺序要求,ul 元素同样可以接受。无论选择哪种列表元素,都要确保列表项之间的逻辑顺序与面包屑导航的层级结构保持一致。
<!-- 推荐做法:使用语义化元素 -->
<nav aria-label="面包屑导航">
<ol>
<li><a href="/">首页</a></li>
<li><a href="/docs/">文档</a></li>
<li aria-current="page">当前页面</li>
</ol>
</nav>
5.2 正确使用 ARIA 属性
面包屑导航的 ARIA 属性使用相对简单,但有几个关键点需要注意。首先,导航容器必须使用 aria-label 或 aria-labelledby 进行标记,以便用户了解这个导航区域的用途。其次,当前页面项必须使用 aria-current="page" 进行标记。最后,确保分隔符元素使用 aria-hidden="true",以避免辅助技术用户听到不必要的标点符号朗读。
<!-- ARIA 属性使用规范 -->
<nav aria-label="面包屑导航">
<ol>
<li><a href="/">首页</a></li>
<li aria-hidden="true">/</li>
<li><a href="/blog/">博客</a></li>
<li aria-hidden="true">/</li>
<li aria-current="page">文章标题</li>
</ol>
</nav>
5.3 响应式设计考虑
面包屑导航在移动设备上可能面临空间受限的挑战。对于层级较深的导航,可以考虑以下策略:一是使用省略号隐藏中间层级,只保留首页和最后几级;二是允许用户展开查看完整路径;三是将面包屑导航改为垂直布局。无论采用哪种策略,都要确保用户能够访问完整的导航信息。
/* 响应式面包屑导航 */
@media (max-width: 768px) {
.breadcrumb-list {
flex-direction: column;
gap: 4px;
}
.breadcrumb-separator {
transform: rotate(90deg);
}
}
六、面包屑导航与主导航的区别
面包屑导航和主导航栏虽然都是网站导航的重要组成部分,但它们服务于不同的目的,理解这种区别对于正确使用这两种导航组件至关重要。
面包屑导航的主要作用是展示用户在网站层级结构中的当前位置,它反映的是网站的逻辑结构,而非用户的浏览历史。面包屑导航通常只出现在层级较深的页面中,帮助用户理解当前位置与网站整体结构的关系。相比之下,主导航栏提供的是网站的整体架构概览,允许用户直接跳转到任何主要版块,而不考虑当前的层级位置。
从实现角度来看,面包屑导航强调的是层级关系,因此通常使用有序列表(ol)来体现这种顺序性。而主导航栏更强调功能分区的划分,可能使用无序列表(ul)或更复杂的布局结构。两者在 ARIA 属性使用上也不同:面包屑导航需要明确标记当前页面(aria-current="page"),而主导航栏通常不需要这种标记。
七、总结
构建无障碍的面包屑导航需要关注语义化结构、ARIA 属性应用和视觉样式三个层面的细节。从语义化角度,应使用 nav 元素定义导航区域,使用 ol 或 ul 元素创建列表结构。从 ARIA 属性角度,需要使用 aria-label 为导航区域提供标签,使用 aria-current="page" 标记当前页面。从视觉样式角度,应确保链接易于识别,分隔符清晰,当前页面状态明确。
WAI-ARIA Breadcrumb Pattern 为我们提供了清晰的指导方针,遵循这些规范能够帮助我们创建更加包容和易用的 Web 应用。每一个正确实现的面包屑导航组件,都是提升用户体验和网站可访问性的重要一步。
美国FCC:SpaceX申请部署百万颗卫星 欲建轨道AI数据中心网络
东莞:2025年地区生产总值12760.2亿元 同比增长4%
CSS伪元素:给HTML穿上"隐形斗篷"的魔法
CSS伪元素:给HTML穿上"隐形斗篷"的魔法
引言:一场看不见的化妆舞会
想象一下,你在参加一场化妆舞会,但规则很特别:你不能给自己戴上面具或穿上戏服,只能请一位"魔法化妆师"(CSS)在你身上直接绘制妆容。这位化妆师手法高超,他能凭空在你耳边画上耳环,在额头点上装饰——而这一切都不需要你真正佩戴任何实物。
这就是CSS伪元素的魔法!它们就像HTML元素的"隐形斗篷",让我们在不修改HTML结构的情况下,为页面添加各种装饰效果。
第一章:认识"双胞胎幽灵"——::before和::after
伪元素中最常用的就是这对"双胞胎兄弟":::before和 ::after。它们不是HTML中真实存在的元素,而是CSS创造的"幽灵元素"。
/* 魔法咒语的基本格式 */
.元素::before {
content: ""; /* 魔法的核心:必须念出咒语 */
/* 其他样式属性 */
}
.元素::after {
content: ""; /* 同样需要咒语 */
/* 其他样式属性 */
}
魔法规则第一条:content咒语
content属性是开启伪元素魔法的钥匙。即使你想创建空的内容,也必须写上 content: "",就像念咒语一样,不念就没魔法!
第二章:伪元素的实战魔法——创建笑脸表情
让我们回到文章开头的代码,看看这个"笑脸魔法"是如何实现的:
/* 基础的脸部 */
.face {
width: 100px;
height: 100px;
background-color: #ffdd59; /* 阳光黄色 */
border-radius: 50%; /* 变成圆形 */
position: relative; /* 重要:为伪元素设置舞台 */
}
/* 召唤两只"幽灵眼睛" */
.face::after, .face::before {
content: ""; /* 念咒语:创造存在 */
position: absolute; /* 让它们漂浮在脸部上 */
width: 18px; /* 眼睛宽度 */
height: 8px; /* 眼睛高度 */
background-color: #badc58; /* 清新的黄绿色 */
top: 20px; /* 从头顶往下20像素 */
border-radius: 50%; /* 椭圆形的眼睛 */
}
/* 右眼:调皮地往右看 */
.face::before {
right: -8px; /* 从右边往左偏移8px,稍微突出 */
}
/* 左眼:俏皮地往左看 */
.face::after {
left: -5px; /* 从左边往右偏移5px,稍微突出 */
}
魔法效果可视化:
👁️ 😊 👁️
(::before) (.face) (::after)
右眼幽灵 笑脸主体 左眼幽灵
向右突出 向左突出
这个笑脸最妙的地方在于:HTML只需要一个简单的 <div class="face"></div>,所有的眼睛装饰都由CSS凭空创造!
第三章:为什么不用真实元素?——魔法的智慧
你可能会问:"为什么不用真实的 <span>元素来当眼睛呢?"
让我们对比一下两种方式:
方式A:使用真实元素(麻瓜方法)
<div class="face">
<span class="eye left"></span>
<span class="eye right"></span>
</div>
方式B:使用伪元素(巫师方法)
<div class="face"></div>
巫师的三大理由:
- HTML保持纯净:HTML应该只关心内容结构,而不是表现装饰。眼睛是装饰,不是内容。
- 维护更容易:想改变眼睛样式?只需修改CSS,完全不用碰HTML。
- 性能更优:伪元素不会增加真实的DOM节点,浏览器渲染起来更轻快。
第四章:伪元素的更多魔法应用
伪元素的魔法远不止画眼睛!它们在Web设计中无处不在:
魔法1:优雅的引用标记
blockquote::before {
content: "“"; /* 左引号 */
font-size: 3em;
color: #ccc;
}
blockquote::after {
content: "”"; /* 右引号 */
font-size: 3em;
color: #ccc;
}
魔法2:自定义列表图标
li::before {
content: "✨"; /* 星星图标 */
margin-right: 10px;
}
魔法3:工具提示的小箭头
.tooltip::after {
content: "";
position: absolute;
/* 绘制三角形箭头 */
border: 10px solid transparent;
border-top-color: #333;
}
第五章:伪元素的工作原理——魔法的科学
虽然叫"伪元素",但它们在浏览器中表现得像真实元素一样:
<!-- 浏览器眼中的伪元素 -->
<div class="face">
<!-- 这是::before -->
😉
<!-- 这是元素的实际内容(如果有的话) -->
😊
<!-- 这是::after -->
😉
</div>
位置关系三兄弟:
/* 源代码顺序 */
.元素::before { /* 大哥:在最前面 */ }
.元素 { /* 二弟:实际内容 */ }
.元素::after { /* 三弟:在最后面 */ }
第六章:伪元素 vs 伪类——不要搞混的魔法
新手常把伪元素和伪类搞混,记住这个简单区别:
-
伪元素:
::before、::after、::first-line- 创造新的虚拟元素
- 双冒号
::(CSS3规范,兼容单冒号:)
-
伪类:
:hover、:focus、:nth-child()- 选择元素的特定状态
- 单冒号
:
/* 伪类:鼠标悬停时变色 */
button:hover {
background-color: blue;
}
/* 伪元素:添加装饰性内容 */
button::before {
content: "👉 ";
}
第七章:魔法的局限与禁忌
虽然伪元素很强大,但也不是万能的:
可以用伪元素(装饰性内容):
✅ 装饰性图标、角标
✅ 工具提示的箭头
✅ 引用符号
✅ 清除浮动的空元素
✅ 我们的笑脸眼睛
不要用伪元素(重要内容):
❌ 重要的导航链接
❌ 需要被搜索引擎收录的文字
❌ 需要JavaScript交互的元素
❌ 对可访问性重要的内容
第八章:现代魔法进阶
组合魔法:多重伪元素
/* 创建更复杂的装饰 */
.icon::before {
/* 背景层 */
}
.icon::after {
/* 前景层 */
}
动态魔法:结合CSS变量
.face::before {
content: "";
width: var(--eye-size, 18px);
height: calc(var(--eye-size, 18px) * 0.444);
}
结语:成为CSS魔法师
CSS伪元素就像Web开发中的"隐形画笔",让我们能够:
- 保持HTML的语义纯净——内容就是内容,装饰交给CSS
- 实现优雅的视觉效果——无需污染DOM结构
- 提高代码维护性——所有样式集中管理
- 优化页面性能——减少不必要的DOM节点
记住这个魔法口诀:
"content不可少,定位要记牢,装饰用伪元,内容用标签"
下次当你想要添加一些装饰性元素时,先问问自己:"这可以用伪元素实现吗?" 很多时候,答案都是肯定的!
现在,拿起你的CSS魔杖(键盘),开始用伪元素创造更多神奇的Web魔法吧!🎩✨
魔法小测验:你能只用CSS伪元素创建一个完整的太阳系动画吗?提示:一个div代表太阳,伪元素代表行星... 无限创意等着你!
