Stencil web component 组件库样式主题设计

概述

最近在研究如何使用 Stencil.js 搭建一个不依赖于框架的 web component 组件库 ,在实现样式主题功能,参考Element Plus的样式方案,主要利用Sass变量和CSS变量,并使用BEM(Block Element Modifier)规范,实现了一个可扩展、可维护的组件库样式系统,不仅提升了组件库的开发效率,还能轻松实现 组件库换肤功能

项目结构

组件库的公共样式放在 global 目录下,包含 sass 的样式变量文件和 css 变量 文件

css 复制代码
stencil-component-ui/packages/components
├── src/
│   ├── components/
│   │   └── swc-button/
│   │       ├── swc-button.tsx
│   │       ├── swc-button.scss
│   ├── global/
│   │   ├── base
│   │   ├── common
│   │   ├── mixins
│   │   ├── base.css
│   ├── index.ts
├── stencil.config.ts
├── package.json

配置 Stencil

stencil 不能编译 sass 文件,需要安装插件 @stencil/sass

stencil.config.ts 中配置 Sass 插件:

js 复制代码
import { Config } from '@stencil/core';
import { sass } from '@stencil/sass';

export const config: Config = {
  namespace: 'swc-ui',
  // 公共样式
  globalStyle: 'src/global/base.css',
  // 全景脚本,自动执行
  globalScript: 'src/global.ts',
  // 编译 sass
  plugins: [sass()],
  outputTargets: [
    {
      type: 'dist',
      esmLoaderPath: '../loader',
    },
    {
      type: 'dist-custom-elements',
      generateTypeDeclarations: true,
    },
    {
      type: 'docs-readme',
    },
    {
      type: 'www',
      serviceWorker: null, // disable service workers
    },
  ],
};

为了减少每个组件引入重复的公共样式,优化包体积,需要提取公共样式在全局引 入,配置 globalStyle 全局引入样式 src/global/base.css

存放 css 变量 公共样式文件

css 复制代码
/* base/var.css */
/* 样式变量文件 */
:root {
  --swc-color-white: #ffffff;
  --swc-color-black: #000000;
  --swc-color-primary-rgb: 64, 158, 255;
  --swc-color-success-rgb: 103, 194, 58;
  --swc-color-warning-rgb: 230, 162, 60;
  --swc-color-danger-rgb: 245, 108, 108;
  --swc-color-error-rgb: 245, 108, 108;
  --swc-color-info-rgb: 144, 147, 153;
...
}

BEM 样式规范

BEM 是一种书写 CSS 的规范,是由 Yandex 团队提出的一种前端 CSS 命名方法论。其目的是为了明确 CSS 作用域,确定相关 CSS 优先级,分离状态选择器和结构选择器。

BEM分别是指:

  • B - block:表示一个块元素,比如一个 Modal 弹窗组件、一个 Button 组件,都可以用一个块来表示。
  • E - element:表示一个子元素,存在于块元素之内,例如 弹窗组件的title、footer。
  • M - modifier: 表示修饰符或者状态,例如 Button 组件的选中态、销毁态。

CSS 实现 BEM

全局定义基础变量,$namespace$common-separator$element-separator$modifier-separator,分别用于表示命名空间、块元素分隔符、元素分隔符和修饰符分隔符,后面会用到

scss 复制代码
$namespace: 'swc' !default;
$common-separator: '-' !default;
$element-separator: '__' !default;
$modifier-separator: '--' !default;

1、Block 函数 b($block)

将命名空间和块组合起来,创建一个包含该块名的 CSS 选择器。@content 是一个占位符,用于插入 Mixin 被调用时的内容。

scss 复制代码
@mixin b($block) {
  $B: $namespace + $common-separator + $block !global;

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

使用 block mixin 函数,下面的 e($element)m($modifier) 使用方式一样

scss 复制代码
@include b(button) {
  display: inline-flex;
  justify-content: center;
  align-items: center;
  //...
}

编译生成 css

css 复制代码
.swc-button {
  display: inline-flex;
  justify-content: center;
  align-items: center;
}

2、 Element 函数 e($element)

添加元素的选择器。

  • $E 是元素名。
  • & 是当前选择器的占位符。
  • $currentSelector 用于构建元素选择器。
  • @each 遍历元素名,构建完整的元素选择器。
  • hitAllSpecialNestRule($selector) 是一个假设存在的辅助函数,用于处理某些特殊情况。如果满足条件,使用 @at-root 指令将元素选择器放在根级别,否则直接插入元素选择器。
scss 复制代码
@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;
      }
    }
  }
}

3、Modifier 函数 m($modifier)

  • @each 遍历修饰符名,构建完整的修饰符选择器。
  • @at-root 指令将修饰符选择器放在根级别。
scss 复制代码
@mixin m($modifier) {
  $selector: &;
  $currentSelector: '';
  @each $unit in $modifier {
    $currentSelector: #{$currentSelector + $selector + $modifier-separator + $unit + ','};
  }

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

JS 创建 BEM 类名

组件根据逻辑判断生成 bem 类名,可以封装成一个函数,生成符合 BEM 规范的 CSS 类名

js 复制代码
export const defaultNamespace = 'swc';
const statePrefix = 'is-';

const _bem = (namespace: string, block: string, blockSuffix: string, element: string, modifier: string) => {
  let cls = ${namespace}-${block};
  if (blockSuffix) {
    cls += -${blockSuffix};
  }
  if (element) {
    cls += __${element};
  }
  if (modifier) {
    cls += --${modifier};
  }
  return cls;
};
export const useGetDerivedNamespace = (namespaceOverrides?: string | undefined) => {
  return namespaceOverrides || defaultNamespace;
};

export const useNamespace = (block: string, namespaceOverrides?: string | undefined) => {
  const namespace = useGetDerivedNamespace(namespaceOverrides);
  const b = (blockSuffix = '') => _bem(namespace, block, blockSuffix, '', '');
  const e = (element?: string) => (element ? _bem(namespace, block, '', element, '') : '');
  const m = (modifier?: string) => (modifier ? _bem(namespace, block, '', '', modifier) : '');
 
  return {
    b,
    e,
    m,
  };
};

useNamespace 函数,返回 bem 函数可以生成特定块(block)、元素(element)和修饰符(modifier)的类名,简化样式的编写和管理

button 组件使用 BEM

button 的多种主题和状态是使用 class 样式层叠实现的,充分使用 bem 实现的典型组件

1、button.scss 样式文件

在头部引入 scss 公共样式文件,使用 bem 函数编写样式

scss 复制代码
@use 'sass:map';

@use 'src/global/common/var' as *;
@use 'src/global/mixins/button' as *;
@use 'src/global/mixins/mixins' as *;
@use 'src/global/mixins/utils' as *;
@use 'src/global/mixins/var' as *;


@include b(button) {
  @include set-component-css-var('button', $button);
}

@include b(button) {
  //...
  @include e(text) {
    @include m(expand) {
      letter-spacing: 0.3em;
      margin-right: -0.3em;
    }
  }
  //...
} 

2、引入样式,并创建 bem 类名

tsx 复制代码
import { Component, Host, h, Prop, Element } from '@stencil/core';
import classNamse from 'classnames';
import { useNamespace } from '../../hooks/useNamespace';

const swcNs = useNamespace('button');

@Component({
  tag: 'swc-button',
  styleUrl: 'swc-button.scss',
})
export class SwcButton {
//...
  render() {
    return (
      <Host
        class={classNamse(
          swcNs.b(),
          swcNs.m(this.type),
          swcNs.m(this.size),
          swcNs.is('disabled', this.disabled),
          swcNs.is('loading', this.loading),
          swcNs.is('plain', this.plain),
          swcNs.is('round', this.round),
          swcNs.is('circle', this.circle),
          swcNs.is('link', this.link),
          swcNs.is('text', this.text),
          swcNs.is('has-bg', this.bg),
        )}
      >
        <slot></slot>
      </Host>
    );
  }
}

从上面可以直观的看到,使用 bem 规范代码非常的简洁,生成的类名样式也不需要写很多逻辑判断,方便代码维护和扩展。

相关推荐
Leyla几秒前
【代码重构】好的重构与坏的重构
前端
影子落人间4 分钟前
已解决npm ERR! request to https://registry.npm.taobao.org/@vant%2farea-data failed
前端·npm·node.js
世俗ˊ28 分钟前
CSS入门笔记
前端·css·笔记
子非鱼92128 分钟前
【前端】ES6:Set与Map
前端·javascript·es6
6230_33 分钟前
git使用“保姆级”教程1——简介及配置项设置
前端·git·学习·html·web3·学习方法·改行学it
想退休的搬砖人42 分钟前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
加勒比海涛1 小时前
HTML 揭秘:HTML 编码快速入门
前端·html
啥子花道1 小时前
Vue3.4 中 v-model 双向数据绑定新玩法详解
前端·javascript·vue.js
麒麟而非淇淋1 小时前
AJAX 入门 day3
前端·javascript·ajax
茶茶只知道学习1 小时前
通过鼠标移动来调整两个盒子的宽度(响应式)
前端·javascript·css