はじめに:動機
WebpackとBrowserifyといえば言わずと知れたJSの依存関係解決ツールだけど、僕がこれらに出会ったのは1年ほど前だった。当時JSで大規模なプログラムを開発する話が出てきて、そこで初めてBrowserifyとWebpackを触ってみた。その時はWebpackの一元性に惹かれて(覚えるのが楽だった)、Webpackを開発で使用することにした。
しかし、色々あってその開発が頓挫。1年ほどJSを書く機会があまりなく最近まで過ごしてきた。1年……、JS界でこのブランクは致命的だ……! ES6とかまだ先の話だろとタカをくくってたら、6to5からのBabelの登場で、みんな「class! class! promise! promise!」って勢いでES6構文でJS書いてる……。AltJSとしてはCoffeeScriptに親しんできてプリコンパイル自体には気後れはないけど、少しついていけない。というわけで、まずは止まった足元からということでJSの依存関係解決ツールについて勉強し直した。
どっちを勉強し直すか迷ったけど、今はBrowserifyの方がElectronアプリの開発環境とかで使われている印象があって興味を持ったのでそっちから。
Browserify:nodeで出来ることをブラウザでも
Browserifyは、npm上のパッケージをnodeと同じようにブラウザでも使えるようにするツール。また、その機構を用いることで、JSの依存関係の解決に困っていたブラウザ上でもrequire('hogehoge')
するだけで依存解決できたらいいよね! というものだ。
基本的な使い方はかんたん。
- ブラウザでnpmのモジュールを使用する
- ブラウザのJSの依存関係を解決する
この二点について紹介する(コードは一部GitHub:substack/browserify-handbookより)。
1. npmのモジュールを使用する
npmでBrowserifyをインストールする。
$ npm install -g browserify
まずは普通にnodeを使っていく。例えば、uniqライブラリを導入して配列の重複排除をするコードを書こう。
$ npm install uniq
num.jsを作成して以下を書き込む。
var uniq = require('uniq');
var nums = [ 5, 2, 1, 3, 2, 5, 4, 2, 0, 1 ];
console.log(uniq(nums));
nodeで実行する。
$ node nums.js
[ 0, 1, 2, 3, 4, 5 ]
この処理をブラウザで実行できるようにする。以下のコマンドを実行してBrowserifyでブラウザ用の実行ファイルを作成しよう。
$ browserify num.js > bundle.js
Browserifyでは変換結果を標準出力に出すので>
演算子を使うことで変換結果ファイルに書き込むことができる。
index.htmlを作成して以下を書き込み、ブラウザで表示してみよう。
<html>
<body>
<script src="bundle.js"></script>
</body>
</html>
インスペクタのコンソールにArrayオブジェクト([0, 1, 2, 3, 4, 5]
)が表示されたと思う。まるで魔法のようだ。でもbundle.jsを見てみると仕組みがうっすらと分かる。大雑把に言ってnpmモジュールをconcat
してるわけだね。
2. ブラウザのJSの依存関係を解決する
大雑把に言い切ったけど、Browserifyで行っていることはれっきとしたCommonJSスタイルのモジュール機構だ。したがって依存関係についてもnodeと同様に解決してくれる。
例えば下図のようにa.jsとb.jsとc.jsがあり矢印のような依存関係があるとする。
これをブラウザ上で正常に動作させるためには以下の順で読み込んであげる必要がある。
<html>
<body>
<script src="c.js"></script>
<script src="b.js"></script>
<script src="a.js"></script>
</body>
</html>
今回は3つのファイルだけだったので簡単に依存関係が分かった。でも、これが5つになると考えるのが億劫になり10つを超える数になってくると考えただけで……おぅ。自動でやってもらいたいね。Browserifyではそれぞれをrequire()
することで、読み込み順序を勝手に解決してくれる。例えば3つのファイルの場合は以下のように書ける。
var b = require('./b');
var c = require('./c');
...
var c = require('./c');
...
module.exports = b;
...
module.exports = c;
<html>
<body>
<script src="bundle.js"></script>
</body>
</html>
$ browserify a.js > bundle.js
矢印の数だけrequire()
すれば良い! かんたん!
ここまでのまとめ
Browserifyはnode環境で実行するのと同じようにブラウザでも実行できるようにするツール。ブラウザでnpmのモジュールを使用できるようになり、ブラウザのJSの依存関係を解決してくれた。
しかし、Browserify(とその周辺ツール)でできることはこれだけではない! Browserify自身もモジュールなので、もちろん他のモジュールと組み合わせることで拡張することが可能なのだ。この機能を使うことでAltJS/CSSのコンパイルやファイルの変更監視などがBrowserifyと組み合わせて使用することができるようになる。
実際、この使い方のほうが一般的なので次の章でBrowserifyで作れる静的サイト開発環境をまとめた。
Browserifyで作る開発環境
この図は今まで紹介した Minimal な実行環境とこれから紹介する Development 環境の簡略図だ。CSSやHTMLが絡んできて自動コンパイル等もやろうとすると一気に複雑になる^^;
- AltJS(ES6)、AltCSS(Stylus)の変換(Browserify)
- Jadeの変換(gulp)
- watch(gulp-watch/watchify)とlivereload(BrowserSync)
の三つに分けて紹介する。タスクランナーはお分かりの通りgulp
を使っている。一言でこの開発環境を言うと「Gulp/Browserify/BrowserSyncで作る静的サイト開発環境」かな?
0. ファイル構成
.
├── Gulpfile.coffee
├── package.json
├── readme.md
├── dist
│ ├── img
│ ├── index.html
│ ├── sub.html
│ └── js
│ ├── index.js
│ └── sub.js
├── gulpfiles
│ ├── config.coffee
│ └── tasks
│ ├── build.coffee
│ ├── default.coffee
│ ├── jade.coffee
│ └── watch.coffee
└── src
├── index.jade
├── sub.jade
├── css
│ ├── index.styl
│ └── sub.styl
└── js
├── _cat.js
├── index.js
└── sub.js
ファイル構成はこんな感じ。gulpタスクはgulpfiles/tasks以下に置いていて、各タスク(build
,jade
,watch
)を使ってsource以下のjade,styl,js(ES6)をdist以下に変換出力する。entryは複数(index.js, sub.js)に分けられるようにしている。プロダクトコードはES6だけどgulpタスクはcoffeeで書いてる。coffee読み返しやすくてまじ神。
複数のentryを試したかったのでindex.jadeとsub.jadeの二つのHTMLを用意してそれぞれ別のJSファイルを読み込むようにした(index.jsとsub.js)。
doctype html
html
head
title Browserify boilerplate
meta(content="text/html; charset=utf-8", http-equiv="Content-Type")
meta(content="ja", http-equiv="Content-Language")
meta(name="viewport", content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=yes")
meta(name="description", content="description")
meta(name="keywords", content="keywords")
script(type="text/javascript" src="js/index.js")
body
a(href="sub.html") link to sub
#content
doctype html
html
head
title Browserify boilerplate
meta(content="text/html; charset=utf-8", http-equiv="Content-Type")
meta(content="ja", http-equiv="Content-Language")
meta(name="viewport", content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=yes")
meta(name="description", content="description")
meta(name="keywords", content="keywords")
script(type="text/javascript" src="js/sub.js")
body
a(href="index.html") link to index
#content
var insertCss = require('insert-css');
insertCss(require('../css/index.styl'));
import Cat from './_cat.js';
window.onload = function() {
var cat = new Cat('Mimi');
var $content = document.querySelector('#content');
$content.innerHTML = cat.meow();
}
var insertCss = require('insert-css');
insertCss(require('../css/sub.styl'));
import Cat from './_cat.js';
window.onload = function() {
var cat = new Cat('Mel');
var $content = document.querySelector('#content');
$content.innerHTML = cat.meow();
}
export default class Cat {
constructor(name) {
this.name = name
}
meow() {
return this.name + ': "meow!"';
}
}
body
color red
body
color blue
1. AltJS(ES6)、AltCSS(Stylus)の変換(Browserify)
gulp build
の解説とも言える。
gulp = require 'gulp'
config = require '../config'
browserify = require 'browserify'
babelify = require 'babelify'
uglifyify = require 'uglifyify'
stylify = require 'stylify'
source = require 'vinyl-source-stream'
plumber = require 'gulp-plumber'
notify = require 'gulp-notify'
glob = require 'glob'
path = require 'path'
gulp.task 'build', ['jade'], ->
files = glob.sync config.es6
files.forEach (file) ->
browserify
entries: file
extensions: config.browserify.extensions
.transform babelify
.transform stylify
.transform {global: true}, uglifyify
.bundle()
.pipe source path.basename(file, path.extname(file)) + '.js'
.pipe gulp.dest config.es5
ここではプロダクションで使用するためのコードをはくタスクを紹介する。browserifyはnpmのストリーム処理に対応しているので、たいていの処理はbrowserifyに任せることにする。
browserifyコマンドの引数で入力ファイルや拡張子を指定、.transform()
でかませたい処理(babelだったりstylusの変換)を指定、.bundle()
でコンパイルする。uglifyifyで{global: true}
にすると出力ファイルに挿入されたnodeモジュールのコードも圧縮できる。.bundle()
後はvinyl-source-streamを使ってnodeのストリームオブジェクトをgulpのvinylオブジェクトに変換、gulp.dest
で出力する。
出力ファイルを複数作りたいのでentryである入力ファイルをglob
で取得、forEach
するようにした。config.es6
がミソで展開するとprocess.cwd() + '/src/js/**/[^_]*.js'
となっており、_
で始まらないJSファイルをentryとして処理するようにしている。
2. Jadeの変換(gulp)
gulp jade
の解説とも言える。
gulp = require 'gulp'
config = require '../config'
jade = require 'gulp-jade'
plumber = require 'gulp-plumber'
notify = require 'gulp-notify'
gulp.task 'jade', ->
gulp
.src config.jade, base: config.jadeBase
.pipe plumber errorHandler: notify.onError('<%= error.message %>')
.pipe jade()
.pipe gulp.dest config.html
普通にjadeのコンパイルしている。出力パスがおかしくなる(dist/src/hogehoge.html
)のでgulp.src
でbaseを指定するのを忘れずに。configオブジェクトは後述している。解説とは言えなかった。
3. watch(gulp-watch/watchify)とlivereload(BrowserSync)
gulp watch
の解説とも言える。
gulp = require 'gulp'
path = require 'path'
config = require '../config'
browserify = require 'browserify'
babelify = require 'babelify'
uglifyify = require 'uglifyify'
stylify = require 'stylify'
watchify = require 'watchify'
source = require 'vinyl-source-stream'
glob = require 'glob'
watch = require 'gulp-watch'
gutil = require 'gulp-util'
browserSync = require 'browser-sync'
notify = require 'gulp-notify'
handleErrors = ->
args = Array.prototype.slice.call(arguments)
notify
.onError {title: 'Browserify Error', message: '<%= error.message %>'}
.apply @, args
@emit 'end'
gulp.task 'watch', ->
browserSync config.browserSync
watch config.jade, -> gulp.start ['jade']
watch config.browserSync.server.baseDir + '/**/*', -> browserSync.reload()
files = glob.sync config.es6
files.forEach (file) ->
b = browserify
entries: file
extensions: config.browserify.extensions
debug: true
cache: {}
packageCache: {}
plugin: [watchify]
.transform babelify
.transform stylify
bundle = (updatedFile) ->
b.bundle()
.on 'error', handleErrors
.pipe source path.basename(file, path.extname(file)) + '.js'
.pipe gulp.dest config.es5
if updatedFile?
updatedFile.map (filename) -> gutil.log 'File updated:', gutil.colors.yellow filename
b.on 'update', bundle
bundle()
タスクの始めの処理ではbrowserSyncを使ってlivereloadするので初期設定をし、gulp-watchでsrc
以下のjadeファイルとdist
以下のすべてのファイルを監視して、jadeコンパイルとlivereloadをまわすようにしている。
開発中はsourcemapを使いたいのでbrowserifyの引数で{debug: true}
を指定している。
browserifyの監視にはwatchifyを使ってる。browserifyの引数で{cache: {}, packageCache: {}, plugin: [watchify]}
を指定すればできる。
watchifyで繰り返したい処理はbundle()
以降なのでそこの部分を関数化してupdate
イベントで実行できるようにした。
browserifyの文脈ではplumberが使えないのでGist:Sigmus/gulpfile.jsを参考に処理が落ちないようにした。
ここまでのまとめ
三つのタスク(build
、jade
、watch
)を使って静的サイト開発環境を構築した。CLIとは違った使用方法だったが、nodeのストリームが分かるとすんなりと理解できるのかなと調べていて思った。あと、巷ではbuild
タスクとwatch
タスクをまとめてif
分で分岐しているコードがよくあったけど、僕的には見づらいと思ったので重複承知で分けて書くようにした。
最後にpackage.jsonとGulpfile.coffee、gulpfiles/config.coffeeを書いておく。gulp関連はこれからはじめるGulp(7):require-dirモジュールを使ったタスク単位のファイル分割を参考にすると良い。
{
"name": "browserify_boilerplate",
"version": "0.1.0",
"description": "boilerplate for browserify",
"main": "dist/index.html",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "kazukitash",
"license": "MIT",
"devDependencies": {
"babelify": "^6.4.0",
"browser-sync": "^2.9.11",
"browserify": "^12.0.1",
"coffee-script": "^1.10.0",
"gulp": "^3.9.0",
"gulp-jade": "^1.1.0",
"gulp-notify": "^2.2.0",
"gulp-plumber": "^1.0.1",
"gulp-util": "^3.0.7",
"gulp-watch": "^4.3.5",
"insert-css": "^0.2.0",
"require-dir": "^0.3.0",
"stylify": "^1.3.1",
"uglifyify": "^3.0.1",
"vinyl-source-stream": "^1.1.0",
"watchify": "^3.5.0"
}
}
requireDir = require 'require-dir'
requireDir './gulpfiles/tasks', recursive: true
path = require 'path'
current = process.cwd()
source = current + '/src'
dist = current + '/dist'
module.exports =
# 入力元の設定
es6: source + '/js/**/[^_]*.js'
jade: source + '/**/*.jade'
jadeBase: 'src'
# 出力先の設定
es5: dist + '/js'
html: dist
# browserifyの設定
browserify:
extensions: ['.js']
# browserSyncの設定
browserSync:
server:
baseDir: dist
port: 3000
全体のまとめ
Browserifyはnpmのエコシステムを最大限に利用するためのツール。そのために自らもいくつものモジュールで構成されており、さらにモジュールを追加することで拡張もできる。多様性を受け入れることで発展を促すことを思想としている。Browserifyでは以下のことができる。
- npmのモジュールを使用する
- ブラウザのJSの依存関係を解決する
- 他のnpmモジュールを使用し拡張することでAltJSやAltCSSのプリコンパイルができる
- 同様にファイルの変更監視も出来る
参考文献
GitHub:substack/browserify-handbook
Gist:substack/browserify_for_webpack_users.markdown
Gist:Sigmus/gulpfile.js
これからはじめるGulp(7):require-dirモジュールを使ったタスク単位のファイル分割