本文讲一种在 SAP 里做"现代化网页报表"的架构:ABAP 只负责取数和校验,页面(HTML/CSS/JS)放在 SMW0 模板里,数据以 JSON 注入 。读完你能照着做出一张可筛选、可切换看板、小计总计自动重算的报表,并知道它怎么防止"模板和程序传输不同步"这类线上事故。
一、要解决什么问题
需求:做一张"按分组 × 类别 × 月份"的数量汇总报表,要求:
- 页面上能再筛选(分组、类别、月份),小计和总计随筛选实时重算;
- 有"经典表格"和"看板"(KPI、堆叠柱、排行、热力图)两种视图,可来回切换;
- 数据量不大(几千行),但改样式、改布局要快,不想每次都改 ABAP、走传输、等审批。
传统 ALV 做不了第 1、2 条;在 ABAP 里一行行拼 HTML 字符串,则第 3 条是噩梦------改一个颜色要动代码。
二、总体架构
┌─────────────────────┐ 每日背景作业写入 ┌────────────────┐
│ 源数据(任意数据表)│ ───────────────────▶ │ 快照表(按日期) │
└─────────────────────┘ └───────┬────────┘
│ SELECT
┌─────────────────────────────────────────────────────▼────────┐
│ ABAP 报表 │
│ 1. get_data 读快照 + 主数据 │
│ 2. load_template 从 SMW0 读 HTML 模板(二进制,UTF-8) │
│ 3. check_template 检查版本/占位符/元素/标签/事件 │
│ 4. build_json 内表 → JSON(并转义)→ 解析校验 │
│ 5. replace 把 JSON 换进模板里的占位符 │
│ 6. display docking 容器 + cl_gui_html_viewer 展示 │
└─────────────────────────────┬────────────────────────────────┘
│ SAPEVENT 回传(JS 错误 / 合计核对值)
┌───────────▼───────────┐
│ 页面 JS:筛选、小计、看板 │
└───────────────────────┘
核心思想一句话:数据与展示分离,模板是"资源",不是"代码"。
| 层 | 内容 | 谁来改 | 怎么发布 |
|---|---|---|---|
| 数据层 | 快照表、取数 SELECT | ABAP 开发 | TR |
| 注入层 | 内表→JSON、模板检查 | ABAP 开发 | TR |
| 展示层 | HTML/CSS/JS 模板 | 前端/任何人 | SMW0 上传(也可带 TR) |
三、为什么用"快照表"而不是实时查
报表的数据来自一个每天由背景作业写入的快照表,主键里带快照日期(快照每天由背景作业生成)。好处:
- 查询只做简单
WHERE datum = ...,毫秒级,不会拖慢生产; - 同一天的数据口径固定,不同日期的数字可以对比;
- 页面上显示"数据写入时间",用户一看就知道是哪个时点的数。
选择画面里"快照日期"默认取 MAX( datum ),并提供 F4,只列表里已有的日期:
abap
SELECT MAX( datum ) FROM zsnap_demo INTO @p_datum.
IF p_datum IS INITIAL.
p_datum = sy-datum.
ENDIF.
F4 用 F4IF_INT_TABLE_VALUE_REQUEST,把 SELECT DISTINCT datum ... ORDER BY datum DESCENDING 的结果丢给它即可。
四、关键技术点一:用选择屏幕承载 HTML
不想另建 Dynpro?可以用代码定义的选择屏幕 9100,上面放一个 docking 容器:
abap
SELECTION-SCREEN BEGIN OF SCREEN 9100 TITLE tit2.
SELECTION-SCREEN END OF SCREEN 9100.
" AT SELECTION-SCREEN OUTPUT 里,仅对 9100 建控件
CHECK sy-dynnr = '9100'.
CHECK go_dock IS INITIAL.
CREATE OBJECT go_dock
EXPORTING repid = sy-repid dynnr = '9100'
side = cl_gui_docking_container=>dock_at_top
ratio = 95
EXCEPTIONS OTHERS = 1.
CREATE OBJECT go_html EXPORTING parent = go_dock EXCEPTIONS OTHERS = 1.
展示时 CALL SELECTION-SCREEN 9100.,返回后别忘了 free( ) 控件,否则再次进入会残留。
结果屏幕上不需要"执行/后台/变式"这些按钮,用 RS_SET_SELSCREEN_STATUS 的 p_exclude 排除掉(ONLI、CRET、PRIN、SJOB、SPOS、GET、VDEL、VSHO)。
abap
" (9100 用 CALL SELECTION-SCREEN 叫出,狀態是 %_CS,執行鍵的功能碼是 CRET)
lt_excl = VALUE #( ( 'ONLI' ) ( 'CRET' ) ( 'PRIN' ) ( 'SJOB' ) ( 'SPOS' ) ( 'GET' ) ( 'VDEL' ) ( 'VSHO' ) ).
CALL FUNCTION 'RS_SET_SELSCREEN_STATUS'
EXPORTING
p_status = sy-pfkey
TABLES
p_exclude = lt_excl.
五、关键技术点二:模板放进 SMW0
5.1 读取
SMW0 的"二进制数据"类型对象,用 WWWDATA_IMPORT 读出来是 W3MIME 行表(每行 1022 字节左右),要自己拼回 xstring:
abap
DATA: ls_key TYPE wwwdatatab,
lt_mime TYPE STANDARD TABLE OF w3mime WITH DEFAULT KEY,
lv_xtpl TYPE xstring,
lv_size TYPE c LENGTH 255,
lv_len TYPE i.
FIELD-SYMBOLS <ls_mime> TYPE w3mime.
CLEAR cv_tpl.
ls_key-relid = 'MI'.
ls_key-objid = gc_tpl_objid.
CALL FUNCTION 'WWWDATA_IMPORT'
EXPORTING
key = ls_key
TABLES
mime = lt_mime
EXCEPTIONS
wrong_object_type = 1
import_error = 2
OTHERS = 3.
IF sy-subrc <> 0 OR lt_mime IS INITIAL.
APPEND |SMW0 找不到二進位物件 { gc_tpl_objid }| TO ct_msg.
RETURN.
ENDIF.
LOOP AT lt_mime ASSIGNING <ls_mime>.
CONCATENATE lv_xtpl <ls_mime>-line INTO lv_xtpl IN BYTE MODE.
ENDLOOP.
CALL FUNCTION 'WWWPARAMS_READ'
EXPORTING
relid = 'MI'
objid = gc_tpl_objid
name = 'filesize'
IMPORTING
value = lv_size
EXCEPTIONS
entry_not_exists = 1
OTHERS = 2.
IF sy-subrc = 0 AND lv_size IS NOT INITIAL AND lv_size CO ' 0123456789'.
lv_len = lv_size.
IF lv_len > 0 AND lv_len < xstrlen( lv_xtpl ).
lv_xtpl = lv_xtpl(lv_len).
ENDIF.
ENDIF.
坑 1:尾部补字节。 最后一行不足会补零,要用 WWWPARAMS_READ 读参数 filesize,把 xstring 截到真实长度,否则末尾出现乱码。
坑 2:BOM。 UTF-8 文件开头可能有 EF BB BF,要判断并去掉:
abap
IF xstrlen( lv_xtpl ) >= 3
AND lv_xtpl(3) = cl_abap_char_utilities=>byte_order_mark_utf8.
lv_xtpl = lv_xtpl+3.
ENDIF.
cv_tpl = cl_abap_conv_codepage=>create_in( )->convert( lv_xtpl ).
5.2 注入数据
模板里留一个唯一的占位符,平时是合法的 JS(本地用浏览器直接打开模板也能跑):
html
<script>
var PAYLOAD = /*@@DATA@@*/null;
</script>
ABAP 侧一句 replace 即可:
abap
gv_html = replace( val = lv_tpl sub = `/*@@DATA@@*/null` with = lv_json ).
好处:前端同学拿到模板,本地放一份 mock JSON 就能调样式,完全不需要 SAP。
六、关键技术点三:内表转 JSON
用 /ui2/cl_json=>serialize,先定义好"页面用的结构",字段名就是 JS 里用的名字(越短越省流量):
abap
TYPES: BEGIN OF ty_row,
w TYPE string, " 分组
b TYPE string, " 类别
m TYPE string, " 月份
q TYPE p LENGTH 13 DECIMALS 3, " 数量
END OF ty_row,
BEGIN OF ty_payload,
meta TYPE ty_meta,
data TYPE STANDARD TABLE OF ty_row WITH EMPTY KEY,
END OF ty_payload.
cv_json = /ui2/cl_json=>serialize(
data = ls_payload
pretty_name = /ui2/cl_json=>pretty_mode-low_case ).
注入 <script> 前必须做三件事,否则偶发"页面空白":
| 字符 | 问题 | 处理 |
|---|---|---|
< |
数据里出现 </script> 会提前结束脚本块 |
换成 < |
| U+2028 / U+2029 | ES5 里算换行,字符串直接断掉 | 换成 ` |
/
` |
||
| 非法 JSON | 前面都对但内容有误 | 用 cl_sxml_string_reader 完整读一遍,抛异常就拦下 |
注意:转义用的反斜线和 u 在 ABAP 里要分开拼(\` && `u003c),否则某些编辑器会把它当转义吃掉。
七、关键技术点四:防呆------不让"半套"上线
这套架构的最大风险是:程序和模板分两个传输,只传了一边。于是做了三道检查。
1)版本号对账。 模板里放 <meta name="ztpl-version" content="1">,程序里常量也写 1。不一致就拒绝显示。注意 ABAP 的 PCRE 默认是 extended 模式,会忽略空格,要加 (?-x):
abap
FIND PCRE `(?-x)<meta name="ztpl-version" content="(\d+)">`
IN iv_tpl SUBMATCHES lv_ver.
2)结构检查。 占位符恰好 1 个;页面程序要用的元素 id 各 1 个;html/head/body/script/style 标签开闭成对;必须包含 SAPEVENT:(回传通道)。
3)运行期回传。 页面里:
javascript
window.onerror = function (msg, src, line, col) {
location.href = 'SAPEVENT:jserr?msg=' + encodeURIComponent(msg)
+ '&line=' + line + '&col=' + col;
};
加载完成后,页面在不筛选的状态下,把"总行数、各类合计、表格总计、看板总计"一并回传:
javascript
location.href = 'SAPEVENT:ready?rows=' + rows + '&q01=' + q01 + '&tbl=' + tbl;
ABAP 侧用 cl_gui_html_viewer=>m_id_sapevent 注册事件(appl_event = abap_false,系统事件,不触发 PAI),把回传值和内表重新汇总的数字逐个比对,容差 0.001,不一致就弹窗:
abap
lt_evt = VALUE #( ( eventid = cl_gui_html_viewer=>m_id_sapevent
appl_event = abap_false ) ).
go_html->set_registered_events( EXPORTING events = lt_evt EXCEPTIONS OTHERS = 1 ).
SET HANDLER lcl_html_event=>on_sapevent FOR go_html.
这样 JS 报错、前端算错,用户第一时间看到提示,而不是拿着错数据去开会。
另外加了一个"只检查模板"的复选框,结果用 WRITE 输出,不依赖 GUI 控件,背景作业也能跑------传输后挂个作业就能确认"程序和模板配套"。
八、关键技术点五:HTML 怎么送到控件
不要用字符行表 load_data(要处理 255 宽度切行,容易切断标签)。改成UTF-8 二进制直传:
abap
lv_xhtml = cl_abap_conv_codepage=>create_out( )->convert( lv_html ).
CONCATENATE cl_abap_char_utilities=>byte_order_mark_utf8 lv_xhtml
INTO lv_xhtml IN BYTE MODE.
lv_size = xstrlen( lv_xhtml ).
lt_bin = cl_bcs_convert=>xstring_to_solix( lv_xhtml ).
go_html->load_data( EXPORTING size = lv_size
IMPORTING assigned_url = lv_url
CHANGING data_table = lt_bin
EXCEPTIONS OTHERS = 1 ).
go_html->show_url( url = lv_url ).
字节原样写到前端,不经 SAP GUI 转码,中文不会乱。
九、使用示例
9.1 用到的另外的小知识点
免选择屏幕的实现要点(sy-calld 防止死循环):
abap
INITIALIZATION.
IF sy-tcode = 'ZDEMO_R' AND sy-calld IS INITIAL.
SUBMIT zdemo_report WITH p_datum = p_datum AND RETURN.
LEAVE PROGRAM.
ENDIF.
9.2 改页面怎么做
- 本地改模板(浏览器直接打开,用 mock JSON 调样式);
- SMW0 → 二进制数据 → 更新上传;
- 如果只改样式、没动结构,不需要改 ABAP,也不用发 TR;
- 如果动了元素 id 或占位符,同时抬高两边的版本号,一起传输。
9.3 排错速查
| 现象 | 原因 | 处理 |
|---|---|---|
| 提示"SMW0 找不到二进制对象" | 没传模板或名字不对 | SMW0 检查对象名 |
| 提示模板版本不一致 | 只传了程序或只传了模板 | 补传另一边 |
| 页面空白,无提示 | 数据里有 </script> 或 U+2028 |
确认转义逻辑未被删 |
| 中文乱码 | 字符行表 load_data 或缺 BOM |
改 UTF-8 二进制直传 |
| 模板末尾多出乱码 | 没按 filesize 截断 | 读 filesize 参数截断 |
| 弹窗"页面程序错误 第 N 行" | JS 语法或运行错误 | N 即模板行号,直接定位 |
| 弹窗"合计不一致" | 前端汇总逻辑与 ABAP 不同 | 对照提示逐项查 |
| 背景执行无画面 | HTML 控件需要前台 GUI | 背景只用"检查模板"模式 |
十、几点经验
- 模板视为资源,检查要自动化。 凡是"代码之外的东西"(模板、配置),都应该有"和代码配套"的校验。
- 前后端各算一遍,互相核对。 页面算的数字用 SAPEVENT 回传,由 ABAP 以原始数据复核,比人工抽查靠谱。
- 数据量别过大。 整个数据集一次性塞进页面,适合几千到几万行;再大应考虑服务端分页或预聚合。
- 控件释放不能省。
free( )掉 html viewer 和 docking container,避免残留。 - 选择文本动态设置。 如果程序文本池只有一种语言,用
SELECTION_TEXTS_MODIFY在运行时按需补,免得其他语言登录时只显示变量名。
十一、小结
| 要点 | 做法 |
|---|---|
| 取数 | 每日快照 + 简单 WHERE |
| 页面 | SMW0 二进制模板,JS 自处理筛选/小计 |
| 注入 | /ui2/cl_json 序列化 + <、U+2028/2029 转义 + sXML 校验 |
| 防呆 | 版本号、结构检查、JS 错误回传、合计核对 |
| 发布 | 改样式只传模板,改结构程序与模板同传 |
这套"ABAP 取数校验 + SMW0 模板展示"的组合,既保留了 SAP 内的权限和数据安全,又拿到了前端的灵活度,非常适合"报表型小工具"。
如果觉得有用,欢迎点赞收藏;有问题评论区交流。
