寫在前面
2020.4.20 更新:目前Vue3.0 已經(jīng)出于 Beta 測試階段,即可以對開發(fā)者開放使用了,官方工具鏈如vue-router 、 vuex 等仍處于 Alpha 階段,距離正式發(fā)布還有一段時(shí)間。
本篇文章發(fā)布于2020.01.14,如果后續(xù)更新/發(fā)布的話,會(huì)考慮更新這篇文章的內(nèi)容。
閱讀依賴
本篇文章默認(rèn)讀者已經(jīng)了解以下知識(shí),沒有掌握的請去補(bǔ)課~
- Vue2.x的用法
- npm的安裝與構(gòu)建,npx局部安裝替代全局安裝方法(了解最好,非必須)
- git相關(guān)操作
-
react hooks相關(guān)知識(shí)(了解最好,非必須)
Vue3.0介紹
其他文章已經(jīng)說得差不多了,幾個(gè)核心點(diǎn)就是Proxy/函數(shù)式API/TS支持以及模板編譯優(yōu)化,不再贅述,想看源碼的去github拉代碼即可:
https://github.com/vuejs/vue-next
快速開始
使用 @vue/cli 構(gòu)建
需要 vue-cli 版本大于 3.x ,如果是2.x版本,得先升級依賴。
的這里只介紹全局安裝方法:
npm i -g @vue/cli
vue create my-vue3-app
注意:目前不建議選擇 Typescript 預(yù)設(shè),因?yàn)?vuex vue-router 等工具尚未完全支持(當(dāng)然精力旺盛的可以自己實(shí)現(xiàn))。
項(xiàng)目初始化完畢,進(jìn)入 my-vue3-app 目錄,執(zhí)行 vue-next 安裝命令:
vue add vue-next
這個(gè)命令可以自動(dòng)運(yùn)行安裝腳本,將你當(dāng)前 vue 項(xiàng)目升級為 vue 3.0 項(xiàng)目:
?? Installing vue-cli-plugin-vue-next...
yarn add v1.22.4
[1/4] Resolving packages...
[2/4] Fetching packages...
success Saved 1 new dependency.
info Direct dependencies
└─ vue-cli-plugin-vue-next@0.1.2
info All dependencies
└─ vue-cli-plugin-vue-next@0.1.2
Done in 24.71s.
? Successfully installed plugin: vue-cli-plugin-vue-next
?? Invoking generator for vue-cli-plugin-vue-next...
? Running completion hooks...
? Successfully invoked generator for plugin: vue-cli-plugin-vue-next
vue-next Installed vuex 4.0.
vue-next Documentation available at https://github.com/vuejs/vuex/tree/4.0
vue-next Installed vue-router 4.0.
vue-next Documentation available at https://github.com/vuejs/vue-router-next
vue-next Installed @vue/test-utils 2.0.
vue-next Documentation available at https://github.com/vuejs/vue-test-utils-next
happy coding.
從源碼構(gòu)建
首先把代碼拉到本地(默認(rèn)使用master分支即可),在根目錄下執(zhí)行npm install && npm run build, 就能在/packages/vue/dist下得到打包后的文件:
vue.cjs.js
vue.esm-bundler.j s
vue.esm.prod.js
vue.global.prod.js
vue.cjs.prod.js
vue.esm.js
vue.global.js
vue.runtime.esm-bundler.js
瀏覽器引入使用
如果在純?yōu)g覽器環(huán)境下,我們選用上面的vue.global.js作為依賴,因?yàn)樗碎_發(fā)提示以及template編譯器。直接來一段小demo:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue3.0 sample</title>
<!-- 在瀏覽器下template可以這么寫了 -->
<script type="text/x-template" id="greeting">
<h3>{{ message }}</h3>
</script>
<script src="vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script type="text/javascript">
const { createApp } = Vue;
const myApp = {
template: '#greeting',
data() {
return {
message: 'Hello Vue3.0'
}
}
}
createApp(myApp).mount('#app')
</script>
</body>
</html>
瀏覽器中打開,你將看到如下文字:
Hello Vue3.0
可以看到:new Vue變成了createApp,不再接受option參數(shù),而是搬到了mount方法中。我們的template字符串,現(xiàn)在可以使用text/x-template的腳本格式引入,至于其他的用法,基本和2.0一模一樣
另外,源碼倉庫中 /packages/vue/examples目錄下,提供了幾個(gè)官方示例,有興趣的可以去參閱
使用webpack構(gòu)建3.0的sfc
仔細(xì)讀讀倉庫中的readme.md,我們發(fā)現(xiàn)尤大已經(jīng)很貼心地為我們做了一個(gè)webpack構(gòu)建項(xiàng)目的最小實(shí)踐: https://github.com/vuejs/vue-next-webpack-preview
同樣的操作:拉代碼->構(gòu)建->運(yùn)行后,看到的是一個(gè)點(diǎn)擊計(jì)數(shù)器的基本用法:
<template>
<img src="./logo.png">
<h1>Hello Vue 3!</h1>
<button @click="inc">Clicked {{ count }} times.</button>
</template>
<script type="text/javascript">
import { ref } from 'vue'
export default {
setup() {
const count = ref(0); // 響應(yīng)式數(shù)據(jù)
// 事件回調(diào)不需要任何處理,直接作為對象方法返回即可;
const inc = () => {
count.value++
}
return {
count,
inc
}
}
}
</script>
與2.0不同了,這個(gè)setup方法就是3.0的新API,定義了一個(gè)響應(yīng)式屬性count和方法inc,將他們作為對象返回,就能在template中使用。其中,ref是對一個(gè)原始值添加響應(yīng)式(裝箱),通過.value獲取和修改(拆箱),對應(yīng)2.0中的data,如果需要對一個(gè)對象/數(shù)組添加響應(yīng)式,則需要調(diào)用reactive()方法
import { reactive } from 'vue'
export default {
setup() {
return {
foo: reactive({ a: 1, b: 2 })
}
}
}
拓展實(shí)踐
為了更進(jìn)一步理解setup,我們改造一下點(diǎn)擊計(jì)數(shù)器——鍵盤計(jì)數(shù)器。
要實(shí)現(xiàn)的目標(biāo)和思路為:
- 將計(jì)數(shù)器變成一個(gè)組件,由外部控制開啟/關(guān)閉:
component、ref和v-if的使用 - 計(jì)數(shù)器監(jiān)聽某個(gè)鍵盤按鍵,按鍵名稱由父組件作為props傳入(如Enter,Space等):
setup(props)獲取 - 組件渲染(
onMounted)后開始監(jiān)聽,組件拆卸(onUnmounted)后取消監(jiān)聽:生命周期鉤子在3.0中的用法 - 添加
is-odd文本,表示按鍵次數(shù)是否為奇數(shù):computedvue3.0中的用法 - 按鍵次數(shù)為5的倍數(shù)(0除外)時(shí),彈出alert窗口:
watch在vue3.0中的用法
Talk is cheap, show me the code
首先是改造App.vue父組件導(dǎo)入key-press-enter子組件,注意看template有何變化
<template>
<!-- 設(shè)置checkbox控制組件開關(guān) -->
<input id="key-counter" type="checkbox" v-model="enableKeyPressCounter">
<label for="key-counter">check to enable keypress counter</label>
<key-press-counter v-if="enableKeyPressCounter" key-name="Enter" />
</template>
<script type="text/javascript">
import { ref } from 'vue'
import KeyPressCounter from './KeyPressCounter.vue';
export default {
components: {
KeyPressCounter // 組件用法和原來一致
},
setup() {
return {
enableKeyPressCounter: ref(false), // 是否開啟組件
}
}
}
</script>
可以發(fā)現(xiàn):template現(xiàn)在可以像jsx一樣作為碎片引入,不需要添加根元素了(當(dāng)然#app根容器還是需要的)
接著是子組件KeyPressCounter.vue:
<script>
<template>
<h3>Listening keypress: {{ keyName }}</h3>
<p>Press {{ pressCount }} times!</p>
<p>Is odd times: {{ isOdd }}</p>
</template>
<script type="text/javascript">
import { onMounted, onUnmounted, ref, effect, computed } from 'vue';
/**
* 創(chuàng)建一個(gè)鍵盤按鍵監(jiān)聽鉤子
* @param keyName 按鍵名稱
* @param callback 監(jiān)聽回調(diào)
*/
const useKeyboardListener = (keyName, callback) => {
const listener = (e) =>{
console.log(`你按下了${e.key}鍵`) // 用來驗(yàn)證監(jiān)聽時(shí)間是否開啟/關(guān)閉
if (e.key !== keyName) {
return;
}
callback()
}
// 當(dāng)組件渲染時(shí)監(jiān)聽
onMounted(()=>{
document.addEventListener('keydown', listener)
})
// 當(dāng)組件拆解時(shí)監(jiān)聽
onUnmounted(()=>{
document.removeEventListener('keydown', listener)
})
}
export default {
name: "EnterCounter",
/**
* @param props 父組件傳入的props
* @return { Object } 返回的對象可以在template中引用
*/
setup(props) {
const { keyName } = props
const pressCount = ref(0)
// hooks調(diào)用
useKeyboardListener(keyName, ()=>{
pressCount.value += 1;
})
// watch的用法,可以看到,現(xiàn)在無需聲明字段或者source,vue自動(dòng)追蹤依賴
effect(()=>{
if (pressCount.value && pressCount.value % 5 === 0) {
alert(`you have press ${pressCount.value} times!`)
}
})
// computed的用法,基本是原來的配方
const isOdd = computed(()=> pressCount.value % 2 === 1)
return {
keyName,
pressCount,
isOdd
}
}
}
</script>
以后編寫組件就是setup一把梭了!是不是越來越像react hooks了?
對比一下傳統(tǒng)寫法:
<template>
<div>
<h3>Listening keypress: {{ keyName }}</h3>
<p>Press {{ pressCount }} times!</p>
<p>Is odd times: {{ isOdd }}</p>
</div>
</template>
<script type="text/javascript">
let listener
export default {
name: "EnterCounter",
props: {
keyName: String
},
computed: {
isOdd() {
return this.pressCount % 2 === 1;
}
},
data() {
return {
pressCount: 0
}
},
mounted() {
listener = (e) =>{
if (e.key !== this.keyName) {
return;
}
this.callback()
}
document.addEventListener('keydown', listener)
},
beforeUnmount() {
document.removeEventListener('keydown', listener)
},
watch: {
pressCount(newVal) {
if (newVal && newVal % 5 === 0) {
alert(`you have press ${newVal} times!`)
}
}
},
methods: {
callback() {
this.pressCount += 1;
}
}
}
</script>
當(dāng)然,聲明式vs函數(shù)式,不能說哪個(gè)一定比另外一個(gè)好。尤大依然為我們保持了傳統(tǒng)api,這也意味著從2.0遷移到3.0,付出的成本是非常平滑的。
使用 Vuex hooks
初始化
首先定義全局的 Store :
import { createStore } from 'vuex';
export default createStore({
state: {
username: 'Xiaoming',
},
modules: {
foo: {
namespaced: true,
state: {
bar: 'baz',
},
modules: {
nested: {
namespaced: true,
state: {
final: 'you\'ve done',
},
},
},
},
},
});
接著在組件根實(shí)例中注入
import { createAPP } from 'vue'
import Store from './store'
import APP from './views/App.vue'
createApp(APP)
.use(Store) // 以插件安裝形式注入
.mount('#app')
如何在setup中引用
目前 vuex 的輔助函數(shù) mapState 、mapGetter 、 mapMutation 、 mapAction,需要綁定組件實(shí)例上下文,而 setup 函數(shù)中無法訪問組件實(shí)例,所以這些輔助函數(shù)在 setup 中應(yīng)該用不上了,取而代之的應(yīng)該是 useState 、useGetter 、 useMutation 、 useAction ,目前官方尚未實(shí)現(xiàn)。這里根據(jù) vuex 源碼寫一個(gè)兼容3.0的 useState ,拋磚引玉(代碼有點(diǎn)長,可以跳著看):
import { computed } from 'vue';
import { useStore } from 'vuex';
const normalizeNamespace = (fn) => (namespace, map) => {
let appliedMap = map;
let appliedNamespace = namespace;
if (typeof appliedNamespace !== 'string') {
appliedMap = namespace;
appliedNamespace = '';
} else if (namespace.charAt(appliedNamespace.length - 1) !== '/') {
appliedNamespace += '/';
}
return fn(appliedNamespace, appliedMap);
};
const normalizeMap = (map) => (Array.isArray(map)
? map.map((item) => ({ key: item, val: item }))
: Object.entries(map).map(([key, val]) => ({ key, val })));
export const useState = normalizeNamespace((namespace, states) => {
const res = {};
// 將源碼中引用的 this.$store 替換成全局store
const store = useStore();
normalizeMap(states).forEach(({ key, val }) => {
res[key] = (computed(() => {
let { state, getters } = store;
if (namespace) {
// eslint-disable-next-line no-underscore-dangle
const module = store._modulesNamespaceMap[namespace];
if (!module) {
return;
}
state = module.context.state;
getters = module.context.getters;
}
// eslint-disable-next-line consistent-return
return typeof val === 'function'
? val.call(store, state, getters)
: state[val];
}));
// mark vuex getter for devtools
res[key].vuex = true;
});
return res;
}, true);
編寫完畢,在實(shí)際組件中使用:
<template>
<header>
<h5>
Hello {{ username }} | {{ bar }} | {{ final }}
</h5>
</header>
</template>
<script>
import { useState } from '../store/hooks';
export default {
name: 'Header',
setup() {
const { username } = useState(['username']);
const { bar } = useState('foo', ['bar']);
const { final } = useState('foo/nested', ['final']);
return {
username,
bar,
final,
};
},
};
</script>
輸出結(jié)果:
Hello Xiaoming | baz | you've done
總結(jié)
- template支持碎片,即除根組件外,子組件無需聲明根元素。
- 傳統(tǒng)的組件option api,現(xiàn)在可以用setup來實(shí)現(xiàn),不僅比以前變得更加靈活,在類型分析上(typescript)將會(huì)支持得更好
- 大部分api如ref/reactive/onMounted等方法,現(xiàn)在支持按需導(dǎo)入,對于tree-shaking優(yōu)化有利
- setup使開發(fā)者不必再關(guān)心令人頭疼的
this問題 - setup是一把雙刃劍,如果你的思路足夠清晰,那么它將會(huì)是你抽象邏輯的利器。反之使用不當(dāng)同樣也會(huì)讓你的代碼變成意大利面條??
- vuex 的輔助函數(shù)將在未來以
useXXXX的形式 兼容 setup 函數(shù)。