screenfull.js 源码解析

一天

产品经理:我想做个打开页面点击某个按钮进入全屏模式,再点击退出全屏模式

我:直接 F11 或 ctrl + cmd + f 快捷键就解决了,退出再按一遍

产品经理:这怎么行,用户不懂啥叫快捷键,这个你先看一下,产品经理转身走了

我:mmp... 本来还想说点啥的

一通搜索后...

我:这个吧,可以做,就是吧,部分浏览器版本不太兼容

产品经理:没关系,只考虑主流浏览器主流版本,你评估个时间

我:mmp...,最后加上摸鱼时间 2 天

这个需求重点就是浏览器是否提供全屏模式的方法

正巧,screenfull.js (opens new window) 这个轮子可以完美实现

而且抹平了各浏览器方法名不一致问题

screenfull github 国内镜像仓库 (opens new window)

# screenfull.js 示例

首先看下所提供的功能

查看功能示例 (opens new window)

示例演示功能如下:

  • 当前浏览器是否支持全屏
  • 打开全屏
  • 退出全屏
  • 全屏打开、退出切换
  • img 标签全屏显示
  • 处于全屏状态的监控

首先

明确全屏模式是指浏览器内容区域全屏

也可以理解为浏览器可视区域

可通过页面标签元素,也只能通过页面标签元素使用全屏接口

比如 document.body 调用全屏 requestFullscreen 接口

div、img 等标签元素当然也可以调用

# 源码解析

源码地址 (opens new window)

下载到本地编辑器中打开体验更佳

# 全屏相关的属性、方法、事件

  • requestFullscreen - 打开全屏方法
  • exitFullscreen - 退出全屏方法
  • fullscreenElement - 获取全屏模式下的元素(不处于全屏时,为 null,基于此判断当前文档是否处于全屏状态)
  • fullscreenEnabled - 判断当前浏览器是否支持全屏模式
  • fullscreenchange - 监听打开、退出全屏事件
  • fullscreenerror - 监听打开、退出全屏出错时事件

更多详细描述,参考 MDN (opens new window)

# 全屏属性、方法、事件归属分类

归属于 document 对象:

  • exitFullscreen
  • fullscreenElement
  • fullscreenEnabled

归属于 html 标签元素:

  • requestFullscreen

归属于 document 对象与 html 标签元素:

  • fullscreenchange
  • fullscreenerror

综上可以看出 全屏的相关属性、方法、事件与 window 对象毫无关系

调用者也不只是单纯的 document 对象

而且只能通过 html 标签元素可调用全屏打开方法

比如 html、div、img 标签元素等

# 源码初探

初始化时

匿名函数自执行包裹了整个源码

当然整个源码处于严格模式下

初始声明 commonjs 支持

判断了 document 对象

var isCommonjs = typeof module !== 'undefined' && module.exports;
// document 如果不是 window 对象属性就是 {}
var document = typeof window !== 'undefined' && typeof window.document !== 'undefined' ? window.document : {};
1
2
3

源码最后,根据 commonjs 支持情况,导出不同格式的模块

 // 不同运行环境导出不同的模块
  if (isCommonjs) {
    // 应用于 webpack 等打包工具或 Node 中 commonjs 模块
    module.exports = screenfull;
  } else {
    // 应用于浏览器中的全局模块
    window.screenfull = screenfull;
  }
1
2
3
4
5
6
7
8

# 源码解析

  1. 抹平各浏览器全屏接口差异

  2. 判断当前浏览器是否支持全屏模式

// fn 函数自执行,用于抹平 Chrome、Firefox、IE 浏览器差异
// fn 为 false 时表示当前浏览器不支持全屏模式
 var fn = (function () {
    var val;
    // 定义兼容 Chrome、Firefox、IE 方法数组
    var fnMap = [
      [
        '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'
      ]
    ];

    var i = 0;
    var l = fnMap.length;
    var ret = {};

    for (; i < l; i++) {
      val = fnMap[i];
      // exitFulllscreen 归属于 document 对象专有
      // 判断 exitFulllscreen 或带前缀的是否在 document 对象上
      // 若不在,表示当前浏览器不支持全屏模式
      if (val && val[1] in document) {
        for (i = 0; i < val.length; i++) {
          // 统一 ret key 值名称,值根据浏览器支持情况自动处理
          // 优秀,保持对象接口名称的一致性,不必添加前缀不前缀的
          // 最终对象输出统一为 fnMap[0] 中的接口
          ret[fnMap[0][i]] = val[i];
        }
        return ret;
      }
    }

    return false;
  })();

  // fn 为 false 当前浏览器表示当前浏览器全屏模式不可用,直接返回
  if (!fn) {
    if (isCommonjs) {
      module.exports = {
        isEnabled: false
      };
    } else {
      window.screenfull = {
        isEnabled: false
      };
    }

    return;
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

若当前浏览器支持全屏模式:

  1. 定义对外统一接口对象 screenfull,所以在浏览器中可通过 screenfull.request 支持打开全屏

  2. 封装、简化全屏相关接口、属性、事件

// 将全屏事件归属到一个对象中、方便使用
 var eventNameMap = {
    change: fn.fullscreenchange,
    error: fn.fullscreenerror
  };

  var screenfull = {
    // 打开全屏
    // 注意只有 html 标签元素才支持此方法
    request: function (element, options) {
      return new Promise(function (resolve, reject) {
        var onFullScreenEntered = function () {
          this.off('change', onFullScreenEntered);
          resolve();
        }.bind(this);

        this.on('change', onFullScreenEntered);

        // 使用 html 标签元素调用打开全屏,若未传入时,默认 html 标签元素
        element = element || document.documentElement;

        // options 为打开全屏时的配置信息
        // 目前只有 navigationUI 这个配置项
        // 详情: https://developer.mozilla.org/zh-CN/docs/Web/API/FullscreenOptions
        // 表示是否显示用户界面其他元素
        var returnPromise = element[fn.requestFullscreen](options);

        if (returnPromise instanceof Promise) {
          returnPromise.then(onFullScreenEntered).catch(reject);
        }
      }.bind(this));
    },
    // 退出全屏
    exit: function () {
      return new Promise(function (resolve, reject) {
        if (!this.isFullscreen) {
          resolve();
          return;
        }

        var onFullScreenExit = function () {
          this.off('change', onFullScreenExit);
          resolve();
        }.bind(this);

        this.on('change', onFullScreenExit);

        // 通过 document 对象调用
        var returnPromise = document[fn.exitFullscreen]();

        if (returnPromise instanceof Promise) {
          returnPromise.then(onFullScreenExit).catch(reject);
        }
      }.bind(this));
    },
    // 打开、退出全屏切换
    toggle: function (element, options) {
      return this.isFullscreen ? this.exit() : this.request(element, options);
    },
    // 监听打开、退出全屏切换快捷方式
    onchange: function (callback) {
      this.on('change', callback);
    },
    // 监听打开、退出全屏切换错误快捷方式
    onerror: function (callback) {
      this.on('error', callback);
    },
    on: function (event, callback) {
      var eventName = eventNameMap[event];
      if (eventName) {
        // 在 html 标签元素上监听打开、退出全屏事件时
        // 默认冒泡到 document 对象上
        // 所以最终只在 document 对象上监听即可
        // 如果就想监听当前标签元素的
        /**
         当前元素上通过 stopPropagation 阻止事件冒泡,这样 document 上就监听不到了
         ele.addEventListener('fullscreenchange', function(ev) {ev.stopPropagation(), false)
        */
        document.addEventListener(eventName, callback, false);
      }
    },
    off: function (event, callback) {
      var eventName = eventNameMap[event];
      if (eventName) {
        document.removeEventListener(eventName, callback, false);
      }
    },
    raw: fn
  };

  // 全屏属性的封装,直接挂载到 screenfull 对象
  Object.defineProperties(screenfull, {
    // 当前文档是否处于全屏模式
    isFullscreen: {
      get: function () {
        return Boolean(document[fn.fullscreenElement]);
      }
    },
    // 处于全屏模式文档元素
    element: {
      enumerable: true,
      get: function () {
        return document[fn.fullscreenElement];
      }
    },
    // 浏览器是否兼容全屏模式
    isEnabled: {
      enumerable: true,
      get: function () {
        // Coerce to boolean in case of old WebKit
        return Boolean(document[fn.fullscreenEnabled]);
      }
    }
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114

以上就是 screenfull.js 源码全部内容

# 扩展

iOS Safari 没有全屏 API,但 Chrome(Android 版)、Firefox 和 IE 11+ 上则有相应的 API,另外 API 存在命名不统一的问题,所以最好使用三方库解决 谷歌开发者也建议使用 screenfull.js 解决

另外,Web 全屏模式的打开

除了使用浏览器全屏模式 API 打开外

还有以下两种方式:

# 从主屏幕以全屏模式启动页面

iOS: 自从 iPhone 发布以来,用户就一直能将网络应用安装到主屏幕,并以全屏模式启动。

<meta name="apple-mobile-web-app-capable" content="yes">
1

Chrome(Android 版):

<meta name="mobile-web-app-capable" content="yes">
1

网络应用清单(Chrome、Opera、Firefox、Samsung):

  • 将清单的相关信息告知浏览器
  • 说明启动方法
<link rel="manifest" href="/manifest.json">
1
{
  "short_name": "Kinlan's Amaze App",
  "name": "Kinlan's Amazing Application ++",
  "icons": [
    {
      "src": "launcher-icon-4x.png",
      "sizes": "192x192",
      "type": "image/png"
    }
  ],
  "start_url": "/index.html",
  // 这一句才是关键,全屏模式
  "display": "fullscreen",
  "orientation": "landscape"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 脚本 hack 自动隐藏地址栏

自动隐藏地址栏来“伪造全屏模式” 需要多加测试,处理兼容问题

window.scrollTo(0,1);
1

以上内容可参考谷歌开发文档 (opens new window)

# screenfull.js Typescript 声明文件问题

使用 Typescript 开发时

// 引入 screenfull
import screenfull from 'screenfull'

// 查看是否处于全屏状态
screenfull.isFullscreen
1
2
3
4
5

报错如下:

Property 'isFullscreen' does not exist on type 'Screenfull | { isEnabled: false; }'. Property 'isFullscreen' does not exist on type '{ isEnabled: false; }'.

意思就是 isFullscreen 不在 screenfull 这个对象上

找到 screenfull.d.ts 声明文件

找到如下声明处:

declare let screenfull: screenfull.Screenfull | {isEnabled: false};
1

{isEnabled: false} 删除

declare let screenfull: screenfull.Screenfull;
1

这样,不报错了

# 小结

通过对 screenfull.js 实际使用演示了所解决需求痛点

对其源码的解读明白了内部原理及实现细节

比如,抹平各浏览器差异

比如,标签元素、document 对象对于事件监听的合并处理

比如,全屏 API 部分归属于 html 标签元素,部分归属于 document 对象

通过扩展看到了全屏模式不同的解锁姿式,不仅限于 API

还可以通过

meta 配置

manifest.json 清单

甚至通过脚本的伪造全屏模式

最后解决了使用时 Typescript 声明文件的问题

扫一扫,微信中打开

微信二维码