写过前端的都知道——以前想给 CSS 加个带参数的函数,只能靠 Sass 预处理器——今天浏览器把这件事用 @function 从根上原生化了

写了多年 CSS,你会发现一个尴尬的事实:你想定义一个可复用的计算逻辑,比如一个流体字号的公式,对不起,原生 CSS 做不到。只能用 Sass 写一个 function,然后在构建流程里编译成静态值。但如果这个值是动态的呢?媒体查询变一次、用户切换主题一次、JavaScript 改了一个 CSS 变量一次——Sass 编译出来的那个数字永远跟不上了。这个坑,今天被 Chrome/Edge 139+ 的原生 @function 彻底填上了。

Sass 函数最大的局限:它在构建时就跑完了

Sass 的 @function 是个编译期工具。你写 fluid-type(1rem, 2rem),构建工具把它翻译成一串 clamp() 代码,这个值从此固定。运行时 CSS 变量变了、媒体查询切换了、容器尺寸改了——对不起,浏览器看到的是构建时那个静态数字。

这个问题在实际项目里非常普遍。比如你有一个响应式字体系统:

// Sass 写法
@function fluid-type($min, $max) {
  @return clamp($min, calc($min + ($max - $min) * ((100vw - 320px) / 960)), $max);
}

h1 { font-size: fluid-type(1.5rem, 3rem); }
p { font-size: fluid-type(1rem, 1.25rem); }

编译后 h1 的 font-size 就变成了类似 clamp(1.5rem, 0.5rem + 1vw, 3rem) 的静态值。如果用户放大字体、切换暗色模式触发了 CSS 变量变化——Sass 完全无能为力。

@function:运行在浏览器里的原生函数

原生 CSS @function 的语法几乎和 Sass 一模一样:

@function --fluid-type(--min, --max, --rate: 4vw) {
  result: clamp(var(--min), var(--rate) + 1rem, var(--max));
}

h1 { font-size: --fluid-type(1.5rem, 3rem); }
p { font-size: --fluid-type(1rem, 1.25rem, 0.5vw); }

关键区别在于:这个函数运行在浏览器里,而不是构建时。每次 --rate 对应的 CSS 变量变了、每次 100vw 对应的视口尺寸变了,函数都会重新计算。

带类型的参数

@function 支持 type() 语法来约束参数类型,构建工具链级别的安全保障:

@function --progression(--current, --total) returns {
  result: calc(var(--current) / var(--total) * 100%);
}

传入非数字值时,函数直接失效,浏览器使用 fallback 值。这意味着你可以在不影响旧浏览器的前提下给新浏览器提供更精确的计算。

支持 @media 和条件逻辑

这是真正颠覆性的能力——@function 内部可以写 @media:

@function --responsive-padding(--desktop, --tablet: var(--desktop), --mobile: var(--tablet)) {
  result: var(--desktop);
  @media (max-width: 1200px) {
    result: var(--tablet);
  }
  @media (max-width: 600px) {
    result: var(--mobile);
  }
}

以前要实现「桌面端一个 padding、移动端一个 padding」,你得写两套选择器或者用 CSS 变量 JS 注入。现在一套函数搞定,逻辑内聚,代码可维护。

配合 @container style() 做组件级响应式

@function 天然能读取 CSS 自定义属性,而自定义属性可以被容器查询驱动:

@function --card-width() returns {
  result: clamp(200px, 50vi, 400px);
}

.card { width: --card-width(); }

@container card (max-width: 300px) {
  .card { --card-width: 100%; } /* 触发函数重新计算 */
}

组件不再依赖全局媒体查询,而是直接感知自身容器尺寸。这套组合以前只能用 JS 实现,现在纯 CSS。

浏览器支持现状

Chrome 和 Edge 从 139 版本开始支持,约 68% 全球覆盖率。Firefox 和 Safari 已在实现路上,但尚未稳定发布。

这意味着 @function 目前不是全量生产可用的,但 progressive enhancement 策略让它值得现在就上手:

.bar {
  width: 75%; /* 所有浏览器的 fallback */
  width: --progression(3, 4); /* 仅 Chrome/Edge 139+ 生效 */
}

浏览器不认识 --progression(...) 时会忽略该声明,使用上面那个 fallback 值。渐进增强,不需要 JavaScript 条件判断。

和 @mixin/@apply 的关系

MDN 文档里提到 CSS 自定义函数和 mixins 是一套规范,@mixin@apply 也在规范里。但目前只有 @function 有浏览器支持,@mixin 和 @apply 任何浏览器都还没实现。所以现在的重点是 @function,@mixin 可以关注但别急着在生产里用。

实际迁移路径

如果你现在有 Sass 函数,往原生 @function 迁移路径很清晰:

/* 以前(Sass)*/
@function section-padding($space: 1rem) {
  @return calc($space * 2) calc($space * 3);
}

/* 现在(原生 @function)*/
@function --section-padding(--space: 1rem) {
  result: calc(var(--space) * 2) calc(var(--space) * 3);
}

函数名从 $ 前缀改成 -- 前缀(和 CSS 变量一致),@return 改成 result:,参数类型声明可选添加。两者语法非常接近,迁移成本很低。

结论

@function 是 CSS 工具链的一次范式转移:它把「在 CSS 里写逻辑」这件事从构建时拉到了运行时。Sass 函数编译后是死的,原生 @function 是活的——它能感知 custom property 的每一次变化、容器查询的每一次触发、媒体查询的每一次切换。对于已经用 CSS 变量做 design token 的团队,这基本上是零成本接入。

唯一需要注意的是浏览器支持还在扩展中,记得给每个函数配 fallback 值,用 cascade 本身提供天然的 fallback 机制,更简单。

下一步:翻出你项目中那个最复杂的 Sass function,试着把它转成一个原生 @function,你可能会发现——再也不需要为它维护一套构建流程了。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 15 阅读