在 Hugo 中實現 Code Splitting | Code Splitting in Hugo
Code splitting(chunk)讓你把大包資源拆成可複用的小包,讓每個頁面可共用底層庫,網站更新時用戶也只需要下載對應的 JS 而不是整包重新下載。本文介紹如何在 Hugo 做到 code splitting。
js.Batch
Hugo 用 esbuild 支援 code splitting,API 是 js.Batch,範例 hugojsbatchdemo
{{ $group := .group }}
{{ with (templates.Defer (dict "key" $group "data" $group )) }}
{{ with (js.Batch "js/mybatch") }}
{{ with .Build }}
{{ with index .Groups $ }}
{{ range . }}
{{ $s := . }}
{{ if eq $s.MediaType.SubType "css" }}
{{ if hugo.IsDevelopment }}
<link rel="stylesheet" href="{{ .RelPermalink }}" />
{{ else }}
{{ with . | fingerprint }}
<link
rel="stylesheet"
href="{{ .RelPermalink }}"
integrity="{{ .Data.Integrity }}"
crossorigin="anonymous" />
{{ end }}
{{ end }}
{{ else }}
{{ if hugo.IsDevelopment }}
<script src="{{ $s.RelPermalink }}" type="module"></script>
{{ else }}
{{ with . | fingerprint }}
<script
src="{{ .RelPermalink }}"
type="module"
integrity="{{ .Data.Integrity }}"
crossorigin="anonymous"></script>
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
這裡的意思是用 templates.Defer 標記這段使用佔位符保留,等到網站全部建立完成已經寫入之後,蒐集完所有資料再二度寫入這個區塊。然後就是裡面的 .Batch/.Build/.Groups API,也不太需要管啥意思,因為文檔自己都寫不清楚照做就對了。最後把 CSS/JS 分開輸出。
importmap
另外一種方式更直觀,是瀏覽器原生支援的 importmap。
但是 importmap 必須要使用 ESM import 語法,這時你的 JS 就無法直接用 js.Build 因為 js.Build 預設會把所有 import 的內容全部打包到單一檔案,就達不到 splitting 的目標了。解決方式是使用 externals 參數避免被一同打包,使用起來像這樣
assets/js/lib-a.js
assets/js/lib-b.js
assets/js/main.js
layouts/index.html
assets/js/lib-a.js
export function helloA(target) {
const p = document.createElement('p');
p.textContent = 'hello world from lib A';
target.appendChild(p);
}
assets/js/lib-b.js
export function helloB(target) {
const p = document.createElement('p');
p.textContent = 'hello world from lib B';
target.appendChild(p);
}
assets/js/main.js
import { helloA } from 'lib-a';
import { helloB } from 'lib-b';
const output = document.getElementById('output');
document.getElementById('btn-a').addEventListener('click', () => {
helloA(output);
});
document.getElementById('btn-b').addEventListener('click', () => {
helloB(output);
});
document.getElementById('btn-reset').addEventListener('click', () => {
output.innerHTML = '';
});
layouts/home.html
{{ $opts := dict "format" "esm" }}
{{/* 把兩個底層庫各自獨立打包,並加上 fingerprint */}}
{{ $libA := resources.Get "js/lib-a.js" | js.Build $opts | fingerprint }}
{{ $libB := resources.Get "js/lib-b.js" | js.Build $opts | fingerprint }}
{{/* 主程式:把 lib-a、lib-b 標記為 external,不打包進來 */}}
{{ $mainOpts := dict
"format" "esm"
"externals" (slice "lib-a" "lib-b")
}}
{{ $main := resources.Get "js/main.js" | js.Build $mainOpts | fingerprint }}
<!DOCTYPE html>
<html>
<head>
<script type="importmap">
{
"imports": {
"lib-a": "{{ $libA.RelPermalink }}",
"lib-b": "{{ $libB.RelPermalink }}"
}
}
</script>
</head>
<body>
<button id="btn-a">Run lib A</button>
<button id="btn-b">Run lib B</button>
<button id="btn-reset">Reset</button>
<div id="output"></div>
<script type="module" src="{{ $main.RelPermalink }}"></script>
</body>
</html>
總結
- js.Batch 繁瑣不直觀
- importmap 需要手動自己維護但是簡單明確
- 只要使用 js.Build,就一定要使用 externals 才能 splitting