がんばらない webpack
TL;DR
- webpack はあまりにも色々できすぎるので最低限の機能を押さえる
- babel-loader 使いません (ECMAScript 6 compatibility table を睨んで必要なら使う)
高度に発達した npm script は魔術と区別がつかない
昔こんな記事を書きましたが,npm script
と browserify
で頑張るの普通にしんどいので webpack に移行する気持ちになりました
Chrome Extension
全人類一度は作ったことがあると思われる*1 Chrome 拡張の開発環境 を webpack で揃えてみます.
以下の構成で $ webpack ...
すると,dist
以下に chrome拡張のコアが生成され,配布できる状態をイメージしています
├── dist # generated
│ ├── background.bundle.js
│ ├── bundle.css
│ ├── manifest.json
│ ├── popup.bundle.js
│ ├── popup.html
│ ├── scss.bundle.js
│ └── vendor.bundle.js
├── manifest.json
├── package-lock.json
├── package.json
├── src
│ ├── html
│ │ ├── _chip.html
│ │ ├── options.html
│ │ └── popup.html
│ ├── js
│ │ ├── background.js
│ │ └── popup.js
│ └── scss
│ └── custom.scss
└── webpack.config.js
はじめての webpack
- 公式ドキュメントを読みました(2019年にもなって今更ですが...)
- Concepts → Configuration → Loaders → Plugins を眺める
install
$ npm i --save-dev webpack@3
webpack.config.js
に設定を書き,webpack ...
を実行してターゲットを生成するのが基本になります.npm v5.2.0+
であれば npx コマンドをnpm run 〜
の代わりに使えます
$ webpack [options] # npm i -g で入れた webpack $ npx webpack [options] # node_modules/ のwebpack
webpack3かよ
後述の Getting Started - Material Components for the Web が webpack3 だったんです...
最も簡単な bundle
- 以下の設定で
dist/bundle.js
が生成され,ブラウザで実行できます
// webpack.config.js const path = require('path'); module.exports = { entry: path.resolve(__dirname, 'src', 'test.js'), output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, }
複数のエントリーポイントと出力を指定する
- Chrome 拡張では構成上
popup.js
,background.js
,contentscript.js
... とファイルが分割されている必要があります*4 - 以下のように entry を複数指定すると,
dist/background.bundle.js
dist/popup.bundle.js
と分けて生成することができます
// webpack.config.js const path = require('path'); module.exports = [{ entry: { background: path.join(__dirname, "src", "js", "background.js"), popup: path.join(__dirname, "src", "js", "popup.js"), }, output: { path: path.join(__dirname, "dist"), filename: "[name].bundle.js" }, }];
共通化して使う外部ライブラリを bundle する
const $ = require("jquery"); const _ = require("lodash");
- jquery, lodash のような共通で使いたいライブラリはまとめて bundle することもできます
webpack.optimize.CommonsChunkPlugin
を使えば,dist/vendor.bundle.js
を簡単に生成できる
// webpack.config.js const path = require('path'); const webpack = require('webpack'); module.exports = [{ entry: { background: path.join(__dirname, "src", "js", "background.js"), popup: path.join(__dirname, "src", "js", "popup.js"), vendor: ['jquery', 'lodash'] }, output: { path: path.join(__dirname, "dist"), filename: "[name].bundle.js" }, plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: Infinity, }), ] }];
webpack で html を扱う
↑ で bundle した js を html でロードする
- 手でやらなくても
HtmlWebpackPlugin
が自動解決してくれます.
$ npm i --save-dev html-webpack-plugin
- 以下のように記述すると,
popup:
vendor:
の output (*.bundle.js) を読み込む scriptノードを 指定の HTMLに勝手に挿入してくれます.
// webpack.config.js module.exports = [{ entry: { background: path.join(__dirname, "src", "js", "background.js"), popup: path.join(__dirname, "src", "js", "popup.js"), vendor: ['jquery', 'lodash'] }, ... plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: Infinity, }), new HtmlWebpackPlugin({ filename: 'popup.html', chunks: ['popup', 'vendor'], template: path.join(__dirname, "src", "html", "popup.html") }),
// popup.html
+ <script type="text/javascript" src="vendor.bundle.js"></script><script type="text/javascript" src="popup.bundle.js">
template を扱う
- lodash や ES6 の template を扱う場合,html template を別ファイルに分離したくなったりする
- html-loader を使えば js から html が自由に読み込める(そして webpack 側で処理してくれるので,dist にコピーする必要がない)
$ npm i --save-dev html-loader
// webpack.config.js ... module: { rules: [{ test: /\.scss$/, use: [ ... }, { test: /\.html$/, use: [ { loader: 'html-loader', options: {includePaths: ['./src/html']} } ] }] }
const templateStr = require('path/to/_chip.html'); const compiled = _.template(templateStr); // compiled({ attr: val , ... });
webpack で scss を扱う
material components for the web
install deps
- Getting Started - Material Components for the Web を参考に必要最小限だけ入れます
$ npm i --save-dev sass-loader file-loader
sass-loader | Loads a Sass file and compiles it to CSS |
file-loader | Serves the .css file as a public URL |
install MDC
material-design-components (MDC) は全部一気に入れたり
$ npm i --save-dev material-components-web
必要な分だけバラバラに入れたりできます.
$ npm i --save-dev @material/button @material/textfield
必要な分だけ bundle したいので今回は後者を選択します*6.
sass-loader
- module.rules に test と use を記述すれば,指定のファイルを処理する際に通す loader が指定できます.
- 以下の設定で dist/bundle.css が吐けるので,それを読み込めば OK
- エントリーポイントには
custom.scss
を指定する *7
// webpack.config.js ... entry: { background: path.join(__dirname, "src", "js", "background.js"), popup: path.join(__dirname, "src", "js", "popup.js"), scss: path.join(__dirname, "src", "scss", "custom.scss"), vendor: ['jquery', 'lodash'] }, ... module: { rules: [{ test: /\.scss$/, use: [ { loader: 'file-loader', options: { name: 'bundle.css' } }, { loader: 'sass-loader', options: { includePaths: ['./node_modules'] } }, ] }] }
sass-loader の includePaths 指定がなければ node_modules 以下を探しにいってくれません.
ERROR in ./src/scss/custom.scss Module build failed: @import '@material/button/mdc-button'; ^ File to import not found or unreadable: @material/button/mdc-button. in /Users/mai/Garage/arXiv2scrap/src/scss/custom.scss (line 1, column 1)
こんな感じで npm で入れた MDC の *.scss をガンガン import できます. スッキリ
# custom.scss @import '@material/button/mdc-button'; @import '@material/icon-button/mdc-icon-button'; ...
MDC の components は,mdc.autoInit()
を使えば単純なものならまとめて js のハンドリングを付与できます(力強いが...)
色々カスタムする場合は以下のように1つずつ初期化します
const $ = require('jquery'); const MDCTextField = require('@material/textfield').MDCTextField; const textfields = $('.mdc-text-field'); for (const tf of textfields) { new MDCTextField(tf); }
manifest.json を dist にコピーする
- Chrome 拡張のコアである manifest.json(の中身については本記事で扱わない)
- npm script では cat とかで複製してましたが,コピー用のプラグインがあります
- 釈然としないがとりあえずこれでコピー問題は解決できた...
$ npm i --save-dev copy-webpack-plugin
// webpack.config.js plugins: [ ... new CopyWebpackPlugin([ { from: path.join(__dirname, 'manifest.json'), to: path.join(__dirname, 'dist') }, ... ]) ],
高度に発達した webpack は魔術と区別がつかない
はい
package.json
こんな感じになります.webpack の設定に逃したので npm script がスッキリしました(当たり前)
{ "name": "webpack ...", "version": "0.0.1", "description": "simple webpack example", "scripts": { "build": "webpack -p", "watch": "webpack --watch", "clean": "rm -rf dist/*" }, "author": "denkiwakame", "dependencies": { "@material/button": "^0.42.0", ... "@material/textfield": "^0.42.1", "jquery": "^3.3.1", "lodash": "^4.17.11", }, "devDependencies": { "copy-webpack-plugin": "^4.6.0", "file-loader": "^3.0.1", "html-loader": "^0.5.5", "html-webpack-plugin": "^3.2.0", "node-sass": "^4.11.0", "sass-loader": "^7.1.0", "webpack": "^3.12.0", } }
webpack.config.js
こんな感じになりました.webpack --watch で js と css のビルドを1本化して丸ごと監視できるのは良いのかなと思いました
$ npx webpack --watch
で継続監視ビルドにできます.tsc -w とか watchify とか catw の組み合わせでなく,一本化できるのは楽かもしれません.
const path = require('path'); const webpack = require('webpack'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = [{ entry: { background: path.join(__dirname, "src", "js", "background.js"), popup: path.join(__dirname, "src", "js", "popup.js"), scss: path.join(__dirname, "src", "scss", "custom.scss"), vendor: ['jquery', 'lodash'] }, output: { path: path.join(__dirname, "dist"), filename: "[name].bundle.js" }, plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: Infinity, }), new HtmlWebpackPlugin({ filename: 'popup.html', chunks: ['popup', 'vendor'], template: path.join(__dirname, "src", "html", "popup.html") }), new CopyWebpackPlugin([ { from: path.join(__dirname, 'manifest.json'), to: path.join(__dirname, 'dist') }, ]) ], module: { rules: [{ test: /\.scss$/, use: [ { loader: 'file-loader', options: { name: 'bundle.css' } }, { loader: 'sass-loader', options: { includePaths: ['./node_modules'] } }, ] } } }];
感想
- 全部 webpack に 1本化できた
- material components for the web(本家),つらかったので今後は別のフレームワーク使いたい
- 小規模なら skeleton-css とかを使うのが良い(SASSも不要)
- css コンパイル周りが javascript 原理主義すぎて不安になる(そこまで全てが javascript で読めなくても良い)
- Chrome 拡張を例にとったが,あまり意味がなかった
[追記] webpack3 -> 4 への移行
この拡張を作るときに webpack4 へ引っ越しました.
wakame.hatenablog.jp
箱根駅伝を見ながら覚えた webpack3,ほぼ1日の付き合いだった
$ npm i --save-dev webpack@4 $ npm i --save-dev webpack-cli
発端
- 元々 material components for the web で webpack@3 が使われていたので合わせていたが,以下の問題がありました
UglifyJsPlugin: ES6 Methods Throw Parse Error · Issue #1542 · webpack/webpack · GitHub
webpack -p
する際,webpack3 が内部で読んでいる uglifyjs だと ES6 を解釈できないため (1) webpack4 に移行 (2) ES6 を babel で変換 (3) ダウングレードした 自前の uglifyjs を用意して config で分岐 の三択に- (2) は古いブラウザのサポートを捨てて babel を回避すると決めたので不可
- (3) は webpack.config.js が汚くなった
- (1) が一番綺麗に解決できる(configもcleanupできる) ということで webpack4 に移行した
CommonsChunkPlugin の死
webpack.optimize.CommonsChunkPlugin has been removed, please use config.optimization.splitChunks instead.
https://qiita.com/soarflat/items/f8212434c4c3cb8ee00d
+ optimization: { + splitChunks: { + name: 'vendor', + chunks: 'initial', + } }, plugins: [ - new webpack.optimize.CommonsChunkPlugin({ - name: 'vendor', - minChunks: Infinity, - }), new HtmlWebpackPlugin({ filename: 'popup.html',
幸い大したことをやっていなかったので軽傷ですみました.
参考
*1:Overview - Google Chrome で大体わかります
*2:GitHub - HaNdTriX/generator-chrome-extension-kickstart: Scaffold out a Web Extension http://yeoman.io
*3:そもそも簡単な拡張なら node の機能を使わず生書きすれば良いという話はあるが
*4:CommonJS <-> browser JSの変換以外に bundleされたら困る
*5:GitHub - google/material-design-lite: Material Design Components in HTML/CSS/JS
*7:custom.bundle.js が生成されるが直接は利用しない. 合ってるのかこれで...?