TypeScript与Vue3的使用
一、构建项目
1、使用vite构建
-
介绍:
官方文档:https://cn.vuejs.org/guide/quick-start.html#vite
vite官网:https://vitejs.cn/
- 新一代前端构建工具
- 优势:
- 开发环境中,无需打包操作,可快速的冷启动
- 轻量快速的热重载(HMR:hot module replacement)简述:改代码就刷新
- 真正的按需编译,不再等待整个应用编译完成
- webpack传统构建与 vite构建对比
-
方式一:
// 输入命令。下载依赖
npm init vite@latest
-
方式二
## 创建工程 npm init vite-app 项目名 ## 进入工程目录 cd 项目名 ## 安装依赖 npm install ## 运行 npm run dev
2、使用用Vue脚手架构建
// 输入命令。下载依赖 npm init vue@latest

3、在vscode中使用
需要装两个插件,提供智能提示等。
注意:如果装过Vue2的Vetur,需要禁用不然可能会出现冲突。出现下面报错
4、在vue3中使用ts
安装:
vue add typescript
//
![]()
5、通过webpack手动搭建Vue3
- 建立项目基本结构
- npm init -y 建立 package.json文件
- tsc --init 生产 tsconfig.json文件。如果没有tsc,安装 npm install typescript -g
- 建立 webpack.config.js 配置文件。需安装以下依赖,并在webpack.config.js进行配置
npm i vue
// webpack-cli 只有3以上的版本才需要
npm i webpack webpack-cli
// 生产HTML页面自动引入资源
npm i html-webpack-plugin
// 开发服务器,在内存中编译打包
npm i webpack-dev-server
修改package.json文件"scripts": {"test": "echo \"Error: no test specified\" && exit 1","dev": "webpack-dev-server","build": "webpack"},const { Configuration } = require('webpack') // 智能提示const path = require('path')const htmlWebpackPlugin = require('html-webpack-plugin')/** 配置智能提示* @type {Configuration}*/const config = {entry: "./src/main.ts",output: {filename: '[hash].js',path: path.resolve(__dirname, 'dist')},module: {rules: []},plugins: [new htmlWebpackPlugin({template: path.resolve(__dirname, "public/index.html")})],mode: "development"}module.exports = config - 注意。ts是识别不了 .vue后缀,需要新建文件进行扩充
出现上面错误,需要在src目录下,新建 env.d.ts文件 // 用来处理识别不了vue后缀的问题 declare module "*.vue" { import { DefineComponent } from "vue" const component: DefineComponent<{}, {}, any> export default component }
- 现在打包运行后会出现以下报错,需要下载两个loader才能正常解析模板
npm i vue-loader@next
npm i @vue/compiler-sfc
还需要在webpack.config.js 文件中进行以下配置
const { VueLoaderPlugin } = require('vue-loader/dist/index')module.exports = {module: {rules: [{test:/\.vue$/,use:"vue-loader"}]},plugins: [new VueLoaderPlugin()],}打包运行后现在还有可能出现以下报错。出现原因是 - 在 webpack.config.js 中配置别名和 自动补全后缀名
module.exports = { resolve:{ alias:{ // 配置别名 '@':path.resolve(__dirname,'src') }, extensions:['.vue','.ts','.js'] // 自动补全后缀 }, }
- 安装ts,和ts-loader
npm i typescript ts-loader -D module.exports = { module: { rules: [ { test: /\.ts$/, //解析ts loader: "ts-loader", options: { configFile: path.resolve(process.cwd(), 'tsconfig.json'), appendTsSuffixTo: [/\.vue$/] }, } ] }, }
- 优化命令行的显示日志
// 安装依赖 npm i friendly-errors-webpack-plugin -D // 在webpack.config.js 进行配置 const FriendlyErrorsWebpackPlugin = require("friendly-errors-webpack-plugin"); module.exports = { stats:"errors-only", // 清空命令行日志 plugins:[ new FriendlyErrorsWebpackPlugin({ compilationSuccessInfo: { //美化样式 messages: ['You application is running here http://localhost:9001'] } }) ] }
-
使用CDN引入减少打包体积,减小服务器压力。但对用户网速有要求
// webpack.config.js module.exports = { // 这样打包,vue就不会打包进入了。 externals:{ // 采用CDN引入方法:使用script链接一个CDN地址(官网有) vue:"Vue" }, } // 在index.html中引入CDN地址。【地址一般官网都有】
二、Vue2和Vue3的区别
1、模板语法
<div>{{ bool ? 'one' : 'two' }}</div> const bool: number = 0 <div :class="[flag ? '样式一' : '样式二']">老六</div> const flag: boolean = true <div :class="cls">老六2</div> type clss = { a: boolean, b: boolean } // 若两个都为true,则取后面的 const cls: clss = { a: true, b: true } <div :key="item" v-for="item in Arr">{{ item }}</div> const Arr: Array<any> = [{ name: "1" }, { name: "2" }, { name: "3" }]
2、区别
- 性能的提升:(打包大小减少41%、初次渲染快55%、内容减少54%、.......)
- 源码的升级(使用Proxy代替defineProperty实现响应式、重写虚拟DOM的实现和Tree-Shaking、......)
- 更好的支持TypeScript
- 新特性
- Composition API(组合API)
- setup 配置
- ref 与 reactive
- watch 与 watchEffect
- provide 与 inject
- ..........
- 新的内置组件
- Fragment:可以存在多个根标签。都会包裹这个Fragment片段中,是虚拟对象
- Teleport
- Suspense
- 其它改变
-
- 新的生命周期钩子
- data 选项应始终被声明为一个函数(必须是函数)
- 移除keyCode 支持做为 v-on 的修饰符。同时不在支持 config.keyCodes
- 移除 v-on.native 修饰符。(在Vue2中给组件添加事件是不能触发的)
/* 在Vue2中给父组件中引入的子组件添加事件,触发不了。需要通过 .native修饰符,才能触发 */ <Footer @click.native="cs" ></Footer> // 需要加上.native事件修饰符,才能正常触发 cs(){ console.log('点击事件’) } /* 在Vue3中 使用了emits配置项来替换native修饰符 */ // 父组件中绑定事件 <my-component v-on:close="handleComponentEvent" v-on:click="handleNativeClickEvent" /> // 子组件声明自定义事件,默认是原生事件,否则出现警告, (已修复,无需声明响应) <script> export default { emits: ['close'] // (已修复,无需声明响应) } </script>
- 移除了过滤器(filter)
- 过滤器虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是 “只是 JavaScript” 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。
- 在同一元素上使用,Vue2中v-for的优先级高于v-if。Vue3中v-if的优先级高于v-for
- Vue3使用template模板,里面的内容不显示时,可使用v-if来控制一下。(v-if="true")
- Vue3中的key值要写在template标签上面,而不是它下面的子元素上
<template v-for="el in list" :key="el"> <p> {{ el }} </p> </template>
- Vue3中提供了两种API风格,分别是选项式API(跟Vue2一样) 与 组合式API
选项式API对比组合式API 的局限性 ? 1、同一个业务逻辑相关的数据,方法等等,写在了不同的选项式选项中,当组件比较大的时候,代码的可读性就不好 2、相同的逻辑代码,很难在多个组件中进行利用;(所以在vue2中提供了一个混入) 3、别外Vue2对TS不太友好
3、Vue2中响应式原来和 Vue3中的响应式原理的对比
-
Vue2中的响应式:
-
- 对象类型:通过 Object.defineProperty(属性所在对象,属性的名字,描述对象)
的读取、修改进行拦截(数据劫持)let data={ name: '张三', age: 18 } Object.defineProperty(data,'name',{ // 下面 value属性与 writable属性不能与 get set同时存在 // value:19, //这个属性的数据值。默认值undefined // enumerable: true, //控制属性是否可以枚举。默认值false // writable:true, //控制属性是否可以被修改。默认值false // configurable:true, //控制属性是否可以被删除。默认值false //当有人读取person的name属性时,get函数(getter)被调用,返回值为name的值 get(){ console.log('被读取了'); return '老六' }, // 当有人修改data的name属性时,set函数(setter)被调用,收到修改后的具体的值 set(value){ console.log('被修改了',value); // a = value } })
- 数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)
- 存在问题:
(1)对象中新增属性、删除属性、是不具备响应式。需要使用Vue.set (2)数组中直接通过下标修改数组、是不具备响应式。只能
使用push()、pop()、shift()、unshift()、splice()、sort()、reverse()才有响应式
person: { name: "张三", age: 18, hobby:['学习','吃饭']}, // 数据可以添加,但页面检测不到, // this.person.sex = '女' 解决方案: set:新增或修改 delete:删除 // 1.使用组件身上的$set,页面可以测检到, this.$set(this.person, "sex", "女"); // 2.引入Vue,使用身上的set,页面也可以测检到, import Vue from 'vue' Vue.set(this.person,'sex','女') // 3.通过数组身上的方法,splice 直接改变原数组 this.person.hobby.splice(0,1,'打游戏')
- 对象类型:通过 Object.defineProperty(属性所在对象,属性的名字,描述对象)
-
- 手写响应式原理
//模拟Vue2中实现响应式 let person = { name: '张三', age: 18 } let p = {} Object.defineProperty(p, 'name', { get() { // 有人读取name时调用 return person.name }, set(value) { // 有人修改name时调用 console.log('有人修改了'); person.name = value } }) Object.defineProperty(p, 'age', { get() { // 有人读取name时调用 return person.age }, set(value) { // 有人修改name时调用 console.log('有人修改了'); person.name = value } })
- 手写响应式原理
-
Vue3中的响应式
-
- 通过 Proxy(代理):拦截对象中任意属性的变化,包括:属性值的读写、属性的添加、属性的删除等
- 通过 Reflect(反射):对源对象的属性进行操作
- 对Proxy 与 Reflect的描述
- 手写响应式原理
let person = { name: '张三', age: 18} // 模拟Vue3中实现响应式(完整) const p = new Proxy(person, { // target(形参):传递进来的这个对象 // propName(形参):读的对象中哪个属性,就是哪个 get(target, propName) { // 捕获读取的操作 console.log(`有人读取了p身上的${propName}`); return Reflect.get(target, propName) }, // target(形参):传递进来的这个对象 // propName(形参):读的对象中哪个属性,就是哪个 // value(形参):修改后的值 set(target, propName, value) { // 捕获修改,增加的操作 console.log(`有人修改了p身上的${propName},更新页`); Reflect.set(target, propName, value) }, // target(形参):传递进来的这个对象 // propName(形参):读的对象中哪个属性,就是哪个 deleteProperty(target, propName) { // 捕获删除的操作 console.log(`有人删除了p身上的${propName},更新页面`); return Reflect.deleteProperty(target, propName) } })
4、Vue3中v-model 与 Vue2区别
- prop:
value
-->modelValue
;/* 父子组件进行数据双向绑定:
Vue2中子组件用value接收
Vue3中子组件用modelValue接收
*/
// 父组件。传递值 <vModelVue v-model="isShow"></vModelVue> import vModelVue from './Vmodel.vue' // 导入子组件 const isShow = ref<boolean>(true) // 子组件。接收值 <div v-if="modelValue">盒子</div> const props = defineProps<{ // 接收父组件传递过来的数据 modelValue:boolean, }>() - 事件:
input
-->update:modelValue
;// 父组件 <vModelVue v-model="isShow"></vModelVue> import vModelVue from './Vmodel.vue' // 导入子组件 const isShow = ref<boolean>(true) // 子组件 <div @click="close">关闭</div> <div v-if="modelValue">盒子</div> const props = defineProps<{ // 接收父组件传递过来的数据 modelValue:boolean, }>() // 向父组件传值。固定写法【update: 父组件传递过来的数据】 const emit = defineEmits(['update:modelValue']) // 触发请求发送数据,把父组件的isShow改为false const close = ()=>{ emit('update:modelValue',false) }
v-bind
的.sync
修饰符和组件的model
选项已移除- 新增 支持多个v-model
// 父组件 <vModelVue v-model:textVal.isBt="text" v-model="isShow"></vModelVue> import vModelVue from './Vmodel.vue' // 导入子组件 const isShow = ref<boolean>(true) const text = ref<string>('小宋') // 子组件 <div @click="close">关闭</div> <div v-if="modelValue">盒子</div> <input :value="textVal" @input="change" type="text"> const props = defineProps<{ // 接收父组件传递过来的数据 modelValue:boolean, textVal:string, // 父组件,v-model后面的那个名字 }>() // 向父组件传值。固定写法【update: 父组件传递过来的数据】 const emit = defineEmits(['update:modelValue','update:textVal']) // 触发请求发送数据,把父组件的isShow改为false const close = ()=>{ emit('update:modelValue',false) } // 触发请求发送数据 const change = (e:Event)=>{ // 因为e.target是EventTarget或null,EventTarget拿不到.value。所以断言为 HTML Input。防止出现报错 const target = e.target as HTMLInputElement emit('update:textVal',target.value ) }
- 新增 支持自定义修饰符 Modifiers
// 父组件 <vModelVue v-model:textVal.isBt="text" ></vModelVue> import vModelVue from './Vmodel.vue' // 导入子组件 const text = ref<string>('小宋') // 子组件 <div v-if="modelValue">盒子</div> <input :value="textVal" @input="change" type="text"> const props = defineProps<{ // 接收父组件传递过来的数据 textVal:string, // textVal + Modifiers(自定义修饰符,固定写法) textValModifiers?:{ isBt:boolean } }>() // 向父组件传值。固定写法【update: 父组件传递过来的数据】 const emit = defineEmits(['update:textVal']) // 触发请求发送数据 const change = (e:Event)=>{ // 因为e.target是EventTarget或null类型,EventTarget拿不到.value。所以断言为 HTML Input。防止出现报错 const target = e.target as HTMLInputElement emit('update:textVal',props?.textValModifiers?.isBt ? target.value + '老六' : target.value) }
三、setup函数
理解:Vue3中的一个新的配置项,值为一个函数
setup 是所有 Composition API(组件API)“表演的舞台”
组件中所用到的:数据、方法等等、均要配置在 setup中。简述:像xue2中所有的钩子,属性,方法都放在里面
1、setup的两种返回值:
<template> <h1>一个人的信息</h1> <h2>姓名:{{name}}</h2> <h2>年龄:{{age}}</h2> <h2>性别:{{sex}}</h2> <button @click="sayHello">点击(vue3所配置的——sayHello)</button><br><br> <button @click="sayWelcome">点击(vue2所配置的——sayWelcome)</button><br><br> </template>
-
返回一个对象。
/* 则对象中的属性,方法可在模板上直接使用 */
export default { name: 'App', // 下面只是测试setup,暂时不考虑响应式的问题 setup(){ // 数据 let name = '张三' // 方法 function sayHello(){ alert(`我叫${name},我${age}岁了,你好啊!`) }// 返回一个对象 return{ name, sayHello,} }} -
返回一个渲染函数。
/* 则可以自定义渲染内容。(记得引入h函数) */
import {h} from 'vue' export default { name: 'App',// 下面只是测试setup,暂时不考虑响应式的问题 setup(){// 返回一个渲染函数 return ()=> h('h1','对对对对') } }
2、setup的注意点。
-
尽量不要与vue2 配置混用:
- Vue2配置中(data、methods、computed) 可以访问 Vue3中setup的属性、方法
- 但Vue3中setup的属性、方法,不能访问Vue2配置中的属性,方法
- 如果混用,出现重名,谁在最下面就只执行谁、
-
setup 不能是一个 async函数:
- 因为返回值不再是return的对象,而是promise,模板看不到return对象中的属性。(后期也可以返回一个Promise实例,但需要Suspense和异步组件的配合)
-
setup的执行时机:
- 在beforeCreate 之前执行一次,this 是 undefined,但在setup里面的函数中,this指向setup里面的所有属性或方法
-
setup接收两个形参:
- props:值为对象,包含组件外部传递过来,且在组件内部声明接收的属性
// 父组件:传递两个数据:msg school <Demo msg="好嗨哟" school="学校"/> // 子组件 export default { name: "Demo", props:['msg','school'], // 声明接收数据 setup(props, context) { // 【注意:若不声明接收数据,则打印为空】 console.log('props') // 打印传递过来的数据 } }
- context:上下文对象
- attrs:值为对象,组件外部传递过来,但没有在props配置中声明的属性, 相当于vue2中
this.$attrs
- slots:收到的插槽内容。相当于Vue2中this.$slots
- emit:自定义事件的函数,
// 父组件中 <Demo @hello="showHelloMsg" msg="好嗨哟" school="学校"> <template v-slot:qwe> <span>宋文俊</span> </template> </Demo> function showHelloMsg(value){ console.log('触发了 hello 事件,收到了参数',value); } // 子组件中 <template> <!-- 插槽 --> <slot name="qwe"></slot> </template> props:['msg','school'], emits:['hello'], //子组件响应父组件绑定的自定义事件,否则出现警告,(已修复,无需响应) setup(props, context) { let person = { name: "张三", age: 20, }; // 外部传递过来的数据 console.log('---attrs---',context.attrs); function test() { // 触发自定义事件,给父组件传递数据 context.emit("hello", 5666); } return { person, test, };}
- attrs:值为对象,组件外部传递过来,但没有在props配置中声明的属性, 相当于vue2中
- props:值为对象,包含组件外部传递过来,且在组件内部声明接收的属性
-
setup语法糖模式:
setup语法糖介绍? <script setup></script> 1、在<script setup>中,相当于就是在setup函数中写组合式代码,在里边定义数据,函数不需要返回,在页面模板中可以直接使用 2、在<script setup>中,可以直接引入子组件,并且直接在页面使用
-
defineProps:父子传参。
// 它里面的参数不能访问<script setup>中定义的其它变量,因为在编译过程中会被移动到外部的函数中。注意:它只能在Vue3语法糖中使用<script setup>。其它情况还是props
// 父组件 <template> <son1 :title="name" :arr="[1,2,3]"></son1> </template> <script> let name = "小宋" </script> // 子组件 <template> // 在模板中可以直接使用 <div>父组件传递过来的值:{{title}}</div> {{arr}} </template> <script setup lang='ts'> // 接收父组件传递过来的值 const props = defineProps({ title:{ type:String, defaul:"默认值" } }) // js中使用需要赋值形式 console.log(props.title) /* 【TS 写法】*/ const props = defineProps<{ title:string, arr:number[] // 或者 arr:any[] }>() // 【定义默认值写法】。TS特有定义默认值的 withDefaults(defineProps(),默认值) withDefaults(defineProps<{ title:string, arr:number[] }>(),{// 默认值 arr:()=>[666] })/* 【TS 写法:传递复杂数据】*/ /* 父组件 */ let data = [{a:1},{b:2},{c:3}] <boutique :data="data"/> /* 子组件*/ // 写法一:基于类型的声明。通过泛型参数来定义 props 的类型 const props = defineProps<{ // 接收父组件传递过来的值 data: any[] }>() console.log('子组件', props.data); // 写法二:运行时声明。通过类型推论定义 props 的类型 interface S { Clothing_name: string, Past_price: number, img: string, price: number, } const props = defineProps({ data: { type: Array as PropType<S[]>, // 类型推论 required: true, // 设置是否必传。可以省略 default: () => [] // 默认值 } }) console.log('子组件', props.data); </script> -
defineEmits:子传父
// 注意:它只能在Vue3语法糖中使用<script setup>。其它情况 emit // 父组件 <son2 ref="waterFall" @on-click="getName"> const getName = (name: string) => { console.log('我是父组件,接收到了值', name); } // 子组件 /* 普通写法 */ const emit = defineEmits(['on-click']) emit('on-click',"小宋") /* TS写法: */ const emit = defineEmits<{ // 限制传递参数的类型为字符串与数值 (e: "on-click", name: number): void (e: "on-click", name: string): void }>() const send = () =>{ emit('on-click',"2") }
-
defineExpose:通过它把子组件的属性或方法暴露出去。
// 父组件 <son2 ref="waterFall"> import son2 from './son/传递值.vue' /* 普通写法:*/ const waterFall = ref() // 必须是ref() setTimeout(() => { // 读取值的时候,不能放在组件的全局。要包裹起来。否则出现undefined console.log(waterFall.value); }, 0) /* TS写法:*/ // 使用TS自带的工具InstanceType 处理。接受一个泛型,通过typeof 读取导入的组件名(或组件上的ref) // let waterFall = ref<InstanceType<typeof waterFall>>() //读取ref,存在红色下划线 let waterFall = ref<InstanceType<typeof son2>>() //读取组件名,没有红色下划线,若存在红线配合可选链操作符 setTimeout(()=>{ // console.log('接收暴露的属性',waterFall.value); console.log('接收暴露的属性',waterFall?.value); // 使用了可选链操作符 },0) console.log('接收暴露的属性',waterFall.value); // 子组件 /* 普通写法:*/ defineExpose({ name: "老六", }) /* TS写法:*/ let list = reactive<number[]>([1,2,3]) defineExpose({ // 使用TS list })
-
useAttrs:接收父组件传入的参。(注意不能使用props,会提前接收参数)
// useAttrs:可以在setup语法糖写法中获取父组件传入的属性(没有被props接收的属性),注意:需要在上面引入useAttrs // 与useAttrs类似的还有一个userSlots
import {useAttrs} from 'vue'const attrs = useAttrs() console.log(attrs.name) console.log(attrs.sex)
-
四、常用Composition APi
1、ref 与 reactive
-
ref对象:用于定义基本数据类型
- 创建一个包含响应式数据的引用对象(reference对象,简称 ref对象)
- JS中操作数据需要加.value。例:xxx.value
- 使用ref 定义对象类型,底层的响应式实现使用的是reactive转为代理对象
- 注意点:
(1)接收的数据可以是:基本类型,也可以是对象类型 (2)基本类型的数据:响应式依然是靠 Object.defineProperty() 的get 与set 完成的 (3)对象类型的数据:内容“求助”了Vue3中的一个新函数——reactive函数。
这个函数里封装了Proxy(原理) - 模板中读取数据不需要.value
/* ref:使用值需要.value */ <div>{{ name }}</div> // 方法一:
type M = { name: string } const man = ref<M>({ name: '小宋' })
// 方法二:使用TS里面的给它确定类型。推荐用于类型比较复杂的情况 import type { Ref } from 'vue'; const man: Ref<M> = ref({ name: '小宋' })
-
reactive对象:用户定义复杂数据类型
- 语法:const 代理对象 = reactive(源对象)接收一个对象(或数组),返回一个代理对象(Proxy的实例对象,简称proxy对象)
- reactive 定义的响应式数据是“深层次的”。不需要写value
- 仅对对象类型生效(包括对象\数组\Map\Set等这样的类型)
- 把一个ref数据赋值给reactive对象的一个属性,则这个属性的ref值会自动解包(相当于自动调用ref的value值,再赋值给reactive对象属性)。访问响应式数组、Map这样的原生集合类型中的ref元素时,ref是不会自动解包的
- 不能直接赋值,会破坏响应式。 建议使用const来定义reactive变量
- 注意:解构一个reactive对象时,解构的属性如果是一个ref基本值, 则这个ref解构出来不会有响应式(因为自动解包),如果解构的属性本身也是对象,则解构的结果会是一个代理对象,有响应式(因为reactive的代理是深层次的)
- 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据进行操作
/* reactive:不能直接赋值否则会破坏响应式对象 */ <input v-model="from.name" type="text"> <input v-model="from.age" type="text"> type M = { name: string, age: number, } let from = reactive<M>({ name: '小宋', age: 20 }) // 解决破坏响应式对象的方案 // 方案一:数组,可以通过push方法加解构 let list = reactive<string[]>([]) let result = ['抽烟','喝酒','烫头'] list.push(...result) // 方案二:把数组当做对象的属性去处理 let list = reactive<{ arr: string[] }>({ arr: [] }) let result = ['抽烟','喝酒','烫头'] list.arr = result
-
ref 与 reactive 的区别对比
- 定义数据角度与赋值前后对比:
-
-
- ref用来定义:基本类型数据。重新赋值后保留响应式
- reactive用来定义:对象或数组类型数。重新赋值后失去响应式,变普通对象。详情
- 注意:ref 也可以定义对象或数组类型数据,它内部通过
-
-
- 原理角度对比:
- 使用角度对比
- 原理角度对比:
2、toRef 与toRefs
- 作用:创建一个 ref 对象,其value值指向另一个对象中的某个属性。(俗称:变成响应式数据)
- 语法:const name = toRef( person, "name")
- 应用:要将响应式对象中的某个属性单独提供给外部使用
// 基本使用 setup() { // 数据 let person = reactive({ name:'老六', age:18, job:{ j1:{ salary:20 } }, }) return { // 方式一:每一个拆分出来 // name: toRef(person,'name'), // age: toRef(person,'age'), // salary: toRef(person.job.j1,'salary'), // 方法二:直接展开,缺点,只能是一层的数据 ...toRefs(person) }; }, // 手写实现 toRefs const man = reactive({name:"小宋",age:23,like:"老六"}) const toRefs = <T extends object>(object:T) =>{ const map:any = {} for(let key in object){ map[key] = toRef(object,key) } return map } const {name,age,like} = toRefs(man) console.log(name,age,like);
3、shallowReactive 与 shallowRef 和 triggerRef
- shallowReactive:只处理对象最外层属性的响应式(浅响应式)
- shallowRef:只处理基本数据类型的响应式,只到.value,不进行对象的响应式处理
- triggerRef:手动控制响应式更新。对内部属性的变化进行响应式追踪
- 什么时候使用?
- 如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 ===> shallowReactive
- 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换 ===> shallowRef。
// let person = shallowReactive(对象) let x = shallowRef(对象)
-
注意:
ref 与 shallowRef不能写在一块,不然ref会影响shallowRef造成视图的更新 (注意reactive 与shallowReactive 也存在同样的问题) 原因:因为ref()底层已经调用过triggerRef()它会强制更新收集的依赖,导致使用shallowRef()也造成了视图更新 案例说明:下面这样就会造成视图更新 import { ref,shallowRef,triggerRef } from 'vue' type M = { name: string } const man = ref<M>({ name: '小宋' }) const man2 = shallowRef<M>({ name: '小宋2' }) const change = () => { man.value.name = "大宋" man2.value.name = "大宋2" // triggerRef(man2) }
4、readonly 与 shallowReadonly
- readonly:让一个响应式数据变为只读的(都变为只读,不允许修改)
- shallowReadonly:让一个响应式数据变为只读的(第一层数据是只读,深层数据还是响应式的)
- 应用场景:不希望数据被修改时使用
// person = readonly(对象) person = shallowReadonly(对象)
5、toRaw 与 markRaw
返回的是源对象
-
toRaw:
/* toRaw:将响应式对象变成普通对象。*/ const tor = toRaw(man) // 手写实现。在源码中依靠['__v_raw'] console.log(man['__v_raw'])
-
-
-
setup() { // 数据 let person = reactive({ name: "老六", age: 18, job: { j1: { salary: 20, }, }, }); // 触发这个函数 function addCar(){ let car = {name:'奔驰',price:'40W'} // 使用这个markRaw() 后,改变页面展示的数据的默认数据,不再是响应式的 person.car =markRaw(car) } // 返回响应式数据 return { sum, person }; }
-
-
6、customRef
- 作用:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。
- 实现防抖效果:
<template> <input type="text" v-model="keyWord" /> <h3>{{ keyWord }}</h3> </template> import { customRef } from "vue"; setup() { let keyWord = myref("hello",500); // 使用自定义的myref,实现响应式数据 function myref(value,delay) { let timer const x = customRef((track, trigger) => { return { get() { track(); //告诉Vue这个value值是需要被“追踪”的 console.log("有人从myRef容器中读取数据了"); return value; }, set(newvalue) { console.log("有人从myRef容器中修改数据了", newvalue); // 没次修改前关闭定时器,实现防抖 clearTimeout(timer) timer = setTimeout(() => { value = newvalue; trigger(); // 通知Vue重新解析模板,更新页面 },delay); }, }; });return x; }
return { keyWord, }; },
7、依赖注入provide 与 inject。(通讯方式【祖--孙】)
// 爷组件 import { provide, ref } from 'vue' import A from './components/A.vue' let colorVal= ref<string>('red') provide('color', colorVal) //向爷组件注入一个ref类型的string数据 // 父组件。A.vue import { inject } from 'vue' import type {Ref} from 'vue' // 使用TS自带的给它确定类型。用于类型比较复杂的情况 import B from './B.vue' // 若直接接收 inject('color'),类型为unknown,因为我们传入的是一个ref类型的string。所以需要使用泛型,然后使用Ref指定泛型string const color = inject<Ref<string>>('color') // 子组件。B.vue。修改爷组件注入的color值 import { ref,inject } from 'vue' import type {Ref} from 'vue' // 使用TS自带的给它确定类型。用于类型比较复杂的情况 const color = inject<Ref<string>>('color') // const color = inject<Ref<string>>('color',ref('red')) // 传递默认值写法 const change = ()=>{ // 使用可选链操作符或等号(=)赋值,不能正常赋值会出现undefined等问题。需要使用非空断言或者传递默认值(如上) color!.value = 'yellow' }
8、isRef、isReactive、isReadonly、isProxy(响应式数据判断)
- isRef: 检查一个值是否为一个 ref 对象
- isReactive: 检查一个对象是否是由 `reactive` 创建的响应式代理
五、声明周期钩子
-
与Vue2的区别
-
Vue3选项式式API 与 组合式API(setup语法糖)写法
-
其它的钩子
onActivated(): 被包含在<keep-alive>中的组件,会多出两个生命周期钩子函数。被激活时执行。 onDeactivated(): 比如从 A 组件,切换到 B 组件,A 组件消失时执行。 onErrorCaptured(): 当捕获一个来自子孙组件的异常时激活钩子函数 // 另外还有两个生命周期,官方说是用来调试使用的 onRenderTracked():状态追踪。跟踪页面上所有响应式变量和方法的状态。就是return返回去的值,它都会跟踪。只要页面有update的情况,它就会跟踪,然后生成一个event对象,可以通过event对象来查找程序的bug所在 onRenderTriggered:状态触发。不会跟踪每一个值,而是给你变化值的信息,并且新值和旧值都会给你明确的展示出来。
六、常用API
1、computed 计算属性
- computed执行返回一个ref数据(就是一个计算结果), ref数据的value值就是 回调函数中返回值
- vue3与vue2的属性计算一样,都有计算值缓存, 当第一次使用后会执行计算,后面每次使用都使用缓存结果
import { computed } from 'vue' setup(props, context) { let person = reactive({ firstName: "张", lastName:'三', }) // 计算属性——简写(没有考虑计算属性被修改的情况,只读) person.fullName = computed(()=>{ return person.firstName + '-' + person.lastName }) // 计算属性——完整写法(读写) person.fullName = computed({ get(){ return person.firstName + '-' + person.lastName }, set(value){ const nameArr = value.split('-') person.firstName = nameArr[0] person.lastName = nameArr[1] } }) return { person, }; }
2、watch、watchEffect、两者区别
-
watch函数
- 基本配置项
watch('',()=>{},{ deep: true, // 深度监听 immediate: true, // 立即执行一次 /* pre:组件更新之前。sync:同步执行。post:组件更新之后执行。*/ flush: "pre" })
- 若监听 ref 定义的响应式数据,这个数据是对象,
- 需要在监听数据的第一个参数加 .value
- 或者在第三个参数,加上深度监听 deep:true
- 原因:ref中已经封装了一个reactive函数,会把对象转为代理对象,跟 reactive 定义的响应式数据一样
// 方式一: watch(person.value,(newValue,oldValue)=>{ console.log('person的值变化了',newValue,oldValue); }) 方式二: watch(person,(newValue,oldValue)=>{ console.log('person的值变化了',newValue,oldValue); },{deep:true})
- 监听reactive 定义的响应式数据时(最外层):
- oldValue无法正确获取,跟newValue一样
- 强制开启了深度监视(deep配置失效)
watch(person,(newValue,oldValue)=>{ console.log('person变化了',newValue,oldValue); },{immediate:true,deep:true}) // deep:true 深度监听无效
- 监听person里面的某一个数据(reactive 定义的数据)
-
// 参数一:除了箭头函数,也可用普通函数,但必须有返回值 watch(()=>person.job.j1.salary,(newValue,oldValue)=>{ console.log('person中最的salary变化了',newValue,oldValue) })
-
- 基本配置项
-
- 监听person里面的多个数据(reactive 定义的数据)
- 用数组把把参数一包裹
watch([()=>person.name,()=>person.age],(newValue,oldValue)=>{ console.log('person变化了',newValue,oldValue) })
- 用数组把把参数一包裹
- 监听person里面的属性,属性值依然是一个对象。(reactive 定义的数据)
- oldValue无法正确获取,跟newValue一样
- 这种情况下,必须要开启 深度监听,不会自动开启
watch(()=>person.job,(newValue,oldValue)=>{ console.log('person的job变化了',newValue,oldValue); },{deep:true})
- 监听person里面的多个数据(reactive 定义的数据)
-
watchEffect 函数
- watchEffect 是:不用指明监视的哪个属性,监视的回调中用到哪个属性,那就监视哪个属性
- 回调函数会初始化默认执行一次,不需要去指定immediate: true;
- atchEffect 有点像 computed:
- 但computed 注重的计算出来的值(回调的返回值),必须要写返回值。
- 但watchEffect 更注重的是过程(回调函数的函数体),所以不用写返回值。
// 回调中用到的数据只要发生变化,则直接重新执行回调。 const wat = watchEffect(()=>{ const x1 = sum.value const x2 = person.job.j1.salary console.log('watchEffect指定的回调执行了'); },{ // pre 组件更新前(默认); sync:强制效果始终同步; post:组件更新后执行 flush: 'pre' }) // 结束监听。就是当前watchEffect 返回的一个函数,调用这个函数时,即可停止监听 wat()
-
watchEffect 与 watch的区别
-
watch的特点:
- 1、watch不会跟踪回调中依赖的数据,它只监指定的数据(可以同时监听一个或者多个), 把我们需要指定要监听的数据
- 2、watch默认是不初始化执行回调函数,需要我们去配置{immediate: true}才行; 所以watch可以更加精确控制监听时机
- 3、watch默认会开启一个深度监听,有时候这可能会造成性能问题;但是我们可以通过把第一个参数设置为getter来关闭这个默认开启
- watchEffect特点
- 1、不需要指定监听的数据,会自动跟踪监听回调函数中依赖的数据,对于写代码来说,更加简洁了
- 2、默认会初始化执行一次,所以使用watchEffect不好精确控制监听的时机
- 3、watchEffect回调函数中可以同时依赖多个属性, 则也会同时跟踪监听这些属性(如果属性本身也是一个对象,则一样也会有深度监听)
-
3、自定义hook函数
- 用于把setup函数中使用的Composition API进行了封装
- 类似于vue2.x中的mixin。
- 自定义hook的优势: 复用代码, 让setup中的逻辑更清楚易懂。
- 在跟src目录下建立 hooks文件夹,文件夹里面文件基本上都是以 use...开头,(潜规则)
- 案例:图片通过canvas 转 base64
// 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 // 防止base64解析错误 img.onload = () => { resolve({ baseUrl:base64(img) }) } console.log(img); }) const base64 = (el: HTMLImageElement) => { // 通过canvas方法 转base64 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) // toDataURL:导出base64 return canvas.toDataURL('image/jpg') } }) } // app.vue <img src="./img.jpg" alt="" width="300" height="300" id="img"> import useBase from './hooks/index' // 导入自定义的hook // 使用 useBase({ el:'#img'}).then((res)=>{ console.log(res.baseUrl); })
4、全局,动态,递归,异步组件,keep-alive缓存组件
-
全局组件
// 在main.ts文件,导入组件 import comp from './components/全局组件/全局.vue' // 挂载全局组件 app.component('card', comp) // 在页面中使用 <card/>
-
动态组件
// 让多个组件使用同一个挂载点,并动态切换, // 采用内置的的 component标签,然后使用v-bind:is=”组件” <div @click="switchCom(item, index)" :class="[active == index?'active':'']" class="tabs" v-for="item,index in data"> <div>{{ item.name }}</div> </div> <component :is="comId"></component> /* 写法一: */ import Avue from './Avue.vue' import Bvue from './Bvue.vue' /* 注意:在这里我们没必要对组件里面的信息做劫持,需要用shallowRef只处理最外层,到.value截止。 在对象里面需要加markRaw 。使用这个之后会有一个__skip__属性,reactive碰到了会跳过代理。否则控制台出现警告 */ const comId = shallowRef(Avue) // 先传入一个默认的组件 const active = ref(0) // 作为条件,添加样式 const switchCom = (item:any,index:number) =>{ // 绑定的点击事件 // item形参就是 data数据的每一项 comId.value = item.com console.log(comId.value); // 所点击的组件信息 active.value = index // 所点击的索引值传递给 active } const data = reactive([ { name:'A组件', com:markRaw(Avue), }, { name:'B组件', com:markRaw(Bvue), }, ]) /* 写法二: */ <script lang="ts"> import Avue from './Avue.vue' import Bvue from './Bvue.vue' import Cvue from './Cvue.vue' export default { components: { Avue, Bvue, Cvue } } </script> <script setup lang='ts'> import { ref, reactive, shallowRef, markRaw } from 'vue' const comId = shallowRef(Avue) const active = ref(0) const switchCom = (item: any, index: number) => { comId.value = item.com console.log(comId.value); active.value = index } const data = reactive([ { name: 'A组件', com: "Avue", }, { name: 'B组件', com: "Bvue", }, { name: 'C组件', com: "Cvue", } ]) </script>
-
递归组件
/* 父组件 */ <TreeVue :data="datass"/> import TreeVue from './Tree.vue' interface Tree { name: string, checked: boolean, children?: Tree[] // ?:可选的 } const datass = reactive<Tree[]>([ { name: '1', checked: false, children: [ { name: '1-1', checked: false } ] }, ]) /* 子组件 :Tree.vue */ <div :style="{marginLeft: 20+'px'}" v-for="item in data"> <input type="checkbox" v-model="item.checked"> <span>{{item.name}}</span> // 写法一:使用文件名做为组件名。v-if:在这里是判断是否有数据,做性能优化。:data="item?.children" 把chidren的数据再次传递进来做处理 <Tree v-if="item?.children?.length" :data="item?.children"/> // 写法二:也可以在写一个script,里面在设立name。写法三():还可以使用插件unplugin-vue-define-options <xiaosong v-if="item?.children?.length" :data="item?.children"/> </div> interface Tree { name: string, checked: boolean, children?: Tree[] // ?:可选的 } defineProps<{ // 接收父组件传递过来的数据 data?:Tree[] }>() // 写法二的 <script lang="ts"> export default { name:"xiaosong" } </script>
-
异步组件
// public目录下的json文件。data.json【注意:public目录是不被编译的可直接访问】 { "data": { "name": "小满", "url": "https://fanyi.baidu.com/", } } // 封装原生Ajax。axios.ts export const axios = { get <T>(url:string):Promise<T>{ return new Promise((resolve)=>{ const xhr = new XMLHttpRequest() xhr.open('GET',url) xhr.send(null) xhr.onreadystatechange = () =>{ if(xhr.readyState == 4 && xhr.status == 200){ setTimeout(()=>{ resolve(JSON.parse(xhr.responseText)) },1000) } } }) } } // 发送请求。sync.vue <div><img :src="data.url"></div> <div>{{ data.name }}</div> import {axios} from './axios' // 导入封装的Ajax interface Data { data:{ name:string, url:string } } // 使用顶层await(es7以后)。【public目录是不被编译的可直接访问,所以直接./data.json】 const {data} = await axios.get<Data>('./data.json') // index.vue。defineAsyncComponent配合动态引入,加Suspense包裹组件 <Suspense> <template #default> <SyncVue></SyncVue> </template> <template #fallback> 正在加载中。。。 </template> </Suspense> import { defineAsyncComponent } from 'vue' const SyncVue = defineAsyncComponent(() => import('/src/components/9.异步组件/sync.vue'))
-
keep-alive缓存组件
-
作用:切换页面,组件不会被重新渲染,页面上输入的数据还存在
-
注意:一个 KeepAlive 里面只能同时存在一个子节点
// :include="['组件名']" 设置缓存那个组件。 // :exclude="['组件名']" 设置不缓存那个组件 // :max="组件数量" 指定缓存组件的数量 <keep-alive :include="['a','b']"> <aa v-if="flag"></aa> <bb v-else></bb> </keep-alive> import aa from './a.vue' import bb from './b.vue' const flag = ref<boolean>(true) /* 被缓存的组件会又两个新的生命周期钩子 */ onActivated(()=>{ console.log('缓存组件KeepAlive初始化'); }) onDeactivated(()=>{ console.log('缓存组件KeepAlive卸载'); })
-
5、插槽
作用:子组件中的提供给父组件使用的一个占位符,用<slot></slot> 表示,父组件可以在这个占位符中填充任何模板代码,如 HTML、组件等。
-
匿名插槽
// 子组件 <slot></slot> // 父组件 <Dialog> <template v-slot> <div>我是匿名插槽</div> </template> </Dialog> import Dialog from './插槽/index.vue'
-
具名插槽
// 子组件 <slot name="header"></slot> // 父组件 <Dialog> <template v-slot:header> // 可简写为i#:header <div>我是具名插槽</div> </template> </Dialog> import Dialog from './插槽/index.vue'
-
作用域插槽
// 子组件 <div v-for="item in data"> <slot :data="item"></slot> </div> type names = { name: string, age: number } const data = reactive<names[]>([ { name: '小宋', age: 20 }, { name: '小小宋', age: 21 } ]) // 父组件:可以接收到子组件传递的值(data) <Dialog> // 把子组件数据解构出来。可简写为#default="{data}" <template v-slot="{data}"> <div> 我是插槽.中。{{data}} </div> </template> </Dialog> import Dialog from './插槽/index.vue'
-
动态插槽
// 子组件 <slot name="cha1"></slot> // 插槽一 <slot name="cha2"></slot> // 插槽二 // 父组件 <Dialog> <template #[name]> <div>我是动态插槽</div> </template> </Dialog> import Dialog from './插槽/index.vue' // 设置动态插槽插入到哪个位置,前提子组件插槽中存在name名字 let name = ref('footer')
6、 兄弟组件的传参
-
方式一:使用Mitt 库。使用的是发布订阅模式的设计
/* 在vue3中$on,$off 和 $once 实例方法已被移除,组件实例不再实现事件触发接口,因此大家熟悉的EventBus便无法使用了。*/ // 1、安装 npm install mitt -S // 2、在main.ts 初始化。挂载全局属性 import mitt from 'mitt' // 引入 Mitt const Mit = mitt() // 注册 // ts注册mitt 方式 declare module 'vue'{ // 需要拓展 ComponentCustomProperties 类型才能获得类型提示 export interface ComponentCustomProperties { $Bus: typeof Mit } } app.config.globalProperties.$Bus = Mit // 挂载全局API
使用方法:通过getCurrentInstance() 获取当前组件的实例
// 兄弟组件A。传递值 <button @click="emitB">emit</button> import { getCurrentInstance } from 'vue'; const instance = getCurrentInstance() const emitB = ()=>{ instance?.proxy?.$Bus.emit('on-xiao','老六') instance?.proxy?.$Bus.emit('on-song','老六六') } // 兄弟组件B。接收值 import { getCurrentInstance } from 'vue'; const instance = getCurrentInstance() // 接收值,写法一 instance?.proxy?.$Bus.on('on-xiao',(str:any)=>{ console.log('*--str',str); }) // 接收值,写法二 instance?.proxy?.$Bus.on('on-song',Bus) const Bus = (str:any)=>{ console.log('**----str',str); } // 接收值,写法三:可以接收多条数据。*代表所有。off好像对它无效 instance?.proxy?.$Bus.on('*',(type,str)=>{ console.log('**----type',type); // 键。on-xiao console.log('**----str',str); // 值。老六 }) //取消指定的mitt 事件。 off(事件名,函数-可省略) instance?.proxy?.$Bus.off('on-song') //清空所有的mitt 事件。 instance?.proxy?.$Bus.all.clear()
-
方式二:借助父组件传参
// 在子组件A中使用defineEmits接收父组件传递过来的自定义函数,并调用,把数据带给父组件定义的getFlag函数。在由父组件传递给子组件B,子组件B通过defineProps接收数据。 // 父组件。index.vue <AA @on-click="getFlag"></AA> <BB :f="Flag"></BB> import { ref } from 'vue' import AA from './A.vue' import BB from './B.vue' let Flag = ref(false) const getFlag = (params:boolean)=>{ Flag.value = params } // 子组件。A.vue <div class="A"> <button @click="emitB">派发一个事件</button> </div> const emit = defineEmits(['on-click']) let flag = false const emitB = ()=>{ flag = !flag emit('on-click',flag) } // 子组件。B.vue <div>{{ f }}</div> type Propss ={ f: boolean } defineProps<Propss>()
-
方式三:运用了JS设计模式之发布订阅模式
/* 我们在Vue2 可以使用$emit 传递 $on监听 emit传递过来的事件 这个原理其实是运用了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{ // 实现这个BusClass list: List constructor(){ // 调用中心,是一个对象 this.list = {} // 初始化list } emit(name:string, ...args:Array<any>){ // 重写emit let evnentName:Array<Function> = this.list[name] evnentName.forEach(fn=>{ fn.apply(this,args) }) console.log(evnentName); } on(name:string,callback:Function){ // 重写on // 如果注册了多次返回一样的,第一次注册返回空数组 let fn:Array<Function> = this.list[name] || [] fn.push(callback) this.list[name] = fn } } export default new Bus() // 组件A.vue import Bus from './Bus'; let flag = false const emitB = ()=>{ // 触发点击事件,把数据带给兄弟组件B flag = !flag Bus.emit('on-click',flag) } // 组件B.vue import Bus from './Bus'; Bus.on('on-click',(flag:boolean)=>{ console.log('接收到了组件A的数据', flag) })
7、自定义指令(directive)
-
在setup内部定义局部指令
/*
但这里有一个需要注意的限制:必须以vNameOfDirective
的形式来命名本地自定义指令,以使得它们可以直接在模板中使用*/ // 父组件 <button @click="flag = !flag">切换</button> // v-move="{background:'pink'}" 把背景颜色传递给A组件覆盖原来的 :aaa 自定义的参数 .xiaosong 自定义修饰符 <A v-if="flag" v-move:aaa.xiaosong="{background:'pink'}"></A> import { ref, reactive, Directive, DirectiveBinding} from 'vue' // 在每一个钩子里面都可以收到模板传递过来的值 const vMove:Directive = { // 定义局部指令 created: () => { console.log("初始化====>"); }, beforeMount(...args:Array<any>) { // 把所有的参数都打印出来 console.log("初始化一次=======>"); // 在元素上做些操作 console.log(args); }, mounted(el:HTMLElement,dir:DirectiveBinding<Dir>) { console.log(dir.value.background); el.style.background = dir.value.background console.log("初始化========>"); }, } // 子组件 <div class="A"> A组件 </div> <style scoped> .A { width: 200px; height: 200px; border: 1px solid red; } </style>
-
函数简写方式
/*
想在mounted
和updated
时触发相同行为,而不关心其他的钩子函数。那么你可以通过将这个函数模式实现 */
// 父组件 <input type="text" v-model="value"> <A v-move="{background: value}"></A> import { ref, Directive, DirectiveBinding} from 'vue' import A from './A.vue' type Dir = { background:string } let value = ref<string>('') const vMove:Directive = (el:HTMLElement,bingding:DirectiveBinding<Dir>) =>{ el.style.background = bingding.value.background } // 子组件 <div class="A">A组件</div> <style scoped> .A { width: 200px; height: 200px; border: 1px solid red; } </style>
-
自定义拖拽指令案例
/* */ <template> <div v-movee class="box"> <div class="header"></div> <div>内容</div> </div> </template> <script setup lang='ts'> import { ref, Directive, DirectiveBinding } from 'vue' // 因为没有返回值,在泛型中添加void const vMovee: Directive<any, void> = (el: HTMLElement, dir: DirectiveBinding) => { // 获取.box里面类名为header的div let moveElement:HTMLDivElement = el.firstElementChild as HTMLDivElement // 鼠标按下 moveElement?.addEventListener('mousedown',(ee:MouseEvent)=>{ console.log('el.offsetLeft:',el.offsetLeft); console.log('ee.clientX:',ee.clientX); // 鼠标点下位置距离左侧浏览器的距离 - 最外层盒子距离左侧浏览器的距离 let X = ee.clientX - el.offsetLeft let Y = ee.clientY - el.offsetTop const move = (e:MouseEvent)=>{ // console.log(e); el.style.left = e.clientX - X + 'px' el.style.top = e.clientY - Y + 'px' } // 鼠标移动 document.addEventListener('mousemove',move) // 鼠标松开 document.addEventListener('mouseup',()=>{ // 清楚鼠标移动事件 document.removeEventListener('mousemove',move) }) }) } </script> <style scoped lang="less"> *{ margin: 0; padding: 0; } .box { position: relative; width: 200px; height: 200px; border: 3px solid black; .header { height: 20px; background: black; cursor: move; } } </style>
8、定义全局函数和变量
由于Vue3 没有Prototype 属性 使用 app.config.globalProperties 代替 然后去定义变量和函数
// Vue2 Vue.prototype.$http = () => {} // Vue3 const app = createApp({}) app.config.globalProperties.$http = () => {}
基本使用:
// main.ts const app = createApp(App) // 定义全局变量 app.config.globalProperties.$env = 'dev' // 定义全局函数。Vue3移除了过滤器 app.config.globalProperties.$filters = { format<T>(str:T){ return `小宋-${str}` } } type Filter = { format<T>(str:T):string } // 声明文件,不然TS无法类型推导。给vue扩充一个类型$filters。解决在其它页面出现红色报错线问题 declare module 'vue'{ export interface ComponentCustomProperties { $filters: Filter, $env:string } } // App.vue <div>{{ $env }}</div> <div>{{ $filters.format('的飞机') }}</div> // getCurrentInstance:获取组件的实例 import { getCurrentInstance } from 'vue' const app = getCurrentInstance() console.log(app?.proxy?.$env); // 获取全局变量 console.log(app?.proxy?.$filters.format('ts')); // 获取全局方法
9、自定义Vue插件,手写 .use()挂载插件
-
基本使用:
// main.ts import loading from './components/20.自定义插件/index' // 引入自定义的插件 app.use(loading) // 使用 自定义插件
-
编写全局调用插件
// 全局调用的插件。loading.ts 。 // 定义app,VNode类型 import type {App,VNode} from 'vue' // 导入组件 import Loading from './index.vue' // createVNode:把组件转为VNode,来操作,类似虚拟dom结构 import {createVNode, render} from 'vue' export default { install(app:App){ console.log('全局的App',app); // 直接打印组件,得到的是一个存在setup的对象,无法直接操作,需要转Vnode才能使用 console.log('导入的组件',Loading); const Vnode:VNode = createVNode(Loading) console.log('转为VNode的组件',Vnode); // 使用该函数挂载 render(Vnode,document.body) app.config.globalProperties.__loading = { show:Vnode.component?.exposed?.show, hide:Vnode.component?.exposed?.hide } app.config.globalProperties.__loading.show() // 会出现报红 // console.log('读取',Vnode?.component?.setupState.hide()); // 采用了暴露的方式,不会出现报红 console.log('读取',Vnode?.component?.exposed); } } // 被插件导入的组件,index.vue const show = () => isShow.value = true const hide = () => isShow.value = false // 通过这个把变量,方法抛出 defineExpose({ show, hide, isShow })
-
使用经过被插件处理好的全局方法
// 其它组件中调用方法 import { getCurrentInstance } from 'vue' // 通过 getCurrentInstance() const instance = getCurrentInstance() // __loading会出现红线波浪线报错,需要在main.ts声明文件 instance?.proxy?.$loading_?.show() // 调用方法 // main.ts type Lod = { show: () => void, hide: () => void } //编写ts loading 声明文件放置报错 和 智能提示 // @vue/runtime-core主要核心库 也可以使用vue,又是无效 declare module 'vue' { export interface ComponentCustomProperties { $loading_: Lod } }
-
手写实现 .use()。用在挂载插件
// myUse.ts 文件 import type {App} from 'vue' import {app} from './main' // 导入主文件中的app interface Use{ install:(app:App,...options:any[])=>void } // 使用Set集合,实现缓存效果 const installList = new Set() export function myUse<T extends Use>(plugin:T,...options:any[]){ if(installList.has(plugin)){ // has 判断集合中是否存在plugin console.log('插件已经注册',plugin); }else{ plugin.install(app,...options) // 把参数传递到接口里面的方法里,实现挂载功能 installList.add(plugin) // 往集合中追加数据 } } // main.ts 主文件 import { myUse } from './myUse' // 导入自定义的 .use() export const app = createApp(App) // 导入出app myUse(loading) // 使用自定义的 .use() 注册插件
10、css Style新特性
-
插槽选择器
// 父组件 index.vue <A> <div class="a">私人定制</div> </A> import A from './插槽.vue' // 子组件 插槽.vue <slot></slot> /* 通过设置这个插槽选择器,就可以在这里写好样式,等待标签填充到这里后,直接使用样式 */ :slotted(.a){ color: pink; }
-
动态样式
// v-bind() <div class="dt">动态样式</div> import { ref } from 'vue' const style = ref({ color: 'red' }) setTimeout(() => { style.value.color = 'pink' }, 2000) .dt { color: v-bind('style.color'); }
-
全局的样式
// 通过 :global() <style> :global(div) { margin: 10px; border: 1px solid rgb(185, 185, 222); } </style>
-
css module
/* <style module> 标签会被编译为 CSS Modules 并且将生成的 CSS 类作为 $style 对象的键暴露给组件 */ /* 普通写法 */ <p :class="$style.mod"></p> <style module> .mod { background: yellow; width: 50px; height: 20px; } </style> /* 自定义名称写法:一般用于TSX 和 render函数居多 */ // 设置了自定义名称,使用名称替换后掉$style <div :class="[$style.mod, xs.modcolor]">666</div> <style module> .mod { background: yellow; width: 50px; height: 20px; } </style> <style module="xs"> .modcolor { color: green; } </style>
11、nextTick
具体详情:点击。
创建一个异步任务,等到同步任务执行完后在执行
// <input type="text" v-model="message"> <div ref="div">{{message}}</div> <button @click="change">change</button> import { ref, nextTick } from 'vue' const message = ref<string>('小宋') const div = ref<HTMLElement>() const change = async() =>{ message.value = '老六' await nextTick() console.log(div.value?.innerHTML); }
七、新的组件
1、Fragment
- 在Vue2中:组件必须有一个根标签
- 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
- 好处: 减少标签层级, 减小内存占用
2、Teleport
// 移动的位置,可以用HTML标签(body 等),或ID(#ab),或class(.cc) <teleport to="移动位置"> <div v-if="isShow" class="mask"> <div class="dialog"> <h3>我是一个弹窗</h3> <button @click="isShow = false">关闭弹窗</button> </div> </div> </teleport>
3、Suspense
- 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
- 使用步骤:
- 异步引入组件
// 异步引入 import {defineAsyncComponent} from 'vue' const child = defineAsyncComponent(()=>import('./components/child.vue'))
<template> <div class="app"> <h3>我是App组件(父)</h3> <Suspense> <template v-slot:default> <child/> </template> <template v-slot:fallback> <h3>稍等,加载中。。。</h3> </template> </Suspense> </div> </template>
- 用异步组件 配合 Suspense 返回一个 Promise实例
export default { name: "Child", // 【方式一】 // setup() { // let sum = ref(0); // return new Promise((resolve,reject)=>{ // setTimeout(()=>{ // resolve({sum}) // },2000) // }) // } // }, // 【方式一】 async setup() { let sum = ref(0); let p = new Promise((resolve, reject) => { setTimeout(() => { resolve({ sum }); }, 2000); }); return await p }, };
- 异步引入组件
八、扩展
1、TSX
在tsx中取变量,只需要一个花括号
-
基本配置
- 方法一:使用插件
// 1、安装插件 npm install @vitejs/plugin-vue-jsx -D // 2、在 vite.config.ts 配置 import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx'; // 引入tsx,编译tsx文件 export default defineConfig({ plugins: [ vue(), vueJsx()] // 使用插件 })
- 方法二:自定义封装
// 在根目录下,新建文件夹plugin。手写实现 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(){ return { esbuild:{ include:/\.ts$/ } } }, async transform(code,id){ // console.log(id); if(/\.tsx$/.test(id)){ console.log(code,id); //@ts-ignore const ts = await import('@babel/plugin-transform-typescript').then(r=>r.default) const res = await babel.transformAsync(code,{ ast:true, configFile:false, babelrc:false, plugins:[jsx,[ts, { isTSX: true, allowExtensions: true }]] }) console.log('res',res.code); return res?.code } return code } } }
// 在vite.config.ts 文件 // 1、导入自定义的tsx插件,编译tsx文件 import tsx from './plugin/index' // 2、如果上一步导入文件出现报错,在tsconfig.node.json中的include配置即可 "include": ["vite.config.ts","plugin"] }
- 方法一:使用插件
-
基本写法
-
写法一:返回一个渲染函数
//dd export default function () { return (<div>小宋</div>) }
-
写法二:通过API的方式。defineComponent
// 类似于 vue2编写风格 import {defineComponent, render} from 'vue' export default defineComponent({ data(){ return{ age:20 } }, render(){ // 在tsx 里面动态数据只需要一个花括号 return (<div>{this.age}</div>) } })
-
写法三:setup函数模式。
import {defineComponent } from 'vue' export default defineComponent({ setup(){ return (<div>{this.age}</div>) } })
-
-
改变
- 支持v-model的使用。不会自动解包,使用ref需要加 .value,
import { defineComponent, ref } from 'vue' export default defineComponent({ setup() { let v = ref<string>('') return () => (<> // tsx不会自动解包,使用ref 需要另外加 .value <input v-model={v.value} type="text" /> </>) } })
- 支持v-show 的使用。
import { defineComponent, ref } from 'vue' export default defineComponent({ setup() { let flag = ref(false) return () => (<> <div v-show={flag.value}>老宋</div> </>) } })
- 不支持v-if 的使用。使用三元表达式替代
import { defineComponent, ref } from 'vue' export default defineComponent({ setup() { let flag = ref(false) return () => (<> { flag.value ? <div>小宋</div> : <div>老宋</div> } </>) } })
- 不支持v-for 的使用。使用map代替
import { defineComponent, ref } from 'vue' export default defineComponent({ setup() { const data = [{name: "小宋1"},{ name: "小宋2"},{name: "小宋3"}] const fn = (item: any) => { // 绑定事件 console.log('触发了', item); emit('on-click', item) }
} return () => (<> {data.map(v => { //绑定函数需要这样写onClick={()=>fn(v)},否则页面加载直接触发 return <div onClick={()=>fn(v)}>{v.name}</div> })} </>) }) -
支持v-bind的使用。直接赋值即可
import { defineComponent} from 'vue' export default defineComponent({ setup() { const data = [1,2] } return () => (<> {data.map(v => { return <div data-arr={data}>{v.name}</div> })} </>) })
- Props接收父组件传递的值
// 父组件 App.vue <tsx name="小宋"></tsx> import tsx from './components/15.tsx与vite插件/index' // 子组件,接收值,index.tsx import { defineComponent} from 'vue' interface Props { // 设置接口类型 name?: string } export default defineComponent({ props: { // 接收父组件传递过来的值 name: String }, setup(props: Props) { return () => (<> <div>{props.name}</div> </>) } })
- emit。向父组件传递数据
// 父组件 App.vue <tsx @on-click="getItem"></tsx> import tsx from './components/15.tsx与vite插件/index' const getItem = (item:any)=>{ console.log('我是父组件',item); } // 子组件,向父组件传递值,index.tsx import { defineComponent} from 'vue' export default defineComponent({ setup(props, contxt) { const data = [1,2,3] // 绑定事件 const fn = (item: any) => { contxt.emit('on-click', item) } return () => (<> <div onClick={() => fn(data)}>点击</div> </>) } })
- 插槽
// 父组件 import { defineComponent} from 'vue' import A from './A' export default defineComponent({ setup(props, contxt) { const slot = { // 传递给子组件的方法 default: () => (<div>小宋default slots</div>), foo: ()=>(<div>小宋foo slots</div>) } return () => (<> <A v-slots={slot}></A> // 把数据带给子组件 </>) } }) // 子组件 import { defineComponent } from 'vue' export default defineComponent({ setup(_: any, { slots }: any) { return () => (<> <div>{slots.default ? slots.default() : '默认值'}</div> <div>{slots.foo?.()}</div> </>) } })
- 支持v-model的使用。不会自动解包,使用ref需要加 .value,
2、自动引入插件
配置完成使用 ref,watch等都无须import导入,可直接使用// 下载 npm i unplugin-auto-import -D // 在vite.config.ts中配置 import AutoImport from 'unplugin-auto-import/vite' export default defineConfig({ plugins: [AutoImport({ imports:['vue'], dts:"src/auto-import.d.ts" })] })
3、Scoped详解和样式穿透deep
-
scoped的原理
vue中的scoped 通过在DOM结构以及css样式上加唯一不重复的标记:data-v-hash的方式,以保证唯一(而这个工作是由过 PostCSS 转译实现的),达到样式私有化模块化的目的。
scoped的渲染规则:
- 给HTML的DOM节点加一个不重复data属性(形如:data-v-123)来表示他的唯一性
- 在每句css选择器的末尾(编译后的生成的css语句)加一个当前组件的data属性选择器(如[data-v-123])来私有化样式
- 如果组件内部包含有其他组件,只会给其他组件的最外层标签加上当前组件的data属性
PostCSS会给一个组件中的所有dom添加了一个独一无二的动态属性data-v-xxxx,然后,给CSS选择器额外添加一个对应的属性选择器来选择该组件中dom,这种做法使得样式只作用于含有该属性的dom——组件内部dom, 从而达到了'样式模块化'的效果.
-
样式穿透
把 el-input 背景色设置为红色!,会无效。因为设置scoped,PostCss会进行转化,往DOM节点添加data属性表示唯一性,编译后的css选择器后面都会加上一个data选择器来私有化样式,但组件中如果有其它的组件,只会给最外层标签加data属性,导致里面的标签用不了写的样式。
也可以不设置scoped,它就会正常显示背景颜色
解决方案:
- Vue2中,使用样式穿透
![]()
- Vue3中,使用样式穿透
![]()
4、函数式编程,h函数(第三种编写风格)
-
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')])
-
传递并接收参数
<Btn text="小宋" @on-click="getBtn"></Btn> import { h} from 'vue' type Props = { // 定义类型 text: string } const Btn = (props: Props, ctx: any) => { return h('div', { class: ['rounded-xl', 'bg-red-500', 'p-1.5', 'text-gray-50', 'text-center'], onClick: () => { console.log(props); // props参数可以获取 Btn组件上定义的所有属性 console.log(ctx); // emit slots attrs等 ctx.emit('on-click', '按钮') // 把值带给自定义函数 } }, props.text // 在浏览器页面上显示的数据 ) } // 自定义函数。接收值 const getBtn = (str: string) => { console.log('接收参数', str); }
-
定义插槽:
<Btns> // #default 可能会出现红线报错线,但不影响。暂未解决 <template #default>按钮slots</template> </Btns> const Btns = (props: Props, ctx: any) => { return h('button', { class: ['rounded-xl', 'bg-red-300', 'p-1.5', 'text-gray-50', 'text-center'] }, ctx.slots.default() // 在页面上默认显示“按钮slots” ) }
5、Vue响应式语法糖新特性(省略.value)
作用:可以使用$...,来省略.value
原理:$...宏函数是基于运行时的,它最终还是会转换为ref加.value,只不过vue帮我们做了这个操作。编译式的操作
注意:这里都是实验性的产物,暂时不要再生产环境使用。要求 Vue版本3.2.25 以上
开启配置
- 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、$computed......等等
<script setup lang='ts'> import { ref, reactive } from 'vue' import { $ref } from 'vue/macros' // 普通写法 const count = ref(0) const add = ()=> count.value++ // 新特性。省略.value let count = $ref(0) const add = ()=> count++ </script>
跟ref 有关的函数都做处理 都不需要.value了
- ref ->
$ref
- computed ->
$computed
- shallowRef ->
$shallowRef
- customRef ->
$customRef
- toRef ->
$toRef
- ref ->
- $ref 存在的弊端。因为它编译后就是count.value,并不是一个ref或reactive对象,所以无法监听并且会抛出一个警告
<script setup lang='ts'> import { ref, watch } from 'vue' import { $ref } from 'vue/macros' let count = $ref(0) // 新特性。省略.value watch(count,(v)=>{ // 监听存在问题 console.log(v); }) </script>
解决方案:通过 $$ 符号,让它编译的时候变成一个ref对象不加.value的
import { ref, watch } from 'vue' import { $ref,$$ } from 'vue/macros' let count = $ref(0) // 新特性。省略.value const add = ()=> count++ watch($$(count),(v)=>{ // 通过 $$() 变为ref对象 console.log('监听了',v); })
-
解构。$()
{{name}} import { ref, watch,reactive,toRefs } from 'vue' import { $ref, $$, $ } from 'vue/macros' const obj = reactive({ name: '老六', desc: '我不是老六' }) // let {name,desc} = obj //解构出现是一个字符串(打印也是),不是响应式的 // let { name, desc } = toRefs(obj) //解构出现是响应式的对象。需要.value let { name, desc } = $(obj) //打印结果是字符串。但它是响应式的。不需要.value setTimeout(()=>{ name= '老8' },2000)
6、vue3开发移动端&&打包App
7、unocss原子化
-
CSS原子化的优缺点
- 减少了css体积,提高了css复用
- 减少起名的复杂度
- 增加了记忆成本 将css拆分为原子之后,你势必要记住一些class才能书写,哪怕tailwindcss提供了完善的工具链,你写background,也要记住开头是bg
- 接入unocss最好用与vite,webpack属于阉割版功能较少
-
基本使用
- 安装
npm i -D unocss
- 安装
-
- 在vite.config.ts中配置
unocss 预设的作用:
-
-
- presetIcons():图标库预设
// 安装图标集合。官网地址:https://icones.js.org/ /* 注意:进入官网,找一套心仪的,安装时斜杠后面的,必须要跟官网所点击那套地址栏最后的要对上。
例如:官网:https://icones.js.org/collection/bi 下载:npm i -D @iconify-json/bi */ npm i -D @iconify-json/ic
点击所要使用的图标打开列表选择最下面那个(做类名使用)
- presetAttributify():属性化模式支持
// 在页面中使用,不需要class <div red></div> //原先写法:class="red" <div m="1"></div> //原先写法:class="m-1"
-
presetUno():工具类预设
// 集成了 Tailwind CSS,Windi CSS,Bootstrap,Tachyons 等。 // 可以直接使用里面的类名
-
import unoCss from 'unocss/vite' // 导入原子化 import { presetIcons,presetAttributify,presetUno } from 'unocss' // 导入预设 export default defineConfig({ plugins: [ unoCss({ // presetIcons():icon图标库。 presetAttributify():属性语义化。 presetUno():工具类预设 presets:[presetIcons(),presetAttributify(),presetUno()], // 定义预设 rules:[ // 自定义css原子化 ['flex',{display:"flex"}], //使用了flex类名,相当于使用了display:"flex"。-----静态。 ['red',{color:"red"}], //使用了red类名,相当于使用了color:"red"。-----静态。 [/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 10}px` })], //m-参数*10 例如 m-10 就是 margin:100px。-----动态 ], shortcuts:{ // 组合样式 cike:['flex','red'] } }) ] })
- presetIcons():图标库预设
-
8、Vue3集成Tailwind
vscode中安装代码提示插件:
具体详情:点击查看
// 1、安装:
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
// 2、生产配置文件
npx tailwindcss init -p
9、JS的的执行机制与nextTick
-
执行机制
- 同步任务:代码从上到下顺序执行
- 异步任务:
- 宏任务:script(整体代码)、setTimeout、setInterval、UI交互事件、postMessage、Ajax
- 微任务:Promise.then catch finally、MutaionObserver、process.nextTick(Node.js 环境)
运行机制:所有的同步任务都是在主进程的执行中形成一个执行栈,主进程之外还有一个”任务队列(异步任务队列)“,在这个队列中先执行宏任务,在清空(执行)当前宏任务中的所有微任务,然后进行下一个tick形成循环。
注意Promise构造函数是同步的,但then是微任务
案例:
<script setup lang='ts'>
async function Prom() {
console.log('y');
await Promise.resolve() // await 相对于微任务
console.log('x'); // 相当于写在Promise.then里面
}
setTimeout(()=>{
console.log('1');
Promise.resolve().then(()=>{
console.log('2');
})
},0)
setTimeout(()=>{
console.log('3');
Promise.resolve().then(()=>{
console.log('4');
})
},0)
Promise.resolve().then(()=>{
console.log('5');
})
Promise.resolve().then(()=>{
console.log('6');
})
Prom()
console.log(0);
// console.log('y 0 5 6 x 1 2 3 4'); // 正确输出结果
</script>
10、electron桌面程序
-
使用vite构建electron项目
/* 1.创建项目 */
npm init vite@latest
/* 2.安装electron */
npm install electron -D
npm install vite-plugin-electron -D // 注意它的版本问题
/* 3.修改vite.config.ts 配置文件 */
import electron from 'vite-plugin-electron'
export default defineConfig({
plugins: [electron({
main: { // vite-plugin-electron的0.10.4版本,需要省略main
entry: "electron/index.ts", // 配置入口文件
}
})],
})
/* 4.根目录新建 electron/index.ts */
// app:应用程序 BrowserWindow:控制打开electron框
import {app,BrowserWindow} from 'electron'
const createWindow = () =>{
const win = new BrowserWindow({
webPreferences: {
devTools: true,
contextIsolation: false,
nodeIntegration: true //允许html页面上的javascipt代码访问nodejs 环境api代码的能力(与node集成的意思)
}
})
console.log('输出',process.env);
// 加载。把本地服务网站放入(自带环境变量)。若本地服务的地址不对,会出现白屏
// win.loadURL(`http://${process.env[' VITE_DEV_SERVER_HOSTNAME']}:${process.env['VITE_DEV_SERVER_PORT']}`)// vite-plugin-electron的0.9.3版本,即之前
win.loadURL(`${process.env['VITE_DEV_SERVER_URL']}`) // vite-plugin-electron的0.10.4版本,即以后(具体看官网)
} app.whenReady().then(createWindow) // 初始化
注意运行项目:npm run dev 会出现几个报错
错误一:
控制台出现以下报错。(vite-plugin-electron的0.9.3版本即之前是这个报错)
或弹出报错框
解决方案:
在根目录package.json,删除 "type":"module",不让它做默认导出
错误二:
弹出报错框
解决方案:
在根目录package.json,指定入口文件:"main": "dist-electron/index.js",这个入口文件是根目录中运行时自动生成的
// vite-plugin-electron版本为0.10.4以上,生成dist-electron目录
"main": "dist-electron/index.js",
// vite-plugin-electron版本为0.9.3以上,生成dist目录
"main": "dist/electron/index.js",
-
打包electron
(1)、安装
npm install electron-builder -D
(2)、修改根目录下 electron/index.ts 文件
const createWindow = () => {
const win = new BrowserWindow({
webPreferences: {
devTools: true,
contextIsolation: false,
nodeIntegration: true //允许html页面上的javascipt代码访问nodejs 环境api代码的能力(与node集成的意思)
}
})
// 打完包【app.isPackaged】这个属性为 true。
console.log('app.isPackaged:', app.isPackaged);
if (app.isPackaged) { // 加载文件。若加载的文件路径不对,也会出现白屏效果
// win.loadFile(path.join(__dirname, "../index.html")) // 0.9.3版本以下。
win.loadFile(path.join(__dirname, "../dist/index.html")) // 0.10.4版本以上
} else { // 加载服务
// 加载。把本地服务网站放入(自带环境变量)
// win.loadURL(`http://${process.env['VITE_DEV_SERVER_HOSTNAME']}:${process.env['VITE_DEV_SERVER_PORT']}`) //vite-plugin-electron插件0.9.3版本以下
win.loadURL(`${process.env['VITE_DEV_SERVER_URL']}`) // vite-plugin-electron插件0.10.4版本以上
}
}
(3)、在 package.json 配置中修改调试中的【build】命令
"build": "vue-tsc && vite build && electron-builder",
(4)、在 package.json 中配置 build
"build": {
"appId": "com.electron.desktop",
"productName": "electron",
"asar": true,
"copyright": "Copyright © 2022 electron",
"directories": {
"output": "release/" // 打包后文件输出的位置
},
"files": [
"dist","dist-electron"
],
"mac": {
"artifactName": "${productName}_${version}.${ext}",
"target": [
"dmg"
]
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
],
"artifactName": "${productName}_${version}.${ext}"
},
"electronDownload": {
"mirror": "https://npm.taobao.org/mirrors/electron/"
},
"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,则使用公司名称 }
(5)、运行
// 执行命令
npm run build
// 运行后根目录生成两个文件【dist 和 release:package.json里面指定的位置】
// 双击 electron_0.0.0.exe 安装
![]()
// 安装后运行,但会呈现白屏。因为上述第二个步骤中的 app.isPackaged为false 不是true

解决方案:
安装 cross-env 代替 app.isPackaged
npm install cross-env
1.修改 electron/index.ts文件中的if 判断
// 不使用原来的app.isPackaged因为永远为false。而通过自己添加的环境变量来区分,开发模式和生产模式
if (process.env.NODE_ENV != 'development') { // 加载文件
// 这么写也会出现白屏。因为我们加载的文件应该是打包后的index文件。而不是源代码中的index文件
// win.loadFile(path.join(__dirname, "../index.html"))
win.loadFile(path.join(__dirname, "../dist/index.html"))
} else { // 加载服务
// 加载。把本地服务网站放入(自带环境变量)
win.loadURL(`${process.env['VITE_DEV_SERVER_URL']}`)
}
2.修改package.josn文件中的调试
"scripts": {
"dev": "cross-env NOOE_ENV=development vite",
"build": "vue-tsc && vite build && electron-builder",
"preview": "vite preview"
},
(6)、出现中文乱码情况
在package.json 文件中添加添加 chcp 65001
-
渲染进程与主进程之间的通信
运用了发布订阅模式
在vite-plugin-electron插件在0.10.4版本后,需要自行安装插件
cnpm i vite-plugin-electron-renderer -D
(1)、在vite.config.ts 中进行修改配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import electron from 'vite-plugin-electron' // 该插件0.9.3版本以前都自带,vite-plugin-electron-renderer
import electronRender from 'vite-plugin-electron-renderer' // vite-plugin-electron的0.10.4版本以后,需要自行下载
export default defineConfig({
plugins: [vue(), electron({
main: { // vite-plugin-electron的0.10.4版本,需要省略main
entry: "electron/index.ts", // 配置入口文件
}
}),electronRender()],
})
(2)、使用
- 渲染进程向主进程传递数据
// 组件中。(渲染进程中发送数据到主进程) import { ipcRenderer } from 'electron' // 导入文件 const open = () => { ipcRenderer.send('message',666) } // electron/index.ts。主进程接收数据 import { ipcMain } from 'electron' ipcMain.on('message',(_,num)=>{ console.log('收到渲染进程的数据',num) })
- 主进程向渲染进程传递数据
// electron/index.ts。主进程向渲染进程,传递参数 import { ipcMain } from 'electron' win.webContents.openDevTools() //打开调试工具 setTimeout(()=>{ win.webContents.send('load',{message:"初始化完成"}) },3000) // 注意:这边时间最好设置3秒以外,否则控制台不打印 // 组件中。接受主进程传递过来的参数 import { ipcRenderer } from 'electron' // 导入文件 ipcRenderer.on('load',(_,value)=>{ console.log('我接受了哦',value); })
11、环境变量
作用:让开发者区分不同的运行环境,来实现 兼容开发和生产
若使用的是vite,vite会在一个特殊的 import.meta.env 对象上暴露环境变量。直接console.log()打印查看
{
"BASE_URL":"/", //部署时的URL前缀
"MODE":"development", //运行模式
"DEV":true," //是否在dev环境
PROD":false, //是否是build 环境
"SSR":false //是否是SSR 服务端渲染模式
}
注意:这个环境变量不能使用动态赋值import.meta.env[key] ,因为这些环境变量在打包的时候是会被硬编码通过JSON.stringify 注入浏览器的。所以会导致生产环境出现问题
// 例如 const BASE_URL = 'BASE_URL' import.meta.env[BASE_URL] = '666'
自定义额外的环境变量
- 在根目录新建 env文件可以创建多个,如:.env.[自定义名]
-
修改启动指令(package.json)
注意:打包不用修改启动指令,会自动查找。
另外打包后点击index.html页面会报跨域错误,因为file://不允许跨域,需要启动服务。在这里我们使用【http-server】
// 安装 npm install -g http-server // 查看安装是否成功 http-server -v // 在需要运行的目录下执行 // # 启动服务器 -p 指定端口号 -o 打开浏览器 http-server -p 6080 -o
在vite.config.ts文件中查看环境变量
import { fileURLToPath, URL } from 'node:url'
// 导入loadEnv
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default ({mode}:any) => {
console.log(mode); // development
// 可以打印出自定义的环境变量(process会报错但不影响) { VITE_HTTP: 'http://www.baidu.com' }
console.log(loadEnv(mode,process.cwd()))
return defineConfig({
plugins: [vue(), vueJsx()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
}
splice(),push(), pop(), shift(), unshift(), sort(), reverse()
type M = { name: string }
但这里有一个需要注意的限制:必须以 vNameOfDirective
的形式来命名本地自定义指令,以使得它们可以直接在模板中使用
transition与transition-group动画组件&&&&&&&&&&&&&&&&&&&&&
// name="fade":自定义的过渡类名(默认是v开头)。appear:页面加载完成立刻执行 <transition name="fade" appear @before-enter="EnterFrom" @enter="EnterActive" @after-enter="EnterTo" @enter-cancelled="EnterCancel" @before-leave="LeaveFrom" @leave="Leave" @after-leave="LeaveTo" @leave-cancelled="LeaveCancelled" > <div v-if="flag" class="box"></div> </transition> <script setup lang='ts'> import { ref } from 'vue' import gsap from 'gsap' // 导入jsap动画库 const flag = ref<boolean>(true) // transition的8个生命周期 const EnterFrom = (el:Element)=>{ gsap.set(el,{ // jsap动画库配合钩子的使用 width:0, height:0 }) console.log('进入之前'); } const EnterActive = (el:Element,done:gsap.Callback)=>{ gsap.to(el,{ width:200, height:200, onComplete:done }) console.log('进入的过度曲线'); } const EnterTo = (el:Element)=>{ console.log('过渡完成'); } const EnterCancel = (el:Element)=>{ console.log('进入过渡效果被打断'); } const LeaveFrom = ()=>{ console.log('离开之前'); } const Leave = (el:Element,done:Function)=>{ console.log('离开过渡曲线'); setTimeout(()=>{ done() },3000) } const LeaveTo = ()=>{ console.log('离开完成'); } const LeaveCancelled = ()=>{ console.log('离开的过渡效果被打断'); } </script> <style scoped> .fade-enter-from { // 进入开始 width: 0; height: 0; } .fade-enter-active { // 进入中 transition: all 1.5s ease; } .fade-enter-to { // 进入结束 width: 200px; height: 200px; } .fade-leave-from{ // 离开开始 width: 200px; height: 200px; } .fade-leave-active{ // 离开中 transition: all 1.5s ease; transform: rotate(360deg); } .fade-leave-to{ // 离开结束 width: 0; height: 0; } </style>