Vue3.2 + vite + Ts + pinia + 实战 + 源码 +electron

Vue3 第二章(配置环境)

https://xiaoman.blog.csdn.net/category_11618172_2.html 配套笔记

(122条消息) Vue3 + vite + Ts + pinia + 实战 +electron(学习ing,笔记未完待续......)_MajorMe的博客-CSDN博客_vue3 + vite + ts + pinia + 实战 + 源码 +electron笔记

Vscode技巧

vue格式输出:点设置-配置用户代码片段

"Print to console": {
        "prefix": "vv",
        "body": [
            "<template>",
       "", "
<div></div>",
       "", "
</template>", "<script>", "export default {", "}", "</script>", "<style scoped>", "</style>" ], "description": "Log output to console" }, "Print2 to console": { "prefix": "vv1", "body": [ "<template>", "\t<div>", "\t</div>", "</template>", "<script>", "export default {", "}", "</script>", "<style lang=\"style\" rel=\"stylesheet/stylus\">", "</style>" ], "description": "Log output to console" }, "Print3 to console": { "prefix": "vv2", "body": [ "<template>", "\t<div>", "\t</div>", "</template>", "<script>", "export default {", "}", "</script>", "<style lang=\"less\" rel=\"stylesheet/less\" scoped>", "</style>" ], "description": "Log output to console" }, "Print4 to console": { "prefix": "vv3", "body": [ "<template>", "\t<div>", "\t</div>", "</template>", "<script lang=\"ts\">", "import { defineComponent } from 'vue'", "export default defineComponent({", "\tname: 'app'", "})", "</script>", "<style>", "</style>" ], "description": "Log output to console" }

 

1.安装nodejs(建议装14,16,版本稳定)

装过的同学可以忽略

下载 | Node.js 中文网

装完之后会有一个命令叫 npm

可以在终端输入npm -v 来检查是否安装成功

  如果用nvm安装切换版本

  

2.构建vite项目

官方文档开始 {#getting-started} | Vite中文网

vite 的优势

冷服务 默认的构建目标浏览器是能 在 script 标签上支持原生 ESM 和 原生 ESM 动态导入

HMR 速度快到惊人的 模块热更新(HMR)

Rollup打包 它使用 Rollup 打包你的代码,并且它是预配置的 并且支持大部分rollup插件

使用vite初始化一个项目

npm

npm init vite@latest

 Yarn

yarn create vite

运行之后

项目名称

  构建的项目模板

 切换目录:如 cd vite-demo

npm install 安装依赖包

npm run dev 启动

 package json 命令解析

{
  "scripts": {
    "dev": "vite", // 启动开发服务器,别名:`vite dev`,`vite serve`
    "build": "vite build", // 为生产环境构建产物
    "preview": "vite preview" // 本地预览生产构建产物
  }
}

 更为全面的安装:选项更多

npm init vue@latest

  

3.安装Vue cli脚手架 

npm install @vue/cli -g

检查是否安装成功

   vue create <project>

构建我们的cli 项目可以去对比一下

nodejs 底层原理(非重要)

Node.js 主要由 V8、Libuv 和第三方库组成:

  Libuv:跨平台的异步 IO 库,但它提供的功能不仅仅是 IO,还包括进程、线程、信号、定时器、进程间通信,线程池等。
  第三方库:异步 DNS 解析( cares )、HTTP 解析器(旧版使用 http_parser,新版使用 llhttp)、HTTP2 解析器( nghttp2 )、 解压压缩库( zlib )、加密解密库( openssl )等等。
  V8:实现 JS 解析、执行和支持自定义拓展,得益于 V8 支持自定义拓展,才有了 Node.js。

你也可以理解成 js应用层  桥C/C++  底层C/C++ 

  

 libuv源码地址GitHub - libuv/libuv: Cross-platform asynchronous I/O

libuvC语言源码解析 

 

 

Vue3 第三章(Vite目录 & Vue单文件组件 & npm run dev 详解)

VsCode插件安装

  安装支持vue3.0的 Volar

  

Vite目录

public 下面的不会被编译 可以存放静态资源

assets 下面可以存放可编译的静态资源

components 下面用来存放我们的组件

App.vue 是全局组件

main ts 全局的ts文件

index.html 非常重要的入口文件 (webpack,rollup 他们的入口文件都是enrty input 是一个js文件 而Vite 的入口文件是一个html文件,他刚开始不会编译这些js文件 只有当你用到的时候 如script src="xxxxx.js" 会发起一个请求被vite拦截这时候才会解析js文件)

vite config ts 这是vite的配置文件具体配置项 后面会详解

VsCode Vue3 插件推荐 Vue Language Features (Volar)

SFC 语法规范

*.vue 件都由三种类型的顶层语法块所组成:<template>、<script>、<style>

<template>
  每个 *.vue 文件最多可同时包含一个顶层 <template> 块。

  其中的内容会被提取出来并传递给 @vue/compiler-dom,预编译为 JavaScript 的渲染函数,并附属到导出的组件上作为其 render 选项。
————————————————

<script>
每一个 *.vue 文件可以有多个 <script> 块 (不包括<script setup>)。

该脚本将作为 ES Module 来执行。

默认导出的内容应该是 Vue 组件选项对象,它要么是一个普通的对象,要么是 defineComponent 的返回值。
————————————————

<script setup>
每个 *.vue 文件最多只能有一个 <script setup> 块 (不包括常规的 <script>)

该脚本会被预处理并作为组件的 setup() 函数使用,也就是说它会在每个组件实例中执行。<script setup> 的顶层绑定会自动暴露给模板。更多详情请查看 <script setup> 文档。
————————————————

<style>
一个 *.vue 文件可以包含多个 <style> 标签。

<style> 标签可以通过 scoped 或 module attribute (更多详情请查看 SFC 样式特性) 将样式封装在当前组件内。多个不同封装模式的 <style> 标签可以在同一个组件中混

————————————————

npm run dev 详解

在我们执行这个命令的时候会去找 package json 的scripts 然后执行对应的dev命令

  那为什么我们不直接执行vite 命令不是更方便吗

应为在我们的电脑上面并没有配置过相关命令 所以无法直接执行

  其实在我们执行npm install 的时候(包含vite) 会在node_modules/.bin/ 创建好可执行文件

.bin 目录,这个目录不是任何一个 npm 包。目录下的文件,表示这是一个个软链接,打开文件可以看到文件顶部写着 #!/bin/sh ,表示这是一个脚本 

 

 

  在我们执行npm run xxx  npm 会通过软连接 查找这个软连接存在于源码目录node_modules/vite

  所以npm run xxx 的时候,就会到 node_modules/bin中找对应的映射文件,然后再找到相应的js文件来执行

1.查找规则是先从当前项目的node_modlue /bin去找,

2.找不到去全局的node_module/bin 去找

3.再找不到 去环境变量去找

  node_modules/bin中 有三个vite文件。为什么会有三个文件呢?

# unix Linux macOS 系默认的可执行文件,必须输入完整文件名
vite
 
# windows cmd 中默认的可执行文件,当我们不添加后缀名时,自动根据 pathext 查找文件
vite
 
# Windows PowerShell 中可执行文件,可以跨平台
vite

我们使用windows 一般执行的是第二个

MacOS Linux 一般是第一个

提前须知:

<script setup lang='ts'>

 

 

组件导入

  

<script setup lang="ts">
import {ref} from 'vue'
// 注:组件引入时Vue3中需要加、加、加后缀.vue
import Demo from './components/Demo.vue'
import Test from './components/Test.vue'

 

Vue3 第四章(模板语法 & vue指令)

模板插值语法

在script 声明一个变量可以直接在template 使用用法为{{变量名称}}

<template>
  <div>{{ message }}</div>
</template>
<script setup lang="ts"> const message = "我是小满" </script> <style> </style>

模板语法是可以编写条件运算的 

<template>
  <div>{{ message == 0 ? '我是小满0' : '我不是小满other' }}</div>
</template>
 
 
<script setup lang="ts">
const message:number = 1
</script>
 
 
 
<style>
</style>

 运算也是支持的

<template>
  <div>{{ message  + 1 }}</div>
</template>
 
 
<script setup lang="ts">
const message:number = 1
</script> 
 
<style>
</style>

 操作API 也是支持的

<template>
  <div>{{ message.split(',') }}</div>
</template>
 
 
<script setup lang="ts">
const message:string = "我,是,小,满"
</script>
 
 
 
<style>
</style>

 

指令

v- 开头都是vue 的指令

v-text 用来显示文本

v-html 用来展示富文本

v-if 用来控制元素的显示隐藏(切换真假DOM)

v-else-if 表示 v-if 的“else if 块”。可以链式调用

v-else v-if条件收尾语句

v-show 用来控制元素的显示隐藏(display none block Css切换)

v-on 简写@ 用来给元素添加事件

v-bind 简写: 用来绑定元素的属性Attr

v-model 双向绑定

v-for 用来遍历元素

v-on修饰符 冒泡案例

<template>
  <div @click="parent">
    <div @click.stop="child">child</div>
  </div>
</template>
 
 
<script setup lang="ts">
const child = () => {
  console.log('child');
 
}
const parent = () => {
  console.log('parent');
}
 
</script>

阻止表单提交案例

<template>
  <form action="/">
    <button @click.prevent="submit" type="submit">submit</button>
  </form>
</template> 
 
<script setup lang="ts">
const submit = () => {
  console.log('child'); 
}  
</script>  
 
<style>
</style>

v-bind 绑定class 案例 1

<template>
  <div :class="[flag ? 'active' : 'other', 'h']">12323</div>
</template> 
 
<script setup lang="ts">
const flag: boolean = false;
</script>
 
<style>
.active {
  color: red;
}
.other {
  color: blue;
}
.h {
  height: 300px;
  border: 1px solid #ccc;
}
</style>

v-bind 绑定class 案例 2

<template>
  <div :class="flag">{{flag}}</div>
</template> 
 
<script setup lang="ts">
type Cls = {
  other: boolean,
  h: boolean
}
const flag: Cls = {
  other: false,
  h: true
};
</script> 
 
<style>
.active {
  color: red;
}
.other {
  color: blue;
}
.h {
  height: 300px;
  border: 1px solid #ccc;
}
</style>

v-bind 绑定style案例

<template>
  <div :style="style">2222</div>
</template>
 
<script setup lang="ts"> 
type Style = {
  height: string,
  color: string
}
 
const style: Style = {
  height: "300px",
  color: "blue"
}
 
</script>
 
 
<style>
</style>

v-model 案例

<template>
  <input v-model="message" type="text" />
  <div>{{ message }}</div>
</template> 
 
<script setup lang="ts">
import { ref } from 'vue'
const message = ref("v-model")
</script>  
 
<style>
.active {
  color: red;
}
.other {
  color: blue;
}
.h {
  height: 300px;
  border: 1px solid #ccc;
}
</style>

Vue3 第五章(Vue核心虚拟Dom和 diff 算法)

为什么要学习源码

1.可以提升自己学习更优秀的API设计和代码逻辑

2.面试的时候也会经常问源码相关的东西

3.更快的掌握vue和遇到问题可以定位

介绍虚拟DOM

虚拟DOM就是通过JS来生成一个AST节点树

 

Vue Template Explorer

为什么要有虚拟DOM?

我们可以通过下面的例子

let div = document.createElement('div')
let str = ''
for (const key in div) {
  str += key + ''
}
console.log(str)

发现一个dom上面的属性是非常多的

aligntitlelangtranslatedirhiddenaccessKeydraggablespellcheckautocapitalizecontentEditableisContentEditableinputModeoffsetParentoffsetTopoffsetLeftoffsetWidthoffsetHeightstyleinnerTextouterTextonbeforexrselectonabortonbluroncanceloncanplayoncanplaythroughonchangeonclickoncloseoncontextmenuoncuechangeondblclickondragondragendondragenterondragleaveondragoverondragstartondropondurationchangeonemptiedonendedonerroronfocusonformdataoninputoninvalidonkeydownonkeypressonkeyuponloadonloadeddataonloadedmetadataonloadstartonmousedownonmouseenteronmouseleaveonmousemoveonmouseoutonmouseoveronmouseuponmousewheelonpauseonplayonplayingonprogressonratechangeonresetonresizeonscrollonsecuritypolicyviolationonseekedonseekingonselectonslotchangeonstalledonsubmitonsuspendontimeupdateontoggleonvolumechangeonwaitingonwebkitanimationendonwebkitanimationiterationonwebkitanimationstartonwebkittransitionendonwheelonauxclickongotpointercaptureonlostpointercaptureonpointerdownonpointermoveonpointeruponpointercancelonpointeroveronpointeroutonpointerenteronpointerleaveonselectstartonselectionchangeonanimationendonanimationiterationonanimationstartontransitionrunontransitionstartontransitionendontransitioncanceloncopyoncutonpastedatasetnonceautofocustabIndexattachInternalsblurclickfocusenterKeyHintvirtualKeyboardPolicyonpointerrawupdatenamespaceURIprefixlocalNametagNameidclassNameclassListslotattributesshadowRootpartassignedSlotinnerHTMLouterHTMLscrollTopscrollLeftscrollWidthscrollHeightclientTopclientLeftclientWidthclientHeightattributeStyleMaponbeforecopyonbeforecutonbeforepasteonsearchelementTimingonfullscreenchangeonfullscreenerroronwebkitfullscreenchangeonwebkitfullscreenerrorchildrenfirstElementChildlastElementChildchildElementCountpreviousElementSiblingnextElementSiblingafteranimateappendattachShadowbeforeclosestcomputedStyleMapgetAttributegetAttributeNSgetAttributeNamesgetAttributeNodegetAttributeNodeNSgetBoundingClientRectgetClientRectsgetElementsByClassNamegetElementsByTagNamegetElementsByTagNameNSgetInnerHTMLhasAttributehasAttributeNShasAttributeshasPointerCaptureinsertAdjacentElementinsertAdjacentHTMLinsertAdjacentTextmatchesprependquerySelectorquerySelectorAllreleasePointerCaptureremoveremoveAttributeremoveAttributeNSremoveAttributeNodereplaceChildrenreplaceWithrequestFullscreenrequestPointerLockscrollscrollByscrollIntoViewscrollIntoViewIfNeededscrollTosetAttributesetAttributeNSsetAttributeNodesetAttributeNodeNSsetPointerCapturetoggleAttributewebkitMatchesSelectorwebkitRequestFullScreenwebkitRequestFullscreenariaAtomicariaAutoCompleteariaBusyariaCheckedariaColCountariaColIndexariaColSpanariaCurrentariaDescriptionariaDisabledariaExpandedariaHasPopupariaHiddenariaKeyShortcutsariaLabelariaLevelariaLiveariaModalariaMultiLineariaMultiSelectableariaOrientationariaPlaceholderariaPosInSetariaPressedariaReadOnlyariaRelevantariaRequiredariaRoleDescriptionariaRowCountariaRowIndexariaRowSpanariaSelectedariaSetSizeariaSortariaValueMaxariaValueMinariaValueNowariaValueTextgetAnimationsnodeTypenodeNamebaseURIisConnectedownerDocumentparentNodeparentElementchildNodesfirstChildlastChildpreviousSiblingnextSiblingnodeValuetextContentELEMENT_NODEATTRIBUTE_NODETEXT_NODECDATA_SECTION_NODEENTITY_REFERENCE_NODEENTITY_NODEPROCESSING_INSTRUCTION_NODECOMMENT_NODEDOCUMENT_NODEDOCUMENT_TYPE_NODEDOCUMENT_FRAGMENT_NODENOTATION_NODEDOCUMENT_POSITION_DISCONNECTEDDOCUMENT_POSITION_PRECEDINGDOCUMENT_POSITION_FOLLOWINGDOCUMENT_POSITION_CONTAINSDOCUMENT_POSITION_CONTAINED_BYDOCUMENT_POSITION_IMPLEMENTATION_SPECIFICappendChildcloneNodecompareDocumentPositioncontainsgetRootNodehasChildNodesinsertBeforeisDefaultNamespaceisEqualNodeisSameNodelookupNamespaceURIlookupPrefixnormalizeremoveChildreplaceChildaddEventListenerdispatchEventremoveEventListener

介绍Diff算法

Vue3 源码地址 https://github.com/vuejs/core

详细解说可以观看视频讲解 小满zs的个人空间_哔哩哔哩_Bilibili

<template>
  <div>
    <div :key="item" v-for="(item) in Arr">{{ item }}</div>
  </div>
</template>
 
<script setup lang="ts">
const Arr: Array<string> = ['A', 'B', 'C', 'D']
Arr.splice(2,0,'DDD')
</script> 
<style>
</style>

 

 splice 用法 太贴心了 

 

 

 

vue的高效的核心,就是虚拟的dom和diff算法

vue不通过修改dom树来达到修改的效果,而是直接在页面上修改那个元素,此时这个元素就是一个虚拟的dom。
那么vue怎么去改呢? 通过diff算法,计算出虚拟的dom修改后和修改前的区别,然后在虚拟dom的原基础上进行修改,这样的效率就大大提升了。

Vue3 第六章(认识Ref全家桶)

 

 

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
}

(重点)注意:let定义后的常量或者变量被ref包装之后需要.value 来进行赋值: xxx.value,但是定义的ref对象  {  }  中的属性获取时: xxx.value.属性名 ,不需要在加value了,因使用proxy——>reactive

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  
}

ref 小妙招

我们console 输出 

 

 是个这玩意 查看起来很不方便 Vue 已经想到 了 帮我们做了格式化

 

 

 

 

 

 

 

 此时观看打印的值就很明了

shallowRef

创建一个跟踪自身 .value 变化的 ref,但不会使其值也变成响应式的

例子

修改其属性是非响应式的这样是不会改变的

<template>
  <div>
    <button @click="changeMsg">change</button>
    <div>{{ message }}</div>
  </div>
</template>
<script setup lang="ts"> import { Ref, shallowRef } from 'vue' type Obj = { name: string } let message: Ref<Obj> = shallowRef({ name: "小满" }) const changeMsg = () => { message.value.name = '大满' } </script> <style> </style>

例子2

这样是可以被监听到的修改value

import { Ref, shallowRef } from 'vue'
type Obj = {
  name: string
}
let message: Ref<Obj> = shallowRef({
  name: "小满"
})
 
const changeMsg = () => {
  message.value = { name: "大满" }
}

triggerRef 

强制更新页面DOM

这样也是可以改变值的

<template>
  <div>
    <button @click="changeMsg">change</button>
    <div>{{ message }}</div>
  </div>
</template> 
 
<script setup lang="ts">
import { Ref, shallowRef,triggerRef } from 'vue'
type Obj = {
  name: string
}
let message: Ref<Obj> = shallowRef({
  name: "小满"
})
 
const changeMsg = () => {
  message.value.name = '大满'
 triggerRef(message)
}
</script>  
 
<style>
</style>

customRef

自定义ref 

customRef 是个工厂函数要求我们返回一个对象 并且实现 get 和 set  适合去做防抖之类的

<template>
 
  <div ref="div">小满Ref</div>
  <hr>
  <div>
    {{ name }}
  </div>
  <hr>
  <button @click="change">修改 customRef</button>
 
</template>
 
<script setup lang='ts'>
import { ref, reactive, onMounted, shallowRef, customRef } from 'vue'
 
function myRef<T = any>(value: T) {
  let timer:any;
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newVal) {
        clearTimeout(timer)
        timer =  setTimeout(() => {
          console.log('触发了set')
          value = newVal
          trigger()
        },500)
      }
    }
  })
} 
 
const name = myRef<string>('小满')
 
const change = () => {
  name.value = '大满'
}
 
</script>
<style scoped>
</style>

 

Vue3 第七章(认识Reactive全家桶)

reactive

用来绑定复杂的数据类型 例如 对象 数组

 

 

 App.vue完整代码

<template>
  <h1>一个人的信息</h1>
  <h2>姓名:{{person.name}}</h2>
  <h2>年龄:{{person.age}}</h2>
  <h3>工作种类:{{person.job.type}}</h3>
  <h3>工作薪水:{{person.job.salary}}</h3>
  <h3>爱好:{{person.hobby}}</h3>
  <h3>测试的数据c:{{person.job.a.b.c}}</h3>
  <button @click="changeInfo">修改人的信息</button>
</template>

<script>
import {reactive} from 'vue'
export default {
  name: 'App',
  setup(){
    //数据
    let person = reactive({
      name:'张三',
      age:18,
      job:{
        type:'前端工程师',
        salary:'30K',
        a:{
          b:{
            c:666
          }
        }
      },
      hobby:['抽烟','喝酒','烫头']
    })

    //方法
    function changeInfo(){
      person.name = '李四'
      person.age = 48
      person.job.type = 'UI设计师'
      person.job.salary = '60K'
      person.job.a.b.c = 999
      person.hobby[0] = '学习'
    }

    //返回一个对象(常用)
    return {
      person,
      changeInfo
    }
  }
}
</script>

reactive 源码约束了我们的类型

 

 

 

 他是不可以绑定普通的数据类型这样是不允许 会给我们报错

import { reactive} from 'vue'
 
let person = reactive('sad')

 

 绑定普通的数据类型 我们可以 使用昨天讲到ref

你如果用ref去绑定对象 或者 数组 等复杂的数据类型 我们看源码里面其实也是 去调用reactive

使用reactive 去修改值无须.value

reactive 基础用法

import { reactive } from 'vue'
let person = reactive({
   name:"小满"
})
person.name = "大满"

数组异步赋值问题

这样赋值页面是不会变化的因为会脱离响应式

let person = reactive<number[]>([])
setTimeout(() => {
  person = [1, 2, 3]
  console.log(person);
  
},1000)

解决方案1

使用push

import { reactive } from 'vue'
let person = reactive<number[]>([])
setTimeout(() => {
  const arr = [1, 2, 3]
  person.push(...arr)
  console.log(person);
  
},1000)

方案2

包裹一层对象

type Person = {
  list?:Array<number>
}
let person = reactive<Person>({
   list:[]
})
setTimeout(() => {
  const arr = [1, 2, 3]
  person.list = arr;
  console.log(person);
  
},1000)

readonly

拷贝一份proxy对象将其设置为只读

import { reactive ,readonly} from 'vue'
const person = reactive({count:1})
const copy = readonly(person)
 
 //person.count++
 
 copy.count++

shallowReactive 

只能对浅层的数据 如果是深层的数据只会改变值 不会改变视图

案例

<template>
  <div>
    <div>{{ state }}</div>
    <button @click="change1">test1</button>
    <button @click="change2">test2</button>
  </div>
</template>
<script setup lang="ts"> import { shallowReactive } from 'vue' const obj = { a: 1, first: { b: 2, second: { c: 3 } } } const state = shallowReactive(obj) function change1() { state.a = 7 } function change2() { state.first.b = 8 state.first.second.c = 9 console.log(state); } </script> <style> </style>

 

案例代码

<!--reactive-->
<template>
  <div>
    <!--reactive对象使用-->
    <!--<form action="">
      <input v-model="form.name" type="text">
      <br>
      <input v-model="form.age" type="text">
      <br>
      &lt;!&ndash;click的修饰符prevent焦点&ndash;&gt;
      <button @click.prevent="submit">提交</button>
    </form>-->
    <!--reactive数组使用-->
    <ul>
      <li v-for="item in list" :key="list">{{item}}</li>
    </ul>
    <button @click="add">添加</button>
  </div>
</template>

<script setup lang="ts">
import {reactive,ref} from "vue";
/*export default {
  name: "App1"
}*/
// ref reactive
// ref 支持所有类型 reactive 引用类型 Array Object Map Set
// ref 取值 赋值 都需要加.value
// let form = reactive( '')// 报错
// reactive proxy 不能直接赋值 否则破坏响应式对象的,解决方法


/*// 类型可以自动推断:也可手动
type M = { // 手动
  name:string;
  age: number;
}
// 接收手动类型:泛型中
let form = reactive<M>({// 使用对象
  name:'juelan',
  age: 18,
})*/

// reactive数组,函数报错:添加定义泛型数组
let list = reactive<string[]>([])

// 修改值时:不需.value
// form.age = 86;

// reactive对象时:注册submit函数
/*const submit = ()=>{
  console.log(form);
}*/

// 数组时:
const add = ()=>{
  // list.push("EDG") // 添加,报错的话 定义类型
  setTimeout(()=>{ // reactive proxy 不能直接赋值 否则破坏响应式对象的,解决方法
    let res = ['EFD',"fje","DJE"]
    // list = res;
    list.push(...res); // 解决方法1: 数组 可以使用push 加es6解构...res
  },2000)
  console.log(list)
}
// 解决方法2: 添加泛型对象,把数组作为一个属性去解决
let list1 = reactive<{
  arr:string[]
}>({
  arr:[]
})
const add1 = ()=>{
  setTimeout(()=>{ // reactive proxy 不能直接赋值 否则破坏响应式对象的,解决方法
    let res1 = ['EFD',"fje","DJE"]
    list1.arr = res1; // 解决方法2: 数组
  },2000)
}

</script>

<style scoped>

</style>

 

Vue3.0中的响应式原理

vue2.x的响应式

- 实现原理:
  - 对象类型:通过```Object.defineProperty()```对属性的读取、修改进行拦截(数据劫持)。 
  - 数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。 
    ```js
    Object.defineProperty(data, 'count', {
        get () {},
        set () {}
    })
    ```
完整代码:
<template>
  <div>
    <h1>我是Vue2写得效果</h1>
    <h2 v-show="person.name">姓名:{{person.name}}</h2>
    <h2 >年龄:{{person.age}}</h2>
    <h2 v-show="person.sex">性别:{{person.sex}}</h2>
    <h2>爱好:{{person.hobby}}</h2>
    <button @click="addSex">添加一个sex属性</button>
    <button @click="deleteName">删除name属性</button>
    <button @click="updateHobby">修改第一个爱好的名字</button>
  </div>
</template>

<script>
import Vue from 'vue'
export default {
  name: "Vue2的响应式原理",
  data(){
    return{
      person:{
        name:'张三',
        age: 19,
        hobby: ['游戏','看书','睡觉'],
      }
    }
  },
  methods:{
    addSex(){
      this.person.sex = '' // 属性添加,但检测无响应问题
      // 解决不响应问题
      this.$set(this.person,'sex','') // $set设置(data函数person属性,key:属性名,value:女
      // Vue.set(this.person,'sex','女') // import引用vue.$set来设置属性

    },
    deleteName(){
      delete this.person.name // 属性删除,但检测无响应问题
      // 解决不响应问题
      this.$set(this.person,'name','')
      // Vue.set(this.person,'name','女')
    },
    updateHobby(){
      this.person.hobby[0] = '逛街' // 数组下标,修改监测不响应
      // 解决不响应问题
      this.$set(this.person.hobby,'0','逛街')
      // Vue.set(this.person.hobby,'0','逛街')
      this.person.hobby.splice('0',1,'逛街') // 用数组splice方法
    }
  }
}
</script>

<style scoped>

</style>

- 存在问题:

  - 新增属性、删除属性, 界面不会更新。
  - 直接通过下标修改数组, 界面不会自动更新。

Vue3.0的响应式

- 实现原理:
  - 通过Proxy(代理):  拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
  - 通过Reflect(反射):  对源对象的属性进行操作。
  - MDN文档中描述的Proxy与Reflect:
    - Proxy:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy   
    - Reflect:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
   

 

 

完整代码:
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>Document</title>
</head>
<body>
<script type="text/javascript" >
  //源数据
  let person = {
    name:'张三',
    age:18
  }

  //模拟Vue2中实现响应式, #regin and #endregin 控制代码段展开
  //#region
  /* let p = {}
  Object.defineProperty(p,'name',{
    configurable:true,
    get(){ //有人读取name时调用
      return person.name
    },
    set(value){ //有人修改name时调用
      console.log('有人修改了name属性,我发现了,我要去更新界面!')
      person.name = value
    }
  })
  Object.defineProperty(p,'age',{
    get(){ //有人读取age时调用
      return person.age
    },
    set(value){ //有人修改age时调用
      console.log('有人修改了age属性,我发现了,我要去更新界面!')
      person.age = value
    }
  }) */
  //#endregion

  //模拟Vue3中实现响应式, #regin and #endregin 控制代码段展开
  //#region
  const p = new Proxy(person,{ // proxy需2个参数(常量名person,对象{})
    //有人读取p的某个属性时调用
    get(target,propName){ // 参数触发target=person,propName=属性名
      console.log(`有人读取了p身上的${propName}属性`)
        // return target[propName] // 返回值
        //window.reflect反射、映射,从对象中读数据
        return Reflect.get(target,propName) //reflect反射.get读取(target对象,获取propName属性)
    },
    //有人修改p的某个属性、或给p追加某个属性时调用,管理:增加、修改
    set(target,propName,value){// 参数触发target=person,propName=属性名,value=要修改的值
      console.log(`有人修改了p身上的${propName}属性,我要去更新界面了!`)
        // target[propName] = value // 修改属性
      Reflect.set(target,propName,value) // reflect反射.
    },
    //有人删除p的某个属性时调用,解决删除响应式
    deleteProperty(target,propName){
      console.log(`有人删除了p身上的${propName}属性,我要去更新界面了!`)
        // return delete target[propName]
      return Reflect.deleteProperty(target,propName)
    }
  })
  //#endregion

  // 讲解Reflect,Object中移植到Reflect
  let obj = {a:1,b:2}
  //通过Object.defineProperty去操作,有提示报错
  //#region
  /* try { // 封装代码时:需要捕捉error
    Object.defineProperty(obj,'c',{
      get(){
        return 3
      }
    })
    Object.defineProperty(obj,'c',{
      get(){
        return 4
      }
    })
  } catch (error) {
    console.log(error)
  } */
  //#endregion

  //通过Reflect.defineProperty去操作,不提示报错,有默认返回值:ture,false
  //#region
  /* const x1 = Reflect.defineProperty(obj,'c',{
    get(){
      return 3
    }
  })
  console.log(x1)

  const x2 = Reflect.defineProperty(obj,'c',{
    get(){
      return 4
    }
  })
  if(x2){
    console.log('某某某操作成功了!')
  }else{
    console.log('某某某操作失败了!')
  } */
  //#endregion

  // console.log('@@@')

</script>
</body>
</html>

 

 

 

 

 

 

 

 

 

 完整代码: 

<template>
    <h2>当前求和为:{{sum}}</h2>
    <button @click="sum">点我+1</button>
    <h2>当前的信息为:{{msg}}</h2>
    <button @click="msg+='!'">点我+1</button>
    <hr>
    <h2>姓名:{{person.name}}</h2>
    <h2>年龄:{{person.age}}</h2>
    <h2>薪资:{{person.job.j1.salary}}K</h2>
    <button @click="person.name+='~'">修改姓名</button>
    <button @click="person.age++">增长年龄</button>
    <button @click="person.job.j1.salary++">涨薪</button>
</template>

<script>
    // Vue3注意:用什么需要引入
    import {ref,watch,reactive,computed} from 'vue'
    export default {
        name: 'Demo1',
        /*watch:{ //监视 Vue2中
            // sum(newValue,oldValue){ // 2个参数(新值,旧值)
            //     console.log('监测到变更',newValue,oldValue)
            // } 
            // 完整版优势在于:配置其他属性例如:immediate、deep深度监视
            immediate:true, // 立即监听属性
            deep:true, // 深度监视开启
            sum:{ // handler回调
                handler(newValue,oldValue){ 
                    console.log('监测到变更',newValue,oldValue) 
                }
            }           
        },*/        

        setup(){
            //数据
            let sum =ref(0)
            let msg = ref('你好啊')
            let person = reactive({
                name:'张三',
                age:18,
                job:{
                    j1:{
                        salary:20
                    }
                }
            })

            //情况一:监视ref所定义的一个响应式数据
            // vue3中watch先引入在使用:函数
            /*watch(sum,(newValue,oldValue)=>{ // (监视谁,回调()=>{})
                console.log('监测到变更',newValue,oldValue) 
            }.{immediate:true})*/

            //情况二:监视ref所定义的多个响应式数据
            /*watch([sum,msg],(newValue,oldValue)=>{
                console.log('监测到变更',newValue,oldValue)
            })*/

            /* 开发应用(常用)
            情况三:监视reactive所定义的一个响应式数据的全部属性
                1.注意:此处无法正确的获取oldValue
                2.注意:强制开启了深度监视(deep配置无效)
            */
            watch(person,(newValue,oldValue)=>{
                console.log('person变化了',newValue,oldValue)
            },{deep:false}) //此处的deep配置无效 

            //情况四:监视reactive所定义的一个响应式数据中的某个属性
            watch(()=>person.age,(newValue,oldValue)=>{
            // watch(person.name,(newValue,oldValue)=>{ // 自定义监视对象属性报错,要写成函数式
                console.log('person的name变化了',newValue,oldValue)
            }) 

            //情况五:监视reactive所定义的一个响应式数据中的某些属性
            watch([()=>person.name,()=>person.age],(newValue,oldValue)=>{
                console.log('person的name或age变化了',newValue,oldValue)
            }) 

            //特殊情况
            watch(()=>person.job,(newValue,oldValue)=>{
                console.log('person的job变化了',newValue,oldValue)
            },{deep:true}) //此处由于监视的是reactive素定义的对象中的某个属性,所以deep配置有效


            //返回一个对象(常用)
            return {
                sum,
                msg,
                person,
            }
        }
    }
</script>

 

watchEffect函数

 

- watch的套路是:既要指明监视的属性,也要指明监视的回调watch(监视属性sum,监视的回调()=>{ },是否开启深度监视deep:true), 3个参数
- watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。
- watchEffect有点像computed:
  - 但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
  - 而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。

  ```js
  //watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
  watchEffect(()=>{
      const x1 = sum.value
      const x2 = person.age
      console.log('watchEffect配置的回调执行了')
  })
  ``
//监视
            /* watch(sum,(newValue,oldValue)=>{
                console.log('sum的值变化了',newValue,oldValue)
            },{immediate:true}) */

            watchEffect(()=>{ // 应用场景:发票报销
                // 智能监测开启deep:true:函数体内用谁监测谁
                const x1 = sum.value // 监测sum的value
                const x2 = person.job.j1.salary // peeson的jbo的j1的salary被监测
                console.log('watchEffect所指定的回调执行了')
            })

 

8.生命周期

  

 Vue3优化节省了环节步骤,Vue3销毁变更卸载 

Vue3.0中可以继续使用Vue2.x中的生命周期钩子,但有有两个被更名:
  - ```beforeDestroy```改名为 ```beforeUnmount```
  - ```destroyed```改名为 ```unmounted```
- Vue3.0也提供了 Composition API 形式的生命周期钩子,与Vue2.x中钩子对应关系如下:onXXXXXX
  - `beforeCreate`===>`setup()`
  - `created`=======>`setup()`
  - `beforeMount` ===>`onBeforeMount`
  - `mounted`=======>`onMounted`
  - `beforeUpdate`===>`onBeforeUpdate`
  - `updated` =======>`onUpdated`
  - `beforeUnmount` ==>`onBeforeUnmount`
  - `unmounted` =====>`onUnmounted`

 

<template>
    <h2>当前求和为:{{sum}}</h2>
    <button @click="sum++">点我+1</button>
</template>

<script>
    import {ref,onBeforeMount,onMounted,onBeforeUpdate,onUpdated,onBeforeUnmount,onUnmounted} from 'vue'
    export default {
        name: 'Demo',
        
        setup(){
            console.log('---setup---')
            //数据
            let sum = ref(0)

            //通过组合式API的形式去使用生命周期钩子,onXXXXXX,onXXXXX.....
            onBeforeMount(()=>{ // 挂在前:传递一个回调函数()=>{}
                console.log('---onBeforeMount---')
            })
            onMounted(()=>{ // 挂载完毕
                console.log('---onMounted---')
            })
            onBeforeUpdate(()=>{
                console.log('---onBeforeUpdate---')
            })
            onUpdated(()=>{
                console.log('---onUpdated---')
            })
            onBeforeUnmount(()=>{
                console.log('---onBeforeUnmount---')
            })
            onUnmounted(()=>{
                console.log('---onUnmounted---')
            })

            //返回一个对象(常用)
            return {sum}
        },
        //通过配置项的形式使用生命周期钩子
        //#region 
        beforeCreate() {
            console.log('---beforeCreate---')
        },
        created() {
            console.log('---created---')
        },
        beforeMount() {
            console.log('---beforeMount---')
        },
        mounted() {
            console.log('---mounted---')
        },
        beforeUpdate(){
            console.log('---beforeUpdate---')
        },
        updated() {
            console.log('---updated---')
        },
        beforeUnmount() {
            console.log('---beforeUnmount---')
        },
        unmounted() {
            console.log('---unmounted---')
        },
        //#endregion
    }
</script>

 Less预处理器 和 scoped

Less (Leaner Style Sheets 的缩写) 是一门向后兼容的 CSS 扩展语言。这里呈现的是 Less 的官方文档(中文版),包含了 Less 语言以及利用 JavaScript 开发的用于将 Less 样式转换成 CSS 样式的 Less.js 工具。

官方文档 Less 快速入门 | Less.js 中文文档 - Less 中文网

在vite中使用less

npm install less less-loader -D 安装即可

在style标签注明即可

<style lang="less">
 
</style>

什么是scoped

实现组件的私有化, 当前style属性只属于当前模块.

 

9.自定义hook函数

- 什么是hook?—— 本质是一个函数,把setup函数中使用的Composition API进行了封装。
- 类似于vue2.x中的mixin。
- 自定义hook的优势: 复用代码, 让setup中的逻辑更清楚易懂。

关于引入组件报错问题:

  引入js路径报红时,lang='ts’ 可不写或者改成js解决 

完整代码:
App.vue
<template>
  <button @click="isShowDemo = !isShowDemo">切换隐藏/显示</button>
  <!--是否显示和隐藏: v-if=-->
  <Demo v-if="isShowDemo"/>
  <hr>
  <!--<Test v-if="isShowTest"/>-->
  <Test/>
</template>

<script setup lang="ts">
import {ref} from 'vue'
// 注:组件引入时Vue3中需要加、加、加后缀.vue
import Demo from './components/Demo.vue'
import Test from './components/Test.vue'

// 是否显示和隐藏:定义= ref(true显示)
let isShowDemo = ref(true)
// let isShowTest = ref(true)

</script>

Demo.vue组件

<template>
  <h2>当前求和为:{{sum}}</h2>
  <button @click="sum++">点我+1</button>
  <hr>
  <h2>当前点击时鼠标的坐标为:x:{{ point.x }},y:{{ point.y }}</h2>
</template>

<script setup>
// 引入api组件
import {ref} from 'vue'
// 引用组件
import usePoint from "../hooks/usePoint.js";

//数据
let sum = ref(0)
// 定义接收js返回值:point = 函数方法userPoint()
let point = usePoint()

/* 将逻辑放入hooks/usePint.js中
// 数据
// 将x,y坐标包装对象,reactive
let point = reactive({ // 鼠标点击:初始化
  x:0,
  y:0
})

// 方法
// 解决卸载指定回调问题:将回调函数写成方法
function savePoint(event){
  point.x= event.pageX
  point.y = event.pageY
  console.log(event.pageX,event.pageY) // 输出鼠标XY坐标
}

onMounted(()=>{// 钩子:挂载完毕后加载:回调,获取鼠标xy坐标
  window.addEventListener('click',savePoint) // 解决卸载时无法指定回调:调用savePint方法
  /!*window.addEventListener('click',(event)=>{// 鼠标点击事件:回调
    point.x= event.pageX
    point.y = event.pageY
    console.log(event.pageX,event.pageY) // 输出鼠标XY坐标
  })*!/
})// 卸载时问题:无法指定回调,把挂载回调封装成对象

onBeforeUnmount(()=>{// 生命周期钩子:卸载前:
  // window.removeEventListener('click',???) // 卸载click事件:2个参数:(事件名,)
  window.removeEventListener("click",savePoint) // 解决问题:卸载方法
})
*/

</script>

Test.vue组件

<template>
  <h2>我是Test组件</h2>
  <h2>当前点击时鼠标的坐标为:x:{{ point.x }},y:{{ point.y }}</h2>
</template>

<script setup lang="js">// 注: 下面引入js时,lang='ts‘修改js或者不写自动识别
// 引入js组件
import usePoint from "../hooks/usePoint.js"; // lang=ts时,报红
// const usePoint = require('usePoint')
const point = usePoint();
</script>

<style scoped>

</style>

hooks/usePoint.js:

// 导入api组件
import {onBeforeUnmount, onMounted, reactive, } from "vue";

// 将方法都封装
// export default function savePint(){
// 导出并暴露接口
export default function (){
    // 数据:实现鼠标“打点”相关的数据
    // 将x,y坐标包装对象,reactive
    let point = reactive({ // 鼠标点击:初始化
        x:0,
        y:0
    })

    // 方法:实现鼠标“打点”相关的方法
    // 将坐标位置的封装起来在进行监听和删除,
    // 解决卸载指定回调问题:将回调函数写成方法
    function savePoint(event){
        point.x= event.pageX
        point.y = event.pageY
        console.log(event.pageX,event.pageY) // 输出鼠标XY坐标
    }

    // 钩子
    onMounted(()=>{// 钩子:挂载完毕后加载:回调,获取鼠标xy坐标
        window.addEventListener('click',savePoint) // 解决卸载时无法指定回调:调用savePint方法
        /*window.addEventListener('click',(event)=>{// 鼠标点击事件:回调
          point.x= event.pageX
          point.y = event.pageY
          console.log(event.pageX,event.pageY) // 输出鼠标XY坐标
        })*/
    })// 卸载时问题:无法指定回调,把挂载回调封装成对象

    onBeforeUnmount(()=>{// 生命周期钩子:卸载前:
        // window.removeEventListener('click',???) // 卸载click事件:2个参数:(事件名,)
        window.removeEventListener("click",savePoint) // 解决问题:卸载方法
    })
    return point  // 将let point数据返回:组件必须返回值,

}
// export default usePoint
 

10.toRef和toRef

基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然

- 作用:创建一个 ref 对象,其value值指向另一个对象中的某个属性。映射属性同步:增删改查
- 语法:```const name = toRef(person,'name')```
- 应用:   要将响应式对象中的某个属性单独提供给外部使用时。
App.vue完整代码:组合式和渐进式
<template>
  <button @click="isShowDemo = !isShowDemo">切换隐藏/显示</button>
  <!--是否显示和隐藏: v-if=-->
  <Demo v-if="isShowDemo"/>
  <hr>

</template>

<!--组合式setup-->
<script setup lang="ts">
import {ref} from 'vue'
// 注:组件引入时Vue3中需要加、加、加后缀.vue
import Demo from './components/Demo.vue'


// 是否显示和隐藏:定义= ref(true显示)
let isShowDemo = ref(true)
// let isShowTest = ref(true)
</script>

<!--渐进式setup-->
<!--<script>
import {ref} from 'vue'
import Demo from './components/Demo.vue'
import Test from './components/Test.vue'
export default {
  name: 'App',
  components:{Demo,Test},
  setup() {
    let isShowDemo = ref(true)
    return {isShowDemo}
  }
}
</script>-->

组合式Demo.vue写法:

<!--组合式写法-->
<template>
  <h4>{{person}}</h4>
  <h2>名字:{{nameRef}}</h2>
  <!--<h2>年龄:{{person.age}}</h2>-->
  <h2>年龄:{{ageRef}}</h2>
  <!--<h2>薪资:{{person.job.jl.salary}}</h2>-->
  <h2>薪资:{{salaryRef}}</h2>

  <button @click="person.name+='~'">修改姓名</button>
  <button @click="person.age++">增长年龄</button>
  <button @click="person.job.jl.salary++">涨薪</button>
</template>

<script setup>
// 引入api组件
import {reactive, toRef, toRefs} from 'vue'

const person = reactive({
  name:'juelan',
  age:19,
  job:{
    jl:{
      salary: 20,
    }
  },
})
/*const name1 = person.name // 无法变成响应式。
console.log("%%%",name1)*/

// 映射将不是ref的变成ref, toref(操作的对象,key:对象的属性名
// 使用person对象中部分属性
const nameRef = toRef(person,'name')
const ageRef = toRef(person,'age')
const salaryRef = toRef(person.job.jl,'salary') // 变更深层次对象时,操作对象写至上层

// toRefs(对象名)优化:批量创建多个 ref 对象
// const personRefs = toRefs(person)


</script>

渐进式Test.vue写法

<!--渐进式写法-->
<template>
  <h4>{{person}}</h4>
  <h2>姓名:{{name}}</h2>
  <h2>年龄:{{age}}</h2>
  <h2>薪资:{{job.j1.salary}}K</h2>
  <button @click="name+='~'">修改姓名</button>
  <button @click="age++">增长年龄</button>
  <button @click="job.j1.salary++">涨薪</button>
</template>

<script>
import {ref,reactive,toRef,toRefs} from 'vue'
export default {
  name: 'Demo',
  setup(){
    //数据
    let person = reactive({
      name:'张三',
      age:18,
      job:{
        j1:{
          salary:20
        }
      }
    })

    // const name1 = person.name
    // console.log('%%%',name1)

    // const name2 = toRef(person,'name')
    // console.log('####',name2)

    const x = toRefs(person)
    console.log('******',x)

    //返回一个对象(常用)
    return {
      person,
      // name:toRef(person,'name'),
      // age:toRef(person,'age'),
      // salary:toRef(person.job.j1,'salary'),
      ...toRefs(person)
    }
  }
}
</script>
- 扩展:```toRefs``` 与```toRef```功能一致,但可以批量创建多个 ref 对象,语法:```toRefs(person)```
基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然 

ref,reactive,toRef, toRefs

总结:定义响应式类型数据

reactive对比ref

从定义数据角度对比:

- ref用来定义基本类型数据,如果不使用 <script setup>,需确保从 setup() 返回return ref:

- reactive用来定义对象(或数组)类型数据

- 备注:ref也可以用来定义对象(或数组)类型数据,它内部会自动通过reactive转为代理对象;

从原理角度对比:

- ref通过Object.defineProperty()的get与set来实现响应式(数据劫持)

- reactive通过使用Proxy来实现响应式(数据劫持),并通过Reflect操作源对象内部的数据。

从使用角度对比

- ref定义的数据:js操作数据需要.value,模板中读取时不需要.value

- reactive定义的数据,操作与读取均不需要.value

toRef, toRefs

作用: 创建一个ref对象,其value值指向另一个对象中的某个属性

语法: const name = toRef(person, 'name')

应用: 要将响应式对象中的某个属性单独提供给外部使用时

扩展: toRefs与toRef功能一致,但可以批量创建多个ref对象,语法: toRefs(person)

 

其它 Composition API

1.shallowReactive 与 shallowRef

- shallowReactive:只处理对象最外层属性响应式(浅响应式)。person.job.jl.salary只能到person。job
- shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理

- 什么时候使用?
  -  如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 ===> shallowReactive。
  -  如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换 ===> shallowRef。

2.readonly 与 shallowReadonly

- readonly: 让一个响应式数据变为只读的(深只读)。
- shallowReadonly:让一个响应式数据变为只读的(浅只读)。
- 应用场景: 不希望数据被修改时
 

 

 3.toRaw 与 markRaw

- toRaw:
  - 作用:将一个由```reactive```生成的<strong style="color:orange">响应式对象</strong>转为<strong style="color:orange">普通对象</strong>。
  - 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。
- markRaw:
  - 作用:标记一个对象,使其永远不会再成为响应式对象。
  - 应用场景:
    1. 有些值不应被设置为响应式的,例如复杂的第三方类库等。如: axios库会引起效率问题
    2. 当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。

 Demo.vue代码:

<!--组合式写法-->
<template>
  <h4>当前求和为:{{sum}}</h4>
  <button @click="sum++">点我sum++</button>
  <hr>
  <!--<h4>{{person}}</h4>-->
  <h2>名字:{{nameRef}}</h2>
  <!--<h2>年龄:{{person.age}}</h2>-->
  <h2>年龄:{{ageRef}}</h2>
  <!--<h2>薪资:{{person.job.jl.salary}}</h2>-->
  <h2>薪资:{{salaryRef}}</h2>
  <!--报错:方法二加上 person.car-->
  <h3 v-show="person.car">座驾信息:{{person.car}}</h3>

  <button @click="person.name+='~'">修改姓名</button>
  <button @click="person.age++">增长年龄</button>
  <button @click="person.job.jl.salary++">涨薪</button>
  <button @click="showRawPerson">输出最原始的person</button>
  <button @click="addCar">给人添加一台车</button>
  <button @click="person.car.name+='!'">换车名</button>
  <button @click="person.car.price++">换价格</button>
</template>

<script setup>
// 引入api组件
import {ref, reactive, toRef, toRefs, shallowRef, readonly, shallowReadonly, toRaw, markRaw} from 'vue'

let sum =ref(0)
let person = reactive({
  name:'juelan',
  age:19,
  job:{
    jl:{
      salary: 20,
    }
  },
  // car:{} // 报错方法一
})

// 方法
// toRaw:返回最原始的对象属性,只针对reactive对象属性,ref不行。
function showRawPerson(){
  const p = toRaw(person) // toRaw:返回最原始的对象属性
  p.age++
  console.log(p)
}
// markRaw:强制无法响应
function addCar(){
  let car = {name:'奔驰',price:40}
  // person.car = Car // 响应式
  person.car = markRaw(car) // 强制不响应
}

/*person = readonly(person) // 对象的属性为深只读
person = shallowReadonly(person) // 对象的属性为浅只读*/

/*const name1 = person.name // 无法变成响应式。
console.log("%%%",name1)*/

// 映射将不是ref的变成ref, toref(操作的对象,key:对象的属性名
// 使用person对象中部分属性
const nameRef = toRef(person,'name')
const ageRef = toRef(person,'age')
const salaryRef = toRef(person.job.jl,'salary') // 变更深层次对象时,操作对象写至上层

// toRefs(对象名)优化:批量创建多个 ref 对象
// const personRefs = toRefs(person)

</script>

4.customRef

- 作用:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。
- 实现防抖效果: 渐进式写法
App.vue
  <template>
      <input type="text" v-model="keyword">
      <h3>{{keyword}}</h3>
  </template>
  
  <script>
      import {ref,customRef} from 'vue'
      export default {
          name:'Demo',
          setup(){
              // let keyword = ref('hello') //使用Vue准备好的内置ref
              //自定义一个myRef
              function myRef(value,delay){
                  let timer
                  //通过customRef去实现自定义
                  return customRef((track,trigger)=>{
                      return{
                          get(){
                              track() //告诉Vue这个value值是需要被“追踪”的
                              return value
                          },
                          set(newValue){
                              clearTimeout(timer)
                              timer = setTimeout(()=>{
                                  value = newValue
                                  trigger() //告诉Vue去更新界面
                              },delay)
                          }
                      }
                  })
              }
              let keyword = myRef('hello',500) //使用程序员自定义的ref
              return {
                  keyword
              }
          }
      }
  </script>
  ```

组合式API写法

App.vue 中引入:自定义的ref, import {customRef} from 'vue'

<template>
  <input type="text" v-model="keyWord">
  <h3>{{keyWord}}</h3>
</template>

<!--组合式setup-->
<script setup lang="ts">
// 引入自定义debouncedRef.js中的MyRef函数组件 import { MyRef } from './hook/debouncedRef.js' // let keyWord = ref('hello') // 使用Vue提供的ref const keyWord = MyRef('hello') // 使用程序员的自定义的ref </script>

debouncedRef.js中的自定义MyRef函数组件:

import { customRef } from 'vue'

export function MyRef(value, delay = 200) { // delay延迟:数字类型 =200
    let timeout // 声明变量
    return customRef((track, trigger) => {
        return {
            get() {
                track() // 开启追踪:通知Vue追踪数据的变化:返回值
                return value // 第三步: 返回更新value,展示的话是第一步
            },
            set(newValue) {
                clearTimeout(timeout) // 防抖:清除变量,当输入未完成时,不输出返回值
                timeout = setTimeout(() => {
                    value = newValue // 第一步: 修改数据
                    trigger() // 第二步: 触发:通知Vue去重新解析模板
                }, delay) // 调用:延迟
            }
        }
    })
}

5.provide 与 inject (组件传参:祖与后代传递数据)

 

 

用于提供可以被后代组件注入的值。provide 和 inject 通常成对一起使用。

- 作用:实现祖与后代组件间通信
- 套路:父组件有一个 `provide` 选项来提供数据,后代组件有一个 `inject` 选项来开始使用这些数据
- 具体写法:
  1. 祖组件中: 
<template>
  <div class="app">
    <h3>我是App组件(祖),{{car.name}}--{{car.price}}</h3>
    <child></child>
  </div>
</template>

<!--组合式setup-->
<script setup lang="ts"> // setup语法糖
import { provide, reactive, toRefs} from "vue";
import Child from "./components/Child.vue"; // setup语法糖效率提升之一:子组件只需要导入,无需声明 --即 components:['Demo'] 被省略

let car = reactive({
  name: '奔驰',
  price:'40w',
})
// 祖孙后代组件间数据传递:provide传递与inject接收成对
provide('car',car) // k:v祖组件提供数据:给自己后代组件传递数据
toRefs(car)


</script>

<style scoped>
.app {
  background-color: gray;
  padding:10px;
}
</style>

2、后代组件中:

<template>
  <div class="son">
    <h3>我是son组件:{{car2.name}}--{{car2.price}}</h3>
  </div>
</template>

<script setup lang="ts">
import {inject} from "vue";

// 组件间数据传递:provid传递与inject接收成对
let car2 = inject('car') // 接收祖组件的数据

</script>

<style scoped>
.son{
  background-color: skyblue;
  padding:10px;
}
</style>

 组件间父子传参:defineProps和defineEmit

父子组件传值

1.1 父组件给子组件传值 —— v-bind

传递字符串类型,不需要加 v-bind(:)

传递非字符串类型,需要加 v-bind(:)

<template>
  <div class="layout">
  <!-- :data='data'是数组, title='字符串’ -->
<Menu :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]); // 非字符串类型:加V-bind </script>

1)父组件给子组件接收传参

如果是在 setup 语法糖中使用 defineProps,则无需引入,直接使用即可 

使用了 ts:defineProps<type>();

未使用 ts:defineProps({});

 

// 使用了 ts 时的写法
<script setup lang="ts">
defineProps<{
  title: string;
  data: number[];
}>();
</script>
 
// 未使用 ts 时的写法
<script setup>
defineProps({
  title: {
    default: "",
    type: string,
  },
  data: Array,
});
</script>

 设置子组件接受参数的默认值 —— withDefaults

withDefaults 接受两个参数:

withDefaults 接受两个参数:

  • defineProps<Props>()
  • 一个对象(里面包含了所有默认值)

 

注意:

  • 仅当使用 ts 时,才可以使用 withDefaults
  • 设置默认值时,如果是引用类型,则要使用 () => {}、() => [] 的形式设置
type Props = {
  title?: string;
  data?: number[];
};
 
withDefaults(defineProps<Props>(), {
  title: "bilibili",
  data: () => [1, 2, 3],
});

 

// ts特有
type Props = {
    title: string,
    menus: string[],
}
const props = withDefaults(defineProps<Props>(), { 
    title: '默认title',
    menus: () => [] // 注意:指针类型数据,要用函数返回默认值
})

2) 子组件给父组件传参(派发事件) —— defineEmits

defineEmits 接受一个数组,用于存储 事件名 数组

defineEmits 返回一个函数,也就是传说中的 emit 

子组件定义派发事件:

<template>
  <div class="menu">
    <button @click="clickTap">派发给父组件</button>
  </div>
</template>
 
<script setup lang="ts">
import { reactive } from "vue";
const list = reactive<number[]>([4, 5, 6]);
 
const emit = defineEmits(["on-click"]);
const clickTap = () => {
  emit("on-click", list);
};
</script>
// 抛出事件
// const emit = defineEmits(['click1', 'click2'])  // js方式
const emit = defineEmits<{ // ts特有方式
  (e: 'click1', id: string): void
  (e: 'click2', value: string): void
}>()

 父组件接受/监听子组件派发的事件:

<template>
  <div class="layout">
    <Menu @on-click="getList"></Menu>
  </div>
</template>
 
<script setup lang="ts">
import Menu from "./Menu/index.vue";
import { reactive } from "vue";
 
const getList = (list: number[]) => {
  console.log(list, "父组件接受子组件");
};
</script>

3)子组件暴露给父组件内部属性 —— defineExpose

defineExpose 使用 script setup 的组件是默认关闭的,也即通过模板 ref 或者 $parent
链获取到的组件的公开实例,不会暴露任何在 <script setup中声明的绑定,组件中明确要暴露出去的属性,使用 defineExpose

// 子组件
<script setup lang='ts'>
import { ref, reactive} from 'vue'
const id = ref<string>('123')
const name = reactive({name1: 'n1'})
const func = () => {
    console.log('使用defineExpose声明后,父组件可以调用func函数')
}
defineExpose({ // 暴露出去
    id,
    name,
    func
})
</script>

用.value获取值:

// 父组件
<template>
   <Menu ref="MenuRef" />
</template>

<script setup lang='ts'>
import { ref, reactive, watchEffect } from 'vue'
import Menu from './Menu/index.vue';

const MenuRef = ref() // 给MenuRef初始化为空,渲染后会自动获取到子组件内部属性
watchEffect(() => {
    console.log("MenuRef:", MenuRef) // 第一次执行打印为undefined
    if(MenuRef) {
        console.log("MenuRef.value:", MenuRef.value)
        console.log("MenuRef.value?.id:", MenuRef.value?.id)
        console.log("MenuRef.value?.name.name1:", MenuRef.value?.name.name1)
        MenuRef.value?.func()
    }
})
</script>

在watchEffect方法中打印:

 在父组件中,获取子组件的 DOM

<Menu ref="menus"></Menu>
const menus = ref<HTMLElement | null>(null);
console.log(menus.value);

打印之后,发现没有任何属性 

父组件若想读取子组件内部的属性,需要在子组件内 把需要的属性 通过 defineExpose 暴露出去

const list = reactive<number[]>([4, 5, 6]);
 
defineExpose({
  list,
});

这样做的目的:让父组件不能随意通过 DOM 修改子组件内的属性,父组件只能读取到子组件主动暴露出来的属性

案例:

App.vue父组件:

<template>
  <div class="layout">父级</div>
  <hr>
  <!--父向子组件:传递arr[]数组和title值-->
  <Child :arr="arr1" :title="name"></Child>
  <hr>
  <!--子向父组件:接收子组件传递数据,ref接收组件实例-->
  <Login @ref="loginVue" @juelan="getName" @tongda="getName1"></Login>
</template>

<!--组合式setup-->
<script setup lang="ts"> // setup语法糖
// import Child from "./components/Child.vue"; // 静态引入
import layout from './layOut/index.vue'
import Child from './components/Child.vue'
import Login from './components/Login.vue'
import {reactive, ref} from "vue";

let name = '爵岚'
let arr1 = reactive<number[]>([1,2,3])

const getName = (name:string) => {
  console.log(name,'=======》我是父组件')

}
const getName1 = (list:number[]) => {
  console.log(list,'=======》我是父组件')
}

// 接收组件实例: 实例名要对应ref=实例名
const loginVue = (list: number[])=>{
    console.log(list,'父组件接收子组件')
}

</script>

<style scoped lang="less">
html,body,#app{// 初始化
  height: 100%;
  overflow: hidden;
}

</style>

Child.vue子组件:接收父组件传参

<template>
  <div class="wraps">
    <!--接收的父组件传递过来的值,直接使用-->
    子级----父组件传递数据:{{title}}
    <div>值:{{arr}}</div>
  </div>
</template>

<script setup lang="ts">
import {defineProps, onMounted, reactive} from "vue";

// 使用defineProps宏命令接收父组件传递的数据
/*defineProps({// 接收对象
  title:{ // title对象:初始类型
    type:String,
    default:'默认值',
  }
})*/
// 代码中调用defineProps
// 需要先定义接收父组件值
/*let props = defineProps({// 接收对象
  title:{ // title对象:初始类型,模板中直接调用
    type:String,
    default:'默认值',
  }
})
// 再代码中调用
console.log('被调用了',props.title)*/
// 再优化
// ts 特有定义默认值 withDefaults
// const props = defineProps<{
withDefaults( defineProps<{
  title:string,
  arr: number[],
}>(),{ // 默认值设置
  arr:()=>[666]
})

</script>

<style scoped lang="less">
.wraps {
  position: relative;
  &.items {
    position: absolute;
    width:120px
  }
}
</style>

Login.vue子组件向父组件App.vue:传参 

<template>
  <div class="menu">
    子集----子组件向父组件传递
    <button @click="send">给父组件传值</button>
  </div>
</template>

<script setup lang="ts">
import {defineProps, reactive,defineEmits} from "vue";

const list = reactive<number[]>([4,5,6])
// defineEmits提交的数据:给父组件传值
// const emit = defineEmits(['juelan','tongda'])
const emit = defineEmits<{
  (e:'juelan',name:String):void
  (e:'tongda',name:String):void
}>()

// 定义方法:点击事件
const send = () => {// 点击时回传
  emit('juelan','嘿嘿')
  emit('tongda','通达')
}

// 给父组件暴露方法:
/*defineExpose({
  name: '爵岚',
})*/

</script>

<style scoped>
.menu{

}
</style>

兄弟传参:父组件当桥梁   (比较繁琐)

A组件:defineEmit提交传参---父组件:当桥梁接收A参,再赋值传B参---B组件:defineProps接收传参:直接使用 {{  参数名  }}

App.vue

<template>
    <div class="app">
        <h3>我是App组件(祖),{{car.name}}--{{car.price}}</h3>

        <!--1.接收子A组件传父:参-->
        <child @on-click="getFlag"></child>

        <!--4.将父组件当桥梁:flag传入B组件-->
        <Son :flag="Flag"></Son>
    </div>

</template>

<!--组合式setup-->
<script setup lang="ts"> // setup语法糖
import {ref, provide, reactive, toRefs} from "vue";
import Child from "./components/Child.vue"; // setup语法糖效率提升之一:子组件只需要导入,无需声明 --即 components:['Demo'] 被省略
import Son from './components/Son.vue'

let car = reactive({
    name: '奔驰',
    price:'40w',
})
// 祖孙后代组件间数据传递:provide传递与inject接收成对
provide('car',car) // k:v祖组件提供数据:参数car给自己后代组件传递数据
toRefs(car)

// 兄弟组件传参:桥梁
let Flag = ref(false) // 2.将父组件当桥梁:flag

const getFlag = (params:boolean) => {
    Flag.value = params; // 3.将子组件值赋值给Flag
}

</script>

<style scoped lang="less">
.app {
    background-color: gray;
    padding:10px;
    //background: v-bing(car2); 特有:绑定变量car2
}
</style>

Child.vue=A组件

<template>
  <div class="child">
      <!---->
      <h3>我是子组件:{{ car1.name}}--{{ car1.price}}</h3>
      <!--兄弟组件传参-->
      <button @click="emitB">派发一个事件</button>

  </div>
</template>

<script setup>
import {inject,defineProps,defineEmits,defineExpose} from "vue";


let car1 = inject('car') // 接收App.vue传递过来的参数car名称对应

// 兄弟组件传参:子传父参,父当桥梁
const emit = defineEmits(['on-click'])
let flag = false
const emitB = ()=>{
    flag = !flag
    emit('on-click',flag)
}


</script>

<style scoped lang="less">
.child {
    background-color: orange;
    padding:10px;
    //background: v-bing(car1); 特有:绑定变量car1
}
</style>

Son.vue=B组件

<template>
  <div class="son">
      <h3>我是son组件:{{car2.name}}--{{car2.price}}</h3>
      {{flag}}
  </div>
</template>

<script setup lang="ts">
import {inject} from "vue";

// 组件间数据传递:provid传递与inject接收成对
let car2 = inject('car') // 接收祖组件的数据

// defineProps接收父组件当桥梁:Flag
type Props = {
    flag:boolean,
}
defineProps<Props>()

/*defineProps<{
    flag:boolean
}>()*/
</script>

<style scoped lang="less">
.son{
      background-color: skyblue;
      padding:10px;
    //background: v-bing(car2); 特有:绑定变量car2
}
</style>

优化兄弟传参:Bus总线

利用JS发布订阅,时间调动中心

以下是Bus.ts文件

type BusClass = {
    emit: (name: string) => void
    on: (name: string, callback: Function) => void
}
type pramsKey = string | number | symbol;

type List = {
    [key: pramsKey]: Array<Function>
}
class Bus implements BusClass {
    list: List
    constructor() {
        this.list = {}
    }
    emit(name: string, ...args: Array<any>) {
        let eventName: Array<Function> = this.list[name]
        eventName.forEach(fn => {
            fn.apply(this, args)
        })
    }
    on(name: string, callback: Function) {
        let fn: Array<Function> = this.list[name] || []
        fn.push(callback)
        this.list[name] = fn
    }
}
export default new Bus()
type BusClass<T> = {
  emit: (name: T) => void
  // emit 中接收的参数,会传递给 callback
  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() {
    // 初始化 list
    this.list = {};
  }
 
  /**
   * emit
   * @param name 自定义事件名称
   * @description 可传递的参数有多个,所以使用解构赋值 ...args
   */
  emit(name: T, ...args: Array<any>) {
    let eventName: Array<Function> = this.list[name]
    eventName.forEach(ev => {
      ev.apply(this, args)
    })
  }
 
/**
 * on
 * @param name 自定义事件名称
 * @param callback 自定义事件方法
 */
  on(name: T, callback: Function) {
    // 如果事件已经注册过了,则直接去 list 中寻找;如果没注册过,则返回空数组
    let fn: Array<Function> = this.list[name] || [];
    fn.push(callback)
    this.list[name] = fn
  }
}
 
export default new Bus<number>()

兄弟组件:A组件代码
***使用Bus.emit传值***

<template>
    <div class="a_main">
        我是A组件
        <button @click="changeEmit">修改值</button>
    </div>
</template>

<script lang='ts' setup>
import { defineEmits, ref } from 'vue'
import Bus from '../Bus'
let flag = false;
const changeEmit = () => {
    flag = !flag
    Bus.emit('on-click', flag)
}
</script>

<style scoped lang="scss">
.a_main {
    width: 100%;
    height: 200px;
    background: red;
    color: #fff;
}
</style>
// 兄弟组件A
import Bus form './bus.ts';
 
// 发送事件
const handleEmit = () => {
  Bus.emit('a-data', 1, 2);
};
 
 
// 兄弟组件B
<A @a-data="handleOn"></A>
 
import Bus form './bus.ts';
 
// 接收事件
const handleOn = () => {
  Bus.on('a-data', (data1, data2) => {
    console.log('data1 ===', data1);
    console.log('data2 ===', data2);
  });
};

兄弟组件B组件代码
使用Bus.on接收值

<template>
    <div class="b_main">
        我是B组件{{ Flag }}
    </div>
</template>

<script lang='ts' setup>
import Bus from '../Bus'
import { ref } from 'vue'
let Flag = ref(false)
Bus.on('on-click', (flag: boolean) => {
    console.log(flag)
    Flag.value = flag
})
</script>

<style scoped lang="scss">
.b_main {
    width: 300px;
    height: 300px;
    background: blue;
}
</style>

如果我们不想使用这个,想使用更多功能的内置的写好的组件请点击下方链接,教你使用Mitt,vue3的EventBus

mitt用法

mitt - npm
Tiny 200b functional Event Emitter / pubsub.. Latest version: 3.0.0, last published: a year ago. Start using mitt in your project by running `npm i mitt`. There are 1346 other projects in the npm registry using mitt.
https://www.npmjs.com/package/mitt

安装 mitt

npm install --save mitt 

4.2.2 新建 mitt-bus.ts

import mitt from 'mitt';
export default mitt();

4.2.3 使用 mitt 发送事件

import mittBus from "@/utils/mitt-bus"; 
mittBus.emit('refreshDraftList', '刷新草稿列表');

4.2.4 使用 mitt 接收事件

接收事件监听的页面,在卸载时,一定要记得调用 off 取消事件监听

import mittBus from "@/utils/mitt-bus";
 
/**
 * 监听:刷新草稿列表页面
 * @desc 发送草稿表单后,会跳转回草稿列表页面,并重新获取草稿
 */
mittBus.on('refreshDraftList', (data: string) => {
  console.log(data);
  // 请求:获取草稿列表
  getDraftList();
}) 
 
/**
 * 页面卸载后,取消事件监听
 */
onUnmounted(() => {
  mittBus.off('refreshDraftList');
})

4.2.5 在全局实例上使用 mitt

上面的 4.2.1-4.2.4 是单独使用 mitt,不是将 mitt 挂载到全局上,下面展示如何挂载到全局使用 

修改 main.ts

// 引入的不再是Vue的构造函数,引入的是createApp工厂函数
// 工厂函数无需new来构造
import { createApp } from 'vue'
// import './style.css'
import App from './App.vue'
// 引入全局生效 Css/reset.less
// import './assets/css/reset.less'
// import CardVue from './components/Card.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

// 引入总线插件Mitt
import mitt from "mitt";

const Mit = mitt()

//创建应用实例对象——app(类似于之前Vue2中的vm,但app比vm更“轻”)
const app = createApp(App)

//挂载
app.use(ElementPlus)
// 总线
// 必须要拓展 ComponentCustomProperties 类型,才能获得关于 mitt 的 ts 提示
declare module 'vue' { 
    export interface ComponentCustomProperties {
        $Bus: typeof Mit
    }
}
app.config.globalProperties.$Bus = Mit

app.mount('#app')

// 引入全局组件component(key,组件名),要在mount()之前
// createApp(App).component('Card',CardVue).mount('#app')
// createApp(App).use(ElementPlus).mount('#app')

派发事件

<template>
    <div>
        <h1>我是A</h1>
        <button @click="handleEmit1">handleEmit1</button>
        <button @click="handleEmit2">handleEmit2</button>
    </div>
</template>
 
<script setup lang='ts'>
import { getCurrentInstance } from 'vue'
 
const instance = getCurrentInstance();
 
const handleEmit1 = () => {
    instance?.proxy?.$Bus.emit('emitone', 100)
}
 
const handleEmit2 = () => {
    instance?.proxy?.$Bus.emit('emittwo', 200)
}
</script>

添加事件监听

注意:如果是监听全部事件,那么会默认接收两个参数:事件名称、事件数据

最后会这么展示:

  • 监听全部事件 === emitone 100
  • 监听全部事件 === emittwo 200
<template>
    <div>
        <h1>我是B</h1>
    </div>
</template>
 
<script setup lang='ts'>
import { getCurrentInstance } from 'vue'
 
const instance = getCurrentInstance()
 
instance?.proxy?.$Bus.on('emit-one', (num) => {
    console.log('监听一个事件 ===', num)
})
 
instance?.proxy?.$Bus.on('*', (emitName, emitData) => {
    console.log('监听全部事件 ===', emitName, emitData)
})
</script>

移除事件监听

const testEmitOff = (num: number) => {
    console.log(num, '测试事件监听移除')
}
 
// 添加事件监听
instance?.proxy?.$Bus.on('emitone', testEmitOff)
// 移除事件监听
instance?.proxy?.$Bus.off('emitone', testEmitOff)

清空所有事件监听

instance?.proxy?.$Bus.all.clear()

 

动态组件

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

在挂载点使用component标签,然后使用v-bind:is=”组件”

用法如下

引入组件

import A from './A.vue'
import B from './B.vue'
  <component :is="A"></component>

通过is 切换 A B 组件

使用场景

tab切换 居多

注意事项

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)
}])
<template>
    <div class="one">
        <div class="content">
            <div
            v-for="item in tabsList" 
            :key="item.name"
            :class="[current.name === item.name ? 'tabActive':'', 'tab']"
            @click="changeTab(item)"
            >
                {{item.name}}
            </div>
            <component :is="current.comName ?? ''"></component>
        </div>
    </div>
</template>

<script setup lang='ts'>
import { ElementPlus } from '@element-plus/icons-vue';
import { ref, reactive, markRaw, toRaw, toRefs } from 'vue'
import A from './A.vue';
import B from './B.vue';
import C from './C.vue';

type Tab = {
    name: string,
    comName: any
}
// type Com = Pick<Tab, 'comName'> // 从一个已知的类型中,取出子集,作为一个新的类型返回。 相当于type Com = {comName: any}
// markRaw 跳过代理,避免出现警告
const tabsList = reactive<Tab[]>([
    {
        name: '我是A组件',
        comName: markRaw(A)
    },{
        name: '我是B组件',
        comName: markRaw(B)
    },{
        name: '我是C组件',
        comName: markRaw(C)
    }
])
const state = reactive({ 
    current: {}
})
// 初始化展示组件
state.current = tabsList[0] // 第一个组件
const { current } = toRefs(state) // 神奇:这个方式解构,后面切换组件,直接state.current = item,页面引用的current也可以直接更新
console.log('初始current:', current)

// 切换组件
const changeTab = (item:Tab) => {
    state.current = item
    console.log('切换组件current:', current)
}

</script>
<style lang="less" scoped>
.one{
    margin-top: 20px;
    width: 400px;
    height: 400px;
    background-color: #fff;
    color: #000;
    .content {
        .tab{
            border: 1px solid #ccc;
            display: inline-block;
            cursor: pointer;
            padding: 2px 10px;
        }
        .tabActive{
            color: aquamarine;
        }
    }
}
</style>

效果:

  

 

 

 

 

四、Composition API 的优势

1.Options API 存在的问题

使用传统OptionsAPI中,新增或者修改一个需求,就需要分别在data,methods,computed里修改 。
 
 
 

 

 

 

 

 

 

 

2.Composition API 的优势

我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起。hook函数
 

 

 

 

 

 

五、新的组件

 1.Fragment

- 在Vue2中: 组件必须有一个根标签
- 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
- 好处: 减少标签层级, 减小内存占用

2.Teleport (开发常用)

 什么是Teleport?—— `Teleport` 是一种能够将我们的<strong style="color:#DD5145">组件html结构</strong>移动到指定位置的技术。
  ```vue
  <teleport to="移动位置">
    <div v-if="isShow" class="mask">
      <div class="dialog">
        <h3>我是一个弹窗</h3>
        <button @click="isShow = false">关闭弹窗</button>
      </div>
    </div>
  </teleport>
  ```

应用:将深层次的组件提取进行处理。

 

3.Suspense

- 等待异步组件时渲染一些额外内容,让应用有更好的用户体验

 

 

  - 使用步骤:

  - 异步引入组件
    ```js
    import {defineAsyncComponent} from 'vue'
    const Child = defineAsyncComponent(()=>import('./components/Child.vue'))
    ```
 - 使用```Suspense```包裹组件,并配置好```default``` 与 ```fallback```
<template>
  <div class="app">
    <h3>我是App组件(祖)</h3>

  </div>
  <!--动态引入插槽:-->
  <Suspense>
    <!--指定真正要使用的插槽组件:default-->
    <template v-slot:default>
      <child/>
    </template>
    <!--应急插槽组件:fallback,当default插槽无法展示时-->
    <template v-slot:fallback>
      <h3>加载中...loading...</h3>
    </template>
  </Suspense>
</template>

<!--组合式setup-->
<script setup lang="ts"> // setup语法糖
// import Child from "./components/Child.vue"; // 静态引入
import {defineAsyncComponent} from "vue"; // 动态引入:异步组件
// 动态引入:异步引入
const Child = defineAsyncComponent(()=>import('./components/Child.vue'))

</script>

<style scoped>
.app {
  background-color: gray;
  padding:10px;
}
</style>

 

为了实现这个功能,Vue3中为我们提供了一个方法,即defineAsyncComponent,这个方法可以传递两种类型的参数,分别是函数类型和对象类型,接下来我们分别学习。

1.无配置项定义方式

 

 2.配置项定义方式

loader:同工厂函数;
loadingComponent:加载异步组件时展示的组件;
errorComponent:加载组件失败时展示的组件;
delay:显示loadingComponent之前的延迟时间,单位毫秒,默认200毫秒;
timeout:如果提供了timeout,并且加载组件的时间超过了设定值,将显示错误组件,默认值为Infinity(单位毫秒);
suspensible:异步组件可以退出<Suspense>控制,并始终控制自己的加载状态。具体可以参考文档;
onError:一个函数,该函数包含4个参数,分别是error、retry、fail和attempts,这4个参数分别是错误对象、重新加载的函数、加载程序结束的函数、已经重试的次数。
如下代码展示defineAsyncComponent方法的对象类型参数的用法:

const asyncPageWithOptions = defineAsyncComponent({
  loader: () => import('./NextPage.vue'), // component配置项重新命名为loader
  delay: 200,
  timeout: 3000,
  errorComponent: ErrorComponent,
  loadingComponent: LoadingComponent
})

六、其他

1.全局API的转移

- Vue 2.x 有许多全局 API 和配置。
  - 例如:注册全局组件、注册全局指令等。

    ```js
    //注册全局组件
    Vue.component('MyButton', {
      data: () => ({
        count: 0
      }),
      template: '<button @click="count++">Clicked {{ count }} times.</button>'
    })
   
    //注册全局指令
    Vue.directive('focus', {
      inserted: el => el.focus()
    }
    ```

- Vue3.0中对这些API做出了调整:

  - 将全局的API,即:```Vue.xxx```调整到应用实例(```app```)上

    | 2.x 全局 API(```Vue```) | 3.x 实例 API (`app`)                        |
    | ------------------------------------------ | ------------------------------------------------------------- |
    | Vue.config.xxxx                          | app.config.xxxx                                                 |
    | Vue.config.productionTip           | <strong style="color:#DD5145">移除</strong> |
    | Vue.component                          | app.component                                                 |
    | Vue.directive                              | app.directive                                                      |
    | Vue.mixin                                   | app.mixin                                                           |
    | Vue.use                                     | app.use                                                              |
    | Vue.prototype                            | app.config.globalProperties                               |
 

## 2.其他改变

- data选项应始终被声明为一个函数。

- 过度类名的更改:

  - Vue2.x写法

    ```css
    .v-enter,
    .v-leave-to {
      opacity: 0;
    }
    .v-leave,
    .v-enter-to {
      opacity: 1;
    }
    ```

  - Vue3.x写法

    ```css
    .v-enter-from,
    .v-leave-to {
      opacity: 0;
    }
   
    .v-leave-from,
    .v-enter-to {
      opacity: 1;
    }
    ```

- <strong style="color:#DD5145">移除</strong>keyCode作为 v-on 的修饰符,同时也不再支持```config.keyCodes```

- <strong style="color:#DD5145">移除</strong>```v-on.native```修饰符

  - 父组件中绑定事件

    ```vue
    <my-component
      v-on:close="handleComponentEvent"
      v-on:click="handleNativeClickEvent"
    />
    ```

  - 子组件中声明自定义事件

    ```vue
    <script>
      export default {
        emits: ['close']
      }
    </script>
    ```

- <strong style="color:#DD5145">移除</strong>过滤器(filter)

  > 过滤器虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是 “只是 JavaScript” 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。

- ......

 

12、插槽

插槽就是子组件中的提供给父组件使用的一个占位符,用<slot></slot> 表示,父组件可以在这个占位符中填充任何模板代码,如 HTML、组件等,填充的内容会替换子组件的<slot></slot>标签。

匿名插槽、具名插槽、作用域插槽、动态插槽

具体使用看下面案例注解:

// 子组件A.vue 内定义插槽
<template>
    <div class="one">
        <!-- 匿名插槽 -->
        <header class="header">
            <slot></slot>
        </header>
        <!-- 具名插槽 name= -->
        <main class="main">
            slot1
            <slot name="slot1"></slot>
        </main>
        <!-- 作用域插槽 :data=传参 -->
        <footer class="footer">
            slot2
            <slot name="slot2" :data="'我出现在slot2插槽'"></slot>
        </footer>
        <!-- 动态插槽 :name=[插槽名变量] -->
        <div class="dynamic">
            动态插槽
            <slot name="slot3"></slot>
        </div>
    </div>
</template>
<script setup lang='ts'>
import { ref, reactive } from 'vue'
</script>
<style lang="less" scoped>
.one{
   color: azure;
   .header{
        height: 100px;
        width: 100%;
        background: rgb(236, 255, 192);
        text-align: center;
    }
   .main{
        height: 100px;
        width: 100%;
        background: pink;
        text-align: center;
    }
   .footer{
        height: 100px;
        width: 100%;
        background: rgb(195, 245, 247);
        text-align: center;
    }
    .dynamic{
        height: 100px;
        width: 100%;
        background: rgb(247, 195, 224);
        text-align: center;
    }
}
</style>

Vue3 第十八章(异步组件&代码分包&suspense)

异步组件

在大型应用中,我们可能需要将应用分割成小一些的代码块 并且减少主包的体积

这时候就可以使用异步组件

顶层 await

在setup语法糖里面 使用方法

<script setup> 中可以使用顶层 await。结果代码会被编译成 async setup() 

<script setup>
const post = await fetch(`/api/post/1`).then(r => r.json())
</script>

父组件引用子组件 通过defineAsyncComponent加载异步配合import 函数模式便可以分包 

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

小案例:进入页面,3秒完成请求,请求过程展示loading,解决浏览页面出现白屏问题。 

  已下父组件引入异步组件,配合Suspense 使用,解决加载过程白屏尴尬

注意引入异步组件要使用 defineAsyncComponent 方法

// 父组件
<template>
    <div class="content">
      <Suspense>
        <!-- 加载完毕,使用默认插槽default -->
        <template #default>
            <A></A>
        </template>
        <!-- 加载过程中,使用插槽fallback -->
        <template #fallback>
            <div>loading...</div>
        </template>
      </Suspense>
    </div>
</template>
<script setup lang='ts'>
import { ref, reactive, defineAsyncComponent } from 'vue'
const A = defineAsyncComponent(() =>  import('../../async/index.vue')) // 异步组件需要defineAsyncComponent引入
</script>

效果展示:
3秒完成加载,过程loading展示

 使用异步组件实现分包:

 

 

Vue3 第十九章(Teleport传送组件)

 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>

多个使用场景 

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

 

// 小弹窗相对参考框居中定位
{
    width: 200px;
    height: 200px;
    left: 50%;
    top: 50%;
    margin-top: -100px;
    margin-left: -100px;
    position: absolute;
    background-color: rgb(233, 135, 152);
}
</style>

父组件:

 使用传送组件前:

 

 把小弹窗传送给body后:

 

 

Vue3 第二十章(keep-alive缓存组件)

内置组件keep-alive
有时候我们不希望组件被重新渲染影响使用体验;或者处于性能考虑,避免多次重复渲染降低性能。而是希望组件可以缓存下来,维持当前的状态。这时候就需要用到keep-alive组件。

开启keep-alive 生命周期的变化

  初次进入时: onMounted> onActivated
  退出后触发 deactivated
  再次进入:
  只会触发 onActivated
  事件挂载的方法等,只执行一次的放在 onMounted中;组件每次进去执行的方法放在 onActivated中

<!-- 基本 -->
<keep-alive>
  <component :is="view"></component>
</keep-alive>
 
<!-- 多个条件判断的子组件 -->
<keep-alive>
  <comp-a v-if="a > 1"></comp-a>
  <comp-b v-else></comp-b>
</keep-alive>
 
<!-- 和 `<transition>` 一起使用 -->
<transition>
  <keep-alive>
    <component :is="view"></component>
  </keep-alive>
</transition>

 include 和 exclude

 <keep-alive :include="" :exclude="" :max=""></keep-alive>

include 和 exclude 允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示:

max

<keep-alive :max="10">
  <component :is="view"></component>
</keep-alive>

keep-alive 源码讲解

源码目录runtime-core/src/components/KeepAlive.ts

 

keep-alive缓存组件

keep-alive内置组件用于缓存组件,再次切入时不需要重新渲染,并且保留切出前状态。

开启keep-alive 生命周期:

初次进入时: onMounted> onActivated
退出后:触发deactivated
再次进入:触发 onActivated
  • 使用方式:
<!-- 基本 -->
<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>
  • include 和 exclude属性

include 和 exclude 允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示:

<keep-alive :include="" :exclude="" :max=""></keep-alive> 

完整代码:

App.vue

<template>
    <el-button type="primary" @click="flag1 = !flag1">切换组件</el-button>
    <!--:include都缓存:[vue文件名],exclude都不缓存-->
    <!--:max=‘10’表示缓存10个组件-->
    <keep-alive :include="['A','B']">
        <AVue v-if="flag1"></AVue>
        <BVue v-else></BVue>
    </keep-alive>
</template>

<!--组合式setup-->
<script setup lang="ts"> // setup语法糖
import { ref, } from "vue";
import AVue from './components/A.vue'
import BVue from './components/B.vue'

const flag1 = ref<boolean>(true)

</script>

<style scoped>

</style>

A.vue子组件和B.vue都是Element-plus复制样式代码

Vue3 第二十一章(transition动画组件)

Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡:

  • 条件渲染 (使用 v-if)
  • 条件展示 (使用 v-show)
  • 动态组件
  • 组件根节点

 自定义 transition 过度效果,你需要对transition组件的name属性自定义。并在css中写入对应的样式

1.过渡的类名

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

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

1. transition 动画组件

1.1 tansition 组件基础用法transition 组件,用于给包裹元素添加 进入/离开 过渡动画

transition 组件上可以接收 name 属性,用于指定六种动画类名前缀(此处以 fade 为栗子)

fade-enter-from
fade-enter-active
fade-enter-to
fade-leave-from
fade-leave-active
fade-leave-to

完整代码

<template>
<div>
  <button @click="flag = !flag">
    切换
  </button>
  <!-- 条件渲染 -->
  <transition name="fade">
    <div v-if="flag" class="my-box"></div>
  </transition>
  <!-- 条件展示 -->
  <transition name="fade">
    <div v-show="flag" class="my-box"></div>
  </transition>
  <!-- 动态组件 -->
  <transition name="fade">
    <component is="A"></component>
  </transition>
</div>
</template>
 
<script lang="ts" setup>
import { reactive, toRefs, defineComponent } from 'vue';
 
const flag = ref(true);
</script>
 
<style lang="scss" scoped>
/* 进入开始 */
.fade-enter-from {
  width: 0px;
  height: 0px;
  background: rgb(255, 153, 0);
  transform: rotate(360deg);
}
/* 进入中 */
.fade-enter-active {
  transition: all 1s linear;
}
/* 进入完成 */
.fade-enter-to {
  width: 200px;
  height: 200px;
  background: rgb(255, 0, 0);
}
/* 离开开始 */
.fade-leave-from {
  width: 200px;
  height: 200px;
}
/* 离开中 */
.fade-leave-active {
  transition: all 0.5s linear;
}
/* 离开完成 */
.fade-leave-to {
  width: 0px;
  height: 0px;
}
</style>

 

2.自定义过渡 class 类名,并使用 animate.css

trasnsition props

  •   enter-from-class
  •   enter-active-class
  •   enter-to-class
  •   leave-from-class
  •   leave-active-class
  •   leave-to-class

自定义类名

transition 组件定义了以下 props,用于接收:

  • 自定义过渡类名
  • 自定义过渡时间(可以直接传入毫秒数,也可以分别定义进入或离开时的毫秒数)

举个栗子~~ 

  <!-- :duration="1000" -->
  <transition
    :enter-from-class="'classA'"
    :enter-active-class="'classB'"
    :enter-to-class="'classC'"
    :leave-from-class="'classD'"
    :leave-active-class="'classE'"
    :leave-to-class="'classF'"
    :duration="{ enter: 500, leave: 800 }"
  >
    <component is="A"></component>
  </transition>

自定义过度时间 单位毫秒

你也可以分别指定进入和离开的持续时间:

<transition :duration="1000">...</transition> 
 
<transition :duration="{ enter: 500, leave: 800 }">...</transition>

通过自定义class 结合css动画库animate css

安装库 npm install animate.css

引入 import 'animate.css'

使用方法

通过 transition 自定义 class,结合 animate.css 动画库,就能实现多种动画效果

Animate.css | A cross-browser library of CSS animations.
Animate.css is a library of ready-to-use, cross-browser animations for you to use in your projects. Great for emphasis, home pages, sliders, and attention-guiding hints.
https://animate.style/

// 安装 animate.css

npm install animate.css --save

yarn add animate.css

// 在 main.ts 中引入 animate.css

import 'animate.css'

举个栗子~~

<transition
  leave-active-class="animate__animated animate__bounceInLeft"
  enter-active-class="animate__animated animate__bounceInRight"
>
  <div v-if="flag" class="my-box"></div>
</transition>

注意:一定要添加类名前缀 animate__animated,否则动画效果无法生效

官方文档 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 生命周期8个,使用 GSAP

transition 的生命周期,相对于 props 多了两条:

  • 显示过渡打断
  • 离开过渡打断
  @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"//离开过度打断

注意事项:

  每个生命周期函数接收一个 el 参数,标识执行过渡动画的 DOM 元素
  enter / leave 函数除了 el 参数,还接收 done 参数,标识动画执行完成(举个栗子:enter 动画执行过程为 3s,加了 done 函数后,三秒之后才执行 after-enter,不加则 同步执行 两个函数)
  只用 JavaScript 过渡时,在 enter 和 leave 钩子中,必须使用 done 进行回调
  enter-cancelled / leave-cancelled 函数指 当元素在执行完动画之前,就已经被按钮控制隐藏了,此时就会调用打断函数

  <transition
    :before-enter="enterFrom"
    :enter="enterActive"
    :after-enter="enterTo"
    :enter-cancelled="enterCancel"
    :before-leave="leaveFrom"
    :leave="leaveActive"
    :after-leave="leaveTo"
    :leave-cancelled="leaveCancel"
  >
    <component is="A"></component>
  </transition>
 
 
// before-enter
const enterFrom = (el: Element) => {
  console.log('进入之前');
};
 
/**
 * enter
 * @param el 执行过渡动画的 DOM 元素
 * @param done 标识动画执行完成
 */
const enterActive = (el: Element, done: Function) => {
  console.log('过渡曲线');
  setTimeout(() => {
    // 三秒后,此动画执行完毕,可以打印下面的 过渡完成 了
    done();
  }, 3000);
};
 
// after-enter
const enterTo = (el: Element) => {
  console.log('过渡完成');
};
 
// enter-cancelled
const enterCancel = (el: Element) => {
  console.log('过渡效果被打断');
  console.log('在 3s 内过渡效果还没完成,元素就隐藏了,此时就会执行被打断函数');
};

结合gsap 动画库使用 GreenSock,使用 GSAP 实现动画效果(Vue 官网推荐)

null
GSAP is an industry standard JavaScript animation library from GreenSock that lets you craft high-performance animations that work in every major browser.
https://greensock.com/

// 使用 cdn 的方式引入

https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.3/gsap.min.js 

// 使用 npm 进行安装

npm install gsap

注意事项:

  • gsap.set —— 动画初始状态
  • gsap.to —— 动画最终状态 
  • onComplete —— gsap 中,过渡完成的回调函数
  • done 函数 ts 类型报错 —— 将 Function 类型替换为 gsap.Callback 即可

 

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

完整代码

  <transition
    :before-enter="enterFrom"
    :enter="enterActive"
    :leave="leaveActive"
  >
    <component is="A"></component>
  </transition>
 
import gsap from 'gsap';
 
// before-enter
const enterFrom = (el: Element) => {
  console.log('进入之前');
  gsap.set(el, {
    width: 0,
    height: 0,
  });
};
 
/**
 * enter
 * @param el 执行过渡动画的 DOM 元素
 * @param done 标识动画执行完成
 * @decription 关于 done 类型的报错,使用 gsap.Callback 替换 Function
 */
const enterActive = (el: Element, done: gsap.Callback) => {
  console.log('过渡曲线');
  gsap.to(el, {
    width: 200,
    height: 200,
    // 过渡完成的回调函数
    onComplete: done,
  });
};
 
/**
 * leave
 * @param el 执行过渡动画的 DOM 元素
 * @param done 标识动画执行完成
 * @decription 关于 done 类型的报错,使用 gsap.Callback 替换 Function
 */
const leaveActive = (el: Element, done: gsap.Callback) => {
  console.log('过渡曲线');
  gsap.to(el, {
    width: 0,
    height: 0,
    // 过渡完成的回调函数
    onComplete: done,
  });
};

4.appear

appear 属性 —— 设置 页面加载完成后,首次、立刻执行的动画

appear 有三个状态,可以接收自定义类名(可以结合 animate.css 使用)

通过这个属性可以设置初始节点过度 就是页面加载完成就开始动画 对应三个状态

appear-active-class=""
appear-from-class=""
appear-to-class=""
appear 
<template>
  <transition
    appear
    appear-from-class="appear-from"
    appear-active-class="appear-active"
    appear-to-class="appear-to"
  >
    <component is="A"></component>
  </transition>
</template>
 
...
 
<style lang="scss" scoped>
.appear-from {
  width: 0px;
  height: 0px;
}
 
.appear-active {
  transition: all 0.5s linear;
}
 
.appear-to {
  width: 200px;
  height: 200px;
}
</style>

transition-group 动画列表

参考视频(强烈推荐,小满讲的非常清楚)

小满Vue3(第二十二章 transitionGroup)_哔哩哔哩_bilibili
小满Vue3(第二十二章 transitionGroup)是Vue3 + vite + Ts + pinia + 实战 + 源码 +electron的第27集视频,该合集共计110集,视频收藏或关注UP主,及时了解更多相关视频内容。
https://www.bilibili.com/video/BV1dS4y1y7vd?p=27&vd_source=8bc01635b95dbe8ecd349b2c23b03a10

列表项过渡

transition-group 组件的 props、生命周期 和 transition 组件一模一样

注意事项:

  transition-group 接收 tag 属性,用于自动生成一个 包裹列表项的 DOM 元素
  transition-group 内部的元素,一般是循环出来的列表项,一定要使用 key 标识唯一值
  transition-group 的样式会在列表项中生效,而不是列表容器

<template>
  <button @click="addItem">
    新增列表项
  </button>
  <button @click="removeItem">
    删除列表项
  </button>
 
  <!-- 过渡列表 -->
  <div class="list-wrap">
    <transition-group
      tag="section"
      enter-active-class="animate__animated animate__bounceIn"
      leave-active-class="animate__animated animate__hinge"
    >
      <div v-for="item in list" :key="item" class="list-item">
        {{ item }}
      </div>
    </transition-group>
  </div>
</template>
 
<script lang="ts" setup>
import { reactive } from 'vue';
import 'animate.css';
 
const list = reactive<number[]>([1, 2, 3, 4, 5, 6]);
 
const addItem = () => {
  list.push(list.length + 1);
};
 
const removeItem = () => {
  list.pop();
};
</script>
 
<style lang="scss" scoped>
.list {
  &-wrap {
    display: flex;
    flex-wrap: wrap;
    word-break: break-all;
    border: 1px solid #D7D7D7;
  }
  &-item {
    margin: 10px;
    font-size: 20px;
  }
}
</style>

平面过渡

2.2.1 初始化数组的小技巧

如果使用 new Array(81) 直接生成数组,那么不会初始化数组的每一项

如果使用 apply 生成数组,则初始化时,会自动填充数组每一项为 undefined

2.2.2 安装 lodash ts 声明文件

Lodash 简介 | Lodash 中文文档 | Lodash 中文网

Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库。

https://www.lodashjs.com/

执行下方命令安装 lodash

npm i --save lodash

安装完毕后,可能出现报错,因为没有安装 ts 类型声明文件,执行下方命令安装

npm install @types/lodash -D

lodash.shuffle —— 将数组内的元素,随机打乱顺序

2.2.3 生成平面过渡的表格

场景描述:生成9行9列的表格,点击按钮后,实现单元格平面移动的效果

<template>
  <div>
    <button @click="shuffleArray">
      随机打乱数组内元素的顺序
    </button>
 
    <transition-group tag="ul" class="list-wraps">
      <!-- 一定要绑定 key,否则会不生效 -->
      <li v-for="item in testList" :key="item.id" class="list-cell">
        {{ item.number }}
      </li>
    </transition-group>
  </div>
</template>
 
<script setup lang="ts">
import { ref } from 'vue';
// 别忘记装 ts 声明文件:npm install @types/lodash -D
import _ from 'lodash';
 
/*
 * [
 *   { id: 1, number: 1 },
 *   { id: 8, number: 8 },
 *   { id: 18, number: 1 },
 *   { id: 27, number: 1 },
 * ]
 */
 
const testList = ref(
  Array.apply(null, { length: 81 } as number[]).map((_, index) => ({
    id: index,
    // 每 9 个为一组
    number: (index % 9) + 1,
  })),
);
 
/**
 * 将数组内的元素,随机打乱顺序
 */
const shuffleArray = () => {
  testList.value = _.shuffle(testList.value);
};
</script>
 
<style scoped lang="less">
.list-wraps {
  display: flex;
  flex-wrap: wrap;
  // 每 9 个一排
  width: calc(25px * 10 + 9px);
  .list-cell {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 25px;
    height: 25px;
    border: 1px solid #dcdcdc;
    list-style-type: none;
  }
}
</style>

状态过渡

举个栗子:数字从 9999 变成 1 时,添加滚动状态过渡的效果

再举个栗子:背景色从 红色 变成 蓝色 时,添加背景色过渡的效果

<template>
  <div>
    <!-- step 每次数字变动大小为 20 -->
    <input v-model="state.inputNumber" type="number" step="20" />
    <!-- 展示过渡效果的值 -->
    <div>{{ state.showTransitionNumber.toFixed(0) }}</div>
  </div>
</template>
 
<script setup lang='ts'>
import { reactive, watch } from 'vue';
import gsap from 'gsap';
 
const state = reactive({
  // 使用 input 改变的值
  inputNumber: 0,
  // 展示过渡效果的值
  showTransitionNumber: 0,
});
 
/**
 * 当 input 数字改变时,调整 展示过渡效果的值
 */
watch(() => state.inputNumber, (newVal) => {
  gsap.to(state, {
    // 过渡时间
    duration: 1,
    // 将 展示过渡效果的值 从原来的值,替换为 使用 input 改变的值
    showTransitionNumber: newVal,
  });
});
 
</script>

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>

Vue3 第二十五章(TSX)

完整版用法 请看 @vue/babel-plugin-jsx - npm

JS和TS混编

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

 

配置完成就可以使用啦

在目录新建一个xxxxxx.tsx文件

 

3.使用TSX

TIPS tsx不会自动解包使用ref加.vlaue ! ! !

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

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

v-if是不支持的

所以需要改变风格

import { ref } from 'vue'
 
let flag = ref(false)
 
const renderDom = () => {
    return (
        <>
            {
                flag.value ? <div>景天</div> : <div>雪见</div>
            }
        </>
    )
}
 
export default renderDom

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

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

v-on绑定事件 所有的事件都按照react风格来

  • 所有事件有on开头
  • 所有事件名称首字母大写
 
const renderDom = () => {
    return (
        <>
            <button onClick={clickTap}>点击</button>
        </>
    )
}
 
const clickTap = () => {
    console.log('click');
}
 
export default renderDom

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

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

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>
      </>
    );
  },
};

加餐实现一个vite插件解析tsx

1.需要用到的第三方插件

npm install @vue/babel-plugin-jsx
npm install @babel/core
npm install @babel/plugin-transform-typescript
npm install @babel/plugin-syntax-import-meta
npm install @types/babel__core

插件代码

import type { Plugin } from 'vite'
import * as babel from '@babel/core'; //@babel/core核心功能:将源代码转成目标代码。
import jsx from '@vue/babel-plugin-jsx'; //Vue给babel写的插件支持tsx v-model等
export default function (): Plugin {
    return {
        name: "vite-plugin-tsx",
        config (config) {
           return {
              esbuild:{
                 include:/\.ts$/
              }
           }
        },
        async transform(code, id) {
            if (/.tsx$/.test(id)) {
                //@ts-ignore
                const ts = await import('@babel/plugin-transform-typescript').then(r=>r.default)
                const res = babel.transformSync(code,{
                    plugins:[jsx,[ts, { isTSX: true, allowExtensions: true }]], //添加babel插件
                    ast:true, // ast: 抽象语法树,源代码语法结构的一种抽象表示。babel内部就是通过操纵ast做到语法转换。
                    babelrc:false, //.babelrc.json
                    configFile:false //默认搜索默认babel.config.json文件
                })
                return res?.code //code: 编译后的代码
            }
           
            return code
        }
    }
}

  

Vue3 第二十六章(深入v-model)

小彩蛋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

v-model

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="modelValue" class="model">
        <div class="close">
            <button @click="close">关闭</button>
        </div>
        <div class="dialog-content">
            内容:<input @input="change" :value="textVal" type="text">
        </div>

    </div>
</template>

<script setup lang='ts'>
import {ref, reactive} from "vue";
// vue2 value vue3 modelValue

type Props = {
    modelValue:boolean, // 接收:
    textVal:string, // 多model
    textValModifiers?:{ // 使用自定义textVal.isBt:text后面固定写法Modifiers
        isBt:boolean
    }
}

const propData = defineProps<Props>()
// 固定写法:['update:事件名‘]
const emit = defineEmits(['update:modelValue','update:textVal'])

const change = (e:Event) => {
    const target = e.target as HTMLInputElement //获取元素:使用断言
    emit('update:textVal',propData?.textValModifiers?.isBt ? target.value + '标题' : target)
}


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>
    <div>
        <h1>父组件App.vue</h1>
        <div>isShow:{{isShow}}</div>
        <div>text: {{text}}</div>
        <div>
            <button @click="isShow = !isShow">开关</button>
            <hr>
            <!--自定义组件绑定model:单model绑定-->
            <!--v-model:textVal='text'多model绑定-->
            <!--自定义v-model:textVal.isBt-->
            <Dialog v-model:textVal.isBt="text" v-model="isShow"></Dialog>
        </div>
    </div>
</template>

<!--组合式setup-->
<script setup lang="ts"> // setup语法糖
import {ref,} from 'vue'
import Dialog from './components/Dialog.vue' // 自定义组件
const isShow = ref(true)
// 多model绑定
const text =ref<string>('通达')

</script>

<style scoped lang="less">

</style>

Vue3 第二十七章(自定义指令directive)

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>
<template>
    <div>
        <!--<button @click="flag = !flag">切换</button>
        &lt;!&ndash;自定义指令directive&ndash;&gt;
        &lt;!&ndash;自定义指令v-move:自定义参数aaa.自定义修饰符juelan&ndash;&gt;
        <A v-if="flag" v-move:aaa.juelan="{background:'red'}"></A>-->
        <input v-model="flag" type="text">
        <A v-move="{background:flag}"></A>
    </div>
</template>

<!--组合式setup-->
<script setup lang="ts"> // setup语法糖
import A from './components/A.vue'
import {ref,Directive,DirectiveBinding} from "vue";
let flag = ref(true)
type Dir = {
    background:string
}
/*const vMove:Directive = {
    created: () => {
        console.log("初始化====>");
    },
    beforeMount(...args: Array<any>) { // 接收参数:v-move
        // 在元素上做些操作
        console.log("初始化一次=======>",args);
    },
    // 开发常用:mounted 元素插入父级dom调用.
    mounted(el: any, dir: DirectiveBinding<Dir>) {
        el.style.background = dir.value.background;
        console.log("初始化========>",el.style.background);
    },
    beforeUpdate() {
        console.log("更新之前");
    },
    // 开发常用:周期方法被移除 改用updated:元素被更新之后调用
    updated() {
        console.log("更新结束");
    },
    beforeUnmount(...args: Array<any>) {
        console.log(args);
        console.log("======>卸载之前");
    },
    // 开饭常用:
    unmounted(...args: Array<any>) {
        console.log(args);
        console.log("======>卸载完成");
    },
}*/

// 函数简写: 赋值颜色red\green自动变更颜色
const vMove:Directive = (el:HTMLElement,bingding:DirectiveBinding<Dir>) => {
    el.style.background = bingding.value.background
}


</script>

<style scoped lang="less">

</style>

3.生命周期钩子参数详解

第一个 el 当前绑定的DOM 元素

第二个 binding

  instance:使用指令的组件实例。
  value:传递给指令的值。例如,在 v-my-directive="1 + 1" 中,该值为 2。
  oldValue:先前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否有更改都可用。
  arg:传递给指令的参数(如果有的话)。例如在 v-my-directive:foo 中,arg 为 "foo"。
  modifiers:包含修饰符(如果有的话) 的对象。例如在 v-my-directive.foo.bar 中,修饰符对象为 {foo: true,bar: true}。
  dir:一个对象,在注册指令时作为参数传递。例如,在以下指令中

 

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

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

4.函数简写

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

<template>
   <div>
      <input v-model="value" type="text" />
      <A v-move="{ background: value }"></A>
   </div>
</template>
   
<script setup lang='ts'>
import A from './components/A.vue'
import { ref, Directive, DirectiveBinding } from 'vue'
let value = ref<string>('')
type Dir = {
   background: string
}
const vMove: Directive = (el, binding: DirectiveBinding<Dir>) => {
   el.style.background = binding.value.background
}
</script> 
 
<style>
</style>

案例自定义拖拽指令 

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

第二十八章(自定义Hooks)

Vue3 自定义Hook

主要用来处理复用代码逻辑的一些封装

这个在vue2 就已经有一个东西是Mixins

mixins就是将这些多个相同的逻辑抽离出来,各个组件只需要引入mixins,就能实现一次写代码,多组件受益的效果。

弊端就是 会涉及到覆盖的问题

组件的data、methods、filters会覆盖mixins里的同名data、methods、filters。

 

 

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

Vue3 的自定义的hook

  • Vue3 的 hook函数 相当于 vue2 的 mixin, 不同在与 hooks 是函数
  • Vue3 的 hook函数 可以帮助我们提高代码的复用性, 让我们能在不同的组件中都利用 hooks 函数

Vue3 hook 库Get Started | VueUse

案例

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

<template>
    <div>
        <img id="img" width="300" height="300" src="./assets/vue.svg"/>

    </div>
</template>

<!--组合式setup-->
<script setup lang="ts"> // setup语法糖
import useBase64 from './hooks'

useBase64({
    el: '#img'
}).then(res=>{
    console.log(res.baseUrl)
})

</script>

<style scoped lang="less">

</style>

Hooks/index.ts

import {onMounted} from 'vue'


type Options = {
    el: string,

}

export default function (options:Options):Promise<{baseUrl:string}>{
    return new Promise((resolve)=>{
        onMounted(()=>{
            let img:HTMLImageElement = document.querySelector(options.el) as HTMLImageElement
            console.log(img,'=====>')
            img.onload = () =>{
                resolve({
                    baseUrl: base64(img)
                })
            }

        })

        const base64 = (el:HTMLImageElement) =>{
            const canvas = document.createElement('canvas')
            const ctx = canvas.getContext('2d')
            canvas.width = el.width
            canvas.height = el.height
            ctx?.drawImage(el,0,0,canvas.width,canvas.height)
            return canvas.toDataURL('image/svg')
        }
    })
} 

Vue3 第二十九章(Vue3定义全局函数和变量)

globalProperties

由于Vue3 没有Prototype 属性 使用 app.config.globalProperties 代替 然后去定义变量和函数

Vue2

// 之前 (Vue 2.x)
Vue.prototype.$http = () => {}

Vue3

// 之后 (Vue 3.x)
const app = createApp({})
app.config.globalProperties.$http = () => {}

main.ts

// 引入的不再是Vue的构造函数,引入的是createApp工厂函数
// 工厂函数无需new来构造
import { createApp } from 'vue'
// import './style.css'
import App from './App.vue'
// 引入全局生效 Css/reset.less
// import './assets/css/reset.less'
// import CardVue from './components/Card.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

//创建应用实例对象——app(类似于之前Vue2中的vm,但app比vm更“轻”)
export const app = createApp(App)

// 引入总线插件Mitt
/*import mitt from "mitt";
const Mit = mitt()*/

// 总线
// 必须要拓展 ComponentCustomProperties 类型,才能获得关于 mitt 的 ts 提示
/*declare module 'vue' {
    export interface ComponentCustomProperties {
        $Bus: typeof Mit
    }
}
// 定义全局变量: 直接使用{{$bus}}
app.config.globalProperties.$Bus = Mit*/

// 定义全局变量: 直接使用{{$env}}
app.config.globalProperties.$env = 'dev'
// 定义全局过滤器: 直接使用 {{filter}}
app.config.globalProperties.$filters = {
    format<T>(str:T){
        return `爵岚-${str}`
    }
}

//挂载
app.use(ElementPlus)
app.mount('#app')

// 引入全局组件component(key,组件名),要在mount()之前
// createApp(App).component('Card',CardVue).mount('#app')
// createApp(App).config.globalProperties.$bus.use(ElementPlus).mount('#app')
// createApp(App).use(ElementPlus).mount('#app')

App.vue中引入

<template>
    <div>
        <!--使用全局变量、过滤器-->
        {{$env}}------{{$filters.format('的飞机')}}
    </div>
</template>

<!--组合式setup-->
<script setup lang="ts"> // setup语法糖
// 全局变量、过滤器在ts中使用方法
// 全局使用工具类 getCurrentInstance
import {getCurrentInstance} from 'vue'
// 在Vue3中,getCurrentInstance()可以用来获取当前组件实例
const app1 =getCurrentInstance()
console.log(app1?.proxy?.$filters.format('ts'))

</script>

<style scoped lang="less">

</style>

getCurrentInstance只能在setup或生命周期钩子中使用。 

过滤器

在Vue3 移除了

我们正好可以使用全局函数代替Filters

案例:

app.config.globalProperties.$filters = {
  format<T extends any>(str: T): string {
    return `$${str}`
  }
}

声明文件 不然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')) 

Vue3 第三十章(编写Vue3插件)

插件

插件是自包含的代码,通常向 Vue 添加全局级功能。你如果是一个对象需要有install方法Vue会帮你自动注入到install 方法 你如果是function 就直接当install 方法去使用

使用插件

在使用 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)
    }
}

 

Vue3 第三十一章(了解UI库ElementUI,AntDesigin等)

常用于后台开发:这几套框架主要用于后台管理系统和移动端的制作,方便开发者快速开发

Element UI Plus (Vue3.0首选)

安装方法

# 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')

tsconfig.json中添加

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. View Design (Vue2规格)

安装

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

Vue3 第三十二章(详解Scoped和样式 穿透)

主要是用于修改很多vue常用的组件库(element, vant, AntDesigin),虽然配好了样式但是还是需要更改其他的样式

就需要用到样式穿透

scoped的原理

vue中的scoped 通过在DOM结构以及css样式上加唯一不重复的标记:data-v-hash的方式,以保证唯一(而这个工作是由过PostCSS转译实现的),达到样式私有化模块化的目的。

总结一下scoped三条渲染规则:

  1.给HTML的DOM节点加一个不重复data属性(形如:data-v-123)来表示他的唯一性
  2.在每句css选择器的末尾(编译后的生成的css语句)加一个当前组件的data属性选择器(如[data-v-123])来私有化样式
  3.如果组件内部包含有其他组件,只会给其他组件的最外层标签加上当前组件的data属性

PostCSS会给一个组件中的所有dom添加了一个独一无二的动态属性data-v-xxxx,然后,给CSS选择器额外添加一个对应的属性选择器来选择该组件中dom,这种做法使得样式只作用于含有该属性的dom——组件内部dom, 从而达到了'样式模块化'的效果.

案例修改Element ui Input样式

发现没有生效

 

 

 

如果不写Scoped 就没问题

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

 

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

 

 

 

 

Vue3 第三十三章(css Style完整新特性)

上一章已经讲过了:deep(),其实还有两个选择器可以补充

1.插槽选择器

A 组件定义一个插槽 

<template>
    <div>
        我是插槽
        <slot></slot>
    </div>
</template>
 
<script>
export default {}
</script>
 
<style scoped>
 
</style>

在App.vue 引入

<template>
    <div>
        <A>
            <div class="a">私人定制div</div>
        </A>
    </div>
</template>
 
<script setup>
import A from "@/components/A.vue"
</script>
 
 
<style lang="less" scoped>
</style>

在A组件修改class a 的颜色

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

无效果

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

解决方案  slotted

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

 

2.全局选择器

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

<style module> 标签会被编译为 CSS Modules 并且将生成的 CSS 类作为 $style 对象的键暴露给组件

<template>
    <div :class="$style.red">
        小满是个弟弟
    </div>
</template>
 
<style module>
.red {
    color: red;
    font-size: 20px;
}
</style>

自定义注入名称(多个可以用数组)

你可以通过给 module attribute 一个值来自定义注入的类对象的 property 键

<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() 和 <script setup> 中使用。对于使用了自定义注入名称的 <style module> 模块,useCssModule 接收一个对应的 module attribute 值作为第一个参数

<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 第三十四章(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 以及其它依赖项

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 引入

 

 

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

 

Vue3 第三十五章(Evnet Loop 和 nextTick)

在我们学习nextTick 之前需要先了解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如此形成循环。

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 给每一个队列创建了微任务

 

Vue3第三十六章(Vue如何开发移动端)

如果使用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>小满{{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>

 

 

 

  

 基本适配百分之99的屏幕

如何将我们的Vue 项目打包成App(会安卓的可以跳过)

1.安装JDK Java Downloads | Oracle

配置环境变量

JAVA_HOME

 

  CLASSPATH (下面的照抄就行)

.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;

 最后一个Path

添加 %JAVA_HOME%\bin

 

2.安卓编辑器下载地址(建议不然很慢)https://developer.android.com/

一直next 就行了 然后磁盘选一下 安装就可以了 

装完启动需要还需要安装sdk 然后就可以打开了

新建一个空项目选这个就可以了

 

 

 

 会java选java 会别的就选别的

 

 等待加载完成

 

 打开之后可以创建一个虚拟机 

 

 创建完成之后就可以运行安卓项目

 

  其实就是一个虚拟手机

 

 切换成代码模式

 

 修改成以下代码

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:orientation="vertical"
    android:layout_height="match_parent">
 
    <WebView
        android:id="@+id/web_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout  >

 其实就是类似于我们前端的display felx 

 

  webView 就和小程序的webView 基本一样 套网页的

match_parent表示让当前控件的大小和父布局的大小一样,也就是让父布局来决定当前控件的大小

 

 java 代码逻辑

package com.example.myapplication;
 
import androidx.appcompat.app.AppCompatActivity;
 
import android.os.Bundle;
 
import android.webkit.WebView;
 
import android.app.Activity;
 
import android.webkit.WebViewClient;
 
public class MainActivity extends Activity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //设置一个Activity的显示界面,
        setContentView(R.layout.activity_main);
 
        WebView view = (WebView)findViewById(R.id.web_view);
        //设置 WebView 属性,能够执行 Javascript 脚本
        view.getSettings().setJavaScriptEnabled(true);
        //加载需要显示的网页 不能使用局域网地址 只能虚拟机专属地址 http://10.0.2.2 端口是我们vue 项目端口
        view.loadUrl("http://10.0.2.2:3000");
 
        view.setWebViewClient(new WebViewClient());
    }
}
加载需要显示的网页 不能使用局域网地址 只能虚拟机专属地址 http://10.0.2.2 端口是我们vue 项目端口

 

 配置权限

 

 

 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.myapplication">
 
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApplication"
        android:usesCleartextTraffic="true"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
</manifest>

你就可以看到我们的项目了

最后打包

打包的时候路径记得换成线上的服务器地址

 

 

 

  安卓手机实测

Vue3第三十七章(unocss原子化)

重新构想原子化CSS - 知乎

什么是css原子化?

CSS原子化的优缺点

1.减少了css体积,提高了css复用

2.减少起名的复杂度

3.增加了记忆成本 将css拆分为原子之后,你势必要记住一些class才能书写,哪怕tailwindcss提供了完善的工具链,你写background,也要记住开头是bg

接入unocss

tips:最好用于vite webpack属于阉割版功能很少

安装 

npm i -D unocss

vite.config.ts

import unocss from 'unocss/vite'
 
 plugins: [vue(), vueJsx(),unocss({
      rules:[
        
      ]
  })],

main.ts 引入 

import 'uno.css'

配置静态css

rules: [
  ['flex', { display: "flex" }]
]

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

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

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

 

 shortcuts 可以自定义组合样式

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

 

 unocss 预设

 presets:[presetIcons(),presetAttributify(),presetUno()]

1.presetIcons Icon图标预设

图标集合安装

npm i -D @iconify-json/ic

首先我们去icones官网(方便浏览和使用iconify)浏览我们需要的icon,比如这里我用到了Google Material Icons图标集里面的baseline-add-circle图标

<div  class="i-ic-baseline-backspace text-3xl bg-green-500" />

2.presetAttributify 属性化模式支持

属性语义化 无须class

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

 

 

3.presetUno 工具类预设

默认的 @unocss/preset-uno 预设(实验阶段)是一系列流行的原子化框架的 通用超集,包括了 Tailwind CSS,Windi CSS,Bootstrap,Tachyons 等。

例如,ml-3(Tailwind),ms-2(Bootstrap),ma4(Tachyons),mt-10px(Windi CSS)均会生效

 

Vue3第三十八章(函数式编程,h函数)

之前跟大家介绍了两种vue编写风格分别是template模板方式,和JSX方式感觉JSX被大家吐槽的很厉害,其实用习惯还挺好用的今天介绍第三种函数式编程

主要会用到h函数

h 接收三个参数

  • type 元素的类型
  • propsOrChildren 数据对象, 这里主要表示(props, attrs, dom props, class 和 style)
  • children 子节点  

h函数拥有多种组合方式

// 除类型之外的所有参数都是可选的
h('div')
h('div', { id: 'foo' })
 
//属性和属性都可以在道具中使用
//Vue会自动选择正确的分配方式
h('div', { class: 'bar', innerHTML: 'hello' })
 
// props modifiers such as .prop and .attr can be added
// with '.' and `^' prefixes respectively
h('div', { '.name': 'some-name', '^width': '100' })
 
// class 和 style 可以是对象或者数组
h('div', { class: [foo, { bar }], style: { color: 'red' } })
 
// 定义事件需要加on 如 onXxx
h('div', { onClick: () => {} })
 
// 子集可以字符串
h('div', { id: 'foo' }, 'hello')
 
//如果没有props是可以省略props 的
h('div', 'hello')
h('div', [h('span', 'hello')])
 
// 子数组可以包含混合的VNode和字符串
h('div', ['hello', h('span', 'hello')])

使用props传递参数

<template>
    <Btn text="按钮"></Btn>
</template>
  
<script setup lang='ts'>
import { h, } from 'vue';
type Props = {
    text: string
}
const Btn = (props: Props, ctx: any) => {
    return h('div', {
        class: 'p-2.5 text-white bg-green-500 rounded shadow-lg w-20 text-center inline m-1',
 
    }, props.text)
}
</script>

接受emit

<template>
    <Btn @on-click="getNum" text="按钮"></Btn>
</template>
  
<script setup lang='ts'>
import { h, } from 'vue';
type Props = {
    text: string
}
const Btn = (props: Props, ctx: any) => {
    return h('div', {
        class: 'p-2.5 text-white bg-green-500 rounded shadow-lg w-20 text-center inline m-1',
        onClick: () => {
            ctx.emit('on-click', 123)
        }
    }, props.text)
}
 
const getNum = (num: number) => {
    console.log(num);
}
</script>

定义插槽

<template>
    <Btn @on-click="getNum">
        <template #default>
            按钮slots
        </template>
    </Btn>
</template>
  
<script setup lang='ts'>
import { h, } from 'vue';
type Props = {
    text?: string
}
const Btn = (props: Props, ctx: any) => {
    return h('div', {
        class: 'p-2.5 text-white bg-green-500 rounded shadow-lg w-20 text-center inline m-1',
        onClick: () => {
            ctx.emit('on-click', 123)
        }
    }, ctx.slots.default())
}
 
const getNum = (num: number) => {
    console.log(num);
}
</script>

Vue3第三十九章(Vue开发桌面程序Electron)

建议视频教程小满Vue3(第三十九章 electron桌面程序)_哔哩哔哩_bilibili

Electron官网Electron | Build cross-platform desktop apps with JavaScript, HTML, and CSS.

我们用的VsCode 也是 electron 开发的

 

  electron 内置了 Chromium 和 nodeJS 其中 Chromium 是渲染进程 主要渲染和解析HTML,Nodejs作为主进程,其中管道用IPC 通信

1.使用vite 构建 electron项目

创建一个vite 项目

npm init vite@latest

 安装electron

npm install electron -D
npm install vite-plugin-electron -D

根目录新建 electron / index.ts

 

修改vite.config.ts 配置文件

引入electron插件配置main  entry对应electron的文件

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import electron from 'vite-plugin-electron'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(), electron({
    main: {
      entry: "electron/index.ts"
    }
  })]
})

 

 编写代码 electron / index.ts

import { app, BrowserWindow } from 'electron'
import path from 'path'
//app 控制应用程序的事件生命周期。
//BrowserWindow 创建并控制浏览器窗口。
 
let win: BrowserWindow | null;
//定义全局变量获取 窗口实例
 
const createWindow = () => {
    win = new BrowserWindow({
        //
        webPreferences: {
            devTools: true,
            contextIsolation: false,
            nodeIntegration: true
            //允许html页面上的javascipt代码访问nodejs 环境api代码的能力(与node集成的意思)
        }
    })
    if (app.isPackaged) {
        win.loadFile(path.join(__dirname, "../index.html"));
    } else {
//VITE_DEV_SERVER_HOST 如果是undefined 换成  VITE_DEV_SERVER_HOSTNAME
        win.loadURL(`http://${process.env['VITE_DEV_SERVER_HOST']}:${process.env['VITE_DEV_SERVER_PORT']}`)
    }
}
//isPackage 不好使换下面的
  //  if(process.env.NODE_ENV != 'development'){
      //  win.loadFile(path.join(__dirname, "../index.html"));
  //  }else{
        //win.loadURL(`http://${process.env['VITE_DEV_SERVER_HOSTNAME']}:${process.env['VITE_DEV_SE//RVER_PORT']}`)
   // }
//在Electron完成初始化时被触发
app.whenReady().then(createWindow)

配置package json 增加main 字段 type 去掉

{
  "name": "electron-vite",
  "private": true,
  "version": "0.0.0",
  "main": "dist/electron/index.js",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc --noEmit && vite build  &&  electron-builder",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.2.37"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^3.0.0",
    "electron": "^19.0.10",
    "electron-builder": "^23.1.0",
    "typescript": "^4.6.4",
    "vite": "^3.0.0",
    "vite-plugin-electron": "^0.8.3",
    "vue-tsc": "^0.38.4"
  }
}

npm run dev

 

2.打包Electron

需要安装electron-builder

npm install electron-builder -D

package json 配置 build 修改npm run build命令

"build": "vue-tsc --noEmit && vite build  &&  electron-builder",
  "build": {
    "appId": "com.electron.desktop",
    "productName": "electron",
    "asar": true,
    "copyright": "Copyright © 2022 electron",
    "directories": {
      "output": "release/"
    },
    "files": [
      "dist"
    ],
    "mac": {
      "artifactName": "${productName}_${version}.${ext}",
      "target": [
        "dmg"
      ]
    },
    "win": {
      "target": [
        {
          "target": "nsis",
          "arch": [
            "x64"
          ]
        }
      ],
      "artifactName": "${productName}_${version}.${ext}"
    },
    "nsis": {
      "oneClick": false,
      "perMachine": false,
      "allowToChangeInstallationDirectory": true,
      "deleteAppDataOnUninstall": false
    },
    "publish": [
      {
        "provider": "generic",
        "url": "http://127.0.0.1:8080"
      }
    ],
    "releaseInfo": {
      "releaseNotes": "版本更新的具体内容"
    }
  }

nsis 配置详解

{"oneClick": false, // 创建一键安装程序还是辅助安装程序(默认是一键安装)
    "allowElevation": true, // 是否允许请求提升,如果为false,则用户必须使用提升的权限重新启动安装程序 (仅作用于辅助安装程序)
    "allowToChangeInstallationDirectory": true, // 是否允许修改安装目录 (仅作用于辅助安装程序)
    "installerIcon": "public/timg.ico",// 安装程序图标的路径
    "uninstallerIcon": "public/timg.ico",// 卸载程序图标的路径
    "installerHeader": "public/timg.ico", // 安装时头部图片路径(仅作用于辅助安装程序)
    "installerHeaderIcon": "public/timg.ico", // 安装时标题图标(进度条上方)的路径(仅作用于一键安装程序)
    "installerSidebar": "public/installerSiddebar.bmp", // 安装完毕界面图片的路径,图片后缀.bmp,尺寸164*314 (仅作用于辅助安装程序)
    "uninstallerSidebar": "public/uninstallerSiddebar.bmp", // 开始卸载界面图片的路径,图片后缀.bmp,尺寸164*314 (仅作用于辅助安装程序)
    "uninstallDisplayName": "${productName}${version}", // 控制面板中的卸载程序显示名称
    "createDesktopShortcut": true, // 是否创建桌面快捷方式
    "createStartMenuShortcut": true,// 是否创建开始菜单快捷方式
    "shortcutName": "SHom", // 用于快捷方式的名称,默认为应用程序名称
    "include": "script/installer.nsi",  // NSIS包含定制安装程序脚本的路径,安装过程中自行调用  (可用于写入注册表 开机自启动等操作)
    "script": "script/installer.nsi",  // 用于自定义安装程序的NSIS脚本的路径
    "deleteAppDataOnUninstall": false, // 是否在卸载时删除应用程序数据(仅作用于一键安装程序)
    "runAfterFinish": true,  // 完成后是否运行已安装的应用程序(对于辅助安装程序,应删除相应的复选框)
    "menuCategory": false, // 是否为开始菜单快捷方式和程序文件目录创建子菜单,如果为true,则使用公司名称
}

npm run build

 

2.打包Electron

需要安装electron-builder

npm install electron-builder -D

package json 配置 build 修改npm run build命令

"build": "vue-tsc --noEmit && vite build  &&  electron-builder",
  "build": {
    "appId": "com.electron.desktop",
    "productName": "electron",
    "asar": true,
    "copyright": "Copyright © 2022 electron",
    "directories": {
      "output": "release/"
    },
    "files": [
      "dist"
    ],
    "mac": {
      "artifactName": "${productName}_${version}.${ext}",
      "target": [
        "dmg"
      ]
    },
    "win": {
      "target": [
        {
          "target": "nsis",
          "arch": [
            "x64"
          ]
        }
      ],
      "artifactName": "${productName}_${version}.${ext}"
    },
    "nsis": {
      "oneClick": false,
      "perMachine": false,
      "allowToChangeInstallationDirectory": true,
      "deleteAppDataOnUninstall": false
    },
    "publish": [
      {
        "provider": "generic",
        "url": "http://127.0.0.1:8080"
      }
    ],
    "releaseInfo": {
      "releaseNotes": "版本更新的具体内容"
    }
  }

nsis 配置详解

{"oneClick": false, // 创建一键安装程序还是辅助安装程序(默认是一键安装)
    "allowElevation": true, // 是否允许请求提升,如果为false,则用户必须使用提升的权限重新启动安装程序 (仅作用于辅助安装程序)
    "allowToChangeInstallationDirectory": true, // 是否允许修改安装目录 (仅作用于辅助安装程序)
    "installerIcon": "public/timg.ico",// 安装程序图标的路径
    "uninstallerIcon": "public/timg.ico",// 卸载程序图标的路径
    "installerHeader": "public/timg.ico", // 安装时头部图片路径(仅作用于辅助安装程序)
    "installerHeaderIcon": "public/timg.ico", // 安装时标题图标(进度条上方)的路径(仅作用于一键安装程序)
    "installerSidebar": "public/installerSiddebar.bmp", // 安装完毕界面图片的路径,图片后缀.bmp,尺寸164*314 (仅作用于辅助安装程序)
    "uninstallerSidebar": "public/uninstallerSiddebar.bmp", // 开始卸载界面图片的路径,图片后缀.bmp,尺寸164*314 (仅作用于辅助安装程序)
    "uninstallDisplayName": "${productName}${version}", // 控制面板中的卸载程序显示名称
    "createDesktopShortcut": true, // 是否创建桌面快捷方式
    "createStartMenuShortcut": true,// 是否创建开始菜单快捷方式
    "shortcutName": "SHom", // 用于快捷方式的名称,默认为应用程序名称
    "include": "script/installer.nsi",  // NSIS包含定制安装程序脚本的路径,安装过程中自行调用  (可用于写入注册表 开机自启动等操作)
    "script": "script/installer.nsi",  // 用于自定义安装程序的NSIS脚本的路径
    "deleteAppDataOnUninstall": false, // 是否在卸载时删除应用程序数据(仅作用于一键安装程序)
    "runAfterFinish": true,  // 完成后是否运行已安装的应用程序(对于辅助安装程序,应删除相应的复选框)
    "menuCategory": false, // 是否为开始菜单快捷方式和程序文件目录创建子菜单,如果为true,则使用公司名称
}

npm run build

 

 

 

3.Electron Vscode 输出乱码解决 方案

dev 的时候 加上chcp 65001 输出中文

 "dev": "chcp 65001 && vite",

 

 加上之后

 4.渲染进程和主进程通信

vite.config.ts 需要修改 不然会报一个错Error: Module "path" has been externalized for browser compatibility. Cannot

只要安装了 vite-plugin-electron 就会带上 vite-plugin-electron-renderer 直接引入用就行

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import electron from 'vite-plugin-electron'
import electronRender from 'vite-plugin-electron-renderer'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(), electron({
    main: {
      entry: "electron/index.ts"
    }
  }),electronRender()],
  build:{
    emptyOutDir: false,
  }
})

渲染进程使用ipcRenderer 发送

import { ipcRenderer } from 'electron'
 
const open = () => {
     ipcRenderer.send('openFlyCar')
}

主进程使用 ipcMain 接受

ipcMain.on('openFlyCar',()=>{
    console.log('收到')
})

主进程通知渲染进程

const  win = new BrowserWindow(xxxxx)
win!.webContents.send('load', { message: "electron初始化了" })

渲染进程接受

ipcRenderer.on('load',(_,data)=>{
  console.log(data)
})

5.更多配置查看该插件

vite-plugin-electron: Vite plugin for electron-builder

 

Vue3第四十章(Vue响应性语法糖)

小提示 本章内容所讲的东西都是实验性的产物 暂时不要再生产环境使用,自己开发玩可以使用,不过大体框架应该不会变了。

要求 vue版本 3.2.25 及以上

1.开启配置(开启之后才能使用新特性)

vite 开启 reactivityTransform

import { fileURLToPath, URL } from 'url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
// https://vitejs.dev/config/
export default defineConfig({
  server: {
    port: 3000
  },
  plugins: [
    vue({
      reactivityTransform:true
    }),
   vueJsx()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
})

 如果你是 vue-cli

// vue.config.js
module.exports = {
  chainWebpack: (config) => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap((options) => {
        return {
          ...options,
          reactivityTransform: true
        }
      })
  }
}

 第一个例子 $ref

在之前ref 修改值 和 获取值 都要.value 一下 感觉很繁琐,不想用.value 我们可以使用vue3的新特性$ref 。

我们可以直接使用$ref 宏函数 就不需要.value 了。能帮我们快速书写,但是宏函数是基于运行时的他最终还是会转换成ref 加.value  只不过vue帮我们做了这个操作了

<template>
    <div>
        <button @click="add">add</button>
    </div>
    <h2>
        {{count}}
    </h2>
</template>
    
<script setup lang='ts'>
import { $ref } from 'vue/macros'
let count = $ref(0)
 
const add = () => {
   count++
}
</script>
    
<style>
</style>

当然跟ref 有关的函数都做处理 都不需要.value了

2.$ref 的弊端

应为他编译之后就是 count.value 并不是一个ref对象所以watch 无法监听而且会抛出一个警告

[Vue warn]: Invalid watch source:  0 A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types. 
  at <App>
<template>
</template>
    
<script setup lang='ts'>
import { reactive, ref, toRefs,watch } from 'vue';
import { $ref} from 'vue/macros'
 
let count = $ref<number>(0) 
watch(count,(v)=>{
    console.log(v)
}) 
 
setInterval(()=>{
    count++
},1000) 
 
</script>
    
<style>
</style>

解决这个问题需要$$ 符号 就是再让他编译的时候变成一个ref 对象不加.value 

<template>
</template>
    
<script setup lang='ts'>
import { reactive, ref, toRefs,watch } from 'vue';
import { $ref,$$ } from 'vue/macros'
 
let count = $ref<number>(0)
 
watch($$(count),(v)=>{
    console.log(v)
})
 
 
setInterval(()=>{
    count++
},1000)
 
 
</script>
    
<style>
</style>

 

3.解构 

在之前我们解构一个对象使用toRefs 解构完成之后 获取值和修改值 还是需要.value

vue3 也提供了 语法糖  $() 解构完之后可以直接赋值

<template>
    <div>
        {{name}}
    </div>
</template>
    
<script setup lang='ts'>
import { reactive, toRefs } from 'vue'
import {$} from 'vue/macros'
const obj = reactive({
    name: '小满'
})
 
let { name } = $(obj);
 
 
setTimeout(()=>{
   name = '大满'
},2000)
 
</script>
    
<style>
</style>

Vue3第四十一章(docker 碰撞 vue3)

 我们来看一下docker(容器) 可以做什么解决了什么问题

如下案例 

领导要求你维护四个项目 每个项目 node版本不同  框架不同 是不是非常头疼 docker可以 帮你解决这个问题

 1.安装docker

Developers - Docker

选择你对应的操作系统

 

 如果你是windows 出现这种情况就是有问题 或者wsl2 报错

 

 

  需要安装wsl  或者更新 wsl

powerShell打开 
 
wsl --install
 
wsl --update

启动docker错误System. InvalidOperationException Failed to set version to docker-desktop: exit code: -1

如果报这个错 执行下面的命令

netsh winsock reset

重启docker 就好了

 出现这个安装成功或者 输入以下命令检查

 

docker 分几个概念:镜像、容器、仓库

镜像:就是像是我们装机时候需要的系统盘或者系统镜像文件,这里它负责创建docker容器的,有很多官方现成的镜像:node、mysql、monogo、nginx可以从远程仓库下载

容器:容器特别像一个虚拟机,容器中运行着一个完整的操作系统。可以在容器中装 Nodejs,可以执行npm install,可以做一切你当前操作系统能做的事情

仓库:仓库就像是github那样的,我们可以制作镜像然后push 提交到云端的仓库,也可以从仓库 pull 下载镜像

2.配置docker 的 国内镜像

"registry-mirrors": [
        "http://hub-mirror.c.163.com",
        "https://docker.mirrors.ustc.edu.cn",
        "https://registry.docker-cn.com"
    ],

 

修改完重启docker 生效

3.创建一个vue3 的项目

npm init vue

 vite config ts 开启网络访问

export default defineConfig({
  plugins: [vue(), vueJsx()],
  server:{
     host:"0.0.0.0"
  },
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

Vue3第四十二章(环境变量)

环境变量:他的主要作用就是让开发者区分不同的运行环境,来实现 兼容开发和生产

例如 npm run dev 就是开发环境 npm run build 就是生产环境等等

Vite 在一个特殊的 import.meta.env 对象上暴露环境变量。这里有一些在所有情况下都可以使用的内建变量

{
"BASE_URL":"/", //部署时的URL前缀
"MODE":"development", //运行模式
"DEV":true,"  //是否在dev环境
PROD":false, //是否是build 环境
"SSR":false //是否是SSR 服务端渲染模式
}

需要注意的一点就是这个环境变量不能使用动态赋值import.meta.env[key] 应为这些环境变量在打包的时候是会被硬编码的通过JSON.stringify 注入浏览器的

配置额外的环境变量

在根目录新建env 文件 可以创建多个

如下  env.[name]

 修改启动命令

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

 配置智能提示

interface ImportMetaEnv {
    VITE_XIAOMAN:string
}

 

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

就已经添加进去了

生产环境使用

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

 

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

 

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 等 

Vue3第四十三章(webpack 构建 Vue3项目)

为了加深我们对webpack 的了解方便以后灵活运用webpack 的技术

1.初始化项目结构(跟cli 结构保持一致)

 2.安装所需要的依赖包

{
    "name": "webpack-vue",
    "version": "1.0.0",
    "description": "",
    "main": "webpack.config.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "dev": "webpack-dev-server",
        "build": "webpack"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "dependencies": {
        "@vue/compiler-sfc": "^3.2.38", //解析vue文件
        "clean-webpack-plugin": "^4.0.0", //打包 的时候清空dist
        "css-loader": "^6.7.1", //处理css文件
        "friendly-errors-webpack-plugin": "^1.7.0", //美化dev
        "html-webpack-plugin": "^5.5.0", //html 模板
        "less": "^4.1.3",  //处理less
        "less-loader": "^11.0.0", //处理less文件
        "style-loader": "^3.3.1", //处理style样式
        "ts-loader": "^9.3.1", //处理ts
        "typescript": "^4.8.2", //ts
        "vue": "^3.2.38", //vue
        "vue-loader": "^17.0.0", //解析vue
        "webpack": "^5.74.0",
        "webpack-cli": "^4.10.0",
        "webpack-dev-server": "^4.10.0"
    }
}

 

npm i webpack
npm i webpack-cli
npm i webpack-dev-server
npm i html-webpack-plugin

 

 

 

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
  }

  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,
    },
    externals: {
        vue: "Vue" //CDN 引入
    },
}
 
 
module.exports = config

看效果

 

 打包报错:查看2.安装依赖包

npm i vue-loader@next
npm i @vue/compiler-sfc

npm i clean-webpack-plugin //在webpack.config.js中配置,plugins中new CleanWebpackPlugin()
npm i css-loader
npm i style-loader

Vue3第四十四章(Vue3 性能优化)

性能优化

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

  

参数介绍

从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按钮。 

代码分析

由于我们使用的是vite vite打包是基于rollup 的我们可以使用 rollup 的插件

npm install rollup-plugin-visualizer

vite.config.ts 配置  记得设置open 不然无效

import { visualizer } from 'rollup-plugin-visualizer';
plugins: [vue(), vueJsx(),visualizer({
      open:true
 })],

然后进行npm run build打包

 我在项目中使用了 Ant Design 发现 这个UI 库非常庞大 这时候 就可以使用 Ant Design 的按需引入减少 包大小

Vite 配置优化

build:{
       chunkSizeWarningLimit:2000,
       cssCodeSplit:true, //css 拆分
       sourcemap:false, //不生成sourcemap
       minify:false, //是否禁用最小化混淆,esbuild打包速度最快,terser打包体积最小。
       assetsInlineLimit:5000 //小于该值 图片将打包成Base64 
},

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 //缓存有效期
 
                }
              }
            }
          ]
      },
    })
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

 

其他性能优化

图片懒加载 

import lazyPlugin from 'vue3-lazy'

<img v-lazy="user.avatar" >

虚拟列表

 

 多线程 使用  new Worker 创建

worker脚本与主进程的脚本必须遵守同源限制。他们所在的路径协议、域名、端口号三者需要相同

const myWorker1 = new Worker("./calcBox.js");

都使用postMessage发送消息

worker.postMessage(arrayBuffer, [arrayBuffer]);

都使用onmessage接收消息

self.onmessage = function (e) {
 // xxx这里是worker脚本的内容
};

关闭

worker.terminate();    

VueUse 库已经集成了 webWorker

防抖节流

同样VueUse 也是集成了

 

Vue3第四十五章(Vue3 Web Components)

什么是 Web Components

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

Custom elements(自定义元素):JavaScript API,允许定义custom elements及其行为,然后可以在我们的用户界面中按照需要使用它们。

Shadow DOM(影子DOM):JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,开发者可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。

HTML templates(HTML模板):和元素使开发者可以编写与HTML结构类似的组件和样式。然后它们可以作为自定义元素结构的基础被多次重用。

京东的跨端框架 Taro 的组件部分,就是用基于 Web Components 开发的 

1.实战案例

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)

2.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>

3.如何在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>

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

 

 

 

Vue3第四十六章(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接受返回的参数

function clickButton() {
    let obj, s
    obj = { "table":"products", "limit":10 }; //添加参数
    s =  document.createElement("script"); //动态创建script
    s.src = "接口地址xxxxxxxxxxxx"  + JSON.stringify(obj);
    document.body.appendChild(s);
 }
//与后端定义callback名称
function myFunc(myObj)  {
    //接受后端返回的参数
    document.getElementById("demo").innerHTML = myObj;
}

cors 设置 CORS 允许跨域资源共享 需要后端设置

{
  "Access-Control-Allow-Origin": "http://web.xxx.com" //可以指定地址
}
{
  "Access-Control-Allow-Origin": "*" //也可以使用通配符 任何地址都能访问 安全性不高
}

使用Vite proxy 或者 node代理 或者 webpack proxy 他们三种方式都是代理

我们先创建一个接口使用express简单构建一下

const express = require('express')
const app = express()
 
//创建get请求
app.get('/xm',(req,res)=>{
     res.json({
        code:200,
        message:"请求成功"
     })
})
//端口号9001
app.listen(9001)

 

 

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

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

 发现是存在跨域的,这时候我们就可以配合vite的代理来解决跨域 用法如下

需要在vite.config.js/ts 进行配置

export default defineConfig({
  plugins: [vue()],
  server:{
     proxy:{
        '/api':{
            target:"http://localhost:9001/", //跨域地址
            changeOrigin:true, //支持跨域
            rewrite:(path) => path.replace(/^\/api/, "")//重写路径,替换/api
        }
     }
  }
})

etch 替换/api 他会截取/api 替换成 target地址

<script lang="ts" setup>
import {ref,reactive } from 'vue'
fetch('/api/xm')
</script>

webpack proxy 和 node proxy 用法都类似

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

 

 

posted @ 2022-12-06 11:18  爵岚  阅读(1248)  评论(0编辑  收藏  举报