教程 - 深度探讨在 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 ...
随机推荐
- Java 基础常见知识点&面试题总结(上),2022 最新版!| JavaGuide
你好,我是 Guide.秋招即将到来,我对 JavaGuide 的内容进行了重构完善,公众号同步一下最新更新,希望能够帮助你. 基础概念与常识 Java 语言有哪些特点? 简单易学: 面向对象(封装, ...
- camunda开源流程引擎的数据库表结构介绍
Camunda bpm流程引擎的数据库由多个表组成,表名都以ACT开头,第二部分是说明表用途的两字符标识.本文以Camunda7.11版本为例,共47张表. ACT_RE_*: 'RE'表示流程资源存 ...
- 视图模板引擎——Vue【双向绑定】原理剖析
首先我们来了解一下MVC.MVP.MVMM这三大架构模式在前端角度上的理解. MVC分别是 Model(模型).View(视图).Controller(控制器)三个模块.View(视图层)最主要完成前 ...
- JavaScript做简单的购物车效果(增、删、改、查、克隆)
比如有时候遇到下面这种情况,点击加入购物车,然后在上方的购物车中动态的添加商品以及商品的信息,我们就可以通过JavaScript实现简单的这些操作. 首先我们需要在html文档中,通过css对页面的布 ...
- 使用Java编写一个日期时间封装类
package base; import java.util.GregorianCalendar; import java.util.StringTokenizer; import java.util ...
- C#.NET笔试题-基础
1.C#中堆和栈的区别? 栈:由编译器自动分配.释放.在函数体中定义的变量通常在栈上. 堆:一般由程序员分配释放.用new.malloc等分配内存函数分配得到的就是在堆上. 存放在栈中时要管存储顺序, ...
- .Net下极限生产力之efcore分表分库全自动化迁移CodeFirst
.Net下极限生产力之分表分库全自动化Migrations Code-First ## 介绍 本文ShardinfCore版本x.6.x.x+ 本期主角: - [`ShardingCore`](htt ...
- Grammarly for Chrome-语法、用词自动检查
从语法和拼写到风格和语气,Grammarly帮助你消除写作错误,找到完美的词语来表达自己.当你在Gmail.Twitter.LinkedIn和几乎任何你发现自己在写作的地方写作时,你都会从Gramma ...
- cup缓存基础知识
目录 cup缓存 缓存结构 直接映射缓存 cup缓存 CPU缓存(CPU Cache)的目的是为了提高访问内存(RAM)的效率,这虽然已经涉及到硬件的领域,但它仍然与我们息息相关,了解了它的一些原理, ...
- Windows JDK 的下载与安装
Java Development Kit 简称 JDK,任何需要开发 Java 程序的环境都需要进行安装 JDK. JDK 下载地址:https://www.oracle.com/java/techn ...