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,至于安装方法不赘述了,请自行查阅资料。我的node和npm版本分别为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-loader和css-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 阅读(2204) 评论(0) 编辑 收藏 举报