Vue3学习

https://v3.cn.vuejs.org/guide/migration/introduction.html#overview 从Vue2到Vue3

学习

在学习Vue3之前、还是需要巩固一下Vue2的知识

介绍

Vue3: https://github.com/vuejs/vue-next/

Vue3中文文档:https://v3.cn.vuejs.org/

新特性:

引入

  • 通过gcore引入:

    1
    <script src="https://cdn.jsdelivr.net/npm/vue@3.0.0/dist/vue.global.min.js"></script>
  • 脚手架使用:

    1
    npm i -g @vue/cli

    查看vue版本(截至2020-10-16)

    1
    2
    C:\Users\Administrator>vue -V
    @vue/cli 4.5.7

使用

Vue-cli创建

这里使用Vue-cli创建项目

1
vue create <项目名>

以下为步骤:

  1. 选择默认模板

    1
    2
    3
    4
    5
    Vue CLI v4.5.7
    ? Please pick a preset: (Use arrow keys)
    Default ([Vue 2] babel, eslint)
    Default (Vue 3 Preview) ([Vue 3] babel, eslint)
    > Manually select features

    这里显示了可以使用Vue2或者Vue3的默认模板、但是我还是使用自定义配置

  2. 选择需要的组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ? Check the features needed for your project:
    >(*) Choose Vue version
    (*) Babel
    ( ) TypeScript
    ( ) Progressive Web App (PWA) Support
    (*) Router
    (*) Vuex
    ( ) CSS Pre-processors
    ( ) Linter / Formatter
    ( ) Unit Testing
    ( ) E2E Testing

    这里根据自己来选择配置

  3. 选择版本

    1
    2
    3
    ? Choose a version of Vue.js that you want to start the project with
    2.x
    > 3.x (Preview)

    当然是Vue3了

  4. 使用hash模式还是history模式

    1
    Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) y

    hash会带#

  5. 配置放在那?

    1
    2
    3
    ? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
    > In dedicated config files
    In package.json

    这里选择放在配置文件里而不是package.json

  6. 保存配置

    1
    ? Save this as a preset for future projects? (y/N) n

    是否保存以上配置

进入目录中、看package.json可以看到vue版本是3.0.0、之后npm run serve

Vite创建

Vite 是一个由原生 ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发,在生产环境下基于 Rollup 打包。比webpack打包更加快速。

它主要具有以下特点:

  1. 快速的冷启动
  2. 即时的模块热更新
  3. 真正的按需编译

快速使用:

1
2
3
4
npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev

简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Vue3学习</title>
</head>
<body>
<div id="app">{{msg}}</div>
<script src="https://cdn.jsdelivr.net/npm/vue@3.0.0/dist/vue.global.min.js"></script>
<script>
const root = {
data() {
return {
msg: "Hello world"
}
}
}
const app = Vue.createApp(root).mount("#app")
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>第一个Vue</title>
</head>
<body>

<div id="app">{{ msg }}</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
var vue = new Vue({
el:"#app",
data:{
msg:"Hello world"
}
});
</script>
</body>
</html>

Composition API

参考文章:Vue3.x 从零开始(三)—— 使用 Composition API 优化组件

Composition API字面意思是组合API,它是为了实现基于函数的逻辑复用机制而产生的。

setup

setup 函数是在解析其它组件选项之前,也就是 beforeCreate 之前执行、所以在 setup 内部,this 不是当前组件实例的引用,也就是说 setup 中无法直接调用组件的其他数据

vue 2 中的 destroyedbeforeDestroy 钩子在 vue 3 中被重命名为 unmountedbeforeUnmount

setup 有着生命周期钩子不具备的参数:propscontext、它只是基于 beforeCreate 运行,但函数内部无法通过 this 获取组件实例

1
2
3
4
5
6
7
8
9
10
11
setup(props, context) {
// 组件 props
console.log(props);
const { attrs, slots, emit } = context;
// Attribute (非响应式对象)
console.log(attrs);
// 组件插槽 (非响应式对象)
console.log(slots);
// 触发事件 (方法)
console.log(emit);
}

ref和reactive

ref(创建一个)和reactive(创建多个)都是用来创建响应式对象、使用前需要先引入

1
import { ref,reactive } from "vue";

简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<div> 数字 {{ count }} - 姓名 {{ people.name }} 、年龄 {{ people.age }}</div>
</template>

<script>
import { ref,reactive } from "vue";

export default {
name: 'Test',
setup(){
// 定义一个ref响应式对象
const count = ref(0)

//在 setup 内部,ref 包装后的变量需要通过 value 来修改变量的值
count.value = 2

// 定义多个使用reactive
const people = reactive({
name: "zykj",
age:18
})

return { count,people } // 这里返回的任何内容都可以用于组件的其余部分
}
}
</script>

注意,从 setup 返回的 refs 在模板中访问时是被自动解开的,因此不应在模板中使用 .value

setup的返回值

setup 显式的返回了一个对象,这个对象的所有内容都会暴露给组件的其余部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ref } from 'vue'

setup() {
return {
name: ref('zykj'),
foo: (text: string) => {
console.log(`Hello ${text}`);
},
};
},
mounted() {
// 直接使用 setup 的返回值
this.foo(this.name);
},

因为 props 是响应式的,所以不能直接对 props 使用 ES6 解构,因为它会消除 prop 的响应性、在Vue3提供全局方法toRefs来解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { reactive,toRefs } from "vue";

export default {
name: 'Test',
setup(){
// 定义多个使用reactive
const people = reactive({
name: "zykj",
age:18
})

// 通过 toRefs 包装后的 props 可以在 ES6 解构之后依然具有响应性
const {name,age} = toRefs(people)

console.log(name.value,age.value) // zykj 18

return { name,age } // 这里返回的任何内容都可以用于组件的其余部分
}
}

生命周期钩子

setup 中注册生命周期钩子 : https://v3.cn.vuejs.org/guide/composition-api-lifecycle-hooks.html

可以通过在生命周期钩子前面加上 on 来访问组件的生命周期钩子

1
2
3
4
5
6
7
8
export default {
setup() {
// mounted
onMounted(() => {
console.log('Component is mounted!')
})
}
}

使用渲染函数

setup 还可以返回一个渲染函数、返回的渲染函数会覆盖template里的内容

1
2
3
4
5
6
7
8
9
10
import { h, ref, reactive } from 'vue'

export default {
setup() {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })
// 返回一个渲染函数以覆盖 template
return () => h('div', [readersNumber.value, book.title])
}
}

模板引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template> 
<div ref="root">这是根元素</div>
</template>

<script>
import { ref, onMounted } from 'vue'

export default {
setup() {
const root = ref(null)

onMounted(() => {
// DOM元素将在初始渲染后分配给ref
console.log(root.value) // <div>这是根元素</div>
})

return {
root
}
}
}
</script>

setup中使用computed、watch

同样使用前、需要先引入

1
import { computed,watch } from 'vue'
  1. computed 计算属性可以在组件中直接使用,并会随着响应式数据的更新而更新

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import { computed,reactive } from 'vue'

    export default {
    name: 'Test',
    setup(){
    const data = reactive({
    counter: 1,
    doubleCounter: computed(()=> datacounter * 2)
    })

    return { data }
    }
    }
  2. watch 用来监听数据的变化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import { computed,watch,reactive } from 'vue'

    export default {
    name: 'Test',
    setup(){
    const data = reactive({
    counter: 1,
    doubleCounter: computed(()=> datacounter * 2)
    })

    watch(()=> data.counter,(newValue,oldValue)=>{
    console.log(newValue,oldValue)
    })

    return { data }
    }
    }

Teleport

https://v3.cn.vuejs.org/guide/teleport.html

传送们组件提供一种简洁的方式可以指定它里面内容的父元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
这里是Teleport学习
<button @click="openTe=!openTe">显示隐藏Teleport</button>
<!-- to="body" 代表插入到 body 里 -->
<teleport to="body">
<div v-if="openTe" style="border: 1px solid red;text-align:center;padding:5px;">这里是Teleport的内容</div>
</teleport>
</template>

<script>
export default {
name: 'Test2',
data() {
return {
openTe: false
}
}
}
</script>

Fragments

在Vue2、模板中需要有一个根节点

1
2
3
4
5
<template>
<div>
哈哈哈
</div>
</template>

Vue3中、可以有多个

1
2
3
4
5
6
<template>
<div>哈哈哈</div>
<p>
???
</p>
</template>

自定义渲染器

http://www.zhufengpeixun.com/jg-vue/vue-analyse/custom-netder.html

自定义渲染器 (netderer):这个 API 可以用来创建自定义的渲染器

CanvasApp.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<template>
<div id="app" @click="handleClick">
<circle :data="state.data" :x="200" :y="300" :r="200"></circle>
</div>
</template>

<script>
import { reactive , ref } from 'vue'

export default {
setup() {
const state = reactive({
data: [{
name: '语文',
count: 200,
color: 'red'
},
{
name: '物理',
count: 100,
color: 'yellow'
},
{
name: '数学',
count: 300,
color: 'gray'
},
{
name: '化学',
count: 200,
color: 'pink'
}]
});

function handleClick() {
state.data.push({
name: '英语',
count: 30,
color: 'green'
})
}
return {
state,
handleClick
}
}
}
</script>

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import { createApp, createnetderer } from 'vue'
import App from './App.vue'
import './index.css'

import CanvasApp from './components/CanvasApp.vue'

const nodeOps = {
insert: (child, panett, anchor) => {
// 处理元素插入逻辑
// 1.如果是子元素,不是真实dom,此时只需要将数据保存到前面的虚拟对象上即可
child.panett = panett

if (!panett.childs) {
panett.childs = [child]
} else {
panett.childs.push(child)
}

// 2.如果是真实dom、需要绘制
if (panett.nodeType == 1) {
// 元素节点
draw(child)
if (child.onClick) {
canvas.addEventListener('click', () => {
child.onClick()
setTimeout(() => {
draw(child)
}, 0)
})
}
}
},
remove: child => { },
createElement: (tag, isSVG, is) => {
// 处理元素创建逻辑
return { tag }
},
createText: text => { },
createComment: text => { },
setText: (node, text) => { },
setElementText: (el, text) => { },
panettNode: node => { },
nextSibling: node => { },
querySelector: selector => { },
setScopeId(el, id) { },
cloneNode(el) { },
insertStaticContent(content, panett, anchor, isSVG) { },
patchProp(el, key, prevValue, nextValue) {
// 属性更新
el[key] = nextValue;
},
}

const netderer = createnetderer(nodeOps)

const draw = (el, noClear) => {
if (!noClear) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
if (el.tag == 'circle') {
let { data, r, x, y } = el;
let total = data.reduce((memo, curnett) => memo + curnett.count, 0);
let start = 0,
end = 0;
data.forEach(item => {
end += item.count / total * 360;
drawCircle(start, end, item.color, x, y, r);
drawCircleText(item.name, (start + end) / 2, x, y, r);
start = end;
});
}
el.childs && el.childs.forEach(child => draw(child, true));
}

const d2a = (n) => {
return n * Math.PI / 180;
}
const drawCircle = (start, end, color, cx, cy, r) => {
let x = cx + Math.cos(d2a(start)) * r;
let y = cy + Math.sin(d2a(start)) * r;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(x, y);
ctx.arc(cx, cy, r, d2a(start), d2a(end), false);
ctx.fillStyle = color;
ctx.fill();
ctx.stroke();
ctx.closePath();
}
const drawCircleText = (val, posistion, cx, cy, r) => {
ctx.beginPath();
let x = cx + Math.cos(d2a(posistion)) * r/1.25 - 20;
let y = cy + Math.sin(d2a(posistion)) * r/1.25;
ctx.fillStyle = '#000';
ctx.font = '20px 微软雅黑';
ctx.fillText(val,x,y);
ctx.closePath();
}

let ctx, canvas

function createCanvasApp(App) {
const app = netderer.createApp(App)
const mount = app.mount
app.mount = function (selector) {
// 创建并且插入画布
canvas = document.createElement("canvas")
ctx = canvas.getContext('2d')
// 设置画布基本属性
canvas.width = 500
canvas.height = 500
document.querySelector(selector).appendChild(canvas)

// 执行默认mount
mount(canvas)
}
return app
}

createCanvasApp(CanvasApp).mount("#app")

全局API改成应用程序实例调用

Vue3使用 createApp 返回app实例、由它暴露一系列全局API

1
2
3
4
5
import { createApp } from 'vue'

const app = createApp({})
.component('comp',{ netder:()=> h('div',"我是一个组件") })
.mount("#app")

v-model的变化

https://v3.cn.vuejs.org/guide/component-custom-events.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<input type="text" :value="foo" @input="$emit('update:foo', $event.target.value)" />
{{ foo }}
</template>

<script>
export default {
name: "Test3",
props: {
foo: String,
},
};
</script>
1
2
3
4
5
6
7
8
9
10
11
<Test3 v-model:foo="bar" />

...

<script>
data() {
return {
bar: "Hello"
}
},
</script>

渲染函数API的变化

https://v3.cn.vuejs.org/guide/netder-function.html

函数式组件使用变化

简单示例

1
2
3
4
5
6
7
8
9
10
11
<script>
import { h } from "vue"

function Heading(props, content) {
return h(`h${props.level}`, content.attrs, content.slots)
}

Heading.props = ['level']

export default Heading
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<netderH level="4">这里是标题</netderH>
</template>

<script>

import netderH from './components/netderH.vue'


export default {
name: 'App',
components: {
netderH
}
}
</script>

异步组件

https://v3.cn.vuejs.org/guide/component-dynamic-async.html

1
2
3
4
5
6
7
8
9
<template>
<div>这是异步组件</div>
</template>

<script>
export default {

}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<AsyncComp />
</template>

<script>
import { defineAsyncComponent} from "vue"

export default {
name: 'App',
components: {
AsyncComp: defineAsyncComponent(() => import('./components/AsyncComp.vue'))
}
}
</script>

带配置的异步组件

1
2
3
4
5
6
7
8
9
10
11
import LoadingComponent from './component/LoadingComponent.vue'
import ErrorComponent from './component/ErrorComponent.vue'
import { defineAsyncComponent} from "vue"

const asyncPageWithOptions = defineAsyncComponent({
loader: ()=> import("./Next.page"),
delay: 200,
timeout: 3000,
errorComponent: ErrorComponent,
loadingComponent: LoadingComponent
})

自定义组件白名单

vue3自定义元素检测发生在模板编译时、如果要添加一些vue之外的自定义元素、需要在编译器选项设置isCustomElement选项

模板使用vue-loader预编译、设置它提供的compilerOptions即可 : vue.config.js

1
2
3
4
5
6
7
8
9
10
11
rules:[
{
test: /\.vue$/,
use: 'vue-loader',
options: {
compilerOptions:{
isCustomElement: tag => tag === 'plastic-button'
}
}
}
]

使用vite、在vite.config.js中配置vueCompilerOptions即可:

1
2
3
4
5
module.exports = {
vueCompilerOptions: {
isCustomElement: tag => tag === 'piechart'
}
}

自定义指令

main.js

1
2
3
4
5
6
7
8
9
10
11
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App)
.directive("highlight",{
beforeMount(el,bing,vnode){
el.style.background = bing.value
}
})
.mount('#app')

App.vue

1
2
3
<template>
<p v-highlight="'green'">哈哈哈</p>
</template>

过渡

https://v3.cn.vuejs.org/guide/transitions-enterleave.html

Vue-router

https://next.router.vuejs.org/guide/migration/index.html

npm

1
npm install vue-router@next

gcore

1
<script src="https://unpkg.com/vue-router@next"></script>

新特性

简单使用
1
2
3
4
5
6
<template>
<div>About</div>
</template>
<script>
export default {};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

import { createRouter, createWebHashHistory } from 'vue-router'
import Home from './components/Home.vue'
import About from './components/About.vue'

// 创建 Router
const router = createRouter({
history: createWebHashHistory(),
routes: [
{ path: '/about', component: About },
],
})

createApp(App)
// 需要使用router
.use(router)
.mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<ul>
<li>
<router-link to="/">Home</router-link>
</li>
<li>
<router-link to="about">About</router-link>
</li>
</ul>
</div>
<!-- 一定要使用这个、不然没有显示 -->
<router-view/>
</template>

<script>
export default {};
</script>
动态路由

main.js部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建动态路由
router.addRoute({
path:"/about",
name: "about",
component: ()=>import('./components/About.vue')
})

// 创建子路由
router.addRoute('about',{
path: '/about/info',
name: 'info',
component:{
netder() {
return h('div',"i am info")
},
}
})

About.vue

1
2
3
4
5
6
7
8
9
10
<template>
<div>
About
<router-view/>
</div>
</template>

<script>
export default {}
</script>

使用代码路由跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<div>
About
<button @click="toHome">点我跳转</button>
<router-view />
</div>
</template>

<script>
import { watch } from 'vue';
import { useRoute, useRouter } from "vue-router";

export default {
setup() {
// 获取路由实例
const router = useRouter();

// route 是响应式对象 可监控变化
const route = useRoute();

watch(()=> route.query, query => {
console.log(query)
})

return {
toHome(){
router.push("/")
}
}
},
};
</script>
路由守卫

https://next.router.vuejs.org/guide/advanced/navigation-guards.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div>
About

<button @click="toHome">点我跳转</button>
<router-view />
</div>
</template>

<script>
import { watch } from 'vue';
import { onBeforeRouteLeave } from "vue-router";

export default {
setup() {
const router = useRouter();

onBeforeRouteLeave((to,from)=>{
const answer = window.confirm("要离开了吗?")
if(!answer){
return false
}
})

}
};
</script>

https://next.router.vuejs.org/guide/advanced/extending-router-link.html#extending-routerlink

NavLink.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<div :class="{ active: isActive }" @click="navigate">
{{ route.name }}
</div>
</template>

<script>
import { RouterLink, useLink } from "vue-router";
export default {
props: {
...RouterLink.props,
inactiveClass: String,
},
setup(props) {
const { navigate, href,route, isActive, isExactActive } = useLink(props);

return {
navigate,route,isActive
}
},
};
</script>

<style>
.active{
color: blue;
background-color: brown;
}
</style>

DashBoard.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<ul>
<li>
<NavLink to="/">DashBoard</NavLink>
</li>
<li>
<NavLink to="/todo">TodoDemo</NavLink>
</li>
</ul>
</div>
</template>

<script>

import NavLink from './NavLink.vue'

export default {
components:{
NavLink
}
};
</script>

变化

初始化从 RoutercreateRouter

1
2
3
4
5
import { createRouter } from 'vue-router'

const router = createRouter({

})

history模式替换mode

  • "history": createWebHistory()
  • "hash": createWebHashHistory()
  • "abstract": createMemoryHistory() 主要用于SSR

移动 base

1
2
3
4
5
import { createRouter, createWebHistory } from 'vue-router'
createRouter({
history: createWebHistory('/base-directory/'),
routes: [],
})

移除* 通配符

例如自定义404页面

1
2
3
4
5
6
7
8
import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
history: createWebHashHistory(),
routes: [
{ path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound },
],
})

isReady 替换成onReady

1
2
3
4
5
6
7
8
9
10
11
// replace
router.onReady(onSuccess, onError)
// with
router.isReady().then(onSuccess).catch(onError)
// or use await:
try {
await router.isReady()
// onSuccess
} catch (err) {
// onError
}

scrollBehavior 变化

x重名名为left、y重名名为top

1
2
3
4
5
6
7
8
9
10
import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
history: createWebHashHistory(),
//...
scrollBehavior(to, from, savedPosition){
//{x:10,y:10} new {left:10,top:10}
return { top: 0 }
}
})

<router-view>, <keep-alive><transition>

1
2
3
4
5
6
7
<router-view v-slot="{ Component }">
<transition>
<keep-alive>
<component :is="Component" />
</keep-alive>
</transition>
</router-view>

更多内容在 :https://next.router.vuejs.org/guide/migration/index.html 可以了解

Vuex

npm

1
npm i vuex@next

后续更新内容…