写在前面
2020.4.20 更新:目前Vue3.0
已经出于 Beta
测试阶段,即可以对开发者开放使用了,官方工具链如vue-router
、 vuex
等仍处于 Alpha
阶段,距离正式发布还有一段时间。
本篇文章发布于2020.01.14,如果后续更新/发布的话,会考虑更新这篇文章的内容。
阅读依赖
本篇文章默认读者已经了解以下知识,没有掌握的请去补课~
- Vue2.x的用法
- npm的安装与构建,npx局部安装替代全局安装方法(了解最好,非必须)
- git相关操作
-
react hooks
相关知识(了解最好,非必须)
Vue3.0介绍
其他文章已经说得差不多了,几个核心点就是Proxy/函数式API/TS支持以及模板编译优化,不再赘述,想看源码的去github拉代码即可:
https://github.com/vuejs/vue-next
快速开始
使用 @vue/cli 构建
需要 vue-cli 版本大于 3.x ,如果是2.x版本,得先升级依赖。
的这里只介绍全局安装方法:
npm i -g @vue/cli
vue create my-vue3-app
注意:目前不建议选择 Typescript 预设,因为 vuex vue-router 等工具尚未完全支持(当然精力旺盛的可以自己实现)。
项目初始化完毕,进入 my-vue3-app
目录,执行 vue-next
安装命令:
vue add vue-next
这个命令可以自动运行安装脚本,将你当前 vue 项目升级为 vue 3.0 项目:
📦 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.
从源码构建
首先把代码拉到本地(默认使用master分支即可),在根目录下执行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
浏览器引入使用
如果在纯浏览器环境下,我们选用上面的vue.global.js
作为依赖,因为它包含了开发提示以及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
参数,而是搬到了mount
方法中。我们的template字符串,现在可以使用text/x-template
的脚本格式引入,至于其他的用法,基本和2.0一模一样
另外,源码仓库中 /packages/vue/examples目录下,提供了几个官方示例,有兴趣的可以去参阅
使用webpack构建3.0的sfc
仔细读读仓库中的readme.md
,我们发现尤大已经很贴心地为我们做了一个webpack构建项目的最小实践: https://github.com/vuejs/vue-next-webpack-preview
同样的操作:拉代码->构建->运行后,看到的是一个点击计数器的基本用法:
<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); // 响应式数据
// 事件回调不需要任何处理,直接作为对象方法返回即可;
const inc = () => {
count.value++
}
return {
count,
inc
}
}
}
</script>
与2.0不同了,这个setup
方法就是3.0的新API,定义了一个响应式属性count
和方法inc
,将他们作为对象返回,就能在template
中使用。其中,ref
是对一个原始值
添加响应式(装箱),通过.value
获取和修改(拆箱),对应2.0中的data
,如果需要对一个对象/数组添加响应式,则需要调用reactive()
方法
import { reactive } from 'vue'
export default {
setup() {
return {
foo: reactive({ a: 1, b: 2 })
}
}
}
拓展实践
为了更进一步理解setup,我们改造一下点击计数器——键盘计数器。
要实现的目标和思路为:
- 将计数器变成一个组件,由外部控制开启/关闭:
component
、ref
和v-if
的使用 - 计数器监听某个键盘按键,按键名称由父组件作为props传入(如Enter,Space等):
setup(props)
获取 - 组件渲染(
onMounted
)后开始监听,组件拆卸(onUnmounted
)后取消监听:生命周期钩子
在3.0中的用法 - 添加
is-odd
文本,表示按键次数是否为奇数:computed
vue3.0中的用法 - 按键次数为5的倍数(0除外)时,弹出alert窗口:
watch
在vue3.0中的用法
Talk is cheap, show me the code
首先是改造App.vue
父组件导入key-press-enter
子组件,注意看template
有何变化
<template>
<!-- 设置checkbox控制组件开关 -->
<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>
可以发现:template
现在可以像jsx一样作为碎片引入,不需要添加根元素了(当然#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';
/**
* 创建一个键盘按键监听钩子
* @param keyName 按键名称
* @param callback 监听回调
*/
const useKeyboardListener = (keyName, callback) => {
const listener = (e) =>{
console.log(`你按下了${e.key}键`) // 用来验证监听时间是否开启/关闭
if (e.key !== keyName) {
return;
}
callback()
}
// 当组件渲染时监听
onMounted(()=>{
document.addEventListener('keydown', listener)
})
// 当组件拆解时监听
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调用
useKeyboardListener(keyName, ()=>{
pressCount.value += 1;
})
// watch的用法,可以看到,现在无需声明字段或者source,vue自动追踪依赖
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
了?
对比一下传统写法:
<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>
当然,声明式vs函数式,不能说哪个一定比另外一个好。尤大依然为我们保持了传统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',
},
},
},
},
},
});
接着在组件根实例中注入
import { createAPP } from 'vue'
import Store from './store'
import APP from './views/App.vue'
createApp(APP)
.use(Store) // 以插件安装形式注入
.mount('#app')
如何在setup中引用
目前 vuex 的辅助函数 mapState
、mapGetter
、 mapMutation
、 mapAction
,需要绑定组件实例上下文,而 setup 函数中无法访问组件实例,所以这些辅助函数在 setup 中应该用不上了,取而代之的应该是 useState
、useGetter
、 useMutation
、 useAction
,目前官方尚未实现。这里根据 vuex 源码写一个兼容3.0的 useState
,抛砖引玉(代码有点长,可以跳着看):
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);
编写完毕,在实际组件中使用:
<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>
输出结果:
Hello Xiaoming | baz | you've done
总结
- template支持碎片,即除根组件外,子组件无需声明根元素。
- 传统的组件option api,现在可以用setup来实现,不仅比以前变得更加灵活,在类型分析上(typescript)将会支持得更好
- 大部分api如ref/reactive/onMounted等方法,现在支持按需导入,对于tree-shaking优化有利
- setup使开发者不必再关心令人头疼的
this
问题 - setup是一把双刃剑,如果你的思路足够清晰,那么它将会是你抽象逻辑的利器。反之使用不当同样也会让你的代码变成意大利面条🍝
- vuex 的辅助函数将在未来以
useXXXX
的形式 兼容 setup 函数。