Salesforce B2C Commerce の SFRA での CSS や JS の適用について



※ これから記載する事項は、私が所属する会社とは一切関係のない事柄です。


今回の記事では、SFRAに対してデザインの修正を行いたい場合やクライアントサイドに新しく機能を追加したい場合の方法を紹介したいと思います。


## 前提としてSFRAについて知っておいた方がいいこと

#### ① app_storefront_base のコードは修正しないようにしましょう

SFRAでは `app_storefront_base` というカートリッジを使用していますが、そのカートリッジ内のソースコードを修正してしまうと、後の SFRA アップデートに影響が出たり、その他想定外の影響が出ることがありますので、修正しないようにしましょう。

#### ②  Bootstrap 4 を利用しています

SFRA では NPM レジストリ上の [bootstrap 4](https://getbootstrap.jp/docs/4.2/getting-started/introduction/) をインポートして利用しています。そのためグリッドシステムやその他の多くのコンポーネントを利用できます。


#### ③  jQuery を利用しています

SFRA では NPM レジストリ上の [jQuery](https://jquery.com/) をインポートして利用しています。クライアントサイドでの機能追加は基本的にはjQueryを利用して行います。


#### ④ static フォルダに静的ファイルは設置します

SFRA では CSS / JS / Image / Font などの静的ファイルは static フォルダに設置して利用します。（もちろん商品画像やバナー画像などは別です）


#### ⑤ SCSS を利用します

SFRA では SCSS ファイルをコンパイルし、static フォルダ内に CSS ファイルを生成してスタイルを適用します。
JS, SCSS のコンパイルについては [Webpack](https://webpack.js.org/) を利用していますが、[sgmf-scripts](https://www.npmjs.com/package/sgmf-scripts) というコマンドを通して利用できますので、[前回紹介した記事](https://qiita.com/syunyama/items/5de237cddd76c0c50877)をご覧ください。




## JS の作成

JS は `cartridge/client/default/js` フォルダ内に作成しますが、
基本的には下記のようなフォルダ構成になります。（箇条書きで書いているので若干分かりにくくてすみません。。）

```
・ /cartridge/client/default/js
    ・ モジュール１
        ・ モジュール１.js
    ・ モジュール２
        ・ モジュール２.js
    ・ モジュールをインポートしてISMLでインポートされる.js
```


シンプルな例を作成してみます。下記のようなフォルダ構成になっているとします。
```
・ /cartridge/client/default/js
    ・ module
        ・ module.js
    ・ sample.js
```


下記のようにモジュールを作成します。 `bindSomething` のように関数を設定するとページが読み込まれた後に実行されるのでここで HTML にイベントをバインドすることが多いです。また、`methods` のようにオブジェクトを設定しているとページ読み込み後に呼び出されませんので、クライアントでの操作時に実行したい関数などを定義できます。

```js:/cartridge/client/default/js/module/module.js
module.exports = {
    bindSomething: function () {
        $('.sample').on('click', function () {
            // Do shomething.
        });
    },
    methods: {
        sampleMethod: function(){
            // Do shomething.
        },
    }
}
```


作成したモジュールは下記のようにインポートしてきます。

```js:/cartridge/client/default/js/sample.js
'use strict';

var processInclude = require('base/util');

$(document).ready(function () {
    processInclude(require('./module/module'));
});
```


作成した JS ファイルがコンパイルされて生成された `sample.js` を ISML ファイルで下記のように呼び出して利用します。

```html
<isdecorate template="common/layout/page">
    <isscript>
        var assets = require('*/cartridge/scripts/assets.js');
        assets.addJs('/js/sample.js');
    </isscript>
    ==OMIT==
</isdecorate>
```




## SCSS の作成

SCSS も JS に似ていて、 `cartridge/client/default/scss` フォルダ内に作成しますが、
基本的には下記のようなフォルダ構成になります。（たまにモジュールのフォルダから直接 ISML でインポートすることもあります。）

```
・ /cartridge/client/default/scss
    ・ モジュール１
        ・ _モジュール１.scss
    ・ モジュール２
        ・ モジュール２.scss
    ・ モジュールをインポートしてISMLでインポートされる.scss
```


シンプルな例を作成してみます。下記のようなフォルダ構成になっているとします。

```
・ /cartridge/client/default/scss
    ・ module
        ・ _module.scss
    ・ sample.scss
```


下記のようにモジュールを作成します。Bootstrap などのライブラリモジュールの呼び出しも可能です。


```scss:/cartridge/client/default/scss/module/_module.scss
@import "bootstrap/scss/variables";
@import "bootstrap/scss/mixins/breakpoints";

.someClass {
   // Some style.
}
```


作成したモジュールは下記のようにインポートしてきます。

```scss:/cartridge/client/default/scss/sample.scss
@import "module/module";
```


作成した SCSS ファイルがコンパイルされて生成された `sample.css` を ISML ファイルで下記のように呼び出して利用します。

```html
<isdecorate template="common/layout/page">
    <isscript>
        var assets = require('*/cartridge/scripts/assets.js');
        assets.addCss('/css/sample.css');
    </isscript>
    ==OMIT==
</isdecorate>
```


**Tips**
ISML からインポートされないようなパーシャル SCSS ファイルはお作法として `_` (アンダーバー)  をプリフィックスとしてつけた方が良さそうです。（[参考](https://sass-lang.com/guide#topic-4)）



## SFRA の CSS の上書き

例えば、`app_custom_sample` というカートリッジを作成してホームページの CSS を上書きたいとします。
SFRA のデフォルトのホームページは下記の SCSS を利用しています。
`app_storefront_base/cartridge/client/default/scss/homePage.scss`

CSS を上書きする場合はカスタムカートリッジの同じ階層に同じファイル名のファイルを作成します。
`app_custom_sample/cartridge/client/default/scss/homePage.scss`

そして下記のように、`@import "base/homePage";`   で SFRA の SCSS をインポートしてから上書きを行います。

```scss:app_custom_sample/cartridge/client/default/scss/homePage.scss
@import "base/homePage";

.page-title {
    color:red !important;
}
```



## Bootstrap の利用

基本的には[サイト上に記載のあるのコンポーネント](https://getbootstrap.jp/docs/4.2/components/alerts/)は使用できます。
例えばサンプルにあるアラートの HTML を下記のように ISML に記載します。

```html
<div class="alert alert-primary" role="alert">
       A simple primary alert—check it out!
</div>
```

すると、下記のようにスタイルが適用された状態で表示されます。
![image (45).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/6f7083f4-b29b-04b6-3b2f-4952b38d5225.png)



***Tips***

一部コメントアウトされて利用できないものもありました。（ [コード](https://github.com/SalesforceCommerceCloud/storefront-reference-architecture/blob/master/cartridges/app_storefront_base/cartridge/client/default/js/thirdParty/bootstrap.js) （Account Manager アカウント必要））
この場合は、`main.js` を修正する必要があります。例えば、`app_custom_sample/cartridge/client/default/js/main.js` を作成して下記のように記載します。

```js:app_custom_sample/cartridge/client/default/js/main.js
require('base/main');
require('bootstrap/js/src/tooltip.js');
```

すると下記のように Bootstap の API を呼び出せます。

```js
$('[data-toggle="tooltip"]').tooltip();
```




## SFRA の JS の上書き

例えば商品詳細ページの JS を上書きしたいとします。

まず、SFRA の商品詳細ページでは、`app_storefront_base/cartridge/client/default/js/productDetail.js`
を読み込んでおり、さらにこの JS はモジュールとして、`app_storefront_base/cartridge/client/default/js/product/detail.js` を読み込んでいます。

上書きする場合は、下記のようにファイルを作成します。

まず、`productDetail.js` をコピーしてきます。今回はそのまま使いますが、SFRA のモジュールをそのまま使う場合は、
`processInclude(require('base/hoge'));`のようにルートのエイリアスに `base` を使うように変更しましょう。

```js:app_storefront_base/cartridge/client/default/js/productDetail.js
'use strict';

var processInclude = require('base/util');

$(document).ready(function () {
    processInclude(require('./product/detail'));
});
```


今回は、`detail.js` の関数 `showSpinner` を上書きしたいと思います。完全に上書きしたい場合は、`base.showSpinner()` の部分は消しても良いかと思います。 

```js:app_storefront_base/cartridge/client/default/js/product/detail.js
'use strict';
var base = require('base/product/detail');

module.exports = {
    ...base,
    showSpinner: function () {
        base.showSpinner()
        // Add logic.
    },
};
```





## サイト全体の CSS、JS 修正


SFRA でサイト全体に適用されている CSS, JS は、`global.scss`, `skin.scss`, `main.js` です。
**あくまで私の所感** ですが、この中で、`global.scss` と `main.js` はあくまでモジュールのインポートに徹した方がいいような気がします。
例えば、下記のような使い方です。

`global.scss` では下記のように自作の変数やモジュール、ライブラリのモジュールをインポートするために使う。

```scss:global.scss
@import "base/global";
@import "variables";
@import "custom/custom";
```

`skin.scss` では、カスタムプロパティの上書きやちょっした CSS の追加として使う。

```scss:skin.scss
@import "base/skin/skin";

:root {
    --skin-banner-background-color-1: #3f0a00;
    --skin-banner-background-color-2: #000009;
}

.hoge {
    // Some code
}
```

`main.js` は自作のモジュールやライブラリのモジュールをインポートするために使う。

```js:main.js
require('base/main');

$(document).ready(function () {
    processInclude(require('./custom/custom'));
});

require('bootstrap/js/src/tooltip.js');
```



## スクリプトを追加したい

Google Analytics などの CDN で提供されているようなライブラリをインポートしたい場合やちょっとしたスクリプトを埋め込みたい時があるかと思います。
そういった場合に便利なのが SFRA であらかじめ用意されている Hooks を利用することです。
Hooks の利用方法については[以前のQiita](https://qiita.com/syunyama/items/7cea968880505ec303db)をご参照ください。

追加場所としては下記の２つがあり、それぞれに Hook 名があります。

* head タグ内
    * **app.template.afterFooter**
* footer タグの後
    * **app.template.htmlHead**


まず下記のように、`htmlHead` , `afterFooter` と言う名前の関数名でエクスポートしたJSを作成します。
下記の例では、footer タグの後には、Business Manager の **マーチャントツール > サイト環境設定 > カスタムサイト環境設定グループ** で設定した値（`SampleSnippet`）をレンダリングしており、head タグ内には、`hooks/afterFooter` テンプレートの中身をレンダリングしています。


```js:cartridge/scripts/hooks/sampleTemplate.js
const Site = require('dw/system/Site');
const velocity = require('dw/template/Velocity');
var ISML = require('dw/template/ISML');

const afterFooter = function () {
    const currentSite = Site.current;
    const snippet = currentSite.getCustomPreferenceValue('SampleSnippet');
    if (snippet) {
        velocity.render(snippet, null);
    }
};

function htmlHead(params) {
    var templateName = 'hooks/afterFooter';
    try {
        ISML.renderTemplate(templateName, params);
        // Another option is to render using Velocity templates
        /*
         params.message = params.message || 'world';
         velocity.render('Hello $message', params);
         */
    } catch (e) {
        Logger.error('Error while rendering template ' + templateName);
    }
}

exports.htmlHead = htmlHead;
exports.afterFooter = afterFooter;
```

呼び出す際は下記のように `hooks.json` の設定をお忘れなく。

```json:cartridges/app_storefront_base/hooks.json
{
  "hooks": [
    {
      "name": "app.template.afterFooter",
      "script": "~/cartridge/scripts/hooks/sampleTemplate"
    },
    {
      "name": "app.template.htmlHead",
      "script": "~/cartridge/scripts/hooks/sampleTemplate"
    }
  ]
}
```