在自己之前做项目的时候也接触过几个优秀的组件库,比如 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 的步骤
- 创建 history 对象
- 创建 router 对象
app.use(router)- 添加
<router-view>用来显示路由对应的页面 - 添加
<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 解构,解构会消除它的响应式。context:context是一个普通的 JavaScript 对象,它暴露组件的三个 property:attrs、slots和emit(非响应式对象)。 可以使用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 注意事项
- 不能使用
scoped:因为data-v-xxx中的xxx每次运行可能不同,我们必须输出稳定不变的 class 选择器来方便使用者覆盖; - 必须添加前缀。添加一个特有的前缀,不容易被覆盖。
- 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>
以上就是几个组件的整体设计思路。具体不再赘述,详细代码可点击源代码查看。预览链接。
总结
封装属于自己的组件库可以更高效的开发,在未来的学习中,也要不断总结、继续扩充它们。