アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

nw.js を使ってみる 4 - 標準 require によるモジュール読み込み

nw.js を使ってみるシリーズその 4。node-webkit v.0.12.0 でプロダクト名が nw.js になったので、シリーズ名もあわせて変更しておく。

前回の記事で作成したサンプルでは JavaScript のモジュール参照と React.js の JSX コンパイルに Browserify & reactify を利用してみた。これを標準の require で実装する場合はどうなるのだろう?という疑問がわいたので試してみる。

require とコンテキスト

nw.js 上で動作するスクリプトは 2 種類のコンテキストをもつ。下記の資料ではそれぞれ windownode (Node.js) と解説されているようなので本記事でもそう呼ぶことにする。

コンテキストはスクリプトの読み込み方法によって変化。HTML 上の <script> タグからだと window、スクリプト内の require なら node になる。

window は nw.js と node.js、そして通常の HTML における JavaScript と同じく自身より前に <script> タグで読まれたスクリプトを利用できる。要するに万能。Browserify で単一スクリプトを生成して <script> タグで読むとこの状態になるため、個々のスクリプトでコンテキストを意識しなくて済む。

node では利用可能な機能が Node.js モジュールに限定される。具体的には以下。

  • nw.js 標準モジュール、fs など
  • node_modules 配下のモジュール
  • 自作モジュール、require にパス指定が必要

コンテキストが window でもそこから require されたものは node になる。よって nw.js の GUI や <script> タグで読み込んだ機能、window.document などの DOM を利用するならブラウザのコンテキストから受け渡す必要がある。

特例として require の代わりに window.require を利用すると Node.js だけでなく nw.js モジュールも参照可能。例えばファイルをシェルで関連付けられたアプリで開く処理の場合、以下のようにする。

/**
 * 指定されたファイルをシェルで開きます。
 *
 * @param {String} path ファイルのパス。
 */
function shellOpenItem( path ) {
    var gui = window.require( 'nw.gui' );
    gui.Shell.openItem( path );
}

前回のサンプルでは Browserify と nw.js の require 競合を回避するため nw.js 側の require をリネームしてビルド時に戻していたが、window.require を利用すればこの処理は不要になる。Browserify で依存管理するものは require、nw.js と node.js モジュールは window.require で読みこめばよい (動作確認済み)。

とはいえ DOM や <script> タグで読み込んだ機能を利用できないことは変わりない。次項ではこの問題への対策を検討する。

node コンテキスト対応

window 固有機能を node コンテキストに受け渡す方法を考える。

引数による受け渡し

まずは正攻法。<script> タグで読み込んだ React.js と DOM の window.document を利用する場合、node コンテキスト側の module.exports を以下のように定義しておく。

module.exports = function( React, document ) {
    var Explorer = React.createClass( {
        // React コンポーネント定義
    } );

    React.render(
        React.createElement( Explorer, null ),
        document.querySelector( '.l-content .explorer' )
    );
};

window コンテキストから呼ぶときに必要な機能を渡す。

var explorer = require( './js/jsx/explorer' );
explorer( React, document );

モジュール数が増えたり node コンテキストのモジュール間で window コンテキスト依存が多いと管理が面倒かもしれない。しかしそれが依存を少なく保つための抑止力になるとも考えられる。面倒になったら依存をまとめた単一 Object にして取り回す手もある。

グローバル定義

引数による受け渡しが面倒とかグローバルな DOM に依存する node モジュールがあるけどサードパーティ製で手を加えられない、といった場合はグローバル定義を利用する。window 側で以下のように定義すると node コンテキストからもグローバルに参照できる。

global.document  = window.document;
global.navigator = window.navigator;

例えば React.js は window.documentwindow.navigator のグローバル定義に依存している。そのため npm でインストールしたものを require して node コンテキストになった場合、依存を解決できずエラーになる。よってこれらの DOM を事前に global へ割り当てておくことで対応。

この記事用に作成したサンプルではそのようにしている。しかし暗黙の DOM 依存があるなら window コンテキストに読むべきで、それを node に渡すほうがグローバル汚染を避けられるため好ましい。

前回のサンプルを標準 require で実装してみる

ここまでの内容を踏まえ、前回のサンプルを標準 require で実装してみた。

Browserify & reactify は利用しないため JSX を自前でコンパイルする必要がある。これは gulp-react で代替可能。ただし reactify と異なりスクリプトごとの *.js ファイルが出力される。

このままだとコンパイル結果が git リポジトリに含まれてしまい好ましくない。そこで src/js/jsx フォルダを用意して JSX ファイルはそこへまとめた。こうしておけば .gitignore に src/js/jsx/*.js を指定してコンパイル結果を除外できる。

以下は gulp タスク例。

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

gulp.task( 'js', function() {
    return gulp.src( 'src/js/jsx/*.jsx' )
        .pipe( $.react() )
        .pipe( gulp.dest( 'src/js/jsx' ) );
} );

window コンテキストから require する場合、モジュールのパス指定は自身から相対ではなく nw.js アプリのルートからとなるので注意する。例えば src がルート、src/index.html がアプリのエントリー ポイントだとして、そこから以下のようにスクリプトを読み込むなら

<script src="js/main.js"></script>

main.js は window コンテキストになるが、そこから src/js/jsx 内のスクリプトを読む処理は、

var explorer = require( './js/jsx/explorer' );

となる。はじめ './jsx/explorer' と指定して読み込めずにハマった。それとモジュールにファイル拡張子は不要。node コンテキストなモジュール間の require は自身からの相対パスで指定する。以下は node_modules に配置した React.js と自作スクリプトの reqire を混在させた例。

var React        = require( 'react' );
var FolderTree   = require( './folder-tree' );
var FolderDetail = require( './folder-detail' );

nw.js モジュールを利用したい場合は前述のように window.require を利用すればよい。

標準 require を試してみて Browserify がなくてもよいのでは?と思ったのだが、デバッグで困った。nw.js というか WebKit のデバッガだと node コンテキストのスクリプトが Sources で no domain になる。その場合、スクリプトの実パスが長いと非常に見辛くなる。

デバッガの Sources 表示

あとなぜかデバッガ上でスクリプト内のマルチバイト文字が化ける。UTF-8 で保存していてもダメ。<script> タグにエンコードを指定しても回避できず。Source Maps だと化けないから単にデバッガの問題な気がする。

今回の調査で知ったwindow.requirerequire 競合も回避できるし reactify や Source Map 利用も考慮すると、やはり Browserify のほうがよいと思った。

サンプル

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

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