Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,24 @@
"author": "Max Kokorin",
"license": "MIT",
"dependencies": {
"@babel/cli": "^7.0.0-beta.51",
"@babel/node": "^7.0.0-beta.51",
"babel-eslint": "^8.2.3",
"babel-loader": "^8.0.0-beta",
"eslint-plugin-babel": "^5.1.0",
"eslint-plugin-react": "^7.9.1",
"express": "^4.16.3",
"helmet": "^3.12.1",
"history": "^4.6.2",
"hpp": "^0.2.2",
"inquirer": "^6.0.0",
"loadable-components": "^2.2.2",
"morgan": "^1.9.0",
"pluralize": "^7.0.0",
"react": "^16.4.0",
"react-dev-utils": "^5.0.1",
"react-dom": "^16.4.0",
"react-helmet": "^5.2.0",
"react-hot-loader": "^4.3.1",
"react-redux": "^5.0.7",
"react-router": "^4.3.1",
Expand All @@ -38,7 +45,9 @@
"react-router-redux": "^5.0.0-alpha.9",
"redux": "^4.0.0",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0"
"redux-thunk": "^2.3.0",
"serve-favicon": "^2.5.0",
"webpack-manifest-plugin": "^2.0.3"
},
"devDependencies": {
"@babel/core": "^7.0.0-beta.49",
Expand Down
2 changes: 2 additions & 0 deletions source/scripts/client.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {renderRoutes} from 'react-router-config'
import createHistory from 'history/createBrowserHistory'

const initialState = window.__INITIAL_STATE__
delete window.__INITIAL_STATE__

const history = createHistory()
const store = configureStore(history, initialState)

Expand Down
160 changes: 160 additions & 0 deletions source/scripts/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@


import path from 'path'
import logger from 'morgan'
import express from 'express'
import compression from 'compression'
import helmet from 'helmet'
import hpp from 'hpp'
// import favicon from 'serve-favicon'
import React from 'react'
import {renderToString} from 'react-dom/server'
import {StaticRouter} from 'react-router-dom'
import {renderRoutes, matchRoutes} from 'react-router-config'
import {Provider} from 'react-redux'
import Helmet from 'react-helmet'

import createHistory from 'history/createMemoryHistory'
import {configureStore} from './store/configureStore'
import renderHtml from './utils/renderHtml'
import routes from './routes'
import assets from '../../dist/webpack-assets.json'

const port = 3000
const host = 'localhost'

const app = express()

// Use helmet to secure Express with various HTTP headers
app.use(helmet())
// Prevent HTTP parameter pollution
app.use(hpp())
// Compress all requests
app.use(compression())

// Use for http request debug (show errors only)
app.use(logger('dev', {skip: (req, res) => res.statusCode < 400}))
// app.use(favicon(path.resolve(process.cwd(), 'public/favicon.ico')))
const __DEV__ = false
if (!__DEV__) {
app.use(express.static(path.resolve(process.cwd(), 'dist')))
} else {
/* Run express as webpack dev server */

const webpack = require('webpack')
const webpackConfig = require('../tools/webpack/config.babel')
const compiler = webpack(webpackConfig)

compiler.apply(new webpack.ProgressPlugin())

app.use(
require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
headers: {'Access-Control-Allow-Origin': '*'},
hot: true,
quiet: true, // Turn it on for friendly-errors-webpack-plugin
noInfo: true,
stats: 'minimal',
serverSideRender: true
})
)

app.use(
require('webpack-hot-middleware')(compiler, {
log: false // Turn it off for friendly-errors-webpack-plugin
})
)
}

// Register server-side rendering middleware
app.get('*', (req, res) => {
const history = createHistory()
const store = configureStore(history)

// The method for loading data from server-side
const loadBranchData = () => {
const branch = matchRoutes(routes, req.path)

const promises = branch.map(({route, match}) => {
if (route.loadData) {
return Promise.all(
route
.loadData({params: match.params, getState: store.getState})
.map(item => store.dispatch(item))
)
}

return Promise.resolve(null)
})

return Promise.all(promises)
};

(async () => {
try {
// Load data from server-side first
await loadBranchData()

const staticContext = {}
const AppComponent = (
<Provider store={store}>
{/* Setup React-Router server-side rendering */}
<StaticRouter location={req.path} context={staticContext}>
{renderRoutes(routes)}
</StaticRouter>
</Provider>
)

// Check if the render result contains a redirect, if so we need to set
// the specific status and redirect header and end the response
if (staticContext.url) {
res.status(301).setHeader('Location', staticContext.url)
res.end()

return
}

// Extract loadable state from application tree (loadable-components setup)
const head = Helmet.renderStatic()
const htmlContent = renderToString(AppComponent)
const initialState = store.getState()

// Check page status
const status = staticContext.status === '404' ? 404 : 200

// Pass the route and initial state into html template
res
.status(status)
.send(
renderHtml(
head,
assets,
htmlContent,
initialState,
''
)
)
} catch (err) {
res.status(404).send('Not Found :(')
// eslint-disable-next-line
console.error(`==> 😭 Rendering routes error: ${err}`)
}
})()
})

if (port) {
app.listen(port, host, err => {
const url = `http://${host}:${port}`

if (err) {
// eslint-disable-next-line
console.error(`==> 😭 OMG!!! ${err}`)
}

// eslint-disable-next-line
console.info(`==> 🌎 Listening at ${url}`)
})
} else {
// eslint-disable-next-line
console.error('==> 😭 OMG!!! No PORT environment variable has been specified')
}
2 changes: 1 addition & 1 deletion source/scripts/store/configureStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function configureStore(history, initialState = {}) {
const router = routerMiddleware(history)
let middlewares = [ router, thunk ]

if (process.env.NODE_ENV !== 'production') {
if (process.env.NODE_ENV !== 'production' && typeof window != 'undefined') {
const logger = createLogger({collapsed: true, diff: true})
middlewares.push(logger)
}
Expand Down
82 changes: 82 additions & 0 deletions source/scripts/utils/renderHtml.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import serialize from 'serialize-javascript'
import {
minify
} from 'html-minifier'

const __DEV__ = false

export default (head, assets, htmlContent, initialState, loadableStateTag) => {
// Use pre-defined assets in development. "main" is the default webpack generated name.
const envAssets = __DEV__ ?
{
js: '/assets/main.js',
css: '/assets/main.css'
} :
assets

const html =
`
<!doctype html>
<html ${head.htmlAttributes.toString()}>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!--[if IE]>
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
<![endif]-->
<link rel="apple-touch-icon" href="apple-touch-icon.png">
<link rel="shortcut icon" href="/favicon.ico">
${head.title.toString()}
${head.base.toString()}
${head.meta.toString()}
${head.link.toString()}
<!-- Insert bundled styles into <link> tag -->
${Object.keys(envAssets)
.map(
key =>
key.substr(key.length - 3) === 'css'
? `<link href="${
envAssets[key]
}" media="screen, projection" rel="stylesheet" type="text/css">`
: ''
)
.join('')}
</head>
<body>
<!-- Insert the router, which passed from server-side -->
<div id="react-view">${htmlContent}</div>
<!-- Insert loadableState's script tag into page (loadable-components setup) -->
${loadableStateTag}
<!-- Store the initial state into window -->
<script>
// Use serialize-javascript for mitigating XSS attacks. See the following security issues:
// http://redux.js.org/docs/recipes/ServerRendering.html#security-considerations
window.__INITIAL_STATE__=${serialize(initialState)};
</script>
<!-- Insert bundled scripts into <script> tag -->
${Object.keys(envAssets)
.map(
key =>
key.substr(key.length - 2) === 'js'
? `<script src="${envAssets[key]}"></script>`
: ''
)
.join('')}
${head.script.toString()}
</body>
</html>
`

// html-minifier configuration, refer to "https://github.com/kangax/html-minifier" for more configuration
const minifyConfig = {
collapseWhitespace: true,
removeComments: true,
trimCustomFragments: true,
minifyCSS: true,
minifyJS: true,
minifyURLs: true
}

// Minify html in production
return __DEV__ ? html : minify(html, minifyConfig)
}
10 changes: 5 additions & 5 deletions webpack.config.base.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
var path = require('path')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var FaviconsWebpackPlugin = require('favicons-webpack-plugin')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const FaviconsWebpackPlugin = require('favicons-webpack-plugin')

module.exports = {
entry: [
'./source/scripts/client'
],
output: {
filename: 'js/app.[hash:4].js',
path: path.join(__dirname, 'dist')
path: path.join(__dirname, 'dist'),
},
resolve: {
extensions: ['.js', '.jsx']
Expand Down
11 changes: 11 additions & 0 deletions webpack.config.prod.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
const path = require('path')
const webpack = require('webpack')
const baseConfig = require('./webpack.config.base')
const ManifestPlugin = require('webpack-manifest-plugin')

module.exports = {
...baseConfig,
output: {
filename: 'js/app.[hash:4].js',
path: path.join(__dirname, 'dist'),
pathinfo: true,
},
mode: 'production',
plugins: [
...baseConfig.plugins,
new ManifestPlugin({
fileName: path.resolve(process.cwd(), 'dist/webpack-assets.json'),
filter: file => file.isInitial
}),
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new webpack.DefinePlugin({
Expand Down
Loading