dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

react-lightning-design-systemをVisualforceで使う

件名の通りですが少しハマりどころがあったのでメモ。
普通に react-lightning-design-system を使おうとすると SVG アイコン使用時に Unsafe attempt to load URL エラーが表示されます。

Unsafe attempt to load URL https://zakiyama-dev-ed.my.salesforce.com/assets/icons/utility-sprite/svg/symbols.svg#event from frame with URL https://zakiyama-dev-ed--c.ap2.visual.force.com/apex/LDSTest. Domains, protocols and ports must match.

いわゆる CORS: Cross Origin Resource Sharing、つまり salesforce.comvisual.force.com という異なるオリジンでリソースのやり取りをしようとしているのが原因です。
Issue にもあります。

これを回避するためにはコメントにもあるように、util.setAssetRoot(path) というメソッドを使います。
また setAssetRoot に渡す静的リソースへのパスは、Visualforce 側で

{!URLFOR($Resource.slds)}

を使い取得します。

Visualforce ページと、React 側のエントリーポイントとなる index.js は以下のようになります。

<apex:page showHeader="false" standardStyleSheets="false" docType="html-5.0">
<html>
  <head>
    <meta charset="utf-8" />
    <apex:stylesheet value="{!URLFOR($Resource.slds,'assets/styles/salesforce-lightning-design-system.min.css')}" />
  </head>
  <body>
    <div id="root"></div>

    <apex:includeScript value="{!URLFOR($Resource.app, 'bundle.js')}" />
    <script type="text/javascript">
      App.init(document.getElementById('root'), '{!URLFOR($Resource.slds)}/assets');
    </script>
  </body>
</html>
</apex:page>
// src/scripts/index.js
import React from 'react';
import { render } from 'react-dom';
import { util } from 'react-lightning-design-system';

import App from './components/App';
import '../stylesheets/index.scss';

export const init = function(el, assetRoot) {
  util.setAssetRoot(assetRoot);
  console.log('Set asset root as ', util.getAssetRoot());
  render(<App />, el);
};

この例では、webpack.config.js の設定でビルドした JavaScriptApp というライブラリ名でエクスポートしてます(参考

また、初期化用関数の引数として渡してますが

window.assetRoot = '{!URLFOR($Resource.slds)}/assets';

のように window 関数に直接変数を生やして index.js 側で参照するという方法もあります。


余談:<script> タグと <apex:includeScript> タグの読み込み順序に関して

上の例では、<apex:includeScript> による JS 読み込みが続く <script> タグ内のスクリプトより先に実行される必要がありますが
https://developer.salesforce.com/docs/atlas.ja-jp.204.0.pages.meta/pages/pages_compref_includeScript.htm
を見ると、loadOnReady を明示的に true にしない限り <apex:includeScript> の方がすぐに読み込まれるようです。
(すぐに、というのが曖昧ですが、一旦こちらの読み込みが先行すると考えておいて良さそうです)


Spring'17 で追加された <apex:slds> を使う場合

Spring'17 から Visualforce ページでも、静的リソースにアップロードすることなくプラットフォームから提供される SLDS が使えるようになります。
参考:Visualforce ページでの Lightning Design System の使用

これを使う場合、先ほどのコードは

 <html>
   <head>
     <meta charset="utf-8" />
-    <apex:stylesheet value="{!URLFOR($Resource.slds,'assets/styles/salesforce-lightning-design-system.min.css')}" />
+    <apex:slds />
   </head>
   <body>
     <div id="root"></div>

     <apex:includeScript value="{!URLFOR($Resource.app, 'bundle.js')}" />
     <script type="text/javascript">
-      App.init(root, '{!URLFOR($Resource.slds)}/assets');
+      App.init(root, '{!URLFOR($Asset.SLDS)}/assets');
     </script>
   </body>
 </html>

というように、静的リソースから SLDS を読み込んでいた部分を <apex:slds /> に置き換え、さらにパス部分は $Asset.SLDS という変数を使って取得するようにします。

(2017/02/20追記)

f:id:dackdive:20170220103817p:plain

バージョンは最新バージョンの 2.2.1 でなく 2.1.3 のよう。
あと読み込んでいるリソースが salesforce-lightning-design-system-vf.min.css なのも気になる。
(追記ここまで)


さらに、applyBodyTag または applyHtmlTag が false だった場合

リリースノートおよび開発者ガイドの該当ページによると、
Using the Lightning Design System | Visualforce Developer Guide | Salesforce Developers

if you set applyBodyTag or applyHtmlTag to false, you must include the scoping class slds-scope

とあるので、

-  <body>
+  <body class="slds-scope">

などとする必要があります。
(そういう意味で、上の例では applyBodyTag および applyHtmlTag を false にしていないのに <html> や <body> を使っているのは適切ではないですね)


おまけ:<apex:slds> の既知のバグ

検証中、<apex:slds> を使うと最初の Unsafe attempt to load URL エラーが発生することがわかりました。
どうやら $Asset.SLDS の URL が salesforce.com になっており、調べてみると StackExchange にありました。バグのようです。

近日中に修正されるようなので1週間後ぐらいに確認してみることとします。

(2017/02/20追記)
今日見たら直ってました。
StackExchange のコメントに更新はなく、Known Issues にもないので、動きを確認した限りですが。
(追記ここまで)


リポジトリ