初始化坑 — 黑屏 / 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. 容器问题
// ❌ 错:DOM 还没渲染
const cadView = new MainView({ ... });
initCadContainer('map', cadView); // 找不到 #map
// ✅ 对:等 DOM 准备好
window.addEventListener('DOMContentLoaded', () => {
const cadView = new MainView({ ... });
initCadContainer('map', cadView);
});
容器 必须 满足:
getElementById('map')不为null- CSS 上
width / height不为 0(vjcad 不会自适应空容器) - 不能同时挂多个 vjcad 到同一个 ID
#map { width: 100%; height: 100vh; }
如果是 SPA / 路由切换:
// React 例:用 useRef + useEffect 管理生命周期
const ref = useRef(null);
useEffect(() => {
const cadView = new MainView({ /* ... */ });
initCadContainer(ref.current.id, cadView);
return () => { cadView.destroy(); }; // 切走时一定 destroy(Promise)
}, []);
3. Engine 未就绪
// ❌ 错: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。
4. WASM 加载失败
vjcad 的部分能力(SVG 解析、文字 / 字体光栅化、瓦片生成)走 WASM。不传 wasmServiceUrl 时默认 <服务根>/_webcad/wasm(服务根 = serviceUrl 去掉 api/…;无 serviceUrl 时为 ./wasm)。加载的是两个文件 webcad_core.js + webcad_core.wasm,按 ${wasmServiceUrl}/webcad_core.wasm 拼接,所以 wasmServiceUrl 末尾不要带 /;自定义部署时:
new MainView({
serviceUrl: 'https://api.your-domain.com/api/v1',
wasmServiceUrl: 'https://cdn.your-domain.com/vjcad/wasm', // 末尾不带 /
});
排查:
// 在控制台
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 通常要加:
types { application/wasm wasm; }
跨域时还要:
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 头。排查:
// 测试 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,必要时手动转发给已配置的回调:
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.OSMODEEngine.ORTHOMODEEngine.GRIDMODE(setter 只同步状态栏按钮,不写 localStorage) -
当前打开的图(
mapid/version) -
当前选中的实体
-
自定义注册的命令 / 插件
要做"刷新后恢复":
// 启动时(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 时一次只保留一个视图:
// 关闭时
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