ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

微信小程序 three.js 3D 展示:GLB 加载、手势交互与性能优化

微信小程序 three.js 3D 展示:GLB 加载、手势交互与性能优化

简介:面向微信小程序开发者与三维展示初学者,这份源码示例聚焦在轻量小程序环境中接入three.js并完成3D模型展示与基础动画控制,适合需要快速验证渲染方案、排查兼容问题或搭建课堂演示的读者参考。压缩包共18个文件,整体约258KB,以8个js逻辑脚本、5个json配置、2个wxml结构文件、2个wxss样式文件及1个gltf模型为主,分别承担交互逻辑、页面配置、视图结构、样式描述与模型资源等职责,目录按pages、components、utils、3Dmodels等模块拆分,层次清楚,便于按功能定位和二次调整。已有2307人学习下载,在入门实践场景中具备一定参考热度。源码保留了可自定义的动画入口,读者可据此实现旋转、掉落等效果,并借助现成虾模型与组件化组织减少模型接入、渲染调试中的常见坑点,整体上手门槛较低。

1. 用户点开详情页,只想把东西转一圈看看

微信小程序 3D 展示这件事,需求方第一次提的时候往往说得特别朴素:用户点进详情页,能把这个产品的正面、侧面、背面转着看一遍,最好再捏合放大看看接口和纹理细节。听上去就是加载个模型加点手势,真动手才发现难点根本不在 three.js 的 API 上,而在运行环境的落差——小程序里没有 DOM,没有document.getElementById,canvas 必须用选择器异步拿节点,触摸事件也不是浏览器的 PointerEvent,模型从打包到上屏的每一步都得重新安排。

这套方案要解决的就是把这个落差填平:一份 GLB 模型进到小程序后,能稳定加载、稳定出图、手势跟手、退出不泄漏内存,并且在低端安卓机上不掉到个位数帧率。适合两类人:正在做 3D 展示类毕设或课程设计、需要一套能跑通也能写进文档的完整结构;以及手上已经有一个小程序,想加一个 3D 看物模块、但不想推倒重来的开发者。下面按选型、架构、实现、避坑、验收的顺序走一遍。

2. 技术选型与运行环境:三条路线的边界和适配层怎么落

动手写第一行代码之前,值得先花半小时把路线定死。选错了,后面所有代码都是在给错误的地基打补丁。这一章把可选路线、适配层的本质、工程初始化三件事讲清楚,后面的架构才有依据。

2.1 原生 WebGL、内置 3D 能力与 three.js 的取舍

摆在小程序面前的其实只有三条路。第一条是手写 WebGL:调gl.drawElements、自己写矩阵变换和着色器,好处是包体最小、没有中间层,坏处是光照、材质、模型解析全得自己实现,一个 GLTF 解析器就能吃掉两周,毕设周期根本不够。

第二条是用小程序生态里自带的 3D 能力(比如某些版本提供的内置 3D 组件)。上手快,但组件封装程度高,能调的参数有限,模型格式支持窄,遇到"这个模型就是加载不出来"的时候几乎没有排查手段,只能换模型。做演示够用,做交付方案不够。

第三条是 three.js 加一层作用域适配。生态最成熟,GLTF/GLB 是官方主推格式,光照、材质、动画混合器、射线拾取全都现成,网上能查到的案例最多,出问题也最容易搜到同类现象。代价是要多引一个适配包,包体大一点,某些依赖 DOM 的扩展模块(比如后处理 EffectComposer)需要改造后才能用。

对比项手写 WebGL内置 3D 组件three.js + 适配层
起步成本极高低中
模型格式支持自己实现窄GLTF/GLB/OBJ 等
可调参数全部有限基本全部
出问题可排查性高(自己写的)低高(社区案例多)
包体增量最小小约 500KB~1MB
适合场景单一特效简单展示完整 3D 展示方案

对绝大多数毕设和课程设计来说,第三条是性价比最高的。方案里我一般直接按 three.js 这条路线写,因为它在"能交作业"和"能上线"之间不需要重写。

2.2 适配层的本质:为什么要一个包装过的 THREE

three.js 内部大量假设自己跑在浏览器里:创建纹理时会碰document.createElement('canvas'),加载资源时会碰XMLHttpRequest,判断环境时会读window。小程序里这些全都没有,直接import * as THREE from 'three'在真机上第一时间就报window is not defined。

适配包做的事说穿了很朴素:对外暴露一个工厂函数,你把手上的 canvas 节点传进去,它在这个作用域内伪造出最小可用的window、document、canvas,再返回一个绑定好的 THREE 对象。之后所有new THREE.Scene()、new THREE.WebGLRenderer()用的都是这个作用域里的版本,而不是全局的那份。

# 在项目根目录初始化并安装适配包 npm init -y npm install threejs-miniprogram --save # 安装完成后必须在开发者工具里执行「工具 - 构建 npm」 # 否则 miniprogram_npm 目录不会生成,require 会直接报模块找不到

构建这一步是新手上手时最容易卡住的地方:装完包不改project.config.json里的packNpmManually或没点构建,代码里require一定失败,报错信息还不指向真正原因。我习惯在 README 里把"装完必须构建"写成第一步,省得后面反复解释。

2.3 工程初始化:目录结构、页面节点与基础库版本

目录按职责切三块:pages/放页面,models/放体积小的本地模型,utils/放渲染器封装和手势处理。模型如果超过 1MB,别塞主包,走分包或者远端域名。

页面节点只需要一个 canvas,但type必须显式写成webgl,写成默认的 2d 会拿到一个 2D 上下文,后面创建渲染器时直接失败:

<!-- pages/viewer/viewer.wxml --> <view class="stage"> <!-- type 必须是 webgl,且要绑定触摸事件 --> <canvas type="webgl" id="glcanvas" class="gl-canvas" bindtouchstart="onTouchStart" bindtouchmove="onTouchMove" bindtouchend="onTouchEnd" bindtouchcancel="onTouchEnd" ></canvas> <!-- 加载进度条覆盖在 canvas 上层,模型就绪后隐藏 --> <view class="loading" wx:if="{{loading}}"> <text>模型加载 {{progress}}%</text> </view> </view>

页面 JSON 里"disableScroll": true建议打开,否则手指在 canvas 上上下滑动时页面会跟着滚,手势和页面滚动打架,体验非常糟。基础库版本建议不低于 2.9.0,canvas的node()取节点能力在这个版本之后才稳定。这些参数不是玄学,是踩过一次真机白屏之后才记住的。

3. 总体架构设计:三层结构与模型从硬盘到屏幕的链路

一套 3D 展示方案能不能维护,取决于有没有把"页面管什么、渲染管什么、资源管什么"切干净。切不干净的典型症状是:所有代码堆在页面的onReady里,两百行下去,想加个新模型就得复制一份,最后没人敢动。

3.1 分层:页面层、渲染层、资源层的职责边界

我一般切成三层。

页面层只负责生命周期和 UI 状态:拿到 canvas 节点、显示加载进度、处理页面onHide/onUnload、把触摸事件原样转发出去。它不写任何 three.js 代码,这样换框架、换页面结构时渲染层可以整体搬走。

渲染层是一个类,对外只暴露init(canvas, width, height)、loadModel(url)、rotate(dx, dy)、zoom(scale)、dispose()这几个方法。内部持有 scene、camera、renderer、mixer,负责渲染循环和资源回收。外部永远不直接碰scene,避免页面代码到处scene.add()导致回收时漏对象。

资源层负责模型文件的组织:命名规范、压缩参数、存放位置。这一层看起来最不像"代码",但它决定了首屏时间和内存占用,往往比渲染层更影响最终体验。

层持有的对象对外方法不该做的事
页面层canvas 节点、UI 状态生命周期钩子直接操作 scene
渲染层scene / camera / renderer / mixerinit、loadModel、dispose读写页面 data
资源层glb 文件、贴图无(构建期完成)运行时动态拼路径

3.2 资源链路:GLB 制作规范、压缩与加载策略

模型从建模软件导出到最终上屏,中间至少要过三道关。

第一道是格式。统一用.glb单文件,不要用.gltf+ 外部.bin+ 一堆贴图。小程序里加载外部贴图要额外发请求,路径稍有偏差就是白模,而且每个文件都要过域名校验。GLB 把几何、材质、贴图全打进一个二进制文件,一次请求搞定,出问题的概率低一个数量级。

第二道是减面。移动端 GPU 对三角形数量很敏感,把 30 万面的模型直接丢进小程序,中低端安卓机基本就是幻灯片。通常的做法是在建模软件里减到 3 万面以内,保留轮廓,法线贴图补细节。纹理尺寸控制在 1024×1024 以内,超过就是浪费显存。

第三道是压缩。GLB 体积超过 2MB 时考虑用 Draco 或者 meshopt 压缩几何数据。Draco 需要额外的解码器文件,在小程序里要手动把解码脚本放进项目并配置路径,麻烦但有效;meshopt 解码器更小,集成相对简单。这属于优化项,不是必选项,先跑通再压。

存放位置按体积分:小于 1MB 的放主包,1~4MB 的放分包,再大的放已配置好的合法域名下,通过wx.downloadFile拿到临时路径再喂给加载器。注意域名必须在后台配好,否则真机上一定失败,而开发者工具里勾了"不校验合法域名"是能跑通的——这就是最经典的"工具里好好的,真机全白"。

3.3 手势交互层:把触摸事件翻译成相机轨道参数

three.js 官方的 OrbitControls 是绑在 DOM 元素上的,监听pointerdown、wheel这些浏览器事件,小程序里直接引进来不会有任何反应。常见的做法有两个:改造 OrbitControls 的事件绑定部分,或者干脆自己写一套轻量手势。

自己写的版本反而更好控制,核心就是记录上一次触摸点和当前触摸点,算位移和距离差:

// utils/gesture.js —— 极简手势解析,输出旋转量和缩放量 export default class Gesture { constructor() { this.lastX = 0; this.lastY = 0; this.lastDist = 0; this.mode = 'none'; // none | rotate | zoom } // 双指距离,用来判断捏合缩放 static distance(t1, t2) { const dx = t1.x - t2.x; const dy = t1.y - t2.y; return Math.sqrt(dx * dx + dy * dy); } start(e) { const t = e.touches; if (t.length === 1) { this.mode = 'rotate'; this.lastX = t[0].x; this.lastY = t[0].y; } else if (t.length === 2) { this.mode = 'zoom'; this.lastDist = Gesture.distance(t[0], t[1]); } } move(e) { const t = e.touches; if (this.mode === 'rotate' && t.length === 1) { // 位移量除以画布宽度做归一化,保证不同机型手感一致 const dx = (t[0].x - this.lastX) / 300; const dy = (t[0].y - this.lastY) / 300; this.lastX = t[0].x; this.lastY = t[0].y; return { type: 'rotate', dx, dy }; } if (this.mode === 'zoom' && t.length === 2) { const dist = Gesture.distance(t[0], t[1]); const delta = dist - this.lastDist; this.lastDist = dist; return { type: 'zoom', delta: delta / 200 }; } return null; } end() { this.mode = 'none'; } }

这里有两个参数值得说明。除以 300 是把像素位移归一化成弧度当量,值越大转得越慢,调试时按模型复杂度调;缩放那边除以 200 同理,控制捏合灵敏度。归一化的意义在于不同屏幕宽度下手感一致,不做这一步,小屏上轻轻一划就转半圈,大屏上怎么划都纹丝不动。

渲染层拿到{type:'rotate', dx, dy}之后,用球坐标更新相机位置:

// 球坐标绕目标点旋转,避免直接用欧拉角导致万向锁 rotate(dx, dy) { this.theta -= dx; // 水平角 this.phi -= dy; // 垂直角 // 限制俯仰角,防止相机翻到模型底下或顶上 const limit = Math.PI / 2 - 0.05; this.phi = Math.max(-limit, Math.min(limit, this.phi)); const r = this.radius; this.camera.position.x = r * Math.cos(this.phi) * Math.sin(this.theta); this.camera.position.y = r * Math.sin(this.phi); this.camera.position.z = r * Math.cos(this.phi) * Math.cos(this.theta); this.camera.lookAt(0, 0, 0); }

用球坐标而不是直接改rotation,是为了避免俯仰角到 ±90° 时画面翻转。这个坑很隐蔽:不加限制时,用户手指往上滑到底,模型会突然上下颠倒,看着像渲染错误,其实是相机穿过了极点。

4. 核心模块实现:场景装配、模型加载与渲染循环

架构定完,落到能跑的代码。这一章三个模块是强耦合的:场景装配不对,模型加载出来是黑的;加载链路没兜底,弱网就是永久转圈;渲染循环没收尾,来回切三次页面就闪退。

4.1 场景装配:相机、光照与画布尺寸对齐

先拿节点。canvas 必须在页面渲染完成后取,onLoad里取一定是 null,onReady才是正确时机:

// 页面层:拿节点、算尺寸、交给渲染层 onReady() { wx.createSelectorQuery() .select('#glcanvas') .node() .exec((res) => { const canvas = res[0] && res[0].node; if (!canvas) { // 拿不到节点时给出可见提示,不要静默失败 this.setData({ loading: false, error: '画布初始化失败' }); return; } // 关键:物理像素尺寸 = 逻辑尺寸 × 像素比 const info = wx.getSystemInfoSync(); const dpr = Math.min(info.pixelRatio, 2); // 上限 2,高分屏不追 3 canvas.width = info.windowWidth * dpr; canvas.height = info.windowHeight * dpr; this.viewer = new Viewer3D(); this.viewer.init(canvas, canvas.width, canvas.height); this.viewer.loadModel(this.data.modelUrl, (p) => { this.setData({ progress: p }); }); }); }

canvas.width/height设的是绘制缓冲尺寸,跟 CSS 尺寸是两回事。只设 CSS 不设这个,画面会糊成一团;设了但没乘 dpr,高分屏上就是马赛克。dpr 卡在 2 是刻意的:3 倍屏渲染像素量是 2 倍的 2.25 倍,肉眼几乎看不出差别,帧率却掉一截。

场景本身反而简单:

init(canvas, width, height) { this.THREE = createScopedThreejs(canvas); // 绑定作用域的 THREE const THREE = this.THREE; this.scene = new THREE.Scene(); // 背景色跟页面底色保持一致,避免出现突兀的色块边界 this.scene.background = new THREE.Color(0xf5f6f8); this.camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000); this.camera.position.set(0, 0, 5); const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); renderer.setSize(width, height, false); // false 表示不改写 CSS 尺寸 renderer.setPixelRatio(1); // 像素比已在 canvas.width 里体现 this.renderer = renderer; // 环境光打底 + 主方向光造型,纯环境光会让模型像纸片 this.scene.add(new THREE.AmbientLight(0xffffff, 0.75)); const key = new THREE.DirectionalLight(0xffffff, 1.2); key.position.set(3, 5, 4); this.scene.add(key); // 补一盏反向弱光,避免背面死黑 const fill = new THREE.DirectionalLight(0xffffff, 0.35); fill.position.set(-3, -2, -4); this.scene.add(fill); }

setSize第三个参数传false很重要。传true时 three.js 会去改 canvas 的style.width,小程序里这个操作没意义还可能触发重排。光照配比也可以调,环境光 0.75 加主光 1.2 是我在多数产品模型上试出来的中性值,金属材质多的模型要把环境光压低、补一盏半球光。

4.2 模型加载:进度、超时与降级兜底

GLTFLoader 在小程序里不能直接 import,需要从 three 的 examples 里取源码,把内部的 THREE 引用替换成作用域返回的实例,再挂到THREE.GLTFLoader上。做完这一步,加载逻辑本身和浏览器里差不多,区别在于必须加超时和失败兜底:

loadModel(url, onProgress) { const THREE = this.THREE; const loader = new THREE.GLTFLoader(); // 15 秒还没加载完就判定失败,避免用户对着转圈干等 const timer = setTimeout(() => { if (!this.loaded) this.onError && this.onError('timeout'); }, 15000); loader.load( url, (gltf) => { clearTimeout(timer); this.loaded = true; const model = gltf.scene; // 自动居中并缩放,保证任意尺寸的模型都能完整入镜 const box = new THREE.Box3().setFromObject(model); const size = box.getSize(new THREE.Vector3()); const center = box.getCenter(new THREE.Vector3()); const maxDim = Math.max(size.x, size.y, size.z); const scale = 2 / maxDim; model.scale.setScalar(scale); model.position.sub(center.multiplyScalar(scale)); this.scene.add(model); this.model = model; // 模型自带骨骼动画时启动混合器 if (gltf.animations && gltf.animations.length) { this.mixer = new THREE.AnimationMixer(model); this.mixer.clipAction(gltf.animations[0]).play(); } onProgress && onProgress(100); this.startLoop(); }, (xhr) => { // 部分环境拿不到 total,此时退化成不确定进度 if (xhr.total) { onProgress && onProgress(Math.floor((xhr.loaded / xhr.total) * 100)); } }, () => { clearTimeout(timer); this.onError && this.onError('load-failed'); } ); }

自动居中缩放这几行是方案里最值钱的部分之一。建模同学导出的模型,原点可能在世界坐标的任意角落,尺寸可能是 0.01 米也可能是 100 米。不做这层归一化,每换一个模型就要手动调一次相机位置,方案就失去了复用性。用包围盒算出最大边长,统一缩放到 2 个单位,再把包围盒中心平移到原点,相机固定在 z=5 就能保证模型完整入镜。

超时那条也别省。弱网环境下加载器可能永远不回调,用户看到的就是无限转圈,最后归因为"小程序卡死了"。给个 15 秒的兜底,超时后展示静态图 + 重试按钮,体验上是降级,但不是死局。

4.3 渲染循环与生命周期:帧率控制、页面隐藏与资源释放

渲染循环要解决三个问题:用谁的时间戳、什么时候停、停了之后怎么回收。

startLoop() { let last = Date.now(); const loop = () => { const now = Date.now(); const delta = (now - last) / 1000; last = now; // 动画混合器按真实时间步进,不受帧率波动影响 if (this.mixer) this.mixer.update(delta); // 相机始终看向原点,配合手势层的球坐标更新 this.camera.lookAt(0, 0, 0); this.renderer.render(this.scene, this.camera); this.rafId = this.canvas.requestAnimationFrame(loop); }; this.rafId = this.canvas.requestAnimationFrame(loop); } stopLoop() { if (this.rafId) { this.canvas.cancelAnimationFrame(this.rafId); this.rafId = null; } }

requestAnimationFrame必须挂在 canvas 上而不是全局,小程序里全局没有这个方法。delta用真实时间差而不是固定 1/60,能让动画速度在不同帧率的机型上保持一致,否则高端机动画飞快、低端机慢动作。

页面生命周期要接死:

// 页面隐藏时停掉循环,省电也省 GPU onHide() { this.viewer && this.viewer.stopLoop(); }, onShow() { this.viewer && this.viewer.startLoop(); }, // 卸载时逐个释放,缺一步就是内存泄漏 onUnload() { if (!this.viewer) return; this.viewer.stopLoop(); this.viewer.scene.traverse((obj) => { if (obj.geometry) obj.geometry.dispose(); if (obj.material) { const mats = Array.isArray(obj.material) ? obj.material : [obj.material]; mats.forEach((m) => { // 贴图是最容易漏的,必须单独释放 Object.keys(m).forEach((k) => { if (m[k] && m[k].isTexture) m[k].dispose(); }); m.dispose(); }); } }); this.viewer.renderer.dispose(); this.viewer = null; }

traverse里遍历材质属性释放贴图这段,是血泪经验。只调material.dispose()的话,贴图占用的显存不会立刻归还,来回进出页面十几次,小程序的 WebGL 上下文就会因为显存超限被强制回收,表现是画面突然全白然后报一堆上下文丢失的错误。renderer.dispose()之后再置空引用,让 GC 能真正收走。

5. 避坑与常见问题排查:黑屏、白模、卡顿、闪退的定位路径

下面五条是按出现频率排的,基本覆盖了从开发到真机测试会遇到的绝大多数问题。每条都按现象、原因、解决三段写,方便直接对照排查。

5.1 黑屏但没有任何报错

现象:页面打开后 canvas 区域一片纯色或者全黑,控制台干净得可疑。

原因:八成是 canvas 节点没拿到。onLoad里取节点、或者 canvas 被wx:if控制初始不渲染,node()返回的都是 null。还有一种隐蔽情况是容器高度为 0,canvas 拿到了但尺寸是 0×0,渲染器照常工作,就是什么都看不见。

解决:把取节点挪到onReady,canvas 用hidden而不是wx:if控制显隐;取到节点后立刻打印canvas.width,为 0 就是样式问题,去查容器有没有写死高度。

5.2 模型加载成功但通体发白或发黑

现象:能看出轮廓,但材质完全没有质感,像塑料纸片,或者干脆黑成一坨。

原因:两类。一类是光照缺失——只加了环境光,PBR 材质没有方向光就没有高光,看着就是纸片。另一类是颜色空间不对,贴图按线性空间解读,整体会偏暗或偏灰。

解决:按 4.1 的光照配比补齐主光加补光;贴图加载后显式设置颜色空间(不同版本 three.js 里是texture.colorSpace或texture.encoding,按你锁定的版本写)。拿不准时先把材质临时换成MeshBasicMaterial,能正常显示说明是光照问题,不能就是贴图没加载上。

5.3 手指滑动没反应或者转得乱七八糟

现象:手势完全无响应,或者模型转的方向跟手指相反,或者一划就飞出去。

原因:直接引了官方 OrbitControls,它监听的是 DOM 事件,在小程序里根本收不到。转得乱则是坐标系没对齐,触摸事件的x/y是相对 canvas 的逻辑坐标,跟相机旋转轴的符号方向不一定一致。

解决:换成自写手势层(3.3 的做法);方向反了就把dx、dy前面的符号取反;一划飞出去是归一化分母太小,把除数调大。调的时候拿个带明显正反面的模型测,比如一个带 logo 的盒子,正反面一眼就能看出来。

5.4 来回进出页面几次后卡顿甚至闪退

现象:第一次打开很流畅,反复切换 tab 或者进出详情页之后,帧率一降再降,最后白屏。

原因:requestAnimationFrame没取消,多个循环叠着跑;几何体和纹理没释放,显存持续增长;页面onUnload没写回收逻辑。

解决:onHide停循环、onShow重启,onUnload里按 4.3 的流程逐个 dispose。自测方法很简单:连续进出页面 20 次,看帧率和内存曲线是否平稳,不平稳就说明漏了。

5.5 开发者工具正常,真机加载失败

现象:工具里模型好好的,扫码到手机上一直转圈或者直接报错。

原因:最常见的是域名没配。工具里可以勾选跳过域名校验,真机不行。其次是模型文件太大,超过单次请求限制;还有一种是 GLB 里引用了外部贴图路径,工具里恰好能读到,真机上路径无效。

解决:把下载域名加进合法域名列表;大文件走分包或者拆分;导出时确认勾选"嵌入贴图",保证是真正的单文件 GLB。真机调试建议开 vConsole,直接看网络面板的失败原因,比猜快得多。

6. 验收与答辩:用一张指标表和一段演示脚本证明方案达标

代码跑通只是及格线,能不能交付要看有没有可量化的验收标准。答辩现场最容易被问倒的不是"你用了什么技术",而是"你怎么证明它好用"。提前准备一张指标表,把主观感受变成可测的数字,说服力完全不一样。

验收项合格标准测量方法
首屏可见时间4G 网络下 ≤ 3 秒真机录屏逐帧数,从点击进入到模型完整出现
交互帧率中端安卓机稳定 ≥ 30fps开发者工具性能面板 + 真机帧率悬浮窗
手势响应延迟拖动后 ≤ 100ms 画面跟手录屏慢放,对比手指位移与画面变化的时间差
内存稳定性连续进出页面 20 次无增长趋势真机内存面板记录每次退出后的占用
弱网可用性3G 模拟下超时后有降级提示与重试开发者工具切换网络为 3G,观察兜底逻辑
模型兼容性3 个不同来源的 GLB 均能正确居中显示换模型不改代码,直接替换文件测试

测帧率有个小技巧:别只看平均值。手指静止时帧率虚高,要测的是"持续旋转过程中的最低帧率",那个数字才是用户真实感受。我一般让模型匀速自转 30 秒,记录期间的最低值。

演示脚本也提前写好,按四步走,控制在三分钟内:第一步,正常打开,展示模型完整入镜和自动居中;第二步,单指旋转到背面,展示双面材质和光照过渡;第三步,双指放大到接口细节,证明纹理清晰度够用;第四步,故意断网重进,展示超时兜底和重试按钮。第四步是加分项,绝大多数演示只演顺利路径,把异常路径也演出来,说明你想过边界情况。

答辩提问高频集中在三个方向:为什么不用原生方案(用 2.1 的对比表回答)、模型太大怎么办(用 3.2 的三道关回答)、多模型怎么管理(用 3.1 的分层回答)。这三个问题提前把答案压缩成三句话,现场就不会卡壳。

从那以后我每次做这类展示方案,都会在写第一行业务代码之前先把验收指标表填完,哪怕只是自己拍脑袋定的数字——没有这张表,做到后面很容易陷入"感觉还行"的自我麻痹,等到真机上被低端机教做人,返工成本比前期定标准高得多。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表