Vue3 封装自己的简易 UI 库

2,210 阅读5分钟

在自己之前做项目的时候也接触过几个优秀的组件库,比如 Ant Design、Element UI、Vant等,它们有丰富的组件和功能。最近学习 Vue3, 决定自己封装几个简单小组件来巩固知识, 同时学习如何封装属于自己的组件库。

项目搭建

  • 使用 Vite 搭建自己的官网 安装 create-vite-app
yarn global add create-vite-app

创建项目

 cva project-name 
 // 或者
 create-vite-app project-name 
 // 接下来根据提示即可
 cd project-name
 npm install
 npm run dev

以上步骤就可以启动这个项目了。

  • 引入 Vue Router 4 用于页面切换
yarn install vue-router@4.0.0

初始化 vue-router 的步骤

  1. 创建 history 对象
  2. 创建 router 对象
  3. app.use(router)
  4. 添加 <router-view> 用来显示路由对应的页面
  5. 添加 <router-link> 中的 to Attribute 用来添加路由的路径

代码如下:

创建 history对象和 router 对象

import {createWebHashHistory, createRouter} from 'vue-router';
import Home from './views/Home.vue';
import Doc from './views/Doc.vue';

const history = createWebHashHistory();
export const router = createRouter({
  history: history,
  routes: [
    {path: '/', component: Home},
    {
      path: '/doc',
      component: Doc,
    },
  ],
});
// main.ts
app.use(router);

添加 router-view

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

添加 router-link

<li>
  <router-link to="/doc">文档</router-link>
</li>

至此,整个项目的搭建基本完成。

知识点记录

1. setup() 的用法

setup 执行时机是在 beforeCreate 之前执行,使用 setup 函数时,它将接收两个参数:

  • props:组件传入的属性;props是响应式的, 当传入新的 props 时,会及时被更新。由于是响应式的, 所以不可以使用 ES6 解构,解构会消除它的响应式。
  • contextcontext 是一个普通的 JavaScript 对象,它暴露组件的三个 property:attrsslotsemit(非响应式对象)。 可以使用setup()函数访问组件的property(props、attrs、slots 和 emit)、结合模板使用、使用渲染函数。

2. ref 的用法

接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

3. provide 和 inject 的用法

通常,当我们需要从父组件向子组件传递数据时,我们使用 props。想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。

对于这种情况,我们可以使用一对 provide 和 inject。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。

const app = Vue.createApp({})

app.component('todo-list', {
  data() {
    return {
      todos: ['Feed a cat', 'Buy tickets']
    }
  },
  provide: {
    user: 'John Doe'
  },
  template: `
    <div>
      {{ todos.length }}
    </div>
  `
})

app.component('todo-list-statistics', {
  inject: ['user'],
  created() {
    console.log(`Injected property: ${this.user}`) // > 注入 property: John Doe
  }
})

4. v-model的使用

v-model 对父子之间的数据绑定进行简化(要求事件名必须为 update:x) 这也是与 vue 2 不同的地方(没有.sync)

用法:

<Switch :value="y" @update:value="y = $event" />
// 可以简写为
<Switch v-model:value="y" />

5. Attribute 继承

当组件返回单个根节点时,非 prop attribute 将自动添加到根节点的 attribute 中。同样的规则也适用于事件监听器。 如果你希望组件的根元素继承 attribute,你可以在组件的选项中设置 inheritAttrs: false。例如:禁用 attribute 继承的常见情况是需要将 attribute 应用于根节点之外的其他元素。

通过将 inheritAttrs 选项设置为 false,使用 $attrs 或者 context.attrs 获取所有属性;使用 v-bind="$attrs" 批量绑定属性。可以使用...剩余操作符(ES6)将属性分开,并且绑定到其他元素。

封装组件思路

  • UI 库的 CSS 注意事项
  1. 不能使用 scoped:因为 data-v-xxx中的xxx每次运行可能不同,我们必须输出稳定不变的 class 选择器来方便使用者覆盖;
  2. 必须添加前缀。添加一个特有的前缀,不容易被覆盖。
  3. CSS最小影响原则:即给组件库添加的样式不能对用户的样式造成影响。
  • Button组件

根据外部传入的参数,给button添加不同的主题、尺寸,是否禁用等。主要是对CSS的设计。

<template>
  <button class="easy-button" :class="classes" :disabled="disabled">
    <span v-if="loading" class="easy-loadingIndicator"></span>
    <slot/>
  </button>
</template>

使用方法:

<Button size="small" disabled>小号禁用按钮</Button>
  • Switch组件

控制该组件状态的 value 由外部传入,通过 v-model 进行双向绑定。

<template>
  <button class="easy-switch"
          @click="toggle"
          :class="{'easy-checked': value}">
    <span></span>
  </button>
</template>

<script lang="ts">
export default {
  props: {
    value: Boolean,
  },
  setup(props, context) {
    const toggle = () => {
      context.emit("update:value", !props.value);
    };
    return {toggle};
  }
};
</script>

使用方法:

<template>
  <Switch v-model:value="bool" />
</template>

<script lang="ts">
import Switch from '../lib/Switch.vue'
import {
  ref
} from 'vue'
export default {
  components: {
    Switch,
  },
  setup() {
    const bool = ref(false)
    return {
      bool
    }
  }
}
</script>
  • Dialog组件

该组件实现模态框的基本样式和框架, 由用户传入各部分内容。使用到了具名插槽。

<div class="easy-dialog">
  <header>
    <slot name="title"/>
    <span @click="close" class="easy-dialog-close"></span>
  </header>
  <main>
    <slot name="content"/>
  </main>
  <footer>
    <Button theme="main" @click="ok">OK</Button>
    <Button @click="cancel">Cancel</Button>
  </footer>
</div>

v-slot指定 name

<Dialog v-model:visible="x" :closeOnClickOverlay="false" :ok="f1" :cancel="f2">
  <template v-slot:content>
    <div>正文部分</div>
  </template>
  <template v-slot:title>
    <strong>标题</strong>
  </template>
</Dialog>

Teleport 包裹 Dialog

Teleport 译为传送,Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML,而不必求助于全局状态或将其拆分为两个组件。由于层叠上下文的原因,我们在使用 Dialog 的时候,可能会出现其他元素覆盖在 Dialog 之上的情况。而子元素的嵌套,使得设置 z-index 变得困难。 Teleport可以帮助我们选择将 Dialog 渲染到任何指定的位置。简单来说就是,既希望继续在组件内部使用Dialog, 又希望渲染的 DOM 结构不嵌套在组件的 DOM 中

  • Tabs组件 使用方法:
<template>
<Tabs v-model:selected="x">
  <Tab title="导航1">内容1</Tab>
  <Tab title="导航2">内容2</Tab>
</Tabs>
</template>

需要用context.slots.default()获取到 slots传来的内容。对其内容进行判断,只有确保子组件为 Tab, 才能进行渲染。根据外部传入的 selected 显示对应的内容。需要遍历子组件的所有 title, 找到与 selected 对应的那一个进行标记,然后添加被选中的样式。

<div class="easy-tabs">
  <div class="easy-tabs-nav" ref="container">
    <div class="easy-tabs-nav-item"
         v-for="(t, index) in titles"
         :ref="el => { if(t===selected) selectedItem = el }"
         @click="select(t)"
         :class="{selected: t === selected}"
         :key="index">
      {{ t }}
    </div>
    <div class="easy-tabs-nav-indicator" ref="indicator"></div>
  </div>
  <div class="easy-tabs-content">
    <component :is="current" :key="current.props.title"/>
  </div>
</div>

以上就是几个组件的整体设计思路。具体不再赘述,详细代码可点击源代码查看。预览链接

总结

封装属于自己的组件库可以更高效的开发,在未来的学习中,也要不断总结、继续扩充它们。