前言
在日常开发过程中,有时候我们会希望全屏一个元素。但是,仅仅是期望它是处于网页全屏状态,而不是浏览器全屏状态。这时候,我们可以统一封装一个 useElementFullscreen
方法,来实现这个功能。
方法分析
在实现这个功能之前,我们先分析一下我们需要这个方法做什么?
功能需求分析
期望传递一个元素,然后这个元素就可以全屏显示。并且仅仅是使用 js + css
实现。
方法设计
- 参数一:元素
- 参数二:方法配置项
实现前需要考虑的问题
在实现这个功能之前,我们需要考虑一下,我们需要解决哪些问题?
如何全屏一个元素
我们分析一下,如何使用 css + js
全屏一个元素?
肯定是先获取到元素,然后将其宽高铺满整个屏幕,然后将其 position
设置为 fixed
,然后将其 top
和 left
设置为 0
,这样就可以实现全屏了。
如何退出全屏
将所有的样式还原即可。
样式注入与属性注入可能产生的问题
由于是手动通过 css + js
的方式,使一个元素实现铺满效果,所以,我们需要考虑一下,如果该元素本身包含一些基础样式,那么我们的样式注入可能会覆盖掉这些样式,导致元素样式错乱。
如何解决这个问题是一个需要考虑的问题。我们后续会在实现的时候,考虑这个问题。
实现
辅助函数
ts
import type { ComponentPublicInstance } from 'vue'
export type TargetValue<T> = T | undefined | null
export type TargetType =
| HTMLElement
| Element
| SVGElement
| Window
| Document
| ComponentPublicInstance
export type BasicTarget<T extends TargetType = Element> =
| (() => TargetValue<T>)
| TargetValue<T>
| Ref<TargetValue<T>>
/**
*
* @param target 获取 ref dom, vue instance 的 dom
* @param defaultTarget 默认值
*
* @example
* <template>
* <div ref="refDom"></div>
* </template>
*
* const refDom = ref<HTMLElement | null>(null)
* const computedDom = computed(() => refDom.value)
*
* unrefElement(refDom) => div
* unrefElement(computedDom) => div
*/
function unrefElement<T extends TargetType>(
target: BasicTarget<T>,
defaultElement?: T,
) {
if (!target) {
return defaultElement
}
let targetElement: TargetValue<T>
if (typeof target === 'function') {
targetElement = target()
} else if (isRef(target)) {
targetElement =
(target.value as ComponentPublicInstance)?.$el ?? target.value
} else {
targetElement = target
}
return targetElement
}
/**
*
* @param fc effect 作用域卸载时需执行函数
*
* @remark 返回 true 表示获取到 effect 作用域并且卸载;false 表示未存在 effect 作用域
*/
export function effectDispose<T extends (...args: any[]) => any>(fc: T) {
if (getCurrentScope()) {
onScopeDispose(fc)
return true
}
return false
}
正式实现
ts
import { isUndefined, isNull } from 'lodash-es'
import { useWindowSize } from '@vueuse/core'
export interface UseElementFullscreenOptions {
beforeEnter?: () => void
beforeExit?: () => void
zIndex?: number
backgroundColor?: string
}
let currentZIndex = 999
let isAppend = false
const ID_TAG = 'ELEMENT-FULLSCREEN-RAY'
const { height } = useWindowSize() // 获取实际高度避免 100vh 会导致手机端浏览器获取不准确问题
const styleElement = document.createElement('style')
export const useElementFullscreen = (
target: BasicTarget,
options?: UseElementFullscreenOptions,
) => {
const { beforeEnter, beforeExit, backgroundColor, zIndex } = options ?? {}
const cacheStyle: Partial<CSSStyleDeclaration> = {} // 缓存一些需要被覆盖的样式,例如: transition
let isSetup = false
const updateStyle = () => {
const element = unrefElement(target) as HTMLElement | null
if (!element) {
return
}
const { left, top } = element.getBoundingClientRect()
const cssContent = `
#${ID_TAG} {
position: fixed;
width: 100% !important;
height: ${height.value}px !important;
transform: translate(-${left}px, -${top}px) !important;
transition: all 0.3s var(--r-bezier);
z-index: ${
isValueType<null>(zIndex, 'Null') ||
isValueType<undefined>(zIndex, 'Undefined')
? currentZIndex
: zIndex
} !important;
background-color: ${backgroundColor ?? ''};
}
`
styleElement.innerHTML = cssContent
// 避免重复添加 style 标签
if (!isAppend) {
document.head.appendChild(styleElement)
}
}
const enter = () => {
const element = unrefElement(target) as HTMLElement | null
beforeEnter?.()
if (element) {
if (!element.getAttribute(ID_TAG)) {
element.setAttribute(ID_TAG, ID_TAG)
}
if (!isSetup) {
isSetup = true
currentZIndex += 1
}
if (!isAppend) {
updateStyle()
isAppend = true
}
cacheStyle.transition = element.style.transition
element.style.transition = 'all 0.3s var(--r-bezier)'
element.setAttribute('id', ID_TAG)
}
}
const exit = () => {
beforeExit?.()
const element = unrefElement(target)
if (element) {
element.removeAttribute('id')
element.removeAttribute(ID_TAG)
}
}
const toggleFullscreen = () => {
const element = unrefElement(target)
if (element) {
if (element.getAttribute(ID_TAG)) {
exit()
} else {
enter()
}
}
}
const stopWatch = watch(() => height.value, updateStyle)
effectDispose(() => {
const element = unrefElement(target) as HTMLElement | null
if (element) {
element.style.transition = cacheStyle.transition ?? ''
element.removeAttribute(ID_TAG)
element.removeAttribute('id')
}
stopWatch()
})
return {
enter,
exit,
toggleFullscreen,
}
}
代码解释
如何解决样式注入与属性注入可能产生的问题
在实现的时候注入了一个 style
标签,这个标签的作用是用来覆盖元素的样式,使其全屏。
并且对于传递元素注入了 id
, ID_TAG
属性。
currentZIndex
这个变量的作用是用来记录当前全屏元素的 z-index
值,每次全屏一个元素,都会自增 1
。
isAppend
这个变量的作用是用来记录 style
标签是否已经被添加到 head
中。
isSetup
标记是否是初始化状态,如果是初始化状态,那么 currentZIndex
就会自增 1
。
使用演示
vue
<template>
<div ref="demoRef">
<div @click="enter">全屏</div>
<div @click="exit">退出全屏</div>
<div @click="toggleFullscreen">切换全屏</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const { enter, exit, toggleFullscreen } = useElementFullscreen(demoRef)
</script>
最后
其实该方法还是有一些问题没有解决:
- 会注入
id
,导致覆盖原有的id
,可能会导致一些问题 - 全局注入了一个
style
标签,可能会导致一些问题 - 方法入侵性很强,并且需要开发者自己考虑
dom
元素结构。否则在调用的时候会出现因为样式覆盖导致样式错乱的问题
并且,这个方法通用性没有那么强。适合在项目中提前约定好某些功能使用,并且在使用的时候,需要开发者自己已经考虑了一些问题。
如果大家有更好的思路,请多多指教~