D31.Vue
F17.打包 + 图片懒加载(K104-K106)
1.打包
1)vue.config.js
module.exports = {
//打包时不生成map文件(用来进行错误提示的文件,很占用空间)
productionSourceMap: false,
// 关闭ESLINT校验工具
lintOnSave: false,
}
pnpm run build
2.图片懒加载
1)安装vue-lazyload插件
pnpm add vue-lazyload@1.3.3 --save-dev
pnpm add vue-lazyload@1.3.3 --save-dev
2)在main.js中进行引用
import VueLazyload from "vue-lazyload";
//或者自定义配置插件
Vue.use(VueLazyload, {
preLoad: 1.3,
error: require('@/assets/image/error.png'),
loading: require('@/assets/image/loading.gif'),
attempt: 1
})
3)各个自定义配置属性含义:
使用(将图片设置为懒加载)
<img v-lazy="psdimg" class="psd" />
注意: 当遇到是v-for循环的时候,或者用div包裹着img的时候,需要在div上面添加 v-lazy-container="{ selector: ‘img’ }"
<div v-lazy-container="{ selector: 'img' }">
<img data-src="//aaa.com/img1.jpg">
<img data-src="//aaa.com/img2.jpg">
<img data-src="//aaa.com/img3.jpg">
</div>
<!--或者这种:-->
<div v-lazy-container="{ selector: 'img' }" v-html="content">
</div>
如果是这种情况的话,一定要使用 data-src 来指定路径,而不使用v-lazy。因为如果使用的是v-lazy的话,拿到了图片地址也会一直显示不出来
F18.动态组件 组件注册(K107-K110)
1.动态组件
A.基本使用
1)composition api写法:只适用于vue3
- 在 < script setup> 中,组件被引用为变量而不是作为 字符串键 来注册
- 核心点 shallowRef()
- 虽然ref()也能正常使用,但官方不推荐,会爆warn: "这可能会导致不必要的性能开销" (原因,组件非动态数据,不需要转为proxy)
- :is 与 component > 设置动态组件的必要条件
- :is 对应绑定的为字符串值即可,值对应引入的组件名
- props 数据父传子
- shallowRef > 声明 :is 绑定的值,值为 import 的组件名
<template> <component :is='dynamic' :datas='{data1,data2}'></component> <button @click='changeComponent'>更改组件</button> </template> <script setup lang="ts"> import test1 from './test-components/test1.vue' import test2 from './test-components/test2.vue' import { ref,shallowRef } from 'vue' let dynamic:any = shallowRef(test1) let state = ref(true) function changeComponent() { if (state.value) { dynamic.value = test2 }else{ dynamic.value = test1 } state.value = !state.value } let data1 = ref('') let data2 = ref({elPsyKongroo:'怀表'}) </script>
用 markRaw 也可以,但是不如shallwRef直观与方便
B.官方案例
1)官方案例<script setup> import Home from './Home.vue' import Posts from './Posts.vue' import Archive from './Archive.vue' import { ref } from 'vue' const currentTab = ref('Home') const tabs = { Home, Posts, Archive } </script> <template> <div class="demo"> <button v-for="(tab, index) in tabs" :key="index" :class="['tab-button', { active: currentTab === index }]" @click="currentTab = index" > {{ tab }} </button> <component :is="tabs[currentTab]" class="tab"></component> </div> </template> <style> .tab-button.active { background: #e0e0e0; } </style>
上面的例子是通过 Vue 的 元素和特殊的 is attribute 实现的:
<!-- currentTab 改变时组件也改变 --> <component :is="tabs[currentTab]"></component>
2)在上面的例子中,被传给 :is 的值可以是以下几种:
- 被注册的组件名
- 导入的组件对象
也可以使用 is attribute 来创建一般的 HTML 元素
当使用 < component :is=“…”> 来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过 `` 组件强制被切换掉的组件仍然保持“存活”的状态
2.组件注册
A.全局注册
1)例如组件使用频率非常高(table,Input,button,等)这些组件 几乎每个页面都在使用便可以封装成全局组件
我们可以使用 Vue 应用实例的 app.component() 方法,让组件在当前 Vue 应用中全局可用import { createApp } from 'vue' const app = createApp({}) app.component( // 注册的名字 'MyComponent', // 组件的实现 { /* ... */ } )
2)如果使用单文件组件,你可以注册被导入的 .vue 文件:
import MyComponent from './test.vue' app.component('MyComponent', MyComponent)
3)app.component() 方法可以被链式调用:
app .component('ComponentA', ComponentA) .component('ComponentB', ComponentB) .component('ComponentC', ComponentC)
4)全局注册的组件可以在此应用的任意组件的模板中使用:
<!-- 这在当前应用的任意组件中都可用 --> <ComponentA/> <ComponentB/> <ComponentC/>
所有的子组件也可以使用全局注册的组件,这意味着这三个组件也都可以在彼此内部使用
B.局部注册
1)全局注册虽然很方便,但有以下几个问题:
a. 全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中
b. 全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性
相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好
在使用<script setup> import ComponentA from './ComponentA.vue' </script> <template> <ComponentA /> </template>
2)如果没有使用 < script setup>,则需要使用 components 选项来显式注册:
import ComponentA from './ComponentA.js' export default { components: { ComponentA }, setup() { // ... } }
3)对于每个 components 对象里的属性,它们的 key 名就是注册的组件名,而值就是相应组件的实现。上面的例子中使用的是 ES2015 的缩写语法,等价于:
export default { components: { ComponentA: ComponentA } // ... }
请注意:局部注册的组件在后代组件中并不可用。在这个例子中,ComponentA 注册后仅在当前组件可用,而在任何的子组件或更深层的子组件中都不可用
C.组件名格式
1)在整个指引中,我们都使用 PascalCase 作为组件名的注册格式,这是因为:
a. PascalCase 是合法的 JavaScript 标识符。这使得在 JavaScript 中导入和注册组件都很容易,同时 IDE 也能提供较好的自动补全
b. < PascalCase /> 在模板中更明显地表明了这是一个 Vue 组件,而不是原生 HTML 元素。同时也能够将 Vue 组件和自定义元素 (web components) 区分开来
在单文件组件和内联字符串模板中,我们都推荐这样做。但是,PascalCase 的标签名在 DOM 模板中是不可用的
为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。这意味着一个以 MyComponent 为名注册的组件,在模板中可以通过 < MyComponent> 或 < my-component> 引用。这让我们能够使用同样的 JavaScript 组件注册代码来配合不同来源的模板F19.TS类型声明 + keepAlive(K110-K116)
1.Ts 类型声明使用 & 讲解
核心:大部分都是运用API函数泛型来定义类型
A.父组件相关的<template> <el-button @click="onsub">测试</el-button> <input type="text" @change="handleChange" /> <child ref='childRef' :child='2' :strData='"1"' :arrFor="[]" @elPsyKongroo='onsub'></child> </template> <script lang='ts' setup> import child from './child.vue' import { ref,Ref,reactive,computed,customRef,watch,provide } from "vue"; //> ref // interface Ref<T> { // value: T // } // function ref<T>(value: T): Ref<T> const year = ref<string | number>('2020') // 如果泛型的类型未知,则建议将 ref 转换为 Ref<T>: function useState<State extends string>(initial: State) { const state = ref(initial) as Ref<State> // state.value -> State extends string return state } //> reactive interface Book { title: string year?: number } const book = reactive<Book>({title:'唉,真有氏的怀表怎么停了!'}) // function reactive<T extends object>(target: T): UnwrapNestedRefs<T> //> customRef // function customRef<T>(factory: CustomRefFactory<T>): Ref<T> // type CustomRefFactory<T> = ( // track: () => void, // trigger: () => void // ) => { // get: () => T // set: (value: T) => void // } function useDebouncedRef(value:string, delay = 200) { return customRef<string>((track, trigger) => { return { get() { return value }, set(newValue) { value = newValue } } }) } let a23 = useDebouncedRef('heelo1') a23.value = '123' //> provide 与 inject // inject示例在同级child.vue里 // interface InjectionKey<T> extends Symbol {} // function provide<T>(key: InjectionKey<T> | string, value: T): void provide('static',year) provide('pbook',book) provide('changeFn',onsub) //! 有时候可能需要在子组件修改响应式的数据,此时provide一个方法给子组件调用 //> computed let count = ref(0) const doubleCount = computed<number>(() => count.value + 2) //> watch watch<number>(count,()=>{}) // watch<Ref<number>>(count,()=>{}) // 也可以 interface ReactiveData2{ content2: { count2: number } } let refData = ref(1) let reactiveData = reactive({content:{count:110}}) let reactiveData2 = reactive<ReactiveData2>({content2:{count2:1}}) watch<[Ref<number>,() => number,ReactiveData2]>([refData, ()=>reactiveData.content.count,reactiveData2], ([a,b,c], oldValue) => { console.log(a,b,c, oldValue) }) // defineExpose 暴露的内容 // let childRef = ref() // setTimeout(() => { // console.log(childRef.value.ex1); // 如果是子组件的ref对象数据,会自动解包 .value // }, 1000); function handleChange(el) { console.log((el.target as HTMLInputElement).value) console.log(el.target.value) } function onsub(val) { console.log(val); year.value = 2036 book.title = '掌管未来女神的作战计划 El psy kongroo' } </script>
B.子组件相关的
<template> <div class="studyContent"> <div>{{a}}</div> <div>{{a2}}</div> <div>{{pbook.title}}</div> </div> </template> <script lang='ts' setup> import { inject,ref,Ref } from "vue"; //! defineProps 或 defineEmits 只能是要么使用`运行时声明`,要么使用`类型声明`。同时使用两种声明方式会导致编译报错。 //> defineProps // 仅限类型的 defineProps 声明的不足之处在于,它没有可以给 props 提供默认值的方式。为了解决这个问题,提供了 withDefaults 编译器宏: //? 运行时声明 的方式只能限制参数类型,无法限制是否必传、和默认值 // const props = defineProps({ // child: String, // sda: String, //undefined // strData: String, // arrFor: Array // }) //? 类型声明 的方式1:能限制是否必传 > defineProps 单独使用该api // interface arrfor { // name:string, // children?:Array<arrfor> // } // const props = defineProps<{ // child?: string|number, // sda?: string, //undefined // strData?: string, // arrFor: [] // }>(); // console.log(props); //? 类型声明 的方式2:能限制是否必传,以及默认值 interface Props { child: string|number, sda?: string, // 未设置默认值,为 undefined strData: string, msg?: string labels?: string[], obj?:{a:number} } const props = withDefaults(defineProps<Props>(), { msg: 'hello', labels: () => ['one', 'two'], obj: () => { return {a:2} } }) console.log(props,props.msg); //> defineEmits // // 等效this.$emit('eventName','data') // const emits = defineEmits(['update', 'delete']) // emits('delete','测试') // emits的类型声明写法,()的e id只是形参名字,不影响其他。 const emit = defineEmits<{ (e: 'elPsyKongroo', id: number): void (e: 'update', value: string): void }>() setTimeout(() => { // emit('elPsyKongroo', 2) }, 1000*2); //> defineExpose interface exFace { ex1:Ref<string>, ex2?:number } let ex1 = ref('1') let exObj:exFace = { ex1, } // 源码类型: const defineExpose: (exposed?: Record<string, any>) => void defineExpose(exObj) //> inject // interface InjectionKey<T> extends Symbol {} // // 没有默认值 // function inject<T>(key: InjectionKey<T> | string): T | undefined // // 有默认值 // function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T // // 有工厂函数 // function inject<T>( // key: InjectionKey<T> | string, // defaultValue: () => T, // treatDefaultAsFactory: true // ): T let defaultFn = inject('ab12',()=>'雏见泽'+'棉流',true) console.log(defaultFn); interface Book { title: string year?: number } let a = inject<Ref>('static') // 无默认值 //! 即使在子组件可以直接修改,但最好不要这么做,将会影响到provide的父组件以及其他所有inject的子组件。 //! 这会导致 溯源 非常麻烦,所以修改方式统一为在父组件provide一个方法,子组件调用进行修改! // a.value = '直接作死修改' let pbook = inject<Book>('pbook') // 无默认值 let changeFn:(val:string)=>void = inject('changeFn') // 无默认值 let a2 = inject('static2','????') // 有默认值 let a3 = inject('static3') // 无默认值且未找到则为 undefined let globalGuide = inject('guide') // 访问全局的 setTimeout(() => { changeFn('injectFn传参') }, 5000); </script>
2.keep-alive
A.基本介绍
1)有时候我们不希望组件被重新渲染影响使用体验;或者处于性能考虑,避免多次重复渲染降低性能。而是希望组件可以缓存下来,维持当前的状态。这时候就需要用到keep-alive组件
开启keep-alive 生命周期的变化- 初次进入时: onMounted> onActivated
- 初次进入时: onMounted> onActivated
- 再次进入:
- 只会触发 onActivated
- 事件挂载的方法等,只执行一次的放在 onMounted中;组件每次进去执行的方法放在 onActivated中
<keep-alive :include="" :exclude="" :max=""></keep-alive>
2)Props:
- include - string | RegExp | Array。只有名称匹配的组件会被缓存
- exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存
- max - number | string。最多可以缓存多少组件实例
3)用法:
< keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 相似, 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中
4)主要用于保留组件状态或避免重新渲染
注意事项:
a. 优先级: 如果 exclude 和 include 内都有同个组件名, exclude 优先级高于 include
b. 缓存销毁: max 当已缓存的组件数量超过 max 值,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉
c. 生命周期: 被缓存的组件才会调用 activated 这些缓存相关的生命周期 ,换句话说就是 exclude 内的组件不会触发 activated!!
d. 离开组件、再度激活: 当组件在 < keep-alive> 内被切换时,它的 mounted 和 unmounted 生命周期钩子不会被调用,取而代之的是 activated 和 deactivated。(这会运用在 的直接子节点及其所有子孙节点。)
B.使用案例<template> <keep-alive :exclude="['test1']" :include="['test1','test2']" max='1'> <component :is='dynamic'></component> </keep-alive> <el-button @click='changeComponent'>更改组件</el-button> </template> <script setup lang="ts"> import test1 from './test-components/test1.vue' import test2 from './test-components/test2.vue' import { ref,shallowRef } from 'vue' let dynamic:any = shallowRef(test1) let state = ref(true) function changeComponent() { if (state.value) { dynamic.value = test2 }else{ dynamic.value = test1 } state.value = !state.value } </script>
1)include 和 exclude
include 和 exclude prop 允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示:<!-- 逗号分隔字符串 --> <keep-alive include="a,b"> <component :is="view"></component> </keep-alive> <!-- regex (使用 `v-bind`) --> <keep-alive :include="/a|b/"> <component :is="view"></component> </keep-alive> <!-- Array (使用 `v-bind`) --> <keep-alive :include="['a', 'b']"> <component :is="view"></component> </keep-alive>
2)使用了 include / exclude 后,必须显式声明组件的 name !!! 这样才能与缓存组件匹配、生效
在 3.2.34 或以上的版本中, 使用注意,< keep-alive> 是用在其一个直属的子组件被切换的情形。如果你在其中有 v-for 则不会工作。如果有上述的多个条件性的子元素,< keep-alive> 要求同时只有一个子元素被渲染
C.缓存实例的生命周期
1)当一个组件实例从 DOM 上移除但因为被 < KeepAlive> 缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活
一个持续存在的组件可以通过 onActivated() 和 onDeactivated() 注册相应的两个状态的生命周期钩子:<script setup> import { onActivated, onDeactivated } from 'vue' onActivated(() => { // 调用时机为首次挂载 // 以及每次从缓存中被重新插入时 }) onDeactivated(() => { // 在从 DOM 上移除、进入缓存 // 以及组件卸载时调用 }) </script>
注:
onActivated 在组件挂载时也会调用,并且 onDeactivated 在组件卸载时也会调用
这两个钩子不仅适用于 < KeepAlive> 缓存的根组件,也适用于缓存树中的后代组件F20.Router 4(K117-K123)
使用Vue3 安装对应的router4版本
npm install vue-router@4
1.路由配置
1)与之前版本区别:
a. 由 createRouter() 替换 new Router()
b. 路由模式由 createWebHistory() 替换 mode: ‘history’
c. main.js中由 .use(router) 替换 new Vue({ router })
2)以前版本写法:// router/index.js import Vue from 'vue'; import Router from 'vue-router'; import routes from './routes' Vue.use(Router); const router = new Router({ // 区别1 mode: 'history', // 区别2 routes }); export default router; // main.js import Vue from 'vue' import router from './router' // ... new Vue({ el: '#app', router, // 区别3 components: { App }, template: '<App/>' })
3)vue-router 4.x 版本写法:
// router/index.js import { createRouter, createWebHistory } from 'vue-router'; import routes from './routes' const router = createRouter({ // 区别1 history: createWebHistory(process.env.BASE_URL), // 区别2 routes }) export default router // main.js import { createApp } from 'vue' import App from './App.vue' import router from './router' const app = createApp(App); app.use(router).mount('#app'); // 区别3
4)路由模式区别:
在src目录下面新建router 文件 然后在router 文件夹下面新建 index.ts
//引入路由对象 import { createRouter, createWebHistory, createWebHashHistory, createMemoryHistory, RouteRecordRaw } from 'vue-router' //vue2 mode history vue3 createWebHistory //vue2 mode hash vue3 createWebHashHistory //vue2 mode abstact vue3 createMemoryHistory //路由数组的类型 RouteRecordRaw // 定义一些路由 // 每个路由都需要映射到一个组件。 const routes: Array<RouteRecordRaw> = [{ path: '/', component: () => import('../components/a.vue') },{ path: '/register', component: () => import('../components/b.vue') }] const router = createRouter({ history: createWebHistory(), routes }) //导出router export default router
最后在main.ts 挂载
import { createApp } from 'vue' import App from './App.vue' import router from './router' createApp(App).use(router).mount('#app')
2.路由跳转及参数
A.router: 是VueRouter的一个全局对象,通过Vue.use(VueRouter)和VueRouter构造函数得到的一个实例对象,包含了所有路由包含了许多关键的对象和属性
B.route: 是一个跳转路由的局部对象,每个路由都会有一个route对象,可以获取对应的name、path、params、query等
1)以上在vue2.x与vue3.x中是一致的,要注意区分
2)在vue3.x setup中 , useRouter、useRoute通常用来:
useRouter:进行路由跳转
useRoute:获取路由参数<script setup> import { useRoute, useRouter } from 'vue-router' const router = useRouter(); const route = useRoute(); console.log(route); // 获取路由参数 router.push('/logo'); // 进行路由跳转 </script>
C.vue-router 3.x中 获取路由参数:
在组件中: {{ $ route.query.color}} 或 {{ $ route.params.color}}
在 JS 中: this. $ route.query.color 或 this.$route.params.color
3.路由(导航)守卫
路由守卫简单来讲就是监听页面进入,修改,和离开的功能
1)每个守卫接受三个参数:
to:即将要进入的路由对象
from:当前导航正要离开的路由
next:一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数
2)关于 next :
a. next():进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)
b. next(false):中断当前的导航。如果浏览器的 URL 改变了(可能是用户手动或浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址
c. next(‘/’) 或 next({ path: ‘/’ }):跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航
d. next(error):(2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。
3)vue-router 3.x中:<script type="text/ecmascript-6"> export default { beforeRouteEnter(to, from, next){ // 在渲染该组件的对应路由被confirm前调用 // 此时组件实例还没被创建,因此不能获取`this` } beforeRouteUpdate(to, from, next){ // 在当前路由改变,但该组件被复用时调用 // 举例:对于一个带有动态参数的路径`/item/:id`,在`/item/1`和`/item/2`之间跳转的时候, // 由于会渲染相同的`Item`组件,因此组件实例会被复用,从而触发这个钩子 // 此时可以获取到`this` } beforeRouteLeave(to, from, next){ // 导航离开该组件的对应路由时调用 // 时可以获取到`this` } } }; </script>
4)vue-router 4.x中:
在setup中,由于路由已经发生了,因此在setup内部设置beforeRouteEnter没有任何意义,因此并无onBeforeRouteEnter
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router' import { ref } from 'vue' export default { setup() { // 与 beforeRouteLeave 相同,无法访问 `this` onBeforeRouteLeave((to, from) => { const answer = window.confirm( 'Do you really want to leave? you have unsaved changes!' ) // 取消导航并停留在同一页面上 if (!answer) return false }) const userData = ref() // 与 beforeRouteUpdate 相同,无法访问 `this` onBeforeRouteUpdate(async (to, from) => { //仅当 id 更改时才获取用户,例如仅 query 或 hash 值已更改 if (to.params.id !== from.params.id) { userData.value = await fetchUser(to.params.id) } }) }, }
4.动态路由
A.动态路由参数 id:<!-- 导航页 --> <template> <router-link :to="'/news/' + newsId">新闻详情</router-link> </template> <script setup> import { ref } from 'vue'; const newsId = ref('001'); </script>
B.获取路由参数:
<!-- 新闻详情页 --> <template> <div id="news"> <p>id:{{$route.params.id}}</p> <p>{{newsId}}</p> </div> </template> <script setup> import {useRoute} from 'vue-router'; import {computed} from 'vue'; const route=useRoute(); const newsId=computed(()=>{ return route.params.id }) </script>
5.keep-alive
A.可利用keep-alive的 include 或 exclude 属性(include 和 exclude 包含的name 是组件的name不是router name)来设置缓存:
1)include 值为字符串或者正则表达式匹配的组件name会被缓存
2)exclude 值为字符串或正则表达式匹配的组件name不会被缓存
B.vue2.x写法:<keep-alive exclude="Home"> // 缓存除了Home外的所有组件 <router-view></router-view> </keep-alive>
C.vue3.x写法:
1)将内容传递给路由组件的 < slot>
之前你可以直接传递一个模板,通过嵌套在 组件下,由路由组件的 来渲染:<router-view> <p>In Vue Router 3, I render inside the route component</p> </router-view>
由于 < router-view> 引入了 v-slot API,你必须使用 v-slot API 将其传递给 < component>:
<router-view v-slot="{ Component }"> <component :is="Component"> <p>In Vue Router 3, I render inside the route component</p> </component> </router-view>
<template> <router-view v-slot="{ Component }"> <keep-alive :include="includeList"> <component :is="Component" /> </keep-alive> </router-view> </template> <script setup> import { ref } from 'vue'; // 需要缓存的组件name值 const includeList = ref(['About', 'User']); // 缓存About和User组件 </script>
也可以在router.js中添加meta属性动态判断:
meta: { title: '缓存页面', keepAlive: true }
import { watch } from 'vue' import {useRouter} from 'vue-router' const router =useRouter() const includeList = []; watch(() => router.currentRoute.value,(val) => { // 只要设置了keepAlive属性的,全部加入缓存列表中 if (val.meta.keepAlive && keepAliveList.indexOf(val.name) === -1) { includeList.push(val.name); } },{immediate: true,deep: true})