首页>>前端>>Vue->使用vue3+vite开发一个仿element ui框架

使用vue3+vite开发一个仿element ui框架

时间:2023-12-01 本站 点击:0

看完这篇文章,你会有以下新的认识:

如何使用vue3+vite封装插件并发布到npm

如何构建一个ui框架文档网站

插件开发中的技巧

前言

在平日的开发中,我们经常使用不同的ui框架,不知道大家有没有想法自己开发一个自己的ui框架,或许很多人感觉,没有必要重复造轮子,但是现在前端工程师的要求越来越高,需要的技术栈也越来越多,学习一下这个开发流程和一些解决方案还是很有必要的。而且我觉得,最重要的是,在平时的项目开发中,会有许多ui框架无法覆盖的组件,这是和这个业务比较绑定的,独属于这个业务需求的组件,当这个业务比较大的时候,这个组件就需要有更高的灵活性和易用性,有时候使用现有的ui框架进行二次封装也具有一定的成本,甚至高过从头开发,所以在这种情况下,我们就可以把常用的组件,封装成ui插件,配合上完整的组件文档,无论是方便以后项目迭代的时候查看,还是分享给其他人,都是极好的。 下面,仿照element plus官网的样子,来仿一个ui框架,以此讲述开发流程和用到的技术与方案。成品展示:

仓库地址: https://gitee.com/biluo_x/biluo-ui npm地址:biluo-ui - npm (npmjs.com)

技术栈

vue3 前端主流框架之一,这里我们使用3.2版本

vite 代替vue-cli的新脚手架

typescript js的超集,提供类型系统

vite-plugin-md vite的md插件,提供把md文件当做vue导入的能力,最厉害的是,也可以在md文件中使用vue组件

tailwindcss 为了快速得到效果,使用原子类提供样式

prismjs 在代码展示的时候,提供代码高亮 如果没有使用过tailwindcss 可以看看这篇文章:受够了重复繁琐的css?来试试原子类吧 - 掘金 (juejin.cn)

需求分析

我们是仿照element plus来写的所以,我们可以观察一下element 的展示情况。

抛开那些其他的功能,主要部分分为三个,左边根据组件分类的导航栏,中间的展示文档,以及右边的文档目录。先看左侧导航 一个组件对应了一个目录,而我们需要把同种的目录分组,比如基础组件放一项,表单组件放一项等。再看主体文档

主体文档应该使用markdown编写,一个组件对应一个md文件,所以我们需要有在vue中导入md的功能。

组件有不同的功能,需要提供一个演示框,这个演示框里面会放不同的组件功能展示,以及固定的查看代码,粘贴代码,前往仓库的固定功能。我可以发现这个演示框应该是一个vue组件,所以需要有在md文件中导入vue组件的功能 最后看右侧的目录

目录需要自动提取md文件中的标题

目录需要跟着文档滚动而滚动

点击目录可以跳转到对应的标题

目录介绍

项目使用vite初始化,选择vue3+ts模板,然后包管理器使用的是yarn。具体初始化就不献丑了。 除此之外,我这里加入了eslint+prettier为代码格式化,jest+@vue/test-utils来提供测试支持(写了两三个组件测试就懒得写了...),这些没有也不影响开发,这里提一嘴。 目录规划如下:

src 和平时的页面开发一致,这里存放展示在外的文档页面,打包成文档网站使用

packages 这里存放我们ui组件相关的代码。主要结构如下:

在components文件夹下编写ui组件,一个文件夹表示一个组件,组件中,src存放组件文件,tests存放测试代码,index.ts 提供默认导出。当然components文件夹下还有一个index.ts提供统一入口,导出所有的组件。

组件开发

这里我们用button组件的开发来展示基础开发流程,用input组件的开发来讲述vue3更好的开发方式。

button组件

button组件的文件夹结构

components├── button│   ├── __tests__│   │   ├── button.test.ts  // bl-button.vue 测试│   │   └── buttonGroup.test.ts   // bl-button-group.vue 测试│   └── src│       └── bl-button.vue  // button 组件        |__bl-button-group.vue // button 组    ├── index.ts  // 模块导出文件|── index.ts  // 组件库导出文件

在button文件夹下的index.ts中我们将src下的两个组件暴露出去: packages/components/button/index.ts

import BlButton from './src/bl-button.vue'import BlButtonGroup from './src/bl-button-group.vue'import { App } from 'vue'export default {  install(app: App) {    app.component('BlButton', BlButton)    app.component('BlButtonGroup', BlButtonGroup)  }}export { BlButtonGroup, BlButton }

这里选择了两种导出,主要是为了能直接全局注册的同时,也支持单独引用。 然后在总的index.ts中全部导出: packages/components/index.ts

import { App } from 'vue'export * from './button'import button from './button'const components = [button]export default {  install(app: App) {    components.map((item) => item.install(app))  }}

后续如果需要添加新的组件,按这个流程导入即可。下面让我们来看一下button组件的具体开发:

<script setup lang="ts">  import { computed, inject, ref, Ref } from 'vue'  import BlIcon from '../../icon/src/bl-icon.vue'  // 定义名称  // 定义事件  const $emit = defineEmits(['click'])  // 定义props  const props = defineProps({    size: {      type: String,      validator: (value: string) => {        return ['default', 'large', 'small'].includes(value)      }    },    // 按钮类型    type: {      type: String,      default: 'default',      validator: (value: string) => {        return ['default', 'primary', 'success', 'info', 'warning', 'danger', 'text'].includes(          value        )      }    },    // 是否为朴素按钮    plain: {      type: Boolean,      default: false    },    // 是否为圆形    round: {      type: Boolean,      default: false    },    // 是否正在加载中    loading: {      type: Boolean,      default: false    },    // 是否为圆形    circle: {      type: Boolean,      default: false    },    // 自定义加载中图标    loadingIcon: {      type: String,      default: 'Loading'    },    // 是否禁用状态    disabled: {      type: Boolean,      default: false    },    iconColor: {      type: String,      default: 'white'    },    // 原生type属性    nativeType: {      type: String as () => 'button' | 'reset' | 'submit' | undefined,      default: 'button'    }  })  // 类名计算属性  const classComputed = computed(() => {    // const sizeInject = inject<Ref<number | undefined>>('button-group-size', ref(undefined))    const typeInject = inject<Ref<string | undefined>>('button-group-type', ref(undefined))    // const typeClass = props.type ? 'bl-button-' + props.type : 'bl-button-default'    const typeClass =      props.type === 'default' && typeInject.value        ? 'bl-button-' + typeInject.value        : 'bl-button-' + props.type    const isPlain = props.plain ? 'bl-is-plain' : ''    const isRound = props.round ? 'bl-is-round' : ''    const isLoading = props.loading ? 'bl-is-disabled is-Loading' : ''    const isDisabled = props.disabled || props.loading ? 'bl-is-disabled' : ''    const isCircle = props.circle ? 'bl-is-circle' : ''    const isSize = props.size ? `bl-is-${props.size}` : ''    return [typeClass, isPlain, isRound, isDisabled, isLoading, isCircle, isSize]  })  // 禁用点击计算属性  const disabledComputed = computed(() => {    const isDisabled = props.disabled || props.loading    return {      isDisabled    }  })  // 接受button-group的注入  const groupInjectComputed = computed(() => {    const sizeInject = inject<Ref<number | undefined>>('button-group-size', ref(undefined))    const typeInject = inject<Ref<string | undefined>>('button-group-type', ref(undefined))    const classData = []    if (sizeInject.value) {      const size = (props.size ? props.size : sizeInject.value) ?? ''      classData.push(`bl-is-${size}`)    }    if (typeInject.value) {      const type = props.type === 'default' ? typeInject.value : props.type      classData.push(`bl-button-${type}`)    }    return classData  })  // 点击事件  const clickEmit = (event: any) => {    const isEmit = props.disabled || props.loading    if (!isEmit) $emit('click', event)  }</script><template>  <button    :class="['bl-button', ...groupInjectComputed, ...classComputed]"    :type="nativeType"    :disabled="disabledComputed.isDisabled"    @click="clickEmit($event)"  >    <span>      <bl-icon v-if="loading" :name="loadingIcon" :color="iconColor" class="animate-spin mr-0.5" />      <slot />    </span>  </button></template><style>  @import '../style/index.css';  /*自身属性*/  .bl-button + .bl-button {    margin-left: 12px;  }  .bl-is-large {    height: 40px !important;    padding: 12px 19px !important;  }  .bl-is-small {    height: 24px !important;    padding: 5px 11px !important;    font-size: 12px !important;  }  .bl-is-large.bl-is-circle {    width: 40px !important;    padding: 12px !important;  }  .bl-is-small.bl-is-circle {    width: 24px;    padding: 5px !important;  }</style>

这个代码看起来不少,实际上很简单,最多的就是,prop和根据prop对类名进行处理。button的所有样式都是使用css来控制的。js只在原生属性上面稍微处理了一下。这个代码其实写的不好,在类名处理哪里写了一堆的三元表达式,后来发现element源码里面写弄了一个hook专门搞这个,我也去整了一个,代码很简单,大概就是根据bool改变类名之类的:

type namespaceStyle = 'backgroundColor' | 'color' | 'width' | 'height'export const DEFAULT_NAMESPACE = 'bl'export const STATE_PREFIX = 'is'export const useNamespace = (namespace: string) => {  return {    b() {      return `${DEFAULT_NAMESPACE}-${namespace}`    },    is(state: boolean, name: string) {      return name && state ? `${STATE_PREFIX}-${name}` : ''    },    m(suffix: string) {      if (suffix) {        return `${DEFAULT_NAMESPACE}-${namespace}-${suffix}`      }      return ''    },    sy(data: string, label: namespaceStyle) {      return {        [label]: data      } as CSSProperties    },    is_sy(is: Boolean, one: CSSProperties, two?: CSSProperties) {      if (!two) {        if (is) return one        return {} as CSSProperties      }      if (is) {        return one      } else {        return two      }    }  }}

有了这个后,后来的类名处理就写了这样

<script setup lang='ts'>const ns = useNamespace('drawer')</script><template><util-modal  :visible="modelValue"  :class="[    ns.is(direction === 'rtl', 'rtl'),    ns.is(direction === 'ltr', 'ltr'),    ns.is(direction === 'ttb', 'ttb'),    ns.is(direction === 'btt', 'btt')  ]"  @close  /></template>

开发方面都很简单,就不过多赘述了.

input 组件

这里为什么把input组件单独拿出来说一下呢,因为大家也看到了上面button的代码,功能不多,但是代码量特别大,而且繁琐。实际上,vue3的开发方式并不是这样的,上面的开发把全部都合并到一起了,有点像以前vue2的感觉,我们来看一下input组件。用过element的朋友应该知道,input组件在开启清除按钮后,鼠标滑入按钮才会显示,滑出后又会隐藏。这个功能我们要怎么实现呢,其实很简单,用一个bool变量,然后监听鼠标的滑入和滑出事件嘛。在这里我们选择封装成hook的写法,其实就是利用闭包

export const useMouseEnterLeave = () => {  const mouse_is = ref(false)  return {    mouse_is,    enter: () => (mouse_is.value = true),    leave: () => (mouse_is.value = false)  }}

然后在vue中引用

const { mouse_is, enter, leave } = useMouseEnterLeave()

因为vue3把响应式的功能封装成了ref和reactive这两个函数,不像以前vue2必须写在data函数返回值里面才具备相应监听,这样就让我们开发与封装更加灵活多变。

路由设计

根据上面的对组件导航栏的分析,我们可以发现,这是由多个类型组件的集合组成的大路由。简而言之,就是一个一级标题代表的就是该分类下的所有组件。

原本我是打算把它设计成数组的,但是考虑到对不同模块的显示隐藏的控制,最终把它设计为了一个对象,各位可以根据自己的实际情况自行处理。 组件路由的类型如下

export interface routerType {  title: string  routerData: RouteRecordRaw[]}

这是具体设计 /src/router/routerConfig/index.ts

export const routerDocsComponentConfig = {  index: {    title: '前言',    routerData: beforeComponent  },  baseComponents: {    title: 'Basic 基础组件',    routerData: baseComponent  },  dataShowComponents: {    title: 'Data 数据展示',    routerData: dataShowComponent  },  ... }

基础路由就是正常vue-router配置的类型 /src/router/routerConfig/base.component.ts

import BlButton from './src/bl-button.vue'import BlButtonGroup from './src/bl-button-group.vue'import { App } from 'vue'export default {  install(app: App) {    app.component('BlButton', BlButton)    app.component('BlButtonGroup', BlButtonGroup)  }}export { BlButtonGroup, BlButton }0

以基础组件路由举例,我们把基础路由相关的文档全部放在这里。可以看到这里引用的组件是一个md文件,具体操作我们等下会讲到。 具体的使用就是在通用路由中配置需要显示的模块的key. /src/components/doc-component-pag.vue

import BlButton from './src/bl-button.vue'import BlButtonGroup from './src/bl-button-group.vue'import { App } from 'vue'export default {  install(app: App) {    app.component('BlButton', BlButton)    app.component('BlButtonGroup', BlButtonGroup)  }}export { BlButtonGroup, BlButton }1

asideKeys里面配置了需要显示的路由模块,可以通过参数的顺序和增伤进一步控制导航的显示。

文档主体

上面我们说到每一个组件路由其实是一个md文件。要想在vue中正常解析md.我们需要下载一个vite插件。

import BlButton from './src/bl-button.vue'import BlButtonGroup from './src/bl-button-group.vue'import { App } from 'vue'export default {  install(app: App) {    app.component('BlButton', BlButton)    app.component('BlButtonGroup', BlButtonGroup)  }}export { BlButtonGroup, BlButton }2

为什么使用这个固定版本,因为当时我下载的最新版,有一个bug,就是无法在md文档中导入vue组件,通过它gitHub上提的issues说这个问题已经被解决,但是npm没有更新,现在不晓得更新了没得,但是我们不需要太多功能,这个版本够用了

接下来我们在vite的配置文件里面配置它

import BlButton from './src/bl-button.vue'import BlButtonGroup from './src/bl-button-group.vue'import { App } from 'vue'export default {  install(app: App) {    app.component('BlButton', BlButton)    app.component('BlButtonGroup', BlButtonGroup)  }}export { BlButtonGroup, BlButton }3

这里用到了markdown-it-anchor这个插件,这个插件的作用是在上面那个插件生成vue组件时候,把h标签的内容作为它的id,这样我们就可以通过id跳转的方式从目录跳转到指定内容了。 如果你使用的是ts,请在环境中提供md支持,将其文件类型定义为vue组件

import BlButton from './src/bl-button.vue'import BlButtonGroup from './src/bl-button-group.vue'import { App } from 'vue'export default {  install(app: App) {    app.component('BlButton', BlButton)    app.component('BlButtonGroup', BlButtonGroup)  }}export { BlButtonGroup, BlButton }4

接下来我们就可以愉快的使用vue和md双向导入功能了。 vue导入md就不多说了,直接导入作为组件就是,在md中使用vue组件的方法,这里简单说一下,md中可以用两种组件.

全局组件 直接当html标签使用,可以直接解析

局部组件,在md文件中导入使用,使用方式如下:

以上,我们就完成了md引入vue组件的操作,接下来我们来开发代码展示组件。 一共三个区域。

展示区:通过slot,展示外部组件。

控件去:前往仓库,一键复制,代码展示,三个控件

代码区:获取展示区传入的外部组件的代码,加上代码高亮展示 这个组件本身很简单,因为使用频繁,所以我们直接注册为全局组件,这样就可以直接在md文件中引入,而展示区的代码,则通过局部引入的方式,导入进行展示。文件结构如下:

每一个展示区,对应一个vue文件,这样控制粒度更加精细。

代码展示

下面我们来看看代码展示功能是如何实现的,vite可以通过如这种形式import xx from 'xx?raw'把一个文件标记为资源文件,从而获取文件的内容,我们可以通过这种形式,获取展示区的代码。但是这种方式只能在开发环境得到支持,所以生产环境需要换成网络请求的方式,具体代码如下: /src/components/common/show-code.vue

import BlButton from './src/bl-button.vue'import BlButtonGroup from './src/bl-button-group.vue'import { App } from 'vue'export default {  install(app: App) {    app.component('BlButton', BlButton)    app.component('BlButtonGroup', BlButtonGroup)  }}export { BlButtonGroup, BlButton }5

判断是否是开发环境,选择静态资源加载或者网络请求。这里也可以看到,在开发环境下,我们需要把docs文件夹复制一份到打包后的根路径。开发到后期经常打包,这样手动cv实在是太恼火了,这里写了一个脚本,在打包后自己复制过去,用到了copy-dir这个包,需要自行下载

import BlButton from './src/bl-button.vue'import BlButtonGroup from './src/bl-button-group.vue'import { App } from 'vue'export default {  install(app: App) {    app.component('BlButton', BlButton)    app.component('BlButtonGroup', BlButtonGroup)  }}export { BlButtonGroup, BlButton }6

使用方式只需要在原本的打包命令后加上,就会自动在打包后执行这个代码,node后面是代码所在相对路径。

import BlButton from './src/bl-button.vue'import BlButtonGroup from './src/bl-button-group.vue'import { App } from 'vue'export default {  install(app: App) {    app.component('BlButton', BlButton)    app.component('BlButtonGroup', BlButtonGroup)  }}export { BlButtonGroup, BlButton }7

一键复制

一键复制功能就比较简单了,就是把代码的内容复制给一个input,进入选择状态后控制键盘执行copy指令 /src/components/common/show-code.vue

import BlButton from './src/bl-button.vue'import BlButtonGroup from './src/bl-button-group.vue'import { App } from 'vue'export default {  install(app: App) {    app.component('BlButton', BlButton)    app.component('BlButtonGroup', BlButtonGroup)  }}export { BlButtonGroup, BlButton }8

md文件使用方式

当我们把show-code组件全局注册后,就可以在md文件中使用它了

import BlButton from './src/bl-button.vue'import BlButtonGroup from './src/bl-button-group.vue'import { App } from 'vue'export default {  install(app: App) {    app.component('BlButton', BlButton)    app.component('BlButtonGroup', BlButtonGroup)  }}export { BlButtonGroup, BlButton }9

showPath是展示组件的路径,以便在展示代码的时候,获取对应的数据。 具体细节请查看 文档

打包上传npm

编写组件打包配置: /config/prod.com.config.ts

import { App } from 'vue'export * from './button'import button from './button'const components = [button]export default {  install(app: App) {    components.map((item) => item.install(app))  }}0

配置package.json

import { App } from 'vue'export * from './button'import button from './button'const components = [button]export default {  install(app: App) {    components.map((item) => item.install(app))  }}1

这里最重要的是这三个字段,files,main,module

files: 设置你要上传的目录,写上我们打包输出的目录

main: 项目主入口 这里主要是require引用的入口

module: 同样的主入口,这里是import引入的入口,比如我使用

import { App } from 'vue'export * from './button'import button from './button'const components = [button]export default {  install(app: App) {    components.map((item) => item.install(app))  }}2

默认就是导入:./BiLuoUI/biluo-ui.es.js

因为这是一个ui框架,用不上require导入,所以我们都写的一样的入口文件。

打包生成BiLuoUI:

上传npm:

登陆 执行npm login命令,系统会提示输入账户和密码。如果没有npm账户,请注册 → npm官网

发布 若账户登录成功后,就可以再次执行 npm publish 进行发布

注意

每次发布,都需要更新版本号,否则无法成功上传

上传到npm上时,要将package.json中的private属性值改为false

最后

这里大概是梳理了一下开发一个开源组件网站的方案和基本流程,希望对有此想法的朋友提供一定的帮助。文章并没有太过详细的简述ui组件的开发,相信这对大家来说都不是什么问题。如果有什么其他需要的可以自行查看本项目仓库。

原文:https://juejin.cn/post/7101567321717604360


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/Vue/5062.html