首页
/ 深入解析 Glance custom-api 组件模板:把任意 JSON API 变成仪表盘组件

深入解析 Glance custom-api 组件模板:把任意 JSON API 变成仪表盘组件

2026-09-05 14:50:36作者:邓越浪Henry

Glance 的 custom-api 组件允许你用一段 Go 模板把任意 HTTP API 的响应直接渲染成仪表盘上的一个组件,本文以仓库中的模板参考文档 docs/custom-api.md 为主线,系统讲解模板上下文、数据访问语法、条件与排序、时间处理、多请求编排等全部官方示例,并结合 组件实现源码 说明每个语法背后的真实行为,读完即可独立编写、调试和复用任意结构的 custom-api 组件。

Random Fact custom-api 组件渲染效果

Steam Specials custom-api 组件渲染效果

custom-api 组件是什么

custom-api 是 Glance 中最“万能”的组件:它向一个 URL 发起 HTTP 请求,拿到响应后用你提供的模板生成 HTML。官方配置文档明确指出,编写这类组件需要基础的 HTML、CSS、Go 模板语言知识(见 docs/configuration.md 的 Custom API 一节)。

从源码 internal/glance/widget-custom-api.goinitialize() 可以看到几条硬性约束与默认值:

  • template 字段是必填项,为空会直接返回 template is required 错误;
  • 组件初始化时会调用 withCacheDuration(1 * time.Hour),即缓存默认 1 小时(可被通用属性 cache 覆盖);
  • 模板通过 Go 标准库 html/template 解析,并注入 customAPITemplateFuncs 函数表(源码)。

请求体与请求头的构造在 initialize() 方法 中完成:

  • 提供了 body 但未指定 method 时,默认使用 POST;没有 body 时默认 GET
  • 提供 bodybody-type 默认为 json,仅允许 jsonstring 两种取值;
  • body-type: json 时会自动追加 Content-Type: application/json 请求头;
  • parameters 会被序列化为查询串写入 RawQuery(这会覆盖 URL 中已有的查询参数);
  • basic-authusername/password 会通过 SetBasicAuth 生效。

模板上下文:.JSON、.Response 与 .Options

模板执行时传入的顶层数据是 customAPITemplateData源码),它内嵌 customAPIResponseData,因此模板中可用的顶层字段包括:

字段 含义
.JSON 响应体经 gjson 解析后的结果,提供 StringIntArray 等方法
.Response 原始 *http.Response,可读取 StatusCodeStatusHeader
.Subrequest "key" 获取命名子请求的响应数据
.JSONLines 按行拆分响应体的便捷方法(JSON Lines 场景)
.Options 组件配置中 options 映射的模板侧访问器

其中 .JSON 的底层是 decoratedGJSONResult源码),它内嵌了 tidwall/gjson 的 Result(依赖见 go.mod),因此所有 gjson 的键路径语法(如 a.b.ca.0.b# 过滤、.. 递归)在这里都有效。

数据访问基础语法

读取标量字段

假设 API 返回:

{
  "title": "My Title",
  "content": "My Content",
}

String 方法即可取出字段:

<div>{{ .JSON.String "title" }}</div>
<div>{{ .JSON.String "content" }}</div>

输出:

<div>My Title</div>
<div>My Content</div>

IntFloatBool 方法同理,用于读取整数、浮点数和布尔值。

遍历数组

如果响应中包含数组:

{
  "author": "John Doe",
  "posts": [
    {
      "title": "My Title",
      "content": "My Content"
    },
    {
      "title": "My Title 2",
      "content": "My Content 2"
    }
  ]
}

使用 range 遍历 posts

{{ range .JSON.Array "posts" }}
  <div>{{ .String "title" }}</div>
  <div>{{ .String "content" }}</div>
{{ end }}

输出:

<div>My Title</div>
<div>My Content</div>
<div>My Title 2</div>
<div>My Content 2</div>

注意循环体内访问字段时不再带 .JSON 前缀——因为 range 会把模板上下文切换为当前数组元素。

在循环中访问根上下文

Go 模板中 $ 始终指代顶层数据。因此可以在循环内通过 $.JSON 回读顶层字段:

{{ range .JSON.Array "posts" }}
  <div>{{ .String "title" }}</div>
  <div>{{ .String "content" }}</div>
  <div>{{ $.JSON.String "author" }}</div>
{{ end }}

输出中每个元素后都会追加一行 John Doe

<div>My Title</div>
<div>My Content</div>
<div>John Doe</div>
<div>My Title 2</div>
<div>My Content 2</div>
<div>John Doe</div>

基本类型数组:空字符串键

当数组元素本身是字符串这类基本类型时,指定其“类型”的方式是传空字符串作为键:

[
    "Apple",
    "Banana",
    "Cherry",
    "Watermelon"
]
{{ range .JSON.Array "" }}
  <div>{{ .String "" }}</div>
{{ end }}

输出:

<div>Apple</div>
<div>Banana</div>
<div>Cherry</div>
<div>Watermelon</div>

从源码看,Array("") 直接对当前结果取数组(源码),String("") 则返回结果本身的字符串表示,这就是空键语法的实现依据。

按索引访问

对基本类型数组,也可以用索引字符串直接取元素:

<div>{{ .JSON.String "0" }}</div>

输出:

<div>Apple</div>

点号路径访问深层对象

嵌套对象支持 gjson 风格的点号路径:

{
    "user": {
        "address": {
            "city": "New York",
            "state": "NY"
        }
    }
}
<div>{{ .JSON.String "user.address.city" }}</div>
<div>{{ .JSON.String "user.address.state" }}</div>

输出:

<div>New York</div>
<div>NY</div>

路径中的任意位置都可以混入数组索引,例如:

{
    "users": [
        { "name": "John Doe" },
        { "name": "Jane Doe" }
    ]
}
<div>{{ .JSON.String "users.0.name" }}</div>
<div>{{ .JSON.String "users.1.name" }}</div>

输出:

<div>John Doe</div>
<div>Jane Doe</div>

遍历对象的键值对

Entries 方法可以枚举对象内的每个属性:

{
  "user": {
    "id": 42,
    "name": "Alice",
    "active": true
  }
}
{{ range $key, $value := .JSON.Entries "user" }}
  <div>{{ $key }}: {{ $value.String "" }}</div>
{{ end }}

输出:

<div>id: 42</div>
<div>name: Alice</div>
<div>active: true</div>

每个属性以键值对形式暴露:$key 是字符串,$value 是支持全套 JSON 方法的对象。实现上 Entries 返回一个 Go 迭代器(iter.Seq2源码),底层按 ForEach 顺序逐条 yield。

条件判断:字段是否存在

Exists 检查路径是否存在,再决定读取还是展示兜底文案:

{
    "user": {
        "name": "John Doe",
        "age": 30
    }
}
{{ if .JSON.Exists "user.age" }}
  <div>{{ .JSON.Int "user.age" }}</div>
{{ else }}
  <div>Age not provided</div>
{{ end }}

输出:

<div>30</div>

算术运算

模板内置了 addsubmuldivmod 五个运算函数。两个整数运算返回整数,否则返回浮点数;除以零返回 0;对非数值返回 NaN

{
    "price": 100,
    "discount": 10
}
<div>{{ sub (.JSON.Int "price") (.JSON.Int "discount") }}</div>

输出:

<div>90</div>

从源码 customAPIDoMathOpdoMathOpWithAny 可以看到:divmod 在除数为 0 时显式返回 0 以避免除零异常,而 NaN 的返回则来自类型断言失败(操作数不是数值类型)的分支。

时间处理

解析时间并展示相对时间

{
  "posts": [
    {
      "title": "Exploring the Depths of Quantum Computing",
      "date": "2023-10-27T10:00:00Z"
    },
    {
      "title": "A Beginner's Guide to Sustainable Living",
      "date": "2023-11-15T14:30:00+01:00"
    },
    {
      "title": "The Art of Baking Sourdough Bread",
      "date": "2023-12-03T08:45:22-08:00"
    }
  ]
}

解析日期并展示动态相对时间:

{{ range .JSON.Array "posts" }}
  <div>{{ .String "title" }}</div>
  <div {{ .String "date" | parseTime "rfc3339" | toRelativeTime }}></div>
{{ end }}

parseTime 接受两个参数:日期布局(layout)和日期字符串本身。布局可使用 "unix""RFC3339""RFC3339Nano""DateTime""DateOnly" 等预定义别名(匹配时不区分大小写,见 源码),也可以直接传入 Go 的自定义时间格式串。

输出:

<div>Exploring the Depths of Quantum Computing</div>
<div data-dynamic-relative-time="1698400800"></div>

<div>A Beginner's Guide to Sustainable Living</div>
<div data-dynamic-relative-time="1700055000"></div>

<div>The Art of Baking Sourdough Bread</div>
<div data-dynamic-relative-time="1701621922"></div>

toRelativeTime 的返回值必须作为 HTML 标签的属性使用(divlispan 等均可),不要直接输出为文本。其内部实现是生成一个 data-dynamic-relative-time="<unix 时间戳>" 属性(dynamicRelativeTimeAttrs),随后由 Glance 前端 JavaScript 在客户端持续刷新显示正确的相对时间(如 2h、1d),无需关心内部机制。

parseRelativeTime 是上面三步的缩写形式,等价于 {{ .String "date" | parseTime "rfc3339" | toRelativeTime }},例如 {{ .String "date" | parseRelativeTime "rfc3339" }}

此外还有几个与时间相关的辅助函数:

  • parseLocalTime(layout, s):与 parseTime 相同,但会转换到服务器时区;无时区信息时使用本地时区而非 UTC;
  • formatTime(layout, t):把 time.Time 格式化为字符串,布局规则与 parseTime 一致。注意其参数顺序是刻意翻转的,方便管道写法:{{ now | formatTime "rfc3339" }}源码注释);
  • now():返回当前时间;
  • offsetNow(offset):返回偏移后的当前时间,偏移量为 Go duration 格式,如 "3h""-1h""2h30m10s"
  • duration(str):把 "1h""5h30m" 之类的字符串解析为 time.Duration
  • startOfDay(t) / endOfDay(t):取某时间所在天的 0 点与 23:59:59。

响应状态码与请求头

模板可以直接判断 HTTP 响应状态,也可以读取响应头:

{{ if eq .Response.StatusCode 200 }}
  <p>Success!</p>
{{ else }}
  <p>Failed to fetch data</p>
{{ end }}
<div>{{ .Response.Header.Get "Content-Type" }}</div>

处理 JSON Lines(NDJSON)响应

有些 API 每行返回一个独立的 JSON 对象(NDJSON / JSON Lines 格式):

{"name": "Steve", "age": 30}
{"name": "Alex", "age": 25}
{"name": "John", "age": 35}

由于默认情况下 Glance 期望响应体是单个合法 JSON 对象,解析此类响应必须先关闭 JSON 校验:

- type: custom-api
  skip-json-validation: true

然后遍历每一行对象:

{{ range .JSONLines }}
  <p>{{ .String "name" }} is {{ .Int "age" }} years old</p>
{{ end }}

输出:

<p>Steve is 30 years old</p>
<p>Alex is 25 years old</p>
<p>John is 35 years old</p>

.JSONLines 的实现是对原始响应体调用 gjson 的 ForEachLine 逐行解析(源码)。

由于底层是 gjson,还可以使用更高级的 gjson 选择器从 JSON Lines 中提取数据。例如用 ..#.name 拿到所有行的 name 字段:

{{ range .JSON.Array "..#.name" }}
  <p>{{ .String "" }}</p>
{{ end }}

输出:

<p>Steve</p>
<p>Alex</p>
<p>John</p>

skip-json-validation 开启后,fetchCustomAPIResponse 会跳过 gjson.Valid 校验分支,直接把原始文本交给 gjson 解析。)

多请求场景

subrequests:并发子请求

若一个组件需要同时展示多个接口的数据,可以在配置中声明 subrequests,它们会与主请求并发执行:

- type: custom-api
  cache: 2h
  subrequests:
    another-one:
      url: https://uselessfacts.jsph.pl/api/v2/facts/random
  title: Random Fact
  url: https://uselessfacts.jsph.pl/api/v2/facts/random
  template: |
    <p class="size-h4 color-paragraph">{{ .JSON.String "text" }}</p>
    <p class="size-h4 color-paragraph margin-top-15">{{ (.Subrequest "another-one").JSON.String "text" }}</p>

子请求支持与主请求相同的全部属性(除 subrequests 自身外),例如 headersparameters 等。从源码 fetchAndRenderCustomAPIRequest 可以看到:存在子请求时,主请求与所有子请求通过 goroutine 并发发出,且任一请求失败会 cancel 整个上下文、本次更新整体失败。为书写方便,可以先定义变量:

  template: |
    {{ $anotherOne := .Subrequest "another-one" }}
    <p>{{ $anotherOne.JSON.String "text" }}</p>

子请求同样可以访问 .Response

  template: |
    {{ $anotherOne := .Subrequest "another-one" }}
    <p>{{ $anotherOne.Response.StatusCode }}</p>

需要留意:.Subrequest "key" 传入未定义的 key 会 panic(源码),Go 模板引擎会把 panic 捕获并作为模板执行错误返回,即该组件会展示错误信息而不是静默输出空值。

newRequest:在模板内发起连续请求

有时需要做两次有依赖的连续 API 调用:第一个请求的结果要拼进第二个请求。这时可以在模板内部用 newRequest 构造新请求,并用管道链式添加参数、请求头,最后由 getResponse 执行:

- type: custom-api
  url: https://api.example.com/get-id-of-something
  template: |
    {{ $theID := .JSON.String "id" }}

    {{
      $something := newRequest (concat "https://api.example.com/something/" $theID)
        | withParameter "key" "value"
        | withHeader "Authorization" "Bearer token"
        | getResponse
    }}

    {{ $something.JSON.String "title" }}

这里 $theID 来自第一次调用的结果,并被拼入第二次调用的 URL。从源码 newRequest 函数 还可以看到:newRequest 支持在 URL 后追加格式化参数(内部走 fmt.Sprintf),例如 {{ newRequest "https://api.example.com/something/%s" $theID }}

模板内请求还支持 withStringBody(设置字符串 body,会自动以 POST 发送)、withBasicAuthwithAllowInsecure 等链式方法(源码)。

如果 URL 本身需要动态参数,可以完全省略 url 属性,把请求整体放到模板里发起——注意此时需要手动判断状态码:

- type: custom-api
  title: Events from the last 24h
  template: |
    {{
      $events := newRequest "https://api.example.com/events"
        | withParameter "after" (offsetNow "-24h" | formatTime "rfc3339")
        | getResponse
    }}

    {{ if eq $events.Response.StatusCode 200 }}
      {{ range $events.JSON.Array "events" }}
        <div>{{ .String "title" }}</div>
        <div {{ .String "date" | parseTime "rfc3339" | toRelativeTime }}></div>
      {{ end }}
    {{ else }}
      <p>Failed to fetch data: {{ $events.Response.Status }}</p>
    {{ end }}

options:让模板可参数化复用

options 是一个映射,会传入模板并通过 .Options 访问器读取,适合作为模板的“可配置开关”,避免每处使用都去改模板本身。与其在模板里硬编码变量:

- type: custom-api
  template: |
    {{ /* User configurable options */ }}
    {{ $collapseAfter := 5 }}
    {{ $showThumbnails := true }}
    {{ $showFlairs := false }}

    <ul class="list list-gap-10 collapsible-container" data-collapse-after="{{ $collapseAfter }}">
      {{ if $showThumbnails }}
        <li>
          <img src="{{ .JSON.String "thumbnail" }}" alt="thumbnail" />
        </li>
      {{ end }}
      {{ if $showFlairs }}
        <li>
          <span class="flair">{{ .JSON.String "flair" }}</span>
        </li>
      {{ end }}
    </ul>

不如改为在模板中通过 .Options 带默认值读取:

- type: custom-api
  template: |
    <ul class="list list-gap-10 collapsible-container" data-collapse-after="{{ .Options.IntOr "collapse-after" 5 }}">
      {{ if (.Options.BoolOr "show-thumbnails" true) }}
        <li>
          <img src="{{ .JSON.String "thumbnail" }}" alt="thumbnail" />
        </li>
      {{ end }}
      {{ if (.Options.BoolOr "show-flairs" false) }}
        <li>
          <span class="flair">{{ .JSON.String "flair" }}</span>
        </li>
      {{ end }}
    </ul>

然后在组件配置中按需覆盖:

- type: custom-api
  options:
    collapse-after: 5
    show-thumbnails: true
    show-flairs: false

配合 YAML 锚点/合并键,同一模板即可复用于多个组件并传入不同取值:

# 注意 `custom-widgets` 不是特殊属性,只是用来定义可复用的 "anchor"
custom-widgets:
  - &example-widget
    type: custom-api
    template: |
      {{ .Options.StringOr "custom-option" "not defined" }}

pages:
  - name: Home
    columns:
      - size: full
        widgets:
          - <<: *example-widget
            options:
              custom-option: "Value 1"

          - <<: *example-widget
            options:
              custom-option: "Value 2"

.Options 提供的方法有 StringOrIntOrFloatOrBoolOr(键不存在时返回默认值)以及 JSON。实现上是一个泛型函数 customAPIGetOptionOrDefault:类型断言失败或键缺失时同样回落到默认值;而 Options.JSON(key) 会把任意配置值序列化回 JSON 字符串(源码),键缺失时 panic。

组件属性速查

结合 docs/configuration.md 的属性表与 CustomAPIRequest 结构体 的 YAML 标签,custom-api 组件的完整属性如下:

属性 类型 必填 默认值 说明
url string 要请求的 URL,必须能从 Glance 所在服务器访问;省略时可在模板内用 newRequest 发请求
headers key/value 映射 附加请求头
method string GET(有 body 时为 POST GETPOSTPUTPATCHDELETEOPTIONSHEAD
body-type string json 请求体类型,jsonstring
body any 请求体,字符串或映射
frameless boolean false true 时移除组件边框与内边距(渲染模板 据此追加 widget-content-frameless 类)
allow-insecure boolean false 忽略无效/自签名证书
skip-json-validation boolean false 跳过 JSON 校验,用于 JSON Lines 等响应
template string Go html/template 模板
options map 传入模板的选项映射,经 .Options 读取
parameters key (string) & value (string|array) 查询参数;设置后会覆盖 URL 中已有的查询参数
subrequests 请求映射 并发子请求,经 .Subrequest "key" 访问

另外两个属性未出现在配置文档的属性表中,但可以从源码结构确认存在:

  • basic-authusername/password):结构体中定义为嵌套结构(源码),配置后走 SetBasicAuth
  • mock-response(string):设置后组件不发起真实请求,而是直接把该字符串作为 JSON 响应体、状态码固定 200(源码),适合离线调试模板。

body 的两种写法示例:

body-type: json
body:
  key1: value1
  key2: value2
  multiple-items:
    - item1
    - item2
body-type: string
body: |
  key1=value1&key2=value2

函数参考

JSON 对象方法

以下方法可用在 .JSON(以及 range 循环内的元素、.Subrequest 结果)上:

  • String(key string) string:按键读取字符串值;
  • Int(key string) int:按键读取整数值;
  • Float(key string) float:按键读取浮点值;
  • Bool(key string) bool:按键读取布尔值;
  • Array(key string) []JSON:按键读取 JSON 对象数组;
  • Exists(key string) bool:键(或路径)是否存在;
  • Entries(key string):返回迭代器,用于遍历对象属性,{{ range $key, $value := .JSON.Entries "user" }}$key 为字符串,$valueJSON 对象。

Options 对象方法

  • StringOr(key string, default string) string:按键读字符串,缺失时返回默认值;
  • IntOr(key string, default int) int:按键读整数,缺失时返回默认值;
  • FloatOr(key string, default float) float:按键读浮点数,缺失时返回默认值;
  • BoolOr(key string, default bool) bool:按键读布尔值,缺失时返回默认值;
  • JSON(key string) JSON:把键对应的配置值序列化为字符串化 JSON;键不存在会抛出错误。

Glance 内置辅助函数

  • toFloat(i int) float:整数转浮点;
  • toInt(f float) int:浮点转整数;
  • toRelativeTime(t time.Time) template.HTMLAttr:生成动态相对时间(2h、1d 等),返回值必须用作 HTML 属性,如 <span {{ toRelativeTime .Time }}></span>
  • now() time.Time:当前时间;
  • offsetNow(offset string) time.Time:偏移后的当前时间,offset 格式如 "3h""-1h""2h30m10s"
  • duration(str string) time.Duration:解析 "1h""24h""5h30m" 等字符串为时长;
  • parseTime(layout string, s string) time.Time:按 Go 时间格式解析字符串;别名可用 "unix""RFC3339""RFC3339Nano""DateTime""DateOnly",其余情况按 Go 布局字面量处理;
  • formatTime(layout string, s string) time.Time:把时间格式化为字符串,布局规则与 parseTime 相同;参数顺序经过翻转以支持管道写法;
  • parseLocalTime(layout string, s string) time.Time:同 parseTime,但自动转换到服务器时区;无时区时使用本地时区而非 UTC;
  • parseRelativeTime(layout string, s string) time.Time{{ .String "date" | parseTime "rfc3339" | toRelativeTime }} 的简写;
  • add(a, b float) float / sub(a, b float) float / mul(a, b float) float / div(a, b float) float:四则运算;
  • mod(a, b int) int:取余(a % b);
  • formatApproxNumber(n int) string:人类可读数量,如 1000 -> 1k(实现:<1k 原样,<10k 一位小数 k,<1m 整数 k,以上一位小数 m);
  • formatNumber(n float|int) string:千分位格式,如 1000 -> 1,000(底层为 message.Printer 按英文 locale 输出,源码);
  • trimPrefix(prefix string, str string) string / trimSuffix(suffix string, str string) string / trimSpace(str string) string:去前缀、去后缀、去首尾空白;
  • replaceAll(old string, new string, str string) string:字符串全量替换;
  • replaceMatches(pattern string, replacement string, str string) string:正则全量替换;
  • findMatch(pattern string, str string) string:返回第一个正则匹配;
  • findSubmatch(pattern string, str string) string:返回第一个子匹配;
  • sortByString(key string, order string, arr []JSON) []JSON / sortByInt / sortByFloat:按指定键与 asc/desc 排序(实现);
  • sortByTime(key string, layout string, order string, arr []JSON) []JSON:按时间键排序,布局同 parseTime
  • concat(strings ...string) string:拼接字符串;
  • unique(key string, arr []JSON) []JSON:按键值去重(实现,按首现顺序保留);
  • percentChange(current float, previous float) float:计算两数的百分比变化(实现);
  • startOfDay(t time.Time) time.Time / endOfDay(t time.Time) time.Time:当天 0 点 / 23:59:59。

Go text/template 内置函数

  • eq / ne / lt / le / gt / ge:相等、不等、小于、小于等于、大于、大于等于比较;
  • and(args ...bool) bool:全部为真才为真(两个及以上参数);
  • or(args ...bool) bool:任一为真即为真(两个及以上参数);
  • not(a bool) bool:取反;
  • index(a any, b int) any:取数组指定下标元素;
  • len(a any) int:数组长度;
  • printf(format string, a ...any) string:格式化字符串。

源码中额外可用的函数

除了文档列出清单外,模板函数表 还注册了这些函数,编写复杂模板时可用:

  • randomElement(arr []JSON):随机取数组一个元素(空数组返回空结果);
  • withStringBody(body string, req) / withBasicAuth(username, password string, req) / withAllowInsecure(val, req):模板内构造请求时的链式方法;
  • iconWithClass(name, class string):输出带 CSS 类的内置图标;
  • safeHTML / safeURL / safeCSS:把字符串声明为安全的 HTML/URL/CSS(关闭自动转义),实现
  • absInt(i int) intformatPrice(price float)formatPriceWithPrecision(precision, price):绝对值与价格格式化。

实战示例

docs/configuration.md 给出了三个由简到繁的官方示例,均可直接复制到 glance.yml 参考:

1. 单字段展示(Random Fact)

- type: custom-api
  title: Random Fact
  cache: 6h
  url: https://uselessfacts.jsph.pl/api/v2/facts/random
  template: |
    <p class="size-h4 color-paragraph">{{ .JSON.String "text" }}</p>

2. 多列统计 + 运算(Immich stats)

演示了请求头、formatNumber 千分位以及单位换算(字节除以 2^30 得 GB):

- type: custom-api
  title: Immich stats
  cache: 1d
  url: https://${IMMICH_URL}/api/server/statistics
  headers:
    x-api-key: ${IMMICH_API_KEY}
    Accept: application/json
  template: |
    <div class="flex justify-between text-center">
      <div>
          <div class="color-highlight size-h3">{{ .JSON.Int "photos" | formatNumber }}</div>
          <div class="size-h6">PHOTOS</div>
      </div>
      <div>
          <div class="color-highlight size-h3">{{ .JSON.Int "videos" | formatNumber }}</div>
          <div class="size-h6">VIDEOS</div>
      </div>
      <div>
          <div class="color-highlight size-h3">{{ div (.JSON.Int "usage" | toFloat) 1073741824 | toInt | formatNumber }}GB</div>
          <div class="size-h6">USAGE</div>
      </div>
    </div>

3. 列表 + 排序条件样式(Steam Specials)

演示了深层路径 specials.items、模板内变量、printf 格式化价格与条件 class:

- type: custom-api
  title: Steam Specials
  cache: 12h
  url: https://store.steampowered.com/api/featuredcategories?cc=us
  template: |
    <ul class="list list-gap-10 collapsible-container" data-collapse-after="5">
    {{ range .JSON.Array "specials.items" }}
      <li>
        <a class="size-h4 color-highlight block text-truncate" href="https://store.steampowered.com/app/{{ .Int "id" }}/">{{ .String "name" }}</a>
        <ul class="list-horizontal-text">
          <li>{{ div (.Int "final_price" | toFloat) 100 | printf "$%.2f" }}</li>
          {{ $discount := .Int "discount_percent" }}
          <li{{ if ge $discount 40 }} class="color-positive"{{ end }}>{{ $discount }}% off</li>
        </ul>
      </li>
    {{ end }}
    </ul>

实践注意事项

  1. 缓存:组件默认缓存 1 小时(源码),频繁变化的数据请用通用 cache 属性调小;
  2. JSON 校验:默认要求响应是合法 JSON;非 2xx 状态码会直接以“状态码 状态文本”形式报错,2xx 但 JSON 非法则会记录错误日志(含截断到 100 字符的响应体)并返回 invalid response JSON校验逻辑)。排查问题时可先看 Glance 日志,或用 mock-response 离线复现;
  3. parameters 覆盖 URL 查询串:设置 parameters 后 URL 中已有的查询参数会被整体替换;
  4. 模板输出即最终 HTML:模板在 custom-api.html 中以 CompiledHTMLtemplate.HTML)形式注入组件主体,因此模板内写什么就渲染什么;类名可复用 Glance 的 CSS 工具类(如 size-h4color-highlightlistcollapsible-container,见 internal/glance/static/css);
  5. 相对时间必须用作属性toRelativeTime/parseRelativeTime 的返回值是 template.HTMLAttr,要放在标签属性位置,客户端脚本会依据 data-dynamic-relative-time 持续更新显示;
  6. 正则函数有全局缓存replaceMatches/findMatch/findSubmatch 使用带锁的正则缓存(源码),同一 pattern 只编译一次,可以放心在循环中使用。

掌握以上内容,就覆盖了 custom-api 组件模板语法的全部官方用法:从标量读取、数组与深层路径遍历,到条件判断、算术、时间相对化、JSON Lines、并发子请求与模板内二次请求,再到 options 参数化复用——足以把绝大多数自有或第三方 JSON API 稳定地接入 Glance 仪表盘。

登录后查看全文
热门项目推荐
相关项目推荐