资讯动态

GEE脚本模块化:用exports和require告别代码混乱

发布时间:2026/10/17 1:09:32 来源:尧图企业网站定制
在GEEGoogle Earth Engine里写脚本最让我头疼的从来不是某个算法不会而是代码仓库越堆越乱。同一个去云函数今天在这份脚本里复制一遍明天又在另一份脚本里改一下等到数据更新要全面调整翻遍十几个脚本也找不全到底哪里用了它。后来我开始用exports和require做模块化把通用的逻辑统一封装成模块脚本再用require在需要的地方加载调用脚本瞬间清爽了很多。这篇文章我就完整复现一遍自己的做法讲讲这套机制的原理、路径规则、代码示例以及我踩过的一些坑适合所有已经在用GEE、但脚本开始变得臃肿的人参考。1. 为什么GEE脚本越来越需要“模块化”1.1 一个脚本走天下的痛早期我写GEE大部分任务都堆在同一个脚本里先加载影像收集再写掩膜然后算指数最后做波段合成。一个功能复杂点的分析脚本轻松超过一千行。刚开始没问题反正自己一个人用哪里不对就往下翻。可一旦项目多了麻烦就来了。最典型的场景是我明明在A脚本里定义了一个去云函数过了两个月在B脚本里要用第一反应是直接把那段代码复制过来。代码是复制过来了但当时可能为了A项目临时改了一个参数下一次在C脚本里又复制了一份结果三份代码慢慢变得不一样。到时候数据更新、参数调整我根本不知道哪个版本才是最新的只能一个个脚本翻效率极低。另一个痛点是“传阅成本”。别人把你的脚本链接拿过去发现函数定义和实际调用全塞在一起改一行都要看半天上下文。如果是团队协作大家共享一个公共代码仓库这种全脚本式写法基本没法合作。每个人都有自己的修订最后合并代码全靠手工几乎是噩梦。1.2 模块化解决的核心问题exports和require的组合解决的就是上面这些事。你可以把去云、时间过滤、指数计算这些功能拆成独立的模块脚本然后用“导出”的方式把它暴露出去。别的脚本要用的时候只需要写一行require把模块加载进来直接调用里面的函数。这个机制带来的价值有三层第一层是复用。同一套代码只维护一份所有引用它的脚本自动使用最新版本。哪怕有二十个脚本调用同一个函数改模块一处全部生效。第二层是可读性。调用脚本里只保留业务逻辑不再堆满工具函数。别人看你的主脚本时一眼就知道它做了什么而不是陷入细节森林。第三层是协作边界。模块可以按功能划分大家各自维护自己的模块谁负责影像预处理、谁负责统计图表互不干扰。模块之间的接口就是导出的函数签名约定清楚后协作效率会高很多。说白了这跟写普通JavaScript时把公共函数抽到工具文件里是一样的思路只是GEE把脚本存在云端需要借助它自己的加载规则来完成。2. exports和require的语法拆解与边界问题2.1 exports导出的三种写法在GEE Code Editor里一个脚本文件要想被其他脚本引用必须用exports把内容暴露出去。最基础的写法如下。// 写法A导出单个功能函数 exports.ndvi function(image) { return image.normalizedDifference([SR-B5, SR-B4]).rename(NDVI); }; // 写法B导出一个对象把多个功能聚合在一起 var CloudTools { applyMask: function(image) { // 去云逻辑 return image; }, addQualityBand: function(image) { // 添加质量波段 return image; } }; exports.CloudTools CloudTools; // 写法C导出普通变量或常量便于统一配置 exports.VERSION 2024.1; exports.DEFAULT_START 2022-01-01;你在编辑器里保存这个脚本时它就是一个“模块”。其他脚本用require加载后拿到的是一个对象里面包含所有exports出去的键值对。三种写法的使用场景不太一样写法A适合模块里只有一个核心函数的情况简单直接写法B更适合一个模块有多个功能用一个命名空间把它们包起来调用的时候写tools.CloudTools.applyMask(...)逻辑更清楚写法C则多用于配置常量比如统一日期范围、统一阈值、统一版本号。有一点需要注意模块里那些没有通过exports暴露出来的变量或函数外部是拿不到的。所以如果你只是想内部辅助一下就别exports它天然就具备了封装性。2.2 require加载的两种路径写法require接收的是一个脚本路径这个路径就是你在Code Editor右侧脚本列表里看到的那个唯一地址。它通常写成下面两种形式之一// 形式一仓库模式 var tools require(users/yourname/my-repo:Modules/tools.js); // 形式二非仓库模式的用户路径 var tools require(users/yourname/Modules/tools.js);具体用哪一种取决于你的脚本是怎么组织的。最省心也最不会出错的做法是在Code Editor左侧的Scripts列表中找到目标文件右键选择“Copy Script Path”然后把复制出来的字符串放进require的引号里。这里特别提醒一句路径是大小写敏感的。Users和users不一样文件夹名字大小写错了也会报错。所以不要手动敲路径复制粘贴最稳。另外require的返回值是一个对象你可以把它当作一个命名空间来用。比如上面复制出来的是“users/yourname/my-repo:Modules/tools.js”那调用代码就是var tools require(users/yourname/my-repo:Modules/tools.js); var ndvi tools.ndvi(ee.Image(LANDSAT/LC08/C02/T1_L2));2.3 服务端对象与客户端函数的边界这是很多人踩坑的地方理解的深后面的问题会少一半。GEE代码同时运行在“客户端”和“服务端”两个层面。你写的JavaScript语言本身在客户端执行用来构建请求流程而ee.Image、ee.FeatureCollection这些对象是“远端引用”真正计算发生在Google服务器上。在模块化时要牢记一条原则导出的函数里应该只操作服务端对象和普通数据尽量避免调用客户端UI相关的能力。比如Map.addLayer、Map.centerObject、print这些都是客户端API它们不应该出现在模块里。如果你写了一个函数exports.drawOnMap function(image) { Map.addLayer(image, {}, layer); };然后在调用脚本里require它GEE很可能不会按你预期工作因为Map对象在模块被解析时并不一定处在当前UI上下文中。尤其是当模块被多个脚本共享时这种写法会让模块变得“不可移植”。更好的方式是模块只负责返回ee.Image或计算出结果把Map.addLayer放在调用脚本里。例如模块里写exports.landsatComposite function(start, end) { var l8 ee.ImageCollection(LANDSAT/LC08/C02/T1_L2) .filterDate(start, end); return l8.median(); };调用脚本里再显示var result tools.landsatComposite(2022-01-01, 2022-12-31); Map.addLayer(result, {bands: [SR_B4, SR_B3, SR_B2], min: 7000, max: 12000}, RGB);这样模块本身只负责“算”显示由调用方决定通用性会强很多。3. 从零封装一个可复用的Landsat处理模块3.1 案例背景封装一个时序影像预处理模块下面我用一个真实常见的需求带大家完整走一遍。假设我经常需要处理Landsat影像每次都要做四件事按日期筛选、去云、计算NDVI、合成季度影像。以前这块逻辑至少要写四五十行每次新建脚本都要复制今天我们就把它封装成模块。模块文件名叫landsat_tools.js它计划导出这些函数filterDate: 按时间范围过滤影像收集applyCloudMask: 基于Landsat质量波段做云掩膜addNDVI: 为单景影像添加NDVI波段quarterComposite: 按月合成中值影像并自动添加NDVI。我先在Code Editor里新建一个文件夹Modules把模块脚本统一放进去。然后开始写代码。3.2 模块脚本的完整代码下面是landsat_tools.js的完整内容我直接用在项目里的版本去掉了一些跟具体业务绑定的部分。// 模块landsat_tools.js // 说明提供Landsat常规预处理函数 var CLOUD_SHADOW_BIT 1 4; var CLOUD_BIT 1 3; function applyCloudMask(image) { var qa image.select(QA_PIXEL); var mask qa.bitwiseAnd(CLOUD_BIT).eq(0) .and(qa.bitwiseAnd(CLOUD_SHADOW_BIT).eq(0)); return image.updateMask(mask); } function addNDVI(image) { var ndvi image.normalizedDifference([SR-B5, SR-B4]).rename(NDVI); return image.addBands(ndvi); } function filterDate(collection, start, end) { return collection.filterDate(start, end); } function quarterComposite(start, end) { var collection ee.ImageCollection(LANDSAT/LC08/C02/T1_L2) .filterDate(start, end) .map(applyCloudMask); var withNdvi collection.map(addNDVI); return withNdvi.median(); } exports.applyCloudMask applyCloudMask; exports.addNDVI addNDVI; exports.filterDate filterDate; exports.quarterComposite quarterComposite; exports.VERSION 2024.1;这里我刻意没有在模块内部用exports.xxx去互相调用而是先定义局部函数最后统一导出。这个细节很关键。如果在quarterComposite里写成exports.applyCloudMask(...)当GEE把函数序列化到服务端执行时它对exports闭包的解析容易出边界问题。用模块内的局部函数名闭包关系干净很多实际跑起来更稳。所以我建议你养成这个习惯模块内部互相调用时直接用内部函数名不要走exports。另外要说明的是quarterComposite返回的是median()合成后的单张影像。它不包含Map显示只返回纯计算结果方便任何调用脚本自己决定如何展示。3.3 调用脚本的完整代码模块写好后新建一个调用脚本process_landsat.js内容如下。// 调用脚本加载模块并输出结果 var tools require(users/yourname/Modules/landsat_tools.js); var start 2022-01-01; var end 2022-12-31; var composite tools.quarterComposite(start, end); var ndvi composite.select(NDVI); // 到这里模块已经把复合影像和NDVI都算好了 Map.addLayer(composite, { bands: [SR-B4, SR-B3, SR-B2], min: 7000, max: 12000 }, Landsat 8 RGB); Map.addLayer(ndvi, { min: -0.2, max: 0.8, palette: [blue, white, green] }, NDVI); print(Version:, tools.VERSION);这段脚本非常短因为它只负责“拉数据、显示结果”。核心的预处理逻辑在模块里维护。下次我想换一个区域、换一个时间只需要修改调用脚本里的变量甚至可以把日期也做成模块级的配置。如果我想把所有相关工具都集中管理还可以再加一层入口模块比如index.js统一把多个模块打包导出这样调用脚本只需要require一次// 入口模块index.js var landsat require(users/yourname/Modules/landsat_tools.js); var cloudStats require(users/yourname/Modules/cloud_stats.js); exports.landsat landsat; exports.cloudStats cloudStats; exports.VERSION 2024.1;调用脚本变成var tools require(users/yourname/Modules/index.js); tools.landsat.quarterComposite(...);这种方式适合模块数量变多之后给调用方提供统一入口避免一次写十几个require。3.4 多模块互相引用的工程化组织当一个项目模块多了以后模块之间也经常需要互相调用。比如有一个config.js专门放配置信息另一个processor.js读取配置并处理数据。// config.js exports.START_DATE 2022-01-01; exports.END_DATE 2022-12-31; exports.REGION ee.FeatureCollection(你的研究区);// processor.js var config require(users/yourname/Modules/config.js); var landsat require(users/yourname/Modules/landsat_tools.js); exports.process function() { var composite landsat.quarterComposite(config.START_DATE, config.END_DATE); var clipped composite.clipToCollection(config.REGION); return clipped; };这种依赖关系是单向的processor依赖configconfig不依赖processor。我最常遇到的工程化问题就是循环依赖——A模块引用BB模块又引用AGEE在加载时报一些看不懂的错误。解决思路只有一个把公共底层抽出来建立清晰的单向依赖树不要让模块互相咬合。还有一个实用的组织技巧给模块加版本号。用exports.VERSION 2024.1然后在调用脚本里print(tools.VERSION)这样团队协作时能马上确认对方跑的是哪个版本排查“我这边结果怎么和你不一样”的问题时会快很多。4. 模块化项目中的常见报错与排错实操4.1 五种最典型的报错场景我用exports和require做模块化的过程中大概也就遇到了下面这些坑。把它们整理成一个速查表基本可以覆盖绝大部分情况。报错信息可能原因解决办法Required algorithm does not exist.require路径填错或模块不存在用编辑器右键的“Copy Script Path”重新复制Cannot read property xxx of undefined导出的属性名/方法名拼写不一致检查模块里的exports.xxx和调用处的.xxx是否一致Cannot export objects of type ...试图导出Map、print、Date等客户端对象模块里只返回EE对象或原始类型显示相关操作挪到调用脚本Module xx not found路径大小写错误目录层级不对逐个确认目录名、文件名、仓库名的大小写出现循环依赖或加载死锁模块A引用BB又引用A重构依赖关系抽公共底层保持单向引用第一类报错最频繁尤其是当我手动敲路径的时候十次有九次会敲错。所以我真的建议路径一律复制不要手输。第二类报错通常发生在刚使用模块化时。模块里写成exports.myFunc但调用脚本里写tools.MyFunc大小写不一致或者函数名写错。这个好办打印一下模块对象就能看出到底有没有导出成功。第三类报错是概念理解没到位。请记住被exports的东西必须可序列化。ee.Image、ee.Number、普通字符串、数字、数组都可以但是UI控件、地图对象和本地对象不行。4.2 排查技巧日志、路径和刷新如果真的遇到问题我一般会按下面这个顺序排查。第一步在调用脚本里直接print(require(模块路径))。如果返回的是一个对象那就说明能加载成功你再仔细看对象里有哪些属性跟自己调用的属性名对不对得上。比如var tools require(users/yourname/Modules/landsat_tools.js); print(tools);这个输出会展示模块导出的所有成员一目了然。第二步检查模块脚本本身是否保存过。Code Editor偶尔会出现“改完模块脚本但调用脚本仍然跑旧逻辑”的情况我会把调用脚本重新运行一遍有时候还要刷新一下编辑器页面。别小看这个操作我至少遇到三次“改了半天结果跑的还是旧缓存”的假象。第三步在模块脚本里加一个临时print验证加载顺序。如果模块加载时print就会执行你可以把它当作模块代码被触发的信号。确认调用脚本确实加载到了这个模块而非另一个同名文件。第四步排查路径层级。比如你的目录结构是Modules/sub/ndvi.js那路径就要包含完整层级。不要只写文件夹名最后一级的脚本名也要带上。通常复制出来的路径是最完整的。4.3 一些经验建议最后分享几个我用了很久的模块化习惯。第一模块文件命名用全小写加下划线。GEE的路径大小写敏感团队协作时如果命名不统一很容易出现同样的脚本在A机器上能跑、在B机器上报错。小写加下划线可以避免大部分问题。第二模块里不要出现过长的私有全局变量。有些变量你只是内部临时用不要放在模块顶层否则每次require都会重新初始化。对于EE对象来说无所谓但如果你在模块里封装了一个原生JavaScript对象并且期望它跨脚本保持状态基本是行不通的。每次require都会重新执行模块代码状态不会保留。第三为模块写一行说明注释。哪怕只是“这个模块提供Landsat预处理和NDVI计算”这一句话在三个月后重新拿起来时会非常有价值。第四把异常的可能前置到导出函数之外。也就是说尽量在模块函数内部只做EE对象层面的操作减少对调用脚本环境的依赖。这样模块的稳定性和复用性都会更好。5. 从脚本调用到团队协作的进一步扩展模块化做到这里其实已经从一个“调用技巧”变成了“项目组织方式”。在GEE的团队协作场景里我会把脚本目录规划成这样Modules/ config.js # 全局配置 landsat_tools.js # Landsat预处理 series_tools.js # 时间序列分析 stats_tools.js # 统计与图表辅助 scripts/ export_mosaic.js # 业务脚本A chart_ndvi.js # 业务脚本B模块文件夹负责“能力”脚本文件夹负责“业务”。任何新成员接手项目时第一件事先看模块目录里有什么函数而不是逐个脚本翻代码。如果你的团队有公共代码仓库模块化还有一个额外优势只需要更新一个公共模块脚本相关业务脚本全部跟着更新。这比把代码复制来复制去或者通过聊天工具发来发去要可靠得多。我个人在实际操作中还有一个体会模块封装到一定程度后你会开始“挑剔”自己的模块API是否好用。比如函数参数太多、返回结构不稳定都会在调用层暴露出来。这时就该重构模块了而不是继续往调用脚本里堆补丁。从这个角度来说exports和require不仅能整理代码还会倒逼你把接口想得更清楚算是一个额外收获吧。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑