普通视图

发现新文章,点击刷新页面。
今天 — 2026年3月5日首页

拿来就用!Vue3+Cesium 飞入效果封装,3D大屏多场景直接复用

作者 李剑一
2026年3月4日 09:43

最近有点儿事儿,之前的大屏项目拖了一段时间,现在打算继续开发。原本以为用熟悉的Cesium能快速搞定,没想到还是踩了几个坑,整理出来和大家分享,避免后续有人走同样的弯路。

页面地球飞入效果采用 Cesium 进行开发,一来 Cesium 作为开源的3D地理信息可视化框架,API 封装完善,开发效率高。

二来个人长期使用该框架,对其核心逻辑比较熟悉,本以为能快速落地,实际开发中却遇到了加载、时机监听、多场景复用等多个问题,逐一排查解决后,才实现了流畅的飞入效果。

实现效果

Video Project 2.gif

Cesium 地球初始化完成后,自动触发指定地点的飞入动画,相机从初始视角平滑过渡到目标经纬度对应的视角,过程流畅无卡顿、无图层闪烁。

核心问题

简单的飞入效果之前都是使用的现成的方法,从零开始遇到点问题。

实际开发中需要兼顾加载性能、时机准确性、多场景复用等,具体问题如下:

  • Cesium 加载速度问题:在线地图服务加载延迟高,弱网环境下易报错,影响用户体验;
  • Cesium 加载完成时机的监听:若监听时机不准确,会导致飞入动画触发时,地图图层、影像未加载完成,出现“空地球”或“图层闪烁”问题;
  • 多地点复用问题:大屏项目中可能需要切换多个目标地点,需对飞入逻辑进行封装,实现灵活调用。

解决方案(附完整代码+细节说明)

加载速度优化:解决在线地图加载慢的问题

我最初采用的是 Cesium 官方的 Ion 在线地图服务,毕竟无需额外配置,直接调用即可,但实际测试后发现两个致命问题:

  1. 加载延迟高:官方服务器位于海外,国内网络环境下,地图影像加载速度极慢,甚至需要10秒以上才能完全渲染,远超用户6~8秒的等待极限;

  2. 稳定性差:弱网环境下,地图会直接加载失败,控制台报错“影像图层加载超时”,导致页面无法正常展示。

在线地图服务受网络环境影响极大,若要实现生产环境的稳定运行,优先替换为本地地图服务(如天地图本地部署、GeoServer 发布的本地影像),从根源上解决加载慢、报错的问题。

由于手头暂无本地地图部署工具,本次开发暂用在线地图过渡,后续会替换为本地服务。

以下是优化后的初始化代码,增加了加载超时处理,提升弱网环境下的容错性:

// 导入 Cesium 核心模块
import * as Cesium from 'cesium'
// 引入 Cesium 样式(必须,否则控件和地球样式异常)
import 'cesium/Build/Cesium/Widgets/widgets.css'

// 初始化 Cesium 地球(增加超时处理,优化加载体验)
const initCesium = async () => {
    try {
        // 配置 Cesium Token(可从 Cesium 官网免费申请,需注册账号)
        Cesium.Ion.defaultAccessToken = '你的官网Token';

        // 创建 Cesium 视图实例,精简界面控件,提升加载速度
        viewer.value = new Cesium.Viewer('cesiumContainer', {
            // 隐藏默认控件,适配大屏简洁风格
            timeline: false, // 时间轴控件
            animation: false, // 动画控件
            baseLayerPicker: false, // 底图切换控件
            geocoder: false, // 地理编码控件(搜索地点)
            homeButton: false, // 首页按钮
            infoBox: false, // 信息弹窗(点击要素时显示)
            sceneModePicker: false, // 场景模式切换(2D/3D/哥伦布视图)
            navigationHelpButton: false, // 导航帮助按钮
            // 性能优化配置
            scene3DOnly: true, // 仅开启3D模式,减少2D渲染开销
            requestRenderMode: true, // 开启请求渲染模式,降低CPU占用
            maximumRenderTimeChange: 1 / 60, // 控制渲染帧率,避免卡顿
            // 开启地形(如果不需要地形展示,可注释,进一步提升加载速度)
            // terrainProvider: Cesium.createWorldTerrain()
        });

        // 隐藏 Cesium 底部版权信息(可选,根据项目需求调整)
        viewer.value._cesiumWidget._creditContainer.style.display = 'none';

        // 等待 Cesium 完全加载完成(包括影像图层、场景渲染)
        await waitForCesiumFullyLoaded();
        
        // 触发 cesiumReady 事件,通知外部执行飞入等后续操作
        emit('cesiumReady', viewer.value);
    } catch (error) {
        console.error('Cesium 初始化失败:', error);
        // 加载失败提示,提升用户体验
        ElMessage.error('地球加载失败,请检查网络或刷新页面重试');
    }
}

加载时机监听

这是本次开发最容易踩坑的点,在创建 viewer 实例后,直接触发飞入动画,导致动画执行时,地图影像还未加载完成,出现“相机飞向空地球”的尴尬场景。

Cesium 初始化是异步过程,创建 viewer 实例只是第一步,后续还需要加载影像图层、渲染场景、初始化相机等操作,这些操作完成后,才能确保飞入动画的流畅性。

封装两个异步方法,分别监听场景渲染就绪影像图层加载完成,只有两个条件都满足,才触发后续的飞入操作,确保时机精准。

代码如下:

/**
 * 等待 Cesium 完全加载完成(包括场景渲染和影像图层)
 * 核心逻辑:先确保场景渲染就绪,再等待影像图层加载完成,双重校验
 * @returns {Promise}
 */
const waitForCesiumFullyLoaded = () => {
    return new Promise((resolve) => {
        const checkSceneReady = () => {
            // 先检查 viewer 和 scene 是否存在(避免初始化未完成时调用)
            if (!viewer.value || !viewer.value.scene) {
                // 每50ms检查一次,避免频繁占用资源
                setTimeout(checkSceneReady, 50);
                return;
            }
            
            // 使用 postRender 事件,确保场景至少完成一帧渲染
            viewer.value.scene.postRender.addEventListener(() => {
                // 场景就绪后,再等待影像图层加载完成
                waitForImageryLoaded().then(resolve);
            }, viewer.value.scene);
        };
        checkSceneReady();
    });
}

/**
 * 等待影像图层加载完成(单独封装,便于后续扩展)
 * 核心逻辑:遍历所有影像图层,检查是否有正在加载的图块
 * @returns {Promise}
 */
const waitForImageryLoaded = () => {
    return new Promise((resolve) => {
        // 若 viewer 或 scene 不存在,直接resolve(容错处理)
        if (!viewer.value || !viewer.value.scene) {
            resolve();
            return;
        }

        const imageryLayers = viewer.value.imageryLayers;
        // 若没有影像图层,直接resolve
        if (!imageryLayers || imageryLayers.length === 0) {
            resolve();
            return;
        }

        // 循环检查所有影像图层是否加载完成
        const checkLoaded = () => {
            let allLoaded = true;
            
            for (let i = 0; i < imageryLayers.length; i++) {
                const layer = imageryLayers.get(i);
                if (layer && layer.imageryProvider) {
                    // 检查当前图层是否有正在加载的图块(_loading 为Cesium内部属性)
                    if (layer._loading) {
                        allLoaded = false;
                        break;
                    }
                }
            }

            if (allLoaded) {
                // 确保影像加载完成后,场景再渲染一帧,避免闪烁
                viewer.value.scene.postRender.addEventListener(() => {
                    resolve();
                }, viewer.value.scene);
            } else {
                // 每100ms检查一次,平衡性能和准确性
                setTimeout(checkLoaded, 100);
            }
        };

        checkLoaded();
    });
}

关键注意点:将两个方法拆分开写,是为了后续扩展——比如项目中需要添加3D模型、矢量数据加载。

可直接在 waitForCesiumFullyLoaded 方法中添加对应的等待逻辑,无需大幅修改代码,提升可维护性。

封装飞入方法

大屏项目中,往往需要切换多个目标地点(如从全国视角飞入各省、从省视角飞入各市),若每次切换都重复编写代码冗余。

因此,简单封装一个通用的飞入方法。

/**
 * 控制 Cesium 相机飞往指定目标地点(通用封装,支持多场景复用)
 * @param {Object} options - 飞行配置项(必传参数标注,可选参数有默认值)
 * @param {Number} options.longitude - 目标经度(必传,如北京:116.4074)
 * @param {Number} options.latitude - 目标纬度(必传,如北京:39.9042)
 * @param {Number} options.height - 目标高度 (米,必传,根据场景调整,如大屏常用5000米)
 * @param {Number} [options.duration=3] - 飞行时长 (秒,可选,默认3秒,兼顾流畅度和效率)
 * @param {Number} [options.heading=0] - 相机朝向 (角度,可选,0 为正北,可根据需求调整)
 * @param {Number} [options.pitch=-60] - 俯仰角 (角度,可选,-90 为垂直向下,-60为常用视角)
 * @param {Function} [options.onComplete] - 飞行完成回调(可选,如飞行结束后加载区域数据)
 * @param {Function} [options.onCancel] - 飞行取消回调(可选,如用户手动中断飞行时的处理)
 */
const flyToLocation = async (options) => {
    // 校验 viewer 实例是否存在,避免报错
    if (!viewer.value) {
        console.warn('Viewer 实例不存在,无法执行飞行操作');
        ElMessage.warning('地球未加载完成,无法执行飞入操作');
        return;
    }

    // 解构配置项,设置默认值
    const {
        longitude,
        latitude,
        height,
        duration = 3,
        heading = 0,
        pitch = -60,
        onComplete,
        onCancel
    } = options

    // 由于 cesiumReady 触发时已确保影像加载完成,这里直接执行飞行
    viewer.value.camera.flyTo({
        // 将经纬度、高度转换为 Cesium 支持的笛卡尔坐标系
        destination: Cesium.Cartesian3.fromDegrees(longitude, latitude, height),
        // 相机朝向配置(heading:方位角,pitch:俯仰角,roll:翻滚角)
        orientation: {
            heading: Cesium.Math.toRadians(heading), // 角度转弧度(Cesium 内部使用弧度)
            pitch: Cesium.Math.toRadians(pitch),
            roll: 0.0 // 翻滚角,默认0,无需调整
        },
        duration: duration, // 飞行时长
        complete: () => {
            console.log('已飞到目标地点!');
            // 执行完成回调(若有)
            if (onComplete) onComplete();
        },
        cancel: () => {
            console.log('飞行被取消!');
            // 执行取消回调(若有)
            if (onCancel) onCancel();
        },
        canInterrupt: true // 允许用户手动中断飞行(如鼠标拖拽相机)
    })
}

注意:项目使用 Vue3 + setup 语法,需通过 defineExposeflyToLocation 方法导出,外部组件才能调用。

总结

Cesium 作为成熟的3D地理可视化框架,本身的 API 封装已经非常完善,实现飞入效果的核心逻辑并不复杂。

但实际开发中,往往是细节问题导致踩坑,总结几点关键经验,供大家参考:

  1. 加载优化优先选本地地图:生产环境中,务必替换掉官方在线地图,改用本地部署的地图服务(天地图、高德地图本地切片等),彻底解决加载慢、报错的问题;

  2. 加载时机监听不能省:不要省略 waitForCesiumFullyLoaded 方法,否则会出现图层闪烁、空地球等问题,拆分方法便于后续扩展;

  3. 封装逻辑提升复用性:多地点切换场景,一定要封装通用的飞入方法,明确配置项的必传/可选,增加容错处理,减少代码冗余;

  4. 内存管理要注意:页面卸载时,务必销毁 Cesium 实例(包括 viewer事件监听等),避免内存泄漏,导致页面卡顿、崩溃,销毁代码示例如下:

// 页面卸载时销毁 Cesium 实例(Vue3 onUnmounted 中调用)
onUnmounted(() => {
    if (viewer.value) {
        // 销毁 viewer 实例,释放内存
        viewer.value.destroy();
        viewer.value = null;
    }
});

最后,Cesium 的坑大多集中在“加载时机”和“性能优化”上,只要理清初始化流程、做好细节校验,就能快速实现流畅的交互效果。

后续我会继续更新这个大屏项目中 Cesium 的其他坑点,欢迎大家留言交流,共勉!

大屏天气展示太普通?视觉升级!用 Canvas 做动态天气遮罩,雷阵雨效果直接封神

作者 李剑一
2026年3月5日 10:37

之前做天气那个模块的时候,突发奇想想做一个大屏实时展示天气状况的蒙版。# Vue实现大屏获取当前所处城市及当地天气(纯免费)

需求

现在大屏上展示天气一般都是在左上/右上做天气的图标/纯文字的展示,虽然看起来非常直观,但是对于大屏这种需要炫酷效果的产品显得不合适。

目前市面上对于天气这一块也并不是非常重视,我接触的大屏项目/产品对这部分基本都没啥要求。

但是能够展示天气效果对于大屏本身有相当不错的加成效果。

屏幕录制 2026-03-05 102909.gif

所以我开发了这个大屏天气展示蒙版组件,能够根据当前天气状况以蒙版的形式展示出来,目前支持多种天气的展示效果。

方案

视频方案

一开始考虑的是纯视频解决方案,首先说这个方案非常的简单,将视频以背景图的形式放在蒙版上,通过 pointer-events: none; 鼠标穿透就能算是完成了。

但是实际操作过程中发现问题比较多,首先是透明背景需要特定格式的视频才能够支持。

必须使用支持 Alpha 通道(透明通道)的视频格式‌。

常见支持透明背景的格式包括:

  • ‌WebM(VP8 或 VP9 编码 + Alpha 通道)‌:Chrome、Firefox 和 WebView2 等 Chromium 内核环境支持良好 。
  • ‌MOV(Apple ProRes 4444 编码)‌:支持 Alpha 通道,但主要在 macOS 和专业软件(如 Final Cut Pro、After Effects)中使用 ‌。
  • ‌MP4(H.265/HEVC 编码)‌:部分平台(如 WebView2)支持含 Alpha 通道的 H.265 视频。

但是我在网上并没有找到相应格式的视频,自己录也弄得不好,所以放弃了。

另外以视频作为背景图在弱网环境下比较难加载,毕竟视频一般都要超过5M以上了。

但是如果有相应的视频,效果做出来绝对是最顶尖的。

GIF动图方案

和视频方案基本一致,唯一的区别是使用 GIF 动图作为背景图使用,效果也非常好。

问题点在于需要UI做一系列的动图效果,GIF 动图在加载上速度也不算太快,毕竟比较好的动图也不会太小。

还有一个问题在于如果屏幕大小发生变化,或者不是标准屏,可能存在图片拉伸/裁切等问题。

如果有UI协助,采用这个方案也非常不错。

Canvas渲染

采用 Canvas 渲染的方案实现这个是我最后的选择,原因有三:

  • Canvas性能开销不算太大,对低端设备相对比较友好
  • Canvas不依赖静态资源,弱网环境下不影响加载效果
  • Canvas能够根据屏幕大小达到自适应效果,避免特殊屏幕尺寸显示异常

采用 Canvas 粒子效果和渐变效果模拟阳光和雨滴、雪花等等状态,实现天气状态。

代码

初始化遮罩层

目前使用的是 Vue3 框架,因为是遮罩层所以采用 pointer-events: none; 鼠标穿透,避免影响大屏正常的操作。

<template>
    <canvas
        ref="weatherCanvas"
        class="weather-mask"
        :style="{ opacity: maskOpacity }"
    ></canvas>
</template>

<style scoped>
.weather-mask {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none; /* 鼠标事件穿透 */
    z-index: 10; /* 确保在内容上方,可根据项目调整 */
}
</style>

这里需要注意,初始化画布的时候要记得设置一下width、height,让画布充满整个屏幕。

晴天效果

晴天效果采用光照渐变效果,在 Canvas 中绘制了一个从左上角到右下角线性渐变的效果,来模拟阳光照射的感觉。

同时增加部分光斑效果,模仿阳光投射在玻璃上的感觉。

// 创建从左上角到右下角的线性渐变(模拟阳光照射)
lightGradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);

// 绘制光斑效果
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 255, 200, ${Math.random() * 0.1 + 0.05})`;
ctx.fill();

雨天效果

雨天采用粒子效果,实现细长的雨丝效果,这里没有做明显的区分,对于小雨、中雨、大雨。

其实想要区分也很简单,只要控制粒子的数量和速度即可。

ctx.beginPath();
ctx.moveTo(particle.x, particle.y);
ctx.lineTo(particle.x, particle.y + particle.height);
ctx.strokeStyle = particle.color.replace('OPACITY', particle.opacity);
ctx.lineWidth = particle.width;
ctx.stroke();

这里我进行了简单的封装,因为雨天、雪天、雾天等等大部分都用到了粒子效果,所以针对粒子的绘制部分进行了封装。

因为下雨是一个连续的绘制过程,所以动画部分做了简单的循环。

const animate = () => {
    updateParticles(props.weatherType);
    animationId = requestAnimationFrame(animate);
};

下雪效果

下雪本质上和下雨区别不大,唯一的区别是粒子的状态、运动速度和运动方向。

这里没有采用雪花造型的粒子,确实做出来了,但是效果并不好,不如这种圆形的效果看起来好一些。

雪花的绘制和雨滴的绘制区别在于,雨滴的宽度是1,而雪花的大小是一定范围内随机的。

ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
ctx.fillStyle = particle.color.replace('OPACITY', particle.opacity);
ctx.fill();

雷阵雨效果

雷阵雨效果是这里面个人觉得做的最好的一个,通过对下雨效果增加随机雷电闪烁屏幕的效果,达到雷阵雨天气的遮罩。

下雨仍然是复用的。

// 绘制主闪电路径
ctx.globalCompositeOperation = 'lighter';
ctx.strokeStyle = `rgba(255, 255, 255, ${thunderAlpha})`;
ctx.lineWidth = Math.random() * 8 + 4;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(startX, startY);

// 绘制闪电分支
ctx.lineWidth = Math.random() * 4 + 1;
ctx.beginPath();
ctx.moveTo(branch.startX, branch.startY);
let bx = branch.startX;
let by = branch.startY;
const dx = Math.random() * 30 - 15;
const dy = Math.random() * 20 + 5;
ctx.lineTo(bx + dx, by + dy);
bx += dx;
by += dy;
ctx.stroke();

// 闪烁效果
ctx.fillStyle = `rgba(255, 255, 255, ${thunderAlpha * 0.1})`;

总结

这个遮罩我做了好几天,Canvas部分我也不是特别的熟悉,所以很多地方仍然有非常大的优化空间,有感兴趣的朋友可以移步下面的文章获取源代码。

# 动态天气实时渲染动态生成组件,附源代码及详细注释

至于为啥收费,我也是想尝试一下代码还能不能搞到钱,毕竟现在的软件行业白嫖是大家的常态。

如果您能支持1元钱那我不胜感激,如果确实认为不值,自己能够写出更好的,那我也祝福。

一共做个几个效果:晴、雨、雪、雾、雷阵雨、多云、沙尘、阴天。有兴趣的朋友可以运行起来自行查看一下。

昨天以前首页

要闹哪样?又出现了一款新的格式化插件,尤雨溪力荐,速度提升了惊人的45倍!

作者 李剑一
2026年3月3日 15:05

前两天刚刚讨论完Vize(参考这篇文章: # 前端圈子又出新东西了,大幅提升解析速度。尤雨溪推荐,但我不太推荐),这两天发现前端又出现新工具了,而且是尤大力荐的,我得到这个消息还算是比较晚的了。

其实这款插件早已官宣,最最关键的一点是,它的速度比咱们常用的Prettier快了整整45倍。

今天咱们简单看一下这款插件 —— oxfmt

image.png

背景

其实前端最近几年一直在致力于底层的革新,原因也非常简单,Js在系统中的运行效率和编解码速度远逊于Rust这样的语言。

所以Vite中的 Rollup 变成了 Rolldownesbuild 变成了 Oxc

大家可能不太清楚 Oxc 是啥,咱们简单过一下。

image.png

OxcVoidZero 团队(Vite 核心团队,尤雨溪的公司)用 Rust 开发的 JS/TS 全链路工具链。

简单说就是以后前端的底层部分全都用 Rust 写了,补齐了 Oxc 以后,Rust在前端领域实现了全替换。

带来的好处不言而喻,首先是速度。

作为编译型语言,Rust 的执行效率接近 C/C++,相比传统前端工具的 JS/Go 实现构建 / 转译速度提升 数倍到数十倍。

并且内存占用降低 50%+,大型项目不会出现 JS 工具的内存溢出 / 卡顿问题,真正意义上实现了闪电般的加载速度

其次做为底层语言,Rust 的所有权、借用检查机制从语法层面杜绝空指针、内存泄漏等常见问题,前端工具的崩溃率、异常率大幅降低,尤其适合大型工程化场景。

最关键的一点,Rust 编译出的二进制文件无需依赖 Node.js 运行时,在 Windows/macOS/Linux 上的执行逻辑、性能表现高度一致。

解决了 JS 工具在不同系统下的兼容性问题,完美解决了跨平台一致性的问题。

Oxfmt

理解了Oxc就能简单说说 Oxfmt 了,Oxfmt 是 Oxc 生态中的代码格式化工具,也是目前已经基本上完成的 Rust 替换 Prettier 的例子。

image.png

Oxfmt Beta 几个关键词过一下:

  • 100% Prettier 兼容,无缝迁移
  • 支持 --migrate-prettier
  • 支持更丰富的文件格式
  • Import 自动排序
  • package.json 自动排序
  • Node.js API
  • IDE 完美支持

因为本身 Oxfmt 就可以看作是 Prettier 的Rust版本,所以团队在开发的时候也选择了对 Prettier API的完全兼容,所以开发者一般是没啥感知的。

你能够感受到的也就是快!

尝鲜

安装 Oxfmt

pnpm add -D oxfmt

这里需要给 oxfmt 配置一下脚本,找到 package.json

{
  "scripts": {
    "fmt": "oxfmt",
    "fmt:check": "oxfmt --check"
  }
}

现在已经可以用了。

# 格式化文件
pnpm run fmt

# 检查格式,但不修改文件
pnpm run fmt:check

以上是比较粗浅的应用,真正想要实现项目内详细可用还需要创建一下配置文件,oxfmt 默认使用 .oxfmtrc.json 作为配置文件。

# 初始化配置文件
oxfmt --init

# 从Prettier迁移
oxfmt --migrate prettier

# 全量格式化
npx oxfmt . --write

工程化应用

日常项目开发过程中主要是保存、提交的时候自动格式化,这个场景应用的比较多。

oxfmt 在 vscode 中可以通过 Oxfmt 官方扩展实现保存格式化。

首先安装 Oxfmt 官方插件,搜索 Oxc 即可。

image.png

.vscode/settings.json 中添加以下配置:

{
    "editor.formatOnSave": true,
    "[vue]": {
        "editor.defaultFormatter": "oxc.oxfmt"
    },
    "[javascript]": {
        "editor.defaultFormatter": "oxc.oxfmt"
    },
    "[typescript]": {
        "editor.defaultFormatter": "oxc.oxfmt"
    }
}

之前用 Prettier 的同学记得关掉,避免冲突。

提交格式化可以通过 pre-commit 钩子实现:

pnpm install -D husky lint-staged
npx husky install
# 添加 pre-commit 钩子
npx husky add .husky/pre-commit "npx lint-staged"

同时在 package.json 中增加相应配置:

{
    "lint-staged": {
        "*.{js,ts,vue,json,css,scss,md}": [
        "oxfmt --write"
        ]
    }
}

总结

我个人比较建议大家从现在开始就把 Prettier 替换为 Oxfmt,原因主要有三:

  • Rust实现前端底层已成为大趋势,未来一定是这套工程大一统。
  • 速度更快,内存用的更少,Vite团队开发。
  • 确实好用,接近无感的存在。
❌
❌