Vue3 开发笔记
Vue3
课程链接:【Vue3 + vite + Ts + pinia + 实战 + 源码 +electron】
一.配置开发环境
1.node.js安装
在前篇的Node.js中有涉及,对Node.js感兴趣的可以了解下
- Node.js中文网 建议下载稳定版
2.nvm安装
-
重要用于控制 node.js npm 版本用的。
-
不太建议使用,建议通过上面的方法安装 node.js。
3.搭建项目
3.1.搭建流程——1
-
打开终端输入
npm init vite@latest
最新版 -
输入项目名称
-
选择
Vue
和TypeScript
-
VsCode 打开项目
public
下存放一些静态资源src/asets
也可存放静态资源src/components
存放组件src/App.vue
vue的入口文件src/main.ts
全局的ts文件,如全局的 api 可在此进行封装src/vitw-env.d.ts
声明文件扩充index.html
vite 通过该文件作为入口文件,webpack通过js文件作为入口文件tsconfig
TS配置文件vite.config.ts
vite的配置文件
-
npm install
安装依赖 -
开发模式
npm run dev
生产模式npm run build
预览生产环境npm run perview
3.2.搭建流程——2
-
打开终端输入
npm init vue@latest
pnpm create vue@latest
-
输入项目名称
-
选择插件
- TypeScript
- JSX是否支持
- vue-router
- 是否状态管理库 Pinia
- 是否 Vitest 单测工具
- 是否 Cypress 自动化测试选择,或 Playwright
- 是否 ESLint 语法检测
- 是否 Prettier 代码格式化工具
-
打开终端,
npm i
下载依赖很遗憾,没有下载成功,镜像源都试过了,还是有的包没有成功。。。如Cypress
3.3.VSCode插件介绍
如果开发过vue2,则要禁用 vetur 插件
- Vue Language Features (Volar)
- TypeScript Vue Plugin (Volar)
二.语法解读
1.基础语法
-
模板语法
<script setup lang="ts"> const message: string = 'Vue3开发' const boolean: number = 0 const api: string = '你,好,我,是,VUE3' </script> <template> <div>{{ message }}</div> <div>{{ boolean ? '如果为真' : '如果为假' }}</div> <div>{{ ++boolean }}</div> <div>{{ api.split(',').map((v) => `+${v}`) }}</div> </template>
-
vue指令
-
v-text 用来显示文本
-
v-html 用来展示富文本
-
v-if 用来控制元素的显示隐藏(切换真假DOM)
-
v-else-if 表示 v-if 的“else if 块”。可以链式调用 v-else v-if条件收尾语句
-
v-show 用来控制元素的显示隐藏(display none block Css切换)
-
v-on 简写@ 用来给元素添加事件
-
v-bind 简写: 用来绑定元素的属性Attr
-
v-model 双向绑定
-
v-for 用来遍历元素
-
v-on 修饰符 冒泡案例
具体见vue2,列举几个略奇怪的用法
<script setup lang="ts"> type Style = { color: string height: string } const style: Style = { color: 'blue', height: '300px' } const flag: boolean = false type Cls = { a: boolean b: boolean } const cls: Cls = { a: true, b: false } </script> <template> <div :style="style">我是Vue3</div> <div :class="['a', 'b']">我是Vue3</div> <div v-bind:class="[flag ? 'a' : 'b', flag ? 'a' : 'c']">我是Vue3</div> <div v-bind:class="cls">我是Vue3</div> </template> <style scoped> .a { color: red; } .b { height: 300px; } .c { color: aqua; } </style>
<script setup lang="ts"> const Arr: Array<any> = [ { name: '1' }, { name: '2' }, { name: '3' }, { name: '4' } ] </script> <template> <div v-for="(item, index) in Arr" :key="index">{{ item }}</div> </template>
-
2.虚拟DOM、Diff算法
2.1.虚拟DOM
虚拟DOM就是通过JS来生成一个AST节点树
为什么需要虚拟DOM?
- 直接操作DOM非常浪费性能
- 尽可能的利用JS的技术性能来换取操作DOM所消耗的性能
2.2.DIff算法
可以理解为寻找差异,所消耗性能尽可能少的算法
3.Ref全家桶
理解在 vue2 中的
return data()
中的数据,是响应式的
-
ref
<template> <div> {{ Man }} </div> <button @click="change">改变</button> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' import type { Ref } from 'vue' type M = { name: string } // 法1 const Man: Object = ref<M>({ name: 'Vue3' }) // 法2,推荐类型复杂的时候去自定义,也可不定义,因为TS兼容JS const man: Ref<M> = ref({ name: 'Vue2' }) const change = () => { // ref 返回的是一个 class 类,所以是固定写法 .value Man.value.name = 'Vue2' console.log(Man) } </script>
-
isRef
用于判断一个对象是否为 ref 对象
<script> import { ref, isRef } from 'vue' import type { Ref } from 'vue' type M = { name: string } const Man: Object = ref<M>({ name: 'Vue3' }) // 返回 true console.log(isRef(Man)); </script>
-
shallowRef
ref 可以判断深层次的
shallowRef 可以判断浅层次的响应
- ref 和 shallowRef 不能一起使用
<template> <div>ref: {{ Man }}</div> <div>shallowRef: {{ Man2 }}</div> <button @click="change">一起改变</button> <button @click="change2">单独改变shallowRef</button> </template> <script setup lang="ts"> import { ref, isRef, shallowRef } from 'vue' const Man = ref({ name: 'Vue3' }) const Man2 = shallowRef({ name: 'Vue3' }) const change = () => { // 如果 ref 和 shallowRef 一起出现,则会同时刷新视图 Man.value.name = 'Vue2' Man2.value.name = 'Vue2' } const change2 = () => { // 值会变,但不会刷新视图 Man2.value.name = 'Vue' // 值会变,也会刷新视图 Man2.value = { name: 'Vue2' } } </script>
-
triggerRef
vue 底层在进行视图更新时,会调用
triggerRef
,它会强制更新收集的依赖,所以在出现 ref 和 shallowRef 时,ref 进行视图更新时调用triggerRef()
,就会导致 shallowRef 的更新<script setup lang="ts"> import { ref, isRef, shallowRef, triggerRef } from 'vue' const Man = shallowRef({ name: 'Vue3' }) const Man2 = shallowRef({ name: 'Vue3' }) const change = () => { Man2.value.name = 'Vue2' // Man 的视图更新在底层调用了 triggerRef,导致视图统一刷新 Man.value = { name:'Vue2' } } const change2 = () => { // 不更新视图 Man2.value.name = 'Vue2' // 调用后,强制更新视图 triggerRef(Man2) } </script>
-
customRef
自定义 ref ,比如在对 ref 进行更新时,调用接口获取数据
<template> <div>customRef {{ obj }}</div> <button @click="change3">改变customRef</button> </template> <script setup lang="ts"> import { ref, isRef, shallowRef, triggerRef, customRef } from 'vue' // 自定义 ref function MyRef<T>(value: T) { let timer: any // 一个回调函数 return customRef((track, trigger) => { return { get() { // 收集依赖 track() return value }, set(newVal) { // 触发依赖 clearInterval(timer) timer = setTimeout(() => { console.log('调用接口') value = newVal timer = null trigger() }, 500) } } }) } const obj = MyRef<string>('Vue3') const change3 = () => { obj.value = 'customRef Vue2' } </script>
-
小技巧
-
开起自定义格式设置工具,vue 开发者的优化,对查看 ref 系列的 value 更方便,不用点开多层对象
-
给 dom 元素添加 ref
<template> <button @click="change3">改变customRef</button> <hr /> <div ref="dom">我是dom</div> </template> <script setup lang="ts"> import { ref, isRef, shallowRef, triggerRef, customRef } from 'vue' const dom = ref<HTMLDivElement>() const change3 = () => { console.log(dom.value?.innerText) // 我是dom } </script> <style scoped></style>
-
-
unref
辅助函数,获取ref中本身的值
<script setup> const a = ref({ log: () => { console.log('打印a') } }) unref(a).log </script>
4.Reactive全家桶
和 ref 一样,把一个变量变成一个响应式对象
-
区别
- ref 支持所有类型,reactive 仅支持 引用类型
查看源码可知:reactive 有泛型约束
-
ref 要通过
.value
进行赋值,而 reactive 不需要<template> <div> <form> <input type="text" :value="form.name" /> <hr /> <input type="text" :value="form.age" /> <hr /> <button @click.prevent="submit">提交</button> </form> </div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' type M = { name: string age: number } let form = reactive<M>({ name: '张三', age: 18 }) form.age = 19 const submit = () => { console.log(form) } </script> <style scoped></style>
-
reactive
reactive 属于 proxy 直接赋值会破坏 reactive 响应式对象
<template> <div> <ul> <li v-for="(item, index) in lists" :key="index"> {{ item }} </li> </ul> <button @click="submit"></button> </div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' let lists = reactive<string[]>([]) const submit = () => { setTimeout(() => { // 页面无法响应式 lists = ['EDG', 'JDG', 'RNG'] console.log(lists) }, 1000) } </script>
-
数组可以采用解构赋值
lists.push(...['EDG', 'JDG', 'RNG'])
-
也可将
lists
当作一个对象,在里面存放一个arr
数组对象lists.arr = lists.push(...['EDG', 'JDG', 'RNG'])
-
-
readonly
只读的值
<script setup lang="ts"> import { ref, reactive, readonly } from 'vue' let obj = reactive<object>({ name: 'Vue3' }) let read = readonly(obj) // read 只读,不可改变 read.name = 'Vue2' const show = function () { console.log(obj, read) } </script>
-
shallowReactive
与 shallowRef 一样,也存在一模一样的问题,也不叫做问题,应该说就是这样设计的。可以优化对象响应式的性能,根据自己的用途来
- shallowRef 响应到
ref.value
- shallowReactive 响应到
reactive.第一个对象
- shallowRef 响应到
4.to系列全家桶
4.1.toRef
toRef 只能修改响应式对象的值,非常响应式视图毫无变化
总结:拿 toRef 给一个非响应式对象没啥用,给响应式对象用的
<template>
<div>
{{ man }}
</div>
<hr />
<div>{{ like }}</div>
<div>
<button @click="change">修改</button>
</div>
</template>
<script setup lang="ts">
import { toRef, reactive, toRefs, toRaw } from 'vue'
const man = reactive({ name: '坤坤', age: 18, like: '唱跳Rap篮球' })
const like = toRef(man, 'like')
const change = () => {
like.value = '你食不食油饼'
console.log(like)
}
</script>
4.2.toRefs
批量创建ref对象,主要是方便我们解构使用
-
源码
<template> <div> {{ man }} </div> <hr /> <div>{{ name }}--{{ age }}--{{ like }}</div> <div> <button @click="change">修改</button> </div> </template> <script setup lang="ts"> import { toRef, reactive, toRaw } from 'vue' const man = reactive({ name: '坤坤', age: 18, like: '唱跳Rap篮球' }) const toRefs = <T extends Object>(object: T) => { const map: any = {} for (let key in object) { map[key] = toRef(object, key) } return map } // 解构 let { name, age, like } = toRefs(man) const change = () => { name.value = '爱坤' console.log(name, age, like) } </script> <style scoped></style>
4.3.toRaw
将响应式对象转化为普通对象
<script setup lang="ts">
import { toRef, reactive, toRefs, toRaw } from 'vue'
const man = reactive({ name: '坤坤', age: 18, like: '唱跳Rap篮球' })
const change = () => {
console.log(man, toRaw(man))
}
</script>
5.响应式原理
- vue 2
- 使用 Object.defineProperty
- 只能劫持设置好的数据,新增的数据需要Vue.Set(),数组只能操作七种方法,修改某一项值无法劫持
- vue 3
- 使用 Proxy
6.computed 计算属性
和vue2没啥变化。。
示例:
<template>
<div>
<input v-model="fistName" type="text" />
<input v-model="lastName" type="text" />
<hr />
<div>{{ name }}</div>
<hr />
<div>{{ name1 }}</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
let fistName = ref('')
let lastName = ref('')
// 写法1
const name = computed(() => {
return fistName.value + '---' + lastName.value
})
// 写法2
const name1 = computed({
get() {
return fistName.value + '---' + lastName.value
},
set() {
fistName.value + lastName.value
}
})
</script>
<style scoped></style>
7.侦听器
7.1watch
-
侦听
-
侦听单个
<template> <div> case1: <input type="text" v-model="messgae"> </div> </template> <script setup lang="ts"> import { ref, reactive, watch } from 'vue' let messgae = ref<string>('Vue3') watch(messgae,(newVal,oldVal)=>{ console.log(newVal,oldVal); }) </script>
-
侦听多个
旧值和新值都将以数组形式返回
<script setup lang="ts"> import { ref, reactive, watch } from 'vue' let messgae = ref<string>('Vue3') let messgae2 = ref<string>('Vue2') watch([messgae, messgae2], (newVal, oldVal) => { console.log(newVal, oldVal) }) </script>
-
-
deep
开启深度监听,只要对象中任何属性变化了,就会触发“对象的监听器”
- 但是,侦听器在侦听引用类型时,所返回的旧值和新值一样
<script setup lang="ts"> import { ref, reactive, watch } from 'vue' let messgae2 = ref({ foo:{ bar:{ name:'Vue2' } } }) watch( messgae2, (newVal, oldVal) => { console.log(newVal, oldVal) },{ // 开启深度监听 deep:true }) </script>
- 监听 reactive 时,默认开启 deep
- 监听对象里特定值时,须把监听值变成一个函数
<template> <div>case1: <input type="text" v-model="messgae.foo.bar.age" /></div> <div>case2: <input type="text" v-model="messgae.foo.bar.name" /></div> </template> <script setup lang="ts"> import { ref, reactive, watch } from 'vue' let messgae = reactive({ foo: { bar: { name: '张三', age:18 } } }) watch( ()=>messgae.foo.bar.age, (newVal, oldVal) => { console.log(newVal, oldVal) }, { // reactive 默认开启深度监听 // deep: true } ) </script>
-
immediate
加载时立即执行
watch( () => messgae.foo.bar.age, (newVal, oldVal) => { console.log(newVal, oldVal) }, { // reactive 默认开启深度监听 deep: true, immediate:true } )
-
flush
控制 watch 的执行顺序
-
"pre"
组件更新之前调用
-
"sync"
同步执行
-
"post"
组件更新之后调用
-
7.2watchEffect
非惰性的监听,立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
如果用到一个,就只会监听那一个,就是用到几个监听几个,而且是非惰性,会默认调用一次
-
示例
<template> <input type="text" v-model="message" /> <input type="text" v-model="message2" /> </template> <script setup lang="ts"> import { ref, watchEffect } from 'vue' let message = ref<string>('Vue3') let message2 = ref<string>('Vue2') watchEffect(() => { // 用到了 message,使用只监听 message console.log('message===>', message.value) }) </script>
-
oninvalidate
在执行侦听前进行执行,首次加载不会执行
watchEffect((oninvalidate) => { console.log('message===>', message.value) oninvalidate(()=>{ console.log('在侦听调用前————执行'); }) })
-
停止监听
以函数形式调用 watchEffect,则会停止监听,点击执行停止监听后,会执行一次oninvalidate
<template> <input type="text" v-model="message" /> <input type="text" v-model="message2" /> <hr /> <button @click="stopWatch">停止监听</button> </template> <script setup lang="ts"> import { ref, watchEffect } from 'vue' let message = ref<string>('Vue3') let message2 = ref<string>('Vue2') const stop = watchEffect((oninvalidate) => { console.log('message===>', message.value) oninvalidate(() => { console.log('在侦听调用前————执行') }) }) const stopWatch = () => stop() </script>
-
flush
-
使用在需要获取元素的情况下
<template> <input type="text" v-model="message2" id="ipt" /> </template> <script setup lang="ts"> import { ref, watchEffect } from 'vue' const stop = watchEffect((oninvalidate) => { let ipt:HTMLInputElement = document.querySelector('#ipt') as HTMLInputElement console.log(ipt,'input'); },{ flush:'post' }) const stopWatch = () => stop() </script>
-
-
onTrigger
用于调试打断点用
const stop = watchEffect( (oninvalidate) => { console.log('message===>', message.value) oninvalidate(() => { console.log('在侦听调用前————执行') }) }, { flush: 'post', onTrigger(e) { debugger } } )
三.组件
1.生命周期
**注意:**setup 语法糖模式下没有 beforeCreate created 这两个生命周期的
<script setup lang="ts">
import {
ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted
} from 'vue'
console.log('setup')
// 创建
onBeforeMount(() => {
// 此刻读取不到 DOM
console.log('创建之前====>')
})
onMounted(() => {
// 可以读取 DOM
console.log('创建完成====>')
})
// 更新的钩子
onBeforeUpdate(() => {
// 更新之前的DOM
console.log('更新组件之前====>')
})
onUpdated(() => {
// 更新之后的DOM
console.log('更新组件完成====>')
})
// 销毁组件
onBeforeUnmount(()=>{
console.log('销毁组件之前====>');
})
onUnmounted(()=>{
console.log('销毁组件之后====>');
})
// 调试用
onRenderTracked((e) => {
console.log(e)
})
onRenderTriggered((e) => {
console.log(e)
})
</script>
2. less以及scoped
Less (Leaner Style Sheets 的缩写) 是一门向后兼容的 CSS 扩展语言。这里呈现的是 Less 的官方文档(中文版),包含了 Less 语言以及利用 JavaScript 开发的用于将 Less 样式转换成 CSS 样式的 Less.js 工具。
npm i less less-loader -D
即可安装
<style lang="less">
</style>
scoped 用于实现组件的私有化,将 style 属性只属于当前模块,避免了全局样式代来的问题
<style scoped></style>
3.父子组件传参
3.1.父传子
使用 defineProps ,是无须引入的直接使用即可
父组件:
字符串类型是不需要 v-bind ,非字符串必须要
<template>
<div>
<div><Menu :title="name"></Menu></div>
</div>
</template>
<script setup lang="ts">
import Menu from '@/layout/Menu/index.vue'
let name = 'vue3'
</script>
<style scoped></style>
子组件:
注意在
<template>
中可以直接使用,在<script>
中,需要接受为一个对象方可使用
<template>
<div>菜单区域</div>
<div>值:{{ title }}</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
// 接受父组件传过来的值
const props = defineProps({
title: {
type: String,
default: '默认值'
}
})
console.log(props.title);
</script>
<style scoped></style>
- ts 的方式去定义
<script setup lang="ts">
import { ref, reactive } from 'vue'
const props = defineProps<{
title: string,
arr:number[]
}>()
console.log(props.title)
</script>
// ts 特有的方式
const props = withDefaults(
defineProps<{
title: string
arr: number[]
}>(),
{
// 默认值
// 复杂数据类型需要用函数返回
arr: () => [1, 2, 3],
// 简单数据类型不用
title: 'Vue2'
}
)
3.2.子传父
传递值:
使用 defineEmits
子组件:
<template>
<div>菜单区域</div>
<button @click="send">给父组件传值</button>
<hr />
</template>
<script setup lang="ts">
// 对父组件传值
const emit = defineEmits(['on-click','on-input'])
const send = () => {
emit('on-click', 'Vue2')
}
</script>
<style scoped></style>
-
使用 ts
<script setup lang="ts"> // 使用 ts const emit = defineEmits<{ (e: 'on-click', name: string): void (e: 'on-input', name: string): void }>() // vue 3.3 const emit = defineEmits<{ on-click: [name: string] }>() const send = () => { emit('on-click', 'Vue2') } </script>
父组件:
<template>
<div>
<div><Menu :title="name" @on-click="getName"></Menu></div>
</div>
</template>
<script setup lang="ts">
import Menu from './Menu/index.vue'
let name = 'vue3'
const getName = (name: string) => {
console.log(name)
}
</script>
<style scoped></style>
传递方法:
使用 defineExpose
子组件:
<template>
<div>菜单区域</div>
</template>
<script setup lang="ts">
defineExpose({
name: '张三',
open:()=> console.log('暴露方法')
})
</script>
父组件:
<template>
<div>
<div><Menu ref="waterFall" :title="name"></Menu></div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import Menu from './Menu/index.vue'
const waterFall = ref<InstanceType<typeof Menu>>()
// 使用值不可直接使用,需要用个函数放进去执行赋值操作,比如在 生命周期 为 onMounted 时
waterFall.value?.name
waterFall.value?.open()
</script>
<style scoped></style>
4.组件分类
4.1.全局组件
当出现频率比较高的一个业务组件,可以封装成全局组件
main.ts
:
import { createApp } from 'vue'
import './style.css'
import './assets/css/reset.less'
import App from './App.vue'
// 导入组件
import CardVue from './components/Card.vue'
export const app = createApp(App)
// 注册为全局组件
app.component('Card',CardVue)
app.mount('#app')
如果需要注册的组件很多,可以如下所示进行遍历注册:
4.2.局部组件
页面上模块上很多时,可以拆分成多个组件进行引用
<template>
<div>
<CardVue></CardVue>
</div>
</template>
<script setup lang="ts">
// 导入组件
import CardVue from './components/Card.vue'
</script>
<style lang="less"></style>
4.3.递归组件
进行递归复用的组件
App.vue
:
<template>
<div>
<TreeVue :data="data"></TreeVue>
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import TreeVue from './components/Tree.vue'
interface Tree {
name: string
checked: boolean
children?: Tree[]
}
const data = reactive<Tree[]>([
{
name: '1',
checked: false,
children: [{ name: '1-1', checked: false }]
},
{
name: '2',
checked: false
},
{
name: '3',
checked: false,
children: [
{
name: '3-1',
checked: false,
children: [
{ name: '3-1-1', checked: false },
{ name: '3-1-2', checked: false }
]
}
]
}
])
</script>
<style lang="less"></style>
Tree.vue
:
- 确定递归组件的名称,Vue3里可以使用文件名(直接使用)
<template>
<div class="tree" v-for="item in data">
<input v-model="item.checked" type="checkbox" /><span>{{ item.name }}</span>
<Tree v-if="item?.children?.length" :data="item.children"></Tree>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
interface Tree {
name: string
checked: boolean
children?: Tree[]
}
const props = defineProps<{ data?: Tree[] }>()
</script>
<style scoped>
.tree {
margin-left: 10px;
}
</style>
- 也可重命名组件名称,须另起一个
<script>
,有些繁琐
<template>
<div class="tree" v-for="item in data">
<input v-model="item.checked" type="checkbox" /><span>{{ item.name }}</span>
<Tree2 v-if="item?.children?.length" :data="item.children"></Tree2>
</div>
</template>
// 略.........
<script lang="ts">
export default {
name:'Tree2'
}
</script>
-
安装插件
npm i unplugin-vue-define-options@0.12.7
vite.config.ts
:import { defineConfig } from 'vite' import DefineOptions from 'unplugin-vue-define-options/vite' import Vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [Vue(), DefineOptions()], })
tsconfig.node.json
:{ "compilerOptions": { "types": ["unplugin-vue-define-options/macros-global"], } }
<template>
<div class="tree" v-for="item in data">
<input v-model="item.checked" type="checkbox" /><span>{{ item.name }}</span>
<Tree2 v-if="item?.children?.length" :data="item.children"></Tree2>
</div>
</template>
<script setup lang="ts">
// 略.....
// 使用插件定义名称
defineOptions({
name: 'Tree2'
})
defineProps<{ data?: Tree[] }>()
</script>
<style scoped>
.tree {
margin-left: 10px;
}
</style>
5.动态组件
让多个组件使用同一个挂载点,并动态切换,这就是动态组件。有点类似路由的功能。
注意事项 :
-
在Vue2 的时候 is 是通过组件名称切换的 在Vue3 setup 是通过组件实例切换的
-
如果你把组件实例放到Reactive Vue会给你一个警告:runtime-core.esm-bundler.js:38 [Vue warn]: Vue received a Component which was made a reactive object. This can lead to unnecessary performance overhead, and should be avoided by marking the component with
markRaw
or usingshallowRef
instead ofref
.
Component that was made reactive:**原因:**这是因为reactive 会进行proxy 代理 而我们组件代理之后毫无用处 节省性能开销 推荐我们使用shallowRef 或者 markRaw 跳过proxy 代理。
Vue3风格:
-
使用
<component>
动态切换<template> <div></div> <component :is="comId"></component> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' import AVue from './xxxxx' import BVue from './xxxxx' import CVue from './xxxxx' const data = reactive([ { name: 'A组件', com: AVue }, { name: 'B组件', com: BVue }, { name: 'C组件', com: CVue } ]) const comId = ref(AVue) </script> <style scoped></style>
-
优化报错:
使用 markRaw,shallowRef 优化
<script setup lang="ts"> import { ref, reactive, markRaw,shallowRef } from 'vue' import AVue from './xxxxx' import BVue from './xxxxx' import CVue from './xxxxx' const data = reactive([ { name: 'A组件', com: markRaw(AVue) }, { name: 'B组件', com: markRaw(BVue) }, { name: 'C组件', com: markRaw(CVue) } ]) const comId = shallowRef(AVue) </script>
Vue2风格:
-
字符串格式
<template> <div></div> <component :is="comId"></component> </template> <script setup lang="ts"> import { ref, reactive, shallowRef } from 'vue' const data = reactive([ { name: 'A组件', com: 'AVue' }, { name: 'B组件', com: 'BVue' }, { name: 'C组件', com: 'CVue' } ]) const comId = shallowRef('AVue') </script> <script lang="ts"> import AVue from './xxxxx' import BVue from './xxxxx' import CVue from './xxxxx' export default { components: { AVue, BVue, CVue } } </script> <style scoped></style>
6.插槽
插槽就是子组件中的提供给父组件使用的一个占位符,用
<slot></slot>
表示,父组件可以在这个占位符中填充任何模板代码,如 HTML、组件等,填充的内容会替换子组件的<slot></slot>
标签。
<script lant="ts" setup>
defineSlots<{
default?: (props: {msg: string}) => any
item?: (props: {id: number}) => any
}>
</script>
-
匿名插槽
-
子组件
Dialog.vue
<template> <div> <header></header> <main class="main"> <slot></slot> </main> <footer></footer> </div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' </script> <style scoped></style>
-
父组件
index.vue
<template> <div> <Dialog> <template v-solt> <div>该内容被插入到子组件中</div> </template> </Dialog> </div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' import Dialog from './xxxxxx' </script> <style scoped></style>
-
-
具名插槽
-
子组件
Dialog.vue
<template> <div> <header> <slot name="header"></slot> </header> <main> <slot name="main"></slot> </main> <footer> <slot name="footer"></slot> </footer> </div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' </script> <style scoped></style>
-
父组件
index.vue
<template> <div> <Dialog> <!-- 简写:#header --> <template v-solt:header> <div>该内容被插入到 子组件 header 中</div> </template> <template v-solt:main> <div>该内容被插入到 子组件 main 中</div> </template> <template v-solt:footer> <div>该内容被插入到 子组件 footer 中</div> </template> </Dialog> </div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' import Dialog from './xxxxxx' </script> <style scoped></style>
-
-
作用域插槽
-
子组件
<template> <div> <main> <div v-for="(item,index) in data"> <!-- 自定义属性data --> <slot :data="item" :index="index"></slot> </div> </main> </div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' type names = { name: string age: number } const data = reactive<names[]>([ { name: '张三', age: 13 }, { name: '张四', age: 14 }, { name: '张五', age: 15 }, { name: '张六', age: 16 } ]) </script> <style scoped></style>
-
父组件
<template> <div> <Dialog> <!-- 简写:#default="{ data, index }" --> <template v-solt="{ data, index }"> <div>{{data.name}}——{{index}}</div> </template> </Dialog> </div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' import Dialog from './xxxxxx' </script> <style scoped></style>
-
-
动态插槽
-
父组件
<template> <div> <Dialog> <template #[name]></template> </Dialog> </div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' import Dialog from './xxxxxx' let name = ref('footer') </script> <style scoped></style>
-
7.异步组件、代码分包、suspense
-
引入异步组件
使用
defineAsyncComponent
引用<template> <Suspense> <!-- 所要展示的这个组件 --> <template #default> <SyncVue></SyncVue> </template> <!-- 在加载过程中展示的,如骨架屏 --> <template #fallback> <skeletonVue></skeletonVue> </template> </Suspense> </template> <script setup lang="ts"> import { ref, reactive, defineAsyncComponent } from 'vue' import skeletonVue from './xxxxx' // 书写风格1 const SyncVue = defineAsyncComponent(() => import('@/xxxx')) // 书写风格2 const SyncVue = defineAsyncComponent({ // 要展示的一个组件 loadingComponent: () => import('@/xxxx'), // 失败时展示的组件 errorComponent:() => import('@/xxxx'), // 超时展示的组件 timeout:() => import('@/xxxx') }) </script> <style scoped></style>
8.Teleport 传送组件
Vue 3.0 新特性:Teleport 是一种能够将我们的模板渲染至指定DOM节点,不受父级style、v-show等属性影响(比如定位问题),但data、prop数据依旧能够共用的技术;类似于 React 的 Portal。
主要解决的问题:因为 Teleport 节点挂载在其他指定的DOM节点下,完全不受父级 style 样式影响。
示例:
<TEleport>
to=""
属性(可以接受任何的CSS选择器),指的是将该标签内的DOM结构传送到to
属性所选择的元素内。:disabled:""
属性(接受 true 和 false),true 则to=""
属性 不起作用,false 则相反,所以可以通过该属性动态的切换来控制里面的结构是否要传送走。
<template>
<div>
<!-- 将 A 传送到 body 节点下 -->
<Teleport to="body">
<A></A>
</Teleport>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import A from './xxxx'
</script>
<style scoped></style>
9.keep-alive 缓存组件
keep-alive组件:有时候,我们不希望组件被重新渲染影响使用体验;或者处于性能考虑,避免多次重复渲染降低性能。而是希望组件可以缓存下来,维持当前的状态。
生命周期的变化:
-
初次进入时:
onMounted
——onActivated
-
退出后触发:
deactivated
-
再次进入只会触发:
onActivated
<script setup lang="ts"> import { log } from 'console'; import { onMounted,onActivated,onDeactivated,onUnmounted } from 'vue' onMounted(()=>{ console.log('初始化'); }) onActivated(()=>{ console.log('keep-alive初始化'); }) onDeactivated(()=>{ console.log('keep-alive卸载'); }) onUnmounted(()=>{ console.log('卸载'); }) </script>
注意: 事件挂载的方法等,只执行一次的放在 onMounted
中;组件每次进去执行的方法放在 onActivated
中
-
基础使用
<template> <KeepAlive> <A v-if="flag"></A> <B v-else></B> </KeepAlive> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' import A from './xxx' import B from './xxx' const flag = ref<boolean>(true) </script> <style scoped></style>
-
:include=""
属性(值可以为字符串、数组、正则表达式),指定哪些组件缓存<template> <!-- 缓存A 'A'组件的name设置为‘A’--> <KeepAlive :include="['A']"> <A v-if="flag"></A> <B v-else></B> </KeepAlive> </template>
-
:exclude=""
属性(值可以为字符串、数组、正则表达式),指定哪些组件不缓存<template> <!-- 不缓存 A --> <KeepAlive :include="['A']"> <A v-if="flag"></A> <B v-else></B> </KeepAlive> </template>
-
:mac=""
属性(值为数值),指定缓存多少个组件,通过算法剔除掉不常用的<template> <KeepAlive :max="2"> <A v-if="flag"></A> <B v-else-if="flags"></B> <C v-else></C> </KeepAlive> </template>
10.transition 动画组件
Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡:
- 条件渲染 (使用 v-if)
- 条件展示 (使用 v-show)
- 动态组件
- 组件根节点
10.1.基本使用
<Transition>
组件的属性:
-
:duration=""
-
动画的时常(单位毫秒)
-
<Transition :duration="500" enter-active-class="animate__animated animate__backInUp" leave-active-class="animate__animated animate__fadeIn" > <div v-if="flag" class="box"></div> </Transition> <Transition :duration="{enter:50,leave:500}" enter-active-class="animate__animated animate__backInUp" leave-active-class="animate__animated animate__fadeIn" >
-
``
<Transition>
组件的使用:
-
方法一:
-
name
属性定义 -
定义过渡的 class ,注意安装标准写样式表
-
v-enter-from
:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。 -
v-enter-active
:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。 -
v-enter-to
:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。 -
v-leave-from
:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。 -
v-leave-active
:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。 -
v-leave-to
:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被移除),在过渡/动画完成之后移除。
-
-
-
方法二:
-
特点:可以结合第三方class库去使用,如 Animate.css
-
定义
<Transition>
如下属性,属性值为 class 类名enter-from-class
enter-active-class
enter-to-class
leave-from-class
leave-active-class
leave-to-class
-
示例——1:
<template>
<div>
<button @click="flag = !flag">switch</button>
<Transition name="fade"><div v-if="flag" class="box"></div></Transition>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
const flag = ref<boolean>(false)
</script>
<style scoped>
.box {
background-color: red;
width: 200px;
height: 200px;
}
/* 开始过度 */
.fade-enter-from {
width: 0px;
height: 0px;
}
/* 开始过度了 */
.fade-enter-active {
transition: all 2.5s linear;
}
/* 过度完成 */
.fade-enter-to {
background: yellow;
width: 200px;
height: 200px;
}
/* 离开的过度 */
.fade-leave-from {
width: 200px;
height: 200px;
}
/* 离开中过度 */
.fade-leave-active {
transition: all 1s linear;
}
/* 离开完成 */
.fade-leave-to {
width: 0px;
height: 0px;
}
</style>
示例——2:
<template>
<div>
<button @click="flag = !flag">switch</button>
<Transition
enter-from-class="e-from"
enter-to-class="e-to"
enter-active-class="e-active"
name="fade"
>
<div v-if="flag" class="box"></div>
</Transition>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
const flag = ref<boolean>(false)
</script>
<style scoped>
.box {
background-color: red;
width: 200px;
height: 200px;
}
/* 开始过度 */
.e-from {
width: 0px;
height: 0px;
}
/* 开始过度了 */
.e-active {
transition: all 2.5s linear;
}
/* 过度完成 */
.e-to {
background: yellow;
width: 200px;
height: 200px;
}
/* 离开的过度 */
.fade-leave-from {
width: 200px;
height: 200px;
}
/* 离开中过度 */
.fade-leave-active {
transition: all 1s linear;
}
/* 离开完成 */
.fade-leave-to {
width: 0px;
height: 0px;
}
</style>
10.2.Animate.cc 动画库
-
安装
npm install animate.css
-
引入,在用到的
.vue
文件里引入<script setup lang="ts"> import { ref, reactive } from 'vue' import 'animate.css' const flag = ref<boolean>(false) </script>
-
使用
animate.css 版本为 3
<template> <div> <button @click="flag = !flag">switch</button> <Transition enter-active-class="animate_fadeIn" leave-active-class="animate__backInUp" > <div v-if="flag" class="box"></div> </Transition> </div> </template>
animate.css 版本为 4,类名前加上前缀
animate__animated
<template> <div> <button @click="flag = !flag">switch</button> <Transition enter-active-class="animate__animated animate__fadeIn" leave-active-class="animate__animated animate__backInUp" > <div v-if="flag" class="box"></div> </Transition> </div> </template>
10.3.transition 生命周期
但某些情况CSS无法满足时,需要JS来计算时,便有了此方案
共有8个:
@before-enter="beforeEnter"
——对应enter-from@enter="enter"
——对应enter-active@after-enter="afterEnter"
——对应enter-to@enter-cancelled="enterCancelled"
——显示过度打断@before-leave="beforeLeave"
——对应leave-from@leave="leave"
——对应leave-active@after-leave="afterLeave"
——对应leave-to@leave-cancelled="leaveCancelled"
——离开过度打断
示例:
<template>
<div>
<button @click="flag = !flag">switch</button>
<Transition
@before-enter="EnterFrom"
@enter="EnterAction"
@after-enter="EnterTo"
@enter-cancelled="EnterCancel"
@before-leave="LeaveFrom"
@leave="LeaveAction"
@after-leave="LeaveTo"
@leave-cancelled="LeaveCancel"
enter-active-class="animate__animated animate__backInUp"
leave-active-class="animate__animated animate__fadeIn"
>
<div v-if="flag" class="box"></div>
</Transition>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import 'animate.css'
const EnterFrom = (el: Element) => {
console.log('进入过渡之前')
}
const EnterAction = (el: Element, done: Function) => {
console.log('进入过渡曲线')
setTimeout(() => {
// 3s 进入 EnterTo 打印结果
// 在这期间内点击切换状态就会执行 EnterCancel
done()
}, 3000)
}
const EnterTo = (el: Element) => {
console.log('进入过渡完成')
}
// 执行的过渡被打断时执行
const EnterCancel = (el: Element) => {
console.log('进入过渡效果被打断')
}
const LeaveFrom = () => {
console.log('离开之前')
}
const LeaveAction = () => {
console.log('离开过渡曲线')
}
const LeaveTo = () => {
console.log('离开完成')
}
// 因为使用的 v-if 所以 LeaveCancel 没有被触发
const LeaveCancel = () => {
console.log('离开过渡曲线被打断')
}
const flag = ref<boolean>(false)
</script>
<style scoped>
.box {
background-color: red;
width: 200px;
height: 200px;
}
</style>
注意:因为使用的 v-if 所以 LeaveCancel 没有被触发
10.4.GreenSock 动画库
-
安装
npm i gsap -S
-
引入,在用到的
.vue
文件里引入<script setup lang="ts"> import { ref, reactive } from 'vue' import gsap from 'gsap' </script> <style scoped>
-
使用
<template> <div> <button @click="flag = !flag">switch</button> <Transition @before-enter="EnterFrom" @enter="EnterAction" @leave="LeaveAction" > <div v-show="flag" class="box"></div> </Transition> </div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' import gsap from 'gsap' const EnterFrom = (el: Element) => { gsap.set(el, { width: 0, height: 0 }) } const EnterAction = (el: Element, done: gsap.Callback) => { gsap.to(el, { width: 200, height: 200, onComplete: done }) } const LeaveAction = (el: Element, done: gsap.Callback) => { gsap.to(el, { width: 0, height: 0, onComplete: done }) } const flag = ref<boolean>(false) </script> <style scoped> .box { background-color: red; width: 200px; height: 200px; } </style>
10.5.appear
通过这个属性可以设置初始节点过度 就是页面加载完成就开始动画 对应三个状态,值为类名
<template>
<div>
<button @click="flag = !flag">switch</button>
<Transition
appear
appear-from-class="from"
appear-active-class="active"
appear-to-class="to"
>
<div v-show="flag" class="box"></div>
</Transition>
</div>
</template>
11.transitionGroup
11.1.基本使用
在渲染整个列表时,一般会使用
<transition-group>
组件
<template>
<div>
<TransitionGroup>
<!-- 此处要求必须要有key -->
<div v-fro="(item,index) in lists" :key="index"></div>
</TransitionGroup>
</div>
</template>
-
tag=""
属性(值为HTML标签名),作用是在渲染时包上一层该标签,默认情况是不会<TransitionGroup tag="div"> <!-- 此处要求必须要有key --> <div v-fro="(item,index) in lists" :key="index"></div> </TransitionGroup>
对应的属性和 transition 组件的一模一样,乃至生命周期都一样。
注意:CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身。
11.2.列表的移动过渡
<transition-group>
组件还有一个特殊之处。除了进入和离开,它还可以为定位的改变添加动画。只需了解新增的 v-move 类就可以使用这个新功能,它会应用在元素改变定位的过程中。像之前的类名一样,它的前缀可以通过 name attribute 来自定义,也可以通过 move-class attribute 手动设置
<template>
<div>
<button @click="shuffle">Shuffle</button>
<transition-group class="wraps" name="mmm" tag="ul">
<li class="cell" v-for="item in items" :key="item.id">
{{ item.number }}
</li>
</transition-group>
</div>
</template>
<script setup lang="ts">
import _ from 'lodash'
import { ref } from 'vue'
let items = ref(
Array.apply(null, { length: 81 } as number[]).map((_, index) => {
return {
id: index,
number: (index % 9) + 1
}
})
)
const shuffle = () => {
items.value = _.shuffle(items.value)
}
</script>
<style scoped lang="less">
.wraps {
display: flex;
flex-wrap: wrap;
width: calc(25px * 10 + 9px);
.cell {
width: 25px;
height: 25px;
border: 1px solid #ccc;
list-style-type: none;
display: flex;
justify-content: center;
align-items: center;
}
}
.mmm-move {
transition: transform 0.8s ease;
}
</style>
11.3.状态过渡
Vue 也同样可以给数字 Svg 背景颜色等添加过度动画
<template>
<div>
<input step="20" v-model="num.current" type="number" />
<div>{{ num.tweenedNumber.toFixed(0) }}</div>
</div>
</template>
<script setup lang='ts'>
import { reactive, watch } from 'vue'
import gsap from 'gsap'
const num = reactive({
tweenedNumber: 0,
current:0
})
watch(()=>num.current, (newVal) => {
gsap.to(num, {
duration: 1,
tweenedNumber: newVal
})
})
</script>
<style>
</style>
12.Provide/Inject
特点:provide 可以在祖先组件中指定我们想要提供给后代组件的数据或方法,而在任何后代组件中,我们都可以使用 inject 来接收 provide 提供的数据或方法。
解决问题:通常,当我们需要从父组件向子组件传递数据时,我们使用 props。想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。
父组件:
<template>
<div class="App">
<button>我是App</button>
<A></A>
</div>
</template>
<script setup lang='ts'>
import { provide, ref } from 'vue'
import A from './components/A.vue'
const color = '#fff'
let flag = ref<number>(1)
provide('flag', flag)
</script>
<style>
.App {
background: blue;
/* Vue3 新增 */
color: v-bind(color);
}
</style>
子组件:
组件修改值后,会传递到父组件,如果不想让子组件修改,就需要使用
readonly
<template>
<div style="background-color: green;">
我是B
<button @click="change">change falg</button>
<div>{{ flag }}</div>
</div>
</template>
<script setup lang='ts'>
import { inject, Ref, ref } from 'vue'
const flag = inject<Ref<number>>('flag', ref(1))
const change = () => {
flag.value = 2
}
</script>
<style>
</style>
13.兄弟组件传参和Bus
借助父组件传参:
app.vue
:
<template>
<div>
<A @on-click="getFalg"></A>
<B :flag="Flag"></B>
</div>
</template>
<script setup lang='ts'>
import A from './components/A.vue'
import B from './components/B.vue'
import { ref } from 'vue'
let Flag = ref<boolean>(false)
const getFalg = (flag: boolean) => {
Flag.value = flag;
}
</script>
<style>
</style>
A.vue
:
<template>
<div>
<button @click="send">传参</button>
</div>
</template>
<script setup lang="ts">
let flag = true
const emit = defineEmits(['on-click'])
const send = () => {
emit('on-click',flag)
}
</script>
<style></style>
B.vue
:
<template>
<div>
{{ flag }}
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
flag: boolean
}>()
</script>
<style></style>
使用Event Bus:
原理:发布订阅模式,不过在 vue3 中 off $once 实例方法已被移除,组件实例不再实现事件触发接口。
简单实现发布订阅模式:
type BusClass<T> = {
emit: (name: T) => void
on: (name: T, callback: Function) => void
}
type BusParams = string | number | symbol
type List = {
[key: BusParams]: Array<Function>
}
class Bus<T extends BusParams> implements BusClass<T> {
list: List
constructor() {
this.list = {}
}
emit(name: T, ...args: Array<any>) {
let eventName: Array<Function> = this.list[name]
eventName.forEach(ev => {
ev.apply(this, args)
})
}
on(name: T, callback: Function) {
let fn: Array<Function> = this.list[name] || [];
fn.push(callback)
this.list[name] = fn
}
}
export default new Bus<number>()
使用 Mitt:
-
安装
npm i mitt -S
-
main.ts
挂载import { createApp } from 'vue' export const app = createApp(App) import App from './App.vue' import mitt from 'mitt' const Mit = mitt() app.config.globalProperties.$Bus = Mit declare module 'vue' { export interface ComponentCustomProperties { $Bus:typeof Mit } } app.mount('#app')
-
兄弟组件使用
派发组件:
<template> <div> <button @click="emit">emit</button> </div> </template> <script setup lang="ts"> import { getCurrentInstance } from 'vue' const instance = getCurrentInstance() const mitt = 'mitt' const emit = () => { instance?.proxy?.$Bus.emit('on-click', mitt) } </script> <style scoped></style>
接收组件:
监听
<script setup lang="ts"> import { getCurrentInstance } from 'vue' const instance = getCurrentInstance() // * 代表监听所有事件 instance?.proxy?.$Bus.on('on-click',(str)=>{ console.log(str); }) instance?.proxy?.$Bus.on('*', (type,str) => { console.log('事件名称==>',type) console.log('参数==>',str) }) </script>
删除
<script setup lang="ts"> // 略...... instance?.proxy?.$Bus.off('on-click') // 删除全部事件 instance?.proxy?.$Bus.all.clear() </script>
14.了解 ElementUI、AntDesigin 等组件库
Element UI Plus:
官网:https://element-plus.gitee.io/zh-CN/
- ts + setup 语法糖模式
- 对 VSCode 的 Volar 有支持
-
安装
npm i element-plus --save
-
引入
main.ts
import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import App from './App.vue' const app = createApp(App) app.use(ElementPlus) app.mount('#app')
-
对 VSCode volar 插件的支持
tsconfig.json
{ "compilerOptions": { // ... "types": ["element-plus/global"] } }
Ant Design Vue:
官网:https://www.antdv.com/docs/vue/introduce-cn
ts + setup 函数模式
非常详细的 demo
-
安装
npm i ant-design-vue@next --save
-
引入
main.ts
import { createApp } from 'vue'; import Antd from 'ant-design-vue'; import App from './App'; import 'ant-design-vue/dist/antd.css'; const app = createApp(App); app.use(Antd).mount('#app');
lview:
vue2 写法
-
安装
npm i view-ui-plus --save
-
引入
main.ts
import { createApp } from 'vue' import ViewUIPlus from 'view-ui-plus' import App from './App.vue' import router from './router' import store from './store' import 'view-ui-plus/dist/styles/viewuiplus.css' const app = createApp(App) app.use(store) .use(router) .use(ViewUIPlus) .mount('#app')
Vant 移动端:
官网:https://vant-contrib.gitee.io/vant/#/zh-CN/home
setup 函数模式
业务组件
-
安装
npm i vant -S
-
引入
main.ts
import Vant from 'vant' import 'vant/lib/index.css'; createApp(App).use(vant).$mount('#app)
14.Scoped和样式穿透
主要是用于修改很多vue常用的组件库(element, vant, AntDesigin),虽然配好了样式,但是还是需要更改其他的样式 就需要用到样式穿透
scoped的原理:
- vue中的 scoped 通过在DOM结构以及css样式上加唯一不重复的标记:data-v-hash的方式,以保证唯一(而这个工作是由过PostCSS转译实现的),达到样式私有化模块化的目的。
- PostCSS 会给一个组件中的所有dom添加了一个独一无二的动态属性data-v-xxxx,然后,给CSS选择器额外添加一个对应的属性选择器来选择该组件中dom,这种做法使得样式只作用于含有该属性的dom——组件内部dom, 从而达到了'样式模块化'的效果.
Scoped 渲染规则:
- 给HTML的DOM节点加一个不重复data属性(形如:data-v-123)来表示他的唯一性
- 在每句css选择器的末尾(编译后的生成的css语句)加一个当前组件的data属性选择器(如[data-v-123])来私有化样式
- 如果组件内部包含有其他组件,只会给其他组件的最外层标签加上当前组件的data属性
案例:
修改 Element ui Input
-
直接修改无效
<template> <div> <el-input class="ipt"></el-input> </div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' </script> <style scoped lang="less"> .ipt { width: 200px; .el-ipt__inner { background: red; } } </style>
-
查看样式实际上是有引入的
-
解决方案
-
Vue2
<style scoped lang="less"> .ipt { width: 200px; /deep/ .el-input__inner { background: red; } } </style>
-
Vue3
<style scoped lang="less"> .ipt { width: 200px; :deep(.el-input__inner) { background: red; } } </style>
-
15.Style 新特性
-
插槽选择器——slotted
默认情况下,作用域样式不会影响到
<slot/>
渲染出来的内容,因为它们被认为是父组件所持有并传递进来的。子组件:
<template> <div> 我是插槽 <slot></slot> </div> </template> <script> export default {} </script> <style scoped> </style>
父组件:
<template> <div> <A> <div class="a">私人定制div</div> </A> </div> </template> <script setup> import A from "@/components/A.vue" </script> <style lang="less" scoped> </style>
-
子组件 直接修改,无效
<style scoped> .a{ color:red } </style>
-
子组件 slotted
<style scoped> :slotted(.a) { color:red } </style>
-
-
全局选择器——global
在之前我们想加入全局样式 通常都是新建一个style 标签 不加scoped 现在有更优雅的解决方案
旧方案:
<style> div{ color:red } </style> <style lang="less" scoped> </style>
新方案:
<style lang="less" scoped> :global(div){ color:red } </style>
-
动态CSS
单文件组件的
<style>
标签可以通过v-bind
这一 CSS 函数将 CSS 的值关联到动态的组件状态上。示例:
<template> <div class="div"> aikun </div> </template> <script lang="ts" setup> import { ref } from 'vue' const red = ref<string>('red') </script> <style lang="less" scoped> .div{ color:v-bind(red) } </style>
如果是对象 v-bind 请加引号
<script lang="ts" setup> import { ref } from "vue" const red = ref({ color:'pink' }) red.value.color = 'blur' </script> <style lang="less" scoped> .div { color: v-bind('red.color'); } </style>
-
CSS module
使用场景一般用于 TSX 和 render 函数 居多
<style module>
标签会被编译为 CSS Modules 并且将生成的 CSS 类作为 $style 对象的键暴露给组件<template> <div :class="$style.red"> aikun </div> </template> <style module> .red { color: red; font-size: 20px; } </style>
自定义注入名称(多个可以用数组) 你可以通过给 module attribute 一个值来自定义注入的类对象的 property 键
<template> <div :class="[zs.red,zs.border]"> aikun </div> </template> <style module="zs"> .red { color: red; font-size: 20px; } .border{ border: 1px solid #ccc; } </style>
注入的类可以通过 useCssModule API 在
setup()
和<script setup>
中使用。对于使用了自定义注入名称的<style module>
模块,useCssModule
接收一个对应的module
attribute 值作为第一个参数<template> <div :class="[zs.red,zs.border]"> aikun </div> </template> <script setup lang="ts"> import { useCssModule } from 'vue' const css = useCssModule('zs') </script> <style module="zs"> .red { color: red; font-size: 20px; } .border{ border: 1px solid #ccc; } </style>
16.defineOptions
解决setup中无法为组件命名的问题
<script setup>
defineOptions({name: 'AButton', inheritAttrs: false })
</script>
四.进阶
1.深入 v-model
- 一个语法糖,props 和 emit 组合而成
- 破坏性更新
对比 Vue2:
- prop:
value
->modelValue
; - 事件:
input
->update:modelValue
; v-bind
的.sync
修饰符和组件的model
选项已移除- 新增 支持多个v-model
- 新增 支持自定义 修饰符 Modifiers
自定义修饰符:
父组件:
<template>
<button @click="show = !show">开关{{show}}</button>
<Dialog v-model.isFlag="show"></Dialog>
</template>
<script setup lang='ts'>
import Dialog from "./components/Dialog/index.vue";
import {ref} from 'vue'
const show = ref(false)
</script>
<style>
</style>
子组件:
<script setup lang='ts'>
type Props = {
modelValue: boolean,
title?: string,
// 固定写法: xxxModifiers
modelModifiers?: {
isFlag:boolean
}
titleModifiers?: {
default: () => {}
}
}
const propData = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'update:title'])
const close = () => {
console.log(propData.modelModifiers);
emit('update:modelValue', props?.modelModifiers?.isFlag ? '存在' : '不存在')
emit('update:title', '我要改变')
}
简单示例:
父组件:
<template>
<button @click="show = !show">开关{{show}}</button>
<Dialog v-model="show"></Dialog>
</template>
<script setup lang='ts'>
import Dialog from "./components/Dialog/index.vue";
import {ref} from 'vue'
const show = ref(false)
</script>
<style>
</style>
子组件:
<template>
<div v-if='propData.modelValue ' class="dialog">
<div class="dialog-header">
<div>标题</div><div @click="close">x</div>
</div>
<div class="dialog-content">
内容
</div>
</div>
</template>
<script setup lang='ts'>
type Props = {
// 默认值
modelValue:boolean
}
const propData = defineProps<Props>()
// 固定写法——'update:xxxxx'
const emit = defineEmits(['update:modelValue'])
const close = () => {
emit('update:modelValue',false)
}
</script>
<style lang='less'>
.dialog{
width: 300px;
height: 300px;
border: 1px solid #ccc;
position: fixed;
left:50%;
top:50%;
transform: translate(-50%,-50%);
&-header{
border-bottom: 1px solid #ccc;
display: flex;
justify-content: space-between;
padding: 10px;
}
&-content{
padding: 10px;
}
}
</style>
绑定多个示例:
父组件:
<template>
<button @click="show = !show">开关{{show}} ----- {{title}}</button>
<Dialog v-model:title='title' v-model="show"></Dialog>
</template>
<script setup lang='ts'>
import Dialog from "./components/Dialog/index.vue";
import {ref} from 'vue'
const show = ref(false)
const title = ref('我是标题')
</script>
<style>
</style>
子组件:
<template>
<div v-if='modelValue ' class="dialog">
<div class="dialog-header">
<div>标题---{{title}}</div><div @click="close">x</div>
</div>
<div class="dialog-content">
内容
</div>
</div>
</template>
<script setup lang='ts'>
type Props = {
modelValue:boolean,
title:string
}
const propData = defineProps<Props>()
const emit = defineEmits(['update:modelValue','update:title'])
const close = () => {
emit('update:modelValue',false)
emit('update:title','我要改变')
}
</script>
<style lang='less'>
.dialog{
width: 300px;
height: 300px;
border: 1px solid #ccc;
position: fixed;
left:50%;
top:50%;
transform: translate(-50%,-50%);
&-header{
border-bottom: 1px solid #ccc;
display: flex;
justify-content: space-between;
padding: 10px;
}
&-content{
padding: 10px;
}
}
</style>
2.自定义指令directive
Vue3 指令的钩子函数:
created
元素初始化的时候beforeMount
指令绑定到元素后调用 只调用一次mounted
元素插入父级 dom 调用beforeUpdate
元素被更新之前调用update
这个周期方法被移除 改用updated
beforeUnmount
在元素被移除前调用unmounted
指令被移除后调用 只调用一次
Vue2 指令 bind inserted update componentUpdated unbind
示例:
必须以
vNameOfDirective
的形式来命名本地自定义指令,以使得它们可以直接在模板中使用。
<template>
<button @click="show = !show">开关{{show}} ----- {{title}}</button>
<Dialog v-move-directive="{background:'green',flag:show}"></Dialog>
</template>
type Value = {
background:String
}
const vMoveDirective: Directive = {
created: () => {
console.log("初始化====>");
},
beforeMount(...args: Array<any>) {
// 在元素上做些操作
console.log("初始化一次=======>");
},
mounted(el: any, dir: DirectiveBinding<Value>) {
el.style.background = dir.value.background;
console.log("初始化========>");
},
// 数据更新
beforeUpdate() {
console.log("更新之前");
},
updated() {
console.log("更新结束");
},
beforeUnmount(...args: Array<any>) {
console.log(args);
console.log("======>卸载之前");
},
unmounted(...args: Array<any>) {
console.log(args);
console.log("======>卸载完成");
},
};
生命周期钩子参数详解:
-
参1——
el
- 当前绑定的 DOM 元素
-
参2——
binding
对象-
instance:使用指令的组件实例。
-
value:传递给指令的值。例如,在 v-my-directive="1 + 1" 中,该值为 2。
-
oldValue:先前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否有更改都可用。
-
arg:传递给指令的参数(如果有的话)。例如在 v-my-directive:foo 中,arg 为 "foo"。
-
modifiers:包含修饰符(如果有的话) 的对象。例如在 v-my-directive.foo.bar 中,修饰符对象为 {foo: true,bar: true}。
-
dir:一个对象,在注册指令时作为参数传递。例如,在以下指令中
-
-
参3——
Vnode
- 当前元素的虚拟 DOM,也就是 Vnode
-
参4——
- prevNode 上一个虚拟节点,仅在
beforeUpdate
和updated
钩子中可用
- prevNode 上一个虚拟节点,仅在
简写示例:
你可能想在
mounted
和updated
时触发相同行为,而不关心其他的钩子函数。那么你可以通过将这个函数模式实现
<template>
<div>
<input v-model="value" type="text" />
<A v-move="{ background: value }"></A>
</div>
</template>
<script setup lang='ts'>
import A from './components/A.vue'
import { ref, Directive, DirectiveBinding } from 'vue'
let value = ref<string>('')
type Dir = {
background: string
}
const vMove: Directive = (el, binding: DirectiveBinding<Dir>) => {
el.style.background = binding.value.background
}
</script>
<style>
</style>
3.自定义Hooks
主要用来处理复用代码逻辑的一些封装 ,这个在vue2 就已经有一个东西是Mixins ,Mixins就是将这些多个相同的逻辑抽离出来,各个组件只需要引入mixins,就能实现一次写代码,多组件受益的效果。
Mixins弊端:
就是会涉及到覆盖的问题,组件的data、methods、filters 会覆盖 mixins里的同名data、methods、filters。
变量来源不明确(隐式传入),不利于阅读,使代码变得难以维护。
Vue3 的自定义的hook:
- Vue3 的 hook函数 相当于 vue2 的 mixin, 不同在与 hooks 是函数。
- Vue3 的 hook函数 可以帮助我们提高代码的复用性, 让我们能在不同的组件中都利用 hooks 函数
**Vue3 hook 库:**https://vueuse.org/guide/
案例:
官方自带的一些hook
<script setup lang="ts">
import { useAttrs, useSlots } from 'vue'
let attr = useAttrs()
// 获取到 父组件 给该子组件 自定义的属性
console.log(attr)
</script>
转 base64 的 hook
import { onMounted } from 'vue'
type Options = {
el: string
}
type Return = {
Baseurl: string | null
}
export default function (option: Options): Promise<Return> {
return new Promise((resolve) => {
onMounted(() => {
const file: HTMLImageElement = document.querySelector(option.el) as HTMLImageElement;
file.onload = ():void => {
resolve({
Baseurl: toBase64(file)
})
}
})
const toBase64 = (el: HTMLImageElement): string => {
const canvas: HTMLCanvasElement = document.createElement('canvas')
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
canvas.width = el.width
canvas.height = el.height
ctx.drawImage(el, 0, 0, canvas.width,canvas.height)
console.log(el.width);
return canvas.toDataURL('image/png')
}
})
}
4.全局函数和变量
由于Vue3 没有 Prototype 属性 使用 app.config.globalProperties 代替然后去定义变量和函数
Vue2:
// 之前 (Vue 2.x)
Vue.prototype.$http = () => {}
Vue3:
mian.ts
// 之后 (Vue 3.x)
const app = createApp({})
app.config.globalProperties.$http = () => {}
案例:
Vue3 中移除了过滤器,正好,我们可以使用全局函数代替 Filters。
// mian.ts
// 定义全局变量
app.config.globalProperties.$env = "dev"
// 定义全局函数
app.config.globalProperties.$filters = {
format<T extends any>(str: T): string {
return `$${str}`
}
}
在xxx.vue
中使用
<template>
<div>{{ $env }}</div>
<div>{{ $filters.format('Vue3') }}</div>
</template>
<script setup lang="ts">
import { getCurrentInstance, ComponentInternalInstance } from 'vue';
const { appContext } = <ComponentInternalInstance>getCurrentInstance()
console.log(appContext.config.globalProperties.$env);
import {ref,reactive,getCurrentInstance} from 'vue'
const app = getCurrentInstance()
console.log(app?.proxy?.$filters.format('js'))
</script>
<style scoped></style>
5.编写Vue3插件
插件是自包含的代码,通常向 Vue 添加全局级功能。
- Object:需要有 install 方法,Vue会帮你自动注入到 install 方法
- function:就直接当 install 方法去使用
案例:
loading.vue
:
<template>
<div v-if="isShow" class="loading">
<div class="loading-content">Loading...</div>
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue';
const isShow = ref(false)//定位loading 的开关
const show = () => {
isShow.value = true
}
const hide = () => {
isShow.value = false
}
//对外暴露 当前组件的属性和方法
defineExpose({
isShow,
show,
hide
})
</script>
<style scoped lang="less">
.loading {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
&-content {
font-size: 30px;
color: #fff;
}
}
</style>
lading.ts
:
import { createVNode, render, VNode, App } from 'vue';
import Loading from './index.vue'
export default {
install(app: App) {
//createVNode vue提供的底层方法 可以给我们组件创建一个虚拟DOM 也就是Vnode
const vnode: VNode = createVNode(Loading)
//render 把我们的Vnode 生成真实DOM 并且挂载到指定节点
render(vnode, document.body)
// Vue 提供的全局配置 可以自定义
app.config.globalProperties.$loading = {
show: () => vnode.component?.exposed?.show(),
hide: () => vnode.component?.exposed?.hide()
}
}
}
main.ts
:
import Loading from './components/loading'
let app = createApp(App)
app.use(Loading)
type Lod = {
show: () => void,
hide: () => void
}
// 编写ts loading 声明文件放置报错 和 智能提示
// 或许导入 'vue'
declare module '@vue/runtime-core' {
// ComponentCustomProperties 固定写法
export interface ComponentCustomProperties {
$loading: Lod
}
}
app.mount('#app')
使用方法:
<template>
<div></div>
</template>
<script setup lang='ts'>
import { ref,reactive,getCurrentInstance} from 'vue'
const instance = getCurrentInstance()
instance?.proxy?.$Loading.show()
setTimeout(()=>{
instance?.proxy?.$Loading.hide()
},5000)
// console.log(instance)
</script>
<style>
*{
padding: 0;
margin: 0;
}
</style>
6.Event Loop 和 nextTick
JS 执行机制:
在我们学js 的时候都知道js是单线程的,如果是多线程的话会引发一个问题在同一时间同时操作DOM 一个增加一个删除JS就不知道到底要干嘛了,所以这个语言是单线程的。
但是随着 HTML5 到来js也支持了多线程, webWorker 但是也是不允许操作DOM ,单线程就意味着所有的任务都需要排队,后面的任务需要等前面的任务执行完才能执行,如果前面的任务耗时过长,后面的任务就需要一直等,一些从用户角度上不需要等待的任务就会一直等待,这个从体验角度上来讲是不可接受的,所以JS中就出现了异步的概念。
Event Loop事件循环机制:
-
同步任务:
代码从上到下按顺序执行
-
异步任务:
- 宏任务
- script(整体代码)、setTimeout、setInterval、UI交互事件、postMessage、Ajax
- 微任务
-
Promise.then catch finally、MutaionObserver、process.nextTick(Node.js 环境)
-
所有的同步任务都是在主进程执行的形成一个执行栈,主线程之外,还存在一个"任务队列",异步任务执行队列中先执行宏任务,然后清空当次宏任务中的所有微任务,然后进行下一个tick如此形成循环。
- 宏任务
nextTick:
nextTick
就是创建一个异步任务,那么它自然要等到同步任务执行完成后才执行。
nextTick 接受一个参数fn(函数)定义了一个变量P 这个P最终返回都是Promise,最后是return 如果传了fn 就使用变量P.then执行一个微任务去执行fn函数,then里面this 如果有值就调用bind改变this指向返回新的函数,否则直接调用fn,如果没传fn,就返回一个promise,最终结果都会返回一个promise。
-
示例:
<template> <div ref="xiaoman"> {{ text }} </div> <button @click="change">change div</button> </template> <script setup lang='ts'> import { ref,nextTick } from 'vue'; const text = ref('小满开飞机') const xiaoman = ref<HTMLElement>() const change = async () => { text.value = '小满不开飞机' console.log(xiaoman.value?.innerText) //小满开飞机 await nextTick(); console.log(xiaoman.value?.innerText) //小满不开飞机 } </script> <style scoped> </style>
7.Vue响应式语法糖
注意: 实验性的产物 暂时不要再生产环境使用
Vue 3.2.25 以上版本
1.开启配置
-
vite
vite.config.ts
import { fileURLToPath, URL } from 'url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' // https://vitejs.dev/config/ export default defineConfig({ server: { port: 3000 }, plugins: [ vue({ reactivityTransform:true }), vueJsx()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, })
-
vue-cli
vue.config.js
// vue.config.js module.exports = { chainWebpack: (config) => { config.module .rule('vue') .use('vue-loader') .tap((options) => { return { ...options, reactivityTransform: true } }) } }
2.示例
ref 相关函数:
- ref ->
$ref
- computed ->
$computed
- shallowRef ->
$shallowRef
- customRef ->
$customRef
- toRef ->
$toRef
在之前 ref 修改值 和 获取值 都要 .value 一下 感觉很繁琐,不想用.value 我们可以使用vue3的新特性$ref 。
原理:我们可以直接使用$ref 宏函数 就不需要.value 了。能帮我们快速书写,但是宏函数是基于运行时的他最终还是会转换成ref 加.value ,只不过vue帮我们做了这个操作了
<template>
<div>
<button @click="add">add</button>
</div>
<h2>
{{count}}
</h2>
</template>
<script setup lang='ts'>
import { $ref } from 'vue/macros'
let count = $ref(0)
const add = () => {
count++
}
</script>
<style>
</style>
3.$ref的弊端 —— $$()
使用 watch 侦听器时,因为使用 $ref 的原因导致,被监听的不是一个 ref 对象,所以 watch 无法监听且会抛出一个警告。
如下:
<template>
</template>
<script setup lang='ts'>
import { reactive, ref, toRefs,watch } from 'vue';
import { $ref} from 'vue/macros'
let count = $ref<number>(0)
watch(count,(v)=>{
console.log(v)
})
setInterval(()=>{
count++
},1000)
</script>
<style>
</style>
- 抛出警告
如何解决?
-
使用 $$ 符号
编译时变成一个 ref 对象 不加 .value
<script setup lang='ts'> import { reactive, ref, toRefs,watch } from 'vue'; import { $ref,$$ } from 'vue/macros' let count = $ref<number>(0) watch($$(count),(v)=>{ console.log(v) }) setInterval(()=>{ count++ },1000) </script>
4.解构 —— $()
在之前我们解构一个对象使用toRefs 解构完成之后 获取值和修改值,还是需要.value vue3 也提供了 语法糖 $() 解构完之后可以直接赋值
<template>
<div>
{{name}}
</div>
</template>
<script setup lang='ts'>
import { reactive, toRefs } from 'vue'
import {$} from 'vue/macros'
const obj = reactive({
name: '小满'
})
let { name } = $(obj);
setTimeout(()=>{
name = '大满'
},2000)
</script>
<style>
</style>
8.环境变量
他的主要作用就是让开发者区分不同的运行环境,来 实现兼容开发和生产 例如 npm run dev 就是开发环境 npm run build 就是生产环境等等
Vite 在一个特殊的 import.meta.env
对象上暴露环境变量。这里有一些在所有情况下都可以使用的内建变量:
{
"BASE_URL":"/", //部署时的URL前缀
"MODE":"development", //运行模式
"DEV":true, //是否在dev环境
"PROD":false, //是否是build 环境
"SSR":false //是否是SSR 服务端渲染模式
}
注意:需要注意的一点就是这个环境变量不能使用动态赋值import.meta.env[key] ,因为这些环境变量在打包的时候是会被硬编码的通过JSON.stringify 注入浏览器的
配置额外的环境变量:
-
在根目录下新建 env 文件,可以创建多个
如 env.[name]
示例:
区别于生产环境和开发环境下的接口请求地址
-
.env.development
-
VITE_HTTP= http://mingcomity.cn
-
-
.env.production
-
VITE_HTTP= https://mingcomity.cn
-
-
-
修改启动命令
-
在 package json 配置 --mode env文件名称
-
"script": { "dev": "vite --mode devlopment" }
开发模式下
-
生产模式不用配置
-
-
在 vite.config.ts 中使用环境变量:
-
导入 loadEnv 包
import { fileURLToPath, URL } from 'node:url' import { defineConfig, loadEnv } from 'vite' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx'
-
更改结构
// https://vitejs.dev/config/ // mode 就是运行模式 // 参2 是一个地址 export default ({mode}:any) => { console.log(loadEnv(mode,process.cwd())) return defineConfig({ plugins: [vue(), vueJsx()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } }) }
9.Vue3 性能优化
1.性能测试工具
- DevTools (谷歌浏览器自带)
- 部分参数介绍
- FCP (First Contentful Paint):首次内容绘制的时间,浏览器第一次绘制DOM相关的内容,也是用户第一次看到页面内容的时间。
- Speed Index: 页面各个可见部分的显示平均时间,当我们的页面上存在轮播图或者需要从后端获取内容加载时,这个数据会被影响到。
- LCP (Largest Contentful Paint):最大内容绘制时间,页面最大的元素绘制完成的时间。
- TTI(Time to Interactive):从页面开始渲染到用户可以与页面进行交互的时间,内容必须渲染完毕,交互元素绑定的事件已经注册完成。
- TBT(Total Blocking Time):记录了首次内容绘制到用户可交互之间的时间,这段时间内,主进程被阻塞,会阻碍用户的交互,页面点击无反应。
- CLS(Cumulative Layout Shift):计算布局偏移值得分,会比较两次渲染帧的内容偏移情况,可能导致用户想点击A按钮,但下一帧中,A按钮被挤到旁边,导致用户实际点击了B按钮。
- 部分参数介绍
2.代码分析
-
安装 rollup
npm install rollup-plugin-visualizer
-
vite.config.ts
配置 (注意设置 open)import { visualizer } from 'rollup-plugin-visualizer'; plugins: [vue(), vueJsx(),visualizer({ open:true })],
-
执行打包好会弹出一个网页
3.Vite 配置优化
build:{
chunkSizeWarningLimit:2000,
cssCodeSplit:true, //css 拆分
sourcemap:false, //不生成sourcemap
minify:false, //是否禁用最小化混淆,esbuild打包速度最快,terser打包体积最小。
assetsInlineLimit:5000 //小于该值 图片将打包成Base64
},
4.PWA离线存储计算
PWA 技术的出现就是让web网页无限接近于Native 应用
- 可以添加到主屏幕,利用manifest实现
- 可以实现离线缓存,利用service worker实现
- 可以发送通知,利用service worker实现
使用:
-
安装
npm install vite-plugin-pwa -D
-
vite.config.ts
配置 (注意设置 open)import { VitePWA } from 'vite-plugin-pwa'
plugins:[ vue(), VitePWA({ workbox:{ cacheId:"XIaoman",//缓存名称 runtimeCaching:[ { urlPattern:/.*\.js.*/, //缓存文件 handler:"StaleWhileRevalidate", //重新验证时失效 options:{ cacheName:"XiaoMan-js", //缓存js,名称 expiration:{ maxEntries:30, //缓存文件数量 LRU算法 maxAgeSeconds:30 * 24 * 60 * 60 //缓存有效期 } } } ] }, vueJsx(), visualizer({ open:true }) })]
5.其它优化
- 图片懒加载
- 虚拟列表
- 多线程
- 防抖节流
10.Vue3 Web Components
Web Components 提供了基于原生支持的、对视图层的封装能力,可以让单个组件相关的 javaScript、css、html模板运行在以html标签为界限的局部环境中,不会影响到全局,组件间也不会相互影响 。
简单来说:就是提供了我们自定义标签的能力,并且提供了标签内完整的生命周期 。
组成:
Custom elements(自定义元素):JavaScript API,允许定义custom elements及其行为,然后可以在我们的用户界面中按照需要使用它们。
Shadow DOM(影子DOM):JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,开发者可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
HTML templates(HTML模板):和元素使开发者可以编写与HTML结构类似的组件和样式。然后它们可以作为自定义元素结构的基础被多次重用。
实例:
class Btn extends HTMLElement {
constructor () {
//调用super 来建立正确的原型链继承关系
super()
const p = this.h('p')
p.innerText = '小满'
p.setAttribute('style','height:200px;width:200px;border:1px solid #ccc;background:yellow')
//表示 shadow DOM 子树的根节点,用于css样式隔离
const shaDow = this.attachShadow({mode:"open"})
shaDow.appendChild(this.p)
}
// template 模式
constructor() {
//调用super 来建立正确的原型链继承关系
super()
const template = this.h('template')
template.innerHTML = `
<div>小满</div>
<style>
div{
height:200px;
width:200px;
background:blue;
}
</style>
`
//表示 shadow DOM 子树的根节点。
const shaDow = this.attachShadow({ mode: "open" })
shaDow.appendChild(template.content.cloneNode(true))
}
h (el) {
return document.createElement(el)
}
/**
* 生命周期
*/
//当自定义元素第一次被连接到文档 DOM 时被调用。
connectedCallback () {
console.log('我已经插入了!!!嗷呜')
}
//当自定义元素与文档 DOM 断开连接时被调用。
disconnectedCallback () {
console.log('我已经断开了!!!嗷呜')
}
//当自定义元素被移动到新文档时被调用
adoptedCallback () {
console.log('我被移动了!!!嗷呜')
}
//当自定义元素的一个属性被增加、移除或更改时被调用
attributeChangedCallback () {
console.log('我被改变了!!!嗷呜')
}
}
window.customElements.define('xiao-man',Btn)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>web Component</title>
<script src="./btn.js"></script>
</head>
<body>
<xiao-man></xiao-man>
</body>
</html>
在vue中使用:
vite.config.ts
配置
告知vue这是一个自定义Component 跳过组件检查
/*vite config ts 配置*/
vue({
template:{
compilerOptions:{
// 以 xiaoman- 开头的标签都跳过组件检查
isCustomElement:(tag)=> tag.includes('xiaoman-')
}
}
})
-
父组件
注意引入的组件以 .ce.vue 结尾
<template> <div> // 传参, 引用类型JSON.stringify <xiaoman-btn :title=" JSON.stringify(name) "></xiaoman-btn> </div> </template> <script setup lang='ts'> import { ref, reactive, defineCustomElement } from 'vue' //自定义元素模式 要开启这个模式,只需要将你的组件文件以 .ce.vue 结尾即可 import customVueVue from './components/custom-vue.ce.vue' const Btn = defineCustomElement(customVueVue) customElements.define('xiaoman-btn', Btn) const name = ref({a:1}) </script> <style scoped lang='less'> </style>
-
子组件
custom-vue.ce.vue
<template> <div> 小满123213 {{title}} </div> </template> <script setup lang='ts'> import { ref, reactive } from 'vue' defineProps<{ title:string }>() </script> <style scoped lang='less'> </style>
11.Proxy 跨域
1.什么是跨域
主要是出于浏览器的同源策略限制,它是浏览器最核心也最基本的安全功能,所以服务端和服务端之间是没有限制的,主要是限制浏览器
当一个请求 url 的 协议、域名、端口 三者之间任意一个与当前页面url不同即为跨域。
- 例如 http://xxxx.com -> https://xxxx.com 存在跨域 协议不同
- 例如 127.x.x.x:8001 -> 127.x.x.x:8002 存在跨域 端口不同
- 例如 www.xxxx.com -> www.yyyy.com 存在跨域 域名不同
2.如何解决跨域
-
jsonp
这种方式在之前很常见,他实现的基本原理是利用了HTML里script元素标签没有跨域限制 动态创建script标签,将src作为服务器地址,服务器返回一个callback接受返回的参数
function clickButton() { let obj, s obj = { "table":"products", "limit":10 }; //添加参数 s = document.createElement("script"); //动态创建script s.src = "接口地址xxxxxxxxxxxx" + JSON.stringify(obj); document.body.appendChild(s); } //与后端定义callback名称 function myFunc(myObj) { //接受后端返回的参数 document.getElementById("demo").innerHTML = myObj; }
-
cors
设置 CORS 允许跨域资源共享 需要后端设置
{ "Access-Control-Allow-Origin": "http://web.xxx.com" //可以指定地址 } { "Access-Control-Allow-Origin": "*" //也可以使用通配符 任何地址都能访问 安全性不高 }
-
Vite proxy / node proxy / webpack proxy
三种方式都是代理
-
使用 express 简单构建一个接口
const express = require('express') const app = express() //创建get请求 app.get('/xm',(req,res)=>{ res.json({ code:200, message:"请求成功" }) }) //端口号9001 app.listen(9001)
-
使用 vite 项目的 fetch 请求,项目端口在 5137 下
<script lang="ts" setup> import {ref,reactive } from 'vue' fetch('http://localhost:9001/xm') </script>
可以看出,是存在跨域的,这时配合 vite 的代理来解决跨域
-
vite.config.ts
配置export default defineConfig({ plugins: [vue()], server:{ proxy:{ '/api':{ target:"http://localhost:9001/", //跨域地址 changeOrigin:true, //支持跨域 rewrite:(path) => path.replace(/^\/api/, "")//重写路径,替换/api } } } })
-
修改请求
<script lang="ts" setup> import {ref,reactive } from 'vue' fetch('/api/xm') </script> 复制代码
此时发起请求即可正常请求, webpack proxy 和 node proxy 用法类似
原理:
-
剥开源码,vite 处理源码是通过 proxyMiddleware
// proxy const { proxy } = serverConfig if (proxy) { middlewares.use(proxyMiddleware(httpServer, proxy, config)) }
-
在 proxyMiddleware 是调用 http-proxy 这个库
import httpProxy from 'http-proxy' export function proxyMiddleware( httpServer: http.Server | null, options: NonNullable<CommonServerOptions['proxy']>, config: ResolvedConfig ): Connect.NextHandleFunction { // lazy require only when proxy is used const proxy = httpProxy.createProxyServer(opts) as HttpProxy.Server 复制代码
-
http-proxy 模块用于转发 http 请求
**原理:**使用 http 或 https 模块搭建 node 代理服务器,将客户端发送的请求数据转发到目标服务器,再将响应输送到客户端。
const http = require('http') const httpProxy = require('http-proxy') const proxy = httpProxy.createProxyServer({}) //创建一个代理服务 代理到9001 http.createServer((req,res)=>{ proxy.web(req,res,{ target:"http://localhost:9001/xm", //代理的地址 changeOrigin:true, //是否有跨域 ws:true //webSocetk }) }).listen(8888)
-
五.Pinia
1.介绍及安装
Pinia.js 全局状态管理工具
特点:
- 完整的 ts 的支持;
- 足够轻量,压缩后的体积只有1kb左右;
- 去除 mutations,只有 state,getters,actions;
- actions 支持同步和异步;
- 代码扁平化没有模块嵌套,只有 store 的概念,store 之间可以自由使用,每一个store都是独立的
- 无需手动添加 store,store 一旦创建便会自动添加;
- 支持Vue3 和 Vue2
-
安装
yarn add pinia npm install pinia
-
注册
Vue3
import { createApp } from 'vue' import App from './App.vue' import {createPinia} from 'pinia' const store = createPinia() let app = createApp(App) app.use(store) app.mount('#app')
Vue2
import { createPinia, PiniaVuePlugin } from 'pinia' Vue.use(PiniaVuePlugin) const pinia = createPinia() new Vue({ el: '#app', // other options... // ... // note the same `pinia` instance can be used across multiple Vue apps on // the same page pinia, })
2.初始化仓库Store
-
新建一个文件夹 Store
-
新建文件 [name].ts
-
定义仓库 Store
index.ts
import { defineStore } from 'pinia'
-
抽离名称 (因为defineStore(),并且它需要一个唯一的名称,作为第一个参数传递)
新建文件
store-namespace/index.ts
export const enum Names { Test = 'TEST' }
-
仓库 Store 引入
index.ts
import { defineStore } from 'pinia' import {Names} from './store-namespace' export const useTestStore = defineStore(Names.TEST,{ // State 箭头函数 返回一个对象 在对象里面定义值 state:()=>{ return { current:1, name:'Pinia' } }, // computed 修饰一些值 getters:{ }, // methods 可以做同步 异步都可以做 提交state actions:{ } })
这个名称,也称为id,是必要的,Pania 使用它来将商店连接到 devtools。将返回的函数命名为use...是可组合项之间的约定,以使其使用习惯。
-
使用
App.vue
<template> <div>pinia:{{ Test.current }}--{{ Test.name }}</div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' import { useTestStore } from './store/' const Test = useTestStore() </script> <style scoped></style>
3.修改 State
-
直接修改值
<template> <div> <button @click="Add">+</button> <div> {{Test.current}} </div> </div> </template> <script setup lang='ts'> import {useTestStore} from './store' const Test = useTestStore() const Add = () => { Test.current++ } </script> <style> </style>
-
批量修改值
使用 $patch 方法批量修改多个值
<template> <div> <button @click="Add">+</button> <div> {{Test.current}} </div> <div> {{Test.age}} </div> </div> </template> <script setup lang='ts'> import {useTestStore} from './store' const Test = useTestStore() const Add = () => { Test.$patch({ current:200, age:300 }) } </script> <style> </style>
-
批量修改函数形式
推荐使用函数形式 可以自定义修改逻辑
<template> <div> <button @click="Add">+</button> <div> {{Test.current}} </div> <div> {{Test.age}} </div> </div> </template> <script setup lang='ts'> import {useTestStore} from './store' const Test = useTestStore() const Add = () => { Test.$patch((state)=>{ state.current++; state.age = 40 }) } </script> <style> </style>
-
通过原始对象修改整个实例
$state 可以通过将store的属性设置为新对象来替换store的整个状态
缺点就是必须修改整个对象的所有属性
<template> <div> <button @click="Add">+</button> <div> {{Test.current}} </div> <div> {{Test.age}} </div> </div> </template> <script setup lang='ts'> import {useTestStore} from './store' const Test = useTestStore() const Add = () => { Test.$state = { current:10, age:30 } } </script> <style> </style>
-
通过 actions 修改
-
定义 Actions
在actions 中直接使用this就可以指到state里面的值
import { defineStore } from 'pinia' import { Names } from './store-naspace' export const useTestStore = defineStore(Names.TEST, { state:()=>{ return { current:1, age:30 } }, actions:{ setCurrent () { this.current++ } } })
-
直接在实例调用
<template> <div> <button @click="Add">+</button> <div> {{Test.current}} </div> <div> {{Test.age}} </div> </div> </template> <script setup lang='ts'> import {useTestStore} from './store' const Test = useTestStore() const Add = () => { Test.setCurrent() } </script> <style> </style>
-
4.解构 store
-
在 Pinia 是不允许直接解构的,因为这是会失去响应性的
<template> <div>origin value {{Test.current}}</div> <div> pinia:{{ current }}--{{ name }} change : <button @click="change">change</button> </div> </template> <script setup lang='ts'> import { useTestStore } from './store' const Test = useTestStore() const change = () => { Test.current++ } // 直接解构 const { current, name } = Test console.log(current, name); </script> <style> </style>
执行 add() 后, 会发现解构完之后的数据不会发生改变,但是源数据是会改变的
使用 store ToRefs:
其原理跟 toRefs 一样的给里面的数据包裹一层 toref
源码:通过toRaw使store变回原始数据防止重复代理,循环 store 通过 isRef isReactive 判断,如果是响应式对象直接拷贝一份给refs 对象 将其原始对象包裹 toRef 使其变为响应式对象
import { storeToRefs } from 'pinia'
const Test = useTestStore()
const { current, name } = storeToRefs(Test)
5.Actions,getters
-
Actions(支持同步异步)
-
同步调用
store/index.ts
import { defineStore } from 'pinia' import { Names } from './store-naspace' export const useTestStore = defineStore(Names.TEST, { state: () => ({ counter: 0, }), actions: { increment() { this.counter++ }, randomizeCounter() { this.counter = Math.round(100 * Math.random()) }, }, })
App.vue
<template> <div> <button @click="Add">+</button> <div> {{Test.counter}} </div> </div> </template> <script setup lang='ts'> import {useTestStore} from './store' const Test = useTestStore() const Add = () => { Test.randomizeCounter() } </script> <style> </style>
-
异步调用
结合 async await
store/index.ts
import { defineStore } from 'pinia' import { Names } from './store-naspace' type Result = { name: string isChu: boolean } const Login = (): Promise<Result> => { return new Promise((resolve) => { setTimeout(() => { resolve({ name: '小满', isChu: true }) }, 3000) }) } export const useTestStore = defineStore(Names.TEST, { state: () => ({ user: <Result>{}, name: "123" }), actions: { async getLoginInfo() { const result = await Login() this.user = result; } }, })
App.vue
<template> <div> <button @click="Add">test</button> <div> {{Test.user}} </div> </div> </template> <script setup lang='ts'> import {useTestStore} from './store' const Test = useTestStore() const Add = () => { Test.getLoginInfo() } </script> <style> </style>
-
多个 action 互相调用 getLoginInfo setName
state: () => ({ user: <Result>{}, name: "default" }), actions: { async getLoginInfo() { const result = await Login() this.user = result; this.setName(result.name) }, setName (name:string) { this.name = name; } },
-
-
getters
主要作用类似于computed 数据修饰并且有缓存
-
使用箭头函数不能使用this,this指向已经改变指向undefined 修改值请用state
getters:{ newPrice:(state)=> `$${state.user.price}` },
-
普通函数形式可以使用this
getters:{ newCurrent ():number { return ++this.current } },
-
getters 互相调用
getters:{ newCurrent ():number | string { return ++this.current + this.newName }, newName ():string { return `$-${this.name}` } },
-
6.API
-
$reset
重置 store 到他的初始状态
// 初始状态 state: () => ({ user: <Result>{}, name: "default", current:1 }) // 修改值 const change = () => { Test.current++ }
执行
$reset()
将会把state所有值 重置回 初始状态
-
$subscribe 订阅 state 的改变
类似于Vuex 的abscribe 只要有state 的变化就会走这个函数
Test.$subscribe((args,state)=>{ console.log(args,state); }) // 参2 :如果你的组件卸载之后还想继续调用请设置第二个参数 Test.$subscribe((args,state)=>{ console.log(args,state); },{ detached:true, // 还有如 deep 、flush })
-
$onAction 订阅 Actions 的调用
只要有actions被调用就会走这个函数
Test.$onAction((args)=>{ console.log(args); }) // 参2 :如果你的组件卸载之后还想继续调用请设置第二个参数 Test.$onAction((args)=>{ console.log(args); },true)
7.Pinia 插件
pinia 和 vuex 都有一个通病,就是页面刷新状态就会丢失
所以:我们可以写一个 pinia 插件缓存他的值
const __piniaKey = '__PINIAKEY__'
//定义兜底变量
type Options = {
key?:string
}
//定义入参类型
//将数据存在本地
const setStorage = (key: string, value: any): void => {
localStorage.setItem(key, JSON.stringify(value))
}
//存缓存中读取
const getStorage = (key: string) => {
return (localStorage.getItem(key) ? JSON.parse(localStorage.getItem(key) as string) : {})
}
//利用函数柯丽华接受用户入参
const piniaPlugin = (options: Options) => {
//将函数返回给pinia 让pinia 调用 注入 context
return (context: PiniaPluginContext) => {
const { store } = context;
const data = getStorage(`${options?.key ?? __piniaKey}-${store.$id}`)
store.$subscribe(() => {
setStorage(`${options?.key ?? __piniaKey}-${store.$id}`, toRaw(store.$state));
})
//返回值覆盖pinia 原始值
return {
...data
}
}
}
//初始化pinia
const pinia = createPinia()
//注册pinia 插件
pinia.use(piniaPlugin({
key: "pinia"
}))
六.Vitest
测试工具
1.示例
pnpm i vitest -D
需要vite版本大于3
示例:
// _test_
import { expect, test } from 'vitest'
test('test common matcher', () => {
const name = 'viking'
expect(name).toBe('viking')
expect(2 + 2).toBe(4)
expect(2 + 2).not.toBe(5)
})
test('test to be true or false', () => {
expect(1).toBeTruthy()
expect(0).toBeFalsy()
})
test('test number', () => {
expect(4).toBeGreaterThan(3)
expect(2).toBeLessThan(3)
})
test('test object', () => {
// 因为是 === 使用导致报错
// expect({ name: 'string' }).toBe({ name: 'string' })
expect({ name: 'string' }).toEqual({ name: 'string' })
})
执行 npx vitest xxxx
可以自动识别xxxx测试文件名
import { expect, test, describe, vi, Mocked } from 'vitest'
import { testFn, request } from './utils'
export function testFn(number: number, callback: Function) {
if (number > 10) callback(number)
}
export async function request() {
const { data } = await axios.get('xxx.url')
return data
}
import axios from 'axios'
vi.mock('axios')
const mockedAxios = axios as Mocked<typeof axios>
describe('fonctions', () => {
test('create a mock function', () => {
const callback = vi.fn()
testFn(12, callback)
// 通过则表示这个函数被调用了
expect(callback).toHaveBeenCalled()
// 下面表示这个函数被调用时传入的参数是 12
expect(callback).toHaveBeenCalledWith(12)
})
// 中间状态
test('spy on method', () => {
const obj = {
getName: () => 1
}
const spy = vi.spyOn(obj, 'getName')
obj.getName()
// 是否被调用
expect(spy).toHaveBeenCalled()
obj.getName()
// 是否被调用两次
expect(spy).toHaveReturnedTimes(2)
})
// 第三方实现
test('mock third party module', async () => {
// 重写get方法的实现
mockedAxios.get.mockImplementation(() => Promise.resolve({ data: 123 }))
// 直接指定返回的值
mockedAxios.get.mockResolvedValue({ data: 123 })
const result = await request()
expect(result).toBe(123)
})
})
2.vue测试库
-
pnpm install --save-dev @vue/test-utils
-
- react常用
示例:
-
import { describe, test } from 'vitest' import { mount } from '@vue/test-utils' import Button from '../src/index' describe('Button component', () => { test('basic button', () => { const wrapper = mount(Button, { props: { type: 'primary' }, slots: { default: 'button' } }) console.log(wrapper.html()) }) })
报错如下
因为vitest是运行在node环境下的,所以是需要dom环境的
-
配置DOM环境
/// <reference types="vitest"/> import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue(), vueJsx()], test: { globals: true, environment: 'jsdom' } })
vitest.config.ts
文件选择jsdom,记得下载 jsdom这个包
可以看到以及打印出具体的html了
-
更多测试
import { describe, test, expect } from 'vitest' import { mount } from '@vue/test-utils' import Button from '../src/index' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import { McIcon } from '../../icon' describe('Button component', () => { test('测试 type, 测试 slot, 测试click事件', () => { // 只有展示,没有操作,所以主要测试类名是否正确 const wrapper = mount(Button, { props: { type: 'primary' }, slots: { default: 'button' } }) // 类名是否包含 expect(wrapper.classes()).toContain('mc-button--primary') // 测试slot:查找 'button' 这个dom元素, text后, toBe 判断值是否正确 // get, find 遍历 get找不到则中断测试, find则不会, 所以一直使用get,除非只是检查元素是否存在 expect(wrapper.get('button').text()).toBe('button') // 测试events:查找 'button' 这个dom元素, trigger一个click事件, toHave.. 表示是否包含一个属性 wrapper.get('button').trigger('click') // console.log(wrapper.emitted()) expect(wrapper.emitted()).toHaveProperty('click') }) test('测试 disabled', () => { const wrapper = mount(Button, { props: { disabled: true }, slots: { default: 'disabled' } }) // 测试这个组件上的原生属性 disabled 是否存在 expect(wrapper.attributes('disabled')).toBeDefined() // find后拿到的是wrapper, 在.element后拿到这个组件真正的dom节点,在判断是否存在 expect(wrapper.find('button').element.disabled).toBeDefined() // 判断是否会发生事件 wrapper.get('button').trigger('click') expect(wrapper.emitted()).not.toHaveProperty('click') }) test('测试 icon', () => { const wrapper = mount(Button, { props: { icon: 'arrow-up' }, slots: { default: 'disabled' }, global: { // 想模拟掉的组件的名称,这个组件会被替换掉 stubs: ['FontAwesomeIcon'] } }) // 判断FontAwesomeIcon组件是否存在 const iconElement = wrapper.findComponent(FontAwesomeIcon) // 是否存在 expect(iconElement.exists()).toBeTruthy() // icon属性是否存在 expect(iconElement.attributes('icon')).toBe('arrow-up') }) test('测试 loading', () => { const wrapper = mount(Button, { props: { loading: true }, slots: { default: 'disabled' }, global: { // 想模拟掉的组件的名称,这个组件会被替换掉 stubs: ['McIcon'] } }) // 判断McIcon组件是否存在 const iconElement = wrapper.findComponent(McIcon) expect(iconElement.exists()).toBeTruthy() expect(iconElement.attributes('icon')).toBe('spinner') expect(wrapper.attributes('disabled')).toBeDefined() }) })
-
使用h 函数和 tsx
import { describe, test, expect } from 'vitest' import { mount } from '@vue/test-utils' import { McCollapseItem, McCollapse } from '../' import { h } from 'vue' describe('Collapse components', () => { test('basic collapse', () => { const wrapper = mount(McCollapse, { props: { modelValue: ['a'] }, slots: { default: h(McCollapseItem, { name: 'a', title: 'Title A' }, 'content a') }, global: { stubs: ['McIcon'] } }) console.log(wrapper.html()) expect(wrapper) }) })
import { describe, test, expect } from 'vitest' import { mount } from '@vue/test-utils' import { McCollapseItem, McCollapse } from '../' describe('Collapse components', () => { test('basic collapse', () => { const wrapper = mount(McCollapse, { props: { modelValue: ['a'] }, slots: { default: ( <McCollapseItem name="a" title="title a"> Content a </McCollapseItem> ) }, global: { stubs: ['McIcon'] } }) console.log(wrapper.html()) expect(wrapper) }) })
-
更胜一筹
import { describe, test, expect } from 'vitest' import { mount } from '@vue/test-utils' import { McCollapseItem, McCollapse } from '../' describe('Collapse components', () => { test('basic collapse', () => { const wrapper = mount( () => ( <McCollapse modelValue={['a']}> <McCollapseItem name="a" title="title a"> content a </McCollapseItem> <McCollapseItem name="b" title="title b"> content b </McCollapseItem> <McCollapseItem name="c" title="title c"> content c </McCollapseItem> </McCollapse> ), { global: { stubs: ['McIcon'] } } ) }) })
-
优化写法
import { describe, test, expect, vi, beforeAll } from 'vitest' import { DOMWrapper, VueWrapper, mount } from '@vue/test-utils' import { McCollapseItem, McCollapse } from '../' const onChange = vi.fn() let wrapper: VueWrapper let headers: DOMWrapper<Element>[], contents: DOMWrapper<Element>[] let firstContent: DOMWrapper<Element>, secondContent: DOMWrapper<Element>, disabledContent: DOMWrapper<Element>, firstHeader: DOMWrapper<Element>, secondHeader: DOMWrapper<Element>, disabledHeader: DOMWrapper<Element> describe('Collapse components2', () => { // 调用测试用例前会执行的事 beforeAll(() => { wrapper = mount( () => ( <McCollapse modelValue={['a']} onChange={onChange}> <McCollapseItem name="a" title="title a"> content a </McCollapseItem> <McCollapseItem name="b" title="title b"> content b </McCollapseItem> <McCollapseItem name="c" title="title c" disabled> content c </McCollapseItem> </McCollapse> ), { global: { stubs: ['McIcon'] }, attachTo: document.body } ) headers = wrapper.findAll('.mc-collapse-item__header') contents = wrapper.findAll('.mc-collapse-item__wrapper') firstHeader = headers[0] secondHeader = headers[1] disabledHeader = headers[2] firstContent = contents[0] secondContent = contents[1] disabledContent = contents[2] }) test('测试基础结构以及对应文本', () => { // 长度 expect(headers.length).toBe(3) expect(contents.length).toBe(3) // 文本 expect(firstHeader.text()).toBe('title a') // 内容 expect(firstContent.isVisible()).toBeTruthy() expect(secondContent.isVisible()).toBeFalsy() expect(firstContent.text()).toBe('content a') }) // .only 表示只测试这一个 可以用于测试加快 test.only('点击标题展开/关闭内容', async () => { // 行为 await firstHeader.trigger('click') expect(firstContent.isVisible()).toBeFalsy() await secondHeader.trigger('click') expect(secondContent.isVisible()).toBeTruthy() }) // .skip 表示跳过这个案例 test.skip('发送正确的事件', () => { expect(onChange).toHaveBeenCalledTimes(2) // 调用两次 expect(onChange).toHaveBeenCalledWith([]) expect(onChange).toHaveBeenLastCalledWith(['b']) }) test('disabled 的内容应该没有反应', async () => { // 重置onChange的调用记录 onChange.mockClear() expect(disabledHeader.classes()).toContain('is-disabled') await disabledHeader.trigger('click') expect(disabledContent.isVisible()).toBeFalsy() expect(onChange).not.toHaveBeenCalled() }) })
扩展
1.TSX
我们之前呢是使用 Template 去写我们模板,现在可以扩展另一种风格 TSX 风格 ,vue2 的时候就已经支持jsx写法,只不过不是很友好,随着 vue3 对 typescript 的支持度,tsx写法越来越被接受。
可以减少学习 React 的学习成本,包括许多第三方组件库也是使用 TSX 去开发的
可在项目搭建流程2中,选好JSX支持
-
安装
npm install @vitejs/plugin-vue-jsx -D
-
配置
vite.config.ts
import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // 重点 import vueJsx from '@vitejs/plugin-vue-jsx' // https://vitejs.dev/config/ export default defineConfig({ // 重点 plugins: [vue(), vueJsx()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } })
-
使用方法
App.tsx
,使用变量都是使用{}
即可-
返回一个渲染函数
// 返回一个渲染函数 export default function () { return (<div>TSX</div>) }
-
optionsAPi
// optionsAPi import { defineComponent } from "vue" export default defineComponent ({ data () { return { age:23 } }, render () { return (<div>{this.age}</div>) } })
-
setup 函数模式
ref 手动解包
// setup 函数模式 import { defineComponent,ref } from "vue" export default defineComponent ({ setup() { // ref 在 template 自动解包 .value 在tsx中不会,所以需要手动.value const flag = ref(false) return ()=>(<div v-show={flag.value}>TSX</div>) } })
-
tsx用法:
-
ref 需要手动解包
// setup 函数模式 import { defineComponent,ref } from "vue" export default defineComponent ({ setup() { // ref 在 template 自动解包 .value 在tsx中不会,所以需要手动.value const flag = ref(false) return ()=>(<div v-show={flag.value}>TSX</div>) } })
-
不支持 v-if
使用 三元表达式 去替代
// setup 函数模式 import { defineComponent,ref } from "vue" export default defineComponent ({ setup() { // ref 在 template 自动解包 .value 在tsx中不会,所以需要手动.value const flag = ref(false) return ()=>(<><div>{flag.value ? <div>true</div> : <div>false</div>}</div></>) } })
-
不支持 v-for
使用 js 的编程思想,使用 map
export default defineComponent ({ setup() { // ref 在 template 自动解包 .value 在tsx中不会,所以需要手动.value const flag = ref(false) const data = [{name:'张三'},{name:'张四'},{name:'张五'}] return()=>(<> {data.map(v=>{ return <div>{v.name}</div> })} </>) } })
-
不支持 v-bind
使用
{}
return()=>(<> {data.map(v=>{ return <div name={v.name}>{v.name}</div> })} </>)
-
props emit
// 返回一个渲染函数 // optionsAPi // setup 函数模式 interface Props { name?:String } import { defineComponent,ref } from "vue" export default defineComponent ({ props:{ name:String }, emit:['on-click'], setup(props:Props) { // ref 在 template 自动解包 .value 在tsx中不会,所以需要手动.value const flag = ref(false) const data = [{name:'张三'},{name:'张四'},{name:'张五'}] return()=>(<> <div>props: {props?.name}</div> <hr /> {data.map(v=>{ return <div>{v.name}</div> })} </>) } })
函数柯里化
const fn = ()=>{ console.log('触发了'); } return()=>(<> <div>props: {props?.name}</div> <hr /> {data.map(v=>{ // 无效,在创建时就自动触发 return <div onClick={fn()}>{v.name}</div> })} </>) return()=>(<> <div>props: {props?.name}</div> <hr /> {data.map(v=>{ // 函数柯里化 return <div onClick={()=>fn()}>{v.name}</div> })} </>) }
稀里糊涂
const A = (_:any,{slots}:any)=>(<> <div>{slots.default ? slots.default():'默认值'}</div> <div>{slots.foo ? slots.foo?.():'默认值'}</div> </>) interface Props { name?:String } import { defineComponent,ref } from "vue" export default defineComponent ({ props:{ name:String }, emit:['on-click'], setup(props:Props,{emit}) { // ref 在 template 自动解包 .value 在tsx中不会,所以需要手动.value const flag = ref(false) const data = [{name:'张三'},{name:'张四'},{name:'张五'}] const fn = (item:any)=>{ console.log('触发了'); emit('on-click',item) } const slot = { default:()=>(<div>default slots</div>), foo:()=>(<div>foo slots</div>) } return()=>(<> <A v-slots={slot}></A> <hr /> <div>props: {props?.name}</div> <hr /> {data.map(v=>{ return <div onClick={()=>fn(v)}>{v.name}</div> })} </>) } })
-
v-model
import { defineComponent,ref } from "vue" export default defineComponent ({ const v = ref<String>('') return()=>(<> <input type="text" v-model={v.value}/> <div>{v.value}</div> </>) } })
2.函数式编程、h函数
函数式编程,是除 template 以及 JSX 外的第三种方式,主要需要用到 h函数
h函数的三个参数:
- type 元素的类型
- propsOrChildren 数据对象, 这里主要表示(props, attrs, dom props, class 和 style)
- children 子节点
h函数的多种组合方式:
// 除类型之外的所有参数都是可选的
h('div')
h('div', { id: 'foo' })
//属性和属性都可以在道具中使用
//Vue会自动选择正确的分配方式
h('div', { class: 'bar', innerHTML: 'hello' })
// props modifiers such as .prop and .attr can be added
// with '.' and `^' prefixes respectively
h('div', { '.name': 'some-name', '^width': '100' })
// class 和 style 可以是对象或者数组
h('div', { class: [foo, { bar }], style: { color: 'red' } })
// 定义事件需要加on 如 onXxx
h('div', { onClick: () => {} })
// 子集可以字符串
h('div', { id: 'foo' }, 'hello')
//如果没有props是可以省略props 的
h('div', 'hello')
h('div', [h('span', 'hello')])
// 子数组可以包含混合的VNode和字符串
h('div', ['hello', h('span', 'hello')])
案例:
-
使用props传递参数
<template> <Btn text="按钮"></Btn> </template> <script setup lang='ts'> import { h, } from 'vue'; type Props = { text: string } const Btn = (props: Props, ctx: any) => { return h('div', { class: 'p-2.5 text-white bg-green-500 rounded shadow-lg w-20 text-center inline m-1', }, props.text) } </script>
-
接收emit
<template> <Btn @on-click="getNum" text="按钮"></Btn> </template> <script setup lang='ts'> import { h, } from 'vue'; type Props = { text: string } const Btn = (props: Props, ctx: any) => { return h('div', { class: 'p-2.5 text-white bg-green-500 rounded shadow-lg w-20 text-center inline m-1', onClick: () => { ctx.emit('on-click', 123) } }, props.text) } const getNum = (num: number) => { console.log(num); } </script>
-
定义插槽
<template> <Btn @on-click="getNum"> <template #default> 按钮slots </template> </Btn> </template> <script setup lang='ts'> import { h, } from 'vue'; type Props = { text?: string } const Btn = (props: Props, ctx: any) => { return h('div', { class: 'p-2.5 text-white bg-green-500 rounded shadow-lg w-20 text-center inline m-1', onClick: () => { ctx.emit('on-click', 123) } }, ctx.slots.default()) } const getNum = (num: number) => { console.log(num); } </script>
3.插件
3.1.Vue3自动引入插件
-
安装
npm i -D unplugin-auto-import
-
配置
vite.config.ts
import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' import AutoImport from 'unplugin-auto-import/vite' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue(), vueJsx(),AutoImport({ imports:['vue'], dts:'src/auto-import.d.ts' })] // 略... })
-
使用
App.vue
<template> <button @click="flag = !flag">change flag</button> <div>{{ flag }}</div> </template> <script setup lang="ts"> let flag = ref<boolean>(false) </script> <style scoped></style>
4.Vue3 集成 Tailwind CSS
Tailwind CSS 是一个由js编写的CSS 框架 他是基于postCss 去解析的
Tailwind CSS 官网:https://www.tailwindcss.cn/
PostCSS - 是一个用 JavaScript 工具和插件来转换 CSS 代码的工具
PostCSS 官网:https://www.postcss.com.cn/
PostCSS 功能介绍:
- 增强代码的可读性 (利用从 Can I Use 网站获取的数据为 CSS 规则添加特定厂商的前缀。 Autoprefixer 自动获取浏览器的流行度和能够支持的属性,并根据这些数据帮你自动为 CSS 规则添加前缀。)
- 将未来的 CSS 特性带到今天!(PostCSS Preset Env 帮你将最新的 CSS 语法转换成大多数浏览器都能理解的语法,并根据你的目标浏览器或运行时环境来确定你需要的 polyfills,此功能基于 cssdb 实现。)
- 终结全局 CSS(CSS 模块 能让你你永远不用担心命名太大众化而造成冲突,只要用最有意义的名字就行了。)
- 避免 CSS 代码中的错误(通过使用 stylelint 强化一致性约束并避免样式表中的错误。stylelint 是一个现代化 CSS 代码检查工具。它支持最新的 CSS 语法,也包括类似 CSS 的语法,例如 SCSS 。)
postCss 处理 tailWind Css 大致流程:
- 将CSS解析成抽象语法树(AST树)
- 读取插件配置,根据配置文件,生成新的抽象语法树
- 将AST树”传递”给一系列数据转换操作处理(变量数据循环生成,切套类名循环等)
- 清除一系列操作留下的数据痕迹
- 将处理完毕的AST树重新转换成字符串
简略流程:
- PostCSS 配置文件 postcss.config.js,新增 tailwindcss 插件。
- TaiWindCss插件需要一份配置文件,比如:tailwind.config.js。
VSCode 插件推荐:
- Tailwind CSS IntelliSense
示例:
-
安装 Tailwind 以及其它依赖项 ,
autoprefixer
用于加前缀的,CSS 兼容用的那堆npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
-
生成配置文件
postcss.config.js
和tailwind.config.js
npx tailwindcss init -p
-
修改配置文件
tailwind.config.js
具体配置项,建议查看 tailwind 文档
-
2.6 版本
module.exports = { // 打包时对于没有用到的 css类名是不会打包进去的 purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], theme: { extend: {}, }, plugins: [], }
-
3.0 版本
module.exports = { // 打包时对于没有用到的 css类名是不会打包进去的 content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], theme: { extend: {}, }, plugins: [], }
-
-
创建一个
src/tailwind/index.css
随意@tailwind base; @tailwind components; @tailwind utilities;
-
引入
mian.ts
import './index.css'
-
使用
<template> <div class="w-screen h-screen bg-red-600 flex justify-center items-center text-8xl text-slate-200" > hello tailwind </div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' </script> <style scoped></style>
5.使用 Vue 开发移动端
1.开发 Vue 移动端
开发移动端最主要的就是适配各种手机,在之前我们用的是rem 根据HTML font-size 去做缩放 现在有了更好用的vw vh
-
安装依赖
npm install postcss-px-to-viewport -D
-
配置
vite.config.ts
因为vite中已经内联了postcss,所以并不需要额外的创建 postcss.config.js文件
import { fileURLToPath, URL } from 'url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' import postcsspxtoviewport from "postcss-px-to-viewport" //插件 // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue(), vueJsx()], css: { postcss: { plugins: [ postcsspxtoviewport({ unitToConvert: 'px', // 要转化的单位 viewportWidth: 750, // UI设计稿的宽度 unitPrecision: 6, // 转换后的精度,即小数点位数 propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换 viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw selectorBlackList: ['ignore-'], // 指定不转换为视窗单位的类名, minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换 mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false replace: true, // 是否转换后直接更换属性值 landscape: false // 是否处理横屏情况 }) ] } }, resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } })
-
配置声明文件
postcss-px-to-viewport.d.ts
declare module 'postcss-px-to-viewport' { type Options = { unitToConvert: 'px' | 'rem' | 'cm' | 'em', viewportWidth: number, viewportHeight: number, // not now used; TODO: need for different units and math for different properties unitPrecision: number, viewportUnit: string, fontViewportUnit: string, // vmin is more suitable. selectorBlackList: string[], propList: string[], minPixelValue: number, mediaQuery: boolean, replace: boolean, landscape: boolean, landscapeUnit: string, landscapeWidth: number } export default function(options: Partial<Options>):any }
-
引入声明文件
tsconfig.config.json
,用于 Vite{ "extends": "@vue/tsconfig/tsconfig.web.json", "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "postcss-px-to-viewport.d.ts"], "exclude": ["src/**/__tests__/*"], "compilerOptions": { "composite": true, "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } }
-
开发
App.vue
<template> <div class="wraps"> <header class="header"> <div>left</div> <div>中间</div> <div>right</div> </header> <main class="main"> <div class="main-items" v-for="item in 100"> <div class="main-port">头像</div> <div class="main-desc"> <div>小满{{ item }}</div> <div>你妈妈喊你回家穿丝袜啦</div> </div> </div> </main> <footer class="footer"> <div class="footer-items" v-for="item in footer"> <div>{{ item.icon }}</div> <div>{{ item.text }}</div> </div> </footer> </div> </template> <script setup lang="ts"> import { reactive } from 'vue' type Footer<T> = { icon: T text: T } const footer = reactive<Footer<string>[]>([ { icon: '1', text: '首页' }, { icon: '2', text: '商品' }, { icon: '3', text: '信息' }, { icon: '4', text: '我的' } ]) </script> <style lang="less"> @import url('@/assets/base.css'); html, body, #app { height: 100%; overflow: hidden; font-size: 14px; } .wraps { height: inherit; overflow: hidden; display: flex; flex-direction: column; } .header { background-color: pink; display: flex; height: 30px; align-items: center; justify-content: space-around; div:nth-child(1) { width: 40px; } div:nth-child(2) { text-align: center; } div:nth-child(3) { width: 40px; text-align: right; } } .main { flex: 1; overflow: auto; &-items { display: flex; border-bottom: 1px solid #ccc; box-sizing: border-box; padding: 5px; } &-port { background: black; width: 30px; height: 30px; border-radius: 200px; } &-desc { margin-left: 10px; div:last-child { font-size: 10px; color: #333; margin-top: 5px; } } } .footer { border-top: 1px solid #ccc; display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; &-items { display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 5px; } } </style>
5.2.将 Vue 项目打包成APP
-
安装 JDK
-
安装 JDK
官网:https://www.oracle.com/java/technologies/downloads/#java8-windows
一般下载 8 版本
-
配置环境变量
我的电脑 > 属性 > 高级系统设置
-
JAVA_HOME
-
CLASSPATH
.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;
-
Path
%JAVA_HOME%\bin
-
-
终端输入
java
和javac
能正常输出即可
-
-
安卓编辑器安装
-
安装,需要科学上网
官网:https://developer.android.com/
安装过程也不太明白,记得安装 SDK
-
新建项目
加载了巨久
-
安装虚拟机
-
运行项目
布局在
res
里右键
Go To XML
-
粘贴如下代码
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/activity_main" android:layout_width="match_parent" android:orientation="vertical" android:layout_height="match_parent"> <WebView android:id="@+id/web_view" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout >
-
java 逻辑代码
package com.example.myapplication; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.webkit.WebView; import android.app.Activity; import android.webkit.WebViewClient; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //设置一个Activity的显示界面, setContentView(R.layout.activity_main); WebView view = (WebView)findViewById(R.id.web_view); //设置 WebView 属性,能够执行 Javascript 脚本 view.getSettings().setJavaScriptEnabled(true); //加载需要显示的网页 不能使用局域网地址 只能虚拟机专属地址 http://10.0.2.2 端口是我们vue 项目端口 view.loadUrl("http://10.0.2.2:3000"); view.setWebViewClient(new WebViewClient()); } }
-
运行后报错
没有权限,配置权限
manifests/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:usesCleartextTraffic="true" android:theme="@style/Theme.MyOneDemo" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <meta-data android:name="android.app.lib_name" android:value="" /> </activity> </application> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> </manifest>
-
至此,能正常显示
-
打包
-
致此,完成
有安卓手机的,可以安装试试,为了能正常使用,项目需要挂载到服务器上。
-
6.使用 Vue 开发桌面程序Electron
Electron 官网:https://www.electronjs.org/
VSCode 就是 electron 开发的
7.CSS原子化
CSS原子化的优缺点:
- 减少了css体积,提高了css复用
- 减少起名的复杂度
- 增加了记忆成本 将css拆分为原子之后,你势必要记住一些class才能书写,哪怕tailwindcss提供了完善的工具链,你写background,也要记住开头是bg
接入unocss:
最好使用 vite ,webpack 属于阉割版功能很少
-
安装
npm i unocss -D
-
配置
vite.config.ts
import unocss from 'unocss/vite' plugins: [vue(), vueJsx(),unocss({ rules:[ // 静态 ['flex',{display:'flex'}], ['red',{color:'red'}] ] })],
-
引入
main.ts
import 'uno.css'
-
App.vue
示例<template> <div class="flex red">UNOCSS</div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' </script> <style scoped></style>
配置unocss:
-
配置静态css
rules: [ ['flex', { display: "flex" }] ]
-
配置动态css
m-参数*10 例如 m-10 就是 margin:100px
rules: [ [/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 10}px` })], ['flex', { display: "flex" }] ]
使用:
<template> <div class="flex m-1">UNOCSS</div> </template>
-
shortcuts 组合样式
rules: [ [/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 10}px` })], ['flex', { display: "flex" }], ['pink', { color: 'pink' }] ], shortcuts: { btn: "pink flex" }
-
unocss 预设
import {presetIcons,presetUno,presetAttributify} from 'unocss' // 略....... plugins: [vue(), vueJsx(), unoCss({ presets:[presetIcons(),presetUno(),presetAttributify()], rules: [ ], shortcuts: { } })],
-
presetIcons
图标预设,首先我们去icones官网(方便浏览和使用iconify)浏览我们需要的icon,比如这里我用到了Google Material Icons图标集里面的baseline-add-circle图标
-
安装
npm i -D @iconify-json/ic
/ic
ic 是官网里对应的图标库 -
使用
<div class="i-ic-baseline-backspace text-3xl bg-green-500"></div>
-
-
presetAttributify
美化属性用的,属性语义化 无须class
<div class="m-10 flex pink">UNOCSS</div> <!-- 等同于如下 --> <div flex red m="10">UNOCSS</div>
-
presetUno
默认的 @unocss/preset-uno 预设(实验阶段)是一系列流行的原子化框架的 通用超集,包括了 Tailwind CSS,Windi CSS,Bootstrap,Tachyons 等。
集成了这些
<div class="m-10 flex pink bg-red-500">UNOCSS</div>
-
-
theme
主题
8.ESlint
9.Compression
使用gizp缓存
https://blog.csdn.net/weixin_46769087/article/details/130202397
- compression-webpack-plugin
- vite-plugin-compression
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本
述文本描述文本描述文本描述文本描述文本描述文本
述文本描述文本描述文本描述文本描述文本描述文本
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本
查看全部3条回复
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本