Commit d257f1cb78aa333ab6399429df9d754ce4f561f5
universal app (server-side rendering)!
- use redux-simple-router instead of redux-router - no more budo, just watchify and our static server (so no more live-reload for now) - use helpers from https://github.com/jlongster/react-redux-universal-hot-example/ - add server-side renderer to stackMichael Williams committed on 11/29/2015, 4:12:35 AM
Parent: 4cb19feb19f09ec24e9ab9ee78c7df9337ce2f68
Files changed
app/client.js | changed |
app/reducers/index.js | changed |
app/stack/index.js | changed |
app/stack/render.js | added |
app/store.js | changed |
app/assets/index.html | deleted |
app/util/fetch-all-data.js | added |
app/util/fetch-element.js | added |
package.json | changed |
app/client.js | ||
---|---|---|
@@ -1,19 +1,38 @@ | ||
1 | 1 | const React = require('react') |
2 | 2 | const { render } = require('react-dom') |
3 | -const { createStore, applyMiddleware } = require('redux') | |
4 | -import { reduxReactRouter, routerStateReducer, ReduxRouter } from 'redux-router'; | |
5 | 3 | const { Provider } = require('react-redux') |
6 | -const thunk = require('redux-thunk') | |
4 | +const { Router } = require('react-router') | |
5 | +const { createHistory } = require('history') | |
6 | +const { syncReduxAndRouter } = require('redux-simple-router') | |
7 | 7 | |
8 | -const configureStore = require('app/store') | |
9 | -const { findTodos } = require('app/actions') | |
10 | -const Root = require('app/containers/root') | |
8 | +const routes = require('app/routes') | |
9 | +const createStore = require('app/store') | |
10 | +const fetchElement = require('app/util/fetch-element') | |
11 | 11 | |
12 | -const store = configureStore() | |
12 | +if (process.env.NODE_ENV === 'development') { | |
13 | + var DevTools = require('app/components/dev-tools') | |
14 | +} | |
13 | 15 | |
14 | -//store.dispatch(getAllTodos()) | |
16 | +const store = createStore(window.__data) | |
17 | +const history = createHistory() | |
15 | 18 | |
19 | +syncReduxAndRouter(history, store) | |
20 | + | |
21 | +const component = ( | |
22 | + <Router createElement={fetchElement} history={history}> | |
23 | + { routes } | |
24 | + </Router> | |
25 | +) | |
26 | + | |
16 | 27 | render( |
17 | - <Root store={store} />, | |
28 | + <Provider store={store} key="provider"> | |
29 | + <div> | |
30 | + { component } | |
31 | + { | |
32 | + (process.env.NODE_ENV === 'development') ? | |
33 | + <DevTools /> : null | |
34 | + } | |
35 | + </div> | |
36 | + </Provider>, | |
18 | 37 | document.querySelector('main') |
19 | 38 | ) |
app/reducers/index.js | ||
---|---|---|
@@ -1,8 +1,8 @@ | ||
1 | 1 | const bulk = require('bulk-require') |
2 | 2 | const { combineReducers } = require('redux') |
3 | -const { routerStateReducer } = require('redux-router') | |
3 | +const { routeReducer } = require('redux-simple-router') | |
4 | 4 | |
5 | 5 | module.exports = combineReducers({ |
6 | 6 | ...bulk(__dirname, '!(index.js)'), |
7 | - router: routerStateReducer | |
7 | + routing: routeReducer | |
8 | 8 | }) |
app/stack/index.js | ||
---|---|---|
@@ -2,9 +2,10 @@ | ||
2 | 2 | const { mapObjIndexed, reduce, toPairs } = require('ramda') |
3 | 3 | |
4 | 4 | const stackCreators = { |
5 | 5 | services: require('./services'), |
6 | - static: require('./static') | |
6 | + static: require('./static'), | |
7 | + render: require('./render') | |
7 | 8 | } |
8 | 9 | |
9 | 10 | module.exports = createStack |
10 | 11 |
app/stack/render.js | ||
---|---|---|
@@ -1,0 +1,69 @@ | ||
1 | +// https://github.com/jlongster/react-redux-universal-hot-example/blob/master/src/server.js | |
2 | + | |
3 | +const React = require('react') | |
4 | +const { renderToString } = require('react-dom/server') | |
5 | +const { Provider } = require('react-redux') | |
6 | +const { createHistory } = require('history') | |
7 | +const { Router, RoutingContext, match } = require('react-router') | |
8 | + | |
9 | +const createStore = require('app/store') | |
10 | +const routes = require('app/routes') | |
11 | +const fetchAllData = require('app/util/fetch-all-data') | |
12 | + | |
13 | +module.exports = createRender | |
14 | + | |
15 | +function createRender (config) { | |
16 | + return function render (req, res) { | |
17 | + const store = createStore() | |
18 | + | |
19 | + match({ | |
20 | + routes: routes, | |
21 | + location: req.path | |
22 | + }, function (err, redirectLocation, renderProps) { | |
23 | + if (redirectLocation) { | |
24 | + res.redirect(redirectLocation.pathname + redirectLocation.search) | |
25 | + } else if (err) { | |
26 | + res.status(500).send(err.message) | |
27 | + } else if (!renderProps) { | |
28 | + res.status(404).send('Not found') | |
29 | + } else { | |
30 | + fetchAllData( | |
31 | + renderProps.components, | |
32 | + store.getState, store.dispatch, | |
33 | + renderProps.location, | |
34 | + renderProps.params | |
35 | + ).then(function () { | |
36 | + const component = <Provider store={store} key="provider"> | |
37 | + <RoutingContext { ...renderProps } /> | |
38 | + </Provider> | |
39 | + | |
40 | + const html = renderToString(component) | |
41 | + | |
42 | + const fullHtml = renderFullPage(html, store.getState()) | |
43 | + | |
44 | + res.send(fullHtml) | |
45 | + }) | |
46 | + } | |
47 | + }) | |
48 | + } | |
49 | +} | |
50 | + | |
51 | +function renderFullPage (html, data) { | |
52 | + return ` | |
53 | + | |
54 | + <html lang="en"> | |
55 | + <head> | |
56 | + <meta charset="utf-8" /> | |
57 | + <title>Production TodoMVC</title> | |
58 | + <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
59 | + </head> | |
60 | + <body> | |
61 | + <main>${ html }</main> | |
62 | + <script> | |
63 | + window.__data = ${ JSON.stringify(data) } | |
64 | + </script> | |
65 | + <script src="bundle.js"></script> | |
66 | + </body> | |
67 | + </html> | |
68 | + ` | |
69 | +} |
app/store.js | ||
---|---|---|
@@ -1,11 +1,9 @@ | ||
1 | 1 | const { createStore, compose, applyMiddleware } = require('redux') |
2 | 2 | const thunk = require('redux-thunk') |
3 | -const { reduxReactRouter } = require('redux-router') | |
4 | 3 | const { createHistory } = require('history') |
5 | 4 | |
6 | 5 | const reducer = require('app/reducers') |
7 | -const routes = require('routes') | |
8 | 6 | |
9 | 7 | let storeEnhancers = [] |
10 | 8 | let middleware = [] |
11 | 9 | |
@@ -21,44 +19,28 @@ | ||
21 | 19 | storeEnhancers.push( |
22 | 20 | applyMiddleware(...middleware) |
23 | 21 | ) |
24 | 22 | |
25 | -storeEnhancers.push( | |
26 | - reduxReactRouter({ | |
27 | - //routes, | |
28 | - createHistory | |
29 | - }) | |
30 | -) | |
31 | - | |
32 | 23 | if (process.env.NODE_ENV === 'development') { |
33 | - | |
34 | 24 | storeEnhancers.push( |
35 | 25 | applyMiddleware(logger()) |
36 | 26 | ) |
37 | 27 | storeEnhancers.push(DevTools.instrument()) |
38 | - storeEnhancers.push(persistState( | |
39 | - window.location.href.match( | |
40 | - /[?&]debug_session=([^&]+)\b/ | |
41 | - ) | |
42 | - )) | |
28 | + | |
29 | + if (module.browser) { | |
30 | + storeEnhancers.push(persistState( | |
31 | + window.location.href.match( | |
32 | + /[?&]debug_session=([^&]+)\b/ | |
33 | + ) | |
34 | + )) | |
35 | + } | |
43 | 36 | } |
44 | 37 | |
45 | 38 | const createEnhancedStore = compose( |
46 | 39 | ...storeEnhancers |
47 | 40 | )(createStore) |
48 | 41 | |
49 | -function configureStore(initialState) { | |
50 | - const store = createEnhancedStore(reducer, initialState) | |
51 | - | |
52 | - if (process.env.NODE_ENV === 'development') { | |
53 | - if (module.hot) { | |
54 | - module.hot.accept('app/reducers', () => | |
55 | - store.replaceReducer(require('app/reducers')) | |
56 | - ) | |
57 | - } | |
58 | - } | |
59 | - | |
60 | - return store | |
42 | +function finalCreateStore(initialState) { | |
43 | + return createEnhancedStore(reducer, initialState) | |
61 | 44 | } |
62 | 45 | |
63 | -module.exports = configureStore | |
64 | - | |
46 | +module.exports = finalCreateStore |
app/assets/index.html | ||
---|---|---|
@@ -1,12 +1,0 @@ | ||
1 | - | |
2 | -<html lang="en"> | |
3 | - <head> | |
4 | - <meta charset="utf-8" /> | |
5 | - <title>Production TodoMVC</title> | |
6 | - <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
7 | - </head> | |
8 | - <body> | |
9 | - <main></main> | |
10 | - <script src="bundle.js"></script> | |
11 | - </body> | |
12 | -</html> |
app/util/fetch-all-data.js | ||
---|---|---|
@@ -1,0 +1,17 @@ | ||
1 | +// https://github.com/jlongster/react-redux-universal-hot-example/blob/master/src/helpers/fetchAllData.js | |
2 | + | |
3 | +const Promise = require('pinkie-promise') | |
4 | + | |
5 | +module.exports = fetchAllData | |
6 | + | |
7 | +function fetchAllData(components, getState, dispatch, location, params) { | |
8 | + const fetchers = components | |
9 | + .filter((component) => !!component) // Weed out 'undefined' routes | |
10 | + .filter((component) => component.fetchData) // only look at ones with a static fetchData() | |
11 | + .map((component) => component.fetchData) // pull out fetch data methods | |
12 | + .map(fetchData => { | |
13 | + return fetchData(getState, dispatch, location, params) | |
14 | + }) // call fetch data methods and return promises | |
15 | + | |
16 | + return Promise.all(fetchers) | |
17 | +} |
app/util/fetch-element.js | ||
---|---|---|
@@ -1,0 +1,14 @@ | ||
1 | +const React = require('react') | |
2 | + | |
3 | +module.exports = fetchElement | |
4 | + | |
5 | +function fetchElement (Component, props) { | |
6 | + if (Component.fetchData) { | |
7 | + Component.fetchData( | |
8 | + store.getState, store.dispatch, | |
9 | + props.location, props.params | |
10 | + ) | |
11 | + } | |
12 | + return React.createElement(Component, props) | |
13 | +} | |
14 | + |
package.json | ||
---|---|---|
@@ -9,11 +9,11 @@ | ||
9 | 9 | "format": "snazzy --format", |
10 | 10 | "test": "(npm run spec & npm run feature)", |
11 | 11 | "spec": "node spec", |
12 | 12 | "feature": "node feature", |
13 | - "dev:client": "budo client --dir assets --serve bundle.js --live --pushstate -- -dv", | |
13 | + "dev:client": "watchify client -o assets/bundle.js -dv", | |
14 | 14 | "dev:assets": "cpx \"app/assets/**/*\" assets -w", |
15 | - "dev:server": "nodemon server", | |
15 | + "dev:server": "node-dev server", | |
16 | 16 | "prod:client": "browserify client -o assets/bundle.js -g envify -g uglifyify", |
17 | 17 | "prod:assets": "cpx \"app/assets/**/*\" assets", |
18 | 18 | "prod:server": "node server", |
19 | 19 | "dev": "NODE_ENV=development npm-run-all -p dev:*", |
@@ -46,17 +46,17 @@ | ||
46 | 46 | "node": "^4.0.0", |
47 | 47 | "npm": "^3.0.0" |
48 | 48 | }, |
49 | 49 | "devDependencies": { |
50 | - "budo": "^6.1.0", | |
51 | 50 | "cuke-tap": "^1.0.2", |
52 | 51 | "jsdom": "^7.1.0", |
53 | - "nodemon": "^1.8.1", | |
52 | + "node-dev": "^2.7.1", | |
54 | 53 | "redux-devtools": "^3.0.0-beta-3", |
55 | 54 | "redux-devtools-dock-monitor": "^1.0.0-beta-3", |
56 | 55 | "redux-devtools-log-monitor": "^1.0.0-beta-3", |
57 | 56 | "redux-logger": "^2.0.4", |
58 | - "tape": "^4.2.2" | |
57 | + "tape": "^4.2.2", | |
58 | + "watchify": "^3.6.1" | |
59 | 59 | }, |
60 | 60 | "dependencies": { |
61 | 61 | "babel-core": "^6.2.1", |
62 | 62 | "babel-plugin-transform-object-rest-spread": "^6.1.18", |
@@ -74,15 +74,16 @@ | ||
74 | 74 | "feathers": "^1.2.0", |
75 | 75 | "history": "^1.13.1", |
76 | 76 | "lnfs-cli": "^1.0.1", |
77 | 77 | "npm-run-all": "^1.3.2", |
78 | + "pinkie-promise": "^2.0.0", | |
78 | 79 | "ramda": "^0.18.0", |
79 | 80 | "react": "^0.14.3", |
80 | 81 | "react-dom": "^0.14.3", |
81 | 82 | "react-redux": "^4.0.0", |
82 | 83 | "react-router": "^1.0.0", |
83 | 84 | "redux": "^3.0.4", |
84 | - "redux-router": "^1.0.0-beta5", | |
85 | + "redux-simple-router": "0.0.10", | |
85 | 86 | "redux-thunk": "^1.0.0", |
86 | 87 | "sheetify": "^3.1.0", |
87 | 88 | "uglifyify": "^3.0.1" |
88 | 89 | } |
Built with git-ssb-web