likes
comments
collection
share

完善工程环境开发第一个组件!

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

本文开始,后续的代码量将会快速增加,所以所有的代码将与文章同步至GitHub - longyanjiang/brain-ui: A Vue.js 3 UI Library for Web仓库,在上一节,我们建设了一个基础的组建演示并且整理了一个基础的样式项目结构,本节我们将开始完善这个组件,并且完成样式的匹配,我们上面写了一个基础组建,并且在example中可以进行实时调试了,但是写的还非常基础,我们先对这个button组件进行完善:

完善Button组件

我预先写好了一个基础组建,我们直接对其进行一个结构梳理讲解即可,这一类基础组建大家日常都可以随便封装,我们先了解其整个流程,我们先看看一个组件的基础目录:

.
├── button
│   ├── index.ts
│   └── src
│       ├── button.ts
│       ├── button.vue
│       └── interface.d.ts
├── button-group
│   ├── index.ts
│   └── src
│       └── button-group.vue
├── components.ts
├── index.ts
└── package.json

这是packages/components目录下面的整个结构,目前我写了两个组件,并且在index.ts中全部导出了,在编写一个组件前,我们需要做什么呢?

第一点我们需要定义组件需要接收的所有参数,也就是props并且对参数进行校验、第二点因为我们是TS编写的,所以我们需要为每一个参数定义结构,所以在上面的基础上,我们新增了一个interfae.d.ts的接口文件,一方面申明并导出类型,一方面供我们的props定义类型,我们参考Element-ui来看看button的组建需要哪些参数:

import { ExtractPropTypes, PropType } from 'vue'
import type { ButtonNativeType, ButtonSizeType, ButtonType } from './interface'


export const Props = {
  type: {
    type: String as PropType<ButtonType>,
    default: (): ButtonType => 'default',
    validator(value: ButtonType) {
      return (['default', 'primary', 'success', 'info', 'danger', 'warning'] as const).includes(value)
    }
  },
  size: {
    type: String as PropType<ButtonSizeType>,
    validator(value: ButtonSizeType) {
      return (['default', 'medium', 'small', 'mini', 'tiny'] as const).includes(value)
    }
  },
  plain: {
    type: Boolean,
    default: (): Boolean => false
  },
  round: {
    type: Boolean,
    default: (): Boolean => false
  },
  circle: {
    type: Boolean,
    default: (): Boolean => false
  },
  loading: {
    type: Boolean,
    default: (): Boolean => false
  },
  disabled: {
    type: Boolean,
    default: (): Boolean => false
  },
  icon: {
    type: String,
    default: (): String => ""
  },
  autoFocus: {
    type: Boolean,
    default: (): Boolean => false
  },
  nativeType: {
    type: String as PropType<ButtonNativeType>,
    default: (): ButtonNativeType => 'button',
    validator(value) {
      return (['button', 'submit', 'reset'] as const).includes(value)
    }
  },
}

export const Emits = {
  click: (evt: MouseEvent): MouseEvent => evt
}

export type ButtonProps = ExtractPropTypes<typeof Props>

这里使我们的button.ts文件,我们定义了所有的props参数并且对齐进行了校验,也给与了默认值,同时为每一个参数赋值了TS类型,除基础类型之外,一些复杂的类型我们从interface中获得,我们在interface定义的类型有这些:

import type { ButtonHTMLAttributes } from 'vue'

export type ButtonSizeType = 'default' | 'medium' | 'small' | 'mini' | 'tiny'

export type ButtonType = 'default' | 'primary' | 'success' | 'info' | 'danger' | 'warning'

export type ButtonNativeType = NonNullable<ButtonHTMLAttributes['type']>

总而言之,这两个文件,一个提供props参数校验,一个提供ts类型支持,然后将其props再给到button组件进行使用,button组件非常简单,我们就不过多讲解了,直接去看看组件代码:

<template>
    <button class="br-button" :class="classList"  :type="nativeType" :autofocus="autoFocus" :disabled="disabled || loading" @click="handlerClick"><slot></slot></button>
  </template>

<script lang="ts" setup name="BrainButton">
import { computed, defineEmits  } from 'vue'
import { Props, Emits } from './button'

const props = defineProps(Props)
const emits = defineEmits(Emits)

const classList = computed(() => {
  const { type, size, round, plain, circle, disabled, nativeType, autoFocus, icon, loading } = props
  return [
    {
      [`br-button--${type}`]: type,
      [`br-button--${size}`]: size,
      [`is-disabled`]: disabled,
      [`is-loading`]: loading,
      [`is-round`]: round,
      [`is-plain`]: plain,
      [`is-circle`]: circle,
    }
  ]
})
function handlerClick (evt: MouseEvent): void {
    emits("click", evt)
  }

</script>

这个组件大家都肯定接触过很多次了,就是拿到props根据不同的props参数绑定不同的类名即可,然后还有两个小的知识点:

  • 我们直接采用了setup script语法糖,所有东西不需要再像普通写法那样导出更加方便了,但是这样带来一个问题,这样的结构让我们定义不了组建的name属性,我们稍后对齐进行补充
  • 其二,在这种语法中,definePropsdefineEmits不需要再引入,可以直接使用

回到上面第一点,在serup script语法糖中,我们是不能直接给组建什么name的,如果不声明name,那么注册组件的时候,component.name就拿不到值,所以我们还是要解决这个问题:

处理setup script中无法直接定义name属性问题

  • 传统的export default defineComponent这种是可以直接定义属性的,所以我们可以另加一个script标签,然后定义name,这种方法能实现但是不优雅,两个script看起来比较笨重,不推荐

  • 第二种我们可以借助一个插件unplugin-vue-define-options,安装完之后在vite.config.js中配置这个插件即可,配置也非常简单:

    • import { defineConfig } from 'vite'
      import vue from '@vitejs/plugin-vue'
      import DefineOptions from 'unplugin-vue-define-options/vite' 
      
      export default defineConfig({
        plugins: [vue(), DefineOptions()],
      })
      
    • 配置完之后,我们就可以在组建中使用defineOptions进行申明了,这样就可以定义组件名称了,element-plus就是这样做的,defineOptions在使用前同样需要在ts中进行申明
    • defineOptions({
        name: 'BrainButton',
      })
      
  • 第三种我们依然是借助插件vite-plugin-vue-setup-extend,其配置和上一个插件一抹一样,引入插件加使用即可:

    • import { defineConfig } from 'vite'
      import vue from '@vitejs/plugin-vue'
      import VueSetupExtend from 'vite-plugin-vue-setup-extend'
      
      export default defineConfig({
        plugins: [vue(), DefineOptions()],
      })
      
    • 与之不同的则是,使用这个插件之后,要为组建添加名称属性更为方便,我们只需要在setup script标签之间加上name属性即可,无需增加其他的任何配置,推荐使用这种方式。
    • <script lang="ts" setup name="BrainButton">
      

这里是对setup script语法糖添加name属性的一个额外说明,不仅仅在组件库开发中,日常的开发我们一样需要使用到,当然,你也可以选择不使用setup script语法糖,使用传统方式也是可以的,那么到这里,我们已经编辑完成了一个基础组建了,接下来该写样式了。

组建配套样式开发

在此之前,我们已经大致了解了基础BEM规范的样式开发,所以接下来代码都将在子项目theme-chalk中进行开发,同样,我们直接来看本章节已经编写完成的代码实例中的项目结构先进行简单分析

├── package.json
└── src
    ├── button-group.scss
    ├── button.scss
    ├── common
    │   ├── color.scss
    │   ├── transition.scss
    │   └── var.scss
    ├── index.scss
    └── mixins
        ├── _button.scss
        ├── config.scss
        ├── function.scss
        ├── mixins.scss
        └── utils.scss

这是我们的一个子项目,我们的所有代码都写在src下,之前我们已经对目录进行了一些简单说明,针对于这个文件,我们再进行一些不传,

  • 根目录下的文件夹都是公共需要的方法变量动画类工具类主题色等等公共配置,
  • 非文件夹的文件就是每一个组件所有样式,基本每一个组建都会去引用公共的配置,变量,方法来完成样式建设,这样后期的变更只需要变更配置文件即可
  • 根目录的index.scss将导出所有文件,我们可以在使用的时候全量加载,也可以按需引入单独的每个组建样式

了解完目录之后,我们开始编写,在编写前我们需要知道一个组件库对于样式除了遵循BEM规范之外,我们还需要有设计的一套规范,比如不同的尺寸如何定义不同的主题色如何定义不同尺寸下的大小字体为多少等等问题是我们需要考虑的。

因为如果是个人开发,很难得到UI的支持,这里的样式如果我们从头开始可能耗费大量的时间,所以在本期专栏中,我们先直接使用element-ui的样式,作为你的个人组件库,我们先基于其样式开发出来,后续自己再在这样的基础上进行改动,在现有已经很长时间沉淀中,这类知名开源项目的规范我们拿来使用就没有过多的负担,非常省心。

但是在使用的过程中,我们来揭秘一下各类的设计都是如何完成的,按照我们需要的步骤来进行吧。

一般在定义样式之前,我们会有几个公告变量配置,一般包含命名空间类名之间的连接符状态类型的prefix前缀。这些在各个开源组件库中你都可以看到,一般放置在config.scss当中,我们的配置如下:

$namespace: 'br';
$element-separator: '__';
$modifier-separator: '--';
$state-prefix: 'is-';

命名空间一般自己定义,我们选择brain的前两个字母,然后就是两个不同的连接符,在之前有讲到其不同的含义。正常来讲,接下来我们需要定义各类mixins方法utils方法function自定义方法主题变量等等,出于方便理解考虑,我们反向来,直接复制到element-uibutton.scss样式,然后对齐进行简单分析。

由于样式文件过多,我们只对其一些方法进行分析,具体代码自行到github仓库下查看,在样式编写前,我们肯定会引入所有的公共变量,方法,等等,但是这里我们需要注意一点,看看其头部引入:

@import "common/var";
@import "mixins/button";
@import "mixins/mixins";
@import "mixins/utils"

全部采用了@import的方式引入的,但是在现在的sass中,我们一般推荐使用@use引入,我们稍微了解下有何不同.

@import与@use引入文件的不同

在现在而言,我们其实在有些时候可以看到警告,已经推荐使用@use了,并且说明后续@import 将会在接下来逐渐淘汰,要清楚为什么这样,我们就得先知道@import的缺点了

sass@import规则能够导入sass和css样式表,提供mixin函数变量等等的访问,并将多个样式表进行组合覆盖到一起,但是这就造成了,这些东西都可以被全局访问,这样使用起来很难明确的知道其来源是哪里,并且如果类名或者变量名相同,会造成覆盖的情况,出现不可预知的错误,因为是全局的原因,所以必须为所有成员加上前缀,同时@extend的这种继承也是全局的,这就造成很难知道哪些内容会被这些规则影响到,基于这种情况,每次@import都会重复引入,造成臃肿的输出等等问题,所以逐渐被官方遗弃

说完@import的缺点,我们来说说@use的优点,首先我们知道上面的这种方式会重复引入,那么@use的第一个优点就是不会重复引入,只会引入一次,并且因为它的原型产生了模块化的概念,假如我们有a.scssb.scss两个文件,并且之中都有一个变量,$color-dange,但是在两个不同的文件中,相同变量的颜色却不同,在之前的形式中,后引入的文件将会覆盖掉前者,而在@use中,我们可以这样使用:

@use 'a.scss' as a;
@use 'b.scss' as b;

这样我们在使用的使用就可以a.$color-dangerb.$color-dange这样的形式去使用不同模块的相同变量名,如果你确定自己没有重复的命名,想要保持之前的那种写法,不想在前面加上模块名,我们可以这样引入即可:

@use 'a.scss' as *;
@use 'b.scss' as *;

这是一些对于二者不同的改动,可以去往scss官网查看更多,好了我们继续。

button组建样式解析

在我们去阅读button组件的时候,在使用到什么方法、变量、或者自定义函数时我们再去认识他和定义并引入他:

我们看到的第一个混入方法是@include utils-user-select(none);,进入查看代码是

@mixin utils-user-select($value) {
  -moz-user-select: $value;
  -webkit-user-select: $value;
  -ms-user-select: $value;
}

非常简单的一个util方法,就是给当前dom设置不允许复制user-select,作为按钮内容当然不能被复制了。

第二个混入方法是@include button-size($--button-padding-vertical, $--button-padding-horizontal, $--button-font-size, $--button-border-radius);

@mixin button-size($padding-vertical, $padding-horizontal, $font-size, $border-radius) {
  padding: $padding-vertical $padding-horizontal;
  font-size: $font-size;
  border-radius: $border-radius;
  &.is-round {
    padding: $padding-vertical $padding-horizontal;
  }
}

传入了四个变量,分别是padding的上下距离 左右距离 字体大小 圆角等四个属性,我们通过这个方法就基本定义了按钮尺寸,对于不同尺寸,我们修改四个变量即可

第三个方法是mix方法,那么这是一个自带的方法,用于颜色混合的,作用呢也很简单,在按钮点击或者划过的时候更改颜色

第四个点呢就是下吗这一个选择器

& [class*="-icon-"] {
    & + span {
      margin-left: 5px;
    }
  }

当遇到按钮里面有icon图标的时候对其进行一定位置平移

第五个方法是用于处理不同状态的方法@include when(plain)`,具体实现是:

@mixin when($state) {
  @at-root {
    &.#{$state-prefix + $state} {
      @content;
    }
  }
}

at-root呢就是将当前的样式编译到使用地方的同层级,如果不加就是嵌套关系,其次呢#{}我们前面讲过就是模板拼接,我们只就定义了这个前缀是is-所以就是拼接一个类名而已,@content内就是内容继承了,引用这个方法下面的所有样式将会出现在这里。

后面还有一些方法是@include m(primary)each循环等等其实都大同小异,这里我们暂时就先不麻烦去自己写这些样式了,全部copy过来,后续自己自由更改吧,然后呢我们再暂时将var.scss中的变量全部也复制过来,其他用到的方法呢我们就用到什么定义什么,将这些样式处理完之后我们需要做一些改变:

将@import改为@use

我们之前已经讲过了,现在的官方都已经不建议使用**@import**了,所以我们也需要替换,我们之前讲到了,要维持之前的想法,也就是不带模块xxx.变量名这种形式我们就要这样引用,比如:

@use './common/var.scss' as *;

as *即可,看起来似乎已经完成了,但是此时如果我们去进行实时编译测试时候就会出现下面的错误:[plugin:vite:css] [sass] Undefined variable.会告诉我们变量未定义,可是我们不是已经引入过了么,打开文档,我们可以看到有这样一个命名规则:

完善工程环境开发第一个组件! 当以-或者_开头的变量会被认为是私有变量,私有变量外部无法访问,所以我们当然读不到了,而刚好Element-ui就全是这样的开头,所以直接复制来被认为是私有变量了,我们拿不到。两种方法解决这个问题,第一种就是还是使用@import,但是很明显官方不推荐了,所以我们曲线救国,直接整页替换,将所有的$--替换为$即可,替换完样式我们就算是完成了button组件的样式编写,当然,实际开发中的样式是我们自己来写的,这里的设计作为前端开发的我而言觉得还是有些困难,所以我们可以在实际开发的时候依然去参考开源项目去规范,尺寸、主题、规范等等,当然如果是公司内部使用,可以让你们的设计师来做这些东西,毕竟术业有专攻。

当然我们也可以做一些基础的调整,比如选用一套自己喜欢的主题色,相信这个小小的要求完全可以找到ui小姐姐帮你实现,这里呢,为大家提供一份,也是我们本次要使用的主题色,后续更多的改变呢我们后续详细再说,关于样式的具体处理,后续在我们开发更为复杂的组建的时候我们再来自己慢慢实现:

完善工程环境开发第一个组件!

接下来呢就是导出样式,在src下的index我们会导出所有的样式,在之前已经讲过了,按需引入就引入单个的,全量使用就引入index.scss即可,我们将其导入之后就可以开始去测试我们的组建了。

引入注册组件

在上一节中,我们去引入组件是单个的,是这样引入的

import button from '@brain-ui/components/button'

这样引入,每次一个组件单独引入比较麻烦,我们将其全量引入吧,要实现这样,我们就得需要一个统一的组件导出接口,在上一节中,我们每个组建下面都有一个index.ts文件为组件添加了install方法并将其导出,我们只需要在根目录统一引入所有组件并且注册即可。

/ conponents.ts **/
export { BrainButton } from './button'
export { BrainButtonGroup } from './button-group'

我们在一个文件下统一导出,并在另一个地方进行统一整理即可:

import type { App } from 'vue'
import * as components from './components'
import { version } from './package.json'

const install = function (app: App) {
    Object.entries(components).forEach(([key, value]) => {
        app.component(key, value)
    })
}

export default {
    install,
    version
}

vue使用插件,是需要我们提供一个install方法的,要一次注册所有组件呢就相当于在install方法注册所有,我们定义它并在内部循环注册一下所有组件就ok了,此时我们回到example全量引入测试一下。

当然这一步在实际开发中应该是先于组件开发编辑的,毕竟我们在开发中需要实时调试,我们本期场景特殊,但是小伙伴么自己开发的时候需要注意。

测试组件

这里的测试组建呢并不是写测试用例,我们还没到这个步骤,我们就将组件引入到我们的开发环境里面进行一下预览,先全量导入组件吧:

import '@brain-ui/theme-chalk/src/index.scss'
import BrainUi from '@brain-ui/components

app.use(BrainUi)

这样就全量引入了,我们直接在app页面使用测试一下

<template>
  <BrainButtonGroup>
    <BrainButton size='small' @click="hanlderClick"> button </BrainButton>
    <BrainButton size='small' type='primary'> button </BrainButton>
    <BrainButton size='small' type='success'> button </BrainButton>
    <BrainButton size='small' type='danger'> button </BrainButton>
    <BrainButton size='small' type='warning'> button </BrainButton>
    <BrainButton size='small' type='info'> button </BrainButton>
  </BrainButtonGroup>

  <br/>
  <BrainButton >按钮默认尺寸</BrainButton>
  <BrainButton size="medium">按钮中等尺寸</BrainButton>
  <BrainButton size="small">按钮小尺寸</BrainButton>
  <BrainButton size="mini">按钮极小尺寸</BrainButton>
  <BrainButton size="tiny">按钮超小尺寸</BrainButton>
  <br/>

  <BrainButton plain type="primary">朴素按钮</BrainButton>
  <BrainButton round type="primary">椭圆</BrainButton>
  <BrainButton disabled type="primary">禁止</BrainButton>
  <BrainButton circle type="primary"></BrainButton>
</template>

我们来看看实际效果吧:

完善工程环境开发第一个组件!

可以看到基本没什么问题,因为毕竟这个组件确实比较基础,当然我们还缺少一点功能,缺少icon图标,导致我们不能loading和自定义icon,因为我们还没开发这个组件,后续呢,我们加上这个组件再来完善它,之前在vue2 当中基本都是使用的字体图标库,发现现在的vue3的组件库都使用了svg类型icon图标,基于这两类图标库呢,后续我们分别来实现,本文就先跳过此处了。

好了到了本节,我们已经搭建了一个基础的开发环境并且可以编码开发一个组件了,但是离完成一个组件库的任何还是任重而道远,并且我们当前的很多内容看起来还有些臃肿,我们也会在后续慢慢更新,我们的每一节内容我都希望能讲解的比较详细,这样方便你自己开始的时候少走弯路。

下一节内容我们会会当前的项目进行一些基础环境的完善升级,一个大型项目一定是多人维护的,每个人有不同的编码习惯,编码风格,甚至不同的设备也是如此,我们需要对其项目进行统一,比如,编码格式编码规范提交规范等等规范,而且对于传统项目和ts项目其实也有异同,在为完善项目的同时为大家讲解其中的实现步骤。

往期回顾