专栏首页与前端沾边[day-ui]DButton 组件和 DIcon 组件实现
原创

[day-ui]DButton 组件和 DIcon 组件实现

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

项目根目录创建 packages 文件夹

新建 button 文件夹

目录结构如下:

  • 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>

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 组件就好写了

同 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 目录中使用

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

配置组件 ts 类型

typings 目录结构如下:

主要考虑的是给组件定义 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. 这里创建服务空间, 阿里云目前免费的,存储大小也没有限制
  3. 点击进入存储空间
  4. 我们可以把需要的文件上传,(也可以免费部署你自己的网站,也不用自己去购买服务器)
  5. 配置参数中域名使用默认的就好
  6. 因为我们的文档地址部署在github上,所以访问我们的样式文件会有跨域,继续配置

配置文档

以下代码在 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 文件夹

以下代码在 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>

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

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

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • [day-ui] 组件库打包环境配置

    上一节我们书写了 button 和 icon 组件,单元测试和文档也都完成了,接下来我们把写好的库打包发布到 npm 上。之后我们建个小 vue3 的项目,安装...

    测不准
  • Angular-UI Bootstrap组件实现警报

    小编推荐:Fundebug专注于 JavaScript、微信小程序、微信小游戏,Node.js 和 Java 线上 bug 实时监控。真的是一个很好用的 bug...

    Fundebug
  • [day-ui] Affix 组件学习

    固钉组件是把页面某个元素相对页面 HTML 或者某个 dom 内定位显示,例如固定页面顶部/底部显示,页面宽高改变也会保持原位置。如果进行滚动,超过定义的范围就...

    测不准
  • ElementUI 实现头部组件和左侧组件效果

    https://www.cnblogs.com/xiao987334176/p/14434383.html

    py3study
  • 组件化实战——组件知识和基础轮播组件

    原文链接:https://juejin.cn/post/6986304993171079176/

    winty
  • Element-UI表格组件实现行拖拽排序

    运营小姐姐说想要可以直接拖拽排序的功能,原来在序号六的广告可能会因为金主爸爸加钱换到序号一的位置,拖拽操作就很方便

    前端黑板报
  • Android UI新组件学习和使用

    今天来学习总结一下,Android 后添加的一些新的组件和UI效果,Material Dialog,SwipeRefreshLayout,ListPopupWi...

    砸漏
  • [Vue源码剖析]如何实现组件

    官网上关于组件继承分为两大类,全局组件和局部组件。无论哪种方式,最核心的是创建组件,然后根据场景不同注册组件。

    娜姐
  • vue使用provide和inject实现父组件与子孙组件传值

    明知山
  • vue2.0实现分页组件

    用户1741436
  • vue实现分页组件

    分页需要的字段:当前页(curPage),每页大小(pageSize),总页数(total) 作为一个组件,所以以上这些参数最好是从父组件传递过来,可以如下定义...

    陨石坠灭
  • vue实现分页组件

    分页需要的字段:当前页(curPage),每页大小(pageSize),总页数(total) 作为一个组件,所以以上这些参数最好是从父组件传递过来,可以如下定义...

    陨石坠灭
  • vue实现弹框组件

    治电小白菜
  • Flutter | 使用 InkResponse和 InkWell组件 实现事件操作

    凌川江雪
  • 《精通react/vue组件设计》之5分钟实现一个Tag(标签)组件和Empty(空状态)组件

    本文是笔者写组件设计的第五篇文章,之所以会写组件设计相关的文章,是因为作为一名前端优秀的前端工程师,面对各种繁琐而重复的工作,我们不应该按部就班的去"辛勤劳动"...

    徐小夕
  • 使用微搭自定义组件实现搜索组件

    微搭作为一款低代码开发平台,最大的特点是拖拽化开发,我们只需要拖选组件就可以快速的完成页面的搭建。但在实际的开发过程中,官方提供的组件可能和业务还有一定的距离,...

    低代码布道师
  • –vue2.0父子组件及非父子组件间实现通信

    大象无痕
  • IOS小组件(6):小组件实现时钟按秒刷新

      上一节中我们了解了IOS小组件的刷新机制,发现根本没法实现按秒刷新,但是看别的App里面有做到,以为用了什么黑科技,原来是因为系统提供了一个额外的机制实现时...

    用户1155943
  • Angular 实现一个 Dialog 组件

    这里有一个细节是base-dialog的z-index一定要大于overlay的,已保证dialog能显示在遮盖层上方。

    mafeifan

扫码关注云+社区

领取腾讯云代金券