为 Cesium 写一个 Umi 插件,快速集成 Cesium 环境

1,160 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第29天,点击查看活动详情

如果想要在工程项目中引入 Cesium ,则需要手动添加几个资源,然后再添加一下资源路径的一些前置需求,比如:

image.png

image.png

而项目用的是基于 Umi 的项目,所以就做成 Umi 插件方便后续更多项目,快速启动 Cesium 环境,将这些操作内敛到配置中。

目前 Umi 已经是@4 的版本了,直接使用@4 的插件初始化即可。

想直接使用的同学,可以直接访问github

使用 father 作为构建插件的工具

初始化使用 father,fatehr 也是出了@4 的版本,比之前更加的好用,感兴趣的同学可以看father@4 的发布介绍

我们初始化项目后,装好依赖。

//package.json
  "devDependencies": {
    "father": "^4.0.0"
  },
  "dependencies": {
    "cesium": "^1.96.0",
    "umi": "^4.0.15"
  }

因为是插件,所以只需要 cjs 即可,编写一下 father 的配置。

// .fatherrc.ts
import { defineConfig } from "father";

export default defineConfig({
  cjs: {
    output: "dist",
  },
});

分析需求

我们先分析下我们的需求是什么:

  • 通过配置开启 cesium 环境
  • 自动注册 accessToken 以及 CESIUM_BASE_URL
  • 通过 umi 导入 Cesium,保持类型声明
  • 能够访问到 widgets.css
  • 打包自动注入资源到 public,方便生成环境使用

那么我们就一个个实现这些需求。

通过配置开启 cesium 环境

通过配置开启,需要使用 describe API,我们设置 cesium 作为我们的插件配置名称。

// src/index.ts
import type { IApi } from "umi";
export default (api: IApi) => {
  api.describe({
    key: "cesium",
    config: {
      schema(joi) {
        return joi.object({});
      },
    },
    enableBy: api.EnableBy.config,
  });
};

config 的配置是 schema,在开发的时候能够校验我们的配置是否正确,默认就用 joi.object({}) 就行了。

自动注册 accessToken 以及 CESIUM_BASE_URL

因为 accessToken 是用户的,所以我们通过配置让用户提供 token,我们先为之前的 config 配置加一个 accessToken 的配置。

// src/index.ts
import type { IApi } from "umi";
export default (api: IApi) => {
  api.describe({
    key: "cesium",
    config: {
      schema(joi) {
        return joi.object({
          accessToken: joi.string(),
        });
      },
    },
    enableBy: api.EnableBy.config,
  });
};

然后我们再使用 onGenerateFiles API 生成静态文件,导出到 umi 中,这样我们就能在项目中使用插件依赖中的 Cesium,并且在导出之前我们先将要处理的逻辑写好

// src/index.ts
//...other code
export default (api: IApi) => {
//...other code
 api.onGenerateFiles({
    name: "cesium",
    fn: () => {
      // index.ts for export
      api.writeTmpFile({
        path: "index.ts",
        content: `
import * as Cesium from 'cesium';

window.CESIUM_BASE_URL = '/Cesium';
${
  api.config.cesium.accessToken
    ? `Cesium.Ion.defaultAccessToken = '${api.config.cesium.accessToken}';`
    : ""
}

export { Cesium };
`,
      });
    },
  });

我们先写入一个入口 index.js 文件,但是 import * as Cesium from 'cesium' 从开发来说,cesium 这个依赖是不对的,因为用户可能没有装 cesium 这个依赖,当然我们也可以要求用户装,但是我们也要提供自己的依赖,所以开始我是装了 cesium 的依赖的。

所以我们先找好 cesium 的依赖路径,优先从用户的依赖读,配置好 alias 即可

const pkgPath =
  resolveProjectDep({
    pkg: api.pkg,
    cwd: api.cwd,
    dep: "cesium",
  }) || dirname(require.resolve("cesium/package.json"));

api.modifyConfig((memo) => {
  memo.alias["cesium"] = pkgPath;
  return memo;
});

但是这又会引发一个问题,那就是没有了类型提示,所以我们还需要提供 cesium 的类型,不过在做之前,我们还需要保证一步,就是开发环境下,提供 /Cesium 的服务访问,通过 addBeforeMiddlewares API 添加中间件来访问

api.addBeforeMiddlewares(() => {
  return [
    (req, res, next) => {
      const { path } = req;
      if (path.startsWith("/Cesium/")) {
        return res.sendFile(join(cesiumPath, "Build", path));
      }
      return next();
    },
  ];
});

很好,我们下面再解决类型的问题

通过 umi 导入 Cesium,保持类型声明

刚刚我们已经导出过 Cesium 了,但是还缺少一个类型提示,通过观察发现 Cesium 仓库是 js 写的,类型是自己维护的

declare module "cesium" {
  //...
}

如果使用 import * as Cesium from '${cesiumPath}' 路径的话 是不对的,不符合 module 的定义 但我们可以通过写入 types.d.ts 导出 cesium 的声明文件来做到

api.onGenerateFiles({
  name: "cesium",
  fn: () => {
    // ...other code
    api.writeTmpFile({
      path: "types.d.ts",
      content: readFileSync(join(cesiumPath, "/Source/Cesium.d.ts"), "utf-8"),
    });
  },
});

这是上面用到的 cesiumPath 的声明,下面就不再说了

  const cesiumPath = dirname(require.resolve("cesium"));

能够访问到 widgets.css

其实就是 import "cesium/Build/Cesium/Widgets/widgets.css"; 这个操作,但如果我们使用 pnpm 开发,这个就是幽灵依赖,但也没办法,我们还是要解决它,也减少手动引入的麻烦。 这个还是比较简单的,我们直接使用 addHTMLStyles API 注入 css 样式即可

api.addHTMLStyles(() => [
  {
    content: readFileSync(
      join(cesiumPath, "Build/Cesium/Widgets/widgets.css"),
      "utf-8"
    ),
  },
]);

打包自动注入资源到 public,方便生成环境使用

开发模式都做完了,但还有一个问题,就是生成模式的 /Cesium 是没有的,我们还需要将刚才访问的依赖写到输入出目录中,这次我们用 modifyConfig API 修改 config 中的 copy 配置就行了

api.modifyConfig({
  fn: (config) => {
    return api.env === "production"
      ? {
          ...config,
          copy: [
            ...(config.copy || []),
            ...["Workers", "ThirdParty", "Assets", "Widgets"].map((dir) => ({
              from: join(getAbsPath(api.cwd, cesiumPath), "Build/Cesium", dir),
              to: join(config.outputPath || "dist", "Cesium", dir),
            })),
          ],
        }
      : config;
  },
  stage: Infinity,
});

总结

至此,Cesium 就开发完了,使用也非常的简单,直接

export default {
  plugins: ["umi-cesium-plugin"],
  cesium: {
    accessToken: "your accessToken",
  },
};

import { Cesium } from "umi"; 就可以用了,不用再自己配置

对全部代码感兴趣的同学可以看github