新Vue3常见问题与技巧整理

Vue3已经发布很长时间了,最新版本已经到了3.4.x2023-12月底开始升级到3.4),功能基本稳定。

Vue官网:https://cn.vuejs.org/

选项式与组合式

首先看到Vue3最先可能需要的是选择选项式还是组合式API,这两种都是Vue3支持的开发模式,可以根据习惯来选择,不过组合式是最新的模式,建议新项目都是用组合式API,下面整理下主要区别与使用

  1. 选项式API核心就是Vue实例对象,所有数据都是附加到组件实例上,因此用得最多的就是this
    1. 选项式API就是返回一个对象,包含各种选项
    2. 数据、计算属性、props配置、方法等有独立的区域,比较适合新手组织数据,简单页面也是很方便
    3. 复杂页面就比较混乱,功能相关的代码被分散在各个选项区域,不利于代码重用
    4. export default {
        // data() 返回的属性将会成为响应式的状态
        // 并且暴露在 `this` 上
        data() {
          return {
            count: 0
          }
        },
        // methods 是一些用来更改状态与触发更新的函数
        // 它们可以在模板中作为事件处理器绑定
        methods: {
          increment() {
            this.count++
          }
        },
        // 生命周期钩子会在组件生命周期的各个不同阶段被调用
        // 例如这个函数就会在组件挂载完成后被调用
        mounted() {
          console.log(`The initial count is ${this.count}.`)
        }
      }
  2. 组合式API由开发者控制怎么组织代码
    1. 功能比较简单的页面可以顺序编写代码,看起来也比较符合开发习惯
    2. 也可以把相同功能整合到一块,甚至可以放到js文件中,减少单文件组件的代码体积
    3. 组合式API可以代替以前的mixins,把通用的功能抽成useXXX模式,参考VueUse库,实现代码重用
    4. export const useXXX = () => { // 组合式功能封装
        const data = ref(0);
        const doubleData = computed(() => data.value * 2);
        const processData = (newData) => {
          // 处理数据,修改数据
          data.value = newData;
        };
        return { data, doubleData, processData };
      };

ref和reactive选择

刚接触Vue3经常会想refreactive怎么选择,要做出选择首先要理解他们的区别

  1. 基本数据类型没有其他选择,只能refreactive是使用代理,只能用于对象和数组
    1. ref支持代理对象和基本数据类型
    2. reactive只能代理对象类型
    3. const data1 = ref(0)
      const data2 = ref({}) // 返回RefImpl实例
      const data3 = reactive({}) // 返回Proxy实例
  2. ref支持重新赋值,reactive只支持修改对象内部属性
    1. 如果需要重新赋值的只能用ref
    2. const data1 = ref({})
      const data2 = reactive({})
      data1.value = newData
      data2.name = 'gary' // 可以响应
      data2 = newData // 赋值会失去响应,变成普通对象
  3. ref用于对象实际也是调用reactive
    1. const data1 = ref({}) // 返回RefImpl实例,内部有个_value属性,值就是reactive({})
      const data2 = reactive({}) // 返回Proxy实例
  4. 响应式原理
    1. refvalue赋值响应式是通过valuegetset方法实现
      1. ref对象必须通过value访问值(模板中可以省略)
      2. /** 
        新建或修改时如果是对象用reactive包装一层
        if(typeof value === 'object'){
           this._value = reactive(value)
        }
        */
        class RefImpl { // 简单实现代码
           constructor(value) {
               this._value = value
           }
           get value(){
               console.log('ref依赖搜集')
               return this._value
           }
           set value(value){
               console.log('ref依赖触发')
               this._value = value
           }
        }
        const ref = value => new RefImpl(value)
    2. reactive的响应式是通过Proxy实现
      1. reactive由于是
      2. const reactive = (value) => {
            return new Proxy(value, {
                get(...args){
                    console.log('reactive依赖搜集')
                    return Reflect.get(...args)
                },
                set(...args){
                    console.log('reactive触发依赖')
                    return Reflect.set(...args)
                }
            })
        }

注意:建议新手直接ref一刀切就可以,没有必要考虑那么多

双向绑定

Vue中数据绑定有双向绑定,以及父传子单向绑定,Vue3相比Vue2v-model这块有不少变化

双向绑定需要子组件中发布更新事件emit('update:xxx', newValue)

Vue2版本

  1. 单向绑定(父传子)
       <input v-bind:value = "data">
       <!--简写-->
       <input :value="data">
  2. 双向绑定
    1. <input v-model="data">
      <!--非value属性双向绑定-->
      <input :test="data" @update:test="data=$event">
      <!--简写-->
      <input :test.sync="data">

Vue3版本

  1. 单向绑定,Vue3v-model使用modelValue代替原value
    1. <input v-bind:modelValue="data">
      <!--简写-->
      <input :modelValue="data">
  2. 双向绑定,vue2中那种.sync修饰符模式废弃掉了
    1. <input v-model="data"/>
      <!--非modelValue属性双向绑定-->
      <input :test="data" @update:test="data=$event">
      <!--简写-->
      <input v-model:test="data">

单向数据流

在开发Vue组件的时候,我们可能需要包装现有内置组件,然后实现v-model转发,简单包装一个文本框的组件如下:

<script setup>
defineProps({
  modelValue: {
    type: String,
    default: ''
  }
})
</script>
<template>
  <input v-model="modelValue">
</template>

我们会发现这里有ESLint的错误,直接报错了,虽然可能关掉相关验证,但是最好保留。

ESLint: Unexpected mutation of “modelValue” prop.(vue/no-mutating-props)

注意,这是因为Vue是推荐单向数据流,不直接更改父组件传递的值,必须通过事件来更新,即:update:modelValue事件,非必要不要打破单向数据流

v-model标准开发

我们有很多种方式来开发这个功能

  1. 定义一个新变量来实现,这种方式实现起来比较繁琐,了解即可
    1. <script setup>
      import { ref, watch } from 'vue'
      const props = defineProps({
        modelValue: { type: String, default: '' }
      })
      const emit = defineEmits(['update:modelValue']) // 事件定义
      const childModel = ref(props.modelValue) // 新变变量
      watch(() => props.modelValue, (val) => { // 监控props数据变化
        childModel.value = val
      })
      watch(childModel, (val) => { // 监控子组件数据变化
        emit('update:modelValue', val)
      })
      </script>
      <template>
        <input v-model="childModel">
      </template>
  2. 使用定制getset的计算属性,这个方式算是比较简单的
    1. <script setup>
      import { computed } from 'vue'
      const props = defineProps({
        modelValue: { type: String, default: '' }
      })
      const emit = defineEmits(['update:modelValue']) // 事件定义
      const childModel = computed({
        get () {
          return props.modelValue
        },
        set (val) {
          emit('update:modelValue', val)
        }
      })
      </script>
      <template>
        <input v-model="childModel">
      </template>
  3. 使用VueUseuseVModel实现
    1. 上面的方法整体看起来还是有点繁琐,VueUse在此基础上做了一个工具方法:useVModel
    2. <script setup>
      import { useVModel } from '@vueuse/core'
      const props = defineProps({
        modelValue: { type: String, default: '' }
      })
      const emit = defineEmits(['update:modelValue']) // 事件定义
      const childModel = useVModel(props, 'modelValue', emit)
      </script>
      <template>
        <input v-model="childModel">
      </template>
  4. Vue3.4.x之后有新的方式(推荐)
    1. 新版Vue3.4以上新增了一个宏来实现,这个用起来最简单
    2. <script setup>
      const childModel = defineModel({
        type: String, default: ''
      })
      </script>
      <template>
        <input v-model="childModel">
      </template>

只要是Vue3.4.0之后,建议用defineModel实现。支持指定名字定义多个defineModel

参考:https://cn.vuejs.org/api/sfc-script-setup.html#definemodel

filter代替

Vue3已经移除了filter支持,不能再使用Vue2的这种语法:date|formatDate('YYYY-MM-DD'),替代方案如下

  1. 使用computed计算属性
    1. import { formatDate } from '@/utils'
      const formated = computed(()=>formatDate(date, 'YYYY-MM-DD'))
  2. 使用方法调用,这里方法可以import进来也可以放到全局
    1. import { formatDate } from '@/utils'
      // 通过插件方式安装到全局
      export const UtilPlugin = {
          install(app){
              app.config.globalProperties.$formatDate = formatDate
          }
      }
      // 安装
      app.use(UtilPlugin)
    2. <span>{{formatDate(date, 'YYYY-MM-DD')}}</span>
      <span>{{$formatDate(date, 'YYYY-MM-DD')}}</span>

环境变量

以前Vue2使用的是webpack打包,Vue3默认用了vite打包,他们环境变量读取有不少的变化

  1. 文件名没有什么变化
    1. .env默认配置文件
    2. .env.production生产环境
    3. .env.staging预发布
    4. .env.development测试环境
  2. Vue2环境变量
    1. html文件中使用,格式<%=变量名%>
      1. <title><%=VUE_APP_APP_NAME%></title>
        <% if(process.env.VUE_APP_XXXX_FLAG === '1'){ %>
            <script src="xxxx.js"></script>
        <%}%>
      2. 支持JS代码
    2. jsvue文件中使用
      1. const gateway = process.env.VUE_APP_API_GATEWAY
      2. VUE_APP_开头的变量可以访问到
  3. Vue3环境变量
    1. html文件中使用,格式:%变量名%
      1. <p>Vite is running in %MODE%</p>
        <p>Using data from %VITE_API_URL%</p>
    2. jsvue文件中使用
      1. const gateway = import.meta.env.VITE_APP_API_GATEWAY
      2. VITE_开头的变量可以访问到

数据透传

我们在开发一个Vue组件的时候,经常在别人已经存在的组件上包装一层开发自己的组件,这里就有一个属性怎么传递的问题

属性透传

Vue属性透传参考:https://cn.vuejs.org/guide/components/attrs.html#fallthrough-attributes

总结如下:

  1. 自动透传,默认开启,可以用defineOptions修改
    1. 如果是单个根节点,默认会把组件没有通过props定义的属性直接附加到根节点上
    2. 如果有多个根节点,需要手动指定透传给哪个
  2. 手动附加属性
    1. <child-component v-bind="$attrs"></child-component>
  3. 属性透传包含事件透传
    1. Vue2$listeners,在Vue3中不需要了
  4. js中访问透传属性
    1. <script setup>
      import { useAttrs } from 'vue'
      const attrs = useAttrs()
      </script>

槽透传

如果要把槽透传给组件,可以使用$slot或者useSlots,也可以过滤部分再透传

<template
  v-for="(_, slotKey) in $slots"
  :key="slotKey"
  #[slotKey]="scope"
>
  <slot
    :name="slotKey"
    v-bind="scope"
  />
</template>

这里透传的时候如果有作用域插槽,可以用v-bind传递

动态调用组件

有些时候我们做一个通用的组件,为了方便使用,我们直接通过JS来动态调用组件,方便组件在没有引入到template的时候使用

这里需要用到动态创建VNode并渲染到页面上,比如ElMessageBox这种调用方式 ,这里定义一个DynamicHelper作为Vue插件,包装container和销毁过程

export class DynamicHelper {
  constructor () {
    this.appDivId = 'app'
    this.context = DynamicHelper.app._context
    this.container = DynamicHelper.createContainer()
    this.destroy = DynamicHelper.getDestroyFunc(this.container)
  }
  static createContainer () {
    return document.createElement('div')
  }
  static getDestroyFunc (container) {
    return () => {
      if (container) {
        render(null, container)
      }
    }
  }
  createAndRender (...args) {
    const container = this.container
    const vnode = h(...args)
    vnode.appContext = this.context
    render(vnode, container)
    const appDiv = document.getElementById(this.appDivId)
    if (appDiv && container.firstElementChild) {
      appDiv.appendChild(container.firstElementChild)
    }
    return vnode
  }
}
export default {
  install (app) {
    DynamicHelper.app = app
  }
}
  1. 上面的DynamicHelper使用:
    1. const dynamicHelper = new DynamicHelper()
      const vnode = dynamicHelper.createAndRender(SomeComponent, {})
      // 调用vnode组件暴露的方法
      vnode.component?.exposed?.xxxMethod()
    2. 注意context问题
      1. 我们在插件安装的时候读取当前appcontext并存下来,this.context = DynamicHelper.app._context
      2. 如果不用全局的app.context,组件访问全局的对象可能报错,比如vue-i18n的 方法$t('xxx.xxx.key')
  2. 最简单的核心代码:
    1. const container = document.createElement('div') // 创建容器
      const vnode = h(...args) // 创建VNode
      render(vnode, container) // 渲染VNode
      document.getElementById('#app').appendChild(container.firstElementChild)

VNode模板渲染

我们在开发一个组件的时候,通常通过暴露槽作为自定义子组件入口,在template中使用,有时候我们可能会暴露一些函数方法,通过函数方法的返回值来渲染页面(如何把VNode渲染到template的指定位置中?)。

比如在element-plustable组件使用的formatter函数:

formatter函数定义如下:(row: any, column: any, cellValue: any, index: number) => VNode | string

  1. 函数返回值可以是String或者VNode
  2. 如何渲染StringVNode结果,其实String可以用插值表达式渲染,VNode可以通过<component :is="result"/>组件直接在页面渲染。
  3. // 结果计算
    const formatResult = computed(() => {
        if (props.formatter) {
          const result = props.formatter(someRef.value, otherRef.value)
          return {
            result,
            vnode: isVNode(result)
          }
        }
      }
      return null
    })
  4. <!--template中使用-->
    <template v-if="formatResult">
      <span v-if="formatResult.result&&!formatResult.vnode"
        >{{formatResult.result}}</span
      >
      <component :is="formatResult.result" v-if="formatResult.vnode" />
    </template>

以上就是目前开发中遇到的常见的问题和开发技巧,更多问题欢迎讨论

评论

  1. 6月前
    2024-5-22 15:43:58

    这篇文章写得深入浅出,让我这个小白也看懂了!

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇