Vue3で作っているVisualforceページでLWCを使う
はじめに
Salesforce上でリッチな画面を開発することになった場合、ReactやVueなどの主要なフロントエンドフレームワークを使うか、Lightning コンポーネントフレームワーク(LWCやAura)を使うか、異なるアプローチがあり、悩むことは多いのではないでしょうか。
この記事ではその 両方 を使った画面の開発方法の一例として、Vue3とLWCを組み合わせて使う実装方法を紹介します。
使っている仕組み自体は、Vue3に限定されるものではないので、Reactなどそれ以外のフレームワークでも応用できるかと思います。
そもそもなぜ両方使うのか?
通常、近しい位置づけのフレームワークを複数組み合わせて使う開発方法は「混ぜるな危険」であり、なるべくなら避けるべきだと考えています。
現代においては、React, Vue, Svelte等のフロントエンドフレームワークを使うのが一般的かつ最適な場合が多いでしょう。TypeScriptによる型の恩恵を受け、ローカルでサクサク開発でき、パフォーマンスやセキュリティについてもフレームワークに沿うことで一定の担保が可能です。私たちが開発・運用している「Cariot(キャリオット)」というサービスでは、ReactとVueの両方を使い分けて開発しているのですが、Salesforceプラットフォーム上で動かすアプリケーションについては、Vueを採用するシーンが多いです。
一方で、LWCの方が向いているケースもあります。Lightning Experience / Lightning Design SystemのスタイルにLook & Feelを統一したい場合や、標準コンポーネントで画面の大半の要素が実現できる場合です。例えば日付選択用のカレンダーやレコードのようなピッカーは、同じものをスクラッチでSLDSを使って作ると大変(不毛)ですが、LWCを使えばタグ1つで実現ができます。Salesforce側がアップデートされた時に、その恩恵を受けられるのもメリットです。
Salesforceプラットフォーム上で画面を開発する場合においては、上記のようにそれぞれ強みがあります。
今回取り上げるのは、独自の要素が多いのでメインをVueで開発し、一部のピッカー系だけ見た目を揃えるのにLWCを使う、という画面でした。混ぜるというよりはアクセントとして使うような使い方、といったほうがよいかもしれません。
仕組み
組み合わせ方として、Vueアプリケーションの中でLWCを使う方法と、LWCにVueアプリケーションを読み込む方法の2つがありますが、今回は前者の方法です。
全体としては次のようなイメージになります。
Lightning OutとLMSについて簡単に紹介します。
Lightning Out
Lightning Outは、Visualforce(に限りませんが)上からLightningコンポーネントを使うための仕掛けです。
詳しい説明は上記に譲りますが、諸々のお膳立てをしてあげると、Visualforce上のJavaScriptで次のようなコードを書くことで、要素ID app
の位置に myComponent
が追加できます。
$Lightning.createComponent("c:myComponent", {}, "app");
Lightning Message Service
画面全体としての動きを実現するのに、VueとLWCの間のインタラクションは必須です。
そのためにLightning Message Service (LMS)を使います。
LMSは、Salesforceプラットフォーム上でPub-Sub型の通信を行うための機能です。
実装例
それぞれの部品の実装サンプルです。
Visualforceページ
<apex:page docType="html-5.0"
controller="SampleController"
applyHtmlTag="false"
applyBodyTag="false"
showHeader="false"
standardStyleSheets="false"
>
<apex:slds />
<!-- Lightning Out 読み込み -->
<apex:includeLightning />
<html lang="ja">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sample</title>
</head>
<body class="slds-scope">
<!-- LMSメッセージチャネルトークンを取得 -->
<script>
var channel = "{!$MessageChannel.sample__c}";
</script>
<!-- Vueアプリケーション(静的リソース) -->
<div id="app"></div>
<script
type="module"
crossorigin="true"
src="{!URLFOR($Resource.Sample, '/index.js')}"
></script>
</body>
</html>
</apex:page>
Vue, Lightning Out, LMS を使ったアプリケーションページです。
今回の構成であれば、他のページでもほとんど同じようなマークアップになるはずです。
Vueアプリケーションのメイン
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { SForce } from './@types/lms'
import App from './App.vue'
const appStart = async () => {
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.config.globalProperties.window = window
app.provide<string>('channel', app.config.globalProperties.window?.channel)
app.provide<any>('$Lightning', app.config.globalProperties.window?.$Lightning)
app.provide<SForce>('sforce', app.config.globalProperties.window?.sforce)
app.mount('#app')
}
await appStart()
特筆するのは globalProperties
の利用です。Vueではコンポーネントの外部からのグローバルな変数に関しては globalProperties
を使って取得ができます。
1つ目の channel
は Visualforce 側で取得したメッセージチャネルトークンです。 $Lightning
, sforce
はどこから来ているのかがわかりにくいですが、Lightning Out を使うと自動的に注入されるものです。
app.provide
は他のコンポーネントからこれらのデータを利用するためのVueの仕組みです。
VueコンポーネントからLWCが使えるように、ここで必要なデータを提供するようにしています。
また @type/lms
は Vue 側で LMS に関する型の恩恵を受けるために、以下のように宣言してます( any
を使って手を抜いていますが、 message
の方はメッセージチャネルのものと揃えておくのがよいでしょう)
export type SForce = {
one: {
publish: (messageChannelToken: string, message: any) => void
subscribe: (messageChannelToken: string, listener: any, subscriberOptions?: any) => void
}
}
Vueコンポーネント
LWCを組み込んだVueコンポーネントは以下のような構成になります。
今回はサンプルのため、LWCを挿入するための div
要素しかありませんが、ここにVueの作法に沿ってマークアップしていくことで、VueとLWCを組み合わせた画面の構成が可能です。
<template>
<div id="lwc-container"></div>
</template>
<script lang="ts" setup>
import { onMounted, inject } from 'vue'
import { SForce } from '../@types/lms'
const $Lightning = inject<any>('$Lightning')
const sforce = inject<SForce>('sforce')
const channel = inject<string>('channel')
onMounted(async () => {
if (!$Lightning) return
$Lightning.use('c:SampleOutApp', function () {
$Lightning.createComponent(
"c:sampleComponent",
{
lmsChannel: channel,
lmsPublish: sforce.one.publish,
lmsSubscribe: sforce.one.subscribe
},
'lwc-container',
function (cmp) {
console.log('success: ' + cmp)
}
)
})
sforce.one.subscribe(channel, (message) => {
// メッセージ受信時の処理
})
})
</script>
inject
を使って main.ts
で提供したデータを注入しています。
あとはそれらを使って、 $Lightning.createComponent
により、LWCを挿し込んでいます。
パフォーマンスの観点で何度もコンポーネントの作成が行われないように onMounted
ライフサイクルの中でコンポーネントの生成は済ませてしまいましょう。
なおこの時、 v-if
バインディングを使って要素を非表示にしている場合、コンポーネント作成先の要素が見つからないためエラーになります。 v-show
を使って要素自体は存在させておくなど、対応が必要になります。
LWC
import { LightningElement, api, track } from "lwc";
export default class SampleComponent extends LightningElement {
@api lmsChannel;
@api lmsPublish;
@api lmsSubscribe;
connectedCallback() {
if (this.lmsChannel && this.lmsSubscribe) {
this.lmsSubscribe(this.lmsChannel, (message) => {
// メッセージ受信時の処理
});
}
}
}
createComponent
時に渡したLMS通信用の各データを使って、メッセージ通信を行います。
なお注意点として、今回のサンプルはVisualforceとLWCで同じメッセージチャネルを使っているため、publishした側でもそのメッセージをsubscribeすることになります。
そのため、処理すべきメッセージ以外は読み捨てるような、識別が必要になるので注意してください。
おわりに
Vue3とLWCの両方を組み合わせて、1つのVFページを開発する方法の紹介でした。
技術的には、それぞれのフレームワークが提供している部品を組み合わせて実現できました。
今回のコードでは、LMSのメッセージ処理などの泥臭いロジックは割愛していますが、実際のプロダクトコードではその部分の比重が増えるでしょう。
また、LWCを使うということは、ローカルで全てが完結するVueの開発者体験を損なう形にもなるので、あくまで最低限の利用に留めておいたほうがよいかもしれません(オープンソースLWCを使えばその限りではないかもしれませんが…)
Discussion