CSS

25 阅读5分钟

盒模型

image.png

image.png

盒模型都是由四个部分组成的,分别是marginborderpaddingcontent

标准盒模型和IE盒模型的区别在于设置widthheight时,对应的范围不同。

  • 标准盒模型widthheight只包含了content
  • IE盒模型的widthheight除了content本身,还包含了borderpadding

通过修改元素的box-sizing属性来改变元素的盒模型

  • box-sizeing: content-box表示标准盒模型(默认值)
  • box-sizeing: border-box表示IE盒模型(IE盒模型)

默认content-box

如果懒得计算宽高,使用border-box

BFC

BFC是块级格式上下文(Block Formatting Context,BFC),是CSS布局的一个概念,在BFC布局里面的元素不受外面元素影响。

创建BFC条件

  • 设置浮动:float有值并不为空
  • 设置绝对定位: position(absolute、fixed)
  • overfilow值为:hiddenautoscroll
  • display值为:inline-blocktable-celltable-captionflex

BFC作用

  • 解决margin重叠问题:由于BFC是一个独立的区域,内部元素和外部元素互不影响,将两个元素变为BFC,就解决了margin重叠问题
  • 创建自适应两栏布局:可以用来创建自适应两栏布局,左边宽高固定,右边宽度自适应。
  • 解决高度塌陷问题:在子元素设置浮动后,父元素会发生高度的塌陷,也就是父元素的高度为0解决这个问题,只需要将父元素变成一个BFC。

CSS选择器和优先级

image.png

优先级

CSS 通过以下四个层级来计算优先级:

  • 行内样式(inline) :1000
  • ID 选择器数量:100 × n
  • 类选择器、属性选择器、伪类选择器数量:10 × n
  • 标签选择器、伪元素选择器数量:1 × n

image.png

BEM 规范及

BEM 基础概念

BEM(Block, Element, Modifier)是一种 CSS 命名方法论。

命名规范

.block {}                 // 块
.block__element {}       // 元素
.block--modifier {}      // 修饰符
.block__element--modifier {} // 元素+修饰符

参考element-plus

核心拼接函数 (_bem)

// 核心的 BEM 类名生成函数
const _bem = (
  namespace: string,
  block: string,
  blockSuffix: string,
  element: string,
  modifier: string
): string => {
  // 1. 构建基础块名:namespace-block
  let cls = `${namespace}-${block}`
  
  // 2. 如果有块后缀,添加:namespace-block-blockSuffix
  if (blockSuffix) {
    cls += `-${blockSuffix}`
  }
  
  // 3. 如果有元素,添加:__element
  if (element) {
    cls += `__${element}`
  }
  
  // 4. 如果有修饰符,添加:--modifier
  if (modifier) {
    cls += `--${modifier}`
  }
  
  return cls
}

例如,_bem('el', 'button', '', 'icon', 'primary') 

会生成 el-button__icon--primary

基于 _bemuseNamespace 提供了 b()e()m() 等一系列更易用的方法

import { computed } from 'vue'
import { useGlobalConfig } from '../use-global-config'

// 核心的 BEM 类名生成函数
const _bem = (
  namespace: string,
  block: string,
  blockSuffix: string,
  element: string,
  modifier: string
): string => {
  // 1. 构建基础块名:namespace-block
  let cls = `${namespace}-${block}`
  
  // 2. 如果有块后缀,添加:namespace-block-blockSuffix
  if (blockSuffix) {
    cls += `-${blockSuffix}`
  }
  
  // 3. 如果有元素,添加:__element
  if (element) {
    cls += `__${element}`
  }
  
  // 4. 如果有修饰符,添加:--modifier
  if (modifier) {
    cls += `--${modifier}`
  }
  
  return cls
}

// 状态类名前缀(如 is-disabled、is-loading)
const statePrefix = 'is-'

/**
 * 创建 BEM 命名空间的完整实现
 * @param block - 块名(如 'button'、'input')
 * @returns BEM 工具对象
 */
export const useNamespace = (block: string) => {
  // 从全局配置获取命名空间(默认 'el')
  const globalConfig = useGlobalConfig('namespace')
  const namespace = computed(() => {
    return globalConfig.value || 'el'
  })
  
  // 1. 基本块:el-button
  const b = (blockSuffix: string = '') => {
    return _bem(namespace.value, block, blockSuffix, '', '')
  }
  
  // 2. 元素:el-button__icon
  const e = (element?: string) => {
    return element ? _bem(namespace.value, block, '', element, '') : ''
  }
  
  // 3. 修饰符:el-button--primary
  const m = (modifier?: string) => {
    return modifier ? _bem(namespace.value, block, '', '', modifier) : ''
  }
  
  // 4. 块 + 元素:el-button-group__content
  // blockSuffix: 'group', element: 'content' => el-button-group__content
  const be = (blockSuffix?: string, element?: string) => {
    return blockSuffix && element 
      ? _bem(namespace.value, block, blockSuffix, element, '')
      : ''
  }
  
  // 5. 元素 + 修饰符:el-button__icon--primary
  // element: 'icon', modifier: 'primary' => el-button__icon--primary
  const em = (element?: string, modifier?: string) => {
    return element && modifier 
      ? _bem(namespace.value, block, '', element, modifier)
      : ''
  }
  
  // 6. 块 + 修饰符:el-button--large
  const bm = (blockSuffix?: string, modifier?: string) => {
    return blockSuffix && modifier 
      ? _bem(namespace.value, block, blockSuffix, '', modifier)
      : ''
  }
  
  // 7. 完整的 BEM:el-button-group__icon--primary
  // blockSuffix: 'group', element: 'icon', modifier: 'primary'
  const bem = (blockSuffix?: string, element?: string, modifier?: string) => {
    if (blockSuffix && element && modifier) {
      return _bem(namespace.value, block, blockSuffix, element, modifier)
    }
    if (blockSuffix && element) {
      return _bem(namespace.value, block, blockSuffix, element, '')
    }
    if (blockSuffix && modifier) {
      return _bem(namespace.value, block, blockSuffix, '', modifier)
    }
    if (element && modifier) {
      return _bem(namespace.value, block, '', element, modifier)
    }
    if (blockSuffix) {
      return _bem(namespace.value, block, blockSuffix, '', '')
    }
    if (element) {
      return _bem(namespace.value, block, '', element, '')
    }
    if (modifier) {
      return _bem(namespace.value, block, '', '', modifier)
    }
    return _bem(namespace.value, block, '', '', '')
  }
  
  // 8. 状态类:is-disabled, is-loading
  // 用于表示元素状态,不与 BEM 主类名直接连接
  const is = {
    // 添加状态类:condition 为 true 时返回类名
    add: (name: string, condition: boolean | undefined) => {
      return condition ? `${statePrefix}${name}` : ''
    },
    // 直接返回状态类名
    name: (name: string) => {
      return `${statePrefix}${name}`
    }
  }
  
  // 9. 生成 CSS 变量名(用于主题定制)
  // 如:--el-button-text-color
  const cssVar = {
    name: (name: string) => {
      return `--${namespace.value}-${block}-${name}`
    },
    // 带块后缀的 CSS 变量
    withBlock: (blockSuffix: string, name: string) => {
      return `--${namespace.value}-${block}-${blockSuffix}-${name}`
    }
  }
  
  // 10. 生成 CSS 变量对象(便于在 style 属性中使用)
  // 如:{ '--el-button-text-color': '#fff' }
  const cssVarBlock = (variables: Record<string, string>) => {
    const styles: Record<string, string> = {}
    Object.keys(variables).forEach(key => {
      styles[`--${namespace.value}-${block}-${key}`] = variables[key]
    })
    return styles
  }
  
  return {
    namespace,
    b,
    e,
    m,
    be,
    em,
    bm,
    bem,
    is,
    cssVar,
    cssVarBlock
  }
}

// 类型定义
export type UseNamespaceReturn = ReturnType<typeof useNamespace>

useNamespace 内部通过 useGlobalConfig 读取,实现所有组件类名前缀(如 el- 改为 ep-)的全局切换。这非常利于微前端等场景的样式隔离。

Sass 核心:mixins 混入

在编写组件样式时,Element Plus 使用对应的 Sass 混入来生成 CSS,确保与 JS 生成的类名结构一致

// 定义 BEM 命名规范变量
$namespace: 'el' !default;           // 命名空间前缀,默认 'el'
$element-separator: '__' !default;   // 元素分隔符
$modifier-separator: '--' !default;  // 修饰符分隔符
$state-prefix: 'is-' !default;       // 状态前缀

// 全局变量,存储当前块的名称
$common-separator: '-' !default;

// 1. 块(Block)混入 - 定义组件块
@mixin b($block) {
  $B: $namespace + '-' + $block !global;  // 设置全局变量 $B
  
  .#{$B} {  // 生成如 .el-button 的选择器
    @content;
  }
}

// 2. 元素(Element)混入 - 定义块内的元素
@mixin e($element) {
  $E: $element !global;  // 设置全局变量 $E
  $selector: &;          // 获取父选择器
  
  @if hitAllSpecialNestRule($selector) {
    // 特殊嵌套规则处理
    @at-root {
      #{$selector} {
        #{currentSelector()} {
          @content;
        }
      }
    }
  } @else {
    // 普通情况:直接拼接元素
    @at-root {
      #{$selector + $element-separator + $element} {
        @content;
      }
    }
  }
}

// 3. 修饰符(Modifier)混入 - 定义块或元素的变体
@mixin m($modifier) {
  $selector: &;
  
  @at-root {
    #{$selector + $modifier-separator + $modifier} {
      @content;
    }
  }
}

// 4. 当(When)混入 - 条件状态(类似于 is- 前缀)
@mixin when($state) {
  $selector: &;
  
  @at-root {
    &#{'.' + $state-prefix + $state} {
      @content;
    }
  }
}

// 5. 特殊选择器混入 - 处理 & 选择器
@mixin spec($selector) {
  @at-root {
    #{&}#{$selector} {
      @content;
    }
  }
}

// 6. 伪类混入 - 处理伪类选择器
@mixin pseudo($pseudo) {
  $selector: &;
  
  @at-root {
    #{&}#{':#{$pseudo}'} {
      @content;
    }
  }
}

// 7. 创建 BEM 选择器的快捷方式
@mixin button-size-mixin($padding-vertical, $padding-horizontal) {
  padding: $padding-vertical $padding-horizontal;
}

// 辅助函数:获取当前选择器
@function currentSelector() {
  $currentSelector: '';
  
  @each $selector in & {
    $currentSelector: #{$currentSelector + $selector + $element-separator + $E + ','};
  }
  
  @return $currentSelector;
}

// 辅助函数:检查是否命中特殊嵌套规则
@function hitAllSpecialNestRule($selector) {
  @return containsModifier($selector) or containWhenFlag($selector) or containPseudoClass($selector);
}

// 辅助函数:检查选择器是否包含修饰符
@function containsModifier($selector) {
  $items: ();
  
  @each $item in $selector {
    @if str-index($item, $modifier-separator) {
      @return true;
    }
  }
  
  @return false;
}

// 辅助函数:检查选择器是否包含状态标志
@function containWhenFlag($selector) {
  @return str-index(#{$selector}, '.' + $state-prefix);
}

// 辅助函数:检查选择器是否包含伪类
@function containPseudoClass($selector) {
  @return str-index(#{$selector}, ':');
}

// 8. CSS 变量相关混入
@mixin set-css-var-type($name, $type, $variables) {
  #{getCssVarName($name, $type)}: $variables;
}

@mixin set-css-var-value($name, $value) {
  #{'--' + $namespace + '-' + $name}: $value;
}

@function getCssVar($args...) {
  @return var(#{getCssVarName($args...)});
}

@function getCssVarName($args...) {
  $name: '--' + $namespace;
  
  @each $arg in $args {
    @if $arg != '' {
      $name: $name + '-' + $arg;
    }
  }
  
  @return $name;
}

实际组件应用示例

<script setup lang="ts">
import { useNamespace } from './use-namespace'

const ns = useNamespace('button')

// 示例类名生成
const classes = {
  basic: ns.b(),                       // 'el-button'
  withModifier: ns.m('primary'),       // 'el-button--primary'
  element: ns.e('icon'),               // 'el-button__icon'
  blockElement: ns.be('group', 'item'), // 'el-button-group__item'
  elementModifier: ns.em('icon', 'large'), // 'el-button__icon--large'
  blockModifier: ns.bm('group', 'round'), // 'el-button-group--round'
  fullBem: ns.bem('group', 'icon', 'primary'), // 'el-button-group__icon--primary'
  state: ns.is.add('disabled', true),  // 'is-disabled'
  cssVarName: ns.cssVar.name('color')  // '--el-button-color'
}

console.log(classes)
</script>

<template>
  <!-- Button 组件示例 -->
  <button
    :class="[
      ns.b(),
      ns.m(type),
      ns.is.add('disabled', disabled),
      ns.is.add('loading', loading),
    ]"
    :style="ns.cssVarBlock({
      'bg-color': bgColor,
      'text-color': textColor
    })"
  >
    <!-- Button 图标 -->
    <span :class="ns.e('icon')">
      <slot name="icon" />
    </span>
    
    <!-- Button 内容 -->
    <span :class="ns.e('content')">
      <slot />
    </span>
  </button>
</template>
@use 'mixins/mixins' as *;
@use 'common/var' as *;

@include b(button) {
  // 基础按钮样式 .el-button
  display: inline-flex;
  justify-content: center;
  align-items: center;
  line-height: 1;
  height: map-get($input-height, 'default');
  white-space: nowrap;
  cursor: pointer;
  color: getCssVar('button', 'text-color');
  text-align: center;
  box-sizing: border-box;
  outline: none;
  transition: .1s;
  font-weight: getCssVar('font-weight', 'primary');
  
  // 禁用状态 .el-button.is-disabled
  @include when(disabled) {
    &,
    &:hover,
    &:focus {
      cursor: not-allowed;
      background-image: none;
    }
  }
  
  // 加载状态 .el-button.is-loading
  @include when(loading) {
    position: relative;
    pointer-events: none;
    
    &::before {
      content: '';
      position: absolute;
      left: -1px;
      top: -1px;
      right: -1px;
      bottom: -1px;
      border-radius: inherit;
      background-color: getCssVar('mask-color', 'extra-light');
    }
  }
  
  // 按钮大小修饰符
  // .el-button--large
  @include m(large) {
    @include button-size-mixin(
      map-get($button-padding-vertical, 'large'),
      map-get($button-padding-horizontal, 'large')
    );
    height: map-get($input-height, 'large');
    font-size: map-get($font-size, 'large');
  }
  
  // .el-button--small
  @include m(small) {
    @include button-size-mixin(
      map-get($button-padding-vertical, 'small'),
      map-get($button-padding-horizontal, 'small')
    );
    height: map-get($input-height, 'small');
    font-size: map-get($font-size, 'small');
  }
  
  // 按钮类型修饰符
  // .el-button--primary
  @include m(primary) {
    @include css-var-from-global(('button', 'bg-color'), ('color', 'primary'));
    
    &:hover {
      background: getCssVar('button', 'hover-bg-color');
    }
    
    &:active {
      background: getCssVar('button', 'active-bg-color');
    }
  }
  
  // .el-button__icon
  @include e(icon) {
    display: flex;
    justify-content: center;
    align-items: center;
  }
  
  // .el-button__text
  @include e(text) {
    display: inline-block;
  }
  
  // 图标在文本右侧 .el-button__icon--right
  @include em(icon, right) {
    order: 1;
    margin-left: 5px;
  }
  
  // 圆形按钮 .el-button--circle
  @include m(circle) {
    border-radius: 50%;
    padding: map-get($button-padding-vertical, 'default');
  }
  
  // 圆角按钮 .el-button--round
  @include m(round) {
    border-radius: getCssVar('border-radius', 'round');
  }
}

// 按钮组样式 .el-button-group
@include b(button-group) {
  display: inline-flex;
  vertical-align: middle;
  
  // 按钮组中的按钮 .el-button-group > .el-button
  & > .el-button {
    position: relative;
    
    // 相邻按钮合并边框
    &:not(:first-child) {
      margin-left: -1px;
    }
    
    // 圆角处理
    &:first-child {
      border-top-right-radius: 0;
      border-bottom-right-radius: 0;
    }
    
    &:last-child {
      border-top-left-radius: 0;
      border-bottom-left-radius: 0;
    }
    
    &:not(:first-child):not(:last-child) {
      border-radius: 0;
    }
    
    // 悬停和激活状态
    &:hover,
    &:focus {
      z-index: 1;
    }
    
    @include when(active) {
      z-index: 2;
    }
  }
}