uni-app SCSS 基础:从 &、:not 到 ::after 的选择器与定位
在 uni-app 的 .vue 文件中,下面这段样式很常见:给列表项添加一条分割线,但最后一项不添加。
<style lang="scss" scoped>
.invite-card-item {
position: relative;
&:not(:last-child)::after {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 2rpx;
background-color: rgba(151, 151, 151, 0.2);
border-radius: 9999rpx;
}
}
</style>
初看时,&、:not、::after、content 和 position: relative 容易被当成一条规则来记。实际上它们来自两个层次:& 属于 SCSS 的嵌套语法,其他部分属于 CSS 选择器或定位规则。把 SCSS 展开成 CSS,再看浏览器或平台最终匹配什么元素,代码就容易理解了。
1. uni-app 中的 SCSS 到底做了什么
<style lang="scss"> 告诉构建工具:这一段内容先交给 SCSS 编译器处理。SCSS 编译结束后,运行时使用的仍然是普通 CSS;手机端、微信小程序和 H5 并不会直接执行 & 这个符号。
例如:
.card {
color: #222;
.title {
font-size: 32rpx;
}
}
编译结果近似为:
.card {
color: #222;
}
.card .title {
font-size: 32rpx;
}
这里的嵌套不是 HTML 的真实嵌套,也不会创建任何节点。SCSS 只是把父选择器和子选择器组合起来,生成更长的 CSS 选择器。
因此,遇到看不懂的嵌套样式时,可以先做一个简单动作:把每个嵌套块手动展开,确认最终选择器是什么。
2. & 是父级选择器本身
在 SCSS 中,& 表示当前嵌套位置外层的完整选择器。
.invite-card-item {
& {
color: #222;
}
}
编译后是:
.invite-card-item {
color: #222;
}
所以,单独写 & {} 通常只是把当前选择器再写一遍。它不会创建一个“父节点”,也不会改变 HTML 结构。只有在需要把当前选择器和其他选择器、状态或伪元素组合时,& 才真正有价值。
2.1 有空格和没有空格,含义不同
.item {
.title {
color: #333;
}
& .title {
font-weight: 600;
}
&.selected {
background-color: #f5f5f5;
}
}
编译结果为:
.item .title {
color: #333;
}
.item .title {
font-weight: 600;
}
.item.selected {
background-color: #f5f5f5;
}
可以按下面的方式区分:
| SCSS 写法 | 编译后的选择器 | 选中的对象 |
|---|---|---|
.item .title |
.item .title |
.item 后代中的 .title |
& .title |
.item .title |
.item 后代中的 .title |
&.selected |
.item.selected |
同时有两个 class 的同一个元素 |
& > .title |
.item > .title |
.item 的直接子元素 .title |
. 和空格是关键:
&.selected中没有空格,表示两个 class 在同一个元素上。& .selected中有空格,表示.selected是后代元素。& > .selected进一步限制为直接子元素。
3. &:not(:last-child) 如何阅读
把这段选择器拆成三部分:
&:not(:last-child)
在 .invite-card-item 内部,它会编译成:
.invite-card-item:not(:last-child)
逐部分看:
&替换为.invite-card-item。:not(...)是 CSS 的否定伪类,表示“不满足括号内的选择器”。:last-child表示“是父元素最后一个子元素”。
因此,.invite-card-item:not(:last-child) 的意思是:选择不是父元素最后一个子元素的 .invite-card-item。
假设列表中有三个同级项目:
<view class="list">
<view class="invite-card-item">第一项</view>
<view class="invite-card-item">第二项</view>
<view class="invite-card-item">第三项</view>
</view>
前两项满足 :not(:last-child),最后一项不满足。因此分割线只会出现在前两项的 ::after 上。
这里的 :not 不是 SCSS 特有语法,而是编译后仍然存在的原生 CSS 伪类。SCSS 只负责把 & 替换成父选择器。
3.1 :last-child 不等于“最后一个同类元素”
:last-child 判断的是:当前元素是否为父元素的最后一个元素子节点,而不是父元素中最后一个相同 class 的元素。
如果需要判断“同类型元素中的最后一个”,可以考虑 :last-of-type,但它按标签类型判断,并不按 class 判断。列表结构复杂时,最好让模板输出明确的状态 class,例如 .is-last,或者在数据层决定是否绘制分割线。
4. 单冒号和双冒号分别表示什么
CSS 中的 : 和 :: 不是装饰符号,它们表示两种不同的特殊选择器。
4.1 单冒号:伪类
伪类描述一个已有元素的状态、位置或条件:
button:hover {
opacity: 0.8;
}
.item:first-child {
padding-top: 0;
}
.item:not(.disabled) {
color: #222;
}
这些选择器不会创建新元素,只是筛选已有元素。
4.2 双冒号:伪元素
伪元素表示元素生成内容中的一个特殊部分,常见的有:
.item::before { }
.item::after { }
::before 和 ::after 不对应模板中的真实节点。它们是浏览器或平台根据 CSS 生成的盒子,通常分别放在元素内容的前面和后面。
所以原选择器可以这样读:
&:not(:last-child)::after
先选出“不属于最后一项”的 .invite-card-item,再选中这些元素各自生成的 ::after 盒子。
旧代码中也经常看到单冒号写法:
.item:after { }
为了和伪类区分,现代 CSS 推荐使用双冒号 ::after。部分运行环境仍兼容单冒号,但新代码使用双冒号更清楚。
5. content: '' 为什么不能省略
::before 和 ::after 是生成内容的伪元素。content 属性决定它们是否生成内容盒子。
.item::after {
content: '';
width: 100%;
height: 2rpx;
background-color: #ddd;
}
这里的空字符串不是要显示一段空白文字,而是用来创建一个没有文字的伪元素。创建后,伪元素才能拥有尺寸、背景色和定位。
如果完全不写 content,通常不会生成可见的 ::after,即使同时写了 width、height 和 background-color,分割线也不会出现。
content 也可以生成文本或图片:
.required::before {
content: '*';
color: #d93025;
}
.external-link::after {
content: url('/static/external-link.png');
}
但伪元素更适合装饰性内容。重要文字、可点击控件和需要被辅助技术读取的内容,应直接写在模板中,例如使用 <text> 或 <view>。不要把业务数据、交互按钮或关键提示只放进 content,因为不同 uni-app 目标平台对生成内容的支持和可访问性表现可能不同。
6. 为什么父元素要 position: relative
示例中的 ::after 使用了:
position: absolute;
right: 0;
bottom: 0;
left: 0;
绝对定位元素需要一个定位参考框。浏览器会向上查找最近的、position 不为 static 的祖先元素,把它作为定位上下文。position: relative 是最常用的选择,因为它可以建立这个参考框,同时不改变元素在普通文档流中的原位置。
因此:
.invite-card-item {
position: relative;
}
表示“以当前列表项为 ::after 的定位参考”。这就是常说的“子绝父相”,但它不是 CSS 的硬性配对规则:父元素也可以是 absolute、fixed 或 sticky。实际要求是,祖先需要建立合适的 containing block;relative 通常最少影响布局。
如果不设置 position: relative,::after 可能继续向上寻找其他定位祖先,最后以页面或外层容器作为参考,导致分割线出现在错误的位置。
6.1 逐项解释定位属性
.invite-card-item::after {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 2rpx;
}
position: absolute:让伪元素脱离普通文档流,不再占用列表项的正常高度。bottom: 0:把伪元素的底边贴到定位参考框的底部。left: 0和right: 0:让它从左边一直铺到右边。height: 2rpx:只设置分割线的厚度,宽度由左右偏移共同确定。
因为分割线脱离了普通文档流,如果它覆盖了内容,需要给列表项增加合适的底部内边距,或者调整分割线的位置。
6.2 border-radius: 9999rpx 为什么能变成圆头
当一个盒子的高度只有 2rpx,而圆角半径设置为很大的值时,CSS 会把实际半径限制在盒子尺寸允许的范围内。最终效果就是高度为 2rpx 的胶囊形线条,两端各呈半圆。
下面两种写法通常都能得到圆头分割线:
border-radius: 9999rpx;
border-radius: 1rpx;
前者表达“无论宽度如何都使用最大圆角”,后者直接使用高度的一半。项目中可以根据团队的样式约定选择一种。
7. 一个完整的 uni-app 示例
下面的示例把列表、项目内容和分割线放在一起:
<template>
<view class="invite-list">
<view class="invite-card-item">
<text class="invite-card-item__title">邀请记录</text>
<text class="invite-card-item__value">已完成</text>
</view>
<view class="invite-card-item">
<text class="invite-card-item__title">奖励状态</text>
<text class="invite-card-item__value">待领取</text>
</view>
<view class="invite-card-item">
<text class="invite-card-item__title">有效期</text>
<text class="invite-card-item__value">2026-12-31</text>
</view>
</view>
</template>
<style lang="scss" scoped>
.invite-list {
padding: 0 32rpx;
}
.invite-card-item {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 88rpx;
&:not(:last-child)::after {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 2rpx;
background-color: rgba(151, 151, 151, 0.2);
border-radius: 9999rpx;
}
&__title {
color: #222;
font-size: 28rpx;
}
&__value {
color: #777;
font-size: 26rpx;
}
}
</style>
其中,&__title 和 &__value 是另一种常见的 & 用法。它们没有空格,表示把后缀直接拼接到父选择器上:
.invite-card-item {
&__title { }
}
编译后近似为:
.invite-card-item__title { }
这属于 BEM 命名方式中的元素写法。它和下面的写法不同:
.invite-card-item {
.title { }
}
后者会生成 .invite-card-item .title,要求 .title 是后代元素。
8. & 的几个常用组合
掌握“替换父选择器”后,下面的写法都可以直接展开阅读:
.button {
&:hover {
opacity: 0.8;
}
&.is-active {
color: #1677ff;
}
&[disabled] {
opacity: 0.5;
}
& > .icon {
margin-right: 8rpx;
}
& + & {
margin-top: 16rpx;
}
}
对应的选择器分别是:
.button:hover { }
.button.is-active { }
.button[disabled] { }
.button > .icon { }
.button + .button { }
最后一种 & + & 表示“紧邻的两个 .button 之间”。它常用于连续列表项之间的间距,但要确认外层结构不会让生成的选择器过于复杂。
& 也可以放在前置上下文之后:
.button {
.theme-dark & {
color: #fff;
}
}
编译后为:
.theme-dark .button {
color: #fff;
}
这表示当 .button 位于 .theme-dark 后代中时使用另一套样式。
9. uni-app 中需要额外注意的地方
9.1 lang="scss" 只是编译入口
下面两种写法的区别在于是否启用 SCSS 编译:
<style>
/* 只能写普通 CSS */
</style>
<style lang="scss">
// 可以使用变量、嵌套、&、mixin 等 SCSS 语法
</style>
如果项目没有配置对应的 Sass 编译依赖,构建会在编译阶段报错。具体安装方式应按项目使用的 uni-app CLI、Vite 或 HBuilderX 配置处理,不能把不同工具链的依赖命令混用。
9.2 scoped 不会改变 & 的含义
<style lang="scss" scoped>
.item {
&::after {
content: '';
}
}
</style>
& 仍然先展开为 .item。随后 Vue 或 uni-app 的样式处理器会为选择器增加组件作用域标记,最终效果可以近似理解为:
.item[data-v-xxxx]::after {
content: '';
}
data-v-xxxx 只是示意,实际标记由编译器生成。scoped 主要解决组件样式隔离,不会把伪元素变成真实节点,也不会让样式自动穿透子组件。需要修改子组件内部样式时,应使用当前项目和版本支持的 deep selector,并在目标平台验证编译结果。
9.3 rpx 是 uni-app 的尺寸单位
rpx 会根据设备屏幕宽度和设计稿宽度进行换算,常见设计稿宽度为 750。它不是浏览器 CSS 中与 px 完全等价的单位。
因此,height: 2rpx 表示按 uni-app 的尺寸规则计算分割线厚度。实际显示可能受到设备像素比和平台渲染方式影响;如果需要非常稳定的物理像素细线,应在目标平台分别检查效果,不要只根据 H5 浏览器截图判断。
9.4 伪元素要按目标平台验证
:not()、绝对定位和伪元素是常见 CSS 能力,但 uni-app 可能把样式编译到 H5、微信小程序和 App 不同的渲染环境。尤其是伪元素生成文本、复杂选择器、触摸设备上的 :hover,不能只按桌面浏览器的行为推断。
装饰性分割线适合使用 ::after;如果元素承担内容、交互或布局职责,使用模板中的真实节点通常更容易保证跨平台结果。
10. 排查“分割线不显示”的顺序
遇到下面的代码没有效果时,可以按顺序检查:
.item {
position: relative;
&:not(:last-child)::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2rpx;
background-color: #ddd;
}
}
- 确认 SCSS 已启用:
style标签是否写了lang="scss",构建日志是否有 Sass 错误。 - 确认选择器命中了项目:最后一个项目本来就不会匹配
:not(:last-child)。 - 确认伪元素被生成:检查是否写了
content: '',没有被后续规则覆盖为none或normal。 - 确认尺寸和颜色可见:检查
height、背景色、透明度,以及父元素是否被overflow裁剪。 - 确认定位参考正确:
position: relative应写在实际项目元素上,而不是只写在更外层列表容器上。 - 确认没有被遮挡:必要时检查
z-index和相邻元素的背景色。 - 确认目标平台支持情况:分别查看 H5、微信小程序或 App 的实际渲染结果。
总结
可以把最初的分割线写法压缩成下面五个判断:
&:引用当前 SCSS 父级选择器。:not(:last-child):筛选不是最后一个子元素的项目。::after:为已选中的项目生成一个伪元素盒子。content: '':让这个空的伪元素真正生成。position: relative+position: absolute:让分割线以当前项目为定位参考,贴在项目底部。
最终选择器是:
.invite-card-item:not(:last-child)::after
当看到任意 SCSS 嵌套代码时,先替换 &,再区分空格、伪类和伪元素,最后检查生成内容和定位上下文,就能把它还原成普通 CSS 来分析。