
1. 大屏全屏这件事远比想象中要绕做过可视化大屏的人都有一个共识Demo 跑起来容易交付到客户现场不出洋相难。而全屏功能恰恰是那个最容易在演示环节翻车的点。客户站在大屏前你点一下右上角的全屏按钮屏幕闪了一下图表没跟着变或者干脆整个页面布局崩了那种尴尬做过一次就忘不了。可视化大屏的全屏功能表面上看就是调一个浏览器接口的事但真正落地时会牵扯出一连串问题Fullscreen API 在不同浏览器里的行为差异、全屏后 DOM 尺寸变化的监听时机、ECharts 或其它图表库的 resize 触发策略、多屏拼接场景下的分辨率适配、以及用户按 ESC 退出全屏后如何优雅还原。这些问题单独拎出来都不算难但叠在一起就变成了一个需要系统性思考的工程问题。这篇文章面向的是正在做或即将做可视化大屏的前端开发者不管你用的是 Vue、React 还是原生 JS不管你图表库选的是 ECharts、AntV 还是 D3全屏与自适应的核心逻辑是相通的。我会从方案选型讲到代码实现从踩坑记录讲到排查技巧尽量把这件事讲透。全文基于我参与过的几个大屏项目的实际经验整理代码可以直接抄作业但更希望你能理解每一步背后的原因这样遇到变体需求时才能自己判断怎么改。2. 全屏方案选型为什么最终都绕不开 Fullscreen API2.1 三种全屏实现路径的对比在动手写代码之前先要把全屏这个词拆开看。可视化大屏场景下的全屏其实有三种不同的实现路径各自适用的场景和代价完全不同。第一种是浏览器原生 Fullscreen API也就是element.requestFullscreen()这套。它的特点是真正让某个 DOM 元素占满整个屏幕浏览器地址栏、标签栏全部隐藏用户按 ESC 可以退出。这是最正统的全屏方案也是绝大多数大屏项目最终采用的方案。第二种是CSS 模拟全屏通过把容器设置为position: fixed; top:0; left:0; width:100vw; height:100vh; z-index: 9999来视觉上占满视口。它并没有真正进入全屏状态浏览器 UI 还在但胜在兼容性极好且不受 Fullscreen API 各种限制的约束。第三种是新窗口全屏用window.open()打开一个新页面再在新页面里请求全屏。这种方案在早期项目里常见现在基本被淘汰了因为新窗口的通信、状态同步都很麻烦。方案真实全屏兼容性实现复杂度适用场景Fullscreen API是现代浏览器良好中正式交付的大屏项目CSS 模拟全屏否极好低内嵌页面、iframe 场景新窗口全屏是一般高已淘汰不推荐选型建议很明确只要不是被 iframe 嵌套限制死优先用 Fullscreen API。因为客户对全屏的预期就是浏览器 UI 消失、内容铺满CSS 模拟全屏在这一点上是不达标的。而 iframe 场景下如果 iframe 没有加allowfullscreen属性Fullscreen API 会直接报错这时候才退而求其次用 CSS 方案。2.2 Fullscreen API 的浏览器前缀与兼容处理Fullscreen API 在标准化之前各浏览器有自己的前缀实现。虽然现在主流浏览器都已经支持标准 API但如果你的项目需要兼容一些老版本环境前缀处理还是得做。标准的 API 是element.requestFullscreen()和document.exitFullscreen()但历史上存在这些变体element.requestFullscreen()/document.exitFullscreen()标准element.webkitRequestFullscreen()/document.webkitExitFullscreen()WebKit 系element.mozRequestFullScreen()/document.mozCancelFullScreen()Firefox 旧版element.msRequestFullscreen()/document.msExitFullscreen()IE/Edge 旧版注意 Firefox 旧版的方法名是mozRequestFullScreenScreen 的 S 是大写这个细节坑过不少人。写兼容函数的时候要逐一判断。function requestFullscreen(element) { const requestMethod element.requestFullscreen || element.webkitRequestFullscreen || element.mozRequestFullScreen || element.msRequestFullscreen; if (requestMethod) { requestMethod.call(element); } else { console.warn(当前环境不支持 Fullscreen API); } } function exitFullscreen() { const exitMethod document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen || document.msExitFullscreen; if (exitMethod) { exitMethod.call(document); } }这段代码看起来简单但它是整个全屏功能的地基。我建议把它单独封装成一个工具模块而不是散落在业务代码里因为后面监听全屏状态变化、判断当前是否全屏都要用到这套前缀逻辑。2.3 全屏状态判断的坑为什么不能用布尔变量记很多初学者会用一个isFullscreen的布尔变量来记录当前是否全屏点击按钮时取反。这个做法在简单场景下能用但一旦用户按 ESC 退出全屏这个变量就和真实状态不一致了导致按钮图标显示错误、再次点击行为异常。正确的做法是始终以浏览器提供的状态为准通过document.fullscreenElement以及各前缀版本来判断当前是否有元素处于全屏状态。function getFullscreenElement() { return ( document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement || null ); } function isFullscreen() { return getFullscreenElement() ! null; }这里有个容易忽略的点document.fullscreenElement返回的是当前处于全屏的那个 DOM 元素而不是布尔值。所以判断是否全屏要用! null而不是直接当布尔用。另外如果页面上有多个元素可能进入全屏比如大屏容器和某个弹窗通过这个返回值还能知道到底是哪个元素在全屏这对后续的 resize 处理很关键。提示不要试图用变量缓存全屏状态所有状态判断都实时读取document.fullscreenElement这是避免状态不一致的唯一可靠方式。3. 图表自适应全屏之后真正难啃的骨头3.1 全屏触发 resize 的时机问题全屏功能本身实现起来不难难的是全屏之后图表要跟着变。这里第一个坑就是resize 事件的触发时机。当你调用requestFullscreen()后浏览器会有一个过渡动画通常是几百毫秒在动画过程中元素的尺寸是逐渐变化的。如果你在调用全屏后立刻触发图表 resize此时容器尺寸还没稳定图表会按错误的尺寸渲染出现留白或溢出。正确的做法是监听fullscreenchange事件在事件回调里再触发 resize。但这个回调触发的时刻尺寸也未必已经完全稳定所以通常还要配合一个短延时或者requestAnimationFrame。document.addEventListener(fullscreenchange, handleFullscreenChange); document.addEventListener(webkitfullscreenchange, handleFullscreenChange); document.addEventListener(mozfullscreenchange, handleFullscreenChange); document.addEventListener(MSFullscreenChange, handleFullscreenChange); function handleFullscreenChange() { // 用 requestAnimationFrame 等一帧让布局先稳定 requestAnimationFrame(() { // 再等一个微小的延时兼容过渡动画 setTimeout(() { resizeAllCharts(); }, 100); }); }这里的requestAnimationFrame加setTimeout的组合看起来有点冗余但实测下来是最稳的。requestAnimationFrame保证在下一帧渲染前执行setTimeout的 100ms 则是给过渡动画留缓冲。这个数值不是绝对的如果你的项目里全屏过渡特别慢可以适当加大但一般 100ms 足够。3.2 ECharts 的 resize 策略与性能考量ECharts 的 resize 是通过实例的resize()方法触发的。最朴素的做法是遍历所有图表实例逐个调用 resize。但大屏项目里图表数量可能十几个甚至几十个全屏切换时集中 resize 会造成明显的卡顿。我一般会做两件事来优化。第一是给 resize 加防抖因为窗口 resize 事件在拖动窗口时会高频触发全屏切换虽然只触发一次但统一走防抖逻辑更省心。第二是只 resize 可见的图表如果大屏有分页或 Tab 切换隐藏的图表不需要立即 resize可以等它显示时再处理。const chartInstances new Map(); // key: 图表ID, value: ECharts实例 function resizeAllCharts() { chartInstances.forEach((chart, id) { if (chart !chart.isDisposed()) { // 检查容器是否可见 const dom chart.getDom(); if (dom dom.offsetParent ! null) { chart.resize(); } } }); } // 防抖包装 function debounce(fn, delay) { let timer null; return function (...args) { if (timer) clearTimeout(timer); timer setTimeout(() fn.apply(this, args), delay); }; } const debouncedResize debounce(resizeAllCharts, 200);chart.isDisposed()这个判断很重要。在 Vue 或 React 项目里组件销毁时图表实例可能已经被 dispose 了但 resize 事件还在触发如果不判断就会报错。dom.offsetParent ! null则是判断元素是否可见的经典技巧display: none的元素offsetParent为 null。3.3 非 ECharts 场景的通用自适应思路如果你的大屏用的不是 ECharts而是 AntV G2、D3 或者自绘 Canvas自适应的思路是类似的但具体 API 不同。AntV G2 的图表实例有changeSize(width, height)方法需要你手动传入新的宽高。D3 通常是响应式的你需要在 resize 时重新计算比例尺和坐标轴然后重新绑定数据。自绘 Canvas 则要重新设置 canvas 的 width/height 属性并注意 devicePixelRatio 的处理否则会模糊。通用的思路是把获取容器尺寸和应用尺寸到图表这两步分开。获取尺寸可以用getBoundingClientRect()它返回的是元素相对于视口的精确尺寸比offsetWidth/offsetHeight更准确尤其是在有 transform 缩放的情况下。function getContainerSize(container) { const rect container.getBoundingClientRect(); return { width: Math.floor(rect.width), height: Math.floor(rect.height) }; }用Math.floor是为了避免出现小数尺寸导致图表渲染出现半像素模糊。这个细节在要求高的大屏项目里很重要尤其是那些对清晰度敏感的展示场景。4. 完整实操从按钮点击到图表重绘的全链路4.1 全屏按钮的交互设计与状态同步全屏按钮通常放在大屏的右上角图标在全屏和退出全屏之间切换。这里的设计要点是按钮的图标状态必须和真实全屏状态同步包括用户按 ESC 退出的情况。在 Vue 里我会把全屏状态放在一个响应式变量里通过fullscreenchange事件来更新它。在 React 里则用useState配合useEffect监听。核心逻辑是一样的。// 以原生 JS 为例 const fullscreenBtn document.getElementById(fullscreen-btn); function updateButtonState() { if (isFullscreen()) { fullscreenBtn.classList.add(is-fullscreen); fullscreenBtn.title 退出全屏; } else { fullscreenBtn.classList.remove(is-fullscreen); fullscreenBtn.title 进入全屏; } } fullscreenBtn.addEventListener(click, () { if (isFullscreen()) { exitFullscreen(); } else { requestFullscreen(document.documentElement); } }); // 监听状态变化同步按钮 [fullscreenchange, webkitfullscreenchange, mozfullscreenchange, MSFullscreenChange] .forEach(event document.addEventListener(event, updateButtonState));注意这里全屏的目标元素我用了document.documentElement也就是整个 html 元素。有些项目会只让大屏容器全屏这时候传容器元素即可。两者的区别在于整个页面全屏时页面上的其他元素比如顶部的导航栏也会一起全屏只让容器全屏时其他元素会被隐藏。大屏项目通常用整个页面全屏因为大屏本身就是全屏展示的。4.2 全屏切换时的布局重算与图表重绘全屏切换会改变容器的尺寸而大屏的布局往往是基于百分比或者 flex 的尺寸变化后布局会自动重排。但图表不会自动重排必须手动触发 resize。完整的处理链路是这样的用户点击全屏按钮调用requestFullscreen()浏览器进入全屏触发fullscreenchange事件事件回调里等布局稳定后调用resizeAllCharts()每个图表实例执行resize()重新计算尺寸并重绘这个链路里第 4 步的等布局稳定是关键。我前面提到的requestAnimationFramesetTimeout组合就是干这个的。但还有一种更稳妥的方式用ResizeObserver监听容器尺寸变化尺寸一变就触发 resize。const resizeObserver new ResizeObserver(entries { for (const entry of entries) { const chart chartInstances.get(entry.target.dataset.chartId); if (chart !chart.isDisposed()) { chart.resize(); } } }); // 给每个图表容器注册监听 chartInstances.forEach((chart, id) { const dom chart.getDom(); dom.dataset.chartId id; resizeObserver.observe(dom); });ResizeObserver的好处是它不依赖全屏事件任何导致容器尺寸变化的情况窗口缩放、布局调整、侧边栏展开收起都能捕获。缺点是兼容性在极老的浏览器上有问题但现代浏览器都支持。如果你的项目需要兼容老环境还是得回到fullscreenchangewindow.resize的组合方案。4.3 多屏拼接与分辨率适配的处理大屏项目经常遇到多屏拼接的场景比如两块 1920x1080 的屏幕拼成 3840x1080 的超宽屏。这种情况下全屏后容器的宽高比和设计稿不一致图表如果按固定比例渲染就会变形。处理思路有两种。一种是保持设计稿比例居中显示两侧留黑边这种方式简单但浪费屏幕空间。另一种是响应式布局图表按实际容器尺寸重新计算这种方式充分利用屏幕但需要图表配置支持动态尺寸。我一般推荐第二种因为大屏的价值就在于充分利用展示空间。具体做法是图表的宽高不写死而是用百分比或者 flex 布局图表内部的 padding、字体大小等则根据容器尺寸动态计算。function getScaleFactor() { const designWidth 1920; const currentWidth window.innerWidth; return currentWidth / designWidth; } function applyResponsiveStyles() { const scale getScaleFactor(); document.documentElement.style.setProperty(--scale-factor, scale); // 字体大小、间距等通过 CSS 变量控制 }用 CSS 变量来控制缩放因子是个很实用的技巧。你可以在 CSS 里写font-size: calc(16px * var(--scale-factor))这样字体大小就会随屏幕宽度自动缩放。图表的配置里也可以用这个因子来动态计算字号。注意缩放因子不要无限制地放大或缩小建议设置上下限比如最小 0.8最大 1.5避免在极端分辨率下出现字体过大或过小的情况。5. 踩坑实录那些文档里不会写的经验5.1 全屏请求被拒绝的几种情况requestFullscreen()返回的是一个 Promise如果全屏请求失败Promise 会 reject。失败的原因有好几种每一种的处理方式不同。最常见的是用户手势限制。浏览器要求全屏请求必须由用户交互触发点击、按键等如果你在页面加载后自动调用全屏会被拒绝。这个限制是为了防止恶意网站强制全屏。解决办法就是确保全屏调用在点击事件的回调里不要放在setTimeout或者异步请求的回调里。第二种是iframe 权限问题。如果你的大屏被嵌在 iframe 里iframe 必须加allowfullscreen属性否则全屏请求会失败。这个属性要加在 iframe 标签上不是加在内部页面里。第三种是浏览器策略限制。某些浏览器在特定情况下会限制全屏比如页面不在顶层窗口、或者用户之前拒绝过全屏请求。这种情况比较少见但排查时要考虑到。function requestFullscreen(element) { const requestMethod element.requestFullscreen || element.webkitRequestFullscreen; if (!requestMethod) return Promise.reject(new Error(不支持全屏)); const result requestMethod.call(element); // 标准 API 返回 Promise前缀版本可能返回 undefined if (result typeof result.then function) { return result.catch(err { console.error(全屏请求失败:, err.message); // 这里可以做降级处理比如用 CSS 模拟全屏 fallbackToCssFullscreen(element); }); } return Promise.resolve(); }把全屏请求的失败处理做好是项目健壮性的体现。我见过太多项目在全屏失败时直接白屏或者报错用户体验很差。加一个 CSS 模拟全屏的降级方案虽然不完美但至少能用。5.2 ESC 退出全屏后的状态还原用户按 ESC 退出全屏时浏览器会触发fullscreenchange事件但不会触发resize事件因为窗口尺寸没变只是全屏状态变了。所以如果你只监听了window.resizeESC 退出后图表不会自适应。这就是为什么必须监听fullscreenchange事件。在这个事件里不管是进入还是退出全屏都统一触发一次 resize。这样逻辑最简单也不容易漏。还有一个细节ESC 退出全屏后document.fullscreenElement会变成 null但fullscreenchange事件的触发时机是在状态变化之后所以你在回调里读到的已经是新状态了。这个顺序要搞清楚否则会写出逻辑颠倒的代码。5.3 图表 resize 后配置丢失的问题ECharts 的resize()方法默认会保留原有配置但如果你在 resize 时手动传了参数或者图表配置里有依赖尺寸的动态计算就可能出现配置丢失或错乱。我遇到过一个典型案例图表的 grid 配置里写了固定的 left/right/top/bottom 像素值全屏后容器变大但 grid 还是按原来的像素值布局导致图表只占了左上角一小块。解决办法是把这些固定值改成百分比或者在 resize 时重新计算并setOption。function resizeChart(chart) { const dom chart.getDom(); const { width, height } getContainerSize(dom); chart.resize(); // 如果 grid 是固定像素需要重新计算 chart.setOption({ grid: { left: Math.floor(width * 0.05), right: Math.floor(width * 0.05), top: Math.floor(height * 0.1), bottom: Math.floor(height * 0.1) } }); }这个问题的根源在于 ECharts 的 grid 配置不会随容器自动缩放它只认你给的值。所以要么用百分比ECharts 支持百分比字符串要么在 resize 时动态计算。我个人更推荐百分比代码更简洁也不容易出错。5.4 常见问题速查表问题现象可能原因排查方向解决方案点击全屏无反应非用户手势触发检查调用栈是否在点击回调内确保在事件回调中同步调用全屏后图表不变未监听 fullscreenchange检查事件监听是否注册注册 fullscreenchange 并触发 resize图表变形拉伸容器宽高比变化检查图表配置是否写死宽高改用百分比或动态计算ESC 退出后布局错乱只监听了 window.resize检查事件监听列表补充 fullscreenchange 监听iframe 内全屏失败缺少 allowfullscreen检查 iframe 标签属性添加 allowfullscreen 属性全屏后字体模糊devicePixelRatio 未处理检查 canvas 尺寸设置按 dpr 缩放 canvasresize 报错 isDisposed图表已销毁仍触发 resize检查组件销毁逻辑resize 前判断 isDisposed这张表里的每一条都是我或同事实际踩过的坑。尤其是最后一条isDisposed报错在 Vue 项目里特别常见因为组件的销毁和事件的解绑有时序问题。养成 resize 前判断实例状态的习惯能省掉很多莫名其妙的报错。6. 进阶优化让全屏体验更顺滑的几个技巧6.1 全屏过渡动画的优化浏览器原生的全屏过渡动画是黑屏渐变视觉上比较生硬。如果你想让全屏切换更顺滑可以在全屏前先给容器加一个过渡效果全屏后再移除。思路是这样的点击全屏时先给大屏容器加一个transition: all 0.3s和轻微的缩放效果然后调用全屏。全屏完成后移除过渡效果。这样视觉上会有一个平滑的放大过程而不是突兀的黑屏切换。.dashboard-container { transition: transform 0.3s ease, opacity 0.3s ease; } .dashboard-container.fullscreen-transition { transform: scale(1.02); opacity: 0.95; }function smoothFullscreen(element) { element.classList.add(fullscreen-transition); requestFullscreen(element).then(() { setTimeout(() { element.classList.remove(fullscreen-transition); }, 300); }); }这个技巧不是必须的但在演示场景下能明显提升观感。客户不一定说得出来哪里好但会觉得这个系统很流畅。6.2 全屏状态下的鼠标隐藏大屏展示时鼠标指针停留在屏幕上会影响观感。可以在全屏状态下监听鼠标移动一段时间不移动就隐藏指针移动时再显示。let mouseTimer null; function setupMouseHide() { document.addEventListener(mousemove, () { document.body.style.cursor default; if (mouseTimer) clearTimeout(mouseTimer); if (isFullscreen()) { mouseTimer setTimeout(() { document.body.style.cursor none; }, 3000); } }); }3 秒是个比较合适的值太短会频繁闪烁太长又起不到隐藏效果。这个功能在展厅、指挥中心这类场景下特别实用。6.3 全屏与数据刷新的协同大屏通常有定时刷新数据的需求。全屏切换时如果正好赶上数据刷新可能会出现图表 resize 和数据更新同时进行的情况导致渲染异常。我的做法是给 resize 和 setOption 加一个简单的互斥锁或者把数据刷新也纳入防抖逻辑。更简单的办法是全屏切换时暂停数据刷新定时器全屏完成后再恢复。let refreshTimer null; function startRefresh() { refreshTimer setInterval(fetchData, 30000); } function stopRefresh() { if (refreshTimer) { clearInterval(refreshTimer); refreshTimer null; } } function handleFullscreenChange() { stopRefresh(); requestAnimationFrame(() { setTimeout(() { resizeAllCharts(); startRefresh(); }, 100); }); }这个处理看起来有点过度设计但在实际项目里确实能避免一些偶发的渲染问题。尤其是当数据刷新涉及大量 DOM 操作时和 resize 撞在一起很容易出问题。7. 我个人的一些实操体会全屏功能这个需求我前前后后在不同项目里做过五六次每次都会遇到新的边角情况。最开始我以为这就是个 API 调用的事后来才发现真正的工作量在于自适应和异常处理。如果让我给正在做这个功能的同行一句建议那就是把全屏状态当成一个全局的、实时的、不可缓存的状态来对待。所有依赖全屏状态的逻辑都实时去读document.fullscreenElement不要用变量记。这一条能帮你避开至少一半的坑。另外图表 resize 的时机比 resize 本身更重要。宁可多等 100ms也不要在尺寸没稳定时触发 resize。我见过为了追求即时响应而在全屏动画还没结束就 resize 的代码结果图表尺寸算错反而要二次 resize体验更差。最后分享一个小技巧在开发阶段可以给全屏切换加一个 console 日志打印出全屏前后的容器尺寸和图表尺寸。这样一旦出现自适应问题一眼就能看出是尺寸没更新还是图表没响应。这个日志在排查现场问题时特别有用比盲猜高效得多。这套方案后续还可以往两个方向扩展。一是支持多显示器场景下的全屏通过window.screen相关 API 获取多屏信息让用户选择在哪块屏幕上全屏。二是支持全屏状态下的截图导出把当前大屏内容导出为图片用于汇报和存档。这两个方向我都在探索中等有成熟方案了再单独整理。