The Repo

Dependencies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
"devDependencies": {
"@types/react": "^16.0.5",
"@types/react-dom": "^15.5.4",
"@types/react-redux": "^5.0.6",
"@types/redux-actions": "^1.2.8",
"@types/webpack": "^3.0.10",
"@types/webpack-dev-server": "^2.4.1",
"autoprefixer": "^7.1.3",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.0",
"babel-preset-react": "^6.24.1",
"bundle-loader": "^0.5.5",
"eslint": "^4.6.1",
"eslint-config-airbnb": "^15.1.0",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-jsx-a11y": "^6.0.2",
"eslint-plugin-react": "^7.3.0",
"html-webpack-plugin": "^2.30.1",
"node-sass": "^4.5.3",
"precss": "^2.0.0",
"redux-devtools": "^3.4.0",
"redux-devtools-dock-monitor": "^1.1.2",
"redux-devtools-log-monitor": "^1.3.0",
"rimraf": "^2.6.1",
"sass-loader": "^6.0.6",
"ts-loader": "^2.3.4",
"typescript": "next",
"uglifyjs-webpack-plugin": "^0.4.6",
"webpack": "^3.5.5",
"webpack-dashboard": "^1.0.0-5",
"webpack-dev-server": "^2.7.1",
"webpack-merge": "^4.1.0"
},
"dependencies": {
"@types/node": "^8.0.26",
"@types/react-router-dom": "^4.0.7",
"babel-plugin-transform-runtime": "^6.23.0",
"extract-text-webpack-plugin": "^3.0.0",
"react": "^15.6.1",
"react-dom": "^15.6.1",
"react-redux": "^5.0.6",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
"redux": "^3.7.2",
"redux-actions": "^2.2.1",
"redux-observable": "^0.16.0",
"rxjs": "^5.4.3"
}

Project Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.
├── config
│   ├── webpack.config.base.js
│   ├── webpack.config.dev.js
│   ├── webpack.config.prod.js
├── src
│   ├── actions
│   │   ├── index.tsx
│   │   ├── hello.tsx
│   ├── components
│   │   ├── AsyncRoute.tsx
│   ├── containers
│   │   ├── App.tsx
│   │   ├── Header.tsx
│   │   ├── Body.jsx
│   │   ├── Footer.tsx
│   │   ├── Index.tsx
│   │   ├── Page2.tsx
│   │   ├── DevTools.tsx
│   ├── epics
│   │   ├── index.tsx
│   │   ├── hello.tsx
│   ├── reducers
│   │   ├── index.tsx
│   │   ├── hello.tsx
│   ├── store
│   │   ├── index.tsx
│   │   ├── configureStore.dev.tsx
│   │   ├── configureStore.prod.tsx
│   ├── templates
│   │   ├── index.html
│   ├── utils
│   │   ├── connect.tsx
│   │   ├── nav.tsx
│   ├── app.tsx
├── package.json
├── tsconfig.json
├── postcss.config.js
├── .babelrc
├── .eslintrc.js
├── .gitignore
├── yarn.lock

Step 1: Create Project

1
2
# init project
yarn init -y
1
2
3
4
# add eslint
yarn add --dev eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react

eslint --init
1
2
# add webpack
yarn add --dev webpack webpack-dev-server webpack-dashboard webpack-merge @types/webpack @types/webpack-dev-server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# add loader for jsx
yarn add --dev babel-core babel-loader babel-preset-env babel-preset-react

# add loader for tsx
yarn add --dev ts-loader typescript@next

# add loader for css
yarn add --dev style-loader css-loader resolve-url-loader postcss-loader node-sass sass-loader

# add loader for file
yarn add --dev url-loader file-loader

# add plugin for js
yarn add babel-plugin-transform-runtime

# add plugin for postcss
yarn add --dev autoprefixer precss

# add plugin for html
yarn add --dev html-webpack-plugin

# add plugin for uglifyjs
yarn add --dev uglifyjs-webpack-plugin

# add plugin for extract css
yarn add --dev extract-text-webpack-plugin

set gitignore

1
2
3
4
5
6
7
8
# Logs
*.log

# dependencies
node_modules

# build
dist

set typescript config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"compilerOptions": {
"outDir": "dist",
"module": "commonjs",
"target": "es5",
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"jsx": "react",
"moduleResolution": "node",
"rootDir": "src",
"noImplicitReturns": true,
"noImplicitThis": false,
"noImplicitAny": false,
"strictNullChecks": true
},
"exclude": [
"config",
"node_modules",
"dist",
"webpack",
"jest"
],
"types": [
"typePatches"
]
}

Set basic webpack config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// /config/webpack.config.base.js
const path = require('path')

module.exports = {
output: {
path: path.resolve(__dirname, '../dist'),
filename: 'scripts/[name]-[hash].js',
chunkFilename: 'scripts/[name]-[chunkhash].js',
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
exclude: /node_modules/
}, {
test: /\.jsx?$/,
loader: 'babel-laoder',
exclude: /node_modules/,
}, {
test: /\.(png|jpg)$/,
loader: 'url-loader',
options: {
limit: 8192,
name: '[name]-[hash].[ext]',
}
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js', '.scss', '.css', '.json']
}
}

Set dev webpack config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// /config/webpack.config.dev.js
const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const HtmlPlugin = require('html-webpack-plugin')
const DashboardPlugin = require('webpack-dashboard/plugin')
const baseConfig = require('./webpack.config.base')

const devConfig = {
entry: {
app: [
'webpack/hot/only-dev-server',
'webpack-dev-server/client?http://localhost:8080',
path.resolve(__dirname, '../src/app.tsx'),
],
},
module: {
rules: [
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
name: '[local]-[name]-[hash]'
},
},
],
'resolve-url-loader',
'sass-loader',
},
],
},
devtool: 'source-map',
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(),
new webpack.DefinePluing({
'process.env': {
NODE_ENV: 'development',
},
}),
new DashboardPlugin(),
new HtmlPlugin({
title: '开发',
template: path.resolve(__dirname, '../src/temlates/index.html'),
}),
],
devServer: {
hot: true,
compress: true,
historyApiFallback: true,
}
}

module.exports = merge(baseConfig, devConfig)

Set prod webpack config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// /config/webpack.config.prod.js

const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
const HtmlPlugin = require('html-webpack-plugin')
const baseConfig = require('./webpack.config.base.js')

const prodConfig = {
entry: {
app: path.resolve(__dirname, '../src/app.tsx'),
},
module: {
rules: [
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
{
loader: 'css-loader',
options: {
modules: true,
name: '[local]-[name]-[hash]',
importLoaders: 3,
},
},
],
exclude: /node_modules/,
}), {
loader: 'postcss-loader',
options: {
config: path.resolve(__dirname, './config/postcss.config.json')
}
},
'resolve-url-loader',
'sass-loader',
],
},
devtool: 'inline-source-map',
plugins: [
new webpack.DefinePluing({
'process.env.NODE_ENV': 'production',
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
filename: 'scripts/vendor-[hash].min.js',
}),
new HtmlPlugin({
title: '生产',
template: path.resolve(__dirnaem, '../src/templates/index.html'),
minify: {
collapseBooleanAttributes: true,
collapseInlineTagWhitespace: true,
collapseWhitespace: true,
minifyCSS: true,
minifyJS: true,
removeAttributeQuotes: true,
removeComments: true,
removeEmptyAttributes: true,
removeRedundantAttributes: true,
removeTagWhitespace: true,
},
}),
new ExtractTextPlugin({
filename: 'styles/[name]-[contenthash].css',
}),
new UglifyJSPlugin(),
],
}

Set config refered by webpack(babelrc, postcss, index.html)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// /.babelrc
{
"presets": [
[
"env", {
"targets": {
"browsers": ["last 2 versions", "safari >= 9"],
}
}
],
"react",
],
"plugins": [
["transform-runtime", {
"polyfill": false,
"regenerator": true
}],
]
}
1
2
3
4
5
6
7
8
9
const precss = require('precss')
const autoprefixer = require('autoprefixer')

module.exports = {
plugins: [
precss,
autoprefixer,
],
}
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
</body>
</html>

Step 2: Skeleton of React App(React + Router)

1
2
3
yarn add --dev bundle-laoder @types/react @types/react-dom @types/node @types/react-router-dom

yarn add react react-dom react-router-dom

In src/app.tsx render the App to html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import * as React from 'react'
import { render } from 'react-dom'
import App from './containers/App'

render(
<App />, document.getElementById('app') as HTMLElement
)

if (module.hot) {
module.hot.accept('./containers/App', () => {
const App = require('./containers/App').default
render (
<App />,
document.getElementById('app')
)
})
}

Create App Represental Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// `./src/containers/App.tsx`

import * as React from 'react'
import {
BrowserRouter as Router,
Route,
} from 'react-router-dom'
import Header from './Header'
import Body from './Body'
import Footer from './Footer'

export default class extends React.Component {
constructor () {
super ()
}
render () {
return (
<Router>
<div>
<Route path="/" component={Header} />
<Route path="/" component={Body} />
<Route path="/" component={Footer} />
</div>
</Router>
)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// ./src/containers/Header.tsx

import * as React from 'react'
import { goTo } from '..utils/nav'

class Header extends React.Component {
protected goTo = goTo.bind(this)
constructor () {
super()
this.state = {
navs: [
{ label: '首页', url: 'index' },
{ label: '第二页', url: 'page2' },
{ label: '第三页', url: 'page3' },
]
}
}

render () {
const { navs } = this.state
return (
<div>
{
navs.map(
(nav: {label: string, url: string}) => <span key={nav.url} onClick={this.goTo(nav.url)}>{nav.label}</span>
)
}
</div>
)
}
}

export default Header
1
2
3
4
5
6
7
// ./src/utils/nav.tsx

export function goTo(url: string) {
retrn () => {
this.props.history.push(url)
}
}

Same structure in Body and Footer.

Add AsyncRoute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import * as React from 'react'

interface PassedProps extends React.Props<any> {
load: any;
children: any;
}

class Bundle extends React.Component<PassedProps, any> {
constructor () {
super()
this.state = {
mod: null,
}
}
componentWillMount () {
this.load(this.props)
}
componentWillReceiveProps (nextProps) {
if (nextProps.load !== this.props.load) {
this.load(nextProps)
}
}
load (props) {
this.setState({
mod: null,
})
props.load((mod) => {
this.setState({
mod: mod.default || mod,
})
})
}
render () {
return this.state.mod ? this.props.children(this.state.mod) : null
}
}

export default module => routerProps => (<Bundle load={module}>
{ Comp => Comp ? <Comp {...routerProps} /> : null}
)

And use it in body container

1
2
3
4
5
6
7
8
9
10
11
import * as React from 'react'
import { Route } from 'react-router'

import Index from 'bundle-loader?lazy!./Index'
import AsyncRoute from '../components/AsyncRoute'

export default () => (
<div>
<Route path="/index" component={AsyncRoute(Index)}
</div>
)

Add Redux

1
2
3
yarn add --dev redux-devtools redux-devtools-dock-monitor redux-devtools-log-monitor

yarn add redux redux-actions react-redux

Create Actions

1
2
3
4
5
6
7
8
9
10
11
// ./src/actions/hello.tsx
import { createActions } from 'redux-actions'

export default createActions({
SAY_HELLO: text => ({ text })
})

// export
// {
// sayHello: (text: string) => ({ text: string })
// }
1
2
3
4
5
// ./src/actions/index.tsx
import helloActions from './hello'
export default {
...helloActions
}

Create Reducers

1
2
3
4
5
6
7
8
9
10
11
// ./src/reducers/hello.tsx
import { handleActions } from 'redux-actions'
import actions from '../actions'

export default handleActions({
[actions.sayHello.toString()]: (state, action) => {
return { ...state, ...action.payload }
},
}, {
text: '',
})
1
2
3
4
5
6
7
// ./src/reducers/index.tsx
import { combineReducers } from 'redux'
import helloReducer from './hello'

export default combineReducers({
hello: helloReducer,
})

Create Store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ./src/store/configureStore.dev.tsx

import { createStore, applyMiddleware, compose, GenericStoreEnhancer } from 'redux'
import reducer from '../reducers'

const enhancer = compose(
// applyMiddleware(...middlewares)

window.devToolsExtension ? window.devToolsExtension() : f => f,
)

function configureStoreDev(initState: any): any {
const store = createStore(reducer, initState, enhancer)
if (module.hot) {
module.hot.accpet('../reducers', () => {
store.replaceReducer(require('../reducers').default)
})
}
return store
}

module.exports = configureStoreDev
1
2
3
4
5
6
7
8
9
10
// ./src/store/configureStoreProd

import { createStore, applyMiddleware } from 'redux'
import reducer from '../reducers'

function configureStoreProd (initState: any) {
return createStore(reducer, initState)
}

module.exports = configureStoreProd
1
2
3
4
5
6
7
// ./src/store/index.jsx

if (process.env.NODE_ENV == 'development') {
module.exports = require('./configureStore.dev')
} else {
module.exports = require('./configureStore.prod')
}

Add Provider to Main Container

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ./container/App.tsx
import * as React from 'react'
import {
BrowserRouter as Router,
Route,
} from 'react-router-dom'
import { Provider } from 'react-redux'
import Header from './Header'
import Body from './Body'
import Footer from './Footer'

const configureStore = require('../store')

const store = configureStore({})

export default class extends React.Component {
constructor () {
super()
}
render () {
returun (
<Router>
<Provider store={store}>
<Route path="/" component={Header} />
<Route path="/" component={Body} />
<Route path="/" component={Body} />
</Provider>
</Router>
)
}
}

custom connect to cache mapStateToProps and mapDispatchToProps

1
2
3
4
5
6
7
8
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import actions from '../actions'

const mapStateToProps = (state: any) => state
const mapDispatchToProps = (dispatch: any) => (bindActionCreators(actions, dispatch))

export default (Comp: any) => connect(mapStateToProps, mapDispatchToProps)(Comp)

Connect the Presentational Container

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import * as React from 'react'
import connect from '../utils/connect'
import { goTo } from '../utils/nav'

interface PassedProps extends React.Props<any> {
sayHello: any;
}

class Header extends React.Component<PassedProps, any> {
protected goTo = goTo.bind(this)
constructor () {
super()
this.state = {
navs: [
{ label: '首页', url: 'index' },
{ label: '第二页', url: 'page2' },
{ label: '第三页', url: 'page3' },
]
}
}
componentDidMount () {
this.props.sayHello('world')
}
render () {
const { navs } = this.state
return (
<div>
{navs.map((nav: { label: string, url: string}) => <span
key={nav.url}
onClick={this.goTo(nav.url)}
>{nav.label}</span>)}
</div>
)
}
}

export default connect(Header)

Add Rxjs

1
yarn add rxjs redux-observable

Create Epics

1
2
3
4
5
6
7
8
9
10
11
// ./src/epics/hello.tsx
import 'rxjs'
export const sayHello = (action$, store) =>
.action$.ofType('SAY_HELLO')
.delay(1000)
.map(() => ({
type: 'SAY_HELLO',
payload: {
text: 'got hello'
}
}))
1
2
3
4
5
6
7
8
// ./src/epcis/index.tsx

import { combineEpics } from 'redux-observable'
import { sayHello } from './hello'

export default combineEpics(
sayHello,
)

Add Epic to Enhancer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { createStore, applyMiddleware, compose, GenericStoreEnhancer } from 'redux'
import { createEpicMiddleware } from 'redux-observable'
import reducer from '../reducers'
import epics from '../epics'

const epicMiddleware = createEpicMiddleware(epics)

const enhancer = compose(
applyMiddleware(epicMiddleware),
window.devToolsExtension ? window.devToolsExtension() : f => f
)

function configureStoreDev (initState: any) {
const store = createStore(reducer, initState, enhancer)
if (module.hot) {
module.hot.accept('../reducers', () => {
store.replaceReducer(require('../reducers').default)
})
}
return store
}

module.exports = configureStoreDev

Add Service Worker

1
yarn add sw-precache-webpack-plugin

Add the Plugin in webpack.config.prod.js

1
2
3
4
const SWPrecachePluing = require('sw-precache-webpack-plugin')
plugins: [
new SWPrecachePluing()
]

Add Bundle Analyzer

1
yarn add --dev webpack-bundle-anaylzer

Add the Plugin in webpack.config.dev.js

1
2
3
4
5
const BundleAnalyzer = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

plugins: [
new BundleAnalyzer()
]

Add Dll

Add webpack.config.dll.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// webpack.config.dll.js
const path = require('path')
const webpack = require('webpack')
const ManifestPlugin = require('webpack-manifest-plugin')

module.exports = {
entry: {
vendor: [/* ... */],
},
output: {
path: path.resolve(__dirname, '../lib'),
filename: '[name]_[hash:5].js',
library: '[name]_[hash:5]',
},
plugins: [
new webpack.DllPlugin({
name: '[name]_[hash:5]',
path: path.resolve(__dirname, '../lib', '[name]-manifest.json'),
}),
new ManifestPlugin(),
],
}

Copy dll to dist and refer

1
2
3
4
5
6
7
8
9
10
// webpack.config.base.js
const vendorManifest = require('../lib/vendor-manifest.json')
plugins: [
new CopyPlugin([
{ from: path.resolve(__dirname, '../lib/*.js') },
]),
new webpack.DllReference({
manifest: vendorManifest,
}),
]

Insert Dll files in html

1
2
3
4
5
6
7
8
9
10
11
12
13
// webpack.config.dev.js
const fs = require('fs')
const manifest = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../lib/manifest.json')))

module.exports = {
// ...
plugins: [
new HtmlPlugin({
// ...
dll: `./lib/${manifest['vendor.js']}`,
}),
],
}