Kekeの日記

エンジニア、読書なんでも

Browserifyの上級指定教科書『Browserify Handbook』を読みすすめる

f:id:bobchan1915:20190221043054p:plain

動機

ハリーポッターの作品中には『幻の動物とその生息地』のようにホグワーツ指定の教科書があります。

Browerifyも同様に、Browserifyを学ぶにあたって必要な上級教科書『Browserify Handbook』があります。 上級といっても非常にシンプルにわかりやすく解説されているので、エンジニア1年生でも読み解くことはできるでしょう。

ただ、Browerify自体もあまり日本語記事がなく、この時期だとWebpackの台頭でもうほとんど使っている人がいないかもしれませんが、Webpackのさらなる理解のためにも、今回書き起こしておこうと思いました。

やはり、ドキュメントをしっかり読んでこそ理解できるのでHandbookなるものがあるのならばしっかり読み、基礎ができていないと怒られないようにします。

Harry Potter Slapping GIF - Find & Share on GIPHYgph.is

それでは解説を始めていこうと思います。

アジェンダ

今回の記事のアジェンダはこちらになります。

Browserify

まずはBrowserifyについて考えて行きましょう。

Browserifyとは

f:id:bobchan1915:20190221043054p:plain

Browserifyとは、

Browserify lets you require('modules') in the browser by bundling up all of your dependencies.

と公式ページにもあるように、依存関係をまとめてくれるもので、またブラウザ上でもrequireという書き方を可能にしてくれます。

またNode.jsで使っていたライブラリをフロントエンドでもrequireとして使うことができるようになるためサーバーサイドのNode.jsを開発しているときに使っていたnpmを同じような感覚で使うことができます。

Browserifyの使い方

このセクションは、あくまでも基本的な使い方、仕組みを解説しているだけです。

Hello Worldでの実例

例えば、以下のようにuniqライブラリを使ったmain.jsがあるとします。

var unique = require('uniq');

var data = [1, 2, 2, 3, 4, 5, 5, 5, 6];

console.log(unique(data));

uniqをインストールしなければならないので以下のようにインストールをします。

npm install uniq

そして、あとは魔法を唱えるだけです。

Harry Potter GIF - Find & Share on GIPHYgph.is

実際には以下のようにmain.jsと、その依存パッケージであるuniqbundle.jsファイルをしてまとめます。

browserify main.js -o bundle.js

なお、-oを指定しない場合は標準出力されるため

browserify main.js > bundle.js

とすることも可能です。

するとHTML内では<body>タグの最後に

<script src="bundle.js"></script>

として書くことができてuniqについて心配する必要がありません。

普通ならば

<script src="url/uniq"></script>
<script src="main.js"></script>

と書かなければエラーとなったでしょう。そもそそブラウザではNode.jsで使うようなrequireは使うことはできないので、以下のようなエラーがでます。

f:id:bobchan1915:20190221044757p:plain

これが、例えば

var foo = require('./foo.js');
var bar = require('../lib/bar.js');
var gamma = require('gamma');

var elem = document.getElementById('result');
var x = foo(100) + bar('baz');
elem.textContent = gamma(x);

のようになっていたとしても、browserifyによってrequireを解析してくれ、まとめてくれます。

軽くサンプルはわかったかなと思います。少し体系的に調査をしたいと思います。

どのようにbundleされるのか

例えば、以下のような依存関係のあるJavascriptファイルを作成してみましょう。

a.js

module.export.sayYourName = function (){
    console.log("A")
}

b.js

module.export.sayYourName = function (){
    console.log("B")
}

main.js

const a = require('./a.js')
const b = require('./b.js')

function sayYourNames() {
     a.sayYourName()
     b.sayYourName()
}

sayYourNames() // Note: 今だけ追加

これを実行すると

node main.js
=> 
A
B  

という結果になります。図示すると以下のような関係になっています。

f:id:bobchan1915:20190221045919p:plain

さきほどの関数をexportしておきましょう。

module.exports.sayYourNames = function (){
     ...
}

これを以下のように出力します。

browserify main.js > bundle.js

図では、このようになったというイメージができます。

f:id:bobchan1915:20190221050133p:plain

これを<script>で入れてみて、ChromeのDevToolのSourceタブでみてみます。

するとすると以下のようになっていました。

(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
module.exports.sayYourName = function (){
    console.log("A")
}
},{}],2:[function(require,module,exports){
module.exports.sayYourName = function (){
    console.log("B")
}
},{}],3:[function(require,module,exports){
const a = require('./a')
const b = require('./b')

module.exports.sayYourNames = function(){
    a.sayYourName()
    b.sayYourName()
}
},{"./a":1,"./b":2}]},{},[3]);

コンパクトに一つにまとめられているようです。

主要なオプション

  • --plugin, -p: プラグインを指定して使用する
  • --require, -r: bundle.require()で使うファイルを指定
  • --outfile, -o: 指定したファイルに出力をする
  • --external, -x: 他のBundleからファイルを参照する

外部Require

f:id:bobchan1915:20190221055222p:plain

バンドルするファイルでrequire()しているパッケージからもrequire()として公開することができます。

例えば、前の例のa.jsb.js、それに依存するmain.jsを使用するとしましょう。ここで外部ライブラリchroma-jsを追加しました。

const a = require('./a')
const b = require('./b')
const chroma = require('chroma-js')

module.exports.sayYourNames = function(){
    a.sayYourName()
    b.sayYourName()
}

そして、今回はこのmain.jschroma-jsを使いたいだけでなくて、HTMLの別の<script>タグでも使用したいので、以下のように外部公開をします。

browserify index.js --require chroma-js > bundle.js

そして、以下のようにインラインスクリプト内でrequire()しても使うことができます。

<body>
    <p>Hello, World</p>
    <script src="bundle.js"></script>
    <script>
        var chroma = require('chroma-js')
    </script>
</body>

非常に簡単にNode.jsで使っていたライブラリを使うことができます。

Sarcastic Harry Potter GIF - Find & Share on GIPHYgph.is

Multi Bundle

ここまでは単一bundleファイルにまとめて利益を得ていました。

例えばAdminページとPublicページがあるときに、必要なJavascriptファイルは異なる可能性が高いです。

それなのに、一つにまとめてしまうと、あらゆるところで必要のないファイルを読み込んでしまい、それがコストとなり、Webページの読み込みが遅くなる原因にもなります。

f:id:bobchan1915:20190221062016p:plain

なるべく、必要なJavascriptファイルは必要なページで読み込むようにします。図の例だと、単一ファイルで依存関係が見えないのでメリットがわかりづらいですが、要点は捉えていると思います。

f:id:bobchan1915:20190221062326p:plain

あるファイルadmin.jsに以下のようなファイルがあります。

var robot = require('./robot.js');
console.log(robot('beep'));

そしてpublic.jsに次のようにします。

var robot = require('./robot.js');
console.log(robot('public'));

そして両方が依存しているrobot.jsは以下のようになっています。

module.exports = function (s){
    return s.toUpperCase() + '!'
};

そして、以下のように実行します。

// 「外部Require」のようにrobotを公開します
$ browserify -r ./robot.js > static/bundle.common.js
$ browserify -x ./robot.js admin.js > static/bundle.admin.js
$ browserify -x ./robot.js public.js > static/bundle.public.js

二つのページには、一つはbeepを必要とするファイルを以下のように

<script src="common.js"></script>
<script src="beep.js"><script>

して、もうひとつはboopを必要とするファイルを以下のようにして使うことができます。

<script src="common.js"></script>
<script src="boop.js"><script>

これは二つのステップで構成されています。

  1. -rによって外部requireとして公開しつつもcommon.jsを作成する
  2. -xで他のBundleファイルを参照しつつ、共通要素を取り除く

f:id:bobchan1915:20190221063848p:plain

上級指定教科書『Browerify Handbook』

それでは本題のドキュメントを読んでいきます。

Animated GIF - Find & Share on GIPHYgph.is

実際のドキュメントは以下のURLです。

github.com

すべてのセクションは取り上げませんが、いくつか有用なものを解説していこうと思います。

章わけをしていますが、雰囲気を出すためであり、ドキュメントはMarkdownで書かれています。

1章: Browserifyの基礎。Hello, worldと他の出力

f:id:bobchan1915:20190221090244p:plain

また、基礎の確認になります。library.jsに以下のようなファイルがあったとします。

module.exports = function (name){
     return 'Hello, world ' + name;
};

そしてmain.jsを以下のようにします。

var library = require('library.js');
library('I am Keke');

すると以下のような出力があります。

Hello, world I am Keke

また、関数だけでなくmodule.exportsで何もかも出力ができます。先ほどのlibrary.jsを以下のように

module.exports = 'Hello directly'

とすることもできます。

もちろんexportsでも渡すことはできますが、これはBrowerifyに限った話ではないので割愛させていただきます。

2章: Auto Recompile

f:id:bobchan1915:20190221090704p:plain

コードの変更を監視して、自動的にbundleをし直してくれるパッケージがあります。

  • watchify: もっともスタンダードなパッケージ
  • beefy: Webサーバーを立てて自動でバンドルしてくれる
  • wzrd: beefyに機能は似ているが、より軽量なパッケージ

他にもいくつかありますが、ここではこの3つだけの紹介にします。

3章: Browerify APIを直接使う

Browserify APIを使って、JavascriptからNode.jsで実行してしまうパターンです。

var browserify = require('browserify');
var b = browserify();
b.add('./browser/main.js');
b.bundle().pipe(process.stdout);

GulpやGruntで使用することができます。

4章: Non Javascriptアセットの変換

f:id:bobchan1915:20190221095803p:plain

Broweserify transformを使って、いろんな非Javascriptファイルをbundleファイルの中に入れることができます。

もっとも簡単なのがbrfsを使うことです。

browserify -t brfs main.js > bundle.js

をすると、ファイルをそのまま取得することができます。

var html = fs.readFileSync(__dirname + 'robot.html', 'utf8');

var html = "<p>Hello</p>"

と同じになります。bundleファイル内で取得することができます。

もちろん、CSSも取り込むこともできますし、まだまだいろんな種類のファイルをbundleの中に取り込めます。

var fs = require('fs');
var insertStyle = require('insert-css');

var css = fs.readFileSync(__dirname + '/style.css', 'utf8');
insertStyle(css);

5章: Bundlingを完全にマスターする

ここではBundlingについて、もうすこしだけ詳しく書いていこうと思います。

容量を抑える

npmでは、重複した依存関係はインストールするパッケージが増えるごとに自動的は排除されず、npm dedupeによって行うことができます。それかnode_modules/を一旦、削除して再度インストールするという手があります。

Browserifyは同じファイルを二度使うことはありませんが、バージョンが別だとうまくはいきません。

そのようなときは

browserify --list

browserify --deps

を行って重複に対して調査をしてみてください。

tinify

tinifyプラグインを用いてさらに容量を抑えることができます。

browserify hoge.js --plugin tinify > bundle.js

tinifyとはBrowserifyのpluginで、いくつもの最適化をしてくれるものです。

github.com

詳しい内容は上記のリポジトリでご覧ください。

特定のモジュールを空オブジェクトで置き換える: Ignore

ある特定のモジョールを使いたくない時は以下のように

browserify --ignore chroma-js

と指定すると無視をすることができる。

特定のファイルを取り除く: Exclude

例えばすでに他のライブラリに取り込まれている時、別のbundleではrequire()ができないようにしたい時があるかもしれません。

図示すると以下のような状況下のときです。

f:id:bobchan1915:20190221091256p:plain

例えばmain.jsjQueryを使っていたとします。

var $ = require('jquery');
$(window).click(function () { document.body.bgColor = 'red' });

そしてすでに

browserify -r jquery > bundle.jquery.js

で使っているときに再度、jqueryを入れたくないので

browserify main.js --exclude jquery > bundle.js

とすることによって重複を避けることができます。APIからは

b.exclude('hoge')

のように使用することができます。

6章: Transform

Javascriptの仕様や、CoffeeScriptなどを変換してくれるものです。他にもCSS飲めた言語を変換してくれたり多機能です。

変換できるものは、以下のリンクにありますが、膨大な数ほどあります。

github.com

例えば、ここでCoffeeScriptの簡単なスクリプトを書いてみましょう。

f:id:bobchan1915:20190221092832p:plain

hello = () -> console.log("Hello from Coffee Script")

hello()

そして以下のようにバンドルします。

$ npm install coffeeify coffeescript
$ browserify -t coffeeify main.coffee > bundle.js

すると以下のように出力がされます。

(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
var hello;

hello = function() {
  return console.log("Hello from Coffee Script");
};

hello();


},{}]},{},[1]);

まとめ

今回の記事でBrowserifyへの理解が深まり、より簡単にアプリケーションを効率よく使うことができそうです。

次はWebpackについて記事をまとめたいと思います。

ありがとうございました。