<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
	<channel>
		<title>大飞的技术博客</title>
		<link>https://shenyifei.com</link>
		<description>大飞的个人技术博客:IoT/嵌入式、Web 全栈与 AI 应用的工程实践记录。</description>
		<language>zh-CN</language>
		<lastBuildDate>Sun, 30 Aug 2026 11:11:21 GMT</lastBuildDate>
		<atom:link href="https://shenyifei.com/rss.xml" rel="self" type="application/rss+xml" xmlns:atom="http://www.w3.org/2005/Atom"/>
		<item>
			<title>Next.js 静态导出的边界与取舍</title>
			<link>https://shenyifei.com/posts/nextjs-static-export-notes</link>
			<guid isPermaLink="true">https://shenyifei.com/posts/nextjs-static-export-notes</guid>
			<pubDate>Sat, 29 Aug 2026 00:00:00 GMT</pubDate>
			<description>Next.js 静态导出(output: &apos;export&apos;)意味着放弃什么、换来什么,以及什么项目适合静态导出、什么项目别碰的判断清单。</description>
			<category>Web</category>
			<category>Next.js</category>
			<category>建站</category>
			<content:encoded><![CDATA[<p>我的几个站全部用 Next.js 的 <code>output: 'export'</code> 静态导出:构建产物是纯
HTML/CSS/JS,扔到任意静态服务器就能跑,没有 Node 进程、没有服务器运维。
这个选择不是免费的,这篇文章把边界讲清楚。</p>
<h2>换来了什么</h2>
<ul>
<li><strong>部署极简</strong>:<code>out/</code> 目录整个拷上服务器/OSS 就完事,不需要 PM2、
不需要容器、不需要考虑 Node 版本漂移;</li>
<li><strong>天然抗流量</strong>:静态文件随便被 CDN 缓存,官网场景下几乎不存在
「被打挂」的概念;</li>
<li><strong>安全面小</strong>:没有服务端运行时,攻击面收缩到静态文件本身;</li>
<li><strong>构建期报错</strong>:所有数据获取都发生在 <code>next build</code>,内容错了构建直接
失败,坏内容到不了线上。</li>
</ul>
<h2>失去了什么</h2>
<p>静态导出本质是把所有「动态」挪到构建期或客户端,以下能力直接不可用:</p>
<table>
<thead>
<tr>
<th>能力</th>
<th>静态导出下的状态</th>
<th>替代方案</th>
</tr>
</thead>
<tbody>
<tr>
<td>服务端 API Routes</td>
<td>不可用(除非 <code>force-static</code>)</td>
<td>外部服务/表单代收</td>
</tr>
<tr>
<td>ISR 增量再生</td>
<td>不可用</td>
<td>重新构建重新发布</td>
</tr>
<tr>
<td>请求级动态渲染</td>
<td>不可用</td>
<td>客户端 fetch</td>
</tr>
<tr>
<td>中间件 Middleware</td>
<td>不可用</td>
<td>CDN 边缘规则</td>
</tr>
<tr>
<td>图片优化服务</td>
<td>关闭(<code>unoptimized</code>)</td>
<td>构建期压好图</td>
</tr>
</tbody>
</table>
<p>对品牌官网和技术博客,这个清单几乎无痛:内容发布频率低(重新构建即
「ISR」),交互都在客户端,表单可以走第三方代收再通知到企微/邮件。</p>
<h2>常用的「伪装动态」手法</h2>
<p><strong>1. Route Handler + force-static 生成派生文件。</strong>
RSS、sitemap 这类「构建期可确定」的动态产物:</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// app/rss.xml/route.ts</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> dynamic</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "force-static"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> async</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> GET</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Response</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">buildRss</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(), {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    headers: { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Content-Type"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"application/rss+xml; charset=utf-8"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>构建时执行一次,输出落成真正的 <code>out/rss.xml</code> 静态文件。</p>
<p><strong>2. generateStaticParams 展开所有路径。</strong>
<code>/posts/[slug]</code> 这类动态路由必须在构建期穷举:</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> generateStaticParams</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getAllPosts</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ({ slug: post.slug }));</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>忘了写这个函数,动态路由在静态导出下会被静默跳过——不报错,只是
产物里没有那些页面,发布后才发现 404。</p>
<p><strong>3. 数据获取全部收口到构建期模块。</strong>
文件读进内存、校验、缓存,页面组件只消费纯函数结果。好处是测试
不需要起 Next,数据层单测就是普通 Node 测试。</p>
<h2>判断 checklist</h2>
<p>什么项目适合静态导出:</p>
<ul>
<li>内容发布频率低,发新内容 = 触发一次重新构建可以接受;</li>
<li>没有按请求变化的个性化内容(登录态、A/B、地理定向);</li>
<li>搜索/评论/表单这类动态能力愿意交给第三方或客户端方案;</li>
<li>有 CI 能自动构建发布(没有的话,发布 = 手动跑命令)。</li>
</ul>
<p>反过来,只要有任何一条硬性不满足——比如需要 SSR 做 SEO 的个性化
首页——就老实用 <code>next start</code> 或托管到 Vercel,别跟 <code>output: 'export'</code>
较劲,它的边界是设计如此,不是 bug。</p>
<h2>一句话结论</h2>
<p>静态导出适合「内容即产品」的站:官网、博客、文档站。它把复杂度从
运行时挪到了构建期,而构建期复杂度是靠 CI 和纪律管理的,比运行时
复杂度便宜得多。</p>]]></content:encoded>
		</item>
		<item>
			<title>一仓四站:pnpm monorepo 里养多个官网</title>
			<link>https://shenyifei.com/posts/multi-site-monorepo</link>
			<guid isPermaLink="true">https://shenyifei.com/posts/multi-site-monorepo</guid>
			<pubDate>Fri, 28 Aug 2026 00:00:00 GMT</pubDate>
			<description>四个官网住在一个仓库里:目录怎么分、依赖怎么共享、端口怎么错开,以及为什么「全都一样」反而是最优解。</description>
			<category>Web</category>
			<category>monorepo</category>
			<category>建站</category>
			<content:encoded><![CDATA[<p>手上有四个网站要维护:三个公司官网加这个博客。它们技术栈相同、部署方式相同、
维护者也是同一个人,于是都住进了一个 pnpm monorepo。这篇文章讲讲这套结构
的实际运作方式,以及几个当时犹豫过的决定。</p>
<h2>目录结构</h2>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0"><code><span class="line"><span>website/</span></span>
<span class="line"><span>├── packages/</span></span>
<span class="line"><span>│   ├── www.yunjuiot.com.cn/   # 公司主站(端口 3000)</span></span>
<span class="line"><span>│   ├── www.elinchong.com/     # 品牌站(端口 3001)</span></span>
<span class="line"><span>│   ├── www.xunhong168.com/    # 单页官网(端口 3002)</span></span>
<span class="line"><span>│   └── www.shenyifei.com/     # 这个博客(端口 3003)</span></span>
<span class="line"><span>├── shared/                    # 跨包共享资产(pcb3d 流水线等)</span></span>
<span class="line"><span>├── scripts/                   # OG 封面、PDF 等 Python 生成脚本</span></span>
<span class="line"><span>└── docs/plans/                # 每个功能的设计文档</span></span></code></pre>
<p>每个站点是一个独立的 Next.js App Router 包,有自己的 <code>package.json</code>
和 <code>next.config.ts</code>,互不引用对方的组件。共享的只有根目录的依赖声明、
生成脚本和资产——组件级别的「复用」我刻意没做,后面说为什么。</p>
<h2>依赖:全在根上</h2>
<p>站点级 <code>package.json</code> 只声明 scripts,依赖全部挂在仓库根:</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  "name"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"www.shenyifei.com"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  "version"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"0.1.0"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  "private"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  "scripts"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    "dev"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"next dev --turbopack -p 3003"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    "build"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"next build"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>四个站共用一个 Next.js 版本,升级时一次升级全部,不存在「A 站还在
Next 14、B 站已经 Next 15」的漂移。代价是升级需要四站都回归一遍,
但静态导出站点的回归成本低(构建过 = 大概率没事)。</p>
<h2>约定大于配置</h2>
<p>同一件事在四个站里长得一模一样:</p>
<ul>
<li><code>output: 'export'</code> 静态导出,<code>images: { unoptimized: true }</code>;</li>
<li><code>app/layout.tsx</code> 里集中管理 SEO 元数据、OG 封面、结构化数据;</li>
<li><code>sitemap.ts</code> / <code>robots.ts</code> / <code>not-found.tsx</code> 三个基础设施文件齐全;</li>
<li>端口号写死在 dev script 里,3000 起,一个站一格。</li>
</ul>
<p>约定带来的好处在改版时最明显:我知道任何一个文件的「对应物」在另外
三个站的哪个位置,AI 辅助改版时提示词都省了。</p>
<h2>为什么不做组件级共享</h2>
<p>最早考虑过把 Header/Footer/ShareBar 抽成 workspace 共享包,后来放弃了:</p>
<ol>
<li>四个站的<strong>品牌视觉完全不同</strong>,能共享的只有逻辑骨架,而骨架只有几十行;</li>
<li>抽象包一旦改坏,四个站同时挂,耦合的爆炸半径反而变大;</li>
<li>每个站代码量都不大(几百行),复用收益撑不起一层抽象的维护成本。</li>
</ol>
<p>真正值得共享的是<strong>资产和流水线</strong>:PCB 的 STEP→GLB 转换脚本、OG 封面
生成脚本,这些放 <code>shared/</code> 和 <code>scripts/</code>,谁用谁调,不产生运行时耦合。</p>
<h2>一个 pnpm 特有的坑</h2>
<p>pnpm 的 node_modules 是绝对路径 junction,某个包里有 node_modules 时
(比如 <code>shared/pcb3d</code> 自带依赖),webpack 的 FileSystemInfo 快照会把它
误处理,构建直接崩:</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// next.config.ts — 按语义并入 managedPaths 跳过快照</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">config.snapshot.managedPaths </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  ...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(config.snapshot.managedPaths </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">??</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> []),</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  /</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">[</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\\</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">/]</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">scripts</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">[</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\\</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">/]</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">pcb3d</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">[</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\\</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">/]</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">node_modules</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">];</span></span></code></pre>
<p>这类问题没有通用解,记住 pnpm 的 monorepo 里出现「莫名其妙的构建崩溃」,
先怀疑 junction 路径。</p>
<h2>小结</h2>
<p>monorepo 对「多个小站、一个人维护」的场景是明确的正解:依赖统一、
约定统一、部署统一。但要忍住两个冲动——不要过早抽共享组件包,
也不要为了让目录好看而引入构建复杂度。</p>]]></content:encoded>
		</item>
		<item>
			<title>从 STEP 到 model-viewer:给官网做 PCB 3D 展示</title>
			<link>https://shenyifei.com/posts/pcb-3d-pipeline</link>
			<guid isPermaLink="true">https://shenyifei.com/posts/pcb-3d-pipeline</guid>
			<pubDate>Wed, 26 Aug 2026 00:00:00 GMT</pubDate>
			<description>立创EDA 导出的 STEP 模型如何走完「三角化 → 配色 → GLB → 量化 → 网页展示」全流程,以及每一步踩过的坑。</description>
			<category>IoT</category>
			<category>Three.js</category>
			<category>工具链</category>
			<content:encoded><![CDATA[<p>公司官网要展示 PCB 板卡的 3D 模型,让客户在下单前能转着看板子。硬件同事交来的是
立创EDA 专业版导出的 STEP 文件,而网页端要的是能在浏览器里流畅渲染的 GLB。
这篇文章记录整条流水线和每一步的取舍。</p>
<h2>总体链路</h2>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0"><code><span class="line"><span>立创EDA STEP → step2obj.py (OCCT 三角化) → obj2glb.mjs (GLB)</span></span>
<span class="line"><span>            → gltf-transform 量化 → public/models/*.glb → &#x3C;model-viewer></span></span></code></pre>
<p>分四步,每步一个独立脚本,串起来跑。</p>
<h2>第一步:STEP 三角化,为什么不用 WASM</h2>
<p>STEP 是 B-Rep 边界表示,浏览器渲染不了,先要三角化成网格。现成方案里
occt-import-js 提供 WASM 版的 OCCT,看似最省事,但实测对这种多层板 STEP
会输出空网格,问题出在它的 WASM 构建裁剪了部分几何内核能力。</p>
<p>最后用的本机 Python + <code>cadquery-ocp</code>(完整 OCCT 绑定)做三角化,
顺带解决另一个需求:按层名配色。</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># step2obj.py 核心逻辑(节选)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> OCP</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.STEPControl </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> STEPControl_Reader</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> OCP</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.BRepMesh </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> BRepMesh_IncrementalMesh</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">reader </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> STEPControl_Reader()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">reader.ReadFile(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"board.step"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">reader.TransferRoots()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">shape </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> reader.OneShape()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">BRepMesh_IncrementalMesh(shape, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">False</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.5</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">True</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
<p>PCB 的丝印层、阻焊层、基板在 STEP 里是不同颜色的实体,三角化后按颜色
拆分材质写入 OBJ,后面 glTF 转换时会自然映射成多个 primitive。</p>
<h2>第二步:OBJ → GLB</h2>
<p>这步没有技术含量,<code>obj2gltf</code> 一行命令,写成脚本只是因为要处理贴图路径和
缺省参数。GLB 比 OBJ+MTL 好在单文件、二进制、带 PBR 材质。</p>
<h2>第三步:量化,模型从 40MB 到 4MB</h2>
<p>原始 GLB 转出来 40MB 左右,网页加载不能接受。<code>gltf-transform</code> 三连:</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">npx</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> gltf-transform</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> weld</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> board.glb</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> t.glb</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">npx</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> gltf-transform</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> prune</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> t.glb</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> t2.glb</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">npx</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> gltf-transform</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> quantize</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> t2.glb</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> board-q.glb</span></span></code></pre>
<p><code>quantize</code> 使用 KHR_mesh_quantization 扩展,把顶点属性从 float32 压到
int16,配合 weld 去重和 prune 清理,体积降到 4MB,视觉上几乎无差别。
<code>&#x3C;model-viewer></code> 原生支持这个扩展,不需要额外解码库。</p>
<p>另一个经验:导出前在 EDA 里隐藏小元件(0402 电阻电容之类)的 3D 模型,
源文件体积直接砍半——这些元件在客户视角下本来也看不清。</p>
<h2>第四步:网页端</h2>
<p>展示组件直接用 <code>@google/model-viewer</code>,自托管它编译好的单文件,
不依赖运行时 CDN:</p>
<pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">model-viewer</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  src</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"/models/board.glb"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  camera-controls</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  auto-rotate</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  shadow-intensity</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"1"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  style</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"width:100%;height:480px"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">model-viewer</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<p>相比自己写 Three.js,<code>model-viewer</code> 把相机惯性、AR 入口、无障碍这些都
处理好了。如果后续要做「点击元件高亮 BOM」这类深度交互,再换 Three.js
也不迟——模型和管线都是复用的。</p>
<h2>小结</h2>
<table>
<thead>
<tr>
<th>坑</th>
<th>解法</th>
</tr>
</thead>
<tbody>
<tr>
<td>occt-import-js WASM 三角化空网格</td>
<td>本机 Python + cadquery-ocp</td>
</tr>
<tr>
<td>GLB 体积过大</td>
<td>weld + prune + quantize 三连</td>
</tr>
<tr>
<td>源文件含设计 IP</td>
<td>STEP 留在私有目录,只有 GLB 进 public</td>
</tr>
<tr>
<td>CDN 依赖</td>
<td>model-viewer 自托管</td>
</tr>
</tbody>
</table>
<p>整条管线写成三个脚本,换新板子就是「导出 STEP → 跑脚本 → 加一条记录」,
非前端同事也能自己发新版。</p>]]></content:encoded>
		</item>
	</channel>
</rss>