mcpField
A
arcgis-starter-app

arcgis-starter-app

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

关于 arcgis-starter-app

arcgis-starter-app 为使用 TypeScript 和 Vite 构建 ArcGIS Maps SDK for JavaScript 应用的开发者提供指南,包含精简与面向生产的配置,以及 Map Components、Core API、React 和 Angular 示例。

主要功能

  1. 01精简与面向生产的项目配置
  2. 02TypeScript 和 Vite 设置
  3. 032D 地图与 3D 场景示例
  4. 04React 和 Angular 集成指南
  5. 05构建、代码检查与部署配置

适用场景

  1. 01启动小型 ArcGIS 制图原型
  2. 02搭建面向生产的 Web 制图项目
  3. 03在 React 或 Angular 中集成 ArcGIS Map Components
  4. 04排查 API 密钥与 TypeScript 模块解析问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/saschabrunnerch/arcgis-maps-sdk-js-ai-context --skill arcgis-starter-app

安装后在 Claude Code 中输入/arcgis-starter-app确认已加载。

附带文件AGENTS.md

详细介绍

ArcGIS Starter 应用

使用此技能,通过 TypeScript 和 Vite 创建 ArcGIS Maps SDK for JavaScript 应用。可选择用于快速原型开发的最简配置,或包含完整工具链的生产就绪配置。

前提条件

  • ArcGIS 账户:需要 ArcGIS Location Platform、ArcGIS Online 或 ArcGIS Enterprise 账户,才能访问内容和服务。
  • 访问令牌:通过门户创建具有适当权限(底图、地理编码、路径规划等)的访问令牌,并为生产环境配置允许的来源。
  • Node.js:LTS 版本(20+),搭配 npm 或 pnpm。

注意: 使用 npm 时,Map Components 的 CSS 会自动加载,无需手动导入 @arcgis/map-components 的 CSS。仅在直接使用 Core API 微件时才需要导入 @arcgis/core 的 CSS。

框架集成

React 19

Map Components 是 Web 组件(自定义元素)。在 React 19 中,可直接使用,并通过 on 前缀附加事件监听器:

import "@arcgis/map-components/dist/components/arcgis-map";
import "@arcgis/map-components/dist/components/arcgis-zoom";

function App() {
  return (
    <arcgis-map
      basemap="topo-vector"
      center="-118.24,34.05"
      zoom={12}
      onarcgisViewReadyChange={(e: CustomEvent) => {
        const { view } = e.detail;
        console.log("View ready:", view);
      }}
    >
      <arcgis-zoom slot="top-left"></arcgis-zoom>
    </arcgis-map>
  );
}

重要: 请使用事件监听器(例如 onarcgisViewReadyChange),而不要直接在元素 ref 上调用方法,因为组件在渲染时可能尚未定义。

Angular

将 CUSTOM_ELEMENTS_SCHEMA 添加到模块或独立组件中,以便 Angular 识别非 Angular 元素:

import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";

@Component({
  selector: "app-map",
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <arcgis-map basemap="topo-vector" center="-118.24,34.05" [zoom]="12">
      <arcgis-zoom slot="top-left"></arcgis-zoom>
    </arcgis-map>
  `,
})
export class MapComponent {}

在根样式表中显式导入 Map Components CSS:

/* styles.css */
@import "@arcgis/map-components/dist/components/arcgis-map/arcgis-map.css";
TypeScript 配置

在 tsconfig.json 中将 moduleResolution 设为 "node16" 或 "bundler",以正确解析 @arcgis/map-components 包导出:

{
  "compilerOptions": {
    "moduleResolution": "bundler"
  }
}

最简配置

项目结构
my-arcgis-app/
├── src/
│   ├── main.ts
│   └── style.css
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
├── .gitignore
└── README.md
package.json
{
  "name": "my-arcgis-app",
  "private": true,
  "version": "0.0.1",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "devDependencies": {
    "typescript": "~5.9.3",
    "vite": "^7.2.7"
  },
  "dependencies": {
    "@arcgis/map-components": "^5.0.0",
    "@esri/calcite-components": "^3.3.3"
  }
}
index.html(2D 地图)
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <title>ArcGIS Map App</title>
    <link rel="stylesheet" href="/src/style.css" />
  </head>
  <body>
    <calcite-shell content-behind>
      <arcgis-map item-id="YOUR_WEBMAP_ID">
        <arcgis-zoom slot="top-left"></arcgis-zoom>
        <arcgis-legend slot="bottom-left"></arcgis-legend>
      </arcgis-map>
    </calcite-shell>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
index.html(3D 场景)
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <title>ArcGIS Scene App</title>
    <link rel="stylesheet" href="/src/style.css" />
  </head>
  <body>
    <calcite-shell content-behind>
      <arcgis-scene item-id="YOUR_WEBSCENE_ID">
        <arcgis-zoom slot="top-left"></arcgis-zoom>
        <arcgis-navigation-toggle slot="top-left"></arcgis-navigation-toggle>
        <arcgis-compass slot="top-left"></arcgis-compass>
      </arcgis-scene>
    </calcite-shell>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
src/main.ts(Map Components)
import "@arcgis/map-components/dist/components/arcgis-map";
import "@arcgis/map-components/dist/components/arcgis-zoom";
import "@arcgis/map-components/dist/components/arcgis-legend";
// For 3D scenes, also import:
// import "@arcgis/map-components/dist/components/arcgis-scene";
// import "@arcgis/map-components/dist/components/arcgis-navigation-toggle";
// import "@arcgis/map-components/dist/components/arcgis-compass";

import "@esri/calcite-components/dist/components/calcite-shell";

import { setAssetPath as setCalciteAssetPath } from "@esri/calcite-components/dist/components";

// Set Calcite assets path
setCalciteAssetPath("https://js.arcgis.com/calcite-components/3.3.3/assets");

// Configure ArcGIS API key
import esriConfig from "@arcgis/core/config";
esriConfig.apiKey = "YOUR_API_KEY";

// Wait for map to be ready
const arcgisMap = document.querySelector("arcgis-map");
arcgisMap?.addEventListener("arcgisViewReadyChange", (event) => {
  const { view } = (event as CustomEvent).detail;
  console.log("Map view ready:", view);
});
src/main.ts(Core API - 编程式)
import "@esri/calcite-components/dist/components/calcite-shell";
import { setAssetPath as setCalciteAssetPath } from "@esri/calcite-components/dist/components";

import Map from "@arcgis/core/Map.js";
import MapView from "@arcgis/core/views/MapView.js";

// Set Calcite assets path
setCalciteAssetPath("https://js.arcgis.com/calcite-components/3.3.3/assets");

const map = new Map({ basemap: "topo-vector" });

// Create view
const view = new MapView({
  container: "viewDiv",
  map: map,
  center: [-118.805, 34.027],
  zoom: 13,
});
src/style.css
@import "@arcgis/core/assets/esri/themes/light/main.css";

html,
body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;
}

calcite-shell {
  width: 100%;
  height: 100%;
}
arcgis-map,
arcgis-scene,
#viewDiv {
  width: 100%;
  height: 100%;
}
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true
  },
  "include": ["src"]
}
vite.config.ts
import { defineConfig } from "vite";

export default defineConfig({
  build: { target: "esnext" },
});
.gitignore
node_modules/
dist/
.env
.env.local
快速开始
npm install @arcgis/map-components @esri/calcite-components
npm install -D typescript vite
npm run dev
常用微件
// Import additional components as needed
import "@arcgis/map-components/dist/components/arcgis-search";
import "@arcgis/map-components/dist/components/arcgis-basemap-gallery";
import "@arcgis/map-components/dist/components/arcgis-layer-list";
<arcgis-map item-id="YOUR_MAP_ID">
  <arcgis-search slot="top-right"></arcgis-search>
  <arcgis-basemap-gallery slot="top-right"></arcgis-basemap-gallery>
</arcgis-map>

生产环境配置

项目结构
my-arcgis-app/
├── .github/
│   └── workflows/
│       ├── test.yml
│       └── deploy.yml
├── src/
│   ├── main.ts
│   └── style.css
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
├── eslint.config.js
├── .prettierrc
├── .prettierignore
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .env.example
└── README.md
package.json
{
  "name": "my-arcgis-app",
  "version": "0.0.1",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "lint": "eslint .",
    "format:check": "prettier --check .",
    "format": "prettier --write .",
    "prepare": "simple-git-hooks"
  },
  "devDependencies": {
    "@eslint/js": "^9.39.1",
    "eslint": "^9.39.1",
    "eslint-config-prettier": "^9.1.0",
    "eslint-plugin-import": "^2.29.1",
    "eslint-plugin-unicorn": "^55.0.0",
    "globals": "^16.5.0",
    "prettier": "^3.7.4",
    "lint-staged": "^16.2.7",
    "simple-git-hooks": "^2.13.1",
    "typescript": "~5.9.3",
    "typescript-eslint": "^8.49.0",
    "vite": "^7.2.7"
  },
  "dependencies": {
    "@arcgis/core": "5.0.0",
    "@arcgis/map-components": "^5.0.0",
    "@esri/calcite-components": "^3.3.3"
  },
  "simple-git-hooks": {
    "pre-commit": "pnpm exec lint-staged",
    "pre-push": "pnpm exec tsc --noEmit"
  },
  "lint-staged": {
    "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{js,json,css,md,yml,yaml}": ["prettier --write"]
  }
}
eslint.config.js
import eslint from "@eslint/js";
import configPrettier from "eslint-config-prettier";
import importPlugin from "eslint-plugin-import";
import unicornPlugin from "eslint-plugin-unicorn";
import globals from "globals";
import tseslint from "typescript-eslint";

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  // Enable type-aware linting for TypeScript files
  {
    files: ["**/*.ts", "**/*.tsx"],
    languageOptions: {
      ecmaVersion: "latest",
      sourceType: "module",
      parserOptions: {
        projectService: true,
        allowDefaultProject: ["vite.config.ts"],
      },
    },
  },
  // Scope type-checked recommendations strictly to TS files
  ...tseslint.configs.recommendedTypeChecked.map((cfg) => ({
    ...cfg,
    files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
  })),
  {
    languageOptions: {
      ecmaVersion: "latest",
      sourceType: "module",
      globals: {
        ...globals.browser,
      },
    },
  },
  {
    ignores: ["dist/**", "node_modules/**"],
  },
  {
    ignores: ["vite.config.ts"],
  },
  {
    plugins: {
      import: importPlugin,
      unicorn: unicornPlugin,
    },
    rules: {
      // import hygiene
      "import/no-duplicates": "error",
      "import/no-useless-path-segments": "error",
      "import/no-extraneous-dependencies": [
        "error",
        {
          devDependencies: [
            "**/eslint.config.*",
            "**/vite.config.*",
            "**/*.config.*",
            "**/scripts/**",
            "**/*.test.*",
            "**/*.spec.*",
            ".github/**",
          ],
          optionalDependencies: false,
          peerDependencies: true,
        },
      ],
      "import/order": [
        "warn",
        {
          groups: [
            "builtin",
            "external",
            "internal",
            "parent",
            "sibling",
            "index",
            "object",
            "type",
          ],
          "newlines-between": "always",
          alphabetize: { order: "asc", caseInsensitive: true },
        },
      ],

      // unused vars/imports
      "no-unused-vars": "off",
      "@typescript-eslint/no-unused-vars": [
        "warn",
        {
          argsIgnorePattern: "^_",
          varsIgnorePattern: "^_",
          caughtErrorsIgnorePattern: "^_",
        },
      ],

      // code quality
      eqeqeq: ["error", "always"],
      "no-console": ["warn", { allow: ["warn", "error"] }],
      "@typescript-eslint/no-explicit-any": "warn",

      // unicorn essentials
      "unicorn/prefer-node-protocol": "error",
      "unicorn/prefer-string-starts-ends-with": "error",
      "unicorn/prefer-array-find": "error",
      "unicorn/throw-new-error": "error",

      // typescript consistency
      "@typescript-eslint/consistent-type-imports": "warn",
      "@typescript-eslint/explicit-function-return-type": "off",
      "@typescript-eslint/no-require-imports": "error",
    },
  },
  // Node environment for config and build scripts
  {
    files: ["eslint.config.js", "vite.config.*", "*.config.*", "scripts/**"],
    languageOptions: {
      globals: {
        ...globals.node,
      },
    },
  },
  // Declarations: relax strictness for ambient types
  {
    files: ["**/*.d.ts"],
    rules: {
      "@typescript-eslint/no-redundant-type-constituents": "off",
      "@typescript-eslint/no-unsafe-member-access": "off",
    },
  },
  // TS-only: enable balanced type-aware rules
  {
    files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
    rules: {
      "@typescript-eslint/no-floating-promises": "error",
      "@typescript-eslint/no-misused-promises": [
        "error",
        { checksVoidReturn: false },
      ],
      "@typescript-eslint/await-thenable": "warn",

      "@typescript-eslint/no-unsafe-assignment": "off",
      "@typescript-eslint/no-unsafe-call": "off",
      "@typescript-eslint/no-unsafe-member-access": "off",
      "@typescript-eslint/no-unsafe-return": "off",
      "@typescript-eslint/no-unsafe-argument": "off",

      "@typescript-eslint/require-await": "off",
      "@typescript-eslint/no-unnecessary-type-assertion": "warn",
      "@typescript-eslint/no-redundant-type-constituents": "off",
    },
  },
  // Disable ESLint rules that would conflict with Prettier formatting
  configPrettier,
);
.prettierrc
{
  "arrowParens": "always",
  "bracketSameLine": false,
  "bracketSpacing": true,
  "endOfLine": "lf",
  "jsxSingleQuote": false,
  "printWidth": 120,
  "quoteProps": "consistent",
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "all",
  "useTabs": false,
  "proseWrap": "preserve",
  "htmlWhitespaceSensitivity": "css"
}
.prettierignore
dist/
node_modules/
*.min.js
package-lock.json
pnpm-lock.yaml
.editorconfig
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false
.gitattributes
* text=auto eol=lf
.gitignore
node_modules/
dist/
.env
.env.local
.env.*.local
*.log
.DS_Store
*.tsbuildinfo
.env.example
# ArcGIS API Key
# Get your API key from https://developers.arcgis.com/
VITE_ARCGIS_API_KEY=your_arcgis_api_key_here
.github/workflows/test.yml
name: Test

on:
  pull_request:
    branches:
      - main

permissions:
  contents: read
  issues: write

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 10

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "24"
          cache: "pnpm"

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Security audit
        run: pnpm audit --audit-level=high

      - name: ESLint
        run: pnpm run lint

      - name: Prettier check
        run: pnpm run format:check

      - name: Annotate formatting issues
        if: failure()
        uses: actions/github-script@v7
        with:
          script: |
            const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
            const body = [
              'Prettier check failed. Please run `pnpm run format` locally to fix formatting.',
              '',
              `Workflow run: ${runUrl}`,
            ].join('\n');
            if (context.payload.pull_request) {
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: context.payload.pull_request.number,
                body,
              });
            } else {
              core.summary.addHeading('Prettier check failed');
              core.summary.addRaw(body);
              await core.summary.write();
            }

      - name: Type check
        run: pnpm exec tsc --noEmit

      - name: Build
        run: pnpm run build
        env:
          NODE_OPTIONS: "--max-old-space-size=4096"
          VITE_ARCGIS_API_KEY: ${{ secrets.VITE_ARCGIS_API_KEY }}
.github/workflows/deploy.yml
name: Build & Deploy to GitHub Pages

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 10

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "24"
          cache: "pnpm"

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build
        run: pnpm run build
        env:
          NODE_OPTIONS: "--max-old-space-size=4096"
          VITE_ARCGIS_API_KEY: ${{ secrets.VITE_ARCGIS_API_KEY }}

      - name: Setup Pages
        uses: actions/configure-pages@v4

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: "./dist"

  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4
生产环境 src/main.ts
import "@arcgis/map-components/dist/components/arcgis-scene";
import "@arcgis/map-components/dist/components/arcgis-zoom";
import "@arcgis/map-components/dist/components/arcgis-navigation-toggle";
import "@arcgis/map-components/dist/components/arcgis-compass";

import { setAssetPath as setCalciteAssetPath } from "@esri/calcite-components/dist/components";

import esriConfig from "@arcgis/core/config";

// Set Calcite assets path
setCalciteAssetPath("https://js.arcgis.com/calcite-components/3.3.3/assets");

// Configure ArcGIS API key from environment variable
esriConfig.apiKey = import.meta.env.VITE_ARCGIS_API_KEY as string;

// Wait for map to be ready
const arcgisScene = document.querySelector("arcgis-scene");
arcgisScene?.addEventListener("arcgisViewReadyChange", (event) => {
  const { view } = (event as CustomEvent).detail;
  console.warn("Scene view ready:", view);
});
快速开始(生产环境)
pnpm create vite my-arcgis-app --template vanilla-ts
cd my-arcgis-app
pnpm add @arcgis/map-components @arcgis/core @esri/calcite-components
pnpm add -D eslint @eslint/js typescript-eslint eslint-config-prettier eslint-plugin-import eslint-plugin-unicorn globals prettier simple-git-hooks lint-staged
pnpm run prepare
pnpm dev

相关技能

  • 查看 arcgis-core-maps,了解地图和视图配置。
  • 查看 arcgis-layers,了解如何向地图添加数据图层。
  • 查看 arcgis-widgets-ui,了解如何添加微件和 UI 组件。

参考示例

  • intro-mapview - 创建基础 2D 地图的分步指南
  • intro-map-components - Web 组件入门
  • intro-sceneview - 为应用创建 3D 场景
  • intro-graphics - 向地图添加图形和绘制功能
  • intro-popup - 添加弹出窗口以显示要素信息

常见问题

  1. 未配置 API 密钥:应用引用了不存在或为空的环境变量。

    // Anti-pattern: referencing env var without creating .env file
    import esriConfig from "@arcgis/core/config";
    esriConfig.apiKey = import.meta.env.VITE_ARCGIS_API_KEY as string;
    // .env file is missing or VITE_ARCGIS_API_KEY is empty
    
    // Correct: create .env file with a valid API key
    // .env
    // VITE_ARCGIS_API_KEY=AAPTxy...your_actual_key_here
    
    import esriConfig from "@arcgis/core/config";
    esriConfig.apiKey = import.meta.env.VITE_ARCGIS_API_KEY as string;
    

    **影响:**没有有效的 API 密钥,底图图块和位置服务(地理编码、路径规划)将无法加载。地图会显示为灰色或空白画布,且界面中不会显示任何错误。

  2. tsconfig 中的 moduleResolution 设置错误:使用 "node" 而不是 "node16" 或 "bundler",会导致 TypeScript 无法解析包导出。

    // Anti-pattern: moduleResolution "node" cannot resolve package exports
    {
      "compilerOptions": {
        "moduleResolution": "node"
      }
    }
    
    // Correct: use "node16" or "bundler" for package exports support
    {
      "compilerOptions": {
        "moduleResolution": "bundler"
      }
    }
    

    **影响:**TypeScript 无法解析 @arcgis/map-components 的子路径导出(例如 @arcgis/map-components/dist/components/arcgis-map)。构建时会出现“Cannot find module”错误。

  3. 未配置 Calcite 资源:Calcite 图标和组件要求在运行时设置资源路径。

    // Anti-pattern: importing Calcite components without setting asset path
    import "@esri/calcite-components/dist/components/calcite-shell";
    // No setAssetPath call - icons and internal assets fail to load
    
    // Correct: set the Calcite asset path before using components
    import "@esri/calcite-components/dist/components/calcite-shell";
    import { setAssetPath as setCalciteAssetPath } from "@esri/calcite-components/dist/components";
    
    setCalciteAssetPath("https://js.arcgis.com/calcite-components/3.3.3/assets");
    

    **影响:**Calcite 图标会显示为空白方框,操作按钮不会显示图标,部分组件(例如 calcite-shell)也可能无法正确布局,因为它们无法获取内部资源。

评论

支持 Markdown