Vue3学习笔记4 自定义hooks、globalProperties、filters、自定义插件、样式穿透、nextTick

寒假学的时候属于是有点过于敷衍了,现在update一下😮‍💨
最近是跟ts一起学的,所以里面会有ts相关的内容(无伤大雅无伤大雅


1. 自定义hooks

1.1 介绍及使用

hook本质上是一个函数
作用是将setup函数中使用的Composition API(ref、reactive、computed、watch、生命周期)进行了封装

一般使用:

  • 将可复用功能抽离为外部js文件
  • 函数名文件名以use开头,形如useXXX
  • 引用时,在setup中解构出自定义hooks的变量和方法
    如: const { name } = useXXX()

在A组件中想要实现一个小功能:鼠标触摸页面的时候,能够拿到鼠标的坐标并显示在页面上

<!-- A.vue -->
<script setup lang="ts">
import { reactive, onMounted, onBeforeUnmount } from "vue";
// 数据
let point = reactive({
	x: 0,
	y: 0,
})
// 方法
const savePoint = (e: MouseEvent) => {
	point.x = e.pageX;
	point.y = e.pageY;
}
// 生命周期钩子
onMounted(() => {
	window.addEventListener("mousemove", savePoint);
})
onBeforeUnmount(() => {
	window.removeEventListener("mousemove", savePoint);
})
</script>

如果其他组件也想使用这个功能,我们可以把关于这个功能的数据、方法、生命周期钩子都提出去封装起来

新建一个文件夹hooks,新建一个文件usePoint.ts

// usePoint.ts
import {reactive,onMounted,onBeforeUnmount,toRefs} from 'vue';
export default function () {
  // 数据
  let point = reactive({
    x: 0,
    y: 0
  })
  // 方法
  const savePoint = (e: MouseEvent) => {
    point.x = e.pageX;
    point.y = e.pageY;
  }
  // 生命周期钩子
  onMounted(() => {
    window.addEventListener('mousemove',savePoint)
  })
  onBeforeUnmount(() => {
    window.removeEventListener('mousemove',savePoint)
  }) 
  // 这里用toRefs,方便引入时解构
  return toRefs(point);
}

修改A组件中的代码

<script setup lang="ts">
import usePoint from "../hooks/usePoint";
const { x, y } = usePoint();
</script>

<template>
  <h1>{{ x }}, {{ y }}</h1>
</template>

1.2 VueUse

VueUse是vue3的一个hook库,帮我们封装了一些常用的功能
还有很多,可以去官网看:
https://vueuse.org/guide/

安装:
npm i @vueuse/core
使用:
跟我们自己封装的hook使用方法是一样的
这里也是使用了一个VueUse中获取鼠标坐标的hook

<script setup lang="ts">
import { useMouse } from '@vueuse/core'

const { x, y } = useMouse()
</script>

<template>
  <h1>{{ x }}, {{ y }}</h1>
</template>

1.3 对比vue2Mixin

Minxin的不足:
1.mixin容易发生冲突,每个mixinproperty都被合并到同一个组件中,难以追溯其到底来自哪个mixin

export default {
  mixins: [a,b,c,d,e],  // 一个组件中可以混入各种功能的mixin
  mounted() {
    console.log(this.name); // 这个name来自哪个mixin?
  }
}

但在vue3自定义hooks中:const { name } = useX(), 就很清晰可以知道来自哪个hook,便于维护管理
2. 无法给mixin传递参数来改变逻辑
可以通过调用mixin内部方法来传递参数,却无法直接给Mixin传递参数

export default {
  mixins: [addMixin],  // 一个组件中可以混入各种功能的mixin
  mounted() {
    this.add(num1, num2)  //调用addMixin内部的add方法
  }
}

vue3自定义hooks可以灵活传递任何参数来改变它的逻辑

形象的讲法:vue3自定义hooks是组件下的函数作用域,vue2mixin是组件下的全局作用域,全局作用域有时是不可控的


2. globalProperties

在vue2.x中我们定义全局变量和函数的时候是把他们加在Vue.prototype
vue3.x中要用app.config.globalProperties代替

// vue2.x
Vue.prototype.$api = () => {}

// vue3.x
const app = createApp({})
app.config.globalProperties.$api = () => {}

使用:

<script setup lang="ts">
import { getCurrentInstance } from 'vue'

const instance = getCurrentInstance();
instance?.appContext.config.globalProperties.$api.xxx
// 也可以这样用:instance?.proxy?.$api.xxx 
</script>

⚠️:getCurrentInstance()必须在setup生命周期钩子中调用,在其他地方调用会为null
⚠️:如果是在模板中用,双括号中可以直接访问挂载在全局的属性,不用getCurrentInstance


3. filters(vue3已移除)

(过滤器filters在vue3中已经被移除了,下面介绍一下vue3中如何替代过滤器)

在vue2.x中,过滤器一般用于一些常见的文本格式化,可以放在双花括号内或者v-bind表达式中
简单复习一下:(其实也没什么必要只是我之前还真没用过这个😢

<template>
  <!-- account经accountInUSD过滤器过滤 -->
  <!-- 相当于 {{ accountInUSD(account) }} -->
  <p>{{ account | accountInUSD }}</p>
</template>

<script>
  export default {
    props: {
      account: {
        type: Number
      }
    },
    filters: {
      accountInUSD(value) {
        return '$' + value
      }
    }
  }
</script>

3.1 局部过滤器

官方推荐局部过滤器可以用方法调用或者计算属性来替换
计算属性怎么写这里就略过了,如何传值看我这篇博客:
vue 计算属性传参数

3.2 全局过滤器

那如果是全局过滤器,用计算属性或方法调用就不太方便了,就可以通过👆说过的全局属性globalProperties让所有组件能使用到

// main.ts
const app = createApp(App)

interface IFilter {
  accountInUSD: (value: number) => string
}
// 不声明用的时候ts会报错,声明之后在模板中用$filters会有提示
declare module 'vue' {
  export interface ComponentCustomProperties {
    $filters: IFilter
  }
}
// 这个$filters对象里可以定义多个函数
app.config.globalProperties.$filters = {
  accountInUSD(value: number): string {
    return '$' + value
  }
}

然后在模板上用这个$filters对象:

<template>
  <p>{{ $filters.accountInUSD(account) }}</p>
</template>

4. 自定义插件

4.1 简单了解一下

如果插件是一个对象,必须提供install()方法
如果插件是一个函数,它会被作为install()方法
install()方法调用时,会将Vue作为参数传入

使用插件:app.use(xxx)

4.2 loading案例

案例:实现一个loading插件

1.components文件夹下新建一个loading文件夹,下面再建index.vueindex.ts
2./loading/index.vue

<!-- index.vue -->
<script setup lang="ts">
import { ref } from "vue";
const isShow = ref<boolean>(false);

const show = () => {
	isShow.value = true;
};
const hide = () => {
	isShow.value = false;
};
// 这里注意属性和方法一定要暴露出去
defineExpose({
  isShow,
  show,
  hide,
});
</script>

<template>
  <div v-if="isShow" class="loading">
	<div class="loading-content">Loading...</div>
  </div>
</template>

<style lang="less" scoped>
.loading {
  position: fixed;
  background-color: #000;
  display: flex;
  justify-content: center;
  align-items: center;
  &-content {
	font-size: 30px;
	color: #fff;
  }
}
</style>

3./loading/index.ts

import { App, createVNode, VNode, render } from 'vue'
import Loading from './index.vue'

// 这里我们将参数定为一个对象,这个对象必须有install方法,参数是vue
export default {
  install (app: App) {
    // 给组件创建一个虚拟dom
    const vnode:VNode = createVNode(Loading)
    // 用render函数转成真实dom,第二个参数是挂载在哪个节点
    render(vnode, document.body)
    // show()和hide()挂载在全局
    app.config.globalProperties.$loading = {
      show: vnode.component?.exposed?.show,
      hide: vnode.component?.exposed?.hide
    }
  }
}

4.main.ts中使用Loading插件,并写$loading的声明

import { createApp } from 'vue'
import App from './App.vue'
import Loading from './components/loading'

const app = createApp(App)

app.use(Loading)

declare module 'vue' {
  export interface ComponentCustomProperties {
    $loading: {
      show: () => void,
      hide: () => void
    }
  }
}

5.在某个组件中使用

<script setup lang="ts">
import { getCurrentInstance } from "vue";
const instance = getCurrentInstance();
const showLoading = () => {
  instance?.appContext.config.globalProperties.$loading.show();
};
const hideLoading = () => {
  instance?.appContext.config.globalProperties.$loading.hide();
}
</script>

<template>
  <button @click="showLoading">show loading</button>
  <button @click="hideLoading">hide loading</button>
</template>

5. 样式穿透:deep()

主要用于修改常用的组件库,有些组件库的样式是需要我们改的,但是用了scoped之后常常发现样式加不上去
举个例子:

<script setup lang="ts">
import { ref } from "vue";
const input = ref("");
</script>

<template>
  <el-input v-model="input" placeholder="Please input" class="my-input" />
</template>

<style lang="less" scoped>
.my-input {
  .el-input__inner {
	background-color: red;
  }
}
</style>

上面这一段样式,没有生效
但是我们的这个样式实际上是加上了的,是因为scoped会在css语句末尾给我们加上属性选择器[data-v-xxx],但是这个.el-input__inner上面并没有这个属性选择器,这个属性选择器在它的父级.my-input身上
在这里插入图片描述
在vue2中,解决方法:用/deep/修改属性选择器的位置

<style lang="less" scoped>
.my-input {
  /deep/ .el-input__inner {
	background-color: red;
  }
}
</style>

这样把属性选择器换到.my-input后面,而.my-input后面是有属性选择器的,就可以生效了
在这里插入图片描述
在vue3中推荐使用:deep()

<style lang="less" scoped>
.my-input {
  :deep(.el-input__inner) {
	background-color: red;
  }
}
</style>

6. :slotted()、:global()、:v-bind()

6.1 插槽选择器:slotted()

举例:
子组件A中定义一个插槽,在父组件App中引入

<!-- A.vue -->
<template>
  <slot></slot>
</template>

<style lang="less" scoped>
  h1 {
    color: red;
  }
</style>
<!-- App.vue -->
<template>
  <A>
	<h1>AAAA</h1>
  </A>
</template>

看下效果:
会发现在A组件中设置的样式未生效
在这里插入图片描述
(其实还是scoped的锅
在这里插入图片描述

解决方法:在A组件中使用插槽选择器

:slotted(h1) {
  color: red;
}

在这里插入图片描述

6.2 全局选择器:global()

之前我们想要加入全局样式,通常都是新建一个不加scoped的style标签

<!-- App.vue -->
<style>
  h1 {
    color: red
  }
</style>
 
<style lang="less" scoped>
 
</style>

现在可以用更优雅的解决方式:全局选择器:global()

<!-- App.vue --> 
<style lang="less" scoped>
:global(h1) {
  color: red
}
</style>

6.3 动态css v-bind()

<script lang="ts" setup>
import { ref } from 'vue'
const red = ref<string>('red')
</script>

<template>
  <div class="box">xxxxxx</div>
</template>
 
<style lang="less" scoped>
.box {
   color:v-bind(red)
}
</style>

如果是对象,要加引号:

<script lang="ts" setup>
import { ref } from 'vue'
const red = ref({
  color: 'red'
})
</script>

<template>
  <div class="box">xxxxxx</div>
</template>
 
<style lang="less" scoped>
.box {
   color:v-bind('red.color')
}
</style>

7. nextTick

更改了一些数据后,需要等dom更新完再执行的操作放在await nextTick后面,(不要忘记包裹在async函数里

<script setup lang='ts'>
import { ref,nextTick } from 'vue';
 
const text = ref<string>('poem')
const box = ref<HTMLElement>()
 
const change = async () => {
   // 更改了一些数据
   text.value = 'change poem'
   // dom还未更新就会执行这句话
   console.log(box.value?.innerText) // poem
   await nextTick();
   // 现在dom更新了
   console.log(box.value?.innerText) // change poem
}
</script>

<template>
   <div ref="box">
      {{ text }}
   </div>
   <button @click="change">change box</button>
</template>

8. 移动端适配解决方案

  • postcss-pxtorem+amfe-flexable
  • postcss-px-to-viewport

8.1 postcss-pxtorem+amfe-flexable

amfe-flexable:
能根据设备的宽高来设置html的font-size,将1rem设置为设备宽度➗10
并且页面宽度变化时会自动重新计算html的font-size

postcss-pxtorem:
将对应的像素单位转换为rem

安装:

npm i postcss-pxtorem -D
npm i amfe-flexible -D

main.ts中引入amfe-flexible

import 'amfe-flexible'

vite.config.ts中配置postcss-pxtorem:

import { defineConfig } from 'vite'
import postCssPxToRem from 'postcss-pxtorem'

export default defineConfig({
  ...
  css: {
    postcss: {
      plugins: [
        postCssPxToRem({
          rootValue: 37.5,	// 1rem的大小
          propList: [*]		// 需要转换的属性,全部转换
        })
      ]
    }
  }
})

8.2 postcss-px-to-viewport

postcss-px-to-viewport会将对应的像素单位自动转换为视口单位

安装:

npm i postcss-px-to-viewport -D

vite.config.ts中配置postcss-pxtorem:

import { defineConfig } from 'vite'
import postCssPxToViewport from 'postcss-pxtorem'

export default defineConfig({
  ...
  css: {
    postcss: {
      plugins: [
        postCssPxToViewport({
          unitToConvert: 'px', // 要转化的单位
          viewportWidth: 750,  // 设计稿的宽度
        })
      ]
    }
  }
})

参考博文:
https://juejin.cn/post/7083401842733875208
https://xiaoman.blog.csdn.net/article/details/125237755
https://blog.csdn.net/weixin_43554584/article/details/121345084?utm_source=app&app_version=5.0.0

官网
https://v3.cn.vuejs.org/api/global-api.html#nexttick

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值