概述
最近在研究如何使用 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
函数,返回 b
、e
、m
函数可以生成特定块(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 规范代码非常的简洁,生成的类名样式也不需要写很多逻辑判断,方便代码维护和扩展。