#いままでの料理の仕方
あなたは今、必要に迫られて、なんやかんやあって、fettuccine.alfredo.jsを作ることになりました。
各モジュールは再利用をするため、細かくモジュール化しました。
<script src="salt.js"></script>
<script src="milk.js"></script>
<script src="butter.js"></script>
<script src="parmigianoReggiano.js"></script>
<script src="fettuccine.js"></script>
<script src="fettuccine.alfredo.js"></script>
各モジュールの依存関係はこんな感じ。
Module | Dependencies |
---|---|
salt | なし |
milk | なし |
butter | salt, milk |
parmigianoReggiano | salt, milk |
fettuccine | なし |
fettuccine.alfredo | salt, butter, parmigianoReggiano, fettuccine |
フィットチーネ・アルフレッドを食べるためには、あなたは塩とバターとパルミジャーノ・レッジャーノとフェットチーネが必要です。
またバターとパルミジャーノ・レッジャーノは牛乳と塩で構成されるため、牛乳も別途用意する必要があります。
ここで面倒なのは牛乳が直接的にフィットチーネ・アルフレッドに必要ないのにも関わらず、呼び出す必要が有ることです。
依存関係を自力で解決するのはモジュールかを進める上で大きな問題になっていきます。
また、牛乳に新たな依存関係が生まれた場合、フェットチーネ・アルフレッドを呼び出している各ページのHTMLに新たなスクリプトタグを追加する必要が有ります。
上記の問題に対してrecipe.jsは大きく分けて、2つの解決方法を提供しています。
- 依存関係をすべて解決し、結合したスクリプトの生成(Concatenation方式)
- 依存関係を定義したファイルから、関連するモジュールを解決し、動的にロードする(Dynamic Loading方式)
#料理の仕方を変えましょう
##recipe.jsとは?
- 各モジュールの依存関係を一元管理
- head.jsを用いた並列ダウンロード
- ブラウザキャッシュのコントロール
- ページの文字エンコードと違うスクリプトの読み込み機能(一部)
require.jsと似てますが、太字の部分において機能差が存在します
##プロジェクトのディレクトリ構成
├── Gruntfile.js (後で説明)
├── package.json (後で説明)
├── recipe.json (後で説明)
├── dist (directory)
└── src
├── acqua.pazza.js
├── butter.js
├── fettuccine.alfredo.js
├── fettuccine.js
├── milk.js
├── parmigianoReggiano.js
├── pomodorini.js
├── salt.js
└── whitefish.js
##node.jsのインストール
割愛ってやつ?してやりますよ!
##recipe.jsonの作成
各モジュールの依存関係を定義します
{
"${module_namespace}": {
"path": "${module_source_path}",
"dest": "${dist_directory_path}",
"url": "${module_url}",
"amd": {
"dest": "${amd_dist_directory_path}",
"url": "${module_amd_url}"
},
"dependencies": [ "${depended_module_namespace}", "${depended_module_namespace}..." ]
}
}
フェットチーネ・アルフレッドの依存関係を定義すると以下の通り。
{
"fettuccine.alfredo": {
"path": "src/fettuccine.alfredo.js",
"dest": "dist",
"url": "/dist/fettuccine.alfredo.js",
"dependencies": ["fettuccine", "butter", "parmigianoReggiano", "salt"]
},
"fettuccine": {
"path": "src/fettuccine.js",
"dest": "dist",
"url": "/dist/fettuccine.js",
"dependencies": []
},
"butter": {
"path": "src/butter.js",
"dest": "dist",
"url": "/dist/butter.js",
"dependencies": ["salt", "milk"]
},
"parmigianoReggiano": {
"path": "src/parmigianoReggiano.js",
"dest": "dist",
"url": "/dist/parmigianoReggiano.js",
"dependencies": ["salt", "milk"]
},
"salt": {
"path": "src/salt.js",
"dest": "dist",
"url": "/dist/salt.js",
"dependencies": []
},
"milk": {
"path": "src/milk.js",
"dest": "dist",
"url": "/dist/milk.js",
"dependencies": []
}
}
##Gruntの準備
プロジェクトのフォルダにコンソールで移動し、
package.jsonがなければ
npm init
で適宜ごにょってください。
###Gruntと関連モジュールのインストール
npm install -g grunt-cli
npm install grunt --save-dev
npm install grunt-recipe --save-dev
npm install grunt-contrib-concat --save-dev
npm install grunt-contrib-uglify --save-dev
#Concatenation方式の場合
##Gruntfile.jsの用意
module.exports = function(grunt) {
grunt.initConfig({
recipe: {
main: {
files: {
'dist': ['recipe.json']
}
}
},
concat: {},
uglify: {}
});
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-recipe');
// By default, lint and run all tests.
grunt.registerTask('default', ['recipe', 'concat', 'uglify']);
};
##Gruntの実行
grunt
distディレクトリにやたらとスクリプト達が出力されますが、どうか平常心を保ってください。
一つのスクリプトに対して、以下のスクリプトが出力されます。
- xxx.js (Minifyされてる)
- xxx.unpack.js (オリジナルのソース)
- xxx.with-dependencies.js (依存するモジュールをすべて結合し、Minifyされたもの)
- xxx.with-dependencies.unpack.js (依存するモジュールをすべて結合されたもの)
また、以下のスクリプトも出力されますが、必要有りません
- recipe.js (recipe本体)
- recipe.version.js (ブラウザキャッシュのコントロール)
- recipe.dependencies.js (依存関係を解決)
##HTMLの修正
<script src="fettuccine.alfredo.with-dependencies.js"></script>
上記を各ページのHTMLに埋め込めば、新たな依存性が増えてもrecipe.jsonを編集して、
gruntを実行し、出力されたファイルに置き換えれば作業は完了です。
##まとめ
- recipe.jsonに依存関係を定義する
- gruntで依存するモジュール群を解決したモジュールを生成する
- HTMLには生成されたモジュールをスクリプトタグで読み込む
この方法であれば、recipe.js自体は必要なく、手軽な方法ですが以下の問題点を含んでいます。
- ファイルを更新しても、ユーザのブラウザキャッシュが使われてしまう可能性がある
- 複数のスクリプト内に同じ依存関係が存在する場合、スクリプトを無駄に読み込んでしまう
#Dynamic Loadingの場合
##HTMLの修正
HTMLに以下を記述し、そのページのスクリプトを全部ぶっこ抜く。(document.write使っているようなアド系のJSは抜けません)
<script src="/path_to_recipe_directory/recipe.js" data-menu="foo"></script>
data-menu属性にはメニュースクリプトの名前を定義します
##メニュースクリプトを定義する
メニュースクリプトは各ページ単位で作成し、そのページに必要なモジュールの定義とページ固有の処理を記述します。
ぶっこ抜いたスクリプトのうち、recipe.jsonに定義したものはlibraries
に名前空間を記述し、
それ以外のスクリプトはscripts
にURLを記述します。
recipe関数はQ
のpromiseを返します。
ページ固有の処理は返り値のthen関数に記述することで、全てのスクリプトがロードされた後に実行されます。
scriptsに指定するURLは末尾に#Shift_JIS
のように文字エンコードを付け足すと、その文字エンコードでロードされます。
foo.js
recipe({
libraries: [
"fettuccine.alfredo"
],
scripts: [
// write script URL
]
}).then(function(){
//write page specific logic
alert(salt && milk && butter && parmigianoReggiano && fettuccine && 'Digin!!');
});
##ディレクトリ構成
メニュースクリプトを定義するため、srcにrecipeディレクトリを掘っております
recipe.version.js, recipe.dependencies.jsはgruntが出力します。
サーバにスクリプトを置く際には、recipe.js, recipe.version.js, recipe.dependencies.js, メニュースクリプトをrecipe.jsと同じディレクトリに配置する必要が有ります。
├── Gruntfile.js
├── dist
│ └── recipe (メニュースクリプト, recipe.version.js, recipe.dependencies.jsの出力先)
├── package.json
├── recipe.json
└── src
├── acqua.pazza.js
├── butter.js
├── fettuccine.alfredo.js
├── fettuccine.js
├── milk.js
├── parmigianoReggiano.js
├── pomodorini.js
├── salt.js
├── whitefish.js
└── recipe (メニュースクリプトの置き場)
└── foo.js
##Gruntfile.jsの用意
module.exports = function(grunt) {
grunt.initConfig({
recipe: {
main: {
options: {
concat: false //結合ファイルを作る必要ないのでfalse
},
files: {
'dist/recipe': ['recipe.json']
}
}
},
concat: {},
uglify: {
menu:{
files: {
'dist/recipe/foo.js': ['src/recipe/foo.js']
}
}
}
});
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-recipe');
// By default, lint and run all tests.
grunt.registerTask('default', ['recipe', 'concat', 'uglify']);
};
##Gruntの実行
grunt
distディレクトリに
一つのスクリプトに対して、以下のスクリプトが出力されます。
- xxx.js (Minifyされてる)
- xxx.unpack.js (オリジナルのソース)
また、下記ファイルも生成されます。
- recipe/recipe.js (recipe.js本体)
- recipe/recipe.version.js (ブラウザキャッシュのコントロール)
- recipe/recipe.dependencies.js (依存関係を解決)
読み込みフロー
- recipe.jsが同ディレクトリのrecipe.version.jsを読み込む
- recipe.jsが同ディレクトリのrecipe.dependencies.jsを読み込む
- recipe.jsが同ディレクトリに存在するdata-menuの名前のメニュースクリプトをロードする
- recipe.jsがlibrariesの依存関係を解決し、scriptsのURLも含めて、スクリプトをロードする
- 全てのスクリプトのロード後にthen関数に与えられたそのページ固有の処理を実行する
##まとめ
- recipe.jsonに依存関係を定義する
- ページに必要なモジュールとページ固有の処理を定義したメニュースクリプトを作成する
- gruntで依存するモジュール群を定義したrecipe.dependencies.jsを生成する
- gruntでブラウザキャッシュ回避用のversionを定義したrecipe.version.jsを生成する
- HTMLにはdata-menu属性にメニュー名を指定したrecipe.jsのスクリプトタグを埋める
この方法であれば、依存性を解決させるだけでなく、ブラウザキャッシュ問題に対しても対応できるうえ、
新たなモジュールをページで利用する際に、HTMLにスクリプトタグを追加する必要はなく、メニュースクリプトに追加するだけでいいというメリットが生まれます。
サンプルのプロジェクトはこちらにあります。
ソース:https://github.com/sideroad/recipe-sample
実行例:http://sideroad.github.io/recipe-sample/
AMDも用意してますが、使い方は疲れたのでまた今度。
ソース:https://github.com/sideroad/recipe-amd-sample-jquery
実行例:http://sideroad.github.io/recipe-amd-sample-jquery/
日本語もちょいちょいおかしいので後で更新します