全屏解决方案:原生、screenfull、useFullscreen,我该用哪个?

7,427 阅读5分钟

vue项目中有一个全屏功能,我该怎么办?本文结合实际的vue项目介绍了浏览器原生的全屏API、 screenfull、useFullscreen的用法并结合源码分析了screenfull和useFullscreen的原理,欢迎阅读学习。

1.原生requestFullScreen等

原生的全屏API有如下几个:

(1)Element.requestFullscreen():用于发出异步请求使元素进入全屏模式;

(2)Document.exitFullscreen() :用于让当前文档退出全屏模式;

(3)fullscreen: 只读属性,报告文档当前是否以全屏模式显示内容;

(4)fullscreenElement: 当前文档中正在以全屏模式显示的节点,如果没有使用全屏模式,则返回null;

(5)allowfullscreen:是否允许激活全屏模式。

更多详细内容可以参照MDN。如下代码是笔者项目中用到的一段代码:

fullScreen() {
  if (this.$refs['cells'].requestFullScreen) {
    this.$refs['cells'].requestFullScreen()
  } else if (this.$refs['cells'].mozRequestFullScreen) {
    this.$refs['cells'].mozRequestFullScreen()
  } else if (this.$refs['cells'].webkitRequestFullScreen) {
    this.$refs['cells'].webkitRequestFullScreen()
  }
},

2.screenfull的使用

2.1 简介

screenfull对各种浏览器全屏的API进行封装,以便于我们可以方便的使页面或者元素切换到全屏状态。例如:

使页面切换到全屏:

import screenfull from 'screenfull';

document.getElementById('button').addEventListener('click', () => {
	if (screenfull.isEnabled) {
		screenfull.request();
	} else {
		// Ignore or do something else
	}
});

使元素切换到全屏:

import screenfull from 'screenfull';

const element = document.getElementById('target');

document.getElementById('button').addEventListener('click', () => {
	if (screenfull.isEnabled) {
		screenfull.request(element);
	}
});

screenfull有如下常用的API:

.request(element, options?) 使元素或者页面切换到全屏;

.exit()退出全屏;

.toggle(element, options?)在全屏和非全屏之间切换;

.on(event, function)监听全屏切换或者错误事件;

.off(event, function)移除之前注册的事件监听;

.isFullscreen 现在是否为全屏;

.isEnabled 浏览器是否允许全屏。

2.2 使用

element-plus-admin中使用了screenfull, 并封装为单独的组件:

<template>
    <div class='hidden-xs-only px-2'>
        <svg-icon v-if='!isFullscreen' class-name='cursor-pointer' icon-class='svg-fullscreen' @click='changeScreenfull' />
        <svg-icon v-else class-name='cursor-pointer' icon-class='svg-exit-fullscreen' @click='changeScreenfull' />
        
        <!-- 切换失效 -->
        <!-- <svg-icon class-name='cursor-pointer' :icon-class='isFullscreen ? "svg-exit-fullscreen" : "svg-fullscreen"' @click='changeScreenfull' /> -->
    </div>
</template>
<script lang='ts'>
import { defineComponent, ref, onMounted, onUnmounted } from 'vue'
import screenfull from 'screenfull'
import { ElNotification } from 'element-plus'

export default defineComponent({
    name: 'Screenfull',
    setup() {
        const isFullscreen = ref(false)
        const changeScreenfull = () => {
            if (!screenfull.isEnabled) {
                ElNotification({
                    message: '浏览器不支持全屏',
                    type: 'warning'
                })
            }else{
                screenfull.toggle()
            }
        }
        const change = () => {
            if(screenfull.isEnabled) isFullscreen.value = screenfull.isFullscreen
        }
        onMounted(() => screenfull.isEnabled && screenfull.on('change', change))
        onUnmounted(() => screenfull.isEnabled && screenfull.off('change', change))
        return {
            isFullscreen,
            changeScreenfull
        }
    }
})
</script>

2.3 screenfull原理

下面我们通过screenful的源码了解一下其原理。

2.3.1 methodMap

首先定义了一个由不同浏览器全屏相关的方法组成的数组:

const methodMap = [
	[
		'requestFullscreen',
		'exitFullscreen',
		'fullscreenElement',
		'fullscreenEnabled',
		'fullscreenchange',
		'fullscreenerror',
	],
	// New WebKit
	[
		'webkitRequestFullscreen',
		'webkitExitFullscreen',
		'webkitFullscreenElement',
		'webkitFullscreenEnabled',
		'webkitfullscreenchange',
		'webkitfullscreenerror',

	],
	// Old WebKit
	[
		'webkitRequestFullScreen',
		'webkitCancelFullScreen',
		'webkitCurrentFullScreenElement',
		'webkitCancelFullScreen',
		'webkitfullscreenchange',
		'webkitfullscreenerror',

	],
	[
		'mozRequestFullScreen',
		'mozCancelFullScreen',
		'mozFullScreenElement',
		'mozFullScreenEnabled',
		'mozfullscreenchange',
		'mozfullscreenerror',
	],
	[
		'msRequestFullscreen',
		'msExitFullscreen',
		'msFullscreenElement',
		'msFullscreenEnabled',
		'MSFullscreenChange',
		'MSFullscreenError',
	],
];

2.3.2 nativeAPI

nativeAPI方法用于检查浏览器支持的全屏相关方法,代码详细分析见注释:

const nativeAPI = (() => {
  // 无浏览器前缀的方法名数组
	const unprefixedMethods = methodMap[0];
	const returnValue = {};
  // 遍历methodMap的每一个元素
	for (const methodList of methodMap) {
    // 是否可以退出全屏
		const exitFullscreenMethod = methodList?.[1];
    // 检查document上是否存在可以退出全屏的方法
		if (exitFullscreenMethod in document) {
      // 如果支持全屏,则把相关方法都注册到returnValue上
			for (const [index, method] of methodList.entries()) {
				returnValue[unprefixedMethods[index]] = method;
			}

			return returnValue;
		}
	}

	return false;
})();

2.3.3 eventNameMap

eventNameMap定义全屏状态切换和全屏发生错误的事件映射:

const eventNameMap = {
	change: nativeAPI.fullscreenchange,
	error: nativeAPI.fullscreenerror,
};

2.3.4 request

screenfull对象上定义了一些全屏方法, 首先看一下request方法:

request(element = document.documentElement, options) {
  return new Promise((resolve, reject) => {
    // 全屏触发的方法
    const onFullScreenEntered = () => {
      // 取消注册进入全屏的事件
      screenfull.off('change', onFullScreenEntered);
      resolve();
    };
    // 注册进入全屏的事件
    screenfull.on('change', onFullScreenEntered);
    // 执行原生的进入全屏的方法,返回值为promise
    const returnPromise = element[nativeAPI.requestFullscreen](options);
    
    if (returnPromise instanceof Promise) {
      returnPromise.then(onFullScreenEntered).catch(reject);
    }
  });
},

request用于发起全屏请求,返回值为一个promise。

2.3.5 exit

exit方法用于退出全屏,具体逻辑和请求全屏类似:

exit() {
  return new Promise((resolve, reject) => {
    if (!screenfull.isFullscreen) {
      resolve();
      return;
    }
    // 退出全屏触发的方法
    const onFullScreenExit = () => {
      screenfull.off('change', onFullScreenExit);
      resolve();
    };
    // 退出全屏触发的事件
    screenfull.on('change', onFullScreenExit);
    // 执行退出全屏的方法
    const returnPromise = document[nativeAPI.exitFullscreen]();

    if (returnPromise instanceof Promise) {
      returnPromise.then(onFullScreenExit).catch(reject);
    }
  });
},

2.3.6 事件相关的方法

onchange(callback) {
  screenfull.on('change', callback);
},
onerror(callback) {
  screenfull.on('error', callback);
},
on(event, callback) {
  const eventName = eventNameMap[event];
  if (eventName) {
    document.addEventListener(eventName, callback, false);
  }
},
off(event, callback) {
  const eventName = eventNameMap[event];
  if (eventName) {
    document.removeEventListener(eventName, callback, false);
  }
}

on方法用于注册事件,off用于取消事件,onchange和onerror分别用于注册全

3.useFullscreen的使用

3.1 简介

useFullscreenvueuse核心提供的全屏功能。官方文档给出了如下图所示的例子:

点击fullscreen按钮可以进入全屏模式,实现代码如下图所示:

<script setup lang="ts">
import { ref } from 'vue'
import { useFullscreen } from '@vueuse/core'
const el = ref(null)
const { toggle, isFullscreen } = useFullscreen(el)
</script>

<template>
  <div class="text-center">
    <div class="flex" p="y-4">
      <video
        ref="el"
        class="m-auto rounded"
        src="https://vjs.zencdn.net/v/oceans.mp4"
        width="600"
        controls
      />
    </div>
    <button @click="toggle">
      Go Fullscreen
    </button>
  </div>
</template>

引入了useFullscreen来控制el(即video元素)的全屏,使用到了toggle方法和isFullscreen这个响应式的变量。

3.2 使用

vue-element-plus-admin 中使用了useFullscreen:

<script setup lang="ts">
import { Icon } from '@/components/Icon'
import { useFullscreen } from '@vueuse/core'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'

const { getPrefixCls } = useDesign()

const prefixCls = getPrefixCls('screenfull')

defineProps({
  color: propTypes.string.def('')
})

const { toggle, isFullscreen } = useFullscreen()

const toggleFullscreen = () => {
  toggle()
}
</script>

<template>
  <div :class="prefixCls" @click="toggleFullscreen">
    <Icon
      :size="18"
      :icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
      :color="color"
    />
  </div>
</template>

引入了useFullscreen来控制页面的全屏,使用到了toggle方法和isFullscreen这个响应式的变量。

3.3 useFullscreen原理

和screenfull类似,首先定义了一个由不同浏览器全屏相关的方法组成的数组functionsMap:

type FunctionMap = [
  'requestFullscreen',
  'exitFullscreen',
  'fullscreenElement',
  'fullscreenEnabled',
  'fullscreenchange',
  'fullscreenerror',
]
const functionsMap: FunctionMap[] = [
  [
    'requestFullscreen',
    'exitFullscreen',
    'fullscreenElement',
    'fullscreenEnabled',
    'fullscreenchange',
    'fullscreenerror',
  ],
  // New WebKit
  [
    'webkitRequestFullscreen',
    'webkitExitFullscreen',
    'webkitFullscreenElement',
    'webkitFullscreenEnabled',
    'webkitfullscreenchange',
    'webkitfullscreenerror',
  ],
  // Old WebKit
  [
    'webkitRequestFullScreen',
    'webkitCancelFullScreen',
    'webkitCurrentFullScreenElement',
    'webkitCancelFullScreen',
    'webkitfullscreenchange',
    'webkitfullscreenerror',
  ]
  // 省略一大堆
] as any

再看useFullscreen方法:

export function useFullscreen(
  target?: MaybeElementRef,
  options: UseFullscreenOptions = {},
) {
  const { document = defaultDocument, autoExit = false } = options
  // 获取目标元素
  const targetRef = target || document?.querySelector('html')
  // isFullscreen根据目标是否全屏是响应式的
  const isFullscreen = ref(false)
  // 是否支持不用响应式的,能支持就是能,不能就是不能
  let isSupported = false
  // 取第一组全屏相关的方法
  let map: FunctionMap = functionsMap[0]
  // document不存在肯定不支持
  if (!document) {
    isSupported = false
  }
  else {
    // 检测functionsMap的所有元素(每一个元素都是一个数组,包含浏览器全屏相关的方法)
    for (const m of functionsMap) {
      // document 上存在退出全屏的方法
      if (m[1] in document) {
        // 哪一种浏览器支持则使用对应那组方法
        map = m
        // 支持后即可退出循环检查
        isSupported = true
        break
      }
    }
  }
   // 解构出要使用的方法(REQUEST:请求全屏方法 EXIT:退出全屏 ELEMENT:当前全屏元素 EVENT:全屏change事件)
  const [REQUEST, EXIT, ELEMENT,, EVENT] = map
  // 退出全屏方法
  async function exit() {
    if (!isSupported)
      return
    if (document?.[ELEMENT])
      await document[EXIT]()

    isFullscreen.value = false
  }
  // 进入全屏方法
  async function enter() {
    if (!isSupported)
      return

    await exit()
    // 目标元素
    const target = unrefElement(targetRef)
    if (target) {
      await target[REQUEST]()
      isFullscreen.value = true
    }
  }
  // 切换
  async function toggle() {
    if (isFullscreen.value)
      await exit()
    else
      await enter()
  }

  if (document) {
    // 事件监听
    useEventListener(document, EVENT, () => {
      isFullscreen.value = !!document?.[ELEMENT]
    }, false)
  }

  if (autoExit) // 自动退出
    tryOnScopeDispose(exit)

  return {
    isSupported,
    isFullscreen,
    enter,
    exit,
    toggle,
  }
}

useFullscreen是使用hook的形式对全屏API进行了封装,useFullscreen方法返回了isSupported和isFullscreen两个变量用于判断是否支持全屏和是否在全屏状态;enter、exit 和toggle方法用于进入、退出和切换全屏。

4.总结

本文介绍了浏览器原生的全屏API, 然后介绍了使用较多的screenfull, 最后介绍了vueuse 提供的useFullscreen。screenfull和useFullscreen在本质上都是对浏览器原生API的封装。在实际项目中该如何选择呢? 欢迎您留言讨论~