はじめに
この記事は、政治ハック:2017年東京都議会議員選挙の候補者情報をJSONデータ化するの後編です。前編で作成したJSONデータを元に、React + Material-UIでサイトを構築、Github Pagesにデプロイするまでを解説します。
ソースコード:
https://github.com/mshk/togisen2017
ウェブサイト:
https://mshk.github.io/togisen2017/
技術スタック
今回使用したツールは下記の通りです。
- Create React App
- React
- Redux
- React Router
- Material-UI
- Github Pages
選挙情報のサイトは、前の記事でも書いたように制作の期間が短くなりがちで、しかも選挙当日に稼働していないと意味をなさないため、サーバの構築やモニタリングなどは可能な限り避ける必要があります。そのため、スタティックなファイル配信のみでインタラクティブなサイトを構築できるReactを採用し、Single Page Applicationとして実装、デプロイ先も自分でサーバの管理をする必要がないGithub Pagesを使用しました。
Create React Appで骨組みを構築する
Create React AppはFacebookが提供のいわゆるボイラープレート・ツールです。
> npm install create-react-app -g
> create-react-app togisen2017
> cd togisen2017
> npm start
でひとまず動作するReactプロジェクトを生成できます。
> npm run build
を実行するとbuild
ディレクトリにデプロイ用のHTML+JavaScriptがダンプされます。このbuild
ディレクトリをそのままスタティック配信すればサイトが動作します。
React Routerを使ったルーティング
ページ遷移を実現するため、React Routerを使用しました。
<Router
basename={'/togisen2017'}
{...props}
>
<Switch>
<Route exact path="/" component={Candidates} />
<Route path="/data" component={Data} />
<Route path="/about" component={About} />
<Route component={NotFound} />
</Switch>
(省略)
</Router>
今回のプロジェクトでは、タイトルバーとドロワーをアプリケーション全体で描画し、Switch
で囲んだボディの部分にそれぞれのページをレンダリングするコンポーネントを差し込む形式で記述しています。
Reduxでデータ管理
先の記事で制作したJSONデータにRedux
を通じてアクセスします。Actionの中でHTTPクライアントaxios
を使ってHTTP経由でJSONを取得しています。今回のサイトは動的にデータを変更する機能がないため、JSONデータをwebpackでコンパイル時にJSに含めてしまい、単純にrequire data from '../candidates.json'
のようにしても良かったのですが、将来的に結果にフィルターをかけるなどの拡張を行うためにこのようにしました。後からGithub Pagesへのデプロイ時に苦労することになったので、正直どうだったのか?という気持ちはありますが...
Material-UIでUIを構築
UIの構築にはMaterial-UIを使用しました。Googleの提唱するMaterial DesignのUIパーツをReactコンポーネントとして提供しています。サイトのギャラリーをチェックして、今回のサイトで使用するUIパーツが揃っていて、Google検索の結果でも良く利用されている様子だったので使用しました。レスポンシブに対応していて、デスクトップとモバイルに同じコードで対応出来る点も決め手になりました。
結果としては、だいたいこんな感じ?と思う通りに動作し、ドキュメントも非常に分かりやすかったので助かりました。サイト独自のUIを追求したい場合のカスタマイズの柔軟度については分かりませんが、また使ってみたいコンポーネントです。
- ページトップのナビゲーション・バーに
AppBar
- メニューボタンを押した時に横から出てくるメニューに
Drawer
とMenu
- それぞれの候補者の情報の表示に
Card
を利用しています。
Cardコンポーネントの「エキスパンド」機能を使って表示する情報を整理する
Card
コンポーネントは、ヘッダー部分にプロフィール画像を表示するなど、定番フォーマットが提供されていますのでこれをそのまま使っています。また、選挙の候補者の情報ということで、一覧では選挙区のみを表示し、ユーザーが自分の選挙区をタップするとさらに候補者の一覧が表示される「アコーディオン・メニュー」方式にしています。この「エキスパンド」の仕組みも、Card
コンポーネントが標準で提供しているものです。
<Card key={this.props.area}>
<CardHeader
title={this.props.area}
showExpandableButton={true}
actAsExpander={true}
/>
<CardText expandable={true}>
{candidates}
</CardText>
</Card>
Drawerをモバイル/デスクトップ両対応にする
前述のように、Material-UIはレスポンシブに対応したライブラリなのですが、なぜかDrawerの幅については「%で指定する」というAPIになっています。両方のデバイスでちょうどよい値を設定することは難しいため、isMobile
を判定して幅を切り替えています。
const isMobile = window.matchMedia("only screen and (max-width: 760px)")
render() {
(省略)
<Drawer
docked={false}
width={ isMobile.matches ? '80%' : '30%'}
open={this.state.open}
onRequestChange={(open) => this.setState({ open })}
>
}
AppBarのmarginを削除する
小ネタですが、Create React Appの生成するプロジェクトにMaterial-UIを使用すると、画面上部のナビゲーション・バーにマージンがつくという問題が発生しました。時間もないので、public/index.html
のbodyタグに直接style="margin: 0"
をつけるという荒業で対応しました。
Github Pagesへのデプロイ
ここまでは順調だったのですが、すっかり忘れていたのがGithub Pagesでホスティングをする際のURLの形式です。今回は独自ドメインを使用しなかったのですが、URLがhttps://mshk.github.io/togisen2017
とサブパスがルートとなります。この場合、画像やJSONのパスが開発環境とことなるため、いくつか考慮が必要です。
- 画像はwebpackのimportの仕組みを使う
- Redux StoreでAPIアクセスに使用する
axios
を初期化する際にbaseURL
を指定する
という対応をしています。
import Axios from 'axios'
let axios = Axios.create({
baseURL: '/togisen2017/'
})
Github PagesにSPAをデプロイする際のルーティング問題
このサイトはスタティック配信のみによるSPAとなっていますが、このプロジェクトをGithub Pagesにデプロイする際にサブパスのルーティングをどうするのか?という問題があります。Github Pagesはサブパスを「ファイルパス」として認識するからです。自前でサーバを立てる場合は、ルーティングを細く設定することができるのですが、今回はそうもいきません。
- ルートパスへのリクエストには
index.html
の内容が返される - 「/assets/xxx.js」などはそのパスに存在するファイルの内容が返される
- それ以外は404
というシンプルなルーティング・ルールになっていて、どうしたものか?と思っていたのですが、「404.html」をカスタマイズできる事に気づき、では404.htmlにindex.htmlの内容をコピーすれば良いのでは?と思い試してみたところ、さくっとうまく行きました。React Routerで独自に404を返す処理を実装しているので、アセットとしても機能としても存在しないパスにも対応できます。
かなりチートぽいですが、ひとまず動いているということで、同じようにGithub PagesでSPAを配信する方は試してみて下さい。あるいは、もっとちゃんとした解決方法があれば教えてください。
index.htmlの内容は、npm run build
をする度に(ロードするJSのファイル名が)変わるので、packages.json
のscript
でReactプロジェクトのビルドを行い、その後にindex.html
を404.html
にコピーする様にしています。
Reactで動的に生成したページはGoogleにインデックスされるのか?問題
このサイトのように、空のindex.htmlを配信して、後からReactで動的にコンテンツをレンダリングする場合に気になるのは「Googleのクローラーはちゃんとインデックスしてくれるのだろうか?」という点です。2016年に書かれた以下の記事ではReactで構築したサイトを[Fetch as Google]でテストしていて、この時点では「Reactは問題なかったけど、React Routerを入れたらインデックスされなくなった」という結論が書かれていました。
Testing a React-driven website’s SEO using “Fetch as Google”
今回React Routerを試したのは、その辺の検証も目的だったのですが、結論としては現在のバージョンでは問題なくインデックスされていました。
React Router入れてみたけど、いまのところFetch as Googleで問題なくレンダリングできてる。まあでも、こうやっていちいち確認しないといけないのは辛いからserver side renderingを導入するのが妥当とは言える https://t.co/zY3tEW4yU8
— 立薗理彦 (@mshk) 2017年6月28日
終わりに
駆け足ですが、Reactを使ったSingle Page Applicationの構築と、そのビルド内容をGithub Pagesで配信する方法について書きました。参考になれば幸いです。