1 問題起因
我使用 vite2 + vanillajs
模板創建 CesiumJS 項目,其中,main.js 是這樣的:
import { Viewer } from 'cesium'
import './style.css'
import 'cesium/Source/Widgets/widgets.css'
let viewer
const main = () => {
const dom = document.getElementById('app')
viewer = new Viewer(dom)
}
document.addEventListener('DOMContentLoaded', () => {
main()
})
看起來邏輯完美,思路清晰,沒什么特別的疑問點。於是我就吭哧吭哧地運行起 npm script:
pnpm dev
可是,Vite 在控制台給我報了個錯:
[vite] Internal server error: Missing "./Source/Widgets/widgets.css" export in "cesium" package
這個問題貌似在各前端框架的模板中是不會出現的,我不確定。也有人在 Webpack 中遇到了這個類似的情況,究其原因,我認為還是 cesium 包的導出有些不完備,見下面第二節的分析。
簡單點說,就是 Vite 的內置預構建工具 esbuild 在搜索依賴樹時,沒有找到 "cesium"
包導出的一個路徑為 "./Source/Widgets/widgets.css"
文件。
2 尋找解決方案
可是,當我打開 node_modules/cesium/Source/Widgets/
目錄,widgets.css
文件的確就在那里放着。
於是我打開了谷歌,果不其然找到了類似的 issue:github.com/CesiumGS/cesium issue#9212,我在 2021 年 8 月也跟帖回復了我的情況。
我當時並沒有找到解決方案,就暫時跳過了。
后來,有外國朋友跟帖回復,大致原因找到了:
cesium
包的 package.json
沒有導出樣式文件,主要是 package.json
中的 exports
屬性。
於是,我打開官方源碼的 package.json
,找到對應的部分:
{
"exports": {
"./package.json": "./package.json",
".": {
"require": "./index.cjs",
"import": "./Source/Cesium.js"
}
}
}
2.1. 歷史原因
眾所周知,NodeJS 最先使用的模塊化機制是 CommonJS,后來才支持的 ESModule,現在 NodeJS 仍然默認新建的包是 CommonJS 模塊的。
外國佬在 NodeJS 的包中允許雙模塊化,這就容易存在兼容性問題。我們看看最開始是怎么實現雙模塊化的,這里以默認 ESModule 為模塊化方式:
- 設置
package.json
的"type": "module"
,這樣所有的.js
文件都是 ESM 了 - 設置
package.json
的"module": "./dist/esm/index.js"
,這個意思是使用 import 語法導入時,ESM 模塊將從哪里尋找主文件 - 設置
package.json
的"main": "./index.cjs"
,這個意思是使用 require 函數導入模塊時,CommonJS 的主文件是哪個
后來,隨着 ESModule 成為主流標准,NodeJS 改進了上面的配置方式,你仍可以設置 "type": "module"
令當前包的模塊化是 ESM,但是對包的多模塊化機制的配置則改用了 "exports"
字段,正如上面 cesium 的配置。
我檢查了我的 NodeJS 版本:
> node -v
> v16.14.0
顯然比較新,那么它應該就是從 "exports"
中讀取的導出信息。
2.2. 增加導出
於是,我增加了 "exports"
的導出字段,讓打包工具在識別 cesium 包的導出時,可以正確識別 widgets.css
文件。
{
"exports": {
"./package.json": "./package.json",
".": {
"require": "./index.cjs",
"import": "./Source/Cesium.js"
},
+ "./Source/Widgets/widgets.css": "./Source/Widgets/widgets.css"
}
}
這樣,下面兩個導入語句:
import { Viewer } from 'cesium'
import 'cesium/Source/Widgets/widgets.css'
實際上就是:
import { Viewer } from 'cesium/Source/Cesium.js'
import 'cesium/Source/Widgets/widgets.css'
2.3. 耍個花招
我覺得這樣導入 css 文件還是太長,不妨在 "exports"
中給它改個名:
{
"exports": {
"./package.json": "./package.json",
".": {
"require": "./index.cjs",
"import": "./Source/Cesium.js"
},
+ "./index.css": "./Source/Widgets/widgets.css"
}
}
然后就可以愉快地使用短路徑導入了:
import { Viewer } from 'cesium'
import 'cesium/index.css'
事實上,package.json
中的這個 exports
屬性,就起到類似導出別名的作用,其中 "."
就相當於包的根路徑。
3 類型提示是哪來的
考慮這樣導入 cesium 各個 API:
import {
Viewer,
Cartesian3,
Camera
} from 'cesium'
當你使用這些類的時候,會得到不錯的類型提示。回顧前面的內容,其實從 "cesium"
導入子模塊,實際上是從 "cesium/Source/Cesium.js"
文件導入的,而這個文件的旁邊就有一個 "Cesium.d.ts"
文件,它就起類型提示的作用。
這個類型聲明文件是 Cesium 使用 gulp 打包時輸出的。
說了這么多,根本原因還是 JavaScript 的歷史包袱導致的各種問題,而且官方也暫時沒有修改 package.json 中 exports 的計划,如果你有報這個錯誤,那么你僅僅需要按我上面的方式稍作修改即可。