TOC
起因
之前创建Hugo网站的时候,我想着一个页面就一个JS文件,这样可以减少浏览器请求的数量,提高页面加载性能。事实上我也确实是这样做的,将所需的各个具有不同功能的js文件全部链接到一起组合成一个文件。但是以前的做法存在一个问题:不相关的内容也都放进去了。
比如,某个js文件写了10个函数,某个页面需要用到其中一个函数,之前的做法是整个js文件内容都打包连接(10个函数都放进去了),这样无疑增大了文件体积,浪费了流量。
思考
既然代码可以也应该复用,那么把一个功能写成一个文件或者函数,以后哪里需要了直接去调用它就是很正常的事了,而且修改起来也简单,只需要修改源文件,调用这个源文件的其他文件就不用再一个个的去改动了。
而且浏览器有缓存功能,不同页面加载一个相同的文件,只需要最开始的时候加载一次,后边直接用缓存的文件就可以了,减少了加载时间和流量消耗。
正常情况下,只要指明type="module", export, import就可以了,比如我在/Users/xdl/Documents/github/blog/assets/js/posts/note-module-js/001.js:
export function greet(name) {
console.log(`Hello, ${name}!`);
}
export function hello(name) {
console.log(`Hello, ${name}!`);
}
然后/Users/xdl/Documents/github/blog/assets/js/posts/note-module-js/002.js:
// 调用一个函数
import { greet } from 'js/posts/note-module-js/001.js';
greet("World");
// 或者更多函数
import { greet, hello } from 'js/posts/note-module-js/001.js';
greet("World");
hello("World");
// 或者全部
import * as utils from 'js/posts/note-module-js/001.js';
utils.greet('World');
utils.hello('World');
在需要使用这个功能的页面的<head>中我们这样加上:
<script type="module" src="js/posts/note-module-js/001.js"></script>
<script type="module" src="js/posts/note-module-js/002.js"></script>
理论上这样就可以正常实现我们想要的功能了,模块化调用,也就是说网页会正常加载这两个文件,文件的内容不会发生任何变化,但是可以正常调用。
Hugo中模块化JS
本来上个部分就说清了JS Module,但是我注意到在Hugo中使用js.Build这个功能有着非常不一样的地方。
只要使用了js.Build,那么你引入的内容会被复制到当前的JS文件里,最后只有一个JS文件存在,这里详细记录下整个过程:
首先说明下,我构建的这个网站的js文件区分main.js 和页面自定义的 page.css文件,这样做的好处在于: main.js文件的内容时全站通用,所以单独保存成一个文件有利于浏览器缓存,页面自定义的js文件单独存在,这样所有文件都区分开,但是又可以各自import,实现了模块化。
{{- /* 加载全局 JS */}}
{{- with resources.Get "js/main.js" }}
{{- if eq hugo.Environment "development" }}
{{- with . | js.Build (dict "target" "es2018") }}
<script type="module" src="{{ .RelPermalink }}"></script>
{{- end }}
{{- else }}
{{- $opts := dict "minify" true "target" "es2018" }}
{{- with . | js.Build $opts | fingerprint }}
<script type="module" src="{{ .RelPermalink }}" integrity="{{- .Data.Integrity }}" crossorigin="anonymous"></script>
{{- end }}
{{- end }}
{{- end }}
{{- /* 加载页面自定义 JS */}}
{{- if .Params.js }}
{{- range .Params.js }}
{{- with resources.Get . }}
{{- if eq hugo.Environment "development" }}
{{- with . | js.Build (dict "target" "es2018") }}
<script type="module" src="{{ .RelPermalink }}"></script>
{{- end }}
{{- else }}
{{- $opts := dict "minify" true "target" "es2018" }}
{{- with . | js.Build $opts | fingerprint }}
<script type="module" src="{{ .RelPermalink }}" integrity="{{- .Data.Integrity }}" crossorigin="anonymous"></script>
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
如前边所说,目前本地文件结构如下:
js
| - main.js
| - posts/
| - note-module-js/
| - 001.js
| - 002.js
我在这篇文章的的头部文件(front matter)写了:
---
date: '2025-03-23T20:48:37+08:00'
title: '笔记 - 模块化JS'
toc: true
js: ['js/posts/note-module-js/002.js']
---
你可以进入浏览器的开发者工具(Mac 上的谷歌浏览器的快捷键是: ⌥⌘I), 在Console下看到这段程序的实际运行结果:显示Hello, World!
你还可以在Sources面板下打开js文件夹,应该是以下目录:
js
| - main.js
| - posts/
| - note-module-js/
| - 002.js
这里是没有 001.js这个文件的,你点击 002.js 这个文件,内容应该是:
(() => {
// ns-hugo-imp:/Users/xdl/Documents/github/blog2/assets/js/posts/note-module-js/001.js
function greet(name) {
console.log(`Hello, ${name}!`);
}
// <stdin>
greet("World");
})();
说明: 也许你看到的不太一样,因为我现在写的时候是在本地预览的,没有指纹(一长串字母数字, hash, 用以进行区分),放到网站上的时候是处理后的文件,应该有指纹, 并且可能没有注释了。
看到了吗,使用js.Build之后,它自动把import相关的内容给复制过来了,而不是根据文件内容自己引入。
基于此,我问了deepseek:
如果使用 Hugo 搭建网站,不使用 js.Build,或者就是自己直接写 HTML、CSS、JS 文件:
在这种情况下,如果你使用 type=“module”,浏览器的行为如下:
动态加载模块:
浏览器会根据 import 语句动态加载模块文件。
浏览器会加载
js/posts/note-module-js/002.js,然后根据import语句再去加载js/posts/note-module-js/001.js。不会自动合并或复制内容:
浏览器不会自动将import的内容复制到
js/posts/note-module-js/002.js中。每个JS文件都会保持独立,完全按照你写的内容加载和执行。
按需加载:
浏览器会根据import语句按需加载模块文件。
这种方式适合小型项目或不需要构建工具的场景。
如果使用 Hugo 搭建网站,并使用 js.Build:
在这种情况下,Hugo 的 js.Build 会改变浏览器的行为:
打包模块内容:
js.Build 会将 import 的模块内容(如
js/posts/note-module-js/001.js)打包到目标文件(如js/posts/note-module-js/002.js)中。减少浏览器请求:
浏览器只会加载打包后的文件(如
js/posts/note-module-js/002.js),而不会单独加载js/posts/note-module-js/001.js。这种方式可以减少浏览器请求的数量,提高页面加载性能。
适合大型项目:
使用 js.Build 打包模块内容适合大型项目,尤其是需要优化性能和减少资源加载的场景。
总结
如果不是用 Hugo 搭建网站,而是自己写 HTML、CSS、JS 文件,使用 type=“module” 时,浏览器会动态加载模块文件,不会自动合并或复制内容。
浏览器会缓存这些模块文件,如果多个页面引用同一个模块文件,后续请求可以直接从缓存读取,减少加载时间和流量消耗。
浏览器缓存是基于文件的 URL 和 HTTP 缓存头(如 Cache-Control)的。如果文件内容变化但 URL 不变,可能会导致缓存失效问题。通常建议在文件名中加入哈希值(如 utils.abc123.js)来解决。
并行加载:浏览器可以并行加载多个模块文件(HTTP/2 下效果更好),但过多的请求仍可能影响性能(尤其是 HTTP/1.1 环境下)
如果使用 Hugo 搭建网站,并使用 js.Build,Hugo 会将模块内容打包到目标文件中,浏览器只会加载打包后的文件, 减少浏览器请求次数。
- 每个页面的 JS 文件是独立的,可能包含重复代码(如多个页面都引用了 utils.js 的函数,这些函数会被复制到每个打包后的文件中)。
如何选择适合的方式?
| 场景 | 推荐方式 |
|---|---|
| 小型项目 | 直接使用 type="module",让浏览器动态加载模块文件。 |
| 大型项目(使用 Hugo) | 使用 js.Build 打包模块内容,减少浏览器请求数量,提高性能。 |
| 需要按需加载 | 使用 type="module" 和动态导入(import()),按需加载模块。 |
| 需要优化性能 | 使用构建工具(如 Hugo 的 js.Build)打包模块内容,减少资源加载。 |