教程 - 深度探讨在 Vue3 中引入 CesiumJS 的最佳方式


目录
  • 配置 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 的静态文件。

    注意 deleteSynccopy 函数的目标文件夹路径,我设为了 public/lib/cesium,与 2.4 小节中 htmlConfig 的配置是一样的。

    为了简单起见,vite.config.ts 中配置的 build.assetsDir 我改为了 ./;否则,deleteSynccopy 的目标路径就要手动加上 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 文件中:

    /// 
    
    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 / inject API,穿透传递 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()
    const unvisibleCreditRef = ref()
    
    const sysStore = useSysStore()
    
    onMounted(() => {
      const viewer = new Viewer(containerRef.value as HTMLElement)
    
      const rawViewer = markRaw(viewer)
      sysStore.setCesiumViewer(rawViewer)
    })
    

    最后,你就可以在兄弟组件中访问到 Viewer 了:

    
    
    
    
    
    

    3. 伸手的看过来 - 工程下载

    由于篇幅原因,有些文章中的代码会省略、简化,工程的源码、配置可能与上述有细微差别,请自行了解。

    https://share.weiyun.com/ndkxAeIv