Skip to content

构建 Vue3 组件库 - 样式

发布于:

Table of contents

展开目录

目录

源码

Css variables

组件库,最重要的就是颜色,一套成体系的颜色可以让人眼前一亮,

通过 Css variables,可以很方便的实现换肤,也可以使 css 更好的组织化,甚至能让使用者更方便的调整 UI。

对于不懂 UI、不懂设计的开发人员来讲,怎么快速整出一套 Css variables

参考第三方组件库的 调色盘 是最快的方式之一,例如:antd-design colors

或者我参考的 vercel-design colors

Light/Dark mode

在暗黑模式全面支持的今天,Css Variables 也需要提供两套。

实现 Light/Dark mode 非常简单,通常的做法是为 html 元素动态添加 class

theme.ts
const THEMES = ["dark", "light"] as const;
 
const setTheme = (theme: (typeof THEMES)[number]): void => {
  if (typeof document === "undefined") return;
 
  const html = document.querySelector("html");
 
  html?.classList.remove(...THEMES);
  html?.classList.add(theme);
};
 
/** 切换为默认色系 */
export const enableLight = (): void => {
  setTheme("light");
};
 
/** 切换为暗黑色系 */
export const enableDark = (): void => {
  setTheme("dark");
};

:root 下声明 Css variables

themes/default/index.scss
:root,
.light {
  // base color
  --accents-1: #fafafa;
  --accents-2: #eaeaea;
  --accents-3: #999;
  --accents-4: #888;
  --accents-5: #666;
  --accents-6: #444;
  --accents-7: #333;
  --accents-8: #111;
  --geist-foreground: #000;
  --geist-background: #fff;
  --geist-selection: var(--geist-cyan);
 
  // status color
  --geist-success: #0070f3;
  --geist-success-light: #3291ff;
  --geist-success-dark: #0366d6;
 
  --geist-error: #e00;
  --geist-error-light: #ff1a1a;
  --geist-error-dark: #c00;
 
  --geist-warning: #f5a623;
  --geist-warning-light: #f7b955;
  --geist-warning-dark: #f49b0b;
 
  // highlight
  --geist-alert: #ff0080;
  --geist-purple: #f81ce5;
  --geist-cyan: #79ffe1;
  --geist-violet: #7928ca;
 
  // etc...
}
themes/dark/index.scss
.dark {
  // base color
  --accents-1: #111;
  --accents-2: #333;
  --accents-3: #444;
  --accents-4: #666;
  --accents-5: #888;
  --accents-6: #999;
  --accents-7: #eaeaea;
  --accents-8: #fafafa;
  --geist-foreground: #fff;
  --geist-background: #000;
  --geist-selection: var(--geist-purple);
 
  // status color
  --geist-success: #0070f3;
  --geist-success-light: #3291ff;
  --geist-success-dark: #0366d6;
 
  --geist-error: #e00;
  --geist-error-light: #ff1a1a;
  --geist-error-dark: #c00;
 
  --geist-warning: #f5a623;
  --geist-warning-light: #f7b955;
  --geist-warning-dark: #f49b0b;
 
  // highlight
  --geist-alert: #ff0080;
  --geist-purple: #f81ce5;
  --geist-cyan: #79ffe1;
  --geist-violet: #7928ca;
 
  // etc...
}

目录结构

不在 SFC 的 <style> 标签书写样式,而是在单独的目录中编写样式文件,是为了:

_styles
 ┣ foundation
 ┣ themes
 ┣ index.scss
 ┗ _avatar.scss

其他实践

我个人比较习惯直入直出的写法,只定义 Css variables,每个组件使用单独的 .scss 文件来编写样式。

如果你对 sass 有非常深入的了解,或者想更进一步,可以参考 《SCSS 和 CSS变量的架构实践》,里面详细介绍了 Element-plus 构建整套样式 theme-chalk 的思路和方法。

用这种思路编写代码之后,SFC 文件中需要通过 js 来单独组装 className,例如 <el-avatar />

<template>
  <span :class="avatarClass" :style="sizeStyle">
    <img
      v-if="(src || srcSet) && !hasLoadError"
      :src="src"
      :alt="alt"
      :srcset="srcSet"
      :style="fitStyle"
      @error="handleError"
    />
    <el-icon v-else-if="icon">
      <component :is="icon"></component>
    </el-icon>
    <slot v-else />
  </span>
</template>
 
<script lang="ts" setup>
  import { computed, ref, watch } from "vue";
  import { ElIcon } from "@element-plus/components/icon";
  import { useNamespace } from "@element-plus/hooks";
  import { addUnit, isNumber, isString } from "@element-plus/utils";
  import { avatarEmits, avatarProps } from "./avatar";
 
  import type { CSSProperties } from "vue";
 
  defineOptions({
    name: "ElAvatar",
  });
 
  const props = defineProps(avatarProps);
  const emit = defineEmits(avatarEmits);
 
  const ns = useNamespace("avatar");
 
  const hasLoadError = ref(false);
 
  const avatarClass = computed(() => {
    const { size, icon, shape } = props;
    const classList = [ns.b()];
    if (isString(size)) classList.push(ns.m(size));
    if (icon) classList.push(ns.m("icon"));
    if (shape) classList.push(ns.m(shape));
    return classList;
  });
 
  const sizeStyle = computed(() => {
    const { size } = props;
    return isNumber(size)
      ? (ns.cssVarBlock({
          size: addUnit(size) || "",
        }) as CSSProperties)
      : undefined;
  });
 
  const fitStyle = computed<CSSProperties>(() => ({
    objectFit: props.fit,
  }));
 
  // need reset hasLoadError to false if src changed
  watch(
    () => props.src,
    () => (hasLoadError.value = false)
  );
 
  function handleError(e: Event) {
    hasLoadError.value = true;
    emit("error", e);
  }
</script>

Element-plus 通过 useNamespace 来构建 className 的前缀,整个 SFC 文件中看不到具体的类名,如果不熟悉这种风格,很难找到对应的样式。

如果再来一些循环等方法来开发 Col 等组件的样式的话:

@use "sass:math";
 
@use "common/var" as *;
@use "mixins/mixins" as *;
@use "mixins/_col" as *;
 
[class*="#{$namespace}-col-"] {
  box-sizing: border-box;
  @include when(guttered) {
    display: block;
    min-height: 1px;
  }
}
 
.#{$namespace}-col-0 {
  display: none;
  @include when(guttered) {
    display: none;
  }
}
 
@for $i from 0 through 24 {
  .#{$namespace}-col-#{$i} {
    max-width: (math.div(1, 24) * $i * 100) * 1%;
    flex: 0 0 (math.div(1, 24) * $i * 100) * 1%;
  }
 
  .#{$namespace}-col-offset-#{$i} {
    margin-left: (math.div(1, 24) * $i * 100) * 1%;
  }
 
  .#{$namespace}-col-pull-#{$i} {
    position: relative;
    right: (math.div(1, 24) * $i * 100) * 1%;
  }
 
  .#{$namespace}-col-push-#{$i} {
    position: relative;
    left: (math.div(1, 24) * $i * 100) * 1%;
  }
}
 
@include col-size(xs);
 
@include col-size(sm);
 
@include col-size(md);
 
@include col-size(lg);
 
@include col-size(xl);

😟 对于 less/sass 只会一个嵌套操作的,这种模式还比较费时费力,

如果你不想花费大精力深入 sass,可以采用单文件、唯一前缀的方式来快速编写组件样式,

如果你已经深入了解过 sass,也可以用上述模式来进行实践。