记录几个vue3 demo项目开发的问题

662 阅读5分钟

开头

最近在学习vue3(使用开发层面),做了一些小demo项目,像[md-editor-v3],vue3-admin等。喜欢在vue项目中使用jsx语法来编写,几乎项目中都使用到了ts,某一些写法,在谷歌的时候没找到,所以想做一个小总结。

之前看别人讨论过在项目中使ts的态度,作者持保留态度,有些地方会增加代码量,如果不熟悉的话,甚至会耽误开发进度。但是从开发公共组件及某些复用组件来看,ts会协助开发,而且能够增加阅读性。

如果内容有错误,请指出。

ts中的props

该问题主要存在于使用了ts开发的项目中,不区别是否使用jsx,从 string、string/object混合、array 角度演示(default值没有写出)。

js开发组件中,props类型验证会像下面这样写

props: {
    propA: String,
    propB: [String, Object],
    propC: [Array]
  }

如果A只能是a\b\c三个中的一个,B如果只能是css样式,对象就需要限制,C只能是数据中存在a/b/c中的组合,你可能会用到validator。但是没有类型,对开发提示不友好。

于是使用ts替换一下写法,

import { PropType, CSSProperties } from 'vue';

props: {
  propA: String as PropType<'a' | 'b' | 'c'>,
  propB: [String, Object] as PropType<string | CSSProperties>,
  propC: [Array] as PropType<Array<'a' | 'b' | 'c'>>,
}

能体会到的显而易见的好处就是下面这样:

image.png image.png

懒人必备的代码提示~

看过ant-design-vue的源码的话,会发现他们使用了一个叫vue-types的库来封装了类型,合适的可以直接使用这种方式,传送门

不用去给setup的第一入参定义类型,会和props冲突,感兴趣的可以试试。

jsx中的插槽

这个在vue3-admin中有用card组件做过演示,传送门

开发组件需要注意的是,如果让组件更好的支持jsx的话,就需要考虑组件的插槽内容是通过props传递的还是通过插槽的方式传递的,前者多出现在jsx语法中,而后者则多数情况是.vue模板语法中。

分析card组件

import { SetupContext, EmitsOptions, } from 'vue';

setup(props, ctx: SetupContext<EmitsOptions>) {
    return () => {
      // 插槽获取一定在函数组件(render)方法内部,否则提示:this will not track dependencies used in the slot
      const slotDefault = getSlot({ ctx });
      const slotTitle = getSlot({ props, ctx }, 'title');
      const slotFunc = getSlot({ props, ctx }, 'func');

      const cardClass = classnames('vra-card', props.border && 'vra-card-border');
      const headerClass = classnames('vra-card-header', props.headerClass);
      const bodyClass = classnames('vra-card-body', props.bodyClass);

      return (
        <div class={cardClass}>
          {slotTitle && (
            <div class={headerClass} style={props.headerStyle}>
              <div class="vra-card-title">{slotTitle}</div>
              {slotFunc && <div class="vra-card-func">{slotFunc}</div>}
            </div>
          )}
          <div class={bodyClass} style={props.bodyStyle}>
            {slotDefault}
          </div>
        </div>
      );
    };
  }

getSlot方法是用来根据名称来自动获取来源中的插槽内容的,不论是vue插槽的方式,还是jsx的属性的方式,代码:

  
import { ComponentPublicInstance, SetupContext, EmitsOptions } from 'vue';

/**
 * 获取指定插槽内容
 * 方法设定:vue组件中v-slot优先级高于props
 *
 * @param param0 组件实例instance,SetupContext(setup二入参),props
 * @param name 插槽名或者props名
 * @returns VNode
 */
export const getSlot = (
  {
    instance,
    ctx,
    props = {}
  }: {
    instance?: ComponentPublicInstance;
    ctx?: SetupContext<EmitsOptions>;
    props?: any;
  },
  name = 'default'
) => {
  const targetSlot = instance?.$slots[name] || ctx?.slots[name];
  return (targetSlot ? targetSlot(instance) : '') || props[name];
};

card组件中注释内容插槽获取一定在函数组件(render)方法内部,该问题的解释为:由于插槽内容是我们通过方法手动获取的,了解过setup的就知道,它在整个生命周期中,只会执行一次,无论后续是否有状态变更,如果在setup中获取到插槽内容,那么后续插槽内容如果有更新,则在card中不会有相应的改变。相反,如果是在setup中的render函数或则在外部render函数中,每次状态变更,render函数均会执行,及保证了最新的插槽内容能获取到。

jsx中的双向绑定

虽然能够实现使用指令,但还是不用做这个打算,jsx中均可以用js语法来代替(巩固下基础也没毛病,之前看过一个问答说jsx中怎么v-if,回答让人沉思:js的if else都不会了么?)。

原生的输入标签能够使用vModel实现双向绑定机制,例如input、textarea等

import { defineComponent, reactive, watchEffect } from 'vue';

export default defineComponent({
  setup() {
    const test = reactive({
      a: 1
    });

    watchEffect(() => {
      console.log(test.a);
    });

    return () => <input vModel={test.a} />;
  }
});

演示:

GIF.gif

tsx中会提示vModel不存在,这时就需要自己定义type了,作者没有使用这种方式所以没有进一步尝试。项目中都是自行绑定数据

setup() {
  const val = ref('')
  return () => <input value={val.value} onChang={({ target }: Event) => (val.value = (target as HTMLTextAreaElement).value)} />
}

target需要断言才能正常,这样看起来仿佛写的代码更多了,啊哈!

antd的图标

这一项一般出现在开发admin项目时,配置左侧菜单,像下面这样:

ant-design现在react ui库和vue ui库都将icon剔出去了,单独引入一个依赖来使用icon,按需加载,的确方便,可是,现在要实现在数据库中配置菜单对应的icon,然后在获取路由配置的时候拿过来显示,方法就只有两种:

  1. @ant-design/icons-vue的所有组件导入,再做一个枚举,在服务端配置组件名称,渲染时根据组件名称匹配组件,像这样:

key-element文件

import { WifiOutlined, WomanOutlined } from '@ant-design/icons-vue'

export default {
  WifiOutlined: <WifiOutlined />,
  WomanOutlined: <WomanOutlined />
}

使用

const router = [{path: '/index',meta: { title: '首页', icon: 'WifiOutlined' },}]
// render
<div>{ keyElement[routerItem.meta.icon] }</div>

这样不科学,icon太多。下面的方法得以优化。

  1. 原理差不多,也是将全部导入,但是不是自己手动导入:
import { h } from 'vue';
// 导入所有组件
import * as Icon from '@ant-design/icons-vue';

// 已获取到的配置
const router = [{path: '/index',meta: { title: '首页', icon: 'WifiOutlined' },}]

// render
<div>{ h(Icon[router.meta.icon]) }</div>

不出意外使用了ts会提示以下错误:

(property) MenuType.iconName?: any Element implicitly has an 'any' type because expression of type 'any' can't be used to index type...

这里需要给meta.icon指明类型:

import Icon from '@ant-design/icons-vue/lib/icons';

export interface MenuType {
  path: string;
  children?: Array<MenuType>;
  meta?: {
    icon?: keyof typeof Icon;
  }
  // 其他类型
  // ...
}

const router: MenuType[] = [{path: '/index',meta: { title: '首页', icon: 'WifiOutlined' }}]

vue3的h方法和react的creatElemet方法类似。

结尾

暂时写到这儿,后面想到再更新吧,都比较基础,看一遍就知道了。