# 初始化坑 — 黑屏 / Engine 未就绪 / token 失败

> 文档：`vjcad/doc/guide/quick-start.md`、`vjcad/doc/tutorial/index.md`
> 示例：`vjcad/example/basic/01init.js`、`02onload.js`

## 1. 现象速查

| 现象 | 可能根因 | 跳到 |
|---|---|---|
| 容器黑屏，没报错 | DOM 容器未渲染 / 容器宽高为 0 | §2 |
| 实体没进图且不报错 / `Engine.editor` 为 `undefined` | 在 `cadView.onLoad()` 之前用了 `Engine` | §3 |
| 控制台一直 "等待加载..." | `wasmServiceUrl` 配置错 / 跨域 | §4 |
| HTTP 403（`code: 7`）/ 返回 `{status: 403, errorCode}` | `accessToken` / `auth_callback` 配置错 | §5 |
| `Cannot read properties of null (reading 'getContext')` | 容器 ID 不对 / 同一 ID 多次挂载 | §2 |
| 一刷新就重置 | 状态保存到了 `localStorage` 之外 | §6 |

## 2. 容器问题

```js
// ❌ 错：DOM 还没渲染
const cadView = new MainView({ ... });
initCadContainer('map', cadView);  // 找不到 #map

// ✅ 对：等 DOM 准备好
window.addEventListener('DOMContentLoaded', () => {
    const cadView = new MainView({ ... });
    initCadContainer('map', cadView);
});
```

容器 **必须** 满足：

1. `getElementById('map')` 不为 `null`
2. CSS 上 `width / height` 不为 0（vjcad 不会自适应空容器）
3. **不能同时挂多个 vjcad** 到同一个 ID

```css
#map { width: 100%; height: 100vh; }
```

如果是 SPA / 路由切换：

```jsx
// React 例：用 useRef + useEffect 管理生命周期
const ref = useRef(null);
useEffect(() => {
    const cadView = new MainView({ /* ... */ });
    initCadContainer(ref.current.id, cadView);
    return () => { cadView.destroy(); };  // 切走时一定 destroy（Promise）
}, []);
```

## 3. `Engine` 未就绪

```js
// ❌ 错：onLoad 之前 pcanvas 为 null，addEntities 静默丢弃（不报错）
const cadView = new MainView({ /* ... */ });
initCadContainer('map', cadView);
Engine.addEntities([line]);          // 实体凭空消失

// ✅ 对：等待 onLoad
await cadView.onLoad();
Engine.addEntities([line]);
```

`Engine` 是**静态类**，永远不为 `null`/`undefined`。`MainView` 实例化后到 `onLoad` 完成 **之间**：`Engine.pcanvas` 为 `null`、`Engine.editor` 为 `undefined`、`Engine.currentDoc` 是懒建的孤儿文档，`Engine.addEntities` 静默丢弃不报错。详见 [setup/onload-lifecycle](../setup/onload-lifecycle.md)。

## 4. WASM 加载失败

vjcad 的部分能力（SVG 解析、文字 / 字体光栅化、瓦片生成）走 WASM。不传 `wasmServiceUrl` 时默认 `<服务根>/_webcad/wasm`（服务根 = `serviceUrl` 去掉 `api/…`；无 `serviceUrl` 时为 `./wasm`）。加载的是两个文件 `webcad_core.js` + `webcad_core.wasm`，按 `${wasmServiceUrl}/webcad_core.wasm` 拼接，所以 **`wasmServiceUrl` 末尾不要带 `/`**；自定义部署时：

```js
new MainView({
    serviceUrl: 'https://api.your-domain.com/api/v1',
    wasmServiceUrl: 'https://cdn.your-domain.com/vjcad/wasm',   // 末尾不带 /
});
```

排查：

```js
// 在控制台
fetch('https://cdn.your-domain.com/vjcad/wasm/webcad_core.wasm', { method: 'HEAD' })
    .then(r => console.log('wasm status:', r.status));
fetch('https://cdn.your-domain.com/vjcad/wasm/webcad_core.js', { method: 'HEAD' })
    .then(r => console.log('wasm js status:', r.status));
```

需要 200 + 正确的 MIME (`application/wasm`)；`webcad_core.js` 也是用 `fetch` 拉取的，同样要允许跨域。Nginx 通常要加：

```nginx
types { application/wasm wasm; }
```

跨域时还要：

```nginx
add_header Access-Control-Allow-Origin *;
```

## 5. accessToken / auth_callback

| 表现 | 含义 |
|---|---|
| HTTP 403 + `{code: 7, msg: 'Token is invalid' / 'Token expired' / ..., data: {reload: true}}` | token 缺失 / 失效 / 过期（服务端没有 401） |
| 响应 JSON `{status: 403, error, errorCode}` | `auth_callback` 业务回调拒绝（需按 `status` / `errorCode` 字段判断） |
| HTTP 500 + `{code: 8}` | 后端错误 |

服务端只认请求头 `token`（或 URL 参数 `?token=`），**不认 `Authorization: Bearer`**，也**没有 `/auth/check` 路由**；vjcad 前端发请求时自动带 `token` 头。排查：

```js
// 测试 token 是否可用：随便调一个需鉴权的接口，带 token 请求头
fetch(`${env.serviceUrl}/service/getWorkspace`, {
    headers: { token: env.accessToken }
}).then(r => r.json().then(j => console.log('HTTP', r.status, 'code', j.code, j.msg)));   // 失效：HTTP 403 + code 7
```

如果 token 失效（HTTP 403 + `code: 7`）：

- 后端是 `auth_callback` 模式 → token 必须由你的业务系统签发
- 是临时 token → 一段时间后失效
- 网址写错（多了 `/api/v1` 或者少了）

⚠️ `MainView({ onAuthError })` 在 vjcad@1.0.16 中**从不被调用**（构造时只保存回调，库内没有任何调用点）。需要自行检查 API 返回的 `errorCode` / `status`，必要时手动转发给已配置的回调：

```js
const cadView = new MainView({
    onAuthError: error => {
        // 错误字段是 errorCode（字符串，如 'session_expired' / 'no_permission'），不是 status
        console.log('认证错误:', error.errorCode, error.message);
        if (error.errorCode === 'session_expired') location.href = '/login';
    }
});

// 自己判断返回值后手动触发（库不会替你调）
function checkAuth(res, operation) {
    if (res && res.status === 403 && res.errorCode) {
        cadView.getOnAuthError()?.({ errorCode: res.errorCode, message: res.error ?? '', operation });
        return false;
    }
    return true;
}
```

## 6. 状态丢失 / 一刷新就重置

vjcad 大部分配置是 **运行时**，刷新后丢失。下面这些会被自动持久化：

- `Engine.THEME_MODE`（主题）
- 浮动工具栏位置
- "设置"对话框里改过的系统变量（`SettingsCacheService` 管理的那批，如 `PICKBOX`、`APERTURE`、`GRIPSIZE` 等）

不会自动持久化的：

- `Engine.OSMODE` `Engine.ORTHOMODE` `Engine.GRIDMODE`（setter 只同步状态栏按钮，不写 localStorage）

- 当前打开的图（`mapid` / `version`）
- 当前选中的实体
- 自定义注册的命令 / 插件

要做"刷新后恢复"：

```js
// 启动时（Engine.service.openMap 只是后端 openmap 接口，不会把图加载进编辑器）
const lastMap = localStorage.getItem('cad-last-map');
if (lastMap) {
    const { mapid, version, branch } = JSON.parse(lastMap);
    const r = await new DrawingManagerService().openDrawing({ type: 'imports', mapid, version, branch });
    if (r.success) {
        const file = new File([r.webcadJson], `${mapid}_${version}`, { type: 'application/json' });
        await Engine.view.openDbDoc(file, r.webcadData);
    }
}
// 或直接用 URL 参数 ?vcad_mapid=…&vcad_version=…，onLoad 时自动打开

// 关闭 / 切走时
const origDestroy = cadView.destroy.bind(cadView);
cadView.destroy = async () => {
    localStorage.setItem('cad-last-map', JSON.stringify({ mapid, version, branch }));
    return origDestroy();
};
```

## 7. 多实例 / 多 MainView

vjcad **按单视图设计**：`Engine` 是静态类，文档集合（`Engine.docs` / `activeDocument`）、撤销、命令表、事件管理器都是全局共享的静态状态。

- `Engine.bindViewCache(view)` 只替换缓存的 `view` / `pcanvas`，**不切换文档**；`MainView` 初始化时（`firstUpdated`）自动绑定，所以多个实例时 `Engine` 指向**最后完成初始化**的那个，与焦点无关（`cadView.Engine` 也不存在）。
- 某个实例 `destroy()` 会释放 `Engine.docs` 里的**全部**文档并清空 Engine 缓存，会连带搞坏其他实例。
- 同页对比 / 预览请用 `PreviewView` 或 `<iframe>` 隔离；弹框反复打开 CAD 时一次只保留一个视图：

```js
// 关闭时
await cadView.destroy();          // 卸载插件、释放全部文档与画布、清理 Engine 缓存、移出 DOM
cadView = null;
// 再次打开时重新 new MainView + initCadContainer + await onLoad()
```

MCP 参考：`vjcad/example/basic/07destroyviewer.js`。

- `CommandRegistry` 是 **全局** 的；注册一次所有视图共用
- `CadEventManager.getInstance()` 也是全局的；事件不区分实例

## 红旗

- 永远 `await cadView.onLoad()` 后再用 `Engine`
- 容器宽高为 0 时不会报错，只是黑屏，**先开浏览器审查元素**
- WASM 资源 404 / MIME 错误 → 黑屏 + 控制台报 wasm streaming compile failed
- 同一 DOM 节点不要 `initCadContainer` 两次；要换实例先 `await cadView.destroy()`
- 不要在同一页面同时跑多个 `MainView`：文档/撤销/命令/事件全局共享，`bindViewCache` 不切文档，A 写、B 改会"串了"
- token 失效（HTTP 403 + `code: 7`）时不要立即重定向到登录页 —— 先判断当前路由 / token 状态；`onAuthError` 不会被库自动调用

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/guide/quick-start.md`、`vjcad/doc/tutorial/index.md`
- 示例：`vjcad/example/basic/01init.js`、`02onload.js`
