本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究
前言
在软件领域有很多的架构思想,通过不同的架构模式,可以让你的软件工程易拓展、易维护、易复用。同样在 CSS 工程当中,我们也需要使用架构思想,如果 CSS 没有使用架构思想的话,就会存在 CSS 代码极度混乱,难复用、难拓展、难维护等问题。特别是如果一个系统页面极度复杂的情况下,没有对 CSS 代码进行一个规划的话,那么后期维护 CSS 代码则堪称灾难。
所以我们需要像其他编程语言那样通过一些架构模式进行提高 CSS 代码的健壮性和可维护性。本文将探讨 Element Plus 组件库中的 CSS 架构思想。
CSS 设计模式之 OOCSS
我们如果对 Element Plus 的 CSS 架构稍微有些了解的话,就知道 Element Plus 的 CSS 架构使用了 BEM 设计模式,而 BEM 是 OOCSS 的一种实现模式,可以说是进阶版的 OOCSS。那么什么是 OOCSS 呢?
OOCSS 的全称为 Object Oriented CSS (面向对象的 CSS),它让我们可以使用向对象编程思想进行编写 CSS。
面向对象有三大特征:封装、继承、多态 ,在 OOCSS 中,主要应用到了面向对象的封装和继承的思想。我们以掘金的下图这个部分来进行说明:
图中画红色的部分,可以看成是有四个容器组成的,每个容器里面的内容又不一样。那么每个容器都有相同的样式,那么我们就可以进行封装。
把每一个容器封装成一个叫 item
的 class
,因为它们都有一些共同的样式。
.item {
position: relative;
margin-right: 20px;
font-size: 13px;
line-height: 20px;
color: #4e5969;
flex-shrink: 0;
}
然后如果我们需要对它们每一项进行拓展的话,那么我们只需要在原来的样式基础上进行新增一个 class,再针对这个 class 写不同的样式即可,这样达到继承原来基础部分的样式进行拓展自己独有的样式。
通过上图可以得知我们相当于继承基础类型 item 后,然后分别拓展出 浏览 view
、点赞 like
、评论 comment
、更多 more
的 CSS 内容。
.item.view {
// 浏览
}
.item.like {
// 点赞
}
.item.comment {
// 评论
}
.item.more {
// 更多
}
通过这种模式就大大增加了 CSS 代码的可维护性,可以在没有修改源代码的基础上进行修正和拓展。同时通过上面的例子,我们可以引出 OOCSS 的两大原则:
- 容器(container)与内容(content)分离
- 结构(structure)与皮肤(skin)分离
例如在 Element Plus 组件库中就有两个经典的布局组件 Container 布局容器 和 Layout 布局,这是 OOCSS 的典型应用:
实质上我们在写 Vue 组件的时候,就是在对 CSS 进行封装,这也是 OOCSS 的实践方式之一。
<el-button class="self-button">默认按钮</el-button>
<style lang="stylus" rel="stylesheet/stylus" scoped>
.self-button {
color: white;
margin-top: 10px;
width: 100px;
}
</style>
例如上述代码,我们的 Element Plus 组件库已经对 el-button 组件的样式进行了封装,但我们还可以基于 el-button 组件的样式进行拓展我们符合我们项目 UI 的样式,这就是典型的封装与继承。
OOCSS 强调重复使用类选择器,避免使用 id 选择器,最重要的是从项目的页面中分析抽象出“对象”组件,并给这些对象组件创建 CSS 规则,最后完善出一套基础组件库。这样业务组件就可以通过组合多个 CSS 组件实现综合的样式效果,这体现了 OOCSS 的显著优点:可组合性高。
OOCSS 为我们提供了一种编写 CSS 代码的思维模型或者说方法论,后续则演化出更加具体的一种实现模式,也就是 BEM。
CSS 设计模式之 BEM
BEM 是由 Yandex 团队提出的一种 CSS 命名方法论,即 Block(块)、Element(元素)、和 Modifier(修改器)的简称,是 OOCSS 方法论的一种实现模式,底层仍然是面向对象的思想。下面我们从 Element Plus 的 Tabs 组件进行讲解 BEM 的核心思想。
那么整一个组件模块就是一个 Block(块),classname 取名为:el-tabs。Block 代表一个逻辑或功能独立的组件,是一系列结构、表现和行为的封装。 其中每个一个切换的标签就是一个 Element(元素),classname 取名为:el-tabs__item。Element(元素)可以理解为块里的元素。 Modifier(修改器)用于描述一个 Block 或者 Element 的表现或者行为。例如我们需要对两个 Block(块) 或者两个 Element(元素)进行样式微调,那么我们就需要通过 Modifier(修改器),Modifier(修改器)只能作用于 Block(块)或者 Element(元素),Modifier(修改器)是不能单独存在的。
例如按钮组件的 classname 取名为 el-button,但它有不通过状态譬如:primary、success、info,那么就通过 Modifier(修改器)进行区分,classname 分别为: el-button--primary、el-button--success、el-button--info。从这里也可以看出 BEM 本质上就是 OOCSS,基础样式都封装为 el-button,然后通过继承 el-button 的样式,可以拓展不同的类,例如:el-button--primary、el-button--success、el-button--info。
BEM 规范下 classname 的命名格式为:
block-name__<element-name>--<modifier-name>_<modifier_value>
- 所有实体的命名均使用小写字母,复合词使用连字符 “-” 连接。
- Block 与 Element 之间使用双下画线 “__” 连接。
- Mofifier 与 Block/Element 使用双连接符 “--” 连接。
- modifier-name 和 modifier_value 之间使用单下画线 “_” 连接。
当然这些规则并不一定需要严格遵守的,也可以根据你的团队风格进行修改。
在 OOCSS 中,我们通过声明一个选择器对一个基础样式进行封装的时候,这个选择器是全局的,当项目庞大的时候,这样就容易造成影响到其他元素。通过 CSS 命名方法论 BEM,则在很大程度上解决了这个问题。因为 BEM 同时规定 CSS 需要遵循只使用一个 classname 作为选择器,选择器规则中既不能使用标签类型、通配符、ID 以及其他属性,classname 也不能嵌套,此外通过 BEM 可以更加语义化我们的选择器名称。BEM 规范非常适用于公共组件,通过 BEM 命名规范可让组件的样式定制具有很高的灵活性。
此外通过 BEM 的命名规范可以让页面结构更清晰。
<form class="el-form">
<div class="el-form-item">
<label class="el-form-item__label">名称:</label>
<div class="el-form-item__content">
<div class="el-input">
<div class="el-input__wrapper">
<input class="el-input__inner" />
</div>
</div>
</div>
</div>
</form>
我们以 Element Plus 的 Form 表单的 HTML 结构进行分析,我们可以看到整个 classname 的命名是非常规范的,整个 HTML 的结构是非常清晰明了的。
通过 JS 生成 BEM 规范名称
在编写组件的时候如果通过手写 classname 的名称,那么需要经常写 el
、-
、 __
、 --
,那么就会变得非常繁琐,通过上文我们可以知道 BEM 命名规范是具有一定规律性的,所以我们可以通过 JavaScript 按照 BEM 命名规范进行动态生成。
命名空间函数是一个 hooks 函数,类似这样的 hooks 函数在 Element Plus 中有非常多,所以我们可以在 packages 目录下创建一个 hooks 模块(具体创建项目过程可参考本专栏的第二篇《2. 组件库工程化实战之 Monorepo 架构搭建》),进入到 hooks 目录底下初始化一个 package.json 文件,更改包名:@cobyte-ui/hooks
。文件内容如下:
{
"name": "@cobyte-ui/hooks",
"version": "1.0.0",
"description": "Element Plus composables",
"license": "MIT",
"main": "index.ts",
"module": "index.ts",
"unpkg": "index.js",
"jsdelivr": "index.js",
"types": "index.d.ts",
"peerDependencies": {
"vue": "^3.2.0"
},
"gitHead": ""
}
接着在 hooks 目录下再创建一个 use-namespace 目录用于创建 BEM 命名空间函数,再在 hooks 目录下创建一个 index.ts 文件用于模块入口文件。
index.ts 文件内容:
export * from './use-namespace'
本项目的 GitHub 地址:github.com/amebyte/ele…
首先引入一个命名空间的概念,所谓命名空间就是加多一个命名前缀。
import { computed, unref } from 'vue'
// 默认命名前缀
export const defaultNamespace = 'el'
export const useNamespace = (block: string) => {
// 命名前缀也就是命名空间
const namespace = computed(() => defaultNamespace)
return {
namespace,
}
}
通过加多一个命名前缀,再加上 BEM 的命名规范就可以大大降低我们组件的 classname 与项目中的其他 classname 发生名称冲突的可能性。
通过前文我们知道 BEM 的命名规范就是通过一定的规则去书写我们的 classname,在 JavaScript 中则表现为按照一定规则去拼接字符串。
BEM 命名字符拼接函数:
// BEM 命名字符拼接函数
const _bem = (
namespace: string,
block: string,
blockSuffix: string,
element: string,
modifier: string
) => {
// 默认是 Block
let cls = `${namespace}-${block}`
// 如果存在 Block 后缀,也就是 Block 里面还有 Block,例如:el-form 下面还有一个 el-form-item
if (blockSuffix) {
cls += `-${blockSuffix}`
}
// 如果存在元素
if (element) {
cls += `__${element}`
}
// 如果存在修改器
if (modifier) {
cls += `--${modifier}`
}
return cls
}
这里值得注意的是 Block 有可能有后缀,也就是 Block 里面还有 Block,例如:el-form
下面还有一个 el-form-item
。
通过 BEM 命名字符拼接函数,我们就可以自由组合生成各种符合 BEM 规则的 classname 了。
export const useNamespace = (block: string) => {
// 命名前缀也就是命名空间
const namespace = computed(() => defaultNamespace)
// 创建块 例如:el-form
const b = (blockSuffix = '') =>
_bem(unref(namespace), block, blockSuffix, '', '')
// 创建元素 例如:el-input__inner
const e = (element?: string) =>
element ? _bem(unref(namespace), block, '', element, '') : ''
// 创建块修改器 例如:el-form--default
const m = (modifier?: string) =>
modifier ? _bem(unref(namespace), block, '', '', modifier) : ''
// 创建后缀块元素 例如:el-form-item
const be = (blockSuffix?: string, element?: string) =>
blockSuffix && element
? _bem(unref(namespace), block, blockSuffix, element, '')
: ''
// 创建元素修改器 例如:el-scrollbar__wrap--hidden-default
const em = (element?: string, modifier?: string) =>
element && modifier
? _bem(unref(namespace), block, '', element, modifier)
: ''
// 创建块后缀修改器 例如:el-form-item--default
const bm = (blockSuffix?: string, modifier?: string) =>
blockSuffix && modifier
? _bem(unref(namespace), block, blockSuffix, '', modifier)
: ''
// 创建块元素修改器 例如:el-form-item__content--xxx
const bem = (blockSuffix?: string, element?: string, modifier?: string) =>
blockSuffix && element && modifier
? _bem(unref(namespace), block, blockSuffix, element, modifier)
: ''
// 创建动作状态 例如:is-success is-required
const is: {
(name: string, state: boolean | undefined): string
(name: string): string
} = (name: string, ...args: [boolean | undefined] | []) => {
const state = args.length >= 1 ? args[0]! : true
return name && state ? `${statePrefix}${name}` : ''
}
return {
namespace,
b,
e,
m,
be,
em,
bm,
bem,
is,
}
}
最后我们就可以在组件中引入 BEM 命名空间函数进行创建各种符合 BEM 命名规范的 classname 了,例如:
- 创建块 el-form、
- 创建元素 el-input__inner、
- 创建块修改器 el-form--default、
- 创建块后缀元素 el-form-item、
- 创建元素修改器 el-scrollbar__wrap--hidden-default、
- 创建动作状态 例如:is-success is-required
具体创建代码使用代码如下:
import {
useNamespace,
} from '@cobyte-ui/hooks'
// 创建 classname 命名空间实例
const ns = useNamespace('button')
然后就可以在 template 中进行使用了:
<template>
<button
ref="_ref"
:class="[
ns.b()
]"
>按钮</button>
<template>
通过 SCSS 生成 BEM 规范样式
我们在本专栏的第二篇《2. 组件库工程化实战之 Monorepo 架构搭建》中已经创建一个样式主题的目录 theme-chalk
。现在我们接着在这个目录下创建组件样式代码,我们在 theme-chalk
目录下创建一个 src
目录,在 src
目录下创建一个 mixins
目录。
Element Plus 的样式采用 SCSS 编写的,那么就可以通过 SCSS 的 @mixin 指令定义 BEM 规范样式。在 mixins
目录下新建三个文件:config.scss、function.scss、mixins.scss。
其中 config.scss 文件编写 BEM 的基础配置比如样式名前缀、元素、修饰符、状态前缀:
$namespace: 'el' !default; // 所有的组件以el开头,如 el-input
$common-separator: '-' !default; // 公共的连接符
$element-separator: '__' !default; // 元素以__分割,如 el-input__inner
$modifier-separator: '--' !default; // 修饰符以--分割,如 el-input--mini
$state-prefix: 'is-' !default; // 状态以is-开头,如 is-disabled
在 SCSS 中,我们使用 $+ 变量名:变量
来定义一个变量。在变量后加入 !default
表示默认值。给一个未通过 !default
声明赋值的变量赋值,此时,如果变量已经被赋值,不会再被重新赋值;但是如果变量还没有被赋值,则会被赋予新的值。
mixins.scss 文件编写 SCSS 的 @mixin 指令定义的 BEM 代码规范。
定义 Block:
@mixin b($block) {
$B: $namespace + '-' + $block !global;
.#{$B} {
@content;
}
}
$B
表示定义一个一个变量,$namespace 是来自 config.scss 文件中定义的变量, !global
表示其是一个全局变量,这样就可以在整个文件的任意地方使用。#{}
字符串插值,类似模板语法。通过 @content
可以将 include{}
中传递过来的内容导入到指定位置。
定义 Element:
@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;
}
}
}
}
首先定义一个全局变量 $E
,接着定义父选择器 $selector
,再定义当前的选择器 $currentSelector
,再通过循环得到当前的选择器。接着通过函数 hitAllSpecialNestRule(hitAllSpecialNestRule 函数在 mixins 目录的 function.scss 文件中) 判断父选择器是否含有 Modifier、表示状态的 .is-
和 伪类,如果有则表示需要嵌套。@at-root
的作用就是将处于其内部的代码提升至文档的根部,即不对其内部代码使用嵌套。
定义修改器:
@mixin m($modifier) {
$selector: &;
$currentSelector: '';
@each $unit in $modifier {
$currentSelector: #{$currentSelector +
$selector +
$modifier-separator +
$unit +
','};
}
@at-root {
#{$currentSelector} {
@content;
}
}
}
这个非常好理解,就是定义了父选择器变量 $selector
和 当前选择器变量 $currentSelector
,并且当前选择器变量初始值为空,再通过循环传递进来的参数 $modifier
,获得当前选择器变量 $currentSelector
的值,再定义样式内容,而样式内容是通过 @content
将 include{}
中传递过来的内容。
定义动作状态:
@mixin when($state) {
@at-root {
&.#{$state-prefix + $state} {
@content;
}
}
}
选择器就是 config.scss 文件中的变量 $state-prefix
加传进来的状态变量,而样式内容是通过 @content
将 include{}
中传递过来的内容。
接着我们再看下上面定义 Element 的时候说到的 hitAllSpecialNestRule 函数,这个函数是定义在 mixins 目录下的 function.scss 文件中。function.scss 文件内容如下:
@use 'config';
// 该函数将选择器转化为字符串,并截取指定位置的字符
@function selectorToString($selector) {
$selector: inspect(
$selector
); // inspect(...) 表达式中的内容如果是正常会返回对应的内容,如果发生错误则会弹出一个错误提示。
$selector: str-slice($selector, 2, -2); // str-slice 截取指定字符
@return $selector;
}
// 判断父级选择器是否包含'--'
@function containsModifier($selector) {
$selector: selectorToString($selector);
@if str-index($selector, config.$modifier-separator) {
// str-index 返回字符串的第一个索引
@return true;
} @else {
@return false;
}
}
// 判断父级选择器是否包含'.is-'
@function containWhenFlag($selector) {
$selector: selectorToString($selector);
@if str-index($selector, '.' + config.$state-prefix) {
@return true;
} @else {
@return false;
}
}
// 判断父级是否包含 ':' (用于判断伪类和伪元素)
@function containPseudoClass($selector) {
$selector: selectorToString($selector);
@if str-index($selector, ':') {
@return true;
} @else {
@return false;
}
}
// 判断父级选择器,是否包含`--` `.is-` `:`这三种字符
@function hitAllSpecialNestRule($selector) {
@return containsModifier($selector) or containWhenFlag($selector) or
containPseudoClass($selector);
}
通过上述代码我们就可以知道 hitAllSpecialNestRule 函数是如何判断父选择器是否含有 Modifier、表示状态的 .is-
和 伪类的了。
测试实践 BEM 规范
接下来,我们要把上面实现的 BEM 规范应用到真实组件中,通过写一个简易的测试组件进行测试实践。首先我们的样式是基于 SCSS 所以我们需要安装 sass。
我们在根目录下执行:
pnpm install sass -D -w
接着我们把上新建的 hooks 模块也安装到根项目上:
我们在根目录下执行:
pnpm install @cobyte-ui/hooks -D -w
而 theme-chalk 模块,我们在本专栏的第二篇的《2. 组件库工程化实战之 Monorepo 架构搭建》已经进行了安装,这里就不用再进行安装了。
我们在 packages 目录下的 components 目录创建一个 icon 目录,再创建以下目录结构:
├── packages
│ ├── components
│ │ ├── icon
│ │ │ ├── src
│ │ │ │ └── icon.vue
│ │ │ └── index.ts
│ │ └── package.json
index.ts 文件内容:
import Icon from './src/icon.vue'
export default Icon
icon.vue 文件内容:
<template>
<i :class="bem.b()">
<slot />
</i>
</template>
<script setup lang="ts">
import { useNamespace } from '@cobyte-ui/hooks'
const bem = useNamespace('icon')
</script>
我们通过导入上面使用 JS 生成 BEM 规范名称 hooks 函数,然后创建对应的命名实例 bem,然后生成对应的 Block 块的 classname。接下来,我们把这个测试组件渲染到页面上,看看具体生成的效果。
我们去到 play 目录下的 src 目录中的 App.vue 文件中把上面写的测试组件进行引入:
<template>
<div>
<c-icon>Icon</c-icon>
</div>
</template>
<script setup lang="ts">
import CIcon from '@cobyte-ui/components/icon'
import '@cobyte-ui/theme-chalk/src/index.scss'
</script>
<style scoped></style>
我们也把 theme-chalk
目录中的样式也进行了导入。接着我们在 theme-chalk 目录下的 src 目录新建一个 icon.scss 文件,文件内容如下:
@use 'mixins/mixins' as *;
@include b(icon) {
background-color: red;
color: #fff;
}
这里我们可以看到 SCSS 的 @mixin
、@include
的用法: @mixin
用来定义代码块、@include
进行引入。
我们需要在 theme-chalk 目录下的 src 目录中的 index.scss 中导入 icon.scss 文件。
index.scss 文件内容:
@use './icon.scss';
这样我们就实现了所有文件的闭环,最后我们把 play 项目运行起来,看看效果,要运行 play 项目,我们专栏的前面的文章中已经说过了,就是在根目录下执行 pnpm run dev
即可。
我们也看到已经成功实现了渲染并和如期一样,那么其他样式的测试,我们将在后续具体的组件实现上再进行测试。
经典 CSS 架构 ITCSS
本小节我们继续通过学习经典 CSS 架构 ITCSS 来对比学习 Element Plus 的样式系统架构。我们可以向一些经典的 CSS 架构去学习取经,看看人家是怎么做架构的,从而可以在我们做自身的 CSS 架构的时候可以带来一些的启发,然后取长补短,从而可以更加优化自身的 CSS 架构。
ITCSS 基于分层的概念把项目中的样式分为七层,分别如下:
- Settings 层: 维护一些包含字体、颜色等的变量,主要是变量层。
- Tools 层: 工具库,例如 SCSS 中的 @mixin 、@function
- Generic 层: 重置和/或标准化样式等,例如 normalize.css、reset.css,主要解决浏览器的样式差异
- Elements 层: 对一些元素进行定制化的设置,如 H1 标签默认样式,A 标签默认样式等
- Objects 层: 类名样式中,不允许出现外观属性,例如 Color,遵循OOCSS方法的页面结构类,主要用来做画面的 layout
- Components 层: UI 组件
- Trumps 层: 实用程序和辅助类,能够覆盖前面的任何内容,也就是设置
important!
的地方
ITCSS 不是一个框架,只是组织样式代码的一种方案,ITCSS 的分层越在上面的分层,被复用性就越广,层的权重是层层递进,作用范围却是层层递减。除了 ITCSS 之外还有其他一些 CSS 架构,比如 SMACSS 、ACSS 等,但它们的核心思想并不是放之四海而皆准的,但是它维护项目样式的思想却是值得借鉴的。我们可以不完全遵守它们的规则,可以根据我们的项目需要进行删减或者保留。
那么根据上面 ITCSS 架构思想,Element Plus 也设置了 Settings 层 ,在 theme-chalk/src/common
目录下的 var.scss 文件中就维护着各种变量。我们上文中实现的 BEM 规范的 mixins
目录下的 function.scss、mixins.scss 等则是 Tools 层。Generic 层 主要解决浏览器的样式差异,这些工作应该在具体的项目中进行处理,而 Element Plus 只是一个第三方的工具库,所以 Element Plus 在这一层不进行设置。Elements 层 主要是对一些基础元素拓展一些样式,从而让我们的网站形成一套自己的风格,例如对 H1 标签默认样式,A 标签默认样式的设置。Element Plus 则在 theme-chalk/src
目录下的 reset.scss 文件进行了设置。Objects 层 和 Components 层 其实就是 OOCSS 层,也就是我们上文所说的 BEM 样式规范,又因为组件库的样式使用一般都分为全量引入和按需引入,所以就根据组件名称分别设置各自样式文件进行维护各自的样式。Trumps 层 在 Element Plus 中也是没有的,同样是因为 Element Plus 只是一个第三方工具库,权重是比较低,所以不需要设置权重层。
总结
我们先从什么是 OOCSS 开始,OOCSS 主要运用了传统编程类中的封装和继承的特性,OOCSS 为我们提供了一种编写 CSS 代码的思维模型或者说方法论,后续则演化出更加具体的一种实现模式,也就是 BEM。
接着我们从 Element Plus 的 Tabs 组件进行讲解 BEM 的核心思想。BEM 本质上就是 OOCSS,通过 BEM 可以更加语义化我们的选择器名称。BEM 规范非常适用于公共组件,通过 BEM 命名规范可让组件的样式定制具有很高的灵活性。
接着我们介绍如何通过 JS 生成 BEM 规范名称。在编写组件的时候如果通过手写 classname 的名称,那么需要经常写 el
、-
、 __
、 --
,那么就会变得非常繁琐,通过上文我们可以知道 BEM 命名规范是具有一定规律性的,所以我们可以通过 JavaScript 按照 BEM 命名规范进行动态生成。
接着学习如何通过 SCSS 生成 BEM 规范样式,并学习了 SCSS 的一些核心知识。
之后写了一个 demo 组件进行验证我们所写的 BEM 规范代码。
最后我们通过学习经典 CSS 架构 ITCSS 的思想,从而更加深入理解 Element Plus 的 CSS 架构设置思想。我们可以看到 Element Plus 的 CSS 架构并不是单一的使用了具体那一种架构,而是融合了多种架构的思想。
所以所谓的 CSS 架构,它们的核心思想并不是放之四海而皆准的,但是它维护项目样式的思想却是值得借鉴的。我们可以不完全遵守它们的规则,可以根据我们的项目需要进行删减或者保留。
Element Plus 的 CSS 样式架构中还有非常多少值得学习的知识,后续具体组件的实现,我们再继续探讨。
此文章的实现代码仓库:github.com/amebyte/ele…
欢迎关注本专栏,了解更多 Element Plus 组件库知识
本专栏文章:
5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理
8. 为什么组件库或插件需要定义 peerDependencies
9. 组件开发中 Vue3 相关知识的应用与解析及 Button 组件的实现
11. 深入理解组件库中SCSS和CSS变量的架构应用和实践
12. 组件 v-model 的封装实现原理及 Input 组件的核心实现
13. 深入理解 Vue3 的 v-model 及自定义指令的实现原理