js.Batch
返回一个批处理器(batcher),用于构建带全局代码分割、钩子与运行器配置灵活的 JavaScript 打包组。
批次 ID 用于创建该批次的基础目录,允许使用正斜杠。js.Batch 函数返回一个对象,其 API 结构如下:
Group
Group 方法接受一个 ID(string)作为参数,其中不能带斜杠。它返回一个对象,包含下列方法:
Script
Script 方法接受一个 ID(string)作为参数,其中不能带斜杠。它返回一个 OptionsSetter,可用于为该脚本设置脚本选项。
{{ with js.Batch "js/mybatch" }}
{{ with .Group "mygroup" }}
{{ with .Script "myscript" }}
{{ .SetOptions (dict "resource" (resources.Get "myscript.js")) }}
{{ end }}
{{ end }}
{{ end }}SetOptions 接受一个脚本选项映射。注意如果你希望该脚本由某个 Runner 处理,就需要设置 export 选项,使其与你想要传给运行器的内容一致(默认是 *)。
Instance
Instance 方法接受两个 string 参数 SCRIPT_ID 与 INSTANCE_ID,其中不能带斜杠。它返回一个 OptionsSetter,可用于为该实例设置参数选项。
{{ with js.Batch "js/mybatch" }}
{{ with .Group "mygroup" }}
{{ with .Instance "myscript" "myinstance" }}
{{ .SetOptions (dict "params" (dict "param1" "value1")) }}
{{ end }}
{{ end }}
{{ end }}SetOptions 接受一个参数选项映射。实例选项会以 JSON 形式传给同一组中的任何 Runner 脚本。
Runner
Runner 方法接受一个 ID(string)作为参数,其中不能带斜杠。它返回一个 OptionsSetter,可用于为该运行器设置脚本选项。
{{ with js.Batch "js/mybatch" }}
{{ with .Group "mygroup" }}
{{ with .Runner "myrunner" }}
{{ .SetOptions (dict "resource" (resources.Get "myrunner.js")) }}
{{ end }}
{{ end }}
{{ end }}SetOptions 接受一个脚本选项映射。
运行器会收到一份数据结构,其中包含该组的全部实例,并对所定义 export 的 JavaScript 导入保持实时绑定(live binding)。
运行器脚本的导出必须是一个函数,它接受一个参数,即该组的数据结构。一份组数据结构的 JSON 示例是:
{
"id": "leaflet",
"scripts": [
{
"id": "mapjsx",
"binding": JAVASCRIPT_BINDING,
"instances": [
{
"id": "0",
"params": {
"c": "h-64",
"lat": 48.8533173846729,
"lon": 2.3497416090232535,
"r": "map.jsx",
"title": "Cathédrale Notre-Dame de Paris",
"zoom": 23
}
},
{
"id": "1",
"params": {
"c": "h-64",
"lat": 59.96300872062237,
"lon": 10.663529183196863,
"r": "map.jsx",
"title": "Holmenkollen",
"zoom": 3
}
}
]
}
]
}下面是一个用 React 渲染元素的运行器脚本示例。注意导出名(default)必须与脚本选项中的 export 选项一致(default 是运行器脚本的默认值)。本页示例的可运行版本见这个 js.Batch 演示仓库。
import * as ReactDOM from 'react-dom/client';
import * as React from 'react';
export default function Run(group) {
console.log('Running react-create-elements.js', group);
const scripts = group.scripts;
for (const script of scripts) {
for (const instance of script.instances) {
/* This is a convention in this project. */
let elId = `${script.id}-${instance.id}`;
let el = document.getElementById(elId);
if (!el) {
console.warn(`Element with id ${elId} not found`);
continue;
}
const root = ReactDOM.createRoot(el);
const reactEl = React.createElement(script.binding, instance.params);
root.render(reactEl);
}
}
}Config
返回一个 OptionsSetter,可用于为该批次设置构建选项。
这些选项与 js.Build 的大体相同,但要注意:
targetPath是自动设置的(可能会有多个输出)。format必须是esm,目前这是唯一支持代码分割的格式。params会在脚本中以@params/config命名空间的形式可用。这样你就可以同时导入 Script 或 Runner 的参数,以及 Config 的参数:
import * as params from "@params";
import * as config from "@params/config";批次的 Config 可以在任何模板(包括 短代码 模板)中设置,但只会设置一次(先设置者生效):
{{ with js.Batch "js/mybatch" }}
{{ with .Config }}
{{ .SetOptions (dict
"target" "es2023"
"format" "esm"
"jsx" "automatic"
"loaders" (dict ".png" "dataurl")
"minify" true
"params" (dict "param1" "value1")
)
}}
{{ end }}
{{ end }}选项
构建选项
format- (
string) 目前esbuild只支持以esm作为代码分割的输出格式。 defines- (
map) 这个选项让你定义一组在构建时执行的字符串替换。它必须是一个映射,其中每个键都会被它的值替换。{{ $defines := dict "process.env.NODE_ENV" `"development"` }} drop- (0.144.0 新增)
- (
string) 在构建前改写源码,丢弃特定构造:取debugger或console之一。 - 参见 https://esbuild.github.io/api/#drop
externals- (
slice) 外部依赖。可以用它裁掉确定不会执行到的依赖。参见 https://esbuild.github.io/api/#external。 inject- (
slice) 这个选项让你把某个全局变量自动替换为另一个文件中的导入。其中的路径必须相对于assets。参见 https://esbuild.github.io/api/#inject。 JSX- (
string) 如何处理与转换 JSX 语法,取transform、preserve、automatic之一。默认是transform。其中automatic转换由 React 17+ 引入,会自动导入所需的 JSX 辅助函数。参见 https://esbuild.github.io/api/#jsx。 JSXImportSource- (
string) 从哪个库自动导入 JSX 辅助函数,仅在JSX为automatic时有效。指定的库需要通过 npm 安装,并暴露相应的导出。参见 https://esbuild.github.io/api/#jsx-import-source。JSX与JSXImportSource搭配使用,可以在 Preact 这类非 React 的 JSX 库中省去手工导入:{{ $js := resources.Get "js/main.jsx" | js.Build (dict "JSX" "automatic" "JSXImportSource" "preact") }}上面的配置下,使用 Preact 组件与 JSX 时不必每次都导入
h与Fragment:import { render } from 'preact'; const App = () => <>Hello world!</>; const container = document.getElementById('app'); if (container) render(<App />, container); loaders- (0.140.0 新增)
- (
map) 为给定文件类型配置加载器后,就可以用import语句或require调用加载该类型文件。例如把.png扩展名配置为 data URL 加载器,导入.png文件就会得到包含该图片内容的数据 URL。可用的加载器有none、base64、binary、copy、css、dataurl、default、empty、file、global-css、js、json、jsx、local-css、text、ts、tsx。参见 https://esbuild.github.io/api/#loader。 minify- (
bool) 是否压缩生成的 JS 代码。默认是false。 params- (
map或slice) 可以在 JS 文件中以 JSON 形式导入的参数,例如:{{ $js := resources.Get "js/main.js" | js.Build (dict "params" (dict "api" "https://example.org/api")) }}然后在 JS 文件中:
import * as params from '@params';注意它适合配置项之类的小数据;数据较大时,应把文件放入或挂载到
assets中直接导入。 platform- (0.140.0 新增)
- (
string) 取browser、node、neutral之一。默认是browser。参见 https://esbuild.github.io/api/#platform。 shims- (
map) 这个选项让你把某个组件替换为另一个。常见用法是生产环境通过 shim 从 CDN 加载 React 这类依赖,而开发环境仍使用打包进来的完整node_modules依赖:{{ $shims := dict "react" "js/shims/react.js" "react-dom" "js/shims/react-dom.js" }} {{ $js = $js | js.Build dict "shims" $shims }}shim 文件的内容可能形如:
// js/shims/react.js module.exports = window.React;// js/shims/react-dom.js module.exports = window.ReactDOM;这样配置之后,下面这些导入在两种场景下都能正常工作:
import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; sourceMap- (
string) 要生成的 source map 类型,取external、inline、linked、none之一。默认是none。linked与external的 source map 会写到目标路径,文件名为输出文件名加 “.map”;取linked时还会在输出文件中写入sourceMappingURL。 sourcesContent- (0.140.0 新增)
- (
bool) 是否在 source map 中包含源文件的内容。默认是true。 target- (
string) 语言目标,取es5、es2015、es2016、es2017、es2018、es2019、es2020、es2021、es2022、es2023、es2024、es2025、esnext之一。默认是esnext。
脚本选项
resource- 要构建的资源。可以是文件资源,也可以是虚拟资源。
export- 运行器要绑定到的导出。设为
*表示导出整个命名空间。Runner 脚本默认是default,其他脚本默认是*。 importContext- 用于解析导入的附加上下文。Hugo 总是先检查它,再回退到
assets与node_modules。一个常见用法是解析页面包内的导入。参见导入上下文。 params- 会以 JSON 形式传给脚本的参数映射。这些参数会绑定到
@params命名空间:import * as params from '@params';
参数选项
params- 会以 JSON 形式传给脚本的参数映射。
导入上下文
默认情况下,Hugo 会首先尝试解析 assets 目录中的导入,找不到时再交给 esbuild 解析(例如从 node_modules 中解析)。importContext 选项可用于设置解析导入时的第一个上下文。一个常见用法是解析页面包内的导入。
{{ $common := resources.Match "/js/headlessui/*.*" }}
{{ $importContext := (slice $.Page ($common.Mount "/js/headlessui" ".")) }}你可以传入任何实现了 Resource.Get 的对象。传入切片即可设置多个上下文。
上例用 Resources.Mount 把 assets 中的某个目录相对于页面包来解析。
OptionsSetter
OptionsSetter 是一种特殊的对象,只会返回一次。也就是说,你应该用 with 把它包起来:
{{ with .Script "myscript" }}
{{ .SetOptions (dict "resource" (resources.Get "myscript.js"))}}
{{ end }}Build
Build 方法返回一个具有下列结构的对象:
- Groups(map)
每个 Resource 的媒体类型要么是 application/javascript,要么是 text/css。
在模板中,你通常会处理某个给定 ID 的组(例如当前 section 的脚本)。由于构建是并发进行的,这需要在 templates.Defer 块中完成:
{{ $group := .group }}
{{ with (templates.Defer (dict "key" $group "data" $group )) }}
{{ with (js.Batch "js/mybatch") }}
{{ with .Build }}
{{ with index .Groups $ }}
{{ range . }}
{{ $s := . }}
{{ if eq $s.MediaType.SubType "css" }}
<link href="{{ $s.RelPermalink }}" rel="stylesheet" />
{{ else }}
<script src="{{ $s.RelPermalink }}" type="module"></script>
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}已知问题
在 esbuild 代码分割特性的官方文档中,页首有一段警告说明。这两个问题是:
esm是目前唯一实现的输出格式。这意味着它在旧式浏览器中无法工作。参见 caniuse。- 存在一个已知的导入顺序问题。
在对这个新特性与不同库进行的大量测试中,我们并没有把该顺序问题视为麻烦。主要有两种情况:
很多人会说上述两种情况都属于代码坏味道。第一种在 Hugo 中有个简单的变通办法:把导入顺序写在一个单独的脚本里,并确保它较早传给 esbuild,例如放进一个名称在字母表中靠前的脚本组。
import './lib2.js';
import './lib1.js';
console.log('entrypoints-workaround.js');