dackdive's blog

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

gulpとJSforceで構築するLightning Component開発環境

これまで、Lightning Component 開発は Force.com Migration Tool(Force.com 移行ツール) を使っていましたが
将来的なことを考えると JavaScript のグローバル名前空間の問題 をどうにかしないといけません。
つまり Grunt や gulp などのビルドツールを利用した JavaScript の事前ビルドが必要です。

※ グローバル名前空間の問題については Appirio さんのブログ記事が参考になります。
http://appirio.co.jp/category/tech-blog/2015/05/lightningdev2/

まあ、この問題はいずれプラットフォーム側で何らかの解決策を示してくれる...と楽観的に考えることもできると思いますが
自身の勉強も兼ねてこの機会に Force.com Migration Tool から乗り換えようと思った次第です。

gulp と JSforce を利用した開発環境については SalesforceDevelopersJapan のリポジトリ にサンプルがあります。

ので、基本的にはそれを参考にしつつ、ビルドの仕組みでよくわからなかったところや、使いやすくするために新たに変更を加えた箇所をメモしておきます。


最終的に構築したもの

現時点でのコードを GitHub に上げてあります。

https://github.com/zaki-yama/lightning-gulp-template

gulpfile.js のみ掲載します。今のところこんな感じです。


元々の gulpfile.js についてのメモ

元々はこのようになっていました。

var componentName = 'HereIsYourComponentName';

var gulp = require("gulp");
var zip = require("gulp-zip");
var browserify = require("browserify");
var debowerify = require('debowerify');
var source = require("vinyl-source-stream");
var deamd = require('deamd');
var through2 = require("through2");
var jsforce = require("jsforce");

var forceDeploy = function(username, password) {
  return through2.obj(function(file, enc, callback) {
    var conn;
    conn = new jsforce.Connection();
    return conn.login(username, password).then(function() {
      return conn.metadata.deploy(file.contents).complete({
        details: true
      });
    })
    .then(function(res) {
      if (res.details !== null && !res.success){
        console.error(res);
        return callback(new Error('Deploy failed.'));
      }
      return callback();
    }, function(err) {
      console.error(err);
      return callback(err);
    });
  });
};

gulp.task("build", function() {
  return browserify({
    entries: ["./src/scripts/"+ componentName +".js"],
    standalone: componentName
  }).transform(debowerify)
  .bundle()
  .pipe(source(componentName + ".resource"))
  .pipe(deamd())
  .pipe(gulp.dest("pkg/staticresources/"));
});

gulp.task("deploy", function() {
  return gulp.src("pkg/**/*", {
    base: "."
  }).pipe(zip('pkg.zip')).pipe(forceDeploy(process.env.SF_USERNAME, process.env.SF_PASSWORD));
});

gulp.task("watch", function() {
  gulp.watch("src/**/*", ["build"]);
  gulp.watch("pkg/**/*", ["deploy"]);
});

gulp.task("default", ["build", "deploy"]);

このうち、JS のビルドを行っているのが build というタスクで、

  return browserify({
    entries: ["./src/scripts/"+ componentName +".js"],
    standalone: componentName
  }).transform(debowerify)

browserify に standalone というオプションをつけると、componentName で指定したグローバル変数の下にモジュールが export されます。
(表現が適切でないかもしれません。。。)

参考:

Generate a UMD bundle for the supplied export name.
This bundle works with other module systems and sets the name
given as a window global if no module system is found.

これにより、自分で開発した JS およびその時に使用した 3rd party JavaScript ライブラリがすべて1つの名前空間におさまるようになり、
他のコンポーネントとライブラリがバッティングするのを気にしなくてよくなります。

ちなみに、ソースコードを見てみると、

src/scripts/HereIsYourComponentName.js
var _ = require("lodash");
var $ = require("jquery");

module.exports = {
  jqueryVersion: function() {
    return $.fn.jquery;
  },
  lodashVersion: function() {
    return _.VERSION;
  }
};
HereIsYourComponentName.cmp
<aura:component implements="flexipage:availableForAllPageTypes">
    ...(略)...
    <ltng:require scripts="/resource/HereIsYourComponentName" afterScriptsLoaded="{!c.doInit}"/>
    ...(略)...
</aura:component>
HereIsYourComponentName.js
({
    doInit : function(component, event, helper) {
        //hey
    component.set("v.jqueryVersion",HereIsYourComponentName.jqueryVersion());
    component.set("v.lodashVersion",HereIsYourComponentName.lodashVersion());
    },
})

コンポーネントとは独立した JS で2つの関数を export しており、これをビルドしたものをコンポーネント側で <ltng:require> を使って読み込んでいます。
そうすると、HereIsYourComponentName というグローバル変数から自作モジュールを参照することができます。

また、その下の debowerify は名前からなんとなく想像がつきますが、
bower でインストールした JS ライブラリを require できる形に変換するパッケージです。


追加でやったこと

  1. username, password をファイルで管理する
  2. ビルドに失敗しても watch が止まらないようにする
  3. 静的リソースは zip 形式でデプロイする&CSS と一緒にデプロイできるようにする
  4. 静的リソースをキャッシュさせないようにする
1. username, password をファイルで管理する

元の gulpfile.js では

$ SF_USERNAME=yourSalesforceUsername SF_PASSWORD=yourSalesforcePassword gulp

というようにコマンド実行時に引数で username, password を指定しますが
Force.com Migration Tool の build.properties のようにファイルで定義して渡せるようにします。

gulp-env というパッケージを使うと実現できそうです。

gulpfile.js には

var env = require('gulp-env');

env({
  file: '.env.json'
});

とし、.env.json

{
  "SF_USERNAME": "Your Salesforce Username",
  "SF_PASSWORD": "Your Salesforce Password"
}

のように与えたい変数を JSON 形式で書きます。

これだけでファイルに記述した変数が読み込まれ、process.env.SF_USERNAME で参照できるようになります。
(なので、deploy タスクに修正は必要ありません)


2. ビルドに失敗しても watch が止まらないようにする

元の gulpfile.js でも、gulp watch コマンドでファイルの変更を検知し、自動的に再ビルド&デプロイが走ります。
しかし、JS の構文ミスなどでビルドに失敗すると watch が止まってしまいます。

そのため、build タスクに以下を追加しました。

var notify = require('gulp-notify');

// ref. https://github.com/vigetlabs/gulp-starter/blob/master/gulpfile.js/lib/handleErrors.js
var handleErrors = function(err, callback) {
  notify.onError({
    message: err.toString().split(': ').join(':\n'),
    sound: false
  }).apply(this, arguments);
  // Keep gulp from hanging on this task
  if (typeof this.emit === 'function') {
    this.emit('end');
  }
}

gulp.task("build", function() {
  return browserify({
    entries: ["./src/scripts/"+ componentName +".js"],
    standalone: componentName
  }).transform(debowerify)
  .bundle()
  .on('error', handleErrors)  // これを追加
  .pipe(source(componentName + ".resource"))
  .pipe(deamd())
  .pipe(gulp.dest("pkg/staticresources/"));
});

参考:browserifyでgulp-plumberが効かない時の対処方法 - Qiita

gulp-notify というのはエラー時にデスクトップ通知を表示してくれるパッケージです。
ビルドに失敗した時にこんな感じになります。

f:id:dackdive:20160127130459p:plain

別に通知を出す必要はなく、単にコンソールに出力するだけでよかったんですが
試した結果うまくいかなかったのと、watch は裏で走らせてて基本はエディタだけ見てるのでビルド失敗に気づけていいかなと思い、参考記事そのままにしてます。


3. 静的リソースは zip 形式でデプロイする&CSS と一緒にデプロイできるようにする

元の gulpfile.js は JS をビルドしたものをそのまま1つの静的リソースとしてデプロイします。
このままでも問題はないのですが、たとえば 3rd party のライブラリに CSS も含まれていて
一緒にデプロイしたい場合にどうしたらいいんだろう?と迷ったため、
CSSsrc/styles に置き、静的リソースを作成するときはまとめて zip 形式に圧縮してしまうことにしました。

これについては JSforce の下記のブログが参考になりました。
Deploying Package to Salesforce Using JSforce and Gulp.js - JSforce

gulpfile.js を以下のように変更します。

gulp.task('build', ['js', 'css', 'statics'], function() {
  return gulp.src('./build/**/*')
  .pipe(zip(componentName + '.resource'))
  .pipe(gulp.dest('./pkg/staticresources'));
});

gulp.task('js', function() {
  return browserify({
    entries: ['./src/scripts/'+ componentName +'.js'],
    standalone: componentName
  }).transform(debowerify)
  .bundle()
  .on('error', handleErrors)
  .pipe(source(componentName + '.js'))
  .pipe(deamd())
  .pipe(gulp.dest('./build/scripts'));
});

gulp.task('css', function() {
  return gulp.src('./src/styles/*.css')
  .pipe(gulp.dest('./build/styles'));
});

// `statics` は画像ファイルなど。CSS と同じような処理なので省略

元々の build タスクは js タスクとし、src ディレクトリ以下の各種リソースを(個別にビルドなどして)一旦 build ディレクトリに格納し、最終的に build タスクではそれを zip 圧縮して静的リソースに変換しています。

CSS は今のところ何もしていませんが、ここで SASS や LESS のコンパイルなどを行うこともできそうです。

また、それに伴いコンポーネント側の静的リソース読み込み部分も修正しました。

HereIsYourComponentName.cmp
<aura:component implements="flexipage:availableForAllPageTypes">
  ...略...
  <ltng:require
    scripts="/resource/HereIsYourComponentName/scripts/HereIsYourComponentName.js"
    styles="/resource/HereIsYourComponentName/styles/HereIsYourComponentName.css"
    afterScriptsLoaded="{!c.doInit}"/>


4. 静的リソースをキャッシュさせないようにする

これについては stomita さんの Qiita の記事の通りです。ありがとうございます。
静的リソースをキャッシュさせない - Qiita

gulp-if, gulp-replace というパッケージを使います。

gulpfile.js
var gulpif = require('gulp-if');
var replace = require('gulp-replace');

// ...略...

gulp.task('deploy', function() {
  var ts = Date.now();  // Timestamp
  return gulp.src('pkg/**/*', {
    base: '.'
  })
  .pipe(gulpif('**/*.cmp', replace(/__NOCACHE__/g, ts)))
  .pipe(zip('pkg.zip'))
  .pipe(forceDeploy(process.env.SF_USERNAME, process.env.SF_PASSWORD))
  .on('error', handleErrors);
});
HereIsYourComponentName.cmp
<aura:component implements="flexipage:availableForAllPageTypes">
  ...略...
  <ltng:require
    scripts="/resource/HereIsYourComponentName/scripts/HereIsYourComponentName.js?__NOCACHE__"
    styles="/resource/HereIsYourComponentName/styles/HereIsYourComponentName.css?__NOCACHE__"
    afterScriptsLoaded="{!c.doInit}"/>


TODO

以上でとりあえず gulp × JSforce での開発が行えるようになりました。
が、まだまだわからないことや修正が必要なところがあるので TODO として挙げておきます。

ビルドに失敗しても deploy が実行される

2. ビルドに失敗しても watch が止まらないようにする の影響だと思いますが、ビルドに失敗してもとりあえずデプロイしようとタスクが進んでしまいます。要修正です。

ビルドとデプロイのタスクが並行して走っている?

まだ gulp の理解が不十分でよくわかっていないんですが。
ふつうに gulp コマンドを実行すると builddeploy が同時にスタートしてるように見え、
デプロイ実行のタイミングによっては最新の静的リソースが正しくデプロイできていない(または、初回の場合静的リソースファイルがないので Required field is missing: content とかエラーが出る)といった現象がたびたび発生します。
このへんも gulpfile.js の書き方を修正する必要がありそうです。

CSS名前空間の問題に対処する必要がある

コンポーネント名前空間が衝突してしまうという問題は CSS でも同様です。
この解決策として提示されているのは、下記のサイトを使って CSS にまとめて名前空間を設定する方法です。

https://bootstrap-namespacer.herokuapp.com/

が、同じことをローカルで実現する方法についてはわかっていません。

CSS の取り扱いがわからない

名前空間問題にも関係するんですが。
調べてみたところ、browserify や webpack は JS と一緒に CSS もビルドすることができます。
そうすると CSS も含めて1つのJSファイルにまとめることができます。

これを使うと、3. 静的リソースは zip 形式でデプロイする&CSS と一緒にデプロイできるようにする でやったことが必要なくなるだけでなく CSS名前空間問題も同時に解決できるんでしょうか。

webpack だとどうなるか試してみる

これはどちらかと言うと自分の勉強のためです。
webpack について言及しているブログ記事などを見た限り
browserify は何がなんでも1つのファイルにまとめてしまうけど、webpack は条件に応じて複数のファイルに出力できる
という違いがあるようです。
(参考:こないだ社内の勉強会でwebpackのこととか話したのでまとめた - getalog

ただ、Lightning Component 開発においては1つの名前空間に全JSを閉じ込められた方が良いので必要ないのかな...?