经验分享
Vue3组件的高级用法 - 函数式调用
H1. 面向函数封装H1.1. 需求目前有这么两个组件: webxue-drawer.vue <script setup lang="ts"> /** * 抽屉组件 * 应用场景:新增、编
2023-05-21 13:16:38
85

H1. 面向函数封装

H1.1. 需求

目前有这么两个组件:

  • webxue-drawer.vue
<script setup lang="ts">
/**
 * 抽屉组件
 *    应用场景:新增、编辑表单内容
 * 组件属性继承于a-drawer
 * 额外添加了一个事件
 *    ok:当点击右下角确认按钮时触发
 * */

const emits = defineEmits(["update:visible","ok"]);

const getContainer = () => document.querySelector(".webxue-drawer");

const onClose = () => {
  emits("update:visible",false)
};
const onOk = () => emits("ok");
</script>

<template>
  <div class="webxue-drawer">
    <a-drawer width="max-content" @close="onClose" :closable="false" :get-container="getContainer" v-bind="$attrs">
      <!-- 自定义header -->
      <template #title>
        <div class="webxue-drawer-title">
          <slot name="headerLeft"></slot>
        </div>
      </template>
      <!-- 右上角扩展图标 -->
      <template #extra>
        <div class="extra-btn" @click="onClose"><CloseOutlined/></div>
      </template>
      <!-- 自定义内容区域 -->
      <div class="webxue-drawer-content">
        <slot></slot>
      </div>
      <!-- 自定义footer -->
      <template #footer>
        <div class="glc-drawer-footer">
          <a-button @click="onClose"><CloseOutlined />取消</a-button>
          <a-button type="primary" @click="onOk"><CheckOutlined />确认</a-button>
        </div>
      </template>
    </a-drawer>
  </div>
</template>

<style lang="less" scoped>
.webxue-drawer{
  background-color: #fff;
  .extra-btn{
    cursor:pointer;
  }
  .webxue-drawer-content{
    min-width: 30vw;
  }
  .webxue-drawer-footer{
    display: flex;
    justify-content: flex-end;
    .ant-btn + .ant-btn{
      margin-left: 8px;
    }
  }
}
</style>
  • webxue-view.vue
<script setup lang="ts">
const submit = () => console.log("我被点击了");
</script>

<template>
  <div>
    <a-button type="primary" @click="submit">点击</a-button>
  </div>
</template>

这是两个 SFC 组件,需求是:

在自定义页面中,通过函数调用的方式挂载并渲染 webxue-drawer 组件,并且可以为 webxue-drawer 组件 传递参数插槽,且能 监听 到组件 emit的事件,并且点击 webxue-drawer 组件的确认按钮可以调用 默认插槽 的方法。

H1.2. 实现

H1.2.1 编写方法函数

utils 目录下新建一个 Render.ts 文件:

// 引入vue的h函数、createApp函数、ref响应式变量
import { h,createApp,ref } from "vue";
// 引入函数式调用的SFC组件
import WebDrawer from "@/components/demo-comp/webxue-drawer/index.vue";
// 引入SFC组件所依赖的组件
import { Drawer,Button } from "ant-design-vue";
import { CloseOutlined,CheckOutlined } from "@ant-design/icons-vue";

/**
 * 函数式打开抽屉的方法
 * @param {WebxueDrawerProp} data
 * @param {HTMLElement} data.el - 组件挂载元素
 * @param {any} data.headerLeftSlot- headerLeft插槽
 * @param {any} data.defaultSlot - default插槽
 * @param {(self:any) => any} data.onConfirm - 确认按钮的回调
 * */
export const WebxueDrawer = ({ el,headerLeftSlot,defaultSlot,onConfirm }:WebxueDrawerProp) => {

  // 创建一个组件
  const app = createApp({
    setup(){
      // 打开状态
      const showModal = ref(true);

      return {
        showModal
      }
    },
    // 渲染函数
    render(){
      return h(WebDrawer,{
        // 组件打开状态
        visible:this.showModal,
        // 组件内部触发update:visible事件
        'onUpdate:visible':(nval) => {
          this.showModal = nval;
        },
        // 抽屉关闭动画结束卸载组件
        onAfterVisibleChange(visible){
          if(!visible) app.unmount();
        },
        // ok时执行回调
        onOk:() => {
          onConfirm && onConfirm(this);
        },
      },{
        // 默认插槽
        default:() => this.renderSlot(defaultSlot),
        // headerLeft插槽
        headerLeft:() => this.renderSlot(headerLeftSlot)
      })
    },
    methods:{
      // 渲染插槽方法
      renderSlot(slotBlock){
        if(!slotBlock) return null;
        if (typeof slotBlock === 'string') {
          return h('div', { innerHTML: slotBlock });
        } else {
          return slotBlock();
        }
      }
    }
  })

  // 所依赖的组件及插件注册
  app.component("CloseOutlined",CloseOutlined);
  app.component("CheckOutlined",CheckOutlined);
  app.use(Drawer).use(Button);
  // 组件挂载
  app.mount(el);
}

export interface WebxueDrawerProp {
  // 挂载元素
  el:HTMLElement,
  // headerLeft插槽
  headerLeftSlot?:any,
  // default插槽
  defaultSlot?:any,
  // 确定按钮的回调
  onConfirm?:(self:any) => any
}

H1.2.2 调用方法

接下来在我们页面中通过调用这个方法来挂载并渲染组件,给组件传参,传递插槽,调用插槽的方法:

<script setup lang="ts">
// 引入h方法是为了给组件的default插槽传入一个组件
import { h } from "vue";
// 引入函数式调用组件的方法
import { WebxueDrawer } from "@/utils/Render";
// default插槽组件
import WebxueDemo from "@/components/demo-comp/webxue-demo/index.vue";

// 点击事件执行
const onOpenShowDrawer = () => {
  // 调用这个方法
  WebxueDrawer({
    // 传入挂载到的元素
    el:document.querySelector(".demo-drawer"),
    // 默认插槽传入一个组件,并为其设置ref属性
    defaultSlot:() => h(WebxueDemo,{
      ref:"webxueDemoRef",
    }),
    // headerLeft插槽,传入html字符串将使用innerHTML渲染
    headerLeftSlot:"<h1 class='h1-class'>哈哈哈</h1>",
    // 确定按钮的回调
    onConfirm(self){
      // self就是组件对象,组件对象下由上面组件定义的ref
      const { $refs } = self;
      // 可以通过ref调用插槽组件的方法,但需要defineExpose,详情看第三步
      console.log($refs.webxueDemoRef.submit())
      // 可以通过设置组件的showModal为false来关闭抽屉
      self.showModal = false;
    }
  })
}
</script>

<template>
  <div class="demo-render">
    <!-- 点击这个按钮函数式打开抽屉 -->
    <a-button type="primary" @click="onOpenShowDrawer">打开</a-button>
    <!-- 定义这个div是为了挂载抽屉 -->
    <div class="demo-drawer"></div>
  </div>
</template>

<style lang="less" scoped>
/* 要给webxue-drawer的插槽设置样式,需要被穿透 */
:deep(.h1-class){
  color: #f00;
}
</style>

H1.2.3 default插槽组件方法可被外部通过ref调用

我们需要在 webxue-view.vue 组件中加入一行代码:

// 将submit方法暴露出去,然后在父组件中就可以通过ref.submit调用
defineExpose({ submit })

H2. 面向对象封装

H2.1 需求

有一个 loading 组件,现需要通过函数式调用的方式来打开和关闭 loading

<script setup lang="ts">
/**
 * 全局loading组件
 * */

defineProps({
  // loading状态
  loading:{ type:Boolean,default:false },
  // loading文字
  loadingText:{ type:String,default:"加载中..." }
})
</script>

<template>
  <div class="webxue-loading" v-if="loading">
    <div class="webxue-loading-mask"></div>
    <div class="webxue-loading-wrap">
      <div class="loading-box">
        <img class="loading-bg" src="@/assets/images/loading-bg.png" alt="">
        <img class="loading-icon" src="@/assets/images/loading-icon.png" alt="">
      </div>
      <div class="loading-text-box">
        <div class="loading-text">{{ loadingText }}</div>
      </div>
    </div>
  </div>
</template>

<style lang="less" scoped>
@keyframes rotateFin {
  from{ transform: rotate(0deg); }
  to{ transform: rotate(360deg); }
}
.webxue-loading{ position: fixed;top: 0;right: 0;bottom: 0;left: 0;z-index: 1000;
  .webxue-loading-mask{ position: absolute;left: 0;right: 0;top: 0;bottom: 0;background-color: rgba(0,0,0,.73); }
  .webxue-loading-wrap{ display: flex;flex-direction: column;align-items: center;height: 100%;padding-top: 15vh;
    .loading-box{ display: flex;align-items: center;position: relative;margin-bottom: 20px;
      .loading-bg{ width: 120px;animation:rotateFin 1.5s linear infinite; }
      .loading-icon{ position: absolute;top: 50%;left: 50%;transform: translate(-50%,-50%); }
    }
    .loading-text-box{ font-size: 16px;color: #fff;z-index: 1; }
  }
}
</style>

H2.2 与示例一对比

  1. 传入挂载节点el,示例二直接主动生成挂载节点,支持传参挂载节点
  2. 示例二使用面向对象的方式,更容易阅读和维护
  3. ref直接定义为class的属性,他本身就是个响应式变量的包裹器,所以可以不用放在setup

H2.3 实现

H2.3.1 定义方法

在组件同级目录下,定义一个 render.ts 文件

/**
 * 函数式调用loading
 * */
import webxueLoading from "./index.vue";
import { createApp, h, ref } from "vue";

export class WebxueLoading {

  // loading组件对象
  private loadingInstance = null;

  // 挂载节点
  private el = null;

  // loading状态
  private showLoading = ref(false);

  constructor(el?:HTMLElement) {
    if(el) this.el = el;
    this.init();
  }

  init(){
    const _this = this;
    this.initEl();

    this.loadingInstance = createApp({
      render(){
        return h(WebxueLoading ,{
          loading:_this.showLoading.value
        })
      }
    })
    this.loadingInstance.mount(this.el);
  }

  // 挂载根
  initEl(){
    if(this.el) return;
    let rootDiv = document.querySelector(".webxue-loading-root");
    if(rootDiv) return;
    rootDiv = document.createElement("div");
    rootDiv.className = "webxue-loading-root";
    document.body.append(rootDiv);
    this.el = rootDiv;
  }

  open(){
    this.showLoading.value = true;
  }

  close(){
    this.showLoading.value = false;
    this.el.remove();
    this.loadingInstance.unmount();
  }
}

H2.3.2 调用方法

假设有一个按钮,这个按钮点击之后会 打开loading,然后请求数据,数据请求完成,关闭loading,这里我就只写点击事件,请求数据我使用定时器来延时

const onOpenLoading = () => {
  const loading = new WebxueLoading();
  loading.open();
  setTimeout(() => {
    loading.close();
  },1500)
}