> 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).

# 构建自定义

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

### 改哪里

构建分为两个阶段：Unity 生成 WebGL 产物，以及将该产物包裹成 Web 项目并打包。内部实现位于 [构建流水线](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 的树摇和压缩，打包成一个 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、树摇、类型检查都会生效。

```
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 故障或网络阻断影响，并且支持树摇和压缩。

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

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

如果只是想在没有构建工具的情况下快速试用， `USER_HEAD`中 `<script src="...">`可以直接加载。但 CDN 故障时应用会加载失败，而且版本写死在 URL 里，复现性较差，也无法享受树摇和类型检查。不建议在日常使用中这样做。

```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（[Modular 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/ 已经被忽略，则无需单独添加）
> 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.
