DButton 组件和 DIcon 组件实现

上一篇中我们已经把组件的基础架构和文档的雏形搭建好了。下面我们从最简单的 buttonicon 组件入手,熟悉下 vue3 的语法结构和组件的单元测试。看这篇文章前最好了解下 vue3 的语法和 compositionAPI,基本就能了解代码为何如此书写,和 vue2 有哪些不同。

项目根目录创建 packages 文件夹

新建 button 文件夹

目录结构如下:
DButton 组件和 DIcon 组件实现

  • index.jsbutton 组件入口文件,按需加载的入口,src 下是 button 的组件,tests 下是组件测试文件
src/index.vue 
dom 中的语法结构和 vue2 相同,通过传不同的参数,动态改变 class 名
<template>
  <button
    :class="[
      'd-button',
      type ? 'd-button--' + type : '',
      buttonSize ? 'd-button--' + buttonSize : '',
      {
        'is-disabled': disabled,
        'is-loading': loading,
        'is-plain': plain,
        'is-round': round,
        'is-circle': circle
      }
    ]"
    :disabled="disabled || loading"  // disabled 和 loading 时都不可点击
    :autofocus="autofocus"
    :type="nativeType"
    @click="handleClick"
  >
    <i v-if="loading" class="d-icon-loading"></i>
    <i v-if="icon && !loading" :class="'d-icon-' + icon"></i>
    <!-- v-if="$slots.default" 作用是防止span标签占位有个小距离 -->
    <span v-if="$slots.default"><slot></slot></span>
  </button>
</template>
<script>
import { computed, defineComponent } from 'vue'

export default defineComponent({
  name: 'DButton', // 注册的组件名
  props: {
    type: {
      type: String,
      default: 'default',
      validator: (val) => {
        return [
          'primary',
          'success',
          'warning',
          'danger',
          'info',
          'text',
          'default'
        ].includes(val)
      }
    },
    size: {
      type: String,
      default: 'medium',
      validator: (val) => {
        return ['', 'large', 'medium', 'small', 'mini'].includes(val)
      }
    },
    icon: {
      type: String,
      default: ''
    },
    nativeType: {
      type: String,
      default: 'button',
      validator: (val) => {
        return ['button', 'reset', 'submit'].includes(val)
      }
    },
    loading: Boolean,
    disabled: Boolean,
    plain: Boolean,
    autofocus: Boolean,
    round: Boolean,
    circle: Boolean
  },
  emits: ['click'], // 触发父组件方法,不写也可以,可以提示,也可以做校验
  setup(props, { emit }) { // 第二个参数 ctx 结构,这里面没有this
    const buttonSize = computed(() => {
      return props.size || 'medium'
    })

    const handleClick = (e) => {
      emit('click', e)
    }
    // dom 中用到的字段都要返回
    return {
      buttonSize,
      handleClick
    }
  }
})
</script>

button/index.js 注册组件

import DButton from './src/index.vue'
import '../../styles/button.scss'
// 如果是 ts 需要单独给 install 定义类型
DButton.install = app => {
  app.component(DButton.name, DButton)
}
export default DButton

packages/index.js 中获取所有组件进行注册导出

import DButton from './button'
import '../styles/index.scss'
const components = [DButton]

const defaultInstallOpt = {
  size: 'medium',
  zIndex: 2000
}

const install = (app, options = {}) => {
  components.forEach(item => {
    app.component(item.name, item)
  })
  // 全局注册默认数据
  app.config.globalProperties.$DAY = Object.assign(
    {},
    defaultInstallOpt,
    options
  )
}

export default {
  version: '1.0.0',
  install
}

export { DButton }

在 examples/main.js 中引入

import { createApp } from 'vue'
import App from './App.vue'
// 引入
import DayUI from '../packages'
const app = createApp(App)
// 注册
app.use(DayUI).mount('#app')

界面中使用

<d-button>按钮</d-button>

DButton 组件和 DIcon 组件实现

button 单元测试

我们在创建项目的时候就选择了使用 jest 测试,vue 中使用的是 vue-jest 库,配置文件在 jest.config.js 中。下面开始书写自己的单元测试

以下内容在 button/__tests__/button.spec.js 文件中

// 返回容器包含组件属性信息
import { mount } from '@vue/test-utils'
import Button from '../src/index.vue'

const text = '我是测试文本'

describe('Button.vue', () => {
  it('create', () => {
    const wrapper = mount(Button, {
      props: { type: 'primary' }
    })
    // 名称中包含
    expect(wrapper.classes()).toContain('d-button--primary')
  })

  it('icon', () => {
    const wrapper = mount(Button, {
      props: { icon: 'search' }
    })
    expect(wrapper.find('.d-icon-search').exists()).toBeTruthy()
  })

  it('nativeType', () => {
    const wrapper = mount(Button, {
      props: { nativeType: 'submit' }
    })
    expect(wrapper.attributes('type')).toBe('submit')
  })

  it('loading', () => {
    const wrapper = mount(Button, {
      props: { loading: true }
    })

    expect(wrapper.classes()).toContain('is-loading')
    expect(wrapper.find('.d-icon-loading').exists()).toBeTruthy()
  })

  it('size', () => {
    const wrapper = mount(Button, {
      props: { size: 'medium' }
    })

    expect(wrapper.classes()).toContain('d-button--medium')
  })

  it('plain', () => {
    const wrapper = mount(Button, {
      props: { plain: true }
    })
    expect(wrapper.classes()).toContain('is-plain')
  })

  it('round', () => {
    const wrapper = mount(Button, {
      props: { round: true }
    })
    expect(wrapper.classes()).toContain('is-round')
  })

  it('circle', () => {
    const wrapper = mount(Button, {
      props: { circle: true }
    })
    expect(wrapper.classes()).toContain('is-circle')
  })

  it('render text', () => {
    const wrapper = mount(Button, {
      slots: {
        default: text
      }
    })

    expect(wrapper.text()).toEqual(text)
  })

  test('handle click', async () => {
    const wrapper = mount(Button, {
      slots: {
        default: text
      }
    })
    // trigger 操作原生的 dom 事件
    await wrapper.trigger('click')
    console.log(wrapper.emitted(), '---')
    // expect(wrapper.emitted()).toBeDefined()
    expect(wrapper.emitted().click).toBeTruthy()
  })

  test('handle click inside', async () => {
    const wrapper = mount(Button, {
      slots: {
        default: '<span class="inner-slot"></span>'
      }
    })
    await wrapper.element.querySelector('.inner-slot').click()
    expect(wrapper.emitted()).toBeDefined()
  })

  test('loading implies disabled', async () => {
    const wrapper = mount(Button, {
      slots: {
        default: text
      },
      props: { loading: true }
    })

    await wrapper.trigger('click')
    // loading 时无法点击
    expect(wrapper.emitted('click')).toBeUndefined()
  })

  it('disabled', async () => {
    const wrapper = mount(Button, {
      props: { disabled: true }
    })
    expect(wrapper.classes()).toContain('is-disabled')
    await wrapper.trigger('click')
    expect(wrapper.emitted('click')).toBeUndefined()
  })
})
  • 执行命令 npm run test:unit

DButton 组件和 DIcon 组件实现

DButton 组件写完了,DIcon 组件就好写了

同 button 文件件新建 icon 目录

以下代码在 icon/src/index.vue 文件中
<template>
  <!-- 这里我是直接传的最后一位,如果跟其他保持一致,可以传整个名称  d-icon-name -->
  <i :class="`d-icon-${name}`"></i>
</template>

<script>
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'DIcon',
  props: {
    name: String
  }
})
</script>
以下代码在 icon/index.js 中
import DIcon from './src/index.vue'
import '../../styles/icon.scss'

DIcon.install = (app) => {
  app.component(DIcon.name, DIcon)
}

export default DIcon

pckages/index.js 中引入 icon 组件,小伙伴可自行添加,与 button 一致,两行代码

编写 icon 组件的测试文件

以下代码在 icon/__tests__/icon.spec.js 中

import { mount } from '@vue/test-utils'
import Icon from '../src/index.vue'

describe('Icon', () => {
  it('test', () => {
    const wrapper = mount(Icon, {
      props: {
        name: 'test'
      }
    })
    expect(wrapper.classes()).toContain('d-icon-test')
  })
})

在 examples 目录中使用

DButton 组件和 DIcon 组件实现

到这里主要的组件搭建就完成了,但是由于我们使用的 js 编写的组件库,如果你创建的项目是 ts 项目,那么下载安装 day-ui 后就会 ts 异常,所以我们需要编写 day-ui 的组件类型。

配置组件 ts 类型

typings 目录结构如下:
DButton 组件和 DIcon 组件实现
主要考虑的是给组件定义 install 方法,定义组件的 props 类型,类型的入口文件是 index.d.ts ,如下:

// 最终对外使用的入口文件类型
export * from './day-ui'
import * as DayUI from './day-ui'
export default DayUI

这里简单贴一个 button 组件的类型,详细的大家可以去 github 看下哈

import { DayUIComponent, DayUIComponentSize } from './component.d'

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

// native button type
export type ButtonNativeType = 'button' | 'submit' | 'reset'

// 写 props 的类型, 继承 install 方法
interface IButton extends DayUIComponent {
  // button size
  size: DayUIComponentSize
  // button type
  type: ButtonType
  // whether it's a plain button
  plain: boolean
  // whether it's a round button
  round: boolean
  // whether it's loading
  loading: boolean
  // disable the button
  disabled: boolean
  // button icon, accepts an icon name of element icon component
  icon: string
  // native buttion's autofocus
  autofocus: boolean
  // native button's type
  nativeType: ButtonNativeType
}

export const DButton: IButton

配置字体样式文件

上一节中我们已经把项目文档基本结构搭建完毕,我们只要把组件的配置添加进去即可。这里为了方便,我把 css 样式文件放到了云存储空间中,我试过 githubraw 方式,但是无法访问,所以我使用了 uniCloud 的云存储空间,也比较简单,下面简单介绍下:

  1. 登录 uniCloud web 控制台(当然如果你之前没用过 dcloud 的产品,可能需要认证)链接
  2. 这里创建服务空间, 阿里云目前免费的,存储大小也没有限制

DButton 组件和 DIcon 组件实现

  1. 点击进入存储空间

DButton 组件和 DIcon 组件实现

  1. 我们可以把需要的文件上传,(也可以免费部署你自己的网站,也不用自己去购买服务器)
  2. 配置参数中域名使用默认的就好

DButton 组件和 DIcon 组件实现

  1. 因为我们的文档地址部署在github上,所以访问我们的样式文件会有跨域,继续配置

DButton 组件和 DIcon 组件实现

配置文档

以下代码在 docs/.vitepress/config.js 中

// 这里修改是打包后引入的本地文件,我的文件放在了项目根目录,所以是 github 仓库名
const base = process.env.NODE_ENV === 'production' ? '/day-ui-docs' : ''
const { resolve } = require('path')

module.exports = {
  title: 'day-ui',
  head: [
    // 全局样式,引入样式文件
    [
      'link',
      {
        rel: 'stylesheet',
        href:
          'https://static-6e274940-2377-4243-9afa-b5a56b9ff767.bspapp.com/css/day-ui-style.css'
      }
    ]
  ],
  description: 'A Component For Vue3',
  // 扫描 srcIncludes 里面的 *.md文件
  srcIncludes: ['src'],
  alias: {
    // 为了能在demo中正确的使用  import { X } from 'day-ui'
    [`day-ui`]: resolve('./src')
  },
  base,
  themeConfig: {
    // logo: '../logo.svg',
    nav: [{ text: 'demo', link: '/math' }],
    lang: 'zh-CN',
    locales: {
      '/': {
        lang: 'zh-CN',
        title: 'day-ui',
        description: 'A Component For Vue3',
        label: '中文',
        selectText: '语言',
        nav: [{ text: '指南', link: '/' }],
        sidebar: [
          { text: '介绍', link: '/' },
          { text: 'Button 按钮', link: '/components/button/' },
          { text: '按钮组', link: '/components/buttonGroup/' },
          { text: 'Icon 图标', link: '/components/icon/' },
          { text: '常见问题', link: '/components/issues/' }
        ]
      },
      '/en/': {
        lang: 'en-US',
        title: 'day-ui',
        description: 'A Component For Vue3',
        label: 'English',
        selectText: 'Languages',
        nav: [{ text: 'Guide', link: '/' }],
        sidebar: [
          { text: 'Getting Started', link: '/en/' },
          { text: 'Button', link: '/en/components/button/' },
          { text: 'ButtonGroup', link: '/components/buttonGroup/' },
          { text: 'Icon', link: '/en/components/icon/' },
          { text: 'Issues', link: '/en/components/issues/' }
        ]
      }
    },
    search: {
      searchMaxSuggestions: 10
    },
    // 右上角打开的仓库地址
    repo: 'Bluestar123/day-ui-docs',
    repoLabel: 'Github',
    lastUpdated: true,
    prevLink: true,
    nextLink: true
  }
}

这里我们写下 icon 组件的文档,src 目录下新建 icon 文件夹
DButton 组件和 DIcon 组件实现

以下代码在 index.zh-CN.md 文件中,英文的大家自行解决了。。。
// 打包后的引用
---
map:
  path: /components/icon
---

# Icon 图标

提供了常用的图标合集

## 代码演示

### 基本用法
// 这里是做了 md 的源码解析,识别路径展示内容
<demo src="./demo/demo.vue"
  language="vue"
  title="基本用法"
  desc="i 标签直接通过设置类名为 d-icon-iconName 来使用即可。也可以直接使用 d-icon 组件,传入 name 属性">
</demo>

### 更多图标名称参考 element-plus

- [地址](https://element-plus.org/#/zh-CN/component/icon)

## Props

| 参数 | 说明 |   类型 |         值 |
| ---- | ---: | -----: | ---------: |
| name | 名称 | string | 例如'edit' |

index.vue 中的代码就是组件的代码,因为我们这里不下载包,所以就是组件源码。

以下代码在 demo.vue 中,这里大家可以随便写了

<template>
  <div>
    <i class="d-icon-edit"></i>
    <i class="d-icon-share"></i>
    <i class="d-icon-delete"></i>
    <d-icon name="setting"></d-icon>
  </div>
</template>

<script lang="ts">
import { DIcon } from 'day-ui'
import { defineComponent } from 'vue'

export default defineComponent({
  components: {
    DIcon
  }
})
</script>
<style lang="scss" scoped>
i + i {
  margin-left: 10px;
}
</style>

我们的文档就实现了,还能看到的我们引入的文件
DButton 组件和 DIcon 组件实现

DButton 组件和 DIcon 组件实现

下一节我们开始组件库打包环境配置,发布到 npm 上,如果那里写的有问题欢迎指正!如果对您有帮助的话欢迎评论转发!

上一篇:day-ui - Affix 组件学习


下一篇:快速了解链码是什么?