| 组件 | 涉及vue用法 | 组件用法 | 属性和事件 | |
|---|---|---|---|---|
| Switch开关 | 组件间通信(常规方法): 用 props实现传值用 context.emit 实现事件绑定 | <Switch :value="y" @update:value="y = $event" />简写为 <Switch v-model:value="bool" /> | v-model:value="bool" | |
| Button按钮 | 组件间通信(升级方法): 用 attrs获取所有属性及事件 | <Button theme="button">你好</Button><Button theme="button">你好</Button> | @click=? @focus=? @mouseover=? theme="button/link/text" level="main/normal/min" size="big/normal/small" | |
| Dialog弹窗 | --- | -- | :visible="true" title="标题" @ok @cancel | |
| Tabs选项卡 | --- | -- |
1. Switch 开关
1.1 UI设计(组件长啥样)
1.2 API设计(组件怎么用)
- 以下两种情况时,显示为开
// SwitchDemo.vue
<Switch value="true" /> //不加冒号 value为字符串"true"
<Switch :value="true" /> //加冒号 value为布尔值true 引号里内容为JS表达式
1.3 实现 Switch 最底层组件
// SwitchDemo.vue 父组件
<template>
<div>
<Switch />
</div>
</template>
<script lang="ts">
import Switch from '../lib/Switch.vue'
export default {
components:{Switch}
}
</script>
// switch.vue 子组件
<template>
<div>
// 01 若x为true .button=checked 否则没有checked的类
<button :class="{checked:x}">
/* 02 绑定单击事件 */
<button @click="toggle" :class="{checked:x}">
/* 03 x命名为checked */
<button @click="toggle" :class="{checked}">
<span>
</span>
</button>
</div>
</template>
<script lang="ts">
imort {ref} from "vue" //ref添加内部数据
export default{
setup(){
const x = ref(false)
const toggle = ()=>{
x.value = !x.value //不能直接用x取反 因为x是个盒子 x.value可以取反
}
return {x, toggle}
}
// x 命名为 checked 后
setup(){
const checked = ref(false)
const toggle = ()=>{
checked.value = !checked.value
}
return {checked, toggle}
}
}
// 1. 初步实现初始switch样式和鼠标悬浮hover时的样式
<style lang="scss" scoped>
$h:22px;
$h2:$h - 4px;
button{ //开关组件的长条型外框
height:$h;
width:$h*2;
border:none;
background:grey;
border-radius:$h/2;
position:relative;
}
sapn{ //能移动的圆点
position:absolute;
top:2px;
left:2px;
height:18px;
width:18px;
bacjground:white;
border-radius:9px;
transition:left 250ms; //left有变化时的动画
}
// 01 当鼠标悬浮上时,圆点就自动移到最右侧
button:hover > span{
left:calc(100% - #{$h2} - 2px);
}
// 02 当button有checked的类时 圆点自动移到最右侧
button.checked > span{
left:calc(100% - #{$h2} - 2px);
}
button:focus{ //去掉默认的外框虚线
outline:none;
}
</style>
1.4 控制初始及更新状态
在使用者引用组件时想控制开关的初始状态或更新状态,使用者获取开关切换的状态
// SwitchDemo.vue
// value变化时,获取到最新的值用 $event表示
<Switch :value="y" @input="y = $event" />
setup(){
const y = ref(false);
return {y}
}
1.5 组件间通信
- 用
props实现组件间通信,子组件switch.vue接受父组件SwitchDemo.vue传入的value - 用
context.emit(事件名, 事件参数)实现子组件switch.vue发出event事件
// Switch.vue
<button @click="toggle" :class="{checked:value}">
props:{
value:Boolean;
},
setup(props, context){
// 把当前值取反 通过input事件发给外面
const toggle =()=>{
context.emit('input', !props.value)
// 事件'input'对应外界的 @input
// !props.value对应外界的 $event的值
// context.emit 作用等同于 this.$emit(这是Vue 2 的写法)
}
returm {toggle}
}
- 用
v-model对父子间数据交流进行简化
context.emit('input', !props.value)
// 属性名假设为x,事件名必须为update:x
context.emit('update:value', !props.value)
<Switch :value="y" @input="y = $event" />
//改写为
<Switch :value="y" @update:value="y = $event" />
//简写为
<Switch v-model:value="y" />
//进一步完善为
<Switch v-model:value="bool" />
2. Button 按钮
2.1 UI设计(组件长啥样)
- 需求 不同等级/链接或文字/click或focus或鼠标悬浮/size/加载中
2.2 API设计 (组件怎么用)
<Button @click=? @focus=? @mouseover=?
theme="button/link/text"
level="main/normal/min"
size="big/normal/small"
disabled
loading
></Button>
2.3 初始化
// ButtonDemo.vue 父组件
<template>
<div>Button 示例</div>
<h1>示例1</h1>
<div>
<!-- 让 Button 支持事件 -->
<Button @click="onClick">你好</Button>
</div>
</template>
<script lang="ts">
import Switch from '../lib/Button.vue'
export default {
components:{Button},
setup(){
const onClick = ()=>{
}
return {onClick}
}
}
</script>
2.4 默认继承属性
- Vue可以把外界的
onClick方法等所有属性都默认绑定到底层组件根元素<button>上
// src/lib/Button.vue 最底层组件
<template>
<button>
<slot />
</button>
</template>
- 若根元素
<button>外层有<div>包裹,如何让div不继承属性?
先让div不继承外部绑定的属性和事件,再让div里的button绑定v-bind="$attrs"批量绑定属性;
使用
context.attrs或$attrs获取所有属性。 使用const{size, ...rest}=context.attrs将属性分开
// src/lib/Button.vue 最底层组件
<template>
<div :size="size">
// 使用 $attrs 或 context.attrs 获取所有属性
<button v-bind="$attrs"> //批量绑定属性
<button v-bind="rest"> //绑定除size外 其余属性
<slot />
</button>
</div>
</template>
<script lang="ts">
export default {
inheritAttrs: false //取消默认所有绑定
setup(props, context){
// 单独把size 取出来 其余的都放在rest变量里
const{size , ...rest} = context.attrs
return {size, rest}
}
}
2.5 支持theme属性
// ButtonDemo.vue
<Button theme="button">你好</Button>
<Button theme="link">你好</Button>
<Button theme="text">你好</Button>
//Button.vue
<button class="gulu-button" :class="`theme-${theme}`">
//或写为下面形式 若theme为undefined 则把class关掉
<button class="gulu-button" :class="{[`theme-${theme}`]:theme}">
props:{
theme:{
type:String,
default:'button'
}
}
2.6 支持size属性,配合css写对应的样式
//Button.vue
<button class="gulu-button" :class="classes">
props:{
theme:{
type:String,
default:'button',
},
size:{
type:String;
default:"normal",
}
}
setup(props){
const {theme, size} = props
const classes = computed(()=>{
return {
[`gulu-theme-${theme}`]:theme,
[`gulu-size-${size}`]:size,
};
});
return {classes};
}
// ButtonDemo.vue
<Button size="big">你好</Button>
<Button size="normal">你好</Button>
<Button size="small">你好</Button>
2.7 支持level属性,配合css写对应的样式
// ButtonDemo.vue
props:{
level:{
type:String;
default:"normal",
}
}
<Button level="main">主要按钮</Button>
<Button level="danger">危险按钮</Button>
2.8 支持disabled属性
// ButtonDemo.vue
<Button disabled>Hi</Button>
// ButtonDemo.vue
<button class="gulu-button" :class="classes"
:disabled:"disabled">
props:{
disabled:{
type:Boolean;
default:false,
}
}
2.9 支持loading属性
// ButtonDemo.vue
<Button loading>加载中</Button>
// ButtonDemo.vue
<button class="gulu-button" :class="classes"
:disabled:"disabled">
<span v-if="loading" class=gulu-loadingIndicator></span>
<slot/>
</button>
props:{
loading:{
type:Boolean;
default:false,
}
}
3. Dialog 对话框
3.1 UI设计(组件长啥样)
- 借鉴 AntD 或 Vant的
- 需求 点击后弹出对话框,有遮罩层overlay
- 对话框元素:有close按钮/有标题 内容 yes/no 按钮
3.2 API设计 (组件怎么用)
<Dialog @yes="fn1" @no="fn2"
:visible="true"
title="标题"
></Dialog>
3.3 初始化
// Dialog.vue
<div class="gulu-dialog-overlay"></div>
<div class="gulu-dialog-wrapper">
<header>标题</header>
<main>
<p>第一行字</p>
<p>第二行字</p>
</main>
<footer>
<Botton>OK</Botton>
<Botton>Cancel</Botton>
</footer>
</div>
3.4 支持visible属性
//Dialog.vue
<template>
<template v-if="visible"> // 新增对话框外层(透明层)
...
</template>
<template>
props:{
visible:{
type:Boolean;
default:false
}
}
//DialogDemo.vue
<Button @click="toggle">toggle</Button>
<Dialog :visible="x"></Dialog>
setup(){
const x = ref(false)
const toggle = ()=>{
x.value = !x.value
}
return {x, toggle}
}
3.5 支持点击关闭
点击 close按钮,OK(函数),Cancel(函数) 三处都能实现关闭
//Dialog.vue
<div class="gulu-dialog-overlay" @click="closeOnClickOverlay">
<span @click="" class="gulu-dialog-close"></span>
props:{
// 是否做到点击遮罩层关闭对话框 默认是
closeOnClickOverlay:{
type:Boolean,
default:true
},
ok:{
type:Function,
},
cancel:{
type:Function,
}
}
setup(props, context){
const close = ()=>{
context.emit('update:visible', false)
}
const closeOnClickOverlay = ()=>{
// 如果开启closeOnClickOverlay功能 就调用close
if(props.closeOnClickOverlay){
close()
}
}
// ok 通过 return false 阻止其关闭
const ok = ()=>{
// props.ok存在 且 ok()执行后不等于false
if(props.ok?.() !=== false){
close()
}
}
const cancel = ()=>{
context.emit('cancel')
close()
}
return {close, closeOnClickOverlay, ok, cancel}
}
//DialogDemo.vue
<Dialog v-model:visible="x"
:closeOnClickOverlay="false"
:ok="f1"
:cancel="f2"></Dialog>
setup(){
const x = ref(false)
const toggle = ()=>{
x.value = !x.value
}
const f1 = ()=>{
return false // 对话框内容填满就 return true
}
const f2 = ()=>{
}
return {x, toggle, f1, f2}
}
3.6 支持自定义内容(title和content)使用插槽slot
- title:支持字符串
- content:支持标签
//DialogDemo.vue
<Dialog v-model:visible="x"
:closeOnClickOverlay="false"
:ok="f1"
:cancel="f2">
<div>hi</div> // 在底层组件中用slot插槽填充
<div>hi2</div>
</Dialog>
//Dialog.vue
<header>{{title}}</header>
<main>
<slot/> // 实现内容自定义
</main>
<footer>
props:{
title:{
type:String
default:'提示'
}
}
- title:支持标签 (使用具名插槽)
外层(用户直接使用) v-slot:content
底层组件 <slot name="content" />
//DialogDemo.vue
<template v-slot:content>
<strong>hi</strong>
<div>hi2</div>
</template>
<template v-slot:title>
<strong>加粗的标题</strong>
</template>
//Dialog.vue
<header>
<slot name="title" />
<span @click="close"></span>
</header>
<main>
<slot name="content" />
</main>
<footer>
props:{
title:{
type:String
default:'提示'
}
}
3.7 用Teleport传送 把Dialog移到body下 防止Dialog被遮挡
//Dialog.vue
<template>
<template v-if="visible">
<Teleport to="body">
<div class="gulu-dialog-overlay">
...
</Teleport>
</template>
<template>
4. Tabs 标签页
4.1 UI设计 (组件长啥样)
- 点击Tab切换内容 有一条横线在动
4.2 API设计 (组件怎么用)
// 用法1:本例中实现此用法
<Tabs>
<Tab title="导航1">内容1</Tab>
<Tab title="导航2"></Component1 /></Tab>
<Tab title="导航3"></Component1 x="hi" /></Tab>
</Tabs>
// 用法2:方便新增内容
<Tabs :data="[
{title="导航1", content:'内容1'},
{title="导航2", content: Component1},
{title="导航3", content: h(Component1, {x:'hi'})},
]"/>
4.3 初始化
//TabsDemo.vue
<template>
<div>Tabs 示例</div>
<h1>示例1</h1>
<Tabs>
<Tab>内容1</Tab> // 01 如果用户使用的是div
<Tab>内容2</Tab> // 02 如何检查子组件类型
</Tabs>
</template>
<script lang="ts">
import Tabs from '../lib/Tabs.vue'
import Tab from '../lib/Tab.vue'
export default {
components:{Tabs, Tab}
}
</script>
4.4 用户如果没用<Tab>,而是用<div>
- 如何检查子组件类型,用
content.slots.default()检查数组的每一项
每一个
.vue文件本质是对象
//Tabs.vue
<component :is="defaults[0]" /> // 展示Tab组件内容
<component :is="defaults[1]" />
setup(props, context){
const defaults = context.slots.default()
defaults.forEach((tag)=>{
if(tag.type !=== Tab){
throw new Error('Tabs 子标签必须是 Tab')
}
})
return {defaults}
}
4.5 渲染嵌套的组件 <嵌套插槽>
- 显示出 内容1、内容2
//Tabs.vue
<component :is="defaults[0]" />
// 改写为
<component v-for="c in defaults" :is="c" />
//Tab.vue
<template>
<div>
<slot />
</div>
</template>
- 显示
title
//Tabs.vue
<div v-for="(t,index) in titles" :key="index">{{t}}</div>
<component v-for="(c,index) in defaults" :is="c" :key="index" />
// 只要用 v-for 就得加个 key 上例中的 index 就是为了给 key 用
defaults.forEach((tag)=>{
console.log({...tag}) // tag 里的属性
console.log(tag.props.title)
})
// 上面改写为
const titles = defaults.map((tag)=>{
return tag.props.title
})
return {defaults, titles}
}
- 课程参考 jirengu.com