样式布局
样式与组件分离
Teek 并没有和普通的 Vue 项目一样,使用如下模板进行编写组件:
<script setup lang="ts">
// 组件逻辑
</script>
<template>
<!-- 组件模板 -->
</template>
<style lang="scss" scoped>
/* 组件样式 */
</style>
而是使用:
<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 的样式目录结构:
styles
├─ 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 定义命名空间
命名空间其实是一个唯一的前缀,如 Element Plus 的命名空间为 el
,在某个 class
中添加 el-
前缀,如 <div class="el-button"></div>
命名空间应该是一个变量,这样只需要修改该变量的值,那么所有的 class
以及样式都不会失效,如在 Element Plus 的命名空间文件里修改 el
为 tk
,那么所有的 class
都变为 tk
开头,且样式不会失效。
提示
Teek 的命名空间为 tk
。
首先 Teek 在 styles/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
文件里,在 styles/module/namespace.module.scss
文件导出 $namespace
:
/* styles/module/namespace.module.scss */
@use "../mixins/config" as *;
:export {
namespace: #{$namespace};
}
信息
SCSS Module API 只对 .module.scss
结尾的文件提供变量暴露功能。
如果是 Typescript 环境使用,则还需要在同级目录下定义一个 namespace.module.scss.d.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
:
<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(块)
- 独立的功能模块,可以独立存在
- 示例:
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 "../../../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>
等于:
<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-lower-color-1: #86909c;
为了共用 $namespace
变量,所以 Teek 提供了 set-css-var
Mixin 和 getCss-Var
函数来进行封装:
/* styles/mixins/mixin.scss */
@use "./function" as *;
@mixin set-css-var($name, $value) {
#{joinVarName($name)}: #{$value};
}
/* 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 变量:
@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 源码。