爲何在打包工具中導入 Cesium 的 css 失敗了?


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 的計劃,如果你有報這個錯誤,那麼你僅僅需要按我上面的方式稍作修改即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章