文章目录
- vue3概述
- vue环境搭建
- 模版语法&指令
- 虚拟dom和diff算法
- Ref全家桶
- Reactive全家桶
- 认识to系列全家桶
- vue3的响应式原理
- computed的计算属性
- watch监听
- 认识组件&vue3生命周期
- 实操组件和认识less sass 和 scoped
- 父子组件传参
- 全局组件 局部组件 递归组件
- 动态组件
- 插槽slot
- 异步组件&代码分包&suspense
- Teleport传送组件
- keep-alive缓存组件
- transition动画组件
- transition-group过渡列表
- 依赖注入Provide/Inject
- 兄弟组件穿参&Bus
- Mitt
- TSX
- v-model深入
- 自定义指令
- 自定义hooks
- vite打包和发布npm包
- vue3定义全局函数和变量
- 自定义vue插件
- ui库ElementUI,AntDesigin等
- 样式穿透和scoped
- CSS完整新特性
- vue3集成Tailwind CSS
- Event Loop 和 nextTick
- 移动端ionic
- h5适配
- unoCss原子化
- 函数式编程,h函数
- electron桌面程序
- 编译宏
- 环境变量
- webpack构建vue3项目
- vue3的性能优化
- web Components
- proxy跨域
小满Vue3(第一章 Vue3概述-梦开始的地方)_哔哩哔哩_bilibili
vue3概述
1.vue2和3区别
vue2风格:选项式API
写起来比较分散
vue3风格:组合式API
选择:
- 如果不使用构建工具,在低复杂度场景中使用vue,就用选项式api。
- 打算用vue构建完整的单页应用,推荐采用组合式API+单文件组件
2.vue3新特性
- 重写双向绑定
- VDOM性能瓶颈
- Fragments
- Tree-Shaking的支持
- Composition API
重写双向绑定
vue2
基于Object.defineProperty()实现
vue3 基于Proxy
proxy与Object.defineProperty(obj, prop, desc)方式相比有以下优势:
//丢掉麻烦的备份数据
//省去for in 循环
//可以监听数组变化
//代码更简化
//可以监听动态新增的属性;
//可以监听删除的属性 ;
//可以监听数组的索引和 length 属性;
let proxyObj = new Proxy(obj,{
get : function (target,prop) {
return prop in target ? target[prop] : 0
},
set : function (target,prop,value) {
target[prop] = 888;
}
})
3. Vue3 优化Vdom
在Vue2中,每次更新diff,都是全量对比,Vue3则只对比带有标记的,这样大大减少了非动态内容的对比消耗
Vue Template Explorer 我们可以通过这个网站看到静态标记
patch flag 优化静态树
<span>Hello world!</span>
<span>Hello world!</span>
<span>Hello world!</span>
<span>Hello world!</span>
<span>{{msg}}</span>
<span>Hello world!</span>
<span>Hello world! </span>
Vue3
编译后的 Vdom
是这个样子的
export function render(_ctx,_cache,$props,$setup,$data,$options){return (_openBlock(),_createBlock(_Fragment,null,[
_createvNode( "span", null,"Hello world ! "),
_createvNode( "span",null,"Hello world! "),
_createvNode( "span",null,"Hello world! "),
_createvNode( "span", null,"Hello world! "),
_createVNode("span", null,_toDisplaystring(_ctx.msg),1/* TEXT */),
_createvNode( "span", null,"Hello world! "),
_createvNode( "span", null,"Hello world! ")],64/*STABLE_FRAGMENT */))
新增了 patch flag 标记
TEXT = 1 // 动态文本节点
CLASS=1<<1,1 // 2//动态class
STYLE=1<<2,// 4 //动态style
PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
FULLPR0PS=1<<4,// 16 //具有动态key属性,当key改变时,需要进行完整的diff比较。
HYDRATE_ EVENTS = 1 << 5,// 32 //带有监听事件的节点
STABLE FRAGMENT = 1 << 6, // 64 //一个不会改变子节点顺序的fragment
KEYED_ FRAGMENT = 1 << 7, // 128 //带有key属性的fragment 或部分子字节有key
UNKEYED FRAGMENT = 1<< 8, // 256 //子节点没有key 的fragment
NEED PATCH = 1 << 9, // 512 //一个节点只会进行非props比较
DYNAMIC_SLOTS = 1 << 10 // 1024 // 动态slot
HOISTED = -1 // 静态节点
BALL = -2
我们发现创建动态 dom 元素的时候,Vdom 除了模拟出来了它的基本信息之外,还给它加了一个标记: 1 /* TEXT */
这个标记就叫做 patch flag(补丁标记)
patch flag 的强大之处在于,当你的 diff 算法走到 _createBlock 函数的时候,会忽略所有的静态节点,只对有标记的动态节点进行对比,而且在多层的嵌套下依然有效。
尽管 JavaScript 做 Vdom 的对比已经非常的快,但是 patch flag 的出现还是让 Vue3 的 Vdom 的性能得到了很大的提升,尤其是在针对大组件的时候。
4.Vue3 Fragment
vue3 允许我们支持多个根节点
<template>
<div>12</div>
<div>23</div>
</template>
同时支持render JSX 写法
render() {
return (
<>
{this.visable ? (
<div>{this.obj.name}</div>
) : (
<div>{this.obj.price}</div>
)}
<input v-model={this.val}></input>
{[1, 2, 3].map((v) => {
return <div>{v}-----</div>;
})}
</>
);
},
同时新增了Suspense teleport 和 多 v-model 用法
5.vue3 Tree shaking
简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码
在Vue2中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是Vue实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到
而Vue3源码引入tree shaking特性,将全局 API 进行分块。如果你不使用其某些功能,它们将不会包含在你的基础包中
就是比如你要用watch 就是import {watch} from ‘vue’ 其他的computed 没用到就不会给你打包减少体积
6.Composition Api
Setup语法糖式编程
例如 ref reactive watch computed toRefs toRaws
vue环境搭建
1.创建vue项目
方法一:使用vite构建
npm init vite@latest
方法二:使用vue脚手架
1.npm init vue@latest //这命令会安装并执行create-vue
运行vue项目
npm run dev
2.开发环境
推荐的IDE配置是Visual Studio Code
+Volar
扩展(适合vue3)
模版语法&指令
v-html
<template>
<div>
<p>{{ rawHtml }}</p>//显示文本
<p v-html="rawHtml"></p> //显示标签
</div>
</template>
<script>
export default {
data () {
return {
rawHtml: "<a href='https://baidu.com'>百度</a>"
}
}
}
</script>
<style scoped>
</style>
虚拟dom和diff算法
1.虚拟dom
虚拟DOM就是通过JS来生成一个AST节点树(抽象语法树)
ts转js的时候,也会进行AST转换
babel插件,es6转es5到时候,也会经过AST这个抽象语法树到转换
js通过v8引擎转这个字节码的时候,也会进行AST。
为什么要有虚拟DOM?
案例
let div = document.createElement('div')
let str = ''
for (const key in div) {
str += key + ''
}
console.log(str)
发现一个dom上面的属性是非常多的。
所以直接操作DOM非常浪费性能
解决方案就是 我们可以用JS
的计算性能来换取操作DOM
所消耗的性能,既然我们逃不掉操作DOM
这道坎,但是我们可以尽可能少的操作DOM
操作JS是非常快的
2.Diff算法
<template>
<div>
<div :key="item" v-for="(item) in Arr">{{ item }}</div>
</div>
</template>
<script setup lang="ts">
const Arr: Array<string> = ['A', 'B', 'C', 'D']
Arr.splice(2,0,'DDD')
</script>
<style>
</style>
splice 用法
![image-20230905081704453](https://img-blog.csdnimg.cn/img_convert/7fc1c1aa00214be4c472f41216fdc15b.png)
Ref全家桶
1.ref
接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value
property,指向该内部值。
案例
我们这样操作是无法改变message 的值 应为message 不是响应式的无法被vue 跟踪要改成ref
<template>
<div>
<button @click="changeMsg">change</button>
<div>{{ message }}</div>
</div>
</template>
<script setup lang="ts">
let message: string = "我是message"
const changeMsg = () => {
message = "change msg"
}
</script>
<style>
</style>
改为ref
Ref TS对应的接口
interface Ref<T> {
value: T
}
注意被ref包装之后需要.value 来进行赋值.
这样点击修改,页面上值也被修改了(响应式)
<template>
<div>
{{ Man }}
</div>
<hr>
<button @click="changeValue">修改</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const Man = ref({ name: "sz" }) //类型推断
const changeValue = () => {
Man.value.name = 'sssszzz'
console.log(Man);
}
</script>
<style scoped>
</style>
2.isRef
判断是不是一个ref对象
import { ref, Ref,isRef } from 'vue'
let message: Ref<string | number> = ref("我是message")
let notRef:number = 123
const changeMsg = () => {
message.value = "change msg"
console.log(isRef(message)); //true
console.log(isRef(notRef)); //false
}
3.shallowRef
浅层响应式
ref是深层次
创建一个跟踪自身 .value
变化的 ref,但不会使其值也变成响应式的
例子
修改其属性是非响应式的这样是不会改变的
import { ref,type Ref,shallowRef} from 'vue'
type M = {
name:string
}
let Man:Ref<M> = shallowRef({ name: "sz" })
const changeValue = () => {
Man.value.name = 'sssszzz'//这样赋值,只改变了值,但是页面并没有发生改变。
console.log(Man);
}
它指到这个.value
,所以要从value开始赋值
Man.value={
name:'sssszzzzzz'
}
ref
和shallowRef
不能混用,会影响
import { ref,shallowRef} from 'vue'
let Man = ref({name:"sz2"})
let Man2 = shallowRef({ name: "sz" })
const changeValue = () => {
Man.value.name = '我是ref'
Man2.value.name='sz1232'
}
点击改变之后,发现页面shallowRef也会跟着变,按理来说这种写法页面是不会变的
4. triggerRef
强制更新页面DOM
import { shallowRef,triggerRef} from 'vue'
let Man2 = shallowRef({ name: "sz" })
const changeValue = () => {
Man2.value.name = '我被影响了'
triggerRef(Man2)
console.log(Man2);
}
这样数据和dom都修改了
5. customRef
自定义ref (自己实现一个ref)
customRef 是个工厂函数要求我们返回一个对象 并且实现 get 和 set 适合去做防抖之类的
<template>
<div>
customRef:
{{ obj }}
</div>
</template>
<script setup lang="ts">
import {customRef } from 'vue'
function MyRef<T>(value: T) {
return customRef((track,trigger) => {
return {
get() {
//收集依赖
track()
return value
},
set(newValue) {
//防抖
clearTimeout(timer)
timer = setTimeout(() => {
//触发依赖
console.log('触发了');//点击多次,只触发一次
value = newValue
timer = null
trigger()
},500)
}
}
})
}
const obj = MyRef<string>('szaaa')
const changeValue = () => {
obj.value = 'customRef修改了'
console.log(obj);
}
</script>
<style scoped>
</style>
6.ref小知识
1.自定义格式工具
在浏览器 设置-》偏好设置-》启用自定义格式设置工具(Enable custom formatters)
2.利用ref读取dom属性
<template>
<div ref="dom">
我是dom
</div>
</template>
<script setup lang="ts">
import { ref, type Ref, shallowRef, triggerRef, customRef } from 'vue'
const dom = ref<HTMLDivElement>()
const changeValue = () => {
console.log(dom.value?.innerText);
}
</script>
<style scoped>
</style>
Reactive全家桶
1.reactive
ref支持所有的类型
reactive只支持引用类型
ref取值和赋值都需要加 .value
,reactive不需要
对象
<template>
<div>
<form >
<input v-model="form.name" type="text">
<br>
<input v-model="form.age" type="text" >
<br>
<!-- 因为因为button在form里 要加阻止默认的提交事件 -->
<button @click.prevent="submit">提交</button>
</form>
</div>
</template>
<script setup lang="ts">
import { ref, reactive} from 'vue'
//ref reactive
//ref 支持所有的类型
type M = {
name: string,
age:number
}
let form = reactive<M>({
name: 'sz',
age:18
})
const submit = () => {
console.log(form);
}
</script>
<style scoped>
</style>
数组
<template>
<div>
<ul>
<li v-for="item in lists">{{ item }}</li>
</ul>
<button @click="add">添加</button>
</div>
</template>
<script setup lang="ts">
import { ref, reactive} from 'vue'
let lists = reactive<string[]>([])
const add = () => {
lists.push('sz')
}
</script>
<style scoped>
</style>
但是注意,如果是这种场景,调用一个接口,去获取数组里面的值.
import { ref, reactive} from 'vue'
let lists = reactive<string[]>([])
const add = () => {
setTimeout(() => {
let res = ['sz', 'aaa', 'bbb']
//不可以进行直接赋值
lists = res;
console.log(lists);
})
}
这个你会发现点击添加,控制台会打印,但是页面没有任何变化。
因为reactive是proxy代理的对象,你直接赋值 会把proxy代理对象进行一个覆盖。
所以不能对他进行一个直接赋值,否则会破坏响应式对象
解决方法:
1.可以用结构赋值解决这个问题
lists.push(...res)
2.添加一个对象 内部添加一个属性arr数组进行赋值
<template>
<div>
<ul>
<li v-for="item in lists.arr">{{ item }}</li>
</ul>
<button @click="add">添加</button>
</div>
</template>
<script setup lang="ts">
import { ref, reactive} from 'vue'
let lists = reactive<{ arr: string[] }> ({
arr:[]
})
const add = () => {
setTimeout(() => {
let res = ['sz', 'aaa', 'bbb']
lists.arr = res
console.log(lists.arr);
})
}
</script>
<style scoped>
</style>
2.readonly
将reactive值变成只读的
import { ref, reactive,readonly} from 'vue'
let obj = reactive({ name: 'sz' })
const read = readonly(obj)
read.name='asaaaa'//报错,只读属性不能赋值
但是readonly是会受reactive影响
<template>
<div>
<button @click="show">添加</button>
</div>
</template>
<script setup lang="ts">
import { ref, reactive,readonly} from 'vue'
let obj = reactive({ name: 'sz' })
const read = readonly(obj)
const show= () => {
obj.name = 'sssssaaa'//会影响到readonly
console.log(obj,read);
}
</script>
<style scoped>
</style>
3.shallowReactive
浅层响应式
<template>
<div>
<div>{{ obj2 }}</div>
<button @click="edit">修改</button>
</div>
</template>
<script setup lang="ts">
import { reactive, shallowReactive,} from 'vue'
let obj = reactive({ name: 'sz' })
const obj2 = shallowReactive({
foo: {
bar: {
num:1
}
}
})
const edit= () => {
obj2.foo.bar.num = 456
console.log(obj2);
}
</script>
<style scoped>
</style>
发现数据改变,但是页面没变
它只到第一层属性这 foo
它跟ref问题一样.
shallowReactive
也会被reactive
影响。会导致页面更新
设计:每次ref或者reactive去修改,它都会将这个模版里面(template)数据更新到最新的。
reactive
都是通过proxy
代理的
认识to系列全家桶
1.toRef
只能修改响应式对象的值
非响应式对象视图毫无变化。
<template>
<div>toref:{{ like }}</div>
<div>
<button @click="change">修改</button>
</div>
</template>
<script setup lang="ts">
import { toRef,toRefs,toRaw} from 'vue'
const man = { name: 'sz', age: 22, like: 'jk' }
const like = toRef(man,'like')
const change = () => {
like.value='aaaaa'//触发事件的时候,值改了,视图没有改变
console.log(like);
}
</script>
<style scoped>
</style>
如果把对象变成响应式的
const man = reactive({ name: 'sz', age: 22, like: 'jk' })
数据改变就会驱动着视图改变
应用场景:
const man = reactive({ name: 'sz', age: 22, like: 'jk' })
useDemo(toRef(man,'like'))//接受一个属性,但是上面是reactive对象。通过toref把属性解构出来赋值给他
弹幕:1.toRef比较常用的方法就是解构赋值
2.toRef可以理解深拷贝一个属性出去,具备.value特性,修改也和原数据无影响,测试不会立即渲染到视图,需要触发一次修改
2.toRefs
可以帮我们批量创建ref对象主要是方便我们解构使用
import { reactive, toRefs } from 'vue'
const obj = reactive({
foo: 1,
bar: 1
})
let { foo, bar } = toRefs(obj)
foo.value++
console.log(foo, bar);
弹幕:reactive一旦解构,响应式就不存在了,所以需要这个
3.toRaw
import {toRaw,reactive} from 'vue'
const man = reactive({ name: 'sz', age: 22, like: 'jk' })
const change = () => {
console.log(man,toRaw(man));
}
打印出来,一个是Proxy对象,通过toRaw
之后,它变成一个原始对象
当一个对象你不想让他响应式,就可以用这个。
vue3的响应式原理
Vue2 使用的是 Object.defineProperty
vue3 使用的是 Proxy
2.0的不足
对象只能劫持 设置好的数据,新增的数据需要Vue.Set(xxx) 数组只能操作七种方法,修改某一项值无法劫持。数组的length修改也无法劫持
reactive和effect的实现
export const reactive = <T extends object>(target:T) => {
//es6新增的proxy对象,代理对象,将对象的操作在proxy的handle中操作。
return new Proxy(target,{
get (target,key,receiver) {
const res = Reflect.get(target,key,receiver) as object
return res
},
set (target,key,value,receiver) {
const res = Reflect.set(target,key,value,receiver)
return res
}
})
}
effect track trigger
实现effect 副作用函数
let activeEffect;
export const effect = (fn:Function) => {
//闭包
const _effect = function () {
activeEffect = _effect;
fn()
}
_effect()
}
使用一个全局变量 active 收集当前副作用函数,并且初始化的时候调用一下
实现track
const targetMap = new WeakMap()
export const track = (target,key) =>{
let depsMap = targetMap.get(target)
if(!depsMap){
depsMap = new Map()
targetMap.set(target,depsMap)
}
let deps = depsMap.get(key)
if(!deps){
deps = new Set()
depsMap.set(key,deps)
}
deps.add(activeEffect)
}
执行完成后,得到下面的数据结构
![image-20230912074951535](/Users/shizheng/Desktop/sz/图片/typoraPictures/image-20230912074951535.png)
实现trigger
export const trigger = (target,key) => {
const depsMap = targetMap.get(target)
const deps = depsMap.get(key)
deps.forEach(effect=>effect())
}
当我们进行赋值的时候会调用 set 然后 触发收集的副作用函数
import {track,trigger} from './effect'
export const reactive = <T extends object>(target:T) => {
return new Proxy(target,{
get (target,key,receiver) {
const res = Reflect.get(target,key,receiver) as object
track(target,key)
return res
},
set (target,key,value,receiver) {
const res = Reflect.set(target,key,value,receiver)
trigger(target,key)
return res
}
})
}
给 reactive 添加这两个方法
测试代码
<!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>Document</title>
</head>
<body>
<div id="app">
</div>
<script type="module">
import { reactive } from './reactive.js'
import { effect } from './effect.js'
const user = reactive({
name: "sz",
age: 18
})
effect(() => {
document.querySelector('#app').innerText = `${user.name} - ${user.age}`
})
setTimeout(()=>{
user.name = 'sz很吊'
setTimeout(()=>{
user.age = '23'
},1000)
},2000)
</script>
</body>
</html>
递归实现reactive
import { track, trigger } from './effect'
const isObject = (target) => target != null && typeof target == 'object'
export const reactive = <T extends object>(target: T) => {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver) as object
track(target, key)
if (isObject(res)) {
return reactive(res)
}
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
trigger(target, key)
return res
}
})
}
<!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>Document</title>
</head>
<body>
<div id="app">
</div>
<script type="module">
import { reactive } from './reactive.js'
import { effect } from './effect.js'
const user = reactive({
name: "小满",
age: 18,
foo:{
bar:{
sss:123
}
}
})
effect(() => {
document.querySelector('#app').innerText = `${user.name} - ${user.age}-${user.foo.bar.sss}`
})
setTimeout(()=>{
user.name = '大满很吊'
setTimeout(()=>{
user.age = '23'
setTimeout(()=>{
user.foo.bar.sss = 66666666
},1000)
},1000)
},2000)
</script>
</body>
</html>
computed的计算属性
1.computed用法
计算属性就是当依赖的属性的值发生变化的时候,才会触发他的更改,如果依赖的值,不发生变化的时候,使用的是缓存中的属性值。
1.函数形式
//只能支持一个getter函数,不允许修改值。只能进行读取
import { computed, reactive, ref } from 'vue'
let price = ref(0)//$0
let m = computed<string>(()=>{
return `$` + price.value
})
price.value = 500
2.对象形式(可以修改值)
支持传入一个对象,在对象里面要求实现get和set两个函数。
get就是读取值的一个操作,set就是设置值的一个操作。
<template>
<div>{{ mul }}</div>
<div @click="mul = 100">click</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
let price = ref<number | string>(1)//$0
let mul = computed({
get: () => {
return price.value
},
set: (value) => {
price.value = 'set' + value
}
})
</script>
<style>
</style>
2.购物车实战
实战用法:
购物车+搜索
<template>
<div>
<div>
<input v-model="keyWord" type="text" placeholder="搜索">
</div>
<div style="margin-top: 20px;">
<table border width="600" cellpadding="0" cellspacing="0">
<thead>
<tr>
<th>物品名称</th>
<th>物品单价</th>
<th>物品数量</th>
<th>物品总价</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item,index) in searchData">
<td align="center">{{ item.name }}</td>
<td align="center">{{ item.price }}</td>
<td align="center"><button @click="item.num > 1 ? (item.num--):null">-</button>
{{ item.num }}
<button @click="item.num<99 ? (item.num++) : null">+</button></td>
<td align="center">{{ item.num * item.price }}</td>
<td align="center"><button @click="del(index)">删除</button></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="5" align="right">
总价:{{total }}
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive,computed} from 'vue'
// let $total = ref<number>(0)
let keyWord =ref<string>("")
interface Data {
name: string,
price: number,
num:number
}
let data = reactive<Data[]>([
{
name: 'sz的帽子',
price: 500,
num:1
},
{
name: 'sz的衣服',
price: 600,
num:1
},
{
name: 'sz的裤子',
price: 700,
num:1
}
])
// const total = () => {
// $total.value = data.reduce((prev:number,next:Data) => {
// return prev + next.num * next.price
// },0)
// }
const total = computed(() => {
return searchData.value.reduce((prev: number, next: Data) => {
return prev + next.num * next.price
},0)
})
const searchData = computed(() => {
return data.filter((item: Data) => {
return item.name.includes(keyWord.value)
})
})
const del = (index: number) => {
data.splice(index,1)
}
</script>
<style scoped>
</style>
3.手写源码
小满Vue3(第九章 computed计算属性-精讲)_哔哩哔哩_bilibili
(1) effect.ts
interface Options {
scheduler?: Function
}
let activeEffect;
export const effect = (fn: Function,options:Options) => {
const _effect = function () {
activeEffect = _effect;
let res= fn()
return res
}
_effect.options = options
_effect()
return _effect
}
const targetMap = new WeakMap()
export const track = (target, key) => {
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(activeEffect)
}
export const trigger = (target, key) => {
const depsMap = targetMap.get(target)
const deps = depsMap.get(key)
deps.forEach(effect => {
if(effect?.options?.scheduler){
effect?.options?.scheduler?.()
}else{
effect()
}
})
}
(2) reactive.ts
import { track, trigger } from './effect'
const isObject = (target) => target != null && typeof target == 'object'
export const reactive = <T extends object>(target: T) => {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver) as object
track(target, key)
if (isObject(res)) {
return reactive(res)
}
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
trigger(target, key)
return res
}
})
}
(3) computed.ts
import { effect } from './effect'
export const computed = (getter: Function) => {
let _value = effect(getter, {
scheduler: () => { _dirty = true }
})
let catchValue
let _dirty = true
class ComputedRefImpl {
get value() {
if (_dirty) {
catchValue = _value()
_dirty = false;
}
return catchValue
}
}
return new ComputedRefImpl()
}
html
<!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>Document</title>
</head>
<body>
<script type="module">
import {computed} from './computed.js'
import {reactive} from './reactive.js'
window.a = reactive({name: 'a', age: 18})
window.b = computed(() => {
console.log('重新计算')
return a.age + 10
})
</script>
</body>
</html>
watch监听
1.普通watch监听
watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用
watch第一个参数监听源
watch第二个参数回调函数cb(newVal,oldVal)
watch第三个参数一个options配置项是一个对象{
immediate:true //是否立即调用一次
deep:true //是否开启深度监听
}
监听Ref 案例
import { ref, watch } from 'vue'
let message = ref({
nav:{
bar:{
name:""
}
}
})
watch(message, (newVal, oldVal) => {
console.log('新的值----', newVal);
console.log('旧的值----', oldVal);
},{
immediate:true,//立即执行一次
deep:true//深度监听 如果是reactive默认是已经开启了。不需要手动开启
})
监听多个ref 注意变成数组啦
import { ref, watch ,reactive} from 'vue'
let message = ref('')
let message2 = ref('')
watch([message,message2], (newVal, oldVal) => {
console.log('新的值----', newVal);
console.log('旧的值----', oldVal);
})
监听Reactive
使用reactive监听深层对象开启和不开启deep 效果一样
import { ref, watch ,reactive} from 'vue'
let message = reactive({
nav:{
bar:{
name:""
}
}
})
watch(message, (newVal, oldVal) => {
console.log('新的值----', newVal);
console.log('旧的值----', oldVal);
}{
immediate:true,//立即执行一次
deep:true//深度监听 如果是reactive默认是已经开启了。不需要手动开启
flush:"pre"//组件更新之前调用, sync同步执行 post组件更新之后执行
})
案例2 监听reactive 单一值
import { ref, watch ,reactive} from 'vue'
let message = reactive({
name:"",
name2:""
})
watch(()=>message.name, (newVal, oldVal) => {
console.log('新的值----', newVal);
console.log('旧的值----', oldVal);
})
2.watchEffect高级侦听器
watchEffect
开始的时候就调用了一下
import { ref, watchEffect} from 'vue'
let message = ref<string>('A')
let message2 = ref<string>('B')
watchEffect((oninvalidate) => {
console.log('message====>', message.value);
console.log('message2===>',message2.value);
oninvalidate(() => { //在触发改变时,先执行这个回调函数
console.log('before');
})
})
比如加一个按钮来停止监听
const stop = watchEffect((oninvalidate) => {
console.log('message====>', message.value);
console.log('message2===>',message2.value);
oninvalidate(() => {
console.log('before');
})
})
const stopWatch = () => stop()
弹幕说:以函数形式调用watchEffect会返回一个停止监听函数,调用了就不会监听了
更多的配置项
副作用刷新时机 flush 一般使用post
onTrigger 可以帮助我们调试 watchEffect
import { watchEffect, ref } from 'vue'
let message = ref<string>('')
let message2 = ref<string>('')
watchEffect((oninvalidate) => {
//console.log('message', message.value);
oninvalidate(()=>{
})
console.log('message2', message2.value);
},{
flush:"post",//dom加载完之后,就可以取到这个dom
onTrigger () {
}
})
认识组件&vue3生命周期
子组件A.vue
<template>
<h2>A</h2>
</template>
<script setup lang="ts">
</script>
<style scoped></style>
父组件App.vue
<template>
<div>
<h1>生命周期</h1>
<A></A>
</div>
<!-- <br /> -->
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import A from './components/A.vue'
</script>
<style scoped></style>
beforeCreate
created
setup语法糖模式是没有这两个生命周期的。 setup去代替
<script setup lang="ts">
import { ref, onBeforeMount,onMounted,onBeforeUpdate,onUpdated,onBeforeUnmount,onUnmounted} from 'vue'
</script>
onBeforeMount() ------创建之前
在组件DOM实际渲染安装之前调用。在这一步中,根元素还不存在。
onMounted() -------创建完成
在组件的第一次渲染后调用,该元素现在可用,允许直接DOM访问
onBeforeUpdate() ----- 更新之前
数据更新时调用,发生在虚拟 DOM 打补丁之前。(获取的是更新之前的dom)
onUpdated() -------更新完成
DOM更新后,updated的方法即会调用。(获取更新之后的)
onBeforeUnmount() -----销毁之前
在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。
onUnmounted() ------销毁完成
卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。
选项式 API | Hook inside setup |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
activated | onActivated |
deactivated | onDeactivated |
使用v-show并不会销毁组件,v-show是样式的隐藏,v-if 却是重新渲染
实操组件和认识less sass 和 scoped
1)less
概览
什么是less
Less (Leaner Style Sheets 的缩写) 是一门向后兼容的 CSS 扩展语言。这里呈现的是 Less 的官方文档(中文版),包含了 Less 语言以及利用 JavaScript 开发的用于将 Less 样式转换成 CSS 样式的 Less.js 工具。
因为 Less 和 CSS 非常像,因此很容易学习。而且 Less 仅对 CSS 语言增加了少许方便的扩展,这就是 Less 如此易学的原因之一。
官方文档 Less 快速入门 | Less.js 中文文档 - Less 中文网
sass 和 less 一样 都是css预处理器
官方文档 Sass教程 Sass中文文档 | Sass中文网
在vite中使用less | sass
npm install less -D 安装即可
npm install sass -D 安装即可
在style标签注明即可
<style lang="less">
</style>
<style lang="scss">
</style>
2) scoped
实现组件的私有化, 当前style属性只属于当前模块.
在DOM结构中可以发现,vue通过在DOM结构以及css样式上加了唯一标记,达到样式私有化,不污染全局的作用,
![image-20230926222051471](https://img-blog.csdnimg.cn/img_convert/4b61baccd920ebc39ae7e54f758452d3.png)
*样式穿透问题学到第三方组件精讲 ::v-deep >>> /deep/ :deep()*
3) bem架构
他是一种css架构 oocss 实现的一种 (面向对象css) ,BEM
实际上是block
、element
、modifier
的缩写,分别为块层、元素层、修饰符层,element UI 也使用的是这种架构
BEM 命名约定的模式是:
.block {}
.block__element {}
.block--modifier {}
使用sass 最小单元复刻一个bem 架构
bem.scss文件
$block-sel: "-" !default;
$element-sel: "__" !default;
$modifier-sel: "--" !default;
$namespace:'sz' !default;
@mixin bfc {
height: 100%;
overflow: hidden;
}
//混入
@mixin b($block) {
$B: $namespace + $block-sel + $block; //变量
.#{$B}{ //插值语法#{}
@content; //内容替换
}
}
@mixin flex {
display: flex;
}
@mixin e($element) {
$selector:&;
@at-root { //跳出嵌套
#{$selector + $element-sel + $element} {
@content;
}
}
}
@mixin m($modifier) {
$selector:&;
@at-root {
#{$selector + $modifier-sel + $modifier} {
@content;
}
}
}
app.vue
<template>
<div class="sz-test">
sz
<div class="sz-test__inner">el</div>
<div class="sz-test--success">test</div>
</div>
<!-- <br /> -->
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import A from './components/A.vue'
</script>
<style lang="scss">
@include b(test) {
color: red;
@include e(inner) {
color: blue;
}
@include m(success) {
color: green
}
}
</style>
全局扩充sass
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
css: {
preprocessorOptions: {
scss: {
additionalData: "@import './src/bem.scss';"
}
}
}
})
Vue 组件用法
<template>
<div class="xm-wraps">
<div>
<Menu></Menu>
</div>
<div class="xm-wraps__right">
<Header></Header>
<Content></Content>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from "vue"
import Menu from './Menu/index.vue'
import Content from './Content/index.vue'
import Header from './Header/index.vue'
</script>
<style lang="scss" scoped>
@include b('wraps'){
@include bfc;
@include flex;
@include e(right){
flex:1;
display: flex;
flex-direction: column;
}
}
</style>
父子组件传参
1.父传子
父组件通过v-bind绑定一个数据,然后子组件通过defineProps接受传过来的值,
如以下代码
给Menu组件 传递了一个title 字符串类型是不需要v-bind
<template>
<div class="layout">
<Menu title="我是标题"></Menu>
<div class="layout-right">
<Header></Header>
<Content></Content>
</div>
</div>
</template>
传递非字符串类型需要加v-bind 简写 冒号
父组件:
<template>
<div class="layout">
<Menu v-bind:data="data" title="我是标题"></Menu>
<div class="layout-right">
<Header></Header>
<Content></Content>
</div>
</div>
</template>
<script setup lang="ts">
import Menu from './Menu/index.vue'
import Header from './Header/index.vue'
import Content from './Content/index.vue'
import { reactive } from 'vue';
const data = reactive<number[]>([1, 2, 3])
</script>
子组件接受值
通过defineProps 来接受 defineProps是无须引入的直接使用即可
如果我们使用的TypeScript
可以使用传递字面量类型的纯类型语法做为参数
如 这是TS特有的
<template>
<div class="menu">
菜单区域 {{ title }}
<div>{{ data }}</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
title:string,
data:number[]
}>()
</script>
如果你使用的不是TS
defineProps({
title:{
default:"",
type:string
},
data:Array
})
TS 特有的默认值方式
withDefaults是个函数也是无须引入开箱即用接受一个props函数第二个参数是一个对象设置默认值
type Props = {
title?: string,
data?: number[]
}
withDefaults(defineProps<Props>(), {
title: "张三",
data: () => [ 1, 2, 3]
})
2.子传父
子组件A.vue
<template>
<h2>子组件</h2>
<button @click="send">给父组件传值</button>
</template>
<script setup lang="ts">
const emit = defineEmits(['on-click'])
const send = () => {
emit('on-click', 'sz子数据')
}
</script>
<style scoped></style>
使用ts
const emit = defineEmits<{ (e: "on-click", name: string): void }>()
父组件
<template>
<A @on-click="getName"></A>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import A from './components/A.vue'
let name = 'sz'
const getName = (name: string) => {
console.log(name, '---------->我是父组件');
}
</script>
<style lang="scss"></style>
3.子属性暴露给父
子组件暴露给父组件内部属性 -> 通过defineExpose
我们从父组件获取子组件实例 -> 通过ref
子组件:
<template>
<h2>子组件</h2>
</template>
<script setup lang="ts">
defineExpose({
name: 'sz',
open: () => console.log(1)
})
</script>
<style scoped></style>
父组件
<template>
<A ref="Achildren"></A>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import A from './components/A.vue'
const Achildren = ref<InstanceType<typeof A>>()
onMounted(() => {
console.log(Achildren.value?.name);//在onmounted里面打印不如是undefined
Achildren.value?.open()
})
</script>
<style lang="scss"></style>
4.案例:封装瀑布流组件
父组件
<template>
<waterFallVue :list="list"></waterFallVue>
</template>
<script setup lang='ts'>
import { ref, reactive } from 'vue'
import waterFallVue from './components/water-fall.vue';
const list = [
{
height: 300,
background: 'red'
},
{
height: 400,
background: 'pink'
},
{
height: 500,
background: 'blue'
},
{
height: 200,
background: 'green'
},
{
height: 300,
background: 'gray'
},
{
height: 400,
background: '#CC00FF'
},
{
height: 200,
background: 'black'
},
{
height: 100,
background: '#996666'
},
{
height: 500,
background: 'skyblue'
},
{
height: 300,
background: '#993366'
},
{
height: 100,
background: '#33FF33'
},
{
height: 400,
background: 'skyblue'
},
{
height: 200,
background: '#6633CC'
},
{
height: 300,
background: '#666699'
},
{
height: 300,
background: '#66CCFF'
},
{
height: 300,
background: 'skyblue'
},
{
height: 200,
background: '#CC3366'
},
{
height: 200,
background: '#CC9966'
},
{
height: 200,
background: '#FF00FF'
},
{
height: 500,
background: '#990000'
},
{
height: 400,
background: 'red'
},
{
height: 100,
background: '#999966'
},
{
height: 200,
background: '#CCCC66'
},
{
height: 300,
background: '#FF33FF'
},
{
height: 400,
background: '#FFFF66'
},
{
height: 200,
background: 'red'
},
{
height: 100,
background: 'skyblue'
},
{
height: 200,
background: '#33CC00'
},
{
height: 300,
background: '#330033'
},
{
height: 100,
background: '#0066CC'
},
{
height: 200,
background: 'skyblue'
},
{
height: 100,
background: '#006666'
},
{
height: 200,
background: 'yellow'
},
{
height: 300,
background: 'yellow'
},
{
height: 100,
background: '#33CCFF'
},
{
height: 400,
background: 'yellow'
},
{
height: 400,
background: 'yellow'
},
{
height: 200,
background: '#33FF00'
},
{
height: 300,
background: 'yellow'
},
{
height: 100,
background: 'green'
}
]
</script>
<style lang='scss'>
#app,
html,
body {
height: 100%;
}
* {
padding: 0;
margin: 0;
}
</style>
子组件:
<template>
<div class="wraps">
<div :style="{height:item.height+'px',background:item.background,top:item.top+'px',left:item.left + 'px'}"
v-for="item in waterList" class="items"></div>
</div>
</template>
<script setup lang='ts'>
import { ref, reactive, onMounted } from 'vue'
const props = defineProps<{
list: any[]
}>()
const waterList = reactive<any[]>([])
const init = () => {
const heightList: any[] = []
const width = 130;
const x = document.body.clientWidth
const column = Math.floor(x / width)
for (let i = 0; i < props.list.length; i++) {
if (i < column) {
props.list[i].top = 10;
props.list[i].left = i * width;
heightList.push(props.list[i].height + 10)
waterList.push(props.list[i])
} else {
let current = heightList[0]
let index = 0;
heightList.forEach((h, inx) => {
if (current > h) {
current = h;
index = inx;
}
})
console.log(current,'c')
props.list[i].top = (current + 20);
console.log(props.list[i].top,'top',i)
props.list[i].left = index * width;
heightList[index] = (heightList[index] + props.list[i].height + 20);
waterList.push(props.list[i])
}
}
console.log(props.list)
}
onMounted(() => {
window.onresize = () => init()
init()
})
</script>
<style scoped lang='less'>
.wraps {
position: relative;
height: 100%;
.items {
position: absolute;
width: 120px;
}
}
</style>
全局组件 局部组件 递归组件
1.全局组件
例如组件使用频率非常高(table,Input,button,等)这些组件 几乎每个页面都在使用便可以封装成全局组件
案例------我这儿封装一个Card组件想在任何地方去使用
<template>
<div class="card">
<div class="card-header">
<div>标题</div>
<div>副标题</div>
</div>
<div v-if='content' class="card-content">
{{content}}
</div>
</div>
</template>
<script setup lang="ts">
type Props = {
content:string
}
defineProps<Props>()
</script>
<style scoped lang='less'>
@border:#ccc;
.card{
width: 300px;
border: 1px solid @border;
border-radius: 3px;
&:hover{
box-shadow:0 0 10px @border;
}
&-content{
padding: 10px;
}
&-header{
display: flex;
justify-content: space-between;
padding: 10px;
border-bottom: 1px solid @border;
}
}
</style>
![image-20231023221951419](https://img-blog.csdnimg.cn/img_convert/162206644d0d79fc4cf67ba7260e4418.png)
使用方法:
在main.ts 引入我们的组件跟随在createApp(App) 后面 切记不能放到mount 后面这是一个链式调用
其次调用 component 第一个参数组件名称 第二个参数组件实例
import { createApp } from 'vue'
import App from './App.vue'
import './assets/css/reset/index.less'
import Card from './components/Card/index.vue'
createApp(App).component('Card',Card).mount('#app')
使用方法
直接在其他vue页面 立即使用即可 无需引入
<template>
<Card></Card>
</template>
批量注册全局组件
可以参考element ui 其实就是遍历一下然后通过 app.component 注册
![image-20231023222409852](https://img-blog.csdnimg.cn/img_convert/bbeca220e87c571a691dcbc30b02d19a.png)
2.局部组件
<template>
<div class="wraps">
<layout-menu :flag="flag" @on-click="getMenu" @on-toogle="getMenuItem" :data="menuList" class="wraps-left"></layout-menu>
<div class="wraps-right">
<layout-header> </layout-header>
<layout-main class="wraps-right-main"></layout-main>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive,ref } from "vue";
import layoutHeader from "./Header.vue";
import layoutMenu from "./Menu.vue";
import layoutMain from "./Content.vue";
就是在一个组件内(A) 通过import 去引入别的组件(B) 称之为局部组件
应为B组件只能在A组件内使用 所以是局部组件
如果C组件想用B组件 就需要C组件也手动import 引入 B 组件
3.递归组件
原理和js递归是一样的,自己调用自己 通过一个条件来结束递归 否则会导致内存泄漏
案例递归树
在父组件配置数据结构 数组对象格式 传给子组件
type TreeList = {
name: string;
icon?: string;
children?: TreeList[] | [];
};
const data = reactive<TreeList[]>([
{
name: "no.1",
children: [
{
name: "no.1-1",
children: [
{
name: "no.1-1-1",
},
],
},
],
},
{
name: "no.2",
children: [
{
name: "no.2-1",
},
],
},
{
name: "no.3",
},
]);
子组件接收值 第一个script
type TreeList = {
name: string;
icon?: string;
children?: TreeList[] | [];
};
type Props<T> = {
data?: T[] | [];
};
defineProps<Props<TreeList>>();
const clickItem = (item: TreeList) => {
console.log(item)
}
+++
子组件增加一个script 定义组件名称为了 递归用
给我们的组件定义名称有好几种方式
1.在增加一个script 通过 export 添加name
<script lang="ts">
export default {
name:"TreeItem"
}
</script>
2.直接使用文件名当组件名
![image-20231023225604058](https://img-blog.csdnimg.cn/img_convert/400a09f1e07ec39e8058afe89980bb2f.png)
3.使用插件
unplugin-vue-macros/README-zh-CN.md at 722a80795a6c7558debf7c62fd5f57de70e0d0bf · sxzz/unplugin-vue-macros · GitHub
unplugin-vue-define-options
import DefineOptions from 'unplugin-vue-define-options/vite'
import Vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [Vue(), DefineOptions()],
})
ts支持
"types": ["unplugin-vue-define-options/macros-global"],
![image-20231023225753236](https://img-blog.csdnimg.cn/img_convert/35bf6d815b8c30ec8ffc4c7b61f588ee.png)
template
TreeItem 其实就是当前组件 通过import 把自身又引入了一遍 如果他没有children 了就结束
<div style="margin-left:10px;" class="tree">
<div :key="index" v-for="(item,index) in data">
<div @click='clickItem(item)'>{{item.name}}
</div>
<TreeItem @on-click='clickItem' v-if='item?.children?.length' :data="item.children"></TreeItem>
</div>
</div>
![image-20231023230051954](https://img-blog.csdnimg.cn/img_convert/4fa48187309e87575997bdaa3bb1d2cb.png)
使用递归组件,阻止一下冒泡
动态组件
什么是动态组件 就是:让多个组件使用同一个挂载点,并动态切换,这就是动态组件。
在挂载点使用component标签,然后使用v-bind:is=”组件”
用法如下
引入组件
import A from './A.vue'
import B from './B.vue'
<component :is="A"></component>
通过is 切换 A B 组件
使用场景
tab切换 居多(也可以使用v-if或者路由)
案例:
A.vue
<template>
<div class="content">我是A组件内容</div>
</template>
<script setup lang='ts'>
</script>
<style scoped lang='scss'>
.content {
width: 500px;
height: 300px;
border: 1px solid black;
text-align: center;
line-height: 300px;
}
</style>
B.vue和C.vue与A类似
App.vue
<template>
<div style="display: flex;">
<div @click="switchCom(item, index)" :class="[active == index ? 'active' : '']" class="tabs"
v-for="(item, index) in data">
<div>{{ item.name }}</div>
</div>
</div>
<component :is="comId"></component>
</template>
<script setup lang='ts'>
import { ref, reactive, markRaw, shallowRef } from 'vue'
import A from './components/A.vue';
import B from './components/B.vue';
import C from './components/C.vue';
const comId = shallowRef(A)
const active = ref(0)
const data = reactive([
{
name: 'A组件',
com: markRaw(A)
},
{
name: 'B组件',
com: markRaw(B)
},
{
name: 'C组件',
com: markRaw(C)
}
])
const switchCom = (item, index) => {
comId.value = item.com
active.value = index;
}
</script>
<style lang='scss' scoped>
.active {
background: skyblue;
}
.tabs {
border: 1px solid #ccc;
padding: 5px 10px;
margin: 5px;
cursor: pointer;
}
</style>
实现tabs的切换
注意事项
1.在Vue2 的时候is 是通过组件名称切换的 在Vue3 setup 是通过组件实例切换的
2.如果你把组件实例放到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 using shallowRef
instead of ref
.
Component that was made reactive:
这是因为reactive 会进行proxy 代理 而我们组件代理之后毫无用处 节省性能开销 推荐我们使用shallowRef 或者 markRaw 跳过proxy 代理
const tab = reactive<Com[]>([{
name: "A组件",
comName: markRaw(A)
}, {
name: "B组件",
comName: markRaw(B)
}])
插槽slot
1.匿名插槽
1.在子组件放置一个插槽
<template>
<div>
<slot></slot>
</div>
</template>
父组件使用插槽
在父组件给这个插槽填充内容
<Dialog>
<template v-slot>
<div>2132</div>
</template>
</Dialog>
2.具名插槽
具名插槽其实就是给插槽取个名字。一个子组件可以放多个插槽,而且可以放在不同的地方,而父组件填充内容时,可以根据这个名字把内容填充到对应插槽中
<div>
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
</div>
父组件使用需对应名称
<Dialog>
<template v-slot:header>
<div>1</div>
</template>
<template v-slot>
<div>2</div>
</template>
<template v-slot:footer>
<div>3</div>
</template>
</Dialog>
插槽简写
<Dialog>
<template #header>
<div>1</div>
</template>
<template #default>
<div>2</div>
</template>
<template #footer>
<div>3</div>
</template>
</Dialog>
3.作用域插槽
在子组件动态绑定参数 派发给父组件的slot去使用
<div>
<slot name="header"></slot>
<div>
<div v-for="item in 100">
<slot :data="item"></slot>
</div>
</div>
<slot name="footer"></slot>
</div>
通过结构方式取值
<Dialog>
<template #header>
<div>1</div>
</template>
<template #default="{ data }">
<div>{{ data }}</div>
</template>
<template #footer>
<div>3</div>
</template>
</Dialog>
4.动态插槽
插槽是一个变量名
<Dialog>
<template #[name]>
<div>
23
</div>
</template>
</Dialog>
const name = ref('header')
这样内容就会被插入到footer里面
异步组件&代码分包&suspense
异步组件
在大型应用中,我们可能需要将应用分割成小一些的代码块 并且减少主包的体积
这时候就可以使用异步组件
顶层 await
在setup语法糖里面 使用方法
父组件引用子组件 通过defineAsyncComponent加载异步配合import 函数模式便可以分包
```vue
<script setup lang="ts">
import { reactive, ref, markRaw, toRaw, defineAsyncComponent } from 'vue'
const Dialog = defineAsyncComponent(() => import('../../components/Dialog/index.vue'))
//完整写法
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
suspense
<suspense>
组件有两个插槽。它们都只接收一个直接子节点。default
插槽里的节点会尽可能展示出来。如果不能,则展示 fallback
插槽里的节点,显示加载中的内容
<Suspense>
<template #default>
<Dialog>
<template #default>
<div>我在哪儿</div>
</template>
</Dialog>
</template>
<template #fallback>
<div>loading...</div>
</template>
</Suspense>
Teleport传送组件
1.使用
Teleport Vue 3.0新特性之一。
Teleport 是一种能够将我们的模板渲染至指定DOM节点,不受父级style、v-show等属性影响,但data、prop数据依旧能够共用的技术;类似于 React 的 Portal。
主要解决的问题 因为Teleport节点挂载在其他指定的DOM节点下,完全不受父级style样式影响
使用方法
通过to 属性 插入指定元素位置 to=“body” 便可以将Teleport
内容传送到指定位置
<Teleport to="body">
<Loading></Loading>
</Teleport>
也可以自定义传送位置 支持 class id等 选择器
<div id="app"></div>
<div class="modal"></div>
<template>
<div class="dialog">
<header class="header">
<div>我是弹框</div>
<el-icon>
<CloseBold />
</el-icon>
</header>
<main class="main">
我是内容12321321321
</main>
<footer class="footer">
<el-button size="small">取消</el-button>
<el-button size="small" type="primary">确定</el-button>
</footer>
</div>
</template>
<script setup lang='ts'>
import { ref, reactive } from 'vue'
</script>
<style lang="less" scoped>
.dialog {
width: 400px;
height: 400px;
background: #141414;
display: flex;
flex-direction: column;
position: absolute;
left: 50%;
top: 50%;
margin-left: -200px;
margin-top: -200px;
.header {
display: flex;
color: #CFD3DC;
border-bottom: 1px solid #636466;
padding: 10px;
justify-content: space-between;
}
.main {
flex: 1;
color: #CFD3DC;
padding: 10px;
}
.footer {
border-top: 1px solid #636466;
padding: 10px;
display: flex;
justify-content: flex-end;
}
}
</style>
![image-20231026074954854](https://img-blog.csdnimg.cn/img_convert/b8bfbcd0463b9759f287701ff7e60883.png)
多个使用场景
<Teleport to=".modal1">
<Loading></Loading>
</Teleport>
<Teleport to=".modal2">
<Loading></Loading>
</Teleport>
动态控制teleport
使用disabled 设置为 true则 to属性不生效 false 则生效
<teleport :disabled="true" to='body'>
<A></A>
</teleport>
2.源码解析
源码解析
在创建teleport 组件的时候会经过patch 方法 然后调用teleport 的process 方法
![image-20231026075337655](https://img-blog.csdnimg.cn/img_convert/78ccf2773c909e54c34b8cfa4969a0b1.png)
主要是创建 更新 和删除的逻辑
![image-20231026075419871](https://img-blog.csdnimg.cn/img_convert/5a9be75b126123164d11b53e02ea03b0.png)
他通过 resolveTarget 函数 获取了props.to 和 querySelect 获取 了目标元素
然后判断是否有disabled 如果有则 to 属性不生效 否则 挂载新的位置
![image-20231026075448582](https://img-blog.csdnimg.cn/img_convert/453874660af70eecbf99e7fb44c96c7e.png)
新节点disabled 为 true 旧节点disabled false 就把子节点移动回容器
如果新节点disabled 为 false 旧节点为true 就把子节点移动到目标元素
![image-20231026075517461](https://img-blog.csdnimg.cn/img_convert/31fb76ad7d84ff458f5db178d2dac0b5.png)
遍历teleport 子节点进行unmount方法去移除
![image-20231026075546941](https://img-blog.csdnimg.cn/img_convert/fa9b9f516ccad753e2c59858d1aa167d.png)
keep-alive缓存组件
内置组件keep-alive
有时候我们不希望组件被重新渲染影响使用体验;或者处于性能考虑,避免多次重复渲染降低性能。而是希望组件可以缓存下来,维持当前的状态。这时候就需要用到keep-alive组件。
场景:
比如A页面表单,填入一些值,然后切换到B再切回来,输入的值还在。
开启keep-alive 生命周期的变化
初次进入时: onMounted> onActivated
退出后触发 deactivated
再次进入:
只会触发 onActivated
事件挂载的方法等,只执行一次的放在 onMounted中(一次请求的接口);组件每次进去执行的方法放在 onActivated中(多次请求的接口)。
卸载操作在onDeactivated
里面
<!-- 基本 -->
<keep-alive>
<component :is="view"></component>
</keep-alive>
<!-- 多个条件判断的子组件 -->
<keep-alive>
<comp-a v-if="a > 1"></comp-a>
<comp-b v-else></comp-b>
</keep-alive>
<!-- 和 `<transition>` 一起使用 -->
<transition>
<keep-alive>
<component :is="view"></component>
</keep-alive>
</transition>
include
和 exclude
include是你想缓存的组件的名称,支持字符串/正则/数组
//这边只缓存A组件
<keep-alive :include="['A']" :exclude="" :max="">
<A v-if="flag"></A>
<B v-else></B>
</keep-alive>
exclud就是不缓存某个组件
<keep-alive :include="" :exclude="" :max=""></keep-alive>
include 和 exclude 允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示:
max
指定你缓存组件的数量
<keep-alive :max="10">
<component :is="view"></component>
</keep-alive>
它会优先剔除掉旧的,不常用的组件。
transition动画组件
Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡:
-
条件渲染 (使用 v-if)
-
条件展示 (使用 v-show)
-
动态组件
-
组件根节点
自定义 transition 过度效果,你需要对transition组件的name属性自定义。并在css中写入对应的样式
1.过渡的类名
-
过渡 class
在进入/离开的过渡中,会有 6 个 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
被移除),在过渡/动画完成之后移除。
如下
<button @click='flag = !flag'>切换</button>
<transition name='fade'>
<div v-if='flag' class="box"></div>
</transition>
//开始过度
.fade-enter-from{
background:red;
width:0px;
height:0px;
transform:rotate(360deg)
}
//开始过度了
.fade-enter-active{
transition: all 2.5s linear;
}
//过度完成
.fade-enter-to{
background:yellow;
width:200px;
height:200px;
}
//离开的过度
.fade-leave-from{
width:200px;
height:200px;
transform:rotate(360deg)
}
//离开中过度
.fade-leave-active{
transition: all 1s linear;
}
//离开完成
.fade-leave-to{
width:0px;
height:0px;
}
2.自定义过渡class类名
trasnsition props
enter-from-class
enter-active-class
enter-to-class
leave-from-class
leave-active-class
leave-to-class
自定义过度时间 单位毫秒
你也可以分别指定进入和离开的持续时间:
<transition :duration="1000">...</transition>
<transition :duration="{ enter: 500, leave: 800 }">...</transition>
通过自定义class 结合css动画库animate css
安装库 npm install animate.css
引入 import ‘animate.css’
使用方法
官方文档 Animate.css | A cross-browser library of CSS animations.
<transition
leave-active-class="animate__animated animate__bounceInLeft"
enter-active-class="animate__animated animate__bounceInRight"
>
<div v-if="flag" class="box"></div>
</transition>
3.transition八个生命周期
@before-enter="beforeEnter" //对应enter-from
@enter="enter"//对应enter-active
@after-enter="afterEnter"//对应enter-to
@enter-cancelled="enterCancelled"//显示过度打断
@before-leave="beforeLeave"//对应leave-from
@leave="leave"//对应enter-active
@after-leave="afterLeave"//对应leave-to
@leave-cancelled="leaveCancelled"//离开过度打断
当只用 JavaScript 过渡的时候,在 enter
和 leave
钩子中必须使用 done
进行回调
结合gsap 动画库使用 GreenSock
const beforeEnter = (el: Element) => {
console.log('进入之前from', el);
}
const Enter = (el: Element,done:Function) => {
console.log('过度曲线');
setTimeout(()=>{
done()
},3000)
}
const AfterEnter = (el: Element) => {
console.log('to');
}
4.appear
通过这个属性可以设置初始节点过度 就是页面加载完成就开始动画 对应三个状态
appear-active-class=""
appear-from-class=""
appear-to-class=""
appear
transition-group过渡列表
1.组件特点
单个节点
多个节点,每次只渲染一个
那么怎么同时渲染整个列表,比如使用 v-for?在这种场景下,我们会使用 组件。在我们深入例子之前,先了解关于这个组件的几个特点:
- 默认情况下,它不会渲染一个包裹元素,但是你可以通过 tag attribute 指定渲染一个元素。
过 - 渡模式不可用,因为我们不再相互切换特有的元素。
- 内部元素总是需要提供唯一的 key attribute 值。
- CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身。
<transition-group>
<div style="margin: 10px;" :key="item" v-for="item in list">{{ item }</div>
</transition-group>
const list = reactive<number[]>([1, 2, 4, 5, 6, 7, 8, 9])
const Push = () => {
list.push(123)
}
const Pop = () => {
list.pop()
}
2.列表的移动过渡
组件还有一个特殊之处。除了进入和离开,它还可以为定位的改变添加动画。只需了解新增的 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>
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>
依赖注入Provide/Inject
Provide / Inject
通常,当我们需要从父组件向子组件传递数据时,我们使用 props。想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。
官网的解释很让人疑惑,那我翻译下这几句话:
provide 可以在祖先组件中指定我们想要提供给后代组件的数据或方法,而在任何后代组件中,我们都可以使用 inject 来接收 provide 提供的数据或方法。
看一个例子
父组件传递数据
<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'
let flag = ref<number>(1)
provide('flag', flag)
</script>
<style>
.App {
background: blue;
color: #fff;
}
</style>
子组件接受
<template>
<div class="box" 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>
//注意:vue3的css里面可以直接通过v-bind来绑定变量
.box{
background:v-bind(flag)
}
</style>
**TIPS 😗*你如果传递普通的值 是不具有响应式的 需要通过ref reactive 添加响应式
使用场景:
当父组件有很多数据需要分发给其子代组件的时候, 就可以使用provide和inject。
兄弟组件穿参&Bus
两种方案
1.借助父组件传参
例如父组件为App 子组件为A 和 B他两个是同级的
A子组件:
<template>
<div class="content">我是A组件内容
<button @click="emitB">派发一个事件</button>
</div>
</template>
<script setup lang='ts'>
const emit = defineEmits(['on-click'])
let flag = false
const emitB = () => {
flag = !flag
emit('on-click', flag)
}
</script>
父组件:
<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>
B组件接受一下父组件传来的
<template>
<div class="content">我是B组件内容{{ flag }}</div>
</template>
<script setup lang='ts'>
type Props = {
flag: boolean
}
defineProps<Props>()
</script>
就是通过A 组件派发事件通过App.vue 接受A组件派发的事件然后在Props 传给B组件
缺点就是比较麻烦 ,无法直接通信,只能充当桥梁。
2.事件总线(Event Bus)
我们在Vue2 可以使用$emit 传递 $on监听 emit传递过来的事件
这个原理其实是运用了JS设计模式之发布订阅模式
简易版:
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>()
然后挂载到Vue config 全局就可以使用啦
A组件:
import Bus from '../Bus'
let flag = false
const emitB =()=>{
flag:!flag
Bus.emit('on-click',flag)
}
B组件:
import Bus from '../Bus'
import {ref} from 'vue'
let Flag = ref(false)
Bus.on('on-click',(flag:boolean)=>{
Flag.value =flag
})
Mitt
![image-20231109225711901](https://img-blog.csdnimg.cn/img_convert/37bc76c8a2625f6302a8b9127bf7df43.png)
1.安装:
npm install mitt -S
2.main.ts 初始化
全局总线,vue 入口文件 main.js 中挂载全局属性
import { createApp } from 'vue'
import App from './App.vue'
import mitt from 'mitt'
const Mit = mitt()
//TypeScript注册
// 由于必须要拓展ComponentCustomProperties类型才能获得类型提示
declare module "vue" {
export interface ComponentCustomProperties {
$Bus: typeof Mit
}
}
const app = createApp(App)
//Vue3挂载全局API
app.config.globalProperties.$Bus = Mit
app.mount('#app')
3使用方法通过emit派发, on 方法添加事件,off 方法移除,clear 清空所有
A组件派发(emit)
<template>
<div>
<h1>我是A</h1>
<button @click="emit1">emit1</button>
<button @click="emit2">emit2</button>
</div>
</template>
<script setup lang='ts'>
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance();
const emit1 = () => {
instance?.proxy?.$Bus.emit('on-num', 100)
}
const emit2 = () => {
instance?.proxy?.$Bus.emit('*****', 500)
}
</script>
<style>
</style>
B组件监听(on)
<template>
<div>
<h1>我是B</h1>
</div>
</template>
<script setup lang='ts'>
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
instance?.proxy?.$Bus.on('on-num', (num) => {
console.log(num,'===========>B')
})
</script>
<style>
</style>
监听所有事件( on(“*”) )
instance?.proxy?.$Bus.on('*',(type,num)=>{
console.log(type,num,'===========>B')
})
移除监听事件(off)
const Fn = (num: any) => {
console.log(num, '===========>B')
}
instance?.proxy?.$Bus.on('on-num',Fn)//listen
instance?.proxy?.$Bus.off('on-num',Fn)//unListen
清空所有监听(clear)
instance?.proxy?.$Bus.all.clear()
TSX
完整版用法 请看 @vue/babel-plugin-jsx - npm
我们之前呢是使用Template去写我们模板。现在可以扩展另一种风格TSX风格
vue2 的时候就已经支持jsx写法,只不过不是很友好,随着vue3对typescript的支持度,tsx写法越来越被接受
1.安装插件
npm install @vitejs/plugin-vue-jsx -D
vite.config.ts 配置
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()]
})
2.修改tsconfig.json配置文件
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
![image-20231112230551755](https://img-blog.csdnimg.cn/img_convert/58db222d85d1d4bbc3c10bd9bf630426.png)
配置完成就可以使用啦
在目录新建一个xxxxxx.tsx文件
3.使用TSX
TIPS tsx不会自动解包使用ref加.vlaue ! ! !
模式一:返回一个渲染函数
App.tsx
export default function () {
return(<div>sz</div>)
}
App.vue
<template>
<div class="">
<sz></sz>
</div>
</template>
<script lang='ts' setup>
import { ref } from "vue"
import sz from "./App"
</script>
<style lang="scss" scoped></style>
模式二:optionsAPI
import { divide } from 'lodash'
import {defineComponent } from 'vue'
export default defineComponent({
data() {
return {
age:23
}
},
render() {
return (<div>{ this.age}</div>)
}
})
模式三:函数模式
App.tsx
import {defineComponent } from 'vue'
export default defineComponent({
setup() {
const flag = false
return ()=>(<div v-show={flag}>sz setup</div>)
}
})
1.tsx支持 v-model 的使用
import { ref } from 'vue'
let v = ref<string>('')
const renderDom = () => {
return (
<>
<input v-model={v.value} type="text" />
<div>
{v.value}
</div>
</>
)
}
export default renderDom
2.v-show支持
import { ref } from 'vue'
let flag = ref(false)
const renderDom = () => {
return (
<>
<div v-show={flag.value}>景天</div>
<div v-show={!flag.value}>雪见</div>
</>
)
}
export default renderDom
注意:ref在template会自动解包.value , tsx并不会。tsx需要手动.value。
3.v-if不支持
所以需要改变风格(三元表达式)
import { ref } from 'vue'
let flag = ref(false)
const renderDom = () => {
return (
<>
{
flag.value ? <div>景天</div> : <div>雪见</div>
}
</>
)
}
export default renderDom
4.v-for不支持
需要使用Map
import { ref } from 'vue'
let arr = [1,2,3,4,5]
const renderDom = () => {
return (
<>
{
arr.map(v=>{
return <div>${v}</div>
})
}
</>
)
}
export default renderDom
5.v-bind使用
直接赋值就可以
import { ref } from 'vue'
let arr = [1, 2, 3, 4, 5]
const renderDom = () => {
return (
<>
<div data-arr={arr}>1</div>
</>
)
}
export default renderDom
6. v-on绑定事件 所有的事件都按照react风格来
- 所有事件有on开头
- 所有事件名称首字母大写
const renderDom = () => {
return (
<>
<button onClick={clickTap}>点击</button>
</>
)
}
const clickTap = () => {
console.log('click');
}
export default renderDom
7.props接受值
import { ref } from 'vue'
type Props = {
title:string
}
const renderDom = (props:Props) => {
return (
<>
<div>{props.title}</div>
<button onClick={clickTap}>点击</button>
</>
)
}
const clickTap = () => {
console.log('click');
}
export default renderDom
第二个案例:
8.Emit派发
type Props = {
title: string
}
const renderDom = (props: Props,content:any) => {
return (
<>
<div>{props.title}</div>
<button onClick={clickTap.bind(this,content)}>点击</button>
</>
)
}
const clickTap = (ctx:any) => {
ctx.emit('on-click',1)
}
案例:
interface Props{
name?:string
}
import { defineComponent, ref } from 'vue'
export default defineComponent({
props: {
name:String
},
emits:['on-click'],
setup(props:Props) {
const flag = ref(false)
const data = [
{
name:'sz1'
},
{
name:'sz2'
},
{
name:'sz3'
}
]
return () => (
<>
<div>props:{ props.name}</div>
<hr />
{data.map(v => {
return <div>{ v.name}</div>
})}
</>
)
}
})
9.slot
const A = (props, { slots }) => (
<>
<h1>{ slots.default ? slots.default() : 'foo' }</h1>
<h2>{ slots.bar?.() }</h2>
</>
);
const App = {
setup() {
const slots = {
bar: () => <span>B</span>,
};
return () => (
<A v-slots={slots}>
<div>A</div>
</A>
);
},
};
// or
const App = {
setup() {
const slots = {
default: () => <div>A</div>,
bar: () => <span>B</span>,
};
return () => <A v-slots={slots} />;
},
};
// or you can use object slots when `enableObjectSlots` is not false.
const App = {
setup() {
return () => (
<>
<A>
{{
default: () => <div>A</div>,
bar: () => <span>B</span>,
}}
</A>
<B>{() => "foo"}</B>
</>
);
},
};
10.实现一个vite插件解析tsx
1.需要用到的第三方插件
npm install @vue/babel-plugin-jsx
npm install @babel/core
npm install @babel/plugin-transform-typescript
npm install @babel/plugin-syntax-import-meta
npm install @types/babel__core
插件代码
import type { Plugin } from 'vite'
import * as babel from '@babel/core'; //@babel/core核心功能:将源代码转成目标代码。
import jsx from '@vue/babel-plugin-jsx'; //Vue给babel写的插件支持tsx v-model等
export default function (): Plugin {
return {
name: "vite-plugin-tsx",
config (config) {
return {
esbuild:{
include:/\.ts$/
}
}
},
async transform(code, id) {
if (/.tsx$/.test(id)) {
//@ts-ignore
const ts = await import('@babel/plugin-transform-typescript').then(r=>r.default)
const res = babel.transformSync(code,{
plugins:[jsx,[ts, { isTSX: true, allowExtensions: true }]], //添加babel插件
ast:true, // ast: 抽象语法树,源代码语法结构的一种抽象表示。babel内部就是通过操纵ast做到语法转换。
babelrc:false, //.babelrc.json
configFile:false //默认搜索默认babel.config.json文件
})
return res?.code //code: 编译后的代码
}
return code
}
}
}
![image-20231113225004624](https://img-blog.csdnimg.cn/img_convert/7def39b6efdff2e0508cb044b20dc679.png)
![image-20231113225021467](https://img-blog.csdnimg.cn/img_convert/9b7d87d8a936ea500cac7206d126a51e.png)
浏览器显示 1
v-model深入
1.vue3自动引入插件
unplugin-auto-import/vite
vite配置
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"
})]
})
配置完成之后使用ref reactive watch 等 无须import 导入 可以直接使用
GitHub - antfu/unplugin-auto-import: Auto import APIs on-demand for Vite, Webpack and Rollup
2.v-model
![image-20231119162417080](https://img-blog.csdnimg.cn/img_convert/cbbb41e28780f971c795743b52191a5e.png)
TIps 在Vue3 v-model 是破坏性更新的
v-model在组件里面也是很重要的
v-model 其实是一个语法糖 通过props 和 emit组合而成的
1.默认值的改变
- prop:
value
->modelValue
; - 事件:
input
->update:modelValue
; v-bind
的.sync
修饰符和组件的model
选项已移除- 新增 支持多个v-model
- 新增 支持自定义 修饰符 Modifiers
案例 子组件
<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>()
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}}</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='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>
父组件
<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>
自定义修饰符
添加到组件 v-model
的修饰符将通过 modelModifiers
prop 提供给组件。在下面的示例中,我们创建了一个组件,其中包含默认为空对象的 modelModifiers
prop
<script setup lang='ts'>
type Props = {
modelValue: boolean,
title?: string,
modelModifiers?: {
default: () => {}
}
titleModifiers?: {
default: () => {}
}
}
const propData = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'update:title'])
const close = () => {
console.log(propData.modelModifiers);
emit('update:modelValue', false)
emit('update:title', '我要改变')
}
自定义指令
directive-自定义指令(属于破坏性更新)
Vue中有v-if,v-for,v-bind,v-show,v-model 等等一系列方便快捷的指令 今天一起来了解一下vue里提供的自定义指令
1.vue3指令的钩子函数
-
created 元素初始化的时候
-
beforeMount 指令绑定到元素后调用 只调用一次
-
mounted 元素插入父级dom调用
-
beforeUpdate 元素被更新之前调用
update 这个周期方法被移除 改用updated -
beforeUnmount 在元素被移除前调用
-
unmounted 指令被移除后调用 只调用一次
Vue2 指令 bind inserted update componentUpdated unbind
2.在setup内定义局部指令
但这里有一个需要注意的限制:必须以 vNameOfDirective
的形式来命名本地自定义指令,以使得它们可以直接在模板中使用。
<template>
<button @click="show = !show">开关{{show}} ----- {{title}}</button>
<Dialog v-move-directive="{background:'green',flag:show}"></Dialog>
</template>
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("======>卸载完成");
},
};
3.生命周期钩子参数详解
第一个 el 当前绑定的DOM 元素
第二个 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:一个对象,在注册指令时作为参数传递。例如,在以下指令中
第三个 当前元素的虚拟DOM 也就是Vnode
第四个 prevNode 上一个虚拟节点,仅在 beforeUpdate
和 updated
钩子中可用
4.函数简写
你可能想在 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>
5.指令案例
1.自定义拖拽指令
<template>
<div v-move class="box">
<div class="header"></div>
<div>
内容
</div>
</div>
</template>
<script setup lang='ts'>
import { Directive } from "vue";
const vMove: Directive = {
mounted(el: HTMLElement) {
let moveEl = el.firstElementChild as HTMLElement;
const mouseDown = (e: MouseEvent) => {
//鼠标点击物体那一刻相对于物体左侧边框的距离=点击时的位置相对于浏览器最左边的距离-物体左边框相对于浏览器最左边的距离
console.log(e.clientX, e.clientY, "-----起始", el.offsetLeft);
let X = e.clientX - el.offsetLeft;
let Y = e.clientY - el.offsetTop;
const move = (e: MouseEvent) => {
el.style.left = e.clientX - X + "px";
el.style.top = e.clientY - Y + "px";
console.log(e.clientX, e.clientY, "---改变");
};
document.addEventListener("mousemove", move);
document.addEventListener("mouseup", () => {
document.removeEventListener("mousemove", move);
});
};
moveEl.addEventListener("mousedown", mouseDown);
},
};
</script>
<style lang='less'>
.box {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 200px;
height: 200px;
border: 1px solid #ccc;
.header {
height: 20px;
background: black;
cursor: move;
}
}
</style>
自写:
import type { Directive, DirectiveBinding } from "vue"
const vMove: Directive<any, void> = (el: HTMLElement, binding: DirectiveBinding) => {
//获取到header头部
let moveElement: HTMLDivElement = el.firstElementChild as HTMLDivElement
const mouseDown = (e: MouseEvent) => {
// 求出鼠标与盒子的相对距离
let X = e.clientX - el.offsetLeft
let Y = e.clientY - el.offsetTop
const mouseMove = (e: MouseEvent) => {
// 拖动时候根据相对距离,来重新计算盒子的位置
el.style.left = e.clientX - X + 'px'
el.style.top = e.clientY - Y + 'px'
}
document.addEventListener("mousemove", mouseMove),
//鼠标抬起的时候,将move清掉
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', mouseMove)
})
}
moveElement.addEventListener("mousedown", mouseDown)
}
2.权限按钮
<template>
<div class="btns">
<button v-has-show="'shop:create'">创建</button>
<button v-has-show="'shop:edit'">编辑</button>
<button v-has-show="'shop:delete'">删除</button>
</div>
</template>
<script setup lang='ts'>
import { ref, reactive, } from 'vue'
import type {Directive} from 'vue'
//permission
localStorage.setItem('userId','xiaoman-zs')
//mock后台返回的数据
const permission = [
'xiaoman-zs:shop:edit',
'xiaoman-zs:shop:create',
'xiaoman-zs:shop:delete'
]
const userId = localStorage.getItem('userId') as string
const vHasShow:Directive<HTMLElement,string> = (el,bingding) => {
if(!permission.includes(userId+':'+ bingding.value)){
el.style.display = 'none'
}
}
</script>
<style scoped lang='less'>
.btns{
button{
margin: 10px;
}
}
</style>
3.图片懒加载
**效果:**默认展示vue官网的图片,进入可视区域之后,变成真正要加载的图片
<template>
<div>
<div v-for="item in arr">
<img height="500" :data-index="item" v-lazy="item" width="360" alt="">
</div>
</div>
</template>
<script setup lang='ts'>
import { ref, reactive } from 'vue'
import type { Directive } from 'vue'
//glob是懒加载模式
//globEager是静态加载
//也可以这样
//const images: Record<string, { default: string }> = import.meta.glob('./assets/images/*.*',{eager:true})效果是一样的
const images: Record<string, { default: string }> = import.meta.globEager('./assets/images/*.*')
let arr = Object.values(images).map(v => v.default)
let vLazy: Directive<HTMLImageElement, string> = async (el, binding) => {
let url = await import('./assets/vue.svg')
el.src = url.default;
//通过api判断是否滑动到可视区域
let observer = new IntersectionObserver((entries) => {
console.log(entries[0], el)
//intersectionRatio代表元素是否在可视区域的一个比例
if (entries[0].intersectionRatio > 0 && entries[0].isIntersecting) {
setTimeout(() => {
//替换成想要展示的图片链接
el.src = binding.value;
//停止监听
observer.unobserve(el)
}, 2000)
}
})
//监听的元素
observer.observe(el)
}
</script>
<style scoped lang='less'></style>
自定义hooks
1.概念
Vue3 自定义Hook
主要用来处理复用代码逻辑的一些封装
这个在vue2 就已经有一个东西是Mixins
mixins就是将这些多个相同的逻辑抽离出来,各个组件只需要引入mixins,就能实现一次写代码,多组件受益的效果。
弊端就是 会涉及到覆盖的问题
组件的data、methods、filters会覆盖mixins里的同名data、methods、filters。
第二点就是 变量来源不明确(隐式传入),不利于阅读,使代码变得难以维护。
Vue3 的自定义的hook
Vue3 的 hook函数 相当于 vue2 的 mixin, 不同在与 hooks 是函数
Vue3 的 hook函数 可以帮助我们提高代码的复用性, 让我们能在不同的组件中都利用 hooks 函数
Vue3 hook 库Get Started | VueUse(https://vueuse.org/guide/)
2.案例
案例一:将一个图片文件地址转成一个base64
index.ts
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')
}
})
}
app.vue使用
<img id="img" width="300" height="400" src="./assets/test.png"
import useBase64 from './hooks'
useBase64({el:'#img'}).then(res=>{
console.log(res.baseUrl)
})
案例二:
自定义指令 + hooks 双管齐下
实现一个监听元素变化的hook
#需求:实现一个函数同时支持hook和自定义指令 去监听dom宽高的变化。
主要会用到一个新的API resizeObserver 兼容性一般 可以做polyfill
但是他可以监听元素的变化 执行回调函数 返回 contentRect 里面有变化之后的宽高。
import { App, defineComponent, onMounted } from 'vue'
//MutationObserver 主要侦听子集的变化,还有属性的变化,以及 增删改查
//IntersectionObserver 主要侦听元素是否在可视窗口区域
//ResizeObserver主要用于侦听元素宽高的变化
function useResize(el: HTMLElement, callback: (cr: DOMRectReadOnly,resize:ResizeObserver) => void) {
let resize: ResizeObserver
resize = new ResizeObserver((entries) => {
for (let entry of entries) {
const cr = entry.contentRect;
callback(cr,resize)
}
});
resize.observe(el)
}
const install = (app: App) => {
app.directive('resize', {
mounted(el, binding) {
useResize(el, binding.value)
}
})
}
useResize.install = install
export default useResize
vite打包和发布npm包
小满Vue3( 自定义Hooks 综合案例)_哔哩哔哩_bilibili
vue3定义全局函数和变量
1.globalProperties
由于Vue3 没有Prototype 属性 使用 app.config.globalProperties 代替 然后去定义变量和函数
// 之前 (Vue 2.x)
Vue.prototype.$http = () => {}
Vue3
// 之后 (Vue 3.x)
import{createApp} from 'vue'
import App from './App.vue'
export const app = createApp(App)
app.config.globalProperties.$http = () => {}
app.mount("#app")
2.过滤器
在Vue3 移除了
我们正好可以使用全局函数代替Filters
案例
app.config.globalProperties.$filters = {
format<T extends any>(str: T): string {
return `$${str}`
}
}
声明文件 不然TS无法正确类型 推导
main.ts
type Filter = {
format<T>(str: T): string
}
// 声明要扩充@vue/runtime-core包的声明.
// 这里扩充"ComponentCustomProperties"接口, 因为他是vue3中实例的属性的类型.
declare module 'vue' {
export interface ComponentCustomProperties {
$filters: Filter
}
}
setup 读取值
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'))
自定义vue插件
1.插件概念
插件是自包含的代码,通常向 Vue 添加全局级功能。你如果是一个对象需要有install方法Vue会帮你自动注入到install 方法 你如果是function 就直接当install 方法去使用
2.使用插件
在使用 createApp()
初始化 Vue 应用程序后,你可以通过调用 use()
方法将插件添加到你的应用程序中。
实现一个Loading
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>
Loading.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 声明文件放置报错 和 智能提示
declare module '@vue/runtime-core' {
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>
Vue use 源码手写
import type { App } from 'vue'
import { app } from './main'
interface Use {
install: (app: App, ...options: any[]) => void
}
//防止重复添加这个组件
const installedList = new Set()
export function MyUse<T extends Use>(plugin: T, ...options: any[]) {
//如果插件在这注册过
if(installedList.has(plugin)){
return console.warn('重复添加插件',plugin)
}else{
plugin.install(app, ...options)
installedList.add(plugin)
}
}
ui库ElementUI,AntDesigin等
vue作为一款深受广大群众以及尤大崇拜者的喜欢,特此列出在github上开源的vue优秀的UI组件库供大家参考
这几套框架主要用于后台管理系统和移动端的制作,方便开发者快速开发
1.Elementui plus
安装方法
# NPM
$ npm install element-plus --save
# Yarn
$ yarn add element-plus
# pnpm
$ pnpm install element-plus
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')
volar插件支持
{
"compilerOptions": {
// ...
"types": ["element-plus/global"]
}
}
2.Ant Design Vue
安装
$ npm install ant-design-vue@next --save
$ yarn add ant-design-vue@next
使用
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');
https://next.antdv.com/docs/vue/introduce-cn
3.lview
安装
npm install view-ui-plus --save
使用
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')
iView / View Design 一套企业级 UI 组件库和前端解决方案
4.Vant移动端
安装
npm i vant -S
使用
import Vant from 'vant'
import 'vant/lib/index.css';
createApp(App).use(vant).$mount('#app)
Vant 3 - Lightweight Mobile UI Components built on Vue
样式穿透和scoped
主要是用于修改很多vue常用的组件库(element, vant, AntDesigin),虽然配好了样式但是还是需要更改其他的样式
就需要用到样式穿透
scoped的原理
vue中的scoped 通过在DOM结构以及css样式上加唯一不重复的标记:data-v-hash的方式,以保证唯一(而这个工作是由过PostCSS转译实现的),达到样式私有化模块化的目的。
总结一下scoped三条渲染规则:
1. 给HTML的DOM节点加一个不重复data属性(形如:data-v-123)来表示他的唯一性
1. 在每句css选择器的末尾(编译后的生成的css语句)加一个当前组件的data属性选择器(如[data-v-123])来私有化样式
1. 如果组件内部包含有其他组件,只会给其他组件的最外层标签加上当前组件的data属性
PostCSS会给一个组件中的所有dom添加了一个独一无二的动态属性data-v-xxxx,然后,给CSS选择器额外添加一个对应的属性选择器来选择该组件中dom,这种做法使得样式只作用于含有该属性的dom——组件内部dom, 从而达到了’样式模块化’的效果.
案例修改Element ui Input样式
发现没有生效
如果不写Scoped 就没问题
原因就是Scoped 搞的鬼 他在进行PostCss转化的时候把元素选择器默认放在了最后
![image-20231122074340953](https://img-blog.csdnimg.cn/img_convert/56edce3a585322e2ffd324cc7438f785.png)
Vue 提供了样式穿透:deep() 他的作用就是用来改变 属性选择器的位置
![image-20231122074411684](https://img-blog.csdnimg.cn/img_convert/e2ee3717ef7dcc7a874fdd93b9512565.png)
总结就是样式穿透挪动属性选择器到最外层类名。
CSS完整新特性
1.插槽选择器
A 组件定义一个插槽
<template>
<div>
我是插槽
<slot></slot>
</div>
</template>
<script>
export default {}
</script>
<style scoped>
</style>
在App.vue 引入
<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>
在A组件修改class a 的颜色
<style scoped>
.a{
color:red
}
</style>
无效果
默认情况下,作用域样式不会影响到 <slot/>
渲染出来的内容,因为它们被认为是父组件所持有并传递进来的。
解决方案 slotted
<style scoped>
:slotted(.a) {
color:red
}
</style>
2.全局选择器
在之前我们想加入全局 样式 通常都是新建一个style 标签 不加scoped 现在有更优雅的解决方案
<style>
div{
color:red
}
</style>
<style lang="less" scoped>
</style>
<style lang="less" scoped>
:global(div){
color:red
}
</style>
3.动态css
单文件组件的 <style>
标签可以通过 v-bind
这一 CSS 函数将 CSS 的值关联到动态的组件状态上:
<template>
<div class="div">
小满是个弟弟
</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 请加引号
<template>
<div class="div">
小满是个弟弟
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue"
const red = ref({
color:'pink'
})
</script>
<style lang="less" scoped>
.div {
color: v-bind('red.color');
}
</style>
4.css module
自定义注入名称(多个可以用数组)
你可以通过给 `module` attribute 一个值来自定义注入的类对象的 property 键
```vue
<template>
<div :class="[zs.red,zs.border]">
小满是个弟弟
</div>
</template>
<style module="zs">
.red {
color: red;
font-size: 20px;
}
.border{
border: 1px solid #ccc;
}
</style>
与组合式 API 一同使用
注入的类可以通过 useCssModule API 在 setup() 和
<template>
<div :class="[zs.red,zs.border]">
小满是个弟弟
</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>
使用场景一般用于TSX 和 render 函数 居多
vue3集成Tailwind CSS
Tailwind CSS 是一个由js编写的CSS 框架 他是基于postCss 去解析的
官网地址Tailwind CSS 中文文档 - 无需离开您的HTML,即可快速建立现代网站。
对于PostCSS的插件使用,我们再使用的过程中一般都需要如下步骤:
PostCSS 配置文件 postcss.config.js,新增 tailwindcss 插件。
TaiWindCss插件需要一份配置文件,比如:tailwind.config.js。
PostCSS - 是一个用 JavaScript 工具和插件来转换 CSS 代码的工具 | PostCSS 中文网
postCss 功能介绍
1.增强代码的可读性 (利用从 Can I Use 网站获取的数据为 CSS 规则添加特定厂商的前缀。 Autoprefixer 自动获取浏览器的流行度和能够支持的属性,并根据这些数据帮你自动为 CSS 规则添加前缀。)
2.将未来的 CSS 特性带到今天!(PostCSS Preset Env 帮你将最新的 CSS 语法转换成大多数浏览器都能理解的语法,并根据你的目标浏览器或运行时环境来确定你需要的 polyfills,此功能基于 cssdb 实现。)
3.终结全局 CSS(CSS 模块 能让你你永远不用担心命名太大众化而造成冲突,只要用最有意义的名字就行了。)
. 4.避免 CSS 代码中的错误(通过使用 stylelint 强化一致性约束并避免样式表中的错误。stylelint 是一个现代化 CSS 代码检查工具。它支持最新的 CSS 语法,也包括类似 CSS 的语法,例如 SCSS 。)
postCss 处理 tailWind Css 大致流程
- 将CSS解析成抽象语法树(AST树)
- 读取插件配置,根据配置文件,生成新的抽象语法树
- 将AST树”传递”给一系列数据转换操作处理(变量数据循环生成,切套类名循环等)
- 清除一系列操作留下的数据痕迹
- 将处理完毕的AST树重新转换成字符串
安装
1.初始化项目
npm init vue@latest
2.安装 Tailwind 以及其它依赖项
vscode插件
![image-20231122230021397](https://img-blog.csdnimg.cn/img_convert/114bf4f20ec81d3c29c6fba855e55927.png)
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
3.生成配置文件
npx tailwindcss init -p
4.修改配置文件 tailwind.config.js
2.6版本
module.exports = {
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
}
3.0版本
module.exports = {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
}
5.创建一个index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
在main.ts 引入
![image-20231122225239002](https://img-blog.csdnimg.cn/img_convert/43aa7a2d1df23e5d322ed2c7cbbaed37.png)
![image-20231122225259573](https://img-blog.csdnimg.cn/img_convert/e454f64e06cbbf0619da99bfe2b14d0b.png)
最后npm run dev 就可以使用啦
<div class="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl">
<div class="md:flex">
<div class="md:flex-shrink-0">
<img class="h-48 w-full object-cover md:w-48" src="http://n.sinaimg.cn/translate/20170815/OoVn-fyixtym5144510.jpg" alt="Man looking at item at a store">
</div>
<div class="p-8">
<div class="uppercase tracking-wide text-sm text-indigo-500 font-semibold">Case study</div>
<a href="#" class="block mt-1 text-lg leading-tight font-medium text-black hover:underline">Finding customers
for your new business</a>
<p class="mt-2 text-gray-500">Getting a new business off the ground is a lot of hard work. Here are five ideas
you can use to find your first customers.</p>
</div>
</div>
</div>
Event Loop 和 nextTick
1.event loop
JS 执行机制
在我们学js 的时候都知道js 是单线程的如果是多线程的话会引发一个问题在同一时间同时操作DOM 一个增加一个删除JS就不知道到底要干嘛了,所以这个语言是单线程的但是随着HTML5到来js也支持了多线程webWorker 但是也是不允许操作DOM
单线程就意味着所有的任务都需要排队,后面的任务需要等前面的任务执行完才能执行,如果前面的任务耗时过长,后面的任务就需要一直等,一些从用户角度上不需要等待的任务就会一直等待,这个从体验角度上来讲是不可接受的,所以JS中就出现了异步的概念。
同步任务
代码从上到下按顺序执行
异步任务
1.宏任务
script(整体代码)、setTimeout、setInterval、UI交互事件、postMessage、Ajax
2.微任务
Promise.then catch finally、MutaionObserver、process.nextTick(Node.js 环境)
运行机制
所有的同步任务都是在主进程执行的形成一个执行栈,主线程之外,还存在一个"任务队列",异步任务执行队列中先执行宏任务,然后清空当次宏任务中的所有微任务,然后进行下一个tick如此形成循环。
![image-20231123220720580](https://img-blog.csdnimg.cn/img_convert/6f8122187c48c2eb28349a0a1d65164c.png)
**弹幕:**新的规范已经没有宏任务的说法了,叫事件队列或消息队列
案例:
![image-20231123222006868](https://img-blog.csdnimg.cn/img_convert/622529f45ecbb405a46baf1d9c57e289.png)
弹幕:从上到下解析,1.两个setTimeout放到宏任务队列 2. 4个promise放到微任务队列 3.执行Prom,其中X放到微任务队列末尾。所以X在5 6 7 8后面
2.nextTick
vue更新dom是异步的,数据是同步的。
nextTick 就是创建一个异步任务,那么它自然要等到同步任务执行完成后才执行。
<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>
源码地址 core\packages\runtime-core\src\scheduler.ts
const resolvedPromise: Promise<any> = Promise.resolve()
let currentFlushPromise: Promise<void> | null = null
export function nextTick<T = void>(
this: T,
fn?: (this: T) => void
): Promise<void> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
nextTick 接受一个参数fn(函数)定义了一个变量P 这个P最终返回都是Promise,最后是return 如果传了fn 就使用变量P.then执行一个微任务去执行fn函数,then里面this 如果有值就调用bind改变this指向返回新的函数,否则直接调用fn,如果没传fn,就返回一个promise,最终结果都会返回一个promise
在我们之前讲过的ref源码中有一段 triggerRefValue 他会去调用 triggerEffects
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref)
if (ref.dep) {
if (__DEV__) {
triggerEffects(ref.dep, {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
triggerEffects(ref.dep)
}
}
}
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
//当响应式对象发生改变后,执行 effect 如果有 scheduler 这个参数,会执行这个 scheduler 函数
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
}
那么scheduler 这个函数从哪儿来的 我们看这个类 ReactiveEffect
export class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
parent: ReactiveEffect | undefined = undefined
/**
* Can be attached after creation
* @internal
*/
computed?: ComputedRefImpl<T>
/**
* @internal
*/
allowRecurse?: boolean
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null, //我在这儿
scope?: EffectScope
) {
recordEffectScope(this, scope)
}
scheduler 作为一个参数传进来的
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(instance.update),
instance.scope // track it in component's effect scope
))
他是在初始化 effect 通过 queueJob 传进来的
//queueJob 维护job列队,有去重逻辑,保证任务的唯一性,每次调用去执行,被调用的时候去重,每次调用去执行 queueFlush
export function queueJob(job: SchedulerJob) {
// 判断条件:主任务队列为空 或者 有正在执行的任务且没有在主任务队列中 && job 不能和当前正在执行任务及后面待执行任务相同
// 重复数据删除:
// - 使用Array.includes(Obj, startIndex) 的 起始索引参数:startIndex
// - startIndex默认为包含当前正在运行job的index,此时,它不能再次递归触发自身
// - 如果job是一个watch()回调函数或者当前job允许递归触发,则搜索索引将+1,以允许他递归触发自身-用户需要确保回调函数不会死循环
if (
(!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)) &&
job !== currentPreFlushParentJob
) {
if (job.id == null) {
queue.push(job)
} else {
queue.splice(findInsertionIndex(job.id), 0, job)
}
queueFlush()
}
}
queueJob 维护job列队 并且调用 queueFlush
function queueFlush() {
// 避免重复调用flushJobs
if (!isFlushing && !isFlushPending) {
isFlushPending = true
//开启异步任务处理flushJobs
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
queueFlush 给每一个队列创建了微任务
如何去理解Tick
例如我们显示器是60FPS
那浏览器绘制一帧就是1000 / 60 ≈ 16.6ms
那浏览器这一帧率做了什么
1.处理用户的事件,就是event 例如 click,input change 等。
2.执行定时器任务
3.执行 requestAnimationFrame
4.执行dom 的回流与重绘
5.计算更新图层的绘制指令
6.绘制指令合并主线程 如果有空余时间会执行 requestidlecallback
所以 一个Tick 就是去做了这些事
课程代码
<template>
<div ref="box" class="wraps">
<div>
<div class="item" v-for="item in chatList">
<div>{{ item.name }}:</div>
<div>{{ item.message }}</div>
</div>
</div>
</div>
<div class="ipt">
<div>
<textarea v-model="ipt" type="text" />
</div>
<div>
<button @click="send">send</button>
</div>
</div>
<HelloWorld></HelloWorld>
</template>
<script setup lang='ts'>
import { reactive,ref,nextTick,getCurrentInstance, watch } from 'vue'
import HelloWorld from './components/HelloWorld.vue';
// let instance = getCurrentInstance()
// console.log(instance);
let current = ref(0)
watch(current,(newVal,oldVal)=>{
console.log(newVal);
})
//next Tick
//60FPS 1000/60 = 16.7ms
// 1.处理用户的事件,就是event 例如 click,input change 等。
// 2.执行定时器任务
// 3.执行 requestAnimationFrame
// 4.执行dom 的回流与重绘
// 5.计算更新图层的绘制指令
// 6.绘制指令合并主线程 如果有空余时间会执行 requestidlecallback
// for (let i =0;i<1000;i++) {
// current.value = i
// }
let chatList = reactive([
{ name: '张三', message: "xxxxxxxxx" },
])
let box = ref<HTMLDivElement>()
let ipt = ref('')
//Vue 更新dom是异步的 数据更新是同步
//我们本次执行的代码是同步代码
//当我们操作dom 的时候发现数据读取的是上次的 就需要使用nextIick
const send = async () => {
chatList.push({
name:"小满",
message:ipt.value
})
//1.回调函数模式
//2.async await 写法
await nextTick()
//异步具有传染性,后面调send的方法全都变成异步了,考下大家,如何消除send的异步传染性
box.value!.scrollTop = 99999999
//ipt.value = ''
}
</script>
<style scoped lang='less'>
.wraps {
margin: 10px auto;
width: 500px;
height: 400px;
overflow: auto;
overflow-x: hidden;
background: #fff;
border: 1px solid #ccc;
.item {
width: 100%;
height: 50px;
background: #ccc;
display: flex;
align-items: center;
padding: 0 10px;
border-bottom: 1px solid #fff;
}
}
.ipt {
margin: 10px auto;
width: 500px;
height: 40px;
background: #fff;
border: 1px solid #ccc;
textarea {
width: 100%;
height: 100%;
border: none;
outline: none;
}
button {
width: 100px;
margin: 10px 0;
float: right;
}
}
</style>
移动端ionic
如果使用npm init vue@latest 报错
error when starting dev server: Error: Cannot find module ‘node:path’
nodejs 升级为16版本就好了
开发移动端最主要的就是适配各种手机,为此我研究了一套解决方案
在之前我们用的是rem 根据HTML font-size 去做缩放
现在有了更好用的vw vh
vw 视口的最大宽度,1vw等于视口宽度的百分之一
vh 视口的最大高度,1vh等于视口高度的百分之一
1.安装依赖
npm install postcss-px-to-viewport -D
因为vite中已经内联了postcss,所以并不需要额外的创建 postcss.config.js文件
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'
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))
}
}
})
*如果你用的vite 是 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.app postcss-px-to-viewport.d.ts跟vite.ts同级
{
"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/*"]
}
}
}
代码案例
<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>sz{{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>
如何将vue项目打包成App
https://xiaoman.blog.csdn.net/article/details/125490078
h5适配
在开发移动端的时候需要适配各种机型,有大的,有小的,我们需要一套代码,在不同的分辨率适应各种机型。
因此我们需要设置meta标签
<meta name="viewport" content="width=device-width, initial-scale=1.0">
移动设备具有各种不同的屏幕尺寸和分辨率,例如智能手机和平板电脑。为了提供更好的用户体验,网页需要根据设备的屏幕宽度进行自适应布局。如果不设置width=device-width,移动设备会按照默认的视口宽度(通常是较宽的桌面屏幕)来渲染网页,导致网页内容在移动设备上显示不正常,可能出现内容被截断或需要水平滚动的情况
圣杯布局
然后我们实现一个经典的圣杯布局
**圣杯布局:**在CSS中,圣杯布局是指两边盒子宽度固定,中间盒子自适应的三栏布局,其中,中间栏放到文档流前面,保证先行渲染;
<template>
<div>
<header>
<div>left</div>
<div>center</div>
<div>right</div>
</header>
</div>
</template>
css
header {
display: flex;
justify-content: space-between;
div {
height: 50px;
color: white;
text-align: center;
line-height: 50px;
}
div:nth-child(1) {
width: 100px;
background: red;
}
div:nth-child(2) {
flex: 1;
background: green;
}
div:nth-child(3) {
width: 100px;
background: blue;
}
}
正常手机看着也还行
![image-20231125170450420](https://img-blog.csdnimg.cn/img_convert/4f8ce0e64d4b0879ad931b0ae27bfc36.png)
但是如果是小手机就会有问题 很挤
![image-20231125170512540](https://img-blog.csdnimg.cn/img_convert/543aea0a10ac69aa3c4fb5ded05a920c.png)
自适应
发现px是相对单位固定的,无法进行自适应,不会随着屏幕尺寸的改变而改变。
而rem 是根据html的font-size 进行缩放的,可以进行自适应,缺点就是需要计算每个屏幕大小所对应的font-size.
百分比是相对父元素的,
vw vh是相对viewport 视口的单位,配合meta标签可以直接使用,无需计算
1vw=1/100视口宽度
1vh=1/100视口高度
当前屏幕视口是375像素,1vw就是3.75像素
现在知道了用什么单位,但是我们还要根据px去换算vw就很麻烦,能不能自动转换???
postCss
https://cn.vitejs.dev/config/shared-options.html#css-postcss
发现vite已经内置了postCss
https://www.postcss.com.cn/
postCss 提供了 把Css 转换AST的能力,类似于Babel,为此我们可以编写一个插件用于将px转换为vw
npm init vue
构建一个vue项目
根目录新建一个plugins文件夹新建两个文件pxto-viewport.ts type.ts
然后在 tsconfig.node.json 的includes 配置 “plugins/**/*”,
compilerOptions 配置 noImplicitAny:false
pxto-viewport.js
//vite内置了postcss 无需安装
import type { Options } from './type'
import type { Plugin } from 'postcss'
const defaultOptions = {
viewPortWidth: 375,
mediaQuery: false,
unitToConvert:'px'
}
export const pxToViewport = (options: Options = defaultOptions): Plugin => {
const opt = Object.assign({}, defaultOptions, options)
return {
postcssPlugin: 'postcss-px-to-viewport',
//css节点都会经过这个钩子
Declaration(node) {
const value = node.value
//匹配到px 转换成vw
if (value.includes(opt.unitToConvert)) {
const num = parseFloat(value)//考虑到有小数点
const transformValue = (num / opt.viewPortWidth) * 100
node.value = `${transformValue.toFixed(2)}vw` //转换之后的值
}
},
}
}
type.ts
export interface Options {
viewPortWidth?: number;
mediaQuery?: boolean;
unitToConvert?: string;
}
vite.config.ts 引入我们写好的插件
css:{
postcss:{
plugins:[
pxToViewport()
]
},
},
这样的话各种屏幕都差不多了。
额外的小知识
比如要增加一个 可以设置全局的字体大小 或者全局背景颜色切换应该怎么做呢?
- 安装vueUse
npm i @vueuse/core
- 定义Css变量
:root {
--size: 14px;
}
div {
height: 50px;
color: white;
text-align: center;
line-height: 50px;
font-size: var(--size);
}
3.切换字体大小
<div>
<button @click="change(36)">大</button>
<button @click="change(24)">中</button>
<button @click="change(14)">小</button>
</div>
import { useCssVar } from '@vueuse/core'
const change = (str: number) => {
const color = useCssVar('--size')
color.value = `${str}px`
}
useCssVar 的底层原理就是
document.documentElement.style.getPropertyValue('--size')`
读取就是get设置就是set 只要想切换的页面用这个css变量就可以了,如果想持久存储就用`localstorage
![image-20231125170948135](https://img-blog.csdnimg.cn/img_convert/e5a841286cef8bb5387de7997880b1dd.png)
![image-20231125171011921](https://img-blog.csdnimg.cn/img_convert/599620af453f518049834ac0180b1a33.png)
unoCss原子化
重新构想原子化CSS - 知乎
什么是css原子化?
CSS原子化的优缺点
1.减少了css体积,提高了css复用
2.减少起名的复杂度
3.增加了记忆成本 将css拆分为原子之后,你势必要记住一些class才能书写,哪怕tailwindcss提供了完善的工具链,你写background,也要记住开头是bg
接入unocss
tips:最好用于vite webpack属于阉割版功能很少
npm i -D unocss
vite.config.ts
import unocss from 'unocss/vite'
plugins: [vue(), vueJsx(),unocss({
rules:[
]
})],
main.ts 引入
import 'uno.css'
配置静态css
rules: [
['flex', { display: "flex" }]
]
配置动态css(使用正则表达式)
m-参数*10 例如 m-10 就是 margin:100px
rules: [
[/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 10}px` })],
['flex', { display: "flex" }]
]
shortcuts 可以自定义组合样式
plugins: [vue(), vueJsx(), unocss({
rules: [
[/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 10}px` })],
['flex', { display: "flex" }],
['pink', { color: 'pink' }]
],
shortcuts: {
btn: "pink flex"
}
})],
unocss 预设
presets:[presetIcons(),presetAttributify(),presetUno()]
1.presetIcons Icon图标预设
图标集合安装
npm i -D @iconify-json/ic
首先我们去icones官网(方便浏览和使用iconify)浏览我们需要的icon,比如这里我用到了Google Material Icons图标集里面的baseline-add-circle图标
<div class="i-ic-baseline-backspace text-3xl bg-green-500" />
2.presetAttributify 属性化模式支持
属性语义化 无须class
<div font="black">
btn
</div>
3.presetUno 工具类预设
默认的 @unocss/preset-uno 预设(实验阶段)是一系列流行的原子化框架的 通用超集,包括了 Tailwind CSS,Windi CSS,Bootstrap,Tachyons 等。
函数式编程,h函数
之前跟大家介绍了两种vue编写风格分别是template模板方式,和JSX方式感觉JSX被大家吐槽的很厉害,其实用习惯还挺好用的今天介绍第三种函数式编程
这个东西在Vue3使用的很少了,大家有个了解就可以了,之前为什么会有这个,应为Vue单文件组件编译是需要过程,他会经过parser ->ast-> transform ->js api -> generate - >render 而h函数直接跳过这三个编译阶段,所以性能上有很大的帮助。
主要会用到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>
electron桌面程序
Electron是一个跨平台的桌面应用程序开发框架,它允许开发人员使用Web技术(如HTML、CSS和JavaScript)构建桌面应用程序,这些应用程序可以在Windows、macOS和Linux等操作系统上运行。
Electron的核心是Chromium浏览器内核和Node.js运行时环境。Chromium内核提供了现代浏览器的功能,例如HTML5和CSS3支持,JavaScript引擎等,而Node.js运行时环境则提供了服务器端JavaScript的能力和模块系统,这使得开发人员可以使用Node.js的模块和工具来构建桌面应用程序。
Electron 案例
- Visual Studio Code:由Microsoft开发的跨平台代码编辑器,支持多种编程语言和插件扩展。使用Electron和TypeScript构建。
- Atom:由GitHub开发的跨平台代码编辑器,支持多种编程语言和插件扩展。使用Electron和CoffeeScript构建。
- Postman:由Postman Inc.开发的API测试和开发工具,允许用户轻松地测试和调试REST API。使用Electron和React构建。
![image-20231126104218840](https://img-blog.csdnimg.cn/img_convert/f6f0d4682ee5b3be138a5f453177d00f.png)
1. 创建项目 dev
# 创建Vue项目
npm init vue
# 安装依赖
npm install
# 一定要安装成开发依赖
npm install electron electron-builder -D
# 安装超时 请使用某宝镜像 或者XX上网
npm config set electron_mirror=https://registry.npmmirror.com/-/binary/electron/
弹幕说:安装electron得用npm源地址 不能使用淘宝的源
2.开发环境启动electron
小满Vue3(第三十九章 electron桌面程序)_哔哩哔哩_bilibili
我们希望npm run dev的时候直接把electron也启动起来而不是开两个启动一次vite再启动一次electron
第一步我们需要先建立一个文件夹
在根目录创建一个plugins
编写vite插件帮我们启动electron
-
plugins
vite.electron.dev.ts //编写electron开发模式
vite.electron.build.ts //打包electron项目 -
index.html
-
src
-
main.ts
-
App.vue
-
background.ts //手动创建文件用于编写electron
-
-
package.json
-
tsconfig.json
-
vite.config.ts
-
background.ts
background.ts
import { app, BrowserWindow } from 'electron'
// 等待Electron应用就绪后创建BrowserWindow窗口
app.whenReady().then(async () => {
const win = await new BrowserWindow({
width: 800,
height: 600,
// 配置窗口的WebPreferences选项,用于控制渲染进程的行为
webPreferences: {
nodeIntegration: true, // 启用Node.js集成
contextIsolation: false, // 禁用上下文隔离
webSecurity: false, // 禁用web安全策略
}
})
// 根据命令行参数加载URL或本地文件
if (process.argv[2]) {
win.loadURL(process.argv[2])
} else {
win.loadFile('index.html')
}
})
这段代码创建了一个Electron应用程序的入口文件。该文件使用了Electron的app和BrowserWindow模块来创建一个窗口。在应用程序准备就绪后,它会创建一个新的BrowserWindow对象,并将其设置为800x600像素的大小。窗口的webPreferences选项用于配置渲染进程的行为,例如启用Node.js集成、禁用上下文隔离和web安全策略等。
接着,该代码检查命令行参数,如果有参数则加载URL,否则加载本地文件index.html。在开发模式下,可以将URL指向本地的开发服务器,以便实现热更新和实时调试。在生产模式下,需要将URL指向本地的index.html文件,以便在本地运行Electron应用程序。
在这段代码中,app.whenReady()函数用于在Electron应用程序准备就绪后执行回调函数。该函数返回一个Promise对象,可以使用async/await语法来等待应用程序就绪后执行其他操作。在这个例子中,我们使用await关键字来等待BrowserWindow对象的创建完成。
vite.electron.dev.ts
// 导入需要使用的类型和库
import type { Plugin } from 'vite'
import type { AddressInfo } from 'net'
import { spawn } from 'child_process'
import fs from 'fs'
// 导出Vite插件函数,vite插件要求必须导出一个对象,对象必须有name属性
export const viteElectronDev = (): Plugin => {
return {
name: 'vite-electron-dev',
// 在configureServer中实现插件的逻辑
configureServer(server) {
// 定义初始化Electron的函数
const initElectron = () => {
// 使用esbuild编译TypeScript代码为JavaScript
require('esbuild').buildSync({
entryPoints: ['src/background.ts'],
bundle: true,
outfile: 'dist/background.js',
platform: 'node',
target: 'node12',
external: ['electron']
})
}
// 调用初始化Electron函数
initElectron()
// 监听Vite的HTTP服务器的listening事件
server?.httpServer?.once('listening', () => {
// 获取HTTP服务器的监听地址和端口号
const addressInfo = server?.httpServer?.address() as AddressInfo
const IP = `http://localhost:${addressInfo.port}`
// 启动Electron进程
let electronProcess = spawn(require('electron'), ['dist/background.js', IP])
// 监听主进程代码的更改
fs.watchFile('src/background.ts', () => {
// 杀死当前的Electron进程
electronProcess.kill()
// 重新编译主进程代码并重新启动Electron进程
initElectron()
//进程重新启动
electronProcess = spawn(require('electron'), ['dist/background.js', IP])
})
// 监听Electron进程的stdout输出
electronProcess.stdout?.on('data', (data) => {
console.log(`日志: ${data}`);
});
})
}
}
}
configureServer是Vite的一个插件钩子函数,用于在Vite开发服务器启动时执行一些自定义逻辑。该函数接受一个ServerOptions对象作为参数,该对象包含有关当前Vite服务器的配置信息。在这个钩子函数中,您可以访问Vite服务器的HTTP服务器对象(httpServer),WebSocket服务器对象(wsServer)和Vite的构建配置对象(config)等。您可以使用这些对象来实现各种功能,例如自定义路由、添加中间件、实现实时重载和调试等。
esbuild.buildSync()
-
entryPoints
:指定要编译的入口文件,这里是src/background.ts
。 -
bundle:指定是否打包所有依赖项,这里是true,表示需要打包所有依赖项。
-
outfile:指定输出文件的路径和名称,这里是dist/background.js。
-
platform:指定编译的目标平台,这里是node,表示编译为Node.js可用的代码。
-
target:指定编译的目标JavaScript版本,这里是node12,表示编译为Node.js 12及以上版本可用的代码。
-
external:指定不需要被打包的外部依赖项,这里是[‘electron’],表示electron模块不需要被打包。
在这段代码中,
esbuild
会将src/background.ts
文件编译为JavaScript 并且放入distfs.watch
主要实现热更新
每次background.ts 修改完成就会重新启动electron进程
vite.electron.build.ts
import type { Plugin } from 'vite'
import * as electronBuilder from 'electron-builder'
import path from 'path'
import fs from 'fs'
// 导出Vite插件函数
export const viteElectronBuild = (): Plugin => {
return {
name: 'vite-electron-build',
// closeBundle是Vite的一个插件钩子函数,用于在Vite构建完成后执行一些自定义逻辑。
closeBundle() {
// 定义初始化Electron的函数
const initElectron = () => {
// 使用esbuild编译TypeScript代码为JavaScript
require('esbuild').buildSync({
entryPoints: ['src/background.ts'],
bundle: true,
outfile: 'dist/background.js',
platform: 'node',
target: 'node12',
external: ['electron'],
})
}
// 调用初始化Electron函数
initElectron()
// 修改package.json文件的main字段 不然会打包失败
const json = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
json.main = 'background.js'
fs.writeSync(fs.openSync('dist/package.json', 'w'), JSON.stringify(json, null, 2))
//electron-builder有一个bug,会给你下载垃圾文件
// 创建一个空的node_modules目录 不然会打包失败
fs.mkdirSync(path.join(process.cwd(), "dist/node_modules"));
// 使用electron-builder打包Electron应用程序
electronBuilder.build({
config: {
appId: 'com.example.app',
productName: 'vite-electron',
directories: {
output: path.join(process.cwd(), "release"), //输出目录
app: path.join(process.cwd(), "dist"), //app目录
},
asar: true,
nsis: {
oneClick: false, //取消一键安装
}
}
})
}
}
}
打包主要依靠electron-builder
这个库 他的参数是有很多的这儿只是简单演示
closeBundle
我们electron打包是需要index.html 所以我们先等vite打完包之后vite会自动调用这个钩子 然后在这个钩子里面打包electron
vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import {viteElectronDev} from './plugins/vite.electron.dev'
import {viteElectronBuild} from './plugins/vite.electron.build'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
viteElectronDev(),
viteElectronBuild()
],
base:'./', //默认绝对路径改为相对路径 否则打包白屏
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
编译宏
Vue 3.3新增了一些语法糖和宏,包括泛型组件、defineSlots、defineEmits、defineOptions
1.defineProps
- 父子组件传参
<template>
<div>
<Child name="xiaoman"></Child>
</div>
</template>
<script lang='ts' setup>
import Child from './views/child.vue'
</script>
<style></style>
子组件使用defineProps
接受值
<template>
<div>
{{ name }}
</div>
</template>
<script lang='ts' setup>
defineProps({
name: String
})
</script>
- 使用TS字面量模式
<template>
<div>
{{ name }}
</div>
</template>
<script lang='ts' setup>
defineProps<{
name:string
}>()
</script>
Vue3.3
新增 defineProps 可以接受泛型
<Child :name="['xiaoman']"></Child>
//-------------子组件-----------------
<template>
<div>
{{ name }}
</div>
</template>
<script generic="T" lang='ts' setup>
defineProps<{
name:T[]
}>()
</script>
2. defineEmits
- 父组件
<template>
<div>
<Child @send="getName"></Child>
</div>
</template>
<script lang='ts' setup>
import Child from './views/child.vue'
const getName = (name: string) => {
console.log(name)
}
</script>
<style></style>
子组件常规方式派发Emit
<template>
<div>
<button @click="send">派发事件</button>
</div>
</template>
<script lang='ts' setup>
const emit = defineEmits(['send'])
const send = () => {
// 通过派发事件,将数据传递给父组件
emit('send', '我是子组件的数据')
}
</script>
子组件TS字面量模式派发
<template>
<div>
<button @click="send">派发事件</button>
</div>
</template>
<script lang='ts' setup>
const emit = defineEmits<{
(event: 'send', name: string): void
}>()
const send = () => {
// 通过派发事件,将数据传递给父组件
emit('send', '我是子组件的数据')
}
</script>
Vue3.3
新写法更简短
<template>
<div>
<button @click="send">派发事件</button>
</div>
</template>
<script lang='ts' setup>
const emit = defineEmits<{
'send':[name:string]
}>()
const send = () => {
// 通过派发事件,将数据传递给父组件
emit('send', '我是子组件的数据')
}
</script>
3. defineExpose
没变化
defineExpose({
name:"张三"
})
4.defineSlots
- 父组件
<template>
<div>
<Child :data="list">
<template #default="{item}">
<div>{{ item.name }}</div>
</template>
</Child>
</div>
</template>
<script lang='ts' setup>
import Child from './views/child.vue'
const list = [
{
name: "张三"
},
{
name: "李四"
},
{
name: "王五"
}
]
</script>
<style></style>
子组件 defineSlots只做声明不做实现 同时约束slot类型
<template>
<div>
<ul>
<li v-for="(item,index) in data">
<slot :index="index" :item="item"></slot>
</li>
</ul>
</div>
</template>
<script generic="T" lang='ts' setup>
defineProps<{
data: T[]
}>()
defineSlots<{
default(props:{item:T,index:number}):void
}>()
</script>
5. defineOptions
- 主要是用来定义 Options API 的选项
常用的就是定义name 在seutp 语法糖模式发现name不好定义了需要在开启一个script自定义name现在有了defineOptions就可以随意定义name了
defineOptions({
name:"Child",
inheritAttrs:false,
})
6.defineModel
由于该API处于实验性特性 可能会被删除暂时不讲
warnOnce(
`This project is using defineModel(), which is an experimental ` +
`feature. It may receive breaking changes or be removed in the future, so ` +
`use at your own risk.\n` +
`To stay updated, follow the RFC at https://github.com/vuejs/rfcs/discussions/503.`
)
7.源码解析
- core\packages\compiler-sfc\src\script\defineSlots.ts
export function processDefineSlots(
ctx: ScriptCompileContext,
node: Node,
declId?: LVal
): boolean {
//是否调用了defineSlots
if (!isCallOf(node, DEFINE_SLOTS)) {
return false
}
//是否重复调用了defineSlots
if (ctx.hasDefineSlotsCall) {
ctx.error(`duplicate ${DEFINE_SLOTS}() call`, node)
}
//函数将 ctx 对象的 hasDefineSlotsCall 属性设置为 true,表示已经调用了 DEFINE_SLOTS 函数
ctx.hasDefineSlotsCall = true
//然后函数检查传递给 DEFINE_SLOTS 函数的参数个数是否为零,如果不是,则函数抛出错误,指示 DEFINE_SLOTS 函数不接受参数。
if (node.arguments.length > 0) {
ctx.error(`${DEFINE_SLOTS}() cannot accept arguments`, node)
}
//接下来,如果函数接收到了一个可选的表示插槽定义的标识符的节点对象,
//则函数使用 ctx.s.overwrite
//方法将该节点对象替换为一个表示使用插槽的帮助函数的调用
if (declId) {
ctx.s.overwrite(
ctx.startOffset! + node.start!, //开始位置
ctx.startOffset! + node.end!, //结束位置
`${ctx.helper('useSlots')}()` //替换的内容 此时就拥有了类型检查
)
}
return true
}
- core\packages\compiler-sfc\src\script\defineOptions.ts
export function processDefineOptions(
ctx: ScriptCompileContext,
node: Node
): boolean {
//是否调用了defineOptions
if (!isCallOf(node, DEFINE_OPTIONS)) {
return false
}
//是否重复调用了defineOptions
if (ctx.hasDefineOptionsCall) {
ctx.error(`duplicate ${DEFINE_OPTIONS}() call`, node)
}
//defineOptions()不能接受类型参数
if (node.typeParameters) {
ctx.error(`${DEFINE_OPTIONS}() cannot accept type arguments`, node)
}
//defineOptions()必须接受一个参数
if (!node.arguments[0]) return true
//函数将 ctx 对象的 hasDefineOptionsCall 属性设置为 true,表示已经调用了 DEFINE_OPTIONS 函数
ctx.hasDefineOptionsCall = true
//函数将 ctx 对象的 optionsRuntimeDecl 属性设置为传递给 DEFINE_OPTIONS 函数的参数
ctx.optionsRuntimeDecl = unwrapTSNode(node.arguments[0])
let propsOption = undefined
let emitsOption = undefined
let exposeOption = undefined
let slotsOption = undefined
//遍历 optionsRuntimeDecl 的属性,查找 props、emits、expose 和 slots 属性
if (ctx.optionsRuntimeDecl.type === 'ObjectExpression') {
for (const prop of ctx.optionsRuntimeDecl.properties) {
if (
(prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') &&
prop.key.type === 'Identifier'
) {
if (prop.key.name === 'props') propsOption = prop
if (prop.key.name === 'emits') emitsOption = prop
if (prop.key.name === 'expose') exposeOption = prop
if (prop.key.name === 'slots') slotsOption = prop
}
}
}
//禁止使用defineOptions()来声明props、emits、expose和slots
if (propsOption) {
ctx.error(
`${DEFINE_OPTIONS}() cannot be used to declare props. Use ${DEFINE_PROPS}() instead.`,
propsOption
)
}
if (emitsOption) {
ctx.error(
`${DEFINE_OPTIONS}() cannot be used to declare emits. Use ${DEFINE_EMITS}() instead.`,
emitsOption
)
}
if (exposeOption) {
ctx.error(
`${DEFINE_OPTIONS}() cannot be used to declare expose. Use ${DEFINE_EXPOSE}() instead.`,
exposeOption
)
}
if (slotsOption) {
ctx.error(
`${DEFINE_OPTIONS}() cannot be used to declare slots. Use ${DEFINE_SLOTS}() instead.`,
slotsOption
)
}
return true
}
环境变量
1.配置主要环境变量
环境变量:他的主要作用就是让开发者区分不同的运行环境,来实现 兼容开发和生产
例如 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 注入浏览器的
2. 配置额外的环境变量
在根目录新建env 文件 可以创建多个
如下 env.[name]
![image-20231126210158072](https://img-blog.csdnimg.cn/img_convert/26a997fcdece326a3d711a3054f3d176.png)
修改启动命令
在 package json 配置 --mode env文件名称
![image-20231126210223162](https://img-blog.csdnimg.cn/img_convert/f33350d7f9a108d8a6c360474b67c60c.png)
3.配置智能提示
interface ImportMetaEnv {
VITE_XIAOMAN:string
}
![image-20231126210323856](https://img-blog.csdnimg.cn/img_convert/813947e110aaf5b2757d7459f0f8dad4.png)
然后App.vue 输出 JSON.stringify(import.meta.env)
就已经添加进去了
4. 生产环境使用
创建 .env.production 在执行npm run build 的时候他会自己加载这个文件
5.如果想在vite.config.ts使用环境变量
![image-20231126211209979](https://img-blog.csdnimg.cn/img_convert/7c6ac43f9a55a78e06f42a9d95a16b16.png)
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/
export default ({mode}:any) => {
console.log(loadEnv(mode,process.cwd()))
return defineConfig({
plugins: [vue(), vueJsx()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
}
我们就可以通过环境变量这个值 做一些事情比如 切换接口url 等
webpack构建vue3项目
为什么要手写webpack 不用cli (脑子有病)并不是 其实是为了加深我们对webpack 的了解方便以后灵活运用webpack 的技术
1.初始化项目结构(跟cli 结构保持一致)
2.安装所需要的依赖包
{
"name": "webpack-vue",
"version": "1.0.0",
"description": "",
"main": "webpack.config.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack-dev-server",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@vue/compiler-sfc": "^3.2.38", //解析vue文件
"clean-webpack-plugin": "^4.0.0", //打包 的时候清空dist
"css-loader": "^6.7.1", //处理css文件
"friendly-errors-webpack-plugin": "^1.7.0", //美化dev
"html-webpack-plugin": "^5.5.0", //html 模板
"less": "^4.1.3", //处理less
"less-loader": "^11.0.0", //处理less文件
"style-loader": "^3.3.1", //处理style样式
"ts-loader": "^9.3.1", //处理ts
"typescript": "^4.8.2", //ts
"vue": "^3.2.38", //vue
"vue-loader": "^17.0.0", //解析vue
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.10.0"
}
}
3.tsc --init 生成ts 文件 如果没有tsc 安装npm install typescript -g
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}
4.配置vue声明文件,不然ts识别不了vue后缀
declare module "*.vue" {
import { DefineComponent } from "vue"
const component: DefineComponent<{}, {}, any>
export default component
}
![image-20231126220845915](https://img-blog.csdnimg.cn/img_convert/14f2c24419d77d87bd61bee4659f1f90.png)
5.编写webpack config js
const { Configuration } = require('webpack')
const path = require('path')
const htmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader/dist/index');
const FriendlyErrorsWebpackPlugin = require("friendly-errors-webpack-plugin");
/**
* @type {Configuration} //配置智能提示
*/
const config = {
mode: "development",
entry: './src/main.ts', //入口文件
output: {
filename: "[hash].js",
path: path.resolve(__dirname, 'dist') //出口文件
},
module: {
rules: [
{
test: /\.vue$/, //解析vue 模板
use: "vue-loader"
},
{
test: /\.less$/, //解析 less
use: ["style-loader", "css-loader", "less-loader"],
},
{
test: /\.css$/, //解析css
use: ["style-loader", "css-loader"],
},
{
test: /\.ts$/, //解析ts
loader: "ts-loader",
options: {
configFile: path.resolve(process.cwd(), 'tsconfig.json'),
appendTsSuffixTo: [/\.vue$/]
},
}
]
},
plugins: [
new htmlWebpackPlugin({
template: "./public/index.html" //html模板
}),
new CleanWebpackPlugin(), //打包清空dist
new VueLoaderPlugin(), //解析vue
new FriendlyErrorsWebpackPlugin({
compilationSuccessInfo:{ //美化样式
messages:['You application is running here http://localhost:9001']
}
})
],
resolve: {
alias: {
"@": path.resolve(__dirname, './src') // 别名
},
extensions: ['.js', '.json', '.vue', '.ts', '.tsx'] //识别后缀
},
stats:"errors-only", //取消提示
devServer: {
proxy: {},
port: 9001,//修改端口号
hot: true,
open: true,
},
//性能优化,通过cdn引入方式,考验网速
externals: {
vue: "Vue" //CDN 引入
},
}
module.exports = config
效果:
显示对应的name。
vue3的性能优化
1.浏览器工具跑分
我们可以使用谷歌浏览器自带的DevTools 进行性能分析 LightHouse
1.1参数介绍
参数介绍
从Performance页的表现结果来看,得分37分,并提供了很多的时间信息,我们来解释下这些选项代表的意思:
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按钮。
1.2代码分析
由于我们使用的是vite vite打包是基于rollup 的我们可以使用 rollup 的插件
npm install rollup-plugin-visualizer
vite.config.ts 配置 记得设置open 不然无效
import { visualizer } from 'rollup-plugin-visualizer';
plugins: [vue(), vueJsx(),visualizer({
open:true
})],
然后进行npm run build打包
我在项目中使用了 Ant Design 发现 这个UI 库非常庞大 这时候 就可以使用 Ant Design 的按需引入减少 包大小
2.vite配置优化
build:{
chunkSizeWarningLimit:2000,
cssCodeSplit:true, //css 拆分
sourcemap:false, //不生成sourcemap
minify:false, //是否禁用最小化混淆,esbuild打包速度最快,terser打包体积最小。
assetsInlineLimit:5000 //小于该值 图片将打包成Base64
},
3.PWA离线存储技术
npm install vite-plugin-pwa -D
import { VitePWA } from 'vite-plugin-pwa'
plugins: [vue(),VitePWA(), vueJsx(),visualizer({
open:true
})],
PWA 技术的出现就是让web网页无限接近于Native 应用
- 可以添加到主屏幕,利用manifest实现
- 可以实现离线缓存,利用service worker实现
- 可以发送通知,利用service worker实现
VitePWA({
workbox:{
cacheId:"XIaoman",//缓存名称
runtimeCaching:[
{
urlPattern:/.*\.js.*/, //缓存文件
handler:"StaleWhileRevalidate", //重新验证时失效
options:{
cacheName:"XiaoMan-js", //缓存js,名称
expiration:{
maxEntries:30, //缓存文件数量 LRU算法
maxAgeSeconds:30 * 24 * 60 * 60 //缓存有效期
}
}
}
]
},
})
进行 npm run build 打包会生成 sw.js
4.其他性能优化
图片懒加载
import lazyPlugin from ‘vue3-lazy’
虚拟列表
多线程 使用 new Worker 创建
worker脚本与主进程的脚本必须遵守同源限制。他们所在的路径协议、域名、端口号三者需要相同
const myWorker1 = new Worker("./calcBox.js");
都使用postMessage发送消息
worker.postMessage(arrayBuffer, [arrayBuffer]);
都使用onmessage接收消息
self.onmessage = function (e) {
// xxx这里是worker脚本的内容
};
关闭
worker.terminate();
VueUse 库已经集成了 webWorker
防抖节流
同样VueUse 也是集成了
web Components
1.概念
什么是 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结构类似的组件和样式。然后它们可以作为自定义元素结构的基础被多次重用。
京东的跨端框架 Taro 的组件部分,就是用基于 Web Components 开发的
2.实战案例
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 子树的根节点。
const shaDow = this.attachShadow({mode:"open"})
shaDow.appendChild(this.p)
}
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)
3.template模式
class Btn extends HTMLElement {
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>
作用:总的来说,就是帮我们隔离一些样式和js
4.如何在vue使用
defineCustomElement
告知vue这是一个自定义Component 跳过组件检查
/*vite config ts 配置*/
vue({
template:{
compilerOptions:{
isCustomElement:(tag)=> tag.includes('xiaoman-')
}
}
})
父组件
<template>
<div>
<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>
子组件
<template>
<div>
小满123213 {{title}}
</div>
</template>
<script setup lang='ts'>
import { ref, reactive } from 'vue'
defineProps<{
title:string
}>()
</script>
<style scoped lang='less'>
</style>
传递参数 如果是对象需要序列化 他是作用于 标签上的
proxy跨域
1.什么是跨域
主要是出于浏览器的同源策略限制,它是浏览器最核心也最基本的安全功能。
当一个请求url的 协议、域名、端口 三者之间任意一个与当前页面url不同即为跨域。
例如 xxxx.com -> 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接受返回的参数.
script标签只能发送get请求,不能发送post请求。
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代理 或者 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 请求一下
<script lang="ts" setup>
import {ref,reactive } from 'vue'
fetch('http://localhost:9001/xm')
</script>
复制代码
发现是存在跨域的,这时候我们就可以配合vite的代理来解决跨域 用法如下
需要在vite.config.js/ts 进行配置
export default defineConfig({
plugins: [vue()],
//只能解决dev环境的跨域问题
server:{
proxy:{
'/api':{
target:"http://localhost:9001/", //跨域地址
changeOrigin:true, //支持跨域
rewrite:(path) => path.replace(/^\/api/, "")//重写路径,替换/api
}
}
}
})
复制代码
fetch 替换/api 他会截取/api 替换成 target地址
<script lang="ts" setup>
import {ref,reactive } from 'vue'
fetch('/api/xm')
</script>
复制代码
![image-20231126233600529](https://img-blog.csdnimg.cn/img_convert/708ecc76b2fc92f94e639ea850b0c3e6.png)
webpack proxy 和 node proxy 用法都类似
注意:开发的时候配devServe,部署生产的时候配nginx
3.vite proxy原理解析
vite源码地址github.com/vitejs/vite
源码路径 vite/packages/vite/src/node/server/index.ts vite源码 发现他处理proxy 是调用了proxyMiddleware
// proxy
const { proxy } = serverConfig
if (proxy) {
middlewares.use(proxyMiddleware(httpServer, proxy, config))
}
复制代码
vite/packages/vite/src/node/server/middlewares/proxy.ts
找到 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 npm地址 www.npmjs.com/package/htt…
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)
复制代码
9001服务
const express = require('express')
const app = express()
//创建get请求
app.get('/xm',(req,res)=>{
res.json({
code:200,
message:"请求成功"
})
})
//端口号9001
app.listen(9001)
复制代码
成功代理 访问8888端口代理9001的请求