目錄
- 1. 本篇適用范圍與目的
- 1.1. 適用范圍
- 1.2. 目的
- 2. 牛刀小試 - 先看到地球
- 2.1. 創建 Vue3 - TypeScript 工程并安裝 cesium
- 2.2. 清理不必要的檔案并創建三維地球
- 2.3. 中段解疑 - 奇怪的路徑
- 2.4. 打包部署
- 2.5. 有限的優化
- 3. CesiumJS 前置知識
- 3.1. CesiumJS 依賴包中的資料說明
- 3.2. 構建后的 CesiumJS 庫組成 - 主庫檔案與四大檔案夾
- 3.3. 鏈接庫檔案和四大檔案夾的 CESIUM_BASE_URL 變數
- 4. 現代前端工具的基本常識
- 4.1. 選擇 Vite 的理由
- 4.2. 為什么外部化引入(External)一個庫
- 4.3. TypeScript 型別提示
- 4.4. 開發服務器的路徑與代碼中的路徑問題
- 5. 教程(原理)正文
- 5.1. 使用 create-vite 在命令列創建工程
- 5.2. 指定版本安裝 cesium
- 5.3. 包管理工具鎖檔案的取舍
- 5.4. 使用插件外部化 CesiumJS
- 5.5. 使用插件自動在 index.html 引入 Cesium.js 庫檔案
- 5.6. 四大靜態檔案夾與庫檔案的拷貝(CDN或獨立部署了 CesiumJS 庫可省略此步)
- 5.7. 額外優化 - 使用環境變數配置 CESIUM_BASE_URL 并適配其它配置
- 5.9. 額外優化 - 使用 gzip 預先壓縮打包產物
- 5.8. 如何共享 CesiumJS 的 Viewer 物件
- 6. 探究 CesiumJS 等庫的前端組件封裝
- 6.1. 以 CesiumJS 等庫為主的看板式工程
- 6.2. 后臺管理系統式工程
- 7. 示例工程下載
這篇如果 Vue 和 CesiumJS 不發生史詩級的變動,應該不會再有后文了,主要是這類文章沒什么營養,
這篇主要修正上篇 https://www.cnblogs.com/onsummer/p/16629036.html 中一些插件的變化,并升級開發服務器的版本,
心急的朋友拉到文末,有示例工程鏈接下載,
1. 本篇適用范圍與目的
1.1. 適用范圍
-
嚴格使用 Vue3 + TypeScript 的前端專案,包管理器默認使用 pnpm
-
構建工具使用 Vite4
-
使用原生 CesiumJS 依賴做應用開發
-
客戶端渲染,因為我不太熟悉 Vue 的服務端渲染,有本篇的介紹后,熟悉 SSR 的讀者可以自己接入
-
單頁應用,多頁應用也可以參考此法
鑒于國內使用 CesiumJS 的比例大多數為應用開發(粗話即“APICaller”),而非擴展開發(基于原始碼作新功能封裝、打包),所以我默認讀者使用 CesiumJS 是通過 npmjs 網站(或鏡像站)拉取的依賴,即:
pnpm add cesium@latest
有想修改原始碼再自己打包的讀者,我覺得應該去看我的原始碼系列博客,
1.2. 目的
在 Vue3 工程中引入 CesiumJS 的最佳方式,并引出地圖組件封裝的簡單經驗兩則,
這篇文章更傾向于給讀者一些原理,而不是提供一套開箱即用的工具,有能力的讀者可以根據這篇文章的原理,結合 Vite 或其它打包工具的 API,寫一個專屬插件,
2. 牛刀小試 - 先看到地球
如果沒有快速看到 3D 虛擬地球,我覺得心急的朋友會心急(廢話),
第 2 節不需要知道原理,原理和最佳實踐請往下閱讀 3、4、5 節,
2.1. 創建 Vue3 - TypeScript 工程并安裝 cesium
如果你沒有命令列基礎,也不懂什么是 NodeJS、npm,不知道 node-package 是什么東西,建議先補補 NodeJS 為基礎的前端工具鏈知識,
直接上命令列(要聯網,配好你的 npm 源),請在任意你方便的地方運行:
pnpm create vite
輸入你想要的手動選擇 Vue、TypeScript 的模板即可,然后進入工程檔案夾,我的工程檔案夾叫作 v3ts-cesium-2023,所以我接下來要安裝 CesiumJS:
cd ./v3ts-cesium-2023
pnpm add [email protected]
pnpm add 會一并把模板的其它依賴下載下來,所以就不用再執行 pnpm install 了,
我在安裝 cesium 時指定了版本,是考慮到 很多專案可能不太注意依賴版本管理,所以干脆鎖死固定版本,
2.2. 清理不必要的檔案并創建三維地球
我移除了 src/assets 和 src/components 檔案夾,并洗掉全部 src/style.css 的代碼,改寫 main.ts、App.vue、style.css 如下:
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import './style.css'
declare global {
interface Window {
CESIUM_BASE_URL: string
}
}
createApp(App).mount('#app')
你注意到了,我在 main.ts 中為全域宣告了 CESIUM_BASE_URL 變數的型別為 string,這在 App.vue 中就會用到:
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { TileMapServiceImageryProvider, Viewer, buildModuleUrl } from 'cesium'
import 'cesium/Build/CesiumUnminified/Widgets/widgets.css'
const viewerDivRef = ref<HTMLDivElement>()
window.CESIUM_BASE_URL = 'node_modules/cesium/Build/CesiumUnminified/'
onMounted(() => {
new Viewer(viewerDivRef.value as HTMLElement, {
imageryProvider: new TileMapServiceImageryProvider({
url: 'node_modules/cesium/Build/CesiumUnminified/Assets/Textures/NaturalEarthII',
})
})
})
</script>
<template>
<div id="cesium-viewer" ref="viewerDivRef"></div>
</template>
<style scoped>
#cesium-viewer {
width: 100%;
height: 100%;
}
</style>
我在 App.vue 組件的 mounted hook 中輕松地創建了 Viewer,語法不再贅述,我做了如下幾個點讓地球顯示出來:
- 向
Viewer構造引數傳遞了div#cesium-viewer元素的ref值,并將其型別as HTMLElement,以滿足 CesiumJS 的型別 - 引入 CesiumJS 自己的 css,供 Viewer 的各個內置界面小組件(時間軸等)提供 CSS 樣式
- 為
Viewer創建了一個 CesiumJS 自帶的離線 TMS 瓦片服務,你可能很奇怪為什么路徑是node_modules起頭的,待會解釋,這個 TMS 瓦片服務只有 2 級 - 設定
CESIUM_BASE_URL
帶著好奇心,先別急,等我講完,最后是 style.css,是一些簡單的樣式:
/* style.css */
html, body {
padding: 0;
margin: 0;
}
#app {
height: 100vh;
width: 100vw;
}
隨后,命令列啟動開發服務器:
pnpm dev
在 Vite4 的強大性能加持下,很快就起起來了,這個時候就可以在瀏覽器看到一個具有兩級離線 TMS 瓦片服務的三維地球:

2.3. 中段解疑 - 奇怪的路徑
你注意到了,2.2 小節里有兩個奇怪的路徑:
window.CESIUM_BASE_URL = 'node_modules/cesium/Build/CesiumUnminified/'
new TileMapServiceImageryProvider({
url: 'node_modules/cesium/Build/CesiumUnminified/Assets/Textures/NaturalEarthII',
})
這是因為 Vite 開發模式下(pnpm dev,NODE_ENV 是 development)是直接把工程根路徑(即 vite.config.ts 所在的檔案夾)映射到 http://localhost:5173/ 這個 URL 上的,所以理所當然填寫 CesiumJS 庫檔案的路徑就要從 node_modules 開始寫起,
我這里選用的是 CesiumUnminified 版本(未壓縮版本),
CESIUM_BASE_URL 的含義是,專案運行的根網路路徑(這里就是指 Vite 開發服務器的默認地址 http://localhost:5173/),加上 CESIUM_BASE_URL 后,在這個拼成的路徑就能訪問到 CesiumJS 的入口檔案,即完整版:
http://localhost:5173/node_modules/cesium/Build/CesiumUnminified/Cesium.js(這個指向的是未壓縮版的 IIFE 庫檔案)
你可以把這個完整地址在啟動后粘貼到瀏覽器的地址欄,然后回車,就能看到 CesiumJS 打包后的庫檔案原始碼了,
同理,自帶的 TMS 瓦片資料就存放在 http://localhost:5173/node_modules/cesium/Build/CesiumUnminified/Assets/Textures/NaturalEarthII 地址下,TMS 服務的識別方法就是觀察網路請求有無一個 tilemapresource.xml 檔案:

2.4. 打包部署
有了 2.3 小節的解釋,現在要上生產環境了,生產環境也許是 nginx,也許是其它的 Web 服務器,這個時候就沒有 node_modules 了,畢竟 Vite 的開發服務器職責已經在 build 后完成,
這個時候就要作出以下修改:
- 修改
CESIUM_BASE_URL為生產環境能訪問的 CesiumJS 庫檔案的地址 - 修改
TileMapServiceImageryProvider的離線 TMS 路徑
在修改之前,需要你把 CesiumJS 的四大靜態資源檔案夾從 node_modules 中拷貝出來,跟著做就行,
我把 node_modules/cesium/Build/CesiumUnminified/ 這個未壓縮版本的檔案夾下所有內容,即 Assets、Widgets、Workers、ThirdParty 四個檔案夾拷貝到 public/libs/cesium/ 下(沒有就自己創建一下):

CesiumJS 的正常運行需要這些靜態檔案,原因在第 3 節會詳細說明,先照做,
然后修改 CESIUM_BASE_URL 和離線 TMS 的地址:
window.CESIUM_BASE_URL = 'libs/cesium/'
new TileMapServiceImageryProvider({
url: 'libs/cesium/Assets/Textures/NaturalEarthII',
})
此時運行 pnpm dev,依舊是正常的,只不過靜態檔案資源已經從 node_modules/cesium/Build/CesiumUnminified/ 改到了 public/libs/cesium/ 下,
順帶一提,Vite 開發服務器的根路徑,除了掛載了工程的根目錄,還掛載了工程根目錄下的
public目錄,public 目錄的作用請自己查閱 Vite 檔案,
這個時候就可以使出 pnpm build 然后 pnpm preview 組合了,打包并使用 http 服務預覽構建后的產物:
pnpm build && pnpm preview
我的 CPU 是 i5 13600K,在 7 秒多的打包后緊接著就啟動了 4173 埠的服務:

運行起來和開發時無異,
2.5. 有限的優化
有人也許對 Vite 等打包工具比較熟悉,可以配置分包(修改 vite.config.ts 中的配置引數)來辨別打包后的產物各自的體積:
import { defineConfig, splitVendorChunkPlugin } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), splitVendorChunkPlugin()],
build: {
rollupOptions: {
output: {
manualChunks: {
cesium: ['cesium']
}
}
}
}
})
這樣之后打包的產物就略有不同:


似乎
splitVendorChunkPlugin()不添加到plugins陣列中也可以生效,但是為了盡可能優化打包產物,還是加上了
但是,即便這樣,也只是把 cesium 依賴分拆到一個塊檔案中,并沒有實質性改變如下事實:
-
Vite 仍需對毫無修改的 cesium 依賴包打包一次,CesiumJS 已經在發布 npm 包時進行了構建,其雖然有 ESModule 格式的產物,但是并不支持
Tree-Shaking減小大小,事實上也沒有必要,CesiumJS 的內部是高度耦合的三維渲染器、各種演算法,這種高度集成的演算法產物保持一致是比較好的(或許官方未來可能有改變,但是至少現在沒有),所以在我這里這 “7秒多” 的打包時間毫無必要,在其它打包工具也是一樣的(Webpack等) -
我需要手動復制
node_modules/cesium/Build/CesiumUnminified/下的四個靜態資源檔案夾 -
對多個發布環境仍需要手動修改
CESIUM_BASE_URL,如果切換到 CDN 或內網已有 CesiumJS 在線庫資源,這個改起來就麻煩許多
考慮到真正的專案大概率不會使用自帶的離線二級 TMS 瓦片服務,所以不算作可優化的點,
所以,我將費點篇幅,先介紹 CesiumJS 包 的基本知識,再介紹一些現代前端工具的常識,最后再介紹我認為最合理最靈活的引入方式,
授人以漁,你可以根據這篇文章的內容自己寫一個方便的 Vite 插件,也可以就此為止,如果你不嫌棄上述三個麻煩事兒,
3. CesiumJS 前置知識
3.1. CesiumJS 依賴包中的資料說明

通過包管理器下載到 node_modules 下的 cesium 依賴,是 CesiumJS 打包好的“包”,它具備如下資料:
-
不完整的源代碼,位于
node_modules/cesium/Source/目錄下,含一個出口檔案Cesium.js和一個 TypeScript 型別定義檔案Cesium.d.ts,出口檔案匯出的所有模塊,也就是真正的原始碼均來自子包@cesium/engine和@cesium/widgets(于 1.100 版本變動,將代碼分割于子包中) -
打包后的庫程式檔案,含
IIFE、ES-Module、CommonJS三種格式,每種格式又有壓縮代碼版本和未壓縮版本,分別存放于node_modules/cesium/Build/Cesium/、node_modules/cesium/Build/CesiumUnminified/目錄下,各種格式各有用途,如果是 CommonJS 環境下,會參考index.cjs,而如果是 ES-Module 環境下,會參考index.js;剩下的Cesium.js則用在 IIFE 環境下, -
無論是不完整的原始碼,還是打包后的庫程式檔案,都會附帶所需的靜態資源檔案
應用級別的開發,只需要用到打包后的庫程式檔案以及 TypeScript 型別定義檔案就好了,
我一般選用的是 IIFE 格式里的壓縮版本,即 node_modules/cesium/Build/Cesium/Cesium.js,這個庫檔案只有 3.7 MB,gzip 壓縮后可小于 1 MB,體積控制很不錯,
3.2. 構建后的 CesiumJS 庫組成 - 主庫檔案與四大檔案夾
主庫檔案在 2.1 小節已經說明,壓縮版和未壓縮版均含 CommonJS、IIFE、ES-Module 三種格式的庫檔案,檔案名有所不同,

CesiumJS 的源代碼(即 node_modules/cesium/Source/ 的出口檔案,以及這個出口檔案引自的 @cesium/engine 和 @cesium/widgets 子包的代碼模塊)并不是完整的 cesium 庫,cesium 庫還包括:
- 一套
WebWorker,用于引數幾何的生成、ktx2 紋理解碼、draco 壓縮資料解碼等多執行緒任務 - 一套 css 檔案,用于
Viewer下具有 HTML 界面的內置組件的樣式表達,例如時間線等組件 - 一套靜態資源檔案,用于構造默認場景和內置組件,例如 SkyBox 背景圖、圖示、離線的兩級 TMS 資料等
- 一些第三方庫,用于 basis 紋理和 draco 資料解碼的 WebAssembly 檔案以及配套的 WebWorker 檔案
僅靠源代碼是不能運行起 Cesium 三維地球場景的,必須使用構建版本的 CesiumJS 庫,而官方構建后的 CesiumJS 庫(即發布在 npm 上的 cesium 包)一定會包含以上四類檔案,即 node_modules/cesium/Build/ 下的壓縮和未壓縮版本檔案夾下的 Workers、Widgets、Widgets、Assets 四大檔案夾,

3.3. 鏈接庫檔案和四大檔案夾的 CESIUM_BASE_URL 變數
在 2.2 和 2.3 小節中已經比較完備地解釋了 CESIUM_BASE_URL 的作用,它就是告訴已經運行的 CesiumJS 上哪去找四類靜態資源,
當然,可以設定私有部署的 CesiumJS 庫或者免費的 CDN:
window.CESIUM_BASE_URL = 'http://localhost:8888/cesium/1.103.0/'
window.CESIUM_BASE_URL = 'https://cdn.bootcdn.net/ajax/libs/cesium/1.103.0/'
不再贅述,
4. 現代前端工具的基本常識
4.1. 選擇 Vite 的理由
尤雨溪在某次 B 站直播介紹 Vue3 測驗版(似乎是2020年)時,在介紹完新的 setup 函式后,帶了個貨,即 Vite 的最初始版本,應該是 1.0 時代的東西了,那時還和 Vue 是強依賴的,在 Vite2 時才與具體前端框架解耦,
我在第一時間就去體驗了 Vite1.0,說實話沒什么特別的感覺,還以為是做了一個什么模板,沒想到經過 2.0 的積累更新、3.0、4.0 的快速迭代后,現在的 Vite 已經是我替代 Webpack 的主力前端開發工具了(說實話我很少用 Webpack 為底子的各種腳手架、框架),
Vite 真的很快,上一篇還是 Vite3,現在已經到 Vite4 了,這更新速度...雖然在 API 和配置上基本沒什么變化,應該在 4.x 算是穩定了,
4.2. 為什么外部化引入(External)一個庫
Vite 和 Webpack 類似,都能把一些依賴無視,不參與打包,一旦某個依賴被配置為“外部的”,即 External 化,就不會打包它了,
社區在普通前端的實踐中經常把 Vue、React、Axios 等不需要打包、可以使用高速 CDN 加速的庫都外部化了,
CesiumJS 這個體積如此巨大的庫(壓縮版 + gzip 后主庫檔案至少也有900+KB)按理說也應該外部化,極大減輕打包時的負擔和產物,使用 CDN 還能些許加速首屏性能,
External 化需要一些比較繁瑣的配置,如果讀者認為不需要外部化,任 Vite 把 CesiumJS 再次打包那幾秒鐘、十幾秒鐘也無所謂的話,其實也可以不做這一步,
既然說了最佳化實踐,那我就一定要寫這一步,萬一有人需要呢?
在之后會使用 vite-plugin-externals 插件(注意,有 s 結尾)完成外部化,
4.3. TypeScript 型別提示
沒有型別提示還得自己手動確認傳值型別是否正確,TS 在靜態代碼編輯環境借助代碼編輯器的各種功能,就可以預先檢查出可能存在的錯誤,最大地規避運行時的問題,
cesium 包自帶了型別檔案,位于 node_modules/cesium/Source/Cesium.d.ts,你也可以在其 package.json 中找到型別欄位,
我們創建工程時,模板已經配置好了 TypeScript,默認情況下不需要我們額外配置什么,正常在組件或 ts 檔案中匯入 cesium 包的模塊即可:
import { Viewer } from 'cesium'
這也是官方推薦的匯入方法,這樣匯入是具備 TS 型別提示的,
噢對了,如果你用的是 VSCode,偶爾你會遇到 TS 型別提示不正常的問題,大多數是這 5 個原因:
- 如果你在用 Volar 插件來智能提示
.vue檔案,那么你需要去 Vue 官方檔案中配置下 “take over” 模式 - 沒有安裝
typescript到開發依賴 - 安裝了 typescript 到開發依賴但是工程沒有使用開發依賴的 ts,而使用了 VSCode 自己的 ts,這個用
Ctrl + Shift + P切換一下 ts 版本即可(搜索“Select Typescript” 或直接搜 “Typescript” 選擇版本即可),會寫入.vscode/settings.json檔案 - 上述問題都排除了,也許是
tsconfig.json沒有包括目標d.ts檔案 - 也有可能某個庫壓根就沒有自帶
d.ts,也沒有對應的型別庫
4.4. 開發服務器的路徑與代碼中的路徑問題
這是一個新手問題,新手在開發工具(例如 Webpack、Vite)的滋潤下能非常熟練地從各種地方 import 各種各樣的資源,例如 ts、js、json、圖片圖示、less/css/sass 等資源模塊,
例如:
import Logo from '@/assets/svg/logo.svg'
這樣的路徑大概率是配置好 @ 指向工程下的 src 目錄,
或者裸模塊匯入:
import { ref } from 'vue'
這些看似“不就是這樣的嗎”的匯入實際上是開發工具做的努力,
然而,在 GIS、三維這些小眾的領域,開發工具就不一定有適配了,例如,你不能把相對目錄或配置好的目錄下的 glTF 模型匯入:
import Duck from './data/duck.gltf'
import Ball from '@/assets/model/duck.glb'
幸運的是對 glTF 模型已經有了 vite 插件,但是我仍然不推薦你這樣引入,
同理,CesiumJS 的 3DTiles 資料集也不要這么做,雖然它的入口檔案是一個 json 檔案,但是瓦片檔案打包器并不會幫你處理,
理清楚匯入問題后,還有一個新手常犯的問題是把“原始碼相對路徑”當作“運行時的路徑”,假設有這么一個代碼檔案 src/views/home.vue 中創建了一個 3DTiles 資料集物件:
// src/views/home.vue
Cesium3DTileset.fromUrl({
url: '../assets/tileset.json'
})
有的新手把資料就放在了上一級的 src/assets/tileset.json 路徑下,這犯了 2 個低級錯誤:
- 認為相對于當前代碼檔案的
../assets/tileset.json資料檔案路徑在運行時也能正常讀取 - 認為 CesiumJS 會幫你處理路徑問題
這點就不說怎么解決了,只求一些新手讀者能了解清楚什么是 “源代碼檔案的相對 URL” 和 “運行時 URL” 這些基本區別,
此處塞一行防爬蟲文字,原文出自 @嶺南燈火,常駐知乎博客園,其余博客社交平臺基本有號,想找原文請勞煩搜索一下~~
5. 教程(原理)正文
與其說是教程,不如說是基于第 2 節的繼續優化,優化到最佳實踐,
5.1. 使用 create-vite 在命令列創建工程
這個參考 2.1 和 2.2 小節即可,
5.2. 指定版本安裝 cesium
指定版本安裝在 2.1 小節有說明,若不指定版本安裝:
pnpm add cesium
那么在 package.json 中,cesium 依賴的版本號(首次 add 時的最新版)前面就會多一個 ^:
{
"dependencies": {
"cesium": "^1.104.0"
}
}
除非手動 update,即 pnpm update cesium@具體版本,否則 ^ 后面的版本號是不會改變的,
那如果不指定版本安裝 cesium 會用哪個版本呢?會用第一次 add 的版本,并且會寫進對應包管理器的鎖檔案中,
- pnpm 是
pnpm-lock.yaml - npm 是
package-lock.json - yarn 是
yarn.lock
5.3. 包管理工具鎖檔案的取舍
這小節可以與 5.2 一起看,鎖檔案的作用是把各個依賴包的具體版本鎖死,
有鎖檔案的 package 會從鎖檔案中找版本,否則會按 package.json 中的 “版本要求” 來獲取特定版本,
如果 package.json 中各個依賴包的版本都是確定的,專案負責人也能管理起依賴的版本控制,那么其實可以不需要鎖檔案,
我在本文就配置了 不需要鎖檔案,且我在安裝依賴時明確指定了具體版本(主要是 cesium),
對于 pnpm 和 npm,只需在工程根目錄下創建一個(如果不存在).npmrc 檔案,并寫入此配置:
package-lock=false
對于 yarn,則是創建 .yarnrc 檔案并寫入:
--install.no-lockfile true
如果專案有要求,或者版本管理比較差,我建議還是把鎖檔案留著并提交到 git 記錄中,但是 cesium 的版本,我還是強烈建議確定版本安裝:
pnpm add [email protected]
5.4. 使用插件外部化 CesiumJS
原理、原因在 4.2 小節,這里主要講配置,
- 插件① -
rollup-plugin-external-globals
外部化依賴有很多插件都可以實作,既然 Vite4 打包時用的是 rollup,用 rollup-plugin-external-globals 插件就可以完成打包時外部化:
pnpm add rollup-plugin-external-globals -D
然后是用法:
import { defineConfig, splitVendorChunkPlugin } from 'vite'
import vue from '@vitejs/plugin-vue'
import externalGlobals from 'rollup-plugin-external-globals'
export default defineConfig({
plugins: [vue(), splitVendorChunkPlugin()],
build: {
rollupOptions: {
externalGlobals({
cesium: 'Cesium'
}),
},
},
})
也可以用 Vite 插件:
- 插件② -
vite-plugin-externals(注意有個 s 結尾)
pnpm add vite-plugin-externals -D
用法:
import { defineConfig, splitVendorChunkPlugin } from 'vite'
import vue from '@vitejs/plugin-vue'
import { viteExternalsPlugin } from 'vite-plugin-externals'
export default defineConfig({
plugins: [
vue(),
splitVendorChunkPlugin(),
vitePluginExternals({
// key 是要外部化的依賴名,value 是全域訪問的名稱,這里填寫的是 'Cesium'
// 意味著外部化后的 cesium 依賴可以通過 window['Cesium'] 訪問;
// 支持鏈式訪問,參考此插件的檔案
cesium: 'Cesium',
})
],
})
上面兩個插件任選一個均可,只不過 vite-plugin-externals 在開發模式也會起作用,而 rollup-plugin-external-globals 只會在生產模式(NODE_ENV = production 條件,即構建打包時)對 rollup 起作用,
我選用 vite-plugin-externals 插件,因為它兩種模式都能起作用,再次啟動 pnpm dev,打開瀏覽器發現找不到模塊:

這是因為在開發模式也把 CesiumJS 外部化了,找不到很正常,
Vite 啟動后會有一個依賴預構建的程序,打開
node_modules/.vite/deps目錄,這里就是預構建的各種代碼中匯入的依賴包
在開發模式只需配置一下即可避免外部化,而讓 Vite 把 cesium 依賴預構建:
vitePluginExternals({
cesium: 'Cesium',
}, {
disableInServe: true, // 開發模式時不外部化
})
執行 pnpm build 后,提升顯著:

但是 pnpm preview 時,依然會找不到從 cesium 依賴匯入的類(注意埠,是 preview 默認的 4173):

這是因為外部化 CesiumJS 后,便不再打包 cesium 依賴,所以打包后的應用找不到 CesiumJS 的類和 API 了,
怎么辦呢?
總結一下現在的進度:
-
創建了 Vue3 + TypeScript 專案,并已經在第 2 節通過手動拷貝的方式把四個靜態資源檔案夾拷貝到
public/libs/cesium/目錄下,配置好了CESIUM_BASE_URL讓 CesiumJS 能訪問到這些靜態資源,并成功看到了具有離線 TMS 瓦片的三維地球 -
使用插件完成了打包外部化 CesiumJS,極大提高了打包速度、極大減小了構建產物的體積
那么現在遇到了什么問題?
- 打包后的頁面因為外部化
cesium找不到 CesiumJS 庫
如何解決問題?
只需打包時把 CesiumJS 的主庫檔案匯入 index.html 不就行了嗎?請緊接著 5.5 小節一起解決問題:
5.5. 使用插件自動在 index.html 引入 Cesium.js 庫檔案
讀者可以手動把 node_modules/cesium/Build/Cesium/Cesium.js 這個壓縮版的 IIFE 格式庫程式檔案復制到 public/libs/cesium/ 下,然后在工程入口檔案 index.html 中添加一行 script 標簽引入庫檔案:
<head>
<script src="https://www.cnblogs.com/onsummer/archive/2023/04/09/libs/cesium/Cesium.js"></script>
</head>
但是,如果是自己手動寫這個標簽,執行打包時會收到 Vite 的一句警告:

為了解決這個問題,最好的辦法就是在 Vite 的組態檔中,用插件的辦法自動插入這個 script 標簽,
有很多插件可以修改 index.html:
vite-plugin-htmlvite-plugin-html-configvite-plugin-insert-html
等等,上述三個插件我都有用過,各有特色,按需選擇,
我這里以 vite-plugin-insert-html 插件為例,在 index.html 的 <head> 標簽下插入這個 script 標簽:
import { defineConfig, splitVendorChunkPlugin } from 'vite'
import vue from '@vitejs/plugin-vue'
import { insertHtml, h } from 'vite-plugin-insert-html'
export default defineConfig({
plugins: [
vue(),
splitVendorChunkPlugin(),
viteExternalsPlugin({
cesium: 'Cesium',
}),
insertHtml({
head: [
h('script', {
src: 'libs/cesium/Cesium.js'
})
]
})
],
}
這樣打包時就是絕對完美的訊息了:

但是到此為止,仍然有兩個需要 “手動” 的事情待解決:
- 四大靜態檔案的復制
- CesiumJS 庫檔案的復制
巧的是,這些資源檔案都可以從 cesium 包內拷貝,壓縮版的 node_modules/cesium/Build/Cesium,非壓縮版的 node_modules/cesium/Build/CesiumUnminified,請讀者緊接著看 5.6 小節:
5.6. 四大靜態檔案夾與庫檔案的拷貝(CDN或獨立部署了 CesiumJS 庫可省略此步)
這里需要一些插件或者 nodejs 腳本來做檔案的靜態復制,簡單起見,就拿 Vite 的靜態檔案復制插件完成這個目的,
有很多可選插件,靜態檔案復制的插件在 Webpack 也有,叫作 CopyWebpackPlugin,在 Vite 中我選用 vite-plugin-static-copy 插件:
import { viteStaticCopy } from 'vite-plugin-static-copy'
export default defineConfig({
plugins: [
vue(),
splitVendorChunkPlugin(),
viteExternalsPlugin({
cesium: 'Cesium',
}),
viteStaticCopy({
targets: [
{
src: 'node_modules/cesium/Build/CesiumUnminified/Cesium.js',
dest: 'libs/cesium/'
},
{
src: 'node_modules/cesium/Build/CesiumUnminified/Assets/*',
dest: 'libs/cesium/Assets/'
},
{
src: 'node_modules/cesium/Build/CesiumUnminified/ThirdParty/*',
dest: 'libs/cesium/ThirdParty/'
},
{
src: 'node_modules/cesium/Build/CesiumUnminified/Workers/*',
dest: 'libs/cesium/Workers/'
},
{
src: 'node_modules/cesium/Build/CesiumUnminified/Widgets/*',
dest: 'libs/cesium/Widgets/'
},
]
}),
insertHtml({
head: [
h('script', {
src: 'libs/cesium/Cesium.js'
})
]
}),
], // End of plugins
}
這個 target 中很多路徑都是相同的,可以通過陣列計算完成,這里就留給讀者自己改進了,dest 是打包后的根路徑的相對路徑,
無論你見到的哪個教程,只要用的是 node_modules 下的 cesium 依賴,你都能看到這四個靜態檔案夾的復制步驟,
5.7. 額外優化 - 使用環境變數配置 CESIUM_BASE_URL 并適配其它配置
至此我認為工程的配置已經滿足非常靈活地運行了,它滿足了:
-
無論開發或生產環境,外部化了 CesiumJS,讓 Vite 不再打包
cesium依賴,大大減少打包時間、減少應用代碼體積(從構建產物中剝離 cesium 庫) -
無論開發或生產環境,都 自動復制四個靜態資源檔案夾、自動在 index.html 注入 CesiumJS 庫檔案的 script 標簽以加載 CesiumJS
但是,一旦改用局域網或已經部署好的 CesiumJS 庫(這種情況請自己解決跨域),或者使用 CDN,那么安裝在 node_modules 下的 cesium 其實已經沒有必要走 5.6 的靜態檔案復制了,而且注入 index.html 的主庫檔案需要修改,
我以國內 bootcdn 上的 CesiumJS 為例,既然 Vite 內置了不同環境檔案的決議的函式 loadEnv(參考 Vite 官方檔案 - 使用環境變數),我就分 development 和 production 簡單講一講,
- 開發模式(
NODE_ENV = development),使用node_modules下的cesium依賴,復制四個靜態檔案和庫檔案 - 生產模式(
NODE_ENV = production),使用 bootcdn 上的 CDN 鏈接
給出最終的 vite.config.ts(注意,默認匯出改成了函式):
import { defineConfig, type PluginOption, splitVendorChunkPlugin, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { viteExternalsPlugin } from 'vite-plugin-externals'
import { insertHtml, h } from 'vite-plugin-insert-html'
import { viteStaticCopy } from 'vite-plugin-static-copy'
export default defineConfig((context) => {
const mode = context.mode
const envDir = 'env' // 環境變數檔案的檔案夾,相對于專案的路徑,也可以用 nodejs 函式拼接絕對路徑
const isProd = mode === 'production'
const env = loadEnv(mode, envDir)
const cesiumBaseUrl = env['VITE_CESIUM_BASE_URL']
// 默認 base 是 '/'
const base = '/'
const plugins: PluginOption[] = [
vue(),
splitVendorChunkPlugin(),
viteExternalsPlugin({
cesium: 'Cesium', // 外部化 cesium 依賴,之后全域訪問形式是 window['Cesium']
}),
insertHtml({
head: [
// 生產模式使用 CDN 或已部署的 CesiumJS 在線庫鏈接,開發模式用拷貝的庫檔案,根據 VITE_CESIUM_BASE_URL 自動拼接
h('script', {
// 因為涉及前端路徑訪問,所以開發模式最好顯式拼接 base 路徑,適配不同 base 路徑的情況
src: isProd ? `${cesiumBaseUrl}Cesium.js` : `${base}${cesiumBaseUrl}Cesium.js`
})
]
})
]
if (!isProd) {
// 開發模式,復制 node_modules 下的 cesium 依賴
const cesiumLibraryRoot = 'node_modules/cesium/Build/CesiumUnminified/'
const cesiumLibraryCopyToRootPath = 'libs/cesium/' // 相對于打包后的路徑
const cesiumStaticSourceCopyOptions = ['Assets', 'ThirdParty', 'Workers', 'Widgets'].map((dirName) => {
return {
src: `${cesiumLibraryRoot}${dirName}/*`, // 注意后面的 * 字符,檔案夾全量復制
dest: `${cesiumLibraryCopyToRootPath}${dirName}`
}
})
plugins.push(
viteStaticCopy({
targets: [
// 主庫檔案,開發時選用非壓縮版的 IIFE 格式主庫檔案
{
src: `${cesiumLibraryRoot}Cesium.js`,
dest: cesiumLibraryCopyToRootPath
},
// 四大靜態檔案夾
...cesiumStaticSourceCopyOptions
]
}),
)
}
return {
base,
envDir,
mode,
plugins,
}
})
為了 ts 能提示 import.meta.env.MODE,需要在 src/vite-env.d.ts 中補充型別定義(參考 Vite 檔案):
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
// 更多環境變數...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
并且告訴 TypeScript 要用由 vite/client 提供的 import.meta 型別,在 tsconfig.node.json 的 compilerOptions 中添加:
{
"compilerOptions": {
"types": ["vite/client"]
}
}
如果是舊版本的 Vite 創建的模板,你可以添加在 tsconfig.json 對應的位置中,
5.9. 額外優化 - 使用 gzip 預先壓縮打包產物
在服務器上使用 gzip 能進一步提升網路傳輸速度,打包時,使用合適的插件即可預先進行 gzip 打包,我選用的是 vite-plugin-compression 插件:
import compress from 'vite-plugin-compression'
// 使用見插件官方檔案
在開發模式這玩意兒沒起作用,就不細談了,
5.8. 如何共享 CesiumJS 的 Viewer 物件
Vue 有 pinia 這個全域狀態大殺器,可以把核心的 Viewer 物件送入全域狀態中,但是要避免 Vue 的回應式劫持,回應式問題可以通過 Vue3 的 shallowRef 或 shallowReactive 來解決:
<script lang="ts" setup>
import { onMounted, shallowRef, ref } from 'vue'
import { Viewer } from 'cesium'
const viewerDivRef = ref<HTMLDivElement>()
const viewerRef = shallowRef<Viewer>()
onMounted(() => {
viewerRef.value = https://www.cnblogs.com/onsummer/archive/2023/04/09/new Viewer(viewerDivRef.value as HTMLElement, /* ... */)
})
</script>
或者用 shallowReactive:
<script lang="ts" setup>
import { onMounted, shallowReactive, ref } from 'vue'
import { Viewer } from 'cesium'
const viewerDivRef = ref<HTMLDivElement>()
const viewerRef = shallowReactive<{
viewer: Viewer | null
}>({
viewer: null
})
onMounted(() => {
viewerRef.viewer = new Viewer(viewerDivRef.value as HTMLElement, /* ... */)
})
</script>
甚至可以更簡單一些:
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { Viewer } from 'cesium'
const viewerDivRef = ref<HTMLDivElement>()
let viewer: Viewer | null = null
onMounted(() => {
viewer = new Viewer(viewerDivRef.value as HTMLElement, /* ... */)
})
</script>
當然也可以用 Vue 的 provide/inject 函式來下發、注入子組件,僅適用于地圖組件在最頂層的情況:
<!-- 頂層組件下發 Viewer -->
<script lang="ts" setup>
import { onMounted, ref, provide } from 'vue'
import { Viewer } from 'cesium'
import { CESIUM_VIEWER } from '@/symbol'
const viewerDivRef = ref<HTMLDivElement>()
let viewer: Viewer | null = null
onMounted(() => {
viewer = new Viewer(viewerDivRef.value as HTMLElement, /* ... */)
provide(CESIUM_VIEWER, viewer)
})
</script>
<!-- 下面是子組件呼叫 -->
<script lang="ts" setup>
import { inject } from 'vue'
import type { Viewer } from 'cesium'
import { CESIUM_VIEWER } from '@/symbol'
const viewer = inject<Viewer>(CESIUM_VIEWER)
</script>
這個 CESIUM_VIEWER 是一個 Symbol,來自 src/symbol/index.ts:
export const CESIUM_VIEWER = Symbol('CESIUM_VIEWER')
如果業務界面組件與地圖組件是兄弟組件或父子,那只能用三種方式傳遞 Viewer 物件:
- defineExpose
- 層層事件冒泡至父級組件,或者使用全域事件庫(如 mitt)
- 使用全域狀態 pinia 或 vuex
不再展示代碼,請讀者參考各種途徑的官方檔案來傳遞,注意一定要避免回應式劫持,
6. 探究 CesiumJS 等庫的前端組件封裝
這里只是以 Vue 為例講個思路,在其它前端框架中也適用,
6.1. 以 CesiumJS 等庫為主的看板式工程
這種工程有一個特點,就是地圖場景會占滿瀏覽器視窗的全部尺寸,并且不可在高度和寬度上出現滾動條,
一般這種就是“XX系統”的原型,這種工程有什么特點呢?那就是地圖/三維場景幾乎占據絕大多數的功能,大多數時候是浮動在地圖場景上的一些 UI 元素在顯示資料、發生互動,也就是說,切換的其實是一些界面組件,地圖組件幾乎不變,反過來看,界面組件大多數時候反而還要去訪問地圖核心物件,像 CesiumJS 是 Viewer,OpenLayers 是 Map 等,
我的建議是,所有業務界面組件應該作為地圖組件的 子組件,在 Vue 中,就有 slot 的設計,
結合前端路由,還能跟隨路由切換(RouteView 也應作為 slot 撰寫在地圖組件中) ,
地圖組件作為最頂層的組件,可以結合前端組件的生命周期特點,當核心物件創建完成后,才通過條件渲染把子組件打開,在 Vue 中利用 provide/inject 實作地圖核心物件的下發和注入,在 React 中使用 useContext 下發也是類似的,
6.2. 后臺管理系統式工程
這種通常是表單的資料通過組件的 props 下傳給地圖,單一地顯示上級操作接收來的資料,這種地圖組件設計就比較簡單,只需設計好 props 的資料結構,在組件掛載時創建核心物件并顯示接收到的資料即可,
7. 示例工程下載
留了兩個版本,讀者可以自己在壓縮包中找自己滿意的,一個是第 2 節的最簡單的,讓 Vite 打包 CesiumJS 的版本,做了分 chunk;另一個則是經過第 5 節完整配置后、具備各種注釋和細節,供讀者自己改造學習的版本,
微云鏈接
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/549536.html
標籤:其他
上一篇:教程 - 在 Vue3+Ts 中引入 CesiumJS 的最佳實踐@2023
下一篇:精靈圖
