Skip to content

样式布局

样式与组件分离

Teek 并没有和普通的 Vue 项目一样,使用如下模板进行编写组件:

vue
<script setup lang="ts">
// 组件逻辑
</script>

<template>
  <!-- 组件模板 -->
</template>

<style lang="scss" scoped>
/* 组件样式 */
</style>

而是使用:

vue
<script setup lang="ts">
// 组件逻辑
</script>

<template>
  <!-- 组件模板 -->
</template>

那么组件样式去哪里了呢?

Teek 专门创建 styles 目录用于存放组件样式,然后将所有的组件样式汇总到一个 index.scss(入口样式文件),最后在 index.ts(入口运行文件)分别引入 styles/index.scss(入口样式文件)和 layout/index.vue(入口组件)。

*.vue ——> layout/index.vue ——> index.ts <—— style/index.scss <—— *.scss
多个功能组件 ——> 入口组件 ——> 入口运行文件 <—— 入口样式文件 <—— 多个功能组件样式

提示

这也就是为什么在 .vitepress/theme/index.ts 里单独引入 Teek 样式的原因。

样式结构设计

Teek 的样式结构设计通用遵循单一原则:每个样式文件只负责渲染单独的模块(组件),如:

  • nav-var.scss 只提供导航栏的 css var 变量
  • nav-search-button.scss 只渲染导航栏的搜索按钮,内部引用了 nav-var.scss
  • nav-switch-button.scss 只渲染导航栏的深色、浅色切换按钮,内部引用了 nav-var.scss
  • nav-translation.scss 只渲染导航栏的国际化下拉框,内部引用了 nav-var.scss

这样的好处是,不同的样式之间是独立的,需要根据自己的需求按需引入对应的样式文件,不会存在引入一个内容超大的样式文件,导致不想要的元素也发生了样式改变。

当然,对于不喜欢折腾、研究的小伙伴来说,Teek 也提供了 nav.scss 样式文件,该文件内部没有编写任何样式代码,而是引入了 nav-xxx.scss 等样式文件,用于快速引入 nav 的所有样式文件。

目录结构

下面给出 Teek 的样式目录结构:

sh
styles
├─ base.scss      # 基础样式文件
├─ index.scss     # 入口样式文件

├─ common         # 通用样式目录
├─ components     # 组件样式目录,对应每个 Vue 组件
├─ md-plugin      # Markdown 插件样式目录
├─ mixins         # 样式混入目录
├─ module         # 模块样式目录
├─ var            # 样式变量目录
└─ vp-plus        # VitePress 样式加强目录

components 目录下,Teek 会给每个组件生成对应的样式文件,文件名与组件名相同(首字母小写)。

index.scss 文件是入口样式文件,它将导入 base.scssvar 目录下的样式文件,以及 components 目录下所有组件的样式文件。

如果需要按需加载组件,您必须要引入 base.scss 文件,这是 Teek 的核心主题样式文件,然后按需引入 Vue 组件和 components 目录下的对应组件的样式文件。

命名空间

Teek 并没有简单的直接给一个 div 元素添加 class="button",然后在 CSS 里 .button {} 定义样式,Teek 使用 命名空间 的设计思想,给每个组件添加唯一标识,确保不同组件的相同 class 发生样式冲突。

提示

命名空间等价于 Vue 组件 stylescoped 属性。

SCSS 定义命名空间

命名空间其实是一个唯一的前缀,如 Element Plus 的命名空间为 el,在某个 class 中添加 el- 前缀,如 <div class="el-button"></div>

命名空间应该是一个变量,这样只需要修改该变量的值,那么所有的 class 以及样式都不会失效,如在 Element Plus 的命名空间文件里修改 eltk,那么所有的 class 都变为 tk 开头,且样式不会失效。

提示

Teek 的命名空间为 tk

首先 Teek 在 styles/mixins/config.scss 文件中定义了命名空间变量 $namespace

scss
$namespace: "tk" !default;

此时其他的 SCSS 文件都需要引入该文件,然后使用 $namespace 变量:

scss
@use "../mixins/config";

.#{$namespace}-button {
}

当然这只是简单的 Demo,实际的使用请看 样式文件使用 BEM

JS/TS 使用命名空间

定义命名空间 中通过 $namespace 定义了命名空间,那么 Vue 组件里的 template 元素如何使用呢?总不能直接写 <div class="tk-button"></div>,一旦这样,修改 $namespace 的值,那么所有的 class 都会失效,因此需要想办法直接在 template 使用 $namespace 变量。

通过 SCSS Module API 可以将 $namespace 变量导出到 jsts 文件里,在 styles/module/namespace.module.scss 文件导出 $namespace

scss
/* styles/module/namespace.module.scss */
@use "../mixins/config" as *;

:export {
  namespace: #{$namespace};
}

信息

SCSS Module API 只对 .module.scss 结尾的文件提供变量暴露功能。

如果是 Typescript 环境使用,则还需要在同级目录下定义一个 namespace.module.scss.d.ts 文件:

ts
// styles/module/namespace.module.scss.d.ts
export interface ScssVariables {
  [x: string]: unknown;
  namespace: string;
}

export let variables: ScssVariables;

export default variables;

最后在 Vue 组件引入 namespace.module.scss

vue
<script setup lang="ts">
import namespaceModule from "../styles/module/namespace.module.scss";
</script>

<template>
  <div :class="${namespaceModule}-button"></div>
</template>

当然这只是简单的 Demo,实际的使用请看 组件元素使用 BEM

提示

$namespace: tk 改为 $namespace: xx(xx 为你的项目名/框架名),那么没人知道它是 teek,它已经完全属于你。

什么是 BEM

Teek 使用 BEM 规范进行样式编写,并使用 SCSS 进行样式编写。

BEM 是一种前端开发方法论,全称是 Block Element Modifier(块、元素、修饰符)。它提供了一种命名约定,用于组织和管理 CSS 类名,从而提高代码的可维护性、可扩展性和复用性。

  • Block(块)

    • 独立的功能模块,可以独立存在
    • 示例:buttonmenuinput
  • Element(元素)

    • 属于某个 Block 的一部分,不能单独存在
    • 使用双下划线 __ 连接 Block 和 Element
    • 示例:menu__itembutton__text
  • Modifier(修饰符)

    • 用于改变 Block 或 Element 的外观或行为
    • 使用双横线 -- 表示
    • 示例:button--largemenu__item--active

BEM 方法的引入主要是为了解决传统 CSS 开发中常见的问题,尤其是在大型项目或团队协作中,这些问题会变得更加突出。以下是使用 BEM 的主要原因以及它解决的痛点:

  • 样式冲突:在传统的 CSS 开发中,类名可能会重复或不够具体,导致样式冲突。例如,多个开发者可能都定义了一个名为 button 的样式,但它们的行为和外观完全不同
  • 可维护性差:随着项目的增长,CSS 文件变得越来越复杂,难以找到特定样式的定义位置,或者修改一个样式时意外影响到其他部分
  • 样式复用困难:在没有明确规范的情况下,开发者可能需要重复编写类似的样式代码,增加了冗余
  • 团队协作困难:在多人协作的项目中,不同开发者可能采用不同的命名习惯,导致代码风格不一致,难以统一管理
  • 样式与结构分离不清晰:在某些情况下,开发者可能直接通过 HTML 结构(如标签选择器、后代选择器)来定义样式,这会导致样式与结构紧密耦合,难以迁移或重构
  • 缺乏扩展性:当需要对现有样式进行扩展或修改时,可能会因为复杂的嵌套关系或不清晰的命名规则而感到困难

BEM 命名规则

  • Block: blockName
  • Block + Element: blockName__elementName
  • Block + Modifier: blockName--modifierName
  • Element + Modifier: blockName__elementName--modifierName
html
<div class="button">
  <span class="button__text">文字按钮</span>
  <button class="button--large">large 按钮</button>
  <span class="button__text--bold">文字加粗按钮</span>
</div>
css
/* Block */
.button {
  /* 样式 */
}

/* Element */
.button__text {
  /* 样式 */
}

/* Modifier */
.button--large {
  /* 样式 */
}

/* Element + Modifier */
.button__text--bold {
  /* 样式 */
}

组件元素使用 BEM

定义一个 Hooks 文件 useNamespace.ts 来封装命名空间和 BEM 规范,命名空间BEM 规范 在上文已经介绍过了。

在组件中引入 useNamespace.ts 文件,使用命名空间 + BEM 规范来编写 class,如:

vue
<script setup lang="ts" name="BEMDemo">
import { useNamespace } from "../../../hooks/useNamespace";

const ns = useNamespace("button");
</script>

<template>
  <div :class="ns.b()">
    <span :class="ns.e('text')">文字按钮</span>
    <button :class="ns.m('large')">large 按钮</button>
    <span :class="ns.em('text', 'bold')">文字加粗按钮</span>
    <button :class="['button', ns.is('primary')]">primary 按钮</button>
  </div>
</template>

等于:

vue
<script setup lang="ts" name="BEMDemo"></script>

<template>
  <div class="tk-button">
    <span class="tk-button__text">文字按钮</span>
    <button class="tk-button--large">large 按钮</button>
    <span class="tk-button__text--bold">文字加粗按钮</span>
    <button class="button is-primary">primary 按钮</button>
  </div>
</template>

具体使用请看 Teek 的组件源码。

样式文件使用 BEM

定义 SCSS 文件 bem.scss 来封装命名空间和 BEM 规范,

在样式文件引入 bem.scss 文件,使用 bem.scss 文件提供的 mixins 来编写样式,如:

scss
@use "../mixins/bem" as *;

@include b("button") {
  @include e("text") {
    @include m("bold") {
    }
  }

  @include m("large") {
  }

  .button {
    @include when("primary") {
    }
  }
}

等于:

scss
.tk-button {
  .tk-button__text {
    &--bold {
    }
  }

  .tk-button--large {
  }

  .button {
    &.is-primary {
    }
  }
}

具体使用请看 Teek 的样式源码。

CSS Var 变量使用命名空间

Teek 给所有的 CSS Var 变量都添加命名空间,如:

css
--tk-lower-color-1: #86909c;

为了共用 $namespace 变量,所以 Teek 提供了 set-css-var Mixin 和 getCss-Var 函数来进行封装:

scss
/* styles/mixins/mixin.scss */
@use "./function" as *;

@mixin set-css-var($name, $value) {
  #{joinVarName($name)}: #{$value};
}
scss
/* styles/mixins/function.scss */
$namespace: "tk" !default; // 假设这里定义了命名空间

/* 合并变量名:joinVarName(('button', 'text-color')) => '--tk-button-text-color' */
@function joinVarName($list) {
  $name: "--" + config.$namespace;
  @each $item in $list {
    @if $item != "" {
      $name: $name + "-" + $item;
    }
  }
  @return $name;
}

/* getCssVar('button', 'text-color') => var(--tk-button-text-color) */
@function getCssVar($args...) {
  @return var(#{joinVarName($args)});
}

使用 set-css-var 来定义 CSS Var 变量:

scss
@use "../mixins/mixins" as *;

:root {
  @include set-css-var("button-width", 84px);
  @include set-css-var("button-height", 32px);
  @include set-css-var("button-color", #3451b2);
  @include set-css-var("button-font-size", 16px);
}

等于

scss
:root {
  --tk-button-width: 84px;
  --tk-button-height: 32px;
  --tk-button-color: #3451b2;
  --tk-button-font-size: 16px;
}

使用 CSS Var 变量:

scss
@use "../mixins/function" as *;

.demo {
  width: getCssVar("button-width");
  height: getCssVar("button-height");
  color: getCssVar("button-color");
  font-size: getCssVar("button-font-size");
}

等于

scss
.demo {
  width: var(--tk-button-width);
  height: var(--tk-button-height);
  color: var(--tk-button-color);
  font-size: var(--tk-button-font-size);
}

具体使用请看 Teek 的 CSS Var 源码。