LoginSignup
15
15

More than 5 years have passed since last update.

今さらBrowserifyを使ってみる(ES6,Stylus,Jadeの開発環境構築)

Posted at

はじめに:動機

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')するだけで依存解決できたらいいよね! というものだ。

基本的な使い方はかんたん。

  1. ブラウザでnpmのモジュールを使用する
  2. ブラウザのJSの依存関係を解決する

この二点について紹介する(コードは一部GitHub:substack/browserify-handbookより)。

1. npmのモジュールを使用する

npmでBrowserifyをインストールする。

$ npm install -g browserify

まずは普通にnodeを使っていく。例えば、uniqライブラリを導入して配列の重複排除をするコードを書こう。

$ npm install uniq

num.jsを作成して以下を書き込む。

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を作成して以下を書き込み、ブラウザで表示してみよう。

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があり矢印のような依存関係があるとする。

dependency.png

これをブラウザ上で正常に動作させるためには以下の順で読み込んであげる必要がある。

index.html
<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つのファイルの場合は以下のように書ける。

a.js
var b = require('./b');
var c = require('./c');
...
b.js
var c = require('./c');
...
module.exports = b;
c.js
...
module.exports = c;
index.html
<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で作る開発環境

around_browserify.png

この図は今まで紹介した Minimal な実行環境とこれから紹介する Development 環境の簡略図だ。CSSやHTMLが絡んできて自動コンパイル等もやろうとすると一気に複雑になる^^;

  1. AltJS(ES6)、AltCSS(Stylus)の変換(Browserify)
  2. Jadeの変換(gulp)
  3. 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)。

index.jade
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
sub.jade
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
index.js
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();
}
sub.js
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();
}
_cat.js
export default class Cat {
  constructor(name) {
    this.name = name
  }

  meow() {
    return this.name + ': "meow!"';
  }
}
index.styl
body
  color red
sub.styl
body
  color blue

1. AltJS(ES6)、AltCSS(Stylus)の変換(Browserify)

gulp buildの解説とも言える。

gulpfiles/tasks/build.coffee
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の解説とも言える。

gulpfiles/tasks/jade.coffee
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の解説とも言える。

gulpfiles/tasks/watch.coffee
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を参考に処理が落ちないようにした。

ここまでのまとめ

三つのタスク(buildjadewatch)を使って静的サイト開発環境を構築した。CLIとは違った使用方法だったが、nodeのストリームが分かるとすんなりと理解できるのかなと調べていて思った。あと、巷ではbuildタスクとwatchタスクをまとめてif分で分岐しているコードがよくあったけど、僕的には見づらいと思ったので重複承知で分けて書くようにした。

最後にpackage.jsonとGulpfile.coffee、gulpfiles/config.coffeeを書いておく。gulp関連はこれからはじめるGulp(7):require-dirモジュールを使ったタスク単位のファイル分割を参考にすると良い。

package.json
{
  "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"
  }
}
Gulpfile.coffee
requireDir = require 'require-dir'
requireDir './gulpfiles/tasks', recursive: true
gulpfiles/config.coffee
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モジュールを使ったタスク単位のファイル分割

15
15
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
15
15