Edited at

ApexからJavaScriptへの値の受け渡しパターン

More than 1 year has passed since last update.


はじめに

Apex/Visualforceの基本ですが、Apexクラス内のプロパティやgetterメソッドの戻り値は、Visualforce中で {!name} の形式で出力することができます。

この形式のことを merge field(差し込みフィールド)と呼ぶみたいですね(要出典)


Apex

public class Sample {

// property
public String message {
get {
return 'Hello!';
}
set;
}

// getter
public String getMessage2() {
return 'Good bye!';
}
}



Visualforce

<apex:page controller="Sample">

{!message} {!message2}
</apex:page>

プレビューをすると、「Hello! Good bye!」が出力されます。


JavaScript上でmerge fieldを利用する

当たり前といえば当たり前ですが、このmerge fieldは、Visualforce中のJavaScriptでも使えます。


Visualforce

<apex:page controller="Sample">

<script>
var msg = '{!message} {!message2}';
console.log(msg);
</script>
</apex:page>

一見お手軽なのですが、このままだと、 messagemessage2 の値如何では、XSSやJavaScriptのシンタックスエラー(戻り値に' が含まれる場合など)が簡単に起きてしまいます。

(エラーがなくとも、一つのVisualforce内にインラインスクリプトが長々と書かれていたり、merge fieldが散在しているのは、アンチパターンだと思いますが)


解決策:JSENCODEを付ける

安全でない文字はきちんと符号化しましょう。


Visualforce

<apex:page controller="Sample">

<script>
var msg = '{!JSENCODE(message)} {!JSENCODE(message2)}';
console.log(msg);
</script>
</apex:page>

https://developer.salesforce.com/docs/atlas.en-us.pages.meta/pages/pages_variables_functions.htm


文字列以外(ちょっと複雑なオブジェクト)を返したい

単純なString以外の値をJavaScriptで処理させたい場合があります。

例としてリストの場合を考えてみます。


Apex

public class Sample2 {

public List<Id> getContactIds() {
List<Contact> contacts = [SELECT Id From Contact LIMIT 10];

List<Id> ids = new List<Id>();
for (Contact c : contacts) {
ids.add(c.Id);
}
return ids;
}
}


適当に取引先責任者のIDのリストを返すケースですね。

JSENCODEは文字列に対してしか適用できないので、いったん符号化はあきらめて、JavaScriptにそのまま解釈させるとします。


Visualforce

<apex:page controller="Sample">

<script>
var contactIds = {!contactIds};
console.log(contactIds);
</script>
</apex:page>

しかしこの場合、展開後のJavaScriptは以下のようになり、シンタックスエラーとなります。

  var contactIds= [0032800000jTvMlAAK, 0032800000jTvMmAAK, 0032800000jTvMnAAK, 0032800000jTvMoAAK, 0032800000jTvMpAAK, 0032800000jTvMqAAK, 0032800000jTvMrAAK, 0032800000jTvMsAAK, 0032800000jTvMtAAK, 0032800000jTvMuAAK];

ならばとmerge fieldの前後をクオートで囲んだ場合、個々のIDそれぞれを文字列として受け取るために、JavaScript側でパースが必要です。

  var contactIds= '[0032800000jTvMlAAK, 0032800000jTvMmAAK, 0032800000jTvMnAAK, 0032800000jTvMoAAK, 0032800000jTvMpAAK, 0032800000jTvMqAAK, 0032800000jTvMrAAK, 0032800000jTvMsAAK, 0032800000jTvMtAAK, 0032800000jTvMuAAK]';

先頭[と末尾]は除外して、 , でsplitして、前後の空白は除いて、みたいなことはまともにやってられないですよね。


解決策:Apex側でJSONで返す

Apex側でJSON文字列を返すのが、シンプルで応用も効く解決策だと思います。


Apex

public class Sample2 {

public String getContactIdsAsJson() {
List<Contact> contacts = [SELECT Id From Contact LIMIT 10];

List<Id> ids = new List<Id>();
for (Contact c : contacts) {
ids.add(c.Id);
}

Map<String, Object> obj = new Map<String, Object>();
obj.put('contactIds', ids);
return JSON.serialize(obj);
}
}


JSON文字列を作成するために、今回はMapをそのまま使いました。

複雑なオブジェクトを返したいケースや、Mapのキーとして文字列を渡しているところをタイプセーフに扱いたい場合などは、Value Object用のクラスを作るのもアリだと思います(この方法は後述の応用編で使います)。

JSONで返すことにより、JSON.parse でオブジェクトに戻すことができます。


Visualforce

<apex:page controller="Sample">

<script>
var obj = JSON.parse('{!JSENCODE(contactIdsAsJson)}');
console.log(obj);
</script>
</apex:page>

Apex側で文字列で返すようになったことで、JSENCODEも適用できるようになりました。


応用編:Visualforceコンポーネントのカスタムコンポーネント属性をコンポーネント側のJavaScriptに渡す

Visualforceコンポーネントの <apex:attribute> でセットされた各属性を、コンポーネント内のJavaScriptで使いたいことがあります。


sample.component(修正前)

<apex:component>

<apex:attribute name="val1" description="hoge" type="String" required="true"/>
<apex:attribute name="val2" description="fuga" type="String" required="true"/>

<script>
var attrs = ...
console.log('init component...', attrs.val1, attrs.val2); // val1, val2 を使いたい!
</script>
</apex:component>


この場合、 apex:attributeassignTo 属性を使い、コントローラ側で属性値を受け取ったうえで、まとめてJSONで返してあげればよいです。

public class SampleComponentController {

// assignTo 属性で受け取るパラメータ
public String attrVal1 { get; set; }
public String attrVal2 { get; set; }

public String getAttributesAsJson() {
Attributes attrs = new Attributes();
attrs.val1 = attrVal1;
attrs.val2 = attrVal2;
return JSON.serialize(attrs);
}

// JSON.serialize 用の入れ物
public Attributes {
String val1 { get; set; }
String val2 { get; set; }
}
}

完成版のコンポーネントは以下のようになります。


sample.component(修正後)

<apex:component controller="SampleComponentController">

<apex:attribute name="val1" description="hoge" assignTo="{!attrVal1}" type="String" required="true"/>
<apex:attribute name="val2" description="fuga" assignTo="{!attrVal2}" type="String" required="true"/>

<script>
var attrs = JSON.parse('{!JSENCODE(attributesAsJson)}');
console.log('init component...', attrs.val1, attrs.val2);
</script>
</apex:component>


こんな感じで作っておくと、多くの部分がコンパイラエラーとして検知可能になるかと思います。

まぁ、これからのSalesforce開発でどれくらいVisualforceコンポーネントが使われるかはわかりませんが…。


おわりに

まとめると、Apexコントローラのproperty/getterの戻り値をJavaScript中のmerge fieldにセットして処理させたい場合、以下のようにしておくのがベスト、というのが現在の結論です。


  • Apexのプロパティ、ゲッターの型はJSON文字列で統一する(JSON文字列は JSON.serialize で組み立てる)

  • 受け取り側(JavaScript)では JSON.parse('{!JSENCODE(value)}') の形で受ける。

Salesforceのドキュメント「ユーザインターフェース開発の考慮事項」の中では、JavaScript アプリケーションコンテナとしてVisualforceを利用するアプローチは、いくつかのマイナス面を許容できるなら、「現在これがインタラクティブアプリケーションを作成する最適な方法」として書かれています。

ApexとVisualforce上のJavaScriptを適切に相互作用させるベストプラクティスについては、今後も模索していきたいところです。

なお、JavaScript Remotingを使った場合の @RemoteAction メソッドの場合、戻り値の型はJSON文字列である必要はないです。