こんにちはみなさん
Javascript。。。というか、nodejsのモジュールの読み込みって、requireとかimportとか使うのですが、どうにもしっくりこない。
これは、nodejsが悪いというわけではなく、おそらく私の脳にPHPの使用感がこびりついているからだと思います。
オートロード使うと感覚的に書けるので、requireとかでいちいちファイルシステムに頭を切り替えなくて済むのですよ。
というわけで、おそらく99%の人が「何言ってんだこいつ」状態になることを承知で、nodejsにPHPチックなオートロードを導入してみましょう。
端的にいうとnpmのパッケージ作りました。
これね
https://www.npmjs.com/package/ns-autoloader
インストールは簡単で、
npm install ns-autoloader
これでおわり。
例えば、以下のようなディレクトリ構成をしていたとします。
.
├── app
│ ├── other
│ │ └── thing.js
│ └── some
│ └── thing.js
├── index.js
ここで、
class thing {
greet() {
return 'Hello World!!'
}
}
module.exports = thing
class thing extends app.some.thing{
greetBye() {
return "Goog Bye!!"
}
}
module.exports = thing
と定義しておいて、
// オートロード登録
const autoloader = require('ns-autoloader')
autoloader.registerApps(__dirname + '/app', 'app');
// オブジェクトの呼び出し
const thing = new app.some.thing()
$greet = thing.greet()
console.log($greet)
// 継承付きクラスの呼び出し
const other = new app.other.thing()
console.log(other.greet())
console.log(other.greetBye())
としてやると、
$ node index.js
Hello World!!
Hello World!!
Goog Bye!!
ってな感じになります。
くどくなりますが、ちょっと順を追いますと、
- ns-autoloaderモジュールを呼び出す
- app.some.thing にアクセスすると、app/some/thing.jsがrequireされる
- さらにapp.some.thingがコンストラクタ呼び出しされているため、変数thingに生成されたthingオブジェクトが格納される
- thing.greetでthingに定義されたgreetメソッドを呼んでいる
以降は、例によってダラダラと目的や技術内容を書いていきます。
目的:PHPのオートロードをJavascriptでも使いたい
郷に入れば郷に従えとは言いますが、メインの言語がPHPなので、なるべくそれっぽい挙動ができたほうが、心情的に楽になります。PHPの中でもお気に入りなのがオートロードの仕組みです。
オートロードは、名前空間付きのフルパスやそのエイリアスでクラスや関数が呼ばれたとき、まだrequireされていないものであれば、名前空間に特定のルールを適用して自動でrequireするという機構です。
PHPの依存性管理ツールcomposerを使っていると、オートローダを自動生成してくれるため、composer.json中で
"autoload": {
"psr-4": {
"App\\": "app/"
}
},
と書いてあった場合、オートローダが設定されると、Appディレクトリ配下を名前空間appで定義し、PSR-4に従ってオートロードできるようになります。
これに似たようなことをjavascriptでもできないかと考えました。
PHPでオートロードを使う仕組みは以下のようにすれば大丈夫です。
require 'vendor/autoload.php'
use app\Some\Thing;
$thing = new Thing();// require 'App/Some/Thing.php'
$other = new app\Some\Other();// require 'App\Some\Other.php'
javascriptにはPHP式に名前空間を作る事ができませんが、オブジェクトのネストでこれを代用してみましょう。
const loader = require('ns-autoloader')
loader.registerApps(__dirname + '/app', 'myApp')
const thing = myApp.some.thing
const myThing = new thing()// require('app/some/thing.js')
const other = new myApp.some.other()// require('app/some/other.js')
こんなふうに作れればいいということで、それを目的にしました。
実装の解説
本体はこのファイルだけ
https://github.com/niisan-tokyo/autoload-js/blob/79c84e91ae12e9eccd36f6e6adbfa9ce09ca005f/index.js
若干トリッキーな実装をしている部分があるので、それについて簡単に解説していきます。
オブジェクトのメンバへのアクセスは2種類ある
オブジェクトのメンバへはドット記法でのアクセス方法と連想配列チックなアクセス方法の二つがあります。
const assert = require('assert')
let myObj = {}
myObj.hoge = 1
assert(myObj.hoge == myObj['hoge'])
今回、名前空間はパスを使って定義するので、メンバを動的に定義できなければなりませんので、各メンバの定義は連想配列風の記法を使っています。
オブジェクト変数は参照である
オブジェクトを格納する変数は、実際にはオブジェクト本体ではなく、オブジェクトへの参照を格納しています。
というわけで、以下のように任意要素数の配列に入った文字列を使って、ネストされたオブジェクトを生成することができます。
let obj = {}
let temp = obj
const arr = ['can', 'take', 'that']
arr.forEach(s => {
temp[s] = temp[s] || {}
temp = temp[s]
})
console.log(obj)
この結果、出力は
$ node test.js
{ can: { take: { that: {} } } }
となり、ネストされたオブジェクトを生成することができました。実際、作成されたオートローダの中では、
for (i = 0; i < length; i++) {
if (i != length - 1) {
temp[arr[i]] = temp[arr[i]] || {}
temp = temp[arr[i]]
} else {
setLoader(temp, arr[i], name)
}
}
のようにして、名前空間を構築しています。
動的プロパティの追加と遅延評価
オートローダに読み込むべきモジュールを定義するsetLoader関数を見てみましょう。
function setLoader(nameobj, filename, path) {
classname = filename.slice(0, -3)
Object.defineProperty(nameobj, classname, {get: () => {
if (typeof namespace._pathList[path] == 'undefined') {
namespace._pathList[path] = require(namespace._basePath + path)
}
return namespace._pathList[path]
}})
}
まず、動的に名前空間オブジェクトに対してクラスをプロパティとして追加しています。
これはObject.definePropertyというメソッドで実現できます。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
ここで追加されるプロパティはgetterのみ定義されています。ゲッターの内容は名前空間オブジェクトにまだモジュールが追加されていない場合は、requireで取得しています。
また、一応遅延評価の仕組みも備わっていて、getterが呼び出される、つまり、名前空間で該当のクラスにアクセスされるまで、requireが走りません。
大した性能差ではないですが、全部読み込みまくるよりは品が良いように思います。
グローバルオブジェクトに突っ込む!
グローバルオブジェクトに名前空間オブジェクトを突っ込むことで、あらゆる箇所で名前空間をブジェクとを呼び出すことができます。
グローバル変数を使用することを蛇蝎の如く嫌う人もいますが、用途目的によってはいいんじゃないかと思います。
名前空間はシステム全体で使うものなので、グローバルオブジェクトに入れても問題ないと考えています。
実際、初めの例に書いたように、app/other/thing.jsでもindex.jsでも、app.some.thing
で同じ場所にあるオブジェクトを取得できます。
その他
利点・不利点表裏一体の課題があります
名前空間を明示しなくても良い
PHPだと、各ファイルの中の先頭部分に名前空間を宣言しなくてはなりません。
<?php
namespace App\Some
class Thing {}
今回のns-autoloader中では、名前空間を宣言する必要はありません。。。というよりは定義しても意味がありません。
ファイルパスに従って、勝手に設定されます。
相対パスを使えない
PHPでは自身の名前空間からの相対パスで、クラスをロードできますが、ns-autoloaderは自身の名前空間はファイルの中の時点では不明なので。。。というより仕組みが存在しないため、自身の名前空間を宣言することも、そこから相対パスで別のモジュールにアクセスすることはできません。
これがいいことなのか悪いことなのかいまいち判別はつきにくいところです。
まとめ
こんな感じで、俺得なオートローダを作成してみました。
思いつきで作ってみましたが、javascriptの面白い動きとかもわかって楽しいものでした。
本日はこの辺です