主题
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=001,company-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.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。
内置变量与函数
| 名称 | 类型 | 使用范围 | 说明 |
|---|---|---|---|
$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])
根据当前语言返回对应的翻译文本,在服务端渲染时直接输出,无需前端二次处理。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
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>语言解析优先级:
- URL 参数
:lang(如?:lang=en) - 用户个人设置的语言偏好
- 浏览器语言(用户设置"使用浏览器语言"时)
- 系统默认语言
$url(path)
构建静态资源的完整 URL,自动处理部署路径和 CDN 场景。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
path | String | 是 | 以 / 开头的资源路径 |
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_CN、en)。
ftl
<html lang="${$language}">
<#if $language == "en">English<#else>中文</#if>$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>内置指令
| 指令 | 使用范围 | 说明 |
|---|---|---|
<@httpHeader name="..." value="..." /> | 所有模板 | 动态设置 HTTP 响应头 |
<@useScript src="..." alias="..." /> | 所有模板 | 引入 TS 脚本并注入导出函数 |
<@html>...</@html> | 仅 HTML 模板 | 输出 <!DOCTYPE html> 和 <html> 标签 |
<@head>...</@head> | 仅 HTML 模板 | 输出 <head> 标签,合并注入系统资源 |
<@body>...</@body> | 仅 HTML 模板 | 输出 <body> 标签,注入主题和动态脚本 |
<@httpHeader ... />指令
设置 HTTP 响应头。必须在模板顶部、所有正文输出之前调用。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | String | 是 | HTTP 响应头名称 |
value | String | 是 | 响应头值 |
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>只有在当前渲染存在 HTTPresponse时才会生效。
如果模板是通过内部脚本 API 渲染为字符串,而不是直接写回浏览器响应,则该指令会被静默忽略。
<@useScript .../>指令
引入 .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.tsalias必填,用于在模板中形成命名空间,避免全局变量污染- 同一脚本被多次引入时,底层会复用同一个 脚本对象,
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.ftlpage.html.ftlpage.htm.ftl
像 data.json.ftl、feed.xml.ftl、export.txt.ftl 这类非 HTML 模板不会注入 <@html>、<@head>、<@body>,误用时会报未定义变量错误。
国际化
FTL 模板支持服务端国际化,通过 $msg() 函数直接输出翻译文本,在服务端模板渲染阶段完成语言替换。
语言解析
每次渲染时,引擎按以下优先级确定当前语言:
- URL 参数
:lang(如page.ftl?:lang=en)——调试和多语言预览时使用 - 用户个人偏好——用户在界面中设置的语言
- 浏览器语言——用户选择"使用浏览器语言"时生效
- 系统默认语言——系统管理员在设置中配置
当前生效的语言可通过 ${$language} 变量读取(返回如 zh_CN、en 的标识符)。
基本用法
$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 转义(防注入),翻译值中的 <、> 会被转义为 <、>。若翻译文本本身含有合法 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.ftl | JSON 转义(" \ 及控制字符) |
*.xml.ftl | XML 转义(< > & " → 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,适合边看边照着写:
- Hello World:
demo-helloworld.ftl <@useScript>最小示例:demo-use-script.ftl- JSON 导出:
demo-export.json.ftl - XML 导出:
demo-export.xml.ftl - 响应头覆盖:
demo-http-headers.txt.ftl - HTML 页面 + 子页面拆分:
www-mysite-com/index.ftl
对应的元数据示例目录在:
/DEMO/app/script-demo.app/ftl
