
做过在线CAD项目的人大概都有过这样的经历产品经理丢过来一个文件夹里面躺着几十张DWG图纸然后问一句“能不能在网页上直接打开就像看PDF那样能缩放能看就行”。这个需求听起来平平无奇但真动手做的时候你会发现DWG不是图片也不是SVG它是AutoCAD的私有二进制格式从R2000到2018有六七个版本还有压缩段、对象句柄、块引用嵌套。想在H5前端把它显示出来本质上是在浏览器里重建一个小型的CAD渲染引擎。这类需求现在越来越常见——厂里的设备台账要挂图纸、工程协同平台要在线看图、审图系统要做批注、移动端现场验收要拿手机翻图。不管你是前端开发、后端开发还是全栈只要你碰到“网页CAD”“在线CAD平台”“H5前端显示CAD”这类活儿接下来的内容应该能帮你少走至少两周弯路。1. 网页打开DWG真正的难点在哪1.1 DWG不是图片先认清它的本质很多人第一反应是“DWG不就是一个文件吗找个库转成图片不就行了”。这个思路在前端显示CAD这件事上只能解决“看一眼”的需求一旦涉及图层开关、缩放清晰度、坐标量测立刻就崩了。从数据结构上讲DWG是一套面向对象的二进制数据库。文件头之后是一个个“段”Section里面有类定义、对象映射表实体之间靠句柄handle互相引用。比如一条直线实体它并不直接写“我是红色虚线在墙体层”而是分别引用图层对象、线型对象、颜色号甚至还有扩展数据。这种设计让文件很紧凑但也意味着你没法像读JSON那样“扫一遍就完事”必须先把对象表建起来再按引用关系还原语义。版本差异更是绕不开的坎。常见的文件头标识有这几个显示版本文件头标识文本编码压缩方式R2000AC1015代码页GBK/ANSI无自定义压缩R2004AC1018代码页自定义压缩R2007AC1021UTF-8 / Unicode自定义压缩R2010AC1024UTF-8自定义压缩R2013AC1027UTF-8自定义压缩R2018AC1032UTF-8自定义压缩这张表里最要命的是“文本编码”那一列。2007之前的图纸中文字符是按代码页存的如果你的解析器不问青红皂白按UTF-8解码图纸里的“一层平面图”就会变成一堆方块或者乱码。我在一个老厂区的项目里就吃过这个亏对方的图纸是2004年归档的打开全是问号排查了半天才发现是编码问题。提示拿到图纸先读文件头前6个字节判断版本再决定用什么解码策略这一步千万不要省。1.2 四条技术路线到底怎么选市面上能走的路其实就四条各有各的适用边界我把它们放在一张表里对比路线实现方式优点代价适合场景服务端转换 前端自绘后端把DWG转成DXF或自定义JSON前端用Canvas/WebGL画浏览器压力小、可控性强、能做图层和量测需要服务端资源、转换有延迟企业级在线CAD平台、审图系统前端直接解析DXFdxf-parser之类的库 three.js不依赖服务端、隐私好、部署简单只支持DXF大文件会卡死小工具、内部单机版、桌面套壳通用格式转换后预览转成SVG、PDF、PNG再显示一天就能出demo丢图层、丢精度、不能交互只做归档预览、附件缩略图商业云渲染SDK平台接口 官方三维查看器开箱即用、三维效果好付费、图纸要外发到第三方预算充足、要快速上线如果只是给用户“看一眼图纸长啥样”第四条最省事。但只要牵扯到图层、量测、批注、图档管理第一和第二条才是正路。我个人的选择顺序是先把服务端转换链路搭起来前端永远只处理结构化的中间数据。这样做的好处是哪天要换成WebGL渲染、要加三维、要做图纸比对前端的数据层完全不用动。1.3 为什么不该在前端硬啃DWG二进制我见过不少人一上来就想在浏览器里直接解析DWG理由听起来很合理“这样不就不用服务端了嘛”。这个想法在技术上不是完全不可行但投入产出比极差。DWG的压缩段是自定义算法恢复出来的数据还要按句柄建对象图光是把实体关系理清楚就得写几千行。即便你写出来了浏览器里跑一遍十万图元的解析主线程直接锁死好几秒用户体验比加载一个图片差得多。开源社区里真正能完整读DWG的库屈指可数而且授权条款往往很严格后面第4章会细说。所以更务实的做法是把难啃的部分放到服务端离线处理前端只干它最擅长的渲染和交互。2. 解析层把DWG拆成前端听得懂的图元2.1 服务端转换链路怎么搭才稳服务端转换的核心思路是“降维”把二进制、带压缩、带对象引用的DWG转换成一个结构扁平、文本可读的中间格式最常见的选择是DXF。DXF分ASCII和二进制两种建议统一转成ASCII版本方便调试和日志排查。转换工具的选择上我的经验是这样如果团队有预算且图纸量大用ODA的官方转换工具最稳妥版本覆盖全兼容性好命令行调用也简单。如果预算有限ACadSharp这类开源库可以顶上MIT授权纯托管实现读常规二维图纸没问题但遇到特殊实体比如某些代理对象、自定义对象会解析不全。LibreDWG也能用但它是GPL授权服务端集成时一定要先让法务过一遍这个坑后面细讲。用命令行工具批量转换的写法大概是这样ODAFileConverter D:\input D:\output ACAD2018 DXF 0 1 *.dwg几个参数的意图我解释一下ACAD2018是输出图纸的版本选它是因为版本足够新、UTF-8编码省心DXF是输出格式后面那两个数字分别控制是否递归子目录和是否审计修复。实际项目里我会把输出目录按“日期 文件哈希”分片避免几万张图纸堆在一个文件夹里导致文件系统变慢。注意转换是有失败率的。图纸损坏、版本过新、包含外部参照都可能导致转换报错。生产环境一定要做失败重试 人工兜底队列别指望一次全过。2.2 DXF组码结构长什么样DXF的核心是“组码 值”成对出现的结构一行组码一行值。举个LWPOLYLINE轻量多段线的片段0 LWPOLYLINE 8 墙体 90 4 70 1 10 0.0 20 0.0 10 100.0 20 0.0 10 100.0 20 50.0 10 0.0 20 50.0对应的含义是组码0表示实体类型是LWPOLYLINE组码8是图层名“墙体”组码90是顶点数量4组码70是标志位值为1表示闭合后面每一组10/20就是一个顶点的X、Y坐标。还有一些常用组码值得记住62是颜色号ACI索引色256表示随层0表示随块6是线型名42是凸度bulge决定这段是直线还是圆弧420是真彩色值39是厚度。解析的时候有个细节特别容易翻车组码行的值可以安全地转成数字但值行的内容不能随便trim。有些文字实体的内容本身就以空格开头或结尾一trim就变了意思。我写解析器时专门给文字类实体保留原始字符串只在做图层名匹配的时候才去空格。一个最小可用的组码配对逻辑大概是这样function parsePairs(text) { const lines text.split(/\r\n|\r|\n/); const pairs []; for (let i 0; i 1 lines.length; i 2) { const code parseInt(lines[i].trim(), 10); if (Number.isNaN(code)) continue; pairs.push({ code, value: lines[i 1] }); } return pairs; }拿到pairs之后再按实体的起始标志组码0切段每段单独解析成图元对象。这个思路比正则匹配靠谱得多因为DXF里的组码是严格成对出现的。2.3 图元数据模型该怎么设计前端渲染层最好只认一套自己的数据结构不要让DXF的字段名直接渗透到渲染代码里否则以后换中间格式就是一场灾难。我在项目里用的模型大致是这样interface CadEntity { id: string; type: line | polyline | arc | circle | ellipse | text | mtext | insert | hatch | spline | point; layer: string; color: number; // 已解析为 0xRRGGBB lineType?: string; lineWeight?: number; points: number[]; // 扁平化坐标 [x1,y1,x2,y2,...] bulges?: number[]; // 与顶点一一对应的凸度 closed?: boolean; transform?: number[]; // 3x3 仿射矩阵 text?: string; height?: number; rotation?: number; }这个模型里最关键的设计是transform。因为图纸里的INSERT块引用会嵌套一个块里可能又引用了另一个块每层都有自己的插入点、缩放和旋转。正确的做法是在解析阶段就把嵌套的变换矩阵乘出来把块里的每个图元“拍平”成带最终矩阵的独立实体。这样渲染层只需要无脑应用一次矩阵不用关心块的层级结构。这里有个性能取舍如果一个块在图纸里被引用了500次每次插入位置都不同拍平意味着内存里会有500份图元副本。我一般会做阈值判断引用次数小于50就拍平大于50就保留块引用结构、在渲染时做实例化。这个数字没有绝对标准按你的图纸特点调就行。实操心得拍平块引用的时候一定要做深度限制和循环检测。我遇到过一张图纸两个块互相引用形成环解析器直接栈溢出。加个最大深度20的保护超了就打日志跳过。3. 渲染层Canvas、SVG还是WebGL3.1 Canvas 2D自绘够用、好控、上手快对于十万图元以下的图纸Canvas 2D完全够用而且调试成本最低。核心工作是坐标变换把图纸的世界坐标映射到屏幕像素。图纸坐标通常是Y轴向上屏幕坐标Y轴向下所以变换矩阵里要对Y取负。基础写法const dpr window.devicePixelRatio || 1; // viewW/viewH 是当前视口覆盖的图纸范围 const scale Math.min(canvas.width / viewW, canvas.height / viewH); ctx.setTransform( dpr * scale, 0, 0, -dpr * scale, offsetX, offsetY );Y轴翻转会带来一个连锁问题圆弧方向反了。Canvas的arc方法里有个anticlockwise参数正常情况下正向凸度是逆时针但Y轴翻转后这个方向要跟着取反否则画出来的圆弧会“跑到另一边去”。我一开始没注意图纸上的门扇弧线全部反着开排查了小半天。对于多段线的凸度bulge需要先把它换算成圆心、半径和起止角。凸度的定义是bulge tan(θ/4)θ是这段圆弧对应的圆心角正值表示逆时针function bulgeToArc(x1, y1, x2, y2, bulge) { const theta 4 * Math.atan(bulge); const dx x2 - x1, dy y2 - y1; const chord Math.hypot(dx, dy); if (chord 0) return null; const r chord / (2 * Math.sin(theta / 2)); const mx (x1 x2) / 2, my (y1 y2) / 2; const nx -dy / chord, ny dx / chord; const h r * Math.cos(theta / 2); const sign bulge 0 ? 1 : -1; const cx mx nx * h * sign; const cy my ny * h * sign; return { cx, cy, r, start: Math.atan2(y1 - cy, x1 - cx), end: Math.atan2(y2 - cy, x2 - cx), counterclockwise: bulge 0 }; }用的时候注意因为整个画布Y轴已经翻转调用ctx.arc时counterclockwise要取反const a bulgeToArc(x1, y1, x2, y2, bulge); if (a) { ctx.beginPath(); ctx.arc(a.cx, a.cy, a.r, a.start, a.end, !a.counterclockwise); ctx.stroke(); }注意不同图纸的凸度写法偶有差异建议先拿一张带圆弧门的图纸做基准测试确认方向和半径都对得上再往下做。3.2 WebGL方案图元一多Canvas就跪当图纸图元超过十万Canvas 2D每帧要遍历所有实体逐个调用绘图API帧率会掉到个位数。这时候得上WebGL。用three.js的思路是把所有线段合并成几个大缓冲区。二维图纸里绝大多数实体最终都能拆成线段所以核心是构造LineSegmentsconst positions []; const colors []; function pushSeg(x1, y1, x2, y2, color) { positions.push(x1, y1, 0, x2, y2, 0); const r ((color 16) 255) / 255; const g ((color 8) 255) / 255; const b (color 255) / 255; colors.push(r, g, b, r, g, b); } // 遍历图元把每条线段塞进数组 // ... const geo new THREE.BufferGeometry(); geo.setAttribute(position, new THREE.Float32BufferAttribute(positions, 3)); geo.setAttribute(color, new THREE.Float32BufferAttribute(colors, 3)); const mat new THREE.LineBasicMaterial({ vertexColors: true }); const lines new THREE.LineSegments(geo, mat); scene.add(lines);这样十万条线也只是一个draw call帧率立刻回来。但有两个坑必须提前知道第一大部分平台尤其是Windows上的Chrome不支持线宽大于1linewidth设了也没用。想要粗线得用三角形条带自己扩边或者退而求其次用后处理描边。第二文字在WebGL里不能直接画得用Sprite或者CSS2DRenderer。图纸里文字一多Sprite数量爆炸这时候要用纹理图集texture atlas把常用汉字预烘焙成一张大图每个文字只用一个quad。3.3 高清屏、手势和响应式布局H5显示CAD多半要跑在手机和平板上这块的细节决定了产品能不能用。高清屏的核心是devicePixelRatio。你给canvas设置的CSS宽高是逻辑像素但绘制缓冲要乘以dpr否则在高分屏上线条会发虚function resizeCanvas(canvas) { const dpr window.devicePixelRatio || 1; const rect canvas.getBoundingClientRect(); canvas.width Math.round(rect.width * dpr); canvas.height Math.round(rect.height * dpr); canvas.style.width rect.width px; canvas.style.height rect.height px; }监听容器尺寸变化用ResizeObserver比监听window的resize事件准得多因为它能感知父容器的变化。不过在部分国产操作系统自带的浏览器里ResizeObserver的原生支持不完整稳妥起见加个polyfill。手势交互统一用Pointer事件处理能同时覆盖鼠标、触摸和手写笔let lastDist 0; canvas.addEventListener(pointermove, (e) { if (activePointers.size 2) { const [p1, p2] [...activePointers.values()]; const dist Math.hypot(p1.x - p2.x, p1.y - p2.y); if (lastDist) { const ratio dist / lastDist; zoomAt((p1.x p2.x) / 2, (p1.y p2.y) / 2, ratio); } lastDist dist; } });布局上看图器的容器用100%宽高加绝对定位canvas铺满即可。有些团队喜欢用Vue3加Element Plus做大屏自适应用CSS transform scale整体缩放这个方案对普通组件很好用但不要用在canvas上因为scale会拉伸位图导致模糊。canvas的正确做法是自己处理dpr和视口尺寸不参与外层缩放。提示移动端上双指缩放要用touch-action: none禁用浏览器的默认手势否则页面会跟着一起缩放体验非常糟糕。4. 工程落地上传、缓存、性能与合规4.1 大文件上传和转换队列一套图纸动辄几百兆直接POST上去既不现实也不稳。标准做法是分片上传前端把文件切成2MB到5MB的块每块算一个哈希服务端按块接收并记录状态全部到齐后再合并。更省事的是加上“秒传”前端先算整个文件的哈希大文件可以用抽样哈希别读全量发给服务端查一下有没有转换过的记录。有的话直接返回已有的转换结果用户瞬间就能看到图纸。这个优化在同一个项目里反复打开同一批图纸的场景下效果非常明显能省掉90%以上的转换时间。转换本身是异步的不要让HTTP请求一直挂着。流程是上传成功 → 创建转换任务返回taskId → 队列消费 → 前端通过WebSocket或者轮询拿进度。WebSocket的实时性更好但要做好断线重连。轮询实现简单两秒一次对服务端压力也不大我一般先用轮询上线稳定了再换WebSocket。转换任务的缓存键建议用“文件哈希 转换参数版本”因为你的解析逻辑可能会升级参数版本一变旧缓存就该失效重转。4.2 首屏性能Worker、裁剪和分块缓存前端体验的第一道坎是首屏时间。三个手段配合使用效果最好。第一解析放Web Worker。哪怕前端只是解析转换后的JSON几十兆的数据在主线程里跑也会卡住UI。Worker里解析完把结果用postMessage传回主线程或者用Transferable Object直接把ArrayBuffer转移过去避免拷贝开销// main.js const worker new Worker(/cad-parser.worker.js); worker.postMessage({ url: jsonUrl }); worker.onmessage (e) { renderEntities(e.data.entities); }; // cad-parser.worker.js self.onmessage async (e) { const res await fetch(e.data.url); const raw await res.json(); const entities normalize(raw); self.postMessage({ entities }); };第二视口裁剪。每帧只绘制当前可见范围里的图元。给每个图元预计算一个包围盒判断它和视口矩形有没有交集没有就跳过。这一步在图纸图元多、但屏幕只显示局部的时候能把绘制量降到原来的百分之几。第三分块缓存tile cache。把画布按屏幕分成若干块每块对应一个离屏canvas缓存。平移的时候大部分块可以直接复用只有新进入视野的块需要重绘。这个方案实现起来有点复杂但缩放平移的流畅度提升立竿见影。如果嫌麻烦先做LOD细节层次也行缩到很小的时候把短线段和密集文字抽稀掉视觉上几乎看不出来性能却能翻倍。4.3 授权合规和开源方案的坑这块是很多技术同学容易忽略、但一出事就是大事的地方。LibreDWG是GPL授权如果你在服务端用它做转换而你的服务端又是对外提供的网络服务GPL的传染性会要求你把服务端相关代码也开源。商业项目用之前务必让法务评估。有些团队图省事直接拿来用等到融资尽调的时候才发现问题返工成本极高。ACadSharp是MIT授权商用友好但它的实现完整度不如商业库遇到复杂实体解析不出来时要有降级策略。ODA的官方SDK和命令行工具是商业授权免费版本有使用范围限制具体条款以官方为准不要凭印象判断。注意图纸是很多制造企业和设计院的核心资产。如果走第三方云渲染等于把图纸原件发到了别人的服务器上。涉及保密项目的一定要评估数据外发风险能用私有化部署就别图省事走公有云。还有一个常见误区是去找来路不明的“看图工具激活版”或者破解补丁。这类东西不仅法律风险大很多还被捆绑了恶意程序装到生产机器上就是给自己埋雷。正规渠道拿授权是长期做这个方向的基本盘。5. 常见问题与排查速查表5.1 图形错乱偏移、反向、比例不对图纸打开后整张图偏到屏幕外这是最常见的现象。八成原因是你的视口范围算错了。正确做法是遍历所有图元的包围盒求并集把并集当成图纸的初始视口再留一点边距。如果图纸里有块引用记得用变换后的坐标算包围盒不能直接用块内原始坐标。圆弧反向我在3.1节提过Y轴翻转导致的anticlockwise取反就行。还有一种情况是椭圆和样条显示异常那是参数没解析对。椭圆要看长轴端点相对中心向量和短长轴比样条要注意区分控制点和拟合点两者用的组码不一样用错了曲线形状就完全跑偏。线型显示也有坑。图纸里的虚线是按图纸单位定义虚线段长的你缩放画布时如果不重算缩到很小时虚线会挤成实线放到很大时又变成几段长线。我的处理方式是在屏幕空间里按设备像素重新计算dash数组保证任何缩放下虚线看起来都均匀。现象可能原因处理方式整图偏移视口范围计算错误用所有图元包围盒并集作为初始视口圆弧反向Y轴翻转arc的anticlockwise参数取反虚线变实线线型比例未随缩放调整按屏幕像素重算dash块内容位置错嵌套矩阵未累乘逐层乘变换矩阵后再绘制椭圆形状不对长短轴参数解析错误检查长轴向量和比值组码5.2 文字与字体乱码、跑位、方块中文乱码的根因是编码前面讲过2007以前的图纸按代码页解码。如果你的图纸来源很杂建议在解析层统一做一次编码探测拿到正确的字符串再往下走。SHX字体是另一个大麻烦。这是AutoCAD自己的字形格式本质上是一堆画线指令浏览器里没有原生支持。工程上常见的处理有三种一是做映射表把常见的gbcbig.shx、hztxt.shx映射到相近的开源中文字体二是用画线的方式自己渲染SHX字形工作量大但保真度高三是直接降级成系统字体接受一定的样式差异。我一般先用第一种遇到客户对字体样式特别敏感的再上第二种。MTEXT的格式化代码也得处理。图纸里的多行文字带一堆控制符\P是换行\H后面跟字高还有花括号分组、字体切换。你需要写一个小解析器把它们翻译成HTML的span或者Canvas的分段绘制不然显示出来就是一堆反斜杠乱码。文字的对齐也不能忽略TEXT实体的插入点和对齐点经常不是同一个位置水平和垂直对齐方式由72、73组码决定算错了文字就会整体偏移半个字高。5.3 性能与兼容卡顿、白屏、内存爆现象可能原因处理方式打开就卡死几秒主线程解析大文件解析移入Web Worker缩放掉帧严重每帧重绘全量图元视口裁剪 分块缓存iOS上白屏canvas尺寸超限或内存超阈值降低dpr、分块渲染移动端闪退单张纹理过大拆图集、限制纹理尺寸老浏览器报错API兼容性不足加polyfill、做特性检测反复打开同一图很慢没有转换缓存按文件哈希做结果缓存iOS的canvas有个著名限制单个canvas的总像素数超过一定值就直接白屏而且不报错。我碰到过一次图纸复杂初始化时按3倍dpr开了个大画布iPhone上直接一片白。后来改成按需分块、动态限制最大尺寸才解决。这个坑在安卓上不一定复现所以一定要拿真机测别只在桌面浏览器里调。国产操作系统上自带浏览器的内核版本可能偏老像OffscreenCanvas、ResizeObserver这些较新的API支持不完整。上线前做一轮目标环境的兼容性矩阵测试把不支持的API列个降级清单比事后救火省心得多。6. 我在实际项目里的取舍和经验6.1 几个踩过的坑不一定人人都会遇到第一个坑是想用前端直接解DWG。我在这上面花了差不多两周写了个半成品解析器能读简单图纸一遇到块嵌套和压缩段就歇菜。后来果断转向服务端转换一天半就把链路跑通了。这个教训是不要在浏览器里重造CAD的内核浏览器擅长的是渲染和交互不是二进制逆向。第二个坑是只转SVG。最早为了赶进度用现成工具把DWG转SVG直接塞进页面确实一天就出了demo。但客户第二天就提了需求要能关图层、要能量距离、要能看线宽。全做不了只能推倒重来。所以如果需求里出现“图层”“量测”“批注”任意一个词就别走SVG这条路。第三个坑是转换服务没做并发控制。上线第一天用户批量上传了三百张图纸转换进程把机器CPU打满整个服务雪崩。后来加了任务队列和并发上限再配合失败重试才稳住。生产环境的转换服务本质上是一个资源调度问题不是简单的“调个库转一下”。第四个坑是移动端画布开太大。前面提过iOS白屏其实安卓也一样脆只是阈值不同。现在我的做法是初始画布按视口尺寸开平移缩放时动态扩展超出一定范围就丢弃不可见的缓存块。宁可多写点缓存管理逻辑也不要一次性开个巨无霸画布。6.2 这套东西后面还能怎么长把基础看图能力跑通之后能扩展的方向挺多而且大多不需要推翻现有架构。图层管理是最容易加的一个解析出来的图元本来就带图层字段做个侧边栏列出所有图层控制显示隐藏和颜色覆盖就行。量测功能稍微复杂点需要在屏幕坐标和图纸坐标之间做双向换算但只要坐标变换做对了无非是多写几个交互状态。图纸对比是个很有意思的方向把两个版本的图元按坐标做哈希比对差异部分高亮显示审图场景里非常实用。图纸合并也能做多张图纸的图元合并到一个坐标系里注意处理各自的基点和单位差异。如果你们公司的系统比较多可以把看图器做成一个独立子应用用微前端的方式嵌到各个业务系统里图纸的加载、缓存、权限都收在这一个服务里比在每个系统里各做一遍要清爽得多。这种架构下前端只负责渲染后端负责一切重活边界很清楚。另外GIS数据和DWG的互转、图纸导出为PNG或PDF、批量脚本处理图纸这些需求在实际项目里出现的频率比想象中高。它们大多可以复用你已经做好的解析层无非是把渲染目标从屏幕换成文件。所以架构设计时尽量把“解析”和“渲染”分离开解析结果是一套与渲染无关的数据结构后面不管加什么输出都只是加一个消费者而已。我在实际使用中发现最省心的做法是服务端只做转换和缓存不做渲染。曾经有团队把渲染也放服务端生成图片切片结果每次交互都要往返请求用户体验极差服务端压力还大。浏览器现在的能力足够强把渲染权交给前端服务端轻装上阵整个系统的伸缩性会好很多。这个分工一旦定下来后面加功能就都是加法不会再动地基。