Lightning ComponentにおけるApexCallsの汎用化


はじめに

Lightning大好きっ子のみんなあつまれー!Salesforce Platform Advent Calendar5日目の時間だよ。
ということで、今日はLightningについて書かせて頂きます。

1年前にも似たようなことを書いたのですが、単に「Lightning」と言った時にComponentだったりExperienceだったりPlatformだったりライセンスだったりブランディングだったりよく分からなくなりますよね。もうせめてUIに関するところだけに留めておいてほしいと思っていますが、なかなかサンフランシスコに思いが届かない今日この頃です。

さて、そんな中でも今日はComponentを開発する時のお話をさせていただきます。

やりたいこと

このブログをご覧になっている大好きっ子皆さんは、当然ながらLightningコンポーネント開発者ガイドを読んだことがありますよね?
Lightning Componentはクライアントで動作するフレームワークなので、サーバ側に保存されたデータを取得したり、更新したり、何かしらサーバ側で処理が必要な場合はApexクラスのメソッドを作成して、ComponentのHelperなどから呼び出すというのはご承知のことと思います。
ちなみに以下は開発ガイドに載っているサンプルコードです。

これをベースにサーバ側の処理を呼ぶと思うのですが、1つの組織やプロジェクト内で複数の開発者がいる場合、もしかしたらそれぞれの開発者やComponentで実装が異なっているという可能性はないでしょうか。
例えばエラー処理1つとっても、Aさんはモーダルを出して、Bさんはthrowして、Cさんはconsole.logにこっそり出力する。。。
実装方法が異なると、一貫性のない動作になってしまいます。

その辺のGapを埋めるためには、ドキュメントなどでルールを決めるのもいいですが、いっそのことサーバーサイドを呼び出す汎用的な処理を作成し、必要なときはそれを使うようにするといいでしょう。また、そうすることによりコードの重複を防ぐこともできます。

ということで、今回は次のところにフォーカスをあてて、汎用的な処理を作ってみましょう。

  • Apexメソッド名指定
  • 引数を渡す
  • Apexを呼ぶ
  • 成功時の戻り値を渡す

まぁこの程度であればオレオレ実装するような人も出てこないかもしれませんが、あくまで最初のとっかかりとしてお考えください。

では、クライアント側の処理を説明する前に、まずサーバ側のApexのコードです。

Apex
public class GeneralizationSampleCtrl {

    @AuraEnabled
    public static List<Account> getAccounts(String filter) {
        List<Account> results;
        if(String.isEmpty(filter)) {
            results = [SELECT Id, Name, Phone FROM Account LIMIT 10];
        } else {
            String str = '%' + filter + '%';
            results = [SELECT Id, Name, Phone FROM Account WHERE Name LIKE :str LIMIT 10];
        }
        return results;
    }
}

これは引数に文字列を受け取り、SOQLを実行してAccountを返す処理です。
単純な処理ですね。

ver1 Helper内で汎用化

Helper内に汎用的に使えるcallApexというサーバーサイドを呼び出す処理を作り、それを他のHelper内のFunctionから呼び出すようにしてみます。
こんな感じです。

Helperを見てましょう。

v1HelperComponentHeler.js
({
    getAccountList: function(component, filterString) {
        this.callApex(component, "c.getAccounts", {"filter": filterString}, this.getAccountListSuccess);
    },

    callApex: function(component, controllerMethod, actionParameters, successCallback) {
        // サーバ側のコントローラを呼ぶactionを生成
        var action = component.get(controllerMethod);
        action.setParams(actionParameters);

        // コールバックアクション
        action.setCallback(this, function(response) {
            var state = response.getState();
            if (state === "SUCCESS") {
                successCallback(component, response.getReturnValue())
            }
            //中略
        });

        // アクションをキューに追加
        $A.enqueueAction(action);
    },

    getAccountListSuccess: function(component, returnValue) {
        component.set("v.accountList", returnValue);
    },

})

getAccountListから「Apexメソッド名」や「引数の情報」「成功時のcallback先」をcallApexに渡しています。
こうすることで、他のfunctionからサーバーサイドを呼び出す必要が出た場合でもcallApexを使うことができます。

ただし、ver1はひとつのLightningComponent、つまり同じHelper内でしか利用することはできません。また、Apexのコントローラークラスもひとつしか使えません。
これではComponentが肥大化してしまいますね。
ということで、もう少し改修していきましょう。

ver2 親LightningComponent

複数のComponentから汎用化されたcallApexを介してサーバーサイドを呼べるようにします。
そのために、親となるLightningComponentを作成し、それを継承する形にしてみましょう。

上記のように親LightningComponentのHelperにcallApexを移し、extendsした子でそれを使います。
コードとしてはこんな感じになります。

v2AbstractComponent.cmp
<aura:component extensible="true" abstract="true">
    {!v.body}
</aura:component>
v2AbstractComponentHeler.js
({
    callApex: function(component, controllerMethod, actionParameters, successCallback) {
    // v1と同じなので省略
})

子Component側は次のように継承します。

v2ConcreteComponent.cmp
<aura:component extends="c:v2AbstractComponent" controller="GeneralizationSampleCtrl">
    <aura:attribute name="accountList" type="Account[]" />
    <!-- 中略 -->
</aura:component>

こうすることで、子Component側のHelperからcallApexを利用することができます。
ここまでで依存関係のない、再利用可能なサーバーサイドを呼ぶ処理を作ることができました。

ただし!まだ問題はあります。
Lightning Componentでは継承できる親は1つだけなので、ver2だと継承を別のケースで使いた場合に困ってしまいます。

ということでもうちょっと改修してみましょう。

ver3 Service Component化

ver2は継承関係にありましたが、これを解消して使えるようにするためにcallApexを持つComponentをService化していきましょう。

上記の図のように、Service Componentを作成し、これを継承関係なく呼べるようにaura:methodによるインタフェースを作ってあげます。

v3ServiceComponent.cmp
<aura:component >
    <aura:method name="callApex" action="{!c.onCallApex}">
        <aura:attribute name="component" type="Aura.Component" />
        <aura:attribute name="controllerMethod" type="String" />
        <aura:attribute name="actionParameters" type="Object" />
        <aura:attribute name="successCallback" type="Object" />
    </aura:method>
</aura:component>

aura:methodのactionとしてcontroller.jsにある次のfuctionを呼び出すようにします。
この時、呼び出し元となるComponentも渡してあげましょう。

v3ServiceComponentController.js
({
    onCallApex : function(component, event, helper) {
        var params = event.getParams().arguments; // メソッドパラメータ取得
        var callerComponent = params.component;
        var controllerMethod = params.controllerMethod;
        var actionParameters = params.actionParameters;
        var successCallback = params.successCallback;
        helper.callApex(callerComponent, controllerMethod, actionParameters, successCallback);
    }
})

helper.jsに置くcallApexはこれまでと同じです。
Service化されたcallApexを呼び出す場合は次のように書きます。

v3OtherComponent.cmp
<aura:component controller="GeneralizationSampleCtrl">
    <aura:attribute name="accountList" type="Account[]" />
    <c:v3ServiceComponent aura:id="service" />

    <!-- 中略 -->
</aura:component>

指定したaura:idを検索し、callApexを呼び出すことができます。

v3OtherComponentHelper.js
({
    getAccountList: function(component, filterValue) {
        // ServiceComponentからAuraメソッドを呼び出す
        component.find("service").callApex(component, "c.getAccounts", {"filter": filterValue}, this.getAccountListSuccess);
    },

はい、ということで、サーバーサイドのApexを呼び出す、汎用的なcallApexができました。
今日はここまでですが、さらなる使い勝手をあげるのであれば、次のような機能を追加することもできるでしょう。

  • エラー処理
  • スピナー表示
  • GUIロック

おわりに

さて、実はこの内容、Dreamforceのブレイクアウトセッションで聞いてきた内容をパク・・・ベースにしています。
英語が苦手な私ですが、スライドにいっぱいコードが出てきたのでわりと理解できました。サンキュー!
最近までタイトルを覚えていなかったのですが、ブログのネタにするためにググっていたらgithubにコードが上がっているのを見つけた。大元を見たいかたはそちらをご覧ください。

今回の記事はSalesforce Platform Advent Calendar 5日目の記事となりますので、引き続き6日目以降もお楽しみください。
では。