笔记 - 模块化JS

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:

  1. 如果使用 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语句按需加载模块文件。

      这种方式适合小型项目或不需要构建工具的场景。

  2. 如果使用 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 打包模块内容适合大型项目,尤其是需要优化性能和减少资源加载的场景。

总结

  1. 如果不是用 Hugo 搭建网站,而是自己写 HTML、CSS、JS 文件,使用 type=“module” 时,浏览器会动态加载模块文件,不会自动合并或复制内容。

    • 浏览器会缓存这些模块文件,如果多个页面引用同一个模块文件,后续请求可以直接从缓存读取,减少加载时间和流量消耗。

    • 浏览器缓存是基于文件的 URL 和 HTTP 缓存头(如 Cache-Control)的。如果文件内容变化但 URL 不变,可能会导致缓存失效问题。通常建议在文件名中加入哈希值(如 utils.abc123.js)来解决。

    • 并行加载:浏览器可以并行加载多个模块文件(HTTP/2 下效果更好),但过多的请求仍可能影响性能(尤其是 HTTP/1.1 环境下)

  2. 如果使用 Hugo 搭建网站,并使用 js.Build,Hugo 会将模块内容打包到目标文件中,浏览器只会加载打包后的文件, 减少浏览器请求次数。

    • 每个页面的 JS 文件是独立的,可能包含重复代码(如多个页面都引用了 utils.js 的函数,这些函数会被复制到每个打包后的文件中)。
  3. 如何选择适合的方式?

场景推荐方式
小型项目直接使用 type="module",让浏览器动态加载模块文件。
大型项目(使用 Hugo)使用 js.Build 打包模块内容,减少浏览器请求数量,提高性能。
需要按需加载使用 type="module" 和动态导入(import()),按需加载模块。
需要优化性能使用构建工具(如 Hugo 的 js.Build)打包模块内容,减少资源加载。