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.scssassets/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 installnpm ci 下载依赖到本地 node_modules 目录。
  4. package.hugo.jsonpackage.jsonpackage-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 以及监听事件。完全异步下载和执行,不会阻塞页面加载过程,不用等待页面加载完成,没有依赖关系,没有顺序要求。
(啥?你问如果 asyncdefer 同时使用是什么情况?答:你为什么要那么做?)

内联模块

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。