背景
我的博客是用 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
主题的代码为例:
|
|
在主题的目录下有 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页面上是这样的:
|
|
主题自身的 JS 内容一般不会有变化,这样的结果不会有什么。但是对于自定义 JS 可能就有问题了。
|
|
在美化或改造站点的过程中,免不了要引入 JS 代码。因为URL始终保持不变,这样在发布的时候就可能会有浏览器缓存问题。解决这个问题的方法就是在 JS 文件名上添加哈希值,这样每次内容变化都会导致 URL 变化,就不会有缓存问题了。同时还能用 integrity
属性增强安全性。
|
|
最终在HTML页面上是这样的:
|
|
集成 npm
npm
已经是最基础和最广泛使用的 JS 包管理器。如果我们的 TypeScript/JavaScript 代码需要引入第三方依赖,可以使用 npm
。
- 首先在站点根目录执行
hugo mod npm pack
,Hugo 会生成package.hugo.json
。跟使用package.json
一样,把需要用到的依赖添加进去。 - 再次执行
hugo mod npm pack
,Hugo 会分析并收集所有 Hugo 模块的package.hugo.json
文件,最终生成package.json
文件。 - 接着执行
npm install
或npm ci
下载依赖到本地node_modules
目录。 - 把
package.hugo.json
,package.json
和package-lock.json
都添加到 Git 库。 - 在 TypeScript/JavaScript 代码里面正常使用
import
即可。例如:import * as React from 'react'
。 - 用
npm ci && hugo
生成站点并发布。注意构建环境比如 Github Actions 需要安装 Node.js。
HTML SCRIPT 标签
上面讲了如何在 Hugo 项目中使用 JS。这里讲一下对 <script>
标签的理解。
<script>
有个 type
属性,如果不指定,默认是 type="text/javascript"
,这也是最常用的设置,没有任何兼容性问题。
内联
|
|
这种方法一般不使用了,更合理的做法是把 JS 代码归类到合适的 JS 文件里面。
外联
|
|
这种方法是最常见的用法。不过这里会阻塞页面渲染,即浏览器解析到这个标签时会立即同步下载和执行引用的 JS 文件,再接着解析后续部分。这就会导致一段时间的空白页面。一般是放到 <body>
的尾部,这样可以减少等待。
延迟
|
|
这种方法是最推荐的用法。defer
属性告诉浏览器异步下载引用的 JS 文件,不再阻塞页面解析。页面解析完成且 DOM TREE 就绪时才会执行 JS 文件。如果页面中有多个 defer
文件,会按照在页面中出现的顺序依次执行。
(啥?你问如果 defer
用于内联是什么情况?答:你为什么要那么做?)
异步
|
|
这种方法适用于外部 JS 文件,比如监控、统计、追踪用途的 JS。这类 JS 无须操作页面本身的DOM,一般操作 Cookie 以及监听事件。完全异步下载和执行,不会阻塞页面加载过程,不用等待页面加载完成,没有依赖关系,没有顺序要求。
(啥?你问如果 async
和 defer
同时使用是什么情况?答:你为什么要那么做?)
内联模块
|
|
这种方法是 ES6/ES2015 引入的模块语法。只有较新的浏览器才支持 type="module"
,旧版浏览器会忽略这个标签内的内容。
这里不讲 import
具体用法,一般不这样使用。常见用法是现代 JS 语法编程,并通过打包工具(比如 webpack
)将整个页面的 JS 资源转译打包成一个 JS 文件。
外联模块
|
|
这种用法允许在 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。