26
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

初めてのReactで初めてのSPAを作りました

Last updated at Posted at 2018-05-30

全てが初めてでもなんとかリリースできたので、同じくこれから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-stagedprettierを使いコードの整形は自動化しています。

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配下にatomsmoleculesを切った
  • 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);

のように使っています。

フローチャート

フロント周りの処理フロー(以下画像)であったり、システム全体のフローなんかを作りました。

chart.png

フローチャートの作成にはdraw.ioを使いました。
google driveに保存できて便利です。

5. コーディング

ここまで作って、あとはひたすらコーディングをしていきました。

主要モジュール

今回使った主要なモジュールと、その時参考にした資料や気付きなんかを書いていきます。

React

参考:Tutorial: Intro To 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の構成は考えておくのが良いと思います。

抽象的なプロパティ名

datalistitemなどの抽象的な単語をプロパティ名に使用して苦戦しました。
上の " 深すぎる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で囲み、エラーをキャッチしたらローディングコンポーネントを表示する、という対応をしていました。
当然参照エラー以外のエラーも発生するわけで、デバッグが非常にし辛くなりました。

idxoptional-chainingを使えればプロパティ有無の判定は楽になりますので、初めから導入しておくのが良いかと思います。

参照:

これからやりたいこと

全てが初めてだったために導入を諦めたもの、新たに知った(理解)したものがあるため、これから導入していこうと思います。

Flow導入

静的型チェッカーです。

参照:Flow

HOC導入

コンポーネントのラッパーで、コンポーネントを拡張します。
storeの初期化やローディングの出しわけを、HOCでモジュール化できればいいなと思っています。

参照:ReactのHigher Order Components詳解 : 実装の2つのパターンと、親Componentとの比較

storybook導入

コンポーネントの管理・動作確認

参照:storybook

おわりに

ほぼ全てが初めての取り組みでしたが、なんとかリリースすることができました。
もちろん作りの荒いところ、改善できるところは多々あります。。。

が、それでもリリースまでもっていけたのは純粋にReactやその周りのモジュールが超絶便利に作りこまれていること、知名度がありドキュメントが豊富なことだと思います。

JSXって・・・とか、俺は絶対vueだ!とか思ってましたが、reactとても良いです。便利です。

SPA開発に興味はあるけどまだ本格的に取り組んだことがない方、ぜひどこかに飛び込んでみてください。
なんとかなりますし、学びもすごく多いです。

26
34
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?