Node.js 実践講座 事例編 その2 | webpackで静的サイトジェネレータを自作する

# はじめに
この記事ではNode.jsのモジュールバンドラであるwebpackを使用して静的サイトジェネレータを構築する方法を紹介します。

# 全体の流れ
全体の流れを下記に示します。

- ワークスペースの作成
- package.jsonの作成 | npm init
- パッケージのインストール | npm install
- ディレクトリとファイルの作成
- まずはwebpackを動かす
- HTML5 Boilerplateで仮組みする
- Jadeで構造を記述する
- Lessで外観を記述する
- JavaScriptで振る舞いを記述する
- Gulpでビルド手順を記述する
- Connectで成果物を確認する

# ワークスペースの作成
コマンドを下記にし示します。

```
mkdir -p ~/workspace/js/practice/static-site-generator
cd ~/workspace/js/practice/static-site-generator
```

# package.jsonの作成 | npm init
コマンドを下記に示します。

```
npm init -f
```

# パッケージのインストール | npm install
コマンドを下記に示します。

```
npm install --save-dev connect css-loader gulp gulp-jade jade jade-loader less-loader rimraf serve-static style-loader webpack webpack-dev-server
```

# ディレクトリとファイルの作成
コマンドを下記に示します。

```
mkdir bin
mkdir public
mkdir src
mkdir src/style
mkdir src/templates
mkdir src/templates/about
mkdir src/templates/layouts
touch bin/www
touch src/style/main.less
touch src/style/sub.less
touch src/templates/about/index.jade
touch src/templates/layouts/default.jade
touch src/templates/index.jade
touch src/entry-dev.js
touch src/entry.js
touch gulpfile.js
touch webpack.config.js
```

# まずはwebpackを動かす

## webpack.config.jsの編集
`webpack.config.js`の内容を下記に示します。

```js:webpack.config.js
'use strict';

var path = require('path')

module.exports = {
  entry: './src/entry-dev.js',
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'js/bundle.js',
  },
  module: {
    loaders: [
      { test: /\.jade$/, loader: 'jade' },
      { test: /\.less$/, loader: 'style!css!less' },
    ],
  },
}
```

## package.jsonの編集
`package.json`を内容を下記に示します。

```json:package.json
{
  "name": "static-site-generator",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node_modules/.bin/webpack-dev-server --inline --history-api-fallback --content-base public --port 8080",
    "build": "node_modules/.bin/gulp build",
    "serve": "node bin/www",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "connect": "^3.4.0",
    "css-loader": "^0.23.1",
    "gulp": "^3.9.0",
    "gulp-jade": "^1.1.0",
    "gulp-less": "^3.0.5",
    "jade": "^1.11.0",
    "jade-loader": "^0.8.0",
    "less-loader": "^2.2.2",
    "rimraf": "^2.4.5",
    "serve-static": "^1.10.0",
    "style-loader": "^0.13.0",
    "webpack": "^1.12.9",
    "webpack-dev-server": "^1.14.0"
  }
}
```

## webpack-dev-serverの起動
コマンドを下記に示します。

```
npm start
```

## 動作確認
ブラウザで http://127.0.0.1:8080/ へアクセスします。現在は`public`ディレクトリに何もないので404 Not Foundが表示されます。

# HTML5 Boilerplateで仮組みする

## ダウンロード
1. http://www.initializr.com/ にアクセスします
2. 1 - Pre ConfigurationでBootstrapをクリックします
3. 2 - Fine Configurationで下記を選びます
  - HTML/CSS Template: Twitter Bootstrap
  - HTML5 Polyfils: Modernizr (Respondのチェックを外します)
  - jQuery: Minified
  - H5BP Optional: すべてのチェックを外します。
4. Download it! ボタンをクリックします
5. ZIPファイルのダウンロードが開始します

## 展開
ダウンロードしたZIPを`public`ディレクトリ内に展開します。

## 動作確認
ブラウザで http://127.0.0.1:8080/ へアクセスするとBootstrapのページが表示されます。

# Jadeで構造を記述する

## index.htmlの編集
`public/index.html`の内容を下記に示します。

```html:public/index.html
<!doctype html>
<html class="no-js" lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <title></title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <link rel="stylesheet" href="/css/bootstrap.min.css">
    <link rel="stylesheet" href="/css/bootstrap-theme.min.css">
    <link rel="stylesheet" href="/css/main.css">

    <!-- separator -->

    <script src="/js/vendor/modernizr-2.8.3.min.js"></script>
  </head>
  <body>

    <!-- separator -->

    <main></main>

    <script src="/js/vendor/jquery-1.11.2.min.js"></script>
    <script src="/js/vendor/bootstrap.min.js"></script>
    <script src="/js/bundle.js"></script>

    <!-- separator -->

  </body>
</html>
```

## entry-dev.jsの編集
`entry-dev.js`の内容を下記に示します。

```js:src/entry-dev.js
'use strict';

require('./entry')
require('./style/main.less')

var context = require.context('./templates/', true, /\.jade$/)

var path = './' + window.location.pathname.slice(1) + 'index.jade'
var template = context(path)

var context = {
  baseUrl: '',
  parts: [],
}

$('main').html(template(context))
```

## テンプレートの編集
`src/templates/`ディレクトリ内の`about/index.jade`、`layouts/default.jade`、`index.jade`の内容を下記に示します。

```jade:src/templates/about/index.jade
extends ../layouts/default

block content
  .container
    p This is about page.

    .row
      .col-md-offset-3.col-md-6
        a.btn.btn-default.btn-block(href='../') Back
```

```jade:src/templates/layouts/default.jade
!= parts[0]

block style

!= parts[1]

nav.navbar.navbar-default.navbar-static-top
  .container
    .navbar-header
      a.navbar-brand(href='#') Project name

block content

.container
  hr
  footer
    p &copy; Company 2015

!= parts[2]

block script

!= parts[3]
```

```jade:src/templates/index.jade
extends ./layouts/default

block content
  .jumbotron
    .container
      h1 Hello, world!
      p
        | This is a template for a simple marketing or informational website. It includes a large callout called a jumbotron and three supporting pieces of content. Use it as a starting point to create something more unique.
      p
        a.btn.btn-primary.btn-lg(href='#{baseUrl}/about/', role='button') Learn more »
  .container
    .row
      .col-md-4
        h2 Heading
        p
          | Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.
        p
          a.btn.btn-default(href='#', role='button') View details »
      .col-md-4
        h2 Heading
        p
          | Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.
        p
          a.btn.btn-default(href='#', role='button') View details »
      .col-md-4
        h2 Heading
        p
          | Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.
        p
          a.btn.btn-default(href='#', role='button') View details »
```

## 動作確認
ブラウザで http://127.0.0.1:8080/ へアクセスするとIndexページが表示されます。ジャンボトロン内のLearn MoreをクリックするとAboutページへ移動します。AboutページのBackボタンをクリックするとIndexページへ移動します。

# Lessで外観を記述する

## main.lessの編集
`main.less`の内容を下記に示します。

```less:src/style/main.less
@import "./sub.less";
```

## sub.lessの編集
`sub.less`の内容を下記に示します。

```less:src/style/sub.less
body {
  background: gray;
}
```

## 動作確認
ブラウザで http://127.0.0.1:8080/ へアクセスすると灰色背景のIndexページが表示されます。

# JavaScriptで振る舞いを記述する
JavaScriptで振る舞いを記述する方法は2つあります。

- `entry.js`に振る舞いを記述する
- Jadeファイルのscriptブロックに振る舞いを記述する

個人的には`entry.js`に関数やクラスを記述してJadeファイルのscriptブロックからそれらを呼び出す書き方が好みです。

```js:src/entry.js
'use strict';

window.printHelloWorld = function () {
  console.log('Hello, world!')
}
```

```jade:src/templates/about/index.jade
'use strict';

extends ../layouts/default

block content
  .container
    p This is about page.

    .row
      .col-md-offset-3.col-md-6
        a.btn.btn-default.btn-block(href='../') Back

block script
  script.
    (function () {
      'use strict';

      printHelloWorld()
    })();
```

# Gulpでビルド手順を記述する

## gulpfile.jsの編集
`gulpfile.js`の内容を下記に示します。

```js:gulpfile.js
'use strict';

var fs = require('fs')
var path = require('path')
var gulp = require('gulp')
var rimraf  = require('rimraf')
var webpack = require('webpack')
var gulpJade = require('gulp-jade')
var gulpLess = require('gulp-less')
var webpackConfig = require('./webpack.config')

gulp.task('clean', function (cb) {
  rimraf('./dist/', cb)
})

gulp.task('copy', ['clean'], function () {
  return gulp.src([
    './public/**',
    '!./public/index.html',
    '!./public/css/main.css',
  ])
    .pipe(gulp.dest('./dist/'))
})

gulp.task('jade', ['clean'], function () {
  var root = fs.readFileSync(path.join(__dirname, './public/index.html')).toString()
  var parts = root.replace('<main></main>', '').split('<!-- separator -->')

  return gulp.src([
    './src/templates/**/*.jade',
    '!./src/templates/layouts/**/*.jade',
  ])
    .pipe(gulpJade({
      locals: {
        baseUrl: '',
        parts: parts,
      },
    }))
    .pipe(gulp.dest('./dist/'))
})

gulp.task('less', ['clean'], function () {
  return gulp.src('./src/style/main.less')
    .pipe(gulpLess())
    .pipe(gulp.dest('./dist/css/'))
})

gulp.task('webpack', ['clean'], function (cb) {
  webpackConfig.entry = './src/entry.js',
  webpackConfig.plugins = (webpackConfig.plugins || []).concat([
    new webpack.optimize.UglifyJsPlugin(),
  ])

  webpack(webpackConfig, cb)
})

gulp.task('build', [
  'clean',
  'copy',
  'jade',
  'less',
  'webpack',
])

gulp.task('default', ['build'])
```

## ビルド
コマンドを下記に示します。

```
npm run build
```

ビルドが成功すると`dist/`ディレクトリが生成されます。

# Connectで成果物を確認する

## bin/wwwの編集
`bin/www`の内容を下記に示します。

```js:bin/www
#!/usr/bin/env node

'use strict';

var port = Number.parseInt(process.env.PORT || '3000', 10)

var http = require('http')
var path = require('path')
var connect = require('connect')
var serveStatic = require('serve-static')

var app = connect()
app.use(serveStatic(path.join(__dirname, '../dist')))

var server = http.createServer(app)
server.listen(port)

server.on('listening', function () {
  console.log('Listening on ' + port)
})
```

## サーバ起動
コマンドを下記に示します。

```
npm run serve
```

## 動作確認
ブラウザで http://127.0.0.1:3000/ へアクセスします。http://127.0.0.1:8080/ と同じように表示されたら成功です。

# おわりに
webpackの豊富なローダーを使用すれば、Lessの代わりにSass/Scssで外観を記述したり、JavaScriptの代わりにCoffeeScriptで外観を記述したりすることが比較的簡単にできます。ご自身の状況に合わせてぜひとも色々とカスタマイズして役立てていただければ幸いです。

次回はElectron.jsを使用してWebアプリケーションをデスクトップアプリケーションにする方法を紹介する予定です。