Vue3 在大版本 3.3 里面推出來了一些新功能(主要是語法糖),網(wǎng)上有各種文章,但是看起來似乎是一樣的。
我覺得吧,有新特性了,不能光看,還要動手嘗試一下。
DefineOptions 宏定義
先來一個簡單的,以前我們有時候想設(shè)個name,有時候不想讓組件自動繼承屬性,這時候需要單獨設(shè)置一個script進行設(shè)置,現(xiàn)在簡化了操作,直接使用 defineOptions 即可。
<script setup lang="ts">
defineOptions({
name: 'Foo',
inheritAttrs: false,
// ... 更多自定義屬性
})
</script>
defineModel
defineModel 這是一個語法糖,目前需要手動開啟,否則無法識別。
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue({
script: {
defineModel: true,
propsDestructure: true // 解構(gòu) props
}
})],
})
有人嫌棄 組件內(nèi)部 v-model 的實現(xiàn)方式有點繁瑣,所以就做了這個語法糖給大家減少代碼量,我們也來體驗一下。
const modelValue = defineModel()
console.log(modelValue)
我們看看 的結(jié)構(gòu)
{__v_isRef: true}
value: (...)
__v_isRef: true
get value: ? value()
set value: ? value(value)
只是一個普通的對象看不出來有什么名堂,我們來看一下內(nèi)部的實現(xiàn)方式:
function useModel(props, name, options) {
const i = getCurrentInstance();
if (process.env.NODE_ENV !== "production" && !i) {
warn(`useModel() called without active instance.`);
return ref();
}
if (process.env.NODE_ENV !== "production" && !i.propsOptions[0][name]) {
warn(`useModel() called with prop "${name}" which is not declared.`);
return ref();
}
if (options && options.local) {
const proxy = ref(props[name]);
watch(
() => props[name], // 監(jiān)聽外部組件的值的變化
(v) => proxy.value = v // 賦值給內(nèi)部屬性
);
watch(proxy, (value) => { // 監(jiān)聽內(nèi)部屬性的變化
if (value !== props[name]) {
i.emit(`update:${name}`, value); // 提交給外部組件
}
});
return proxy;
} else {
return {
__v_isRef: true,
get value() {
return props[name]; // 返回外部組件的值
},
set value(value) {
i.emit(`update:${name}`, value); // 內(nèi)部組件賦值,提交給外部組件
}
};
}
}
前面各種判斷,然后option模式下返回一個 ref;setup 模式下返回一個對象。取值的時候,返回 props[name]
Props 的響應(yīng)式解構(gòu)
我個人是不喜歡解構(gòu)的,直接使用不香嗎?其實vue表面上不讓我們用,其實內(nèi)部悄悄的在用,比如上面那個useModel 不就是嘛。
這個也是實驗性的,想要體驗需要手動設(shè)置,設(shè)置方法在上面。
const { name } = defineProps<{ name: string }>()
watchEffect(() => {
console.log(`name is: ${name}`)
})
const aa = computed(() => { return name + '響應(yīng)'})
看打印效果,只是普通的string,那么是如何實現(xiàn)響應(yīng)的呢?還得看看“編譯后”的代碼是什么樣子的。
setup(__props, { expose: __expose }) {
__expose();
watchEffect(() => {
console.log(`name is: ${__props.name}`);
});
const aa = computed(() => {
return __props.name + "\u54CD\u5E94";
});
編譯后會生成一個 setup 函數(shù),props 通過 參數(shù) __props 傳入,需要監(jiān)聽的地方,會把 name 變成 __props.name,這樣就實現(xiàn)響應(yīng)性了。也就是說,還是一個語法糖。
從外部文件引入 props 的定義( 單文件組件類型導(dǎo)入)
從外部引入 props 的定義,這個功能非常實用,以前封裝UI庫,想實現(xiàn)共享屬性定義的時候卡了好久,使用OptionAPI,還是使用CompositionAPI,都各有優(yōu)缺點,最后只好折中一下。
現(xiàn)在支持外部導(dǎo)入那就方便多了。
比如我們先在一個ts文件里面定義一個接口:
export interface IFromItemProps {
/**
* 表單的 model
*/
model: {[key: string]: any},
/**
* 對應(yīng)的字段名稱
*/
colName: string,
/**
* 控件的備選項,單選、多選、等控件需要
*/
optionList?: Array<{
label: string,
value: string | number | boolean,
disabled: boolean
}>,
/**
* 是否顯示可清空的按鈕,默認顯示
*/
clearable?: boolean,
/**
* 浮動的提示信息,部分控件支持
*/
title?: string,
/**
* 組件尺寸
*/
size?: string
}
text 組件
然后我們可以 基于 el-input 做一個自己的 nf-text ,然后引入接口定義,還可以在 nf-list 等里面引入,這比以前使用的方式正規(guī)多了,也能更好的支持TS。
<template>
<el-input
v-model="model[colName]"
v-bind="$attrs"
:id="'c' + colName"
:name="'c' + colName"
:size="size"
:clearable="clearable"
>
</el-input>
</template>
<script setup lang="ts">
// 引入 類型定義
import type { IFromItemProps } from './base'
// 定義 props
const props = defineProps<IFromItemProps>()
console.log('props - text', props)
</script>
看看效果
Proxy {model: Proxy, colName: 'name', title: '姓名', size: 'small', clearable: true, …}
[[Handler]]: Object
[[Target]]: Proxy
[[Handler]]: Object
[[Target]]: Object
clearable: true
colName: "name"
model: Proxy {name: 'jyk', city: Array(0), time: ''}
optionList: undefined
size: "small"
title: "姓名"
[[Prototype]]: Object
[[IsRevoked]]: false
[[IsRevoked]]: false
select 組件
你可能會覺得,這封裝的有意義嗎?只看一個確實沒啥意思,不過表單里面不是只有文本框這一種,還需要其他類型,定義接口就是為了統(tǒng)一風(fēng)格。
我們再封裝一個select看看:
<template>
<el-select
v-model="model[colName]"
v-bind="$attrs"
:id="'c' + colName"
:name="'c' + colName"
:size="size"
:clearable="clearable"
:multiple="multiple"
>
<el-option
v-for="item in optionList"
:key="'select' + item.value"
:label="item.label"
:value="item.value"
:disabled="item.disabled"
>
</el-option>
</el-select>
</template>
這里使用 v-for 創(chuàng)建 el-option,外部調(diào)用的時候就方便多了。
<script setup lang="ts">
import type { IFromItemProps } from './base'
const props = defineProps<IFromItemProps & {multiple?: boolean}>()
console.log('props - list', props)
</script>
可以增加屬性。
最后看一下使用情況
import nfText from './form/text.vue'
import nfList from './form/list.vue'
import nfDatetime from './form/datetime.vue'
const model = reactive({
name: 'jyk',
city: '',
time: ''
})
const myText = {
colName: 'name'
}
const myList = {
colName: 'city',
multiple: true,
optionList: [
{
label: '北京',
value: 1
},
{
label: '上海',
value: 2
}
]
}
<nf-text :model="model" v-bind="myText"></nf-text>
<nf-list :model="model" v-bind="myList"></nf-list>
...
封裝之后,我們不用關(guān)心組件是否需要子組件(比如el-select需要設(shè)置 el-option),都是
<nf-text v-bind="myText"></nf-text>
這種簡單粗暴的方式,而組件需要的屬性,我們可以做成json的形式,這樣更方便。
另外大家不要忘記 vue 提供的動態(tài)組件(component :is="xxx"),這樣我們用 v-for 就可以把一個表單里的所有子組件都給遍歷出來,不用一個一個的寫了。
小結(jié)
目前只對這幾個新特性感興趣體驗了一下,其他的還沒來得及。還有一個 props 設(shè)置默認值的問題,可以使用 withDefaults:
const props = withDefaults(defineProps< IFromItemProps >(), {
clearable: true
})
只是好像 默認值的部分需要直接寫進去。這個,等待以后版本的更新吧,估計以后都會支持外部導(dǎo)入的方式吧。
參考文檔
- Announcing Vue 3.3 | The Vue Point
- Vue 3.3 主要新特性詳解 - 三咲智子 Kevin Deng
參考資料
[1] Generic component enhancements - Discussion #436: https://github.com/vuejs/rfcs/discussions/436
[2] unplugin-vue-define-options - npm: https://www.npmjs.com/package/unplugin-vue-define-options
[3] Announcing Vue 3.3 | The Vue Point: https://blog.vuejs.org/posts/vue-3-3
[4] Vue 3.3 主要新特性詳解 - 三咲智子 Kevin Deng: https://xlog.sxzz.moe/vue-3-3