From eae1788296d909b09d4ca56fd4732b0ccffaf4c6 Mon Sep 17 00:00:00 2001 From: okendoken Date: Sun, 31 Jan 2016 21:23:02 +0300 Subject: [PATCH 1/2] Use react-router; client- and server side rendering --- package.json | 2 +- src/client.js | 84 +++++++++++++------------ src/components/App/App.js | 27 +------- src/components/Footer/Footer.js | 2 +- src/components/Header/Header.js | 6 +- src/components/Link/Link.js | 63 ------------------- src/components/Link/package.json | 6 -- src/components/Navigation/Navigation.js | 2 +- src/core/ContextHolder.js | 38 +++++++++++ src/core/Location.js | 6 +- src/routes.js | 51 +++++++-------- src/server.js | 46 ++++++++------ tools/webpack.config.js | 5 +- 13 files changed, 142 insertions(+), 196 deletions(-) delete mode 100644 src/components/Link/Link.js delete mode 100644 src/components/Link/package.json create mode 100644 src/core/ContextHolder.js diff --git a/package.json b/package.json index b01c90e64..0a85997c3 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "pretty-error": "2.0.0", "react": "0.14.7", "react-dom": "0.14.7", - "react-routing": "0.0.7", + "react-router": "2.0.0-rc5", "source-map-support": "0.4.0", "whatwg-fetch": "0.11.0" }, diff --git a/src/client.js b/src/client.js index aaaefb6a1..afa90b5f4 100644 --- a/src/client.js +++ b/src/client.js @@ -8,10 +8,13 @@ */ import 'babel-polyfill'; -import ReactDOM from 'react-dom'; +import React from 'react'; +import { match, Router } from 'react-router'; +import { render } from 'react-dom'; import FastClick from 'fastclick'; -import Router from './routes'; +import routes from './routes'; import Location from './core/Location'; +import ContextHolder from './core/ContextHolder'; import { addEventListener, removeEventListener } from './core/DOMUtils'; let cssContainer = document.getElementById('css'); @@ -41,59 +44,58 @@ const context = { // rendering, as it was already sent by the Html component. let trackPageview = () => (trackPageview = () => window.ga('send', 'pageview')); -function render(state) { - Router.dispatch(state, (newState, component) => { - ReactDOM.render(component, appContainer, () => { - // Restore the scroll position if it was saved into the state - if (state.scrollY !== undefined) { - window.scrollTo(state.scrollX, state.scrollY); - } else { - window.scrollTo(0, 0); - } - - trackPageview(); - - // Remove the pre-rendered CSS because it's no longer used - // after the React app is launched - if (cssContainer) { - cssContainer.parentNode.removeChild(cssContainer); - cssContainer = null; - } - }); - }); -} - function run() { - let currentLocation = null; - let currentState = null; + const scrollOffsets = new Map(); + let currentScrollOffset = null; // Make taps on links and buttons work fast on mobiles FastClick.attach(document.body); - // Re-render the app when window.location changes const unlisten = Location.listen(location => { - currentLocation = location; - currentState = Object.assign({}, location.state, { - path: location.pathname, - query: location.query, - state: location.state, - context, - }); - render(currentState); + const locationId = location.pathname + location.search; + if (!scrollOffsets.get(locationId)) { + scrollOffsets.set(locationId, Object.create(null)); + } + currentScrollOffset = scrollOffsets.get(locationId); + // Restore the scroll position if it was saved + if (currentScrollOffset.scrollY !== undefined) { + window.scrollTo(currentScrollOffset.scrollX, currentScrollOffset.scrollY); + } else { + window.scrollTo(0, 0); + } + + trackPageview(); + }); + + const { pathname, search, hash } = window.location; + const location = `${pathname}${search}${hash}`; + + match({ routes, location }, (error, redirectLocation, renderProps) => { + render( + + + , + appContainer + ); + // Remove the pre-rendered CSS because it's no longer used + // after the React app is launched + if (cssContainer) { + cssContainer.parentNode.removeChild(cssContainer); + cssContainer = null; + } }); - // Save the page scroll position into the current location's state + // Save the page scroll position const supportPageOffset = window.pageXOffset !== undefined; const isCSS1Compat = ((document.compatMode || '') === 'CSS1Compat'); const setPageOffset = () => { - currentLocation.state = currentLocation.state || Object.create(null); if (supportPageOffset) { - currentLocation.state.scrollX = window.pageXOffset; - currentLocation.state.scrollY = window.pageYOffset; + currentScrollOffset.scrollX = window.pageXOffset; + currentScrollOffset.scrollY = window.pageYOffset; } else { - currentLocation.state.scrollX = isCSS1Compat ? + currentScrollOffset.scrollX = isCSS1Compat ? document.documentElement.scrollLeft : document.body.scrollLeft; - currentLocation.state.scrollY = isCSS1Compat ? + currentScrollOffset.scrollY = isCSS1Compat ? document.documentElement.scrollTop : document.body.scrollTop; } }; diff --git a/src/components/App/App.js b/src/components/App/App.js index 82fb058ba..da836759c 100644 --- a/src/components/App/App.js +++ b/src/components/App/App.js @@ -8,7 +8,6 @@ */ import React, { Component, PropTypes } from 'react'; -import emptyFunction from 'fbjs/lib/emptyFunction'; import s from './App.scss'; import Header from '../Header'; import Feedback from '../Feedback'; @@ -17,36 +16,16 @@ import Footer from '../Footer'; class App extends Component { static propTypes = { - context: PropTypes.shape({ - insertCss: PropTypes.func, - onSetTitle: PropTypes.func, - onSetMeta: PropTypes.func, - onPageNotFound: PropTypes.func, - }), children: PropTypes.element.isRequired, error: PropTypes.object, }; - static childContextTypes = { - insertCss: PropTypes.func.isRequired, - onSetTitle: PropTypes.func.isRequired, - onSetMeta: PropTypes.func.isRequired, - onPageNotFound: PropTypes.func.isRequired, + static contextTypes = { + insertCss: PropTypes.func, }; - getChildContext() { - const context = this.props.context; - return { - insertCss: context.insertCss || emptyFunction, - onSetTitle: context.onSetTitle || emptyFunction, - onSetMeta: context.onSetMeta || emptyFunction, - onPageNotFound: context.onPageNotFound || emptyFunction, - }; - } - componentWillMount() { - const { insertCss } = this.props.context; - this.removeCss = insertCss(s); + this.removeCss = this.context.insertCss(s); } componentWillUnmount() { diff --git a/src/components/Footer/Footer.js b/src/components/Footer/Footer.js index ab9e35366..badd2245b 100644 --- a/src/components/Footer/Footer.js +++ b/src/components/Footer/Footer.js @@ -10,7 +10,7 @@ import React, { Component } from 'react'; import withStyles from 'isomorphic-style-loader/lib/withStyles'; import s from './Footer.scss'; -import Link from '../Link'; +import { Link } from 'react-router'; class Footer extends Component { diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index 0a44156d5..a3e248a4b 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -10,7 +10,7 @@ import React, { Component } from 'react'; import withStyles from 'isomorphic-style-loader/lib/withStyles'; import s from './Header.scss'; -import Link from '../Link'; +import { IndexLink } from 'react-router'; import Navigation from '../Navigation'; class Header extends Component { @@ -20,10 +20,10 @@ class Header extends Component {
- + React Your Company - +

React

Complex web apps made easy

diff --git a/src/components/Link/Link.js b/src/components/Link/Link.js deleted file mode 100644 index 9bb8ee4df..000000000 --- a/src/components/Link/Link.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * React Starter Kit (https://www.reactstarterkit.com/) - * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE.txt file in the root directory of this source tree. - */ - -import React, { Component, PropTypes } from 'react'; -import Location from '../../core/Location'; - -function isLeftClickEvent(event) { - return event.button === 0; -} - -function isModifiedEvent(event) { - return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); -} - -class Link extends Component { - - static propTypes = { - to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, - onClick: PropTypes.func, - }; - - handleClick = (event) => { - let allowTransition = true; - let clickResult; - - if (this.props && this.props.onClick) { - clickResult = this.props.onClick(event); - } - - if (isModifiedEvent(event) || !isLeftClickEvent(event)) { - return; - } - - if (clickResult === false || event.defaultPrevented === true) { - allowTransition = false; - } - - event.preventDefault(); - - if (allowTransition) { - const link = event.currentTarget; - if (this.props && this.props.to) { - Location.push(this.props.to); - } else { - Location.push({ pathname: link.pathname, search: link.search }); - } - } - }; - - render() { - const { to, ...props } = this.props; // eslint-disable-line no-use-before-define - return ; - } - -} - -export default Link; diff --git a/src/components/Link/package.json b/src/components/Link/package.json deleted file mode 100644 index 7feb9cce0..000000000 --- a/src/components/Link/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Link", - "version": "0.0.0", - "private": true, - "main": "./Link.js" -} diff --git a/src/components/Navigation/Navigation.js b/src/components/Navigation/Navigation.js index 8de90b875..a20faf158 100644 --- a/src/components/Navigation/Navigation.js +++ b/src/components/Navigation/Navigation.js @@ -11,7 +11,7 @@ import React, { Component, PropTypes } from 'react'; import cx from 'classnames'; import withStyles from 'isomorphic-style-loader/lib/withStyles'; import s from './Navigation.scss'; -import Link from '../Link'; +import { Link } from 'react-router'; class Navigation extends Component { diff --git a/src/core/ContextHolder.js b/src/core/ContextHolder.js new file mode 100644 index 000000000..69a06e2a8 --- /dev/null +++ b/src/core/ContextHolder.js @@ -0,0 +1,38 @@ +import React, { PropTypes } from 'react'; +import emptyFunction from 'fbjs/lib/emptyFunction'; + +class ContextHolder extends React.Component { + + static propTypes = { + context: PropTypes.shape({ + insertCss: PropTypes.func, + onSetMeta: PropTypes.func, + onPageNotFound: PropTypes.func, + }), + children: PropTypes.element.isRequired, + }; + + static childContextTypes = { + insertCss: PropTypes.func, + onSetTitle: PropTypes.func, + onSetMeta: PropTypes.func, + onPageNotFound: PropTypes.func, + }; + + getChildContext() { + const context = this.props.context; + return { + insertCss: context.insertCss || emptyFunction, + onSetTitle: context.onSetTitle || emptyFunction, + onSetMeta: context.onSetMeta || emptyFunction, + onPageNotFound: context.onPageNotFound || emptyFunction, + }; + } + + render() { + const { children } = this.props; + return React.Children.only(children); + } +} + +export default ContextHolder; diff --git a/src/core/Location.js b/src/core/Location.js index 53ad8fb1f..330250ac2 100644 --- a/src/core/Location.js +++ b/src/core/Location.js @@ -7,10 +7,8 @@ * LICENSE.txt file in the root directory of this source tree. */ -import createHistory from 'history/lib/createBrowserHistory'; -import createMemoryHistory from 'history/lib/createMemoryHistory'; -import useQueries from 'history/lib/useQueries'; +import { browserHistory, createMemoryHistory } from 'react-router'; -const location = useQueries(process.env.BROWSER ? createHistory : createMemoryHistory)(); +const location = process.env.BROWSER ? browserHistory : createMemoryHistory(); export default location; diff --git a/src/routes.js b/src/routes.js index da62d7197..6f89c666d 100644 --- a/src/routes.js +++ b/src/routes.js @@ -8,7 +8,7 @@ */ import React from 'react'; -import Router from 'react-routing/src/Router'; +import { IndexRoute, Route } from 'react-router'; import fetch from './core/fetch'; import App from './components/App'; import ContentPage from './components/ContentPage'; @@ -16,31 +16,26 @@ import ContactPage from './components/ContactPage'; import LoginPage from './components/LoginPage'; import RegisterPage from './components/RegisterPage'; import NotFoundPage from './components/NotFoundPage'; -import ErrorPage from './components/ErrorPage'; -const router = new Router(on => { - on('*', async (state, next) => { - const component = await next(); - return component && {component}; - }); - - on('/contact', async () => ); - - on('/login', async () => ); - - on('/register', async () => ); - - on('*', async (state) => { - const query = `/graphql?query={content(path:"${state.path}"){path,title,content,component}}`; - const response = await fetch(query); - const { data } = await response.json(); - return data && data.content && ; - }); - - on('error', (state, error) => state.statusCode === 404 ? - : - - ); -}); - -export default router; +async function getContextComponent(location, callback) { + const query = '/graphql?' + + `query={content(path:"${location.pathname}"){path,title,content,component}}`; + const response = await fetch(query); + const { data } = await response.json(); + // using an arrow to pass page instance instead of page class; cb accepts class by default + callback(null, () => ); +} + +export default ( + + + + + + + + + + + +); diff --git a/src/server.js b/src/server.js index cf530d356..d2954fddc 100644 --- a/src/server.js +++ b/src/server.js @@ -16,10 +16,13 @@ import expressJwt from 'express-jwt'; import expressGraphQL from 'express-graphql'; import jwt from 'jsonwebtoken'; import ReactDOM from 'react-dom/server'; +import React from 'react'; +import { match, RouterContext } from 'react-router'; import PrettyError from 'pretty-error'; import passport from './core/passport'; import schema from './data/schema'; -import Router from './routes'; +import routes from './routes'; +import ContextHolder from './core/ContextHolder'; import assets from './assets'; import { port, auth, analytics } from './config'; @@ -80,29 +83,32 @@ server.use('/graphql', expressGraphQL(req => ({ // ----------------------------------------------------------------------------- server.get('*', async (req, res, next) => { try { - let statusCode = 200; - const template = require('./views/index.jade'); - const data = { title: '', description: '', css: '', body: '', entry: assets.main.js }; + match({ routes, location: req.url }, (error, redirectLocation, renderProps) => { + let statusCode = 200; + const template = require('./views/index.jade'); + const data = { title: '', description: '', css: '', body: '', entry: assets.main.js }; - if (process.env.NODE_ENV === 'production') { - data.trackingId = analytics.google.trackingId; - } + if (process.env.NODE_ENV === 'production') { + data.trackingId = analytics.google.trackingId; + } - const css = []; - const context = { - insertCss: styles => css.push(styles._getCss()), - onSetTitle: value => (data.title = value), - onSetMeta: (key, value) => (data[key] = value), - onPageNotFound: () => (statusCode = 404), - }; - - await Router.dispatch({ path: req.path, query: req.query, context }, (state, component) => { - data.body = ReactDOM.renderToString(component); + const css = []; + const context = { + insertCss: styles => css.push(styles._getCss()), + onSetTitle: value => (data.title = value), + onSetMeta: (key, value) => (data[key] = value), + onPageNotFound: () => (statusCode = 404), + }; + data.body = ReactDOM.renderToString( + + + + ); data.css = css.join(''); - }); - res.status(statusCode); - res.send(template(data)); + res.status(statusCode); + res.send(template(data)); + }); } catch (err) { next(err); } diff --git a/tools/webpack.config.js b/tools/webpack.config.js index abda59111..8092e6bb8 100644 --- a/tools/webpack.config.js +++ b/tools/webpack.config.js @@ -68,7 +68,6 @@ const config = { { test: /\.jsx?$/, include: [ - path.resolve(__dirname, '../node_modules/react-routing/src'), path.resolve(__dirname, '../src'), ], loader: 'babel-loader', @@ -162,9 +161,7 @@ const serverConfig = extend(true, {}, config, { /^\.\/assets$/, function filter(context, request, cb) { const isExternal = - request.match(/^[@a-z][a-z\/\.\-0-9]*$/i) && - !request.match(/^react-routing/) && - !context.match(/[\\/]react-routing/); + request.match(/^[@a-z][a-z\/\.\-0-9]*$/i); cb(null, Boolean(isExternal)); }, ], From e1ea6840de2dc5e2b2b15e0ad6f04d2003f74b2b Mon Sep 17 00:00:00 2001 From: goldensunliu Date: Wed, 3 Feb 2016 14:02:56 -0500 Subject: [PATCH 2/2] add react router redirect and error handling --- src/server.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/server.js b/src/server.js index d2954fddc..8c73907db 100644 --- a/src/server.js +++ b/src/server.js @@ -84,6 +84,14 @@ server.use('/graphql', expressGraphQL(req => ({ server.get('*', async (req, res, next) => { try { match({ routes, location: req.url }, (error, redirectLocation, renderProps) => { + if (error) { + throw error; + } + if (redirectLocation) { + const redirectPath = `${redirectLocation.pathname}${redirectLocation.search}`; + res.redirect(302, redirectPath); + return; + } let statusCode = 200; const template = require('./views/index.jade'); const data = { title: '', description: '', css: '', body: '', entry: assets.main.js };