Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

https://docs.google.com/presentation/d/1TIEg5rYJ9vlWTSO6etwupBBO0egapA1lIwVh3Fw4k84 アニメーションが動く google スライドの方が見やすい方は以下のリンクから

Slide 3

Slide 3 text

自己紹介

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

対象者 - ある程度 react や redux を触ったことがある方。 - SPA の開発に興味がある方 - 以前の勉強会に参加された方 目的 - 具体的な実装例をもとに知見を共有し、何かの役に立ててほしい 話す内容 - 利用しているライブラリや開発環境、開発フロー - コードベースでの実装例の紹介 - その他 SPA サービスを運用する上での構成や知見 今日お話しする内容

Slide 6

Slide 6 text

現在運営しているサービスについて

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

React Redux のおさらい

Slide 10

Slide 10 text

React - Facebook が開発した UI ライブラリ - 旧来の DOM 操作による状態管理を props や state で抽象化 - パーツをコンポーネントごとに管理するのが得意

Slide 11

Slide 11 text

SPAなどの複雑化する React の ステート(状態)管理を、ルール (哲学)に従って書かせることで、 フロントエンドの動きを追いやす くするためのライブラリ (*個人的解釈です) Redux animation from http://slides.com/jenyaterpil/redux-from-twitter-hype-to-production#/27(動きます)

Slide 12

Slide 12 text

フロント周りで利用している技術

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

静的型チェックには flow Javascript は動的型付け言語なので、実行時に値が渡ってからでないと、エラーになる かどうかがわからない。 - flow を使うと、ビルド時に type チェックを行い、間違った型が渡されるコードだとエ ラーを出してくれる - 独自に User などの型を定義できるのでコードがリーダブルになる - //@flow を省略すればそのファイルは型チェックされない

Slide 15

Slide 15 text

構文チェックには ESLint 構文バグの検証や、インデントなどの書き方を統一ができる。 - コードレビューで、 ; が抜けている、インデントが揃ってないなどを指摘しなくて良く なる。 - atom などに linter ツールを入れると、リアルタイムに検証してくれる CSS 用には stylelint というツールがある

Slide 16

Slide 16 text

UIコンポーネントにはmaterial-ui Google の提唱する material-design を React.js ですぐ使えるように用意してくれてい るライブラリ。 - レゴブロックのように、コンポーネントを組み合わせるだけで利用可能 - アプリのデモがすぐに作れるが、独自のデザインには向いていない(後述)

Slide 17

Slide 17 text

React で css を書くなら CSS-Modules css はグローバルに適応されるので、BEMなどの命名規則でスコープを縛る必要が あった。BEMなどに頼らず、CSS は利用するコンポーネント内で独自の名前をプログラ ムが自動生成すればいいよねという発想。 - 命名規則で縛らずとも、限定的な名前がかける - 採用するかどうかは How to style React Components を読むと良い

Slide 18

Slide 18 text

コンポーネントのテストには Enzyme React のコンポーネントをテストするためのツール。 - airbnb が開発。 - react のコンポーネントのレンダリングをアサーションしてくれる - 受け取ったprops によって、何がレンダリングされるべきかなどのテストが書ける コンポーネントのテスト以外にも、Redux の action や reducer のテストを書いている。

Slide 19

Slide 19 text

実際のプロダクトの構成

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

API を呼び出す一連の処理を例にコードを紹介

Slide 22

Slide 22 text

// @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

Slide 23

Slide 23 text

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, }

Slide 24

Slide 24 text

// @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; 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

Slide 25

Slide 25 text

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, }; }, 〜後略

Slide 26

Slide 26 text

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; loadingFlag: boolean; }; } render() { const { data, loadingFlag } = this.props.serviceList; if (loadingFlag) { return } return (
{data && data.map((service, i) =>
{service.title}
})}
); } } export default connect(mapStateToProps, mapDispatchToProps)(ServiceListContainer);

Slide 27

Slide 27 text

// @flow import React, { Component } from 'react'; import CircularProgress from 'material-ui/CircularProgress'; // 省略... render() { const { loadingFlag } = this.props.serviceList; if (loadingFlag) { return ; } return (
ロードが終わった後に表示されるコンテンツ
) } // 省略... [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, }; },

Slide 28

Slide 28 text

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 { 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 { while (true) { const action = yield take(ActionTypes.GET_SERVICE_LIST_START); yield fork(getServiceList, action); } }

Slide 29

Slide 29 text

middleware redux-saga で API コールを監視 import { fork } from 'redux-saga/effects'; import { watchGetServiceList } from './service_list_saga'; export default function* rootSaga(): Generator { yield [ fork(watchGetServiceList), ]; } 上記のコードを store で呼び出す

Slide 30

Slide 30 text

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;

Slide 31

Slide 31 text

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 ( ); } } export default Routes; routing

Slide 32

Slide 32 text

その他 WEB 開発でよくある実装の例

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

import { PrimaryButtonFullWidth, AccentButtonHalfWidth, NSecondaryButtonRounded } from './buttons'; export const renderSomePageWithButton = buttonAction =>
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 (); } } } export const PrimaryButtonFullWidth = createButton(RaisedButton, 'primary', 'fullWidth', 'square'); export const SecondaryButtonRounded = createButton(RaisedButton, 'secondary', 'original', 'round'); export const AccentButtonHalfWidth = createButton(RaisedButton, 'accent', 'halfWidth', 'original');

Slide 36

Slide 36 text

HOCパターンのメリット - HOC を用いればコンポーネントに新しく props を追加できる - material-ui など、既に作成されたコンポーネントなどにも柔軟に適応できる - PrimaryButtonFullWidthなど、限定的な名前を持たせることで、呼び出し側のコー ドがリーダブルになる

Slide 37

Slide 37 text

ちょっとだけスタイルを当てたい場合 material-ui の Avatar で縦長画像が歪む import React from 'react'; import Avatar from 'material-ui/Avatar'; ... ... photo from https://unsplash.com/

Slide 38

Slide 38 text

import NAvatar from './componsnts/n_avatar.js'; … … // @flow import React from 'react'; import Avatar from 'material-ui/Avatar'; const overrideStyle = { style: { // 画像を歪ませないためのスタイル objectFit: 'cover', }, }; const NAvatar = (props: Object) => { return ( ); }; export default NAvatar; - 特定の props をスプレッドシンタックスでオーバーラ イドする - プレフィックスつけて管理

Slide 39

Slide 39 text

- 素早くそれなりの UI が実装できる - アニメーションをお任せできる - 細かいスタイル当てる方法が微妙 - material-ui に記されているプロパティを利用 - style プロパティを上書き - theme ファイルを自作してプロパティを全体的に上書き - css を当てる - 上記を行っても上書きできないプロパティもあり、ライブラリのコード読みに行くこと がよくある - マテリアル UI 推しで使わない限り、利用は控えた方が良いかもしれない - ver 1.0 から色々変わるみたいです material UI を使ってみて

Slide 40

Slide 40 text

ダイアログで yes, no アクションしたい 「キャンセル」で何もしない。 「はい」でログインページへ遷移。 ボタンにアクションを毎回埋め込むのもいいけど、 できれば、アクションをフックにダイアログを 出すパターンとして共通化したい

Slide 41

Slide 41 text

ダイアログの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 (
); } render() { return ( ); } } export default GlobalDialog;

Slide 42

Slide 42 text

Action の呼び出し const payload = {} const meta = {   dialogInfo: {     title: 'ログインが必要な操作です ',   }, } // dialog が出現する action になる this.props.actions.update(payload, meta); meta に Object を渡す middleware で 「dialogInfo」 をキャッチさせる

Slide 43

Slide 43 text

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/

Slide 44

Slide 44 text

middleware を活用する際のポイント - どんなアクション呼び出しにも、共通した処理ができる - action の引数を payload, meta に統一するとフックの処理が書きやすい - ロジックを middleware に集めることでテストしやすい 注意)middleware は定義した順に next(action) で次の処理が呼び出される。 dialog アクションの次に api コールをしたい場合などは sagaMiddleware の前に置く。 と言うような実装が必要 const enhancer = compose( applyMiddleware( routing, // 1番目に実行 dialogChecker, // 2番目に実行 sagaMiddleware, // 3番目に実行 ), );

Slide 45

Slide 45 text

ログインしている人だけ見れるページを作りたい import loggedInRequired from '../logged_in_required.js' // 省略... class ProjectContainer extends Component { // 省略... } export default loggedInRequired( connect(mapStateToProps, mapDispatchToProps) (ProjectContainer) ); HOCを利用して、複数ページで利用したい。 呼び出し例)

Slide 46

Slide 46 text

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でアクセス コンポーネントのレンダリングをハイジャックできる

Slide 47

Slide 47 text

ローカルストレージに state を保存したい - ユーザー情報など、扱っているステートをそのまま localStorage に入れたい=> redux-persist が便利 - whiteList 形式で特定の state をローカルストレージに保存できる - アプリ起動時に autoRehydrate で state を復元してくれる

Slide 48

Slide 48 text

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; }

Slide 49

Slide 49 text

redux-persist の補足 - redux-persist-transform-encrypt を使えばストレージを暗号化できる - redux-action-buffer と組み合わせて使うやり方 - 現時点の最新 ver. 5 では PersistGate コンポーネントを使うことでレンダリングを遅ら せてくれる  

Slide 50

Slide 50 text

テスト

Slide 51

Slide 51 text

No content

Slide 52

Slide 52 text

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); }); }

Slide 53

Slide 53 text

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); }); }

Slide 54

Slide 54 text

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(); assert.ok(component.containsMatchingElement(Dialog)); }); }

Slide 55

Slide 55 text

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); }); }

Slide 56

Slide 56 text

テストのポイント - ビジネスロジックを middleware に集めて、middleware のテストを厚めにやる。 - 新規サービスのため UI がすぐ変わるので components のテストは全てカバーし ていない。 - middleware にロジックを持っていくと action, reducer のテストが簡単になる - 書いてて無駄に感じることもあるが、何か機能を消した時のレグレッションに気付け るので、書く意味はあると思う。

Slide 57

Slide 57 text

モニタリング

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

導入例 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();

Slide 60

Slide 60 text

No content

Slide 61

Slide 61 text

Sentry でよく使っている機能 エラー送信時にユーザー情報も付与できるので お問い合わせ時に即原因究明できたりする Raven.setUserContext({ id: currentUser.id, displayName: currentUser.displayName, });

Slide 62

Slide 62 text

ユーザー分析 in SPA

Slide 63

Slide 63 text

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/

Slide 64

Slide 64 text

ちなみに。。。 Q. SPA って google に index されるの? A. されます。

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

No content

Slide 67

Slide 67 text

// @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 ( {this.props.title} | pook(プック) ); } } export default DocumentMeta; react-helmet meta 情報を動的に書き換え可能

Slide 68

Slide 68 text

アプリっぽくする

Slide 69

Slide 69 text

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" }

Slide 70

Slide 70 text

アドレスバーが出ない ホームに icon 追加 Splash が付く "display": "standalone", "name": "pook(プック)自分らしい暮らし をもっと身近に。", "short_name": "pook", "icons": [{ "src": "img/logo/icon-128x128.png", "type": "image/png", "sizes": "128x128" },

Slide 71

Slide 71 text

アプリっぽくした結果 ユーザー: pook って、アプリなのに通知こないんですか? 私: すみません、アプリじゃないんです。。。 ユーザーの割合的に ios が圧倒的なので、 一旦 service worker による通知は見送り。 safari の service worker 対応が待たれる。

Slide 72

Slide 72 text

開発体制とフロー

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

APIドキュメントを共通仕様として作成&運用 - API Blueprint という API仕様書を mark down で書ける構文を利用 - Github で管理し、PRで新規作成・更新し、機能の詳細は issue で管理 - API はフロントエンドエンジニアが設計 フロントエンド GET: /users したら、ユーザー 一覧のJson返して欲しい。 バックエンド name name name API サーバー API仕様書 仕様書に基 づいたjson を返す

Slide 76

Slide 76 text

API仕様書の作成と運用 ブログにまとめました。 API仕様書をMarkDownで書き、GitHubをつかって運用する方法

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

フロントエンド開発の実装フロー

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

デプロイ 開発のフロー全体(レビューとCI、デプロイ) コードを書く テストを書く 構文チェック、型チェック github から pull トランスパイル、ビルド 実機確認 CIでgithubにあげた ファイルをチェック コードレビュー オープンソースのフローベースの開発 - commit は anguralr のルールをベース - ドキュメントは厚めに書く hoge huga bar

Slide 81

Slide 81 text

デプロイとインフラ

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

S3 CloudFront EC2 build & deploy index.html bundle.js bundle.css gzip API Server GET: /services/1 ...etc

Slide 84

Slide 84 text

CloudFront から gzip で圧縮して配信 Cloud Front の管理画面で、Compress Objects Automatically を Yes にするだけ 44% Down 通常 build gzip 配信

Slide 85

Slide 85 text

課題に思っていること

Slide 86

Slide 86 text

- デザイナーがjsxを書くのに敷居が高く、HTML直書きより協業しにくい - Reducer がどんどん肥えていく => normalize したい - コミュニティライブラリに依存しすぎると思わぬところでハマる - ビジネスロジックの置き場に悩む => middleware においた - FlowType入れたけど、型を使いこなせているか不安 - Google は js 動くが、 Facebook twitter は js 動かない。 OGP 画像でない - ユーザーがアプリと勘違いするので、アプリクオリティが求められる - トレンドを追いかけつつ、プロダクトの製作は結構忙しい React Redux で SPA を開発、運用してみた課題感

Slide 87

Slide 87 text

まとめ

Slide 88

Slide 88 text

React Redux で SPA を開発、運用して - Redux は結構薄いフレームワークなので随所に工夫が必要 - middleware を活用するとコードの見通しは良くなる - ユーザーが求める 「普通」 は確実に難易度が上がってきている - 継続的に改善していける健全なコードを書く環境が必要そう まとめ。と言うか感想です

Slide 89

Slide 89 text

ランサーズエンジニアブログでリポジトリ公開しています - ランサーズ流 React.js/redux アプリ開発入門@mori-dev - レスポンシブ対応の LP を簡単に作れるツールを React Redux で作った。ソース コードあり@numanomanu 「ランサーズ エンジニアブログ」で是非検索してください

Slide 90

Slide 90 text

https://www.wantedly.com/projects/161195

Slide 91

Slide 91 text

Thank you !