LoginSignup
2
2

More than 5 years have passed since last update.

How to get started with recipe.js

Last updated at Posted at 2014-01-04

いままでの料理の仕方

あなたは今、必要に迫られて、なんやかんやあって、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>

各モジュールの依存関係はこんな感じ。

Information Systems Help Desk.png

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/

日本語もちょいちょいおかしいので後で更新します

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2