vue3基础

文章目录


小满Vue3(第一章 Vue3概述-梦开始的地方)_哔哩哔哩_bilibili

vue3概述

1.vue2和3区别

vue2风格:选项式API

写起来比较分散

vue3风格:组合式API

选择:

  1. 如果不使用构建工具,在低复杂度场景中使用vue,就用选项式api。
  2. 打算用vue构建完整的单页应用,推荐采用组合式API+单文件组件

2.vue3新特性

  1. 重写双向绑定
  2. VDOM性能瓶颈
  3. Fragments
  4. Tree-Shaking的支持
  5. 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 我们可以通过这个网站看到静态标记

img

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>

image-20230901075913014

虚拟dom和diff算法

1.虚拟dom

虚拟DOM就是通过JS来生成一个AST节点树(抽象语法树)

ts转js的时候,也会进行AST转换

babel插件,es6转es5到时候,也会经过AST这个抽象语法树到转换

js通过v8引擎转这个字节码的时候,也会进行AST。

img

为什么要有虚拟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>

image-20230905081632106 splice 用法

image-20230905081704453

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'
}

refshallowRef不能混用,会影响

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>

image-20230907232406825

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>

发现数据改变,但是页面没变

image-20230907233020387

它只到第一层属性这 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));
}

image-20230911080829113

打印出来,一个是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

实现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>

image-20230912080312011

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

image-20230920080619453 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>

img

onBeforeMount() ------创建之前

在组件DOM实际渲染安装之前调用。在这一步中,根元素还不存在。

onMounted() -------创建完成

在组件的第一次渲染后调用,该元素现在可用,允许直接DOM访问

onBeforeUpdate() ----- 更新之前

数据更新时调用,发生在虚拟 DOM 打补丁之前。(获取的是更新之前的dom)

onUpdated() -------更新完成

DOM更新后,updated的方法即会调用。(获取更新之后的)

onBeforeUnmount() -----销毁之前

在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。

onUnmounted() ------销毁完成

卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。

选项式 APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated

使用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

*样式穿透问题学到第三方组件精讲 ::v-deep >>> /deep/ :deep()*

3) bem架构

他是一种css架构 oocss 实现的一种 (面向对象css) ,BEM实际上是blockelementmodifier的缩写,分别为块层、元素层、修饰符层,element UI 也使用的是这种架构

BEM 命名约定的模式是:

.block {}
 
.block__element {}
 
.block--modifier {}

image-20230926222300420

image-20230926222320935

使用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

使用方法:

在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

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

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

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

使用递归组件,阻止一下冒泡

动态组件

什么是动态组件 就是:让多个组件使用同一个挂载点,并动态切换,这就是动态组件。

在挂载点使用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的切换

image-20231024232304417

注意事项

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

多个使用场景

<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

主要是创建 更新 和删除的逻辑

image-20231026075419871

他通过 resolveTarget 函数 获取了props.to 和 querySelect 获取 了目标元素

然后判断是否有disabled 如果有则 to 属性不生效 否则 挂载新的位置

image-20231026075448582

新节点disabled 为 true 旧节点disabled false 就把子节点移动回容器

如果新节点disabled 为 false 旧节点为true 就把子节点移动到目标元素

image-20231026075517461

遍历teleport 子节点进行unmount方法去移除

image-20231026075546941

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>

includeexclude

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.过渡的类名

  1. 过渡 class

    在进入/离开的过渡中,会有 6 个 class 切换

  2. v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。

  3. v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。

  4. v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。

  5. v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。

  6. v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。

  7. 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 过渡的时候,在 enterleave 钩子中必须使用 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 提供的数据或方法。
image-20231107074656882

看一个例子

父组件传递数据

<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

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

配置完成就可以使用啦

在目录新建一个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

image-20231117074627324

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 image-20231113225021467

浏览器显示 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

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:一个对象,在注册指令时作为参数传递。例如,在以下指令中

image-20231119173019163

第三个 当前元素的虚拟DOM 也就是Vnode

第四个 prevNode 上一个虚拟节点,仅在 beforeUpdateupdated 钩子中可用

4.函数简写

你可能想在 mountedupdated 时触发相同行为,而不关心其他的钩子函数。那么你可以通过将这个函数模式实现

<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。

image-20231119225728214

第二点就是 变量来源不明确(隐式传入),不利于阅读,使代码变得难以维护

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 代替 然后去定义变量和函数

Vue2

// 之前 (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"]
  }
}

一个 Vue 3 UI 框架 | Element Plus

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样式

发现没有生效
image-20231122074232221

如果不写Scoped 就没问题

原因就是Scoped 搞的鬼 他在进行PostCss转化的时候把元素选择器默认放在了最后

image-20231122074340953

Vue 提供了样式穿透:deep() 他的作用就是用来改变 属性选择器的位置

image-20231122074411684

image-20231122074427432

总结就是样式穿透挪动属性选择器到最外层类名。

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>

无效果

image-20231122221828055

默认情况下,作用域样式不会影响到 <slot/> 渲染出来的内容,因为它们被认为是父组件所持有并传递进来的。

解决方案 slotted

<style scoped>
 :slotted(.a) {
    color:red
}
</style>

image-20231122221908972

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
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

3.生成配置文件

npx tailwindcss init -p

配置 - Tailwind CSS 中文文档

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 image-20231122225259573

最后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

**弹幕:**新的规范已经没有宏任务的说法了,叫事件队列或消息队列

案例:

image-20231123222006868

弹幕:从上到下解析,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

但是如果是小手机就会有问题 很挤

image-20231125170512540

自适应
发现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
image-20231125170623167

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()
         ]
     },
  },

image-20231125170725885

这样的话各种屏幕都差不多了。

额外的小知识

比如要增加一个 可以设置全局的字体大小 或者全局背景颜色切换应该怎么做呢?

  1. 安装vueUse
npm i  @vueuse/core
  1. 定义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 image-20231125171011921

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" }]
]

image-20231125225117087

配置动态css(使用正则表达式)

m-参数*10 例如 m-10 就是 margin:100px

rules: [
  [/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 10}px` })],
  ['flex', { display: "flex" }]
]

image-20231125225420158

shortcuts 可以自定义组合样式

  plugins: [vue(), vueJsx(), unocss({
    rules: [
      [/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 10}px` })],
      ['flex', { display: "flex" }],
      ['pink', { color: 'pink' }]
    ],
    shortcuts: {
      btn: "pink flex"
    }
  })],

image-20231125225449415

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" />

image-20231125225616012

2.presetAttributify 属性化模式支持

属性语义化 无须class

<div font="black">
     btn
</div>

image-20231125225703564

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 案例

  1. Visual Studio Code:由Microsoft开发的跨平台代码编辑器,支持多种编程语言和插件扩展。使用Electron和TypeScript构建。
  2. Atom:由GitHub开发的跨平台代码编辑器,支持多种编程语言和插件扩展。使用Electron和CoffeeScript构建。
  3. Postman:由Postman Inc.开发的API测试和开发工具,允许用户轻松地测试和调试REST API。使用Electron和React构建。
image-20231126104218840

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 并且放入dist

    fs.watch 主要实现热更新
    每次background.ts 修改完成就会重新启动electron进程

    image-20231126115040918

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))
    }
  }
})


image-20231126115151803

编译宏

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

修改启动命令

在 package json 配置 --mode env文件名称

image-20231126210223162

3.配置智能提示

interface ImportMetaEnv {
    VITE_XIAOMAN:string
}
image-20231126210323856

然后App.vue 输出 JSON.stringify(import.meta.env)

image-20231126210358267

就已经添加进去了

4. 生产环境使用

创建 .env.production 在执行npm run build 的时候他会自己加载这个文件

image-20231126211039066

image-20231126211105912

5.如果想在vite.config.ts使用环境变量

image-20231126211209979
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 结构保持一致)

image-20231126220550878

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

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

效果:

image-20231126221220838

显示对应的name。

vue3的性能优化

1.浏览器工具跑分

我们可以使用谷歌浏览器自带的DevTools 进行性能分析 LightHouse

image-20231126225049013

image-20231126225110373

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打包

image-20231126225239787

我在项目中使用了 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 应用

  1. 可以添加到主屏幕,利用manifest实现
  2. 可以实现离线缓存,利用service worker实现
  3. 可以发送通知,利用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

image-20231126225429070

image-20231126225445030

4.其他性能优化

图片懒加载

import lazyPlugin from ‘vue3-lazy’

虚拟列表

image-20231126225516776

多线程 使用 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

image-20231126225655370

防抖节流

同样VueUse 也是集成了

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

web Components

1.概念

什么是 Web Components

Web Components 提供了基于原生支持的、对视图层的封装能力,可以让单个组件相关的 javaScript、css、html模板运行在以html标签为界限的局部环境中,不会影响到全局,组件间也不会相互影响 。 再简单来说:就是提供了我们自定义标签的能力,并且提供了标签内完整的生命周期 。

image-20231126231502054

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>

传递参数 如果是对象需要序列化 他是作用于 标签上的

image-20231126231837321

image-20231126231854225

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)
复制代码

image-20231126233447651

我们使用vite项目的fetch 请求一下

<script lang="ts" setup>
import {ref,reactive } from 'vue'
fetch('http://localhost:9001/xm')
</script>
复制代码

image-20231126233514961

发现是存在跨域的,这时候我们就可以配合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

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的请求

image-20231126233823263

  • 20
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值