标签: defineProps

  • 理解 Vue3 里的 defineProps 和 defineEmits

    理解 Vue3 里的 defineProps 和 defineEmits

    大家请先看这个问题:https://segmentfault.com/q/1010000041497872/a-1020000041498716,看看你们能不能给出答案。

    Vue3 增加了 Composition API,是一个很大的改进。一方面可以提升代码复用效率,另一方面通过更好的 tree-shaking,打包体积也会小很多:作为参考,前面博客中提到 mywordle.org,打包后 vendor.js 只有区区 96kB,里面可是用到了 vue3 全家桶。

    最初的 Composition API 是在 Options API 基础上改进的,不仅需要使用 setup() 函数,还要在 setup() 末尾返回所有模版需要用到的变量和函数,使用起来相当繁琐。于是后面就增加了 <script setup> 语法糖:

    1. 从生命周期来讲,相当于 created
    2. 支持顶层 await(因为实际上这还是个 setup() 函数)
    3. 所有 import 的内容、声明的变量和函数默认都返回
    4. 至少省了两层缩进

    但是由于少了 export,没法传参,也不方便暴露接口,所以作者就增加了三个工具方法:

    • defineProps
    • defineEmits
    • defineExpose

    注意,这三个工具方法只是帮助 Vue 编译器构建组件,它们不会出现在最终代码里,我们也不能预期它们会像普通函数那样工作。比如下面这段代码,就得不到常见的结果:

    const props = defineProps({
      userMenu: {
        type: Array,
        default() {
          return []
        }
      }
    })
    console.log(props) // 该对象中的 userName 总是有值
    console.log(props.userMenu) // 该对象始终是一个空数据

    因为 Vue 是 MVVM 框架,它的视图会在数据变化后自动渲染,于是通常情况下,props 里的值什么时候被填充并不重要,Vue 开发团队也不想追求 defineProps 工作的一般化。所以使用目前版本,上面这段代码,访问到的 props 是的 reactive 对象,数据被填充后就能看到 userName 里有值;而 props.userMenu 在访问时还没有被填充,所以得到的是 default() 返回的默认值,一直是空的。

    同时大家还要知道,console.log() 输出的是对象的指针,而非快照。所以里面的值只跟你展开时有关,跟运行时关系不大。

  • Vue3 < script setup > + TypeScript 笔记

    Vue3 < script setup > + TypeScript 笔记

    近期把一个老项目(Vue 组件库)从 Options API 风格重构成了 <script setup> 风格 + TypeScript,写篇博客记录下坑和心得。

    注:这篇博客只记录超出我踩过坑和不熟悉的内容,符合直觉、看了文档写完一次过的部分,我就不记录了。

    0. Composition API 与 <script setup>

    Vue 3 给我们带来了 Composition API,改变了之前 Options API 使用 mixins 继承和扩展组件的开发模式。这种模式让前端在开发时复用组件、改进组件变得更容易。具体改进我在本文里不再详述,大家可以看下 Vue master 的视频教程:Why the Composition API

    之后尤大又提出了 script-setup rfc,进一步改进了使用 Composition API 的体验。简单总结下新提案:

    1. 使用 <script setup> 取代 setup(),里面定义的所有变量,包括函数,都可以直接在模版中使用,不需要手动 return
    2. 可以使用顶级 await
    3. <script setup> 可以与其它 <script> 在同一个 SFC 里共存

    实际体验之后,我觉得这个提案的确能节省不少代码,不过也会增加一些复杂度。

    1. 导出 name 等非模版属性

    <script setup>setup() 的语法糖,主要目的是降低 composition API 的代码复杂度。它直接向模板暴露整个 <script setup> 的上下文,所有定义过的变量自动导出供模版使用,这样我们就不需要手动一个一个 return 了。

    所以它就不适合导出其它非模版属性,比如组件名称 name。如有需要,可添加一个普通 <script> 节点,用常见的 export default 导出初始化对象。如下:

    <script>
    export default {
      name: 'SomeVue3Component',
    };
    </script>
    
    <script setup>
    import {ref} from 'vue';
    
    const foo = ref('bar');
    </script>

    2. defineProps/defineEmits

    要在 <script setup> 里使用 props(传入参数)和 emit(广播事件),需要使用 defineProps()defineEmits() 定义。但要注意,这俩东西其实是编译宏,并非真实函数,不能把它们当作函数来使用。所以也不需要 import,当它们是全局函数直接用就好。同时记得修改 .eslintrc.js 把它们添加到 global 里。

    最简单的使用方法如下,以前的 props 定义规则可以沿用。

    const props = defineProps({
      foo : {
        type: String,
        default: 'bar',
      },
    });
    const emit = defineEmits(['change']);

    要配合 TypeScript,通常需要使用 withDefaults()props 生成默认值。这也是个宏,不能当函数用,也不能使用当前环境里的变量。改造后的代码如下:

    interface Props = {
      foo: string;
    };
    const props = withDefaults(defineProps<Props>(), {
      foo: 'bar', // 'bar' 不能是变量
    });
    const {
      foo,
    } = toRefs(props);
    
    const emit = defineEmits<{
      (e:'change', value:string),
    }>();

    这个地方的设计相当不完善,我不知道 Vue 团队会如何改进这里。比如,我们不能使继承 interface,然后再初始化 props,因为继承是常规 ts 语句,而 defineProps 是编译宏,两者的工作环境不一样。而因为无法使用变量,我们也无法将父级组件的 props 混入本地 props。于是复用组件又变得麻烦起来。

    耐心等待吧。

    3. 使用 undefined 初始化对象

    定义 props 问题真不少。有一些参数是可选参数,不一定要定义,也不一定要用到;但是使用 ts 定义时,即使如此,也要初始化他们,可以传值为 undefined,不然 tsc 可能会报告错误:变量可能为空。之所以用 undefined,而不是 null,则是因为 TypeScript 认为 null 是独立类型。

    即:

    interface Props {
      foo?: string;
    }
    const props = withDefaults(defineProps<Props>(), {
      foo: undefined,
    });
    const {
      foo,
    } = toRefs(props);
    
    // 接下来才能正常使用
    const bar = computed(() => {
      return foo.value || 'bar';
    });

    4. 使用的变量未初始化错误

    有些函数,可以传入为定义变量做参数,但是函数自身的签名没有很好的体现这一点,就会报错。比如 setTimeout,以下这段代码就会报告:

    type timer = ReturnType<typeof setTimeout>;
    let timeout:timer;
    function doAction() {
      clearTimeout(timeout);
    }

    我确定这段代码没问题,但是 tsc 不给过,只好给 timeout 加上 ! 修饰符,即:let timeout!:timer。这样就可以了。

    5. Vue SFC 文件类型定义

    为让 tsc 能够正确理解 Vue SFC 格式,需要创建这个描述文件,并且告诉 tsc 加载这个描述文件。

    declare module "*.vue" {
      import { defineComponent } from "vue";
      const component: ReturnType<typeof defineComponent>;
      export default component;
    }
    {
      "compilerOptions": {
        "target": "esnext",
        "module": "esnext",
        "strict": true,
        "jsx": "preserve",
        "moduleResolution": "node",
        "suppressImplicitAnyIndexErrors": true,
        "allowSyntheticDefaultImports": true,
        "allowJs": true,
        "resolveJsonModule": true,
        "esModuleInterop": true,
    
        "paths": {
          "@/*": [
            "./src/*"
          ]
        },
        "lib": ["DOM", "ESNext"]
      },
      "include": [
        "src/**/*.ts",
        "src/**/*.vue",
        "test/**/*.ts"
      ],
      "exclude": [
        "node_modules"
      ],
      "files": ["src/types/vue-shims.d.ts"]
    }

    6. 待解决问题

    • 因为第一次使用 TypeScript,很多不熟悉的地方,tsc 大量报错。但是由于使用了 vue-loader,所以 tsc 报告的错误行号基本都不对,很难查找问题所在,浪费了大量时间。
    • 导入了 moment locale 文件,但是缺少定义,不知道该怎么声明某个对象属于某个接口。可能要通过前面类似定义 Vue SFC 的方式。

    7. 项目地址

    对项目感兴趣,或者寻求范例的同学可以在 GitHub 上找到这个项目:meathill/muimui-ui: A simple vue 3 ui components suit. (github.com)