あしたからがんばる

プログラミングの話と時々サッカーの話

フロントエンドでURLの入力とリスト表示を作る

作りたいのはブックマークサービスなので、URLを登録できて、自分が登録したURLを一覧したい。

久々に React のサイトを訪れた所、Hooks というのを使うのが今時っぽいので、まずはステートフックを使って、Input に入力した URL をステートとして保持し、一覧表示するところまでをやってみる。

ステートフックの利用法 – React

フックは関数のトップレベルで使わないといけないとか、縛りがあるようだ。 Stateless Functional Component という概念があった気がするけど、Stateless では無くなりそうだなとか思ってたら、それっぽいことが書いてあった。

これらのことを「ステートレスコンポーネント (stateless component)」だと理解していたかもしれません。これらの内部で React の state を利用できるようにしていますので、「関数コンポーネント (function component)」という名前を利用します。

とりあえず Input 置いて、Enter 押したら Input の内容がリストに保存されていくような実装をしてみた。

import React, {useState} from 'react';
import ReactDOM from 'react-dom';
import styled from 'styled-components';


const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;


const Wrapper = styled.section`
  padding: 4em;
  background: papayawhip;
`;

function Test() {
    const [urls, setUrls] = useState([]);

    function onKeyPress(e) {
        const url = e.target.value;
        if (e.key === 'Enter' && url) {
            e.preventDefault();
            const newUrls = urls.concat(url);
            setUrls(newUrls);
        }
    }

    return (
        <Wrapper>
            <Title>
                URLを保存するぞ
            </Title>
            <div>
                <input
                    type="url"
                    onKeyPress={onKeyPress}
                />
                <ul>
                    {urls.map((url, index) => {
                        return <li key={index}>{url}</li>;
                    })}
                </ul>
            </div>
        </Wrapper>
    );
}

ReactDOM.render(<Test/>, document.getElementById('root'));

動いているので今日のところは良しとしよう。

f:id:mwtnb:20191204010621p:plain

styled-componentsを導入する

まずはpackageのインストールから。

npm install styled-components

注意点としては、render メソッドの中で styled component を作るのは良くないとのこと。

styled-components: Basics

あと、ユーザーの入力値をスタイル定義で使うような場合はセキュリティ対策としてサニタイズが必要そう。

styled-components: Advanced Usage

ビルド環境は特にいじらずに、styled-components の Hello World を持ってきて動かしてみる。

import React from 'react';
import ReactDOM from 'react-dom';
import styled from 'styled-components';

 const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;

const Wrapper = styled.section`
  padding: 4em;
  background: papayawhip;
`;

const test = <Wrapper>
    <Title>
        Hello World!
    </Title>
</Wrapper>;
    
ReactDOM.render(test, document.body);

ちゃんと表示できてた。 f:id:mwtnb:20191128235210p:plain

あとはこのビルド結果を ktor の static 配信用のディレクトリに配置するように webpack の設定を書き換えれば良いはず。 なんで entry は相対パスでも良くて output は絶対パスじゃないといけないんだろうという疑問が沸いたけど、深みにハマりそうなので無視して進める。

const path = require('path');

module.exports = {
    mode: "development",
    entry: "./src/assets/js/test.jsx",
    output: {
        path: path.resolve(__dirname, 'src/main/resources/static/js'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.jsx$/,
                exclude: /node_modules/,
                loader: "babel-loader",
                options: {presets: ["@babel/react"]}
            }
        ]
    }
};

動いた。 f:id:mwtnb:20191129002440p:plain

バックエンドとフロントエンドの繋ぎ込みもできたので、そろそろ画面を考えていきたいけれども、疲れたので今日はここまで。

フロントエンドの開発を始めるつもりが気づいたらビルド環境整備してた

Nuxt 使ってみたいとか書いたけどなんか思ってたのと違ったのでやっぱやめた。 まだ触ったことないのと久々に React 触ってみたいので、styled-components でやってみる。 開始時点では、node v12.13.1 がローカルに入っているだけの状態。

まずは styled-components を npm で入れる。--save は無くてもよくなってた。お仕事じゃないので何も考えず beta で。

npm install styled-components@beta

React とかが足りないので警告がでた。

npm WARN styled-components@5.0.0-rc.2 requires a peer of react@>= 16.8.0 but none is installed. You must install peer dependencies yourself.
npm WARN styled-components@5.0.0-rc.2 requires a peer of react-dom@>= 16.8.0 but none is installed. You must install peer dependencies yourself.
npm WARN styled-components@5.0.0-rc.2 requires a peer of react-is@>= 16.8.0 but none is installed. You must install peer dependencies yourself.
npm WARN stylis-rule-sheet@0.0.10 requires a peer of stylis@^3.5.0 but none is installed. You must install peer dependencies yourself.
npm WARN babel-plugin-styled-components@1.10.6 requires a peer of styled-components@>= 2 but none is installed. You must install peer dependencies yourself.

うーん、そりゃそうだろうから何はともあれ React が要るよな、ということでまずは React の環境を整える。 さっき入れた中途半端な styled-components は無視したいので、node_modules と package.json は削除して最初からやり直し。

最終的には TypeScript も入れたいけど、最初は JS でいく。webpack と Babel が必要そう。

Create a New React App – React

Creating a React App… From Scratch. - Noteworthy - The Journal Blog

まずは Babel だけで React(というかJSX) を JS にトランスパイルするところを目指す。(なんか数年前も同じことやってたな。。)

npm install --save-dev @babel/core @babel/cli @babel/preset-react

preset-env は一旦置いておいて、preset-react だけ使う。 presetとは何かというと、いくつかの Babel plugin を用途に合わせてまとめたもの。

Usage Guide · Babel

Instead of adding all the plugins we want one by one, we can use a "preset" which is just a pre-determined set of plugins

これで React で良くみる JSX のトランスパイルに必要な依存は揃った気がするので、以下のようなテスト用 JSX ファイルを作成してトランスパイルしてみた。

// src/assets/js/test.jsx

function test() {
    return (
        <div>aaa</div>
    )
}

test();
❯ npx babel --presets @babel/preset-react src/assets/js/test.jsx

function test() {
  return React.createElement("div", null, "aaa");
}

test();

ということで、トランスパイルはできてる。 もちろんこのままだとReact.createElementが解決できなくて実行できないので、次は webpack で依存ライブラリをまとめて bundle する。

Getting Started | webpack

まずは webpack と babel-loader をインストール。 babel-loader は、webpack で処理対象のファイルを読み込むときに Babel でトランスパイルしてくれる、ぐらいのフワッとした理解。

npm install -D webpack webpack-cli babel-loader

React も入れる。

npm install react react-dom

あとは webpack を設定して、bundle 実行して、適当に書いた HTML から読み込む。

bundle は webpack-cli を使うだけ。

npx webpack

作ったファイルはこんな感じ。

<!-- test.html -->
<body>
<script src="./dist/main.js"></script>
</body>
// webpack.config.js
module.exports = {
    entry: "./src/assets/js/test.jsx",
    mode: "development",
    module: {
        rules: [
            {
                test: /\.jsx$/,
                exclude: /node_modules/,
                loader: "babel-loader",
                options: {presets: ["@babel/react"]}
            }
        ]
    }
};
// src/assets/js/test.jsx
import React from 'react';
import ReactDOM from 'react-dom';

function test() {
    return (
        <div>test dayo</div>
    )
}

ReactDOM.render(test(), document.body);

ここまでやって、test.html を開くと test dayo と表示される。

ということで、webpack で React のビルドまでできるようになったけど、疲弊したので一旦終了。 styled-components にたどり着けるのはいつになるやら。

最後に、package.json を丸っと載せておく。

{
  "name": "bookmark",
  "devDependencies": {
    "@babel/cli": "^7.7.4",
    "@babel/core": "^7.7.4",
    "@babel/preset-react": "^7.7.4",
    "babel-loader": "^8.0.6",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10"
  },
  "dependencies": {
    "react": "^16.12.0",
    "react-dom": "^16.12.0"
  }
}

よくあるWebページを作る

前回のktorのコードに加えて、 application.confとlogback.xmlを追加して、watchの設定もした。 あと、便利らしいのでIntelliJの設定で import * をデフォルトで使うようにしておいた。

参考にしたのはこの辺り。

引き続き、website の quickstart をやっていく。 テンプレートエンジンは無心でfreemarkerを使う。 Website - Quick Start - Ktor

とりあえず、ログインフォームとログイン後画面の表示までやってみて、気になった点だけ雑に書く。

まず、ktor関係ないだろうけど、freemarkerのvariable定義は便利そう。この辺は気にしたことすらなかった。。 auto completion 効くようになるらしい。

<#-- @ftlvariable name="data" type="com.example.IndexData" -->
<html>
    <body>
        <ul>
        <#list data.items as item>
            <li>${item}</li>
        </#list>
        </ul>
    </body>
</html>

あと、partial content云々の話があったので、videoタグで動画を表示してみた。 動画は適当に画面収録して、movからwebmにffmpeg使って変換した。 無事にシークも実現できてた。 response statusも206だし、本当にplugin installするだけでpartial content(rangeヘッダのやつ)対応できるんだなぁ。便利な世の中。

f:id:mwtnb:20191127012321p:plain
写経ftlのリスト表示と追加のvideo

ログイン周りも写経してみたけど、セッションといいつつ単にクッキーに文字列突っ込んでるだけだから、そのうちちゃんと作んないといけない。 f:id:mwtnb:20191127013018p:plain

この辺り読まないと。 Authentication - Servers - Ktor

まぁその辺は当面はすっ飛ばして、次はAPI作るかフロントをやっていこう。NuxtとかGraphQLを試してみたい所存。

触りは簡単なやつだけにするはずが謎の深みに嵌るよくある展開

自分用とはいえ、サービスを作るにあたって本当は要件とかから考えるべきなんだろうけど、そんなに頑張れないので適当にサーバサイドから作ることにする。 最近仕事で触ってる ktor を使う。まっさらの状態から組んだことはないので勉強になるだろうし。

Quick Start - Quick Start - Ktor

ビルドツールは gradle で良いので、↓をまるっとコピーしてから、最小構成を目指して設定を削ったりバージョンを上げたりしてみる。

Gradle - Quick Start - Ktor

削った結果がこちら。

buildscript {
    ext.kotlin_version = '1.3.60'
    ext.ktor_version = '1.2.5'

    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin'

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    compile "io.ktor:ktor-server-netty:$ktor_version"
}

以下の設定はコルーチンが experimental だった頃の名残っぽかったので削除した。 1.3になる前までは、↓のようにオプトインしないとコンパイル時に警告出てたらしいけど、現状では出なかったので問題ないと判断した。

kotlin {
    experimental {
        coroutines 'enable'
    }
}

簡素化した build.gradle を IntelliJ で読んで、src/main/kotlin にパッケージ掘って、BookmarkApp.kt というファイルを作成して、以下の内容にすると無事に起動できて、localhost:8080 にアクセスするとレスポンスも返ってきた。

package com.github.mwatanabe.bookmark

import io.ktor.application.call
import io.ktor.http.ContentType
import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty


fun main(args: Array<String>) {
    embeddedServer(Netty, 8080) {
        routing {
            get("/") {
                call.respondText ("My Bookmark", ContentType.Text.Html)
            }
        }
    }.start(wait = true)
}

しかし、起動はするものの、以下の警告が発生した。

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

gradle dependencies で見たところ、ktor server が slf4j-api に依存しているようだった。

まぁそれはそうだよね、という気がするので、logback-classic を dependency に追加した。logback-classic の classic とはなんぞやと思ったが、slf4j の API を実装しているやつらしいので、これを使えば良い。

http://logback.qos.ch/manual/architecture_ja.html

compile "ch.qos.logback:logback-classic:1.2.3"

その結果、Netty が謎のログを出すようになったが、何やらただのデバッグログしいので、気にしないことにする。

UnsatisfiedLinkError with Netty · Issue #1203 · ktorio/ktor · GitHub

0:09:31.787 [main] DEBUG io.netty.util.internal.NativeLibraryLoader - netty_transport_native_kqueue cannot be loaded from java.library.path, now trying export to -Dio.netty.native.workdir: /var/...
java.lang.UnsatisfiedLinkError: no netty_transport_native_kqueue in java.library.path
...

触りはこのぐらいまでにしておこう。 今日は結構頑張ったから、明日は頑張らないようにしたい。

ひっそりと

ここのところyoutubenetflixも飽きてきて、週末のdaznぐらいしか見るものがない。 仕方ないので、久々に趣味プログラミングを始めることにする。

なんとなくブックマーク系のサービス使ってなくて、あとで読むブクマがデバイスに分散しがちなので、自分用ブクマサービスを作ることにした。

車輪を再発明していきたい所存、何日続くかはわからないけれども。

Babelとwebpackを使ってReactとReduxをビルドする方法を一歩一歩調べた話

ふとjsを書こうと思い立ち、少し調べたところ、何やらReduxが流行っているようでした。 にわかな自分としては、とりあえずReduxに乗っかろうと考えたのですが、Babelとかwebpackとかgulpとか色々出てきて、最終的な設定ファイルの構成だけ見ても何が何やらだったので、少しずつ調べた結果のメモを晒します。 あまり深い話はできないので(よくわかってn..)、操作の結果がどうなるか、という内容になってます。

調べ始める前はぼんやりとこんなことを考えてました。つまり、対象読者はこのぐらいの人です。

  • webpackはrequireを解決してファイルを結合してくれるらしい
  • ReactはBabelで変換するのが便利らしい
  • Babel入れるならes2015のシンタックスも使いたい
  • gulp使わなくてもnpm scriptsだけで色々できるという噂を聞いた

目標

Reduxのexamples/counterをwebpackでbundleして実行することを目指します。

大まかな流れとして、以下の順で見ていきます。

  1. Babelを使ってReactを動かす
  2. webpackを使ってmodule分割してみる
  3. webpackとBabelを使ってReactを動かす
  4. webpackとBabelを使ってReduxを動かす

TL;DR

  • Babelとwebpackを組み合わせたjsのビルドを色々試した
  • 目標はBabelとwebpackを使ってReduxのサンプルをビルドすること
  • 色々試したコードはリポジトリにあげたよ

1. Babel

とっつきやすそうなBabelから手をつけます。

babel-es2015

まず最初のステップとして、Babelを使ってes2015のコードを変換してみます。

元となるソースはこちら。arrow functionがes2015の部分です。

// src.js
[1, 2, 3].map(n => n + 1);

Babelを使うためにpackage.jsonに依存ライブラリを追加します。 es2015の変換なので、babel-preset-es2015を使います。

// package.json
{
  "scripts": {
    "build": "babel src.js -o bundle.js"
  },
  "devDependencies": {
    "babel-cli": "^6.7.5",
    "babel-preset-es2015": "^6.6.0"
  }
}

また、Babel用の設定ファイル(.babelrc)が必要なので用意します。 この設定ファイルでes2015を変換することを指定します。

// .babelrc
{
  "presets": ["es2015"]
}

npm installした後に、npm run buildを実行すると変換結果のbundle.jsが生成されます。 npm run buildではソースと出力先を指定して、babel-cliを実行しています。 (以降全てのパターンにおいて、npm run buildでbundle.jsを生成するのは統一してます)

変換結果はこんな感じで、arrow functionが変換されていることがわかります。

// bundle.js
[1, 2, 3].map(function (n) {
  return n + 1;
});

babel-jsx

次に、Reactでよくみるjsxを変換してみます。 jsxについては、js内にhtmlのタグが書けるやつぐらいの認識しかないです。 設定ファイルのes2015との違いは、babel-preset-2015がbabel-preset-reactになっただけです。

// src.jsx
<div>Hello, world!</div>
// package.json
{
  "scripts": {
    "build": "babel src.jsx -o bundle.js"
  },
  "devDependencies": {
    "babel-cli": "^6.7.5",
    "babel-preset-react": "^6.5.0"
  }
}
// .babelrc
{
  "presets": ["react"]
}
// bundle.js
React.createElement(
  "div",
  null,
  "Hello, world!"
);

src.jsx内のdivタグは、bundle.jsではReact.createElementに変換されているのが分かります。 へー、こんなことやってんのか、という印象です。

babel-react

一歩一歩ということで、src.jsxをもう少しReactっぽいコードにしてみます。 package.jsonと.babelrcはbabel-jsxと同じです。

// src.jsx
var Hello = React.createClass({
    render: function () {
        return <div>Hello world</div>;
    }
});
// bundle.js
var Hello = React.createClass({
    displayName: "Hello",

    render: function () {
        return React.createElement(
            "div",
            null,
            "Hello world"
        );
    }
});

babel-jsxの場合と同様に、jsxはcreateElementに変換されてます。 displayNameはデバッグ用らしいです。

babel-react-es2015

ここまでくると、Helloをes2015のclassとして書きたくなります。 babel-preset-reactとbabel-preset-es2015を両方とも使えば実現できます。

//src.jsx
class Hello extends React.Component {
    render() {
        return <div>Hello world</div>
    }
}
ReactDOM.render(<Hello/>, document.body);
// package.json
{
  "scripts": {
    "build": "babel src.jsx -o bundle.js"
  },
  "devDependencies": {
    "babel-cli": "^6.7.5",
    "babel-preset-es2015": "^6.6.0",
    "babel-preset-react": "^6.5.0"
  }
}
// .babelrc
{
  "presets": ["es2015","react"]
}

classに関連する部分をBabelが頑張ってbundle.jsが長くなるので、 bundle.jsは目では見ないことにして(見たい場合はリポジトリを見てください)、 代わりに、以下のようなhtmlを使ってbundle.jsを読み込んで動作確認します。

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react-dom.js"></script>
<body>
<script src="bundle.js"></script>
</body>

htmlを開くと、Hellow worldと出力されるので、もろもろちゃんと動いてそうです。

Babelまとめ

  • .babelrcで変換の対象を指定できる
  • 変換には対応するpresetが必要
  • jsxはReact.createElementに変換される

2. Webpack

Webpackに関しては、分割されたモジュールを一つのファイルに結合するツール、ぐらいの認識です。 js意外にも使えるらしいけど、とりあえずはjs以外のことは考えないことにします。

また、現時点ではes2015のモジュール(importとか)よりCommonJSのモジュール(requireとか)の方が 安定していると偉い人が言っていたので、無心でCommonJSスタイルを採用します。

webpack

まず最初に単純なモジュール分割を試します。 lib.jsというモジュールを作成し、src.jsからlib.jsを読み込み、webpackでbundle.jsを作成します。 また、そろそろjsファイルが多くなってくるので、jsはjsディレクトリ以下に配置することにします。

// js/lib.js
module.exports = function () {
    return 'Hello world';
};
// js/src.js
var lib = require('./lib');
document.write(lib());
// package.json
{
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "^1.13.0"
  }
}

設定ファイルとしてwebpack.conf.jsが必要なので用意します。

// webpack.conf.js
module.exports = {
    entry: './js/src.js',
    output: {
        path: __dirname + "/js",
        filename: 'bundle.js'
    }
};

entryが起点となる入力ファイルで、出力先はoutputに設定します。 entryは相対パスなのに、outputは__dirnameが付いてるのが気になりますが、 ドキュメントを見る限り output.pathは絶対パス必須らしいので、無心で__dirnameをつけましょう。

Babelの時と同様にnpm run buildを実行すると、bundle.jsが作成されます。 作成したbundle.jsは以下のようなhtmlを作成することで動作確認できます。

<script src="js/bundle.js"></script>

webpack-babel

つぎに、webpackとBabelの一番シンプルなサンプルのes2015の変換と組み合わせてみます。

webpackにBabelの変換を組み込むにはloaderというwebpackの仕組みを使います。 babel-loaderという良い奴が用意されているのでそれを使いましょう。

まずは、babelの依存パッケージや.babelrcをbabel-es2015のサンプルからコピペしてきます。 (.babelrcではなくwebpack.conf.jsに設定を移すこともできますが、それはそれでということで、.babelrcをそのまま使います。)

// package.json
{
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "babel-core": "^6.7.7",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.6.0",
    "webpack": "^1.13.0"
  }
}
//.babelrc
{
  "presets": ["es2015"]
}

次に、webpack.conf.jsにloaderの設定を追加します。

// webpack.conf.js
module.exports = {
    entry: './js/src.js',
    output: {
        path: __dirname + "/js",
        filename: 'bundle.js'
    },
    module: {
        loaders: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: "babel-loader"
            }
        ]
    }
};

最後に、src.jsとlib.jsをes2015を使って書きます。

// js/lib.js
module.exports = () => {
    return 'Hello world';
};
// js/src.js
const lib = require('./lib');
document.write(lib());

先ほどと同様にbundle.jsを作成してindex.htmlから読み込むと動作確認できます。

webpack-react

es2015の変換に加えてReactのjsxの変換を追加してみます。 .babelrcやpackage.jsonはbabel-reactの時と同様にbabel-preset-reactを追加するだけなので省略します。

まずはsrc.jsとlib.jsを見てみます。

// js/lib.js
const React = require('react');

class Hello extends React.Component {
    render() {
        return <div>Hello world!!!!!</div>
    }
}

module.exports = Hello;
// js/src.js
const React = require('react'); // Must require react because <Hello/> is translated to React.createElement(Hello).
const ReactDOM = require('react-dom');
const Hello = require('./lib');

ReactDOM.render(<Hello />, document.body);

src.jsでは一見Reactのrequireが必要ないように見えますが、 Babelで変換した後はReact.createElementを呼んでいるので、require('react')が必要です。 (requireしないとreact is not definedと怒られて涙目になりました。良い回避法既にありそうな気もする。)

index.htmlは若干修正してbodyをちゃんと書きます。 ReactDom.renderにbodyを渡すと、コンソールにお叱りのログがでるのですが、今回は簡単のためそのままいきます。 Hello world!!!!! が出力されてればOKです。

// index.html
<body>
<script src="js/bundle.js"></script>
</body>

webpackまとめ

  • requireでモジュール読み込み
  • module.exportsでモジュール公開
  • webpack.conf.jsに設定を書く
  • babel-loaderでBabelの処理を挟める

3. Redux

なんやかんややっていて忘れてましたが、そういえばReduxを試したかったんだ、 ということで、今までの流れを踏まえてReduxを組み込んでいきます。

Reduxに関してはfluxのstoreをいい感じに管理してくれるライブラリぐらいの認識です。 ということで、Reduxに関して深いお話はできないので、 Redux/example にあるサンプルを組み込む話だけします。

webpack-redux

一歩一歩が基本方針なので、まずはReactなしで、webpackにReduxを組み込みます。

Reduxの一番簡単なサンプルということで examples/counter-vanilla をwebpackを使って実装してみます。

// src.js
var Redux = require('redux');

function counter(state, action) {
    if (typeof state === 'undefined') {
        return 0
    }
    switch (action.type) {
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1;
        default:
            return state
    }
}

var store = Redux.createStore(counter);
var valueEl = document.getElementById('value');

function render() {
    valueEl.innerHTML = store.getState().toString()
}
render();
store.subscribe(render);

document.getElementById('increment')
    .addEventListener('click', function () {
        store.dispatch({type: 'INCREMENT'})
    });
document.getElementById('decrement')
    .addEventListener('click', function () {
        store.dispatch({type: 'DECREMENT'})
    });

ロジックの部分は最小限の部分をコピーしてきただけなので、無視でよいです。 大事なのは1行目で、require('redux')として、モジュールを読み込んでいます。

ということで、package.jsonにはReduxの依存を追加する必要があります。

// package.json
{
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "redux": "^3.5.1",
    "webpack": "^1.13.0"
  }
}

index.htmlにはcounter-vanillaと同様のElementが必要です。

// index.html
<body>
Clicked: <span id="value">0</span> times
<button id="increment">+</button>
<button id="decrement">-</button>

<script src="js/bundle.js"></script>
</body>

webpack-redux-react

最後にwebpack-reactとwebpack-reduxを組み合わせて、Reactを使いつつ、Reduxで状態管理をします。 参考にするコードはReduxのexamples/counterです。

まず、moduleとしてreactのcomponentとreduxのreducerを切り出します。

// js/counter.js
const React = require('react');

class Counter extends React.Component {
    constructor(props) {
        super(props)
    }

    render() {
        const {value, onIncrement, onDecrement} = this.props;
        return (
            <p>
                Clicked: {value} times
                <button onClick={onIncrement}>+</button>
                <button onClick={onDecrement}>-</button>
            </p>
        )
    }
}

module.exports = Counter;
// js/reducer.js
module.exports = function (state = 0, action) {
    switch (action.type) {
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1;
        default:
            return state;
    }
};

次に、分割したmoduleをsrc.jsxから読み込んで、examples/counterのようにreduxを組み込みます。

// js/src.jsx
const React = require('react');
const ReactDOM = require('react-dom');
const Redux = require('redux');
const Counter = require('./counter.jsx');
const reducer = require('./reducer');

const store = Redux.createStore(reducer);

function render() {
    ReactDOM.render(
        <Counter
            value={store.getState()}
            onIncrement={() => store.dispatch({ type: 'INCREMENT' })}
            onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
        />,
        document.body
    )
}

render();
store.subscribe(render);

最後に、いつも通りビルドして、index.htmlからbundle.jsを読み込んで動作確認をします。

<!-- index.html -->
<body>
<script src="js/bundle.js"></script>
</body>

その他の設定ファイル類は、最終的には以下のようになります。

// package.json
{
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "babel-core": "^6.7.7",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.6.0",
    "babel-preset-react": "^6.5.0",
    "react": "^15.0.1",
    "react-dom": "^15.0.1",
    "redux": "^3.5.1",
    "webpack": "^1.13.0"
  }
}
// .babelrc
{
  "presets": ["react", "es2015"]
}
// webpack.conf.js
module.exports = {
    entry: './js/src.jsx',
    output: {
        path: __dirname + "/js",
        filename: 'bundle.js'
    },
    module: {
        loaders: [
            {
                test: /\.jsx?$/,
                exclude: /node_modules/,
                loader: "babel-loader"
            }
        ]
    }
};

以上で全ておしまいです。長かった。。

まとめ

  • .babelrcでbabelの変換を設定。対応するpresetを読み込む必要あり。
  • requireでモジュール読み込み。module.exportsでモジュール公開。
  • webpackでmoduleを結合する。babel-loaderでBabelの処理を挟める。
  • 全部のパターンをまとめたリポジトリはこちら
    github.com

ザキオカさん先発や!