
简介这份文档面向具备基础 ABAP 知识、希望快速上手 SAP BSP Web 报表开发的开发者与运维人员通过一个可复现的 HTML 报表实例串联 SE80 中 BSP 应用与页面的创建、CSS 样式引用、ABAP 脚本嵌入以及表单提交和事件处理等关键环节。压缩包内共 1 个 doc 文件约 433KB以图文步骤与代码片段结合的方式呈现便于按章节查阅和落地练习。目前已有 382 人学习说明其对入门与查漏补缺具有一定参考价值。读者可从中了解 BSP 页面如何混合 HTML 与 ABAP 表达式动态输出数据如何用内部表循环把业务数据绑定到前端表格以及提交按钮触发 onInputProcessing 后参数获取、输入校验与业务逻辑衔接的完整思路适合作为 SAP 传统 Web 报表开发的自学样例和项目参考。1. 老系统里最难处理的需求业务方要一个能自己查的网页老系统里最麻烦的需求是业务方拿着导出到 Excel 的报表说能不能给我一个网页输入客户编号和日期自己查。这份 BSP 报表开发实例HTML就是 ABAP 环境下把这件事做完整的样本一个页面、两个输入框、一段内嵌 ABAP从 VBAK 中查询订单抬头再补客户名称、销售组描述、业务员、订单金额最后渲染成 HTML 表格。适合两类人刚接触 BSP、想知道% %怎么和 HTML 混写的 ABAP 开发以及手里有一堆老报表要迁到 Web 上的人。它不讲框架讲的是页面怎么搭、参数怎么传、内表怎么循环输出。2. 工程骨架一个 BSP 应用里到底有哪几块东西先把对象建对后面写代码才有地方放。BSP 应用的本质是「一个开发包 一个页面集合 一个资源目录」页面不是单个文件而是由四个视图拼出来的一个页面类。2.1 从 SE80 建应用到建页面事务代码 SE80 打开工作台左侧对象类型下拉选 BSP Application输入应用名Z 开头走客户命名空间回车。名字不存在会弹创建确认。应用建好后右键应用节点 → Create → Page这时候要填的东西不多但每个都不能随便填字段填什么为什么重要Page namecustord_list.htm必须带.htm后缀goto_page/next_page里写的名字要和它完全一致Description客户订单查询只影响 SE80 目录展示但对后期维护很重要Page typeHTML 页面无流程逻辑纯查询报表选无流程逻辑的页面即可带流程逻辑的页面多一层事件模型这里用不上Layout / Event Handler建完后自动生成四个视图同时出现缺一个都编不过建好之后SE80 目录里这个页面会展开成 Layout、Event Handler、Page Attributes、Type Definitions 四个编辑入口。很多人第一次用会把它当成四个不同的文件其实不是编译之后它们是同一个页面类的不同部分Attributes 变成员变量Event Handler 变方法Layout 变输出方法。理解这点后面写代码时「为什么 Layout 里能直接写% gt_custno %」就不用解释了它们本来就是同一个对象的成员。提示应用名和页面名一旦被外部链接或者其它程序引用改名成本很高。我一般在上手前就把命名规则定死例如ZRPT_模块_用途。2.2 四个编辑视图各管一段逻辑拆开看每个视图该放什么、不该放什么比记语法有用得多视图角色该放不该放Layout视图层HTML 骨架、表单、循环输出的% %脚本大查询、复杂业务判断Event Handler控制层onInputProcessing事件方法、取参、校验、取数任何 HTML 标签Page Attributes页面状态errormsg、gt_custno、gt_dat、内表一次性临时变量Type Definitions类型声明TYPES tt_yshddcx TYPE STANDARD TABLE OF ...运行期数据这个分工不是形式主义。Attributes 里的变量是页面对象的属性页面实例在会话里是复用的所以上次提交留下的值下次还在Layout 每次渲染都会重新执行循环变量写在 Layout 里等于每次重建。搞反了就会出现「查了两次表格里数据翻倍」这种典型问题第五章会细说。Type Definitions 里那句定义值得单独看一眼 定义一个标准内表类型初始行数 0不带表头行 TYPES: tt_yshddcx TYPE STANDARD TABLE OF zsdfb_yshddcx INITIAL SIZE 0.zsdfb_yshddcx是自定义结构DDIC 里的结构或 Type 定义包含 KUNNR、NAME1、VBELN、AUDAT、KZWI2、YCHJE、VTEXT 等字段。INITIAL SIZE 0表示初始不预分配内存行小结果集用得上大结果集其实无所谓——真正影响性能的是后面循环里的查询次数不是这一行。2.3 静态资源CSS 文件放在 MIME 目录里页面里写link relstylesheet href./style.css只是引用文件本身要上传到 BSP 应用的 MIME 目录位置在 SE80 应用节点下的 MIME 资源里右键 Create → MIME object 上传即可。相对路径./指向当前 BSP 应用的资源根所以别写成/sap/public/...那种绝对路径换系统就废了。!-- Layout 里引入样式路径相对当前 BSP 应用的 MIME 根目录 -- link relstylesheet href./style.css typetext/css样式文件本身要跟着页面的 class 命名走Layout 里用了InputBox、table1、th这些 classCSS 里就得有对应规则否则页面能出但难看/* 输入框统一边框避免不同浏览器下高度不一致 */ .InputBox { border: 1px solid #999; height: 20px; } /* 表格细线靠 cellspacing1 背景色模拟边框 */ .table1 { border-collapse: collapse; font-size: 12px; } /* 表头行Layout 里写成 tr classth 才生效 */ .table1 .th { background: #e6e6e6; font-weight: bold; }还有一个容易漏的点MIME 文件上传后要单独激活页面激活了不代表资源激活了。样式没生效时第一件事不是翻 CSS 语法而是去 MIME 目录看文件状态是不是灰的。3. Layout 页签拆解把内表渲染成 HTML 表格Layout 是这个实例最值得看的部分因为它把「服务端数据怎么变成浏览器里的表格」这件事摊开得很干净。整段代码就是 HTML 外壳 几个 ABAP 标签没有模板引擎也没有前端框架。3.1 页面指令和表单骨架第一行%page languageabap %是页面指令声明页面脚本用 ABAP 解释。少了它后面的% %会被当普通文本输出到浏览器。表单部分基本是标准 HTML只有一处是 BSP 特有的%page languageabap % html head title客户订单查询/title link relstylesheet href./style.css typetext/css /head body h3% errormsg %/h3 form namePageForm idPageForm methodpost table aligncenter border0 width80% tr td客户编号 input classInputBox typetext namest_custno size10 value% gt_custno % maxlength10 /td td订单日期 input classInputBox typetext namest_ord size10 value% gt_dat % maxlength10 /td td alignright !-- 关键提交按钮的名字里带事件名 -- input nameonInputProcessing(sub) typesubmit value查询 /td /tr /table /form /body /htmlnameonInputProcessing(sub)是 BSP 的事件绑定约定按钮被点击时框架把请求路由到事件方法onInputProcessing并把括号里的sub作为event_id传进去方法里CASE event_id. WHEN sub.就命中了。这行是整个交互的入口名字写错一个字符页面照样能打开但点按钮没反应而且不报错——这是新手最容易卡住的地方。value% gt_custno %的作用是回显。查询完之后输入框里还留着上次的客户编号不用重新输。gt_custno在 Attributes 里定义Layout 直接引用会话内的值自动带过来。3.2 两种标签% %与% %页面里出现两种 ABAP 标签含义完全不同混了就会输出一堆莫名其妙的字符% 表达式 %输出表达式编译后大致等价于out-write( 表达式 )结果会进 HTML 流。写% errormsg %就是把这个变量的内容打到页面上。% 语句 %语句块纯服务端执行不产生任何输出。DATA、LOOP、IF、SELECT都放在这里面。一个常被忽略的细节是% %不做 HTML 转义。客户名称里如果有、之类字符表格结构会被撑破严重时整页排版乱掉。稳妥写法是套一层转义% 客户名称可能含特殊字符输出前先转义避免破坏表格结构 % td nowrap aligncenter% cl_http_utilityescape_html( wa_yshddcx-name1 ) %/td另外% %里不要塞复杂表达式。BSP 的表达式解析能力有限写个嵌套函数调用可能直接语法错误拆成% %里先算好再输出更省心。3.3 循环输出表格行表格部分的结构是表头行写死在循环外数据行在循环里逐行拼。列数必须严格对齐否则表头和数据会错位table border0 width100% classtable1 cellspacing1 !-- 表头行在循环外只输出一次 -- tr classth td nowrap alignLEFTnbsp;凭证日期/td td nowrap alignLEFTnbsp;销售凭证/td td nowrap alignLEFTnbsp;采购订单编号/td td nowrap alignLEFTnbsp;业务员/td td nowrap alignLEFTnbsp;产品组/td td nowrap alignLEFTnbsp;交货日期/td td nowrap alignright累计数量/td td nowrap alignright订单净值/td td nowrap alignright已出货金额/td td nowrap alignLEFTnbsp;分销渠道/td /tr % DATA: wa_yshddcx LIKE LINE OF zsdfb_yshddcx. % % LOOP AT zsdfb_yshddcx INTO wa_yshddcx. % tr td nowrap aligncenter% wa_yshddcx-audat %/td td nowrap aligncenter% wa_yshddcx-vbeln %/td td nowrap aligncenter% wa_yshddcx-bstnk %/td td nowrap aligncenter% wa_yshddcx-smnam %/td td nowrap aligncenter% wa_yshddcx-bezei20 %/td td nowrap aligncenter% wa_yshddcx-vdatu %/td td nowrap alignright% wa_yshddcx-zjsl %/td td nowrap alignright% wa_yshddcx-kzwi2 %/td td nowrap alignright% wa_yshddcx-ychje %/td td nowrap aligncenter% wa_yshddcx-vtext %/td /tr % ENDLOOP. % /table几个写 HTML 报表的经验点nowrap防止客户名太长把列挤扁金额列统一alignright肉眼对数字快得多cellspacing1配合 CSS 背景色模拟细边框比border1好看且兼容性好。日期字段audat、vdatu是 DATS 类型直接输出是20240101这种内部格式要好看得在 Event Handler 里先转成2024-01-01Layout 里不做格式化。LOOP AT ... INTO wa用的是带表头工作区的写法每次循环把当前行拷进wa_yshddcx。这个拷贝在几千行时无感几十万行时就是明显开销只读场景改成ASSIGNING fs能省掉。判断标准很简单循环体里只读不改就用字段符号。4. Event Handler 拆解一次提交流程的完整链路Layout 负责展示真正的活儿在事件方法里。从点按钮到表格出现新数据中间是一条固定链路取参数 → 校验 → 查主表 → 逐行补描述 → 回写内表 → 重新渲染 Layout。4.1 取参数、校验与导航事件方法的第一段是固定的三段式拿参数、做校验、决定下一步去哪。* 事件方法入口event_id 由 Layout 里 onInputProcessing(sub) 传入 CASE event_id. WHEN sub. 先从请求里把表单字段捞到 navigation 的参数集合 navigation-set_parameter( name st_custno ). gt_custno navigation-get_parameter( name st_custno ). navigation-set_parameter( name st_ord ). gt_dat navigation-get_parameter( name st_ord ). WHEN OTHERS. ENDCASE. * 校验客户编号为空直接退出把提示写进 errormsg CLEAR errormsg. IF gt_custno IS INITIAL. errormsg 请输入客户编号. EXIT. ENDIF. 关键一步客户编号是纯数字时补前导零否则匹配不上 KUNNR 的 10 位格式 CALL FUNCTION CONVERSION_EXIT_ALPHA_INPUT EXPORTING input gt_custno IMPORTING output gt_custno.set_parameterget_parameter这一对看起来多余——明明可以直接request-get_form_field( st_custno )。它的价值在跨页面导航参数放进 navigation 之后next_page跳到下一页时能带过去下一页再get_parameter就能拿到。单页面自刷新的话直接读 request 更短。导航那句navigation-next_page( xxx.htm )和navigation-goto_page( xxx.htm )的区别值得记一下next_page走导航栈用户能后退goto_page直接替换当前页面不留退路。而这个报表其实根本不需要跳转——事件方法执行完框架会自动用当前页面重新渲染 Layouterrormsg和内表的新值自然就显示出来了。多建一个结果页面反而把状态搞复杂了。UNPACK那一步原实例也写了作用是清洗输入字符串。但客户编号这种纯数字主键用CONVERSION_EXIT_ALPHA_INPUT补前导零才是正解这也是 ABAP 里处理 KUNNR、MATNR 这类字段的标准动作。少了这一步用户输入123456和数据库里的0000123456永远对不上查出来是空表还以为是 SQL 写错了。4.2 主查询与逐行补充描述主查询从 VBAK 中取订单抬头条件就是两个输入框DATA: wa_yshddcx LIKE LINE OF zsdfb_yshddcx. 每次都先清空内表否则连续提交两次数据会累加 CLEAR zsdfb_yshddcx. SELECT kunnr vbel vbel bstnk audat vdatu spart vkgrp vtweg INTO CORRESPONDING FIELDS OF TABLE zsdfb_yshddcx FROM vbak WHERE kunnr EQ gt_custno AND audat EQ gt_dat.INTO CORRESPONDING FIELDS OF TABLE按字段名匹配所以内表结构的字段名要能和 VBAK 的列名对上或同名别名。日期条件是这里最容易出问题的地方gt_dat在 Attributes 里的类型如果不是 DATS 而是字符型比较时会不会命中全看运气严格点就在取参后做一次格式校验。主查询只给了订单抬头客户名称、销售组描述、业务员这些描述性字段都不在 VBAK 里得逐行补LOOP AT zsdfb_yshddcx INTO wa_yshddcx. 1. 客户名称KNA1 按客户编号取 SELECT SINGLE name1 FROM kna1 INTO wa_yshddcx-name1 WHERE kunnr wa_yshddcx-kunnr. 2. 销售组描述TVGRT 是文本表SPRAS 一定要带上 SELECT SINGLE bezei FROM tvgrt INTO wa_yshddcx-bezei20 WHERE vkgrp wa_yshddcx-vkgrp AND spras sy-langu. 3. 业务员编号VBPA 里 PARVW VE 表示销售雇员 SELECT SINGLE t~pernr FROM vbpa AS t INTO wa_yshddcx-bupartnr WHERE t~parvw VE AND t~vbeln wa_yshddcx-vbeln. 4. 业务员姓名PA0001 按人员编号取 SELECT SINGLE sname FROM pa0001 INTO wa_yshddcx-smnam WHERE pernr wa_yshddcx-bupartnr. 5. 订单净值VBAP 里按凭证汇总条件小计 SELECT SINGLE SUM( kzwi2 ) FROM vbap INTO wa_yshddcx-kzwi2 WHERE vbeln wa_yshddcx-vbeln. 6. 产品组描述 SELECT SINGLE vtext FROM tspat INTO wa_yshddcx-vtext WHERE spart wa_yshddcx-spart. 7. 回写内表不写这句前面全白干 MODIFY zsdfb_yshddcx FROM wa_yshddcx. ENDLOOP.这里有个原实例里埋下的坑第 6 步和第 7 步分销渠道描述都往VTEXT里写后一次覆盖前一次。表现是页面上「产品组」列显示的是分销渠道的名称。真要两个都留结构里得有两个字段比如VTXTK产品组和VTEXT分销渠道。这类字段复用问题在改老报表时特别常见看代码时留意一下每个INTO的目标字段。另外TVGRT、TVTWT这类文本表查询SPRAS不写会捞到多语言行写上sy-langu或1都行硬编码1在中文环境里也没问题中文语言键是 1但换环境要复查。4.3 工作区、MODIFY 与性能取舍LOOP AT ... INTO wa是值拷贝改wa_yshddcx不会影响内表本身必须MODIFY回写。这一点原实例的注释里专门强调过因为漏写MODIFY的现象很迷惑循环里明明赋了值页面上那几列全是空。更省事的写法是用字段符号直接改内表 用字段符号直接操作内表行省一次拷贝和一次 MODIFY FIELD-SYMBOLS: fs_yshddcx LIKE LINE OF zsdfb_yshddcx. LOOP AT zsdfb_yshddcx ASSIGNING fs_yshddcx. SELECT SINGLE name1 FROM kna1 INTO fs_yshddcx-name1 WHERE kunnr fs_yshddcx-kunnr. 不需要 MODIFYfs_yshddcx 就是内表那一行 ENDLOOP.真正制约这段代码的是查询次数。一行数据 6 次SELECT SINGLE100 行就是 600 次数据库往返在应用服务器和数据库分离的部署下这个延迟很实在。业务量小的时候没人管等某天订单量上来页面转圈十几秒就成了性能优化工单。这部分的收敛办法放到最后一章讲。5. 避坑与排查BSP 报表最容易遇到问题的地方这类老技术栈的资料少出问题基本靠自己读现象。下面几条都是这类页面里高频出现的按「现象 → 原因 → 解决」记下来能省不少时间。5.1 渲染与静态资源类现象页面能打开但完全没有样式输入框是浏览器默认样式表格没有边框。原因style.css只写了引用文件没上传到 BSP 应用的 MIME 目录或者上传了但没激活。解决SE80 里展开应用节点看 MIME 资源状态灰色就是未激活再检查 Layout 里的路径是不是./style.css写绝对路径换系统必挂。现象表格里某一列突然显示lt;或者整行错位。原因% %不转义客户名称、采购订单号里带、、直接输出了。解决所有输出文本字段的地方套cl_http_utilityescape_html( )数字和日期字段不用管。5.2 提交与事件类现象点「查询」按钮完全没反应页面刷新一下又回到原样errormsg也没变。原因按钮name里的事件名和方法里WHEN的值不一致。onInputProcessing(sub)对应WHEN sub多一个空格、大小写不同都匹配不上。解决把事件名抽成一个常量思路——Layout 里写什么方法里就复制什么别手打第二遍。也可以在方法入口加一句BREAK-POINT确认到底有没有进来。现象查了两次表格里的数据变成两份。原因Attributes 里的内表是页面对象属性会话内一直存在SELECT ... INTO TABLE是追加不是覆盖。解决每次查询前CLEAR zsdfb_yshddcx.或者FREE zsdfb_yshddcx.放在取参之后、SELECT 之前。现象页面上% DATA: wa LIKE LINE OF ... %这段代码原样显示出来了。原因%page languageabap %指令缺失或写错整个页面被当纯 HTML 输出。解决指令必须是 Layout 的第一行前面不能有空行或者 HTML 注释。5.3 数据与类型类现象输入客户编号123456明明有数据却查不出来。原因KUNNR 是 10 位字符前导零没补123456和0000123456不相等。解决取参之后立刻走CONVERSION_EXIT_ALPHA_INPUT不要指望用户自己补零。现象日期条件怎么填都查不到或者报类型转换错误。原因输入框传上来的是字符串VBAK-AUDAT 是 DATS直接比较依赖隐式转换格式不对就匹配失败。解决属性里把gt_dat定义成DATS或者取参后做一次格式校验只接受YYYYMMDD。现象金额列显示成科学计数法或者小数点后一长串。原因KZWI2是 CURR 类型带参考字段和货币单位直接输出走的是内部表示。解决在事件方法里用WRITE ... TO先格式化到字符字段Layout 里输出字符字段。6. 二次开发把硬编码报表改成可复用查询页这份实例的骨架是对的但离「能交给业务长期用」还差几步。我按改造优先级排一下。6.1 查询条件外置两个写死的输入框换成一组可配置条件至少在 Attributes 里维护一个条件表Layout 循环生成输入框事件方法循环读取并拼 WHERE 条件。这样加一个「销售组织」条件不用同时改两个地方。注意拼接动态 WHERE 时用WHERE (lv_where)的括号写法别用字符串拼 SQL注入风险在老系统里一样存在。6.2 从逐行查询收敛成批量LOOP里 6 次SELECT SINGLE是性能瓶颈。改法是先收集所有 VBELN 到一张小表然后用FOR ALL ENTRIES批量捞 先把本页所有凭证号收集起来驱动后续批量查询 DATA: lt_vbeln TYPE STANDARD TABLE OF vbak-vbeln. lt_vbeln VALUE #( FOR wa IN zsdfb_yshddcx ( wa-vbeln ) ). SORT lt_vbeln. DELETE ADJACENT DUPLICATES FROM lt_vbeln. 一次性取回所有订单金额按凭证号汇总 SELECT vbeln, SUM( kzwi2 ) AS kzwi2 FROM vbap INTO TABLE DATA(lt_sum) WHERE vbeln IN lt_vbeln GROUP BY vbeln.FOR ALL ENTRIES有个硬性前提驱动表不能为空空表会导致全表扫描用之前必须判空并去重。这是 ABAP 里最常见的性能陷阱之一。批量取回的数据再READ TABLE ... BINARY SEARCH或建哈希表回填整体从 N 次往返降到 3 到 4 次。6.3 验证清单改完不要只看页面出没出数。我的习惯是每次动这类报表必走四步清空条件提交一次看报错提示正常条件提交两次看数据有没有翻倍输入一个不存在的客户看空表状态输入带特殊字符的客户名看排版会不会崩。这四步能覆盖八成回归问题。改造的收益也很直观原来一行 6 次查询、100 行 600 次往返收敛后固定 4 次原来加一个条件改两个文件现在改配置表就行。真要往下走还可以把查询逻辑抽到独立的类方法里让 BSP 页面只做展示同一套逻辑再挂到别的入口上复用成本几乎为零。血泪经验是这类老页面最危险的不是写不出来而是「看着能跑」。MODIFY漏写、内表没CLEAR、事件名差一个字符页面都能正常打开只有业务用两天之后才会发现数据不对。所以从那以后我每次改完 BSP 页面都强制连点两次提交、看一眼空表状态再交付。希望帮到你。本文还有配套的精品资源点击获取