はじめに
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!'); } }
(画面)
詳細はサンプルコードをご参照下さい。
おまけ
引数の型や数が間違ったときなどに
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 開発者ガイドより、