Sails.jsでwebpackを利用する


[Sails.js](http://sailsjs.org/)はとても便利です。
ただ、View側はSPAを前提とした構造を想定しているためか、複数ページ構成(あるいはSPAとマルチページの複合)の場合はちょっと都合が悪くなる場合があります。

そこで解決策の一つとして[WebPack](http://webpack.github.io/)を用いたページ別に関連するjsモジュールの埋め込みを行う方法を記載します。

## Sails.jsのViewに対する既存の動作
Sails.jsの既存の動きでは、assets以下のスクリプトを
`layout.jade`などに定義された以下のマーカーのHTMLに対して埋め込みを行います。

```html
<!--SCRIPTS-->
ここに書き込まれる
<!--SCRIPTS END-->
```

この処理自体は`sails lift`などを実行した際に実行され、
内部的にはgruntのタスクとして定義されています。

実体は`tasks/register/linkAssets*.js`でタスクとして登録されている`tasks/config/sails-linker.js`で行われており、
通常版ではjsをそのまま読み込んだり`--prod`の場合に[uglify](http://lisperator.net/uglifyjs/)を用いたMinifyが行われたりします。

## Webpackの提供するMULTIPLE ENTRY POINTS
WebPackには[MultipleEntryPoints](http://webpack.github.io/docs/multiple-entry-points.html)という機能があり、複数のファイルを複数の結合したjsファイルとして出力する事が可能です。

詳細は省きますが、簡単に言うと例えば`A.js`と`B.js`がそれぞれ毎に依存した別ファイルに出力する事が可能です。

* A.jsはA.output.jsへ
* B.jsはB.output.jsへ

これを利用して、各ページ毎にそれぞれjsの出力を行います。

## SailsのビルドにWebPackビルドとHTMLへの埋め込みを実装する
ここからが本題です。

以下の流れで実装していきます。

1. WebPackによるビルドを構築する
2. View側にマーカーを設定する
3. sails-linkerにWebPack用の埋め込み処理を追加する
4. compileAssetsとlinkAssetsに組み込む

#### 基本的な設計と前提条件
複数ページで構築する場合、各画面ごとに1つのJSファイルが必要となることが多いです。
そこで、1つのViewファイルに対して相対的にjsファイルを構築する事を前提とします。
つまり、`views/user/detail.jade`の場合、`assets/js/views/user/detail.js`が存在している事になるわけです。

＃ *(私はjadeが好きなのでViewはjadeです)*

![sails-webpack.png](https://qiita-image-store.s3.amazonaws.com/0/11214/d02ec948-ecab-31d4-00f9-87cd8cabc687.png "sails-webpack.png")


この前提により生成されたJSファイルをEntryPointと見なして、Viewsに対してlinkAssetタスクと同様に埋め込みを行います。

### 1. WebPackによるビルドを構築する
`grunt-webpack`に依存した構築を行うので、事前に以下のようにインストールしておきます。

```bash
npm install grunt-webpack
```

現時点のバージョンは`1.0.8`です。

新規に`tasks/config/webpack.js`を作成します。これが実際にWebpackのビルドの実体になります。

```javascript
"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)が存在していた場合は除外しています。

```javascript
"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`で例外定義を一応可能にしてあります。

```javascript
"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`に書かれている以下のマーカーは不要ですので削除します。

```jade
// SCRIPTS
// SCRIPTS END
```

WebPack用のマーカーをそれぞれのjadeファイルに埋め込みます。
この際に`layout.jade`側ではなく個別のviewファイルに埋め込みます。
これは`layout.jade`側ではJavascriptファイルの読み込みが必要か判断出来ないため、個別ファイル側で定義する必要があるためです。

```jade
block append script
  // WEBPACK_SCRIPTS
  // WEBPACK_SCRIPTS END
```

### 3. sails-linkerにwebpack用の埋め込み処理を追加する
あと、一息です。

先ほど定義した`WEBPACK_SCRIPTS`をマーカーとして、置き換えを行うように`sails-linker.js`に新規タスクを追加します。

```javascript
		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`タスクに先ほど作成したタスクを実行するように設定を変更します。

```tasks/register/linkAssets.js
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",
```

```tasks/register/compileAssets.js
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フォルダも監視対象にするようにしておく必要があります。

なお、追加したファイルは以下の通りです。

```bash
.
├── 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](https://github.com/hikaruworld/sails-webpack-sample)

以上です。