dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

Salesforce:JavaScript Remoting(@RemoteAction)をPromiseで扱う

はじめに

Visualforce で Angular や React などの JavaScript ライブラリを使ったアプリケーションを作ろうと思った場合、
Apex で定義したメソッドの実行には JavaScript Remoting を使うのが一般的かと思います。

似たようなことを実現するための手段として リモートオブジェクト(Remote Object) というのもありますが、オブジェクトの単純な CRUD 処理以上のことをやろうとすると JavaScript Remoting が必要になります。両者の比較は こちら

JavaScript Remoting は以下のような書き方で、JavaScript から対象の Apex クラスおよびメソッドを指定します。

// 方法1
[namespace.]controller.method(
    [parameters...,]
    callbackFunction,
    [configuration]
);

// 方法2
Visualforce.remoting.Manager.invokeAction(
  '{!$RemoteAction.controller.method}',
  [parameters...,]
  callbackFunction
);

ここで問題点としては、どちらの書き方にも言えることですが、 Apex 側からのレスポンスがきた後の処理をコールバック関数として渡さなければならない点です。

コールバック地獄としてたびたび問題になる話ですが、最近では Promise や async/await 構文といった書き方があるので
今回は Promise でこの部分を書けないか試してみます。


コード

できあがったものを先に置いておきます。
なお、アプリケーション自体は React/Redux で書かれています。


Promise を使わない(= コールバックを使った)書き方

はじめに、Redux で JavaScript Remoting をそのまま使った場合の書き方。

// actions.js
import Remoting from '../service';

export function sayHelloCallback() {
  return (dispatch, getState) => {
    dispatch(loadingStart());
    new Remoting().sayHelloCallback((result) => {
      // 実際にはここでレスポンスの正否を確認する
      console.log(result);
      dispatch(loadingEnd());
    });
  };
}
// service.js
export default class SfRemoting {
  getRemoting() {
    // eslint-disable-next-line
    return __NAMESPACE__RemoteActionController;
  }

  sayHelloCallback(callback) {
    this.getRemoting().sayHello((result, event) => {
      console.log('result:', result);
      console.log('event:', event);
      callback(result);
    });
  }
}

Action と JavaScript Remoting 呼び出しを分離するために service.js というファイルを作っています。
Apex 側には sayHello() という引数を取らない RemoteAction メソッドが定義されているという前提です。

// RemoteActionController.cls
public class RemoteActionController {
    @RemoteAction
    public static String sayHello() {
        return 'Hello, World';
    }
}

また、方法1 の書き方で JavaScript Remoting を実行しようとすると namespace の問題をなんとかしないといけないので
webpack の string-replace-loader で(やや強引に)解決しています。

// webpack.config.js
module.exports = {
  module: {
    loaders: [
      ...
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'string-replace-loader',
        query:{
          search: '__NAMESPACE__',
          replace: process.env.NAMESPACE ? `${process.env.NAMESPACE}.` : '',
        }
      },

ビルド実行時に NAMESPACE=foo というように環境変数を渡したときだけ namespace を付与します。

Action の

new Remoting().sayHelloCallback((result) => {

のところでコールバックによるネストが発生しており、複数の JavaScript Remoting の呼び出しがあるとネストが深くなります。
また並列に実行するような書き方もできません。(私の知る限りですが)


Promise を使った書き方

service.js 内のメソッドを Promise を使った書き方に直してみます。自信はないですがこんな感じになるはず...!

// service.js
export default class SfRemoting {
  execute(methodName, ...args) {
    return new Promise((resolve, reject) => {
      this.getRemoting()[methodName](...args, (result, event) => {
        if (event.status) {
          resolve(result);
        } else {
          console.log('Remote Action error occured:', event);
          reject({ message: event.message, where: event.where });
        }
      });
    });
  }

  sayHelloPromise() {
    return this.execute('sayHello');
  }
}

RemoteAction メソッドのコールを execute メソッドに集約し、その中で Promise を使っています。
また、コールバック関数内でレスポンスの内容を元に resolve() または reject() を実行しています。

このような書き方にすると、Action 側は以下のように書けます。

export function sayHello() {
  return (dispatch, getState) => {
    dispatch(loadingStart());
    new Remoting().sayHelloPromise()
      .then((result) => {
        console.log(result);
        dispatch(loadingEnd());
      })
      .catch((err) => {
        console.error(err.message, err.where);
        dispatch(loadingEnd());
        dispatch(raiseError(err.message, err.where));
      });
  };
}

ネストすることなく後続の処理を書くことができました。
また sayHelloPromise の後に RemoteAction メソッドの呼び出しが続く場合も

.then((result) => {
  return new Remoting().anotherRemoteActionMethod();
})
.then((result) => {
  ...
})

というように then 内で別の RemoteAction メソッドの呼び出しを行うことで、ネストを深くせずにメソッドをチェインすることができます。


補足:エラーハンドリングについて

JavaScript Remoting のレスポンスは第一引数が Apex メソッドの戻り値、第二引数が以下の項目を含む event オブジェクトです。
(参考:リモート応答の処理 | Visualforce 開発者ガイド | Salesforce Developers

項目 説明
event.status 成功のときは true、エラーのときは false になります。
event.type 応答の種別: 成功したコールは rpc、リモートメソッドが例外を返した場合は exception のようになります。
event.message 返されたエラーメッセージが含まれます。
event.where リモートメソッドにより生成された場合は、Apex スタック追跡が含まれます。

そのため、基本的には event.status の値を見て成功/失敗を判断すれば良いことになります。
また event.message には Apex 側で throw した Exception にセットしたメッセージです。

// RemoteActionController.cls
public class RemoteActionController {

    public class MyException extends Exception {}

    @RemoteAction
    public static void sayHelloError() {
        throw new MyException('Something bad happened!');
    }
}

(画面)

f:id:dackdive:20170505211953p:plain:w320

詳細はサンプルコードをご参照下さい。


おまけ

引数の型や数が間違ったときなどに

Visualforce Remoting: Parameter length does not match remote action parameters: expected 1 parameters, got

のようなエラーがブラウザのコンソールに出力されますが、これは補足できないぽい...?

参考:名前空間および JavaScript Remoting | Visualforce 開発者ガイド | Salesforce Developers

invokeAction のコール時に発生したエラーは JavaScript コンソールでのみレポートされます。たとえば、$RemoteAction で複数の名前空間に一致する @RemoteAction メソッドが見つかった場合、最初に一致したメソッドを返し、JavaScript コンソールに警告を記録します。 一致するコントローラまたはアクションが見つからない場合は、そのコールはエラーを表示することなく失敗し、エラーは JavaScript コンソールに記録されます。


リファレンス

Visualforce 開発者ガイドより、