Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第三章:技术选型的艺术 —— 为什么选择“更难”的路?

3.1 一个让你看起来不太聪明的选择

如果此刻有一个经验丰富的前端开发老手路过,瞥见你在构建一个3D元素周期表的项目。他大概会拍拍你的肩膀说:“嘿,用Three.js啊,很简单的,几行代码就搞定了。”

他说得没错。用Three.js来做我们这个项目,确实会省很多力气。

但你回想一下,我们最初的那个念头是什么?

“如果不依赖任何3D库,我能做到吗?”

所以,你要做的,可能是一个在老手看来“不太聪明”的选择。但这恰恰是我们整个项目存在的理由。我们不是要最快地到达终点,我们是要把走向终点的每一步路都看得清清楚楚

请记住这个比喻:你面前有两辆车。一辆是豪华的自动驾驶汽车,你只需说出目的地,它就能把你带到终点;另一辆是手动挡的老式跑车,你需要亲手操控离合、油门和方向盘,去感受每一个机械齿轮的咬合。Three.js 就是那辆自动驾驶汽车。而我们,选择坐进那辆手动挡跑车。

这将是一次“引擎盖下的旅程”。

3.2 Three.js 的魔法,和魔法背后的代价

在用“不好”来评价一个东西之前,我们必须先了解它的“好”。

Three.js 做了什么?

它在我们和 GPU(图形处理器,你电脑里专门处理图像的那块芯片)之间,架起了一座桥。它把“渲染3D场景”这件极其复杂的事情,包装成了几个简单的概念:

  • 场景 (Scene):一个可以容纳所有3D物体的空间。
  • 相机 (Camera):我们的眼睛,决定了我们能看到什么。
  • 物体 (Mesh):由几何体 (Geometry,形状) 和材质 (Material,表面) 组成。
  • 渲染器 (Renderer):负责把相机看到的场景“画”到屏幕上。

用Three.js画一个旋转的立方体,大概只需要写一个类的代码。它会处理好所有复杂的数学问题:光线照射、物体遮挡、视角变换……它全包了,就像是在变魔法。

但魔法带来的问题是什么?

舒适是有代价的。这个代价就是:你不理解它。 你不知道它为什么要创建一个叫 WebGLRenderer 的东西,你不知道你的立方体是如何从一堆坐标点变成屏幕上彩色的像素块的,你更不知道那个控制着物体旋转、平移、缩放的,叫做“矩阵”的东西,到底是个什么玩意。

所以一旦你离开了Three.js,你就什么都不会了。更要命的是,当Three.js的行为和你的预期不一致时,你连从哪开始调试都不知道。你只能去网上搜“Three.js 为什么xxx”,然后寄希望于有人遇到过同样的问题。

但我们不仅仅是来当司机的,我们更是来当一个合格的机修师的。

所以,不是Three.js不好,而是它不适合我们此刻的目标。我们的目标是用一个项目,去撬动对3D渲染基础原理的理解,是要去赋能,而不是被封装。

3.3 那么,原生 CSS 3D 够用吗?

好,我们决定了不依赖 WebGL。那我们用什么在浏览器里画3D图形呢?

答案是:CSS 3D Transform

你用过的 rotateX()rotateY()translateZ(),就是它的一部分。这是浏览器提供的一套原生能力,不需要任何插件或库。

用它们来做我们的五种布局,理论上也是可行的,但过程会非常痛苦。为什么?

旋转矩阵 vs. matrix3d()—— 一个遥控器的比喻

想象一下,你面前有118个元素卡片。你需要让一个ID为5的元素:先绕X轴旋转20度,再沿Y轴平移100px,再绕Z轴旋转-15度,再缩放到0.5倍,再……你可能会写出这样的CSS字符串:

css

/* 这看起来还行,对吧? */
transform: rotateX(20deg) translateY(100px) rotateZ(-15deg) scale(0.5);

这完全没问题,但请思考这几个问题:

  1. 顺序陷阱:你试过把 translateY 放到 rotateX 前面吗?结果完全不一样。当你要对118个元素进行复杂的、顺序敏感的变换时,用空格拼接字符串简直是噩梦。
  2. 性能瓶颈:如果你需要不断地改变一个元素的旋转角度,你只能通过JavaScript去更新这个由多个函数组成的CSS字符串。每一次更新,浏览器都要去解析这个字符串。这在需要同时更新118个元素、且每秒更新60次(动画帧率)时,是巨大的性能浪费。
  3. 无法批量计算:这是我们遇到的最大问题。在每一个模型布局中,我们都需要根据数学公式,为每个元素算出一个唯一的、包含了旋转和平移的最终变换。这种复杂的数学计算,很难映射到一连串分散的CSS变换函数上。

此时用 rotateX()translateY() scale(0.5) 就像是在用一堆功能单一的遥控器,分别按顺序去控制设备。

这就好比,打开门禁你需要找到你家大门的遥控器,打开空调你需拿到空调遥控器,你想看会电视,还得去电视柜里翻出电视机的遥控器。不难想象,管理这一堆“遥控器”是一件很麻烦的事情。

如果将所有的遥控器都统一装到一个APP里,我们只需要用到一台手机,就可以远程控制你家里的所有设备。尽管你人还没回到家,依然可以开启空调,而无需先打开门禁。

transform 中的 “matrix3d()” 就是这样的一个“万能遥控器”,它让我们拥有了一个统一的、可编程的控制接口。可以一次性地把一个元素从初始状态“变”到最终状态

3.4 万能遥控器:matrix3d() 登场

幸好,CSS 提供的不只有零散的工具,它还提供了一个“万能遥控器”:matrix3d()

它接收16个数字,这16个数字构成了一个 4x4 的矩阵。这个矩阵,就是所有3D变换的终极统一表达。

css

/* 一个包含了旋转、平移和缩放的复合变换,被压缩成了一个4x4矩阵 */
transform: matrix3d(
   0.87, 0, -0.5, 0,
   0, 0.5, 0, 0,
   0.5, 0, 0.87, 0,
   100, 50, -200, 1n
);

为什么它这么强大?

1. 它是单一的、可计算的值。
我们不再需要处理一长串的CSS函数。我们可以用JavaScript随心所欲地生成、修改、缓存这16个数字的数组。这为我们的项目带来了决定性的优势:

  • 可批量计算:我们可以在JavaScript里根据球体公式,轻松地用一个循环为118个元素计算出它们各自的 matrix3d 数组。
  • 可缓存:计算好的矩阵数组可以存起来。当用户切换布局时,我们直接把缓存好的矩阵应用到元素上,性能极佳。
  • 可组合:多个复杂的变换,可以通过矩阵乘法合并成一个新的矩阵。这意味着,我们可以把一个元素的“布局位置”和“用户交互旋转”合并,得到一个最终的变换矩阵,一次性应用到元素上。

2. 它给了我们一个机会,去触碰一个更大的世界——线性代数。
一旦你接受了用 matrix3d 来控制元素,你就打开了线性代数的大门。你会开始明白,原来所有的旋转、缩放、平移,本质上都是在改变物体的坐标系。这种认识,能让你在将来面对任何3D图形库(无论是Three.js还是游戏引擎)时,都能一眼看穿它们的底层原理。

所以,当前我们的技术选型,不是基于“方不方便”,而是基于“学不学得到东西”。 我们放弃方便,就是为了能亲手摸到这些底层的基础概念。

对于我们的项目来说,我们需要为118个元素分别计算它们在5种不同布局下的精确位置,这正是用数学矩阵编程控制的优势所在。

3.5 除了matrix3d(),我们还选了些什么?

除了核心的 matrix3d() 渲染方案,我们还需要为项目搭建一个现代前端应用的骨架。这部分我们也会做出深思熟虑的选择。

1. 编程语言:TypeScript

我们从JavaScript换到了TypeScript。为什么?因为当你的代码量超过一定规模(比如我们现在的项目),你会遇到一个很尴尬的问题:不知道这个变量里存的到底是什么。

javascript

// JavaScript: a 是什么?数字?字符串?数组?
function calculate(a) {
    return a + 2; // 如果a是字符串 "1" 呢?
}
typescript

// TypeScript: a 必须是 number,编译阶段就会帮你检查错误
function calculate(a: number): number {
    return a + 2;
}

TypeScript 能在代码运行之前,帮你发现那些低级错误。更重要的是,它的类型注解就像一张活的代码文档,让你在看别人(哪怕是三个月前的自己)写的代码时,不再一头雾水。

2. UI 框架:React

我们选择 React,不是因为它是“最好的”,而是因为它是“最合适的”之一。它基于组件的构建方式,非常适合我们将复杂的3D界面拆分成可管理的积木块。

3. 构建工具:Vite

Vite 是新一代的构建工具,它有两个核心优势:

  • 快得离谱:启动开发服务器几乎在瞬间完成,热更新让你不用刷新就能看到代码变更。
  • 开箱即用:它原生支持 TypeScript、CSS模块等我们需要的技术,几乎不需要复杂配置。

3.6 做决策的艺术:跳出“好不好”,思考“为了什么”

在我们结束这一章之前,我想和你分享一个超越本章具体内容的、更通用的思维模式。

以后,当你自己独立面对一个技术选择时,你会发现网上充满了各种争论:这个好,那个不好。React 好还是 Vue 好?Vite 好还是 Webpack 好?用 Redux 还是不用?

不要陷入这种非黑即白的争论。正确的思维方式是,先问自己三个问题:

  1. 我的目标是什么? 我是要快速上线产品,还是要深入学习原理?我是做一个小工具,还是做需要多人协作的大型应用?
  2. 这个技术解决了什么问题? 它为什么被发明出来?它出现的背景是什么?
  3. 我为此付出了什么代价? 它带来了哪些复杂性?它增加了我的学习成本吗?它锁死了我未来的选择吗?

好的技术决策,不是找到“最好”的技术,而是找到最适合你当前目标的技术。

对于我们来说,我们的目标是学习。所以,我们宁愿选择一条更难、更底层、但能让我们理解原理的路。

好了,技术的航海图已经标好。我们已经决定了方向,解释清了原因。是时候清理我们的船坞,准备开工了。

在开始写核心代码之前,我们先要做好一个容易被忽视,但又极其重要的工作:搭建开发环境。从下一章开始,我们将配置 Git、Node.js、Vite,并逐步引入那些能将我们塑造成专业开发者的工具。

不要急着看3D画面,相信我,把脚手架搭稳,大楼才能盖得高。