教程 - 深度探讨在 Vue3 中引入 CesiumJS 的最佳方式
由于 Vue2 已经进入维护期,且 Vue2 对待组件内的 data 是无差别使用 Object.defineProperties 递归将其劫持的,对于复杂状态的对象会造成严重的 JavaScript 访问路径过长而导致的 性能问题,这个应该是老生常谈了。
Vue3 提供了 markRaw 函数,标记一个对象,令 Vue 不再将其视作 响应式 数据,所以本文基于 Vue3 来介绍如何引入 CesiumJS。
心急的朋友可以直接跳过本文的介绍,拉到文末,有示例工程 zip 下载。
1. 你应该先知道的基础知识
除了 Vue3 和 Vue2 的响应式设计区别外,我认为还需要补充一点知识。
1.1. CesiumJS 的库构成
Cesium 是一个高度集成的重型 JavaScript 库,这是共识。它的源码虽然是 ESModule 格式的,但是并没有直接提供类似 index.js 的出口文件,也不存在子包的概念,只是在 Source 文件夹下简单分了几个大板块文件夹,例如 Source/Renderer 文件夹就是 CesiumJS 中整个渲染器的代码模块。
通常,除了二次修改 CesiumJS 源代码构建自己的分支版本,一般不会在 WebAPP 中直接使用 CesiumJS 的源码。一般使用的是 CesiumJS 的 构建版本,也就是 Build 文件夹下的压缩版或未压缩版库文件。
主库文件有三种格式,ESModule 的是 index.js,IIFE 的是 Cesium.js,CommonJS 的是 index.cjs。除了主库文件外,构成构建版本的 CesiumJS 还有 4 个文件夹下的静态资源:
Assets文件夹,图片或 JSON 等前端运行时可能用到的资源ThirdParty文件夹,WebAssembly 等前端运行时可能用到的第三方资源Widgets文件夹,主要是各个 CesiumJS 自带的界面小部件的 CSS 文件Workers文件夹,前端运行时用到的 WebWorker 的构建版本(WebWorker 由于一些原因,在前端运行时仍然用 CommonJS 格式加载)

因此,你在任何所谓的教程里面都会看到这四个静态资源文件夹的复制操作,除了 CDN 直接使用的方式。我在这里说清楚,希望你知道原因。
1.2. 选择 Vite3 和 pnpm 的理由
笔者是 Vite 1.0 的首批用户。尤雨溪第一次介绍 Vite 是在 Vue 3.0 测试版网络会议上,只是作为一个很小的“玩具”介绍了一下,当时的 Vite 还是与 Vue 强关联的,后来到了 Vite 2.0 才解耦合。简单的说,Vite 3.0 对 Vite 2.x 并不是破坏性更新,只是考虑到 NodeJS 12.x 已经 EOL 了,索性 3.0 就不再支持 NodeJS 12.x,其余特性笔者没特别了解。
简单的说,使用 Vite 作为开发服务器和打包工具,不外乎几个原因:
- esbuild 速度有目共睹
- 中文文档齐全
- 是 cli 的官方指定继任者
对于开项目,我有几点建议:
- 如果你只是写一个小的项目,可以用 Vite 官方模板;如果是 Vue3 项目,直接使用
create-vue脚手架或者安东尼小哥的vitesse模板工程替代@vue/cli即可;这条也适用于想更多自定义的项目、团队; - 如果你需要开箱支持的文件式路由、SSR、全栈开发等特性,请使用 Nuxt
简单起见,我将使用 create-vue 来演示。
最后说明为什么用 pnpm —— 它速度足够快,也有效缩小了 node_modules 的体积,对付 peer 依赖也很棒。你当然也可以用 npm 和 yarn。
1.3. 使用 External 模式引入静态库 - 不打包静态库
在 1.1 小节我已经说明了 CesiumJS 库的构成,有一个库文件,以及 4 个静态资源文件夹。
由于 npm 下载的 cesium 包中已经有官方打包好的 构建版本 库了,没有必要让 Vite 再次将 CesiumJS 源代码再次打包,而应将其作为外部依赖,也就是配置 Vite 的 external 项,不打包,使用 CDN 或 public 文件夹下的库程序、资源。
当然,这是对官方库没有任何修改、直接使用的前提;如果想二次修改 CesiumJS 源代码,无论是自己打包,还是使用 npm-patch,上述方法便不再需要参考。
在 Vite 中,需要借助两个社区插件完成 CesiumJS 的外部化:
- vite-plugin-externals
- vite-plugin-html-config
前者告诉 Vite 什么 dependencies 不参与打包,后者告诉 Vite 打包后的产物哪些 dependencies 需要在页面入口 html 文件中随 public 目录(或 CDN)引入。
具体配置过程参考 2.4 小节。
1.4. 切勿什么都 import - 以及页面运行的时候的路径与开发时的路径
在代码中,有一些特殊的关键字、指令会被打包器识别,打包器会帮你把相关的资源打包、转译。
在 Webpack 时代,你就见过使用 import 指令引入 css 文件或图片:
import 'foo.css'
import Logo from '@/assets/logo.svg'
Webpack 本身只能处理 import 进来的 JavaScript 文件,对于其它的资源,则使用各种 Loader 完成打包处理过程。
Vite 则开箱支持了众多 Web 前端的资源的导入。但是,3D 领域的模型文件就没有支持,不能通过 import 命令导入,除非安装了处理对应文件格式的插件。像下面的导入指令,Vite 并不会帮你处理:
import CarModel from '@/assets/data/model.glb'
并且会在启动时给你报错。
另一个问题是要明白,当前工程的路径 ≠ 运行时的路径。运行时又分开发运行时、打包后的运行时。
所以,在一些 API 需要传递资源路径时,请一定要确保在运行时它是可以被浏览器正确请求到的,例如:
new Cesium3DTileset({
// vite 等打包器并不会帮你处理这个路径,Cesium 在发出请求也不会
url: '@/assets/tilesets/tileset.json'
// 或下面的例子,运行时的
url: './data/tileset.json'
})
又如:
new Cesium3DTileset({
// 或下面的例子,运行时的基础地址是 http://localhost:5173,
// 那么前端发起请求就会是 http://localhost:5173/data/tileset.json
url: './data/tileset.json'
})
最后,我认为 CompositionAPI 和 OptionAPI 并不是本文讨论的重点,但是我会使用 setup-script + CompositionAPI 来介绍。
顺便,既然都 Vue3 了,那 TypeScript 肯定是少不了的。
2. 一步一步教你创建项目
请确保你的机器安装了 NodeJS,版本最好使用 LTS(写文的时候,推荐 16+ 版本),以及 node 包管理工具能正常在你的命令行环境(Windows - powershell/cmd/gitbash,macOS 和 Linux 应该是有自带的 shell)使用。
我的包管理工具是 pnpm。
虽然但是,我不是很想说 NodeJS 于前端的关系,请读者自行了解 NodeJS 包以及其包管理工具。这里简单说明:NodeJS 是 vite 或 webpack 开发时的程序服务器(简称开发服务器,devServer)的基石,就像 jdk 于 Spring 框架一样。运行你的页面代码的仍旧是浏览器,打包器(vite 内置的是 rollup,webpack 自己就是)会把你写的 Vue 单文件组件、ts 代码合并、打包、转译成优化后的产物。
2.1. 使用 create-vue 或 vite 模板
为了使用最新的全局状态管理器 pinia,我选择用 create-vue 这个能替代 @vue/cli 的新版脚手架,完成具备如下开发工具配置的工程创建:
- 使用
pinia - 使用
typescript - 使用
eslint - 使用
prettier
不要再问我这些是什么,这些属于 Vue 和 Web 前端的生态。
创建命令:
pnpm create vue
确保你的网络没有问题,那么你就可以伴随着如下命令行提示创建出与我一样的初始工程:

2.2. 指定版本安装 cesium 依赖
node 包管理器安装依赖包,如果不指定版本,会默认当前版本以及以上的版本都可以运行,也就是会在 package.json 的依赖列表中的版本号前加一个 ^ 号:
{
"dependencies": {
"cesium": "^1.96.0"
}
}
但是,CesiumJS 每个月都会更新,而且时不时会有重大变动,我的建议是手动锁死版本,而不是依赖锁文件(pnpm 是 pnpm-lock.yaml,npm 是 package-lock.json,yarn 是 yarn.lock)。
pnpm add cesium@1.96.0
这样,以后安装依赖就不会安装到最新版本,以至于项目出现因重大变动导致运行不起来的问题了。
2.3. 不使用锁文件
在 package.json 同级别路径下创建 .npmrc 文件,配置包管理器的行为、参数,使用如下配置即可不产生锁文件:
package-lock=false
这对于严格控制 package.json 中依赖版本的项目,而且不指定包管理器(即允许任意使用 pnpm、yarn、npm 来管理依赖)的项目来说是十分有利的。
2.4. 配置 External 和构建后的 index.html
先安装 Vite 插件:

然后,在 vite.config.ts 中修改 Vite 的配置:
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import htmlConfig from 'vite-plugin-html-config'
import { viteExternalsPlugin } from 'vite-plugin-externals'
// https://vitejs.dev/config/
export default ({ mode: VITE_MODE }: { mode: string }) => {
const env = loadEnv(VITE_MODE, process.cwd())
console.log('VITE_MODE: ', VITE_MODE)
console.log('ENV: ', env)
const plugins = [vue()]
const externalConfig = viteExternalsPlugin({
cesium: 'Cesium'
})
const htmlConfigs = htmlConfig({
headScripts: [
{
src: './lib/cesium/Cesium.js'
}
],
links: [
{
rel: 'stylesheet',
href: './lib/cesium/Widgets/widgets.css'
}
]
})
plugins.push(
externalConfig,
htmlConfigs
)
return defineConfig({
root: './',
build: {
assetsDir: './',
minify: ['false'].includes(env.VITE_IS_MINIFY) ? false : true
},
plugins: plugins,
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
}
注意到导出的是一个函数,与 Vite 初始化的配置文件直接使用 import { defineConfig } from 'vite' 函数定义的是略有区别的。这个函数的参数是一个类型为 { mode: string } 的对象,参考:配置 Vite | Vite 官方中文文档
之后在 2.6 会详细说明这个 mode 有什么用,这里先略过。
这小节主要是对这两个插件的配置:
const plugins = [vue()]
const externalConfig = viteExternalsPlugin({/* ... */})
const htmlConfigs = htmlConfig({/* ... */})
plugins.push(
externalConfig,
htmlConfigs
)
return defineConfig({
/* ... */
plugins: plugins,
})
这两个插件的用法和用途,就不详细说明了,简单说明:
vite-plugin-external 插件的 key 是 dependencies 的名称,value 是打包后代码全局访问的变量名称(作为 Namespace),即 cesium 依赖在打包后在 window.Cesium 上访问。
vite-plugin-html-config 插件中,如果像我一样是从 node_modules 中复制的 CesiumJS 库文件,而不是填写的 CDN 外链,那么打包后页面运行时,静态库文件的相对路径是从 defineConfig 中的 root 起算的。
在 2.5 小节会讲到 CesiumJS 的静态资源复制。
2.5. 静态资源复制脚本
在 1.1 小节中已详细说明了 CesiumJS 的静态资源的 4 个文件夹。由于此示例工程使用 node_modules 下的 CesiumJS,也即 node_modules/cesium/Build/Cesium 或未压缩版的 node_modules/cesium/Build/CesiumUnminified,并且 Vite 构建时会把 public 文件夹下的资源原封不动复制到发布文件夹下,所以需要借助 NodeJS 文件操作 API 复制这些资源到 public 文件夹下。
如果你使用 CDN 上的 CesiumJS,而不是 node_modules 下的 CesiumJS 依赖,就不需要这一步,但是还是得配置 CESIUM_BASE_URL,告诉前端运行时的 CesiumJS 相对路径起源于哪里(参考 2.6 小节)。
这个脚本可以放置于 scripts/ 目录下,方便起见,我放在了项目根目录。
复制我使用 recursive-copy 包,删除文件我使用 del 包,都作为 devDependencies 安装。
import copy from 'recursive-copy'
import {
deleteSync
} from 'del'
const baseDir = `node_modules/cesium/Build/CesiumUnminified`
const targets = [
'Assets/**/*',
'ThirdParty/**/*',
'Widgets/**/*',
'Workers/**/*',
'Cesium.js',
]
deleteSync(targets.map((src) => `public/lib/cesium/${src}`))
copy(baseDir, `public/lib/cesium`, {
expand: true,
overwrite: true,
filter: targets
})
然后,我在 package.json 的 scripts 中添加了两个命令:
{
"scripts": {
"postinstall": "node static-copy.js",
"static-copy": "node static-copy.js"
}
}
postinstall 会在 pnpm install 后自动执行静态资源复制,static-copy 则允许手动升级 cesium 包后更新 public 文件夹下 CesiumJS 的静态文件。
注意 deleteSync 和 copy 函数的目标文件夹路径,我设为了 public/lib/cesium,与 2.4 小节中 htmlConfig 的配置是一样的。
为了简单起见,vite.config.ts 中配置的 build.assetsDir 我改为了 ./;否则,deleteSync 和 copy 的目标路径就要手动加上 build.assetsDir 了。例如,默认的 assetsDir 是 assets,那么目标路径就从 public/lib/cesium 变成了 public/assets/lib/cesium。
请十分仔细地注意这些路径问题,分清楚 public 文件夹、build.assetsDir 的意义,static-copy.js 文件的 cwd 等,分清楚 NodeJS 脚本和前端运行时的相对路径问题。
2.6. 使用环境变量配置 CESIUM_BASE_URL
CESIUM_BASE_URL 告诉 CesiumJS 在前端运行时相对哪个路径访问那 4 个文件夹下的静态资源,与 2.4、2.5 小节中的路径配置十分相关,请务必读懂 2.4、2.5 小节中的路径配置。
当然,如果你使用的是 CDN 上的 CesiumJS 库,那么这个环境变量配置就要配置成 CDN 的基础路径。例如,
https://unpkg.com/cesium@1.96.0/Build/Cesium/Cesium.js对应的 CESIUM_BASE_URL 就是https://unpkg.com/cesium@1.96.0/Build/Cesium
考虑到我使用的是 node_modules 下的包,复制到 public 文件夹下,所以我在环境变量文件 .env 中指定的 CESIUM_BASE_URL 是一个相对于工程运行时的地址:
VITE_CESIUM_BASE_URL = './lib/cesium'
随 Vite 启动工程后,在入口文件 src/main.ts 中将 CesiumJS 的前端运行时基路径挂在至全局:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import './main.css'
Object.defineProperty(globalThis, 'CESIUM_BASE_URL', {
value: import.meta.env.VITE_CESIUM_BASE_URL
})
createApp(App)
.use(createPinia())
.mount('#app')
为了便于类型提示,我将 VITE_CESIUM_BASE_URL 的类型写在了工程根目录下的 env.d.ts 文件中:
/// <reference types="vite/client" />
interface ImportMetaEnv {
VITE_CESIUM_BASE_URL: string
}
这是使用 TypeScript 的 interface 补全 import.meta.env 的类型定义。
为了让 TypeScript 识别这个类型声明文件,还得在 tsconfig.json 中配置类型文件路径,把 env.d.ts 添加进来:
{
"include": [
"env.d.ts",
"src/**/*",
"./vite.config.*"
]
}
环境变量是 Vite 的功能,参考:环境变量和模式 | Vite 官方中文文档
在 2.4 小节有完整的 vite.config.ts 配置文件,其中默认导出的是一个函数,函数参数的意义已经在 2.4 中有官方参考资料。
下面这几行代码就是在启动工程时,让 Vite 加载与 vite.config.ts 同路径下的环境变量文件,并读取里面的环境变量:
export default ({ mode: VITE_MODE }: { mode: string }) => {
// 根据当前 mode 读取对应文件中的环境变量
const env = loadEnv(VITE_MODE, process.cwd())
// 在控制台打印出来
console.log('VITE_MODE: ', VITE_MODE)
console.log('ENV: ', env)
/* ... */
}
2.7. 使用全局状态库跨组件共享 Viewer 对象
这一步是可选的,当然,我强烈推荐你做这一步,这对跨组件访问 Viewer 很有帮助。
作为替代方案,你可以使用 Vue 的
provide / injectAPI,穿透传递 Viewer 给所有子组件,对兄弟组件就无能为力了(可以借助 EventBus,略麻烦,不再赘述)。
首先,是在 src/main.ts 中让 Vue 实例安装 pinia 状态管理库:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import './main.css'
/* ... */
createApp(App)
.use(createPinia())
.mount('#app')
然后,是创建状态存储器,位于 src/store/sys.ts:
import { defineStore } from 'pinia'
import { Viewer } from 'cesium'
export interface SysStore {
cesiumViewer: Viewer | null
}
export const useSysStore = defineStore({
id: 'sys',
state: (): SysStore => ({
cesiumViewer: null
}),
actions: {
setCesiumViewer(viewer: Viewer) {
this.cesiumViewer = viewer
}
}
})
紧接着,是在 App.vue 中使用 Vue 的 markRaw API,将 Viewer 对象标记为非响应式,避免 Vue 响应式劫持产生的访问性能问题,并调用 store 对应的 set 方法:
import { ref, onMounted, markRaw } from 'vue'
import { ArcGisMapServerImageryProvider, Camera, Viewer, Rectangle } from 'cesium'
import { useSysStore } from '@/store/sys'
const containerRef = ref<HTMLDivElement>()
const unvisibleCreditRef = ref<HTMLDivElement>()
const sysStore = useSysStore()
onMounted(() => {
const viewer = new Viewer(containerRef.value as HTMLElement)
const rawViewer = markRaw(viewer)
sysStore.setCesiumViewer(rawViewer)
})
最后,你就可以在兄弟组件中访问到 Viewer 了:
<!-- BrotherComponent.vue -->
<template>
<button @click="onClick">控制台打印 viewer</button>
</template>
<script setup lang='ts'>
import { useSysStore } from '@/store/sys'
const sysStore = useSysStore()
const onClick = () => {
// 也可以写 getter,但我觉得这样就足够说明问题了
console.log(sysStore.$state.cesiumViewer)
}
</script>
3. 伸手的看过来 - 工程下载
由于篇幅原因,有些文章中的代码会省略、简化,工程的源码、配置可能与上述有细微差别,请自行了解。
https://share.weiyun.com/ndkxAeIv
教程 - 深度探讨在 Vue3 中引入 CesiumJS 的最佳方式的更多相关文章
- 在Vue-cli3.x中引入element-ui的新方式
今天登上element官网,发现element对vue-cli3.x的项目做了特殊定制: 这意味着即使是按需引入,也无需像过去那样手动安装babel-plugin-component,配置babel. ...
- 【转载】在Angular 2/Typescript中声明全局变量的最佳方式是什么?
问题详细描述 我想在Typescript语言中的Angular 2中声明一些全局可见的变量.最佳的实践方法是? 推荐的实现方法 这是最简单的解决方案,无需使用Service或Observer: 将全局 ...
- Android初级教程获取手机位置信息GPS与动态获取最佳方式
简单介绍一下gps定位的操作. 主要是靠locationmanger这个api完成的一些操作:通过获取这个实例,然后调用它的requestLocationUpdates方法进行注册.传入的参数分别有以 ...
- [golang]使用gomail发邮件(在Go中发送电子邮件的最佳方式)
1 前言 定义邮箱服务器连接信息,如果是网易邮箱 pass填密码,qq邮箱填授权码(客户端专用密码). gomail包: go get gopkg.in/gomail.v2 更多功能可以参考 http ...
- jsp文件引入js文件的方式(项目部署于web容器中)
在页面中引入javascript文件的方式是多种多样的,本文介绍两种. 通过<script>标签插入js文件 通过这种方式引入的js,写对js文件和jsp文件的路径很重要.下面给出一个项目 ...
- Vue3.0中引入地图(谷歌+高德+腾讯+百度)
1 概述 项目需求需要引入地图,对于目前最新的Vue3.0,无论是百度/高德/腾讯地图目前还没有适配,只有Vue 2.x版本的: 目前只有谷歌地图的Vue3.0适配: 但是没有适配并不代表不能使用,本 ...
- Spring Data JPA系列2:SpringBoot集成JPA详细教程,快速在项目中熟练使用JPA
大家好,又见面了. 这是Spring Data JPA系列的第2篇,在上一篇<Spring Data JPA系列1:JDBC.ORM.JPA.Spring Data JPA,傻傻分不清楚?给你个 ...
- 『vue踩坑日常』 在index.html中引入静态文件不生效
Vue日常踩坑日常 -- 在index.html中引入静态文件不生效问题 本文针对的是Vue小白,不喜勿喷,谢谢 出现该问题的标志如下 控制台warning(Resource interpreted ...
- 【IDEA】项目中引入Spring MVC
一.原文说明: IntelliJ idea创建Spring MVC的Maven项目 - winner_0715 - 博客园 https://images2015.cnblogs.com/blog/82 ...
随机推荐
- NetCore框架WTM的分表分库实现
介绍 本期主角: ShardingCore 一款ef-core下高性能.轻量级针对分表分库读写分离的解决方案,具有零依赖.零学习成本.零业务代码入侵 WTM WalkingTec.Mvvm框架(简称W ...
- Caller 服务调用 - Dapr
前言 上一篇我们讲了使用HttpClient的方式调用,那么如果我们现在需要更换为通过dapr实现服务调用,我们需要做哪些事情呢? Caller.Dapr 入门 如果我们的项目原本使用的是Caller ...
- .NET 处理[未能为 SSLTLS 安全通道建立信任关系]问题
更新记录 2022年4月16日本文迁移自Panda666原博客,原发布时间:2021年7月16日. 在.NET的开发过程中,发现[基础连接已经关闭: 未能为 SSL/TLS 安全通道建立信任关系]问题 ...
- kruskal 及其应用
kruskal 最小生成树 kruskal 是一种常见且好理解的最小生成树(MST)算法. 前置知识 并查集和路径压缩 生成树 在有 n 的顶点的无向图中,取其中 n-1 条边相连,所得到的树即为生成 ...
- BUUCTF-佛系少年
佛系少年 这题我感觉超扯,不知道当时环境是不是断网的,断网咋解密的出来.. 下载后有个压缩包,带加密的,首先16进制看看是否是真加密 这里可以看到,压缩包数据区这里都是未加密的方式 但是到了压缩包目录 ...
- JavaScript写倒计时
在网页中,特别是电商网站中,倒计时的出现频率很高,接下来给大家介绍一下怎么用JavaScript写一个倒计时.代码如下: 首先我们通过Date构造函数的方法创建一个倒计时的结束的时间.并将其转换为毫秒 ...
- 使用C#编程语言开发Windows Service服务
转载-https://www.cnblogs.com/yubao/p/8443455.html Create Windows Service project using Visual Studio C ...
- 攻防世界 miscmisc
63.miscmisc(感觉这题挺有意思的,单独拿出来记录一下) 得到一张png,扔进kali中,foremost得到两个zip,打开其中一个,发现一张jpg和一个加密的zip,在jpg中分离出一个z ...
- CSS 浮动 (二)
CSS 浮动 本人是一名大二学生,欢迎大家进行交流 V15774135883 推荐一个是自学的网站 里面有超多培训机构的大课,地址 有需要可以加我免费拿! 传统网页布局的三种方式 网页布局的本质--用 ...
- rust实战系列-base64编码
前言 某些只能使用ASCII字符的场景,往往需要传输非ASCII字符的数据,这时就需要一种编码可以将数据转换成ASCII字符,而base64编码就是其中一种. 编码原理很简单,将原始数据以3字节(24 ...