Vue 3 造轮子回顾

186 阅读2分钟
组件涉及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);
  }
  // 02button有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/加载中

image.png

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 按钮

image.png

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切换内容 有一条横线在动

105.png

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

111.png

//Tabs.vue
<component :is="defaults[0]" />  
// 改写为
<component v-for="c in defaults" :is="c" />  
//Tab.vue
<template>
 <div>
     <slot />
 </div>
</template>
  • 显示title

103.png

//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