Fiori-Demo1

下面是用WEBIDE开发SAPUI5页面实现一个简单重置ERP用户密码功能,前端又做了一遍,也借此理一下思路。

分析设计


以目标为导向,我们要开发这么一个页面,输入用户名和手机号,或者邮箱,点击提交后,ERP重置密码发送指定的短信或邮箱。我们只做到弹出消息,接下的发送短信和邮件功能不做。

接到这么一个需求,首先想到需求要分为两大部分,前端的页面开发和后端的OData服务(实现密码重置功能)。

前端页面

对于习惯了后端开发的ABAPer来说前端让人无从下手,我是这么做的,因为之前把WalkThrough做了一遍,有了大概的印象,从最简单的HelloWorld只有一个html,到慢慢加入XMLview,再加入Controller,再加入多语言,再加入Component配置文件,再加入Descriptor manifest文件,再加入CSS等等吧,WalkThrough真的是由浅入深的带我们进入到SAPUI的开发,所以我建议像我一样的初学者认真的把这些示例做上n(n>3)遍,对于关键章节,比如View和Controller要一直重复做,直到可以脱离copy、paste,随后就能把框架搭出来,个人认为非常有必要,不啰嗦了,继续。

  • index.html

    新建项目A_DEMO,创建Fileindex.html,敲入一个页面最基本的节点:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!DOCTYPE html>
    <html>
    <head>
    <title>ERP秘密重置</title>
    </head>
    <body>
    </body>
    </html>

再加入两个’meta’,这里不有深究

1
2
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">

上面很容易理解,然后因为我们要用sapui5开发所以需要加入bootstrap,然后引入我们的View:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script
id="sap-ui-bootstrap"
src="../../resources/sap-ui-core.js"
data-sap-ui-theme="sap_belize"
data-sap-ui-libs="sap.m"
data-sap-ui-compatVersion="edge"
data-sap-ui-preload="async"
data-sap-ui-resourceroots='{
"sap.ui.demo.wt": "./"
}' >
</script>
<script>
sap.ui.getCore().attachInit(function () {
sap.ui.xmlview({
viewName : "sap.ui.demo.wt.view.App"
}).placeAt("content");
});
</script>

这里我在Fiori-Step4-XML-views里做过介绍,就不再多说了,需要解释一点src="../../resources/sap-ui-core.js",如果src用的是相对路径,那么一定要在项目根目录创建一个项目配置文件’neo-app.json’。
然后我们再加入指定View的代码和把View放到body的代码

1
2
3
4
5
6
7
<script>
sap.ui.getCore().attachInit(function () {
sap.ui.xmlview({
viewName : "sap.ui.demo.wt.view.App"
}).placeAt("content");
});
</script>
1
2
<body class="sapUiBody" id="content">
</body>

至此index.html就已经完成不需要做任何调整了,这里就是指定了我们的运行环境和View文件,接下接就完全由View和Controller文件完全接手了。

  • 创建neo-app.json文件
    详细信息可以参考Create a neo-app.json Project Configuration File,后面OData的配置时我们还要此文件进行配置。

  • 创建’view’文件夹

  • 创建’App.view.xml’

    我们要写一个XMLview,就一定要<mvc:View>做为根节点,然后一定有一个controllerName属性来指定controller文件的路径然,这里就是文件和文件之间的联系,然后加入View所用到的namespace,就是下面的一堆xmlns

1
2
3
4
5
6
7
8
9
<mvc:View
controllerName="sap.ui.demo.wt.controller.App"
xmlns="sap.m"
xmlns:core="sap.ui.core"
xmlns:l="sap.ui.layout"
xmlns:f="sap.ui.layout.form"
xmlns:mvc="sap.ui.core.mvc" >
</mvc:View>



怎么理解这个namespace呢,结合上面的API文档,我们可以看到其实它就是我们要用到的不同的API,冒号和后面的’core’ ‘l’ ‘f’ ‘mvc’又是干吗的呢,比如说我要在View中加一个Label控件,简单一搜API文档就看到这几个库里都有Label,这时就需要加一个前缀,就是namespace来区分,第一个'xmlns="sap.m"后面没有冒号,那它就是老大,就是默认namespace,页面中所有什么都不加直接写标签名的都是用的它下面的控件。


看这一个布局,其实就是一个简单的simpleform控件,上面是title,下面是一个分成两列的form结构。

API中那么多控件我应该选择那一个用,怎么入手呢,不用急,SAP提供了很好的参考示例,并且都附有模版代码,我们只需要像逛超市一样选择相中的样式copy过来改改就可以。

打开后可以根据需要进行过滤筛选,也可以在进而进行search

既然来选择布局的,那我就输入layout来进行搜索咯,然后会出现好多layout供我们选择。

下面这个布局很面熟吧,就它了

点击右上角查看源码,找到想要的部分copy下来或者可以整个下载下来慢慢试,下面我想要的部分:


OK,拷过来就可以开始改了,不会?没关系,SAP又提供了API的具体用法,参考API reference


这里就可以看到有两个Module都有这个SimpleForm控件,所以我们要用:f来加以区分。这里对每一个属性的用法都有做介绍,非常详细。
改完以后的代码就是下面这样了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<mvc:View
controllerName="sap.ui.demo.wt.controller.App"
xmlns="sap.m"
xmlns:core="sap.ui.core"
xmlns:l="sap.ui.layout"
xmlns:f="sap.ui.layout.form"
xmlns:mvc="sap.ui.core.mvc" >
<f:SimpleForm id="SimpleForm1" layout="ResponsiveGridLayout"
title="ERP系统-用户密码自主重置"
labelSpanXL="4" labelSpanL="4" labelSpanM="12" labelSpanS="12" adjustLabelSpan="false"
emptySpanXL="0" emptySpanL="0" emptySpanM="0" emptySpanS="0" columnsXL="2" columnsL="2" columnsM="2" singleContainerFullSize="false">
<f:content>
<core:Title text="信息输入"/>
<Label text="用户帐号" width="100%" id="__label0"/>
<Input width="50%" valueLiveUpdate="true" id="UserId"/>
<Label text="手机号" width="100%" id="__label1"/>
<Input width="50%" valueLiveUpdate="true" id="UserTel"/>
<Label text="邮箱" width="100%" id="__label2"/>
<Input width="50%" id="UserMail"/>
<Label/>
<Button text="提交" width="100px" id="__button0" press="onSubmit"/>
<core:Title text="输入说明"/>
<Text text="1.邮箱和移动电话任意输入一项即可;\n 2.两项都输入时,先验证邮箱,后验证移动电话,只要有一项和ERP系统用户信息中维护的一致即可;\n 3.有任何建议或意见,请发邮件到:basis@shenhua.cc" id="__text0"/>
</f:content>
</f:SimpleForm>
</mvc:View>

OK至此,view也做完了,剩下的就是controller了。

  • 新建controller文件夹和App.controller.js文件

    速度太慢了,加快速度!contrller的写法我在之前的帖子上也做了详细说明了,就不再赘述了,看注释来理解吧,很简单,最关键的就是callFucntion来调用ODATA服务和成功后的处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
sap.ui.define(["sap/ui/core/mvc/Controller", "sap/ui/model/json/JSONModel"], function(Controller, JSONModel) {
"use strict";
return Controller.extend("sap.ui.demo.wt.controller.App", {
// 按钮事件
onSubmit: function() {
//定义三个变量来取屏幕上三个Input控件输入的值
var UserId = this.getView().byId("UserId").getValue();
var UserTel = this.getView().byId("UserTel").getValue();
var UserMail = this.getView().byId("UserMail").getValue();
//定义OModel,并指定OModel路径
var oModel = new sap.ui.model.odata.ODataModel("/sap/opu/odata/sap/ZGJZ_PWD_RESET_SRV/");
//绑定oModel到view上
this.getView().setModel(oModel);
//Function import的参数
var urlParameters = {
"userMail": UserMail,
"userTel": UserTel,
"userId": UserId
};
//Call ODATA
oModel.callFunction("/ZGJZ_PWD_RESET", {
method: "GET", //Default "GET"
urlParameters: urlParameters, //Function import的参数
//ODATA访问成功的话执行下面的处理
success: function(oData, response) {
// var aa = oData.results.length;
var resType = oData.Type;
var resMsg = oData.Msg;
var resPwd = oData.Pwd;
if (resType == "S") {
sap.m.MessageToast.show(resMsg + resPwd);
} else if (resType == "F") {
sap.m.MessageToast.show(resMsg);
}
},
//ODATA访问失败的话执行下面的处理
error: function(error) {
sap.m.MessageToast.show("Server Connection Aborted");
}
});
}
});
});


这里最关键的就是这个callFunction了,两个参数,第一个是要调的function名字,第二个是{}引起来的一个map, 其中又可以有多个参数,比如method,这里写了GET,默认也是GET,所以可以省略,最最重要的就是后面的两个,success和error,这是两个返回函数,分别是ODATA执行成功时和失败时执行的过程,所以我们要写的逻辑也就在这个里面。success: function(oData, response)error: function(error)可以理解成固定写法,执行ODATA后所有的返回值都是在这里面,debug打开找吧,这个例子中返回值是一个entity所以直接用oData.属性值就能访问,如果是entityset,就要for循环出来一一处理了。

后端ODATA开发

  • SE11 创建结构

    这里主要是为了方便后面用import的方式建entity,不建结构也可以
  • SEGW 创建Project

import创建entity(如果Import DDIC遇到下拉列表为空的情况,用en语言重新登录GUI即可)

说一下的是这些字段Creatable/Updatable/Sortable/..等这些flag,它们的作用就是在生成metadata时告诉oData的使用者,哪些字段可以做Sort,哪些不为空,这里的设置并不会影响功能。

建了entity以后同样在Data Model那里右键创建一个Function import,



注意我这里Return的是一个Entity,并没有返回EntitySet,意思就是返回的是一行数据或者说一个结构,不是内表。HTTP要用GET。

建完以后生成然后进行到EXT中进行方法的redefine

在这个类中找到EXECUTE_ACTION方法,右键redefine

下面是实现的代码,ABAP的内容就不介绍了,时间仓促,写的也是最简单的代码,不考虑效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
method /IWBEP/IF_MGW_APPL_SRV_RUNTIME~EXECUTE_ACTION.
DATA: ls_parameter TYPE /iwbep/s_mgw_name_value_pair,
lt_userinfo TYPE TABLE OF zuserinfo,
ls_PWD TYPE ZGJZ_PWD,
ls_entity TYPE ZCL_ZGJZ_PWD_RESET_MPC=>TS_REPMSG.
TYPES: BEGIN OF ty_input,
userMail(30),
userTel(11),
userId(20),
END OF ty_input.
data ls_input type ty_input.
data l_pass.
data l_pwd_randow type string.
data l_pwd_new type XUNCODE.
data l_XUBNAME type XUBNAME.
DATA LT_PWD TYPE /GRCPI/GRIA_T_PASWDRESTDATA.
DEFINE return_msg.
ls_entity-type = &1.
ls_entity-msg = &2.
ls_entity-pwd = &3.
copy_data_to_ref( EXPORTING is_data = ls_entity
CHANGING cr_data = er_data ).
end-OF-DEFINITION.
IF iv_action_name = 'ZGJZ_PWD_RESET'. " Check what action is being requested
IF it_parameter IS NOT INITIAL.
* Read Function import parameter value
READ TABLE it_parameter INTO ls_parameter WITH KEY name ='userId'.
IF sy-subrc = 0.
ls_input-userId = ls_parameter-value.
ENDIF.
READ TABLE it_parameter INTO ls_parameter WITH KEY name ='userTel'.
IF sy-subrc = 0.
ls_input-userTel = ls_parameter-value.
ENDIF.
READ TABLE it_parameter INTO ls_parameter WITH KEY name ='userMail'.
IF sy-subrc = 0.
ls_input-userMail = ls_parameter-value.
ENDIF.
* get user detail via BAPI
DATA LS_USERNAME TYPE BAPIBNAME-BAPIBNAME.
DATA LT_RETURN TYPE TABLE OF BAPIRET2.
DATA LT_ADDTEL TYPE TABLE OF BAPIADTEL.
DATA LT_ADDSMTP TYPE TABLE OF BAPIADSMTP.
DATA LS_RETURN like LINE OF LT_RETURN.
DATA LS_ADDTEL like LINE OF LT_ADDTEL.
DATA LS_ADDSMTP like LINE OF LT_ADDSMTP.
LS_USERNAME = ls_input-userId.
CALL FUNCTION 'BAPI_USER_GET_DETAIL'
EXPORTING
USERNAME = LS_USERNAME
TABLES
RETURN = LT_RETURN
ADDTEL = LT_ADDTEL
ADDSMTP = LT_ADDSMTP.
IF SY-SUBRC = 0.
READ TABLE lt_return INTO LS_RETURN with key TYPE = 'E'.
IF sy-subrc = 0.
return_msg 'F' LS_RETURN-message ''.
RETURN.
ENDIF.
LOOP AT LT_ADDSMTP INTO LS_ADDSMTP.
TRANSLATE LS_ADDSMTP-E_MAIL TO UPPER CASE.
TRANSLATE ls_input-userMail TO UPPER CASE.
IF LS_ADDSMTP-E_MAIL = ls_input-userMail.
l_pass = 'X'.
exit.
ENDIF.
ENDLOOP.
LOOP AT LT_ADDTEL INTO LS_ADDTEL.
IF LS_ADDTEL-telephone = ls_input-userTel.
l_pass = 'X'.
exit.
ENDIF.
ENDLOOP.
IF l_pass = 'X'.
*GENERAL_GET_RANDOM_PWD 生成随机密码
CALL FUNCTION 'GENERAL_GET_RANDOM_PWD'
EXPORTING
NUMBER_CHARS = 8
IMPORTING
RANDOM_PWD = l_pwd_randow
.
l_pwd_new = l_pwd_randow.
l_XUBNAME = ls_username.
CALL FUNCTION '/GRCPI/GRIA_USER_RESET_PWD'
EXPORTING
IV_USERID = l_XUBNAME
IV_PASSWORD_NEW = l_pwd_new
* IV_SYNCHONIZE =
* IV_VERIFICATION_SYSTEM =
* IMPORTING
* EV_PASSWORD =
* EV_EMAIL =
* EV_RETURN_CODE =
* ET_MESSAGE =
TABLES
ET_USRPSWDDATA = LT_PWD
.
return_msg 'S' '密码重设成功,初始密码为:' l_pwd_new.
*/GRCPI/GRIA_USER_RESET_PWD 重设密码
ELSE.
return_msg 'F' '邮箱和手机号验证失败' ''.
ENDIF.
ELSE.
return_msg 'F' 'BAPI执行失败' ''.
ENDIF.
ENDIF.
ENDIF.
endmethod.

  • 注册服务

    注册此服务到前端。

    点击Maintain进行Gateway Server,然后打开Gateway Client进行测试



/sap/opu/odata/sap/ZGJZ_PWD_RESET_SRV/$metadata 查看 metadata

/sap/opu/odata/sap/ZGJZ_PWD_RESET_SRV/ZGJZ_PWD_RESET?userMail='aaa@lg.com'&userTel='15801615353'&userId='ZZGUOJZ'直接对我们的函数进行测试,注意多个参数的先后顺序和大小写,中间用’&’分开。可以看出’?’后的部分’userMail=’aaa@lg.com’&userTel=’15801615353’&userId=’ZZGUOJZ’’就是我们在前端页面中需要传入的参数

  • 后端debug
    执行,此时如果我们在实现的方法中加了外部断点,就可以debug到方法中,注意这里如果停的时候过长,整个请求会connection time out

    这里看一下这些传入参数的值:


    我们最终的输出就在这里,不论是返回的结构(Entity)还是内表(EntitySet),只要填充到这里就可以。

    下面就可以看到服务的返回值了:

OK。接下来我们就要到前端去验证使用这个ODATA服务了

  • 前端debug
    执行我们开发的前端页面,如果用的是chrome浏览器,点击F12进行控制台,在resource中找到我们要debug的controller,代码前加入断点。

    在前端页面输入对应的值后点击按钮“提交”就会自动运行至断点,在这里我们可以查看相应的变量有没有正确赋值。更详细的debug方法可以百度jsdebug,基本都是类似的。

忘了一个重要的内容,以上执行是不能成功的,因为没有连接后端的配置,我一开始也是做到这里,但执行时发现ODATA服务并不是连的后端的域名,而是locaohost:xxxx,原因就是在neo-app.json文件中需要增加后台服务器的配置信息。
完整的neo-app.json文件应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"welcomeFile": "/index.html",
"routes": [
{
"path": "/resources",
"target": {
"type": "service",
"name": "sapui5",
"entryPath": "/resources"
},
"description": "SAPUI5 Resources"
},
{
"path": "/sap/opu/odata",
"target": {
"type": "destination",
"name": "N74",
"entryPath": "/sap/opu/odata"
},
"description": "N74"
},
{
"path": "/test-resources",
"target": {
"type": "service",
"name": "sapui5",
"entryPath": "/test-resources"
},
"description": "SAPUI5 Test Resources"
}
],
"sendWelcomeFileRedirect": true
}

完整代码:http://pan.baidu.com/s/1bpgwAqN 不用谢

结构转化

加入component

上面的示例非常简单,基本实现了需求,但文件结构有点简单,往FLP中迁移的话结构需要再做调整,我们下面就继续按tutorials中文件结构一步步调整它。

  1. 加入component容器
    这里首先要修改index.html,修改<script>标签,加入对component容器的引用声明:
    As-Is:
    1
    2
    3
    4
    5
    6
    7
    <script>
    sap.ui.getCore().attachInit(function () {
    sap.ui.xmlview({
    viewName : "sap.ui.demo.wt.view.App"
    }).placeAt("content");
    });
    </script>

To-Be:

1
2
3
4
5
6
7
<script>
sap.ui.getCore().attachInit(function() {
new sap.ui.core.ComponentContainer({
name : "sap.ui.demo.wt"
}).placeAt("content");
});
</script>

index.html根目录下新建文件Component.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sap.ui.define(
["sap/ui/core/UIComponent"],
function(UIComponent) {
"use strict";
return UIComponent.extend("sap.ui.demo.wt.Component", {
//关联rootview
metadata : {
rootView: "sap.ui.demo.wt.view.App"
},
init: function() {
//call the init function of the parent
UIComponent.prototype.init.apply(this, arguments);
//定义OModel,并指定OModel路径
var oModel = new sap.ui.model.odata.ODataModel("/sap/opu/odata/sap/ZGJZ_PWD_RESET_SRV/");
//绑定oModel到view上
this.setModel(oModel);
}
});
});

可以看到这里首先用metadata/rootview来定义的应用打开的初始view,并且将Model的代码从Controller中挪了过来,真正实现了MVC结构。

  1. 调整Controller.js

再次执行程序,一样的效果说明我们的component加入成功

加入manifest和多语言i18n

在加入component之后,我们再来尝试加入manifest文件,同时将文本字段写到一个多语言的i18n.properties文件中集中进行维护。

  1. index.html根目录下新建文件manifest.json:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    {
    "_version": "1.1.0",
    "sap.app": {
    "_version": "1.1.0",
    "id": "sap.ui.demo.wt",
    "type": "application",
    "i18n": "i18n/i18n.properties",
    "title": "{{appTitle}}",
    "description": "{{appDescription}}",
    "applicationVersion": {
    "version": "1.0.0"
    }
    },
    "sap.ui": {
    "_version": "1.1.0",
    "technology": "UI5",
    "deviceTypes": {
    "desktop": true,
    "tablet": true,
    "phone": true
    },
    "supportedThemes": ["sap_belize"]
    },
    "sap.ui5": {
    "_version": "1.1.0",
    "rootView": "sap.ui.demo.wt.view.App",
    "dependencies": {
    "minUI5Version": "1.30",
    "libs": {
    "sap.m": {}
    }
    },
    "models": {
    "i18n": {
    "type": "sap.ui.model.resource.ResourceModel",
    "settings": {
    "bundleName": "sap.ui.demo.wt.i18n.i18n"
    }
    }
    },
    "contentDensities": {
    "compact": true,
    "cozy": true
    }
    }
    }

我们来分别理解一下这个APP配置文件中这些内容都是什么意思,其中像’version’这类直观就能看懂的就不再说了,捡几个重要的:

  • "id": 看内容应该也能猜到这里配置的是这个APP的resourceroot属性
  • "i18n": 配置了多语言文件的相对路径和文件名,一般都是"i18n/i18n.properties"
  • "rootView": 这里又将rootvie从component中挪到了这里来定义
  • "models": 至此我们发现model可以远处不在,可以在manifest中定义,可以在component中定义,可以在controller中定义,如果实在是懒甚至可以写在html中,但一个清晰的文件结构也是我们的学习内容之一,从此以后我们就尝试将model定义在manifest文件中,不论是JSONModel、XMLModel(最好以文件形式出现)还是ODataModel,都是在这里进行定义,WEBIDE提供的Descriptor Editor也方便我们直观的进行配置
  1. 调整Component.js
    1
    2
    3
    metadata : {
    manifest: "json"
    },

使用metadata/manifest来声明应用将使用的配置文件

  1. 创建i18n.properties
    接下来要建一个i18n文件夹和一个i18n.properties文件,这个文件完全可以理解成ABAP开发中的“文本元素”,就不多说了
  2. 替代文本元素
    接下来我们将view中的文本都替代到i18n文件中,下图可以看到一个修改后的和修改前的一个对比,这里我已经将“用户帐号”用{i18n>userID}进行了替代,其实利用WEBIDE的layout Editor维护起来非常的方便,比如下面的“手机号”文本,只需要点点击Text后面的链接按钮

    删除原来的文本,选择’i18n’,然后点击’+’

    在弹出的窗口中填入占位符和相应的文本

    把常量文本全部替换后App.view.xml和i18n.properties的样子,这样维护起来是不是轻松多了


    此时再执行程序,效果和原来一模一样:
  • 创建Model
    使用Descriptor Editor来创建oModel,以替换在component中定义oModel的语句:(同时可以看到在manifest文件中已经写入了ResourceModel的配置)






此时再执行程序,效果依然和原来一模一样:

至此,我们又成功加入的manifest.json和i18n.properties!

下面放上导出的完整代码,供参考:

http://pan.baidu.com/s/1pL2tb8B

参考

Create a neo-app.json Project Configuration File

Jim Guo wechat
ex. subscribe to my blog by scanning my public wechat account