HTML SCRIPT 标签的理解

介绍如何在 Hugo 里处理 JavaScript 内容,以及个人对 <script> 标签的理解。

背景

我的博客是用 Hugo 搭建的,主题是 hugo-theme-stack。经过一段时间的摸索,对 Hugo 和 hugo-theme-stack 都有了一定的了解。
hugo-theme-stack 简洁大方同时又预留了扩展性,Hugo 构建流水线支持处理 Sass 和 TypeScript/JavaScript。

说到扩展性,最简单地就是通过自定义 CSS 和 Javascript 来对站点进行美化或改造。hugo-theme-stack 预留了 assets/scss/custom.scss 和 assets/ts/custom.ts 分别用于自定义 CSS 和 TypeScript/JavaScript。

这里并不谈论如何美化或改造站点,只是记录在此过程中对 Hugo 集成 TypeScript/Javascript,以及页面加载 JavaScript 的一些理解。

JavaScript 构建

官方文档 基本上包含了所有 TypeScript/JavaScript 处理的内容。

基本使用方法

以 hugo-theme-stack 主题的代码为例:

1
2
3
4
{{- $opts := dict "minify" hugo.IsProduction -}}
{{- $script := resources.Get "ts/main.ts" | js.Build $opts -}}

<script type="text/javascript" src="{{ $script.RelPermalink }}" defer></script>

在主题的目录下有 assets/ts/main.ts 文件,主题自身的所有 JavaScript 内容都在这里。
| 表示管道,跟 Linux Shell 的管道命令类似,| 前面的执行结果做为后面处理的输入,支持任意级联。输入和输出都是 resource.Resource。
Hugo 使用 js.Build 编译和处理 TypeScript / JavaScript 文件。Hugo 本身没有外部依赖,不需要依赖本地环境安装 tsc。也正是如此,Hugo 只支持少量的选项。
如果没有指定 targetPath 参数,默认使用与输入路径相同的输出路径,比如这个例子会输入到 public/ts/main.js。这个例子对于生产环境还会压缩生成的 JS 文件。需要注意 target 选项的默认值是 esnext,基本上不会对 JS 语法做任何转译了。如果有兼容性考虑的话,Hugo 也支持 Babel。个人博客没有兼容性考量,使用默认设置即可。

最终在HTML页面上是这样的:

1
<script type="text/javascript" src="/ts/main.js" defer>

主题自身的 JS 内容一般不会有变化,这样的结果不会有什么。但是对于自定义 JS 可能就有问题了。

1
<script type="text/javascript" src="/ts/custom.js" defer>

在美化或改造站点的过程中,免不了要引入 JS 代码。因为URL始终保持不变,这样在发布的时候就可能会有浏览器缓存问题。解决这个问题的方法就是在 JS 文件名上添加哈希值,这样每次内容变化都会导致 URL 变化,就不会有缓存问题了。同时还能用 integrity 属性增强安全性。

1
2
3
{{- $script := resources.Get "ts/custom.ts" | js.Build $opts | fingerprint -}}

<script type="text/javascript" src="{{ $script.RelPermalink }}" integrity="{{ $script.Data.Integrity }}" defer></script>

最终在HTML页面上是这样的:

1
<script type="text/javascript" src="/ts/custom.f674679881e30bf214f10acf02f08fb4109e73dab259b4654b247ec11e1fd4c1.js" integrity="sha256-9nRnmIHjC/IU8QrPAvCPtBCec9qyWbRlSyR&#43;wR4f1ME=" defer>

集成 npm

npm 已经是最基础和最广泛使用的 JS 包管理器。如果我们的 TypeScript/JavaScript 代码需要引入第三方依赖,可以使用 npm。

  1. 首先在站点根目录执行 hugo mod npm pack,Hugo 会生成 package.hugo.json。跟使用 package.json 一样,把需要用到的依赖添加进去。
  2. 再次执行 hugo mod npm pack,Hugo 会分析并收集所有 Hugo 模块的 package.hugo.json 文件,最终生成 package.json 文件。
  3. 接着执行 npm install 或 npm ci 下载依赖到本地 node_modules 目录。
  4. 把 package.hugo.json,package.json 和 package-lock.json 都添加到 Git 库。
  5. 在 TypeScript/JavaScript 代码里面正常使用 import 即可。例如:import * as React from 'react'。
  6. 用 npm ci && hugo 生成站点并发布。注意构建环境比如 Github Actions 需要安装 Node.js。

HTML SCRIPT 标签

上面讲了如何在 Hugo 项目中使用 JS。这里讲一下对 <script> 标签的理解。

<script> 有个 type 属性,如果不指定,默认是 type="text/javascript",这也是最常用的设置,没有任何兼容性问题。

内联

1
2
3
<script>
    // code here
</script>

这种方法一般不使用了,更合理的做法是把 JS 代码归类到合适的 JS 文件里面。

外联

1
<script src="https://mydomain.com/x.js"></script>

这种方法是最常见的用法。不过这里会阻塞页面渲染,即浏览器解析到这个标签时会立即同步下载和执行引用的 JS 文件,再接着解析后续部分。这就会导致一段时间的空白页面。一般是放到 <body> 的尾部,这样可以减少等待。

延迟

1
<script src="https://mydomain.com/x.js" defer></script>

这种方法是最推荐的用法。defer 属性告诉浏览器异步下载引用的 JS 文件,不再阻塞页面解析。页面解析完成且 DOM TREE 就绪时才会执行 JS 文件。如果页面中有多个 defer 文件,会按照在页面中出现的顺序依次执行。
(啥?你问如果 defer 用于内联是什么情况?答:你为什么要那么做?)

异步

1
<script src="https://thirdparty.com/y.js" async></script>

这种方法适用于外部 JS 文件,比如监控、统计、追踪用途的 JS。这类 JS 无须操作页面本身的DOM,一般操作 Cookie 以及监听事件。完全异步下载和执行,不会阻塞页面加载过程,不用等待页面加载完成,没有依赖关系,没有顺序要求。
(啥?你问如果 async 和 defer 同时使用是什么情况?答:你为什么要那么做?)

内联模块

1
2
3
<script type="module">
    import { o } from 'x.js';
</script>

这种方法是 ES6/ES2015 引入的模块语法。只有较新的浏览器才支持 type="module",旧版浏览器会忽略这个标签内的内容。
这里不讲 import 具体用法,一般不这样使用。常见用法是现代 JS 语法编程,并通过打包工具(比如 webpack)将整个页面的 JS 资源转译打包成一个 JS 文件。

外联模块

1
<script type="module" src="https://mydomain.com/x.js" defer></script>

这种用法允许在 x.js 里面嵌套使用 import 语法。但整体而言,这个用法没有为当前作用域引入任何符号。如果 x.js 没有用到 import,就相当于普通地下载并执行这个 JS 文件,和 type="text/javascript" 一样的效果。

区别在于:

  • 只有较新的浏览器才支持 type="module"。利用这个特性可以为较新的浏览器和老旧浏览器加载不同的 JS 文件。

    1
    2
    
    <script type="module" src="https://mydomain.com/x.js" defer></script>
    <script nomodule src="https://mydomain.com/y.js" defer></script>
    

    较新的浏览器能识别 type="module" 和 nomodule,所以会忽略 y.js,只会使用 x.js。
    老旧浏览器不认识 type="module",所以会忽略 x.js,只会使用 y.js 并忽略 nomodule 属性。
    例如:hugo-mod-jslibs/alpinejs。

  • 如果想要在浏览器引入全局对象,需要在JS模块里面为 window 对象设置相应的属性。
    例如 Alpinejs 和各种 Polyfill。

使用 Hugo 构建
主题 Stack 由 Jimmy 设计