关于 arcgis-core-utilities
为 ArcGIS JavaScript SDK 核心实用工具提供用法模式和示例,适合开发 ArcGIS 应用的开发者,涉及响应式状态、集合、异步操作、Worker、请求与本地化。
主要功能
- 01Accessor 类与属性模式
- 02响应式监听和事件处理
- 03可观察 Collection 用法
- 04Promise 与取消操作工具
- 05后台 Worker 执行
- 06请求与本地化功能
适用场景
- 01监听 ArcGIS 视图或图层状态
- 02管理集合变更事件
- 03在 Worker 中执行繁重任务
- 04处理可取消的 ArcGIS 请求
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ saschabrunnerch/ arcgis-maps-sdk-js-ai-context --skill arcgis-core-utilities安装后在 Claude Code 中输入/arcgis-core-utilities确认已加载。
详细介绍
ArcGIS Core Utilities
将此技能用于核心基础设施模式,例如 Accessor、Collection、响应式实用工具和 workers。
导入模式
直接 ESM 导入
import * as reactiveUtils from "@arcgis/core/core/reactiveUtils.js";
import * as promiseUtils from "@arcgis/core/core/promiseUtils.js";
import Collection from "@arcgis/core/core/Collection.js";
import Handles from "@arcgis/core/core/Handles.js";
动态导入(CDN)
const reactiveUtils = await $arcgis.import(
"@arcgis/core/core/reactiveUtils.js",
);
const promiseUtils = await $arcgis.import("@arcgis/core/core/promiseUtils.js");
Accessor 模式
Accessor 类是所有 ArcGIS 类的基础,提供属性监听和计算属性功能。
创建自定义 Accessor 类
import Accessor from "@arcgis/core/core/Accessor.js";
import {
subclass,
property,
} from "@arcgis/core/core/accessorSupport/decorators.js";
@subclass("myapp.CustomClass")
class CustomClass extends Accessor {
@property()
name = "default";
@property()
value = 0;
// Computed property
@property({
readOnly: true,
dependsOn: ["name", "value"],
})
get summary() {
return `${this.name}: ${this.value}`;
}
}
const instance = new CustomClass({ name: "Test", value: 42 });
console.log(instance.summary); // "Test: 42"
属性装饰器
@subclass("myapp.MyClass")
class MyClass extends Accessor {
// Basic property
@property()
title = "";
// Property with type casting
@property({ type: Number })
count = 0;
// Read-only computed property
@property({ readOnly: true })
get displayName() {
return this.title.toUpperCase();
}
// Property with custom setter
@property()
get status() {
return this._status;
}
set status(value) {
this._status = value?.toLowerCase() || "unknown";
}
// Property that depends on others
@property({
readOnly: true,
dependsOn: ["count", "title"],
})
get info() {
return `${this.title} (${this.count})`;
}
}
属性类型
@property({ type: String })
name = "";
@property({ type: Number })
count = 0;
@property({ type: Boolean })
enabled = true;
@property({ type: Date })
createdAt = new Date();
@property({ type: [Number] }) // Array of numbers
values = [];
@property({ type: SomeClass }) // Custom class
child = null;
Collection
Collection 是一个类似数组且支持变更通知的类。
基本用法
import Collection from "@arcgis/core/core/Collection.js";
const collection = new Collection([
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
]);
// Add items
collection.add({ id: 3, name: "Item 3" });
collection.addMany([
{ id: 4, name: "Item 4" },
{ id: 5, name: "Item 5" },
]);
// Remove items
collection.remove(item);
collection.removeAt(0);
collection.removeMany([item1, item2]);
collection.removeAll();
// Access items
const first = collection.at(0);
const all = collection.toArray();
const length = collection.length;
Collection 方法
// Find items
const found = collection.find((item) => item.id === 3);
const index = collection.findIndex((item) => item.name === "Item 2");
const filtered = collection.filter((item) => item.value > 10);
// Transform
const mapped = collection.map((item) => item.name);
const reduced = collection.reduce((acc, item) => acc + item.value, 0);
// Check
const hasItem = collection.includes(item);
const exists = collection.some((item) => item.active);
const allActive = collection.every((item) => item.active);
// Sort and reorder
collection.sort((a, b) => a.name.localeCompare(b.name));
collection.reverse();
collection.reorder(item, 0); // Move item to index 0
// Stack operations
collection.push(item); // Add to end
collection.pop(); // Remove from end
collection.unshift(item); // Add to beginning
collection.shift(); // Remove from beginning
collection.splice(1, 2); // Remove 2 items at index 1
// Iterate
collection.forEach((item) => console.log(item.name));
for (const item of collection) {
console.log(item);
}
// Filter by type
const featureLayers = map.layers.ofType(FeatureLayer);
Collection 事件
// Watch for changes
collection.on("change", (event) => {
console.log("Added:", event.added);
console.log("Removed:", event.removed);
console.log("Moved:", event.moved);
});
// Watch length
collection.watch("length", (newLength, oldLength) => {
console.log(`Length changed from ${oldLength} to ${newLength}`);
});
类型化 Collection
// Collection of specific type
import Graphic from "@arcgis/core/Graphic.js";
const graphics = new Collection();
graphics.addMany([
new Graphic({ geometry: point1 }),
new Graphic({ geometry: point2 }),
]);
// Map's layers is a Collection
map.layers.add(featureLayer);
map.layers.reorder(featureLayer, 0);
reactiveUtils
用于监听属性和状态变更的现代响应式实用工具。
watch()
监听属性变化。
import * as reactiveUtils from "@arcgis/core/core/reactiveUtils.js";
// Watch single property
const handle = reactiveUtils.watch(
() => view.scale,
(scale) => {
console.log("Scale changed to:", scale);
},
);
// With options
reactiveUtils.watch(
() => view.extent,
(extent) => console.log("Extent:", extent),
{ initial: true }, // Call immediately with current value
);
// Stop watching
handle.remove();
when()
等待条件成立。
// Wait for view to be ready
await reactiveUtils.when(
() => view.ready,
() => console.log("View is ready!"),
);
// Wait for layer to load
await reactiveUtils.when(() => layer.loaded);
console.log("Layer loaded");
// With timeout (throws if not met)
await reactiveUtils.when(() => view.stationary, { timeout: 5000 });
once()
仅监听一次值变化。
// Execute once when condition is met
await reactiveUtils.once(() => view.ready);
console.log("View became ready");
// Once with callback
reactiveUtils.once(
() => layer.visible,
(visible) => console.log("Layer visibility changed to:", visible),
);
whenOnce()
等待属性首次变为真值,并返回一个 Promise。
// Wait for view to be ready
await reactiveUtils.whenOnce(() => view.ready);
console.log("View is ready");
on()
以响应式方式监听事件。
// Listen to click events
const handle = reactiveUtils.on(
() => view,
"click",
(event) => {
console.log("Clicked at:", event.mapPoint);
},
);
// Listen to layer view events
reactiveUtils.on(
() => view.whenLayerView(layer),
"highlight",
(event) => console.log("Highlight changed"),
);
多个属性
// Watch multiple properties
reactiveUtils.watch(
() => [view.center, view.zoom],
([center, zoom]) => {
console.log(`Center: ${center.x}, ${center.y}, Zoom: ${zoom}`);
},
);
// Computed expression
reactiveUtils.watch(
() => view.scale < 50000,
(isZoomedIn) => {
layer.visible = isZoomedIn;
},
);
Sync 选项
// Synchronous updates (use carefully)
reactiveUtils.watch(
() => view.extent,
(extent) => updateUI(extent),
{ sync: true },
);
句柄
管理多个监听句柄,以便清理。
import Handles from "@arcgis/core/core/Handles.js";
const handles = new Handles();
// Add handles
handles.add(
reactiveUtils.watch(
() => view.scale,
(scale) => {},
),
);
handles.add(
view.on("click", (e) => {}),
"click-handlers", // Group name
);
handles.add(
[
reactiveUtils.watch(
() => view.center,
() => {},
),
reactiveUtils.watch(
() => view.zoom,
() => {},
),
],
"view-watchers",
);
// Remove specific group
handles.remove("click-handlers");
// Check if group exists
handles.has("view-watchers"); // true
// Remove all
handles.removeAll();
// Destroy (also removes all)
handles.destroy();
在自定义类中
@subclass("myapp.MyWidget")
class MyWidget extends Accessor {
constructor(props) {
super(props);
this.handles = new Handles();
}
initialize() {
this.handles.add(
reactiveUtils.watch(
() => this.view?.scale,
(scale) => this.onScaleChange(scale),
),
);
}
destroy() {
this.handles.destroy();
super.destroy();
}
}
在 Accessor 上使用 addHandles
所有基于 Accessor 的类都内置了句柄管理功能:
// Add handles directly to an Accessor instance
view.addHandles(
reactiveUtils.watch(
() => view.scale,
(scale) => {},
),
"my-group",
);
// Check if handles exist
view.hasHandles("my-group"); // true
// Remove handles by group
view.removeHandles("my-group");
promiseUtils
用于处理 Promise 的实用工具。
import * as promiseUtils from "@arcgis/core/core/promiseUtils.js";
eachAlways()
等待所有 Promise 完成,无论成功还是失败。
const results = await promiseUtils.eachAlways([
fetch("/api/data1"),
fetch("/api/data2"),
fetch("/api/data3"),
]);
results.forEach((result, index) => {
if (result.error) {
console.error(`Request ${index} failed:`, result.error);
} else {
console.log(`Request ${index} succeeded:`, result.value);
}
});
debounce()
对函数进行防抖处理。
const debouncedSearch = promiseUtils.debounce(async (query) => {
const results = await searchService.search(query);
return results;
});
// Rapid calls will be debounced
input.addEventListener("input", async (e) => {
try {
const results = await debouncedSearch(e.target.value);
displayResults(results);
} catch (e) {
if (!promiseUtils.isAbortError(e)) {
console.error(e);
}
}
});
isAbortError()
检查错误是否由已中止的操作引起。
try {
await someAsyncOperation();
} catch (error) {
if (promiseUtils.isAbortError(error)) {
console.log("Operation was cancelled");
} else {
throw error;
}
}
ignoreAbortErrors()
忽略 Promise 中止错误。
// Instead of try/catch with isAbortError check
await promiseUtils.ignoreAbortErrors(debouncedFunction());
createAbortError()
创建可与 AbortSignal 搭配使用的中止错误。
const error = promiseUtils.createAbortError();
// Useful in custom async operations that support cancellation
filter()
对数组进行异步筛选。
const validItems = await promiseUtils.filter(items, async (item) => {
const result = await validateItem(item);
return result.isValid;
});
Workers
在后台线程中运行繁重的计算任务。
import * as workers from "@arcgis/core/core/workers.js";
打开 Worker 连接
const connection = await workers.open("path/to/worker.js");
// Execute method on worker
const result = await connection.invoke("methodName", { data: "params" });
// Close when done
connection.close();
Worker 脚本示例
// worker.js
define([], function () {
return {
methodName: function (params) {
// Heavy computation here
const result = processData(params.data);
return result;
},
anotherMethod: function (params) {
return params.value * 2;
},
};
});
使用 Workers 处理繁重任务
// Main script
import * as workers from "@arcgis/core/core/workers.js";
async function processLargeDataset(data) {
const connection = await workers.open("./dataProcessor.js");
try {
const result = await connection.invoke("process", {
data: data,
options: { threshold: 100 },
});
return result;
} finally {
connection.close();
}
}
错误处理
import Error from "@arcgis/core/core/Error.js";
// Create custom error
const error = new Error("layer-load-error", "Failed to load layer", {
layer: layer,
originalError: e,
});
// Check error type
if (error.name === "layer-load-error") {
console.log("Layer failed:", error.details.layer.title);
}
URL 实用工具
import * as urlUtils from "@arcgis/core/core/urlUtils.js";
// Add proxy rule
urlUtils.addProxyRule({
urlPrefix: "https://services.arcgis.com",
proxyUrl: "/proxy",
});
esriRequest
SDK 内置的 HTTP 客户端。可自动处理 ArcGIS 身份验证、代理规则和 CORS。对于所有访问 ArcGIS REST 端点的请求,以及需要 SDK 级身份验证和代理处理的通用 HTTP 请求,都应使用它。
基本请求
import esriRequest from "@arcgis/core/request.js";
// GET request with JSON response
const response = await esriRequest(url, {
query: { f: "json" },
responseType: "json",
});
console.log("Status:", response.httpStatus);
console.log("Data:", response.data);
带选项的请求
const response = await esriRequest(url, {
query: {
f: "json",
param1: "value1",
},
responseType: "json", // "json" | "text" | "array-buffer" | "blob" | "image" | "document" | "xml"
method: "post", // "auto" | "head" | "post"
body: formData, // For POST requests
timeout: 30000, // Timeout in ms
headers: {
"X-Custom-Header": "value",
},
});
下载二进制数据
// Image response
const imageResponse = await esriRequest(imageUrl, {
responseType: "image",
});
const imageElement = imageResponse.data;
// Binary data
const binaryResponse = await esriRequest(fileUrl, {
responseType: "array-buffer",
});
const arrayBuffer = binaryResponse.data;
可中止的请求
搭配 AbortController 进行取消,尤其适用于自定义图层和防抖工作流。使用 promiseUtils.isAbortError 区分用户取消的操作与实际错误。
import esriRequest from "@arcgis/core/request.js";
import * as promiseUtils from "@arcgis/core/core/promiseUtils.js";
const controller = new AbortController();
try {
const response = await esriRequest(url, {
responseType: "json",
signal: controller.signal,
});
} catch (error) {
if (promiseUtils.isAbortError(error)) {
// Cancelled — expected, do nothing
} else {
throw error;
}
}
// Cancel the in-flight request
controller.abort();
调度
import * as scheduling from "@arcgis/core/core/scheduling.js";
// Schedule for next frame
const handle = scheduling.addFrameTask({
update: (event) => {
// Called every frame
console.log("Delta time:", event.deltaTime);
console.log("Elapsed:", event.elapsedTime);
},
});
// Pause/resume frame task
handle.pause();
handle.resume();
// Remove task
handle.remove();
// One-time frame callback
scheduling.schedule(() => {
console.log("Executed on next frame");
});
常见模式
清理模式
class MyComponent {
constructor(view) {
this.view = view;
this.handles = new Handles();
this.setup();
}
setup() {
this.handles.add([
reactiveUtils.watch(
() => this.view.scale,
(scale) => this.onScaleChange(scale),
),
reactiveUtils.watch(
() => this.view.extent,
(extent) => this.onExtentChange(extent),
),
this.view.on("click", (e) => this.onClick(e)),
]);
}
destroy() {
this.handles.removeAll();
}
}
防抖更新
const debouncedUpdate = promiseUtils.debounce(async () => {
const extent = view.extent;
const results = await layer.queryFeatures({
geometry: extent,
returnGeometry: true,
});
updateDisplay(results);
});
reactiveUtils.watch(() => view.stationary && view.extent, debouncedUpdate);
条件监听
// Only react when view is stationary
reactiveUtils.watch(
() => (view.stationary ? view.extent : null),
(extent) => {
if (extent) {
updateForExtent(extent);
}
},
);
国际化(intl)
intl 模块提供针对区域设置的数字、日期和坐标格式化功能。
数字格式化
import * as intl from "@arcgis/core/intl.js";
// Format numbers with locale settings
const formatted = intl.formatNumber(1234567.89);
// "1,234,567.89" (en-US) or "1.234.567,89" (de-DE)
// Format with options
const currency = intl.formatNumber(1234.5, {
style: "currency",
currency: "USD",
});
// "$1,234.50"
const percent = intl.formatNumber(0.75, {
style: "percent",
});
// "75%"
日期格式化
import * as intl from "@arcgis/core/intl.js";
const date = new Date();
// Format dates
const formatted = intl.formatDate(date);
// Format with options
const longDate = intl.formatDate(date, {
dateStyle: "full",
timeStyle: "short",
});
// Format date only (from ISO string)
const dateOnly = intl.formatDateOnly("2024-03-15");
// Format time only (from ISO string)
const timeOnly = intl.formatTimeOnly("14:30:00");
// Format timestamp (from ISO string)
const timestamp = intl.formatTimestamp("2024-03-15T14:30:00Z");
设置区域设置
import * as intl from "@arcgis/core/intl.js";
// Get current locale
const locale = intl.getLocale();
// Get language code
const lang = intl.getLocaleLanguage();
// Normalize locale string
const normalized = intl.normalizeMessageBundleLocale("en-us"); // "en-US"
消息包(i18n)
import * as intl from "@arcgis/core/intl.js";
// Create a JSON message bundle loader
const loader = intl.createJSONLoader({
pattern: "my-app/",
base: "./nls",
});
// Fetch a message bundle
const messages = await intl.fetchMessageBundle("my-app/messages");
console.log(messages.greeting); // Localized string
格式转换实用工具
import * as intl from "@arcgis/core/intl.js";
// Convert SDK date format to Intl options
const intlOptions = intl.convertDateFormatToIntlOptions(
"short-date-short-time",
);
// Convert field format to Intl options
const numberOptions = intl.convertNumberFieldFormatToIntlOptions(fieldFormat);
const dateOptions =
intl.convertDateTimeFieldFormatToIntlOptions(dateFieldFormat);
坐标格式化
import * as coordinateFormatter from "@arcgis/core/geometry/coordinateFormatter.js";
// Load the coordinate formatter module
await coordinateFormatter.load();
// Format point as DMS (degrees, minutes, seconds)
const dms = coordinateFormatter.toLatitudeLongitude(point, "dms", 2);
// "34°01'12.00\"N 118°48'18.00\"W"
// Format as UTM
const utm = coordinateFormatter.toUtm(point, "north-south-indicators", true);
// "11S 357811 3764417"
// Parse coordinate string to Point
const parsed = coordinateFormatter.fromLatitudeLongitude("34.02N 118.805W");
参考示例
watch-for-changes- 监听地图对象的属性变化watch-for-changes-reactiveutils- 使用现代方式监听响应式属性chaining-promises- 使用该 API 的 Promise 链接模式event-explorer- 探索视图和图层事件
相关技能
- 请参阅
arcgis-core-maps,了解使用 reactiveUtils 的地图和视图设置。 - 请参阅
arcgis-layers,了解使用 Collection 管理图层。 - 请参阅
arcgis-interaction,了解事件处理模式。
常见问题
-
内存泄漏:使用完后务必移除句柄。
// Anti-pattern: never removing the handle reactiveUtils.watch( () => view.scale, (scale) => updateUI(scale), ); // Handle is lost, watcher runs forever// Correct: store and remove the handle const handle = reactiveUtils.watch( () => view.scale, (scale) => updateUI(scale), ); // Remove when no longer needed handle.remove();影响: 监听器会随时间不断累积,导致性能下降;如果它们引用了已销毁的对象,还可能引发错误。
-
初始值:使用
initial: true立即获取当前值。// Anti-pattern: missing initial value reactiveUtils.watch( () => view.scale, (scale) => updateUI(scale), ); // updateUI is NOT called until scale changes// Correct: use initial option reactiveUtils.watch( () => view.scale, (scale) => updateUI(scale), { initial: true }, // Called immediately with current scale );影响: UI 未使用当前值初始化,在首次属性变更前会一直为空或显示过时内容。
-
同步与异步:默认采用异步批处理,请谨慎使用
sync: true。// Async (default) - batched, better performance reactiveUtils.watch(() => value, callback); // Sync - immediate, can cause performance issues reactiveUtils.watch(() => value, callback, { sync: true });影响: 同步监听器会在每次变更时触发;如果被监听的属性频繁更新,可能导致布局抖动和性能不佳。
-
中止错误:务必在 catch 块中检查中止错误。
// Anti-pattern: treating abort errors as real errors try { await debouncedFunction(); } catch (e) { console.error("Error!", e); // Logs abort errors as errors }// Correct: filter out abort errors try { await debouncedFunction(); } catch (e) { if (!promiseUtils.isAbortError(e)) { console.error("Error!", e); } }影响: 中止错误(来自防抖操作、已取消的查询或导航变更)会被记录为真实错误,导致控制台日志泛滥,并可能触发错误报告系统。