由于 vue3 使用的新技术,一些旧版本的插件或依赖,可能支持的不太完善,下面是 vue3 技术栈的相关文档们。
官方提供了两种方式创建脚手架。
传统的 vue/cli,使用 webpack 编译打包。
yarn global add @vue/cli
# 或
npm install -g @vue/cli
然后在 Vue 项目运行:
vue upgrade --next
Vite 是一个 web 开发构建工具,由于其原生 ES 模块导入方法,它允许快速提供代码。
需要 node 版本 >=12.0.0。
直接执行下面指令,后面再填写项目名称、选择项目模板时选择 vue :
# 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/app
或npm init vite-app 【项目名称】
,这些是之前版本的 vite 的生成项目的方式,已弃用。
Tip:后续启动开发服务和打包时,可能会报错如下所示:
throw new Error(`esbuild: Failed to install correctly
^
Error: esbuild: Failed to install correctly
...
则有可能是因为,当前 nodejs 使用的 npm 是 v7 以上的版本,这是 npm 的一个小bug,需要再执行一下下面的指令,即可正常使用:
node node_modules/esbuild/install.js
此后,只有当重新安装了 esbuild 这个依赖后,才需要再执行该指令。
组合API,又名注入API。
setup 方法:
所以,要使用组合 API,就必须使用 setup,组合 API主要就是在 setup 中,使用 ref 和 reactive 两个新方法的开发方式。
ref 和 reactive 方法的使用方式一模一样,区别就是:
两个方法生成的数据,都会使用 proxy 进行深度包装,也就是会深度监听。
下面是三者简单的配合使用。
基本使用方法,就是调用 ref 方法,传入一个简单类型的值,它再返回被包装过的值。
对于传入的值,会复制一份,修改不影响原值,即使原值是对象。
以下是使用组合 API,创建一个点击按钮,给数值 + 1的功能:
<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: 传入的必须是个对象或数组。
和 ref 一样,对于传入的值,会复制一份,修改不影响原值。
<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 进行深度包装,这一点在处理大批量对象或数组、而又没有必要深度监听时,会比较浪费性能。
所以两个方法各自都有一个浅层包装监听的方法,分别为 shallowRef 和 shallowReactive。
两个的用法和 ref 、reactive 一样,区别仅仅是只浅层监听,当直接修改深层数据时,不会触发重新渲染。
但使用浅层监听后,有时候又想改了某个深层数据,还又想触发界面渲染怎么办?
ref 还有一个方法 --- triggerRef,使用 shallowRef 生成的深层数据后,当我们修改了深层数据,界面没有重新渲染,此时再使用 triggerRef 方法,就可以出发一次渲染了:
<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>
ref 和 reactive,是把原本的对象,使用 proxy 包装一下,我们也可以通过 toRaw 方法,来获取原始对象。
需要注意的是,ref 需要传入 ref.value:
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 和 ref 的区别还是挺大的。
用法:
import { toRef } from 'vue'
// 定义对象
let obj = {age: 100}
// 使用 toRef 包装
let age = toRef(obj, 'age')
可见,toRef 是对对象的某个字段进行包装,返回一个 ref 对象,但是,这个用 toRef 包装的 age,如果用在页面上,当值修改时,是不会自动触发重新渲染的。
toRef 和 ref 不同,它是源值的引用,修改也会修改源值:
<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 包装出来的对象,单独再包装某个需要的字段,方便使用和传参等。
<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 的批量包装用法:
<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 中的方法。
<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>
Vue 提供了provide 和 reject 属性为高阶组件提供了便利的数据传递。
父组件,使用 provide 传递数据:
<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 接收数据:
<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 监听,常规用法:
<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 方法取消监听:
<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 和 watch 都是监听,但它的用法不太一样。
见例子:
<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>
用法和 vue2 的计算属性类似,直接使用:
<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() ,配置更复杂的赋值功能:
<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>
正常开发,常用的有 provide 和 props 两种方式,可以传递数据给子组件。
但正常情况,两种方式传递给子组件,子组件是可以修改数据的。
如果使用 props 传递的是简单数据类型,则子组件得到的是一个直接量,修改不会影响父组件,但父组件的这个变量更新时,子组件会跟着更新。
父组件中:
<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>
子组件中:
<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 包装一下。
<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>
这样,父组件可另写方法来更新两个值,再把更新的方法传递给子组件即可。
onMounted 方法,用法类似于 addEventListener。
且使用 onMounted 添加的方法,将先于 mounted 执行。
<template>
<div></div>
</template>
<script>
import { onMounted } from 'vue'
export default {
setup() {
onMounted(() => {
console.log('onMounted') // 先执行
})
},
mounted() {
console.log('mounted') // 后执行
},
}
</script>
vue 获取 dom 元素,之前是使用 this.$refs[ref名字] 的方式,现在 vue3 提供了新的方式。
<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>