フロントエンドの全部揃っている環境が欲しくてreact-ssr-starterを作った

# react-ssr-starterとは
フロントエンド開発でよく使うものを導入しまくって動くようにセットアップしたもの。

---

# 経緯
僕は開発中はhot reloading（`react-hot-loader`）して欲しいし、lint（`eslint stylelint prettier`）はもちろん欲しいし、code splitting（`loadable-components`）もやりたいし、テスト（`jest`, `enzyme`）も書きたいし、コンポーネントも確認（`storybook`）したいし...etc

といった感じでとにかく全部揃っている環境が欲しかった。

---

# Next.js使えばいいんじゃない？
Next.jsを使うのもありですが、使ったとしても結局ある程度は自分で導入しないといけないので、とにかく作業するときに考える手間と導入する手間をなくしたかったのが発端。（でもNext.jsにかなり影響受けてます）

---

# リポジトリ
https://github.com/osamu38/react-ssr-starter

---

# 画面

<img src="https://qiita-image-store.s3.amazonaws.com/0/76971/8d1908b8-395a-1993-8da2-c21b28ff8fe3.png" width="320">


---

# スタック

```json
  "dependencies": {
    "axios": "^0.18.0",
    "body-parser": "^1.18.3",
    "compression": "^1.7.2",
    "cookie-parser": "^1.4.3",
    "express": "^4.16.3",
    "helmet": "^3.12.0",
    "history": "^4.7.2",
    "hpp": "^0.2.2",
    "html-minifier": "^3.5.15",
    "jsonwebtoken": "^8.2.1",
    "loadable-components": "^2.1.0",
    "morgan": "^1.9.0",
    "react": "^16.3.2",
    "react-cookie": "^1.0.5",
    "react-dom": "^16.3.2",
    "react-helmet": "^5.2.0",
    "react-hot-loader": "^4.1.3",
    "react-redux": "^5.0.7",
    "react-router-config": "^1.0.0-beta.4",
    "react-router-dom": "^4.2.2",
    "react-router-redux": "^5.0.0-alpha.9",
    "redux": "^4.0.0",
    "redux-logger": "^3.0.6",
    "redux-thunk": "^2.2.0",
    "serialize-javascript": "^1.5.0",
    "serve-favicon": "^2.5.0",
    "styled-components": "^3.2.6"
  },
  "devDependencies": {
    "@babel/core": "^7.0.0-beta.47",
    "@babel/preset-env": "^7.0.0-beta.47",
    "@babel/preset-flow": "^7.0.0-beta.47",
    "@babel/preset-react": "^7.0.0-beta.47",
    "@babel/preset-stage-0": "^7.0.0-beta.47",
    "@babel/register": "^7.0.0-beta.47",
    "@storybook/addon-links": "^3.4.4",
    "@storybook/react": "^3.4.4",
    "babel-core": "^7.0.0-bridge.0",
    "babel-eslint": "^8.2.3",
    "babel-jest": "^22.4.3",
    "babel-loader": "^8.0.0-beta.2",
    "babel-plugin-add-module-exports": "^0.2.1",
    "babel-plugin-dynamic-import-node": "^1.2.0",
    "babel-plugin-module-resolver": "^3.1.1",
    "babel-plugin-styled-components": "^1.5.1",
    "clean-webpack-plugin": "^0.1.19",
    "copy-webpack-plugin": "^4.5.1",
    "core-js": "^2.5.6",
    "enzyme": "^3.3.0",
    "enzyme-adapter-react-16": "^1.1.1",
    "enzyme-to-json": "^3.3.3",
    "eslint": "^4.19.1",
    "eslint-config-airbnb": "^16.1.0",
    "eslint-config-prettier": "^2.9.0",
    "eslint-import-resolver-babel-module": "^5.0.0-beta.0",
    "eslint-plugin-flowtype": "^2.46.3",
    "eslint-plugin-import": "^2.11.0",
    "eslint-plugin-jsx-a11y": "^6.0.3",
    "eslint-plugin-prettier": "^2.6.0",
    "eslint-plugin-react": "^7.8.2",
    "flow-bin": "^0.72.0",
    "flow-typed": "^2.4.0",
    "friendly-errors-webpack-plugin": "^1.7.0",
    "husky": "^0.14.3",
    "jest": "^23.0.0-beta.2",
    "json-loader": "^0.5.7",
    "lint-staged": "^7.1.0",
    "nodemon": "^1.17.4",
    "prettier": "^1.12.1",
    "raf": "^3.4.0",
    "stylelint": "^8.4.0",
    "stylelint-config-prettier": "^3.2.0",
    "stylelint-config-standard": "^18.2.0",
    "stylelint-config-styled-components": "^0.1.1",
    "stylelint-processor-styled-components": "^1.3.1",
    "uglify-js": "^3.3.25",
    "uglifyjs-webpack-plugin": "^1.2.5",
    "webpack": "^3.11.0",
    "webpack-bundle-analyzer": "^2.11.3",
    "webpack-dev-middleware": "^2.0.6",
    "webpack-hot-middleware": "^2.22.2",
    "webpack-node-externals": "^1.7.2",
    "workbox-sw": "^3.2.0",
    "workbox-webpack-plugin": "^3.2.0"
  },
```

---

# ディレクトリ構成

```
├── public                # 静的ファイルディレクトリ
│   ├── manifest.json     # Web App Manifest
│   ├── favicon.ico       # ファビコン 
│   └── static            # 静的ファイル
│        └── images       # 画像
├── src                   # ソース
│   ├── config            # config
│   ├── components        # コンポーネント
│   ├── pages             # ページ
│   ├── actions           # reduxのaction
│   ├── reducers          # reduxのreducer
│   ├── utils             # ユーティリティー
│   ├── styles            # styled-componentsでinjectGlobalするやつとか
│   ├── types             # flowのためのreducer, action, state, storeの型
│   ├── client.js         # クライアントのエントリー
│   ├── routes.js         # ルーティングの設定ファイル
│   ├── server.js         # サーバーのエントリー
│   └── sw.js             # Service Workerのファイル
├── tools                 # ツール
│   ├── flow              # flowの設定
│   ├── jest              # jestの設定
│   ├── storybook         # storybookの設定
│   └── webpack           # webpackの設定
└── index.js              # アプリケーションのエントリー
```

---

# 使い方

---

## ①導入
```
$ git clone https://github.com/osamu38/react-ssr-starter.git
$ cd react-ssr-starter
$ npm i
```

---

## ②開発
```
$ npm start （http://localhost:2525）
```
<img src="https://qiita-image-store.s3.amazonaws.com/0/76971/43e3e6b2-fad4-0787-c706-bcf4e226e05c.png" width="640">

---

## ③ビルド
```
$ npm run build
$ npm run build:client （クライアントのみ）
$ npm run build:server （サーバーのみ）
```

<img src="https://qiita-image-store.s3.amazonaws.com/0/76971/13b08fdf-3bcc-f975-1faa-8f7a89bf0bc4.png" width="640">

---

## ④本番
```
$ npm run start:prod （http://localhost:2525）
``` 
<img src="https://qiita-image-store.s3.amazonaws.com/0/76971/bbc13d85-0437-dafe-6ced-aba0859af755.png" width="640">

---

## ⑤flow
```
$ npm run flow
```
<img src="https://qiita-image-store.s3.amazonaws.com/0/76971/1dd138ce-1e44-3256-6a3f-762627978234.png" width="640">

---

## ⑥lint（eslint & prettier）
```
$ npm run lint
```
<img src="https://qiita-image-store.s3.amazonaws.com/0/76971/c0c10caa-a771-862f-7e7e-3f493d29b4bd.png" width="640">

---

## ⑦stylelint
```
$ npm run lint:css
```
<img src="https://qiita-image-store.s3.amazonaws.com/0/76971/0232b33c-edd5-3850-6f62-df15f08ed5f3.png" width="640">

---

## ⑧test（jest, enzyme）

### normal
```
$ npm test
```
<img src="https://qiita-image-store.s3.amazonaws.com/0/76971/49407a5b-d5d0-8318-2a9b-b362c99370f4.png" width="640">

### coverage
```
$ npm run test:coverage
```

### update snapshot
```
$ npm run test:update
```

---

## ⑨storybook
```
$ npm run storybook （http://localhost:2626）
```
<img src="https://qiita-image-store.s3.amazonaws.com/0/76971/ed9e8921-0a99-470a-fa99-85e67791896b.png" width="640">

---

# コンポーネントの作り方

---

components以下に`Button`のようなディレクトリを作り、`index.js`,`index.stories.js`, `index.spec.js`を作る。

* `index.js`にはコンポーネントを書く。
* `index.stories.js`は`storiesOf().add()`でstorybookにコンポーネントを表示させる。
* `index.spec.js`ではこのコンポーネントのテストを書く。（テスト内で`toMatchSnapshot`した場合はテスト実行時に`__snapshops__/index.spec.js.snap`が生成される。）

---

## 例（index.js）
```jsx

/* @flow */

import * as React from 'react';
import styled, { css } from 'styled-components';
import { colors } from 'styles/variables';

const ButtonUI = styled.button`
  display: block;
  width: 100%;
  max-width: 240px;
  line-height: 40px;
  font-size: 16px;
  font-weight: bold;
  color: ${colors.accent};
  text-align: center;
  cursor: pointer;
  border: 2px ${colors.accent} solid;
  ${({ isCenter }) =>
    isCenter &&
    css`
      margin: 0 auto;
    `};
`;

type Props = {
  children?: React.Node,
  onClick?: Function,
  isCenter?: boolean,
};

export default function Button(props: Props) {
  const { children, onClick, isCenter } = props;

  return (
    <ButtonUI onClick={onClick} isCenter={isCenter}>
      {children}
    </ButtonUI>
  );
}
```

---

## 例（index.stories.js）
```jsx


import React from 'react';
import { storiesOf } from '@storybook/react';
import Button from 'components/Button';

storiesOf('Button', module).add('normal', () => <Button>Button</Button>);
```

---

## 例（index.spec.js）
```jsx

import React from 'react';
import Button from 'components/Button';

describe('<Button />', () => {
  it('render children', () => {
    const children = 'Button';
    const wrapper = shallow(<Button>{children}</Button>);

    expect(wrapper.prop('children')).toEqual(children);
  });
  it('render snapshots', () => {
    const wrapper = shallow(<Button>Button</Button>);

    expect(toJson(wrapper)).toMatchSnapshot();
  });
  it('call onclick', () => {
    const mock = jest.fn();
    const wrapper = shallow(<Button />);

    wrapper.setProps({ onClick: mock });
    wrapper.simulate('click');
    expect(mock).toHaveBeenCalled();
  });
});
```

---

## code splitting
`src/components/index.js`で下記のように記述するとcode splittingを制御できる。

```javascript

  // code splittingする場合
  Button: loadable(() => import('components/Button')),
  // code splittingしない場合
  Button: require('components/Button'),
```

---

# ページの作り方

---

pages以下に`About`のようなディレクトリを作り、`index.js`, `index.spec.js`を作る。あとはコンポーネントの作り方と一緒。

---

## pagesの設計
アンチパターンかもしれませんが、pagesの中のコンポーネントにはあらかじめ`bindActionCreators`で`dispatch`された全てのactionと全てのstore情報が渡って来ているので、`connect`する必要はありません。
さらに`withRouter`や`hot(module)(component)`もする必要はないので、とてもシンプルにコンポーネントを書くことが出来ます。

---

## 例（index.js）
```jsx


/* @flow */

import * as React from 'react';
import Helmet from 'react-helmet';
import { LoginForm, Title } from 'components';

type Props = {
  userActions: {
    login: Function,
  },
};

export default function Landing(props: Props) {
  const {
    userActions: { login },
  } = props;

  return (
    <>
      <Helmet title="Landing" />
      <Title>Landing Page</Title>
      <LoginForm login={login} />
    </>
  );
}
```

---

## サーバーサイドリクエスト

`src/routes.js` 40行目付近

```javascript
    ...
    component: loadable(() => import('pages/UserDetail')),
    loadData: (dispatch, state, params) => dispatch(fetchUser(params.id)),
  },
  ...
```

`loadData`でserver sideでのリクエストが送れます。（Next.jsにおける`getInitialProps`）

---

# 変わった特徴

---

## ①画像はimportやrequireせずに絶対パスで指定

```html


<img src="/static/images/svg/oct-icon.svg" alt="oct-icon" />
```

require出来るようにしても良かったんですが、あまりメリットを感じなかったのでやらず。

---

## ②エイリアスを設定している

```javascript

import user from 'reducers/user';
import ui from 'reducers/ui';
```

rootとsrc以下はエイリアスが設定されているので、参照が楽。

---

## ③開発中はSPA、本番はSSR
開発中はSPAの方がやりやすい部分があったのでSPAで動いてます。

---

## ④ちょっとした認証機能を入れている

`src/routes.js` 20行目付近

```javascript

...
export default [
  {
    path: '/',
    component: Auth,
    routes: addExact([
      {
        path: url.endpoint.landing,
        isLoggedIn: false,
        component: loadable(() => import('pages/Landing')),
      },
      ...
```
このAuthというcomponentsで認証をしているため、store内の`user.status.isLoggedIn`とroutesの中にある`isLoggedIn`の値を見てサーバーサイドとクライアントでリダイレクトがかかります。
Landingページは`isLoggedIn: false`と設定してあるので、ログインしている状態では遷移することができません。

---
## ⑤本番でservice workerが動いてる
`workbox`を導入しているので、そのままの設定だとhtml以外の全ての静的ファイルがPre cacheされ、apiへのリクエストもRuntime cacheされるようになっています。htmlをキャッシュしてるわけではないのでofflineで動いたりはしません。

---

## ⑥ちなみに
どうしてもloginのapiが必要でserver.jsにしれっと書いてありますが、使わない場合は消してください。

---

# 悔しさ
* webpack4に出来なかった。（loadable-componentsと相性が悪い？）
* stylelintが9系にするとstyled-componentsに対して機能しなくなる。（謎）
* READMEをちゃんと書けていない。。。（申し訳ない）

---

# まとめ
フロントエンドのセットアップめんどくせーと思っている人にはぴったりのスターターになったかなと思います。
もし使ってくれたら感想欲しいです！
そして「ここがイケてない」ってのもあれば、コメントお願いします。

https://github.com/osamu38/react-ssr-starter