基础 app.use()
注册插件,有点像 Express 中的 use;所谓的插件,即具备某些功能的一段代码,这段代码用于添加全局功能;
插件可以是一个对象,也可以是一个函数;
如果是一个对象,需要有一个 install 方法,以便调用;该 install 函数的第一个参数是 app,第二个参数是 options
1 2 3 4 5 6 7 8 9 import { createApp } from 'vue' const app = createApp ({}) app.use (myPlugin, { greetings : { hello : "Bonjour!" } });
1 2 3 4 5 6 7 8 9 10 11 12 13 export default { install : (app, options ) => { app.config .globalProperties .$translate = (key ) => { return key.split ('.' ).reduce ((o, i ) => { if (o) return o[i] }, options) } } }
1 <h1 > {{ $translate('greetings.hello') }}</h1 >
插件的几种使用场景:
添加一些全局属性和方法;
添加一个全局资源;
添加一个全局组件
添加自定义指令;
app.config.isCustomElement
有些元素是从外部引入的,并没有在 vue 中编写,此时需要备注一下哪些元素是自定义的,以免在编译时报错找不到;
1 app.config .isCustomElement = tag => /^x-/ .test (tag);
app.mount
将 app 关联到 HTML 文件中的 Tag
1 2 3 4 5 6 7 8 9 10 11 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="utf-8" /> <title > TodoMVC built with Vue Composition Api and Vuex</title > </head > <body > <app-root > </app-root > <script type ="module" src ="./main.js" > </script > </body > </html >
reactive reactive 可用来创建一个对象,这个对象可以被多个组件引入,共享使用;
对象可以有自己的方法,通过调用该方法,改变对象的状态;这个改变会在所有的组件上同时更新;
1 2 3 4 5 6 7 8 import { reactive } from "vue" ;export const store = reactive ({ count : 0 , increment ( ) { this .count ++ } });
1 2 3 4 5 <template > <button @click ="store.increment()" > {{ store.count }} </button > </template >
除了用 reactive 来创建全局对象外,其实 ref 或者函数也可以实现该功能;
函数之所以可以,主要是利用了闭包的特性;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { ref } from "vue" const globalCount = ref(1 ); export function useCount () { const localCount = ref(1 ); return { globalCount, localCount, } }
问:reactive 和 ref 有什么区别?
答:有以下一些区别:
reactive 只能处理对象,不能处理原始类型;ref 的底层实现其实最终也有调用 reactive;
ref 可以通过 .value 重新赋值,reactive 不行,因此 reactive 在处理新的 array 时,不如 ref 重新赋值方便;
不过 reactive 修改对象的属性时,无须使用 .value,写起来会简单一些;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const person = reactive ({ name : "John" , age : 37 , isTall : true , }); const name = ref ("Albert" );const age = ref (37 );const isTall = ref (true );const person = ref ({ name : "John" , age : 37 , isTall : true , });
computed() 其实也是返回一个 ref
computed 当值 B 依赖于值 A 时,通过 computed 可以实现当 A 变动时,B 实现实时更新;
computed 接收一个函数做为参数,返回的是一个 ref
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <script setup > import { reacitve, computed } from "vue" const author = reactive ({ name : "john" , books : [ "vue1" , "vue2" , "vue3" , ] }); const message = computed (() => { return author.books .length > 0 ? "yes" : "no" ; }); </script > <template > <p > Has Published books:</p > <span > {{ message }}</span > </template >
computed 的好处是有缓存,也就是说,如果所依赖的值没变的话,它是不会重新计算的;
实际上 message 也可以定义成一个函数,结果一样,示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <script setup > const author = reactive ({ name : "john" , books : [ "vue1" , "vue2" , "vue3" , ] }); function message ( ) { return author.books .length > 0 ? "yes" : "no" ; } </script > <template > <p > Has Published books:</p > <span > {{ message() }}</span > </template >
状态管理器 vue2 的状态管理器,在 vue3 中使用 Pinia
类与样式绑定 有多种写法可用来绑定样式
1 2 3 4 5 6 // 方式一: 使用单个 ref<script setup > const isAcitve = ref (true );</script > <div :class ="{ acitve: isActive }" > </div >
1 2 3 4 5 6 7 8 9 10 11 12 // 方式二:使用多个 ref<script setup > const isAcitve = ref (true );const hasError = ref (false );</script > <template > <div class ="static" :class ="{ active: isActive, 'text-danger': hasError }" > </div > </template >
1 2 3 4 5 6 7 8 9 // 方式三:使用对象<script setup > const classObject = { active : true , 'text-danger' : false , } </script > <div :class ="classObject" > </div >
1 2 3 4 5 6 7 8 9 10 11 12 // 方法四:使用数组<script setup > const activeClass = ref ('active' );const errorClass = ref ('text-danger' ); const isActive = ref (true );</script > <div :class ="[isActive ? activeClass : '', errorClass]" > </div > // 或者<div :class ="[{[activeClass]: isActive }, errorClass]" > </div >
自定义组件上的 class 值,会传递到组件内部的 Tag 上面,示例如下:
1 2 // 组件 myComponent 内部的内容<p class ="foo bar" > </p >
1 2 // 在调用 myComponent 组件时<myComponent class ="baz boo" > </myComponent >
1 2 // 渲染结果为<p class ="foo bar baz boo" > </p >
如果 myComponent 内部有多上根Tag,那么需要指定哪个根 Tag 接收外部传进来的 class 值,示例如下
1 2 3 // 内部有两个根元素 p 和 span,此处指定 p 接收 myComponent 的 class 值<p :class ="$attrs.class" > hi</p > <span > message</span >
适用于 class 的绑定规则,同样也适用于 style 的绑定,示例如下:
1 2 3 4 const styleObject = reactive ({ color : 'red' , fontSize : '30px' })
1 <div :style ="styleObject" > </div >
事件修饰符 当我们想阻止某个事件的冒泡时,可以在绑定的方法中调用 event.stopPropagation(),但 vue 还提供了一种更简便的方法,示例如下:
1 2 3 4 5 6 7 8 function warn (message, event ) { if (event) { event.preventDefault () } alert (message) }
以下是使用事件修饰符进行绑定的方式:
1 2 3 4 5 6 7 8 <script setup > function warn (message, event ) { alert (message) } </script > <a @click.stop ="warn" > </a >
按键修饰符 按键修饰符可用于监听键盘上某个特定的键被按下的事件
1 2 3 4 5 6 <input @keyup.enter ="submit" /> <input @keyup.page-down ="onPageDown" />
鼠标修饰符 用来监听鼠标事件
.left 左键
.right 右键
.middle 中键
表单输入绑定 在处理表单输入时,是需要双向绑定的,即改动 data,会更新 html;而改动 input 时,也会更新 data
vue 使用 v-model 关键字来实现这种双向绑定
多个复选框可以绑定到一个数组或集合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <script setup > const checkedNames = ref ([]);</script > <template > <div > checked names: {{ checkedNames }}</div > <input type ="checkbot" id ="jack" value ="jack" v-model ="checkedNames" > <label for ="jack" > </label > <input type ="checkbot" id ="john" value ="john" v-model ="checkedNames" > <label for ="john" > </label > <input type ="checkbot" id ="mike" value ="mike" v-model ="checkedNames" > <label for ="mike" > </label > </template >
v-bind v-bind 可用于标签的属性绑定
1 2 3 4 5 6 7 8 9 10 11 <div v-bind ="{ id: 'blue'}" > </div > <div id ="blue" > </div > <script setup > const id = ref ("abc" ); </script > <div :id ="id" > </div >
v-model 修饰符 .lazy 默认情况下,v-model 的更新是实时的,但可使用 .lazy 修饰符,让更新不再实时,而是触发 change 事件后再更新
1 <input v-model.lazy ="msg" />
.number 将输入的字符串自动转成数字
1 <input v-model.number ="age" />
.trim 自动去除字符串首尾的空格
1 <input v-mdoel.trim ="msg" />
生命周期 最常用的几个生命周期
onMounted
onUpdated
onUnmounted
watch 侦听器 当某个对象的值出现变化时,就执行回调函数;监听的对象可以是如下几种类型
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 const x = ref (0 );const y = ref (0 );watch (x, (new_x ) => { });watch ( () => x.value + y.value , (sum ) => { console .log ("sum of x and y is: " , sum); } );watch ( [x, () => y.value ], ([new_x, new_y] ) => { console .log (`new x is ${new_x} and new y is ${new_y} ` ); } );
watch 并非马上执行,而是当监听对象的值出现变化时,才会执行。因此,如果想让它立即执行,那么需要加个 { immediate: true } 参数;
默认情况下,如果在回调函数中访问监听对象,此时监听对象的值,是原始状态;如果未被回调函数改变前的状态;如果需要访问改变后的状态,则需要给 watch 传递 { flush: “post” } 选项;
watchEffect watchEffect 有点像是 watch 的语法糖,在使用 watch 时,需要显示的指定某个监听对象;watchEffect 则不用,它可以自动从回调函数中判断需要监听的对象;而且是加载后,马上执行
1 2 3 4 5 6 7 const todoId = ref (1 );const data = ref (null );watchEffect (async () => { const res = await fetch (`https://example.com/${todoId.value} ` ) data.value = await res.json (); })
访问 DOM 如果想直接访问 DOM,则可以给标签的 ref 属性设置名称,之后就可以在代码中引用它,示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 <script setup > import { ref, onMounted } from "vue" ;const myInput = ref (null ); onMounted (() => { myInput.value .focus (); }) </script > <template > <input ref ="myInput" > </template >
当 ref 被用在子组件上时,此时引用的不再是标签,而是子组件实例
1 2 3 4 5 6 7 8 9 10 <script setup > import { ref, onMounted } from "vue" ; import Child from "./Child.vue" ; const child = ref (null ); </script > <template > <Child ref ="child" > </Child > </template >
默认情况下,子组件内部的属性和方法是私有的,父组件无法直接访问,除非子组件使用 defineExpose 进行暴露;
1 2 3 4 5 6 7 8 9 10 11 <script setup > import { ref } from "vue" ; const a = 1 ; const b = ref (2 ); defineExpose ({ a, b, }) </script >
此时父组件可通过 ref 引用来访问子组件中的 a 和 b 变量
组件API 以下两种形式的 API 是等价的
1 2 3 4 5 6 7 8 9 <script setup > import { ref } from 'vue' ; const count = ref (0 ); </script > <template > <button @click ="count++" > </button > </template >
1 2 3 4 5 6 7 8 9 10 11 12 <!-- 选项式 API -->import { ref } from 'vue' ;export default { setup : () => { const count = ref (0 ); return { count }; }, template : `<button @click="count++"></button>` }
父组件可通过 props 向子组件传递数据;子组件可 emit 事件,父组件通过监听事件来获得子组件传递的数据;
slot 插槽 slot 的作用类似于占位符,可接收由父组件传进来的 HTML 内容,示例如下:
1 2 3 4 5 6 7 <template > <div > <strong > This is an Error box</strong > <slot > </slot > </div > </template >
1 2 <AlertBox > Something bad happen</AlertBox >
深入组件 注册 全局注册 组件需要注册后才能使用,通过 app.component 方法,可将某个组件注册为全局的组件,之后可以在任意文件中使用该全局组件;
1 2 3 4 5 6 import { createApp } from 'vue' ;import MyComponent from "./App.vue" ;const app = createApp ({}); app.component ('MyComponent' , MyComponent );
局部注册 局部注册:仅在需要使用的位置,导入相应的组件
1 2 3 4 5 6 7 <script setup > import ComponentA from './ComponentA.vue' </script > <template > <ComponentA /> </template >
props 除了 attribute 外,考虑父组件还可通过 props 传递数据给子组件。因此,最好显式的声明 props,这样有利于 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 defineProps ({ title : String , likes : Number })defineProps ({ propA : { type : String , required : true , default : "hello" }, propB : { validator (value, props ) { return ['success' , 'warning' , 'danger' ].includes (value); } }, propC : { type : Function , default ( ) { return 'Default Function' } } })
Vue 倾向在写 HTML atribute 时,使用传统的 kecal-case 枨,然后它还会自动映射 kebab-case 和 camelCase 格式,以便和传统的 javascript camelCase 保持一致;
个人感觉这种两边讨好的做法不是很好;缺少一致性,容易让人感到困惑;
1 <MyComponent greeting-message ="hello" > </MyComponent >
当使用 v-bind 时,引号中的内容,实际上是一个表达式,而不是字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <BlogPost :likes ="42" /> <BlogPost :likes ="post.likes" /> <BlogPost :is-published ="false" /> <BlogPost :commend-ids ="[234, 245, 273]" /> <BlogPost :author ="{ name: 'John', age: 47 }" />
可通过 v-bind=对象,批量绑定多个 prop
1 2 3 4 5 6 7 8 9 <script setup > const post = { id : 1 , title : "My Journey" } </script > <BlogPost v-bind ="post" />
注意:prop 是单向绑定,即数据由父组件传递给子组件,这意味着它是只读的,我们不能在子组件的代码中,修改 prop 的值
1 2 3 4 const props = defineProps (['foo' ]); props.foo = "bar" ;
由于在 Javascript 中,对象类型的参数,实际上是一个引用,因此,虽然无法直接更改对象绑定的变量,但可以改变对象内部的属性。但这是一种不良做法,应该在实践中避免;如有需要修改,应使用 emit 事件的方式;由监听事件的父组件对 prop 进行修改;
事件 在组件的 template 模板中,可使用内置的 $emit 函数来触发事件
1 <button @click ="$emit('someEvent')" />
事件支持携带参数
1 <button @click ="$emit('someEvent', param)" />
通过使用 defineEmit() 宏显式的声明可触发的事件后,会返回一个 emit 函数,能够在代码中直接调用,它的效果跟 template 中的 $emit 是一样的;
1 2 3 4 5 6 7 <script setup > const emit = defineEmits (['inFocus' , 'submit' ]); function buttonClick ( ) { emit ("submit" ); } </script >
组件 v-model 通过在子组件上使用 v-model,可以实现父子组件之间数据的双向绑定;父子组件传统的通信方式是使用 prop 和 emit,事实上在组件上使用 v-model 只是一个语法糖,它的底层仍然还是 prop 和 emit,只是它由解释器完成补全;
1 2 3 4 5 6 <script setup > const myRef = ref (); </script > <Child v-model ="myRef" />
1 2 3 4 5 6 7 8 <script setup > const myRefVar = defineModel (); </script > <template > <input v-model ='myRefVar' /> </template >
可以绑定多个 v-model
1 2 3 4 5 <UserName v-model:firstName ="first" v-model:lastName ="last" />
1 2 3 4 5 6 7 8 9 10 11 <script setup > const firstName = defineModel ("firstName" );const lastName = defineModel ("lastName" );</script > <template > <input type ="text" v-model ="firstName" /> <input type ="text" v-model ="lastName" /> </template >
组件 v-model 同样支持修饰符,例如 v-model.capitalize,之后在 defineModel 中,可以基于传入的修饰符的值,自定义 set 函数,实现想要的处理;
1 2 <MyComponent v-model.capitalize ='myText' />
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <script setup > const [model, modifiers] = defineModel (); console .log ("modifiers" ) </script > <template > <input type ='text' v-model ="model" /> </template > <script setup > const [model, modifiers] = defineModel ({ set (value ) { if (modifiers.capitalize ) { return value.charAt (0 ).toUppercase () + value.slice (1 ) } return value; } }) </script >
透传 Attributes 最常见的透传包括 class, style, id 等几个 HTML 标签的属性;但其实 v-on 监听器也会实现透传
1 2 <MyButton @click ="onClick" > </MyButton >
1 2 <button @click ="onChildClick" />
当 button 触发点击事件时,onChildClick 和 onClick 两个函数都会被执行,事实上 button 标签绑定了来自父子组件的两个点击事件;
深层组件继承 如果子组件的根元素也是一个组件,那么父组件的 attributes 会持续向下一级透传;
如果不想要继承透传,可在组件选项中设置 inheritAttrs: false
1 2 3 4 5 <script setup > defineOptions ({ inheritAttrs : false , }) </script >
透传的 attributes 可在 template 中使用 $attris 进行访问
1 <span > {{ $attrs }}</span >
@click 在透传后,子组件可使用 $attrus.onClick 进行访问;
如果子组件有多个根节点,那么需要显式指定由哪个根节点继承父组件透传的 attris,否则编译器会抛出警告;
如果想要在 js 代码中访问 attrus,则可以使用 useAttrs
1 2 3 4 5 <script setup > import { useAttrs } from 'vue' ; const attrs = useAttrs (); </script >
插槽 父组件可通过插横向子组件传递内容;插槽从某种意义上来说,有点像是一个形式参数。子组件本身只提供样式,内容则由参数来决定,这样可以提高子组件的通用性和灵活性;
1 2 3 4 5 <FancyButton > <span style ="color: red" > Click me!</span > <AwesomeIcon name ="plus" /> </FancyButton >
1 2 3 4 <button class ="fancy-btn" > <slot > </slot > </button >
1 2 3 4 5 <button class ="fancy-btn" > <span style ="color: red" > Click me!</span > <AwesomeIcon name ="plus" /> </button >
作用域:插槽内容可以访问父组件中定义的变量,但无法访问子组件中的数据;
默认内容:插槽允许指定默认内容,这样当父组件没有传入内容时,可显示默认内容;就像默认参数值一样;
1 2 3 4 5 <button type ="submit" > <slot > Submit </slot > </button >
具名插槽 组件支持多个插槽,为了避免混淆,需要为每个插槽指定名称,这样传入内容的时候,才能够匹配;
1 2 3 4 5 6 7 8 9 10 11 12 <div class ="container" > <header > <slot name ="header" > </slot > </header > <main > <slot > </slot > </main > <footer > <slot name ="footer" > </slot > </footer > </div >
1 2 3 4 5 6 7 8 9 <BaseLayout > <template v-slot ="header" > </template > <template #footer > </template > </BaseLayout >
父组件的插槽名称必须和子组件中的插槽名称完全一样,如果不一样,会无法匹配,因此也无法渲染
插槽的名称可以是动态的
1 2 3 <base-layout > <template v-slot: [dynamicSlotName ]> </template > </base-layout >
反向传递 子组件可以将自己的数据,通过插槽,反向传递给父组件
无渲染组件 利用插槽机制,再加上 v-slot 让子组件能够向父组件传递数据,那么接下来便出现了一种有趣的用法,即子组件只封装了逻辑,但没有封装要渲染的内容。它在通过逻辑获得数据后,可以将数据传递给父组件,由父组件自行决定如何渲染;
依赖注入 当需要向深层次的组件时,使用 props 会导致逐级透传的问题
Vue 使用 provide/inject 机制来解决逐级透传的问题
1 2 3 4 5 6 7 <script setup > import { provide } from 'vue' ; provide ({ 'message' , 'hello' }) const count = ref (0 ); provide ('count' , count); </script >
app 可以提供全局依赖/注入
1 2 3 4 5 import { createApp } from 'vue' const app = createApp ({}); app.provide ("message" , "hello" );
在子组件中,使用 inject 来获得想要的数据
1 2 3 4 5 6 7 8 9 10 11 <script setup > import { inject } from 'vue' const message = inject ("message" ); const value = inject ("count" , "defaultValue" ) const value = inject ("key" , () => new DefautlValue (), true ); </script >
如果需要在子组件中更改注入的数据,那么 provide 最好提供一个方法,供子组件调用,而不是直接修改。这样有利于未来的维护;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <script setup > import { ref, provide } from 'vue' const location = ref ("North Pole" ); function updateLocation ( ) { location.value = 'South Pole' ; } provide ("location" , { location, updateLocation, }) </script >
1 2 3 4 5 6 7 8 9 10 11 12 <script setup > import { inject } from 'vue' const { location, updateLocation } = inject ("location" ); </script > <template > <button @click ="updateLocation" > {{ location }} </button > </template >
如果提供方想保护自己的数据不能被修改,可以使用 readonly 将其装饰为只读的状态
1 2 3 4 5 6 7 <script setup > import { ref, provide, readonly } from 'vue' const count = ref (0 ) provide ('readOnlyCount' , readonly (count)) </script >
使用 Symbol 避免命名冲突 如果构建的应用很大,或者所编写的组件会被很多人调用,那么有可能产生命名冲突。解决办法就是将名称放在一个单独的文件中统一管理
1 2 export const myInjectKey = Symbol ();
1 2 3 4 5 import { provide } from 'vue' import { myIndectKey } from "./key.js" provide (myInjectKey, {})
1 2 3 4 5 import { inject } from 'vue' import { myInjectKey } from "./key.js" const injected = inject ("myInjectKey" );
异步组件 当应用变得很大时,每次打开便加载所有组件将耗费很长的等待时间,更好的做法是懒加载,即等用到某个组件时,再去加载它;
1 2 3 4 5 6 7 8 9 import MyComponet from "./components/MyComponent.vue" import { defineAsyncComponent } from 'vue' const AsyncComp = defineAsyncComponent (() => { import ("./components/MyComponent.vue" ) })
加载错误 异步加载因为是使用时再加载的,那么有可能因为网络信号不好,导致加载失败,此时可提供一个组件,来应对出错的情况,defineAsyncComponent 支持多个配置选项
1 2 3 4 5 6 7 const AsyncComp = defineAsyncComponent ({ loader : () => import ("../components/Foo.vue" ), loadingComponent : LoadingComponent , errComponent : ErrorComponent , delay : 200 , timeout : 3000 , })
逻辑复用 组合式函数 当某个行为逻辑被很多个组件复用时,可以把它抽象到一个公式的函数中,然后由各组件引入使用;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import { ref, onMounted, onUnmounted } from 'vue' export function useMouse ( ) { const x = ref (0 ) const y = ref (0 ) function update (event ) { x.value = event.pageX y.value = event.pageY } onMounted (() => window .addEventListener ('mousemove' , update)) onUnmounted (() => window .removeEventListener ('mousemove' , update)) return { x, y } }
1 2 3 4 5 <Script setup > import { useMouse } from "./mouse.js const { x, y } = useMouse(); // useMouse 会创建单独的实例,因此各个组件间的状态不会相互影响</Script >
我们可以将动作拆分成更小的函数,然后不同的函数可以相互组合,这样可以尽可能实现复用;
例如从后端获取数据是一个很常见的动作,获取的过程涉及三个动作,显示正在获取中;如果成功,显示数据;如果失败,显示失败提示;由于该动作很常见,因此我们可以将它封装成一个单独的函数,以便各个组件可以复用该逻辑;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <script setup > import { ref } from 'vue' const data = ref (null )const error = ref (null )fetch ('...' ) .then ((res ) => res.json ()) .then ((json ) => (data.value = json)) .catch ((err ) => (error.value = err)) </script > <template > <div v-if ="error" > Oops! Error encountered: {{ error.message }}</div > <div v-else-if ="data" > Data loaded: <pre > {{ data }}</pre > </div > <div v-else > Loading...</div > </template >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { ref } from 'vue' export function useFetch (url ) { const data = ref (null ) const error = ref (null ) fetch (url) .then ((res ) => res.json ()) .then ((json ) => (data.value = json)) .catch ((err ) => (error.value = err)) return { data, error } }
1 2 3 4 5 6 <script setup > import { useFetch } from './fetch.js' const { data, error } = useFetch ('...' )</script >
理论上也可以直接使用普通的函数,没有必要将函数封装组装,这种做的好处其实在于让它变成响应式的。因为普通的函数每次执行,都需要手动主动调用。而如果封装成了组件,同时参数为 ref 或者 getter 函数等动态类型,那么每当参数值发生变化时,组件就会自动运行。这是相对普通函数的好处;
1 2 3 4 5 6 const url = ref ('/initial-url' )const { data, error } = useFetch (url) url.value = '/new-url'
另外,也可以在函数式组件中使用 watchEffect 来监听参数变化;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { ref, watchEffect, toValue } from 'vue' export function useFetch (url ) { const data = ref (null ) const error = ref (null ) const fetchData = ( ) => { data.value = null error.value = null fetch (toValue (url)) .then ((res ) => res.json ()) .then ((json ) => (data.value = json)) .catch ((err ) => (error.value = err)) } watchEffect (() => { fetchData () }) return { data, error } }
解构重命名
1 2 3 4 5 6 7 const obj = { a : 1 , b : 2 , c : 3 , }const {a : a1, b : b2, c : c3, d4 = 'default' } = obj;
自定义指令 Vue 有一些内置的指令,例如 v-model、v-show 等,这些指令从本质上来,其实是为了操作和控制 DOM;除了内置指令,Vue 也支持编写自定义的指令,这些指令可以在不同的组件上实现复用;
1 2 3 4 5 6 7 8 9 <script setup > const vFocus = { mounted : (el ) => el.focus (), } </script > <template > <input v-focus /> </template >
vFocus 是一种强制的命名规范,以小写字母 v 开头;
指令支持多种钩子函数
1 2 3 4 5 6 7 8 9 10 11 12 13 const myDirective = { created (el, binding, vnode ){}, beforeMount (el, binding, vnode ){}, mounted (el, binding, vnode ){}, beforeUpdated (el, binding, vnode ){}, updated (el, binding, vnode ){}, beforeUnmounted (el, binding, vnode ){} unmounted (el, binding, vnode ){} }
注:应避免有组件上面使用自定义指令,而是只在原生的 HTML 元素上使用,以避免冲突,产生预期外的效果;
插件 插件可用来给 Vue 添加全局功能;
1 2 3 4 5 import { createApp } from 'vue' const app = createApp (); app.use (myPlugin), {...};
1 2 3 4 5 6 7 8 const myPlugin = { install : (app, options) {...}, }const myPlugin = (app, options ) => {}
编写插件示例 该插件给在 app 上注册一个全局可用的 $translate 函数,用来翻译指定字段
1 2 3 4 5 6 7 8 9 10 11 12 export default { install : (app, options ) => { app.config .globalProperties .$translate = (key ) => { return key.split ("." ).reduce ((0 , i ) => { if (o) { return o[i]; } }, options); } } }
1 2 3 4 5 6 7 8 import i18nPlugin from "./plugins/i18n" ; app.use (i18nPlugin, { greetings : { hello : "Bonjour!" , } })
1 2 <h1 > {{ $translate("greetings.hello")}}</h1 >
插件中也可以引入 provide / inject,这样各个组件就可以直接读取插件提供的值了
1 2 3 4 5 export default { install : (app, options ) => { app.provide ('i18n' , options), } }
1 2 3 4 5 6 <script setup > import { inject } from 'vue' ; const i18n = inject ('i18n' ); console .log (i18n.greetings .hello ); </script >
内置组件 Transition 内置的 Transition 组件,可用来给组件加载或卸载时提供动画效果;
1 2 3 4 <button @click ="show =!show" > Toggle</button > <Transition > <p v-if ="show" > hello</p > </Transition >
动画效果可以自定义,并且可以命名,以方便管理多种不同的动画效果;
TransitionGroup 可用来设置列表的动画,当列表添加或删除元素时,呈现动画效果;
KeepAlive KeepAlive 可用来缓存实例
Teleport Teleport 有点像是一个传送门,用来将组件中的部分模板,传送到外部组件上面;之所以这么做,是为了能够更好的展示传送的内容,避免受到深层嵌套过程中的其他组件的布局影响;
1 2 3 4 5 6 7 8 <button @click ="open = true" > Open Modal</button > <Teleport to ="body" > <div v-if ="open" class ="model" > <p > Hello from the modal</p > <button @click ="open = false" > Close</button > </div > </Teleport >
Teleport 会改变 DOM 的层级关系,但不会改组件之间的层级关系;
Suspence 在某个组件内部存在多个异步组件时,有可能这些异步组件都有自己的异步处理机制,例如显示加载图标。当这些子组件同时加载时,会导致页面上出现多个异步图标。Suspence 的目标是对这些异步状态统一管理,展示统一的加载状态;
应用规模化 单文件组件 一个 Vue 文件同时包含 js、html 和 css 三部分内容,即同时包含逻辑、模板和样式数据;
单文件组件是一种代码的组织方式,如果需要实现的功能非常小,例如只是给静态文件添加一些简单的交互,则可以考虑使用 petite-vue,只有 6k 大小;
路由 非常简单的页面,可用 computed 配合监听浏览器的 haschange 来切面页面;它的原理很简单,即 js 代码调用浏览器的接口,更新了 url,触发了 haschange 事件,从而调用监听函数,完成组件的更新,实现页面的切换;
正式的路由器则使用 Vue Router
状态管理 如果多个组件依赖同一份数据,那么使用 props 逐级透传的方式,会让代码变得臃肿。解决办法是将数据封装成一个全局的单例,供各个组件使用;
其中一个方案是使用 reactive、ref、computed 或者组合式函数,创建一个响应式对象,放在单独的文件中,供各个组件引用;
如果应用使用服务器渲染,则以上方案变得不太可行;此时需要使用单独的状态管理器,例如 Pinia 或者 Vuex;
测试 需要测试的东西:
单元测试:确保函数正常
组件测试:确保 component 的功能正常
端到端测试:类似于集合测试,确保整个应用正常运行;常用框架:Cypress,Playwright,Nightwatch 等;
其中端到端是最重要的,因为它确保了应用程序的运行正常;
服务端渲染 有两种场景需要用到服务端渲染 SSR:
最佳实践 生产部署 在生产服务器部署,那些提高开发效率的工具就不需要了,因此记得在打包代码是地,排除它们,以缩小文件的体积;
性能优化 Vue 本身包含了优化功能,在绝大部分场景下,vue 的性能都是够用的,除非遇到一些极端的场景,才需要手动优化;
两个常见的优化指标:
页面加载优化 常用的手段包括:
服务端渲染
减小包体积:例如构建工具使用 Tree Shaking,预编译等,避免引入太大的依赖;
代码分割:实现懒加载;
页面更新优化 当 props 变更时,会触发组件的更新,因此,在设计组件时,应该确保它的 props 值尽量稳定,以减少不必要的更新触发;
v-once 指令可用来标识无需更新的组件,这样进行更新计算时,会跳过该组件;
v-memo 指令可用来设置更新的条件;
computed 的计算结果如果发生变化,也会触发更新。 如果是值还好说,可直接比较;如果比较的是对象,那么即使值没有变化,也会触发更新;此时可考虑引入自定义的比较函数;
通用优化
大型列表的虚拟化;
绕开深层级对象的深度检查;
在大型列表中,减少不必要的组件抽象;
进阶 使用 Vue 的多种方式
独立脚本,像引入 jQuery 一样轻量化使用;
作为 Web Component 嵌入原有的旧应用;
单页面应用:主流用法;
全栈 / SSR:适用于 SEO 很重要的场景;
静态 SSG:静态站点生成 JAMStack,作用静态文件部署;