Vue3
已经发布很长时间了,最新版本已经到了3.4.x
(2023-12
月底开始升级到3.4
),功能基本稳定。
Vue官网:https://cn.vuejs.org/
选项式与组合式
首先看到Vue3
最先可能需要的是选择选项式还是组合式API
,这两种都是Vue3
支持的开发模式,可以根据习惯来选择,不过组合式是最新的模式,建议新项目都是用组合式API
,下面整理下主要区别与使用
- 选项式
API
核心就是Vue
实例对象,所有数据都是附加到组件实例上,因此用得最多的就是this
- 选项式API就是返回一个对象,包含各种选项
- 数据、计算属性、props配置、方法等有独立的区域,比较适合新手组织数据,简单页面也是很方便
- 复杂页面就比较混乱,功能相关的代码被分散在各个选项区域,不利于代码重用
-
export default { // data() 返回的属性将会成为响应式的状态 // 并且暴露在 `this` 上 data() { return { count: 0 } }, // methods 是一些用来更改状态与触发更新的函数 // 它们可以在模板中作为事件处理器绑定 methods: { increment() { this.count++ } }, // 生命周期钩子会在组件生命周期的各个不同阶段被调用 // 例如这个函数就会在组件挂载完成后被调用 mounted() { console.log(`The initial count is ${this.count}.`) } }
- 组合式
API
由开发者控制怎么组织代码- 功能比较简单的页面可以顺序编写代码,看起来也比较符合开发习惯
- 也可以把相同功能整合到一块,甚至可以放到
js
文件中,减少单文件组件的代码体积 - 组合式
API
可以代替以前的mixins
,把通用的功能抽成useXXX
模式,参考VueUse库,实现代码重用 -
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
经常会想ref
和reactive
怎么选择,要做出选择首先要理解他们的区别
- 基本数据类型没有其他选择,只能
ref
,reactive
是使用代理,只能用于对象和数组ref
支持代理对象和基本数据类型reactive
只能代理对象类型-
const data1 = ref(0) const data2 = ref({}) // 返回RefImpl实例 const data3 = reactive({}) // 返回Proxy实例
ref
支持重新赋值,reactive
只支持修改对象内部属性- 如果需要重新赋值的只能用
ref
-
const data1 = ref({}) const data2 = reactive({}) data1.value = newData data2.name = 'gary' // 可以响应 data2 = newData // 赋值会失去响应,变成普通对象
- 如果需要重新赋值的只能用
ref
用于对象实际也是调用reactive
-
const data1 = ref({}) // 返回RefImpl实例,内部有个_value属性,值就是reactive({}) const data2 = reactive({}) // 返回Proxy实例
-
- 响应式原理
ref
的value
赋值响应式是通过value
的get
和set
方法实现ref
对象必须通过value
访问值(模板中可以省略)-
/** 新建或修改时如果是对象用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)
- reactive的响应式是通过Proxy实现
- reactive由于是
-
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
相比Vue2
在v-model
这块有不少变化
双向绑定需要子组件中发布更新事件emit('update:xxx', newValue)
Vue2
版本
- 单向绑定(父传子)
<input v-bind:value = "data"> <!--简写--> <input :value="data">
- 双向绑定
-
<input v-model="data"> <!--非value属性双向绑定--> <input :test="data" @update:test="data=$event"> <!--简写--> <input :test.sync="data">
-
Vue3
版本
- 单向绑定,
Vue3
的v-model
使用modelValue
代替原value
-
<input v-bind:modelValue="data"> <!--简写--> <input :modelValue="data">
-
- 双向绑定,
vue2
中那种.sync
修饰符模式废弃掉了-
<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标准开发
我们有很多种方式来开发这个功能
- 定义一个新变量来实现,这种方式实现起来比较繁琐,了解即可
-
<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>
-
- 使用定制
get
和set
的计算属性,这个方式算是比较简单的-
<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>
-
- 使用
VueUse
的useVModel
实现- 上面的方法整体看起来还是有点繁琐,
VueUse
在此基础上做了一个工具方法:useVModel
-
<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>
- 上面的方法整体看起来还是有点繁琐,
Vue3.4.x
之后有新的方式(推荐)- 新版
Vue3.4
以上新增了一个宏来实现,这个用起来最简单 -
<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')
,替代方案如下
- 使用
computed
计算属性-
import { formatDate } from '@/utils' const formated = computed(()=>formatDate(date, 'YYYY-MM-DD'))
-
- 使用方法调用,这里方法可以import进来也可以放到全局
-
import { formatDate } from '@/utils' // 通过插件方式安装到全局 export const UtilPlugin = { install(app){ app.config.globalProperties.$formatDate = formatDate } } // 安装 app.use(UtilPlugin)
-
<span>{{formatDate(date, 'YYYY-MM-DD')}}</span> <span>{{$formatDate(date, 'YYYY-MM-DD')}}</span>
-
环境变量
以前Vue2
使用的是webpack
打包,Vue3
默认用了vite
打包,他们环境变量读取有不少的变化
- 文件名没有什么变化
.env
默认配置文件.env.production
生产环境.env.staging
预发布.env.development
测试环境
Vue2
环境变量html
文件中使用,格式<%=变量名%>
-
<title><%=VUE_APP_APP_NAME%></title> <% if(process.env.VUE_APP_XXXX_FLAG === '1'){ %> <script src="xxxx.js"></script> <%}%>
- 支持
JS
代码
-
js
或vue
文件中使用-
const gateway = process.env.VUE_APP_API_GATEWAY
- 仅
VUE_APP_
开头的变量可以访问到
-
Vue3
环境变量html
文件中使用,格式:%变量名%
-
<p>Vite is running in %MODE%</p> <p>Using data from %VITE_API_URL%</p>
-
js
或vue
文件中使用-
const gateway = import.meta.env.VITE_APP_API_GATEWAY
- 仅
VITE_
开头的变量可以访问到
-
数据透传
我们在开发一个Vue
组件的时候,经常在别人已经存在的组件上包装一层开发自己的组件,这里就有一个属性怎么传递的问题
属性透传
Vue
属性透传参考:https://cn.vuejs.org/guide/components/attrs.html#fallthrough-attributes
总结如下:
- 自动透传,默认开启,可以用
defineOptions
修改- 如果是单个根节点,默认会把组件没有通过props定义的属性直接附加到根节点上
- 如果有多个根节点,需要手动指定透传给哪个
- 手动附加属性
-
<child-component v-bind="$attrs"></child-component>
-
- 属性透传包含事件透传
Vue2
的$listeners
,在Vue3
中不需要了
js
中访问透传属性-
<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 }
}
- 上面的
DynamicHelper
使用:-
const dynamicHelper = new DynamicHelper() const vnode = dynamicHelper.createAndRender(SomeComponent, {}) // 调用vnode组件暴露的方法 vnode.component?.exposed?.xxxMethod()
- 注意
context
问题- 我们在插件安装的时候读取当前
app
的context
并存下来,this.context = DynamicHelper.app._context
- 如果不用全局的
app.context
,组件访问全局的对象可能报错,比如vue-i18n
的 方法$t('xxx.xxx.key')
- 我们在插件安装的时候读取当前
-
- 最简单的核心代码:
-
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-plus
的table
组件使用的formatter
函数:
formatter
函数定义如下:(row: any, column: any, cellValue: any, index: number) => VNode | string
- 函数返回值可以是
String
或者VNode
- 如何渲染
String
和VNode
结果,其实String
可以用插值表达式渲染,VNode
可以通过<component :is="result"/>
组件直接在页面渲染。 -
// 结果计算 const formatResult = computed(() => { if (props.formatter) { const result = props.formatter(someRef.value, otherRef.value) return { result, vnode: isVNode(result) } } } return null })
-
<!--template中使用--> <template v-if="formatResult"> <span v-if="formatResult.result&&!formatResult.vnode" >{{formatResult.result}}</span > <component :is="formatResult.result" v-if="formatResult.vnode" /> </template>
以上就是目前开发中遇到的常见的问题和开发技巧,更多问题欢迎讨论