5.2 装卸工序:加载器与异步装配 本节摘要:加载器的本职是把异步运输编排成可靠工序:进度上报让用户等得起,失败兜底让产线不停车,进场整理让模型即插即用。本节承接格式验收,走完从发起加载到动画片段转交的全流程,把 LoadingManager 升级为全站统一的进场看板。 异步是常态,编排是本事 第 3 章已经埋过一次伏笔:贴图加载是异步的。模型加载只会更甚——一个 GLB 动辄数 MB,网络上走几百毫秒到几秒,期间主线程照常渲染。异步工序的全部难点在于"编排":多个模型并发加载时进度怎么汇总、其中一个失败时整单怎么办、加载完成后的整理代码写在哪个回调里。散着写回调是新手写法,回调套回调、错误无处接、进度各报各的;
本节摘要:加载器的本职是把异步运输编排成可靠工序:进度上报让用户等得起,失败兜底让产线不停车,进场整理让模型即插即用。本节承接格式验收,走完从发起加载到动画片段转交的全流程,把 LoadingManager 升级为全站统一的进场看板。
第 3 章已经埋过一次伏笔:贴图加载是异步的。模型加载只会更甚——一个 GLB 动辄数 MB,网络上走几百毫秒到几秒,期间主线程照常渲染。异步工序的全部难点在于"编排":多个模型并发加载时进度怎么汇总、其中一个失败时整单怎么办、加载完成后的整理代码写在哪个回调里。散着写回调是新手写法,回调套回调、错误无处接、进度各报各的;本节的标准答案是把编排交给两套机制——单个加载器的进度回调,以及全局的 LoadingManager 看板。

背景:叉车、货架、传送带三个模型分头加载,要求页面顶部一条总进度、任一失败不阻塞其余两个、全部到齐后统一开场。
操作:LoadingManager 管汇总,单加载器回调管细节,进场整理三件套(轴心、单位、面数)封装成固定函数。
// 全站看板:汇总所有经手的资源 const manager = new THREE.LoadingManager(); manager.onProgress = (url, loaded, total) => { const pct = Math.round((loaded / total) * 100); document.querySelector('#bar').style.width = pct + '%'; }; manager.onLoad = () => startShowcase(); // 全部到齐才开场,避免半成品露脸 manager.onError = (url) => console.warn('进厂失败:', url); const gltfLoader = new GLTFLoader(manager); // 进场整理三件套:轴心落底、单位归一、面数报数 function prepModel(gltf, targetSize = 2) { const model = gltf.scene; // 1. 单位归一:按包围盒实际尺寸等比缩放到目标大小 const box = new THREE.Box3().setFromObject(model); const size = box.getSize(new THREE.Vector3()); const scale = targetSize / Math.max(size.x, size.y, size.z); model.scale.setScalar(scale); // 2. 轴心落底:重算缩放后的包围盒,把底部压到 y=0 box.setFromObject(model); model.position.y -= box.min.y; // 3. 动画片段转交:有就带出,交给动画系统(第 6 章) return { model, clips: gltf.animations }; } function loadForklift(url) { gltfLoader.load(url, (gltf) => { const { model, clips } = prepModel(gltf, 2.5); model.position.set(2, 0, -1); stage.scene.add(model); console.log('叉车进场,动画片段数:', clips.length); }, (evt) => { /* 单件进度:evt.loaded 与 evt.total,看板已汇总,这里可打点 */ }, () => placePlaceholder(2, 0, -1) // onError:失败不停车,放个占位件 ); }
结果:顶部进度条从 0 走到 100,三件模型陆续上架,全部到齐后展示开场;把叉车的地址故意改错,进度照常走完,叉车位置出现一个半透明占位方块,控制台报出失败清单——产线没停。
解读:三个设计决策值得复盘。第一,onLoad 放在 manager 上而不是单个加载器上:开场的条件是"全部到齐",单件的 onLoad 只够做单件的上架。第二,prepModel 里"先缩放、再重算包围盒、再压底"的顺序不能乱——包围盒必须按缩放后的实际尺寸重算,否则轴心落底永远差一截,这是装卸工序的隐性顺序依赖。第三,占位件兜底不是锦上添花:线上环境模型丢失的原因五花八门(缓存失效、路径笔误、跨域限制),占位件让画面保持完整结构,错误进清单而不是进用户眼睛。
变式:把 manager.onProgress 的进度条升级为带条目清单的看板(每件资源一行:进行中、完成、失败),交付评审时客户对"加载了什么、还差什么"一目了然。再进阶:对重复使用的模型做缓存——第一次加载后把 gltf 场景克隆入库,后续同款直接克隆上架,省网络也省解析;克隆时注意材质默认共享,需要独立换色的副本要单独处理材质实例。
线上装卸还要过两道环境关。跨域:模型与贴图由第三方地址供给时,加载会因跨域限制静默失败,控制台只有一条模糊的加载错误;自托管资源目录是正解,实在要跨域引用则要求对方配置允许头——这条写进技术方案,别等上线才发现。缓存:浏览器对重复请求自动缓存,但模型迭代频繁时缓存会变成"更新不生效"的元凶,给资源地址附版本号参数是行业惯例,发布即换版本。重复进场:同一模型在页面多处使用(货架上的同款叉车三台),第一次加载后的场景对象可以克隆复用——但要注意克隆体默认共享材质,需要独立变色(比如选中高亮只亮一台)时,克隆后单独实例化该副本的材质,否则三台叉车会同时变红。这三道关都是"上线才暴露"的类型,验收阶段就在测试环境里用真实部署方式预演一遍,能省下上线夜的救火。
外协件已入库上架。下一章装传动装置:渲染循环的心跳节拍、动画剪辑的混合调度、射线拾取的命中处理——沙盘要从静展台变成能玩的活场景。