ReactのカスタムRendererを使ってLightningネイティブコンポーネントを開発する


Lightningコンポーネント開発の現状

いままでLightningコンポーネント開発には、大きく分けて2つの方向性がありました

  1. Lightning Componentフレームワークに則って開発する
  2. React/Vue.js(あるいはjQuery)などオープンなJavaScriptフレームワークを利用して開発する

1.に関しては、Salesforceが推し進める本道ではあるのですが、世の中のフロントエンドアプリケーション開発の動きとは少々乖離しているため、今後投資したことが負債になるのではないか、という不安はエンジニアを中心に多く聞かれているところです。経営者としても高品質な人材を確保することが難しくなるのはリスクのうちの1つでしょう。

2.に関しては、Lightning Design Systemなどを用いることで、Lightningネイティブに近いLaFを実現することが可能になっていました。しかしながら、途中からのLockerServiceの導入もあり、互換性を保った状況で安定的に動作させることが難しくなってきています。もちろんLightning Containerは一つの解ですが、IFRAMEという制約を受け入れざるを得ないことになります。

ReactとReact Native

現在、フロントエンド開発では、Reactはかなりメジャーな選択肢の1つです。実際多くの企業(業務系も含め)での採用事例が出てきており、変化の激しいフロントエンド界隈でもそれなりに安心して選択できる選択肢と考えてもいいかと思います。

Reactの面白いところは、もともとSPAのためのライブラリであったのが、途中からモバイルネイティブアプリケーション開発のためにも利用できるようになった、というところです。

React Nativeは、Reactのコンポーネント記述方法(JSX)で構築された仮想的なビュー構造を、そのままモバイルOS(iOS/Android)ネイティブなUIコンポーネントの描画につなげています。これにより、JavaScriptでのスキルや各種資産を活かしたネイティブモバイルアプリが開発できるようになりました。

これが可能であったのは、仮想DOMというアイディアによるところが大きいですが(実際はReact Nativeの場合はDOMを仮想化しているわけではないですが)、その汎用性について考えるとき、このアイディアは他のプラットフォームにも応用可能なのではないか、という期待を抱かせます。

ReactでLightningネイティブコンポーネントを描画構築する

実際、最新版のReactでは Reconciler とよばれる機構が提供されており、この仕組を利用して描画部分の実装を任意に作成できるようになっています。Reactで開発する際に通常利用するReact DOMもこのReconcilerを用いて実装されたRendererです。他にReconcilerを用いたカスタムRendererが実装されている例としては、React Nativeの他に、CanvasやSVGなどのベクタ画像をレンダリングするReact ARTなどもあります。

このReconcilerを用いることで、Lightningコンポーネントを描画するカスタムのRendererが作成できるのではないか、というのが、今回の取り組みの出発点となります。

具体的には以下の図のようなイメージです。

上記の図のうち、1.および2.の部分をReconcilerが提供しているので、Lightningコンポーネントに対応するには「3.パッチ適用」の部分をカスタムRendererで実装してあげれば良い、ということになります。

Lightningコンポーネントを描画するカスタムRendererの作成

Reconcilerを使ったRenderer実装の際には、appendChild()や、removeChild(), commitUpdate()など、ビューのツリー構造の差分更新情報がRenderer側に伝えられてきます。カスタムRendererは、このメッセージを受け取ってプラットフォーム毎に差分を適用していくコードを記述することになります。

通常、Lightningコンポーネント開発では ComponentName.cmp ファイルを作成してマークアップで静的にビューの構造を定義することがほとんどかと思いますが、Reconcilerを使ったRendererの実装ではプログラムで動的にコンポーネント階層構造を操作できることが重要になります。

Lightningコンポーネントでも動的にコンポーネントの構造を変更することは可能です。そのうち重要なメソッドが $A.createComponent()(あるいは$A.createComponents())メソッドとなります。これにより任意のLightningコンポーネントを動的に作成し、階層内に追加することが可能となります。

Dynamically Creating Components - Lightning Components Developer Guide

$A.createComponent(
  "lightning:button",
  {
    "aura:id": "findableAuraId",
    "label": "Press Me",
    "onclick": cmp.getReference("c.handlePress")
  },
  function(newButton, status, errorMessage){
    //Add the new button to the body array
    if (status === "SUCCESS") {
      var body = cmp.get("v.body");
      body.push(newButton);
      cmp.set("v.body", body);
    }
  }
);

属性の変更、要素の削除などはcmp.set("v.propName", value)cmp.set("v.body", children)などとしていけば良いでしょう。

React Lightning Renderer を用いたLightningコンポーネント開発

このようなカスタムRendererの実装を行ったものをReact Lightning Rendererとして以下のレポジトリに上げています。
(現状まだ動作が不安定なのはご容赦ください)

詳細はREADMEおよびexampleフォルダを見てもらえればよいでしょうが、少しだけこちらにどのようなコードになるか記述します

  • メインとなるReactアプリケーションのJavaScript (こちらをwebpack等でbundleファイルにした後、静的リソースとしてデプロイします)
index.js
import React from 'react';
import { Button, render, handleEvent } from 'react-lightning-renderer';

class App extends React.Component {
  constructor() {
    super();
    this.state = { count: 0 };
  }
  render() {
    const { count } = this.state;
    return (
      <div>
        <p>
          Count: { count }
        </p>
        <Button
          iconName="utility:add"
          onclick={ () => this.setState({ count: this.state.count + 1 }) }
        />
        <Button
          iconName="utility:dash"
          onclick={ () => this.setState({ count: this.state.count - 1 }) }
        />
      </div>
    );
  }
}

function init(cmp) { // Controllerの初期化メソッドからLightningコンポーネントのインスタンスを受け取る
  render(<App />, cmp);
}

export { init, handleEvent }; // LightningコンポーネントのControllerから利用する
  • Lightningコンポーネントのマークアップ (.cmpファイル)
ReactLightningTestComponent.cmp
<aura:component implements="flexipage:availableForAllPageTypes">
  <!--
     静的リソース(ReactLightningTest)としてデプロイしたbundle JavaScriptファイルを<ltng:require>で読み込み
  -->
  <ltng:require
    scripts="{!$Resource.ReactLightningTest}"
    afterScriptsLoaded="{!c.doInit}"
  />
  {!v.body}
</aura:component>
  • Lightningコンポーネントコントローラ
ReactLightningTestComponentController.js
({
  doInit: function(cmp, event, helper) { // 静的リソースJavaScriptロード後に呼び出される初期化メソッド
    // webpack等で library="ReactLightningTest" として公開した場合
    window.ReactLightningTest.init(cmp);
  },
  handleEvent: function(cmp, event) { // 現状、メソッドの名前は必ず"handleEvent"である必要があります
    window.ReactLightningTest.handleEvent(cmp, event);
  }
})

この結果、以下のようなコンポーネントが動作します。実際に+/-ボタンを押すとカウンターの数値が上下します。

ReactのJavaScriptコードを見ると、ほとんどReactの世界で完結しているのが分かるかと思います。ボタンonclickのハンドラもreact側で処理しており、stateの変更に伴ってrender()メソッドが動作し、最終的に画面が更新されて表示されます。

つまり、React Lightning Rendererを使った開発では、Lightningコンポーネントのデータバインディング、およびイベントハンドリングの仕組みは全く利用しません。このことはLightningコンポーネントフレームワークを設計した人にとっては不満かもしれませんが、開発者がReactやFluxが前提とするー方向データフローによる開発の利点を深く理解しているのであれば、この選択は妥当なものになります。

ちなみに上記サンプルでは素のReactのみでやっていますが、Reduxやその他のReact周辺の資産をそのまま持って来ることはもちろん可能です。

Lightning Rendererを利用した開発手法の優位性

カスタムのRendererを導入することで、Reactの開発手法をLightningネイティブコンポーネント開発に持ち込めるだけでもかなり価値がありますが、この方式を利用する利点はこれだけではありません。以下に挙げる機能ははまだ実装前ではありますが、本方式における可能性として見ていただければと思います。

ホットリロードによるリアルタイム開発

React Nativeのホットリロード対応や、それをさらに簡単にしたExpoなどで実現したリアルタイムな開発は、モバイルネイティブアプリケーション開発者にとってかなり衝撃的に受け止められました。いままでローカルマシンでのビルド⇒実機への配布が必要だった実機上での動作確認を、まるでWeb開発のようにホットリロードして確かめることができるようになりました。

これと同じことをReact Lightning Rendererを利用すれば達成できる可能性があります。以下に目論見図を記します。

要は、Platform Eventを利用して、差分更新情報をローカルの開発環境から送信し、UIからのイベントについては逆方向のPlatform Eventで送信することができればいいのではないかということです。Platform Eventの性質上、多少の遅延はあると思いますが、すくなくともプロトタイプ開発には十分使い物になるのではないか、と考えています。

LockerService対策

LockerServiceは、仮想的にSandboxを実現する機構です。しかしながらブラウザ内のオブジェクトを強制的にwrapしたりアクセスできるオブジェクトやプロパティを制限したりなど、既存コードの利用に際して互換性に問題のあるケースが多く見受けられます。

これらの問題にアプリケーション開発者が逐一対応するのは大変なことです。特に依存しているライブラリがLockerServiceに対応していない場合、お手上げになります。今のバージョンで動いていてもいずれ動かなくなる可能性もあり、それをライブラリの作者は保証してくれないのです。

このため、できるだけLockerServiceに影響を受けない開発が望まれますが、Lightning Rendererを用いればこれについても解消可能です。

こちらも先に挙げたリアルタイム開発の場合と同様に、Reactアプリケーション/Reconcilerの動く環境とRendererの動く環境を分離させています。リアルタイム開発の際はローカルのNode.jsプロセス上でReact/Reconcilerは動作しましたが、こちらの場合ではLightning Containerの中で動作させることになります。これにより、アプリケーション開発者が作成したコードはすべてLightning Containerの中で動作することになるので、LockerServiceの影響を受けずに安定してコードを動かすことができます。

まとめ

Lightning Rendererはまだ開発途中であり、正直PoCに近い状況です。しかしながらLightningに携わる(あるいは携わらざるをえない)開発者にとっては少なからず福音として作用するのではないかと考えています。もちろん今すぐにプロダクションに採用すべきではないでしょうが、同じ問題を共有する人たちが実際に参加することで、より多くの知見が得られていくのではないかと期待しています。