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にかなり影響受けてます)
リポジトリ
画面
スタック
"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)
③ビルド
$ npm run build
$ npm run build:client (クライアントのみ)
$ npm run build:server (サーバーのみ)
④本番
$ npm run start:prod (http://localhost:2525)
⑤flow
$ npm run flow
⑥lint(eslint & prettier)
$ npm run lint
⑦stylelint
$ npm run lint:css
⑧test(jest, enzyme)
normal
$ npm test
coverage
$ npm run test:coverage
update snapshot
$ npm run test:update
⑨storybook
$ npm run storybook (http://localhost:2626)
コンポーネントの作り方
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)
/* @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)
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)
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を制御できる。
// 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)
/* @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行目付近
...
component: loadable(() => import('pages/UserDetail')),
loadData: (dispatch, state, params) => dispatch(fetchUser(params.id)),
},
...
loadData
でserver sideでのリクエストが送れます。(Next.jsにおけるgetInitialProps
)
変わった特徴
①画像はimportやrequireせずに絶対パスで指定
<img src="/static/images/svg/oct-icon.svg" alt="oct-icon" />
require出来るようにしても良かったんですが、あまりメリットを感じなかったのでやらず。
②エイリアスを設定している
import user from 'reducers/user';
import ui from 'reducers/ui';
rootとsrc以下はエイリアスが設定されているので、参照が楽。
③開発中はSPA、本番はSSR
開発中はSPAの方がやりやすい部分があったのでSPAで動いてます。
④ちょっとした認証機能を入れている
src/routes.js
20行目付近
...
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をちゃんと書けていない。。。(申し訳ない)
まとめ
フロントエンドのセットアップめんどくせーと思っている人にはぴったりのスターターになったかなと思います。
もし使ってくれたら感想欲しいです!
そして「ここがイケてない」ってのもあれば、コメントお願いします。