mcpField
A
arcgis-core-utilities

arcgis-core-utilities

github.com/saschabrunnerch开发2362 次安装29 周前更新
代码仓库

关于 arcgis-core-utilities

为 ArcGIS JavaScript SDK 核心实用工具提供用法模式和示例,适合开发 ArcGIS 应用的开发者,涉及响应式状态、集合、异步操作、Worker、请求与本地化。

主要功能

  1. 01Accessor 类与属性模式
  2. 02响应式监听和事件处理
  3. 03可观察 Collection 用法
  4. 04Promise 与取消操作工具
  5. 05后台 Worker 执行
  6. 06请求与本地化功能

适用场景

  1. 01监听 ArcGIS 视图或图层状态
  2. 02管理集合变更事件
  3. 03在 Worker 中执行繁重任务
  4. 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,了解事件处理模式。

常见问题

  1. 内存泄漏:使用完后务必移除句柄。

    // 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();
    

    影响: 监听器会随时间不断累积,导致性能下降;如果它们引用了已销毁的对象,还可能引发错误。

  2. 初始值:使用 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 未使用当前值初始化,在首次属性变更前会一直为空或显示过时内容。

  3. 同步与异步:默认采用异步批处理,请谨慎使用 sync: true。

    // Async (default) - batched, better performance
    reactiveUtils.watch(() => value, callback);
    
    // Sync - immediate, can cause performance issues
    reactiveUtils.watch(() => value, callback, { sync: true });
    

    影响: 同步监听器会在每次变更时触发;如果被监听的属性频繁更新,可能导致布局抖动和性能不佳。

  4. 中止错误:务必在 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);
      }
    }
    

    影响: 中止错误(来自防抖操作、已取消的查询或导航变更)会被记录为真实错误,导致控制台日志泛滥,并可能触发错误报告系统。

评论

支持 Markdown