Day 17 談的是程式碼「怎麼被組織」成一個個模組;今天要接著談這些模組最終要怎麼被送到瀏覽器裡執行——這背後牽涉到一段工具演進的歷史,也說明了為什麼現代前端專案幾乎都離不開一個叫做 Bundler 的角色。
<script> 標籤時代的問題說起在 ES Modules 普及之前,最原始的做法是在 HTML 裡用一堆 <script> 標籤依序載入每個檔案。這種做法有幾個明顯的問題:所有變數都暴露在全域作用域,容易互相覆蓋;檔案之間的依賴關係,完全要靠開發者自己記得「哪個要先載入」,<script> 標籤的順序寫錯,程式就會壞掉;而且瀏覽器必須為每一個檔案各自發一次網路請求,檔案一多,載入速度就會變慢。
定義:模組打包工具(Bundler)會從一個或多個「進入點(entry point)」開始,沿著 import / export 的關係,建立出一張完整的「依賴圖(dependency graph)」,再把所有用到的檔案合併、輸出成一個或少數幾個檔案(bundle),交給瀏覽器載入。
附帶的優化能力:
import 語句,把程式碼裡實際上沒有被用到的部分移除,縮小最終輸出的檔案大小。早期的 Webpack 需要先把整個依賴圖打包完成,開發階段每次改程式碼都要重新打包一次,專案一大,等待時間就會拉長。近年的 Vite 等工具則改用不同的策略:開發階段直接利用瀏覽器原生支援的 ES Modules,讓瀏覽器自己去請求用到的模組,搭配 esbuild 等工具做即時轉譯,大幅縮短開發時的等待時間;正式上線打包時,才用 Rollup 之類的工具做完整的打包與優化。
假設代購 App 隨著功能增加,商品列表頁、訂單頁、會員中心頁各自的程式碼都被打包進同一個巨大的 bundle 裡,使用者不管造訪哪一頁,都要先下載整包程式碼,首次進站的速度會因此變慢。透過 Code Splitting,可以依照路由拆分:
// 沒有做 Code Splitting:所有頁面的程式碼都打包在一起,首次載入就要下載全部
import ProductListPage from './pages/ProductListPage.js';
import OrderPage from './pages/OrderPage.js';
import ProfilePage from './pages/ProfilePage.js';
// 用動態 import 讓 Bundler 自動拆成多個檔案,每個頁面各自一包
const ProductListPage = () => import('./pages/ProductListPage.js');
const OrderPage = () => import('./pages/OrderPage.js');
const ProfilePage = () => import('./pages/ProfilePage.js');
// 使用者造訪商品列表頁時,只會下載 ProductListPage 對應的那一包,
// OrderPage、ProfilePage 的程式碼要等真的切換過去才會被載入
Day 17 的 ES Modules 解決的是「程式碼怎麼被組織」,今天的 Bundler 解決的則是「這些組織好的模組怎麼被有效率地送到使用者手上」。從最早混亂的 <script> 標籤,到打包成單一巨大檔案的 Webpack,再到善用瀏覽器原生 ES Modules、只在需要時才轉譯打包的 Vite——這段演進的主軸,始終是在「開發體驗」與「使用者實際下載到的程式碼量」之間,找到更好的平衡點。