二十,兄弟组件传值,Bus
兄弟组件直接的传值,最基础的是通过同一个父级进行数值的传递,使用prop和emit,太过繁琐。
// 父级
<div>
<A @on-click="getFlag"></A>
<B :flag="flag"></B>
</div>
let flag = ref(false);
let getFlag = (params: boolean) => {
flag.value = params;
};
// A组件
<div>A</div>
<button @click="emitB">派发一个事件</button>
let emit = defineEmits(["on-click"]);
let flag = false;
let emitB = () => {
flag = !flag;
emit("on-click", flag);
};
// B组件
<h1>b组件</h1> {{ flag }}
type Props = {
flag: boolean;
};
let props = defineProps<Props>();
那我们有什么更好的方法呢,可以直接进行任意组件通讯呢,当然可以,那就是Bus,底层逻辑就是消息订阅形式,订阅的on就可以接收到发送消息的emit的内容。下面的Bus是手动封装的一个简单的版本,利于理解实现原理。
Bus.ts封装,Bus类中包含 on注册方法,emit传递参数,执行方法,通过List进行调度。
// 订阅派发 类
type BusClass = {
emit:(name:string)=>void
on:(name:string,callback:Function)=>void
}
type ParamsKey = string | number | symbol
type List = {
// key值是动态的,对象签名方式
// value 返回可以是多个的,用 Array ,返回的是callback
[key:ParamsKey] : Array<Function>
}
// 约束 implements
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,this指向,可以调用on(),args传入数组
fn.apply(this,args)
})
}
// on 可以多次注册,名字一样,支持多个。
on(name:string,callback:Function){
let fn:Array<Function> = this.list[name] || []
fn.push(callback)
this.list[name] = fn
}
}
export default new Bus()
那使用方法也很简单,Bus.emit(name,args) 和 Bus.on(name,fn) ,不用涉及到父级是谁。
// A组件
import Bus from "../../Bus";
let emitBBus = ()=>{
flag = !flag
Bus.emit('a-click',flag)
}
// B组件
let flag2 = ref(false);
Bus.on("a-click", (flag: boolean) => {
flag2.value = flag;
});
我们还可以使用第三方的Mitt来实现,封装的更加完善和功能扩充。
先安装包 mitt,然后在main.ts中全局引入,就可以使用了。
// main.ts
import mitt from "mitt";
const Mitt = mitt();
// 全局ts声明
declare module "vue" {
export interface ComponentCustomProperties {
$Bus: typeof Mitt;
}
}
// 挂载全局
app.config.globalProperties.$Bus = Mitt;
// A组件
const instance = getCurrentInstance()
let emitBMit = ()=>{
instance?.proxy?.$Bus.emit('on-xx','mitt')
}
// B组件
const instance = getCurrentInstance();
let fn = (str: any) => {
console.log(str, "============str");
};
instance?.proxy?.$Bus.on("on-xx", fn); // 监听
// instance?.proxy?.$Bus.off('on-xx',fn) // 停止
要注意的是,instance?.proxy? 这里相当于 vue2中的this,这样可以获得实例的绑定属性,得到$Bus。也可以监听所有的事件,清除所有的事件。
// * 代表监听所有事件,回调中多一个参数
instance?.proxy?.$Bus.on('*',(type,str)=>{
console.log(type,str,'============str');
})
// 删除所有的方法
instance?.proxy?.$Bus.all.clear()
二十一,tsx的使用
如果想使用tsx的语法(类似于react的编写方式),需要先安装 npm i @vitejs/plugin-vue-jsx -D,然后在 vite.config.ts 中进行注册,plugins里,函数形式。
这样我们就可以使用tsx了,新建一个app.tsx的文件,尝试编写。
1,首先是函数渲染形式,没有很复杂的逻辑处理和状态。
export default function(){
return (<div>小小</div>)
}
2, vue中defineComponent,导出一个对象形式,类似于vue2的写法。
export default defineComponent({
data() {
return{
age:34
}
},
render(){
// tsx 的变量用单花括号
return (<div>{this.age}</div>)
}
})
3,vue3的写法,setup函数使用。在setup中处理逻辑,return返回html内容。使用的是表达式的方式,所以v-if是不支持的。
export default defineComponent({
// 变量写法: setup函数模式:支持v-show
setup(){
// ref 在 template 中会自动解包的(.value) tsx中不会自动解包,需要手动
let flag = ref(false)
// return () => (<div v-show={flag.value}>setup</div>)
// v-if是不支持的,可以使用js的写法处理(三元表达式)
// return () => (<div v-if={flag.value}>setup</div>)
return () => (
<>
<div>{flag.value?'v-if的js写法':'false'}</div>
</>
)
}
})
4,setup参数,更加复杂的使用。包括,props,emits,插槽,传值。这里要注意v-slots={slots}的用法,是传入一个对象,有key值(具名插槽,default),value是对应的展示内容。
interface Props{
name?:string
}
// 插槽 定义一个渲染函数
const A = (_,{ slots })=>(
<>
<div>{slots.default ? slots.default():'默认值'}</div>
<div>{slots.foo?.()}</div>
</>
)
export default defineComponent({
props:{
name:String
},
emits:['back-click'],
// setup参数:第一个:props 第二个对象:emit
setup(props:Props,{emit}){
let data = [
{name:'小小1'},
{name:'小小2'},
{name:'小小3'}
]
let fn = (item:any)=>{
console.log('触发事件',item)
emit('back-click',item) // 在emits中声明的事件名
}
let slots = {
default:()=>(<div>default slots</div>),
foo:()=>(<div>foo slots</div>)
}
let ipt = ref<string>('')
return ()=>(
<>
<input type="text" v-model={ipt.value}/>
<div>{ipt.value}</div>
<hr />
<A v-slots={slots}></A>
<hr/>
<div>props:{props?.name}</div><hr/>
{/* 数组没有办法使用v-for,用js的循环思想 */}
{/* 单花括号,属性绑定,代替v-bind */}
{/* 事件使用 on+类型 ,用函数形式,避免一上来就被调用 */}
{data.map(v=>{
return <div onClick={()=>fn(v)} name={v.name}>{v.name}</div>
})}
</>
)
}
})
知识点:setup() 参数,参数一,props传值,可以使用toRef,toRefs进行解构。参数二,context上下文,是个非响应式对象,其中包括attrs,slots,emit,expose.
二十二,尝试手写简单的tsx插件 ☆
知识铺垫:
babel 代码转换,es6->es5 将低版本不识别函数语法进行转换
babel 主要有3个核心功能:
源代码--(编译器parse)-- 抽象语法树AST -- (转换过程transform)-- 修改后的AST -- (生成器generator) -- 转换后代码
实现 tsx 插件的5个依赖包:
- @vue/babel-plugin-jsx 编译v-show等指令
- @babel/core babel核心库
- @babel/plugin-transform-typescript 编译ts
- @babel/plugin-syntax-import-meta 编译import
- @types/babel__core 声明文件
现在的plugin的注册形式是函数,所以写一个export default function(){},类型是 Plugin。
import type {Plugin} from 'vite'
import * as babel from '@babel/core' // 核心库,将源代码转换为目标代码
import jsx from '@vue/babel-plugin-jsx' // vue给babel写的插件支持tsx v-model等
export default function():Plugin{
return {
// Plugin 的名称是有要求的 vite-plugin 开头
name:'vite-plugin-vue-tsx',
config(){
return {
// 使用 esbuild 编译ts文件(默认使用的是react.createElement
esbuild:{
include:/.ts$/
}
}
},
async transform(code,id){
// code 是 转换代码,id是路径。
if(/.tsx$/.test(id)){
// console.log(code,id,'>>');
// ts忽略下一行的检测(没有声明文件)
// @ts-ignore
const ts = await import('@babel/plugin-transform-typescript').then(r=>r.default)
// babel的转换功能 异步的
const res = await babel.transformAsync(code,{
ast:true,
configFile:false,
babelrc:false,
plugins:[jsx,[ts,{isTSX:true, allowExtensions:true}]]
})
console.log(res?.code);
return res?.code
}
return code
}
}
}
二十三,自动引入
vue3使用的是import引入vue里的使用的内容,可以有效的tree-shaking,但是也很繁琐,那么可以使用 unplugin-auto-import 插件来自动引入模块。该插件的作用是识别当前代码中所使用的未导入的模块,并自动根据需要将它们导入到代码中。
unplugin-auto-import 不是一个独立的插件,而是一个适用于不同编辑器和构建工具的插件集合。这里展示的是使用 unplugin-auto-import 在 Vite 构建工具中的配置方式。
在 vite.config.ts 中引入并且注册。也会生成对应的dts文件。
import AutoImport from 'unplugin-auto-import/vite'
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports:['vue'],
dts:'src/auto-import.d.ts', // dts 声明文件
})
]
})
二十四,v-model使用
v-model是实现双向绑定的,对比vue2的可以说是破坏性的更新。
对比vue2 :
- prop:value => modelValue
- 事件: input => update:modelValue
- v-bind的 .sync 修饰符和组件的model移除
- 支持多个v-model
- 新增自定义,修饰符 Modifiers
通过下面的例子来实现 自定义的v-model
// 父组件
<div>
<h1>父组件</h1>
<div>isShow:{{ isShow }}</div>
<div>text:{{ text }}</div>
<button @click="isShow = !isShow">开关</button>
<hr>
<vModelVue v-model="isShow" v-model:textVal.isBt="text"></vModelVue>
</div>
// 子组件
<div v-if="modelValue" class="model">
<div class="close"><button @click="close">关闭</button></div>
<h3>v-model 子组件 dialog</h3>
<div>内容: <input @input="changetext" type="text" :value="textVal"></div>
</div>
子组件的接收很重要,modelValue默认是v-model的对应值,那自定义的则需要父级
v-model:xxx='' 的形式定义,自己在props中接收 xxx,作为对应值,那更改xxx时,就需要emit以update:xxx 的形式进行响应。
同时还有v-model的自定义修饰符,比如内置的 lazy,number,trim等,例子使用的是 .isBt ,那在接收时用 xxxModifiers 的形式作为 xxx 的修饰符。
const props = defineProps<{
modelValue:boolean,
textVal:string,
textValModifiers?:{ // 传入的修饰符
isBt:boolean
}
}>()
// 子组件改父组件的内容,固定语法update:modelValue
const emit = defineEmits(['update:modelValue','update:textVal'])
let close = () => {
emit('update:modelValue',false)
}
let changetext = (e:Event)=>{
const target = e.target as HTMLInputElement
emit('update:textVal',props?.textValModifiers?.isBt ? target.value+' 变态 ' : target.value)
}
二十五,自定义指令 directive
directive 在vue3也是破坏性更新。生命周期大调整。
vue2: bind,inserted,update,componentUpdated,unbind
vue3:created,beforeMount,mounted,beforeUpdate,updated,beforeUnmount,unmounted
//vue2的形式
Vue.directive('focus', {
//每当指令绑定到元素上时,会立即执行这个bind函数,只执行一次
bind: function () {
},
//inserted表示元素插入到DOM中时,会执行inserted函数,
//insert方法只触发一次,el表示被绑定的那个标签元素
inserted: function (el,binding) {
console.log(binding.name) // 标签名
console.log(binding.value) // 值
console.log(binding.expression) // 表达式
el.focus()
},
//当VNode更新时会执行updated,可能触发多次
updated:function(){
}
})
// vue3的形式
// 自定义指令命名: 必须以 vNameOfDirective 的形式来命名,可以直接在模板中使用
const vMove: Directive = {
created() {
console.log("========created,初始化");
},
beforeMount() {
console.log("========beforeMount,绑定到元素后,只调用一次");
},
/*
参数: 通过 ...args:Array<any> 传入打印
el:当前绑定这个指令的元素。
dir: 传过来的值都会放在这里
vnode: 虚拟dom
prenode: 上一个虚拟dom
*/
mounted(el: HTMLElement, dir: DirectiveBinding<Dir>) {
console.log("========mounted,插入父级dom调用");
console.log(dir.value.background);
el.style.background = dir.value.background;
},
beforeUpdate() {
console.log("========beforeUpdate,元素更新之前");
},
updated() {
console.log("========updated,更新后调用");
},
beforeUnmount() {
console.log("========beforeUnmount,在元素移除之前");
},
unmounted() {
console.log("========unmounted,被移除之后,调用一次");
},
};
v-if 触发的是 beforeUnmount,unmounted;属性改变,触发的是beforeUpdate,updated。还可以自定义参数,修饰符,可以在dir中得到。
<!-- v-if 触发beforeUnmount,unmounted -->
<A v-move:aaa.xiaoxiao="{ background: 'red' }" v-if="flag"></A>
<!-- 元素变了,触发beforeUpdate,updated -->
<A v-move:aaa.xiaoxiao="{ background: 'green', flag: flag }"></A>
<!-- 也可以自定义参数,修饰符,可以在dir中得到 -->
在具体实战中呢,指令会用在哪里呢,比如权限控制,弹框的拖拽,那以下就是两个例子的实现。
1,权限控制,我们可以在mock中得到权限数组,实战中是接口获得,我这里直接用数组表示;通过指令来判断是否具有这个权限,可以在元素上进行控制。
// html
<button v-has-show="'shop:create'">创建</button>
<button v-has-show="'shop:edit'">编辑</button>
<button v-has-show="'shop:delete'">删除</button>
//js
let permission = [
'xiaoxiao-id:shop:create',
'xiaoxiao-id:shop:edit',
// 'xiaoxiao-id:shop:delete',
]
let userid = localStorage.getItem('userid') as string
const vHasShow:Directive<HTMLElement,string> = (el,binding)=>{
if(! permission.includes( userid + ':' + binding.value ) ){
el.style.display = 'none'
}
}
2,弹框的拖拽 v-move,针对这个指令的元素,都可以进行拖拽移动。
import type { Directive, DirectiveBinding } from "vue";
const vMove: Directive<any, void> = (
el: HTMLElement,
binding: DirectiveBinding
) => {
let moveElement: HTMLDivElement = el.firstElementChild as HTMLDivElement;
// console.log(moveElement);
const mousedown = (ev: MouseEvent) => {
// 初始位置
let x = ev.clientX - el.offsetLeft;
let y = ev.clientY - el.offsetTop;
const move = (e: MouseEvent) => {
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);
});
};
// 按下
moveElement.addEventListener("mousedown", mousedown);
};