likes
comments
collection
share

如何开发一款前端UI组件库??

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

写在最前,很久之前用vue3+ts+vite开发了一款前端UI组件库,工作很忙没有太多精力去维护扩展,今天补个教程,最主要的是其中开发思路。

在线预览:unicorn-zbf.gitee.io/zealousui

源码:gitee.com/unicorn-zbf…

看完本章你将会学会:

  • 如何封装通用组件
  • 如何实现代码预览功能
  • 如何发布一个npm包
  • 如何部署Gitee or Github站点(静态网页)

一、创建demo

//npm、yarn or pnpm
yarn create vite my-baseui --template vue

//创建好后启动项目
yarn
yarn dev

二、梳理文件结构

//在根目录下创建packages文件夹,目录结构如下:
`packages/components`: 存放组件。
`packages/styles`: 存放全局样式和组件样式。
`packages/index.ts`:注册和导出组件
`packages/hooks`:拖拽的函数
`packages/iconfont`:字体
`packages/utils`:事件总线bus和其他公共方法

如何开发一款前端UI组件库??

三、封装按钮组件

1.首先是按钮属性的封装,属性通过props传递,按钮的type,size,circle,disable,loading,round属性,这些属性通过computed包装成一个数组然后给button动态绑定class,这样对应属性的样式scss样式文件写好引入button文件,在就能动态渲染到按钮上面了。

2.其次是注册组件,组件同级的ts文件下导入button组件,然后给button一个install方法,方法里面注册button组件,app.component(zButton.name,zButton),然后export default zButton暴露出去,最后在入口文件处,导入zButton,再app.use(zButton),这是按需导入,如果是全局导入的话,最好在组件库的文件夹下创建一个批量注册组件的ts文件,遍历注册全局组件后再导出,main.ts里面引入,再app.use即全局导入组件。

//在packages/components下创建button文件夹
`packages/components/button/doc/demo{n}`: 使用组件。
`packages/components/button/doc/doc.md`: md预览文件。
`packages/components/button/index.ts`: 用来把写好的组件暴露出去。
`packages/components/button/index.vue`: 封装的通用button组件。

如何开发一款前端UI组件库??

1.准备style文件

如何开发一款前端UI组件库??

// `styles/common/base.scss`组件库的主要颜色
$primary: #57a3f3;
$success: #19be6b;
$info: #909399;
$warning: #f90;
$error: #ed4014;
// `styles/components/button.scss`组件的样式

@import '../common/base.scss';
.z-button {
  display: inline-block;
  padding: 12px 20px;
  line-height: 1;
  border-radius: 8px;
  border: 1px solid #d9d9d9;
  box-sizing: border-box;
  background: #fff;
  color: #333;
  white-space: nowrap;
  outline: none;
  font-size: 14px;
  cursor: pointer;

}


.z-button--default {
  border: 1px solid #d9d9d9;
  background: #fff;
  color: #333;

  &:hover {
    opacity: 0.8;
    border-color: $primary;
    color: $primary;
  }
}

.z-button--primary {
  color: #fff;
  background-color: $primary;
  border-color: $primary;

  &:hover {
    opacity: 0.8;
    color: #fff;
  }
}

.z-button--success {
  color: #fff;
  background-color: $success;
  border-color: $success;

  &:hover {
    opacity: 0.8;
    background-color: $success;
    color: #fff;
  }
}

.z-button--error {
  color: #fff;
  background-color: $error;
  border-color: $error;

  &:hover {
    color: #fff;
    border-color: $error;
    opacity: 0.8;
  }
}

.z-button--info {
  color: #fff;
  background-color: $info;
  border-color: $info;

  &:hover {
    color: #fff;
    border-color: $info;
    opacity: 0.8;
  }
}

.z-button--warning {
  color: #fff;
  background-color: $warning;
  border-color: $warning;

  &:hover {
    color: #fff;
    border-color: $warning;
    opacity: 0.8;
  }
}

.z-button--text {
  border-color: transparent;
  color: #409eff;
  background: transparent;
  padding-left: 0;
  padding-right: 0;

  &:hover {
    border-color: transparent;
    opacity: 0.8;
  }
}

// 禁止
.is-disabled {
  color: #fff;
  cursor: not-allowed;
  background-color: #fff;
  border-color: #ebeef5;
}

.z-button--default.is-disabled {
  color: #c0c4cc;
  cursor: not-allowed;
  background-color: #fff;
  border-color: #ebeef5;
}

.z-button--primary.is-disabled {
  color: #fff;
  background-color: #a0cfff;
  border-color: #a0cfff;
}

.z-button--success.is-disabled {
  color: #fff;
  background-color: #b3e19d;
  border-color: #b3e19d;
}

.z-button--error.is-disabled {
  color: #fff;
  background-color: #fab6b6;
  border-color: #fab6b6;
}

.z-button--info.is-disabled {
  color: #fff;
  background-color: #c8c9cc;
  border-color: #c8c9cc;
}

.z-button--warning.is-disabled {
  color: #fff;
  background-color: #f3d19e;
  border-color: #f3d19e;
}

.z-button--text.is-disabled {
  cursor: no-drop;
  color: #555;
  border: 1px solid #fff;
  background: none;
  opacity: .5;
}

// 圆角
.is-round {
  border-radius: 20px;
}

// 圆
.is-circle {
  border-radius: 50%;
  padding: 12px;
}

.noText {
  margin-left: 0 !important;
  margin-right: 0 !important;
}

.icon-loading {
  display: inline-block;
  margin-right: 4px;
  animation: rotating 2s linear infinite;
  -webkit-animation: rotating 2s linear infinite;
}

@keyframes rotating {
  0% {
    transform: rotate(0deg);
  }

  50% {
    transform: rotate(180deg);
  }

  100% {
    transform: rotate(360deg);
  }
}

.z-button--medium {
  padding: 10px 20px;
  font-size: 14px;
}

.z-button--small {
  padding: 9px 15px;
  font-size: 12px;
}

.z-button--mini {
  padding: 7px 15px;
  font-size: 12px;
}

2.编写button组件

<template>
    <button class="z-button" :class="zClass" :round="round" @click="handleClick">
        <i class="iconfont icon-loading" v-if="loading"></i>
        <i :class="isIconClass" v-if="leftIcon && !loading"></i>
        <span ref="slotRef" :style="slotStyle" :class="isHaveSlot ? 'noText' : ''">
            <slot></slot>
        </span>
        <i :class="isIconClass" v-if="rightIcon"></i>
    </button>
</template>
<script lang="ts">
export default {
    name: 'zButton'
};
</script>
<script lang="ts" setup>
import { ref, reactive, computed, watch, nextTick, onMounted } from "vue";
const props = defineProps({
    type: {
        type: String,
        default: 'default',
        validator(value:any) {
            return ['default', 'primary', 'success', 'info', 'warning', 'error', 'text'].indexOf(value) > -1;
        }
    },
    round: {
        type: Boolean,
        default: false
    },
    circle: {
        type: Boolean,
        default: false
    },
    disabled: {
        type: Boolean,
        default: false
    },
    size: {
        type: String,
        default: "default"
    },
    loading: {
        type: Boolean,
        default: false
    },
    leftIcon: {
        type: String,
        default: ""
    },
    rightIcon: {
        type: String,
        default: ""
    },
});
const emits = defineEmits(['click'])
const slotRef:any= ref(null)
const isHaveSlot:any = ref(null)
const zClass = computed(() => {
    return {
        [`z-button--${props.type}`]: props.type,
        'is-round': props.round,
        'is-circle': props.circle,
        'is-disabled': props.loading ? true : props.disabled,
        [`z-button--${props.size}`]: props.size
    }
});
const isIconClass = computed(() => {
    return [
        'iconfont',
        props.leftIcon || props.rightIcon
    ]
})
const slotStyle = computed(() => {
    return {
        'margin-left': props.leftIcon ? '4px' : '0',
        'margin-right': props.rightIcon ? '4px' : '0'
    }
})
const handleClick = (e:any) => {
    emits('click')
}
onMounted(() => {
    if (!slotRef?.value.innerText) {
        isHaveSlot.value = true
    }
})
</script>

<style scoped lang="scss">
@import '../../styles/components/button.scss';
</style>

3.button组件中 index.ts文件,用来把写的组件暴露出去

import zButton from './index.vue'
zButton.install = (app:any) => {
  app.component(zButton.name, zButton)
} 
export default zButton;

四、packages文件夹下的index.ts文件,用来管理所有的组件

import zButton from "./components/button/index";
const components = [
    zButton,
]
// 定义 install 方法,接收 Vue 作为参数。如果使用 use 注册插件,那么所有的组件都会被注册
const install: any = (Vue: any) => {
    // 判断是否安装
    if (install.installed) return
    // 遍历注册全局组件
    components.map(component => Vue.component(component.name, component))
    Vue.config.globalProperties.$message = zMessage
    Vue.config.globalProperties.$loading = zLoading.service
    Vue.directive('loading', zLoading.directive);
}

if (typeof window !== 'undefined' && (window as any).Vue) {
    install((window as any).Vue);
}

export default {
    install,
    zButton,
}; 
//提供按需引入 import {zButton} from 'ZUI'
// export default ZUI;

五、组件引入

1.在src 的main.ts中引入

import { createApp } from 'vue'
import App from './App.vue';
import ZUI from '../packages/index';
import router from './router';
// 引入全局样式
import './styles/index.scss';
import '~/iconfont/iconfont.css';

//代码高亮插件
import hljs from "highlight.js";
import "highlight.js/styles/color-brewer.css";
const app = createApp(App);
app.directive("highlight", function (el) {
  const blocks = el.querySelectorAll("pre code");
  blocks.forEach((block: any) => {
    hljs.highlightBlock(block);
  })
})

app.use(ZUI);
app.use(router)
app.mount('#app');

六、解析md文件

组件的文档一般是用 Markdown 来写。这里我们使用一个插件 vite-plugin-md vite pulgin 将 md 文件转换成 vue 组件渲染的主要流程是:

  1. 配置 vue router 路由,指向 .md 文件
  2. 编写 vite 插件将 .md 文件解析成 vue 文件字符串
  3. 最后由 vite 的插件@vitejs/plugin-vue 将 vue 文件字符串编译成函数组件返回给前端
  • 配置路由
import routerPages from './routerPage/pages'
const routes = [
    {
      path: '/',
      name: '主页',
      component: () => import('@/views/index.vue')
    },
    {
      path: '/home',
      name: '组件页面',
      component: () => import('@/views/home.vue'),
      children: [
          { 
              path: '/button', 
              name: 'Button 按钮', 
              component: () => import('/packages/components/button/doc/doc.md') 
          }
    }
  ]

  const router = createRouter({
    history: createWebHashHistory(),
    routes:routes,
  });

export default router
  • 配置vite.config.js文件,需要引入 vite-plugin-md 插件来解析 Markdown 文件并把它变成 Vue 文件。
import { defineConfig } from 'vite'
import Vue from '@vitejs/plugin-vue'
import Markdown from 'vite-plugin-md';
import { resolve } from "path";
// https://vitejs.dev/config/
export default defineConfig({
  base:'./',
  plugins: [
    Vue({
      include: [/\.vue$/, /\.md$/],
    }),
    Markdown()
  ],
  resolve:{
    alias:{
      "@": resolve(__dirname, "src"),
      '~': resolve(__dirname, "packages")
    },
    extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".vue"],
  }
})

七.编写home页面,增加侧边栏和主区域

<template>
    <div class="main-container">
        <aside @mousemove="mousemove" @mouseleave="mouseleave" :class="{ 'sidebar-scroll': isEnter }"
        v-show="!windowWidth">
            <div class="sidebar" id="sidebarScroll">
                <ul v-for="(item, ii) in menuList" :key="ii">
                    <p class="title">{{  item.name  }}</p>
                    <li class="sidebarLi" v-for="(ele, index) in item.list" :key="index" :class="{ 'active': mIndex == ii + '-' + index }"
                        @click="goPath(ele, ii, index)">
                        {{  ele.name  }}
                    </li>
                </ul>
            </div>
        </aside>
        <main class="app-main" ref="mainScroll">
            <router-view></router-view>
        </main>
        <div class="content-slidebar" v-show="!windowWidth">
            <div class="content-section">
                <p class="content-title">内容导航</p>
                <ul v-for="(item, ii) in state.contentList" :key="ii">
                    <p @click="mainScrollHandler(ii)" class="nav-title" :class="{ selected: item.active }">
                        <span class="desc"> {{  item.title  }}</span>
                    </p>
                    <!-- <li v-for="(ele, index) in item.list" :key="ele" :class="{ 'active': mIndex == ii + '-' + index }"
                        @click="goPath(ele, ii, index)">
                        {{ ele.name }}
                    </li> -->
                </ul>
            </div>
        </div>
    </div>
</template>

<script setup lang=ts>
import { computed, ref, reactive, onMounted, nextTick, watch, onBeforeMount } from 'vue';
import { useRouter } from 'vue-router';
import Header from '../components/header.vue';
import { menuList } from '../router/routerPage/index';
import emitter from "../../packages/utils/bus";

const router = useRouter()
const mIndex: any = ref(sessionStorage.getItem("mIndex") || '0');
const isEnter = ref(false)
const state:any = reactive({
    contentList: [],
    topList: []
})
const mainScroll:any = ref(null)
const sidebarScroll = ref(null)
state.contentList = menuList[0]['list'][0]?.content;

watch(() => router.currentRoute.value,(newVal) => {
        mIndex.value = sessionStorage.getItem("mIndex")
        nextTick(() => {
                //左侧菜单滚动
                document.querySelector('.sidebarLi.active')?.scrollIntoView({
                    behavior: 'auto',
                    block: 'center',
                    inline: 'nearest'
                })
                confirmContentSlider(0); //初始化 内容导航栏
                calcH2TopList(); // 回到顶部
            })
    },
{ immediate: true }
);

const goPath = (ele:any, ii:any, index:any) => {
    mIndex.value = ii + '-' + index
    console.log(mIndex.value)
    state.contentList = menuList[ii]['list'][index]?.content;
    router.push({
        path: ele.path
    })
    sessionStorage.setItem("mIndex", mIndex.value);
}

const confirmContentSlider = (index:any) => {
    let arr = state.contentList;
    arr && arr.forEach((item:any, indexPath:any) => {
        item.active = false;
        if (indexPath == index) {
            item.active = true;
        }
    })
    state.contentList = arr;
}

const mousemove = () => {
    isEnter.value = true
}

const mouseleave = () => {
    isEnter.value = false
}

const calcH2TopList = () => {
    let h2List = document.querySelectorAll('h2');
    let arr:any = [];
    h2List.forEach(item => {
        arr.push(item.offsetTop);
    })
    state.topList = arr;
}

let isScrollStatus = false;

const mainScrollHandler = (index:any) => {
    confirmContentSlider(index);
    isScrollStatus = true;
    mainScroll.value?.scrollTo({
        top: state.topList[index] - 70,
        left: 0,
        behavior: 'smooth'
    })
    setTimeout(() => {
        isScrollStatus = false;
    }, 500);
}

const handleScroll = () => {
    if (isScrollStatus) return;
    //获取dom滚动距离
    const scrollTop = mainScroll.value.scrollTop;
    // console.log('滚动的距离:' + scrollTop);
    for (let i = 0; i < state.topList.length; i++) {
        if (scrollTop > state.topList[i] - 80 && scrollTop <= state.topList[i + 1] - 80) {
            confirmContentSlider(i);
            break;
        }
    }
}

let flag = true;

let that = this;
const thorrle = (fn:any, interval:any) => {
    let last:any = 0;
    return function () {
        if (!flag) return false;
        let context = that;
        let args = arguments;
        let now:any = new Date();
        if ((now - last) > interval) {
            last = now;
            //劫持当前所在的方法返回fn的方法的内容
            fn.apply(context, args);
        }
    }
}
//监听设备
let windowWidth = ref(false);
const resizeWidth = () => {
    if(window.innerWidth<800){
        windowWidth.value = true
    } else {
        windowWidth.value = false
    }
}
window.addEventListener('resize',()=>{
    resizeWidth()
})
onMounted(() => {
    resizeWidth();
    nextTick(() => {
        calcH2TopList();
        mainScroll.value.addEventListener("scroll", thorrle(handleScroll, 200));
    })
    emitter.on('previewChange', (res) => {
        console.log('erer',res)
        setTimeout(() => {
            calcH2TopList();
        }, 500);
    })
})

onBeforeMount(() => {
    emitter.off('previewChange');
    nextTick(() => {
        mainScroll.value.removeEventListener('scroll', () => { }); // 离开当前组件别忘记移除事件监听
    })
});

</script>

<style lang="scss" scoped>
.main-container {
    display: flex;
    height: calc(100vh - 64px - 2vh);
    margin-top: 8px;
    overflow: hidden;

    aside {
        height: 100%;
        border-right: 1px solid #eee;
        overflow-y: auto;
        box-shadow: 0 2px 8px #f0f1f2;

        .sidebar {
            width: 250px;
            height: auto;
            margin-top: 10px;
            box-sizing: border-box;

            .title {
                font-weight: 700;
                line-height: 40px;
                margin-left: 40px;
                color: #333;
                border-bottom: 1px solid #f0f0f0;
            }

            ul {
                li {
                    height: 50px;
                    line-height: 50px;
                    padding-left: 40px;
                    font-size: 13px;
                    color: #606266;
                    cursor: pointer;
                }

                .active {
                    color: #409eff;
                    background-color: #ECF5FF;
                }
            }
        }
    }

    .sidebar-scroll {
        &::-webkit-scrollbar {
            display: block;
            width: 6px;
            height: 1px;
        }

        &::-webkit-scrollbar-thumb {
            border-radius: 8px;
            background: #ddd;
        }

        &::-webkit-scrollbar-track {
            border-radius: 8px;
            background: #fff;
        }
    }

    .content-slidebar {
        width: 300px;
        height: 100%;

        .content-section {
            margin-top: 50px;

            .content-title {
                font-weight: 550;
                font-size: 17px;
                padding-bottom: 20px;
            }

            .nav-title {
                cursor: pointer;
                font-size: 15px;
                padding: 10px 0;
                position: relative;

                .desc {
                    margin-left: 10px;
                }

                &:after {
                    content: '';
                    width: 0;
                    height: 0;
                    border-right: 4px solid #409eff;
                    position: absolute;
                    top: 50%;
                    left: 0;
                    transform: translateY(-50%);
                    transition: .3s;
                }
            }

            .selected {
                color: #409eff;

                &:after {
                    height: 40%;
                }
            }
        }
    }

    .app-main {
        flex: 1;
        padding: 10px 50px;
        overflow-y: auto;

        &::-webkit-scrollbar {
            display: none;
            width: 10px;
            height: 1px;
        }
    }
}
</style>

再把app.vue改一下:

<template>
    <Header />
    <router-view></router-view>
</template>

<script lang="ts" setup>
import Header from '@/components/header.vue';
</script>

<style>
html,body{
  margin: 0;
  padding: 0;
}
#app {
  overflow: hidden;
}
</style>

packages/components/button/doc/demo1下使用组件。

<template>
    <z-space>
        <z-button>Default</z-button>
        <z-button type="primary">Primary</z-button>
        <z-button type="success">Success</z-button>
        <z-button type="error">Error</z-button>
        <z-button type="info">Info</z-button>
        <z-button type="warning">Warning</z-button>
        <z-button type="text">Text</z-button>
    </z-space>
</template>

完成后,浏览器查看就有效果了。 如何开发一款前端UI组件库??

八、代码预览功能

接下来我们来实现代码预览功能,虽然说代码预览也很简单,可以直接在 Markdown 中贴代码,但是代码又写一遍就显得很繁琐了,有没有方法可以直接把demo.vue中的代码展示出来呢?

答案是可以的。

在 Vite 的开发文档里有记载到,它支持在资源的末尾加上一个后缀来控制所引入资源的类型。比如可以通过 import xx from 'xx?raw' 以字符串形式引入 xx 文件。基于这个能力,我们可以编写一个 组件来获取所需要展示的文件源码。

1、新建一个 Preview.vue 文件

<template>
    <div class="pre-code-box">
        <transition name="slide-fade">
            <pre class="language-html" v-if="showCode" v-highlight>
        <code class="language-html">{{ sourceCode }}</code>
      </pre>
        </transition>
        <div class="showCode" @click="showOrhideCode">
            <i :class="iconClass"></i>
            <span>{{ showCode ? "隐藏代码" : "显示代码" }}</span>
        </div>
    </div>
</template>

<script lang="ts" setup>
import { ref, reactive, computed, watch, nextTick, onMounted } from "vue";
import emitter from "../../packages/utils/bus";
const props = defineProps({
    compName: {
        type: String,
        default: "",
        required: true
    },
    demoName: {
        type: String,
        default: "",
        required: true
    }
})
const showCode = ref(false);
const sourceCode = ref("");
const isDev = import.meta.env.MODE === 'development';

const iconClass = computed(() => {
    return [
        'iconfont',
        showCode.value ? 'icon-arrow-up-filling' : 'icon-arrow-down-filling'
    ]
})
const showOrhideCode = () => {
    emitter.emit('previewChange', 'ok');
    showCode.value = !showCode.value;
}
const getSourceCode = async () => {
    if (isDev) {
        let msg = await import( /* @vite-ignore */`../../packages/components/${props.compName}/doc/${props.demoName}.vue?raw`)

        sourceCode.value = msg.default
    } else {
        sourceCode.value = await fetch(`./components/${props.compName}/doc/${props.demoName}.vue`).then(res => res.text());
    }
}
onMounted(() => {
    getSourceCode()
})
</script>

<style lang="scss" scoped>
.slide-fade-enter-active {
    transition: all 0.1s ease-out;
}

.slide-fade-leave-active {
    transition: all 0.1s cubic-bezier(1, 0.5, 0.8, 1);
}

.slide-fade-enter-from,
.slide-fade-leave-to {
    transform: translateY(-10px);
    opacity: 0.5;
}

.pre-code-box {
    width: 100%;
    height: auto;
    overflow: hidden;
    border-top: 0;
    position: relative;
    transition: all 0.15s ease-out;

    &:hover {
        box-shadow: 0px 16px 15px -16px rgb(0 0 0 / 10%);
    }

    pre {
        margin-top: -15px;
        margin-bottom: 0;
    }

    .showCode {
        width: 100%;
        line-height: 40px;
        font-size: 14px;
        text-align: center;
        background: #f9f9f9;
        box-shadow: 0px 16px 15px -16px rgb(0 0 0 / 10%);
        color: #666;
        display: flex;
        justify-content: center;
        align-items: center;
        cursor: pointer;

        &:hover {
            background: #f9f9f9;
            color: #0e80eb;
        }

        span {
            margin-left: 10px;
        }
    }
}
</style>

这里需要加 @vite-ignore 的注释是因为 Vite 基于 Rollup,在 Rollup 当中动态 import 是被要求传入确定的路径,不能是这种动态拼接的路径。此处加上该注释则会忽略 Rollup 的要求而直接支持该写法。

但是这样的写法,在开发模式下可行,build打包到线上后报错,拿不到资源,我们可以通过判断环境变量,在 build 模式下通过 fetch 请求文件的源码来绕过,我们后面再讲。

2.在md文件中引入preview

<script setup>
    import demo1 from './demo1.vue'
    import demo2 from './demo2.vue'
    import demo3 from './demo3.vue'
    import demo4 from './demo4.vue'
    import demo5 from './demo5.vue'
    import preview from '@/components/preview.vue'
</script>
## 按钮Button

按钮用来触发一些操作。
### 演示
<div class="container">
    <div class="container-gird">
        <div class="source">
            <p class="z-text-fs18">基础</p>
            <p class="z-text">按钮的 type 分别为 
                <code class="z-text--code">default</code>
                <code class="z-text--code">primary</code>
                <code class="z-text--code">success</code>
                <code class="z-text--code">error</code>
                <code class="z-text--code">info</code>
                <code class="z-text--code">warning</code>
                <code class="z-text--code">text</code>
            </p>
            <demo1/>
            <preview compName="button" demoName="demo1"></preview>
        </div>
        <div class="source">
            <p class="z-text-fs18">不同的尺寸</p>
            <p class="z-text">额外的尺寸:
                <code class="z-text--code">medium</code>
                <code class="z-text--code">small</code>
                <code class="z-text--code">mini</code>
                通过设置size属性来配置它们。
            </p>
            <demo2/>
            <preview compName="button" demoName="demo2"></preview>
        </div>
        <div class="source">
            <p class="z-text-fs18">禁用状态</p>
            <p class="z-text">按钮可以被禁用。
            </p>
            <demo5/>
            <preview compName="button" demoName="demo5"></preview>
        </div>
    </div>
    <div class="container-gird">
        <div class="source">
            <p class="z-text-fs18">图标按钮</p>
            <p class="z-text">在按钮上使用图标,通过设置
                <code class="z-text--code">leftIcon</code>
                <code class="z-text--code">rightIcon</code>
                属性来配置图标的位置。
            </p>
            <demo3/>
            <preview compName="button" demoName="demo3"></preview>
        </div>
        <div class="source">
            <p class="z-text-fs18">加载ing...</p>
            <p class="z-text">要设置为 loading 状态,只要设置 <code class="z-text--code">loading</code> 属性为true即可。
            </p>
            <demo4/>
            <preview compName="button" demoName="demo4"></preview>
        </div>
    </div>
</div>

<br/>


## API
| 参数   | 类型    | 说明         | 可选值       | 默认值   |
|---------- |-------- |---------- |-------------  |-------- |
| type  | string    | 类型      |   primary / success / warning / error / info / text |     —    |
| size  | string   | 尺寸     |   medium / small / mini            |    —     |
| round | boolean    | 是否圆角按钮       | — | false   |
| circle  | boolean   | 是否圆形按钮       | — | false   |
| loading  | boolean   | 是否加载中状态       | — | false   |
| disabled | boolean | 是否禁用状态       | —   | false   |
| leftIcon | string | 图标按钮,并且icon展示在左侧    | 参考图标库 |  —  |
| rightIcon  | string | 图标按钮,并且icon展示在右侧    |  参考图标库  |  —  |

<br/>

main.ts已经注册了v-highlight自定义指令,查看页面效果:

如何开发一款前端UI组件库??

九.gitee部署静态站点

到这里,我们的组件库基本结构算是完成了,接下来,我们上传到gitee部署静态站点。 执行npm run build 时,默认会打包成dist文件,打包后的文件夹里并没有components文件,我们得把packages下的components文件加入进去

如何开发一款前端UI组件库??

开启Gitee Pages服务(需要上传身份证实名审核),选择分支和部署的目录,点击更新按钮等待片刻

如何开发一款前端UI组件库??

十、发布npm

打包完之后执行npm login 命令,输入用户名和密码,输入密码时是看不到的,之后提示输入email,成功后你的邮箱会收到一个one-time password,填入这个一次性密码,登录之后,执行npm publish进行发布(每次进行发布的时候记得改下版本号),发布成功后,到npm上,查看头像---->packages,就可以看到发布的包了。