Skip to content

FTL 模板开发

SuccBI 提供了基于 FreeMarker 的服务端模板引擎,允许开发者在元数据目录中编写 .ftl 文件和 .action.ts 脚本,通过 URL 直接访问即可输出动态 HTML 页面、JSON 数据、XML 报文、CSV 文件等任意格式内容。

FTL 页面由服务端预编译,并在渲染是直接生成最终内容,可以做到极致精简,性能接近静态HTML文件;页面可以不默认下载额外的前端框架、JS 和 CSS,天然适合搜索引擎抓取和索引。需要交互或可视化能力时,也可以按需引入系统内的图表、组件和其他资源。

适合以下场景:

  • 政府开放网站首页及相关二级页面,如企业法人服务网站、数据开放网站等
  • 企业网站,尤其是需要嵌入部分动态数据的服务型页面
  • 自定义 JSON/XML API 端点(数据导出、第三方对接)

URL 路由入口

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

Action 路由入口(脚本主导)

用户访问 .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>${company.QYMC}</title></@head>
<@body>
  <#if company??>
    <h1>${company.QYMC}</h1>
    <p>法定代表人:${company.FDDBR}</p>
  <#else>
    <p>未找到企业 ${queryId}</p>
  </#if>
</@body>
</@html>

访问方式:company.action?id=001,也可省略扩展名写成 company?id=001


FTL 直接入口(模板主导)

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

ftl
<#-- company-list.json.ftl:直接通过 URL 访问 -->
<@httpHeader name="Content-Type" value="application/json; charset=utf-8" />
<@useScript src="./company-queries.action.ts" alias="q" />
<#assign list = q.listCompanies("01", 10)>
{"total": ${list?size}, "items": [<#list list as row>{"name":"${row.name}"}<#sep>,</#sep></#list>]}
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.json.ftl,也可省略 .ftl 写成 company-list.json


自定义内容类型

FTL 模板不仅可以输出 HTML 页面,也可以输出 JSON、XML、纯文本等多种内容类型。系统会根据文件名中间扩展名自动决定 Content-Type,无需手动配置:

文件名模式Content-Type
page.ftl / page.html.ftltext/html; charset=UTF-8
data.json.ftlapplication/json
feed.xml.ftlapplication/xml
export.txt.ftltext/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/csvContent-Disposition


内置变量与函数

名称类型使用范围说明
$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])

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

参数类型必填说明
keyString国际化 key
defaultStringkey 不存在时的默认值,不传则返回 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)

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

参数类型必填说明
pathString/ 开头的资源路径
ftl
<img src="${$url('/public/logo.png')}" />
<link rel="stylesheet" href="${$url('/dist/app/style.css')}" />

返回值:完整资源 URL。

$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

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

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

$language

当前渲染语言标识(如 zh_CNen)。

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

$browser

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

属性类型说明
$browser.mobileBoolean是否为移动设备
$browser.tabletBoolean是否为平板设备
$browser.nameString浏览器名称(Chrome / Firefox / Safari 等)
$browser.majorVersionint浏览器主版本号
$browser.osString操作系统名称
$browser.deviceTypeString设备类型(COMPUTER / MOBILE / TABLET)
$browser.browserCompatibleBoolean浏览器是否兼容(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>

内置指令

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

<@httpHeader ... />指令

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

参数类型必填说明
nameStringHTTP 响应头名称
valueString响应头值
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" />

说明:<@httpHeader> 只有在当前渲染存在 HTTP response 时才会生效。
如果模板是通过内部脚本 API 渲染为字符串,而不是直接写回浏览器响应,则该指令会被静默忽略。

<@useScript .../>指令

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

参数类型必填说明
srcString脚本路径,按当前 FTL 文件所在目录解析,推荐使用相对路径
aliasString注入的命名空间名,通过 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>指令

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

<@head>指令

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

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

<@body>指令

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

HTML 指令使用范围:

  • page.ftl
  • page.html.ftl
  • page.htm.ftl

data.json.ftlfeed.xml.ftlexport.txt.ftl 这类非 HTML 模板不会注入 <@html><@head><@body>,误用时会报未定义变量错误。


国际化

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

语言解析

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

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

当前生效的语言可通过 ${$language} 变量读取(返回如 zh_CNen 的标识符)。

基本用法

$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 转义(防注入),翻译值中的 <> 会被转义为 &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.ftl 模板的输出格式为 JSON,${} 自动进行 JSON 转义(双引号 "\",反斜杠 \\\)。无需额外处理,翻译值可直接内嵌在 JSON 字符串中:

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

多语言切换演示

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

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

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

输出转义

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

自动转义规则

文件名模式转义行为
*.ftl(无中间扩展名)HTML 转义(< > & " → HTML 实体)
*.html.ftl同上
*.json.ftlJSON 转义(" \ 及控制字符)
*.xml.ftlXML 转义(< > & " → XML 实体)
*.txt.ftl不转义

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

关闭转义

当输出原始内容时(如含 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


完整示例

HTML 页面(Action 驱动)

ftl
<@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>
    </#if>

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

JSON API(FTL 自引入)

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 下载

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

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

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

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

微信公众号微信公众号:山川软件