znlgis 博客

GIS开发与技术分享 — GDAL · GeoServer · PostGIS · QGIS · OpenLayers · Cesium · FreeCAD · NPOI

第02章:环境搭建与第一个全景应用

工欲善其事,必先利其器。本章将从零开始,手把手带你搭建 Photo-Sphere-Viewer 的开发环境,并运行第一个全景查看器应用。读完本章,你将拥有一份可直接使用的 HTML 文件,双击打开就能在浏览器中漫游 360° 全景图。

2.1 环境要求

Node.js

Photo-Sphere-Viewer v5.x 要求 Node.js 22 或更高版本。如果你计划使用 npm 构建项目,请先确认 Node.js 版本:

node -v
# 应输出 v22.x.x 或更高
npm -v
# 应输出 10.x.x 或更高

如果版本过低,建议使用 nvm-windows 或直接从 Node.js 官网 下载安装最新 LTS 版本。

如果你只使用 CDN 方式(纯 HTML 文件),则不需要 Node.js,只需一个现代浏览器即可。

浏览器

Photo-Sphere-Viewer 基于 Three.js 在 WebGL 上渲染全景图,因此必须使用支持 WebGL 的现代浏览器

浏览器 最低版本 推荐版本
Chrome 56+ 最新版
Firefox 52+ 最新版
Edge 79+ 最新版
Safari 11+ 最新版

你可以在浏览器地址栏输入 chrome://gpu(Chrome)或 about:support(Firefox)查看 WebGL 支持状态。也可以访问 get.webgl.org 快速检测。

全景图素材准备

在开始之前,你需要准备一张等距柱状投影(Equirectangular Projection)的全景图作为素材。这种投影方式将 360° 球面信息映射到一个 2:1 宽高比的矩形平面中——宽度恰好是高度的两倍。

分辨率 宽高比 常见用途
4096 × 2048 2:1 标准全景展示
8192 × 4096 2:1 高清晰度展示
16384 × 8192 2:1 VR 头显级别

如果你手头暂时没有全景图,以下资源可以免费获取测试素材:

  • polyhaven.com — 高质量 HDR 全景图,下载时可选择 JPG 格式
  • unsplash.com — 搜索 “360 panorama” 获取摄影作品
  • 使用手机拍摄全景照片后,用工具拼接成 2:1 等距柱状投影

本教程示例使用一张示例图,你替换成自己的图片路径即可。

2.2 安装方式详解

Photo-Sphere-Viewer 提供三种安装方式,适应不同的开发场景。

2.2.1 npm / yarn 安装(推荐用于工程化项目)

这是标准的前端工程化安装方式,适合使用 Vite、Webpack 等打包工具的项目。

# 使用 npm
npm install @photo-sphere-viewer/core

# 或使用 yarn
yarn add @photo-sphere-viewer/core

安装完成后,node_modules/@photo-sphere-viewer/core 目录下包含完整的源码、类型定义和样式文件。后续章节安装插件时,也使用相同的 npm scope 前缀,例如:

npm install @photo-sphere-viewer/markers-plugin
npm install @photo-sphere-viewer/gallery-plugin

2.2.2 CDN 方式(推荐用于快速原型)

如果你不想搭建构建工具链,或者只是快速验证想法,可以使用 jsDelivr CDN 配合浏览器原生 Import Map 直接在 HTML 中加载。Import Map 是现代浏览器的一项特性,它允许你用简洁的模块名替代冗长的 CDN 路径。

完整的 Import Map 配置如下:

<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.163.0/build/three.module.js",
    "@photo-sphere-viewer/core": "https://cdn.jsdelivr.net/npm/@photo-sphere-viewer/core@5/index.module.js"
  }
}
</script>

几点说明:

  • type="importmap" 告诉浏览器这是一张模块映射表,必须在 <script type="module"> 之前声明。
  • three 是 Photo-Sphere-Viewer 的底层依赖,必须显式指定。版本号应与 @photo-sphere-viewer/core 要求的 Three.js 版本匹配。v5.x 通常依赖 Three.js r163 左右,建议查阅官方文档确认精确版本。
  • @photo-sphere-viewer/core 指向 jsDelivr 上的 ES Module 版本(index.module.js)。@5 表示 v5 的最新版;生产环境建议锁定精确版本号,例如 @5.11.0/index.module.js
  • Import Map 在 Chrome 89+、Edge 89+、Firefox 108+、Safari 16.4+ 中得到支持。如需兼容更老的浏览器,可使用 es-module-shims polyfill。

2.2.3 手动下载

你还可以从 GitHub Releases 页面 下载预构建的打包文件。下载后的目录结构如下:

photo-sphere-viewer-5.x.x/
├── index.module.js       # ES Module 版本
├── index.js              # UMD 版本
├── index.css             # 样式文件
└── plugins/              # 插件文件

下载后将文件放到项目目录中,通过相对路径引用即可。这种方式适用于无网络环境或需要内网部署的场景。

2.3 项目初始化

下面分别演示两种最常见的项目初始化方式。

2.3.1 方式一:纯 HTML/JS 项目(零构建)

新建一个 index.html 文件,写入以下内容。这是你能写出的最简全景查看器,不到 30 行代码,双击即可运行:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>我的第一个全景应用</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    html, body { width: 100%; height: 100%; overflow: hidden; }
    #viewer {
      width: 100%;
      height: 100%;
    }
  </style>
</head>
<body>
  <div id="viewer"></div>

  <script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.163.0/build/three.module.js",
      "@photo-sphere-viewer/core": "https://cdn.jsdelivr.net/npm/@photo-sphere-viewer/core@5/index.module.js"
    }
  }
  </script>

  <script type="module">
    import { Viewer } from '@photo-sphere-viewer/core';

    const viewer = new Viewer({
      container: document.querySelector('#viewer'),
      panorama: 'https://photo-sphere-viewer-data.netlify.app/pano/example.jpg',
    });
  </script>
</body>
</html>

将上述代码保存为 .html 文件,用浏览器打开,你应该能看到一张可以拖拽旋转的全景图。

重要提醒:由于使用了 ES Module 和 Import Map,不能直接双击本地 HTML 文件打开——浏览器会因 file:// 协议限制而拒绝加载远程模块。你需要使用本地静态服务器:

# 如果安装了 Node.js
npx serve .

# 或使用 Python
python -m http.server 8080

# 或使用 VS Code 的 Live Server 插件

然后通过 http://localhost:3000(或对应端口)访问页面。

2.3.2 方式二:Vite + TypeScript 项目

对于正式项目,推荐使用 Vite + TypeScript 的组合。以下是完整的初始化步骤:

步骤 1:创建 Vite 项目

npm create vite@latest my-panorama -- --template vanilla-ts
cd my-panorama

步骤 2:安装依赖

npm install @photo-sphere-viewer/core

步骤 3:配置 TypeScript

编辑 tsconfig.json,确保 moduleResolution 设置为 bundler(Vite 推荐配置):

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

步骤 4:编写代码

编辑 src/main.ts

import '@photo-sphere-viewer/core/index.css';
import { Viewer } from '@photo-sphere-viewer/core';

const viewer = new Viewer({
  container: document.querySelector<HTMLDivElement>('#viewer')!,
  panorama: 'https://photo-sphere-viewer-data.netlify.app/pano/example.jpg',
});

编辑 index.html,确保有一个 #viewer 容器,并且 body 和 html 铺满全屏:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>全景应用</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    html, body { width: 100%; height: 100%; overflow: hidden; }
    #viewer { width: 100%; height: 100%; }
  </style>
</head>
<body>
  <div id="viewer"></div>
  <script type="module" src="/src/main.ts"></script>
</body>
</html>

步骤 5:启动开发服务器

npm run dev

打开浏览器访问 Vite 提示的本地地址(通常是 http://localhost:5173),即可看到全景图。

关键点:CSS 导入 @photo-sphere-viewer/core/index.css必须的。如果没有这行,Viewer 的核心 UI 组件(导航栏、加载提示等)将没有任何样式,页面看起来会像是坏掉了一样。

2.4 创建第一个 Viewer

上一节的示例能跑起来,但我们对每一行代码背后的含义还需要深入理解。这一节逐行拆解 Viewer 的创建过程。

2.4.1 container — 挂载容器

container: document.querySelector('#viewer')

container 接受一个 DOM 元素CSS 选择器字符串(如 '#viewer')。Photo-Sphere-Viewer 会将 Three.js 的 <canvas> 以及导航栏等 UI 组件渲染到这个容器内部。

容器在创建 Viewer 时必须已经存在于 DOM 中,并且具有明确的尺寸。 如果容器的 widthheight 为 0,canvas 将无法渲染,页面显示为一片空白或黑屏。

常见的容器样式设置:

/* 方式一:铺满整个视口 */
#viewer {
  width: 100vw;
  height: 100vh;
}

/* 方式二:固定尺寸 */
#viewer {
  width: 800px;
  height: 600px;
}

/* 方式三:通过父元素撑开 */
.parent {
  display: flex;
  height: 100vh;
}
#viewer {
  flex: 1;
}

2.4.2 panorama — 全景图源

panorama: 'https://example.com/pano.jpg'

panorama 参数指定全景图的来源,支持多种格式:

类型 示例 说明
URL 字符串 './pano.jpg' 最常用,直接给出图片地址
函数 () => '/dynamic/path.jpg' 动态决定图片路径,每次创建时调用
Blob / File new Blob(...)fileInput.files[0] 从用户上传的文件中读取
相对路径 '../assets/pano.jpg' 相对于 HTML 页面的路径

对于等距柱状投影(Equirectangular)全景图,Photo-Sphere-Viewer 使用默认的适配器即可。更多全景图类型(立方体贴图、鱼眼等)将在第 04 章详细介绍。

2.4.3 CSS 导入

无论使用 npm 还是 CDN,都必须在代码中导入 CSS

// npm 方式
import '@photo-sphere-viewer/core/index.css';

// CDN 方式(在 HTML 中)
<link rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/@photo-sphere-viewer/core@5/index.css" />

如果你看到导航栏文字挤在一起、按钮没有图标、加载动画不显示,十有八九是忘记了导入 CSS。

2.5 Viewer 生命周期

一个 Viewer 实例从创建到销毁,经历以下生命周期阶段:

new Viewer(config)
    │
    ▼
启动 Three.js 渲染器,创建场景与相机
    │
    ▼
开始加载全景纹理(TextureLoader)
    │
    ▼
纹理加载完成 → 渲染第一帧 → 触发 'ready' 事件
    │
    ▼
用户交互(拖拽、缩放、调用 API)
    │
    ▼
viewer.destroy() → 释放 WebGL 资源、移除事件监听、DOM 清理

2.5.1 监听 ready 事件

ready 事件在纹理加载完成、首帧渲染之后触发。此时 Viewer 的所有功能已就绪,可以安全地调用 API 或添加插件:

const viewer = new Viewer({
  container: document.querySelector('#viewer'),
  panorama: './pano.jpg',
});

viewer.addEventListener('ready', () => {
  console.log('全景图加载完成,可以开始交互');

  // 此时可以安全地调用 API
  viewer.rotate({
    yaw: Math.PI / 2,
    pitch: 0,
  });

  // 或动态添加插件
  // viewer.getPlugin(...)
}, { once: true });

{ once: true } 表示该监听器只执行一次,执行后自动移除。对于 ready 事件来说这是好习惯,避免重复触发时执行多余逻辑。

2.5.2 错误处理

全景图加载可能因为多种原因失败——网络断开、图片不存在、跨域限制等。Photo-Sphere-Viewer 会在控制台输出错误信息,但你也可以通过监听事件来做更精细的错误处理:

viewer.addEventListener('load-error', (e) => {
  console.error('全景图加载失败:', e.error);
  // 在页面上显示错误提示
  const msg = document.createElement('div');
  msg.textContent = '全景图加载失败,请检查网络或图片地址。';
  msg.style.cssText = 'color: red; padding: 20px; text-align: center;';
  e.target.container.appendChild(msg);
});

在 Vite 或 TypeScript 环境中,也可以使用 try-catch 包裹构造函数调用(尽管大部分错误是异步的,需要在事件中捕获):

try {
  const viewer = new Viewer({ ... });
  // 异步错误由事件系统处理
} catch (err) {
  // 构造阶段同步错误
  console.error('Viewer 创建失败:', err);
}

2.5.3 destroy() — 销毁实例

当 Viewer 不再需要时(例如 SPA 页面切换、组件卸载),必须调用 destroy() 来释放资源。

为什么必须调用 destroy()?

  • Three.js 的 WebGL 上下文不会自动释放,不调用 destroy() 会导致 GPU 内存泄漏。
  • 事件监听器不会自动移除,不销毁会导致”幽灵监听器”累积。
  • 在单页应用中,反复创建和忘记销毁 Viewer 会让浏览器标签页越来越卡,最终崩溃。

正确做法:

// 页面卸载时销毁
window.addEventListener('beforeunload', () => {
  viewer.destroy();
});

// 或 SPA 路由切换时销毁
function onRouteLeave() {
  viewer.destroy();
}

destroy() 调用后,Viewer 实例不应再被使用。如果之后需要再次显示全景图,应当重新 new Viewer(...)

2.6 基础交互操作

Viewer 默认开启了一套完整的交互系统。以下是各交互方式及其对应的配置选项。

2.6.1 鼠标拖拽旋转

这是最核心的交互方式:按住鼠标左键拖拽,全景图跟随旋转。

  • 水平拖拽 → 调整 yaw(偏航角),即左右旋转
  • 垂直拖拽 → 调整 pitch(俯仰角),即上下旋转

如果需要捕获鼠标交互事件(例如实现自定义光标、记录用户行为),可以监听 clickposition-updated 等事件。

2.6.2 滚轮缩放

滚动鼠标滚轮控制缩放,实际改变的是 FOV(视场角,Field of View)——FOV 越小,画面越”拉近”;FOV 越大,画面越”推远”。

const viewer = new Viewer({
  container: document.querySelector('#viewer'),
  panorama: './pano.jpg',
  mousewheel: true,             // 默认开启滚轮缩放
  mousewheelCtrlKey: false,     // 设为 true 则需按住 Ctrl 才能缩放
});

mousewheelCtrlKey 的意义:在全景图占据整个页面的场景中,用户本能地会用滚轮滚动页面。如果你的页面还有可滚动内容,将该选项设为 true 可以避免误触缩放,只有按住 Ctrl + 滚轮时才触发全景缩放。

2.6.3 键盘控制

方向键和 WASD 提供了另一种导航方式:

const viewer = new Viewer({
  container: document.querySelector('#viewer'),
  panorama: './pano.jpg',
  keyboard: true,               // 默认开启(也可以通过 'always' 强制)
});

键盘映射:

按键 动作
← / A 向左旋转
→ / D 向右旋转
↑ / W 向上旋转
↓ / S 向下旋转
+ / = 放大(减小 FOV)
- 缩小(增大 FOV)

注意:如果页面中存在 <input><textarea> 等可编辑元素,Photo-Sphere-Viewer 会自动在这些元素获得焦点时禁用键盘导航,避免干扰文字输入。如果你希望始终响应键盘(即使焦点在输入框上),可以将 keyboard 设置为 'always'

2.6.4 移动端触摸手势

在手机上,Photo-Sphere-Viewer 自动适配触摸交互:

手势 动作
单指滑动 旋转全景图
双指捏合/张开(pinch) 缩放
双指旋转 调整视角(部分浏览器支持)

移动端有两个特殊选项需要关注:

const viewer = new Viewer({
  container: document.querySelector('#viewer'),
  panorama: './pano.jpg',
  touchmoveTwoFingers: false,   // 设为 true 则单指滑动不旋转,必须双指
});

touchmoveTwoFingers:如果你的全景图嵌入在一个可滚动的页面中,设为 true 可以避免用户单指滑动页面时误触发全景旋转——只有双指滑动才会旋转全景,单指滑动正常滚动页面。对于独立的全景页面(body 无滚动),保持 false 即可。

2.7 简单自定义

Viewer 提供了一系列配置选项,让你在零代码的情况下实现常见的定制需求。

2.7.1 初始视角

默认情况下,全景图打开时视角对准正前方(yaw = 0, pitch = 0)。你可以指定初始视角,让用户打开页面时直接看到你最想展示的方向:

const viewer = new Viewer({
  container: document.querySelector('#viewer'),
  panorama: './pano.jpg',
  defaultYaw: '90deg',         // 初始水平旋转 90 度(朝右看)
  defaultPitch: '-20deg',      // 初始俯仰 -20 度(稍向下看)
});

defaultYawdefaultPitch 接受数字(弧度)或带单位的字符串(degradgrad)。建议使用 deg 字符串,直观易读。

2.7.2 缩放范围

限制用户可以缩放到的最近和最远距离:

const viewer = new Viewer({
  container: document.querySelector('#viewer'),
  panorama: './pano.jpg',
  minFov: 30,     // 最小视场角(最大放大倍数),默认约 30 度
  maxFov: 90,     // 最大视场角(最小放大倍数),默认约 90 度
  defaultZoomLvl: 50,  // 初始缩放级别对应的 FOV,默认约 75 度
});
  • FOV 越小,画面越大(更”近”)——防止用户放大到看到像素格子
  • FOV 越大,画面越小(更”远”)——防止用户缩到全景图只占屏幕一小块

defaultZoomLvl 的值应在 minFovmaxFov 之间。如果不确定设多少,保持默认即可。

2.7.3 移动和缩放速度

调整旋转和缩放的灵敏度:

const viewer = new Viewer({
  container: document.querySelector('#viewer'),
  panorama: './pano.jpg',
  moveSpeed: 0.5,    // 旋转速度倍率,默认 1。值越大旋转越快
  zoomSpeed: 0.8,    // 缩放速度倍率,默认 1。值越大缩放越灵敏
});
  • moveSpeed: 0.3 — 适合需要精细调整的场景(如虚拟展厅,用户需要仔细看展品)
  • moveSpeed: 2.0 — 适合快速浏览的场合

2.7.4 添加标题(caption)

导航栏默认显示在顶部,caption 选项可以在导航栏中显示标题文字:

const viewer = new Viewer({
  container: document.querySelector('#viewer'),
  panorama: './pano.jpg',
  caption: '黄石国家公园 · 老忠实泉',
});

标题文字显示在导航栏的中间位置,字体大小和颜色由主题 CSS 控制。

2.7.5 添加描述(description)

description 将文本显示在侧边信息面板中(需要用户点击导航栏的信息按钮):

const viewer = new Viewer({
  container: document.querySelector('#viewer'),
  panorama: './pano.jpg',
  caption: '黄石国家公园',
  description: '老忠实泉(Old Faithful)是黄石国家公园最著名的间歇泉,每隔约 90 分钟喷发一次,水柱高达 30-55 米。',
});

描述支持纯文本。如果需要更丰富的内容(图片、链接),可以结合 MarkersPlugin 来实现,这部分将在第 05 章介绍。

2.7.6 加载中提示

在全景图加载期间,Photo-Sphere-Viewer 会显示一个加载动画。你可以自定义这个阶段的提示:

const viewer = new Viewer({
  container: document.querySelector('#viewer'),
  panorama: './pano.jpg',
  loadingImg: './assets/loading.gif',   // 自定义加载动画图片
  loadingTxt: '正在加载全景图,请稍候...',   // 自定义加载文字
});
  • loadingImgnull 时,使用默认的 CSS 旋转圆圈动画
  • loadingTxt 的默认值为 'Loading...',你可以改为中文提示

2.7.7 导航栏按钮

默认导航栏包含缩放按钮和全屏按钮。你可以在初始化时调整导航栏的按钮布局:

const viewer = new Viewer({
  container: document.querySelector('#viewer'),
  panorama: './pano.jpg',
  navbar: [
    'zoom',        // 缩放按钮组(放大/缩小/重置)
    'fullscreen',  // 全屏按钮
    'download',    // 下载按钮
    'caption',     // 标题文字
    'description', // 描述信息按钮
  ],
});

navbar 数组中的字符串按顺序从左到右排列。如果你不需要某个按钮,只需在数组中移除对应的字符串即可。

2.8 完整示例代码

下面给出一个功能完整的 HTML 文件,整合了本章介绍的各项功能。你可以直接保存运行:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Photo-Sphere-Viewer 完整示例</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    html, body {
      width: 100%;
      height: 100%;
      overflow: hidden;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    }

    #viewer {
      width: 100%;
      height: 100%;
    }
  </style>
</head>
<body>
  <div id="viewer"></div>

  <!-- Import Map:映射模块名到 CDN 地址 -->
  <script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.163.0/build/three.module.js",
      "@photo-sphere-viewer/core": "https://cdn.jsdelivr.net/npm/@photo-sphere-viewer/core@5/index.module.js"
    }
  }
  </script>

  <script type="module">
    import { Viewer } from '@photo-sphere-viewer/core';

    const viewer = new Viewer({
      // ========== 基础配置 ==========
      container: document.querySelector('#viewer'),
      panorama: 'https://photo-sphere-viewer-data.netlify.app/pano/example.jpg',

      // ========== 初始视角 ==========
      defaultYaw: '0deg',
      defaultPitch: '-15deg',

      // ========== 缩放 ==========
      minFov: 30,
      maxFov: 90,
      defaultZoomLvl: 60,
      mousewheel: true,
      mousewheelCtrlKey: false,

      // ========== 速度 ==========
      moveSpeed: 1.0,
      zoomSpeed: 1.0,

      // ========== 键盘 ==========
      keyboard: true,

      // ========== 触摸 ==========
      touchmoveTwoFingers: false,

      // ========== 导航栏 ==========
      navbar: [
        'zoom',
        'fullscreen',
        'download',
        'caption',
        'description',
      ],

      // ========== 标题与描述 ==========
      caption: '全景图示例',
      description: `这是一个完整的 Photo-Sphere-Viewer 示例。

      操作提示:
      · 鼠标拖拽旋转视角
      · 滚轮缩放画面
      · 方向键/WASD 移动视角
      · 移动端单指滑动旋转,双指缩放`,

      // ========== 加载提示 ==========
      loadingTxt: '正在加载全景图...',
    });

    // ========== 生命周期事件 ==========
    viewer.addEventListener('ready', () => {
      console.log('[PSV] 全景图已就绪,可以开始交互');
    }, { once: true });

    viewer.addEventListener('load-error', (e) => {
      console.error('[PSV] 加载失败:', e.error);
    });

    // ========== 页面卸载时销毁 ==========
    window.addEventListener('beforeunload', () => {
      viewer.destroy();
    });
  </script>
</body>
</html>

运行效果:打开页面后,你会看到一个全屏的全景图查看器,顶部导航栏显示标题和操作按钮,左下角有描述信息入口。拖拽鼠标即可自由探索全景场景。

2.9 常见问题排查

全景图不显示 / 黑屏

这是最常被问到的问题。按以下顺序逐一排查:

1. 容器尺寸为零

打开浏览器开发者工具(F12),选中 #viewer 元素,查看其 widthheight 是否为 0。如果是,检查 CSS 中是否设置了尺寸:

html, body { width: 100%; height: 100%; }
#viewer { width: 100%; height: 100%; }

2. 图片路径错误

检查浏览器 Network 面板,看全景图片的 HTTP 请求是否返回 404 或 CORS 错误。如果是相对路径,确认路径相对于 HTML 文件的位置是否正确。

3. 跨域(CORS)问题

当全景图片托管在不同域名下,且服务器未设置 Access-Control-Allow-Origin 响应头时,浏览器会阻止跨域加载。表现为控制台报错:

Access to image at 'https://example.com/pano.jpg' from origin 'http://localhost:3000'
has been blocked by CORS policy

解决方法:

  • 确保图片服务器设置了正确的 CORS 头
  • 将图片放在与 HTML 同域下
  • 在开发阶段,可使用支持 CORS 的测试图源或代理服务器

4. WebGL 不支持

在控制台输入以下代码检测:

const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
console.log(gl ? 'WebGL 已支持' : 'WebGL 不可用');

如果返回 null,检查浏览器是否启用了硬件加速,或者尝试更新显卡驱动。

图片加载失败

除了 CORS,以下情况也会导致加载失败:

  • 文件名包含特殊字符或中文:建议文件名仅使用英文字母、数字和连字符
  • 文件格式不兼容:确保使用 JPEG、PNG 或 WebP 格式(推荐 JPEG 以获得较好兼容性和压缩比)
  • 图片体积过大:超高清全景图可能因加载超时而失败,建议控制在 5MB 以内用于 Web 展示

交互不响应

如果全景图显示正常,但鼠标拖拽、滚轮缩放无效:

  • 检查是否有其他元素覆盖#viewer 之上(例如一个透明的 <div> 遮罩层)
  • 检查 mousewheel 是否被显式设为 false
  • 检查 navbar 数组中是否错误地移除了某些按钮(这不影响底层交互,但可能让你觉得”按钮消失了”)
  • 在开发者工具中检查是否有 JavaScript 错误导致 Viewer 初始化中断

内存泄漏 / 页面越来越卡

在 SPA(单页应用)中,如果切换路由时没有销毁 Viewer,每次切换都会在内存中残留一个完整的 WebGL 上下文:

// 错误做法:路由切换时不销毁
router.beforeEach(() => {
  new Viewer({ ... });  // 每次创建新实例,旧的未销毁 → 内存泄漏
});

// 正确做法:先销毁旧实例
let viewer = null;
router.beforeEach(() => {
  if (viewer) {
    viewer.destroy();
    viewer = null;
  }
  viewer = new Viewer({ ... });
});

你可以在 Chrome 开发者工具的 Performance Monitor 中观察 JS heap size 和 DOM Nodes 数量——如果这些数字只增不减,说明存在内存泄漏。

CSS 未导入

如果导航栏裸奔(无样式)、按钮没有图标、加载动画不显示,十有八九是没导入 CSS:

// 务必在入口文件顶部加入这行
import '@photo-sphere-viewer/core/index.css';

2.10 本章小结

本章从环境准备开始,详细介绍了 Photo-Sphere-Viewer 的三种安装方式,演示了纯 HTML 和 Vite + TypeScript 两种初始化路径,深入讲解了 Viewer 的生命周期、交互系统和常见配置项。最后给出了一个开箱即用的完整示例。

关键要点回顾:

要点 一句话总结
容器尺寸 必须明确设置,否则黑屏
CSS 导入 不导入则 UI 裸奔
Import Map CDN 用户的必经之路,注意加载顺序
lifecycle ready 事件之后才能安全操作 Viewer
destroy() 不用时一定要销毁,否则内存泄漏
CORS 跨域图片需要服务端配合或同域部署

从下一章开始,我们将深入 Viewer 的核心配置与 API,学习如何通过代码精确控制全景图的每一个行为。


← 上一章 下一章 →