编程崽

登录

一叶在编程苦海沉沦的扁舟之上,我是那只激情自射的崽

Vue3 使用

Vue3 使用

由于 vue3 使用的新技术,一些旧版本的插件或依赖,可能支持的不太完善,下面是 vue3 技术栈的相关文档们。

使用脚手架创建 vue3 项目

官方提供了两种方式创建脚手架。

使用传统 vue/cli

传统的 vue/cli,使用 webpack 编译打包。

sh 复制代码
yarn global add @vue/cli
# 或
npm install -g @vue/cli

然后在 Vue 项目运行:

sh 复制代码
vue upgrade --next

使用官方推荐的新编译工具框架 vite

Vite 是一个 web 开发构建工具,由于其原生 ES 模块导入方法,它允许快速提供代码。

需要 node 版本 >=12.0.0。

直接执行下面指令,后面再填写项目名称、选择项目模板时选择 vue

sh 复制代码
# npm 6.x
$ npm init vite@latest <project-name> --template vue

# npm 7+,需要加上额外的双短横线
$ npm init vite@latest <project-name> -- --template vue

$ cd <project-name>
$ npm install
$ npm run dev

以前还有几种方式,比如执行 npm init @vitejs/appnpm init vite-app 【项目名称】 ,这些是之前版本的 vite 的生成项目的方式,已弃用。

Tip:后续启动开发服务和打包时,可能会报错如下所示:

sh 复制代码
throw new Error(`esbuild: Failed to install correctly
^

Error: esbuild: Failed to install correctly
...

则有可能是因为,当前 nodejs 使用的 npm 是 v7 以上的版本,这是 npm 的一个小bug,需要再执行一下下面的指令,即可正常使用:

sh 复制代码
node node_modules/esbuild/install.js

此后,只有当重新安装了 esbuild 这个依赖后,才需要再执行该指令。

组合API --- Composition API

组合API,又名注入API。

setup、ref 和 reactive

setup 方法:

  • 它是组合API的入口函数
  • 在 beforeCreate 和 created 之间执行
  • 方法内部的 this 为 undefined,所以无法访问 this(可以通过 vue 的 getCurrentInstance 方法,获取到当前实例 ctx
  • 第一个参数为 props,但前提需要先用 vue2 中的 props 字段接收一下
  • 方法返回一个对象,对象中的字段,比如字段和方法,后续都可以使用 this 访问,且可以用在页面模板中

所以,要使用组合 API,就必须使用 setup,组合 API主要就是在 setup 中,使用 ref 和 reactive 两个新方法的开发方式。

ref 和 reactive 方法的使用方式一模一样,区别就是:

  • ref 官方建议只传入一个简单数据类型,这样再后续使用时,会比较方便
  • reactive 官方建议,也只能传入复杂数据类型(对象或数组)

两个方法生成的数据,都会使用 proxy 进行深度包装,也就是会深度监听。

下面是三者简单的配合使用。

创建简单类型变量 - ref

基本使用方法,就是调用 ref 方法,传入一个简单类型的值,它再返回被包装过的值。

对于传入的值,会复制一份,修改不影响原值,即使原值是对象。

以下是使用组合 API,创建一个点击按钮,给数值 + 1的功能:

js 复制代码
<template>
  <p>{{val}}</p>
  <button @click="setVal(val + 1)">点击 + 1</button>
</template>

<script>
// 进入
import { ref } from 'vue'

export default {
  setup() {
    // 调用 ref 方法进行包装
    const val = ref(0)
    
    // val 为一对象,其中只有对我们有意义的,只有一个 value 字段,其值就是我们传入给 ref 的值:val => { value: 0 }
    
    // 定义方法修改值,注意修改值需要访问 .value 属性来修改,因为
    function setVal(num) {
      val.value = num
    }
    return { val, setVal }
  },
}
</script>

创建复杂数据类型 - reactive

Reactive: 传入的必须是个对象或数组。

和 ref 一样,对于传入的值,会复制一份,修改不影响原值。

js 复制代码
<template>
  <p>{{obj.people.name}}</p>
  <p>{{obj.age}}</p>
  <button @click="setAge(obj.age + 1)">点击 + 1</button>
  <button @click="setName('大明')">点击改名</button>
</template>

<script>
// 进入
import { reactive } from 'vue'

export default {
  setup() {
    // 调用 reactive 方法进行包装,方法必须传入复杂数据类型(对象或数组)
    const obj = reactive({age: 100, people: {name: '小明'}})

    // obj 的结构,和我们上面传入的对象结构一致,只不过被深度包装成了 proxy
    
    // 定义方法修改值
    function setAge(age) {
      obj.age = age
    }

    // 修改对象深层数据,可能监听到
    function setName(name) {
      obj.people.name = name
    }
    return { obj, setAge, setName }
  },
}
</script>

非深度监听版本

ref 其实 reactive 默认都会使用 proxy 进行深度包装,这一点在处理大批量对象或数组、而又没有必要深度监听时,会比较浪费性能。

所以两个方法各自都有一个浅层包装监听的方法,分别为 shallowRefshallowReactive

两个的用法和 refreactive 一样,区别仅仅是只浅层监听,当直接修改深层数据时,不会触发重新渲染。

但使用浅层监听后,有时候又想改了某个深层数据,还又想触发界面渲染怎么办?

ref 还有一个方法 --- triggerRef,使用 shallowRef 生成的深层数据后,当我们修改了深层数据,界面没有重新渲染,此时再使用 triggerRef 方法,就可以出发一次渲染了:

js 复制代码
<template>
  <p>{{obj.age}}</p>
  <button @click="setAge(obj.age + 1)">点击 + 1</button>
</template>

<script>
// 进入
import { shallowRef, triggerRef } from 'vue'

export default {
  setup() {
    const obj = shallowRef({age: 100})

    // 定义方法修改值
    function setAge(age) {
      // 修改 obj 的深层对象,无法触发页面更新
      obj.value.age = age

      // 再使用此方法,可触发一次页面更新
      triggerRef(obj)
    }

    return { obj, setAge }
  },
}
</script>

toRaw 获取原始数据

ref 和 reactive,是把原本的对象,使用 proxy 包装一下,我们也可以通过 toRaw 方法,来获取原始对象。

需要注意的是,ref 需要传入 ref.value:

js 复制代码
import { ref, reactive, toRaw } from 'vue'

export default {
  setup() {
    // 定义原始对象
    let ref_raw = {age: 100}
    let reactive_raw = {age: 200}

    // 进行包装
    const ref_proxy = ref(ref_raw)
    const reactive_proxy = reactive(reactive_raw)

    // 进行比较
    console.log(ref_raw === toRaw(ref_proxy.value)) // true
    console.log(reactive_raw === toRaw(reactive_proxy)) // true
  },
}

toRef 和 toRefs

toRef 和 ref 的区别还是挺大的。

用法:

js 复制代码
import { toRef } from 'vue'

// 定义对象
let obj = {age: 100}

// 使用 toRef 包装
let age = toRef(obj, 'age')

可见,toRef 是对对象的某个字段进行包装,返回一个 ref 对象,但是,这个用 toRef 包装的 age,如果用在页面上,当值修改时,是不会自动触发重新渲染的。

toRef 和 ref 不同,它是源值的引用,修改也会修改源值:

js 复制代码
<template>
  <p>{{age}}</p>

  <button @click="setAge(age + 1)">点击修改</button>
</template>

<script>
import { toRef } from 'vue'

export default {
  setup() {
    // 定义原始对象
    let obj = {age: 100}
    
    // 使用 toRef 包装
    let age = toRef(obj, 'age')
    
    function setAge(newVal) {
      age.value = newVal

      console.log(age.value) // 没点击一次,两个值都会被 + 1,但页面上始终显示为 100
      console.log(obj.age) // 没点击一次,两个值都会被 + 1,但页面上始终显示为 100
    }
    return { age, setAge }
  },
}
</script>

大多数用法:

使用 toRef 对 reactive 包装出来的对象,单独再包装某个需要的字段,方便使用和传参等。

js 复制代码
<template>
  <p>{{age}}</p>

  <button @click="setAge(age + 1)">点击修改</button>
</template>

<script>
import { reactive, toRef } from 'vue'

export default {
  setup() {
    // 定义 reactive
    let reactObj = reactive({name: '小明', age: 100})
    
    // 使用 toRef 包装
    let age = toRef(reactObj, 'age')
    
    function setAge(newVal) {
      reactObj.age = newVal
      // 与下面指令等效,两个 reactObj.age 和 age.value 会同时修改,且会出发页面更新
      // age.value = newVal
    }
    return { age, setAge }
  },
}
</script>

toRefs 的用法比较直接,直接是 toRef 的批量包装用法:

js 复制代码
<template>
  <p>{{age}}</p>
  <p>{{name}}</p>

  <button @click="setAge(age + 1)">点击修改</button>
</template>

<script>
import { reactive, toRefs } from 'vue'

export default {
  setup() {
    // 定义 reactive
    let reactObj = reactive({name: '小明', age: 100})
    
    // 使用 toRefs,批量包装
    let reactObjRefs = toRefs(reactObj)
    
    function setAge(newVal) {
      reactObj.age = newVal
      // 与下面指令等效,两个 reactObj.age 和 age.value 会同时修改,且会出发页面更新
      // reactObjRefs.age.value = newVal
    }
    return { ...reactObjRefs, setAge }
  },
}
</script>

获取路由信息

setup 中可以获取到路由信息,需要调用 vue-router 中的方法。

js 复制代码
<template>
  首页
</template>

<script>
import { useRouter, useRoute } from 'vue-route'

export default {
  setup() {
    console.log(useRoute()) // 获取 meta、params、query、name 等字段
    console.log(useRouter()) // 执行 go、back、push、replace 等方法
  },
}
</script>

数据传输 provide 和 reject

Vue 提供了providereject 属性为高阶组件提供了便利的数据传递。

父组件,使用 provide 传递数据:

js 复制代码
<template>
  <Child></Child>
</template>
<script>
import Child from '../components/Child.vue'
import { ref, provide } from 'vue'

export default {
  components: {Child},
  setup() {
    // 生成数据
    let num = ref(1)
    function setNum(newVal) {
      num.value = newVal
    }

    // 使用 provide,向子组件们传递两个参数
    provide('num', num)
    provide('setNum', setNum)
  },
}
</script>

子组件(直接子组件或更深层组件),使用 reject 接收数据:

js 复制代码
<template>
  {{num}}
  <button @click="setNum(num + 1)">点击 + 1</button>
</template>

<script>
import { inject } from 'vue'

export default {
  name: 'Child',
  setup() {
    // 使用 inject 接收两个参数
    let num = inject('num', 100) // 第二个参数为选填,默认值
    let setNum = inject('setNum')

    // 暴露出去,供组件使用
    return { num, setNum }
  },
}
</script>

watch 监听

使用 watch 监听,常规用法:

js 复制代码
<template>
  <button @click="refNum++">点击 + 1</button>
  <button @click="reactiveNum.num++">点击 + 1</button>
</template>

<script>
import { ref, reactive, watch } from 'vue'

export default {
  setup() {
    let refNum = ref(18)
    let reactiveNum = reactive({num: 18})

    // 监听 ref 值
    watch(
      refNum,
      (newVal) => {
        console.log('ref 监听', newVal) // ref 监听 19
      },
    )

    // 监听 reactive 对象
    watch(
      reactiveNum,
      (newVal) => {
        console.log('reactive 监听', newVal.num) // reactive 监听 19
      },
    )

    // 监听 reactive 对象中的某个字段
    watch(
      () => reactiveNum.num,
      (newVal) => {
        console.log('reactive 字段监听', newVal) // reactive 字段监听 19
      },
    )

    return {
      refNum,
      reactiveNum,
    }
  },
}
</script>

可以给 watch 传入配置,也可以在合适时候,执行 stop 方法取消监听:

js 复制代码
<template>
  <button @click="refNum++">点击 + 1</button>
</template>

<script>
import { ref, watch } from 'vue'

export default {
  setup() {
    let refNum = ref(18)

    // 监听 ref 值
    let stop = watch(
      refNum,
      (newVal) => {
        console.log('ref 监听', newVal) // ref 监听 19
      },
      { immediate: true }, // 初始化后,自动执行一次监听程序
    )

    setTimeout(() => {
      // 一段时候后,取消监听
      stop()
    }, 1000);

    return {
      refNum,
    }
  },
}
</script>

watchEffect 监听

watchEffectwatch 都是监听,但它的用法不太一样。

  • 只传入一个方法,方法中用到的变量更新时,该方法自动执行。
  • 传入的方法,在组件刚初始化后,会自动执行一次
  • 和 watch 一样,同样可以使用 stop 方法
  • 传入方法,会给方法传入一个回调方法 onInvalidate,可以给 onInvalidate 再传入一个回调方法【B】,B 方法会在以下三种情况下执行:
    • watchEffect 监听函数即将执行
    • 调用了 stop 方法,即将停止监听
    • 该组件即将被销毁

见例子:

js 复制代码
<template>
  <div>{{obj.num}}</div>
  <button @click="obj.num++">点击 + 1</button>
</template>

<script>
import { reactive, watchEffect } from 'vue'
export default {
  setup() {
    let obj = reactive({num: 10})

    let stop = watchEffect(onInvalidate => {
      // 组件初始化后,自动执行一次
      // 此方法中用到的变量更新时,自动执行该方法

      console.log('组件初始化,更新了用到的变量', obj.num)

      onInvalidate(() => {
        console.log('watchEffect 监听函数即将执行,或即将停止监听,或该组件即将被销毁')
      })
    })

    setTimeout(() => {
      stop()
    }, 2000);

    return { obj }
  },
}
</script>

computed 计算

用法和 vue2 的计算属性类似,直接使用:

js 复制代码
<template>
  {{result}}
  <button @click="mul.params++">乘数 + 1</button>
</template>

<script>
import { ref, reactive, computed } from 'vue'

export default {
  setup() {
    let num = ref(2)
    let mul = reactive({params: 2})

    // 最终返回会自动包装为 ComputedRefImpl 类型的数据
    let result = computed(() => {
      // 直接返回,为 ref 类型
      return num.value * mul.params
    })

    return { result, mul }
  },
}
</script>

可以配置 get()set() ,配置更复杂的赋值功能:

js 复制代码
<template>
  {{result.num}} | {{result.mul}}
  <button @click="result = 'num'">被乘数 + 1</button>
  <button @click="result = 'mul'">乘数 + 1</button>
</template>

<script>
import { ref, reactive, computed } from 'vue'

export default {
  setup() {
    let num = ref(2)
    let mul = reactive({params: 2})

    // 最终返回会自动包装为 ComputedRefImpl 类型的数据
    let result = computed({
      get() {
        return { num: num.value,  mul: mul.params }
      },
      set(newVal) {
        // newVal 就是新设置的值,下面根据 newVal 判断修改
        if (newVal === 'num') num.value = num.value + 1
        else if (newVal === 'mul') mul.params = mul.params + 1
      },
    })

    return { result }
  },
}
</script>

readonly 数据只读包装

正常开发,常用的有 provide 和 props 两种方式,可以传递数据给子组件。

但正常情况,两种方式传递给子组件,子组件是可以修改数据的。

如果使用 props 传递的是简单数据类型,则子组件得到的是一个直接量,修改不会影响父组件,但父组件的这个变量更新时,子组件会跟着更新。

父组件中:

js 复制代码
<template>
  <HelloWorld :numFromProps="numFromProps"/>
</template>

<script>
import { reactive, provide } from 'vue'
import HelloWorld from './components/HelloWorld.vue'

export default {
  components: {
    HelloWorld,
  },
  setup() {
    // 声明两个 ref 变量
    let numFromProvide = reactive({num: 18})
    let numFromProps = reactive({num: 18})

    // 方式一:使用 provide 传给子组件
    provide('numFromProvide', numFromProvide)

    setInterval(() => {
      console.log(numFromProvide.num) // 按了方式一的按钮后,此值变化
      console.log(numFromProps.num) // 按了方式二的按钮后,此值变化
    }, 1000)

    // 方式二:使用 props 传给子组件
    return { numFromProps: numFromProps }
  },
}
</script>

子组件中:

js 复制代码
<template>
  <!-- 方式一的展示和功能 -->
  <div>来自 numFromProvide:{{numFromProvide.num}}</div>
  <button @click="numFromProvide.num++">点击 + 1</button>

  <hr>

  <!-- 方式二的展示和功能 -->
  <div>来自 numFromProps:{{numFromProps.num}}</div>
  <button @click="numFromProps.num++">点击 + 1</button>
</template>

<script>
import { inject } from 'vue'
export default {
  props: ['numFromProps'],
  setup() {

    let numFromProvide = inject('numFromProvide')

    return { numFromProvide }
  },
}
</script>

可见,子组件是可以修改父组件传递的数据的。

但很多时候,为了保持数据的统一性和维护性,我们只希望子组件能调用父组件方法来更新传递的值,不希望子组件能直接修改。

这时就需要使用 readonly 方法了,使用此方法包装后的值,只读,不可修改。

父组件把即将传递给子组件的数据,使用 readonly 包装一下。

js 复制代码
<template>
  <HelloWorld :numFromProps="numFromProps"/>
</template>

<script>
import { reactive, provide, readonly } from 'vue'
import HelloWorld from './components/HelloWorld.vue'

export default {
  components: {
    HelloWorld,
  },
  setup() {
    // 声明两个 ref 变量
    let numFromProvide = reactive({num: 18})
    let numFromProps = reactive({num: 18})

    // 方式一:使用 provide 传给子组件
    provide('numFromProvide', readonly(numFromProvide))

    setInterval(() => {
      console.log(numFromProvide.num) // 按了方式一的按钮后,出现警告提示,修改失败,此值变化
      console.log(numFromProps.num) // 按了方式二的按钮后,出现警告提示,修改失败,此值变化
    }, 1000)

    // 方式二:使用 props 传给子组件
    return { numFromProps: readonly(numFromProps) }
  },
}
</script>

这样,父组件可另写方法来更新两个值,再把更新的方法传递给子组件即可。

钩子函数新用法

mounted => onMounted

onMounted 方法,用法类似于 addEventListener

且使用 onMounted 添加的方法,将先于 mounted 执行。

js 复制代码
<template>
  <div></div>
</template>

<script>
import { onMounted } from 'vue'

export default {
  setup() {
    onMounted(() => {
      console.log('onMounted') // 先执行
    })
  },
  mounted() {
    console.log('mounted') // 后执行
  },
}
</script>

其他功能

获取dom元素

vue 获取 dom 元素,之前是使用 this.$refs[ref名字] 的方式,现在 vue3 提供了新的方式。

sh 复制代码
<template>
  <!-- 新的方式 -->
  <div ref="box_1">盒子1</div>

  <!-- 旧的方式 -->
  <div ref="box_2">盒子2</div>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {
    const box_1 = ref(null)

    // 声明并输出
    return { box_1 }
  },
  mounted() {
    console.log(this.box_1) // 新旧两种方式
    console.log(this.$refs.box_2) // 新旧两种方式
  },
}
</script>