webpack 4 + Babel 7 の環境で React + MobX アプリを動かす
今年は2月にwebpack 4がリリースされ、4月にNode ver.10がリリース、Babel 7もリリースを間近に控えるなど、フロントエンド周りのバージョンアップが相次いでいます。ここでは webpack 4 + Babel 7 の開発環境で React + Mobx を動かすための設定をまとめておきます。Mac + node v10.3.0で動作確認をしています。
パッケージのインストール
まずは適当なフォルダに移動してpackage.json
を作成します:
yarn init
ついでReact関連をインストールします:
yarn add react react-dom mobx mobx-react
開発環境のインストールはこんな感じです:
yarn add @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/preset-env @babel/preset-react babel-loader@^8.0.0-beta css-loader sass-loader style-loader webpack webpack-cli@2.1.5 webpack-dev-server -D
いくつか注意点があります。
まず @babel/...
ですが、これはBabel 7からインストールするパッケージ名が変更されました。angularやionicもそうですが、Babelもパッケージ名の先頭に@がついてスコープ化されるようになりました。
@babel/plugin-proposal-class-properties
と @babel/plugin-proposal-decorators
はMobXでデコレーターを使用するために必要になります。この二つに関しては次のwebpackの設定で、プラグインに指定するオプションについて説明します。
また webpack-cli
のバージョンが2.1.5で指定されていますが、これは記事公開当日に公開された webpack-cli@3.0.0
と webpack-dev-server@3.1.4
との相性が悪いようで、2系の最新版をインストールするようにしています。
インストールが終わった package.json
はこんな感じになります:
{
"name": "CounterApp",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"dev": "webpack-dev-server",
"build": "yarn run webpack"
},
"dependencies": {
"mobx": "^4.3.0",
"mobx-react": "^5.1.2",
"react": "^16.4.0",
"react-dom": "^16.4.0"
},
"devDependencies": {
"@babel/core": "^7.0.0-beta.49",
"@babel/plugin-proposal-class-properties": "^7.0.0-beta.49",
"@babel/plugin-proposal-decorators": "^7.0.0-beta.49",
"@babel/preset-env": "^7.0.0-beta.49",
"@babel/preset-react": "^7.0.0-beta.49",
"babel-loader": "^8.0.0-beta",
"css-loader": "^0.28.11",
"sass-loader": "^7.0.2",
"style-loader": "^0.21.0",
"webpack": "^4.10.2",
"webpack-cli": "2.1.5",
"webpack-dev-server": "^3.1.4",
"webpack-merge": "^4.1.2"
}
}
webpackの設定
パッケージがインストールできたらwebpackの設定を行います。ここではBabelの設定を .babelrc
ではなく webpack.config.js
で行うため、jsx
の module.rules
で query.babelrc
を false
にセットしています。
// webpack.confing.js
const path = require('path')
module.exports = {
mode: 'development',
entry: './app.jsx',
output: {
path: path.resolve(__dirname, './'),
filename: 'app.min.js'
},
module: {
rules: [
{
test: /\.jsx$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
babelrc: false,
presets: [
'@babel/preset-env',
'@babel/preset-react'
],
plugins: [
['@babel/plugin-proposal-decorators', {
legacy: true
}],
['@babel/plugin-proposal-class-properties', {
loose: true
}]
]
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader?modules']
},
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}
]
},
resolve: {
extensions: ['.js', '.jsx']
},
devServer: {
contentBase: path.join(__dirname, './'),
compress: true,
port: 9000
}
}
module.rules
の中の query.plugins
ではまず @babel/plugin-proposal-decorators
を指定しています。これがないと、MobXで @observable
などのデコレーターを使用すると、コンパイル時に
ERROR in ...
Module build failed: SyntaxError: ...: Support for the experimental syntax 'decorators-legacy' isn't currently enabled
というシンタックスエラーがターミナルに表示されます。
さらに @babel/plugin-proposal-decorators
に対して {legacy: true}
というオプションを渡してやらないと、コンパイル時に
ERROR in ...
Module build failed: Error: [BABEL] ...: The new decorators proposal is not supported yet. You must pass the `"legacy": true` option to @babel/plugin-proposal-decorators
というコンパイルエラーが表示されます。
@babel/plugin-proposal-class-properties
では {loose: true}
というオプションを指定しています。これがないと、
Uncaught Error: Decorating class property failed. Please ensure that proposal-class-properties is enabled and set to use loose mode. To use proposal-class-properties in spec mode with decorators, wait for the next major version of decorators in stage 2.
というランタイムエラーがブラウザのコンソールに出力されます。
この二つのプラグインの順番も大切です。もし @babel/plugin-proposal-decorators
を一番最初に記述せずに
['@babel/plugin-proposal-class-properties', {
loose: true
}],
['@babel/plugin-proposal-decorators', {
legacy: true
}]
のようにすると、
ERROR in ...
Module build failed: SyntaxError: .....: Decorators transform is necessary.
というシンタックスエラーがターミナルに表示されます。
単純なカウンターアプリを動かす
環境設定が終わったので、単純なカウンターアプリを動かします。用意するファイルは index.html
, style.scss
, app.jsx
です:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script src="app.min.js"></script>
</body>
</html>
body {
margin: 0;
line-height: 1;
}
span {
display: inline-block;
width: 3em;
font-size: 30px;
text-align: center;
}
button {
margin: 1em;
padding: .5em 1em;
border: 0;
font-size: 20px;
background-color: #fafafa;
}
import React from 'react'
import { render } from 'react-dom'
import { observable } from 'mobx'
import { observer } from 'mobx-react'
import './style.scss'
class CounterStore {
@observable counter = 0
increment() { this.counter++ }
decrement() { this.counter-- }
}
@observer
class App extends React.Component {
render() {
return (
<div className="wrapper">
<button onClick={() => this.props.data.increment()}>+1</button>
<span>{this.props.data.counter}</span>
<button onClick={() => this.props.data.decrement()}>-1</button>
</div>
)
}
}
const counterStore = new CounterStore()
render(<App data={counterStore} />, document.getElementById('root'))
スタイルは sass-loader
が動くのを確かめるために適当に書いてあります。
最後に
webpack-dev-server
で開発サーバーhttp://localhost:9000を立ち上げて、こんな感じのカウンターが表示されて動作すればOKです。(下のカウンターは画像ではなくamp-iframe
で読み込んでいるため、ボタンを押すとカウンターが動作します。)