ReactをES2015で書いてVisualforce上で動かす

# 概要
- [gulpを使ってJavaScriptをwebpackでビルドしてForce.comにデプロイする](http://qiita.com/kenichi_odo/items/8be8e773cfb98b0bb1bf)(前回記事)

　gulpのデプロイ環境構築から1ヶ月経ち、実務であれやこれやと設定ファイルを修正しながらようやく落ち着いて開発できる環境になった気がするので、記録として残しておきます。

# 動作確認環境
- OS
    - OS X El Capitain 10.11.5
- node
    - v6.2.2
- npm
    - 3.9.5
- gulp
    - 3.9.1

# package.json
　ES2015で書くために、Babel系のモジュールを一通りインストールします。ReactのJSXのSyntaxを使うなら`babel-preset-react`も必要です。
　`.babelrc`もしくは`package.json`でbabelをkeyにpresetsを指定する必要があります。

```json:package.json
{
  "devDependencies": {
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.9.0",
    "babel-preset-react": "^6.5.0",
    "babel-register": "^6.9.0",
    …
    …
    …
  },
  "babel": {
    "presets": [
      "es2015",
      "react"
    ],
    "compact": false
  }
}
```

# gulp処理
　Babelを導入したので、gulpfileもES2015の記述で書くことにしました。ファイル名を`gulpfile.babel.js`にすることでES2015のSyntaxで記述できます。
　gulpの処理は非同期なのでwebpackのビルドやデプロイ前のzip圧縮などの時間のかかる処理は実行順が前後します。前回はtask分割してreturnでコールバックを返すことで同期をとりましたが、`.on()`を使ってエラーや終了をbindすることにしました。よって、gulpのタスク分割はやめてdefaulタスクのみ定義し、ターミナル上ではgulpの記述のみでファイル監視(watch)を開始するようにしています。
　また、Macの場合は`display notification`を利用して処理間にどこまで実行完了したかを通知センターに流しています。

### 保存処理前
<img width="211" alt="before.png" src="https://qiita-image-store.s3.amazonaws.com/0/110768/c8cc1b8a-4636-9c8a-e6a1-30a51769c9c0.png">

### 保存処理後
<img width="276" alt="スクリーンショット 2016-07-03 16.40.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/110768/ca867d07-c0bc-6186-18c3-0b843d68c913.png">

　ファイル監視は`original`というフォルダの配下全体を見るようにしています。originalの中に親フォルダ、またその直下にはmain.jsというファイルを配置し、保存処理がかかるとmain.jsをビルドし親フォルダ名で静的リソースへビルドします。
　セットで上げる必要のあるメタデータもsalesforce-metadata-xml-builderでプログラム上で作成できるので手作業で作る必要もなくなりました（便利ですね！）。

```js:gulpfile.babel.js
// Force.comのログインユーザ名とパスワードの設定
process.env.SF_USERNAME = 'xxx@xxx.xxx';
process.env.SF_PASSWORD = 'xxxxxxxx';

// moduleのロード
import gulp from 'gulp';
import file from 'gulp-file';
import { exec } from 'child_process';
import webpack from 'webpack-stream';
import zip from 'gulp-zip';
import deploy from 'gulp-jsforce-deploy';
import metadata from 'salesforce-metadata-xml-builder';
import os from 'os';
import precss from 'precss';

// ターミナルからgulpを実行した時の処理
gulp.task('default', () => {
  
  // original配下のファイルを監視する
  gulp.watch('./original/**/*').on('change', event => {
    
    // Windows or Macのファイル区切り文字
    const split_str = os.type().toString().match('Windows') !== null ? '\\' : '/';
    
    // デプロイ時の静的リソース名
    // ファイル更新した際の属するフォルダ名(original直下のフォルダ名)を設定
    let resource_name = null;
    
    // デプロイ時のパス設定
    const paths = [];
    let end_flg = false;
    for (const path of event.path.split(split_str)) {
      if (end_flg) {
        paths.push(path);
        resource_name = path;
        break;
      }
      
      if (path === 'original') {
        end_flg = true;
      }
      paths.push(path);
    }
    
    // 静的リソースのメタデータ作成
    const resource_meta_xml = metadata.StaticResource({
      cacheControl: 'Public',
      contentType: 'application/zip',
    });
    
    // デプロイ用package.xmlの作成
    const package_xml = metadata.Package({
      types: [
        { name: 'StaticResource', members: [resource_name] },
      ],
      version: '35.0',
    });
    
    // ビルドするmain.js
    const entry = `${paths.join(split_str)}/main.js`;
    
    // ビルド開始をMacの通知センターに流す
    exec(`echo 'display notification "ビルド開始: ${resource_name}" with title "gulp" subtitle "webpack" ' | osascript`);
    
    // webpackのビルド
    gulp.src(entry)
      .pipe(webpack({
        entry,
        output: {
          filename: 'bundle.js',
          libraryTarget: 'umd', // ライブラリとして出力
          library: `${resource_name}`, // windowにフォルダ名で書きだして呼び出せるようにする
        },
        devtool: 'inline-source-map', // ビルドしたjsをデバッグできるようにsource mapをインラインで埋め込み
        module: {
          loaders: [
            {
              test: /(\.js)$/,
              loaders: ['babel'],
              exclude: /node_modules/,
            },
            {
              test: /\.sass$/,
              loaders: ['style', 'css', 'postcss?parser=sugarss'],
            },
          ],
        },
        postcss: [precss],
      }))
      .on('error', () => {
        // webpackのビルドでエラー時、デプロイせずエラーを通知
        exec(`echo 'display notification "ビルドエラー: ${resource_name}" with title "gulp" subtitle "webpack" ' | osascript`);
      })
      .pipe(gulp.dest(`./build/${resource_name}`)) // webpackでビルドしたbundle.jsをbuildフォルダに格納
      .on('finish', () => {
        // ビルドの終了を通知
        exec(`echo \'display notification "ビルド終了: ${resource_name}" with title "gulp" subtitle "webpack" ' | osascript`);
        
        // zip圧縮→デプロイ
        gulp.src(`./build/${resource_name}/*`)
          .pipe(zip(`${resource_name}.resource`))
          .pipe(file(`${resource_name}.resource-meta.xml`, resource_meta_xml))
          .pipe(gulp.dest('./src/staticresources'))
          .on('finish', () => {
            
            // zip圧縮が完了したらデプロイ開始通知
            exec(
              `echo 'display notification "デプロイ開始: ${resource_name}.resource" with title "gulp" subtitle "Salesforce" ' | osascript`
            );
            
            // デプロイ処理
            gulp.src('./src/**', { base: '.' })
              .pipe(file('src/package.xml', package_xml))
              .pipe(zip('pkg.zip'))
              .pipe(deploy({
                username: process.env.SF_USERNAME,
                password: process.env.SF_PASSWORD,
                loginUrl: 'https://test.salesforce.com',
              }))
              .on('finish', () => {
                
                // デプロイが完了したら通知を流す
                exec(
                  `echo 'display notification "デプロイ完了: ${resource_name}.resource" with title "gulp" subtitle "Salesforce" ' | osascript`
                );
              });
          });
      });
  });
});
```
<font color="red">※上記exec呼び出し部分の`echo`の後と`| osascript`の前のシングルクォートのエスケープ(`\`)は、Qiita上のシンタックスカラーが正常に働かないため、あえて除去しています。</font>

# 以下Sampleの静的リソースの簡単な説明
## Sass
　前から書いてみたかったのですが、変数を使えたりネストしたりすごく便利ですね。最初はsass-loaderを通していましたが今はPostCSSとSugarSS、precssを使っています。

```sass:original/SampleResource/style/main.sass
$red: #f00

.hoge-style
  background: $red
```

## React
　itemsという配列を回して中身を表示させています。Reactについては、いろんな参考記事が既にあると思うので調べてみてください。

```js:original/SampleResource/react/sample-component.js
import React from 'react';
import Classname from 'classnames';


export default class SampleComponent extends React.Component {
  constructor(props_) {
    super(props_);
    this.state = props_;
  }

  componentWillReceiveProps(props_) {
    this.setState(props_);
  }

  render() {
    if (this.state.items.length === 0) {
      return null;
    }

    return (
      <div style={ { display: 'table' } }>
        {
          this.state.items.map(item_ => {
            return (
              <div key={ item_.id } style={ { display: 'table-row' } }>
                <div className={ Classname({ 'hoge-style': item_.name === 'hoge' }) } style={ { display: 'table-cell' } }>
                  { item_.name }
                </div>
              </div>
            );
          })
        }
      </div>
    );
  }
}
```

## メイン処理
　使用するライブラリをimport(npmでinstall必須)、自分で書いたJavaScriptファイルはパス指定でimportします(export defaultしておく)。
　CSSやSassなどのスタイル系のファイルも、importしておくことで動的に`<style>`を生成し`<head>`タグ内に追加されます。
　このメインのクラスは、Visualforceで呼び出せるようにexportしておきます。

```js:original/SampleResource/main.js
import './style/main.sass';
import $ from 'jquery';
import React from 'react';
import ReactDom from 'react-dom';
import SampleComponent from './react/sample-component';


export class Main {
  constructor(name_) {
    this._name = name_;

    this._$sample_component_div = $('<div />');
    $('#contents_area').append(this._$sample_component_div);
  }

  load() {
    const items = [{
      id: '001',
      name: 'hoge',
    }, {
      id: '002',
      name: 'fuga',
    }];

    ReactDom.render(<SampleComponent items={ items } />, this._$sample_component_div.get(0));

    alert(this._name);
  }
}
```

## Visualforce
　Salesforceへのデプロイ後は、bundle.jsを読み込みます。webpackでlibraryTargetを指定することでJavaScriptのグローバルのwindowにmain.jsのクラスが出力されているので、あとは親フォルダ名で呼び出して使ってください。
　Visualforceのグローバル変数などは、コンストラクター等で渡すとかいろんな方法がありそうですね。

```html:Sample.page
<apex:page
  cache="false"
  sidebar="false"
  showHeader="false"
  standardStylesheets="false"
  applyBodyTag="false"
  applyHtmlTag="false"
  docType="html-5.0">

<html>

  <body>
    <div id="contents_area"></div>

    <script src="{!URLFOR($Resource.SampleResource, '/bundle.js')}"></script>
    <script>
      var _main = new SampleResource.Main('{!$User.UserName}');
      _main.load();
    </script>
  </body>

</html>

</apex:page>
```

## 実行結果
<img width="568" alt="result.png" src="https://qiita-image-store.s3.amazonaws.com/0/110768/a8da2388-baa2-d1cc-12a0-e573b0e280b2.png">


# 終わりに
　上記のソースは、[GitHub](https://github.com/kenichi-odo/deploy_salesforce_using_gulp_webpack/releases/tag/2.0)にあります。
　Source code (zip)を落として展開したら、`gulpfile.babel.js`の**process.env.SF_USERNAME**と**process.env.SF_PASSWORD**を設定します(loginUrlの[https://test.salesforce.com](https://test.salesforce.com)も適宜変更)。
　その後、ターミナルで展開したフォルダまで移動しモジュールをインストールした後、`gulp`で実行してサンプルのソースを適当に変更して保存してみてください。きっと動く。。。と思います。

```bash
cd deploy_salesforce_using_gulp_webpack-2.0
sudo npm i
gulp
```

　また、コード補完やコード解析の設定ファイルも含んでいるので開発するエディタがAtomならば、`atom-ternjs` `linter` `linter-eslint` `linter-sass-lint`のPackageをAtomに入れてからソースのフォルダを読み込むと、エディタ上で確認できます。