样式布局
样式与组件分离
Teek 并没有和普通的 Vue 项目一样,使用如下模板进行编写组件:
<script setup lang="ts">
// 组件逻辑
</script>
<template>
<!-- 组件模板 -->
</template>
<style lang="scss" scoped>
/* 组件样式 */
</style>而是使用:
<script setup lang="ts">
// 组件逻辑
</script>
<template>
<!-- 组件模板 -->
</template>那么组件样式去哪里了呢?
Teek 专门创建 theme-chalk/src/components 目录用于存放组件样式,然后将所有的组件样式汇总到一个 index.scss(入口样式文件),最后在 index.ts(入口运行文件)分别引入 theme-chalk/index.scss(入口样式文件)和 layout/index.vue(入口组件)。
*.vue ——> Layout/index.vue ——> index.ts <—— theme-chalk/index.scss <—— *.scss
多个功能组件 ——> 入口组件 ——> 入口运行文件 <—— 入口样式文件 <—— 多个功能组件样式提示
这也就是为什么在 .vitepress/theme/index.ts 里单独引入 Teek 样式的原因。
样式结构设计
Teek 的样式结构设计遵循单一原则:每个样式文件只负责渲染单独的模块(组件),如:
nav-var.scss只提供导航栏的css var变量nav-search-button.scss只渲染导航栏的搜索按钮,内部引用了nav-var.scssnav-switch-button.scss只渲染导航栏的深色、浅色切换按钮,内部引用了nav-var.scssnav-translation.scss只渲染导航栏的国际化下拉框,内部引用了nav-var.scss
这样的好处是,不同的样式之间是独立的,需要根据自己的需求按需引入对应的样式文件,不会存在引入一个内容超大的样式文件,导致不想要的元素也发生了样式改变。
当然,对于不喜欢折腾、研究的小伙伴来说,Teek 也提供了 nav.scss 样式文件,该文件内部没有编写任何样式代码,而是引入了 nav-xxx.scss 等样式文件,用于快速引入 nav 的所有样式文件。
目录结构
下面给出 Teek 的样式目录结构:
theme-chalk/src.
├─ base.scss # 基础样式文件
├─ index.scss # 入口样式文件
│
├─ common # 通用样式目录
├─ components # 组件样式目录,对应每个 Vue 组件
├─ md-plugin # Markdown 插件样式目录
├─ mixins # 样式混入目录
├─ module # 模块样式目录
├─ var # 样式变量目录
└─ vp-plus # VitePress 样式加强目录在 components 目录下,Teek 会给每个组件生成对应的样式文件,文件名与组件名相近(字母小写 + - 分割来命名)。
index.scss 文件是入口样式文件,它将导入 base.scss、var 目录下的样式文件,以及 components 目录下所有组件的样式文件。
如果需要按需加载组件,您必须要引入 base.scss 文件,这是 Teek 的核心主题样式文件,然后按需引入 Vue 组件和 components 目录下的对应组件的样式文件。
命名空间
Teek 并没有简单的直接给一个 div 元素添加 class="button",然后在 CSS 里 .button {} 定义样式,Teek 使用 命名空间 的设计思想,给每个组件添加唯一标识,确保不同组件的相同 class 发生样式冲突。
提示
命名空间等价于 Vue 组件 style 的 scoped 属性。
SCSS 定义命名空间
命名空间其实是一个唯一的前缀,如 ElementPlus 的命名空间为 el,在某个 class 中添加 el- 前缀,如 <div class="el-button"></div>
命名空间应该是一个变量,这样只需要修改该变量的值,那么所有的 class 以及样式都不会失效,如在 ElementPlus 的命名空间文件里修改 el 为 tk,那么所有的 class 都变为 tk 开头,且样式不会失效。
提示
Teek 的命名空间为 tk。
首先 Teek 在 theme-chalk/mixins/config.scss 文件中定义了命名空间变量 $namespace:
$namespace: "tk" !default;此时其他的 SCSS 文件都需要引入该文件,然后使用 $namespace 变量:
@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 变量导出到 js 或 ts 文件里,在 theme-chalk/module/namespace.module.scss 文件导出 $namespace:
/* theme-chalk/module/namespace.module.scss */
@use "../mixins/config" as *;
:export {
namespace: #{$namespace};
}信息
SCSS Module API 只对 .module.scss 结尾的文件提供变量暴露功能。
如果是 Typescript 环境使用,则还需要在同级目录下定义一个 namespace.module.scss.d.ts 文件:
// theme-chalk/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:
<script setup lang="ts">
import namespaceModule from "../theme-chalk/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(块)
- 独立的功能模块,可以独立存在
- 示例:
button、menu、input
Element(元素)
- 属于某个 Block 的一部分,不能单独存在
- 使用双下划线
__连接 Block 和 Element - 示例:
menu__item、button__text
Modifier(修饰符)
- 用于改变 Block 或 Element 的外观或行为
- 使用双横线
--表示 - 示例:
button--large、menu__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
<div class="button">
<span class="button__text">文字按钮</span>
<button class="button--large">large 按钮</button>
<span class="button__text--bold">文字加粗按钮</span>
</div>/* Block */
.button {
/* 样式 */
}
/* Element */
.button__text {
/* 样式 */
}
/* Modifier */
.button--large {
/* 样式 */
}
/* Element + Modifier */
.button__text--bold {
/* 样式 */
}组件元素使用 BEM
定义一个 Hooks 文件 useNamespace.ts 来封装命名空间和 BEM 规范,命名空间 和 BEM 规范 在上文已经介绍过了。
在组件中引入 useNamespace.ts 文件,使用命名空间 + BEM 规范来编写 class,如:
<script setup lang="ts" name="BEMDemo">
import { useNamespace } from "../../../composables/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>等于:
<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 来编写样式,如:
@use "../mixins/bem" as *;
@include b("button") {
@include e("text") {
@include m("bold") {
}
}
@include m("large") {
}
.button {
@include when("primary") {
}
}
}等于:
.tk-button {
.tk-button__text {
&--bold {
}
}
.tk-button--large {
}
.button {
&.is-primary {
}
}
}具体使用请看 Teek 的样式源码。
CSS Var 变量使用命名空间
Teek 给所有的 CSS Var 变量都添加命名空间,如:
--tk-text-color-secondary: #86909c;为了共用 $namespace 变量,所以 Teek 提供了 set-css-var Mixin 和 getCss-Var 函数来进行封装:
/* theme-chalk/mixins/mixin.scss */
@use "./function" as *;
@mixin set-css-var($name, $value) {
#{joinVarName($name)}: #{$value};
}/* theme-chalk/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 变量:
@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);
}等于
:root {
--tk-button-width: 84px;
--tk-button-height: 32px;
--tk-button-color: #3451b2;
--tk-button-font-size: 16px;
}使用 CSS Var 变量:
@use "../mixins/function" as *;
.demo {
width: getCssVar("button-width");
height: getCssVar("button-height");
color: getCssVar("button-color");
font-size: getCssVar("button-font-size");
}等于
.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 源码。