likes
comments
collection

组件库开发之基础样式环境架构

作者站长头像
站长
· 阅读数 16

本节我们正式进入组件库的编写环节了,我在此之前,我们已经对基础的目录结构和Monorepo进行了开发,现在开始,我们开始进入到packages目录下的components进行第一个组件的开发把,然后将我们开发的组件引入到我们开始所开发的本地调试项目当中去,实时查看我们所开发的组件。

我们先简单的编写一个组件,在编写一个组件之前,我们要考虑一下目录的结构,这里呢我们同样参考Element-plus

组件库开发之基础样式环境架构 注意看目录结构是packages/components/button/__tests__ src style index.ts,一个完整的组件大概的目录是这样的,一个test用来写单元测试的、一个src是资源目录里面就是所有组件代码、一个style样式文件,和一个index.ts用于对文件的导出,同时src 目录下放置了我们代码组件和Ts的类型,也就是我们组件所用到的PropsType ,那么我们也来按照这个目录创建文件,然后按照这个结构先来编写一个简单的Button组件把。

组件基础开发

首先我们先来定义一点简单的Porps来进行简单的测试,后续我们再对齐就行补全

/* types.ts 所有组件下放置要给 types.ts 用于组件的接受参数的类型定义 */
import { ExtractPropTypes } from 'vue'
export const ButtonType = ['primary', 'success', 'info', 'warning', 'danger','text']
export const ButtonSize = ['large', 'medium', 'samll', 'mini']
export const buttonProps = {
  type: {
    type: String,
    validator(value: string) {
      return ButtonType.includes(value)
    }
  },
  size: {
    type: String,
    validator(value: string) {
      return ButtonSize.includes(value)
    }
  }
}
export type ButtonProps = ExtractPropTypes<typeof buttonProps>

我们可以将propsts所需要的类型一起定义在这个文件,需要注意的是buttonProps并不能直接用于type,需要使用vue自带的ExtractPropTypes进行包装,导出的才能是真正的类型,其次我们再对组件的进行一点简单的测试:

<template>
  <button>
      <slot>{{p.size}}||{{p.type}}</slot>
  </button>
</template>

<script lang="ts">
import { computed, defineComponent } from 'vue'
import { buttonProps } from './types'

export default defineComponent({
    name: 'BrainButton',
    props: buttonProps,
    setup(props){
        const p = computed(()=>{
            const { size= 'meduim', type = 'text' } = props
            return { size, type }
        })
        return { p  }
    }
})
</script>

组件导出

我们暂时不去做什么操作,我们就简单实验一下组件传参是否正常,所以我们需要将组件引入到,我们的example当中,用于实时查看效果调试使用,我们可以直接引入这个组件,但是很明显不方便,正常来讲,我们都需要在每个组件下定义一个index.ts文件用于导出我们的不同组件,所以,我们在src下创建这个文件并且将这个组件导出:

import Button from './src/button.vue'
export default Button ;

简单来看,我们似乎两步就搞定了,但是实际的使用过程中一般组件库除了可以直接引入组件外,有时也会使用插件的方式注册组件,想要插件的形式使用组件也很简单,我们只需要为组件提供一个install方法即可,很多同学应该自己去编写过组件,一般给组件加一个方法我大概是这样操作的:

import Button from './src/button.vue'
import type { App } from 'vue';
Button.install = function(app: App){
  app.component(Button.name, Button)
}
export default Button;

这和我们日常去添加install方法一样看起来十分简单,首先我们这里可以了解一个知识点,import type {App} from vue 这样的方法导入的意思表示只导入类型,而不是导入值,是一种优化的写法,其次,实际这样是并不能使用,为什么呢?

我们将组件引入到example当中,并且以插件的形式使用就会发现下面这个错误:

组件库开发之基础样式环境架构

可以看到use并不能使用这个组件,因为这个需要传入一个类型为Plugin_2类型的参数,很明显,我们的组件并不符合这个规范,所以并不像我们日常写js那样直接赋值,在ts中,这些参数是都需要这些类型满足才能正常使用的,所以我们为组件去添加install的方式就需要一点改变,如果你看过一些开源组件库,会发现,在导出组件的过程中,一般都会去自己封装一个withInstall方法为组件添加这个方法,同时因为我们最开始也定义了一个utils的模块,所以我们也来实现这样一个方法,让我们每次导出组件的时候,为其添加上install方法:

import type { App, Plugin } from 'vue'

export const withInstall = <T, E extends Record<string, any>>(main: T, extra?: E) => {
    (main as SFCWithInstall<T>).install = (app): void => {
        for (const comp of [main, ...Object.values(extra ?? {})]) {
            app.component(comp.name, comp)
        }
    }

    if (extra) {
        for (const [key, comp] of Object.entries(extra)) {
            ; (main as any)[key] = comp
        }
    }
    return main as SFCWithInstall<T> & E
}

这个方法的实现形式有很多,目的呢也都是为组件添加一个install方法,这里的方法我直接使用了element-plus当中提供的方法,我们主要是为了知道这个方法是干嘛的,知道即可,要实现也有多种方法,在有了这个方法时候,我们所有的组件导出只需要这样即可:

import button from './src/button.vue'
import {withInstall} from '@brain-ui/utils'
export const BrainButton =  withInstall(button)
export default BrainButton;

之后的组件使用这个方法,就相当于添加了install方法,从而可以实现插件使用了,此时我们去到example,引入组件之后在页面先随便传参,看看是否可以拿到参数:

<template>
  <BrainButton size='small' type='aaa'/>
</template>

组件库开发之基础样式环境架构

我们可以看到,我们已经成功拿到了参数,那么此时我们已经学会了基础的组件props定义type定义,实现了组件的导出和插件形式的使用,并且可以在线实时调试了,此时我们开始下一个问题,组件样式问题

组件样式处理

我们知道,组件库的使用一般都分为全量引入和按需引入,所以,如果我们把所有样式写在组件内,然后相互引用会变得麻烦,同时,组件的样式会有非常多的公共的样式,可能会通过预处理器去实现不同的函数组合,并且我们为了整体的规范以及后续的维护,大多数情况下,我们都需要通过定义变量来完成整体的组件库样式的设计,基于这种场景,我们需要使用到样式的一种规范,我们称这种规范为BEM规范

什么是BEM规范

BEMBlock Element Modifier,块元素修饰符)方法是 CSS 类的命名约定,旨在通过定义命名空间来解决范围问题来使 CSS 更具可维护性。

它原则上建议为独立的 CSS 类命名,并且在需要层级关系时,将关系也体现在命名中,这自然会使选择器高效且易于覆盖。

这样看起来不是很容易理解,我们来一段html来实际看看:

<button class="br-button br-button--info is-circle">
    <i class="br-icon-delete"></i>
</button>
  • br就是一个命名空间,所有的样式前缀都会带上。
  • .br-button 封装一个独立的实体,它本身是有意义的。虽然块可以嵌套并相互交互,但在语义上它们是相等的;没有优先级或等级制度。
  • .br-button--info 块的一部分,没有独立的意义。任何元素在语义上都与其块相关联,用双中划线链接让后续的info表示一种类型,不同的按钮类型。
  • .is-circle 块或元素上的修饰符。使用它们来改变外观、行为或状态,此处就表示这是一个状态为infocircle圆形按钮。

我们来看看element-ui中的例子,比如下图这个按钮,我们可以明显通过类名语义化的了解到当前按钮的主要信息。

组件库开发之基础样式环境架构

一般来讲,第一层就是我们的语义化名称,其次后面可以通过双下划线或者双中划线来进行定义,我们称之为修饰符,比如上面双下划线进行定义修饰位置,双中划线修饰状态为是否选中,在有些场景也会使用is-checked类似的is语法修饰状态,这里的BEM规范介绍的会比较简单,有兴趣的同学建议去查看更多文档来详细看看,我在这里不做过多的说明,因为在后续的实战中,我们可以更快的去了解其具体的组织形式,或者参考一些开源组件库的命名,你就可以发现这些规范的优点所在,这些在element-ui中都有所体现,所以接下来我们以这种方式来设计一下样式的组织:

样式整体设计

当我们面对这样一个大型工程的时候,第一时间依然要想到工程化,所以,我们依然要对可以抽离的文件进行抽离,在我们初级搭建工程的时候,我们定义了一个模块theme-chalk专门用于样式的处理,所以我们在这个文件下进行操作,我们首先应该想到我们需要哪些东西呢:

  • 定义一套全局变量供所有组件使用
  • 定义一整套mixin方法将公共样式逻辑进行封装整理
  • 定义好命名空间,防止组件库的样式与外层命名冲突

基于此我们对theme-chalk做一个基本的项目结构并分析为什么这样设计,我们先来看看element-ui的目录结构吧,在element-ui中,他的样式是单独写的一个组件,但是其目录结构和我们所需要是相似的,来看看它的设计吧:

组件库开发之基础样式环境架构

可以看到一个公共文件,一个fonts放置字体图标,一个mixins放置抽离的方法,其他的呢就是不同的组件单独的一个样式文件,大致就是这个样子,再来看看我们的结构:

theme-chalk
├── src
│   ├── common
│   │   ├── transition.scss
│   │   └── var.scss
│   ├── font
│   ├── mixins
│   │   ├── config.scss
│   │   ├── function.scss
│   │   └── mixins.scss
│   └── button.scss
└── package.json
  • 我们在common文件下放置全局动画,和我们定义的所有变量
  • fonts下放置字体和字体图标
  • mixins下放置配置文件,比如命名空间,连接符,prefix等等
  • mixins文件下定义所有公共的样式方法和一些自定义函数用于所有组件使用
  • 然后在根目录对应一个组件一个样式文件即可

这样的结构看起来非常清晰,类似element-ui的主题更换实际就是覆盖var.scss中的变量,要实现主题换肤就会非常简单,同时,使用统一的mixins方法,已经统一的命令空间、连字符等等都让后续的维护变得更加简单了。

此时我们已经学会通过BEM规范来组织我们的组件库样式结构了,接下来我们进行实战,但是在实战前,再来为大家回顾一些基础的scss知识点,在后续我们将会使用到,方便在后续的使用中,能更加清晰的知道这些方法的便利之处。

scss基础语法回顾

  • 在scss中,我们使用$+变量名:变量来定义一个变量
  • 在变量后加入!default什么为默认变量,在后续可以对其进行赋值覆盖操作
  • 全局变量是元素外申明的变量,反之局部变量是元素里申明的变量,局部可以覆盖全局
  • 我们可以为局部变量后添加!global将其变成为全局变量
  • 变量嵌套引用,在这里也可以用字符串插值,类似模板语法,用#{}来进行包裹
  • 我们可以使用&符来引用父元素,或者嵌套的情况,可以简写很多代码量
  • 和js类似,我们也可以使用@extend标识符来继承一个类的样式,这在复用共有代码将会变得非常方便
  • scss拥有%占位符,这类代码不被@extend调用继承就不会被编译
  • 类似vue,通过标识符@mixins混合命令定义的样式,可以通过@include引入直接使用,可以将公共样式抽离出来,在不同的地方调用即可。同时类似函数,这个过程是可以传入参数的。
  • scss支持函数式方法,例如:for循环while循环each in循环等等
  • 我们可以通过标识符@function来自定义一个函数,在调用中传参即可,

这是一些scss的基础语法,也是后续我们必须用到的知识,我们先对其不同的语法是可以干嘛的有个初步印象,此时我们可以打开elment-ui的theme-chalk项目看看,我们上述所提到的所有语法在其中都有体现。

到此为止,我们已经学会了一个组件库的样式结构,和使用scss来开发一个这样的项目所组织的一个项目结构,但是直接开发编码会导致有些地方不和文章同步造成不容易理解的地方,所以,所有的代码将会和文章同步,所有用到的东西都会在文章中进行体现,好让后续 的阅读更加轻松。

专栏的后续所有代码将同步到longyanjiang/brain-ui · GitHub仓库,有任何问题可以在项目中提issues,后续将同步专栏文章持续更新项目,最终开发出一个配套的工程化项目,下一期我们将完善button按钮,并通过BEM规范去单独书写scss样式,抽离掉所有公共变量,定义一些公共mixins方法,让我们目前的项目进一步完善。

历史回顾