馬上少年ブログ

無用の備忘録と雑談

Javascriptでアップロード画像のプレビューを表示する(jQueryなし)

意外と使う場面が多くなってきたのでメモ。

<div>
  <label><input id="file" type="file">画像を選択</label>
  <div id="preview"></div>
</div>

input[type=file]のchangeイベントに処理を書きます。

e.target.filesにアップロードしたFileオブジェクトの配列のようなシーケンスが渡されるので、1枚しかアップロードしない場合でも以下のようにして取り出します。

var file_input = document.getElementById('file');
var file_preview = document.getElementById('preview');

file_input.addEventListener('change', function(e){
  var file = e.target.files[0];

  console.log(file.name) // => ファイル名
  console.log(file.size) // => サイズ(バイト)
  console.log(file.type) // => ファイル種類
  
  var fileReader = new FileReader();  // 手順1
  
  fileReader.onload = function(){
    var img_src = this.result;  // 手順3
    
    if ( file_preview.childElementCount != 0 ) {
      file_preview.textContent = null;
    }

    var img = document.createElement('img');  // 手順4
    img.src = img_src;

    file_preview.appendChild(img);
  }

  fileReader.readAsDataURL(file);  // 手順2
}, false);

手順1

ローカルのファイルを非同期に読み込むことができる、FileReaderオブジェクトのインスタンスを作ります。onloadで画像のURIを受け取り(this.result)、画像を表示する処理を行っています。

手順2

ファイルを実際に読み込んでくれるreadAsDataURLメソッドを呼び出します。引数にはアップロードするファイルを渡しておきます。

手順3

readAsDataURLメソッドでファイルを読み込んだ後は、fileReader の result プロパティにファイルのデータを表す、base64 エンコーディングされた data: URL の文字列が格納されます。

手順4

あとはこれをimg要素のsrc属性に入れて表示するだけです。

上記コードでは、繰り返しファイルを選択したらプレビュー画像を入れ替えるようにしています。

ちなみに

changeイベントは、ファイルをアップロードした状態で、同じinputでもう一度選択しようとしてキャンセルした場合にも走ります。その場合はe.target.filesはundefinedとなるので、より厳格にするなら分岐しなければなりません。

参考

developer.mozilla.org

developer.mozilla.org

Wordpressで技術系ブログを書く時に使えそうなプラグイン・ツール・機能

はてなブログからWordpressへの移行を進めているところですが、直近railsに夢中でサボり気味です。

尻を叩く意味を込めて、Wordpressに関する覚書。

5系を入れてしまった

何も考えず最新版のWordpressをインストールしたら、昨年末に出たバージョン5系を入れてしまいました。今まで使ったことのある4系とはかなり勝手が違い、さらに対応しているプラグインもまだ少ない...。

しかしせっかく入れてしまったので、5系のまま慣れていきたいと思います。

React + WP REST API によるブログ構築

3月末を目標にReact + WP REST API によるブログ構築を進め、年度内に公開したいと考えています。

Wordpressのテーマは使わず、DBとやりとりする記事投稿・管理のみWordpressの力を借ります。

エディタ

はてなブログでの記事編集はMarkdown方式で行なっています。WordpressでもMarkdownで編集したいので、プラグインを入れます。

Markdown Editorというわかりやすい名前のプラグインがよく使われているようだったので、入れてみました。

ja.wordpress.org

ただ、これが5系だと思うような動きになりませんでした...。

最新版で検証済みのエディタの中から発見したのが、WP Editor.mdというプラグインです。使いやすく、挙動も期待通りなので、採用しました。

ja.wordpress.org

ソースコードの貼り付け

技術系をやっていく上で、<pre><code></code></pre>で囲んだ範囲を自動でシンタックスハイライトしてくれるツールが必要です。

Highlight.jsというライブラリを使いたいんですが、Wordpressにはこれを含んだPrismaticというプラグインがありました。

ja.wordpress.org

この中のHighlight.jsを適用すると、記事の中のコードがハイライトさせて表示することができます。

しかし、今回はテーマを使わずReactで構築するので、WordpressプラグインとしてHighlight.jsを入れるわけにはいきません。

npmパッケージにも同じものがあるので、そっちの環境でインストールすることにします。

WP REST API

Wordpressの投稿情報を返してくれるAPIです。プラグインが必要なのかと思っていましたが、すでに標準機能として使えるようで、

ドメイン(/wpディレクトリ)/wp-json/wp/v2/posts/

にアクセスするだけでOKでした。投稿だけでなくいろんな情報を返してくれるようです。必要に応じて調べていきます。

今後に向けて

現状Routingの設定をしたところなので、APIを組み込めばかなり形が見えてくると思います。

Wordpressの投稿まわりの整備もしなければ...

FluxのActionCreatorの中でaxiosを使ってAPIを叩く

Reactのfluxアーキテクチャでは、外部APIとのやりとりはActionCreatorのところで行われるとのことです。

今回は無料のOpenWeathermap API を使って、お天気を表示するだけのアプリを作成します。

axiosとは

PromiseベースでHTTP通信を簡単に行うことができるライブラリ。

以下のようにPromiseベースなので簡単にリクエストを生成でき、とてもみやすいです。

const url = 'https://xxx.com'
const params = { hoge: 'fuga' }

axios
  .get(url, { params })
  .then(data => {
    //成功時の処理
    console.log(data)
  })
  .catch(error => {
    //失敗時の処理
    console.log(error)
  })

以下のように設定をオブジェクトで渡すこともできます。パラメータがある時などこっちの方がわかりやすいですね。

const url = 'https://xxx.com'
const params = { hoge: 'fuga' }

axios({
  method : 'GET',
  url    : url,
  params : params
}).then(data => {
  //略
})

post通信の時はpostメソッドを使い、paramsではなくdataを使います。

const url = 'https://xxx.com'
const data = { hoge: 'fuga' }

axios({
  method : 'POST',
  url    : url,
  data   : data
}).then(response => {
  //略
})

環境構築

Nodo.jsをインストールして、プロジェクトディレクトリを作成してください。

まずは必要なパッケージをインストール。

$ npm install react react-dom
$ npm install --save-dev flux @babel/core @babel/preset-env @babel/preset-react babel-loader webpack webpack-cli webpack-dev-server

今回使用するaxiosもインストールしておきます。

$ npm install axios

package.jsonおよびbabel, webpackの設定ファイルは以下のとおり。

package.json

{
  "name": "otenki_app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server",
    "build": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^0.18.0",
    "react": "^16.8.1",
    "react-dom": "^16.8.1"
  },
  "devDependencies": {
    "@babel/core": "^7.2.2",
    "@babel/preset-env": "^7.3.1",
    "@babel/preset-react": "^7.0.0",
    "babel-loader": "^8.0.5",
    "flux": "^3.1.3",
    "webpack": "^4.29.3",
    "webpack-cli": "^3.2.3",
    "webpack-dev-server": "^3.1.14"
  }
}

webpack.config.js

module.exports = {
  mode: 'development',
  entry: './src/index',
  output: {
    filename: 'app.js',
    path: __dirname + '/public/js'
  },
  devServer: {
    contentBase: __dirname + '/public',
    port: 8080,
    publicPath: '/js/'
  },
  devtool: 'eval-source-map',
  module: {
    rules: [{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'babel-loader'
    }]
  }
};

.babelrc

{
  "presets": [
    [
      "@babel/preset-env", {
        "targets": {
          "node": "current"
        }
      }
    ], 
    "@babel/preset-react"
    ]
}

必要なディレクトリを作っておき、エントリーポイントのsrc/index.jsと、読み込み用のpublic/index.htmlを記述します。

$ mkdir public src src/containers src/data src/Views

src/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import AppContainer from './containers/AppContainer';

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

src/index.html

<!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="js/app.js"></script>
</body>
</html>

Fluxの各部門を実装する

Dispatcher

モジュールをimportしてインスタンス化するだけでOKです。ありがたい。

src/data/Dispatcher.js

import { Dispatcher } from 'flux';

export default new Dispatcher();

Container

ViewとStoreとStateを渡しておけばOK。ほんとうにありがたい。

src/containers/AppContainer.js

import AppView from '../Views/AppView';
import ActionCreator from '../data/ActionCreator'
import { Container } from 'flux/utils';
import OtenkiStore from '../data/OtenkiStore';

const getStores = () => {
  return [
    OtenkiStore,
  ];
};

const getState = () => {
  return {
    otenki: OtenkiStore.getState(),
    onGetOtenki: ActionCreator.getOtenki,
  };
}
export default Container.createFunctional( AppView, getStores, getState );

ActionCreator

今回の肝です。axiosで非同期通信しています。APIは都市名とAPIキーが必要です。APIキーは別途取得しておいてください。

qiita.com

返ってきたデータをActionTypeとともにDispatcherに送ります。ActionTypeは別ファイルで定義しています。

import ActionTypes from './ActionTypes';
import Dispatcher from './Dispatcher';
import axios from 'axios';

const Actions = {

    getOtenki(city) {
    const url = 'https://api.openweathermap.org/data/2.5/weather'
    const params = {
      q: city,
      units: 'metric',
      appid: 'YOUR_API_KEY'
    }

    axios({
      method : 'GET',
      url    : url,
      params : params
    })
    .then((data) => {
      Dispatcher.dispatch({
        type: ActionTypes.GET_OTENKI,
        data: data.data,
      })
    })
    .catch((error) => {
      console.log(`失敗しました:${error}`)
    })
  }

};

export default Actions;

src/data/ActionTypes.js

const ActionTypes = {
  GET_OTENKI: 'GET_OTENKI',
};

export default ActionTypes;

Store

データをそのまま返します。

src/data/OtenkiStore.js

import {ReduceStore} from 'flux/utils';
import ActionTypes from './ActionTypes';
import Dispatcher from './Dispatcher';

class OtenkiStore extends ReduceStore {
  constructor() {
    super(Dispatcher);
  }

  getInitialState() {
    return '';
  }

  reduce(state, action) {
    switch(action.type) {

      case ActionTypes.GET_OTENKI: {
        if(action.data === undefined) {
          return state;
        } else {
          return action.data;
        }
      }

      default: {
        return state;
      }

    }
  }
}

export default new OtenkiStore();

View

東京、大阪、岡山の3都市を用意しました。citiesをmapしてボタンを生成し、valueの都市名をコールバックの引数としてそのままActionに渡します。この都市名はAPIを叩くときに使用しています。

Views/AppView.js

import React from 'react'

const AppView = props => (
  <div>
    <OtenkiBtn {...props} />
    <OtenkiView {...props} />
  </div>
)

const cities = [ 'Tokyo', 'Osaka', 'Okayama' ]

const OtenkiBtn = props => {
  const onGetOtenki = (e) => props.onGetOtenki(e.target.value);
  return (
    <div>
      {cities.map(city => (
        <button key={city} onClick={onGetOtenki} value={city}>{city}</button>
      ))}
    </div>
  )
}

const OtenkiView = props => {
  if(props.otenki) {
    return (
      <div>
        <p>{props.otenki.name}の天気</p>
        <div>{props.otenki.weather.main}</div>
        <div>最高気温:{props.otenki.main.temp_max}&deg;C</div>
        <div>最低気温:{props.otenki.main.temp_min}&deg;C</div>
      </div>
    )
  } else {
    return (
      <div>
        <p>ここにお天気情報を表示します。</p>
      </div>
    )
  }
}

export default AppView

OtenkiViewでは、渡ってききたお天気データを表示しています。もっといろんな情報をもらえるのですが、今回は簡素にしてます。

実行

npm run dev

localhost:8080にwebpack-dev-serverが立ち上がり、ボタンの切り替えで都市ごとの天気を取得することができました。

Node.js開発環境でSassをコンパイルする

Sassをコンパイルするときにターミナル開いたりGUIツール開いたりすると思いますが、reactでサイトやアプリ作ってる時はNode.jsで一気にコンパイルしてくれたらありがたいですよね。

そんなとき便利なnode-sassを使ってみました。

インストール

global環境にinstallすればいいんでしょうが、なんとなくglobalいじりたくなくて、devにしときました。お好きな方にinstallしてください。

$ npm install --save-dev node-sass

scriptにnode-sassのコマンドを追加しておきます。css/style.scssを同名のcssコンパイルする設定です。

package.json

{
  "name": "sass_sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "node-sass css/style.scss css/style.css"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "node-sass": "^4.11.0"
  }
}

実行してみる

適当にscssを記述して、コンパイルを試します。

css/style.scss

$color-text: #3b3b39 !default;
$color-brand: #9aebe1 !default;
$max-width: 1080px !default;
$font-family: "Hiragino Kaku Gothic ProN", Meiryo, sans-serif !default;

$breakpoints: (
  'sp': '(max-width: 400px)',
  'tb': '(max-width: 768px)',
) !default;

@mixin mq($breakpoint: tb) {
  @media #{map-get($breakpoints, $breakpoint)} {
    @content;
  }
}

/*-----------------------------------------------------------*/

body {
  color: $color-text;
  font-family: $font-family;
}

.wrapper {
  background-color: $color-brand;
  max-width: $max-width;

  @include mq(tb) {
    max-width: auto;
    width: 100%;
  }
}

.logo {
  width: 50%;

  & .text {
    color: $color-brand;
  }
}

変数の定義、mixinによるbreacpoint管理、セレクタのネストなど書いてみました。コンパイルします。

npm run dev

css/style.cssコンパイル結果)

/*-----------------------------------------------------------*/
body {
  color: #3b3b39;
  font-family: "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; }

.wrapper {
  background-color: #9aebe1;
  max-width: 1080px; }
  @media (max-width: 768px) {
    .wrapper {
      max-width: auto;
      width: 100%; } }

.logo {
  width: 50%; }
  .logo .text {
    color: #9aebe1; }

問題なくコンパイルできますね。しかしネストしたところにインデントが残っていて非常にキモいです。

これはnode-sassコマンドにオプションを加えることで解決しますが、これは記事の後半に掲載しています。

複数ファイルをまとめてコンパイル

モジュールごとに別ファイルでscssを書いて、最後に@importするのも同じやり方です。

css/style.scss

@import "modules/_header";
@import "modules/_footer";

css/modules/_header.scss

.header {
  width: 100%;
  &.logo {
    width: 50%;
  }
}

css/modules/_footer.scss

.footer {
  width: 100%;
  text-align: center;
  &.logo {
    width: 30%;
  }
}

結果

css/style.css

.header {
  width: 100%; }
  .header.logo {
    width: 50%; }

.footer {
  width: 100%;
  text-align: center; }
  .footer.logo {
    width: 30%; }

node-sassコマンドのオプション

node-sassコマンドにいくつかオプションを加えることができます。

全てのオプションは以下のページに記載されています。いくつか取り上げます。

www.npmjs.com

--output-style

先ほど非常にキモいインデントされたCSSコンパイルされていましたが、--output-styleオプションを加えることで、コンパイルのフォーマットを指定することができます。

  "scripts": {
    "build": "node-sass css/style.scss css/style.css --output-style expanded"
  },

--output-style expandedを設定してコンパイルした例がこちらです。

/*-----------------------------------------------------------*/
body {
  color: #3b3b39;
  font-family: "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
}

.wrapper {
  background-color: #9aebe1;
  max-width: 1080px;
}

@media (max-width: 768px) {
  .wrapper {
    max-width: auto;
    width: 100%;
  }
}

.logo {
  width: 50%;
}

.logo .text {
  color: #9aebe1;
}

.header {
  width: 100%;
}

.header.logo {
  width: 50%;
}

.footer {
  width: 100%;
  text-align: center;
}

.footer.logo {
  width: 30%;
}

デフォルトがnestedで、他にも各行を改行せずに一行にしたcompactや、minify化してくれるcompressedがあります。

-w, --watch

ファイルの変更を監視してくれます。この例でいうと、style.scssを保存すると自動的にstyle.cssコンパイルされます。

  "scripts": {
    "build": "node-sass css/style.scss css/style.css -w"
  },

--source-map

通常だとブラウザで検証する際は、コンパイルされ実際に読み込まれたcssファイルのみ検証可能ですが、このオプションをつけることでscssファイルを使って検証を行うことができます。

まとめ

簡単なので即導入します。

react-router@v4でURLを管理する:URLのネスト編

以前の記事で、react-routerを使ったページ遷移を実現することができました。

blanco20.hatenablog.com

次は、NewsページやWorksページ用の個別の記事データを用意し、URLに割り振られた値ごとにコンポーネントを描画してみたいと思います。

NewsとWorksでやることは全く同じなので、記事ではNewsの例だけ載せておきます。Worksも含めた完全なコードはgithubを参照してください。

データの用意

dataディレクトリ内にデータの配列を用意してexportしておきます。ほんとになんでもいいので適当に作ります。

pathに指定する値を、URLのパスとして用いることにします。pathで各投稿データを検索して返してくれる関数も同時に用意しておきます。

data/NewsData.js

const NewsData = [
  {
    path: '0',
    date: '2019-1-3',
    category: 'topics',
    title: 'Webサイトを開設しました',
    text: 'News01のテキストです。News01のテキストです。News01のテキストです。',
  },
  {
    path: '1',
    date: '2019-2-5',
    category: 'info',
    title: '新しいPCを購入しました',
    text: 'News02のテキストです。News02のテキストです。News02のテキストです。',
  },
  {
    path: '2',
    date: '2019-3-9',
    category: 'topics',
    title: 'ブログを始めました',
    text: 'News03のテキストです。News03のテキストです。News03のテキストです。',
  },
]

export default NewsData;

export const newsByPath = (path) => {
  return NewsData.find(news => news.path === path)
}

ここで作ったデータをもとに、Newsの一覧ページと個別の詳細ページを表示していきます。

/newsで呼び出されるコンポーネントを書き換える

以前の記事では静的なリストを返していただけのsrc/Templates/News.jsですが、つくったNewsDataを使ってリストを描画するように書き換えます。

トップページから/newsというpathが指定されたRouteにより呼び出されるNewsコンポーネントですが、この中でさらにRouteを記述します。これでRouteのネストが実現できます。

このネストしたRouteの中で、pathが/newsのときはNewsListを、/news/パス名の時はNewsItemを描画するようにします。

import React from 'react'
import NewsData, { newsByPath } from '../data/NewsData';
import { Route, Link } from 'react-router-dom'

const News = () => (
  <div>
    <Route exact path="/news" component={NewsList} />
    <Route path="/news/:path" component={NewsItem} />
  </div>
)

export default News

上のように、Routeのpathに/news/:pathのようにコロンをつけて記述することで、そこに指定した値を呼び出されたコンポーネントに渡すことができます。その値には、props.match.paramsでアクセスできます。

例:/news/1 にアクセス => NewsItemにprops.match.params = '1'が渡される

同じファイル内で、NewsListNewsItemを定義しておきます。

const NewsList = () => (
  <div>
    <h2>Newsの一覧ページです。</h2>
    <ul>
      {NewsData.map(news => (
        <li key={news.path}>
          <Link to={`/news/${news.path}`}>
            <time>{news.date}</time>
            <span>{news.category}</span>
            <div>{news.title}</div>
          </Link>
        </li>
      ))}
    </ul>
  </div>
)

const NewsItem = props => {
  const { path } = props.match.params
  const news = newsByPath(path)

  if (typeof news === 'undefined')  {
    return (
      <div>
        <p>記事が存在しません</p>
      </div>
    )
  }

  return (
    <div>
      <h2>News記事の詳細ページです</h2>
      <div>
        <p>{news.date}</p>
        <span>{news.category}</span>
        <h3>{news.title}</h3>
        <p>{news.text}</p>
      </div>
    </div>
  )

}

NewsListコンポーネントでは、NewsDataをmapしてリストを作り、それぞれに/news/パス名というリンクを設定しています。

NewsItemコンポーネントでは、受け取ったpathの値を使ってデータを特定し、そのデータを描画しています。

データ型は今回の場合ではObjectですが、存在しないpathキーで呼び出された場合はundefinedとなってしまうので、その際は「存在しません」と表示するようにしました。

実行

npm run dev

これ実は失敗しました。理由はなんだろうと探しましたが、以下の記事に行き当たりました。

qiita.com

これをもとに自分のindex.htmlも修正。

<!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>React Router Sample</title>
</head>
<body>
<div id="root"></div>
<script src="/js/app.js"></script>
</body>
</html>

これで実行したらできました!

まとめ

ルーティングの基本的な手法を学びました。パラメータを扱ったりリダイレクトしてみたりと、まだまだreact-routerの機能はたくさんあるので、随時試していければと思います。

今回の記事だと動画を貼った方がわかりやすいと思いますが、面倒でした…ごめんなさい。

↓完成品はこちらです。

github.com

react-router@v4でURLを管理する

react-routerとは

Reactはページ遷移せず非同期でDOMの描画・APIへのアクセスを行います。そのためReactでWebサイトを作るとき、URLと画面の状態が関連づいていない状態になってしまいます。

それを解決するのがreact-routerです。react-routerは、コンテンツの切り替えと同時にURLを切り替える、URLをリクエストしたら然るべきコンポーネントを描画する、といった、画面とURLの関連付けのための機能を提供してくれます。

今回作るサンプルサイト

CSSを書く元気が無く、生のHTMLで失礼します。
以下のような構造のサイトを作っていきます。

── TOP
   ├──About
   ├── News
   │   ├── News記事
   │   └── News記事
   ├── Works
   │   ├── Works記事
   │   └── Works記事
   └── Contact

よくあるシンプルな構造ですね。

react-routerのインストール

npmでreact-router-domと、その他の必要なパッケージをインストールします。

$ npm i react-router-dom
$ npm i react react-dom
$ npm i -D @babel/core @babel/preset-env @babel/preset-react babel-loader webpack webpack-cli webpack-dev-server

package.jsonは以下のとおり。あとで出てきますが、scriptsのdevコマンドがwebpack-dev-serverだけでは、思うような動きになりません。しかし今はとりあえずこれで進めます。

{
  "name": "router_sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server",
    "build": "webpack",
    "watch": "webpack --watch"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^16.8.1",
    "react-dom": "^16.8.1",
    "react-router-dom": "^4.3.1"
  },
  "devDependencies": {
    "@babel/core": "^7.2.2",
    "@babel/preset-env": "^7.3.1",
    "@babel/preset-react": "^7.0.0",
    "babel-loader": "^8.0.5",
    "webpack": "^4.29.2",
    "webpack-cli": "^3.2.3",
    "webpack-dev-server": "^3.1.14"
  }
}

ディレクトリ構成

Reactでサイトを作ったことがないので、どういう構成が適しているのか手探りです。とりあえず以下のような感じ。

├── node_modules ── 略
├── package-lock.json
├── package.json
├── public
│   └── index.html
├── src
│   ├── AppRouter.js
│   ├── Components
│   │   ├── Footer.js
│   │   └── Header.js
│   ├── Templates
│   │   ├── About.js
│   │   ├── Contact.js
│   │   ├── Home.js
│   │   ├── News.js
│   │   └── Works.js
│   ├── data
│   │   ├── NewsData.js
│   │   └── WorksData.js
│   └── index.js
└── webpack.config.js

AppRouterに全てのコンポーネントとルーティングを集約し、レンダリングします。

src/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import AppRouter from './AppRouter'

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

webpackを使っています。バンドルしたファイルはpublic/jsにapp.jsとして吐き出してもらいます。以下はそれを読み込むhtmlです。

public/index.html

<!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>router sample site</title>
</head>
<body>
  <div id="root"></div>
  <script src="js/app.js"></script>
</body>
</html>

webpack.config.jsを含めた、その他packageとconfigファイルは以下のコードをご参照ください。

github.com

Routeの設定

AppRouter

それではAppRouterを書いていきます。<BrowserRouter>の中に、パスごとに<Route>を記述します。pathに指定したURLとcomponentに指定したコンポーネントが対応します。
例:/aboutにアクセスすると、<About />が呼び出される

src/AppRouter.js

import React from 'react'
import { BrowserRouter, Route, Switch } from 'react-router-dom'

// Components
import Header from './Components/Header'
import Footer from './Components/Footer'

// Templates
import Home from './Templates/Home'
import About from './Templates/About'
import News from './Templates/News'
import Works from './Templates/Works'
import Contact from './Templates/Contact'

const AppRouter = () => (
  <BrowserRouter>
    <div>
      <Header />
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
        <Route path="/news" component={News} />
        <Route path="/works" component={Works} />
        <Route path="/contact" component={Contact} />
      </Switch>
      <Footer />
    </div>
  </BrowserRouter>
)

export default AppRouter

pathはURLの前方一致で探すので、path="/"としていると、どのURLにアクセスしても<Home />が呼び出されてしまいます。

そのため<Home />を呼び出す<Route />にはexact属性をつけて、パスと完全一致のときのみ描画するよう設定します。

Header

headerナビゲーションにはリンクを設定します。Linkto属性でパスを渡しておきます。

Header.js

import React from 'react'
import { Link } from 'react-router-dom'

const Header = () => (
  <header>
    <small>【Header】</small>
    <nav>
      <ul>
        <li><Link to="/">Home</Link></li>
        <li><Link to="/about">About</Link></li>
        <li><Link to="/news">News</Link></li>
        <li><Link to="/works">Works</Link></li>
        <li><Link to="/contact">Contact</Link></li>
      </ul>
    </nav>
  </header>
)

export default Header;

Linkにはいろんな機能がありますが、とりあえず上のようにします。これだと普通の<a>タグと変わらないかもしれない…。

その他コンポーネント

他のコンポーネントは、普通の静的コンポーネントです。ざっと書きます。ざっと。

※reactのimportとコンポーネントのexportは省略してます。完全なコードはgithubをご参照ください。

//src/Component/Footer.js
const Footer = () => (
  <footer>
    <small>【Footer】 Copyright</small>
  </footer>
)

//src/Templates/About
const About = () => (
  <div>
    <h1>Aboutページです。説明を書きます。</h1>
  </div>
)

//src/Templates/News
const News = () => (
  <div>
    <h1>News一覧ページです。</h1>
    <ul>
      <li>news1</li>
      <li>news2</li>
      <li>news3</li>
    </ul>
  </div>
)

//src/Templates/Works
const Works = () => (
  <div>
    <h1>Works一覧ページです。</h1>
    <ul>
      <li>works1</li>
      <li>works2</li>
      <li>works3</li>
      <li>works4</li>
      <li>works5</li>
    </ul>
  </div>
)

//src/Templates/Contact
const Contact = () => (
  <div>
    <h1>Contactページです。</h1>
    <p>お名前</p>
    <input type="text"/>
    <p>メールアドレス</p>
    <input type="mail"/>
    <p>メッセージ</p>
    <textarea name="" id="" cols="30" rows="10"></textarea>
  </div>
)

/data/以下のファイルはとりあえず何も書かなくてOKです。

webpack-dev-serverで確認

$ npm run dev

立ち上がった環境にアクセスすると、リンクナビゲーションが機能しているのがわかります。 しかしこの状態ではリロードが機能せず、さらにURLを直叩きすると見失ってしまうようです…かわいそうに。

解決策

これは、存在しないディレクトリにアクセスしようとしているのが原因だそうです。404エラーです。

サーバーなら.htaccessで対応すればいいと思いますが、今はwebpack-dev-serverの環境なので、historyApiFallbackのオプションを使うことができます。

package.jsonのscriptsの部分で、コマンドにオプションを追加しておきます。

package.json

// 略
  "scripts": {
    "dev": "webpack-dev-server --history-api-fallback",
    "build": "webpack",
    "watch": "webpack --watch"
  },
// 略

これでもう一度実行すれば、リロードが可能になり、URLの直叩きでも画面が切り替わります。

$ npm run dev

次はネストしたルーティングを実装して、NewsやWorksの個別記事のURLを制御します。

長くなったので、記事をまたぎます。続きはこちら。

blanco20.hatenablog.com

React + Flux について:TodoアプリUI構築チュートリアルを終えての備忘録

Fluxのチュートリアルを写経して学んだことを記録しておく。理解が追いついていない部分は、追々理解したタイミングで追記する。

github.com

Fluxとは

Facebookが提唱するUI設計思想。データの流れを一方向に限定することで、状態管理や機能拡張をしやすくする。

f:id:blanco20:20190205220836p:plain
flux

登場人物

View

ユーザーが触るUIそのもの。Reactでやる。イベントによりActionを発行する。

Action Creators

何をどうする、という指示内容をActionとして取りまとめてDispatcherに渡す。Todoリストに項目を追加する、項目を削除する、編集するなど。処理の内容だけを指示する。APIを叩く場合もActionで行う。

Dispatcher

StoreにActionの指示内容を送る。Storeは複数作ることができ、各StoreにあらゆるActionを渡す。

Store

DispatcherからActionを受け取り、Actionごとに定めるロジックに従ってStateを更新してchangeイベントを発行。

さいごにView

Storeによる状態変更(changeイベント)を検知し、再レンダリングして書き換わる。

Flux/Utils

FacebookによるFluxライブラリ。Fluxの各部門に対応するコンポーネントを提供する。

ReduceStore

Storeで状態変更された際に、自動でchangeイベントを発行してくれる。

Container

ReduceStoreのデータを自動で受け取ってViewに反映させる。

実装

ActionCreator

ActionTypeを定義。指示内容を示す命名

const ActionTypes = {
  ADD_TODO: 'ADD_TODO',
 // 略
};

export default ActionTypes;

指示内容としてのActionTypeと、textやidなどのデータをDispatcherに送る。

import TodoActionTypes from './TodoActionTypes';
import TodoDispatcher from './TodoDispatcher';

const Actions = {
  addTodo(text) {
    TodoDispatcher.dispatch({
      type: TodoActionTypes.ADD_TODO,
      text,
    })
  },

 // 略

};

export default Actions;

Dispatcher

Dispatcherモジュールを読み込む。

import { Dispatcher } from 'flux';

export default new Dispatcher();

Store

ReduceStoreのサブクラスとして定義。getInitialStateでStateの初期状態を定義し、reduceでDispatcherから送られたAction(及び付随するデータ)を受け取り、ActionTypeごとにStateを更新するロジックを記述。

import Immutable from 'immutable';
import {ReduceStore} from 'flux/utils';
import TodoActionTypes from './TodoActionTypes';
import TodoDispatcher from './TodoDispatcher';
import Todo from './Todo';

class TodoStore extends ReduceStore {
  constructor() {
    super(TodoDispatcher);
  }

  getInitialState() {
    return Immutable.OrderedMap();
  }

  reduce(state, action) {
    switch(action.type) {

      case TodoActionTypes.ADD_TODO: {
        if(!action.text) {
          return state;
        }
        const id = Counter.increment();
        return state.set(id, new Todo({
          id,
          text: action.text,
          complete: false
        }))
      }

  // 略

      default: {
        return state;
      }
    }
  }
}

export default new TodoStore();

View

UIレンダリングのロジック。propsにより情報とコールバックを受け取る。Reactで書く。

import React from 'react';

function AppView(props) {
  return (
    <div>
      <Main {...props} />
    </div>
  );
}

const Main = (props) => {
  if (props.todos.size === 0) {
    return null;
  }
  return (
    <section id="main">
      <section id="main">
      <ul id="todo-list">
        {[...props.todos.values()].reverse().map(todo => (
          <li key={todo.id}>
            <div className="view">
              <input
                className="toggle"
                type="checkbox"
                checked={todo.complete}
                onChange={() => props.onToggleTodo(todo.id)}
              />
              <label>{todo.text}</label>
              <button
                className="destroy"
                onClick={() => props.onDeleteTodo(todo.id)}
              />
            </div>
          </li>
        ))}
      </ul>
    </section>
  )
}


export default AppView;

Container

StoreからViewにStateをコネクトする。

import AppView from '../views/AppView';
import {Container} from 'flux/utils';
import TodoActions from '../data/TodoActions';
import TodoStore from '../data/TodoStore';

function getStores() {
  return [
    TodoStore,
  ];
}

function getState() {
  return {
    todos: TodoStore.getState(),

    onAdd: TodoActions.addTodo,
    onDeleteTodo: TodoActions.deleteTodo,
    onToggleTodo: TodoActions.toggleTodo,
  };
}

export default Container.createFunctional(AppView, getStores, getState);

レンダリング

Containerを指定してレンダリング

import AppContainer from './containers/AppContainer';
import React from 'react';
import ReactDOM from 'react-dom';

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

import TodoAction from './data/TodoAction';