上一篇中我们已经把组件的基础架构和文档的雏形搭建好了。下面我们从最简单的 button
和 icon
组件入手,熟悉下 vue3
的语法结构和组件的单元测试。看这篇文章前最好了解下 vue3
的语法和 compositionAPI
,基本就能了解代码为何如此书写,和 vue2
有哪些不同。
项目根目录创建 packages 文件夹
新建 button 文件夹
目录结构如下:
index.js
是button
组件入口文件,按需加载的入口,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
样式文件放到了云存储空间中,我试过 github
的 raw
方式,但是无法访问,所以我使用了 uniCloud
的云存储空间,也比较简单,下面简单介绍下:
- 登录 uniCloud web 控制台(当然如果你之前没用过 dcloud 的产品,可能需要认证)链接
- 这里创建服务空间, 阿里云目前免费的,存储大小也没有限制
- 点击进入存储空间
- 我们可以把需要的文件上传,(也可以免费部署你自己的网站,也不用自己去购买服务器)
- 配置参数中域名使用默认的就好
- 因为我们的文档地址部署在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
上,如果那里写的有问题欢迎指正!如果对您有帮助的话欢迎评论转发!