これはなに?
新しいAppExchangeアプリを作る予定なので、そのためにSalesforce DX + Vue.js + TypeScriptを使ったSalesforceのISVパッケージ開発のためのサンプル兼開発テンプレートを作りました。
Webpackで一から設定するのは面倒なので、Vue CLI 3で生成したアプリをベースにして修正を加えています。
コードはatskimura/sfdx-vue-typescript-isv-templateで見ることができます。
npm run setup
を実行するとスクラッチ組織でこれが表示されます。
シンプルですが、Lightning Design System適用して、Apex呼んでカスタムオブジェクトを表示して、アイコン/画像を表示してと一通りの要素は揃っているかと思います。
構成は?
構成としては一枚のVisualforceページでVue.jsアプリを動かすといういわゆるSPAです。ApexはJavaScript Remotingで呼んでます。
Lightning Component Frameworkを使わず、Lightning Containerでもなくこの構成にしています。Lightning Containerは悩んだのですが、この構成にしました。後々Lightningコンポーネントを作ることになったら、Lightning Containerを使ってVueのコンポーネントをラップして提供することになると思います。
VueとTypeScriptを選んだ理由はなんとなくです。
Salesforce DXはどう組み込んでいるの?
Salesforce DXの使い方としては、前にSFDX-Falconを使ったISVパッケージの開発の記事で紹介したこの図の通りの流れです。
(AppExchange 開発者向けウェビナー 〜アプリ構築編〜 - Speaker Deck P.29)
ただ、今回はパッケージ作成までしか作っていません。CI周りもまだです。
参考にしたもの
同じようなことをしようとしている人はそれなりにいるようで、参考になるものはいくつかありました。
ただ、ISVパッケージ開発を目的としているのはSFDX-Falconくらいで、パッケージング周りはSFDX-Falconが参考になりました。
- SFDX-Falconを使ったISVパッケージの開発
- Salesforce DX with LDS + TypeScript + Vue.js - My note
- mattsimonis/vue-sfdx
- ChuckJonas/bad-ass-salesforce-stack: B.A.S.S. Starter: react / redux / typescript / antd / ts-force / sfdx / webpack / salesforce
- tq-jappy/sfdx-template: Modern Salesforce Development Template and Samples
Lightning Containerに書き換えたい場合は3番目のが参考になると思います。
使い方は?
使い方はsfdx-vue-typescript-isv-template/README.mdにけっこう書いたので、詳しくはそちらを読んでください。
簡単にだけ書いておくと、
npm run setup
で、スクラッチ組織が作成されてアプリが動きます。
npm run package
で、パッケージ開発組織にベータパッケージが作成されます。
開発方法
npm run serve
で、ローカルのWebサーバーで動かして開発します。
このときのApexへのアクセスはモックにしてしまうと割り切りました。どこかで開発に行き詰まる可能性はあります。
弊社製品のCalsketの開発環境ではローカルだとREST APIを呼ぶようにしてるんですけどね。
Salesforce側の開発はサーバーで修正してnpm run pull
したり、ローカルで修正してnpm run push
したりしてください。
注意点
自分用なので、Windowsのことは考えていません。
npm scriptsの環境変数のところとrm
を呼んでいるところあたりを直せば、たぶんWindowsでも動くんじゃないかと思います。
ディレクトリ構成
ディレクトリ構成は雑です
クラインアントサイド
-
src
: クライアントサイドのコード -
tests
: クライアントテストコード -
public
: index.html, faviconなどルートに置かれるもの。
ビルド結果がsfdx-src/managed/staticresources/app
に出力され、Salesforceに静的リソースとしてアップされる。
Salesforce関係
-
sfdx-src
: SFDX形式のSalesforceのソースコード-
managed
: 管理パッケージに含むリソースを入れる。 -
unmanaged
: 管理パッケージに含まないが、開発環境として共有したいリソースを入れる。 -
untracked
: gitで管理しないリソースを入れる。pullするとデフォルトでここに入る。
-
-
config
: スクラッチ組織の設定。サンプルデータ。 -
mdapi-src
: Metadata API形式のソースコード。パッケージ作成のために一時的に作成される。
作り方は?
Vue CLI 3で生成されたアプリをどう修正してこのテンプレートを作ったかを書いておこうと思います。
このまま使う人はあまりいないと思うので、その方が参考になるかなと思いました。
Vue CLIで生成したアプリからの変更点はcompareで見れます。
Comparing a27a5bd...master · atskimura/sfdx-vue-typescript-isv-template
Vue CLIで生成したアプリをそのままVisualforceで動かそうとしたときに問題になるのは以下のような点です。
- SFDXの設定はどうするの?
- VueアプリをどうSalesforceにアップするか
- アセットのURLが相対パスでは辿れずVfpページを実行しないと決まらない
- Lightning Design System(LDS)をどう組み込むか
- LDSアイコンは基本SVG Spriteで表示するが、これも動的にURLが決まる
- JavaScript Remotingをどう呼ぶか
- ネームスペースの対応は?
- パッケージはどう作るの?
それぞれどう対応したかをまとめておきます。
SFDXの設定はどうするの?
これはsfdx-project.json
とconfig/project-scratch-def.json
を用意するだけでOKです。
ディレクトリ構成は上に書いたようにSFDX-Falconを参考にしていますが、untracked
をデフォルトにしています。こうするとpullしたときにuntracked
に落ちてくるので間違えてgitにpushしてしまうという事故が防げるかなと思ってます。
{
"packageDirectories": [
{ "path": "sfdx-src/managed" },
{ "path": "sfdx-src/unmanaged" },
{ "path": "sfdx-src/untracked", "default": true }
],
"namespace": "mynamespace",
"sfdcLoginUrl": "https://login.salesforce.com",
"sourceApiVersion": "44.0"
}
ただ、pullしたリソースのディレクトリを移動すると、pushするとエラーになる問題が起きました。
.sfdx/orgs
にリソースごとにパスの情報も持ってしまっているのが原因のようです。いまいち手順は確立していないのですが、.sfdx/org
を削除してforce:source:push -f
するとpushできました。
まあもう困ったらスクラッチ組織作り直したらいいと思います。
config/project-scratch-def.json
はたぶんどこに置いてもいいのですが、SFDXプロジェクトだとここに置かれることが多いのでなんとなくそうしました。ディレクトリ名を変えた方がわかりやすいと思います
VueアプリをどうSalesforceにアップするか
ビルド結果を直接、SFDXのパッケージディレクトリの静的リソースとして直接出力するようにします。
今回はsfdx-src/managed/staticresources/app
に出力するので、vue.config.js
に以下のように設定します。
outputDir: 'sfdx-src/managed/main/default/staticresources/app',
デフォルトだとビルド時にapp.8fy9a8.js
のようにJavaScriptやCSSに乱数をつけてしまうので、これを無効にします。
(将来キャッシュで問題になるかもしれないので、何か修正は必要な気がします。)
filenameHashing: false,
ビルドされたindex.htmlを参考にVisualforceページを作成します。
<apex:page
sidebar="false"
showHeader="false"
standardStylesheets="false"
applyBodyTag="false"
applyHtmlTag="false"
docType="html-5.0">
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="{!URLFOR($Resource.app, '/favicon.ico')}"></link>
<title>sfdx-vue-typescript-sample</title>
<link href="{!URLFOR($Resource.app, '/css/app.css')}" rel="stylesheet"></link>
</head>
<body>
<div id="app"></div>
<script src="{!URLFOR($Resource.app, '/js/chunk-vendors.js')}"></script>
<script src="{!URLFOR($Resource.app, '/js/app.js')}"></script>
</body>
</html>
</apex:page>
これでforce:source:push
すると、デフォルトのサンプルくらいは動くはずです。
アセットのURLが相対パスでは辿れずVfpページを実行しないと決まらない
たぶんこの状態だとVue.jsのロゴが表示されません。
デフォルトだと4KB以上の画像はfile-loaderで処理され、HTMLからの相対パスで取得しようとしますが、静的リソースのURLは相対パスでは辿れないからです。
かといって、静的リソースのパスをfile-loaderに渡そうにもVfpを実行しないとわからないので、ビルド時に渡すのは無理です。
しょうがないのでここはurl-loaderのみで処理するようにします。これで画像は全てData URI形式でHTML/CSSの中に埋め込まれます。
ただし、ファイルサイズが大きい画像を使いたくなったら別の作戦がいるかもしれません。
chainWebpack: config => {
// 画像はurl-loaderでData URI形式で埋め込む
const imagesRule = config.module.rule('images')
imagesRule.uses.clear();
imagesRule
.use('url-loader')
.loader('url-loader')
}
この辺りの設定は詳しくはWorking with Webpack | Vue CLI 3を見てください。
Lightning Design System(LDS)をどう組み込むか
LDSのCSSもstyleタグで展開してしまうという方法もあるのですが、それは無駄なので、Vfpではapex:slds
タグを使い、ローカルではCDNから読み込むという方針にしました。
vfpにはapex:slds
タグとbodyタグにslds-scope
クラスを追加します。
<apex:page
{中略}
docType="html-5.0">
<apex:slds />
<html lang="ja">
<head>
{中略}
</head>
<body class="slds-scope">
<div id="app"></div>
{中略}
</body>
</html>
</apex:page>
public/index.html
にはCDNからLDSを読み込むコードを追加します。
<!DOCTYPE html>
<html lang="en">
<head>
{中略}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/design-system/2.7.5/styles/salesforce-lightning-design-system.css">
</head>
<body>
{中略}
</body>
</html>
LDSアイコンは基本SVG Spriteで表示するが、これも動的にURLが決まる
これでLDSのクラスは使えるのですが、LDSではアイコンは基本的にSVG Spriteで使うことになっているので、<use xhtml:href='ここにパス'>
とパスを書く必要があります。
しかし、apex:slds
で読み込むとアイコンのパスはvfpの$Asset.SLDS
で提供されます。
<svg aria-hidden="true" class="slds-icon">
<use xlink:href="{!URLFOR($Asset.SLDS, 'assets/icons/standard-sprite/svg/symbols.svg#account')}"></use>
</svg>
その上、useはクロスドメインが許されていないので、ローカルで動かしたときにCDNから読み込むことができません。
よい方法が思いつかず、仕方なく@salesforce-ux/design-system npmモジュールを読み込んで、url-loaderでimgタグに埋め込むことにしました。
npmパッケージをインストールして、
npm i @salesforce-ux/design-system --save
svgファイルはurl-loader使うように設定
chainWebpack: config => {
{中略}
// svgはurl-loaderでData URI形式で埋め込む
const svgRule = config.module.rule('svg')
svgRule.uses.clear();
svgRule
.use('url-loader')
.loader('url-loader')
}
使う時はsrc属性にこう指定します。
<span class="slds-icon_container slds-icon-custom-custom33" title="会議室">
<img class="slds-icon slds-page-header__icon" src="~@salesforce-ux/design-system/assets/icons/custom/custom33.svg">
<span class="slds-assistive-text">会議室</span>
</span>
JavaScript Remotingをどう呼ぶか
まずは普通にSalesforce側でJS Remotingの設定をします。
public class RemotingService {
@RemoteAction
public static Room__c[] getAllRooms(){
return [SELECT Id, Name, Capacity__c FROM Room__c];
}
}
<apex:page
{中略}
controller="RemotingService">
クライアントサイドはproductionのとき(実質vfpで動くとき)のみJSRemoting呼んで、ローカルではダミーデータを返すという方針にしました。
export default {
getAllRooms(): Promise<any> {
if (process.env.NODE_ENV === 'production') {
return new Promise<any>((resolve) => {
Visualforce.remoting.Manager.invokeAction(
`RemotingService.getAllRooms`,
(result: any[], event: any) => {
resolve(result);
},
);
});
} else {
return Promise.resolve([
{ Id: '1', Name: 'ローカル会議室A', Capacity__c: 4 },
{ Id: '2', Name: 'ローカル会議室B', Capacity__c: 5 },
{ Id: '3', Name: 'ローカル会議室C', Capacity__c: 6 },
]);
}
},
};
これで困らなければローカルはREST APIを呼ぶなんてするより楽だなあと。
それとTypeScriptから使うために以下の型定義も追加しています。
declare namespace Visualforce.remoting {
export class Manager {
static invokeAction(actionName:string, ...args:any): void;
}
}
ネームスペースの対応は?
二箇所で設定しています。
sfdx-project.json
src/config.ts
sfdx-project.json
はスクラッチ組織をネームスペース付きで作るのに必須です。
src/config.ts
はJS内でネームスペースが必要になることがあるので、その読み込み用です。
export default {
$namespace: 'mynamespace',
};
今のところさきほどのJS Remotingを呼ぶところだけ修正すれば大丈夫でした。
呼び出し時に${config.$namespace}.RemotingService.getAllRooms
として、レスポンスにもネームスペースがついてくるのでそれをただ除去しています。
import config from '@/config';
export default {
getAllRooms(): Promise<any> {
if (process.env.NODE_ENV === 'production') {
return new Promise<any>((resolve) => {
window.Visualforce.remoting.Manager.invokeAction(
`${config.$namespace}.RemotingService.getAllRooms`,
(result: any[], event: any) => {
resolve(result.map((record) => this.stripNamespace(record)));
},
);
});
} else {
return Promise.resolve([
{ Id: '1', Name: 'ローカル会議室A', Capacity__c: 4 },
{ Id: '2', Name: 'ローカル会議室B', Capacity__c: 5 },
{ Id: '3', Name: 'ローカル会議室C', Capacity__c: 6 },
]);
}
},
stripNamespace(record: any): any {
const newRecord: {[s: string]: any} = {};
for (const [name, value] of Object.entries(record)) {
const match = name.match(/^[\w\d]+__([\w\d_]+__c)/);
newRecord[match ? match[1] : name] = value;
}
return newRecord;
},
};
パッケージはどう作るの?
基本的にはpackage.jsonのscripts
を見てください。
パッケージ作成についてはnpm run package
でできるので、以下のコマンドを追ってもらえればわかると思います。
Metadata API形式にソースを変換し、パッケージ開発組織にデプロイし、ベータパッケージ作成をしています。
"package": "npm run source:convert && npm run mdapi:deploy && npm run package1:create",
ついでに他のnpm scripts
も軽く説明しておきましょう。
npm run setup
はスクラッチ組織作って、ソースをpushして、権限セットをつけて、サンプルデータを入れます。
"setup": "npm run org:create && npm run push && npm run permset:assign && npm run data:import",
あと開発用によく使いそうなのは、serve
、deploy
、push
、pull
、test
あたりですね。
他のスクリプトは単独では使わないのがほとんどかと思います。
終わりに
Vue CLIで他の設定にしても、Reactアプリでも必要な修正はだいたい同じかと思います。
あえてLightningではないので需要があるかわかりませんが、どなたかの参考になれば幸いです。
最後に軽く宣伝を。
株式会社co-meetingはエンジニアを募集しています。Salesforceエンジニアと言いつつ、このようにVue.jsを使ったりもしますので、興味のある方はぜひお気軽にご連絡ください!