写在前面
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
└─ [email protected]
info All dependencies
└─ [email protected]
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:
Vue3.0 sample
浏览器中打开,你将看到如下文字:
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
同样的操作:拉代码->构建->运行后,看到的是一个点击计数器的基本用法:
Hello Vue 3!
与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
现在可以像jsx一样作为碎片引入,不需要添加根元素了(当然#app根容器还是需要的)
接着是子组件KeyPressCounter.vue
:
以后编写组件就是setup一把梭了!是不是越来越像react hooks
了?
对比一下传统写法:
Listening keypress: {{ keyName }}
Press {{ pressCount }} times!
Is odd times: {{ isOdd }}
当然,声明式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);
编写完毕,在实际组件中使用:
Hello {{ username }} | {{ bar }} | {{ final }}
输出结果:
Hello Xiaoming | baz | you've done
总结
- template支持碎片,即除根组件外,子组件无需声明根元素。
- 传统的组件option api,现在可以用setup来实现,不仅比以前变得更加灵活,在类型分析上(typescript)将会支持得更好
- 大部分api如ref/reactive/onMounted等方法,现在支持按需导入,对于tree-shaking优化有利
- setup使开发者不必再关心令人头疼的
this
问题 - setup是一把双刃剑,如果你的思路足够清晰,那么它将会是你抽象逻辑的利器。反之使用不当同样也会让你的代码变成意大利面条
- vuex 的辅助函数将在未来以
useXXXX
的形式 兼容 setup 函数。