[Titanium勉強日記 6] CommonJSモジュールとrequireについて調べた

学校が始まったのと、触っていくにつれてTitaniumが銀の弾丸ではない事が分かってきて、ペースダウンしてしまっているのですが、一応継続してTitaniumの習得を試みています。

表題の件なのですが、Titaniumの1.8以降ではシングルコンテキストなコーディング作法というのが推奨されています。その作法ではrequire関数でモジュールをロードします。まぁrequireを使っている分にはあまり疑問も無かったのですが、先日試してみた軽量ORMのjoliに含まれていた以下のコードを見て疑問が湧いてきました。

/**
 * In case joli.js is loaded as a CommonJS module
 * var joli = require('joli').connect('your_database_name');
 * var joli = require('joli').connect('your_database_name', '/path/to/database.sqlite');
 */
if (typeof exports === 'object' && exports) {
    exports.connect = function(database, file) {
        var joli = joliCreator();

        if (database) {
            if (file) {
                joli.connection = new joli.Connection(database, file);
            } else {
                joli.connection = new joli.Connection(database);
            }
        }

        return joli;
    };
}

CommonJSモジュールって何やっけ?とか、そもそもexportsって何なん?とかexportsとmodule.exportsって何が違うん?という事ですね。

まずCommonJSですが、これは最近JSがブラウザ飛び出してサーバになったりアプリになったりと、いろんな処理系が出てきててるから、モジュール機構やらライブラリやらテストやらの処理系横断的な標準仕様を作ろうという動きのようです。その仕様に沿ったモジュールがCommonJSモジュールということですね。オフィシャルのWikiに仕様があります。

仕様中のサンプルは以下のようなコードになっています。exportsオブジェクトに好きな関数を刺しておいて、requireで必要な関数にアクセスする形になります。

// math.js
exports.add = function() {
    var sum = 0, i = 0, args = arguments, l = args.length;
    while (i < l) {
        sum += args[i++];
    }
    return sum;
};

// increment.js
var add = require('math').add;
exports.increment = function(val) {
    return add(val, 1);
};

// programs.js
var inc = require('increment').increment;
var a = 1;
inc(a); // 2

あとTitaniumでは各画面をモジュールにしておくのが推奨されていて、この場合はmodule.exportsにコンストラクタにあたる関数を代入しておくようです。

// HogeWindow.js
function HogeWindow() {
	var self = Ti.UI.createWindow(opts);
	// construct window
	...
	
	return self;
};

module.exports = HogeWindow;

// myapp.js
var HogeWindow = require('HogeWindow');
var win = new HogeWindow();
win.open();

ではこのexportsとかmodule.exportsとは一体何なんなのでしょうか。いまいちピンとこないので、このモジュール機構を実装しているnodejsのコードを読んでみました。(Titaniumより実験しやすかったので。)実際の処理はsrc/module.jsに実装されています。ざっくりとコードを抜き出して処理の流れが見えるようにしたものが以下になります。(実際のコードはもっと細切れな関数に分かれています。) contentがrequireされたモジュールの内容です。wrapで元のコードを無名関数としてラッピングして実行し、その後module.exportsを返却しています。

Module.prototype._compile = function(content, filename) {
  ...
  var wrapper = Module.wrap(content);

  var compiledWrapper = runInThisContext(wrapper, filename, true);
  ...
  var args = [self.exports, require, self, filename, dirname];
  return compiledWrapper.apply(self.exports, args);
};

Module.prototype.require = function(...) {
  ...
  var module = new Module(filename, parent);
  var content = NativeModule.require('fs').readFileSync(filename, 'utf8');
  module._compile(stripBOM(content), filename);
  return module.exports;
};

ちなみにModuleオブジェクトはコンストラクタを見るとイメージがつかめると思います。

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  if (parent && parent.children) {
    parent.children.push(this);
  }

  this.filename = null;
  this.loaded = false;
  this.children = [];
}

先ほどのmath.jsを例に取ってもう少し具体的に見てみます。要は以下のJSのコードが評価された後に、self.exportsが返却されるという事です。

(function (exports, require, module, __filename, __dirname) {
    exports.add = function() {
        var sum = 0, i = 0, args = arguments, l = args.length;
        while (i < l) {
            sum += args[i++];
        }
        return sum;
    };
})(self.exports, require, self, filename, dirname);

Moduleオブジェクトを見れば分かる通りexportsは特別な仕組みでも何でもなくて、ただのオブジェクトです。またexportsとmodule.exportsの違いは何かなと不思議に思っていましたが、コードを見てみると全く同じオブジェクトを参照しています。requireでオブジェクトではなくて関数(コンストラクタ)を返したい場合に、分かりやすいようにという事だと思います。

ここまでを確認するのに以下のような実験コードを書いてみました。

// mymod.js
console.log(module.exports);
console.log(exports);

module.exports['key'] = 'val';
console.log(module.exports);
console.log(exports);

console.log(__filename);
console.log(__dirname);

// prog.js
var mymod = require('./mymod');

prog.jsをnodeコマンドで実行すると、結果は以下のようになります。exportsとmodule.exportsが同じオブジェクトを参照している事が確認できます。また__filenameや__dirnameから、それぞれファイル名やディレクトリを取得できています。

{}
{}
{ key: 'val' }
{ key: 'val' }
/Users/taichino/Documents/test/js_test/node_test/require_test/mymod.js
/Users/taichino/Documents/test/js_test/node_test/require_test

ここまでで冒頭のjoliのコードも理解できますね。require中で評価される場合はexportsオブジェクトがundefinedでなくなるので、exportsオブジェクトに必要な処理を刺してCommonJSモジュールにしているという事でした。

以上で、冒頭の疑問が晴れたので引き続きアプリを作っていきたいと思います。

参考ページ

Node.js : exports と module.exports の違い(解説編)

Leave a Reply

Your email address will not be published. Required fields are marked *