目录
2.1 app.config.globalProperties
3.2.1 添加 Loading 插件页面(loading.vue)
3.2.2 编写 Loading 插件 ts 逻辑(loading.ts)
3.2.3 在 main.ts 中全局注册 Loading 插件
将多个组件 相同逻辑 抽离出来,各个组件引入 mixins 进行复用

mixins 的生命周期调用,先于组件内部的生命周期调用
变量来源不明确:隐式传入,不利于阅读
存在覆盖问题:组件的 data、methods、filters 会覆盖 mixins 里同名的 data、methods、filters
Vue3 的 Hook 函数 相当于 Vue2 的 mixins,区别在于 Hooks 是函数
Vue3 Hooks 库:
Get Started | VueUseCollection of essential Vue Composition Utilities
https://vueuse.org/guide/
先来看下官方提供的一些例子,比如 useAttrs、useSlots...
- // 父组件中使用子组件,并传入一些属性
- <Test :name="'6666'" :value="8888">Test>
-
- // 子组件中,使用 useAttrs 获取 attrs
- import { useAttrs } from 'vue';
-
- const attrs = useAttrs();
-
- console.log(attrs); // name:.....
定义 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:
- <div class="my-demo">
- <img id="img" src="../images/test.png" />
- div>
-
- <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>
Vue2 中,使用 Prototype 定义全局函数和全局变量
Vue.prototype.$http = () => {}
Vue3 中,没有 Prototype,使用 app.config.globalProperties 定义全局函数和全局变量
- const app = createApp({})
- app.config.globalProperties.$http = () => {}
Vue3 中,移除了过滤器,使用 全局函数 替代过滤器
下面在 main.ts 中,声明了一个全局函数:
- app.config.globalProperties.$filters = {
- format
extends any>(str: T): string { - return `格式化输出文字 --- {str}`
- }
在 main.ts 中,补充声明文件,否则 TypeScript 无法正确推导数据类型,导致页面爆红
- type Filter = {
- format
(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'))
关于 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 进行重复代理
举个栗子~
ElementPlus 中的 ElMessage 就可以是一个插件,因为全局皆可使用
使用 createApp() 初始化 Vue 项目后,通过 use() 方法,将插件添加到项目中
Vue3 插件支持两种形式:
此文件中需要注意:将外部需要使用的 组件内部方法,进行暴露(defineExpose)
- <div v-if="isShow" class="loading">
- <div class="loading-content">Loading...div>
- div>
-
- <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>
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(),
- }
-
- }
- }
- // 引入 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')
- <div>div>
-
- <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>
添加 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
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);
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
- },