element的css样式解析

528 阅读5分钟

使用

我们先从正常的使用过程来了解elementcss是如何实现的。如果是完整引入,会在main.js中写一行代码

import 'element-ui/lib/theme-chalk/index.css';

如果是部分引入,则会在使用到的地方引入对应的element组件的css样式,比如我需要部分引入一个el-color-picker组件

import 'element-ui/lib/theme-chalk/color-picker.css';

我们可以在element的源码中看到并没有lib这个文件夹,那么是怎么生成的呢? image.png

自动化构建

其实elementcss文件是自动构建生成的,在package.json中有一条构建样式的指令:

{
    "script": {
        "build:theme": "node build/bin/gen-cssfile && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib lib/theme-chalk"
    }
}

上面的指令会按顺序执行,bash命令&&表示左侧执行完再执行右侧;而&表示左侧放入后台执行,右侧整体命令放入"前台"执行,从而实现并行效果。所以上面的执行顺序为

1.node build/bin/gen-cssfile

2.gulp build --gulpfile packages/theme-chalk/gulpfile.js

3.cp-cli packages/theme-chalk/lib lib/theme-chalk

我们按照第一步打开所在文件,里面的内容如下

var fs = require('fs');
var path = require('path');
var Components = require('../../components.json');
var themes = [
  'theme-chalk'
];
Components = Object.keys(Components);
// Conponents: ['pagination', 'dialog', ...]
var basepath = path.resolve(__dirname, '../../packages/');

function fileExists(filePath) {
  try {
    return fs.statSync(filePath).isFile();
  } catch (err) {
    return false;
  }
}

themes.forEach((theme) => {
  var isSCSS = theme !== 'theme-default';
  var indexContent = isSCSS ? '@import "./base.scss";\n' : '@import "./base.css";\n';
  // indexContent: @import './base.css'\n;
  Components.forEach(function(key) {
    if (['icon', 'option', 'option-group'].indexOf(key) > -1) return;
    var fileName = key + (isSCSS ? '.scss' : '.css');
    // fileNmae: pagination.scss
    indexContent += '@import "./' + fileName + '";\n';
    // indexContent: @import './base.css'\n @import './pagination.scss'\n;
    var filePath = path.resolve(basepath, theme, 'src', fileName);
    if (!fileExists(filePath)) {
      fs.writeFileSync(filePath, '', 'utf8');
      console.log(theme, ' 创建遗漏的 ', fileName, ' 文件');
    }
  });
  fs.writeFileSync(path.resolve(basepath, theme, 'src', isSCSS ? 'index.scss' : 'index.css'), indexContent);
});

遍历components.json中的key,如果是默认的theme,且存在独立的样式文件,则逐一将对应的样式文件中的内容写入到../../packages/theme-chalk/src下的index.scss或者index.css,如果不存在独立的样式文件,则自动补一个对应的空文件。

再看下第二步的gulpfile.js。这一步给样式添加了浏览器前缀,并对样式和图标都做了压缩。

'use strict';

const { series, src, dest } = require('gulp');
const sass = require('gulp-sass');
const autoprefixer = require('gulp-autoprefixer');
const cssmin = require('gulp-cssmin');

function compile() {
  return src('./src/*.scss')
    .pipe(sass.sync())
    .pipe(autoprefixer({ // 添加前缀
      browsers: ['ie > 9', 'last 2 versions'],
      cascade: false
    }))
    .pipe(cssmin()) // 压缩
    .pipe(dest('./lib'));
}
// 压缩资源
function copyfont() {
  return src('./src/fonts/**')
    .pipe(cssmin())
    .pipe(dest('./lib/fonts'));
}

exports.build = series(compile, copyfont);

第三步就是将生成好的css文件复制到lib文件夹下。前面有提到在element的源码中并没看到lib文件夹,这一步就是生成并复制内容lib文件夹的,所以我们可以像上面那样使用。

el-color-picker的样式

找到packages/theme-chalk/src/color-picker.scss,里面是这样的
image.png\

这里的be是什么意思呢?我们找到mixin.scss,可以看到定义了b、e、m的混合 image.png
这个bem听起来很熟悉,我们是不是在哪里听过?没错,在CSS命名中,有一种规范叫bem规范
bem规范由块Block、元素Element、修饰符Modifier组成,通常的命名连接方式如下:

- 中划线:仅作为连字符使用,表示某个块或某个子元素的多单词之间的连接记号
__ 双下划线:双下划线用来连接块和块的子元素
_ 单下划线:单下划线用来描述一个块或者块的子元素的一种状态

例如.el-color-picker.el-color-picker--small.el-color-predefine.el-color-predefine__colors,在代码里是这样的:

@include b(color-picker) {
    ...
    @include m(small) {
        ...
    }
}

@include b(color-predefine) {
    ...
    @include e(colors) {
        ...
    }
}

mixin-b

我们先来看在config.scss中定义的变量

$namespace: 'el';
$element-separator: '__';
$modifier-separator: '--';
$state-prefix: 'is-';

再来看b$namespace是上面定义的el$block是传进b的参数$block,用了一个 !global$B提升为全局变量,这样$B就可以在别的地方使用了。使用#{}插值可以在选择器和属性名中使用scss变量。 @content可以占位替换include传递进来的内容。

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

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

比如el-color-predefine的这段样式,使用了b混合后,会被编译成下面一段代码

@include b(color-predefine) {
  display: flex;
  font-size: 12px;
  margin-top: 8px;
  width: 280px;
}

// 编译后
@mixin b(color-predefine) {
  $B: el+'-'+color-predefine;
  
  .el-color-predefine {
    display: flex;
    font-size: 12px;
    margin-top: 8px;
    width: 280px;
  }
}

mixin-e

再来看下e混合,传入了一个$element参数,并用!global提升为了全局变量,定义了$selectorcurrentSelector,遍历拼接了当前选择器。有一个hitAllSpecialNestRule方法,下文简称hit,如果符合hit的规则,则嵌套一层父元素,没有则不嵌套。这里还使用了一个@at-root来跳出嵌套。

@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;
      }
    }
  }
}

打开function.scss,可以看到hitAllSpecialNestRule,我们来解析一下。

/* BEM support Func
 -------------------------- */
 // 将选择器返回为字符形式
@function selectorToString($selector) {
  // inspect($value)表示返回\$value的字符串表示
  $selector: inspect($selector);
  // str-slice(beginIndex, endIndex) 截取字符串
  $selector: str-slice($selector, 2, -2);
  @return $selector;
}
// 判断是否包含 --
@function containsModifier($selector) {
  $selector: selectorToString($selector);
  // str-index(str, value) 返回value在str字符串中的第一个索引位置,没有则返回null
  // $modifier-separator在config.scss中定义了,为--
  @if str-index($selector, $modifier-separator) {
    @return true;
  } @else {
    @return false;
  }
}
// 判断是否包含 is-
@function containWhenFlag($selector) {
  $selector: selectorToString($selector);
  // $state-prefix在config.scss中定义了,为'is-'
  @if str-index($selector, '.' + $state-prefix) {
    @return true
  } @else {
    @return false
  }
}
// 判断是否包含伪元素 :
@function containPseudoClass($selector) {
  $selector: selectorToString($selector);

  @if str-index($selector, ':') {
    @return true
  } @else {
    @return false
  }
}
// 返回上述规则
@function hitAllSpecialNestRule($selector) {
  @return containsModifier($selector) or containWhenFlag($selector) or containPseudoClass($selector);
}

比如

@include b(color-predefine) {
  display: flex;
  font-size: 12px;
  margin-top: 8px;
  width: 280px;

  @include e(colors) {
    display: flex;
    flex: 1;
    flex-wrap: wrap;
  }
}

// 解释
@mixin e($element) {
  // $element: colors
  $E: $element !global;
  $selector: &;
  $currentSelector: "";
  @each $unit in $element {
    // $currentSelector: .el-color-predefine__colors,
    $currentSelector: #{$currentSelector + "." + $B + $element-separator + $unit + ","};
  }
  // $selector: &,表示引用当前父级选择器,当前父级元素为`b`中的选择器,则为.el-color-predefine
  @if hitAllSpecialNestRule($selector) {
    @at-root {
      // 如果符合`hit`的规则:选择器中带--、is-、:的,则需要嵌套一下当前的父选择器
      #{$selector} {
        #{$currentSelector} {
          @content;
        }
      }
    }
  } @else {
    // .el-color-predefine不符合`hit`的规则,走else分支
    // 用@at-root跳出嵌套
    @at-root {
      #{$currentSelector} {
        @content;
      }
    }
  }
}

// 最后则编译为
.el-color-predefine {
    display: flex;
    flex: 1;
    flex-wrap: wrap;
}
.el-color-predefine__colors {
    display: flex;
    flex: 1;
    flex-wrap: wrap;
}

mixin-m

el-color-picker的样式中还有个m,我们来看一下

@include b(color-picker) {
  display: inline-block;
  position: relative;
  line-height: normal;
  height: 40px;
  @include m(small) {
    height: 32px;
    .el-color-picker__trigger {
      height: 32px;
      width: 32px;
    }
    .el-color-picker__mask {
      height: 30px;
      width: 30px;
    }
  }
}

// 解释
@mixin m($modifier) {
  // $modifier: small
  $selector: &; // 父级选择器 el-color-picker
  $currentSelector: "";
  @each $unit in $modifier {
    // $currentSelector: .el-color-picker--small,
    $currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","};
  }
  // 同`e`
  @at-root {
    #{$currentSelector} {
      @content;
    }
  }
}

// 编译后
.el-color-predefine {
    display: flex;
    flex: 1;
    flex-wrap: wrap;
}
.el-color-picker--small {
    height: 32px;
    .el-color-picker__trigger {
      height: 32px;
      width: 32px;
    }
    .el-color-picker__mask {
      height: 30px;
      width: 30px;
    }
}

观察一下em可以发现,两者有相同的地方,都是拼接字符来生成选择器,不同之处在于e拼接了全局变量$B__分隔符,而m只拼接了--分隔符。

// e
@each $unit in $element {
    // $currentSelector: .el-color-predefine__colors,
    $currentSelector: #{$currentSelector + "." + $B + $element-separator + $unit + ","};
}

// m
@each $unit in $modifier {
    // $currentSelector: .el-color-picker--small,
    $currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","};
}

在前面我们提到__是用来连接块和块的子元素的,-表示一种状态。在element中,元素的结构也遵循这个原则。em用不同的分隔符来拼接是因为元素的结构中不一定有B-E-M,可能只有B-M
image.png image.png

小结

至此我们已经分析完了elementcss是如何设计的,以及el-color-pickercss样式是如何编译的。虽然我们平时都在用scss,但是一些高级用法没接触过,去阅读elementcss样式还真是有点吃力。如果有错误的地方,请大家帮我指出哦~