目录
2.1 app.config.globalProperties
3.2.1 添加 Loading 插件页面(loading.vue)
3.2.2 编写 Loading 插件 ts 逻辑(loading.ts)
3.2.3 在 main.ts 中全局注册 Loading 插件
1. Hooks
1.1 Vue2 中的 mixins
1.1.1 mixins 是什么?
将多个组件 相同逻辑 抽离出来,各个组件引入 mixins 进行复用
mixins 的生命周期调用,先于组件内部的生命周期调用
1.1.2 mixins 缺点?
变量来源不明确:隐式传入,不利于阅读
存在覆盖问题:组件的 data、methods、filters 会覆盖 mixins 里同名的 data、methods、filters
1.2 Vue3 中的 Hooks
1.2.1 Vue3 Hooks 是什么?
Vue3 的 Hook 函数 相当于 Vue2 的 mixins,区别在于 Hooks 是函数
Vue3 Hooks 库:
Get Started | VueUseCollection of essential Vue Composition Utilitieshttps://vueuse.org/guide/
1.2.2 Vue3 内置 hooks 举例
先来看下官方提供的一些例子,比如 useAttrs、useSlots...
// 父组件中使用子组件,并传入一些属性
<Test :name="'6666'" :value="8888"></Test>
// 子组件中,使用 useAttrs 获取 attrs
import { useAttrs } from 'vue';
const attrs = useAttrs();
console.log(attrs); // name:.....
1.2.3 自定义 Hooks
定义 hooks.ts:
// hooks 中直接使用 vue 中的 APIs 即可
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(() => {
// 获取图片 DOM
const file: HTMLImageElement = document.querySelector(option.el) as HTMLImageElement;
// 在图片加载完成后,再进行 base64 转换
file.onload = ():void => {
resolve({
Baseurl: toBase64(file)
})
}
})
const toBase64 = (el: HTMLImageElement): string => {
// 创建 canvas
const canvas: HTMLCanvasElement = document.createElement('canvas')
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
// 设置 canvas 宽高
canvas.width = el.width
canvas.height = el.height
// 绘制图片
ctx.drawImage(el, 0, 0, canvas.width, canvas.height)
// 通过这个函数,可以导出一个 base64 格式的图片
return canvas.toDataURL('image/png')
}
})
}
在 .vue 中,使用 hooks.ts:
<template>
<div class="my-demo">
<img id="img" src="../images/test.png" />
</div>
</template>
<script lang="ts">
import { reactive, toRefs, defineComponent } from 'vue';
import useBase64 from './hoo';
export default defineComponent({
setup(props, { emit }) {
useBase64({
el: '#img',
}).then((res) => {
console.log('Base64 格式的图片 ----', res.Baseurl);
});
const state = reactive({});
return {
...toRefs(state),
};
},
});
</script>
2. 全局函数和全局变量
2.1 app.config.globalProperties
Vue2 中,使用 Prototype 定义全局函数和全局变量
Vue.prototype.$http = () => {}
Vue3 中,没有 Prototype,使用 app.config.globalProperties 定义全局函数和全局变量
const app = createApp({})
app.config.globalProperties.$http = () => {}
2.2 过滤器还在吗?如何使用全局变量?
Vue3 中,移除了过滤器,使用 全局函数 替代过滤器
下面在 main.ts 中,声明了一个全局函数:
app.config.globalProperties.$filters = {
format<T extends any>(str: T): string {
return `格式化输出文字 --- {str}`
}
在 main.ts 中,补充声明文件,否则 TypeScript 无法正确推导数据类型,导致页面爆红
type Filter = {
format<T>(str: T): string
}
// 声明要扩充 @vue/runtime-core 包的声明
// 这里扩充 "ComponentCustomProperties" 接口, 因为他是 Vue3 中实例的属性的类型
declare module 'vue' {
export interface ComponentCustomProperties {
$filters: Filter
}
}
setup() 中,通过 getCurrentInstance().proxy.$filters.xxx 使用全局函数
import { getCurrentInstance } from 'vue'
// 获取 实例
const app = getCurrentInstance()
// 通过 实例.proxy 调用全局方法
console.log(app?.proxy?.$filters.format('test'))
2.3 createApp()、proxy 源码分析
关于 createApp():packages\runtime-core\src\apiCreateApp.ts
// 调用初始化函数,返回 createApp 函数
export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
/**
* @param rootComponent 根组件
*/
return function createApp(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) {
rootComponent = { ...rootComponent }
}
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
// 初始化,就是返回了 app、config.globalProperties、mixins 等等之类的对象
const context = createAppContext()
const installedPlugins = new Set()
let isMounted = false
// 将初始化数据,赋值给 app 对象,继续填充属性、版本、方法等
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
version,
get config() {
return context.config
},
set config(v) {
...
},
// 注册插件
use(plugin: Plugin, ...options: any[]) {
...
},
mixin(mixin: ComponentOptions) {
...
},
component(name: string, component?: Component): any {
...
},
directive(name: string, directive?: Directive) {
...
},
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
...
},
unmount() {
...
},
provide(key, value) {
...
}
})
...
// 返回 app
return app
}
}
关于 createAppContext():packages\runtime-core\src\apiCreateApp.ts
export function createAppContext(): AppContext {
return {
app: null as any,
config: {
isNativeTag: NO,
performance: false,
globalProperties: {},
optionMergeStrategies: {},
errorHandler: undefined,
warnHandler: undefined,
compilerOptions: {}
},
mixins: [],
components: {},
directives: {},
provides: Object.create(null),
optionsCache: new WeakMap(),
propsCache: new WeakMap(),
emitsCache: new WeakMap()
}
}
关于 proxy:packages\runtime-core\src\component.ts
instance.accessCache = Object.create(null)
// markRaw 会添加 __skip__ 属性,进而跳过 reactive,防止重复代理
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
ctx 就是 $、$attrs、$data、$el、$emit、$props、$parent、$refs、$root、$slots、$watch...等等
打印 ctx 能够看到 __v_skip: true 表示跳过 reactive,防止对 ctx 进行重复代理
3. Vue3 插件
3.1 什么是插件?怎么使用插件?
举个栗子~
ElementPlus 中的 ElMessage 就可以是一个插件,因为全局皆可使用
使用 createApp() 初始化 Vue 项目后,通过 use() 方法,将插件添加到项目中
3.2 编写 Vue3 插件(实现 Loading 效果)
Vue3 插件支持两种形式:
- 对象形式:要求必须有 install() 方法,Vue3 会将 app 实例 注入 install() 方法
- 函数形式:就直接当 install() 方法去使用
3.2.1 添加 Loading 插件页面(loading.vue)
此文件中需要注意:将外部需要使用的 组件内部方法,进行暴露(defineExpose)
<template>
<div v-if="isShow" class="loading">
<div class="loading-content">Loading...</div>
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue';
// 是否展示 loading
const isShow = ref(false)
const show = () => {
isShow.value = true
}
const hide = () => {
isShow.value = false
}
// 对外暴露 当前组件的 属性和方法,不暴露就不能被使用,会报错
defineExpose({
isShow,
show,
hide
})
</script>
<style scoped lang="less">
.loading {
position: fixed;
display: flex;
justify-content: center;
align-items: center;
inset: 0;
background: rgba(0, 0, 0, 0.8);
&-content {
font-size: 30px;
color: #fff;
}
}
</style>
3.2.2 编写 Loading 插件 ts 逻辑(loading.ts)
Vue3 插件的 对象 形式,必须有 install 函数
Vue3 自动给 install() 内传入 app 实例
createVNode —— Vue3 提供的底层方法,它会给组件创建一个虚拟 DOM,也就是 Vnode
render 把 Vnode 生成真实 DOM,并且挂载到指定节点
不通过 vnode.component.setupState 获取 loading.vue 的方法,因为 loading.vue 没有主动暴露方法的话,Vue3 不推荐在插件中使用 没被暴露的方法
虽然在打印出来的 vnode 中找到了 component.setupState,但是使用会报错
使用 exposed 替代 vnode.component.setupState 方法(.vue 里要使用 defineExpose 导出哦)
import { createVNode, render, VNode, App } from 'vue';
import Loading from './loading.vue'
// Vue3 插件的 对象 形式,必须有 install 函数
export default {
// Vue3 自动给 install() 内传入 app 实例
install(app: App) {
// createVNode —— Vue3 提供的底层方法
// 它会给组件创建一个虚拟 DOM,也就是 Vnode
// 此时打印 vnode,会发现 component 没有值
const vnode: VNode = createVNode(Loading)
// render 把 Vnode 生成真实 DOM,并且挂载到指定节点,此处挂载点是全局 body
// 此时打印 vnode,会发现 component 已经有值了
render(vnode, document.body)
// Vue 提供的全局配置,可以自定义(此处注意命名啊,别跟 elementplus 重复了)
app.config.globalProperties.$loading = {
// 为啥不通过 vnode.component.setupState 获取 loading.vue 的方法?
// 因为 loading.vue 没有主动暴露方法的话,Vue3 不推荐在插件中使用
// 虽然在打印出来的 vnode 中找到了 component.setupState,但是使用会报错
// 使用 exposed 替代上述方法
show: () => vnode.component?.exposed?.show(),
hide: () => vnode.component?.exposed?.hide(),
}
}
}
3.2.3 在 main.ts 中全局注册 Loading 插件
// 引入 Vue3 插件(ts 文件)
import Loading from './components/loading'
const app = createApp(App)
// 注册插件
app.use(Loading)
type Lod = {
show: () => void,
hide: () => void
}
// 编写 ts loading 声明文件
// 目的是为了 防止编写插件时报错,还能顺便增加智能提示
// @vue/runtime-core 可以替换成 vue,但是可能会出现报错
declare module '@vue/runtime-core' {
export interface ComponentCustomProperties {
$loading: Lod
}
}
app.mount('#app')
3.2.4 在 .vue 中使用 Loading 插件
<template>
<div></div>
</template>
<script setup lang='ts'>
import { ref, reactive, getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
// 展示 loading
instance?.proxy?.$Loading.show()
// 5s 后关闭 loading
setTimeout(()=>{
instance?.proxy?.$Loading.hide()
}, 5000)
</script>
3.3 Vue3 中的 app.use() 方法
3.3.1 手写一个 app.use()
添加 my-use.ts
import type { App } from 'vue'
import { app } from './main'
// 定义泛型,要求插件中,必须有 install 函数
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)
}
}
在 main.ts 中使用 my-use.ts
import { myuse } from './my-use'
myuse(Loading);
3.3.2 app.use() 源码分析
packages\runtime-core\src\apiCreateApp.ts
// 注册插件
use(plugin: Plugin, ...options: any[]) {
// 如果当前组件注册过,就进行报错
if (installedPlugins.has(plugin)) {
__DEV__ && warn(`Plugin has already been applied to target app.`)
// 没有注册过,就判断下 plugin 有没有值,里面有没有 install 方法(对象格式)
} else if (plugin && isFunction(plugin.install)) {
// 如果是函数,就将插件添加到缓存中
installedPlugins.add(plugin)
// 调用 install 方法,将 app、用户自定义参数 传进去
plugin.install(app, ...options)
// 函数格式
} else if (isFunction(plugin)) {
installedPlugins.add(plugin)
plugin(app, ...options)
} else if (__DEV__) {
warn(
`A plugin must either be a function or an object with an "install" ` +
`function.`
)
}
return app
},