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 规范代码非常的简洁,生成的类名样式也不需要写很多逻辑判断,方便代码维护和扩展。

相关推荐
庸俗今天不摸鱼25 分钟前
【万字总结】前端全方位性能优化指南(十)——自适应优化系统、遗传算法调参、Service Worker智能降级方案
前端·性能优化·webassembly
黄毛火烧雪下32 分钟前
React Context API 用于在组件树中共享全局状态
前端·javascript·react.js
Apifox42 分钟前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员
一张假钞1 小时前
Firefox默认在新标签页打开收藏栏链接
前端·firefox
高达可以过山车不行1 小时前
Firefox账号同步书签不一致(火狐浏览器书签同步不一致)
前端·firefox
m0_593758101 小时前
firefox 136.0.4版本离线安装MarkDown插件
前端·firefox
掘金一周1 小时前
金石焕新程 >> 瓜分万元现金大奖征文活动即将回归 | 掘金一周 4.3
前端·人工智能·后端
三翼鸟数字化技术团队1 小时前
Vue自定义指令最佳实践教程
前端·vue.js
Jasmin Tin Wei2 小时前
蓝桥杯 web 学海无涯(axios、ecahrts)版本二
前端·蓝桥杯
圈圈编码2 小时前
Spring Task 定时任务
java·前端·spring