---
title: .ftl FreeMarker 模板页面
navTitle: .ftl
---
# .ftl FreeMarker 模板页面

`.ftl` 是基于 [FreeMarker](https://freemarker.apache.org/) 模板引擎的页面文件，适合在元数据目录中用代码编写开放网站、门户首页、栏目页、详情页、专题页等自定义页面。它在服务器端渲染，根据模板、请求参数和业务数据生成动态 HTML 页面。

FTL 页面可以和 `.action.ts` 脚本配合使用：脚本负责处理参数、权限和业务查询，模板负责组织页面结构和输出内容。页面可以不默认下载额外的前端框架、JS 和 CSS，天然适合搜索引擎抓取和索引；需要交互或可视化能力时，也可以按需引入系统内的图表、组件和其他资源。

当你要做的是可被搜索引擎抓取的开放网站、轻量动态页面、代码型 HTML 页面，或者个性化程度很高、需要重度编码自定义 HTML 结构、样式、资源加载和交互细节的页面时，可以优先考虑 FTL。

如果目标是普通企业级表单、流程办理、信息展示、数据查询、管理台页面、可视化分析或报表输出，通常应优先考虑对应的低代码页面和业务类型，例如 SuperPage（spg）、超级模板页面（tpg）、报表填报应用（fapp）、仪表板（dash）或报表（rpt）。

常见场景包括：

- 政府开放网站首页及相关二级页面，如企业法人服务网站、数据开放网站等
- 企业网站，尤其是需要嵌入部分动态数据的服务型页面
- 面向搜索引擎或公开访问的信息列表页、详情页和专题页
- 对 HTML 结构、首屏内容和加载资源有精细控制要求的轻量页面
- 需要高度定制版式、动效、前端资源组织或第三方脚本集成的专题页面、活动页、嵌入页

## 相关类型{#types}

`.ftl` 不是 JSON 元数据文件，本页保留模板语法、路由入口和内置指令说明。模板中引入的 `.action.ts` 脚本可以参考以下类型：

- 数据查询接口：[`types/api/succ.dw.d.ts`](../../../../../dev-types/types/api/succ.dw.d.ts)
- 程序流和动作接口：[`types/api/succ.afl.d.ts`](../../../../../dev-types/types/api/succ.afl.d.ts)

## URL 路由入口{#url-routing}

前端访问 FTL 模板渲染结果有两种 URL 入口：可以访问 `.action`，由脚本先处理参数、权限和业务查询，再返回要渲染的 FTL；也可以直接访问 `.ftl`，把模板本身作为路由入口，在模板中按需引入脚本获取数据。两种入口都支持在 URL 中省略最后的扩展名，例如 `company.action?id=001` 可写成 `company?id=001`，`company-list.ftl` 可写成 `company-list`。

### Action 路由入口（脚本主导）{#action-route}

用户访问 `.action` URL，TypeScript 脚本执行业务逻辑后，通过返回值指定要渲染的模板和传入的变量。

**脚本返回值约定（这里只列出 FTL / HTML 页面相关的常用返回值）：**

| 返回值类型 | 行为 |
|-----------|------|
| 字符串 `"page.html.ftl"` | 渲染指定 FTL 模板 |
| 字符串 `"page.ftl"` | 渲染指定 FTL 模板 |
| 字符串 `"page.html"` / `"page.htm"` | 按静态 HTML 文件输出 |
| Map `{ $ftl: "page.html.ftl", key: value, ... }` | 渲染指定 FTL，非 `$` 开头的键可在模板中直接使用 |

> 说明：脚本返回值还支持一些更通用的 HTTP 语义（如 `redirect:`、JSON 输出等），本文聚焦
> FTL 开发常用场景，不展开所有返回值分支。

**示例：企业详情页**

```typescript
// company.action.ts
const id = request.getParameter("id");
const company = dw.executeQueryDSL({
    sources: ["/data/tables/企业基本信息.tbl"],
    filter: `ID = '${id}'`,
    options: { limit: 1 }
}).data[0];

// 返回 $ftl Map：company 和 queryId 可在模板中直接使用
return { $ftl: "company.html.ftl", company, queryId: id };
```

```ftl
<#-- company.html.ftl：直接使用 Action Push 的变量 -->
<@html>
<@head>
    <title>${$msg("page.title", "我的页面")}</title>
    <link rel="stylesheet" href="${$url('/dist/custom/style.css')}" />
</@head>
<@body>
    <h1>${$msg("page.title", "欢迎")}</h1>

    <#if $browser.mobile>
        <p>您正在使用移动设备访问</p>
    </#if>

    <p>当前语言：${$language}</p>

    <#-- 使用 Action Push 的变量 -->
    <#if company??>
        <p>企业名称：${company.QYMC}</p>
    <#else>
        <p>未找到企业 ${queryId}</p>
    </#if>

    <script type="module">
        import { ready } from 'sys/sys';
        ready('myModule').then(m => m.init());
    </script>
</@body>
</@html>
```

**访问方式：**`company.action?id=001`，也可省略扩展名写成 `company?id=001`。

---

### FTL 直接入口（模板主导）{#ftl-route}

用户直接访问 `.ftl` URL，模板内部通过 `<@useScript>` 指令引入脚本，调用脚本中定义的导出函数获取数据。

```ftl
<#-- company-list.ftl：直接通过 URL 访问 -->
<@useScript src="./company-queries.action.ts" alias="q" />
<#assign list = q.listCompanies("01", 10)>
<@html>
<@head><title>企业列表</title></@head>
<@body>
  <h1>企业列表</h1>
  <ul>
    <#list list as row>
      <li>${row.name}</li>
    </#list>
  </ul>
</@body>
</@html>
```

```typescript
// company-queries.action.ts：导出供 FTL 调用的函数
export function listCompanies(industry: string, limit: number) {
    return dw.executeQueryDSL({
        sources: ["/data/tables/企业基本信息.tbl"],
        filter: `HYLBDM = '${industry}'`,
        options: { limit }
    }).data;
}
```

**访问方式：**`company-list.ftl`，也可省略 `.ftl` 写成 `company-list`。

---

## 内置变量与函数{#variables-functions}

| 名称 | 类型 | 使用范围 | 说明 |
|------|------|---------|------|
| `$msg(key, [default])` | 函数 | 所有模板 | 服务端国际化文本 |
| `$url(path)` | 函数 | 所有模板 | 资源 URL 构建 |
| `$params` | 变量 | 所有模板 | 当前 HTTP 请求参数（QueryString/FormData） |
| `$contextPath` | 变量 | 仅 HTML 模板 | 应用 Context Path |
| `$language` | 变量 | 所有模板 | 当前渲染语言标识 |
| `$browser` | 变量 | 仅 HTML 模板 | 浏览器和设备信息 |

> 说明：模板中不直接暴露 `request` / `response` 对象。读取请求参数请优先使用 `$params`，
> 复杂逻辑建议放到 `.action.ts` 脚本中处理。

### `$msg(key, [default])`{#func-msg}

根据当前语言返回对应的翻译文本，在服务端渲染时直接输出，无需前端二次处理。

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `key` | String | 是 | 国际化 key |
| `default` | String | 否 | key 不存在时的默认值，不传则返回 key 本身 |

```ftl
<h1>${$msg("page.title")}</h1>
<button>${$msg("button.submit", "提交")}</button>
<input placeholder="${$msg("login.page.user")}" />

<#-- innerHTML 场景：翻译值含 HTML 标签，用 ?no_esc 关闭转义 -->
<div>${$msg("help.richContent")?no_esc}</div>
```

**语言解析优先级：**

1. URL 参数 `:lang`（如 `?:lang=en`）
2. 用户个人设置的语言偏好
3. 浏览器语言（用户设置"使用浏览器语言"时）
4. 系统默认语言

### `$url(path)`{#func-url}

构建静态资源的完整 URL，自动处理部署路径和 CDN 场景。

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `path` | String | 是 | 以 `/` 开头的资源路径 |

```ftl
<img src="${$url('/public/logo.png')}" />
<link rel="stylesheet" href="${$url('/dist/app/style.css')}" />
```

**返回值**：完整资源 URL。

### `$params`{#var-params}

获取当前 HTTP 请求的参数（如 URL 的 QueryString 或 POST 的表单数据），用来替代繁琐的 `request.getParameter(name)` 调用。

| 用法 | 说明 |
|------|------|
| `${$params.key_name}` | 获取常规命名的参数值 |
| `${$params["key-with-dash"]}` | 获取含短横线等特殊字符的参数值 |

使用示例：

```ftl
<#-- 1. 获取参数并提供默认兜底值 -->
<#assign status = $params.status!"01">

<#-- 2. 数值转换（通过 ?number 转换为数字供脚本或比较使用） -->
<#assign limit = ($params.limit!"20")?number>

<#-- 3. 直接输出到页面 -->
<p>当前分类：${$params.category!""}</p>
```

### `$contextPath`{#var-context-path}

应用的 Context Path（如 `/bi`），仅 HTML 模板可用。推荐使用 `$url()` 替代手工拼接。

```ftl
<img src="${$contextPath}/public/logo.png" />
```

### `$language`{#var-language}

当前渲染语言标识（如 `zh_CN`、`en`）。

```ftl
<html lang="${$language}">
<#if $language == "en">English<#else>中文</#if>
```

### `$browser`{#var-browser}

浏览器和设备信息检测对象，仅 HTML 模板可用。

| 属性 | 类型 | 说明 |
|------|------|------|
| `$browser.mobile` | Boolean | 是否为移动设备 |
| `$browser.tablet` | Boolean | 是否为平板设备 |
| `$browser.name` | String | 浏览器名称（Chrome / Firefox / Safari 等） |
| `$browser.majorVersion` | int | 浏览器主版本号 |
| `$browser.os` | String | 操作系统名称 |
| `$browser.deviceType` | String | 设备类型（COMPUTER / MOBILE / TABLET） |
| `$browser.browserCompatible` | Boolean | 浏览器是否兼容（Chrome 49+ 等） |

```ftl
<#if $browser.mobile>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</#if>

<#if !$browser.browserCompatible>
  <div class="compat-warning">请升级浏览器</div>
</#if>
```

## 内置指令{#directives}

| 指令 | 使用范围 | 说明 |
|------|---------|------|
| `<@httpHeader name="..." value="..." />` | 所有模板 | 动态设置 HTTP 响应头 |
| `<@useScript src="..." alias="..." />` | 所有模板 | 引入 TS 脚本并注入导出函数 |
| `<@html>...</@html>` | 仅 HTML 模板 | 输出 `<!DOCTYPE html>` 和 `<html>` 标签 |
| `<@head>...</@head>` | 仅 HTML 模板 | 输出 `<head>` 标签，合并注入系统资源 |
| `<@body>...</@body>` | 仅 HTML 模板 | 输出 `<body>` 标签，注入主题和动态脚本 |

### `<@httpHeader ... />指令`{#directive-http-header}

设置 HTTP 响应头。**必须在模板顶部、所有正文输出之前调用**。

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `name` | String | 是 | HTTP 响应头名称 |
| `value` | String | 是 | 响应头值；中文等 HTTP/1.x header 不能直接承载的字符会按 UTF-8 百分号编码，`Content-Disposition` 中的中文文件名会自动使用 `filename*=` |

```ftl
<#-- 覆盖 Content-Type -->
<@httpHeader name="Content-Type" value="application/json; charset=utf-8" />

<#-- 禁止缓存 -->
<@httpHeader name="Cache-Control" value="no-store, no-cache, must-revalidate" />

<#-- 触发文件下载 -->
<@httpHeader name="Content-Disposition" value="attachment; filename=report.csv" />

<#-- 中文下载文件名会自动转换为 filename*=UTF-8''... -->
<@httpHeader name="Content-Disposition" value="attachment; filename=中国企业信息.csv" />

<#-- 中文业务头会自动编码，客户端按 UTF-8 百分号解码即可还原 -->
<@httpHeader name="X-Data-Name" value="中国企业信息" />
```

> 说明：`<@httpHeader>` 会自动编码 header value 中的中文、换行等不能直接写入 HTTP/1.x 响应头的字符。客户端读取这类自定义响应头时，可按 UTF-8 百分号解码还原原始业务文本；`Content-Disposition` 中的中文文件名会按浏览器下载协议转换为 `filename*=UTF-8''...`。该自动编码只作用于 FTL 指令；`.action.ts` 中直接调用 `response.setHeader()` / `response.addHeader()` 时遵守 Servlet API 原生语义，不会自动编码。`<@httpHeader>` 只有在当前渲染存在 HTTP `response` 时才会生效。如果模板是通过内部脚本 API 渲染为字符串，而不是直接写回浏览器响应，则该指令会被静默忽略。

### `<@useScript .../>指令`{#directive-use-script}

引入 `.action.ts` 脚本并调用脚本的**导出函数**。

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `src` | String | 是 | 脚本路径，按当前 FTL 文件所在目录解析，推荐使用相对路径 |
| `alias` | String | 是 | 注入的命名空间名，通过 `alias.funcName()` 调用 |

```ftl
<@useScript src="./queries.action.ts" alias="q" />
<#assign result = q.getData($params.id!"")>
${result.name}
```

**注意事项：**

- `src` 推荐写成 `./foo.action.ts` 或 `../foo.action.ts`
- `alias` 必填，用于在模板中形成命名空间，避免全局变量污染
- 同一脚本被多次引入时，底层会复用同一个 脚本对象，`alias` 只影响模板中的访问名称

### `<@html>指令`{#directive-html}

输出 `<!DOCTYPE html>` 和 `<html>` 标签，自动设置 `lang` 等属性，仅 HTML 模板可用。

### `<@head>指令`{#directive-head}

输出 `<head>` 标签，仅 HTML 模板可用。它会把用户在指令内部编写的自定义标签合并到页面
头部，同时自动写入 HTML 页面运行所需的系统内容，包括 charset、viewport、系统 CSS/JS、
favicon、当前语言、页面标题、meta 信息、动态脚本、脚本变量和主题样式。

因此在 HTML 页面场景中，推荐优先使用 `<@html>`、`<@head>`、`<@body>` 组合，让模板专注
于页面内容和必要的自定义标签，不需要手写一整套裸 HTML 壳。

### `<@body>指令`{#directive-body}

输出 `<body>` 标签，仅 HTML 模板可用。自动设置主题 class 和 data-theme 属性，并在底部注入动态脚本。

需要在 `.ftl` 输出的 HTML 页面中加载同目录前端脚本或系统模块时，参考 [HTML 和 FTL 自定义页面](../../../script/frontend/html-ftl-pages.md)。

**HTML 指令使用范围：**

- `page.ftl`
- `page.html.ftl`
- `page.htm.ftl`

像 `data.json.ftl`、`feed.xml.ftl`、`export.txt.ftl` 这类非 HTML 模板不会注入
`<@html>`、`<@head>`、`<@body>`，误用时会报未定义变量错误。

---

## 引用第三方前端库{#third-party-libraries}

在 FTL 页面中引用第三方前端库时，参考[引用第三方前端库](http://localhost:5173/dev/script/frontend/third-party-libraries)。

---

## 国际化{#i18n}

FTL 模板支持服务端国际化，通过 `$msg()` 函数直接输出翻译文本，在服务端模板渲染阶段完成语言替换。

### 语言解析{#language-resolution}

每次渲染时，引擎按以下优先级确定当前语言：

1. **URL 参数 `:lang`**（如 `page.ftl?:lang=en`）——调试和多语言预览时使用
2. **用户个人偏好**——用户在界面中设置的语言
3. **浏览器语言**——用户选择"使用浏览器语言"时生效
4. **系统默认语言**——系统管理员在设置中配置

当前生效的语言可通过 `${$language}` 变量读取（返回如 `zh_CN`、`en` 的标识符）。

### 基本用法{#basic-usage}

`$msg(key)` 按当前语言查找翻译文本；`$msg(key, default)` 在 key 不存在时返回第二个参数（省略则返回 key 本身）。

```ftl
<h1>${$msg("page.welcome")}</h1>

<#-- 带默认值（key 缺失时兜底） -->
<button>${$msg("button.submit", "提交")}</button>

<#-- 与 $language 配合实现条件输出 -->
<#if $language == "en">
  <p>This page is in English.</p>
<#else>
  <p>此页面为中文。</p>
</#if>
```

### 翻译值含 HTML 标签{#html-messages}

`${}` 默认对输出进行 HTML 转义（防注入），翻译值中的 `<`、`>` 会被转义为 `&lt;`、`&gt;`。若翻译文本本身含有合法 HTML 标签（如富文本帮助信息），使用 `?no_esc` 关闭转义：

```ftl
<#-- 翻译值为纯文本——自动转义，无需处理 -->
<p>${$msg("notice.plain")}</p>

<#-- 翻译值含 HTML 标签——必须加 ?no_esc，否则标签会被转义为实体 -->
<div>${$msg("help.richContent")?no_esc}</div>
<a href="${$msg("download.url")}">${$msg("download.label")}</a>
```

> **安全原则**：只有确认翻译值来自受控的 i18n 文件（非用户输入）时，才能使用 `?no_esc`。

### 在 JSON 模板中使用{#json-i18n}

`.json.ftl` 模板的输出格式为 JSON，`${}` 自动进行 JSON 转义（双引号 `"` → `\"`，反斜杠 `\` → `\\`）。无需额外处理，翻译值可直接内嵌在 JSON 字符串中：

```ftl
<#-- data.json.ftl -->
{
  "title": "${$msg("page.title")}",
  "description": "${$msg("page.desc")}"
}
```

### 多语言切换演示{#language-switch}

通过 URL 参数 `:lang` 可在不切换用户设置的情况下预览任意语言的渲染结果，便于开发调试：

```text
# 中文渲染
https://host/app/mypage.ftl

# 英文渲染
https://host/app/mypage.ftl?:lang=en
```

---

## 输出转义{#escaping}

`${}` 插值会根据文件名中间扩展名自动转义，防止注入攻击和格式错乱。

### 自动转义规则{#auto-escaping}

| 文件名模式 | 转义行为 |
|-----------|----------|
| `*.ftl`（无中间扩展名） | HTML 转义（`<` `>` `&` `"` → HTML 实体） |
| `*.html.ftl` | 同上 |
| `*.json.ftl` | JSON 转义（`"` `\` 及控制字符） |
| `*.xml.ftl` | XML 转义（`<` `>` `&` `"` → XML 实体） |
| `*.txt.ftl` | 不转义 |

也可在模板头部用 `<#ftl output_format="JSON">` 手动指定格式。

### 关闭转义{#disable-escaping}

当输出原始内容时（如含 HTML 标签的翻译文本、URL），使用 `?no_esc` 关闭转义：

```ftl
<#-- innerHTML：翻译值含 <strong>、<a> 等 HTML 标签 -->
<div>${$msg("help.richContent")?no_esc}</div>

<#-- href：翻译值是完整 URL -->
<a href="${$msg("download.url")}">下载</a>

<#-- 脚本函数返回 HTML 片段 -->
<div>${q.renderWidget()?no_esc}</div>
```

> **原则**：默认走自动转义（安全），只在确认内容安全时才用 `?no_esc`。

---

## 输出任意格式内容{#content-type}

FTL 默认用于输出 HTML 页面，也可以作为模板化的文本输出能力使用，生成 JSON、XML、纯文本、CSV 等内容。系统会根据文件名中间扩展名自动决定 Content-Type：

| 文件名模式 | Content-Type |
|-----------|-------------|
| `page.ftl` / `page.html.ftl` | `text/html; charset=UTF-8` |
| `data.json.ftl` | `application/json` |
| `feed.xml.ftl` | `application/xml` |
| `export.txt.ftl` | `text/plain` |
| `export.txt.ftl` + `<@httpHeader name="Content-Type" value="text/csv; charset=utf-8" />` | `text/csv` |

也可以通过 `<@httpHeader>` 指令在模板顶部手动覆盖内容类型，或在 Action 脚本中调用 API 设置 response 的内容类型。

> 说明：系统内建的自动输出格式切换主要覆盖 HTML、JSON、XML、TXT。\
> CSV 下载推荐使用 `*.txt.ftl` 模板输出纯文本内容，再用 `<@httpHeader>` 显式设置
> `text/csv` 和 `Content-Disposition`。

### JSON API（FTL 自引入）{#json-example}

```ftl
<@httpHeader name="Content-Type" value="application/json; charset=utf-8" />
<@httpHeader name="Cache-Control" value="no-store" />
<@useScript src="./data-queries.action.ts" alias="q" />
<#assign rows = q.queryList($params.type!"")>
{
  "total": ${rows?size},
  "items": [
    <#list rows as row>
    {"id": "${row.ID}", "name": "${row.NAME}"}<#sep>,</#sep>
    </#list>
  ]
}
```

### CSV 下载{#csv-download}

```ftl
<@httpHeader name="Content-Type" value="text/csv; charset=utf-8" />
<@httpHeader name="Content-Disposition" value="attachment; filename=export.csv" />
<@useScript src="./export.action.ts" alias="exp" />
<#assign rows = exp.getExportData()>
ID,名称,日期
<#list rows as row>
${row.ID},${row.NAME},${row.DATE}
</#list>
```

---

## 可参考的 Demo{#demos}

下面这些示例来自系统内置的 `script-demo.app/ftl`，适合边看边照着写：

- Hello World：[`demo-helloworld.ftl`](https://demo.succbi.com/DEMO/app/script-demo.app/ftl/demo-helloworld.ftl)
- `<@useScript>` 最小示例：[`demo-use-script.ftl`](https://demo.succbi.com/DEMO/app/script-demo.app/ftl/demo-use-script.ftl)
- JSON 导出：[`demo-export.json.ftl`](https://demo.succbi.com/DEMO/app/script-demo.app/ftl/demo-export.json.ftl?status=01\&limit=3)
- XML 导出：[`demo-export.xml.ftl`](https://demo.succbi.com/DEMO/app/script-demo.app/ftl/demo-export.xml.ftl?status=01\&limit=3)
- 响应头覆盖：[`demo-http-headers.txt.ftl`](https://demo.succbi.com/DEMO/app/script-demo.app/ftl/demo-http-headers.txt.ftl)
- HTML 页面 + 子页面拆分：[`www-mysite-com/index.ftl`](https://demo.succbi.com/DEMO/app/script-demo.app/ftl/www-mysite-com/index.ftl)

对应的元数据示例目录在：

`/DEMO/app/script-demo.app/ftl`
