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>
|
脚手架使用:
查看vue版本(截至2020-10-16)
1 2
| C:\Users\Administrator>vue -V @vue/cli 4.5.7
|
使用
Vue-cli创建
这里使用Vue-cli创建项目
以下为步骤:
选择默认模板
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的默认模板、但是我还是使用自定义配置
选择需要的组件
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
|
这里根据自己来选择配置
选择版本
1 2 3
| ? Choose a version of Vue.js that you want to start the project with 2.x > 3.x (Preview)
|
当然是Vue3了
使用hash模式还是history模式
1
| Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) y
|
hash会带#
配置放在那?
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
保存配置
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 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 中的 destroyed
和 beforeDestroy
钩子在 vue 3 中被重命名为 unmounted
和 beforeUnmount
setup 有着生命周期钩子不具备的参数:props
和 context
、它只是基于 beforeCreate 运行,但函数内部无法通过 this 获取组件实例
1 2 3 4 5 6 7 8 9 10 11
| setup(props, context) { console.log(props); const { attrs, slots, emit } = context; 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(){ const count = ref(0) count.value = 2 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() { 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(){ const people = reactive({ name: "zykj", age:18 }) const {name,age} = toRefs(people) console.log(name.value,age.value) 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() { 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' }) 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(() => { console.log(root.value) })
return { root } } } </script>
|
setup
中使用computed、watch
同样使用前、需要先引入
1
| import { computed,watch } from 'vue'
|
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 } } }
|
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> <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) => { child.panett = panett
if (!panett.childs) { panett.childs = [child] } else { panett.childs.push(child) }
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(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'
const router = createRouter({ history: createWebHashHistory(), routes: [ { path: '/about', component: About }, ], })
createApp(App) .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(); 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>
|
RouterLink
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>
|
变化
初始化从 Router
到 createRouter
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
| router.onReady(onSuccess, onError)
router.isReady().then(onSuccess).catch(onError)
try { await router.isReady() } catch (err) { }
|
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){ 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
后续更新内容…