js

js.Batch

返回一个批处理器(batcher),用于构建带全局代码分割、钩子与运行器配置灵活的 JavaScript 打包组。

签名
js.Batch [ID]
返回类型
js.Batcher

批次 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 方法返回一个具有下列结构的对象:

每个 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。
  • 存在一个已知的导入顺序问题。

在对这个新特性与不同库进行的大量测试中,我们并没有把该顺序问题视为麻烦。主要有两种情况:

  1. 导入的执行顺序不确定,参见这条评论
  2. 导入只有一种执行顺序,参见这条评论

很多人会说上述两种情况都属于代码坏味道。第一种在 Hugo 中有个简单的变通办法:把导入顺序写在一个单独的脚本里,并确保它较早传给 esbuild,例如放进一个名称在字母表中靠前的脚本组。

import './lib2.js';
import './lib1.js';

console.log('entrypoints-workaround.js');