前段时间,由新东方出品直播带货品牌
东方甄选
火爆全网,其中最受大家关注的主播董宇辉,用网友的调侃来说“长着一个颗粒无收的脸,却拥有五谷丰登的灵魂”
。他在直播中推荐五常大米时说:“厨房里充满了饭香,就是人间浪漫。”
,介绍水蜜桃:“这个水蜜桃,美好的像穿越大峡谷的风,像仲夏夜的梦”
。卖牛排,告诉观众这是“Original Cutting”
。让网友赞叹不绝,大家都说这买的不是吃的,买的是知识付费
,买的是灵魂洗礼
。最重要的是,他尽然还是一个英语老师。很多人感叹,好好读书太重要了,因为知识能给人带来力量,带来高贵的灵魂。相比那些快节奏、声嘶力竭、充满商业诱导的的直播模式,简直就是降维打击
。
从笔者的角度来看,董宇辉的成功并非偶然,能够饱读诗书,一定源于自己多年不断的思考跟总结,不断的追求学习的本质才能让自己在无意之间沉淀的像个诗人,像个哲学家。这背后的付出,常人肯定无法想象。作家周岭说过
“所谓的学习,不是努力,努力,在努力。而是反馈,反馈,再反馈。光靠一味的输入,而不输出,这种学习大概率是低效率的”。
就像咱们前端技术圈一样,框架层出不穷,版本迭代快的让人无法喘息。很多小伙伴都焦虑的呐喊,学不动了。笔者认为,真正高效的学习一定是需要在输入的同时,要有很好的输出,让自己积累更多的正向反馈,就像我们平时学习某一种技术栈一样,光是一味的学习不行,还要做出高质量的实践跟输出才行!
笔者这篇文章会从vue3基础的知识点开始剖析,特别是在将
composition API
的时候,在代码示例中不会一上来就使用setup语法糖
,而是用早期的setup函数
,这样方便于初学的小伙伴们理解跟学习。文章篇幅较大,接下来,请您花个10分钟耐心的看完,或许会有不一样的收货。
优点
vue3
最大的优势个人认为倒不是它的Api,而是配合使用的vite
打包工具,特别是大型项目本地启动要比当前的webpack5
要快至少2倍
以上(项目中测试过)vue 2.x
,Composition Api
的优势要明显的多,如果习惯了setup语法糖
的写法,你会发现爽的飞起,很多之前在vue 2.x
中大量重复逻辑不存在了Proxy
来实现双向绑定,性能上提升了很多TypeScript
支持度更好,可以很愉快的在项目中使用TypeScript
缺点
IE情节
的公司,那vue3
确实不太适合,因为vue3
已经抛弃了对IE11
的支持,再说了 微软人家自己都不打算维护IE
了,兄弟们,放弃IE
拥抱chrome
吧!Composition Api
的写法需要花一点点时间来适应,毕竟学习新语法还是需要成本的<script src="https://unpkg.com/vue@next"></script>
# 最新稳定版 npm install vue@next npm install -D @vue/compiler-sfc
如果你是从Vue 2.x
升级的,请注意 @vue/compiler-sfc
替换掉了 vue-template-compiler
npm install -g @vue/cli vue upgrade --next
npm init vite@latest <project-name> -- --template vue cd <project-name> npm install npm run dev
推荐使用第4种方式,直接使用官方推荐最新的vite打包工具,直接初始化项目。
setup
是vue3
提出的一个非常重要的选项,也是Composition Api
最为核心的语法之一。setup
执行时机是在beforeCreate
之前执行的。setup
返回的是一个对象,对象中的所有属性都是可以在template
中使用setup
中不能使用this
setup
中注册生命周期onMounted
、watch
、computed
等,我们会在下边详细讲解setup参数
<script> export default { setup (props, context) { return {} } } </script>
既然上边提到了setup
语法,那就有必要把setup
语法糖介绍一下,我们在实际的项目开发中在熟悉了setup语法的本质后,也推荐大家使用setup
语法糖来编写,这样也可以大大提升开发效率。
return
,只需要在<script setup>
中声明一下即可<script setup>
声明的顶层的绑定 (包括声明的变量,函数声明,以及 import
引入的内容) 都可以在模板中直接使用components
进行注册<script setup> import {ref} from 'vue' let property = ref('这里是响应式属性'); // 这里我们引入了子组件SetUp.vue import SetUp from '@/components/SetUp.vue' </script>
ref
跟reactive
都是vue3
中用来做数据定义使用的,如同vue2
中在data
中做数据定义一样,示例代码如下:<template> <h3>{{ state.count }}</h3> <h3>{{ num }}</h3> <el-button @click="handleAdd" type="primary">ref计算</el-button> </template> <script> import { ref, reactive } from 'vue' export default { setup() { const num = ref(0) const state = reactive({ count: 1 }) function handleAdd() { state.count++; num.value += 2; } return { state, num, handleAdd } } } </script>
ref
跟reactive
的区别在哪呢?很多人分不清楚,网上有很多文章简单的定义为ref
负责处理基本数据类型的双向绑定,reactive
负责处理对象的双向绑定。其实,这样笔者会觉得给很多初学者带来很多误导,其实ref
也可以处理对象的双向绑定,就像下边这段代码一样。<template> <el-button @click="handleAdd" type="primary">ref计算</el-button> <h3>{{ obj.count }}</h3> </template> <script> export default { setup() { // ref 对象双向绑定 const obj = ref({ count: 1 }) function handleAdd() { obj.value.count = obj.value.count + 1 } return { obj, handleAdd } } } </script>
watchEffect
flush: post
,否则依赖在监听时无法被立即更新stop
来立即停止对函数的监听<template> <div ref="root">This is a root element</div> </template> <script> import {ref, watchEffect} from 'vue' export default { setup() { const root = ref(null) watchEffect(() => { console.log(`watchEffect监听:${root.value}`); }, { flush: 'post' }) return { root } }, } </script>
watch
watch
API 与选项式 APIthis.$watch
(以及相应的watch
选项) 完全等效。watch
需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在侦听源发生变化时被调用。
与 watchEffect
相比,watch
:
getter
函数ref
对象、也可以reactive
对象deep: true
,否则回调函数无法被触发<template> <h3>监听单个数据源1:{{state1.count}}</h3> <button @click="handleWatchSingle1">watch监听测试1</button> <h3>监听单个数据源2:{{state2}}</h3> <button @click="handleWatchSingle2">watch监听测试2</button> <h3>监听复杂对象数据源:{{state3.player}}</h3> <button @click="handleWatchSingle3">watch监听测试3</button> </template> <script> import {ref, reactive, watch} from 'vue' export default { setup() { const state1 = reactive({ count: 1 }) const state2 = ref(0) const state3 = reactive({ player: { name: 'James', achievement: ['4次NBA常规赛mvp', '03年选秀状元', '4次NBA总冠军'] } }) watch(() => state1.count, (newVal, oldVal) => { console.log('watch监听reactive中的newVal:', newVal); console.log('watch监听reactive中的oldVal:', oldVal); }) watch(() => state2.value, (newVal, oldVal) => { console.log('watch监听ref中的newVal:', newVal); console.log('watch监听ref中的oldVal:', oldVal); }) watch(() => state3.player, (newVal, oldVal) => { console.log('watch监听复杂对象中的newVal:', newVal); console.log('watch监听复杂对象中的oldVal:', oldVal); }, { deep: true, // immediate: true }) // 同时监听多个值 // watch([() => state1.count, state2.value], ([newVal1, newVal2], [oldVal1, oldVal2]) => { // console.log('watch监听中的newVal:', newVal1, newVal2); // console.log('watch监听oldVal:', oldVal1, oldVal2); // }) function handleWatchSingle1() { state1.count++ } function handleWatchSingle2() { state2.value++ } function handleWatchSingle3() { state3.player = { name: 'Wade', achievement: ['3次NBA总冠军', '曾经的热火三巨头之一', '1次NBA总决赛mvp'] } } return { state1, state2, state3, handleWatchSingle1, handleWatchSingle2, handleWatchSingle3 } }, } </script>
getter
函数,并根据getter
的返回值返回一个不可变的响应式 ref
对象。get
和 set
函数的对象,用来创建可写的 ref
对象<template> <div style="margin-top:30"> <h3>computedNum值为:{{computedNum}}</h3> <h3>computedNum2值为:{{computedNum}}</h3> <button @click="handleComputed">computed计算测试</button> </div> </template> <script> import { ref, computed } from 'vue' export default { setup() { const state = ref(1) const computedNum = computed(() => { return state.value + 1 }) console.log('computed缓存后的值:', computedNum.value); // 只可读属性,不可写,会抛出警告 Write operation failed: computed value is readonly function handleComputed() { computedNum.value++ } const computedNum2 = computed({ get: () => state.value + 2, set: val => { count.value = val - 0 } }) return { computedNum, computedNum2, handleComputed } }, } </script>
组件通信这块跟vue2的区别不大,我们就拿常用的props跟emit来讲解一下。
emits
选项来定义组件可触发的事件父组件
<template> <Children :msg1="msg1" :msg2="msg2" @childClick="handleClick" /> </template> <script> import {ref, reactive} from 'vue'; import Children from './children.vue' export default { setup() { const msg1 = ref('给子组件传递的消息1') const msg2 = reactive({ name: '给子组件传递的消息2' }) return { msg1, msg2 } }, methods: { handleClick(val) { console.log('接收子组件emit过来的数据:', val); } }, components: { Children } } </script>
子组件
<template> <div style="margin-top: 30px">props传递给子组件的消息:{{ msg1 }}</div> <button @click="$emit('childClick', 6666)" style="margin-top: 30px">向父组件emits事件</button> </template> <script> export default { props: ['msg1', 'msg2'], emits: ['childClick'], setup(props) { console.log('子组件接收父级组件传递过来的消息:', props); }, } </script>
子组件
<template> <slot name="title"></slot> </template>
父组件
<template slot="title"> <h2>周岭:《认知觉醒》</h2> <template>
vue3
插槽中提供了v-slot:name
写法,我们就拿作用域插槽来举例
子组件
我们定一个可循环的插槽content
<template> <!-- <slot name="title"></slot> --> <div v-for="(item, index ) in items" :key="index"> <slot :item="item" name="content"></slot> </div> </template> <script setup> import {ref} from 'vue'; const items = ref(['认知觉醒', '认知驱动']); </script>
父组件
父组件中可以有两种方式来引入子组件中的插槽,其一是通过v-slot:content="scopend"
的方式,其二是通过简写#content="{item}"
的方式
<template> <SlotChild> <!-- <template v-slot:content="scoped"> <div>{{ scoped.item }}</div> </template> --> <template #content="{item}"> <div>{{ item }}</div> </template> </SlotChild> </template> <script setup> import SlotChild from './SlotChild.vue' </script>
vue3的声明周期如果是使用选项性Api的话,原来的生命周期钩子可以照常使用,那如果选用vue3组合式Api的话,生命周期需要通过import引入的方式在setup中调用。下图是vue3跟vu2声明周期的区别
<template> <div id="test"> <h3>{{ counter }}</h3> <button @click="handleClick">声明周期测试</button> </div> </template> <script> import { ref, onMounted, onBeforeMount, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue' export default { setup() { const counter = ref(0); console.log('....'); function handleClick() { counter.value += 1; } onBeforeMount(() => { console.log("组件挂载之前"); }); onMounted(() => { console.log("DOM挂载完成"); }); onBeforeUpdate(() => { console.log("DOM更新之前", document.getElementById("test").innerHTML); }); onUpdated(() => { console.log("DOM更新完成", document.getElementById("test").innerHTML); }); onBeforeUnmount(() => { console.log("实例卸载之前"); }); onUnmounted(() => { console.log("实例卸载之后"); }); return { counter, handleClick } }, } </script>
vue-router 3.x跟vue-router 4.x比起来写法上的区别
vue-router 3.x
// router/index.js import Vue from 'vue' import Router from 'vue-router' import routes from './routes' Vue.use(Router) const router = new Router({ routes }) export default router // main.js import Vue from 'vue' import router from './router' // ... new Vue({ el: '#app', router, components: { App }, template: '<App/>' })
vue-router 4.x
// router/index.js import { createRouter } from 'vue-router' import routes from './routes' const router = createRouter({ history: createWebHistory(), // history模式 routes }) // main.js import { createApp } from 'vue' import router from './router' const app = createApp(App) app.use(router) app.mount('#app')
new Router()
改成createRouter()
mode: 'history'
改成 history: createWebHistory()
useRouter、useRoute
通过useRouter
进行路由跳转
<template> <div class="mg30"> <el-button @click="handleJump" type="primary">关于我们</el-button> </div> </template> <script setup> import { useRouter } from 'vue-router' const router = useRouter() const handleJump = (query) => { router.push({ name: "about", query: { id: 1 } }) } </script>
通过useRoute
来获取传递过来的id
<template> <div>关于我们</div> </template> <script setup> import { useRoute } from 'vue-router' const route = useRoute() console.log('id>>>', route.query.id); </script>
全局守卫
/router/index.js
详情页面meta中
添加登录标识needLogin
let routes = [ { path: '/detail', name: 'detail', component: () => import('@/views/detail.vue'), meta: { needLogin: true } } ]
main.js
添加守卫
import router from './router' // 全局路由守卫 router.beforeEach((to, from) => { if (to.meta.needLogin) { return { name: 'login' } } })
路由独享守卫
/router/index.js
let routes = [ { path: '/category/:id', name: 'category', component: () => import('@/views/category.vue'), beforeEnter: (to, from) => { // 如果不是正确的分类,跳转到NotFound的页面 console.log('id>>>>', to.params.id); if (!["0", "1", "2"].includes(to.params.id)) { return { name: "NotFound", // 这个是在地址栏保留输入的信息,否则地址栏会非常的丑 params: { pathMatch: to.path.split("/").slice(1) }, query: to.query, hash: to.hash, }; } } } ]
组件内部守卫
<template> <div>关于我们</div> </template> <script setup> import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router' // 页面内部的路由守卫 onBeforeRouteLeave((to, from) => { const answer = window.confirm('是否确认离开') if (answer) { console.log('不离开'); return false } }) // 对于一个带有动态参数的路径 /category/:catId,在 /category/1 和 /category/2 之间跳转的时候, 会触发onBeforeRouteUpdate的路由钩子函数,在钩子函数中可以进行数据的更新。 onBeforeRouteUpdate((to, from) => { console.log('to>>>', to); console.log('from>>>', from); // if (to.params.id !== from.params.id) { // userData.value = await fetchUser(to.params.id) // } }) </script>
// vue-router 3 <keep-alive> <router-view /> </keep-alive> // vue-router 4 <router-view v-slot="{component}"> <keep-alive> <component :is="component" /> </keep-alive> </router-view>
跟vue2不同的是,vue3中提供了提供了很多不同的选择器方便我们在样式编写上更加的灵活多变。
类似于sass
语法中的v::deep
,不过vue3
中的样式自带深度作用域
<style scoped> .parent :deep(div) { margin-bottom: 10px; } </style> <template> <div class="parent"> <div class="set-up">:deep 深度作用域测试</div> </div> </template>
不用像vue2
一样写全局作用域时,需要单独开启一个style
标签,同时去掉scoped
属性;vue3
提供了一种便捷的写法,只需要使用global
属性传递你想全局修改的样式即可。
<template> <div>全局选择器测试</div> <p :class="$style.green">module样式测试</p> </template> <style scoped> :global(div) { color: red; } </style>
<style module>
标签会被编译为 CSS Modules
并且将生成的 CSS
类作为 $style
对象的键暴露给组件。
<template> <p :class="$style.green">module样式测试</p> </template> <style module> .green { color: green; } </style>
<template> <p :class="classes.blue">useCssModule样式测试</p> </template> <style module="classes"> .blue { color: blue; } </style>
<script> import { h, useCssModule } from 'vue' export default { setup() { const style = useCssModule() return () => h( 'div', { class: style.success }, 'Task complete!' ) } } </script> <style module> .success { color: #090; } </style>
对于TS,笔者认为小项目中也不必集成TS,反倒会提升项目的编译成本。那如果是大型项目的话,有必要尝试接入TS,一方面可以减少不必要的类型判断及文档注释,同时可以及早的发现错误,做静态类型检查时就可以及时的发现问题。另一方面,类、接口的使用更易于构建和维护组件;那么,对于初学者我们有必要对TS的一些基本用法做一下普及。
/** * @description: 基本的数据类型 * @return {*} boolean(布尔值)number(数值) Array<number> (泛型数组)Object (对象)null undefined */ let isDone: boolean = false; console.log('isDon', isDone); let num: number = 1; console.log('num', num); let str: string = '认知觉醒'; console.log('str', str); let arr: number[] = [1, 2, 3]; console.log('arr', arr); // 泛型数组 let arr2: Array<number> = [1, 2, 3] console.log('arr2', arr2); let obj: Object = { id: 1 } console.log('obj', obj); let u: undefined = undefined console.log('u', u); let n: null = null; console.log('n', n);
// 数字类型枚举与数字类型 enum CardSuit { Clubs, Diamonds, Hearts, Spades } console.log('CardSuit', CardSuit.Clubs); // 0 let col = CardSuit.Clubs; col = 0 // 安全有效的 console.log('col', col); // 0 // 数字类型枚举与字符串类型 enum Tristate { False, True, Unkonw } console.log('字符串', Tristate[0]); // 'False' console.log('number', Tristate['False']); // 0 console.log('字符串', Tristate[Tristate.False]); // 'False' // 字符串枚举 enum LogLevel { info = 'info', warn = 'warn', error = 'error' } console.log('LogLevel', LogLevel.info); // 'info'
/** * @description: 元祖 * @return {*} 允许数组各元素的类型不必相同 */ let x: [string, number, boolean]; x = ['hello', 10, true]; console.log('正确元祖', x); // ['hello', 10, true] // y = [10, 'hello', false] // console.log('错误的元祖', y);
/** * @description: 任意值 Any * @return {*} 表示任意类型, 通常用于不确定内容的类型,比如用户的输入或者是第三方库代码;实际项目中,此类型建议少用 */ let notSure: any = 4; notSure = 'maybe a string instead'; console.log('notSure', notSure); // 'maybe a string instead' notSure = true; console.log('notSure', notSure); // true
/** * @description: 空值 void * @return {*} 与any相反,通常用于函数,表示没有返回值 */ const voidFunc = (): void => { console.log('这个函数没有返回任何值'); // return msg; // 不能return } voidFunc()
/** * @description: 接口 interface * @return {*} 类型契约,跟我们平时与服务端接口要先定义字段是一个道理 */ interface Point { x: number y: number z?: number readonly l: number } const point: Point = { x: 10, y: 20, z: 30, l: 40 } console.log('point', point); const point2: Point = { x: '10', y: 20, z: 30 } // Error x应该是Number类型 const point3: Point = { x: 10, y: 20, z: 30 } // Error l字段也是必传 const point4: Point = { x: 10, y: 20, z: 30, l: 40, m: 50 } // Error m字段没有定义 const point5: Point = { x: 10, y: 20, l: 40 } // 正常 point5.l = 20; // Error l字段是只读类型,不能修改
/** * @description: 函数参数类型与返回值类型 * @return {*} */ function sum(a: number, b: number): number { return a + b; } console.log('sum', sum(2, 3)); // 5 // 配合interface使用 interface Point { x: number y: number } function sum2({x, y}: Point): number { return x + y; } console.log('sum2', sum2({x: 1, y: 2})); // 3
/** * @description: 泛型 * @return {*} 泛型的意义在于函数的重用性,设计原则希望组件不仅能够支持当前的数据类型,同时也支持未来的数据类型 * 语法:<T>(arg: T): T */ // 比如我们最初设计函数identity 入参为String function identity(arg: String) { return arg; } console.log(identity('hello')); // hello // 后来随着业务的迭代我们又需要支持 Number function identity2(arg: String) { return arg; } console.log(identity(2)); // Argument of type 'number' is not assignable to parameter of type 'String' // 那我们为什么不用any呢?使用any会导致丢失掉一些信息,我们无法确定要返回值到底是属于什么数据类型 const hello1: String = 'Hello vue3'; const hello2: Number = 666; function say<T>(arg: T): T { return arg; } console.log('泛型1:', say(hello1)); // Hello vue3 console.log('泛型2:', say(hello2)); // 666 // 泛型约束 // 我们使用同样的例子,加了一个console,但是很不幸运,报错了,因为泛型无法保证每种类型都有.length 属性 const hello3: String = 'Hello vue3'; function say2<T>(arg: T): T { console.log(arg.length); // Property 'length' does not exist on type 'T' return arg; } console.log('泛型3:', say2(hello3)); // Hello vue3 interface Lengthwise { length: number } function say3<T extends Lengthwise>(arg: T): T { console.log(arg.length); return arg; } console.log(say3(1)); // Argument of type 'number' is not assignable to parameter of type 'Lengthwise'. console.log(say3({ value: 'hello vue', length: 10 })); // '{ value: 'hello vue', length: 10 }'
interface foo { x: number } interface bar { b: string } type intersection = foo & bar const result: intersection = { x: 10, b: 'hello' } console.log('result', result);
/** * @description: 联合类型 * @return {*} 表示一个值可以为几种数据类型之一 */ type arg = string | number | boolean const foo = (arg: arg): any => { console.log('arg', arg); } foo(1) foo('1') foo(true)
/** * @description: 函数重载 * @return {*} 1个函数可以执行多项任务的能力 */ // add函数,它可以接收string类型的参数进行拼接,也可以接收number类型的参数进行相加 function add <T, U>(arg1: T, arg2: U) { // 在实现上我们要注意严格判断两个参数的类型是否相等,而不能简单的写一个 arg1 + arg2 if (typeof arg1 === 'string' && typeof arg2 === 'string') { return arg1 + arg2 } else if (typeof arg1 === 'number' && typeof arg2 === 'number') { return arg1 + arg2 } } console.log('number类型相加', add(1, 2)); console.log('string类型拼接', add('1', '2'));
安装TS
npm i typescript
项目根目录新建tsconfig.json
文件,用于TS的编译基础文件
{ "compilerOptions": { "target": "esnext", "module": "esnext", "sourceMap": true, "outDir": "./dist", "rootDir": "./src", "strict": true, "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] }
script标签中声明langg="ts",然后就可以愉快的使用TS的项目语法了,下边这段代码只是一些简单的示例。
<template> <div> <h2>标题:{{book.title}}</h2> <h2>作者:{{book.author}}</h2> <h2>出版日期:{{book.year}}</h2> <hr> <h3>{{allTitle}}</h3> <el-button @click="setTitle('我是传入的数据')" type="primary">设置数据</el-button> </div> </template> <script lang="ts"> import { defineComponent, ref, toRefs, reactive } from 'vue'; // 定义Book接口 interface Book { title: String author: String year?: Number, handleChangeName?(): void } export default defineComponent ({ data() { let book: Book = { title: 'vue3 typescript', author: "vue Team", year: 2020, } return { book } }, setup() { let year1 = ref<String | Number>('2022') console.log('year1', year1.value); // 第一种方式 // const book1: Book = reactive({ // name: year1.value, // desc: "vue3 进阶学习加油", // setNamechange(){ // this.name = "我是新设置的" // } // }); // // 第二种方式 // const book2 = reactive<Book>({ // name: "vue3--typeScript", // desc: "学习ts加油", // year: 2020, // }); // // 第三种方式 // const book3 = reactive({ // name: "vue3--typeScript-第三种方式", // desc: "ts类型第三种方式", // year: 2022, // }) as Book; return { // ...toRefs(book1), // book2, // book3, // year1, }; }, computed: { // 返回值类型为String allTitle(): String { return `欢迎语 : ${this.book.title}` } }, methods: { // 入参为String 返回空值 setTitle(arg: String): void { this.book.title = arg; this.book.year = 2022 this.book.author = '尤雨溪' } } }) </script>
由于本文章篇幅较大,会在之后的文章中单独来讲解。