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

相关推荐
彭世瑜12 分钟前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund40413 分钟前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish14 分钟前
Token刷新机制
前端·javascript·vue.js·typescript·vue
小五Five15 分钟前
TypeScript项目中Axios的封装
开发语言·前端·javascript
小曲程序15 分钟前
vue3 封装request请求
java·前端·typescript·vue
临枫54116 分钟前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript
前端每日三省17 分钟前
面试题-TS(八):什么是装饰器(decorators)?如何在 TypeScript 中使用它们?
开发语言·前端·javascript
小刺猬_98517 分钟前
(超详细)数组方法 ——— splice( )
前端·javascript·typescript
渊兮兮19 分钟前
Vue3 + TypeScript +动画,实现动态登陆页面
前端·javascript·css·typescript·动画
鑫宝Code19 分钟前
【TS】TypeScript中的接口(Interface):对象类型的强大工具
前端·javascript·typescript