Three.js 工程化與專案結構:使用 Vite / Webpack 打包
簡介
在開發 Three.js 互動 3D 應用時,單純把所有程式碼寫在一個 HTML 檔案裡雖然能快速驗證概念,卻不利於維護與擴充。隨著模型、材質、特效、UI 等資源的增加,專案的檔案結構、模組化、以及建置流程變得相當重要。
- Vite 與 Webpack 是目前前端社群最常使用的兩套打包工具。它們不僅能將 ES 模組、CSS、圖片、GLTF 等資源自動轉換成瀏覽器可直接載入的檔案,還提供熱重載(HMR)、代碼分割、Tree‑shaking 等效能優化功能,讓開發 Three.js 時的迭代速度與最終產出品質都大幅提升。
本篇文章將從 專案初始化、設定 Vite / Webpack、常見問題 以及 實務案例 四個面向,逐步說明如何在 Three.js 專案中正確運用這兩套工具,讓你的 3D 應用從「雜亂」變成「可維護、可擴充」的工程化作品。
核心概念
1. 為什麼需要打包工具?
| 項目 | 手寫 <script> 方式 | 使用 Vite / Webpack |
|---|---|---|
| 模組化 | 只能使用全域變數或 IIFE,難以管理依賴 | 支援 ES Modules、CommonJS,依賴自動解析 |
| 資源載入 | 必須自行寫 loader 讀取 GLTF、HDR 等 | 內建 loader,支援圖片、字體、模型等 |
| 開發體驗 | 每次修改都要手動重新整理 | Hot Module Replacement (HMR) 即時更新 |
| 效能優化 | 無法自動壓縮、分割程式碼 | Tree‑shaking、Code Splitting、壓縮 (Terser) |
| 跨平台 | 只能在瀏覽器執行 | 支援 Node、Electron、SSR 等多種環境 |
結論:在任何稍微複雜的 Three.js 專案中,使用 Vite 或 Webpack 幾乎是必備的基礎設施。
2. Vite 基本設定
Vite 以原生 ES 模組為核心,啟動速度極快,特別適合開發階段需要頻繁切換場景或調整 Shader 的情境。
2.1 初始化專案
# 建立一個新的 Vite + Three.js 專案
npm create vite@latest three-vite-demo -- --template vanilla
cd three-vite-demo
npm install
npm install three
這裡使用
vanilla模板,之後會自行加入 Three.js 相關程式碼。
2.2 建立專案結構
three-vite-demo/
├─ public/ # 靜態資源 (favicon、模型、HDR)
│ └─ models/
│ └─ scene.gltf
├─ src/
│ ├─ assets/ # 直接在程式碼中 import 的資源
│ │ └─ textures/
│ │ └─ wood.jpg
│ ├─ components/ # 可重用的 Three.js 元件 (OrbitControls、PostProcessing)
│ │ └─ OrbitControls.js
│ ├─ shaders/ # 自訂 GLSL 程式碼
│ │ ├─ vertex.glsl
│ │ └─ fragment.glsl
│ ├─ main.js # 入口檔案
│ └─ style.css
├─ vite.config.js # Vite 設定
└─ index.html
2.3 Vite 設定檔
vite.config.js 中加入 GLTF、HDR 等 loader,讓 Vite 能正確處理這些二進位檔案。
import { defineConfig } from 'vite';
import { resolve } from 'path';
export default defineConfig({
// 設定別名,方便 import
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
// 靜態資源的處理
assetsInclude: ['**/*.gltf', '**/*.glb', '**/*.hdr'],
// 開發伺服器設定
server: {
open: true, // 自動開啟瀏覽器
port: 3000,
},
});
2.4 主程式範例
// src/main.js
import * as THREE from 'three';
import { OrbitControls } from '@/components/OrbitControls.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import woodTexture from '@/assets/textures/wood.jpg';
// 建立場景、相機、渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(0, 1.5, 3);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 加入軌道控制
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// 加載紋理
const texture = new THREE.TextureLoader().load(woodTexture);
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(10, 10),
new THREE.MeshStandardMaterial({ map: texture })
);
floor.rotation.x = -Math.PI / 2;
scene.add(floor);
// 加載 GLTF 模型
const loader = new GLTFLoader();
loader.load('@/public/models/scene.gltf', (gltf) => {
scene.add(gltf.scene);
});
// 環境光 + 平行光
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 10, 7);
scene.add(dirLight);
// 窗口調整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 渲染循環
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
以上程式碼展示了 Vite 如何透過 ES 模組直接匯入 Three.js、GLTFLoader、以及本地圖片,且不需要額外的 Webpack loader 設定。
3. Webpack 基本設定
Webpack 的優勢在於 高度可客製化,適合需要多階段編譯(如 TypeScript + GLSL)或與後端整合的專案。
3.1 初始化專案
mkdir three-webpack-demo
cd three-webpack-demo
npm init -y
npm install --save-dev webpack webpack-cli webpack-dev-server \
html-webpack-plugin clean-webpack-plugin \
css-loader style-loader \
file-loader
npm install three
3.2 建立檔案結構
three-webpack-demo/
├─ src/
│ ├─ assets/
│ │ └─ textures/
│ │ └─ metal.jpg
│ ├─ shaders/
│ │ ├─ vertex.glsl
│ │ └─ fragment.glsl
│ ├─ index.js # 入口
│ └─ index.html
├─ webpack.config.js
└─ .babelrc (若使用 Babel)
3.3 Webpack 設定檔
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = (env, argv) => {
const isProd = argv.mode === 'production';
return {
entry: './src/index.js',
output: {
filename: isProd ? '[name].[contenthash].js' : '[name].js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
extensions: ['.js', '.json'],
},
module: {
rules: [
// 處理圖片、GLTF、HDR
{
test: /\.(png|jpe?g|gif|hdr|gltf|glb)$/i,
type: 'asset/resource',
},
// 處理 GLSL
{
test: /\.(glsl|vs|fs)$/,
use: ['raw-loader', 'glslify-loader'],
},
// CSS
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
inject: 'body',
}),
],
devServer: {
static: path.join(__dirname, 'dist'),
compress: true,
port: 8080,
open: true,
hot: true,
},
optimization: {
splitChunks: {
chunks: 'all',
},
},
};
};
3.4 主程式範例
// src/index.js
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import metalImg from '@/assets/textures/metal.jpg';
import vertexShader from '@/shaders/vertex.glsl';
import fragmentShader from '@/shaders/fragment.glsl';
// 基礎設定
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(0, 2, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enablePan = false;
// 環境光
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
// 自訂材質 - 使用 GLSL
const customMaterial = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
uTime: { value: 0 },
uTexture: { value: new THREE.TextureLoader().load(metalImg) },
},
});
// 簡單幾何體
const box = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
customMaterial
);
scene.add(box);
// GLTF 載入
const gltfLoader = new GLTFLoader();
gltfLoader.load('/models/room.glb', (gltf) => {
gltf.scene.position.set(0, 0, -2);
scene.add(gltf.scene);
});
// 視窗調整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 動畫迴圈
const clock = new THREE.Clock();
function render() {
const elapsed = clock.getElapsedTime();
customMaterial.uniforms.uTime.value = elapsed;
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
此範例展示 Webpack 如何透過
raw-loader與glslify-loader直接匯入 GLSL 程式碼,並在ShaderMaterial中使用自訂 uniform。
4. 常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 / 最佳實踐 |
|---|---|---|
| 資源路徑錯誤 | Vite 會把 public 目錄視為根目錄,Webpack 則需要 file-loader 處理。 |
1. 使用絕對路徑 (/models/...) 於 public 中;2. 在 Webpack output.publicPath 設為 '/'。 |
| GLTF 加載失敗 | 模型內部的貼圖路徑相對於模型檔案,打包後路徑可能改變。 | 使用 GLTFLoader.setPath() 或在 Vite/Webpack 中設定 asset/resource 的 generator.filename 為 assets/[name][ext]。 |
| Shader 變數未同步 | 在開發時忘記更新 uniforms 會導致渲染錯誤。 |
把 uniforms 放在模組外層,並在 requestAnimationFrame 中統一更新。 |
| 開發時熱重載失效 | 某些 Three.js 內建模組(如 OrbitControls)會在 HMR 時保持舊的實例。 |
在 module.hot.dispose 中手動銷毀舊的控制器或渲染器,或使用 import.meta.hot(Vite)/ module.hot(Webpack)清理。 |
| 建置檔案過大 | 未啟用 Tree‑shaking,導致整個 Three.js 全部被打包。 | 確保 sideEffects: false 在 package.json,或在 webpack.config.js 中使用 mode: 'production',Vite 自動執行。 |
4.1 HMR 清理範例(Vite)
if (import.meta.hot) {
import.meta.hot.dispose(() => {
// 停止渲染迴圈、釋放 WebGL 資源
renderer.dispose();
controls.dispose();
// 移除 canvas
document.body.removeChild(renderer.domElement);
});
}
4.2 渲染器資源回收(Webpack)
if (module.hot) {
module.hot.dispose(() => {
renderer.forceContextLoss();
renderer.context = null;
});
}
5. 實際應用場景
產品展示網站
- 使用 Vite 快速開發,結合
GLTFLoader載入高品質模型,透過環境貼圖(HDR)提升真實感。 - 透過
code-splitting把每個產品頁面分割成獨立 chunk,減少首次下載大小。
- 使用 Vite 快速開發,結合
互動式教育平台
- Webpack 的多階段編譯(TS → Babel → WebGL)能確保程式碼在舊版瀏覽器仍能正常執行。
- 以
shader為核心的視覺化教材,可在webpack中加入glslify讓 GLSL 支援#include、#define。
AR/VR 雲端渲染
- Vite 的原生 ESM 可結合
three-mesh-ui、WebXR,在開發階段即時測試 XR 互動。 - 產出的靜態檔案可部署至 CDN,配合
service worker實作離線快取。
- Vite 的原生 ESM 可結合
資料視覺化儀表板
- Webpack 的
DefinePlugin讓環境變數(如 API 金鑰)在建置時注入,避免在前端程式碼中硬編碼。 - 使用
dynamic import()按需載入大型貼圖或模型,提升儀表板的載入速度。
- Webpack 的
總結
- Vite 與 Webpack 各有千秋:Vite 以開發速度見長、設定簡潔;Webpack 則在高度客製化與大型專案上更具彈性。
- 在 Three.js 專案中,最重要的是 資源管理(模型、貼圖、HDR)與 渲染效能(Shader、材質、光照)能夠透過打包工具自動化處理,減少手動錯誤與維護成本。
- 透過本文的 專案結構範例、設定說明、以及 常見陷阱 的對應方案,你可以快速把一個散亂的 Three.js demo,升級為 可維護、可擴充、具備 CI/CD 流程 的工程化專案。
下一步:將此專案加入 GitHub Actions,於每次 Pull Request 時自動執行
npm run build、npm run lint,確保所有提交的程式碼都符合最佳實踐。
祝你在 Three.js 的工程化旅程中,玩得開心、寫得順手! 🚀