◆

Sass @function 自定义函数用法

2023-02-02 01:370

function

@function 的定义方式和 @mixin 非常类似,但是调用的时候不需要 @include 而是直接调用。

@function 与 @mixin 不同之处在于,函数返回的只能是一个值,而不是一段 css 样式代码,也就是说函数返回的结果只能作为变量值或属性值。

@function my-color() {
  @return #f00;
}

.div {
  background-color: my-color();
}

参数:默认值、关键字传参、任意参数

函数的参数写法和 @mixin 完全一致。参数带默认值时可以省略,并且支持按名字传参,这样多参数函数就不用记顺序:

// $base 必传,$ratio 有默认值
@function rem($px, $base: 16px) {
  @return math.div($px, $base) * 1rem;
}

.title {
  font-size: rem(24px); // 1.5rem
  // 关键字传参,跳过中间的参数
  padding: rem($px: 8px, $base: 8px); // 1rem
}

参数个数不定时在最后一个参数后面加 ...,它会把剩余实参收成一个 list:

@use 'sass:list';
@use 'sass:math';

// 求最大值
@function my-max($values...) {
  $result: list.nth($values, 1);
  @each $v in $values {
    @if $v > $result {
      $result: $v;
    }
  }
  @return $result;
}

.box {
  width: my-max(10px, 40px, 25px); // 40px
}

调用时同样可以用 ... 把一个 list 展开成多个实参:my-max($sizes...)。

单位运算是最大的坑

Sass 的数字带单位,运算时单位也参与,这一点和 JS 完全不同。除法尤其容易出问题:

@use 'sass:math';

// 错:24px / 16px 在旧语法下得到 1.5,但 Sass 3+ 会警告,且在某些上下文里
//     斜杠会被当成 CSS 的分隔符原样输出(比如 font: 12px/1.5)
@function bad-rem($px) {
  @return $px / 16px * 1rem;
}

// 对:用 math.div,单位相除会正确消掉
@function good-rem($px) {
  @return math.div($px, 16px) * 1rem;
}

从 Dart Sass 1.33 开始 / 做除法已被废弃,2.0 会彻底移除,必须改用 math.div()。这是我升级 Sass 版本时改动量最大的一处。

单位相乘也要小心:math.div(24px, 16px) 得到无单位的 1.5,再 * 1rem 才是 1.5rem。如果写成 math.div(24px, 16),结果是 1.5px,乘 1rem 会报 1.5px*1rem isn't a valid CSS value——px * rem 这种复合单位在 CSS 里不存在。

想剥掉单位有个惯用写法:

@use 'sass:math';

@function strip-unit($n) {
  @if math.is-unitless($n) {
    @return $n;
  }
  // 除以 1 个同单位的数,单位互相消掉
  @return math.div($n, ($n * 0 + 1));
}

参数校验

函数是给别人(包括三个月后的自己)调用的,加校验能把错误挡在编译期。@error 会中断编译并输出信息,@warn 只打印警告:

@use 'sass:meta';
@use 'sass:math';

@function rem($px, $base: 16px) {
  @if meta.type-of($px) != 'number' {
    @error '$px 必须是数字,当前传入 #{meta.inspect($px)}';
  }
  @if math.is-unitless($px) {
    @warn '$px 建议带 px 单位,已按 px 处理';
    $px: $px * 1px;
  }
  @return math.div($px, $base) * 1rem;
}

meta.inspect() 会把值连同类型一起打印出来,调试 map 和 list 时比直接插值有用得多——直接插值一个 list 只会得到 1px 2px,看不出它是 list 还是 string。

用 map 做配置查表

实际项目里 @function 用得最多的场景是从配置 map 里安全取值:

@use 'sass:map';

$spacings: (
  'xs': 4px,
  'sm': 8px,
  'md': 16px,
  'lg': 24px,
);

@function space($key) {
  @if not map.has-key($spacings, $key) {
    @error '未定义的间距 #{$key},可选值:#{map.keys($spacings)}';
  }
  @return map.get($spacings, $key);
}

.card {
  padding: space('md') space('lg');
}

比直接写 map.get($spacings, 'md') 好在拼错 key 时会立刻报错,而 map.get 拿不到会静默返回 null,编译出来的 padding: null 那行会被整条丢掉——样式莫名其妙不生效,排查起来很费时间。

@function 和 @mixin 怎么选

原文那句「函数返回一个值,mixin 输出一段样式」是核心判据,实操时我这样分:

  • 需要算出一个值再赋给某个属性 → @function。
  • 需要批量输出多条声明,或者输出带嵌套/媒体查询的规则块 → @mixin。
  • 需要输出规则块并且让调用方往里塞内容 → @mixin 配 @content。

@function 里不能写属性声明,@return 之后的代码不会执行。反过来 @mixin 里不能 @return。

有个细节:@function 里可以用 @if / @each / @for 等控制指令,也可以调用其他函数,甚至递归调用自己,只要每条分支最终都有 @return。少了任何一条分支的 @return,那条路径会返回 null。

模块化后的命名空间

用 @use 引入自己的函数文件时会带上命名空间:

// _functions.scss
@function rem($px) {
  /* ... */
}

// index.scss
@use 'functions';

.a {
  font-size: functions.rem(24px);
}

嫌前缀长可以 @use 'functions' as f; 或者 @use 'functions' as *; 直接摊平到当前作用域。摊平方便但容易和内置函数或其他文件撞名,我一般只在项目自己的工具文件上用 as *,第三方库保留命名空间。

另外内置模块(sass:math、sass:map、sass:list、sass:string、sass:color、sass:meta)都必须先 @use 才能用带前缀的写法,忘了引入会报 There is no module with the namespace "math"。

💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVESass @function 自定义函数用法加载中