文章目录
Vue3
官方文档:https://cn.vuejs.org/
课程链接:【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
-
输入项目名称
-
选择插件
- 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>
-
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 }>() 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>
标签。
-
匿名插槽
-
子组件
Dialog.vue
<template> <div> <header></header> <main class="main"> <solt></solt> </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 动画库
官网:https://animate.style/
-
安装
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 动画库
官网:https://greensock.com/
-
安装
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 中 $on $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:
官网:https://www.iviewui.com/
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>
四.进阶
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:一个对象,在注册指令时作为参数传递。例如,在以下指令中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2kKRZsCH-1692947916424)(Vue3.assets/image-20221212141018388.png)]
-
-
参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 全局状态管理工具
文档: https://pinia.vuejs.org/
特点:
- 完整的 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"
}))
扩展
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
-
新建项目
加载了巨久
-
安装虚拟机
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DUrbWdZ1-1692947916427)(Vue3.assets/image-20221213144211163.png)]
-
运行项目
布局在
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
https://segmentfault.com/a/1190000041954694
https://segmentfault.com/a/1190000042194328
9.Compression
使用gizp缓存
https://blog.csdn.net/weixin_46769087/article/details/130202397
- compression-webpack-plugin
- vite-plugin-compression