Sails.jsはとても便利です。
ただ、View側はSPAを前提とした構造を想定しているためか、複数ページ構成(あるいはSPAとマルチページの複合)の場合はちょっと都合が悪くなる場合があります。
そこで解決策の一つとしてWebPackを用いたページ別に関連するjsモジュールの埋め込みを行う方法を記載します。
Sails.jsのViewに対する既存の動作
Sails.jsの既存の動きでは、assets以下のスクリプトを
layout.jade
などに定義された以下のマーカーのHTMLに対して埋め込みを行います。
<!--SCRIPTS-->
ここに書き込まれる
<!--SCRIPTS END-->
この処理自体はsails lift
などを実行した際に実行され、
内部的にはgruntのタスクとして定義されています。
実体はtasks/register/linkAssets*.js
でタスクとして登録されているtasks/config/sails-linker.js
で行われており、
通常版ではjsをそのまま読み込んだり--prod
の場合にuglifyを用いたMinifyが行われたりします。
Webpackの提供するMULTIPLE ENTRY POINTS
WebPackにはMultipleEntryPointsという機能があり、複数のファイルを複数の結合したjsファイルとして出力する事が可能です。
詳細は省きますが、簡単に言うと例えばA.js
とB.js
がそれぞれ毎に依存した別ファイルに出力する事が可能です。
- A.jsはA.output.jsへ
- B.jsはB.output.jsへ
これを利用して、各ページ毎にそれぞれjsの出力を行います。
SailsのビルドにWebPackビルドとHTMLへの埋め込みを実装する
ここからが本題です。
以下の流れで実装していきます。
- WebPackによるビルドを構築する
- View側にマーカーを設定する
- sails-linkerにWebPack用の埋め込み処理を追加する
- compileAssetsとlinkAssetsに組み込む
基本的な設計と前提条件
複数ページで構築する場合、各画面ごとに1つのJSファイルが必要となることが多いです。
そこで、1つのViewファイルに対して相対的にjsファイルを構築する事を前提とします。
つまり、views/user/detail.jade
の場合、assets/js/views/user/detail.js
が存在している事になるわけです。
# (私はjadeが好きなのでViewはjadeです)
この前提により生成されたJSファイルをEntryPointと見なして、Viewsに対してlinkAssetタスクと同様に埋め込みを行います。
1. WebPackによるビルドを構築する
grunt-webpack
に依存した構築を行うので、事前に以下のようにインストールしておきます。
npm install grunt-webpack
現時点のバージョンは1.0.8
です。
新規にtasks/config/webpack.js
を作成します。これが実際にWebpackのビルドの実体になります。
"use strict";
/**
* Compile WebPack
*
* ---------------------------------------------------------------
*
* WebPackのビルドを実行します。
* 設定は、アプリケーションルートの`webpack.config.js`から読み込まれます。
*/
module.exports = function(grunt) {
var webpackConfig = require("../../webpack.config.js");
grunt.config.set("webpack", {
options: webpackConfig,
build: {
}
});
grunt.loadNpmTasks("grunt-webpack");
};
WebPackはカレントディレクトリに存在するwebpack.config.js
を読み込んで実行されるのがデフォルトの動きです。これは任意に変更する事が可能ですが、アプリケーションルートでCLIからwebpack
コマンドを実行する事を考慮して、アプリケーションルート直下にwebpack.config.js
を配置します。
webpack.config.js
は以下の様になります。
大した事はやっておらず、views以下を再帰的に探索し、拡張子をjs
に変更したものがassets/js/
以下に存在している場合にwebpackのビルドを対象としています(ファイル名ではなくパスで探します)。
但し、再帰検索中に不要なフォルダやファイル(layout.jade)が存在していた場合は除外しています。
"use strict";
var path = require("path"),
webpack = require("webpack"),
entry = require("./tasks/entry");
module.exports = {
cache: true,
entry: (function() {
var valueGenerator = function(filename) { return "./assets/js/" + entry.splitext(filename); };
var _entry = entry.entryFiles("./views", undefined, valueGenerator);
return _entry;
}()),
output: {
path: path.join(__dirname, ".tmp/public/dist/js"),
publicPath: "dist/",
filename: "[name].js",
chunkFilename: "[chunkhash].js"
},
resolve: {
// jQueryなどはnpmでインストールした物を読み込むため、`node_modules`以下にパスを通しておく。
modulesDirectories: ["node_modules"]
}
};
依存するentry.js
は以下の通り。
layout.jade
などは対象外としたいため、excludeFolders
やexcludeFiles
で例外定義を一応可能にしてあります。
"use strict";
var path = require("path"),
fs = require("fs");
/** @type {Array} 除外フォルダ一覧 */
var excludeFolders = [
];
/** @type {Array} 除外ファイル一覧(現状ではファイル名の単純一致を用いる) */
var excludeFiles = [
"403.jade",
"404.jade",
"500.jade",
"layout.jade",
];
/**
* 引数で渡された基点から再帰的に存在するファイル一覧を返す。
*
* 但し、{@code excludeFolders}に存在するディレクトリは再帰探索対象外として探索しない。
* @param {string} base 探索基点パス
* @return {Array} 探索基点パス以下のファイル一覧(順序は保証されない)
*/
var walk = function(base) {
return fs.readdirSync(base).reduce(function(prev, cur) {
var joined = path.join(base, cur);
if (fs.statSync(path.resolve(joined)).isDirectory()) {
if (excludeFolders.indexOf(cur) === -1) {
prev = prev.concat(walk(joined));
}
} else {
prev.push(joined);
}
return prev;
}, []);
};
/**
* 拡張子を除外したファイルパスを返す
*
* @param {string} filename ファイルパス
* @return {string} 引数から拡張子が除外された文字列
*/
var splitext = function(filename) {
return path.join(path.dirname(filename), path.basename(filename, path.extname(filename)));
};
/**
* base以下を再帰的に探索し、引数のkeyGeneratorとvalueGeneratorの実装に従ってオブジェクトを構築する
*
* keyGeneratorおよびvalueGeneratorが指定されていない場合、拡張子を除外したパスを返す。
* @param {string} base 探索基点となるパス(相対パスを想定)
* @param {Function} keyGenerator 再帰探索中に見つかったファイル名に対してkey構築を行うメソッド
* @param {Function} valueGenerator 再帰探索中に見つかったファイル名に対してvalue構築を行うメソッド
*/
var entryFiles = function(base, keyGenerator, valueGenerator) {
keyGenerator = keyGenerator || function(filename) { return splitext(filename); };
valueGenerator = valueGenerator || function(filename) { return splitext(filename); };
return walk(base)
.filter(function(f) {
return excludeFiles.indexOf(path.basename(f)) === -1 && path.basename(f).charAt(0) !== "_";
})
.reduce(function(prev, cur) {
prev[keyGenerator(cur)] = valueGenerator(cur);
return prev;
}, {});
};
// export
module.exports.entryFiles = entryFiles;
module.exports.splitext = splitext;
2. view側にマーカーを設定する
差し込みたいポイントにマーカーを設定します。
既存のlayout.jade
に書かれている以下のマーカーは不要ですので削除します。
// SCRIPTS
// SCRIPTS END
WebPack用のマーカーをそれぞれのjadeファイルに埋め込みます。
この際にlayout.jade
側ではなく個別のviewファイルに埋め込みます。
これはlayout.jade
側ではJavascriptファイルの読み込みが必要か判断出来ないため、個別ファイル側で定義する必要があるためです。
block append script
// WEBPACK_SCRIPTS
// WEBPACK_SCRIPTS END
3. sails-linkerにwebpack用の埋め込み処理を追加する
あと、一息です。
先ほど定義したWEBPACK_SCRIPTS
をマーカーとして、置き換えを行うようにsails-linker.js
に新規タスクを追加します。
devWebPackJade: {
options: {
startTag: "// WEBPACK_SCRIPTS",
endTag: "// WEBPACK_SCRIPTS END",
fileTmpl: "script(src=\"%s\")",
appRoot: ".tmp/public",
relative: false
},
files: function() {
var path = require("path"),
entry = require("../entry");
var keyGenerator = function(f) { return f; },
valueGenerator = function(f) { return path.join(".tmp/public/dist/js", entry.splitext(f) + ".js"); };
return entry.entryFiles("./views", keyGenerator, valueGenerator);
}()
},
4. linkAssetsに組み込む
最後にビルドの仕組みを弄って、linkAssets
タスクとcompileAssets
タスクに先ほど作成したタスクを実行するように設定を変更します。
module.exports = function (grunt) {
grunt.registerTask("linkAssets", [
"sails-linker:devJs",
"sails-linker:devStyles",
"sails-linker:devTpl",
"sails-linker:devJsJade",
"sails-linker:devStylesJade",
"sails-linker:devTplJade",
// これを追加
"sails-linker:devWebPackJade",
module.exports = function (grunt) {
grunt.registerTask("compileAssets", [
"clean:dev",
"jst:dev",
"less:dev",
"copy:dev",
"coffee:dev",
// 以下を追加
"webpack:build"
]);
};
最後に
これで、sails lift
した場合にwebpackでビルドされたモジュールを参照したもので確認できるようになります。
但し、jsファイルを修正したタイミングで再ビルドが必要になるので、supervisorなどで監視している場合は、jsフォルダも監視対象にするようにしておく必要があります。
なお、追加したファイルは以下の通りです。
.
├── tasks
│ ├── config
│ │ ├── sails-linker.js # 修正
│ │ └── webpack.js # 新規追加
│ ├── register # 修正
│ │ └── compileAssets.js # 修正
│ └── entry.js # 新規追加
└── webpack.config.js # 新規追加
注意事項
なお、此処までの処理はあくまでもdevelopmentモードのみで、productionモード側には手を入れていません。productionモードの場合はUglifyなどを考慮して別途設定が必要です(後で追記するかも)。
また、このまま設定を行ってもWindows環境ではWebpack側の問題で正しく動作しません。
詳細は[こちら(まだ書いていない...)]。
参考
サンプルプロジェクトをgithubにあげておきました => sails-webpack-sample
以上です。