> For the complete documentation index, see [llms.txt](https://developers-apps-in-toss.toss.im/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers-apps-in-toss.toss.im/documentation/api-and-sdk-zh/unity/build/build-customization.md).

# 构建自定义

说明如何修改包裹迷你应用的网页层（HTML、TypeScript、npm 依赖、Vite 配置），使其在 SDK 更新后仍能存活。

### 修改哪里

构建分为 Unity 生成 WebGL 产物，以及用网页项目包裹并打包该产物的阶段。内部动作为 [构建流水线](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-process)在 **用户可编辑位置**仅讨论。

| 阶段             | 输出                               | 编辑点                                                                                                |
| -------------- | -------------------------------- | -------------------------------------------------------------------------------------------------- |
| Unity WebGL 构建 | `webgl/` （中间产物）                  | 不编辑。设置在 [构建配置文件](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-profiles) |
| Granite 打包     | `ait-build/` → `ait-build/dist/` | `Assets/WebGLTemplates/AITTemplate/` 下方——本文档                                                       |

> **注意**: `webgl/`和 `ait-build/`的文件不要直接修改。 `webgl/`是 Unity 每次构建都会新建的中间产物，打包不是基于这个文件夹，而是 `Assets/WebGLTemplates/AITTemplate/`的模板进行。即使修改这两个文件夹，也不会反映到最终包里，并会在下一次构建时消失。

> **参考**：QR 测试和实际发布使用的最终包是 `ait-build/dist/`。直接查看构建结果时请看这个文件夹。

### 用户区域标记

SDK 模板在每次进入构建时都会与最新 SDK 版本合并。这时 **仅保留标记之间的内容**，标记外会用 SDK 值更新。合并何时发生、发生在什么文件中，见 [构建流水线](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-process)的模板合并时机一节。

#### HTML 标记

`index.html`提供两个区域。

```html
<!-- USER_HEAD_START - 请在此区域添加用户自定义脚本/样式 -->
<!-- USER_HEAD_END -->

<!-- USER_BODY_END_START - 请在此区域添加用户自定义脚本 -->
<!-- USER_BODY_END_END -->
```

`USER_HEAD`是 `<head>` 中， `USER_BODY_END`是 `</body>` 会插入在其之前。

#### TypeScript 配置文件标记

`vite.config.ts`, `granite.config.ts`, `apps-in-toss.config.ts`使用相同的标记对。

```typescript
//// SDK_GENERATED_START - 请勿编辑此部分 ////
// 由 SDK 管理的代码。写在这里的内容会在 SDK 更新时消失。
//// SDK_GENERATED_END ////

//// USER_CONFIG_START ////
// 用户自定义代码。SDK 更新时会保留。
//// USER_CONFIG_END ////
```

> **重要**: `USER_CONFIG`在其中重新声明由 SDK 管理的设置（应用名称、品牌、权限、 `webViewProps` 等），合并时 SDK 值会获胜，因此不会生效。构建会正常进行，但会出现以下警告——请把相应键从 `USER_CONFIG`中删除。
>
> ```
> [AIT]   ⚠ apps-in-toss.config.ts 的 USER_CONFIG 中仍包含由 SDK 管理的设置。
> ```

相反 `SDK_GENERATED` 区域中如果残留未替换的占位符，构建会以硬错误中断。此时请通过 Clean Build 重新生成模板。

### 可自定义的文件

**所有文件都在 `Assets/WebGLTemplates/AITTemplate/` 下。**

| 文件                                    | 作用                   | 合并方式                                                              |
| ------------------------------------- | -------------------- | ----------------------------------------------------------------- |
| `index.html`                          | HTML 入口点             | `USER_HEAD` / `USER_BODY_END` 保留标记区域                              |
| `BuildConfig~/package.json`           | npm 依赖               | 合并 dependencies / devDependencies（冲突时优先 SDK）                      |
| `BuildConfig~/vite.config.ts`         | Vite 构建配置            | `USER_CONFIG` 保留标记区域                                              |
| `BuildConfig~/granite.config.ts`      | Granite 打包配置（2.x）    | `USER_CONFIG` 保留标记区域                                              |
| `BuildConfig~/apps-in-toss.config.ts` | Apps in Toss 配置（3.x） | `USER_CONFIG` 保留标记区域。若为空则 `granite.config.ts`的 `USER_CONFIG`自动迁移  |
| `BuildConfig~/tsconfig.json`          | TypeScript 编译器设置     | SDK 必需选项（`moduleResolution`, `esModuleInterop`）强制使用 SDK 值，其余优先项目值 |
| `BuildConfig~/pnpm-workspace.yaml`    | pnpm 工作区配置           | 如果有项目文件则使用它，否则复制 SDK 文件                                           |
| `BuildConfig~/src/`                   | TypeScript 入口点及模块    | 整个文件夹保留（递归复制）                                                     |
| `BuildConfig~/` 其他文件                  | `.env`、静态资源等         | 除下方排除列表外，原样复制所有根文件和子文件夹                                           |

在其他文件复制中排除的内容——根文件 `package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`, `vite.config.ts`, `tsconfig.json`, `unity-bridge.ts`, `granite.config.ts`, `apps-in-toss.config.ts` （各自有专用合并路径）以及文件夹 `node_modules/`, `.npm-cache/`, `dist/`.

> **dependencies 冲突处理**：如果将 SDK 已声明的包（`@apps-in-toss/web-framework`, `@apps-in-toss/web-analytics`, `vite`, `typescript` 等）以其他版本添加，优先采用 SDK 版本。SDK 未声明的包（例如： `firebase`, `canvas-confetti`）则会原样添加。

> **参考**: `pnpm-workspace.yaml`用于 pnpm 的供应链保护（`minimumReleaseAge`）中的 `@apps-in-toss/*`而存在。由于 pnpm 只会在 `pnpm-workspace.yaml`中读取该设置，所以必须复制到构建目录。若无特殊原因，请保持 SDK 默认值。

### index.html 自定义

修改对象是 `Assets/WebGLTemplates/AITTemplate/index.html`。 **必须 `_START`和 `_END` 添加在标记之间**才能被保留。

`USER_HEAD`用于声明静态资源，例如 meta 标签、字体、preload 提示、外部样式表。

```html
<!-- USER_HEAD_START - 请在此区域添加用户自定义脚本/样式 -->
<meta name="theme-color" content="#3182f6">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR&display=swap">
<!-- USER_HEAD_END -->
```

`USER_BODY_END`用于引用用户代码的入口点。推荐模式是将 TypeScript 入口点作为模块加载——入口点中写入的所有 import 都会经过 Vite 的 tree-shaking 和压缩，最终打包成一个 bundle。

```html
<!-- USER_BODY_END_START - 请在此区域添加用户自定义脚本 -->
<script type="module" src="./src/main.ts"></script>
<!-- USER_BODY_END_END -->
```

构建完成后 `ait-build/index.html`可以打开它来确认你写入的代码是否已包含其中。如果 Unity Console 打出以下内容，就说明合并正常工作了。

```
[AIT] index.html USER_HEAD 段已合并
[AIT] index.html USER_BODY_END 段已合并
```

### TypeScript 入口点

用户代码以 `BuildConfig~/src/main.ts`为入口点编写。由于 Vite 会对该文件进行打包，所以 npm 包 import、tree shaking 和类型检查都会生效。

```
Assets/WebGLTemplates/AITTemplate/
├── index.html                    ← 在 USER_BODY_END 中引用 main.ts
└── BuildConfig~/
    ├── package.json              ← 依赖
    ├── tsconfig.json             ← TypeScript 选项（可选）
    └── src/
        └── main.ts               ← 入口点
```

`BuildConfig~/src/main.ts`:

```ts
window.addEventListener('load', () => {
    console.log('User entry loaded');
});
```

`BuildConfig~/tsconfig.json`可以用来定制编译器选项。SDK 必需选项（`moduleResolution`, `esModuleInterop`）会被强制使用 SDK 值。

```json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "paths": {
      "@/*": ["./src/*"]
    },
    "baseUrl": "."
  },
  "include": ["src", "*.ts", "*.tsx"]
}
```

### 添加外部库

推荐安装为 npm 包并在入口点中 import。这样版本固定、构建可复现，不受 CDN 故障或网络阻断影响，并且可应用 tree shaking 和压缩。

流程与库无关，都是相同的—— `package.json`在中添加依赖 → `main.ts`中 import → `index.html`中引用入口点。具体示例请看下面的 **教程** 部分。

#### 另一种方式是直接从 CDN 加载

如果只是想在没有构建工具的情况下快速试用，可以直接通过 `USER_HEAD`中 `<script src="...">`加载。不过 CDN 故障时应用会加载失败，版本写在 URL 里导致可复现性较差，也无法获得 tree shaking 和类型检查。日常使用不推荐。

```html
<!-- USER_HEAD_START -->
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/dist/confetti.browser.min.js"></script>
<!-- USER_HEAD_END -->
```

```html
<!-- USER_BODY_END_START -->
<script>
    window.addEventListener('load', () => {
        confetti({ particleCount: 100, spread: 70, origin: { y: 0.6 } });
    });
</script>
<!-- USER_BODY_END_END -->
```

### Vite 配置自定义

`BuildConfig~/vite.config.ts`的 `USER_CONFIG` 在该部分添加插件或构建选项。

```typescript
//// USER_CONFIG_START ////
const userConfig = defineConfig({
  plugins: [
    // 添加用户插件
  ],
  define: {
    __CUSTOM_FLAG__: JSON.stringify(true),
  },
});
//// USER_CONFIG_END ////
```

`granite.config.ts`和 `apps-in-toss.config.ts`也同样有 `USER_CONFIG` 部分。

### 使用 React 组件

如果要用 React 实现 UI 覆盖层，需要在外部库添加和 TypeScript 入口流程中加入 React 依赖和 Vite 插件。

`BuildConfig~/package.json`:

```json
{
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.0.0"
  }
}
```

`BuildConfig~/tsconfig.json`:

```json
{
  "compilerOptions": {
    "jsx": "react-jsx"
  },
  "include": ["src"]
}
```

`BuildConfig~/vite.config.ts`:

```typescript
//// USER_CONFIG_START ////
import react from '@vitejs/plugin-react';

const userConfig = defineConfig({
  plugins: [react()],
});
//// USER_CONFIG_END ////
```

`BuildConfig~/src/main.tsx`:

```tsx
import React from 'react';
import { createRoot } from 'react-dom/client';

function GameUI() {
  return <div id="game-ui">游戏 UI</div>;
}

const container = document.getElementById('ui-root');
if (container) {
  createRoot(container).render(<GameUI />);
}
```

`index.html`:

```html
<!-- USER_BODY_END_START -->
<script type="module" src="./src/main.tsx"></script>
<!-- USER_BODY_END_END -->
```

### 构建产物结构

打包完成后 `ait-build/`会生成如下结构。

```
ait-build/
├── index.html              ← Unity 占位符替换 + USER_HEAD/USER_BODY_END 合并
├── public/
│   ├── Build/              ← Unity WebGL 构建文件
│   ├── TemplateData/       ← 样式、图片
│   ├── Runtime/            ← 调试控制台等附加脚本
│   └── StreamingAssets/    ← StreamingAssets（如果有）
├── src/                    ← 用户 TypeScript 代码（如果有）
├── .env                    ← 用户环境变量（如果有）
├── package.json            ← SDK + 用户依赖合并（冲突时优先 SDK）
├── vite.config.ts          ← 保留 SDK 最新版本 + USER_CONFIG
├── granite.config.ts       ← 应用元数据占位符替换 + 保留 USER_CONFIG
├── apps-in-toss.config.ts  ← 3.x 配置（仅在 SDK 模板中存在时）
├── tsconfig.json           ← SDK 必需选项 + 用户选项合并
├── pnpm-workspace.yaml     ← 先用项目文件，没有则用 SDK 文件
├── pnpm-lock.yaml          ← 项目 lockfile（完整性验证）或 SDK 回退
└── dist/                   ← 最终发布包（granite build 结果，QR 测试对象）
```

`node_modules`和 `pnpm-lock.yaml`会在重新构建时也保留，从而提高构建速度。

### SDK 更新时的行为

即使更新 SDK，用户自定义也会自动保留。

| 情况      | 行为                        |
| ------- | ------------------------- |
| 带标记的模板  | 保留用户区域，仅更新 SDK 区域         |
| 无标记的旧模板 | 整个文件替换为新的 SDK 模板 + 手动迁移警告 |

无标记的现有 `index.html`会被整体替换，并输出以下警告。请把备份的旧文件中的自定义部分迁移到新模板的标记区域。

```
[AIT] 模板更新：将旧版本模板替换为新的基于标记的模板。
[AIT] ⚠️ 如果现有 index.html 做过自定义修改，请手动重新应用到 USER_* 标记区域。
```

正常合并时会留下如下日志。

```
[AIT] ✓ index.html 模板更新（保留用户自定义区域）
[AIT]   ✓ vite.config.ts（SDK 最新版本 + 保留 USER_CONFIG）
[AIT]   ✓ granite.config.ts（SDK 最新版本 + 保留 USER_CONFIG）
```

### 教程

下面两个教程（#1 canvas-confetti、#2 Firebase Analytics）会由 E2E 测试实际构建并在浏览器中运行验证。代码块与测试期望的形式完全一致，所以最好先原样照做，再在此基础上修改。

#### 使用 canvas-confetti 添加屏幕特效

[canvas-confetti](https://github.com/catdad/canvas-confetti)这是一个最简单的示例：打包后在页面加载时显示彩纸效果。可以一次性了解添加外部库的完整流程。

**1. `BuildConfig~/package.json`在中添加依赖**

```json
{
  "dependencies": {
    "canvas-confetti": "^1.9.3"
  },
  "devDependencies": {
    "@types/canvas-confetti": "^1.6.4"
  }
}
```

**2. `BuildConfig~/src/main.ts` 编写**

```ts
import confetti from 'canvas-confetti';

window.addEventListener('load', () => {
    confetti({ particleCount: 100, spread: 70, origin: { y: 0.6 } });
});
```

**3. `index.html`在中引用入口点**

```html
<!-- USER_BODY_END_START -->
<script type="module" src="./src/main.ts"></script>
<!-- USER_BODY_END_END -->
```

**4. 构建后确认**

运行构建并在浏览器中打开产物后，页面加载后会立刻在屏幕上喷出彩纸。控制台中 `confetti is not defined`如果看到，则请重新检查入口点引用或 `package.json` 依赖添加步骤。

#### 接入 Firebase Analytics

Firebase Web SDK（[模块化 SDK](https://firebase.google.com/docs/web/modular-upgrade)）打包后将应用初始化与 Analytics 连接起来。API 密钥通过 `.env`注入——这样就不用把密钥写死在代码里，避免提交到仓库，并且可以按环境使用不同值。

**1. `BuildConfig~/package.json`在中添加依赖**

```json
{
  "dependencies": {
    "firebase": "^10.7.0"
  }
}
```

**2. `Assets/WebGLTemplates/AITTemplate/BuildConfig~/.env` 编写**

```bash
VITE_FIREBASE_API_KEY=your-api-key
VITE_FIREBASE_PROJECT_ID=your-project-id
VITE_FIREBASE_APP_ID=your-app-id
VITE_FIREBASE_MEASUREMENT_ID=your-measurement-id
```

该文件在构建时 `ait-build/.env`会自动复制到其中，供 Vite 使用。

> Vite 只会把 `VITE_` 前缀的环境变量暴露到客户端 bundle。使用其他前缀则 `import.meta.env`无法通过读取。
>
> **`.gitignore` 设置**: `.env`包含密钥，请将下面两个路径都加入 ignore。团队共享的默认值通常放在 `.env.example`中。
>
> ```gitignore
> # 用户编写的原始文件（Unity 项目）
> Assets/WebGLTemplates/AITTemplate/BuildConfig~/.env
>
> # 构建产物（如果整个 ait-build/ 已经被 ignore，则无需另加）
> ait-build/.env
> ```

**3. `BuildConfig~/src/main.ts` 编写**

```ts
import { initializeApp } from 'firebase/app';
import { getAnalytics } from 'firebase/analytics';

const app = initializeApp({
    apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
    projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
    appId: import.meta.env.VITE_FIREBASE_APP_ID,
    measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID,
});
getAnalytics(app);
```

**4. `index.html`在中引用入口点**

```html
<!-- USER_BODY_END_START -->
<script type="module" src="./src/main.ts"></script>
<!-- USER_BODY_END_END -->
```

**5. 构建后确认**

可以在浏览器开发者工具控制台中确认以下内容。

```js
> getApp().options.projectId
"your-project-id"
```

也可以在 Firebase 控制台的 Analytics > DebugView 中确认实时事件接收（需要启用调试模式—— [官方文档](https://firebase.google.com/docs/analytics/debugview) 参见).

> **要同时应用两个教程**: `package.json`在其中添加这两个依赖项， `main.ts`在其中依次放入两个 import 块即可。入口点只需一个（`src/main.ts`）就足够了。

### 相关文档

* [构建流水线](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-process) — 实际发生合并和替换的位置
* [构建配置文件](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-profiles) — Unity WebGL 构建设置
* [加载画面自定义](https://developers-apps-in-toss.toss.im/documentation/unity/build/loading-screen-customization) — 替换加载画面
* [开始使用](https://developers-apps-in-toss.toss.im/documentation/unity/first-steps/getting-started) — 安装及基本设置
* [问题解决](https://developers-apps-in-toss.toss.im/documentation/unity/first-steps/faq) — 构建受阻时


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://developers-apps-in-toss.toss.im/documentation/api-and-sdk-zh/unity/build/build-customization.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
