黒ムツ本の第5章「Bootstrapを適用する」ではCDNやBowerからBootstrapをインストールする方法が書かれていますが、今回はフロントエンドのモジュール管理にWebpackを使ってみます。こちらの投稿にあるようにWebpackはnpm の利用を推奨しています。サーバーサイドもNode.jsで書く場合は、フロントエンドも同じにするとnpmだけで両方のパッケージ管理ができます。
package.json
リポジトリはこちらです。Bootstrap用にnpmでインストールしたパッケージです。
"devDependencies": {
...
"jquery": "~2.1.4",
"mithril": "~0.2.0",
"bootstrap": "~3.3.5",
...
"style-loader": "~0.13.0",
"css-loader": "~0.21.0",
"imports-loader": "~0.6.5",
"file-loader": "~0.8.4",
"url-loader": "~0.5.6",
...
loaders
Webpackのloadersタスクを使いURLを検証してファイルをロードするときに必要な前処理をインジェクトします。
babel-loader
前回のようにES6でコードを書くためnpmでインストールしたパッケージ以外の.js
ファイルはbabel-loaderを通してロードします。
..
test: /\.js?$/,
exclude: /node_modules/,
loader: 'babel'
...
import-loader
Bootstrapの中にはjQuery
変数に依存しているコードがあります。WebpackからjQueryをロードする方法はいくつもあり、書き方もいろいろで悩みます。今回はimport-loaderを使いました。bootstrap
モジュールのjQuery
変数にjquery
モジュールをインジェクトします。
...
loaders: [{
...
}, {
test: /bootstrap\/js\//,
loader: 'imports?jQuery=jquery'
}, {
...
style-loader, css-loader, less-loader
CSSはstyle-loaderからcss-loader経由でロードします。LESSをロードする場合はless-loaderを経由すると自動的に書式を変換してくれます。
...
loaders: [{
...
}, {
test : /\.css$/,
loader : 'style!css'
}, {
test : /\.less$/,
loader : 'style!css!less'
}, {
...
url-loader
Bootstrapで使うフォントはURLをtestの条件にして、100kbより小さいファイルをurl-loaderからロードします。
...
}, {
test: /\.(png|woff|woff2|eot|ttf|svg)$/,
loader: 'url?limit=100000'
}],
...
まとめ
まとめるとmodule
のloaders
フィールドは以下のようになります。
...
loaders: [{
test: /\.js?$/,
exclude: /node_modules/,
loader: 'babel'
}, {
test: /bootstrap\/js\//,
loader: 'imports?jQuery=jquery'
}, {
test : /\.css$/,
loader : 'style!css'
}, {
test : /\.less$/,
loader : 'style!css!less'
}, {
test: /\.(png|woff|woff2|eot|ttf|svg)$/,
loader: 'url?limit=100000'
}],
...
ViewでBootstrapをロードする
フロントエンドのパッケージ管理をWebpackとnpmからインストールしました。Bootstrapの.jsと.cssをimport
句を使ってエントリーポイントのmain.js
にロードします。
import m from 'mithril';
import 'bootstrap';
import 'bootstrap/dist/css/bootstrap.css';
...
NavBarコンポーネント
BootstrapのNavBarをMithrilのコンポーネントにしたES6で書いてみます。MithrilのViewはmのDSLを使います。ReactのJSXを移植したMSXという名前がノスタルジックなシンタックスもあります。m関数はオブジェクトと配列で仮装DOMを書くことができます。個人的にはJSXスタイルより書きやすいです。
import m from 'mithril';
const NavBar = {
view: (ctrl, args) => {
return m('.container', [
m('.nav-header', [
m('a.navbar-brand', { href: '#' }, args.title)
])
]);
}
}
export default NavBar;
NavBarコンポーネントはmain.jsにimportしてm.mount
をコールしてindex.htmlのnavbar
idにマウントします。m.component
は引数をviewにバインドしたNavBarコンポーネントを返します。ここではタイトルの名称を渡しています。
...
import NavBar from './navbar';
m.mount(document.getElementById('navbar'),
m.component(NavBar, {
title: 'Mithril Study'
}));
...
Homeコンポーネント
Homeコンポーネントも普通のオブジェクトで書きました。Glyphアイコンを使ったBootstrapのアラートです。
import m from 'mithril';
export default {
controller: () => {},
view: () => {
return [
m('div', {class: 'container'}, [
m('.page-header', [
m('h3', 'Bootstrap アラート')
]),
m('.alert.alert-info[role="alert"]', [
m('span.glyphicon.glyphicon-exclamation-sign[aria-hidden="t\
rue"]'),
m('span.sr-only', 'Info:'),
' Infoのサンプルです。'
])
])
];
}
};
仮装DOMのDIV要素はm('div')
と書いたり、省略してCSSのクラス名をつなげてm('.alert.alert-info[role="alert"]'
のようにも書けます。以下のようなHTMLを生成します。
<div role="alert" class="alert alert-info">
<span aria-hidden="true" class="glyphicon glyphicon-exclamation-sign"></span>
<span class="sr-only">Info:</span>
Infoのサンプルです。
</div>
index.html
SPAのHTMLはとてもシンプルでWebpackのjsをバンドルしてサーブするだけです。html-webpack-pluginを使えばHTMLファイルも生成してくれますが、navbarも別にマウントしたかったのでindex.htmlを用意しました。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mithril Study</title>
<link rel="shortcut icon" href="/images/favicon.ico" type="image/x-icon">
</head>
<body>
<nav id="navbar" class="navbar navbar-default"></nav>
<div id="root"></div>
<script src="/js/main.js"></script>
</body>
</html>
/js/main.js
はwebpack.config.jsのoutputフィールドに指定したWebpackがバンドルしたjsファイルの出力先になります。filenameを[name].js
とするとエントリポイントに指定したファイル名が[name]
で置換されます。
...
entry: [
'webpack-hot-middleware/client',
path.join(__dirname, 'app/client/js/main.js')
],
output: {
path: path.join(__dirname, 'dist/js'),
filename: '[name].js',
publicPath: '/js/'
},
...
ラウター
エントリポイントのmain.js
を書きます。m.mount
コンポーネントを使いNavBarを<nav id="navbar"
マウントします。m.route
はURLを使って複数のコンポーネントをマウントできるラウターです。今回はURLは/
の1つしかないので、デフォルトの/
にHomeコンポーネントをマップします。
<div id="root">
をラウター
import m from 'mithril';
import 'bootstrap';
import Home from './home';
import NavBar from './navbar';
import 'bootstrap/dist/css/bootstrap.css';
import '../css/main.css';
const home = m.component(Home);
m.mount(document.getElementById('navbar'),
m.component(NavBar, {
title: 'Mithril Study'
}));
m.route(document.getElementById('root'), '/', {
'/': home
});
テスト
Expressの開発サーバーはDocker Composeからワンショットのコンテナで起動します。--service-ports
フラグを指定するとDockerホストにコンテナのポートがマップされます。
$ docker-compose run --rm \
--service-ports \
mithril \
npm run nodemon
npm info it worked if it ends with ok
npm info using npm@3.3.12
npm info using node@v5.1.0
npm info lifecycle docker-mithril-study@0.0.1~prenodemon: docker-mithril-study@0.0.1
npm info lifecycle docker-mithril-study@0.0.1~nodemon: docker-mithril-study@0.0.1
> docker-mithril-study@0.0.1 nodemon /usr/src/app
> gulp nodemon
[10:00:41] Using gulpfile /usr/src/app/gulpfile.js
[10:00:41] Starting 'nodemon'...
[10:00:41] Finished 'nodemon' after 2.24 ms
[10:00:41] [nodemon] 1.8.1
[10:00:41] [nodemon] to restart at any time, enter `rs`
[10:00:41] [nodemon] watching: /usr/src/app/app/server/**/*
[10:00:41] [nodemon] starting `node ./app/server/server.js`
Example app listening at http://:::3000
webpack built 75d57784c725d9d93cbb in 4404ms
docker-compose.ymlのmithril
サービスの3000ポートをDockerホストの3030ポートにマップしています。
mithril:
restart: always
build: .
volumes:
- .:/usr/src/app
ports:
- "3030:3000"
docker-compose ps
コマンドからコンテナの起動と開いているポートの状態を確認できます。
$ docker-compose ps
Name Command State Ports
----------------------------------------------------------------------
part1_mithril_run_2 npm run nodemon Up 0.0.0.0:3030->3000/tcp
クラウドの仮想マシンにパブリックIPアドレスの3030ポートをブラウザで開きます。MithrilでBootstrapを使った簡単なアラート画面が表示されました。