浅谈 vue 3.2 单文件组件和setup

在vue 3.0时代加入了组合式API,其setup语法大大改变了vue2.x的一些使用方式,接下来就简单列举一些setup的使用方法。

一、“我全都要!”

在vue2.x时,所有的参数都是在data里先进行初始化声明,然后其他地方再进行调用,有专门的props接收父组件传递过来的参数,有methods负责管理所有的执行方法,以及每个生命周期函数。而在vue3.x里,data,methods那些还是继续保留,但现在一个setup就能在里面使用那些方法。

<script lang="ts">
import { defineComponent, reactive, toRefs, ref, onMounted } from "vue";
export default defineComponent({
props: {
    detailQueryParams: {
      type: Object,
      required: true
    }
  },
  setup(props) {
    const { detailQueryParams } = toRefs(props);

    const unCommitParams = Object.assign(detailQueryParams.value);
    const areaTypeList = useAreaType();

    const UncommittedTableData: ItableData<PartialIWeeklyStatisticDetail> = {
      data: [],
      columns: useWeeklyStatisticItemTableColumn("Uncommitted")
    };

    const tableRequestFunc = getWeeklyUncommit;
    const usetableFunc = useTableData;
    const failSimpleTableRef = ref<ComponentRef>(null);

    const reactiveData = reactive({
      UncommittedTableData,
      unCommitParams,
      areaTypeList,
      tableRequestFunc,
      usetableFunc
    });

    const dataRefs = toRefs(reactiveData);
    const methods = {
      loadTableData():void {
        const simpleTable = failSimpleTableRef.value as any;
        simpleTable.loadSimpleTableData();
      },

      onSelectStoreType(): void {
        methods.loadTableData();
      }
    };

    onMounted(() => {
      methods.loadTableData();
    });

    return {
      ...dataRefs,
      ...methods,
      failSimpleTableRef
    };
  }
})

上面这段代码是vue3.x + ts 的使用方式,我们可以看到的一些异同点。首先props还是2.x的用法,接收父组件传递过来的参,而在setup中我们看到了一些不一样的内容,比如生命周期函数的名称,还有作为行参的props和reactive和toRefs等,下面我们会逐个介绍它们。

二、setup入参

在最新的官方文档中,已经取消了attr属性,只保留了 props和context这两个参数。补充一点,由于setup的特殊机制, 在其内部,this 不是该活跃实例的引用,因为 setup() 是在解析其它组件选项之前被调用的,所以 setup() 内部的 this 的行为与其它选项中的 this 完全不同。这使得 setup() 在和其它选项式 API 一起使用时可能会导致混淆。翻译成人话就是,以前调用props的参数直接this.xxx就拿到数据了,现在在setup里面就不好使了,只能以参数的形式去获取props内的数据。
在3.2.6版本的源码里setup是这样的一个形式

setup?: (this: void, props: Readonly<LooseRequired<Props & UnionToIntersection<ExtractOptionProp<Mixin>> & UnionToIntersection<ExtractOptionProp<Extends>>>>, ctx: SetupContext<E>) => Promise<RawBindings> | RawBindings | RenderFunction | void;

在上面的代码中我们了解到了第一个参数props,还有第二个参数context。
context 是一个普通 JavaScript 对象,它里面包含了以下的值

// Attribute (非响应式对象,等同于 $attrs)
console.log(context.attrs)

// 插槽 (非响应式对象,等同于 $slots)
console.log(context.slots)

// 触发事件 (方法,等同于 $emit)
console.log(context.emit)

// 暴露公共 property (函数)
console.log(context.expose)

由于它们都只是普通的js对象,并不是一个响应式数据,所以可以对他们使用解构方法

setup(props, { attrs, slots, emit, expose }) {
   ...
 }setup(props, content) {
   const { attrs, slots, emit, expose } = content
 }

这里要注意一下,attrs 和 slots 是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以 attrs.x 或 slots.x 的方式引用 property。请注意,与 props 不同,attrs 和 slots 的 property 是非响应式的。如果你打算根据 attrs 或 slots 的更改应用副作用,那么应该在 onBeforeUpdate 生命周期钩子中执行此操作。

三、按需引入

我们看完了setup的类型结构之后就应该明白setup(props)这段代码的意思了。接下来是

const { detailQueryParams } = toRefs(props);

由于我们的props有时是需要保持一个数据的响应性,如果使用ES6的解构方法就会使其失去响应性,于是这个时候就有了toRefs的出现。
我们看到的toRefs, reactive是vue3新增的响应式api,更多响应式api可以移步官方文档查看

toRefs 的官方定义是将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的 ref,所以我们这样消费组件就可以在不丢失响应性的情况下对返回的对象进行解构/展开。并且vue3现在是根据业务场景的需要去引入对应的api,这样的方式大大优化了页面的性能。

四、单文件组件

在vue3.x 提出了单文件组件(SFC),有两种方式。

  • 普通 script: 其默认导出的内容应该是 Vue 组件选项对象,它要么是一个普通的对象,要么是 defineComponent 的返回值, 比如上面的那段代码
  • script setup 形式:
    1. 每个 *.vue 文件最多可同时包含一个 script setup 块 (不包括常规的 script 标签)
    2. 该脚本会被预处理并作为组件的 setup() 函数使用,也就是说它会在每个组件实例中执行。script setup 的顶层绑定会自动暴露给模板

也就是说当使用setup的单文件时,无需defineComponent和return各种返回值,达到代码量更简洁的效果

<script lang="ts" setup>
import { ref, withDefaults, defineProps, defineEmits } from "vue";
import { IselectCommon } from "/@/model/app-modules";
import { IEditDepartmentsType } from "/@/model/system/api";
import { ElForm } from "element-plus";
import { useMessage } from "/@/hooks/common";


// 把 props的interface抽出去,但是会报错,如果interface组件里定义的就正常渲染
// https://github.com/vuejs/vue-next/issues/4294  How to import interface for defineProps #4294
interface IOrganizetionOptionProp {
  drawerVisiable: boolean;
  departmentTypes: IselectCommon[];
  recordItem: Iobj | null;
}

const privateProps = withDefaults(defineProps<IOrganizetionOptionProp>(), {
  drawerVisiable: false,
  departmentTypes: () => {
    const list: IselectCommon[] = [];
    return list;
  }
});

const privateEmit = defineEmits<{ (e: "update:drawerVisiable", drawerVisiable): void; (e: "closeOption"): void }>();

const { createMessage } = useMessage();
const organizationFormRef = ref<typeof ElForm>();

const optionForm = ref<IEditDepartmentsType>({
  id: 0,
  type: "",
  typeName: ""
});

const organizationFormRuls = {
  isBranch: [
    {
      require: true,
      message: "请选择门店",
      trigger: "change"
    }
  ],
  type: [
    {
      require: true,
      message: "请选择部门类型",
      trigger: "change"
    }
  ]
};

function onChangeDepartment(value: string): void {
  const departmentItem = privateProps.departmentTypes.find((item) => item.value === value);
  optionForm.value.typeName = departmentItem?.label as string;
}

function onCancelForm() {
  privateEmit("update:drawerVisiable", !privateProps.drawerVisiable);
}
function onSubmitForm() {
  const organizationForm = organizationFormRef.value as any;
  const isValidForm = organizationForm.validate();
  if (isValidForm) {
    optionForm.value.id = privateProps.recordItem?.id;
    setOrganizationType(optionForm.value).then((res) => {
      const { code } = res;
      if (!code) {
        createMessage.success("修改成功");
        privateEmit("update:drawerVisiable", !privateProps.drawerVisiable);
        privateEmit("closeOption");
      }
    });
  }
}
</script>
<template>
  <div>
    <el-form :model="optionForm" ref="organizationFormRef" :rules="organizationFormRuls">
      <el-form-item label="部门类型" prop="type">
        <el-select clearable v-model="optionForm.type" @change="onChangeDepartment" class="w100">
          <el-option
            v-for="item in privateProps.departmentTypes"
            :key="item.id"
            :label="item.label"
            :value="item.value"
          ></el-option>
        </el-select>
      </el-form-item>
    </el-form>
    <div class="form-btn">
      <el-button size="mini" @click="onCancelForm">取消</el-button>
      <el-button size="mini" type="primary" @click="onSubmitForm">保存</el-button>
    </div>
  </div>
</template>
<style lang="scss" scoped>
.form-btn {
  text-align: right;
}
</style>

在vue3.2.x新增了defineProps, defineEmits和withDefaults 语法糖,它们是服务于 script setup的,并且必须使用 defineProps 和 defineEmits API 来声明 props 和 emits

js使用时

const props = defineProps({
  foo: String
})

const emit = defineEmits(['change', 'delete'])
// setup code

ts使用时

 withDefaults(defineProps<{
   size?: number
   labels?: string[]
 }>(), {
   size: 3,
   labels: () => ['default label']
 })

// 还可用 PropType 配合 TS 进行接口类型管理
const failTableProps = defineProps({
 // 时间范围
 rangeDateTime: {
   type: Array as PropType<string[]>,
   required: true
 }
});

const emit = defineEmits<{
   (event: 'change'): void
   (event: 'update', id: number): void
 }>()

 emit('change')
 emit('update', 1)

注:

  • defineProps 和 defineEmits 都是只在 script setup 中才能使用的编译器宏。他们不需要导入且会随着 script setup 处理过程一同被编译掉。
  • defineProps 接收与 props 选项相同的值,defineEmits 也接收 emits 选项相同的值。
  • defineProps 和 defineEmits 在选项传入后,会提供恰当的类型推断。
  • 传入到 defineProps 和 defineEmits 的选项会从 setup 中提升到模块的范围。因此,传入的选项不能引用在 setup 范围中声明的局部变量。这样做会引起编译错误。但是,它可以引用导入的绑定,因为它们也在模块范围内。
五、其他一些setup里的调用方式
<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()
</script>

useSlots 和 useAttrs 是真实的运行时函数,它会返回与 setupContext.slots 和 setupContext.attrs 等价的值

vue-router
<script setup>
import {useRoute,useRouter} from "vue-router";

// 建议不要用$route = useRoute() 的形式,因为$route还算是保留的关键词,尽量在命名时以私有形式命名
const privateRoute = useRoute();
const privateRouter = useRouter();
</script>

useRoute :返回当前路由地址。相当于在模板中使用 $route
useRouter: 返回 router 实例。相当于在模板中使用 $router

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值