Vue 组件开发中 CSS 的 BEM 规范完全指南
介绍
什么是 BEM?
什么是 BEM?
BEM 是一种 CSS 命名规范,全称为 Block(块)、Element(元素)、Modifier(修饰符)。这是一种可以帮助我们更好地组织和管理 CSS 代码的方法,特别适合在大型项目或团队协作中使用。
想象一下,你正在打造一座城市(你的网站):
- Block(块) 就像是城市中的一栋独立建筑,例如商场、医院、学校。
- Element(元素) 是这栋建筑内部的组成部分,如商场里的商店、电梯、走廊。
- Modifier(修饰符) 则是描述这些建筑或内部组件的特定状态或变体,比如一个"豪华版"的商场或"紧急状态"的电梯。
为什么要使用 BEM?
也许你会问:"我一直用普通的 CSS 类名也没问题,为什么要学习新的命名规范呢?"
这里有几个让你心动的理由:
- 减少样式冲突:BEM 提供了一种命名约定,可以显著降低类名冲突的概率。
- 提高代码可读性:通过类名就能清晰地看出元素之间的层级关系。
- 增强代码可维护性:当项目变大时,BEM 可以帮助你快速定位和修改样式。
- 利于团队协作:团队成员可以更容易理解彼此的代码,减少沟通成本。
BEM 的基本语法
BEM 命名规则看起来是这样的:
.block {}
.block-block {}
.block__element {}
.block--modifier {}
.block__element--modifier {}
让我们详细解释每个部分:
- Block(块):独立存在的组件,如
.nc-button
、.nc-header
、.nc-button-group
- Element(元素):块的一部分,依赖于块,如
.nc-button__icon
、.nc-menu__item
- Modifier(修饰符):改变块或元素的外观或行为,如
.nc-button--large
、.nc-menu__item--active
BEM 还有一个常见扩展:State(状态),通常以 is-
前缀表示,如 .is-disabled
、.is-active
。
TIP
讲到这里你应该发现了,很多流行的组件库如 Element Plus
、Ant Design Vue
等都在使用 BEM 命名规范。这不是巧合,而是因为 BEM 确实能够有效地解决大型项目中的样式管理问题。
BEM 在前端开发中的应用
传统 CSS 中的 BEM
在传统 CSS 中,我们通常直接写类名:
<button class="button button--primary button__icon--left">
<span class="button__icon"> 图标 </span>
<span class="button__text"> 按钮文本 </span>
</button>
.button {
display: inline-block;
padding: 10px 15px;
}
.button--primary {
background-color: blue;
color: white;
}
.button__icon {
margin-right: 5px;
}
.button__icon--left {
float: left;
}
.button__text {
font-weight: bold;
}
虽然这种方法可行,但在大型项目中手动编写这些类名会变得繁琐和易错。别担心,接下来我们将介绍更高效的方法!
在 Vue 组件中使用 BEM
在 Vue 组件中,我们可以使用 TypeScript 和 SCSS 来简化 BEM 的使用。下面我们将探讨两种方式:
- 使用 TypeScript 函数生成 BEM 类名
- 使用 SCSS 混入创建 BEM 样式
工具函数封装
一、TypeScript 版 BEM 工具函数
NOTE
首先,让我们来看看如何使用 TypeScript 封装一个 BEM 工具函数。这个函数可以帮助我们在 Vue 组件的模板中轻松生成符合 BEM 规范的类名。
我们将分步骤讲解这个工具函数的实现:
步骤 1:定义 BEM 接口
先创建一个
create.ts
文件
首先,我们需要定义清晰的接口,指定我们的 BEM 函数应该具有什么功能:
// 定义 BEM 函数接口
interface BEM {
// 主函数:可以同时指定块、元素和修饰符
(block?: string, element?: string, modifier?: string): string
// 单独的方法
b: (block?: string) => string // 仅块
e: (element: string) => string // 仅元素
m: (modifier: string) => string // 仅修饰符
be: (block: string, element: string) => string // 块+元素
bm: (block: string, modifier: string) => string // 块+修饰符
em: (element: string, modifier: string) => string // 元素+修饰符
is: (name: string, state: string | boolean) => string // 状态
}
这个接口定义了我们的 BEM 函数的所有功能:
- 它可以作为函数直接调用,同时指定块、元素和修饰符
- 它也有多个单独的方法,用于不同的 BEM 组合情况
步骤 2:创建基础类名生成函数
NOTE
接下来,我们需要一个基础函数来生成 BEM 类名:
/**
* 生成 BEM 规范的类名
* @private
* @param prefixName - 前缀名称
* @param block - 块名称
* @param element - 元素名称
* @param modifier - 修饰符名称
* @returns 生成的类名
*/
const generateBEM = (prefixName: string, block?: string, element?: string, modifier?: string): string => {
let className = prefixName
if (block) {
className += `-${block}`
}
if (element) {
className += `__${element}`
}
if (modifier) {
className += `--${modifier}`
}
return className
}
这个函数很简单,它接收前缀、块、元素和修饰符作为参数,并将它们按照 BEM 规范组合成类名。
步骤 3:创建 BEM 函数生成器
NOTE
现在,我们创建一个函数,它会返回符合我们定义的 BEM 接口的函数:
/**
* 创建 BEM 命名生成器
* @private
* @param prefixName - 前缀名称
* @returns BEM 命名生成函数及其方法集合
*/
const createBEM = (prefixName: string): BEM => {
// 基础方法
const b = (block?: string): string => generateBEM(prefixName, block)
const e = (element: string): string => (element ? generateBEM(prefixName, '', element) : '')
const m = (modifier: string): string => (modifier ? generateBEM(prefixName, '', '', modifier) : '')
// 组合方法
const be = (block: string, element: string): string => generateBEM(prefixName, block, element)
const bm = (block: string, modifier: string): string => generateBEM(prefixName, block, '', modifier)
const em = (element: string, modifier: string): string =>
!element && !modifier ? '' : generateBEM(prefixName, '', element, modifier)
// 状态方法
const is = (name: string, state: string | boolean): string => (state ? `is-${name}` : '')
// 主函数
const bem = ((block?: string, element?: string, modifier?: string): string =>
generateBEM(prefixName, block, element, modifier)) as BEM
// 扩展方法
Object.assign(bem, {
b,
e,
m,
be,
bm,
em,
is
})
return bem
}
这个函数创建了所有我们需要的 BEM 方法,并将它们挂载到主函数上,使其符合我们定义的 BEM 接口。
步骤 4:创建命名空间函数
NOTE
最后,我们创建一个公共函数,用于创建特定组件的 BEM 命名空间:
/**
* 创建命名空间
* @public
* @param name - 组件名称
* @param prefix - 组件前缀
*
*/
export const createNamespace = (name: string, prefix: string = 'nc'): BEM => {
const prefixName = `${prefix}-${name}`
return createBEM(prefixName)
}
这个函数允许我们为每个组件创建一个独立的 BEM 命名空间,使用统一的前缀(这里我默认为 nc
,你也可以设置为任意的默认值,比如el
)。
步骤 5:在 Vue 单文件组件中使用
NOTE
现在我们已经完成了 BEM 工具函数的封装,让我们看看如何在实际的 Vue 单文件组件中使用它。
首先,创建一个简单的 Card
组件:
<template>
<!-- 基本用法1:使用 bem.b() 生成块类名 -->
<div :class="bem.b()">
<!-- 基本用法2:使用 bem.e() 生成元素类名 -->
<div :class="bem.e('header')">
<h3 :class="bem.e('title')">{{ title }}</h3>
<!-- 基本用法3:使用 bem.is() 生成状态类名 -->
<span :class="bem.is('closable', closable)" v-if="closable" @click="emit('close')">×</span>
</div>
<!-- 基本用法4:使用 bem.e() 和 bem.m() 组合 -->
<div
:class="[
bem.e('body'),
bem.m(type) // 添加修饰符类名
]"
>
<slot></slot>
</div>
<!-- 基本用法5:在条件渲染中使用 -->
<div v-if="footer" :class="bem.e('footer')">
{{ footer }}
</div>
</div>
</template>
<script setup lang="ts">
import { createNamespace } from '@/utils/create'
interface Props {
title?: string
type?: string
closable?: boolean
footer?: string
}
const props = withDefaults(defineProps<Props>(), {
title: '标题',
type: 'default',
closable: false,
footer: ''
})
const emit = defineEmits<{
(e: 'close'): void
}>()
// 创建BEM命名空间,参数是组件名
const bem = createNamespace('card')
</script>
<style lang="scss">
// 不使用SCSS混入的普通样式写法(有更方便的方式,下面会继续讲解)
.nc-card {
border: 1px solid #eee;
border-radius: 4px;
overflow: hidden;
margin-bottom: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
&__header {
padding: 15px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eee;
}
&__title {
margin: 0;
font-size: 16px;
font-weight: bold;
}
&__body {
padding: 20px;
}
// 不同类型的卡片
&--primary {
border-color: #1890ff;
.nc-card__header {
background-color: #1890ff;
color: white;
}
}
&--success {
border-color: #52c41a;
.nc-card__header {
background-color: #52c41a;
color: white;
}
}
&__footer {
padding: 15px;
border-top: 1px solid #eee;
background-color: #f7f9fa;
}
// 状态类
&.is-closable {
.nc-card__header span {
cursor: pointer;
font-size: 20px;
&:hover {
color: #f56c6c;
}
}
}
}
</style>
解释说明:
初始化 BEM 命名空间:
tsconst bem = createNamespace('card')
这一步,创建了一个名为
card
的 BEM 命名空间。生成块类名:
vue<div :class="bem.b()"></div>
这会生成
.nc-card
类名。vue<div :class="bem.b('box')"></div>
这会生成
.nc-card-box
类名。生成元素类名:
vue<div :class="bem.e('header')"></div>
这会生成
.nc-card__header
类名。生成带修饰符的类名:
vue<div :class="[bem.e('body'), bem.m(type)]"></div>
当
type="success"
时,会生成.nc-card__body
和.nc-card--success
类名。生成状态类名:
vue<span :class="bem.is('closable', closable)"></span>
当
closable=true
时,会生成.is-closable
类名。
通过这种方式,我们可以在 Vue 组件中轻松管理 BEM 类名,同时保持代码的清晰和可维护性。当组件名称或样式前缀需要更改时,我们只需修改一处即可(
createNamespace
的参数),而不需要在模板中搜索并替换所有类名。
二、SCSS 版 BEM 混入
NOTE
除了使用 TypeScript 生成类名外,我们还可以使用 SCSS 混入来简化 BEM 样式的编写。SCSS 混入允许我们在样式表中以更加结构化和语义化的方式定义 BEM 样式。
首先,我们需要创建一些配置变量:
步骤 1:创建 SCSS 配置文件
首先创建
config.scss
存放变量名称
// config.scss
// 命名空间,用于所有组件类名的前缀
// 例如:.nc-button, .nc-card 等
$namespace: 'nc' !default;
// 块与命名空间之间的连接符
// 例如:nc-button 中的 '-'
$common-separator: '-' !default;
// 元素分隔符,用于连接块和元素
// 例如:.nc-button__icon 中的 '__'
$element-separator: '__' !default;
// 修饰符分隔符,用于连接块/元素和修饰符
// 例如:.nc-button--primary 中的 '--'
$modifier-separator: '--' !default;
// 状态前缀,用于表示组件的不同状态
// 例如:.is-disabled, .is-active 等
$state-prefix: 'is-' !default;
这些变量定义了 BEM 命名规范中使用的前缀和分隔符。通过修改这些变量,你可以定制自己的命名风格,比如将命名空间从 nc
改为 el
(Element UI 使用的前缀)或 a
(Ant Design 风格)。
!default
标记表示这些是默认值,你可以在导入配置文件之前重新定义这些变量来覆盖默认值,非常适合主题定制。
步骤 2:创建基础的 BEM 混入
创建
mixin.scss
,首先引入config.scss
然后继续定义 mixin 混入
@use 'config' as *;
@forward 'config';
我们先从最简单的 block 混入开始:
// 创建 BEM 块级选择器
// $block - 块名称
@mixin b($block) {
$B: $namespace + $common-separator + $block !global;
.#{$B} {
@content;
}
}
这个混入可以帮助我们创建块级选择器,并将块名保存到一个全局变量中,以便其他混入使用。
用法示例:
scss@include b(button) { color: red; font-size: 14px; }
css.nc-button { color: red; font-size: 14px; }
步骤 3:创建元素混入
接下来,我们创建用于定义元素的混入:
// 创建 BEM 元素选择器
// $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;
}
}
}
}
这个混入可以在块内部创建元素选择器,并处理特殊嵌套规则。
用法示例:
scss@include b(button) { // 定义块样式 background: #fff; // 定义元素样式 @include e(icon) { margin-right: 5px; } }
css.nc-button { background: #fff; } .nc-button__icon { margin-right: 5px; }
步骤 4:创建修饰符混入
然后,我们创建用于定义修饰符的混入:
// 创建 BEM 修饰符选择器
// $modifier - 修饰符名称
@mixin m($modifier) {
$selector: &;
$currentSelector: '';
@each $unit in $modifier {
$currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ','};
}
@at-root {
#{$currentSelector} {
@content;
}
}
}
这个混入可以在块或元素内部创建修饰符选择器。
用法示例:
scss@include b(button) { color: #333; // 定义修饰符样式 @include m(primary) { color: white; background: blue; } @include m(danger) { color: white; background: red; } }
css.nc-button { color: #333; } .nc-button--primary { color: white; background: blue; } .nc-button--danger { color: white; background: red; }
步骤 5:创建状态混入
我们还需要创建一个用于定义状态的混入:
// 创建状态类选择器
// $state - 状态名称
@mixin when($state) {
@at-root {
&.#{$state-prefix + $state} {
@content;
}
}
}
这个混入可以创建状态选择器,用于定义元素的不同状态。
用法示例:
scss@include b(button) { cursor: pointer; // 定义状态样式 @include when(disabled) { cursor: not-allowed; opacity: 0.5; } @include when(active) { border-color: blue; } }
css.nc-button { cursor: pointer; } .nc-button.is-disabled { cursor: not-allowed; opacity: 0.5; } .nc-button.is-active { border-color: blue; }
步骤 5.5:创建伪类选择器混入
我们再来创建一个用于定义伪类的混入,它可以方便地为元素添加伪类样式:
// 创建伪类选择器
// $pseudo - 伪类名称
@mixin pseudo($pseudo) {
@at-root {
&:#{$pseudo} {
@content;
}
}
}
这个混入可以创建伪类选择器,用于定义元素的不同状态的样式,如:hover
、:focus
、:active
等。
用法示例:
scss@include b(button) { color: blue; // 定义伪类样式 @include pseudo(hover) { color: darkblue; text-decoration: underline; } @include pseudo(focus) { outline: 2px solid blue; } @include e(icon) { margin-right: 5px; // 元素的伪类 @include pseudo(hover) { transform: scale(1.2); } } }
css.nc-button { color: blue; } .nc-button:hover { color: darkblue; text-decoration: underline; } .nc-button:focus { outline: 2px solid blue; } .nc-button__icon { margin-right: 5px; } .nc-button__icon:hover { transform: scale(1.2); }
步骤 6:创建辅助函数
最后,我们需要一些辅助函数来支持我们的混入:
// 检查是否命中所有特殊嵌套规则
@function hitAllSpecialNestRule($selector) {
@return containsModifier($selector) or containWhenFlag($selector) or containPseudoClass($selector);
}
// 检查选择器是否包含修饰符
@function containsModifier($selector) {
$selector: selectorToString($selector);
@return str-index($selector, $modifier-separator);
}
// 检查选择器是否包含状态类
@function containWhenFlag($selector) {
$selector: selectorToString($selector);
@return str-index($selector, '.' + $state-prefix);
}
// 检查选择器是否包含伪类
@function containPseudoClass($selector) {
$selector: selectorToString($selector);
@return str-index($selector, ':');
}
// 将选择器转换为字符串
@function selectorToString($selector) {
$selector: inspect($selector);
$selector: str-slice($selector, 2, -2);
@return $selector;
}
这些辅助函数主要在内部使用,不需要直接调用。它们帮助混入处理嵌套规则,比如:
scss@include b(button) { @include m(primary) { background: blue; // 在修饰符内部使用元素 @include e(icon) { // 这里会被正确处理为:.nc-button--primary .nc-button__icon color: white; } } }
css.nc-button--primary { background: blue; } .nc-button--primary .nc-button__icon { color: white; }
实战应用
TIP
现在,我们已经了解了两种 BEM 实现方式。让我们看看如何在实际的 Vue 组件中使用它们。
结合两种方式的最佳实践
实际上,我们可以结合这两种方式,获得最佳的开发体验:
示例 :结合 TS 和 SCSS 的按钮组件
<template>
<button :class="[bem.b(), bem.m(type), bem.is('disabled', disabled)]">
<span v-if="icon" :class="bem.e('icon')">
<i :class="icon"></i>
</span>
<span :class="bem.e('text')">
<slot></slot>
</span>
</button>
</template>
<script setup lang="ts">
import { createNamespace } from '@/utils/create'
interface Props {
type?: string
icon?: string
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
type: 'default',
icon: '',
disabled: false
})
// 创建 BEM 命名空间
const bem = createNamespace('button')
</script>
<style lang="scss">
@use '@/theme-chalk/src/mixin' as *;
@include b(button) {
display: inline-block;
padding: 10px 15px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
@include m(primary) {
background-color: #1890ff;
color: white;
border-color: #1890ff;
}
@include m(success) {
background-color: #52c41a;
color: white;
border-color: #52c41a;
}
@include e(icon) {
margin-right: 5px;
}
@include e(text) {
font-weight: 500;
}
@include when(disabled) {
opacity: 0.5;
cursor: not-allowed;
}
}
</style>
在这个例子中:
- 我们在模板中使用 TS 版 BEM 工具生成类名,这使得类名的生成更加灵活和动态
- 我们在样式中使用 SCSS 混入定义样式,这使得样式代码更加结构化和可读
这种结合方式让我们能够充分利用两种工具的优势,实现最佳的开发体验。
完整代码
IMPORTANT
为方便读者直接使用,以下是本教程中涉及的三个关键文件的完整代码:
create.ts
/**
* BEM 命名规范工具
*
* @description
* Block:独立的组件块 (.block)
* Element:组件的子元素 (.block__element)
* Modifier:组件或子元素的变体 (.block--modifier, .block__element--modifier)
* State:组件的状态 (.is-state)
*
*/
// BEM接口定义
interface BEM {
(block?: string, element?: string, modifier?: string): string
b: (block?: string) => string
e: (element: string) => string
m: (modifier: string) => string
be: (block: string, element: string) => string
bm: (block: string, modifier: string) => string
em: (element: string, modifier: string) => string
is: (name: string, state: string | boolean) => string
}
/**
* 生成BEM规范的类名
* @private
* @param prefixName - 前缀名称
* @param block - 块名称
* @param element - 元素名称
* @param modifier - 修饰符名称
* @returns 生成的类名
*/
const generateBEM = (prefixName: string, block?: string, element?: string, modifier?: string): string => {
let className = prefixName
if (block) {
className += `-${block}`
}
if (element) {
className += `__${element}`
}
if (modifier) {
className += `--${modifier}`
}
return className
}
/**
* 创建BEM命名生成器
* @private
* @param prefixName - 前缀名称
* @returns BEM命名生成函数及其方法集合
*/
const createBEM = (prefixName: string): BEM => {
// 基础方法
const b = (block?: string): string => generateBEM(prefixName, block)
const e = (element: string): string => (element ? generateBEM(prefixName, '', element) : '')
const m = (modifier: string): string => (modifier ? generateBEM(prefixName, '', '', modifier) : '')
// 组合方法
const be = (block: string, element: string): string => generateBEM(prefixName, block, element)
const bm = (block: string, modifier: string): string => generateBEM(prefixName, block, '', modifier)
const em = (element: string, modifier: string): string =>
!element && !modifier ? '' : generateBEM(prefixName, '', element, modifier)
// 状态方法
const is = (name: string, state: string | boolean): string => (state ? `is-${name}` : '')
// 主函数
const bem = ((block?: string, element?: string, modifier?: string): string =>
generateBEM(prefixName, block, element, modifier)) as BEM
// 扩展方法
Object.assign(bem, {
b,
e,
m,
be,
bm,
em,
is
})
return bem
}
/**
* 创建命名空间
* @public
* @param name - 组件名称
* @param prefix - 组件前缀,默认为 'nc'
* @returns BEM命名生成器
*/
export const createNamespace = (name: string, prefix: string = 'nc'): BEM => {
const prefixName = `${prefix}-${name}`
return createBEM(prefixName)
}
// 类型导出
export type { BEM }
config.scss
$namespace: 'nc' !default;
$common-separator: '-' !default;
$element-separator: '__' !default;
$modifier-separator: '--' !default;
$state-prefix: 'is-' !default;
mixin.scss
@use 'config' as *;
@forward 'config';
// 创建 BEM 块级选择器
// $block - 块名称
@mixin b($block) {
$B: $namespace + $common-separator + $block !global;
.#{$B} {
@content;
}
}
// 创建 BEM 元素选择器
// $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;
}
}
}
}
// 创建 BEM 修饰符选择器
// $modifier - 修饰符名称
@mixin m($modifier) {
$selector: &;
$currentSelector: '';
@each $unit in $modifier {
$currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ','};
}
@at-root {
#{$currentSelector} {
@content;
}
}
}
// 创建状态类选择器
// $state - 状态名称
@mixin when($state) {
@at-root {
&.#{$state-prefix + $state} {
@content;
}
}
}
// 创建伪类选择器
// $pseudo - 伪类名称
@mixin pseudo($pseudo) {
@at-root {
&:#{$pseudo} {
@content;
}
}
}
// 检查是否命中所有特殊嵌套规则
@function hitAllSpecialNestRule($selector) {
@return containsModifier($selector) or containWhenFlag($selector) or containPseudoClass($selector);
}
// 检查选择器是否包含修饰符
@function containsModifier($selector) {
$selector: selectorToString($selector);
@return str-index($selector, $modifier-separator);
}
// 检查选择器是否包含状态类
@function containWhenFlag($selector) {
$selector: selectorToString($selector);
@return str-index($selector, '.' + $state-prefix);
}
// 检查选择器是否包含伪类
@function containPseudoClass($selector) {
$selector: selectorToString($selector);
@return str-index($selector, ':');
}
// 将选择器转换为字符串
@function selectorToString($selector) {
$selector: inspect($selector);
$selector: str-slice($selector, 2, -2);
@return $selector;
}
常见问题与解答
1. 为什么我的 SCSS 混入没有正确生成选择器?
可能的原因:
- 确保你正确导入了 mixin.scss 文件
- 检查是否正确使用了混入语法,例如
@include b(button)
- 确保 config.scss 中的配置变量与你的预期一致
2. TS 版 BEM 工具生成的类名与我手动写的不一样?
可能的原因:
- 检查传入的参数是否正确
- 确保你正确导入和使用了 createNamespace 函数
- 确保默认前缀与你的预期一致
3. 我应该选择哪种 BEM 实现方式?
这取决于你的具体需求:
- 如果你需要动态生成类名,或者需要在 JavaScript 中操作类名,选择 TS 版 BEM 工具
- 如果你主要关注样式的组织和可读性,选择 SCSS 混入
- 如果你想要最佳的开发体验,可以结合两种方式使用
4. BEM 命名是否会导致类名过长?
是的,BEM 命名确实会导致类名变长,但这是为了提高可读性和避免冲突的权衡。在实际项目中,类名的长度对性能影响很小,而良好的命名规范带来的可维护性收益远大于这一点小小的代价。
最后,希望这篇教程对你有所帮助,祝你的前端开发之旅愉快!