使用
我们先从正常的使用过程来了解element的css是如何实现的。如果是完整引入,会在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这个文件夹,那么是怎么生成的呢?
自动化构建
其实element的css文件是自动构建生成的,在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,里面是这样的
\
这里的b、e是什么意思呢?我们找到mixin.scss,可以看到定义了b、e、m的混合
这个b、e、m听起来很熟悉,我们是不是在哪里听过?没错,在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提升为了全局变量,定义了$selector和currentSelector,遍历拼接了当前选择器。有一个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;
}
}
观察一下e和m可以发现,两者有相同的地方,都是拼接字符来生成选择器,不同之处在于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中,元素的结构也遵循这个原则。e和m用不同的分隔符来拼接是因为元素的结构中不一定有B-E-M,可能只有B-M
小结
至此我们已经分析完了element的css是如何设计的,以及el-color-picker的css样式是如何编译的。虽然我们平时都在用scss,但是一些高级用法没接触过,去阅读element的css样式还真是有点吃力。如果有错误的地方,请大家帮我指出哦~