通过Element Plus的样式库探索Sass
前言
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);
$alert
在common/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内置函数inspect
和str-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
的dest
,parallel
,series
,src
函数用于创建和管理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