Electron + React.js でちょっとした Markdown Viewer を作成して少し知見が溜まったので宣伝とハマりどころなどまとめた (gulpfile編)


ちょっと色々ネタがあるので分けて作成していきます。今回は gulpfile 編です。

# 作成したもの

**markcat**  
Electron製の Markdown Viewer

## 機能的な特徴

* **Intellij IDEA の Darula 風の表示テーマも用意**
* 表示テーマの変更も可能
* Github Flavored Markdown
* コードハイライト表示
* 編集時の自動更新
* ドラッグ＆ドロップからの Markdown 表示
* (Windows) SendTo に配置することにより、エクスプローラーの「送る」からの Markdown 表示
* (Windows)ファイル関連付けを行うと、ダブルクリックからの Markdown 表示
* (Mac)このアプリケーションで開く からの起動。
* (Mac)ファイル拡張子で関連付けを行うと、ダブルクリックからの Markdown 表示

## 開発としての特徴

* Typescript + gulp + react + electron と今風の技術を利用しています。
* 実装はそれなりにシンプルなので学習にも最適
* とはいえ Markdown としての基本機能に追加して上記のこだわり機能を実装しています。

## 開発動機

Markdown は普段は Atom などのエディタを利用して作成しています。  
Markdown を見る時も同じく Atom を利用したり、Chrome の拡張機能を利用して表示したりしていたのですが、md ファイルをダブルクリックしたり、エクスプローラーのコンテキストメニューからサクッと見ることができないかと考えていました。  
そんな時 WEB+DB Press の React の記事の中で marked という markdown parser が紹介されていたのをきっかけに、また [rhysd/Shiba](https://github.com/rhysd/Shiba) を拝見して、自分用の Markdown Viewer を作ってみたいと思いました。

## ソースファイル

以下で公開しています。  
https://github.com/ma-tu/markcat

## 利用方法

GitHub の [Release](https://github.com/ma-tu/markcat/releases) にコンパイル済みのファイルを配置しているので、環境に合わせた zip ファイルをダウンロードして、適当なフォルダに解凍後 markcat を実行するだけです。
詳細は [こちらの README](https://github.com/ma-tu/markcat) を参照ください。

## 知見

#### LiveReload を利用した開発

LiveReload は JavaScriptファイルの変更を監視して、変更があったら自動反映を行ってくれる機能になります。  
Electron でもこれを利用して開発することが可能です。  
いやむしろこれ利用しないと開発効率が悪いです。

[electon-connect](https://www.npmjs.com/package/electron-connect) を利用して行います。

以下の task が LiveRelad に関する設定となります
* ```electron.start()```で electronを 起動します。
* ```gulp.watch('src/app.js', ['serve:app-js'])``` は src/app.js を監視して、変更されると 'serve:app-js' のタスクを実行します。結果 dist/app.js にファイルを出力します。
* ```gulp.watch('dist/app.js', electron.restart)``` は dist/app.js の変更を監視して、変更されると electron をリスタートします。
* ```gulp.watch('src/**/*.{js,jsx,ts,tsx}', ['serve:compile']);```は src以下の js,jsx,ts,tsx ファイルを監視して、変更されると 'server:compile' のタスクを実行します。結果 dist以下に jsファイルを出力します。
* ```gulp.watch(['dist/**/*.html', 'dist/renderer/**/*.js', 'dist/services/**/*.js', 'dist/**/*.css'], electron.reload);``` は　dist以下の各ファイルを監視して、変更されると electron をリロードします。

```gulpfile:gulpfile
var watch = require('gulp-watch');
var electronServer = require('electron-connect').server;

gulp.task('serve:wait', function (done) {
  var electron = electronServer.create();
  electron.start();

  gulp.watch('src/**/*.html', ['serve:html']);
  gulp.watch('src/app.js', ['serve:app-js']);
  gulp.watch('src/**/*.{js,jsx,ts,tsx}', ['serve:compile']);
  gulp.watch('src/css/*.css', ['concat:css']);

  gulp.watch('dist/app.js', electron.restart);
  gulp.watch(['dist/**/*.html', 'dist/renderer/**/*.js', 'dist/services/**/*.js', 'dist/**/*.css'], electron.reload);
  done;
});
```

electron-connectを利用するために、electronで表示する html に以下の記述を追加します。

```html
<!-- build:remove-->
<script>require('electron-connect').client.create()</script>
<!-- endbuild -->
```

ただし  electron-connect を利用した LiveReload は最終的なビルド時には不要となるので、ビルド時のみ [gulp-useref](https://www.npmjs.com/package/gulp-useref) を実行するようにして上記記述が削除されるようにしています。

electron-connect については以下が参考になります

* [electon-connect](https://www.npmjs.com/package/electron-connect)
* [ぼくのかんがえたさいきょうのElectron](http://qiita.com/Quramy/items/90d61ff37ca1b95a7f6d)

#### build時の node_modules の最小化

作業フォルダをそのまま build対象にすると、実行時には不要な devDependencies に指定されているライブラリについての node_modulesフォルダまでもが対象に含まれてしまいます。  
結果ビルド時間が長くなり、ビルド成果物も無駄に巨大なサイズになってしまいます。

Browserify を利用すれば良いのかと思いましたが、RendererProcess側 で require('remote') などを利用している場合はうまくいかないようです。  
うまく Browserify をする方法がこちらに記述されていたのですが [ぼくのかんがえたさいきょうのElectron](http://qiita.com/Quramy/items/90d61ff37ca1b95a7f6d) 難しそうだったの別のアプローチで対応しました。

* ビルド対象は distフォルダ以下とする
* 実行時に必要な dependencies だけ記述した package.json を用意する。
* ビルド時には、上記の package.json　を利用して　npm install を自動で実行し、ビルドするように定義する。

以下で実行します。

* ```build:package-json``` タスクが　src/package/package.json ファイル（ビルド用に実行に必要な dependencies だけ指定したファイル）を dist フォルダにコピーします。
* ```build:install-dependencies``` タスクが dist/package-json ファイルを利用して　gulp-install にて distフォルダに　npm install を実行します。

```gulpfile:gulpfile
var install = require('gulp-install')

gulp.task('build:package-json', function() {
  return gulp.src(srcDir + '/package/package.json')
    .pipe(gulp.dest(distDir))
    ;
});

gulp.task('build:install-dependencies', ['build:package-json'], function() {
  return gulp.src(distDir + '/package.json')
    .pipe(gulp.dest('dist/'))
    .pipe(install({production: true}))
    ;
});
```

#### run-sequence

Gulp は基本的には タスクは並列で実行します。依存タスクを指定することにより直列に実行するように指定することが可能ですが、直感的でなく少し面倒です。  
もっと簡単に直列/並列化したいという場合には [run-sequence](https://www.npmjs.com/package/run-sequence) を利用します。  
直列にしたいタスクを順に書いていき、並列化したいタスクは配列にします。

下記の例の場合は　clean (distフォルダ削除)が完了してから、'build:html','build:comile' 等を並列で実行し、すべて完了したら ```'build:package:darwin'``` でビルド成果物の作成を行います。

利用の注意点が１つあって、run-sequence で扱うタスクは　return で終了しなければならないようです。[gulp の run-sequence を使うときは return を忘れずに](http://qiita.com/7pepper/items/deaecc7e8ca79fe57dd4) が詳しいです。

```gulpfile:gulpfile
var runSequence = require('run-sequence');

gulp.task('build:mac', function(callback) {
  runSequence('clean',
              ['build:html', 'build:app-js', 'concat:css', 'build:compile', 'build:install-dependencies'],
              'build:package:darwin',
              callback);
});
```

#### 小ネタ

##### ビルド時等のフォルダの削除およびコピー

del を利用しています。ビルド時の distフォルダの削除に利用しています。

[Gulp 始めてみましたのメモ – 5 – コピーと削除, ファイル・ディレクトリ](http://www.inazumatv.com/contents/archives/9859) が参考になります。

##### css の結合

gulp-concat を利用して単純に結合するだけです。

## gulpファイル全体

```gulpfile:gulpfile

var gulp = require('gulp');
var watch = require('gulp-watch');
var plumber = require('gulp-plumber');
var install = require('gulp-install')
var useref = require('gulp-useref')
var ts = require('gulp-typescript');
var babel = require('gulp-babel');
var concat = require('gulp-concat');
var del = require('del');
var runSequence = require('run-sequence');
var packager = require('electron-packager');
var electronServer = require('electron-connect').server;

var srcDir      = 'src';
var distDir     = 'dist';
var releaseDir  = 'release';

gulp.task('clean:dist', function (done) {
  return del(distDir);
});

gulp.task('clean:release', function (done) {
  return del(releaseDir);
});

gulp.task('clean', ['clean:dist', 'clean:release']);

gulp.task('concat:css:thema-normal', function () {
  return gulp.src([
    srcDir + "/renderer/css/markcat.css",
    srcDir + '/renderer/css/markcat-normal.css',
    'node_modules/github-markdown-css/github-markdown.css',
    'node_modules/highlight.js/styles/github.css'])
    .pipe(concat('thema-normal.css'))
    .pipe(gulp.dest(distDir + "/renderer/css"));
});

gulp.task('concat:css:thema-dark', function () {
  return gulp.src([
    srcDir + "/renderer/css/markcat.css",
    srcDir + '/renderer/css/markcat-dark.css',
    srcDir + '/renderer/css/github-markdown-dark.css',
    srcDir + '/renderer/css/github-dark.css'])
    .pipe(concat('thema-dark.css'))
    .pipe(gulp.dest(distDir + "/renderer/css"));
});

gulp.task('concat:css', ['concat:css:thema-normal', 'concat:css:thema-dark']);

gulp.task('serve:html', function() {
  return gulp.src(srcDir + '/**/*.html')
    .pipe(gulp.dest(distDir))
    ;
});

gulp.task('serve:app-js', function() {
  return gulp.src(srcDir + '/app.js')
    .pipe(gulp.dest(distDir))
    ;
});

gulp.task('serve:compile', function() {
  var tsProject = ts.createProject('tsconfig.json', {});
  return tsProject.src(srcDir + '/**/*.{js,jsx,ts,tsx}')
    .pipe(ts(tsProject))
    .pipe(plumber())
    .pipe(babel())
    .pipe(gulp.dest(distDir))
    ;
});

gulp.task('serve:wait', function (done) {
  var electron = electronServer.create();
  electron.start();

  gulp.watch(srcDir + '/**/*.html', ['serve:html']);
  gulp.watch(srcDir + '/app.js', ['serve:app-js']);
  gulp.watch(srcDir + '/**/*.{js,jsx,ts,tsx}', ['serve:compile']);
  gulp.watch(srcDir + '/css/*.css', ['concat:css']);

  gulp.watch(distDir + '/app.js', electron.restart);
  gulp.watch([distDir + '/**/*.html', distDir + '/renderer/**/*.js', distDir + '/services/**/*.js', distDir + '/**/*.css'], electron.reload);
  done;
});

gulp.task('serve', function(callback) {
  runSequence('clean:dist',
              ['serve:html', 'serve:app-js', 'concat:css', 'serve:compile'],
              'serve:wait',
              callback);
});

gulp.task('build:html', function() {
  return gulp.src(srcDir + '/**/*.html')
    .pipe(useref())
    .pipe(gulp.dest(distDir))
    ;
});

gulp.task('build:app-js', function() {
  return gulp.src(srcDir + '/app.js')
    .pipe(useref())
    .pipe(gulp.dest(distDir))
    ;
});

gulp.task('build:compile', function() {
  var tsProject = ts.createProject('tsconfig.json', {});
  return tsProject.src(srcDir + '/**/*.{js,jsx,ts,tsx}')
    .pipe(ts(tsProject))
    .pipe(babel())
    .pipe(gulp.dest(distDir))
    ;
});

gulp.task('build:package-json', function() {
  return gulp.src(srcDir + '/package/package.json')
    .pipe(gulp.dest(distDir))
    ;
});

gulp.task('build:install-dependencies', ['build:package-json'], function() {
  return gulp.src(distDir + '/package.json')
    .pipe(gulp.dest('dist/'))
    .pipe(install({production: true}))
    ;
});

gulp.task('build:package:win', function (done) {
  return packager({
    dir: distDir,
    name: 'MarkCat',
    arch: 'x64',
    platform: 'win32',
    out: releaseDir + '/win32-x64',
    version: '0.36.4',
    icon: 'resource/markcat-win.ico',
    overwrite: true
  }, function (err, path) {
    done();
  });
});


gulp.task('build:package:darwin', function (done) {
  return packager({
    dir: distDir,
    name: 'MarkCat',
    arch: 'x64',
    platform: 'darwin',
    out: releaseDir + '/darwin',
    version: '0.36.4',
    icon: 'resource/markcat-mac.icns',
    overwrite: true
  }, function (err, path) {
    done();
  });
});

gulp.task('build:win', function(callback) {
  runSequence('clean',
              ['build:html', 'build:app-js', 'concat:css', 'build:compile', 'build:install-dependencies'],
              'build:package:win',
              callback);
});

gulp.task('build:mac', function(callback) {
  runSequence('clean',
              ['build:html', 'build:app-js', 'concat:css', 'build:compile', 'build:install-dependencies'],
              'build:package:darwin',
              callback);
});

```
##　終わりに

いかがだったでしょうか？
同じところでハマっている人に、一人でもいいので参考になると幸いなのですが。。。  
できましたら MarkCat 使ってみてください。  
または、もっとすごい Markdown Viewer を作成して教えていただけると幸いです。