全てが初めてでもなんとかリリースできたので、同じくこれからReactなりSPAなりに挑戦しようと思っている方に、何か参考になれば嬉しいです。
スケジュール感
2018年3月初 〜 2018年5月末
フロント開発体制
1人
スキル感
-
React
: チュートリアルを数年前に1度やったことがある -
SPA
: API叩いて画面の一部を更新するくらいはやったことはあるものの、routerとか使ったことはない -
js
: それなりにかけるかなーといった感じ
作ったもの
全10ページ程度のSPA
(まだ前面に押し出しているサービスではないので紹介ができません・・・)
コーディング前にやったこと
1. 技術選定
もともとReactを使っていこうという雰囲気が社内にあり、Reactを使うことは決まっていました。
本当はvueがやりたかったんですが社内ではreact一択の雰囲気だったので
あとは個人的に使ってみたかったもの、話題になっていて使いやすそうなものを選んでいきました。
また、開発期間が短く、サーバーサイドのAPIが出来上がるのを待っていると明らかに間に合わなかったため、フロント主導のAPI設計のためにswagger
を使いました。
依存モジュールはこんな感じです。
※主要なモジュールについては後述します
"dependencies": {
"ajv": "^6.1.1",
"axios": "^0.17.1",
"idx": "^2.3.0",
"lodash": "^4.17.10",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-redux": "^5.0.6",
"react-router-dom": "^4.2.2",
"redux": "^3.7.2",
"redux-saga": "^0.16.0",
"styled-components": "^3.1.6"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.1",
"babel-preset-react-app": "^3.1.1",
"body-parser": "^1.18.2",
"browser-sync": "^2.23.6",
"cpx": "^1.5.0",
"cross-env": "^5.1.1",
"express": "^4.12.3",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^4.1.0",
"gulp-iconfont": "^9.1.0",
"gulp-iconfont-css": "^2.1.0",
"gulp-imagemin": "^4.1.0",
"gulp-load-plugins": "^1.5.0",
"gulp-plumber": "^1.2.0",
"gulp-rename": "^1.2.2",
"gulp-sourcemaps": "^2.6.4",
"gulp-stylus": "^2.7.0",
"husky": "^0.14.3",
"lint-staged": "^6.1.1",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"nodemon": "^1.14.12",
"npm-run-all": "^4.1.2",
"opener": "^1.4.3",
"prettier": "^1.10.2",
"replace": "^0.3.0",
"rimraf": "^2.6.2",
"serve-favicon": "^2.5.0",
"stylus": "^0.54.5",
"swagger-express-mw": "^0.1.0",
"touch": "^3.1.0",
"wait-on": "^2.1.0",
"watch": "^1.0.2",
"webpack": "^3.11.0",
"webpack-dev-server": "^2.11.1"
},
2. 開発環境構築
- js:
webpack
- static files:
gulp
- タスク実行:
npm scripts
js周りは全てwebpack
に、cssプリプロセッサや画像圧縮、その他静的ファイルの操作はgulp
に、各ビルドの実行はnpm scripts
から行います。
link-staged
、prettier
を使いコードの整形は自動化しています。
npm scripts
周りはこんな感じです。
"scripts": {
"start": "node app.js",
"prod": "cross-env NODE_ENV=production npm-run-all ready --parallel \"prod:* -- {@}\" --",
"prod:imagemin": "gulp imagemin",
"prod:replace": "gulp replace",
"dev": "cross-env NODE_ENV=development npm-run-all ready --parallel dev:*",
"dev:server": "nodemon app.js",
"dev:client": "wait-on http://localhost:10010/docs/ && run-p dev:client:*",
"dev:client:docs": "opener http://localhost:10010/docs/#/",
"dev:client:components": "npm run components && opener http://localhost:10010/static/components.html",
"dev:client:static": "npm run liveserver:static",
"dev:watch": "npm run watch",
"api": "run-p api:*",
"api:server": "nodemon app.js",
"api:client": "wait-on http://localhost:10010/docs/ && npm run liveserver:api",
"components": "cross-env NODE_ENV=development webpack --config webpack.components.js",
"components:dev": "cross-env NODE_ENV=development npm-run-all ready --parallel components:dev:*",
"components:dev:server": "webpack-dev-server --config webpack.components.js",
"build": "run-p build:*",
"build:js": "webpack",
"build:css": "gulp css",
"build:assets": "cpx 'src/assets/**/*.*' public/static",
"prebuild": "run-p _build:*",
"_build:iconfont": "gulp iconfont",
"watch": "run-p watch:*",
"watch:js": "watch 'npm run build:js' src/js",
"watch:css": "watch 'npm run build:css' src/css",
"watch:assets": "watch 'npm run build:assets' src/assets",
"clean": "rimraf public/static && mkdirp public/static && nodetouch public/static/.gitkeep",
"ready": "run-s clean build",
"liveserver:static": "browser-sync start -p http://localhost:10010/ -f public/static --startPath static/",
"liveserver:api": "browser-sync start -p http://localhost:10010/ -f api/swagger/swagger.yaml --reload-delay 1000 --startPath docs/#/",
"precommit": "lint-staged",
"test": "echo 'write test'"
},
"lint-staged": {
"*.js": [
"prettier --write --single-quote 'src/js/**/*.js'",
"git add"
]
}
npm scripts
周りでお世話になっている記事:
npm-scripts で使える便利モジュールたち
3. モック作成
SPAを感覚を掴みたかったので、実際に作っていく画面から2、3画面を選択してモックを作りました。
この時に各モジュールのチュートリアルやドキュメントを読み込んでいます。
4. ドキュメント化
モックを作った時点である程度のドキュメント化はできそうだったので、社内で使っているコンフルエンスにドキュメントを残していきました。
ディレクトリ構造、処理の流れをフローチャートにしてみたりしました。
ディレクトリ構造
/root
|- /api
|- /{ダミーのレスポンスデータの設定ファイル}.js
|- /config
|- /{swaggerの設定ファイル}.yaml
|- /public
|- /{expressからアクセス可能な静的ファイル}.*
|- /src
|- /assets
|- /{静的ファイル}.*
|- /css
|- /{resetなどの全画面共通CSS}.styl
|- /js
|- /actions
|- /index.js
|- /{index.jsから読み込まれるactionファイル}.js
|- /components
|- /atoms
|- /{atom}.js
|- /molecules
|- /{molecule}.js
|- /constants
|- /{画面共通定数}.js
|- /containers
|- /{pages}
|- /index.js
|- /mapping.js
|- /styled.js
|- /helpers
|- /{ヘルパー関数}.js
|- /reducers
|- /index.js
|- /{index.jsから読み込まれるreducerファイル}.js
|- /sagas
|- /index.js
|- /{index.jsから読み込まれるsagaファイル}.js
|- その他設定ファイル(.babelrcとか)
基本は各モジュールのチュートリアルに沿ったディレクトリ構造です。
以下は独自に変更している点です。
-
atomic designを採用したため、
conponents
配下にatoms
とmolecules
を切った -
container
が容易に膨れ上がることが見えていたため、cssを記述するstyled.js
とpropsへのマッピングをするmapping.js
を切り出した
mapping.js
は
import { connect } from 'react-redux';
const mapStateToProps = state => ({
someProperty: value,
someProperty: value
});
const mapDispatchToProps = dispatch => ({
someProperty: value,
someProperty: value
});
const mergeProps = (stateProps, dispatchProps, ownProps) => {
// ...merge props
return {
mergedProperty: value,
mergedProperty: value,
};
};
export default function(Component) {
Component = connect(mapStateToProps, mapDispatchToProps)(Component);
return Component;
}
のようになっており、index.js
で
// ...create ReactComponent
export default mapping(ReactComponent);
のように使っています。
フローチャート
フロント周りの処理フロー(以下画像)であったり、システム全体のフローなんかを作りました。
フローチャートの作成にはdraw.ioを使いました。
google driveに保存できて便利です。
5. コーディング
ここまで作って、あとはひたすらコーディングをしていきました。
主要モジュール
今回使った主要なモジュールと、その時参考にした資料や気付きなんかを書いていきます。
React
Reactのチュートリアルです。
Redux
参考:Basics
Reduxのチュートリアル的なものです。
(私はいきなりExample: Todo Listを見てしまったのですが、Basicsからやれば順に細かく見て行けることに後から気付きました。。。)
React Router DOM
参考:React Router: Declarative Routing for React.js
React Routerのドキュメントです。
画面遷移時のスクロール位置リセットは初めに組み込んでおくのが良いと思います。
Scroll Restoration
Redux Saga
参考:redux-sagaで非同期処理と戦う from Qiita
Redux Sagaの中の人?が書いてくださったQiitaの記事です。
非同期処理を書く時に大変お世話になります。
非同期処理のactionをsagaで拾うようにして、非同期処理完了後にreducerに渡すようにします。
例:REQUEST_HOGE
がdispatchされた場合
import { select, all, call, put, fork, takeEvery } from 'redux-saga/effects';
import * as actions from '../actions';
import { get } from 'axios';
export default function*() {
yield takeEvery(actions.REQUEST_HOGE, fetchHoge);
}
function* fetchHoge() {
let response = null;
try {
response = yield get('/api/hoge');
} catch (err) {
// error handring
return;
}
yield put(
type: actions.UPDATE_HOGE,
response
);
}
Styled Components
参考:Basics
Styled Componentsのドキュメントです。
Basicsだけでもやりたいことはほとんど出来るのではないかと思います。
他のモジュールと区別がつくようにしたくて、Styled ComponetsのモジュールにはS_
のprefixを付けるようにしました。
Over 200 classes were generated for component Component.
Styled Componentsで生成されるクラスが200を超えるとwarningが出るようです。
ユーザー操作によるスタイルの変更などはstyle
属性を使ったほうが良さそうです。
Change warning message when creating too many classes
Swagger Express MW
参考:
Swagger Express MWで使われているswaggerがv2.0のようで、v2.0の記法にリンクしています。
Howto
こうしたよ、というお話です。
ローディング
bundle.jsがとてつもなく大きくなるため、ローディングはjsの外に出すようにしました。
- index.html
<body>
<div id="loading">
<!-- loading表示要素 -->
</div>
<div id="app"></div>
<script src="/static/bundle.js"></script>
</body>
js処理が終わったタイミング(LifeCycleのフックなど)でElement.remove()
をするようにしています。
thisの束縛
thisの束縛はconstructor
内で行うのが良いようです。
constructor(props) {
super(props);
this.handleFuga = this.props.handleFuga.bind(this);
}
参考:React.jsのrenderの戻り値の中で.bindで新しい関数を定義してはいけないわけ
条件分岐による描画
好き嫌いが分かれそうですが、以下のような書き方もできるようです。
render() {
return (
<div>
{variable && (
<p>piyo</p>
)}
</div>
);
}
苦戦したこと
フロントとサーバーの役割分担
データの整形など、フロントでもサーバーでもできるようなことは、なるべくサーバーに任せてしまうのが良いです。
フロントは受け取ったデータを描画するだけ。それ以外にもやることは多々ありますので、なんでも担ってしまうとすぐに破綻します。
これは事前にしっかり握っておくと良いと思います。
深すぎるstoreオブジェクト
ユーザーのデータを次々と取得していくとします。
大きな括りで見ればユーザーデータという1つのデータなので、1つのオブジェクトを拡張していくようにしました。
{
user: {
name: value,
age: value,
history: {
school: [{
...someProperty
}],
business: [{
...someProperty
}],
},
}
}
こうなるとreducerで更新するのがとても大変になります。
あまり深くなりすぎないように、できれば事前にstoreの構成は考えておくのが良いと思います。
抽象的なプロパティ名
data
、list
、item
などの抽象的な単語をプロパティ名に使用して苦戦しました。
上の " 深すぎるstoreオブジェクト " もあり、ループで回すと悲惨なことになります。
list.forEach(item => {
if (!item.list) return;
item.list.forEach(/* 何を回しているか分からなくなる */);
});
面倒だったり多少気持ち悪かったとしても、なるべく限定的な単語を使用するのが良いと思います。
// danger
data
list
item
// safety
foodData
drinkList
dessertItem
URL直叩き対応
SSRをしなかったため、URLを直叩きされた場合に「データがない!」ということが多々起きました。
トップから落ちてくるとデータが揃う、という構造になっていたため、コンポーネントのWillMountやsagasの中でデータの有無の判定し、なければ先に別のAPIを叩く、という対策をとりました。
" これからやりたいこと " でもあるHOCを使えば解決できるかもしれません。
プロパティ有無の判定
プロパティ有無の判定が面倒で、全てtry-catchで囲み、エラーをキャッチしたらローディングコンポーネントを表示する、という対応をしていました。
当然参照エラー以外のエラーも発生するわけで、デバッグが非常にし辛くなりました。
idx
かoptional-chaining
を使えればプロパティ有無の判定は楽になりますので、初めから導入しておくのが良いかと思います。
参照:
これからやりたいこと
全てが初めてだったために導入を諦めたもの、新たに知った(理解)したものがあるため、これから導入していこうと思います。
Flow導入
静的型チェッカーです。
参照:Flow
HOC導入
コンポーネントのラッパーで、コンポーネントを拡張します。
storeの初期化やローディングの出しわけを、HOCでモジュール化できればいいなと思っています。
参照:ReactのHigher Order Components詳解 : 実装の2つのパターンと、親Componentとの比較
storybook導入
コンポーネントの管理・動作確認
参照:storybook
おわりに
ほぼ全てが初めての取り組みでしたが、なんとかリリースすることができました。
もちろん作りの荒いところ、改善できるところは多々あります。。。
が、それでもリリースまでもっていけたのは純粋にReactやその周りのモジュールが超絶便利に作りこまれていること、知名度がありドキュメントが豊富なことだと思います。
JSXって・・・とか、俺は絶対vueだ!とか思ってましたが、reactとても良いです。便利です。
SPA開発に興味はあるけどまだ本格的に取り組んだことがない方、ぜひどこかに飛び込んでみてください。
なんとかなりますし、学びもすごく多いです。