アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

node-webkit を使ってみる 3 - 簡易ファイラー

node-webkit を使ってみるシリーズその 3。前回の最後に予告した簡易ファイラーのサンプル作成で得られた知見を記録しておく。サンプルに実装した機能は以下。

  • フォルダのツリー表示 (ルートは USER HOME)
  • ファイル・フォルダ情報の表示
  • ファイルのダブル クリックで関連付いているアプリを起動

簡易ファイラー

削除やリネームのようにファイル自体を変更するものは割愛。この仕様ならファイラーではなくビューアーのほうが妥当かも。

  • 2015/1/12 23:22 追記

    • React.js 自体の読み込みとプロジェクト構成の説明を書き忘れていたので追記

node-webkit-builder

シリーズ 1、2 で node-webkit アプリのプロジェクト作成とビルドに Nuwk! を紹介したが、その後 Web フロント エンド開発で gulp を利用するようになってから node-webkit アプリのビルドもこれで管理したくなった。

そこで node-webkit 公式の How to package and distribute your apps にも紹介されている node-webkit-builder (長いので以降は nwb と呼ぶ) を採用。これは npm で配布されており gulp タスクから利用できる。

今回のサンプルだと以下のようにビルドしている。

var gulp = require( 'gulp' );
var $    = require( 'gulp-load-plugins' )();

/**
 * node-webkit イメージを生成します。
 *
 * @return {Object} gulp ストリーム。
 */
gulp.task( 'release', [ 'copy' ], function () {
    var builder = require( 'node-webkit-builder' );

    var nw = new builder( {
        version: '0.11.5',
        files: [ 'release/src/**' ],
        buildDir: 'release/bin',
        cacheDir: 'release/nw',
        platforms: [ 'osx' ]
    });

    nw.on( 'log', function( message ) {
        $.util.log( 'node-webkit-builder', message );
    } );

    return nw.build().catch( function( err ) {
        $.util.log( 'node-webkit-builder', err );
    } );
} );

nwb の version オプション指定により対応する node-webit 本体をダウンロード & キャッシュしてくれる。versionplatforms を変更してもビルド毎にキャッシュ済みか否かを判定。キャッシュされていなければ不足分をダウンロードしてくれるという親切設計。

Nuwk! は現時点で OS X のみ対応となっているが nwb なら node-webkit 本体のサポート環境すべてを対象にできる。一部機能 (ico ファイルの利用に Windows もしくは Wine が必要) をのぞきビルドもクロス プラットフォームで実行できる。

ただし Issue #147 で報告されている現象 (処理は正常なのにエラーが表示される) とか、私の環境だと macIcns を指定した時にファイルのコピーでエラーが起きるなど心配な点もある。とはいえ非常に便利。現時点で node-webkit アプリをビルドするなら最良の選択肢ではなかろうか。

React.js

UI 部分は React.js で実装した。フォルダ ツリーは この間の記事で実装したものを踏襲。コンポーネント関連は JSX で実装。コンパイルは reactify 任せ。

コンポーネント構成は以下のようになる。

  • Explorer

    • FolderTree
    • FolderDetail

Explorer 内にフォルダ ツリーとフォルダ詳細コンポーネントが入れ子になっている。もしコンポーネント間でやりとりする場合は Explorer を経由する設計。現在はフォルダ ツリーのクリック時に選択フォルダの変更とファイル、フォルダ情報の再描画を実施している。実装の詳細については記事の末尾にリンクしたサンプル プロジェクトを参照のこと。

残念ながらこのサンプルにはユニット テストがない。React.js の場合 Jest がよく利用されているようなので別途調査したい。もしかすると次項に書く require の問題がユニット テストに影響するかもしれない。

余談だが今回のサンプルではコンポーネント定義をモジュール外に公開するとき

var Component = React.createClass( {
} );

module.exports = Component;

のようにしているが、これは

module.exports = React.createClass( {
} );

にしたほうが簡潔でよいのかも。require する側で Reacr.render する設計なら元側で名前をつける意義は薄い。

Browserify と require

node-webkit の require は node.js 標準ライブラリとプロジェクト内の node_modules に配置したモジュールを読み込んでくれる。

では自作スクリプトは?というと zcbenz/nw-sample-apps の file-explorer みたいに node_modules 直下に置くとか javascript - node-webkit loading module fails - Stack Overflow のようにプロセス実行時のパスと結合してフルパス指定するようだ。

var path  = require( 'path' );
var mymod = require( path.join( process.cwd(),'js/mymodule.js' ) );

前者はモジュール衝突とか思わぬ事故を招きそうだし基本的に開発環境で npm install をトリガーとして動的生成するものだから .gitignore でリポジトリからも除外しているだろう。よってここに自作スクリプトは置きたくない。後者は記述が長くて面倒。もしこれを採用するとしてら自作スクリプト用 require として myrequire みたいなユーテリティ関数を用意してグローバルに読み込んでおくとかしたい。

require を利用しない場合、HTML の script タグを利用するのだけどスクリプトの増減や順番管理が面倒である。スクリプト間に依存をスクリプト外に定義すると見通しも悪くなる。というわけで require のために Web フロントエンド開発で利用している Browserify を採用してみた。JSX のコンパイルに reactify も利用できるしこれはよさそうだと思ったのだが、普通に使うと node-webkit の require と競合する。

そのため node-webkitでもbrowserify使いたいしnodeのrequireも使いたい - Qiita で解説されているようにグローバルとビルトインの require 書き変えを無効化しなければならない。これで npm の require は OK なのだが今度は node-webkit モジュールの読み込みが失敗する。例えば nw.guirequire したとき Error: Cannot find module 'nw.gui' となる。

  • 2015/1/21 補足 nw.js のモジュールを読み込むとき require ではなく window.require を利用すれば以下の処理が不要になる。この問題は node コンテキストから nw.js モジュールを参照できないことが原因である。次回の記事でこの辺の話を取り上げている。

結局、自作と node.js/node-webkit 系の require を分けて呼ぶ必要がある。前述の Qiita エントリにも引用されてる Support for node-webkit Issue #481 substack/node-browserify にも解説されているとおり

  • 自作スクリプトは require、node.js/node-webkit モジュールは nequire で読む

    • require 以外の名前なら nequire でなくてもよい
    • require に近いスペルのほうが分かりやすく既存関数とも競合しにくいだろう
  • Browserify 処理の際、requirenequire を置換する

    • 自作スクリプト用の requirerequireClient に置換
    • node.js/node-webkit 用の nequirerequire に置換
  • Browserify による結合実行

    • Browserify による自作スクリプトの依存解決は requireClient で実施
    • node.js/node-webkit 用の依存解決は require になる

これで本来の require はそのままに自作スクリプトの依存解決を別関数によって代替できる。gulp タスクだと以下のようになる。

var gulp = require( 'gulp' );
var $    = require( 'gulp-load-plugins' )();

/**
 * JavaScript と JSX ファイルをコンパイルした単一ファイルを開発フォルダに出力します。
 *
 * @return {Object} gulp ストリーム。
 */
gulp.task( 'js', function() {
    var browserify = require( 'browserify' );
    var source     = require( 'vinyl-source-stream' );
    var buffer     = require( 'vinyl-buffer' );

    return browserify(
            './src/js/main.js',
            {
                debug: true,
                detectGlobals: false,
                builtins: [],
                transform: [ 'reactify' ]
            }
        )
        .bundle()
        .pipe( source( 'app.js' ) )
        .pipe( buffer() )
        .pipe( $.sourcemaps.init( { loadMaps: true } ) )
        .pipe( $.replace( 'require', 'requireClient' ) )
        .pipe( $.replace( 'nequire', 'require' ) )
        .pipe( $.sourcemaps.write( './' ) )
        .pipe( gulp.dest( 'src/js' ) );
} );

Source Maps と併用する場合、sourcemapsinitwrite 間で置換を指定すること。

Browserify を利用しないなら process.cwd を利用しつつ JSX コンパイルは gulp-react で実施する。この路線も検討したけど管理が面倒なので Browserify を選んだ。

Bower でインストールしたライブラリの参照

今回のサンプルでは React.js を Bower でインストールしたのだが、これは require ではなく HTML 側の script タグで読み込んでいる。

<!-- build:js js/lib.js -->
<script src="./bower_components/react/react.min.js"></script>
<!-- endbuild -->

Browserify を利用するのであれば debowerify もセットにして require するところだが前述の require 競合をややこしくしそうなのでやめた。代わりに汎用ライブラリ系は script タグでグローバルに読み gulp-useref で単一の lib.js 化するようにしている。

こうしたライブラリは多用されるので都度 require するのが面倒というのもある。しかし依存明示や参照範囲の局所化などの観点から好ましくないので debowerify を利用しないにしても、gulp-replace による置換を導入した時点でクライアント require 化すべきだったかも。

あと今回の require 対策をすべて実行したならば debowerify を利用できたかもしれない。この辺、試行錯誤中。

プロジェクト構成

今回のプロジェクト構成 README.md などは除く) は以下のようになる。

/
├ package.json
├ gulpfile.js
├ node_modules/
│  └ ビルド用 node.js パッケージ
├ release/
│  ├ bin/
│  │  └ リリース用 node-webkit アプリ バイナリ
│  ├ nw/
│  │  └ リリース ビルド用 node-webkit 本体バイナリ
│  └ src/
│     └ リリース用 src
└ src/
   ├ index.html
   ├ css
   │  └ スタイルシート
   ├ js
   │  └ JavaScript、JSX とそのコンパイル結果の出力先
   └ fonts
      └ アイコン フォント

リリース用ビルドに関するものは release に集約。これは gulp release を実行した時に構築される。

開発リソースは /src 内へ格納。gulp js で JavaScript & JSX コンパイルが実施され /src/js 内に app.js が出力される。node-webkit に src を読み込んだ状態でソース編集 & コンパイル、アプリのツール バーでリロードという流れで実装を進めていた。

node-webkit は How to run appsi の提案どおり .bash_profilenw というエイリアスを設定している。よって Terminal から src を読み込ませる場合はプロジェクトのルートに cd してから、

$ nw src

とするだけ。node-webkit にコマンドラインからリロードを送信できればもっと効率化できる (Web 開発でブラウザを更新するようなイメージ) のだけど可能なのだろうか?

サンプル

今回作成したサンプルを以下に公開する。

clone して README.md の Installation & Build に書かれた手順を実行するとアプリが生成される。