因为 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.jsIIFE 的是 Cesium.jsCommonJS 的是 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 依赖也很棒。你当然也能够用 npmyarn

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 的动态文件。

留神 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 文件中:

/// <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 / 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<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/ndkx...