Thujikun blog

気になった技術系のネタだったり備忘録だったり・・・

webpackを使い倒す

本エントリーJavaScript Advent Calendar 2014 7日目の記事になります。

webpackとは

概要については最近いろんな方が書いてるいるのでそちらを参考にしていただければつかめるかと。

ようはナウいフロントエンドの依存解決ツールですね。

っていうネタで書こうと思っていたら、昨日yutaponさんが既に書いていたりするので(gulp.jsを使ってフロントエンドのビルドをする【webpack, stylus】)、いいところはパクリ参考にしつつ、もうちょっとwebpackに突っ込んだ内容を書いてみようと思います。

Options

entryとoutput

簡単に試すときはentryは1ファイルだけ指定できればよいですが、実際にプロジェクトで使うとなると複数のファイルを指定しなければならない状況になるかと思います。

webpackのentryはオブジェクト形式に対応していて、outputでそのproperty名を使用して動的に名前をつけることでその問題が解決できます。

1
2
3
4
5
6
7
8
    entry: {
        top: './app/main.js',
        list: './app/list/main.js'
    },
    output: {
        filename: '[name].bundle.js',
        publicPath: '/assets/'
    }

上記のような設定をすることで結果的に top.bundle.jsとlist.bundle.jsの2ファイルがdestに指定したディレクトリに置かれることになります。

loader

Webpackの大きな特徴としてjs以外のどんなファイルでもloaderさえ使えば読み込むことができる、というのがあります。

loader一覧

上記を見てわかるようにかなりの数のloaderがあるので、いくつかpuckupして紹介します。

  • html-loader

    htmlファイルを読み込んで文字列としてjs内で使用することが可能。underscoreのtemplateなど、フロントでrenderingをするときに非常に便利。

  • css-loader(sass-loader, stylus-loader)

    cssファイルをテキストとして読み込んで、headに埋め込むことが可能。sass-loaderやstylus-loaderの場合compileも同時に行う。angularやreact等を使ってHTMLをcomponentとして扱っている場合に有効。

  • json-loader

    jsonファイルを読み込んで、jsのオブジェクトに変換して使うことが可能。

  • coffee-loader

    coffee scriptで書かれたファイルをjsに変換して読み込むことが可能。

  • jade-loader

    jadeで書かれたファイルをhtmlに変換して読み込むことが可能。

  • es6-loader

    es6で書かれたコードをes5互換にして読み込むことが可能。

  • url-loader ファイルサイズが小さい場合data-uriに変換して読み込み、大きい場合はそのままpathとして読み込むことが可能。

  • expose-loader

    指定したファイル内のオブジェクトをグローバル変数として外に公開することが可能。詳しくは下の例を見るとわかりやすい。

  • export-loader

    commonJS形式に対応していない(module.exportsがない)jsファイルにmodule.exportsの記述を追加し、内部の変数を外に公開することが可能。

などなどwebpackを強力なツールたらしめているのがloaderです。是非使いこなしましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// configで指定する場合
module: {
    loaders: [
        // htmlファイルを読み込んだ場合にhtml-loaderを使用する。その際minimizeする。
        { test: /\.html$/, loader: 'html?minimize' },

        // cssファイルを読み込んだ場合にcss-loaderを使用する。その際minimizeする。
        { test: /\.css$/, loader: 'css?minimize' },

        // jsonファイルを読み込んだ場合にjson-loaderを使用する。
        { test: /\.json$/, loader: 'json' },

        // coffeeファイルを読み込んだ場合にcoffee-loaderを使用する。
        { test: /\.coffee$/, loader: 'coffee' },

        // jadeファイルを読み込んだ場合にjade-loaderを使用する。
        { test: /\.jade$/, loader: 'jade' },

        // es6で記述されたjsファイルを読み込んだ場合にes6-loaderを使用する。
        { test: /\.js$/, loader: 'es6' },

        // 画像ファイルを読み込んだ場合にurl-loaderを使用する。ファイルサイズが8kb以下であればdata-uriに変換する
        { test: /\.png$/, loader: 'url?limit=8192' },

        // jQueryを読み込んだ場合に"jQuery"をグローバルオブジェクトにする。
        { test: /\.jquery.js$/, loader: 'espose?jQuery' },
        // var $ = require('jquery');
        // でグローバル変数にjQUeryが登録される

        // angularJsを読み込んだ場合に"angular"を外に公開する。
        { test: /angular\.js$/, loader: 'exports?angular' }
        // var angular = require('angular');
        // で変数"angular"が取得できる
    ],
}

// requireのタイミングでloaderを指定することもできる。
// settings.jsonをjsのオブジェクトとして使用する。
var settings = require('json!./data/settings.json');

// angularオブジェクトを使用する
var angular = require('exports?angular!angular');

resolve

extention

読み込む際に拡張子を省略できるようにする。jsはデフォルトで入っている。個人的にはコンポーネントを作成した場合に名前を被らせることが多いのでjs以外の省略はしていない。

1
2
3
4
5
6
resolve: {
    extenstions: ['', '.js', '.json', '.html']
}
// 上記のようになっているとき、下記のように省略することが可能。
// require('settings.json') ->require('settings')
// require('item-template.html') ->require('item-template')

root

requireで読み込むときのrootのpathを指定できる。配列で複数の指定が可能。下記のようなディレクトリ構造の場合

1
2
3
4
5
6
gulpfile.js
gulp  config.js
      tasks  各タスクファイル
app  js  app.js
           modules  dialog.js
                      tab.js

通常であれば

1
2
3
//@file app.js
var dialog = require('./modules/dialog');
var tab = require('./modules/tab');

としなければならないところを、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//@ config.js
var path = require('path'),
    current = process.cwd();
module.exports = {
    // 他の設定
    resolve: {
        root: path.join(current, 'app/js/modules')
    }
    // 他の設定
};


//@app.js
var dialog = require('dialog');
var tab = require('tab');

のように記述することが可能。

alias

ファイル単位でaliasをはる。root使えばいらない子のようなきがする。用途としてはbowerでとってきたときのmainに設定されてないファイルを使いたい場合くらいかな。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//@ config.js
var path = require('path'),
    current = process.cwd();
module.exports = {
    // 他の設定
    resolve: {
        alias: {
            bar: path.join(current, 'bower_components/foo/plugins/bar.js')
        }
    }
    // 他の設定
};


//@app.js
var bar = require('bar');

plugins

loaderと並んでwebpackの強力な機能。というか一部どっち使えばいいか迷うものもある。後述。

plugins一覧

見てわかるように非常に沢山あるので自分も見切れてないです。なので便利なやつだけ一部紹介します。

ResolverPlugin

これとresolveのrootの設定と組わせると、bowerでとってきたmoduleをそのままrequireすることが可能。非常に便利。他の用途もあるんだろうけど知らない。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//@ config.js
var path = require('path'),
    current = process.cwd();

module.exports = {
    // 他の設定
    resolve: {
        root: [
            path.join(current, 'bower_components')
        ]
    },
    plugins: {
        new webpack.ResolverPlugin(
            new webpack.ResolverPlugin.DirectoryDescriptionFilePlugin('.bower.json', ['main'])
        ),
    }
};


//@app.js
var $ = require('jquery');

※ bowerモジュールの中にはbower.jsonをignoreに設定しているものがあるため、bowerが動的に作成する.bower.jsonを参照する方が安全。

ProvidePlugin

指定した変数を他のモジュール内で使用できるようにする。globalには置かない。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//@ config.js
var path = require('path'),
    current = process.cwd();

module.exports = {
    // 他の設定
    resolve: {
        root: [
            path.join(current, 'bower_components')
        ]
    },
    plugins: {
        new webpack.ResolverPlugin(
            new webpack.ResolverPlugin.DirectoryDescriptionFilePlugin('.bower.json', ['main'])
        ),
    },
    new webpack.ProvidePlugin({
        jQuery: "jquery",
        $: "jquery"
    })
};


//@app.js
var $ = require('jquery');
require('jquery-ui');

jQueryとかAngularのプラグインは、jQueryやangular変数に対してオブジェクトを追加する形となるため、その変数を参照できないとエラーになってしまいます。

そのときこのprovidePluginを使用するとこの問題を解決できるのですが、グローバル変数を生成するexpose-loaderでも解決することができます。

グローバル汚染しない分providePluginを使用する方が望ましいですが、別機能でwebpackに依存しないモジュールが画面内にあったりする場合はexpose-loaderで解決してもよいでしょう。

開発環境向けplugin

  • HotModuleReplacementPlugin

    webpack-dev-serverを使っているときに、画面をリロードすることなくモジュールの差し替えを可能にする。(実験的機能)

  • NoErrorsPlugin

    compile時にエラーが出たらskipする。

本番環境向けplugin

  • DedupePlugin

    被ってるモジュールがいたらひとつにまとめる。

  • UglifyJsPlugin

    compile時にuglifyでminimizeする。

  • OccurenceOrderPlugin

    よく使われるモジュールに降るIDの桁数をより短くすることでよりコードを圧縮する。

  • AggressiveMergingPlugin

    ファイルを細かく分析し、まとめられるところはできるだけまとめてコードを圧縮する。Closure CompilerのADVANCED_OPTIMIZATIONみたいなことはしない。

Webpack Deb Server

webpackが提供している開発環境向けのサーバー。compile時に時間がかかる問題を、修正がかかった箇所だけcompileする方式を採用することで高速化している。

上記で紹介したHotModuleReplacementPluginを使用するとreloadなしにモジュールの再読み込みが可能。

が、これ実際に使おうとすると情報が少なかったりして結構苦戦したので備忘録。

今回サンプルのアプリケーションを作成したときの開発環境、本番環境、webpack dev server、gulp-serveでたてたstaticサーバのそれぞれのconfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// 本番環境
{
    entry: {
        top: dir.js + '/main.js',
    },
    output: {
        filename: '[name].bundle.js',
        publicPath: '/js/'
    },
    resolve: {
        extensions: ['', '.js'],
        root: [
            path.join(current, 'bower_components'),
            path.join(current, dir.js, 'modules'),
            path.join(current, dir.js, 'templates')
        ]
    },
    debug: false,
    devtool: false,
    stats: {
        colors: true,
        reasons: false
    },
    plugins: [
        new webpack.ResolverPlugin(
            new webpack.ResolverPlugin.DirectoryDescriptionFilePlugin('.bower.json', ['main'])
        ),
        new webpack.optimize.DedupePlugin(),
        new webpack.optimize.UglifyJsPlugin(),
        new webpack.optimize.OccurenceOrderPlugin(),
        new webpack.optimize.AggressiveMergingPlugin(),
        new webpack.ProvidePlugin({
            jQuery: "jquery",
            $: "jquery"
        })
    ],
    module: {
        loaders: [
            { test: /\.html$/, loader: 'html?minimize' }
        ]
    }
}

// 開発環境
{
    entry: {
        top: dir.js + '/main.js',
    },
    output: {
        path: path.join(current, 'app',  'js'),
        filename: '[name].bundle.js',
        publicPath: '/js/'
    },
    resolve: {
        extensions: ['', '.js'],
        root: [
            path.join(current, 'bower_components'),
            path.join(current, dir.js, 'modules'),
            path.join(current, dir.js, 'templates')
        ]
    },
    cache: true,
    debug: true,
    devtool: false,
    stats: {
        colors: true,
        reasons: false
    },
    plugins: [
        new webpack.ResolverPlugin(
            new webpack.ResolverPlugin.DirectoryDescriptionFilePlugin('.bower.json', ['main'])
        ),
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoErrorsPlugin(),
        new webpack.ProvidePlugin({
            jQuery: "jquery",
            $: "jquery"
        })
    ],
    module: {
        loaders: [
            { test: /\.html$/, loader: 'html?minimize' }
        ]
    }
}

// webpack dev server
{
    contentBase: "http://localhost:3000",
    hot: true,
    quiet: false,
    noInfo: false,
    lazy: false,
    watchDelay: 300,
    publicPath: "http://localhost:9000/js/",
    stats: { colors: true }
}

// static server by gulp-serve
{
    port: 3000,
    root: [dir.tmp, dir.app]
}

ハマりポイント1

webpack dev serverでは”http://localhost:9000/webpack-dev-server/“のようなURLでページを確認することになるため、pathをどう通していいかわからない。

→ と思っていたらiframeで”/webpack-dev-server/“の部分を削ったページを表示していたので、pathに関してはやってみたら問題なかった。

ハマりポイント2

webpack dev serverでは、baseが一箇所しか指定できないため、開発環境でcompileしたファイルだけtmpディレクトリに置き、compileせずにそのまま使うファイルはappディレクトリに置いてどちらも参照する方法がわからない。

→ 別でたてたサーバをcontentBaseに設定することが可能。そのためgulp-serveを使って.tmpとappをrootにしたサーバを立ち上げ、webpack dev serverをそこに向けることができる。

contentBase: "http://localhost:3000", のところ。

はまりポイント3

2の対策をした場合に、webpack dev serverが動的にcompileをかけるjsファイルの参照の仕方がわからない。

→ jsファイルのみwebpack dev serverの方のportを参照する必要あり。自分の場合”http://localhost:9000/js/top.bundle.js“を参照している。

またその場合に本番環境ではそのままにするわけにはいかないので、自分の場合はgulp-ectでhtmlも本番用と開発用を分けてbuildできるようにした。

ただ正直ここは自動で書き換えられるようにwebpack側で対応入れて欲しい。

はまりポイント4

webpack dev serverのwatchとgulp-watchの住み分けをどうすればいいかわからない。

→ webpack dev serverとwatchを並列で実行してしまえば問題ない。もちろんwatchの方でwebpackで管理しているファイルの監視をする必要はない。

以上、なかなか厄介でしたがとりあえず使えるようになりました。

gruntで使いたい場合はgenerator-react-webpackがやっているので参考にすると良いかもしれない。

AMD形式の読み込み

requireの第二引数のfunctionを入れれば、結果がcallbackで返ってくる。

この後紹介するsampleでは使っていないが、publicPathに設定したディレクトリに非同期で読み込むファイルが置かれる。

requirejsとは違い、非同期で読み込む先のファイルも最適なbuildがかかった状態になるためパフォーマンスがよい。また、複雑なファイル解析はせず、単純にjsonp形式で追加モジュールを読み込むため非常にシンプル。

その他

  • webpackでは全てのモジュールが閉じたスコープ内で管理される。(もちろんexpose-loaderを使用しない限り)
  • 何度requireしてもキャッシュされた同じ箇所を参照するのみなので、パフォーマンスに悪影響を与えない。例えばurl-loaderで何度画像をrequireしても、文字列として展開される箇所は一箇所。

サンプルアプリケーション

今回紹介したポイントをほとんど抑えたサンプルを作成しました。

demo

やっていることは

  1. instagramのAPIで画像及びテキストデータ取得。
  2. pinterest風に並べる。

の2つです。環境は

  • gulp
  • webpack
  • webpack-dev-server
  • stylus
  • ect

を用いて開発環境と本番環境をそれぞれ別々に構築していて、Backbone、Angular、react等のフレームワークには依存していないので、応用しやすいかなと思います。

source

ふー、なんとか日にち間に合った。駆け足だったので変なこと書いていたら教えて下さい。使い倒すとか言ってますがまだまだ使い倒せてません><

JavaScript Advent Calendar 2014の8日目は、muyuuさんです!よろしくお願いします!


CompassでSpriteなんてもう古い!?grunt-spritesmithのすすめ

釣りっぽいタイトルになってますが、ようはCompassのSprite Generatorだと解決できないことをgrunt-spritesmithならやってくれるよって話です。 Compassの方については山ほど記事があるので、あまり知らないよって方はてきとーにググってください。

Compassの問題点

Sassでしか使えない

一つ目は当然ですが、CompassはSassのフレームワークなのでLessとかStylus派の人は使えないってことです。 私は最近はStylus派なので、なにか良いSpriteツールないかなーと探し始めたのが最初でした。

SassのCompileのたびにSprite化処理が実行される

これは画像が多いサイトではかなり深刻な問題となります。画像とは関係ないStyleの変更時でも毎回Sprite化が実行されるため、確認までかなりの時間をロスします。

正方形に画像を並べるとPaddingの指定が効かない

Sprite画像のサイズをできるだけ小さくしようとすると、当然正方形に近い方が望ましいといえます。が!CompassのSprite Generatorの場合正方形にするとpaddingの指定が効かず、画像どおしがくっついちゃいます。 そうするとちょっとpaddingをもたせて配置したいときとか、拡大縮小されたときに隣の画像がチラホラ見えておかしなことになります。

Retina対応わかりづらい

なんかいろんな指定方法があって正直わかりづらいと個人的には感じてます。画像サイズも手で指定しなきゃいけないし。

Compassの開発が停滞中

最近はめっきり開発が止まっちゃってますね。CSS3のmixinも不十分だし、ちょっとこれからも使っていくには不安です。ちなみにCSS3のmixinについてはbourbon入れれば一応解決できます。

だいたいぱっと思い浮かぶのはこのへんでしょうか。とくに1と2は深刻な問題ですね。

grunt-spritesmithができること

CSS, Sass, Less, Stylus, JSON形式に対応

生のCSSや各CSSプリプロセッサ、さらにはJSON形式でも出力してくれます。

  • 生のCSSの場合昔ながらのGeneratorを使ったときと同じように、画像名でclassがつくられます。もちろんprefixもつけられます。

  • 各CSSプリプロセッサの形式を指定した場合、画像名ごとのmixinと画像の各パラメータ(width, height, offset, 画像名, positionなど)を全て変数として出力してくれます。なのでRetina対応とかもそのへんのパラメータ使えば簡単にできます。

  • JSON形式の場合もCSSプリプロセッサの形式と同じように各パラメータをJSON形式で出力してくれます。個人的にはまだ利用はしていないですが、gruntを経由してjade, js, stylusで同じJSONファイルを元に開発を行ったりできそうだなーとは思ってます。

画像が増えたときのみタスクを実行すれば良い

当然CSSプリプロセッサとは別のタスクとしてGruntの設定を行うため、Sprite画像生成とCSSプリプロセッサのCompileをわけることができます。高速Compile最高ですね。

画像を正方形に並べてpaddingもとれる

こちらは問題なく画像を正方形に並べつつ、かつpaddingも設定することができます。なのでSprite画像のサイズを最小限に抑えることができます。

使い方

サンプルをこちらに作成しましたので、よかったら試してみてください。 cloneしてnpm installしてgruntって実行すると、distフォルダに各出力結果ごとの結果が見れます。 他にもいくつかパラメータつけてるので、Gruntfile.jsと合わせて見てもらえればと思います。

結論

みんなこれで心置きなくStylusに移行できる☆彡


便利なjavascriptテクニック集

今回はjavascriptで知ってる人にとっては当たり前だけど、そうでない人は全然知らないようなテクニックをまとめていきます。

globalを汚さない無名関数スコープ

javascriptの問題のひとつとしてスコープがよくとりあげられますが、ここではグローバルスコープを汚さない手法を紹介していきます。

悪い例

1
2
// これはグローバルスコープ
var foo = 'bar';

ちなみにglobalスコープとはwindowオブジェクトにプロパティを追加していくことと同義です。

1
2
3
// 下記の2つは結果として同じことを行っている。
var foo = 'bar';
window.foo = 'bar';

解決するには下記のように無名関数を用います。

1
2
3
4
5
// 無名関数をつくり即時実行
;(function() {
    // スコープが関数内で閉じる。
    var foo = 'bar';
}.call(this));

また、jQueryを使うときは、下記のように引数でjQueryを渡して、$という名前で引数として受け取ってあげると、$のconflict問題から開放されます。
というか$をグローバル参照するのはバグの元になるのでやめましょう。

1
2
3
4
5
;(function($) {
    // スコープが関数内で閉じる。
    var foo = 'bar';
    $('#hoge').text('hogehoge')
}.call(this, jQuery));

ちなみに無名関数の書き方は他にもあります。気にしなきゃいけないほどの違いはないので好みで選んでよいでしょう。

1
2
3
4
5
6
7
// カッコの位置がちょっと違う。
;(function() {
}).call(this);

// jslintだとsyntaxエラーになるかも。一応一文字少ないので一番軽い。
;!function() {
}.call(this);

※1 先頭のセミコロンはconcatした場合に、エラーになる可能性を排除するためです。
※2 callを使っているのは、通常の関数呼び出しだとstrictモードの場合に無名関数内部のthisがwindowオブジェクトにならないためです。

ArrayっぽいけどArrayじゃないObjectをArrayにする方法

document.getElementsByClassNameとかdocument.querySelectorAllとかで複数のDOMを取得したときの戻り値とか、argumentsプロパティは見た感じArrayっぽいのにArrayにはなってません。
その結果何がどうなるかというと、pushとかjoinとかArrayが持っているメソッドが使えず、非常に使いづらい状態です。
それを解消するための方法が下記のテクニックです。

1
2
3
4
5
// リストのDOMを取得
var list = document.querySelectorAll('.hoge-list li');

// ArrayっぽいObjectからArrayに変換
list = Array.prototype.slice.call(list);

ちょっと解説すると、まずArray型が持っているメソッドで、配列の一部を切り出すsliceメソッドがあります。
これは非破壊的メソッドで、このメソッドを実行したArrayはそのままで、実行結果を新しいArrayとして返却してくれます。
その結果引数を指定しなければ、Arrayをシャローコピー(1階層のみコピー)することができます。
さらにその特性を活かして、sliceをArrayのprototypeから直接呼び出し、callを使ってthisをArrayっぽいObjectで上書くことで完全なArray型にキャストすることができます。

関数のthisを指定したObjectでbind

当たり前に使われてますが、知らない人も結構多いかと思うので一応。applyとcallです。
javascriptのfunction型はprototypeにcallとapplyってメソッドを持っていて、呼び出したfunctionのthisを第一引数でbindすることができます。

1
2
3
4
5
6
function hoge(foo) {
    console.log(this.hoge, foo);
    // -> hogehoge bar
}
// hoge関数のthisに文字列’hogehoge’を指定して実行。
hoge.call({hoge: 'hogehoge'}, 'bar');

jQueryでthisを参照するとDOMオブジェクトになってるのはこういう方法を使ってます。そのせいでthisってなんぞ?みたいなことになるわけですが。
callとapplyの違いは関数の引数の指定方法です。
callは第2引数が第1引数に、第3引数が第2引数に、といった具合に順番がひとつずつずれる方法になります。
applyは第2引数をArray指定する形になっていて、それが順番に引数になります。
apply使うと$.whenみたいなArrayの引数ではなく可変長な引数の関数が格段に使いやすくなるのでお勧めです。

可変長引数の扱い方

関数で可変長な引数に対応したい場合がたまにあります。その方が綺麗に見える場合だったり、プラグインを独自拡張したい場合だったり。
そんなときに使えるのargumentsプロパティです。

1
2
3
4
5
6
7
8
9
function hoge(name1, name2) {
    console.log(name, name2);
    // -> foo bar

    // arumentsプロパティには引数が入っている
    console.log(arguments[0], arguments[1]);
    // -> foo bar
}
hoge('foo', 'bar');

ただ、実際に可変長引数を使いたいと思った場合は、for文とかでまわして処理を行いたいことが多いです。
しかし、argumentsプロパティは配列っぽい要素であって配列ではありません。というわけで、上で書いた手法がよく使われることになります。

1
2
3
4
5
6
7
function hoge() {
    var args = Array.prototype.slice.call(arguments);

    args.forEach(function(value)) {
        // 引数それぞれに対しての処理
    }
}

ちなみにargumentsプロパティは実装がヤバイらしく、あまり使用が推奨されていません。
ES6からは正式に可変長引数がサポートされるので将来的にはそっちを使うほうがよいです。
あとこれ必要ない場合に使うと可読性が著しく落ちるのでご利用は計画的に。
最後に、プラグイン内部で関数どおしが何やってるかわからないけど、一部の値をちょっと書き換えたり、取得したいって場合がまれによくあります。
そんなときは下記のようなテクニックが使えます。

1
2
3
4
5
6
7
8
9
// 元の処理を退避
var _ajax = Backbone.ajax;

Backbone.ajax = function() {
    // なんか独自処理

    // 元々のBackbone.ajaxを本来呼ばれたときと同じ形で呼び出す。
    _ajax.apply(this, arguments);
};

このときはBackboneのajaxをカスタマイズして、エラーハンドリングを全処理で共通的に行うようなことをしたんですが、長くなるので今回は見送ります。

argumentsの隠された力

argumentsプロパティは上で書いたようによく引数取得で使われるわけですが、実は隠された力を持っています。
ただその力はあまりにチートで危険すぎるため、strictモードでは禁止されていますのであらかじめご了承ください。

arguments.callee

まず一つ目はarguments.calleeです。これはその関数自身のことです。無名関数だけど再帰処理がしたい場合とかに使えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var deepCopy = function(obj) {
    var name,
        ret;

    if(typeof obj === 'object') {
        ret = {};
        for(name in obj) {
            if(typeof obj[name] === 'object') {
                ret[name] = arguments.callee(obj[name]);
            } else {
                ret[name] = obj[name];
            }
        }
    } else {
        ret = obj;
    }

    return ret;
};

上記はObjectをdeepコピーする関数ですが、無名関数で作っているため再帰処理をする場合に自分自身を関数名で呼び出すことができません。
そのため、arguments.calleeで自分自身を呼んでいます。
もちろん名前付き関数として定義すれば問題なく動作しますが、関数名変えたりする場合に一箇所修正すればいいので個人的には好きな書き方です。
あと@azu_reさんからご指摘もらった件について追記します。
IE8を無視すれば名前付き関数式がサポートされてるので下記のような書き方が可能です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
var deepCopy = function dc(obj) {
    var name,
        ret;

    if(typeof obj === 'object') {
        ret = {};
        for(name in obj) {
            if(typeof obj[name] === 'object') {
                ret[name] = dc(obj[name]);
            } else {
                ret[name] = obj[name];
            }
        }
    } else {
        ret = obj;
    }

    return ret;
};

// IE8に対応した書き方は下記のような感じ
var deepCopy = function (obj) {
    return dc(obj);

    function dc(obj) {
        var name,
            ret;

        if(typeof obj === 'object') {
            ret = {};
            for(name in obj) {
                if(typeof obj[name] === 'object') {
                    ret[name] = dc(obj[name]);
                } else {
                    ret[name] = obj[name];
                }
            }
        } else {
            ret = obj;
        }

        return ret;
    }
};

arguments.callee.caller.arguments

次に紹介するのはarguments.callee.caller.argumentsです。
これはなんと呼び出し元の関数の引数を取得することができます。ちなみにarguments.callee.callerは呼び出しの関数が取得できます。

1
2
3
4
5
6
7
8
9
function hoge(name) {
    hogehoge();
}
function hogehoge() {
    // 呼び出し元の一つ目の引数を表示
    console.log(arguments.callee.caller.arguments[0]);
    // -> foo
}
hoge('foo');

ただしこれをやってしまうと、可読性が著しく悪化するばかりか、関数の呼び出し順に過度に依存することになるので、非常事態以外には触るな危険です。
また、もちろんこれどんどん遡って行くことが可能で、一番最初の呼び出し元関数まで全部取得可能です。
私はBackbone.ajaxでBackbone.syncの引数の取得に使用しました。ごめんなさい。


ちょっと未来のJavaScript

本エントリは JavaScript Advent Calendar 201314日目となります。
来年遂にXPが逝去されるということで、IE9以降のシェアが飛躍的に伸びることを祈りつつ、IE9以降でJavaScriptでできるようになることを気がつく限りまとめてみました。

DOM

addEventListener / removeEventListener

イベントを登録/削除するためのメソッド。IE8まではattachEventとdetachEventという似たような、でも割と細かいところで動きが違うメソッドを使う必要があったが、IE9から標準のaddEventListenerがサポートされている。
※ jQueryのon/offとかbind/unbindとだいたい同じ。
※ 第3引数はuseCaptureといって、trueにするとイベント伝播を上位のDOMから発生させることができる。まあ使うことないけど。
addEventListener, removeEventListener

1
2
3
4
5
6
7
8
function onClickFunction(e) {
    //クリック時の処理
}
// クリックイベント設定
document.getElementById('hoge').addEventListener('click', onClickFunction, false);

// クリックイベント削除
document.getElementById('hoge').removeEventListener('click', onClickFunction, false);

createEvent / dispatchEvent

イベントを作成/発火するためのメソッド。IE8まではcreateEventObjectとfireEventというこれまた似たような、でもやっぱり色々動きが異なるメソッドがあった。
※ jQueryの$.eventとかtriggerとだいたい同じ。
createEvent, dispatchEvent

1
2
3
4
5
6
7
8
9
10
11
// イベントオブジェクト作成
// 引数にはイベント種別を渡す。
var evt = document.createEvent('MouseEvents');

// イベントオブジェクト初期化
// 引数の内容はhttps://developer.mozilla.org/ja/docs/Web/API/event.initMouseEventを参照。
// マウス座標とかctrlキーが押されてるかどうかなど色々指定可能。
evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);

// イベント発火
document.getElementById('hoge').dispatchEvent(evt);

Composition Events

composition Eventsは文字の変換イベントを取得できるメソッドです。
これまではFFにも対応したければsetIntervalで入力中にテキストフィールドを監視する必要があったのが、これを使えば解消することができます。
まあIE系ならkeydown + setTimeoutでもいいし、FFならinputイベント使えばいいって話もあるけど。
※ compositionstartイベントとcompositionupdateイベントは同時に発火するが、compositionupdateイベントとcompositionendイベントは同時に発火しない。
CompositionEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var textElement = document.getElementById('hoge');

// テキスト変換開始時イベント監視
textxElement.addEventListener('compositionstart', function(e) {
    //変換開始時処理
}, false);

// テキスト変換中イベント監視
textxElement.addEventListener('compositionupdate', function(e) {
    //変換中処理
}, false);

// テキスト変換完了時イベント監視
textxElement.addEventListener('compositionend', function(e) {
    //変換完了時処理
}, false);

Mutation Events

DOMの追加/変更/削除などDOMの状態監視ができるイベント。類似メソッドは今までなかったのでかなり画期的なんじゃないかと思う。Angularの処理を強引にjQueryで補足したいときとかに使えるかもね。
※ 多用すると処理が追えなくなる可能性あり。
※ IE9ではDOMNodeInsertedが動かないらしい。
※ パフォーマンスの関係でMutation Eventsは非推奨になり、最新仕様はMutation Observerになっている。そのためMutation EventsはPolyfillとして用いるべし。
Mutation Events, Mutation Observer

1
2
3
4
5
6
7
8
// エレメントの属性の変更を監視
document.addEventListener('DOMNodeInserted', function(e) {
    // 属性変更時処理
    console.log('追加された!');
});

document.body.appendChild(document.createElement('div'));
// -> 追加された!
DOMAttrModifiedはどのブラウザでも動作しない模様。  

DOMContentLoaded Event

DOMの読み込みを待ち合わせるイベント。
※ $(document).ready(function(){})とか$(function(){})と同じ。IE9以降なら内f部的にこれが使われてる。
DOMContentLoaded

1
2
3
document.addEventListener('DOMContentLoaded', function() {
    // DOM読み込み時処理
}, false);

querySelector / querySelectorAll

CSSのselectorでDOM探索ができるメソッド。完全にjQuery感覚で使えます。まあgetElementByIdとかgetElementsByClassNameとか別のが使える場合はそっち使った方が速いです。
querySelectorAll
※ 追記 querySelectorはIE8からサポートされてましたm(__)m。@teppeisさんご指摘ありがとうございますー!

1
var list = document.querySelectorAll('ul li');

classListはIE10から、datasetはIE11からの実装のようです。。。

ECMA Script 5

Array

Array型の追加メソッドはunderscoreとか使ってる人ならピンとくるはず。  
  • forEach
    引数に関数をとり、値とインデックスがループでまわって引数に入ってくる。ようはfor文回すのに、iとかlengthとか必要ないし、 ループ内スコープの変数が使えるってこと。
    forEach
1
2
3
4
5
6
7
8
9
var arr = ['a', 'b', 'c', 'd', 'e'];

arr.forEach(function(value, index) {
    // function内部なので、スコープがループの中で閉じる
    var foo = 'bar';

    console.log(value, index);
    // -> 値とインデックスがコンソールに出力される
});
  • filter
    配列の中から指定した条件を満たすものだけを取り出して新たな配列を作成することができる。非破壊的メソッド。
    filter
1
2
3
4
5
6
var arr = [2, 45, 21, 67, 32, 2, 43];

arr.filter(function(value, index) {
    // 偶数のものだけ取り出す。
    return value % 2 === 0;
});
  • indexOf
    配列を指定された値で検索するメソッド。返却値は見つかった最初のインデックスで、ない場合は-1が返る。
    検索を始めるインデックスを第二引数で指定することも可能。
    indexOf
1
2
3
4
5
6
var arr = ['abc', 'def', 'ghi', 'jkl'];

console.log(arr.indexOf('def'));
// -> 1
console.log(arr.indexOf('mno'));
// -> -1
  • some
    配列の各要素に対してテストを実行し、一つでもテストに合格する要素があればtrueを返し、一つもなければfalseを返す。
    some
1
2
3
4
5
6
7
8
9
10
11
12
13
var arr = [2, 45, 21, 67, 32, 2, 43];

// 60以上の値があるかどうか判定
console.log(arr.some(function(value) {
    return 60 < value;
}));
// -> true

// 70以上の値があるかどうか判定
console.log(arr.some(function(value) {
    return 70 < value;
}));
// -> false
  • every
    配列の各要素に対してテストを実行し、全要素がテストに合格であればtrueを返し、一つでも欠ければfalseを返す。
    every
1
2
3
4
5
6
7
8
9
10
11
12
13
var arr = [2, 45, 21, 67, 32, 2, 43];

// 全ての値が1以上かどうか判定
console.log(arr.every(function(value) {
    return 1 < value;
}));
// -> true

// 全ての値が5以上かどうか判定
console.log(arr.every(function(value) {
    return 5 < value;
}));
// -> false
  • map
    配列の各要素に対して処理を行い、その結果からなる新しい配列を生成する。非破壊的メソッド
    map
1
2
3
4
5
6
7
var arr = [2, 45, 21, 67, 32, 2, 43];

// 全ての値が1以上かどうか判定
console.log(arr.map(function(value) {
    return value * 10;
}));
// -> [20, 450, 210, 670, 320, 20, 430]
  • reduce
    配列の隣り合う値を左から順に呼び出し、その結果から単一の値をつくる。
    ※ 右から実行するreduceRightもある。
    reduce
1
2
3
4
5
6
7
8
var arr = [2, 45, 21, 67, 32, 2, 43];

// 全ての値が1以上かどうか判定
console.log(arr.reduce(function(value1, value2) {
    console.log(arguments);
    return value1 + value2;
}));
// -> 212

Object

  • defineProperty / defineProperties
    -Object.hoge = function(){};の形で書くのに比べ、writable属性、enumerable属性、configurable属性の設定ができたり、アクセサ(配列のlengthみたいなやつ)がつくれたりする。
    • writable: 上書き可能かどうか
    • enumerable: for inループなどのObjectのプロパティ列挙時に表示されるかどうか。
    • configurable: プロパティの設定を変更することができるかどうか。
    • get: プロパティのゲッターとなるメソッド。(アクセサの場合のみ)
    • set: プロパティのセッターとなるメソッド。(アクセサの場合のみ)
      ※ 個人的にはprototype拡張の際によく使うが、様々な用途で使えるとは思う。
      defineProperty
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
var MyClass = function() {
    this.members = {};
};

// メンバーを登録するsetMemberメソッドを作成
Object.defineProperty(MyClass.prototype, 'setMember', {
    writable: false,
    enumeable: false,
    configurable: false,
    value: function(id, name) {
        this.members[id] = name;
    }
});

// メンバーを取得するgetMemberメソッドを作成
Object.defineProperty(MyClass.prototype, 'getMember', {
    writable: false,
    enumeable: false,
    configurable: false,
    value: function(id) {
        return this.members[id];
    }
});

// 現在のメンバー数を返却するcountアクセサを追加。値を指定された場合は何もしない。
Object.defineProperty(MyClass.prototype, 'count', {
    get: function() {
        return Object.keys(this.members).length;
    },
    set: function() {}
});

var myClass = new MyClass();

myClass.setMember('foo', 'bar');
myClass.setMember('hoge', 'hogehoge');
console.log(myClass.getMember('foo'));
// -> 'bar'

console.log(myClass.count);
// -> 2

// setメソッドは何もしないので値を登録しても値は変化しない。
myClass.count = 10;
console.log(myClass.count);
// -> 2
  • create
    指定したオブジェクトのプロトタイプ及び、プロパティを持つオブジェクトを生成する。簡単に既存のオブジェクトを継承して、新しいプロトタイプも持つオブジェクトがつくれる。
    definePropertyと同様に、各属性の設定や、アクセサもつくれる。
    create
1
2
3
4
5
6
7
8
9
10
11
12
13
// 通常の配列を追加
var arr = ['a', 232, 'b', true, 1111];
// 配列を拡張して、0から数えたlengthプロパティを新たに追加。
var customArr = Object.create(arr, {
    lengthFromZero: {
        get: function() {
            return this.length - 1;
        },
        set: function() {}
    }
});

console.log(customArr.lengthFromZero);
  • getOwnPropertyNames
    enumerable属性に関わらず、オブジェクトのプロパティを配列で返却する。
    getOwnPropertyNames
1
2
console.log(Object.getOwnPropertyNames(Array.prototype));
// -> ["length", "constructor", "toString", "toLocaleString", "join", "pop", "push", "concat", "reverse", "shift", "unshift", "slice", "splice", "sort", "filter", "forEach", "some", "every", "map", "indexOf", "lastIndexOf", "reduce", "reduceRight"]
  • keys
    オブジェクトの列挙可能なプロパティを配列で返却する。for-inループと違い、プロトタイプのプロパティはとってこない。
    keys
1
2
3
4
5
6
var obj = {
    foo: 'bar',
    hoge: 'hogehoge'
};
console.log(Object.keys(obj));
// -> ['foo', 'hoge']
  • freeze
    オブジェクトを凍結する。具体的にはプロパティの追加/編集/削除、設定変更を不可能にする。プロパティがObjectの場合、その中身までは凍結できない。
    freeze
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var obj = {
    foo: 'bar',
    hoge: 'hogehoge',
    list: {
        foofoo: 'barbar'
    }
};

// オブジェクトを凍結
Object.freeze(obj);

// obj.list.foofooのみ編集可能。
obj.hoge = 'hogehogehoge';
obj.name = 'taro';
obj.list.foofoo = 'barbarbarbar';
console.log(obj);

Function

  • bind Functionのthisを引数のオブジェクトでバインドする。apply/callと違ってその場でfunctionコールせず、thisでbindされたfunctionオブジェクトを返却してくれる。
    ※ $.proxyとか_.bindと同じ。
    bind
1
2
3
4
5
6
7
8
function foo() {
    console.log(this);
}

var bar = foo.bind({hoge: 'hogehoge'});

bar();
// -> {hoge: 'hogehoge'}

String

  • trim / trimRight / trimLeft
    念願のtrimがようやくjsに。文字列の前後の空白を削除する。非破壊的メソッド
    ※ 驚くべきことに全角空白にも対応している!!!
    trim
1
2
3
4
var foo = '      bar      ';

console.log(foo.trim());
// -> bar

Date

  • now
    UTC(協定世界時)での1970年1月1日00時00分00秒から現在までの経過ミリ秒を数値で取得するメソッド。new Date().getTime()と同じ。相違点はスピード。
    now
1
console.log(Date.now());

IE8とどうしてもまだ付き合わなきゃいけない方へ

es5のshimを使ってくださってる方がいるので、これを入れると上記のメソッド達はだいたい使えます。
https://github.com/kriskowal/es5-shim

DOM系のやつもそれぞれshimを作ってる方がいたり、あとは自分でpolyfill作ってしまってもよいかもですね。

まとめ的な

HTML5とかCSS3はまだだいぶ弱いですが、DOM系とES5系でそれなりに強化されるので、小さいサイトの制作であればjQueryを使わない選択肢があってもいいんじゃないかなーと思ってます。
まあこのへんが前からサポートされてるスマートフォン向けサイトでも、Zeptoすらあまり使われずjQueryが当たり前に使われてる現状を考えると難しそうですが。。。
ネイティブのjsでかかれたプラグインが充実してくれば変わってくるのかな。

HTML5

HTML5のAPIについてはいろんなところにサンプルあるので詳細は割愛しますー。
やはりCanvasとSVGが目立つところですかね。

canvas

SVG

Sectioning Elements

Geolocation

Video(H.264) not Mpeg4, WebM

Audio(AAC and MP3)

IE10+ (HTML5)

ちゃんと洗い出1してないのでこれだけではないと思いますが、IE10まで移行できれば強力なAPIが目白押しですね。

History API

websocket

input[type=xxx]

Form VDalidation

Pointer Events

XHR2

Drag and Drop

Native Binary Data

Web Workers

CORS

Sandboxed iframe

Application cache

Indexed DB

File API

pagevisibility API

CSS3 Animations

さらにES6も使いたい方へ

ES6では、定数、ブロックスコープ変数、Class、Module、Promise、generator、iterator、配列内包表記、arrow function、Map、WeakMap、Set、of loopなどなどまさに夢のようなAPI達がこれでもかと使うことができます。現在の実装状況は下記を参照すると良いです。
http://kangax.github.io/es5-compat-table/es6/ 見ていただければわかりますが、FFの実装が最も進んでいます。Chromeも頑張ってはいますが、FlagをONにしなきゃいけないので非現実的ですね。
まとめると以下の方法が現在ES6を使うための手法です。

主要な機能は大体使えます。具体的な使い方はこんな感じ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<html>
  <body>
    <script src="https://traceur-compiler.googlecode.com/git/bin/traceur.js"
        type="text/javascript"></script>
    <script src="https://traceur-compiler.googlecode.com/git/src/bootstrap.js"
        type="text/javascript"></script>
    <script type="text/traceur">
      class Greeter {
        constructor(message) {
          this.message = message;
        }

        greet() {
          let element = document.querySelector('#message');
          element.innerHTML = this.message;
        }
      };

      let greeter = new Greeter('Hello, world!');
      greeter.greet();
    </script>
  </body>
</html>
  • typescriptを使う。 MicroSoftの開発しているtypescriptはES6の先行実装的な思想で作られてるjsのプリコンパイラなので、これを使う手もあります。
    typescript
  • node.jsを使う。
    Gruntなどサーバを立てる用途でなくてもnodeを使う機会は確実に増えている今日この頃、これを使わない手はありません。
    node起動時に—harmonyオプションをつけるか、Gruntなど内部的にNodeを呼ぶやつで使う場合はpackage.jsonをちょっと工夫してやるとES6の機能が開放されます。
1
2
3
4
5
{
  "scripts": {
    "start": "node --harmony ./node_modules/.bin/grunt start"
  }
}
  • FirefoxOSアプリ開発を行う。
    上記でも言ったようにFirefoxの実装は最も進んでいる状況なので、FirefoxOS向けアプリ開発ではES6の機能がガンガン使えます。
    本来Web標準で作れることのメリットであるクロスプラットフォームの互換性は失われますが、そんなものは現場からすると初めから幻想なのでムシムシ。
  • RhinoもしくはNashornを使う。
    これはだいぶマニアックな方法ですがMozillaがJVM上でjavascriptで書いたコードをJavaに変換して実行するライブラリを出しています。
    MozillaということはFirefoxと大体同じ実装状況だと言えるので、ES6の機能もだいぶサポートされています。
    Java7までがRhino、Java8からがNashornになります。
    ちなみにClosure CompilerはRhinoを使ってますね。

というわけでここまで読んでいただいてありがとうございましたー!


Hello World

Blog始めました

本日初のAdvent Calendarを投稿するにあたってGithub + Octopress + Gitbucketを利用してBlogを作成しました。
みんな大好きmarkdownで記述できるのはものすごく楽ですね。もちろんHTMLも使えますが。
構築には下記のサイトを参考にさせていただきましたので、興味のある方はやってみてください。

http://morizyun.github.io/blog/octopress-gitpage-minimum-install-guide/