React Redux を用いた SPA 新規サービスを運用して得た知見と実装例

2c3d3c163a86d378aab4c0144d2f538c?s=47 numanomanu
November 15, 2017

React Redux を用いた SPA 新規サービスを運用して得た知見と実装例

2c3d3c163a86d378aab4c0144d2f538c?s=128

numanomanu

November 15, 2017
Tweet

Transcript

  1. Tsuyoshi Numano from:Lancers 2017/11/14 React Redux を用いた SPA 新規サービスを運用して 得た知見と実装例 React

    Redux を用いた SPA 新規サービスを運用して得た知見と実装例
  2. https://docs.google.com/presentation/d/1TIEg5rYJ9vlWTSO6etwupBBO0egapA1lIwVh3Fw4k84 アニメーションが動く google スライドの方が見やすい方は以下のリンクから

  3. 自己紹介

  4. 沼野剛志 1990年生まれ 出身高専  :神戸高専専攻科(~2012):ロボット制御 出身大学院 :JAIST(~2014):VRの研究、出会いの研究で修論 株式会社リッチメディア(~2015):メディアの運営・開発 現在はランサーズ株式会社でフロントエンドエンジニアとして 働いています。主に React.js

    を利用。 最近ツイッター始めました(遅) 自己紹介 @numanomanu
  5. 対象者 - ある程度 react や redux を触ったことがある方。 - SPA の開発に興味がある方

    - 以前の勉強会に参加された方 目的 - 具体的な実装例をもとに知見を共有し、何かの役に立ててほしい 話す内容 - 利用しているライブラリや開発環境、開発フロー - コードベースでの実装例の紹介 - その他 SPA サービスを運用する上での構成や知見 今日お話しする内容
  6. 現在運営しているサービスについて

  7. pook(プック) https://pook.life C2Cスキルシェア *アプリっぽいけど全部WEBです

  8. オフィスの会議室で座ったまま 目や肩をほぐしてもらっている様 子。 とても気持ちいいので凝ってるエ ンジニアにオススメ。

  9. React Redux のおさらい

  10. React - Facebook が開発した UI ライブラリ - 旧来の DOM 操作による状態管理を

    props や state で抽象化 - パーツをコンポーネントごとに管理するのが得意
  11. SPAなどの複雑化する React の ステート(状態)管理を、ルール (哲学)に従って書かせることで、 フロントエンドの動きを追いやす くするためのライブラリ (*個人的解釈です) Redux animation

    from http://slides.com/jenyaterpil/redux-from-twitter-hype-to-production#/27(動きます)
  12. フロント周りで利用している技術

  13. pook のフロント周りで利用している技術 ランサーズ流 React.js/redux アプリ開発入門@mori-dev エンジニアブログでコアの部分のリポジトリ公開してます!

  14. 静的型チェックには flow Javascript は動的型付け言語なので、実行時に値が渡ってからでないと、エラーになる かどうかがわからない。 - flow を使うと、ビルド時に type チェックを行い、間違った型が渡されるコードだとエ

    ラーを出してくれる - 独自に User などの型を定義できるのでコードがリーダブルになる - //@flow を省略すればそのファイルは型チェックされない
  15. 構文チェックには ESLint 構文バグの検証や、インデントなどの書き方を統一ができる。 - コードレビューで、 ; が抜けている、インデントが揃ってないなどを指摘しなくて良く なる。 - atom

    などに linter ツールを入れると、リアルタイムに検証してくれる CSS 用には stylelint というツールがある
  16. UIコンポーネントにはmaterial-ui Google の提唱する material-design を React.js ですぐ使えるように用意してくれてい るライブラリ。 - レゴブロックのように、コンポーネントを組み合わせるだけで利用可能

    - アプリのデモがすぐに作れるが、独自のデザインには向いていない(後述)
  17. React で css を書くなら CSS-Modules css はグローバルに適応されるので、BEMなどの命名規則でスコープを縛る必要が あった。BEMなどに頼らず、CSS は利用するコンポーネント内で独自の名前をプログラ ムが自動生成すればいいよねという発想。

    - 命名規則で縛らずとも、限定的な名前がかける - 採用するかどうかは How to style React Components を読むと良い
  18. コンポーネントのテストには Enzyme React のコンポーネントをテストするためのツール。 - airbnb が開発。 - react のコンポーネントのレンダリングをアサーションしてくれる

    - 受け取ったprops によって、何がレンダリングされるべきかなどのテストが書ける コンポーネントのテスト以外にも、Redux の action や reducer のテストを書いている。
  19. 実際のプロダクトの構成

  20. /src ├── actions ├── components ├── containers ├── routes.js ├──

    index.js ├── index.html ├── middleware ├── reducers ├── sagas ├── store ├── types /test flux standard なフォルダ構成 アクション UI群 state を受け取る層 ルーティング アプリの起点 ホスティングされるファイル ミドルウェアの処理 リデューサー redux-saga による非同期処理 Store 生成処理 FlowType による型定義群 ↑  大体の色の内訳を合わせた
  21. API を呼び出す一連の処理を例にコードを紹介

  22. // @flow import * as ActionTypes from './action_types'; import type

    { Meta, ErrorMessage } from './../types/models'; import type { GetPayload, GetOkPayload } from './../types/payload/service_list'; import type { Action } from '../types/actions'; export function get(payload: GetPayload = {}, meta: any = {}): Action{ return { type: ActionTypes.GET_SERVICE_LIST_START, payload, meta, }; } export function getOk(payload: GetOkPayload, meta: Meta = {}): Action { return { type: ActionTypes.GET_SERVICE_LIST_OK, payload, meta, }; } export function getNg(payload: ErrorMessage, meta: Meta = {}): Action { return { type: ActionTypes.GET_SERVICE_LIST_NG, payload, meta, }; } action
  23. action の引数は payload, meta で統一  export function get(payload: Object =

    {}, meta: Object = {}): Action { return { type: ActionTypes.GET_SERVICE_LIST_START, payload, meta, }; } どんなアクションも引数のインターフェイスを payload, meta と命名したObjectで統一する。flux-standard-action payload には例えば { usreId: 1 } など Object でラップして渡す metaには副作用的に利用する情報を渡す(statusCode や error 情報など) middleware などで共通の処理を書きやすくなるメリットがある(後述) Actionの型イメージ type Action = { type: string, payload: Object, meta?: Object, }
  24. // @flow import { handleActions } from 'redux-actions'; import *

    as ActionTypes from '../actions/action_types'; import type { Action } from '../types/actions'; import type { Service } from '../types/models'; type StateType = { data: Array<Service>; loadingFlag: boolean; } export const initialState: StateType = { data: [], loadingFlag: false, }; const serviceList = handleActions({ [ActionTypes.GET_SERVICE_LIST_START]: (state: StateType) => { return { ...state, loadingFlag: true }; }, [ActionTypes.GET_SERVICE_LIST_OK]: (state: StateType, action: Action) => { return { ...state, data: action.payload, loadingFlag: false, }; }, [ActionTypes.GET_SERVICE_LIST_NG]: (state: StateType, action: Action) => { return { ...state, loadingFlag: false }; }, }, initialState); export default serviceList; reducer
  25. reducer redux-actionsを使って、case 文を省略 ...state, など スプレッドシンタックス で Object を上書き reducer

    では action.type ごとに loadingFlag を持たせている 複雑な処理は middleware に寄せて、reducer をシンプルにしている。 import { handleActions } from 'redux-actions'; const serviceList = handleActions({ [ActionTypes.GET_SERVICE_LIST_START]: (state: StateType) => { return { ...state, loadingFlag: true }; }, [ActionTypes.GET_SERVICE_LIST_OK]: (state: StateType, action: Action) => { return { ...state, data: action.payload, loadingFlag: false, }; }, 〜後略
  26. container // @flow import React, { Component } from 'react';

    import { bindActionCreators, compose } from 'redux'; import { connect } from 'react-redux'; import CircularProgress from 'material-ui/CircularProgress'; import * as actions from '../../actions/service_list'; function mapStateToProps(state: Object): Object { return { serviceList: state.currentUsersServiceList }; } function mapDispatchToProps(dispatch: Function): Object { return { actions: bindActionCreators(actions, dispatch) }; } class ServiceListContainer extends Component { componentWillUnmount() { this.props.actions.get(); // マウント時に API をコール } props: { actions: { get: Function; }; serviceList: { data: Array<Object>; loadingFlag: boolean; }; } render() { const { data, loadingFlag } = this.props.serviceList; if (loadingFlag) { return <CircularProgress/> } return ( <div> {data && data.map((service, i) => <div key={i}>{service.title}</div>})} </div> ); } } export default connect(mapStateToProps, mapDispatchToProps)(ServiceListContainer);
  27. // @flow import React, { Component } from 'react'; import

    CircularProgress from 'material-ui/CircularProgress'; // 省略... render() { const { loadingFlag } = this.props.serviceList; if (loadingFlag) { return <CircularProgress/>; } return ( <div>ロードが終わった後に表示されるコンテンツ </div> ) } // 省略... [ActionTypes.GET_SERVICE_LIST_START]: (state: StateType) => { return { ...state, loadingFlag: true }; }, [ActionTypes.GET_SERVICE_LIST_OK]: (state: StateType, action: Action) => { return { ...state, data: action.payload, loadingFlag: false, }; },
  28. middleware redux-sagaで非同期処理と戦う https://qiita.com/kuy/items/716affc808ebb3e1e8ac redux-saga で API コールを監視 非同期的な処理だが、ネストに ならないので読みやすい!? //

    @flow import 'babel-polyfill' // 古いブラウザで動かない場合があるため import { call, fork, put, take } from 'redux-saga/effects'; import { getOk, getNg } from '../actions/service_list'; import * as ActionTypes from '../actions/action_types'; import Api from '../services/api'; import type { Action } from '../types/actions'; function* getServiceList(action: Action): Generator<any, any, any> { try { const response: any = yield call(Api.getServiceList, action.payload); const payload = response.body; const meta = { statusCode: response.statusCode }; yield put(getOk(payload, meta)); } catch (e) { const payload = e.response.error.message; const meta = { statusCode: e.response.statusCode }; yield put(getNg(payload, meta)); } } export function* watchGetServiceList(): Generator<any, any, any> { while (true) { const action = yield take(ActionTypes.GET_SERVICE_LIST_START); yield fork(getServiceList, action); } }
  29. middleware redux-saga で API コールを監視 import { fork } from

    'redux-saga/effects'; import { watchGetServiceList } from './service_list_saga'; export default function* rootSaga(): Generator<any, any, any> { yield [ fork(watchGetServiceList), ]; } 上記のコードを store で呼び出す
  30. store // @flow import { createStore, applyMiddleware, compose } from

    'redux'; import createSagaMiddleware from 'redux-saga'; import { routerMiddleware } from 'react-router-redux'; import createHistory from 'history/createHashHistory' import rootSaga from '../sagas/index'; import rootReducer from '../reducers'; export const history = createHistory(); const routing = routerMiddleware(history); const sagaMiddleware = createSagaMiddleware(); const enhancer = compose( applyMiddleware( routing, sagaMiddleware, ), ); function configureStore(initialState: any) { const store = createStore(rootReducer, initialState, enhancer); sagaMiddleware.run(rootSaga); return store; } export default configureStore;
  31. import React, { Component } from 'react'; import { ConnectedRouter

    } from 'react-router-redux'; import { Route, Redirect, Switch } from 'react-router-dom'; import MainContainer from './containers/main_container'; import ServiceListContainer from './containers/service_list_container'; import { history } from '../store/configure_store'; class Routes extends Component { render() { return ( <ConnectedRouter history={history}> <Switch> <MainContainer> <Route path="/services" component={SeriviceListContainer} /> </MainContainer> </Switch> </ConnectedRouter> ); } } export default Routes; routing
  32. その他 WEB 開発でよくある実装の例

  33. ボタンの種類を管理したい(色、サイズ、形) 通常サイズ 画面の半分サイズや色違い フルサイズ、角四角のボタン

  34. import React, { Component } from 'react'; import RaisedButton from

    'material-ui/RaisedButton'; function createPrimaryButton(WrappedComponent) { return class designedButtonComponent extends Component { render() { return (<WrappedComponent {...this.props} primary={true} />); } } } export const PrimaryButton = createPrimaryButton(RaisedButton); HOC を利用してコンポーネントの種類を管理 HOC(Higher-order-components) とは - コンポーネントに関数を適応し、機能が合成されたコンポーネントを返す - propsを新しく加えたり、ライフサイクルメソッドを追加することも可能 Higher-order Components の利用方法 https://qiita.com/numanomanu/items/2b66d8b2887d44f857dc
  35. import { PrimaryButtonFullWidth, AccentButtonHalfWidth, NSecondaryButtonRounded } from './buttons'; export const

    renderSomePageWithButton = buttonAction => <div> <NPrimaryButtonFullWidth label={'プライマリボタン幅MAX'} /> <NSecondaryButtonRounded label={'角丸セカンダリボタン '} /> <NAccentButtonHalfWidth label={'アクセントボタン幅半分 '} /> </div> import React, { Component }from 'react'; import RaisedButton from 'material-ui/RaisedButton'; import { buttonColor, buttonSize, buttonShape } from './button_styles'; function createButton(WrappedComponent, color, size, shape) { return class designedButtonComponent extends Component { render() { return (<WrappedComponent {...this.props} {...buttonColor[color]} {...buttonSize[size]} {...buttonShape[shape]} />); } } } export const PrimaryButtonFullWidth = createButton(RaisedButton, 'primary', 'fullWidth', 'square'); export const SecondaryButtonRounded = createButton(RaisedButton, 'secondary', 'original', 'round'); export const AccentButtonHalfWidth = createButton(RaisedButton, 'accent', 'halfWidth', 'original');
  36. HOCパターンのメリット - HOC を用いればコンポーネントに新しく props を追加できる - material-ui など、既に作成されたコンポーネントなどにも柔軟に適応できる -

    PrimaryButtonFullWidthなど、限定的な名前を持たせることで、呼び出し側のコー ドがリーダブルになる
  37. ちょっとだけスタイルを当てたい場合 material-ui の Avatar で縦長画像が歪む import React from 'react'; import

    Avatar from 'material-ui/Avatar'; ... <Avator src="..."> ... photo from https://unsplash.com/
  38. import NAvatar from './componsnts/n_avatar.js'; … <NAvatar src="..." /> … //

    @flow import React from 'react'; import Avatar from 'material-ui/Avatar'; const overrideStyle = { style: { // 画像を歪ませないためのスタイル objectFit: 'cover', }, }; const NAvatar = (props: Object) => { return ( <Avatar {...props} style={{ ...overrideStyle.style, ...props.style, }} /> ); }; export default NAvatar; - 特定の props をスプレッドシンタックスでオーバーラ イドする - プレフィックスつけて管理
  39. - 素早くそれなりの UI が実装できる - アニメーションをお任せできる - 細かいスタイル当てる方法が微妙 - material-ui

    に記されているプロパティを利用 - style プロパティを上書き - theme ファイルを自作してプロパティを全体的に上書き - css を当てる - 上記を行っても上書きできないプロパティもあり、ライブラリのコード読みに行くこと がよくある - マテリアル UI 推しで使わない限り、利用は控えた方が良いかもしれない - ver 1.0 から色々変わるみたいです material UI を使ってみて
  40. ダイアログで yes, no アクションしたい 「キャンセル」で何もしない。 「はい」でログインページへ遷移。 ボタンにアクションを毎回埋め込むのもいいけど、 できれば、アクションをフックにダイアログを 出すパターンとして共通化したい

  41. ダイアログのComponent material-ui の Dialog を拡張 トップレベルのコンテナ内に設置 (or HOC にする) props

    でアクションを受け取る OK を押した時、アクションを発行 import Dialog from 'material-ui/Dialog'; import RaisedButton from 'material-ui/RaisedButton'; class GlobalDialog extends Component { props: { title: string; openFlag: boolean; okClickHandler: Function; }; renderDialogButtons(): React.Element<*> { return ( <div> <RaisedButton            label="キャンセル" onClick={/*ダイアログを閉じる処理*/}/> <RaisedButton            label="はい" onClick={this.props.okClickHandler} /> </div> ); } render() { return ( <Dialog open={this.props.openFlag} title={this.props.title} actions={this.renderDialogButtons()} /> ); } } export default GlobalDialog;
  42. Action の呼び出し const payload = {} const meta = {

      dialogInfo: {     title: 'ログインが必要な操作です ',   }, } // dialog が出現する action になる this.props.actions.update(payload, meta); meta に Object を渡す middleware で 「dialogInfo」 をキャッチさせる
  43. middleware export default (store: any) => (next: any) => (action:

    any) => { if (action.meta && action.meta.dialogInfo) { const payload = { ...action.meta.dialogInfo, okClickHandler: () => { // meta 情報を削除. 次のアクションで dialog を出さない delete action.meta.dialogInfo; next(action); }, }; return next(dialogOpen(payload)); } // dialogInfo があれば上で return するからここに来ない next(action); }; meta.dialogInfo があれば dialogOpen でダイアログを開くアクションを呼ぶ next(action) は呼ばない okClickHandler に 本来の action を渡す アクションを middleware で せき止めるイメージ React × Redux で action 発行時に確認ダイアログを挟む middleware の実装例 https://engineer.blog.lancers.jp/2017/07/react_redux_update_state/
  44. middleware を活用する際のポイント - どんなアクション呼び出しにも、共通した処理ができる - action の引数を payload, meta に統一するとフックの処理が書きやすい

    - ロジックを middleware に集めることでテストしやすい 注意)middleware は定義した順に next(action) で次の処理が呼び出される。 dialog アクションの次に api コールをしたい場合などは sagaMiddleware の前に置く。 と言うような実装が必要 const enhancer = compose( applyMiddleware( routing, // 1番目に実行 dialogChecker, // 2番目に実行 sagaMiddleware, // 3番目に実行 ), );
  45. ログインしている人だけ見れるページを作りたい import loggedInRequired from '../logged_in_required.js' // 省略... class ProjectContainer extends

    Component { // 省略... } export default loggedInRequired( connect(mapStateToProps, mapDispatchToProps) (ProjectContainer) ); HOCを利用して、複数ページで利用したい。 呼び出し例)
  46. HOC export default function loggedInRequired(WrappedComponent) { class loggedInRequiredComponent extends WrappedComponent

    { componentWillMount() { // react-redux のステートには this.store.getState() でアクセスできます if (!this.store.getState().session.userId) { // ログインしていないユーザが見た時のアクションを書く } } render() { if (!this.store.getState().session.userId) { // ログインしていないユーザが閲覧したらレンダリングしない return null; } return super.render(); } } return loggedInRequiredComponent; } ラップしたコンポーネント自身を extends している。(Inheritance Inversion) HOCのライフサイクルメソッドを、ラップしたコンポーネントに適応 HOCから、ラップしたコンポーネントの state や props に thisでアクセス コンポーネントのレンダリングをハイジャックできる
  47. ローカルストレージに state を保存したい - ユーザー情報など、扱っているステートをそのまま localStorage に入れたい=> redux-persist が便利 -

    whiteList 形式で特定の state をローカルストレージに保存できる - アプリ起動時に autoRehydrate で state を復元してくれる
  48. redux-persist の設定(configureStore) import { persistStore, autoRehydrate } from 'redux-persist'; const

    enhancer = compose( autoRehydrate(), applyMiddleware( routing, sagaMiddleware, ), ); export default function configureStore(initialState) { const store = createStore(rootReducer, initialState, enhancer); persistStore(store, { whiteList: ["currentUser", "permanence"] }, () => { // autoRehydrate が終わった後に呼ばれる }); return store; }
  49. redux-persist の補足 - redux-persist-transform-encrypt を使えばストレージを暗号化できる - redux-action-buffer と組み合わせて使うやり方 - 現時点の最新

    ver. 5 では PersistGate コンポーネントを使うことでレンダリングを遅ら せてくれる  
  50. テスト

  51. None
  52. action import assert from 'power-assert'; import * as actions from

    '../../src/actions/service'; import * as ActionTypes from '../../src/actions/action_types'; describe('service Action', () => { it('get', () => { const payload = {}; const meta = {}; const expectedAction = { type: ActionTypes.GET_SERVICE_START, payload, meta, }; assert.deepStrictEqual(actions.get(payload), expectedAction); }); }
  53. reducer import assert from 'power-assert'; import * as ActionTypes from

    '../../src/actions/action_types'; import reducer, { initialState } from '../../src/reducers/service'; describe('Service reducer', () => { it('GET_SERVICE_START で loadingFlag が true になること', () => { const executed = reducer(initialState, { type: ActionTypes.GET_SERVICE_START }); const expected = { ...initialState, loadingFlag: true, }; assert.deepStrictEqual(executed, expected); }); }
  54. component import React from 'react'; import assert from 'power-assert'; import

    { shallow } from 'enzyme'; import Dialog from 'material-ui/Dialog'; import NDialog from '../../../../src/browser/components/common/n_dialog'; describe('NDialog component', () => { it('this.props.message がある場合は Dialog が描画されること', () => { const props = { closeNDialog: () => {}, isOpen: true, message: 'foobar', }; const component = shallow(<NDialog {...props} />); assert.ok(component.containsMatchingElement(Dialog)); }); }
  55. middleware import assert from 'power-assert'; import sinon from 'sinon'; import

    confirmationDialogChecker from '../../../src/middleware/common/dialog_checker'; import * as ActionTypes from '../../../src/actions/action_types'; import * as action from '../../../src/actions/confirmation_dialog'; const createFakeStore = () => {}; let spy; describe('dialog_checker' ミドルウェア', () => { beforeEach(() => { spy = sinon.spy(action, 'open'); }); afterEach(() => { action.open.restore(); }); it('action の meta に DialogInfo があれば確認ダイアログがでること', () => { const meta = { dialogInfo: { title: 'message', }, }; const action = { type: ActionTypes.GET_SERVICE_LIST_START, payload: {}, meta, }; const dispatch = confirmationDialogChecker(createFakeStore())(() => {}); dispatch(action); assert.ok(spy.calledOnce); assert.equal(spy.args[0][0].title, meta.dialogInfo.title); }); }
  56. テストのポイント - ビジネスロジックを middleware に集めて、middleware のテストを厚めにやる。 - 新規サービスのため UI がすぐ変わるので

    components のテストは全てカバーし ていない。 - middleware にロジックを持っていくと action, reducer のテストが簡単になる - 書いてて無駄に感じることもあるが、何か機能を消した時のレグレッションに気付け るので、書く意味はあると思う。
  57. モニタリング

  58. エラー通知 https://sentry.io

  59. 導入例 import Raven from 'raven-js'; Raven.config('https://セントリーのurl', { release: RELEASE, environment:

    process.env.NODE_ENV, // production, stating 環境以外のエラーを送信しない設定 shouldSendCallback: () => { return ['production', 'staging'].indexOf(process.env.NODE_ENV) !== -1; }, }).install();
  60. None
  61. Sentry でよく使っている機能 エラー送信時にユーザー情報も付与できるので お問い合わせ時に即原因究明できたりする Raven.setUserContext({ id: currentUser.id, displayName: currentUser.displayName, });

  62. ユーザー分析 in SPA

  63. import * as ActionTypes from '../../actions/action_types'; export default (store: any)

    => (next: any) => (action: any) => { next(action); // react-router-redux の発行するアクションを監視 if (action.type === ActionTypes.REACT_ROUTER_LOCATION_CHANGE) { const routing = store.getState().routing; const nextPath = routing.location.pathname; const query = routing.location.search; ga('set', 'page', nextPath + query); ga('send', 'pageview'); } }; PV計測などは、独自にイベントを発行する必要がある middleware で react-router-redux の発行するアクションタイプを監視して、 動的に pageview のイベントを発火させる React.js/redux アプリでの Google Analytics のイベントトラッキングの設定 https://engineer.blog.lancers.jp/2017/09/google-analytics-redux/
  64. ちなみに。。。 Q. SPA って google に index されるの? A. されます。

  65. None
  66. None
  67. // @flow import React, { Component } from 'react'; import

    Helmet from 'react-helmet'; // see https://github.com/nfl/react-helmet class DocumentMeta extends Component { static defaultProps = { image: 'https://pook.life/img/logo/ogp_image.jpg', // 画像に相対パスは使えない title: 'pook(プック)| スキルシェアリングサービス', keywords: 'シェアリングサービス、スキルシェア、CtoC', description: 'pook(プック)は、得意や趣味などのできることを売り買いするサー... } props: { image?: string; title?: string; keywords?: string; description?: string; } render() { return ( <Helmet> <title>{this.props.title} | pook(プック)</title> <meta name="keywords" content={this.props.keywords} /> <meta name="description" content={this.props.description} /> </Helmet> ); } } export default DocumentMeta; react-helmet meta 情報を動的に書き換え可能
  68. アプリっぽくする

  69. manifest.json を書くと Android で「ホームに追加」できる https://developers.google.com/web/fundamentals/web-app-ma nifest/?hl=ja { "short_name": "pook", "name":

    "pook(プック)自分らしい暮らしをもっと身近に。", "icons": [{ "src": "img/logo/icon-128x128.png", "type": "image/png", "sizes": "128x128" }, { その他サイズの画像 }], "background_color": "#FFFFFF", "display": "standalone", "theme_color": "#0086D1", "start_url": "/home?utm_source=homescreen" }
  70. アドレスバーが出ない ホームに icon 追加 Splash が付く "display": "standalone", "name": "pook(プック)自分らしい暮らし

    をもっと身近に。", "short_name": "pook", "icons": [{ "src": "img/logo/icon-128x128.png", "type": "image/png", "sizes": "128x128" },
  71. アプリっぽくした結果 ユーザー: pook って、アプリなのに通知こないんですか? 私: すみません、アプリじゃないんです。。。 ユーザーの割合的に ios が圧倒的なので、 一旦

    service worker による通知は見送り。 safari の service worker 対応が待たれる。
  72. 開発体制とフロー

  73. 現在の開発体制について - バックエンド側、フロントエンド側で完全に分離 - フロントは自社内、バックエンドは拠点でリモート開発 API サーバー フロントエンド バックエンド(ベトナム拠点) APIでやり取り

  74. 開発の全体的なフロー 1. デザイン(UI/UX) 2. API仕様書作成 3. 開発 4. QA、リリース

  75. APIドキュメントを共通仕様として作成&運用 - API Blueprint という API仕様書を mark down で書ける構文を利用 -

    Github で管理し、PRで新規作成・更新し、機能の詳細は issue で管理 - API はフロントエンドエンジニアが設計 フロントエンド GET: /users したら、ユーザー 一覧のJson返して欲しい。 バックエンド name name name API サーバー API仕様書 仕様書に基 づいたjson を返す
  76. API仕様書の作成と運用 ブログにまとめました。 API仕様書をMarkDownで書き、GitHubをつかって運用する方法

  77. APIができるまではモックサーバーを使う - API仕様書をそのままモックサーバーにできる仕組みが存在 (api-mock) - API が完成していなくても、フロントエンドの実装に着手できる モック サーバー localhost:3002

    GET: http://localhost:3002/users フロントエンド API 仕様書 [ { id: 1, name: hoge}, { id: 2, name: huga}, { id: 3, name: bar} ] api-mock で、仕様書を モックサーバー化 hoge huga bar
  78. フロントエンド開発の実装フロー

  79. 開発のフロー全体 コードを書く テストを書く 構文チェック、型チェック github から pull トランスパイル、ビルド 実機確認 hoge

    huga bar
  80. デプロイ 開発のフロー全体(レビューとCI、デプロイ) コードを書く テストを書く 構文チェック、型チェック github から pull トランスパイル、ビルド 実機確認

    CIでgithubにあげた ファイルをチェック コードレビュー オープンソースのフローベースの開発 - commit は anguralr のルールをベース - ドキュメントは厚めに書く hoge huga bar
  81. デプロイとインフラ

  82. S3 CloudFront build & deploy index.html bundle.js bundle.css gzip

  83. S3 CloudFront EC2 build & deploy index.html bundle.js bundle.css gzip

    API Server GET: /services/1 ...etc
  84. CloudFront から gzip で圧縮して配信 Cloud Front の管理画面で、Compress Objects Automatically を

    Yes にするだけ 44% Down 通常 build gzip 配信
  85. 課題に思っていること

  86. - デザイナーがjsxを書くのに敷居が高く、HTML直書きより協業しにくい - Reducer がどんどん肥えていく => normalize したい - コミュニティライブラリに依存しすぎると思わぬところでハマる

    - ビジネスロジックの置き場に悩む => middleware においた - FlowType入れたけど、型を使いこなせているか不安 - Google は js 動くが、 Facebook twitter は js 動かない。 OGP 画像でない - ユーザーがアプリと勘違いするので、アプリクオリティが求められる - トレンドを追いかけつつ、プロダクトの製作は結構忙しい React Redux で SPA を開発、運用してみた課題感
  87. まとめ

  88. React Redux で SPA を開発、運用して - Redux は結構薄いフレームワークなので随所に工夫が必要 - middleware

    を活用するとコードの見通しは良くなる - ユーザーが求める 「普通」 は確実に難易度が上がってきている - 継続的に改善していける健全なコードを書く環境が必要そう まとめ。と言うか感想です
  89. ランサーズエンジニアブログでリポジトリ公開しています - ランサーズ流 React.js/redux アプリ開発入門@mori-dev - レスポンシブ対応の LP を簡単に作れるツールを React

    Redux で作った。ソース コードあり@numanomanu 「ランサーズ エンジニアブログ」で是非検索してください
  90. https://www.wantedly.com/projects/161195

  91. Thank you !