本記事はNode.js Advent Calendar 201518日目の記事です。
atomやbracketsなど、WWWの技術で作られたアプリケーションの中にはエクステンションを入れて拡張できるものがいろいろ存在します。
これらのアプリケーションは、どのようにしてエクステンションの仕組みを実現しているのでしょうか。
bracketsとatom(electron)を例にとって調べてみました。
bracketsはRequireJSベース
bracketsは結論からいうとRequireJSベースのようです。以下詳しく説明します。
エクステンションの作り方
ざっくりですが、まずはエクステンション自身の作り方を紹介します。
main.js
下記は、wikiに示されているHello World!のサンプルです。
エクステンションは、必ずmain.js
に本体を記述します。
/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global define, $, brackets, window */
/** Simple extension that adds a "File > Hello World" menu item */
define(function (require, exports, module) {
"use strict";
var CommandManager = brackets.getModule("command/CommandManager"),
Menus = brackets.getModule("command/Menus");
// Function to run when the menu item is clicked
function handleHelloWorld() {
window.alert("Hello, world!");
}
// First, register a command - a UI-less object associating an id to a handler
var MY_COMMAND_ID = "helloworld.sayhello"; // package-style naming to avoid collisions
CommandManager.register("Hello World", MY_COMMAND_ID, handleHelloWorld);
// Then create a menu item bound to the command
// The label of the menu item is the name we gave the command (see above)
var menu = Menus.getMenu(Menus.AppMenuBar.FILE_MENU);
menu.addMenuItem(MY_COMMAND_ID);
// We could also add a key binding at the same time:
//menu.addMenuItem(MY_COMMAND_ID, "Ctrl-Alt-W");
// (Note: "Ctrl" is automatically mapped to "Cmd" on Mac)
});
これは、ファイルメニューに項目を追加して、それを選択すると「Hello World」のアラートを表示するというものです。
define
でエクステンションを定義しているようです。
エクステンション本体の関数はfunction (require, exports, module)
となっています。
これらの記法はRequireJSのsimplified CommonJS wrapperですね。
グローバル変数には、$
, brackets
, window
が宣言されているようです。
require("text!templates/panel.html")
のようにして用意したテンプレートを読み込んだりできます。
package.json
エクステンションのメタ情報は、package.json
に記述します。
下記はGist Manager
というエクステンションから持ってきました。
{
"name": "fezvrasta.gist-manager",
"title": "Gist Manager",
"description": "Create and view Github Gists within Brackets. (View > Show Gists Manager)",
"keywords": ["gist", "GitHub", "snippets"],
"homepage": "https://github.com/FezVrasta/gist-manager",
"version": "0.1.1",
"author": "Fez Vrasta <info@mywebexpression.com> (http://www.mywebexpression.com)",
"license": "MIT",
"engines": {
"brackets": ">=0.24.0"
}
}
エクステンション読み込み機構
下記のモジュールにエクステンションの読み込み処理が記述されています。
// Read optional requirejs-config.json
var promise = _mergeConfig(extensionConfig).then(function (mergedConfig) {
// Create new RequireJS context and load extension entry point
var extensionRequire = brackets.libRequire.config(mergedConfig),
extensionRequireDeferred = new $.Deferred();
contexts[name] = extensionRequire;
extensionRequire([entryPoint], extensionRequireDeferred.resolve, extensionRequireDeferred.reject);
return extensionRequireDeferred.promise();
}).then(function (module) {
extensionRequire
でエクステンションのエントリポイント(main.js)を読み込んでいます。
extensionRequire
の元になっているbrackets.libRequire
は以下のように定義されています:
// Loading extensions requires creating new require.js contexts, which
// requires access to the global 'require' object that always gets hidden
// by the 'require' in the AMD wrapper. We store this in the brackets
// object here so that the ExtensionLoader doesn't have to have access to
// the global object.
global.brackets.libRequire = global.require;
つまり、エクステンションの読み込み機構はrequire.jsそのものだったということですね。
atom(electron)はNode.jsベース
ブラウザプロセスでも普通にrequireできる!
Electronではなぜかブラウザプロセス上からでも動的にrequire
できます。
例えば以下のコードをグローバルに宣言してみます:
global.loadModule = function (name) {
return require(name);
};
ElectronアプリケーションのDeveloper Toolsのコンソールで以下のコードを実行すると、正しくモジュールがロードできます。
loadModule("util"); // -> Object {}
という事は、パッケージの読み込みやパッケージ内の依存関係の解決もできますね。
まとめ
RequireJSって動的なパスでモジュールを読み込めるんですね。
WebpackやBrowserifyのようにあらかじめコンパイルして依存関係を解決しなくてもいいという点が勉強になりました。
atomはnode.jsと同じ感覚で依存関係を記述できる点がすばらしいです。