Lightningとは
要はSalesforceの内部で使われているUI開発フレームワークで、今まで(Visualforce)と違って大部分がJSでできているため、モダンなアプリの開発ができるよ、などといわれています。
Lightningのイケてない所
いろいろあるのですが、一番まずいのはJavaScriptが培ってきたさまざまなプラクティスを無神経に壊しているところです。
だいたいそもそもController.jsは完全なJSではない。
({
init: function(cmp, evt, helper) {
// ...
},
handleClick: function(cmp, evt, helper) {
// ...
}
})
一件ハッシュ形式でのオブジェクト(JSONではない)を返す特殊なJSに見えますが、問題があります。このJSは保存時にSalesforceのJavaのパーサで強引にチェックしており、オブジェクトには関数以外の要素を入れることが出来ないようになっています。
org.auraframework.util.json.JsonStreamReader$JsonStreamParseException: Only functions are allowed in javascript controllers
(JSONでないものをJsonStreamReaderという名前のクラスで処理しているあたりについてはおいておくとして)
さらに、構文解析の段階でルートがオブジェクトであることを大前提にしており、それ以外の要素をいれこむ余地がありません。
つまり、たとえばクロージャでプライベートな名前空間に分けるテクニックも使えません。
すると、それぞれの関数に処理を詰め込むことになってしまい、コントローラが肥大化してしまいます。
あるいはグローバルの名前空間を汚染することが前提になります。
コントローラ内のメソッド間での処理の共有もだめなようです。
コントローラ定義は一見オブジェクトなのですが、実際は名前付きのメソッドの羅列として考えたほうがいいです。事実、コントローラのメソッドが呼び出される時のthisは全く違うオブジェクトになってます。
ヘルパーもおなじ問題を抱えているので、そちらに逃してやることもできません。
ヘルパー内部ならプロパティへのアクセスも可能であるようです。参考:helper内でのプロパティー定義
あと、これは流石になんとかしないとまずいのでは...
Lightningのjsがエラーになってしまう理由がわからない時
プリプロセッサを動かすのはJSX(React)もそうだし別に構わない(個人的にはあまり好きではない)のですが、まずそれをJSとはいわないですし、あとそういうのプラットフォームとしてやるのだったらきちんと構文解析できている前提でしょう。もしかして正規表現でやってるのと違うのこれ、と疑ってしまうほどお粗末です(ただ一応まだここは今後の改善を期待できるところ)。
外部スクリプト読み込みにはRequireJSを利用しない
Salesforceの見解では外部ロードのスクリプトが必要な場合はRequireJSを使え、という話らしいのですが、最近はRequireJSはそのAMDという制約から次第に忌避されてきており、ビルド時に依存解決してブラウザでの実行用に1つのJSファイルに組み立てるのが主流になってきています。代表としてよくとりあげられるのがbrowserifyというツールです。
参考:Browserify: それはrequire()を使うための魔法の杖
browserifyではNode.jsでも活用されているCommonJSという形式でrequire('modulename')
と書くことで、外部のモジュールファイルを読み込むことができます(下記の例は特にjQueryは使ってないんですが、とりあえず例として読み込んでます)。
var $ = require('jquery');
var _ = require('lodash');
module.exports = {
sumUp: function(cmp, evt, helper) {
var nums = cmp.get('v.nums');
var sum = _.reduce(nums, function(sum, n) {
return sum + Number(n);
}, 0);
cmp.set("v.sum", sum);
}
};
その後、browserifyコマンド(あるいはgrunt-browserifyやgulpなどのビルドツール)を利用してすべての外部リソースを組み込んだJSファイルを静的リソースとして作成します。
$ browserify src/scripts/my-component-controller.js -s myComponentController > pkg/staticresources/MyComponentJS.resource
--standalone
(あるいは-s
)オプションを付けてビルドされたJSファイルはオプションで指定された名前でグローバルに公開されます(上記の例ではmyComponentController
)。内部で読み込んでいるjQueryやlodashはクロージャ内に隔離されるので他に影響をあたえることはありません。もしJSファイルの外部からアクセスする必要がなければstandaloneオプションは必ずしも必要ありませんが。
あとはbrowserifyで作成されたファイルを静的リソースから読み込んでやります。ここで残念ながらグローバル汚染が生じますが、仕方ありません。スクリプトロードには動的scriptタグインジェクションという前世紀のテクニックを使います。
参考:静的リソースの読み込み方
あとは、読み込んだモジュールを呼び出すようにLightningのコントローラを作りましょう。
({
init: function(cmp, evt, helper) {
var s = document.createElement('script');
s.src = '/resource/MyComponentJS';
document.body.append(s);
},
sumUp: function(cmp, evt, helper) {
myComponentController.sumUp.apply(myComponentController, arguments);
}
})
ご覧のように、このController.jsはただのローダー兼ブリッジなので、ビルド時に動的生成することも可能でしょう。時間があれば生成スクリプトを作りたいところです。厳密にはスクリプトロード完了までUIをブロックするような仕組みを盛り込んでおくとよいかもしれません。
ちなみにコンポーネントはこのようにしています。
<aura:component>
<aura:handler name="init" value="{!this}" action="{!c.init}" />
<aura:attribute name="nums" type="List" />
<aura:attribute name="sum" type="Integer" />
<ul>
<aura:iteration items="{!v.nums}" var="n" indexVar="index">
<li>{!n}</li>
</aura:iteration>
</ul>
<ui:button press="{!c.sumUp}" label="sumUp" />
{!v.sum}
</aura:component>
これでギクシャクしながらもなんとかフレームワークと分離できたので、あとはCoffeeScriptなりTypeScriptなり、うまくやればいいと思います。
とりあえずCoffeeScriptでコントローラを書いてビルドするスクリプトのテンプレは以下に作りました。
今後どうすべきか
Lightningで開発する一番の理由ってのは、開発したコンポーネントを今後Lightning AppBuilderに組み込めるってことなのだと思います。
ですので、とりあえずコンポーネント開発は Lightningコンポーネントを通してiframeをSalesforce1に表示する みたいにIFRAME(ただしabout:blankの)にして、Lightningのコントローラの中身は、AngularJSやその他のモダンJSフレームワークをIFRAME中に呼び出すHTMLを生成するのが一番いいんじゃないでしょうか。Lightningの肝であるところのイベントのやりとりもpostMessageでいけますし、矩形UIが前提であればコンポーネントとしてのリソースの隔離もそちらのほうが優れています。同一ドメインであればCSPも問題ないはずです。
何も人生を浪費する必要はありません。できるだけ工夫していきましょう。