SAP 基于VUE的BSP OO单页移动端Web App开发

孤者浪人 提交于 2020-01-31 02:11:18

前言

之前的一篇文章,通过传统的Page with Flow Logic形式去实现了基于Vue的One Page Application。这种做法其实存在一些设计上的问题,前端页面交互层还好说,但后端服务层明显不符合RESTful设计风格,一个API居然还有View的代码?(二当家:帮主哇,我们不用OO,光去看人家的Page with Flow Logic,被同行知道了会笑我们变态的)咳咳,有鉴于此,恰好近日因肺炎推迟上班,被禁足在家,所以特别补全一个OO方式实现的例子。
注:本篇仍将实现一个查询库存的例子,但项目的结构、前端HTML页面代码以及SAP后端代码很多重要细节都与上一篇文章有所不同,请留意。

与传统BSP方式相比,OO实现BSP的方式有两点好处

  • 前后端逻辑可以完全分离,符合MVC,代码更易阅读和维护
  • 可以独立于页面实现类似RESTful风格的API,显得更专业

但受制于NetWeaver平台,BSP本身也还有如下缺点

  • 不能完全实现MVC,Model在BSP里面其实不存在,直接Open SQL完事。skr…skr…
  • 尽管能做成RESTful API,但分发还得通过SICF,一个服务一个Application。另外,个人猜测,可能并非所有的HTTP方法BSP都支持,但这个无伤大雅,一般HTTP POST日常也足够了。

GKD 搞快点

Step I 资源和业务分开

在上一篇中,我们将应用依赖的js和css文件都上传到了应用本身的目录下,但它们其实是可以被整个Package下的BSP应用共享的,如果你其它的BSP应用也引用了这些文件,那么类库的版本、前端样式能够统一,利于代码复用和后期维护,同时浏览器当发现资源来自同一个url时,不会去重复下载,节约带宽资源。基于这种种好处,也为了避免这些MIME中的文件被业务代码污染,笔者建议单独创建一个BSP应用,专门用于存储这些常用类库。见下:

01_01

为确保允许匿名访问,请注意去掉Application中缺省勾选的XSRF Protection。下面的YBSP_OO_DEMO也是同样的,不再赘述。
01_2

将这些公用的类库放到一个归口的应用后,我们就可以创建一个空的业务BSP应用,YBSP_OO_DEMO:
01_3

Step II 通过Page Fragments复用html

正如之前我们的BSP应用,Buefy控制前端控件样式的展现,Vue.js扮演Controller的角色来做渲染和事件处理,axios调用HTTP POST异步访问SAP Server。这三板斧是我们每次都会用到的,所以必定在每个应用的html header中引用到它们。虽说我们的例子目前只涉及一个前端页面,但实际生产环境中,随着业务复杂,一个应用中可能还会包含多个页面。为了减少重复的代码,我们把这个通用的html header代码存放到Page Fragments中,以利于后续的项目使用。
02_1

02_2
_header.htm的代码见下,代码中备注的信息请留意:

<%@page language="ABAP"%>
<!DOCTYPE html> <!--解决Safari中Buefy移动模式mobile card显示不正确的问题-->
<html>
  <head>
    <title>Inventory Report</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0"><!--页面自适应移动端-->
    <meta name="format-detection" content="telephone=no"><!--防止纯数字被识别为手机号码-->
    <meta name="apple-mobile-web-app-capable" content="yes" ><!--Safari识别为Web应用-->
    <meta name="mobile-web-app-capable" content="yes"><!--Chrome识别为Web应用-->

    <link rel="icon" href="../YBSP_LIBRARY/monkey.png" type="image/x-icon"/> <!--页面图标-->
    <link rel="stylesheet" href="../YBSP_LIBRARY/buefy.min.css" /> <!--buefy-->
    <script src="../YBSP_LIBRARY/vue.js"></script> <!--vue.js-->
    <script src="../YBSP_LIBRARY/axios.min.js"></script> <!--axios-->
  </head>
  <body>
    <script src="../YBSP_LIBRARY/buefy.min.js"></script> <!--buefy-->
    <script src="../YBSP_LIBRARY/all.min.js"></script> <!--icon相关-->
    <div id="app"> <!--app 根节点-->

Buefy 支持两种icon类型,缺省是Materail Design Icon,而另一种是FontAwesome。最大的区别在于icon的实现机制不同。使用Material Design Icon,会有访问SAP服务资源导致弹出账号密码验证的问题,故放弃。使用FontAwesome,须要引入其js文件,即上面的all.min.js。这个文件,你可以在FontAwesome在Github上的代码库中找到并下载下载:https://github.com/FortAwesome/Font-Awesome
FA

Step III 创建查询页面

不同于传统方式,OO的BSP,我们创建的View是不带Flow Logic的。
03_1

main.htm的代码见下,它在头部引用了我们的Page Fragments。

<%@page language="abap"%>
<%@include file="_header.htm" %>
      <section class="section">

          <b-field label="Material Number" :label-position="labelPosition">
            <b-input maxlength="18" v-model="matnr"></b-input>
          </b-field>

          <b-field label="Plant" :label-position="labelPosition">
              <b-input type="search" maxlength="4" v-model="werks" @keyup.enter.native="search"></b-input>
              <p class="control">
                  <b-button class="button is-primary" @click="search">Search</b-button>
              </p>
          </b-field>

          <b-table
            :data="tab"
            :bordered="false"
            :striped="true"
            :narrowed="false"
            :hoverable="false"
            :loading="false"
            :focusable="false"
            :icon-pack="icon_pack"
            :mobile-cards="true"
            :paginated="true"
            :per-page="20">
            <template slot-scope="tab">
              <b-table-column field="id" label="ID" sortable numeric>
                  {{ tab.row.id }}
              </b-table-column>
              <b-table-column field="matnr" label="Material" sortable>
                  {{ tab.row.matnr }}
              </b-table-column>
              <b-table-column field="maktx" label="Description">
                  {{ tab.row.maktx }}
              </b-table-column>
              <b-table-column field="lgort" label="Location" sortable>
                  {{ tab.row.lgort }}
              </b-table-column>
              <b-table-column field="labst" label="Quantity" centered sortable numeric>
                <span class="tag is-success">
                  {{ tab.row.labst }}
                </span>
              </b-table-column>
            </template>
            <template slot="empty">
                <section class="section">
                    <div class="content has-text-grey has-text-centered">
                        <b>No Data</b>
                    </div>
                </section>
            </template>
          </b-table>
      </section>
          <b-loading :is-full-page="true" :active.sync="isLoading" :can-cancel="false"></b-loading>
    </div>
    <script>
      const restService = '_main.do';
      var app = new Vue({
        el: "#app",
        data:{
          viewName: "Inventory Report",
          labelPosition: 'on-border',
          isLoading: false,
          matnr: '',
          werks: '',
          tab: [],
          hide_hd: true,
          icon_pack: 'fa'
        },
        methods:{
          search: function(){
          axios.post(restService,{
          data:{
            'matnr': this.matnr,
            'werks': this.werks
          }
          }).then((response) =>{
            try{
              if(response.data.msg.msgty == "E"){
                this.$buefy.toast.open({
                duration: 5000,
                message: response.data.msg.msg,
                position: 'is-bottom',
                type: 'is-danger'
                });
              }
              else if(response.data.msg.msgty == "S"){
                this.tab = eval(response.data.tab);
              }
              else{
                this.$buefy.toast.open({
                duration: 5000,
                message: 'Unknow Exceptions',
                position: 'is-bottom',
                type: 'is-danger'
                });
              }
            }
            catch(err)
            {
                this.$buefy.toast.open({
                duration: 5000,
                message: 'Unknow Exceptions',
                position: 'is-bottom',
                type: 'is-danger'
                });
            }
            finally{
              this.isLoading = false;
            }
            });
            this.isLoading = true;
            this.hide_hd = false;
      }
        }
      });
    </script>
</body>
</html>

上面的代码中icon-pack使用fa即是使用FontAwesome中的图标,以免Buefy缺省使用Material Design中的icon导致界面图标无法显示。

OO中的View是不能直接访问的,必须通过Controller去调用它。所以我们必须新建一个Controller,在SE24中我们创建一个类YCL_BSP_DEMO,它继承了一个BSP标准父类CL_BSP_CONTROLLER2

04_01

我们须要实现它的方法DO_REQUEST,当它接收到HTTP GET请求时,返回main.htm:
04_02

  method DO_REQUEST.
*CALL METHOD SUPER->DO_REQUEST
*    .
    DATA: MYVIEW TYPE REF TO IF_BSP_PAGE.
    MYVIEW = CREATE_VIEW( VIEW_NAME = 'main.htm' ).
    CALL_VIEW( MYVIEW ).
  endmethod.

再次回到SE80控制台,我们新建一个Controller YBSP_OO_DEMO:
04_05
04_06
Controller Class使用我们刚才创建的类并保存激活:
04_07

接着在SICF中设置好登陆页面时使用的公共账号和密码:
04_03

04_04
回到SE80控制台中,在YBSP_OO_DEMO.do上右击并选择Test:
04_08
如无意外,你将看到熟悉的查询界面:
04_09

Step IV 创建RESTful服务

完成了前端展现页面的实现后,我们接着须要实现后端查询的功能。就像前面一早提过的,API不须要页面,它只是个接口,所以这次我们只须要新建一个Controller来实现查询功能就行了,和上面一样,我们先在SE24中新建一个继承了CL_BSP_CONTROLLER2的类,YCL_BSP_DEMOS:
05_01

同样实现DO_REQUEST方法如下:

  METHOD DO_REQUEST.
*CALL METHOD SUPER->DO_REQUEST
*    .
    DATA:LS_PARA TYPE TY_INPUT,
         LV_STR  TYPE STRING.

    DATA:BEGIN OF RESP,
           MSG TYPE TY_MSG,
           TAB TYPE TY_T_TAB,
         END OF RESP.

    CLEAR:LS_PARA,
          LV_STR,
          RESP.

    LV_STR = RUNTIME->SERVER->REQUEST->GET_CDATA( ).

    CALL METHOD CL_FDT_JSON=>JSON_TO_DATA
      EXPORTING
        IV_JSON = LV_STR
      CHANGING
        CA_DATA = LS_PARA.

    TRANSLATE:LS_PARA-DATA-MATNR TO UPPER CASE,
              LS_PARA-DATA-WERKS TO UPPER CASE.

    IF LS_PARA-DATA-MATNR IS INITIAL.
      RESP-MSG-MSGTY = 'E'.
      RESP-MSG-MSG = 'Please input a Materail Number!'.
    ELSEIF LS_PARA-DATA-WERKS IS INITIAL.
      RESP-MSG-MSGTY = 'E'.
      RESP-MSG-MSG = 'Please input a Plant Number!'.
    ELSE.
      RESP-MSG-MSGTY = 'S'.
      SELECT A~MATNR
             A~LGORT
             A~LABST
        INTO CORRESPONDING FIELDS OF TABLE RESP-TAB
        FROM MARD AS A
       WHERE A~MATNR LIKE LS_PARA-DATA-MATNR
         AND A~WERKS EQ LS_PARA-DATA-WERKS
         AND A~LABST GT 0.

      IF RESP-TAB[] IS NOT INITIAL.
        SELECT MATNR,MAKTX
          INTO TABLE @DATA(LT_MAKTX)
          FROM MAKT
           FOR ALL ENTRIES IN @RESP-TAB
         WHERE MATNR EQ @RESP-TAB-MATNR.

        SORT:LT_MAKTX BY MATNR,
             RESP-TAB BY MATNR LGORT.

        LOOP AT RESP-TAB ASSIGNING FIELD-SYMBOL(<FS_TAB>).
          <FS_TAB>-ID = SY-TABIX.

          READ TABLE LT_MAKTX ASSIGNING FIELD-SYMBOL(<FS_MAKTX>)
                               WITH KEY MATNR = <FS_TAB>-MATNR
                                        BINARY SEARCH.
          IF SY-SUBRC EQ 0.
            <FS_TAB>-MAKTX = <FS_MAKTX>-MAKTX.
          ENDIF.
        ENDLOOP.
      ENDIF.
    ENDIF.

    LV_STR = /UI2/CL_JSON=>SERIALIZE( DATA = RESP COMPRESS = ABAP_TRUE PRETTY_NAME = /UI2/CL_JSON=>PRETTY_MODE-CAMEL_CASE ).

    RUNTIME->SERVER->RESPONSE->SET_HEADER_FIELD( NAME = 'Content-Type' VALUE = 'application/json;charset=utf-8' ).
    RUNTIME->SERVER->RESPONSE->SET_CDATA( DATA = LV_STR ).
  ENDMETHOD.

上面的代码和之前一篇文章的代码大同小异,但有几点须要注意:

  • 都须要通过一个结构去解析传过来的JSON格式Request
  • 都须要在返回的HTTP header中指明Response的类型格式和编码
  • 非常重要的一点,使用/UI2/CL_JSON这个类去将表结构转化为了JSON格式,而在上篇文章中,我们使用的是CL_TREX_JSON_SERIALIZER。两者的区别在于,/UI2/CL_JSON不会将数字转化成文本格式返回,这样前端Buefy的Table控件才能正常的对它们排序,而后者则是无脑全部以文本形式返回,这点使用时须留意。

另用到的TYPE的定义:

CLASS YCL_BSP_DEMOS DEFINITION
  PUBLIC
  INHERITING FROM CL_BSP_CONTROLLER2
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.
    TYPES:BEGIN OF TY_DATA,
            MATNR TYPE MATNR,
            WERKS TYPE WERKS_D,
          END OF TY_DATA,
          BEGIN OF TY_INPUT,
            DATA TYPE TY_DATA,
          END OF TY_INPUT,
          BEGIN OF TY_TAB,
            ID    TYPE I,
            MATNR TYPE MATNR,
            MAKTX TYPE MAKTX,
            LGORT TYPE LGORT_D,
            LABST TYPE LABST,
          END OF TY_TAB,
          TY_T_TAB TYPE STANDARD TABLE OF TY_TAB,
          BEGIN OF TY_MSG,
            MSGTY TYPE C LENGTH 1,      "S/E.
            MSG   TYPE CHAR50, 			"Message.
          END OF TY_MSG.

    METHODS DO_REQUEST
        REDEFINITION .

最后,让我们回到BSP应用中,创建一个基于YCL_BSP_DEMOS类的Controller。细心的读者可能已经猜到这个Controller的命名,在上面的html代码中,已经提示了这个Controller _main.do
05_02

至此,我们已经完成了从前端页面到后端服务的搭建,回到浏览器中,输入查询条件并执行,如数据没有问题,即宣告BSP开发成功:
final

YBSP_OO_DEMO 最终目录结构:
在这里插入图片描述

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!