在 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::aftercontentposition: 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)

逐部分看:

  1. & 替换为 .invite-card-item
  2. :not(...) 是 CSS 的否定伪类,表示“不满足括号内的选择器”。
  3. :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,即使同时写了 widthheightbackground-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 的硬性配对规则:父元素也可以是 absolutefixedsticky。实际要求是,祖先需要建立合适的 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: 0right: 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;
  }
}
  1. 确认 SCSS 已启用style 标签是否写了 lang="scss",构建日志是否有 Sass 错误。
  2. 确认选择器命中了项目:最后一个项目本来就不会匹配 :not(:last-child)
  3. 确认伪元素被生成:检查是否写了 content: '',没有被后续规则覆盖为 nonenormal
  4. 确认尺寸和颜色可见:检查 height、背景色、透明度,以及父元素是否被 overflow 裁剪。
  5. 确认定位参考正确position: relative 应写在实际项目元素上,而不是只写在更外层列表容器上。
  6. 确认没有被遮挡:必要时检查 z-index 和相邻元素的背景色。
  7. 确认目标平台支持情况:分别查看 H5、微信小程序或 App 的实际渲染结果。

总结

可以把最初的分割线写法压缩成下面五个判断:

  • &:引用当前 SCSS 父级选择器。
  • :not(:last-child):筛选不是最后一个子元素的项目。
  • ::after:为已选中的项目生成一个伪元素盒子。
  • content: '':让这个空的伪元素真正生成。
  • position: relative + position: absolute:让分割线以当前项目为定位参考,贴在项目底部。

最终选择器是:

.invite-card-item:not(:last-child)::after

当看到任意 SCSS 嵌套代码时,先替换 &,再区分空格、伪类和伪元素,最后检查生成内容和定位上下文,就能把它还原成普通 CSS 来分析。