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 種類のコンテキストをもつ。下記の資料ではそれぞれ window
、node
(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.document
と window.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
になる。その場合、スクリプトの実パスが長いと非常に見辛くなる。
あとなぜかデバッガ上でスクリプト内のマルチバイト文字が化ける。UTF-8 で保存していてもダメ。<script>
タグにエンコードを指定しても回避できず。Source Maps だと化けないから単にデバッガの問題な気がする。
今回の調査で知った window.require
で require
競合も回避できるし reactify や Source Map 利用も考慮すると、やはり Browserify のほうがよいと思った。
サンプル
今回の調査で作成したサンプルを以下に公開する。
clone して README.md の Installation & Build に書かれた手順を実行するとアプリが生成される。