Skip to main content

Vue-JSX 常用钩子函数

一、getAllElements

用于模拟 React.children 获取所有子元素,在 Arco 组件库中常用。

定义:

import type { Component, Slots, VNode, VNodeTypes } from 'vue'
import _ from 'lodash'

export enum ShapeFlags {
ELEMENT = 1,
FUNCTIONAL_COMPONENT = 1 << 1,
STATEFUL_COMPONENT = 1 << 2,
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT,
TEXT_CHILDREN = 1 << 3,
ARRAY_CHILDREN = 1 << 4,
SLOTS_CHILDREN = 1 << 5,
TELEPORT = 1 << 6,
SUSPENSE = 1 << 7,
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
COMPONENT_KEPT_ALIVE = 1 << 9
}

export const isElement = (vn: VNode) => {
return Boolean(vn && vn.shapeFlag & ShapeFlags.ELEMENT)
}

export const isComponent = (
vn: VNode,
type?: VNodeTypes
): type is Component => {
return Boolean(vn && vn.shapeFlag & ShapeFlags.COMPONENT)
}

export const isTextChildren = (
child: VNode,
children: VNode['children']
): children is string => {
return Boolean(child && child.shapeFlag & 8)
}

export const isArrayChildren = (
vn: VNode,
children: VNode['children']
): children is VNode[] => {
return Boolean(vn && vn.shapeFlag & ShapeFlags.ARRAY_CHILDREN)
}

export const isSlotsChildren = (
vn: VNode,
children: VNode['children']
): children is Slots => {
return Boolean(vn && vn.shapeFlag & ShapeFlags.SLOTS_CHILDREN)
}

export const getAllElements = (
children: VNode[] | undefined,
includeText = false
) => {
const results: VNode[] = []
for (const item of children ?? []) {
if (
isElement(item) ||
isComponent(item) ||
(includeText && isTextChildren(item, item.children))
) {
results.push(item)
} else if (isArrayChildren(item, item.children)) {
results.push(...getAllElements(item.children, includeText))
} else if (isSlotsChildren(item, item.children)) {
results.push(...getAllElements(item.children.default?.(), includeText))
} else if (_.isArray(item)) {
results.push(...getAllElements(item, includeText))
}
}
return results
}

使用:

import { defineComponent, mergeProps } from 'vue';
import { getAllElements } from '../_utils/hook';

export default defineComponent({
name: 'Parent',
setup(props, { slots }) {
return () => {
const children = getAllElements(slots.default?.() ?? []);
const childItems = children.map((child, index) => {
// 这里的顺序决定父子组件属性的优先级谁高谁低
child.props = mergeProps(
child.props ?? {},
{ txt: '传入的值' } // 这里显然父组件传入的属性优先级更高
);
return child;
})

return (
<div>
{childItems}
</div>
);
};
},
});

二、useChildComponentSlots

用于获取指定的子元素,在 TDesign 中常用。

定义:

import { Slots, VNode, Component, getCurrentInstance } from 'vue';

/**
* 渲染 default slot,获取子组件 VNode。处理多种子组件创建场景
* @returns {function(childComponentName: string, slots: Slots): VNode[]}
* @param childComponentName
* @param slots
* @example const getChildByName = useChildComponentSlots()
* @example getChildComponentByName('xxItem')
*/
export const useChildComponentSlots = () => {
const instance = getCurrentInstance();
return (childComponentName: string, slots?: Slots): VNode[] => {
if (!slots) {
slots = instance.slots;
}
const content = slots?.default?.() || [];

return content
.map((item: VNode) => {
if (item.children && Array.isArray(item.children)) return item.children;
return item;
})
.flat()
.filter((item: VNode) =>
(item.type as Component).name?.endsWith(childComponentName)
) as VNode[];
};
};

使用:

import { defineComponent } from 'vue';
import { useChildComponentSlots } from '../_utils/hook';
import Child from './Child.tsx'

export default defineComponent({
name: 'Parent',
setup(props) {
const childItems = () => {
const getChildComponentByName = useChildComponentSlots();
const childrenList = getChildComponentByName('Child')
const carouselItemList = childrenList.map((item: any) => {
return (
// 在这里决定父子组件属性的优先级谁高谁低
<Child txt={item.props?.txt ?? '传入的值'}>
{item.children.default()}
</Child>
);
});
return carouselItemList
}
return () => {
return (
<div>
{childItems()}
</div>
);
};
},
});

三、useWindowResize

用于获取 window 的宽度和高度。

定义:

import { ref, onMounted, onUnmounted } from 'vue';

/**
* 获取 window 的宽度和高度
* @returns width/height
*/
export const useWindowResize = () => {
const width = ref(window.innerWidth);
const height = ref(window.innerHeight);
const handleResize = () => {
width.value = window.innerWidth;
height.value = window.innerHeight;
}

onMounted(() => {
window.addEventListener('resize', handleResize)
});

onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})

return {
width,
height
}
}

使用:

const { width, height } = useWindowResize()

四、usePageVisibility

用于判断用户是否聚焦于网站窗口。

定义:

import { onMounted, onUnmounted } from 'vue';

export const usePageVisibility = (callback = () => { }) => {
let hidden, visibilityChange;
if (typeof document.hidden !== "undefined") {
hidden = "hidden";
visibilityChange = "visibilitychange";
} else if (typeof document.msHidden !== "undefined") {
hidden = "msHidden";
visibilityChange = "msvisibilitychange";
} else if (typeof document.webkitHidden !== "undefined") {
hidden = "webkitHidden";
visibilityChange = "webkitvisibilitychange";
}

const handleVisibilityChange = () => {
callback(document[hidden]);
}

onMounted(() => {
document.addEventListener(visibilityChange, handleVisibilityChange, false);
});

onUnmounted(() => {
document.removeEventListener(visibilityChange, handleVisibilityChange);
});
}

使用:

usePageVisibility((hidden) => {
console.log(`User is${hidden ? ' not' : ''} focus your site`);
});

五、useViewport

用于检测当前设备。

定义:

import { ref, onMounted, onUnmounted } from 'vue';

export const MOBILE = 'MOBILE'
export const TABLET = 'TABLET'
export const DESKTOP = 'DESKTOP'

export const useViewport = (config = {}) => {
const { mobile = null, tablet = null } = config;
let mobileWidth = mobile ? mobile : 768;
let tabletWidth = tablet ? tablet : 922;
let device = ref(getDevice(window.innerWidth));
function getDevice(width) {
if (width < mobileWidth) {
return MOBILE;
} else if (width < tabletWidth) {
return TABLET;
}
return DESKTOP;
}

const handleResize = () => {
device.value = getDevice(window.innerWidth);
}

onMounted(() => {
window.addEventListener('resize', handleResize);
});

onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});

return {
device
}
}

使用:

const { device } = useViewport({ mobile: 700, table: 900 });

六、useScrollToBottom

用于判断用户是否滚动至底部。

定义:

import { onMounted, onUnmounted } from 'vue';

export const useScrollToBottom = (callback = () => { }) => {
const handleScrolling = () => {
if ((window.innerHeight + window.scrollY) >= document.body.scrollHeight) {
callback();
}
}

onMounted(() => {
window.addEventListener('scroll', handleScrolling);
});

onUnmounted(() => {
window.removeEventListener('scroll', handleScrolling);
});
}

使用:

useScrollToBottom(() => { console.log('Scrolled to bottom') })

七、useNetworkStatus

用于检测当前网络状态。

定义:

import { onMounted, onUnmounted } from 'vue';

export const useNetworkStatus = (callback = () => { }) => {
const updateOnlineStatus = () => {
const status = navigator.onLine ? 'online' : 'offline';
callback(status);
}

onMounted(() => {
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
});

onUnmounted(() => {
window.removeEventListener('online', updateOnlineStatus);
window.removeEventListener('offline', updateOnlineStatus);
})
}

使用:

useNetworkStatus((status) => {
console.log(`Your network status is ${status}`);
})

八、useCopyToClipboard

用于获取剪贴板。

定义:

function copyToClipboard(text) {
let input = document.createElement('input');
input.setAttribute('value', text);
document.body.appendChild(input);
input.select();
let result = document.execCommand('copy');
document.body.removeChild(input);
return result;
}

export const useCopyToClipboard = () => {
return (text) => {
if (typeof text === "string" || typeof text == "number") {
return copyToClipboard(text);
}
return false;
}
}

使用:

const copyToClipboard = useCopyToClipboard();
copyToClipboard('just copy');