Cesium深入浅出之webpack搭建框架

引子

一年前刚开始搞Cesium的时候还是使用的require.js进行模块封装,r.js进行打包,后面又用了gulp进行打包,但总感觉是不够智能。require.js自然不用说了,It's too old,gulp配置也太麻烦,也是这之后才玩的webpack,后知后觉的,原来还有这么智能的打包工具啊。用它来打包Cesium项目挺香,虽然你们都喜欢Vue+Cesium的组合,但我还是偏爱ES6+webpack,原生质感让我流连忘返。

预期效果

准备工作

今天问了下群友有没有玩webpack+Cesium的,结果是没又几个人,基本都是Vue+Cesium,至于打包的工具嘛,好像现在又流行rullup。反正我是挺懵逼的,现在前端技术变化这么快吗,webpack我才刚玩溜,一度怀疑这篇文章还有没有继续写下去的必要了。后来想想还是有始有终吧,既然开了头就要完成它,况且我觉得webpack还能再战几年啊,我还是坚持用它吧,毕竟熟悉嘛。闲话不多说了,开始准备工作。

新建项目

因为我打算从零开始搭建基于webpack的Cesium开源平台,本篇做为平台第一篇,所以要从新建项目开始。关于IDE我是用IDEA,大家先不要开喷为啥你做前端的不用VSCODE,因为我是做后台出身的,而且后面也有可能要上后台的,所以一套IDEA搞定前后两端得了。

1、IDEA中点击新建,选择Javascript类型项目,输入项目名称,选择创建好项目。我的项目名字叫simple-cesium,意为浅显易懂、简单易用的Cesium,符合深入浅出的理念。

2、创建一个src文件夹来存放源码,创建一个css文件夹来存放样式表,创建一个public文件夹来存放静态文件。

3、新建一个index.js文件放到src文件夹中,内容:

console.log("Hello World!");

新建一个index.html文件,标题为simple-cesium,放到public文件夹下,内容:

 1 <!DOCTYPE html>
 2 <html lang="en">
 3     <head>
 4         <meta charset="utf-8">
 5         <meta http-equiv="X-UA-Compatible" content="IE=edge">
 6         <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
 7         <title><%= htmlWebpackPlugin.options.title %></title>
 8     </head>
 9     <body>
10         <noscript>
11             <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript
12                 enabled. Please enable it to continue.</strong>
13         </noscript>
14         <div id="cesiumContainer" class="cesiumContainer">
15             <div id="cesiumSlider" class="cesiumSlider"></div>
16             <div id="creditContainer" class="creditContainer"></div>
17         </div>
18         <!-- built files will be auto injected -->
19     </body>
20 </html>

新建一个main.css文件,放到css文件夹中,内容:

1 @charset "utf-8";
2 html { height: 100%; overflow: hidden;}
3 body { background: #000; color: #eee; font-family: sans-serif; font-size: 9pt; padding: 0; margin: 0; width: 100%; height: 100%; overflow: hidden;}
4 
5 .cesiumContainer { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; }
6 .cesiumSlider { display: none; position: absolute; left: 50%; top: 0; background-color: rgba(210, 255, 255, .5); width: 4px; height: 100%; z-index: 1; }
7 .cesiumSlider:hover { cursor: ew-resize; }
8 .creditContainer { display: none; }

4、创建package.json文件,后面的模块依赖会自动写入到这个文件中。 命令行输入:

npm init

填入相关信息,生成package配置如下:

 1 {
 2   "name": "simple-cesium",
 3   "version": "0.0.1",
 4   "dependencies": {},
 5   "devDependencies": {},
 6   "description": "Make the Cesium simple!",
 7   "main": "index.js",
 8   "scripts": {
 9     "test": "echo \"Error: no test specified\" && exit 1"
10   },
11   "keywords": [
12     "Simple",
13     "Cesium"
14   ],
15   "author": "Helsing",
16   "license": "Apache-2.0"
17 }

5、新建webpack.config.js文件,内容:

1 const config = {};
2 module.exports = config;

这样,一个空项目就建好了。目录结构如下:

|- simple-cesium
  |- .idea
|- css
main.css |- node_modules
|- public
favicon.ico
index.html |- src index.js index.html package.json webpack.config.js

安装webpack

在安装webpack之前需要先安装node.js,至于安装方法不赘述了,请自行查阅资料。我的nodenpm版本分别为10.16.1和6.9.0,这个是之前装好的,因为我觉得它们的版本对本项目影响不大,所以没更新到最新版本。

Terminal中执行命令:

npm -i webpack -D

这样就将webpack安装到项目中了,如果要全局安装请使用-g参数。安装后,package.json文件发生了变化:

1 {
2   "name": "simple-cesium",
3   "version": "0.0.1",
4   "dependencies": {},
5   "devDependencies": {
6     "webpack": "^5.9.0"
7   }
8   ...
9 }

我们看到webpack已经被安装到开发依赖中了,版本是5.9.0,这里要说明的是,webpack最近的大版本升级4.0到5.0变化是巨大的,而且5.0现在还存在着很多BUG以及插件的适配问题,目前的生产环境暂时不推荐升级,我用的最新版本,踩坑是难免的。

webpack在4.0以后都要安装webpack-cli,所以继续执行命令:

npm i webpack-cli -D

上述都是使用开发环境依赖安装,如须全局安装选择参数-g即可。现在去看看node_modules目录的下面,“全家桶”已装满,说明webpack算安装完成了。

配置webpack

我们都知道webpack之所以强大,和它的插件生态密不可分的,所以安装完webpack只是第一步,后面还要上很多的插件。

1、webpack-dev-server,我认为这最重要的插件,开发调试之利器也。

首先,安装webpack-dev-server,执行命令:

npm i webpack-dev-server -D

然后,配置一下package.json文件,在里面添加调试脚本:

1 "scripts": {
2   "test": "echo \"Error: no test specified\" && exit 1",
3   "build": "cross-env NODE_ENV=production node_modules/.bin/webpack --progress --config webpack.config.js",
4   "start": "cross-env NODE_ENV=development node_modules/.bin/webpack-dev-server --progress --config webpack.config.js --open chrome --hot"
5 }

生产模式下使用build,开发模式下使用start进行调试,open参数设置打开chrome浏览器,hot参数设置热更新。另外,上面出现了cross-env,它是一个可以运行跨平台设置和使用环境变量的插件,可以让你在Windows系统下使用process.env.NODE_ENV

需要事先安装cross-env,执行命令:

npm i cross-env -D

最后,来配置下webapck.config.json文件:

 1 const config = {
 2     devServer: {
 3         hot: true, // 是否启用热更新。
 4         contentBase: path.join(__dirname, "dist"), // 告诉服务器从哪里提供内容。只有在你想要提供静态文件时才需要。
 5         // openPage: "index.html", // 指定打开的页面。指定路径会改变URL地址。
 6         compress: true, // 是否启用gzip压缩。
 7         port: 9999, // 端口号。
 8         lazy: false // 当启用lazy时,dev-server只有在请求时才编译包(bundle)。这意味着webpack不会监视任何文件改动。我们称之为“惰性模式”。
 9     }, // 开发调试工具
10 }

上述步骤完成后,启动start脚本调试项目,结果意料之中的抛出错误:

error:Cannot find module 'webpack-cli/bin/config-yargs'

上面说过了要踩坑的,这个算第一个吧。解决方法是改写package.json中的start脚本:

"start": "cross-env NODE_ENV=development node_modules/.bin/webpack serve --progress --config webpack.config.js --open chrome --hot"

可以看出,并不是webpack5.0不兼容webpack-dev-server了,而是调用方式发生了变化,现在是使用webpack serve的方式启动调试。

安装Cesium

常规的Cesium开发是直接下载编译好的库,然后使用标签或require方式进行全局引用,这种方式简单,不需要进行安装。但是这种方法没有办法将Cesium以模块方式重新打包,而我们这篇文章的目的就是在讲webpack与Cesium的结合,所以我们还要来安装一下Cesium,执行命令:

npm i cesium -S 

这里我们选择的是生产环境依赖安装,另外npm模块安装是区分大小写的,cesium首字母为小写,否则就是404了。

配置Cesium

在配置Cesium之前,我们先来完善一下webpack的基本配置:

 1 const config = {
 2     mode: nodeEnv, // 编译模式:"production" | "development" | "none"。
 3     context: __dirname, // 基础目录
 4     entry: {
 5         "simple-cesium": "./src/index.js"
 6     }, // 入口:string | object | array。这里应用程序开始执行,webpack开始打包。
 7     output: {
 8         filename: "[name].js", // 输出文件名:"[name].[hash:8].js", "bundle.js" | "[name].js" | "[chunkhash].js"。
 9         // publicPath: "/assets/", // 输出解析文件的目录,设置之后所有资源文件会自动加上这个路径,url地址是相对于HTML页面的。
10         path: path.resolve(__dirname, "dist"), // path.join(__dirname,'dist'), // 输出路径,一般为绝对路径。
11         chunkFilename: "[name].js", // 未被列入entry中,却又需要被打包出来的文件命名配置。
12         library: "SimpleCesium", // 导出库的名称。
13         libraryTarget: "umd" // 导出库的类型。常用umd模式,让输出的内容支持amd、commonJS模式加载。
14     }, // 输出,webpack如何输出结果的相关选项。
15     ...
16 }

各项参数含义请看注释,或到webpack的官网查阅。上述nodEnv变量为编译模式,需要自行要定义一下,path需要引用一下才能使用,如下:

1 const path = require("path"); // 路径组件。
2 const nodeEnv = process.env.NODE_ENV; // 编译模式。

做完上述配置后,运行build脚本生成一下,发现dist目录下出现了simple-cesium.js文件,这说明项目已经成功生成了,但是一看大小才1KB,这明显没有把Cesium打包进来嘛。于是乎在index.js中添加Cesium引用:

1 // import * as Cesium from "/node_modules/cesium/Source/Cesium.js";
2 import Viewer from "/node_modules/cesium/Source/Widgets/Viewer/Viewer.js";

我们先是直接引用Cesium的入口文件,引了这个文件基本就是把Cesium全家桶引进来了,然后运行build脚本,果不其然,3,055KB,这个大小可以用惊人来描述了。那么我们还是模块化引用吧,于是我们再引入Viewer,这个应该算是Cesium中的必备的Widget了吧,build,结果大小为2,683KB,这也没好哪里去嘛,只能说这个Viewer的家伙事儿也挺全的,但不管怎么样大小跟全家桶还是有区别的,所以我们还是坚持模块化引用吧。顺便要提一下,使用import引入的模块路径是否添加.js后缀是有区别的,webpack会把它们当成两个不同的模块,如果代码中刚好出现了用instanceof判断A是否为B的实例的时候,就会掉进坑里。我的习惯还是所有模块都加上.js后缀。

到这里为止,我们已经能将Cesium和项目文件一起打包了,接下来的目标就是运行起Cesium的三维界面。

为了动态引用JS脚本,我们要使用webpack-html-plugin插件,这个插件可以说是仅次于webpack-dev-server的存在,请看配置:

 1 plugins: [
 2     new HtmlWebpackPlugin({ // 打包输出HTML
 3         title: 'Simple Cesium', // 给模板中的html注入标题,需要在模板的html中指明配置,<%= htmlWebpackPlugin.options.title %>
 4         filename: 'index.html', // 指index定要生成的html路径,基于输出目录。
 5         template: 'public/index.html', // 指定html模板文件路径。这里的模板类型可以是任意你喜欢的模板,可以是 html、jade、ejs、hbs,等等,但是要注意的是,使用自定义的模板文件时,需要提前安装对应的loader,否则webpack不能正确解析。
 6         inject: 'body', // 注入选项。1.true:默认值,script标签位于html文件的body底部;2.body:script标签位于html文件的body底部(同true);3.head:script标签位于head标签内;4.false:不插入生成的js文件,只是单纯的生成一个html文件。
 7         favicon: 'public/favicon.ico', // 给生成的html文件生成一个favicon,属性值为favicon文件所在的路径名。
 8         hash: true, // 给生成的js文件一个独特的hash值,该hash值是该次webpack编译的hash值。默认值为false。
 9         cache: true, // 默认是true,表示内容变化的时候是否生成一个新的文件。
10         showErrors: true, // 默认是true,作用是如果webpack编译出现错误,webpack会将错误信息包裹在一个pre标签内,属性的默认值为 true,也就是显示错误信息,开启这个方便定位错误。
11         //chunks:['index','main'], // 主要用于多入口文件,当你有多个入口文件,编译后生成多个打包后的文件,那么chunks就能选择你要使用那些js文件,如果不设置则默认全部引入。
12         //excludeChunks:['main.js'], // 排除掉一些js。
13         minify: nodeEnv === 'production' ? { // 压缩html文件。属性值是一个压缩选项或者false。默认值为false,即不对生成的html文件进行压缩。
14             caseSensitive: true, // 是否对大小写敏感,默认false。
15             collapseBooleanAttributes: true, // 是否简写boolean格式的属性如:disabled="disabled" 简写为disabled  默认false。
16             collapseWhitespace: true, // 是否删除空格与换行符,默认false。
17             minifyCSS: true, // 是否压缩内联css(使用clean-css进行的压缩),默认值false。
18             minifyJS: true, // 是否压缩html里的js(使用uglify-js进行的压缩)。
19             preventAttributesEscaping: true, // 是否阻止属性值转义。
20             removeAttributeQuotes: true, // 是否移除属性的引号,默认false。
21             removeComments: true, // 是否删除注释,默认false。
22             removeCommentsFromCDATA: true, // 是否从CDATA中删除注释,默认false。
23             removeEmptyAttributes: true, // 是否删除空属性,默认false。
24             removeOptionalTags: false, // 是否删除可选的标签,若开启此项,生成的html中没有body和head,html也未闭合。
25             removeRedundantAttributes: true, // 是否删除多余的属性。
26             removeScriptTypeAttributes: true, // 是否删除script的类型属性,在h5下面script的type默认值:text/javascript 默认值false。
27             removeStyleLinkTypeAttributes: true, // 是否删除style的类型属性,type="text/css" 同上。
28             useShortDoctype: true, // 是否使用短文档类型,默认false。
29         } : {}
30     })
31     ...
32 ],

我这注释已经够详细了,相信已经不用多讲了。上述minify参数主要是用来做压缩的,可以选用默认设置或不设置,不用这么麻烦。

接着我们再改造一下index.js,增加内容:

const viewer = new Cesium.Viewer("cesiumContainer");

如果使用模块方式引用的则改为如下写法:

1 import Viewer from "/node_modules/cesium/Source/Widgets/Viewer/Viewer.js";
2 const viewer = new Viewer("cesiumContainer");

然后运行start脚本调试,页面并未正常显示,控制栏报错:DeveloperError: Unable to determine Cesium base URL automatically, try defining a global variable called CESIUM_BASE_URL。原来Cesium中有动态路径存在,使用webpack打包后CESIUM_BASE_URL的值就取不到了。这种动态路径的方式显然不符合webpack的模块式打包理念,但你又不能要求人家Cesium去改吧,所以只能自己改了。至于解决方法有以下几种:

第一种,也是网上你能找到的最常见的解决方式,就是用DefinePlugin内置插件来重新定义CESIUM_BASE_URL的值。通常我们会把它定义为空,也就是默认的根路径,但如果你想更换别的路径,比如你想放到./libs目录下,那么你在用的时候也要把你打包后的文件放置在这个目录中,否则还是访问不到的。先说一下这种方式的具体做法吧,在webapck.config.js中的plugins配置中增加:

1 plugins: [
2     ...
3     new webpack.DefinePlugin({
4         CESIUM_BASE_URL: JSON.stringify("")
5     })
6 ],

这里面用到了webapck的内置插件,因此先要引入webapck

const webpack = require('webpack'); // 访问内置的插件

这种做法很简单,唯一要注意的是字符串的值要使用JSON.stringify()来处理一下,否则是得不到预期的值的。

第二种,替换Cesium中的获取动态路径的方法,这种方法比较繁琐,需要自定义插件,这里暂时不列出了。

CESIUM_BASE_URL配置完成后,再运行页面,发现Cesium视窗已经能加载了,只是页面样式很乱,这是因为我们还未添加Cesium的内置CSS样式。添加样式引用:

import "/node_modules/cesium/Source/Widgets/widgets.css"

但是很不幸,又报错了:

ERROR in ./node_modules/cesium/Source/Widgets/widgets.css 1:0
Module parse failed: Unexpected character '@' (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. 

这说的是它不识别“@”字符?我也是懵逼的,但脑中第一反应是要装loader,果不其然,安装style-loadercss-loader之后解决问题。安装后要做如下配置:

1 module: {
2     rules: [
3         ...
4         {
5             test: /\.css$/i, // 正则表达式,表示.css后缀的文件。
6             use: ['style-loader', 'css-loader'] // 针对css文件使用的loader,注意有先后顺序,数组项越靠后越先执行。
7         }
8     ]
9 }

这样应该就没问题了。不过如果你的css中引用了图片,最好配置一下url-loader,因为图片文件也是需要loader的。安装之后做一下配置:

 1 module: {
 2     rules: [
 3         ...
 4         {
 5             test: /\.(png|jpg|jpeg|gif|svg)$/,
 6             loader: 'url-loader',
 7             options: {
 8                 name: './Assets/sc/[name].[ext]', // 这个路径其实是为了兼容Cesium的资源文件目录
 9                 limit: 10240 // 超过10K的不转换base64
10             }
11         }
12     ]
13 }

已经胜利在望了,不过还没有结束,控制栏里仍然有好多404错误,这是因为Cesium的静态资源没有取到。上面我们讲过了CESIUM_BASE_URL的基本原理,知道Cesium的静态资源要放置于根目录下的,因此我们要把资源都拷贝过来,使用的插件是copy-webpack-plugin,配置如下:

1 new CopyWebpackPlugin({
2     patterns: [
3         {from: path.join(cesiumSource, '../Build/Cesium/Workers'), to: 'Workers'},
4         {from: path.join(cesiumSource, '../Build/Cesium/Assets'), to: 'Assets'},
5         {from: path.join(cesiumSource, '../Build/Cesium/Widgets'), to: 'Widgets'},
6         {from: path.join(cesiumSource, '../Build/Cesium/ThirdParty'), to: 'ThirdParty'}
7     ]
8 }), // 拷贝Cesium资源、控件、WebWorker到静态目录。

大家请看上面的写法,跟你在网上看到的不一样,新版本的webpack请使用这里是新写法,否则报错哦。另外,最近的Cesium版本中增加了ThirdParty资源目录,某些高级应用中会使用到,因此也要将它拷贝过来。

至此,Cesium+webpack的搭建工作应该算告一段落了。运行一下脚本,发现黑黑的夜空已经出现在眼前了,但是地球呢,去哪里了?

首先,地球出不来的原因是影像数据没有正确加载,在没有指定影像数据的时候Cesium会默认从ION加载,而ION数据是需要token授权的。先去Cesium官网申请一个token,然后赋值。其次,还需要加载些3d模型,让地图更丰满些,我们可以直接加载Cesium的OSM城市模型。把index.js整体改造一下:

 1 import "/css/main.css";
 2 import "/node_modules/cesium/Source/Widgets/widgets.css";
 3 // import * as Cesium from "/node_modules/cesium/Source/Cesium.js";
 4 import Viewer from "/node_modules/cesium/Source/Widgets/Viewer/Viewer.js";
 5 import createOsmBuildings from "/node_modules/cesium/Source/Scene/createOsmBuildings.js";
 6 import Cartesian3 from "/node_modules/cesium/Source/Core/Cartesian3.js";
 7 import CesiumMath from "/node_modules/cesium/Source/Core/Math.js";
 8 import Ion from "/node_modules/cesium/Source/Core/Ion.js";
 9 
10 const viewer = new Viewer("cesiumContainer", {
11     creditContainer: "creditContainer"
12 });
13 viewer.scene.primitives.add(createOsmBuildings());
14 viewer.scene.camera.flyTo({
15     destination: Cartesian3.fromDegrees(-74.019, 40.6912, 750),
16     orientation: {
17         heading: CesiumMath.toRadians(20),
18         pitch: CesiumMath.toRadians(-20),
19     },
20 });
21 Ion.defaultAccessToken = 'Your Token';

再次运行调试,就出现了纽约的城市模型,但是影像还是没出来,什么鬼?我也不知道是什么鬼,以前设置了token就可以了的。没办法,手工切换到OSM地图源,总算加载出来了。但是总不至于每次手动加载吧,那太麻烦了,于是乎我们来点自动化的:

viewer.baseLayerPicker.viewModel.selectedImagery = viewer.baseLayerPicker.viewModel.imageryProviderViewModels[6];

上述代码的意思让底图自动切换到第七个地图源,即OpenStreetMap。

小结

至此,Cesium+webpack平台搭建完成。后续可能会做一些诸如分包之类的优化工作,弄好了之后我会直接在这里更新,敬请关注!另外,小伙伴们有空可以来群854943530逛逛,一定不虚此行哦。

 

-------------------- 不,这还不是我的底线 --------------------

 

补充

说好的后续优化来了。

首先,来做个分包。分包种类很多,但我只喜欢简单粗暴的,就是分两个包,一个程序包一个运行时包,对应到本项目就是:simple-cesium.js和simple-cesium.runtime.js。程序包负责打包所有自写的代码,运行时包则是负责打包第三方库和一些通用库。这样做的目的是为了以最小的IO代价来进行版本更新,运行时包除非第三方库升级否则基本不会变动的,那么每次更新版本只需要更新程序包即可,所以我们把Cesium库也打包进运行时中。来看配置:

 1 optimization: {
 2     splitChunks: onePackage ? {} : {
 3         chunks: "initial", // 从哪些chunks里面抽取代码,还可以通过函数来过滤所需的chunks:"initial" | "async" | "all" | function。
 4         minSize: 30000, // 抽取出来的文件在压缩前的最小大小,默认为30000。
 5         maxSize: 0, // 抽取出来的文件在压缩前的最大大小,默认为0,表示不限制最大大小。
 6         minChunks: 1, // 被引用次数,默认为1。如common中minChunks为2,表示将被两次以上引用的代码抽离成common。
 7         maxAsyncRequests: 6, // 按需加载chunk的并发请求数量,默认为5。
 8         maxInitialRequests: 4, // 页面初始加载时的并发请求数量,默认为3。
 9         automaticNameDelimiter: '~', // 抽取出来的文件的自动生成名字的分割符,默认为~。
10         cacheGroups: {
11             vendor: {
12                 name: "simple-cesium.runtime", // 抽取出来文件的名字,表示自动生成文件名。
13                 test: /[\\/](node_modules|thirdParty)[\\/]/,
14                 chunks: "all",
15                 priority: 10 // 优先级。
16             },
17             common: {
18                 name: "simple-cesium.common",
19                 test: /[\\/]src[\\/]/,
20                 minChunks: 2,
21                 minSize: 0, // 如果被多次引用的通用代码文件不超过minSize,则不会被抽离。
22                 chunks: "all",
23                 priority: 15,
24                 reuseExistingChunk: true
25             }
26         } // 缓存组。
27     }
28 }

 下面是打包后的目录:

Cesium是个庞大的库,所以打包后的文件远不止这些,截图只截取了一部分。不过我还是有些疑问,相同的配置,我在另外一个webpack的项目中打包出来就两个文件,一个程序文件,一个运行时文件,也就是说所有的runtime都打包成一个文件了。于是网上苦寻答案,翻遍了竟然还是找不到究竟是哪个参数控制的,如果有大神知道,还请指点啊,毕竟运行时打包成一个文件也是有应用场景的。

其次,就是自动清理dist文件夹,这个也是有必要的,Cesium每次大版本更新里面就会有大批文件变更,如果每次手动去清理就太麻烦了。可以使用clean-webpack-plugin来解决这个问题,安装后做如下配置:

1 plugins: [
2     new CleanWebpackPlugin(),
3     ...
4 ]

这里要注意,它的引用方式也不一样:

const {CleanWebpackPlugin} = require("clean-webpack-plugin");

暂时就补充这两个吧,后续如果有别的优化还会继续补充的。

最后,我把项目放到GitHub上了,后面我会以这个框架为起点继续深化的,感兴趣的小伙伴可以点个小星星持续关注一下哦。

posted on 2020-12-01 01:00  Helsing·Wang  阅读(2187)  评论(0编辑  收藏  举报

导航