react-isomorphic-boilerplate
Advanced tools
Comparing version 0.0.0 to 0.1.0
{ | ||
"name": "react-isomorphic-boilerplate", | ||
"version": "0.0.0", | ||
"version": "0.1.0", | ||
"main": "index.js", | ||
"license": "MIT", | ||
"scripts": { | ||
"start": "DEBUG=*,-nodemon*,-express*,-send nodemon dist/server.js", | ||
"start": "DEBUG=*,-nodemon*,-express*,-send nodemon --inspect dist/server.js", | ||
"build:client:dev": "rm -f dist/*-client.js && webpack --env=dev --progress --profile --colors", | ||
@@ -14,4 +14,5 @@ "build:server:dev": "webpack --env=dev --config=webpack.server.js --progress --profile --colors", | ||
"build:server:prod": "webpack --env=prod --config=webpack.server.js --progress --profile --colors", | ||
"eslint": "eslint src", | ||
"test": "npm run eslint" | ||
"eslint": "eslint ./", | ||
"test": "npm run eslint && cross-env NODE_ENV=test nyc ava --verbose", | ||
"report": "cross-env NODE_ENV=test nyc ava && nyc report --reporter=lcov" | ||
}, | ||
@@ -21,5 +22,7 @@ "devDependencies": { | ||
"autoprefixer": "^7.1.6", | ||
"ava": "^0.23.0", | ||
"babel-core": "^6.26.0", | ||
"babel-eslint": "^8.0.1", | ||
"babel-loader": "^7.1.2", | ||
"babel-plugin-istanbul": "^4.1.5", | ||
"babel-preset-env": "^1.6.0", | ||
@@ -29,4 +32,7 @@ "babel-preset-es2015": "^6.24.1", | ||
"babel-preset-stage-2": "^6.24.1", | ||
"cross-env": "^5.1.1", | ||
"css-loader": "^0.28.7", | ||
"debug": "^3.1.0", | ||
"enzyme": "^3.2.0", | ||
"enzyme-adapter-react-16": "^1.1.0", | ||
"eslint": "^4.8.0", | ||
@@ -37,9 +43,15 @@ "eslint-loader": "^1.9.0", | ||
"file-loader": "^1.1.5", | ||
"ignore-styles": "^5.0.1", | ||
"immutability-helper": "^2.4.0", | ||
"lodash": "^4.17.4", | ||
"mock-require": "^2.0.2", | ||
"node-sass": "^4.6.1", | ||
"nodemon": "^1.12.1", | ||
"nyc": "^11.3.0", | ||
"postcss-loader": "^2.0.8", | ||
"prop-types": "^15.6.0", | ||
"react-addons-test-utils": "^15.6.2", | ||
"react-redux": "^5.0.6", | ||
"react-router": "^4.2.0", | ||
"react-router-dom": "^4.2.2", | ||
"redux": "^3.7.2", | ||
@@ -51,3 +63,6 @@ "redux-logger": "^3.0.6", | ||
"sass-loader": "^6.0.6", | ||
"sinon": "^4.1.2", | ||
"style-loader": "^0.19.0", | ||
"superagent": "^3.8.1", | ||
"supertest": "^3.0.0", | ||
"uglifyjs-webpack-plugin": "^1.0.1", | ||
@@ -61,3 +76,4 @@ "url-loader": "^0.6.2", | ||
"react": "^16.1.1", | ||
"react-dom": "^16.1.1" | ||
"react-dom": "^16.1.1", | ||
"react-helmet": "^5.2.0" | ||
}, | ||
@@ -79,3 +95,23 @@ "description": "This boilerplate would help you build a react/redux/react-router isomorphic/universal web app", | ||
}, | ||
"homepage": "https://github.com/ddhp/react-isomorphic-boilerplate#readme" | ||
"homepage": "https://github.com/ddhp/react-isomorphic-boilerplate#readme", | ||
"ava": { | ||
"files": [ | ||
"src/**/__tests__/**/*.js", | ||
"!test/fixtures/**/*", | ||
"!test/helpers/**/*" | ||
], | ||
"require": [ | ||
"babel-register", | ||
"ignore-styles", | ||
"./src/enzyme-setup.js" | ||
], | ||
"babel": "inherit" | ||
}, | ||
"nyc": { | ||
"require": [ | ||
"babel-register" | ||
], | ||
"sourceMap": false, | ||
"instrument": false | ||
} | ||
} |
# react-isomorphic-boilerplate | ||
[![Build Status](https://img.shields.io/travis/ddhp/react-isomorphic-boilerplate/master.svg?style=flat-square)](https://travis-ci.org/ddhp/react-isomorphic-boilerplate) | ||
[![Dependency Status](https://dependencyci.com/github/ddhp/react-isomorphic-boilerplate/badge)](https://dependencyci.com/github/ddhp/react-isomorphic-boilerplate) | ||
This boilerplate would help you build a react/redux/react-router isomorphic/universal web app | ||
## Feature | ||
- isomorphic: same code runs on server and browser | ||
- SEO: information benefits to search engine would be rendered on server side | ||
- easy to start | ||
- production ready | ||
## Concept | ||
### development | ||
Start 3 process to start developing your app: | ||
1. `npm run build:client:dev:w`: build client side code and watch it's change | ||
2. `npm run build:server:dev:w`: build server side conde and watch it's change | ||
3. `npm start`: nodemon executing dist/server.js, only watches on it's change | ||
### Development | ||
0. `yarn` and run 3 processes to start developing your app: | ||
1. `yarn run build:client:dev:w`: build client side code and watch file change. | ||
2. `yarn run build:server:dev:w`: build server side conde and watch file change. | ||
3. `yarn start`: nodemon executing `dist/server.js`, and only watches on it's change, | ||
[--inspect](https://nodejs.org/en/docs/guides/debugging-getting-started/#enable-inspector) param is given, | ||
you can debug nodejs server on chrome-devtools. | ||
All development code are built with [source map](http://blog.teamtreehouse.com/introduction-source-maps). | ||
### Log | ||
import stdout and define namespace ([example](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/server/pages.js)), then turn on debug message depends on platform: | ||
- browser: allow debug log by type `localStorage.debug = '*'` in console | ||
- server: run node with `DEBUG=*`, see `package.json.scripts.start`. | ||
### Packing code | ||
@@ -19,7 +37,22 @@ - Fonts: font face are set in `src/client/global.scss` | ||
### Style | ||
- [global.scss](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/client/global.scss) imports [reset.css](https://www.npmjs.com/package/reset-css) | ||
- [reset.css](https://www.npmjs.com/package/reset-css) reseting default style imported in [global.scss](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/client/global.scss). | ||
### SEO | ||
- Define `loadData` method in your route to prefetch data needed for SEO. ([example](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/routes/main.js)) | ||
- [react-helmet](https://github.com/nfl/react-helmet) help us set head (or specific property) in container and overwrites setting from parent, very handy. | ||
- Define your basic helmet setting in each route file, see [src/routers/main.js](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/routes/main.js), | ||
my idea is - head can be different for different entry of app. | ||
- Overwrites head info in containers. ([example](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/containers/About/index.js)) | ||
### Test | ||
- [AVA](https://github.com/avajs/ava) as test runner. | ||
- Don't use [webpack alias](https://webpack.js.org/configuration/resolve/#resolve-alias) in code base | ||
- We use [mock-require](https://github.com/boblauer/mock-require) to mock dependencies to make test as independent as possible. | ||
As it's name says, it only support `require` not import, so if your importing module has some dependencies needs to be mocked, | ||
remember to `require` instead of import them in your test code. | ||
Also append `.default` to get the right reference if your module is defined in es6 way. (see [server test](https://github.com/ddhp/react-isomorphic-boilerplate/blob/master/src/server/__tests__/index.js) for example) | ||
### Production build | ||
1. `npm run build:client:prod` | ||
2. `npm run build:server:prod` | ||
1. `yarn run build:client:prod` | ||
2. `yarn run build:server:prod` | ||
@@ -31,8 +64,12 @@ ## TODOS: | ||
4. ~font / img loader~ | ||
5. test on server | ||
6. test on react component | ||
7. apply react router | ||
8. apply logic base on path(seo optimized) | ||
5. ~test on server~ | ||
6. ~source map~ | ||
7. ~test on react component~ | ||
7-1. ~coverage report~ | ||
8. ~apply react router~ | ||
9. ~apply logic base on path(seo optimized)~ | ||
10. ~set head info~ | ||
11. ~fetch data from and submit to local api~ | ||
## LICENSE | ||
MIT |
@@ -1,11 +0,11 @@ | ||
import { get as _get } from 'lodash'; | ||
// import { get as _get } from 'lodash'; | ||
import stdout from '../stdout'; | ||
const debug = stdout('action'); | ||
import request from 'superagent'; | ||
export const ACCUMULATE_COUNT = 'ACCUMULATE_COUNT'; | ||
export function accumulateCount() { | ||
return (dispatch, getState) => { | ||
let count = _get(getState(), 'pages.home.count', 0); | ||
count ++; | ||
return (dispatch/*, getState*/) => { | ||
dispatch({ | ||
type: ACCUMULATE_COUNT, | ||
payload: count | ||
}); | ||
@@ -30,1 +30,36 @@ }; | ||
} | ||
export const FETCH_POSTS = 'FETCH_POSTS'; | ||
export function fetchPosts() { | ||
return function (dispatch) { | ||
return request | ||
.get('http://localhost:3333/api/post') | ||
.then((res) => { | ||
dispatch({ | ||
type: FETCH_POSTS, | ||
payload: JSON.parse(res.text) | ||
}); | ||
}, (err) => { | ||
debug(err); | ||
}); | ||
}; | ||
} | ||
export const ADD_POST = 'ADD_POST'; | ||
export function addPost(post) { | ||
return function (dispatch) { | ||
return request | ||
.post('/api/post') | ||
.send(post) | ||
.end((err, res) => { | ||
if (err) { | ||
debug(err); | ||
} else { | ||
dispatch({ | ||
type: ADD_POST, | ||
payload: JSON.parse(res.text) | ||
}); | ||
} | ||
}); | ||
}; | ||
} |
import React from 'react'; | ||
import { hydrate } from 'react-dom'; | ||
import { Provider } from 'react-redux'; | ||
import Home from 'Src/containers/Home'; | ||
import configureStore from 'Src/configureStore'; | ||
import { BrowserRouter as Router, browserHistory } from 'react-router-dom'; | ||
import Routes from '../routes/entry-main'; | ||
import configureStore from '../configureStore'; | ||
import './global.scss'; | ||
@@ -22,5 +23,7 @@ | ||
<Provider store={store}> | ||
<Home /> | ||
<Router history={browserHistory}> | ||
<Routes /> | ||
</Router> | ||
</Provider>, | ||
document.getElementById('app-mount-point') | ||
); |
import React from 'react'; | ||
import { Component } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { Helmet } from 'react-helmet'; | ||
import { connect } from 'react-redux'; | ||
import { get as _get } from 'lodash'; | ||
import { accumulateCount, updateMeID, updateMe } from '../../actions'; | ||
import { Link } from 'react-router-dom'; | ||
import { accumulateCount, updateMeID, updateMe, addPost } from '../../actions'; | ||
import stdout from '../../stdout'; | ||
@@ -11,3 +13,3 @@ const debug = stdout('container/home/index'); | ||
import './style.scss'; | ||
import logoImg from 'Src/assets/images/react-logo.png'; | ||
import logoImg from '../../assets/images/react-logo.png'; | ||
debug(logoImg); | ||
@@ -21,3 +23,5 @@ | ||
updateMe: PropTypes.func, | ||
me: PropTypes.object | ||
addPost: PropTypes.func, | ||
me: PropTypes.object, | ||
posts: PropTypes.array | ||
} | ||
@@ -27,3 +31,6 @@ | ||
super(props); | ||
this.state = {value: ''}; | ||
this.state = { | ||
value: '', | ||
postText: '' | ||
}; | ||
@@ -33,2 +40,4 @@ this.handleChange = this.handleChange.bind(this); | ||
this.onClick = this.onClick.bind(this); | ||
this.onPostTextChanged = this.onPostTextChanged.bind(this); | ||
this.onPostSubmit = this.onPostSubmit.bind(this); | ||
} | ||
@@ -53,2 +62,15 @@ | ||
onPostTextChanged(e) { | ||
this.setState({postText: e.target.value}); | ||
} | ||
onPostSubmit(e) { | ||
e.preventDefault(); | ||
const { postText } = this.state, | ||
payload = { | ||
text: postText | ||
}; | ||
this.props.addPost(payload); | ||
} | ||
componentDidMount() { | ||
@@ -58,19 +80,25 @@ this.props.accumulateCount(); | ||
shouldComponentUpdate(nextProps, nextState) { | ||
const { name: thisName, sex: thisSex } = this.props.me, | ||
{ name: nextName, sex: nextSex } = nextProps.me; | ||
if (thisName !== nextName || | ||
thisSex !== nextSex || | ||
this.state !== nextState) { | ||
return true; | ||
} else { | ||
return false; | ||
} | ||
} | ||
// shouldComponentUpdate(nextProps, nextState) { | ||
// const { name: thisName, sex: thisSex } = this.props.me, | ||
// { name: nextName, sex: nextSex } = nextProps.me; | ||
// if (thisName !== nextName || | ||
// thisSex !== nextSex || | ||
// this.state !== nextState) { | ||
// return true; | ||
// } else { | ||
// return false; | ||
// } | ||
// } | ||
render() { | ||
debug('render method'); | ||
const { name, sex } = this.props.me; | ||
const { name, sex } = this.props.me, | ||
{ posts } = this.props; | ||
return ( | ||
<div className="page--home"> | ||
<Helmet> | ||
<title>Home</title> | ||
<meta name="description" content="home page shows posts" /> | ||
<meta name="og:title" content="home page" /> | ||
</Helmet> | ||
<h1 className="demo--font"> | ||
@@ -80,12 +108,32 @@ Title in Spectral SC | ||
</h1> | ||
<Link to="/about">To About</Link> | ||
<div className="demo--bg"></div> | ||
<img className="demo--img-src" src={logoImg} /> | ||
<ul className="posts"> | ||
{posts.map((p) => { | ||
return ( | ||
<li key={p.id}> | ||
{p.id}, {p.text} | ||
</li> | ||
); | ||
})} | ||
</ul> | ||
<form className="form--post" onSubmit={this.onPostSubmit}> | ||
<label> | ||
TEXT: | ||
<input className="input--post-text" type="text" value={this.state.postText} onChange={this.onPostTextChanged} /> | ||
</label> | ||
<input type="submit" value="Submit" /> | ||
</form> | ||
counter: {this.props.count} | ||
<div>name: {name}</div> | ||
<div>sex: {sex}</div> | ||
<form onSubmit={this.handleSubmit}> | ||
<form className="form--me" onSubmit={this.handleSubmit}> | ||
<label> | ||
ID: | ||
<input type="text" value={this.state.value} onChange={this.handleChange} /> | ||
<input className="input--id" type="text" value={this.state.value} onChange={this.handleChange} /> | ||
</label> | ||
@@ -102,7 +150,13 @@ <input type="submit" value="Submit" /> | ||
const count = _get(state, 'pages.home.count', 0), | ||
me = _get(state, 'entities.me', {}); | ||
entities = _get(state, 'entities'), | ||
me = _get(entities, 'me'), | ||
post = _get(entities, 'post'), | ||
posts = Object.keys(post.byId).map((k) => { | ||
return post.byId[k]; | ||
}); | ||
return { | ||
count, | ||
me | ||
me, | ||
posts | ||
}; | ||
@@ -121,2 +175,5 @@ } | ||
return dispatch(updateMe(me)); | ||
}, | ||
addPost: (post) => { | ||
return dispatch(addPost(post)); | ||
} | ||
@@ -123,0 +180,0 @@ }; |
import { combineReducers } from 'redux'; | ||
import meReducer from './me'; | ||
import postReducer from './post'; | ||
export default combineReducers({ | ||
me: meReducer | ||
me: meReducer, | ||
post: postReducer | ||
}); |
@@ -6,7 +6,7 @@ const initialState = { | ||
export default function homeReducer(state = initialState, action) { | ||
const payload = action.payload; | ||
// const payload = action.payload; | ||
switch (action.type) { | ||
case 'ACCUMULATE_COUNT': { | ||
return Object.assign({}, state, { | ||
count: payload | ||
count: ++ state.count | ||
}); | ||
@@ -16,3 +16,3 @@ // state.count = payload | ||
} | ||
default: | ||
@@ -19,0 +19,0 @@ return state; |
@@ -1,11 +0,7 @@ | ||
import React from 'react'; | ||
import ReactServer from 'react-dom/server'; | ||
import Express from 'express'; | ||
import { Provider } from 'react-redux'; | ||
import Layout from './layout'; | ||
import configureStore from 'Src/configureStore'; | ||
import Home from 'Src/containers/home'; | ||
import stdout from 'Src/stdout'; | ||
import stdout from '../stdout'; | ||
import pagesMiddleware from './pages'; | ||
import apiMiddleware from './api'; | ||
const debug = stdout('server-app'); | ||
const debug = stdout('app-server'); | ||
const app = Express(), | ||
@@ -17,22 +13,5 @@ port = 3333; | ||
app.use('/', (req, res) => { | ||
const store = configureStore(), | ||
reduxState = JSON.stringify(store.getState()).replace(/</g, '\\u003c'); | ||
apiMiddleware(app); | ||
pagesMiddleware(app); | ||
const content = ( | ||
<Provider store={store}> | ||
<Home /> | ||
</Provider> | ||
); | ||
const htmlString = ReactServer.renderToString( | ||
<Layout | ||
content={content} | ||
reduxState={reduxState} | ||
/> | ||
); | ||
res.send(`<!DOCTYPE HTML>${htmlString}`); | ||
}); | ||
const server = app.listen(port, function () { | ||
@@ -44,1 +23,3 @@ const host = server.address().address, | ||
}); | ||
export default app; |
@@ -10,7 +10,8 @@ import React from 'react'; | ||
content: PropTypes.object, | ||
reduxState: PropTypes.string | ||
reduxState: PropTypes.string, | ||
head: PropTypes.object | ||
} | ||
render() { | ||
const { content, reduxState } = this.props; | ||
const { content, reduxState, head } = this.props; | ||
return ( | ||
@@ -22,7 +23,12 @@ <html> | ||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> | ||
<link href={assetsJSON.client.css} rel="stylesheet" /> | ||
{head.title.toComponent()} | ||
{head.meta.toComponent()} | ||
<link rel="icon" type="image/x-icon" href="/assets/images/favicon.ico" /> | ||
<link rel="apple-touch-icon" href="/assets/images/icon.png" /> | ||
<link href={assetsJSON.main.css} rel="stylesheet" /> | ||
<script type="text/javascript" charSet="utf-8" dangerouslySetInnerHTML={{__html: ` | ||
window.__REDUX_STATE__ = ${reduxState} | ||
`}} /> | ||
`}} /> | ||
</head> | ||
@@ -34,3 +40,3 @@ <body> | ||
{/* entry script generated by webpack*/} | ||
<script src={assetsJSON.client.js} type="text/javascript" charSet="utf-8"></script> | ||
<script src={assetsJSON.main.js} type="text/javascript" charSet="utf-8"></script> | ||
</body> | ||
@@ -37,0 +43,0 @@ </html> |
@@ -18,3 +18,3 @@ const path = require('path'); | ||
entry: { | ||
client: path.resolve(__dirname, 'src/client/entry-main') | ||
main: path.resolve(__dirname, 'src/entries/main') | ||
}, | ||
@@ -46,6 +46,3 @@ output: { | ||
use: [{ | ||
loader: 'babel-loader', | ||
options: { | ||
presets: ['react', 'es2015', 'stage-2'] | ||
} | ||
loader: 'babel-loader' | ||
}, { | ||
@@ -95,3 +92,4 @@ loader: 'eslint-loader', | ||
] | ||
} | ||
}, | ||
devtool: 'cheap-module-eval-source-map' | ||
}; |
const webpack = require('webpack'); | ||
const path = require('path'); | ||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin') | ||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); | ||
const AssetPlugin = require('assets-webpack-plugin'); | ||
@@ -29,6 +28,3 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin'); | ||
use: [{ | ||
loader: 'babel-loader', | ||
options: { | ||
presets: ['react', 'es2015', 'stage-2'] | ||
} | ||
loader: 'babel-loader' | ||
}, { | ||
@@ -75,3 +71,3 @@ loader: 'remove-debug-loader', | ||
} | ||
} | ||
}] | ||
} | ||
@@ -78,0 +74,0 @@ ] |
const webpack = require('webpack'); | ||
const path = require('path'); | ||
const devConfig = require('./webpack.dev.js'); | ||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin') | ||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); | ||
const ExtractTextPlugin = require('extract-text-webpack-plugin'); | ||
@@ -52,2 +52,4 @@ const nodeExternals = require('webpack-node-externals'); | ||
); | ||
} else { | ||
config.devtool = 'cheap-module-eval-source-map'; | ||
} | ||
@@ -54,0 +56,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
446791
45
951
74
4
48
+ Addedreact-helmet@^5.2.0
+ Addedreact-fast-compare@2.0.4(transitive)
+ Addedreact-helmet@5.2.1(transitive)
+ Addedreact-side-effect@1.2.0(transitive)
+ Addedshallowequal@1.1.0(transitive)