TypeScriptでUI5アプリを開発する

## はじめに
SAPUI5関係での2023年の大きなニュースの一つは、TypeScriptが公式にサポートされるようになったことです（バージョン116.0から）。

https://blogs.sap.com/2023/07/27/typescript-for-ui5-yay-its-official-and-a-round-up-of-recent-changes/

それまでは「ベータ」のステータスで、型定義は使えるけれども完全な状態ではないという位置づけでした。TypeScriptを使用したプロジェクトのテンプレートは[generator-easy-ui5](https://www.npmjs.com/package/generator-easy-ui5)では用意されていましたが、SAP Business Application Studioで利用可能なテンプレートはなく、プロジェクトで使用するのはためらわれる状態でした。

今後はこの状況が変わります。少なくともBTP用に開発するUI5アプリに関しては、TypeScriptが現実的な選択肢となってきます（S/4HANAで利用するUI5アプリは、バックエンドで利用可能なバージョンに依存）。

そこでこの記事では、2023年12月時点でのUI5&TypeScriptの開発をする手順について紹介します。

## 開発環境
IDE: SAP Business Application Studio
UI5バージョン：1.120.3
バックエンドサービス：OData V4（前回の記事で作成。※TypeScriptを使用する前提条件ではありません）

## ステップ
1. UI5プロジェクトを作成
1. TypeScriptのソースを追加
1. Managed Approuterの設定
1. デプロイ

UI5アプリを作成する前のプロジェクトは以下の状態です。
ソースコード：https://github.com/miyasuta/cap-cds-typer
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c11732c5-5e3f-2ef4-afcb-765649b533b3.png)

mta.yamlを右クリックし、"**Create MTA Module from Template**"を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/ad4a2e4e-fb70-67c6-89ed-8b943940f47d.png)

"Approuter Configuration"をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/0f503937-3c55-83ba-668b-63141869f7f9.png)


### 1. UI5プロジェクトを作成
Template Wizardから"SAP Fiori application"を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/4774a895-9eca-f48e-995d-fd7338eee73d.png)

"List Report Page"のテンプレートを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/46d639cb-b11b-7520-3c39-1f66a59bbfaf.png)

データソースに"Use a Local CAP Project"を選択し、あらかじめ作成しておいたCAPのプロジェクト、サービスを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/b55a3976-6b2e-9ef0-7943-7d57d27b16c1.png)

メインのエンティティを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c8aaaa78-d7b3-a859-9851-ae6f2bf92be1.png)

プロジェクトの属性を入力します。TypeScriptを使うために"**Configure advanced options**"を"Yes"にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/5f026b0f-aba4-7058-a039-6a091c8c6a88.png)

Configure advanced optionsを有効にしたことで出てくるオプションのうち、最後の2つを有効化します。
- CAPプロジェクトにcds-plugin-ui5（※）を追加
- TypeScriptを有効化

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/9c5c2339-f758-5173-e247-f90e374727c6.png)

※[cds-plugin-ui5](https://www.npmjs.com/package/cds-plugin-ui5?activeTab=readme)はCAPのサーバの中でUI5アプリを実行するためのプラグインです。CAPプロジェクトの中でTypeScriptのUI5アプリを開発するために必須となります。

生成されたプロジェクトにはTypeScriptの特徴があります。
- tsconfig.json：TypeScriptに関する設定
- .babelrc.json：TypeScriptのソースをJavaScriptにトランスパイル（変換）するための設定
- Component.ts：TypeScriptで作成されている

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/be66ab64-6ebe-4a7d-baee-aff75ba44eb5.png)

TypeScriptで必要となる型の定義は、node_modules/@sapui5/typesにインポートされています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/37d3cb2f-674f-96bc-cf8a-96136e97f0ca.png)

`cds-ts watch`によりCAPアプリを起動すると、Web ApplicationsにUI5のプロジェクトのhtmlファイルが表示されます。一番上のリンクをクリックして開きます。動作は[こちら](https://qiita.com/tami/items/ea7017c1c4348043eac5)の記事の通りになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/cf1dd4ca-c162-1724-3ac4-fdfce863497d.png)

### 2. TypeScriptのソースを追加
List Reportを拡張し、保存前にメッセージを出すようにしてみます。

以下のコマンド、Page Mapを表示させます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e51aa78f-bdb0-6645-7801-8cd7f745dfc0.png)

Object Pageの"Show Controller Extensions"ボタンをクリックし、サイドパネルを表示させます。サイドパネルの"Add Controller Extension"ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/271e1aaa-8214-8063-c705-587220348d7d.png)

コントローラーの名称を入力し、"Add"をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/dd3948c4-c19d-2d14-8bb1-81cb7d2f21f6.png)

コントローラー拡張のファイルがTypeScriptで生成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/caf5df40-0ef2-d4ec-133b-51276dececb6.png)

以下コードを追加します。削除ボタンが押されたら、確認のダイアログを表示するというものです。標準のダイアログが出た後に追加のダイアログが出ます。ソースコードは[Flexible Programming Modelのサンプルソース](https://sapui5.hana.ondemand.com/test-resources/sap/fe/core/fpmExplorer/index.html#/controllerExtensions/basicExtensibility)を参考にしました。

```typescript
import ControllerExtension from 'sap/ui/core/mvc/ControllerExtension';
import ExtensionAPI from 'sap/fe/templates/ObjectPage/ExtensionAPI';
import Dialog from 'sap/m/Dialog';
import Button from 'sap/m/Button';
import Text from 'sap/m/Text';
import { ButtonType, DialogType } from 'sap/m/library';

/**
 * @namespace miyasuta.ordersui.ext.controller.ObjectPageExt
 * @controller
 */
export default class ObjectPageExt extends ControllerExtension<ExtensionAPI> {
	static overrides = {
		/**
		 * Called when a controller is instantiated and its View controls (if available) are already created.
		 * Can be used to modify the View before it is displayed, to bind event handlers and do other one-time initialization.
		 * @memberOf miyasuta.ordersui.ext.controller.ObjectPageExt
		 */
		onInit(this: ObjectPageExt) {
			// you can access the Fiori elements extensionAPI via this.base.getExtensionAPI
			const model = this.base.getExtensionAPI().getModel();
		},

		editFlow: {
			onBeforeDelete(mParameters: object) {
				return createDialog("Do you wan to delete this object?")
			}
		}
	}
}

async function createDialog(sText: string): Promise<void> {
	return new Promise<void>((resolve, reject)=> {
		const oApproveDialog = new Dialog({
			type: DialogType.Message,
			title: "Confirm",
			content: new Text({text: sText}),
			beginButton: new Button({
				type: ButtonType.Emphasized,
				text: "Continue",
				press: () => {
					oApproveDialog.close()
					resolve()
				}
			}),
			endButton: new Button({
				text: "Cancel",
				press: () => {
					oApproveDialog.close()
					reject()
				}
			}),
			escapeHandler: (pCloseDialog) => {
				pCloseDialog.resolve()
				reject()
			}
		})
		oApproveDialog.open()
	})
}
```
TypeScriptのソースには、以下のような特徴があります。
- `sap.ui.define`の代わりにimport文により使用するクラスをインポートする
- メソッド定義で引数、および返り値の型を指定する

詳しくは以下のブログをご参照ください。

https://qiita.com/tami/items/fa703b212982a094b7f5#13-typescript%E3%81%AB%E5%AF%BE%E5%BF%9C%E3%81%97%E3%81%9Fui5%E3%81%AE%E7%89%B9%E5%BE%B4

#### 開発時の気づき
- ソースの中でコントロールやクラスなどを使用すると、自動的にimport文が追加される
- 必ずしも正しいものがインポートされているとは限らないので、確認が必要
- Enum系（sap.m.DialogTypeやsap.m.ButtonType）のインポートは以下のように記述する
`import { ButtonType, DialogType } from 'sap/m/library';`

#### 動作確認
"Delete"ボタンをクリックする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/2f8720f7-f329-1558-8398-2ea26ced0837.png)
標準ダイアログが出る。"Delete"をクリックする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/932ddd93-b53f-664c-8f62-cf185915e741.png)
追加のダイアログが出る。"Continue"をクリックすると削除される
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/ce439857-977f-64f5-edb4-d05be4c8ffce.png)

### 3. デプロイの設定
※以下にやや煩雑な設定がありますが、これはTypeScriptを使用しているためではなく、CAPとUI5を1つのMTAアプリケーションでデプロイしようとすることによるものです。UI5アプリを単体で開発する場合はテンプレートによって自動的に入ってくる設定なので、以下の手順は不要です。

#### 3.1. Managed Approuterの設定
UIアプリをBTPコックピットのHTML Applications、またはSAP Build Work Zoneから実行できるようにするため、Managed Approuterを利用する設定が必要です。

まず、webapp/manifest.jsonに以下の設定を追加します。serviceの名前は任意ですが、ここではアプリIDと同じにしました。

```json
    "sap.cloud": {
        "public": true,
        "service": "miyasuta.ordersui"
    }
```

続いてmta.yamlに以下のモジュールを追加します。(A)、(B)のサービス名はresourcesセクションに登録済みのものを利用します。

```yaml
- name: capts_destination-content
  type: com.sap.application.content
  build-parameters:
    no-source: true
  requires:
    - name: capts-auth # xsuaaサービス名 (A)
      parameters:
        service-key:
          name: capts-auth-key # (A) + "-key"
    - name: capts-repo-host # html5-apps-repoサービス名 (B)
      parameters:
        service-key:
          name: capts-repo-host-key # (B) + "-key"
    - name: capts-destination-service # destinationサービス名
      parameters:
        content-target: true
  parameters:
    content:
      instance:
        existing_destinations_policy: update
        destinations:
          - Name: capts-repo-host # (B)
            ServiceInstanceName: capts-html5-srv # (B') html5-apps-repoサービス名（Bと異なる場合あり）
            ServiceKeyName: capts-repo-host-key # (B) + "-key"
            sap.cloud.service: miyasuta.ordersui # manifest.jsonのservice名
          - Name: capts-auth # (A)
            Authentication: OAuth2UserTokenExchange
            ServiceInstanceName: capts-auth # (A)
            ServiceKeyName: capts-auth-key # (A) + "-key"
            sap.cloud.service: miyasuta.ordersui # manifest.jsonのservice名
```
#### 3.2. ODataサービス呼び出しの設定
UI5アプリが使用するxs-app.jsonに、ODataサービスを呼ぶときのルートを追加します。このために、ODataサービスをDestinationとして登録する必要があります。Fiori toolsにより、これらの設定を自動で行うことができます。

以下のコマンドを実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/54e518ff-f5d3-3381-f561-fe0e24b08b58.png)

"**Add Deploy Config**"のメニューを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/f5517428-69bd-a59b-620c-f3839e87ea10.png)

Destination nameに"**Local CAP Project API (Instance Based Destination)**"を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/b235a3ec-340e-e6e9-94eb-6b21fc5c3db8.png)

これにより、UI5アプリプロジェクトのxs-app.jsonとmta.yamlに以下の設定が追加されます。

```json:xs-app.json
  "routes": [
    {
      "source": "^/odata/(.*)$",
      "target": "/odata/$1",
      "destination": "capts-srv-api",
      "authenticationType": "xsuaa",
      "csrfProtection": false
    },
```

```yaml:mta.yaml
- name: capts-destination-service
  type: org.cloudfoundry.managed-service
  parameters:
    config:
      HTML5Runtime_enabled: true
      init_data:
        instance:
          destinations:
          - Authentication: NoAuthentication
            Name: ui5
            ProxyType: Internet
            Type: HTTP
            URL: https://ui5.sap.com
            # 追加されたDestination
          - Authentication: NoAuthentication
            HTML5.DynamicDestination: true
            HTML5.ForwardAuthToken: true
            Name: capts-srv-api
            ProxyType: Internet
            Type: HTTP
            URL: ~{srv-api/srv-url}
          existing_destinations_policy: update
```
### 4. デプロイ
以下のコマンドによりビルド、デプロイします。

```
mbt build --mtar archive
cf deploy mta_archives/archive.mtar
```

HTML5 Applicationsから利用可能になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/9b786606-690d-a987-8284-f23412982822.png)
TypeScriptで実装したロジック（JavaScriptに変換済み）が動いていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/f8fc1457-2147-8009-2d0b-4a6bbfb02984.png)

## おわりに
ここまでで見てきたように、UI5でのTypeScript開発はテンプレートによって必要な設定が自動で入ってくるので、開発者が特別な設定をする必要がありません。つまり、ほとんど労力をかけずにTypeScript開発を始めることができるのです。公式にTypeScriptがサポートされるようになった今が、TypeScriptの始め時だと私は思います。

**UI5でTypeScriptを使うメリット**
- 間違ったプロパティやメソッド名を指定するとエディタ上でエラーになるので、実行する前に気づくことができる　→　生産性の向上
- ES6に対応した書き方ができる　→　UI5独特の構文を使わなくてよく、一般のWeb開発者が早く適応できる

**UI5でTypeScriptを使うハードル**
- UI5の学習リソースがほぼすべてJavaScriptベースなので、TypeScriptに切り替えるにあたり「差分」の学習が必要
- [SDK](https://sapui5.hana.ondemand.com/)のドキュメントやサンプルがJavaScriptベースなので、自分のソースで使う場合TypeScriptへの変換が必要

あえて「デメリット」ではなく「ハードル」と書きました。TypeScriptを使うデメリットは特になく、使用にあたって多少のハードルがあるととらえています。これは使うことで乗り越えていくしかないので、私もUI5（およびCAP）のコードを書く際はTypeScriptを積極的に使っていこうと思います。

## 参考リンク
- [TypeScript in SAPUI5 | SAPUI5 SDK](https://sapui5.hana.ondemand.com/1.116.0/#/topic/a7ee9617bc794b6fad21e4df38e31128.html)
- [ui5-typescript-tutorial](https://github.com/SAP-samples/ui5-typescript-tutorial)