likes
comments
collection
share

通过Element Plus的样式库探索Sass

作者站长头像
站长
· 阅读数 44

前言

Element Plus的样式库基于Sass构建,Sass是一种功能强大的CSS预处理器。通过深入探索 Element Plus 的样式库,我们将有机会更加深入地理解 Sass 的语法、特性以及使用方式,从而提升我们的样式开发效率。

如果你之前未曾接触过 Sass,建议首先查阅我之前翻译的 Sass 官方文档,熟悉Sass的基础知识,这将有助于你更好地理解接下来的文章内容。在接下来的文章中,我将结合样式库中的实际代码,详细介绍 Sass 的使用方法,以便我们在项目中能够更全面地使用 Sass,而不仅仅局限于其嵌套功能。

前置知识

由于 Element Plus 在其项目中采用了 BEM 类命名规范,因此我们有必要首先对这个规范进行一些基本的了解。

BEM简介

BEM 是一种 CSS 类命名规范,是 Block、Element、Modifier(块、元素、修饰符)的缩写。这种命名规范的主要目的是帮助开发者更清晰、更系统地编写 CSS 类,以便更容易地被理解和复用。

  • 块(Block)

    块(Block)是页面中的一个独立实体,具有一定的功能意义,比如一个按钮(.button)、一个导航栏(.navbar),等等。块应该是独立的,可以在任何地方复用。

  • 元素(Element)

    元素(Element)是块的一个组成部分,不能脱离块独立存在。元素的命名规则是块名后面跟上两个下划线,再加上元素名,比如 .block__element

  • 修饰符(Modifier)

    修饰符(Modifier)用来描述块或元素的一种状态或者属性。修饰符的命名规则是块或元素名后面跟上两个破折号,再加上修饰符名,比如 .block--modifier.block__element--modifier

例如,一个按钮块可能有一个大小修饰符(.button--large),一个导航项元素可能有一个激活状态修饰符(.navbar__item--active)。

BEM 命名规范的使用可以帮助我们更好地组织 CSS 代码,更清晰地表示元素之间的关系,降低样式冲突的可能性,使得样式更加易于维护。

准备工作

以下是一些你在开始之前需要做的准备工作:

  • 安装pnpm。Element Plus 项目使用 pnpm 作为其包管理器。你可以通过运行 npm install -g pnpm 来全局安装 pnpm。

  • 克隆element-plus仓库到你的本地。你可以通过运行 git clone https://github.com/element-plus/element-plus.git 命令来完成这个步骤。

  • 运行项目。你可以在项目的根目录下打开终端,然后运行 pnpm dev 命令来启动项目。

  • 寻找样式库的代码。你可以在 packages\theme-chalk 目录下找到它。

在继续阅读之前,请确保你已经理解并完成了以上的准备工作。

Affix固钉组件样式

首先,查看Affix固钉组件的样式代码,这些代码被保存在src\affix.scss文件中。

@use 'mixins/mixins' as *;

@include b(affix) {
  @include m(fixed) {
    position: fixed;
  }
}

在这段代码中,使用 @use 指令来引入 mixins 文件夹下的 mixins.scss 文件。这样就可以在当前文件中使用 mixins.scss 文件中定义的变量、混入和函数。

默认情况下,使用 @use 指令引入的模块会带有命名空间,这个命名空间通常是模块 URL 中不包括文件扩展名的最后一个部分。但是,如果我们在 @use 指令后面添加了 as *,这将会引入一个没有命名空间的模块。例如,在上述代码中,如果不添加 as *,那么应该这样书写代码:

@use 'mixins/mixins';

@include mixins.b(affix) {
  @include mixins.m(fixed) {
    position: fixed;
  }
}

可以看到,在确保不会发生命名冲突的前提下,省略命名空间会让代码看起来更为简洁和清爽。

接下来可以看到使用了@include指令把在mixins.scss中定义的混入b包含到了当前上下文中。进入到mixins.scss文件中,查看混入b的定义:

@mixin b($block) {
  $B: $namespace + $common-separator + $block !global;

  .#{$B} {
    @content;
  }
}

1、@mixin是 Sass 语言中定义混入的关键字。混入是一种可以重用的代码块,类似于函数。这个混入的名字是b,并且它有一个参数$block。在这个例子中$block变量引用的是affix

2、在混入的主体中,使用!global标志定义了一个变量$B。没有!global标志时,$B仅在当前作用域可用;加了!global标志之后,$B在全局作用域可用。$namespace$common-separator都是在config.scss中定义的变量,一个是"el",另一个是“-”。所以这里的$B是"el-affix"

3、使用插值来生成选择器。插值让加引号的字符串变成未加引号的字符串,所以这里的.#{$B}代表的是.el-affix而不是."el-affix"。

4、混入可以通过在其主体中包含@content指令来声明它接受一个内容块。例如:

// base.scss

@mixin main {
  .main {
    @content;
  }
}

// style.scss
@use "base" as *;

@include main {
  color: red;
}

上面的代码会编译成:

.main {
  color: red;
}

混入b已经讲解完毕,接下来看混入m

@mixin m($modifier) {
  $selector: &;
  $currentSelector: '';
  @each $unit in $modifier {
    $currentSelector: #{$currentSelector +
      $selector +
      $modifier-separator +
      $unit +
      ','};
  }

  @at-root {
    #{$currentSelector} {
      @content;
    }
  }
}

1、&表示父选择器,在这里的值是.el-affix。

2、@each用于遍历列表或映射,此处用于遍历列表。也就是说$modifier代表的是一个列表。但是代码中传入的是fixed,实参是字符串,行参是列表。为什么会这样呢?因为sass会在需要的时候将单个值视为只包含一个元素的列表。这里使用@each来遍历它,正是需要的时候。

3、拼接字符串,这里$modifier的值是fixed,列表中只有一个值,效果不是很明显。假如$modifier的值是一个列表(left, right),那么$currentSelector的值是.el-affix--left,.el-affix--right,

4、@at-root会使其中的所有内容直接在文档的根级别输出,而不是按照正常的嵌套规则。例如:

.first {
   @at-root {
     .second {
       color: red;
     }
   }
}

上面的代码会编译成:

.second {
  color: red;
}

5、sass在编译选择器的时候会将最后的,去掉,所以最终编译的结果是:

.el-affix--fixed {
  position: fixed;
}

affix.scss文件中的代码比较简单,接下来看一个稍微复杂点的代码。

Alert提示组件样式

在alert.scss文件中,混入b被使用,生成.el-alert选择器。紧接着,混入set-component-css-var也被使用,传入了两个参数'alert'$alert

@include set-component-css-var('alert', $alert);

$alertcommon/var.scss文件中被定义:

$alert: () !default;
$alert: map.merge(
  (
    'padding': 8px 16px,
    'border-radius-base': getCssVar('border-radius-base'),
    'title-font-size': 13px,
    'description-font-size': 12px,
    'close-font-size': 12px,
    'close-customed-font-size': 13px,
    'icon-size': 16px,
    'icon-large-size': 28px,
  ),
  $alert
);

!default标志被用于为$alert变量设置默认值。如果外部没有定义该变量,将使用默认值;如果外部定义了,使用外部的值。例如:

// base.scss
$color: red !default;
@mixin main {
  .main {
    color: $color;
  }
}

// first.scss
@use 'main' as * with ($color: #ddd);
@include main;

// second.scss
@use 'main' as *;
@include main;

以上代码将会编译成:

// first.css
.main {
  color: #ddd;
}
// second.css
.main {
  color: red;
}

$alert变量的值类型是map,通过map.merge函数合并两个map值。map类似于JavaScript中的对象,包含键值对,而map.merge则类似于Object.assign

此段代码中使用了自定义函数getCssVar 函数,它的功能是将参数拼接转换为CSS变量。getCssVar函数的定义如下:


// join var name
// joinVarName(('button', 'text-color')) => '--el-button-text-color'
@function joinVarName($list) {
  $name: '--' + config.$namespace;
  @each $item in $list {
    @if $item != '' {
      $name: $name + '-' + $item;
    }
  }
  @return $name;
}

// getCssVar('button', 'text-color') => var(--el-button-text-color)
@function getCssVar($args...) {
  @return var(#{joinVarName($args)});
}

@if类似于javasript中的if,而@return则类似于javasript中的return

set-component-css-var混入的两个参数中,一个是字符串,另一个是map。它的定义如下:

// set all css var for component by map
@mixin set-component-css-var($name, $variables) {
  @each $attribute, $value in $variables {
    @if $attribute == 'default' {
      #{getCssVarName($name)}: #{$value};
    } @else {
      #{getCssVarName($name, $attribute)}: #{$value};
    }
  }
}

这段代码的主要作用是定义一组 CSS 变量,生成的结果如下:

.el-alert {
  --el-alert-padding: 8px 16px;
  --el-alert-border-radius-base: var(--el-border-radius-base);
  --el-alert-title-font-size: 13px;
  --el-alert-description-font-size: 12px;
  --el-alert-close-font-size: 12px;
  --el-alert-close-customed-font-size: 13px;
  --el-alert-icon-size: 16px;
  --el-alert-icon-large-size: 28px;
}

接下来的代码:

@include when(light) {
    .#{$namespace}-alert__close-btn {
      color: getCssVar('text-color', 'placeholder');
    }
  }

  @include when(dark) {
    .#{$namespace}-alert__close-btn {
      color: getCssVar('color', 'white');
    }
    .#{$namespace}-alert__description {
      color: getCssVar('color', 'white');
    }
  }

  @include when(center) {
    justify-content: center;
  }

这三段代码都使用了混入when,查看其定义:

@mixin when($state) {
  @at-root {
    &.#{$state-prefix + $state} {
      @content;
    }
  }
}

这里使用at-root让生成的css脱离.el-alert的限制,直接定义在文档的根级别,否则生成的css都将定义在.el-alert下。

之后,代码遍历了列表(success, info, warning, error)。这部分的代码在之前都有提及,请自行查看。

引入了混入e,查看其定义:

@mixin e($element) {
  $E: $element !global;
  $selector: &;
  $currentSelector: '';
  @each $unit in $element {
    $currentSelector: #{$currentSelector +
      '.' +
      $B +
      $element-separator +
      $unit +
      ','};
  }

  @if hitAllSpecialNestRule($selector) {
    @at-root {
      #{$selector} {
        #{$currentSelector} {
          @content;
        }
      }
    }
  } @else {
    @at-root {
      #{$currentSelector} {
        @content;
      }
    }
  }
}

@if之前的代码和混入m的定义很相似,hitAllSpecialNestRule函数的作用是判断传入的选择器中是否包含修饰符(--)状态前缀(is-)以及伪类(:)

hitAllSpecialNestRule里面用到了sass内置函数inspectstr-slice。inspect可以将任何类型的SassScript值转换为字符串;str-slice用于获取字符串的子串,接收三个参数:需要被切割的字符串、子串的起始位置(从1开始)、字串的结束位置(包含当前位置,最后一位也可以写成-1)。

设置css变量

在上述代码中用到了类似var(--el-color-white)这种css变量,这些变量在哪里定义的呢?除了组件自有的CSS变量在组件内部定义外,其它基础的CSS变量都定义在var.scss中。

编译打包scss

样式库使用gulp编译打包scss文件。命令gulp --require @esbuild-kit/cjs-loader中的--require @esbuild-kit/cjs-loader表示先使用@esbuild-kit/cjs-loader包编译gulpfile.ts文件,把TypeScript编译成JavaScript,把ESModule语法编译成CommonJS语法。

gulpfile的主要任务是编译和构建项目中的SCSS样式文件,并复制必要的文件到指定的目录。

1、导入的模块

  • path 是Node.js的内置模块,用于处理文件和目录的路径。
  • chalk 是一个用于在控制台输出带颜色的文本的库。
  • gulp 的destparallelseriessrc 函数用于创建和管理Gulp任务。
  • gulp-sass 是一个Gulp插件,用于编译Sass文件。
  • sass 是一个纯JavaScript编写的Sass编译器。
  • gulp-autoprefixer 是一个Gulp插件,用于自动添加CSS前缀。
  • gulp-clean-css 是一个Gulp插件,用于压缩CSS。
  • gulp-rename 是一个Gulp插件,用于重命名文件。
  • consola 是一个用于打印各种类型的控制台日志的库。
  • @element-plus/build-utils 是Element Plus的构建工具库。

2、构建任务

  • buildThemeChalk:这个函数首先将指定路径的SCSS文件编译为CSS,然后使用autoprefixer添加前缀,接着使用cleanCSS进行压缩,最后重命名文件并将它们保存到distFolder目录。在重命名时,如果文件名不是'index', 'base', 'display'其中之一,那么在其前面添加'el-'前缀。

  • buildDarkCssVars:这个函数用于构建dark模式的CSS变量。它的工作原理和buildThemeChalk类似,但它只处理'dark/css-vars.scss'文件,并将结果保存到'distFolder/dark'目录。

  • copyThemeChalkBundle:这个函数将distFolder目录下的所有文件复制到distBundle目录下。

  • copyThemeChalkSource:这个函数将'src'目录下的所有文件复制到'distBundle/src'目录下。

3、 导出的任务

build任务是一个并行任务,同时执行copyThemeChalkSource和一个序列任务。这个序列任务首先执行buildThemeChalk,然后执行buildDarkCssVars,最后执行copyThemeChalkBundle

4、 默认导出的任务:这个文件默认导出的是'build'任务,意味着如果你在命令行中直接运行'gulp'命令,那么'build'任务会被执行。

完结撒花。这篇文章简单介绍了element-plus样式库中样式的sass语法以及使用gulp编译构建scss文件步骤,更多详细的内容请查阅element-plus源码。

转载自:https://juejin.cn/post/7239593701805817911
评论
请登录