Created
February 12, 2018 16:34
-
-
Save Superilya/f182963b4bb6b30cb1d1a96ca205e922 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// load-route-data.js | |
import { matchPath } from 'react-router-dom'; | |
export default (dispatch, pathname, routes, query) => { | |
const promises = []; | |
routes.some(route => { | |
const match = matchPath(pathname, route); | |
if (match && match.isExact && route.loadData) { | |
promises.push(route.loadData({ | |
dispatch, | |
match, | |
query | |
})); | |
} | |
return match && match.isExact; | |
}); | |
return Promise.all(promises); | |
} | |
// page.js (container) | |
import React, { Component, Fragment } from 'react'; | |
import { connect } from 'react-redux'; | |
import { bindActionCreators } from 'redux'; | |
import Meta from 'src/components/meta'; | |
import ArticleView from 'src/components/article'; | |
import { fetchArticle } from 'src/actions'; | |
export const loadData = ({ dispatch, match, query }) => new Promise(res => { | |
const ta = bindActionCreators({ fetchArticle }, dispatch); | |
const cb = article => { | |
if (article) { | |
res(prepareMeta(article)); | |
return; | |
} | |
res(); | |
}; | |
ta.fetchArticle({ cb, alias: match.params.alias, hidden: query.hidden }); | |
}); | |
const prepareMeta = ({ header, preview, image }) => ({ | |
title: header, | |
description: preview, | |
image: image | |
}); | |
class ArticleContainer extends Component { | |
render() { | |
const {page, article} = this.props; | |
const content = article ? article.content : null; | |
const header = article ? article.header : null; | |
return ( | |
<Fragment> | |
{article && <Meta {...prepareMeta(article)} />} | |
<ArticleView | |
isLoading={page.isLoading} | |
isError={page.isError} | |
content={content} | |
header={header} | |
/> | |
</Fragment> | |
); | |
} | |
} | |
const mapStateToProps = ({ articles, pages }, props) => ({ | |
article: articles[props.match.params.alias], | |
page: pages.article | |
}); | |
export default connect(mapStateToProps)(ArticleContainer); | |
// server-side-render.js | |
const path = require('path'); | |
const fs = require('fs'); | |
import React from 'react' | |
import { renderToNodeStream, renderToStaticMarkup } from 'react-dom/server' | |
import { StaticRouter } from 'react-router-dom' | |
import { routes } from 'src/routes' | |
import configureStore from 'src/store' | |
import { Provider } from 'react-redux' | |
import App from 'src/routes'; | |
import { staticMeta } from 'src/components/meta'; | |
import loadRouteData from 'src/utils/load-route-data'; | |
// Prevent XSS | |
const printWindowData = data => { | |
const dataSource = JSON.stringify(data); | |
if (dataSource) { | |
return dataSource.replace(/([<>/\u2028\u2029])/g, '\\$1'); | |
} | |
return dataSource; | |
}; | |
const filePath = path.resolve(__dirname, '..', 'build', 'index.html'); | |
const htmlData = fs.readFileSync(filePath, 'utf8'); | |
const delimetr = '<div id="root">'; | |
const head = htmlData.substr(0, htmlData.indexOf(delimetr) + delimetr.length); | |
const tail = htmlData.substr(htmlData.indexOf(delimetr) + delimetr.length, htmlData.length); | |
module.exports = function universalLoader(req, res) { | |
const cspNonce = req.headers['x-csp-nonce']; | |
const store = configureStore(); | |
loadRouteData(store.dispatch, req.path, routes, req.query) | |
.then(([meta]) => { | |
const context = {}; | |
const state = store.getState(); | |
const currentHead = meta ? head.replace('</head>', `${renderToStaticMarkup(staticMeta(meta))}</head>`) : head; | |
res.header('Content-Type', 'text/html'); | |
res.write(currentHead); | |
const stream = renderToNodeStream( | |
<Provider store={store}> | |
<StaticRouter | |
location={req.url} | |
routerContext={{}} | |
context={context} | |
> | |
<App /> | |
</StaticRouter> | |
</Provider> | |
); | |
stream.pipe(res, { end: false }); | |
stream.on('end', () => { | |
const currentTail = tail | |
.replace('initialState={}', 'initialState=' + printWindowData(state)) | |
.replace('ssr=!1', 'ssr=1') | |
.replace('%NONCE%', cspNonce); | |
res.write(currentTail); | |
res.end(); | |
}); | |
}) | |
.catch((errors) => { | |
console.log('err', errors); | |
}); | |
}; | |
// client-render.js | |
import React from 'react' | |
import ReactDOM from 'react-dom' | |
import { Provider } from 'react-redux' | |
import history from 'src/history'; | |
import configureStore from 'src/store' | |
import { Router } from 'react-router-dom'; | |
import queryString from 'query-string'; | |
import Routes, { routes } from 'src/routes'; | |
import loadRouteData from 'src/utils/load-route-data'; | |
const store = configureStore(window.initialState); | |
const renderParams = [ | |
( | |
<Provider store={store}> | |
<Router basename={''} history={ history }> | |
<Routes /> | |
</Router> | |
</Provider> | |
), | |
document.getElementById('root') | |
]; | |
if (window.ssr) { | |
ReactDOM.hydrate(...renderParams); | |
} else { | |
loadRouteData(store.dispatch, window.location.pathname, routes, queryString.parse(window.location.search)) | |
.then(() => { | |
ReactDOM.render(...renderParams) | |
}); | |
} | |
// componen-meta.js | |
import React, { Component } from 'react'; | |
import { Helmet } from "react-helmet"; | |
const renderStyles = styles => styles.map(({ path }) => <link key={path} type="text/css" rel="stylesheet" href={path} />); | |
const renderScripts = scripts => scripts.map(({ defer, path }) => <script defer={defer} src={path} />); | |
export const staticMeta = ({ title, image, description, url, styles, scripts }) => ([ | |
title && <title key="titls">{title}</title>, | |
title && <meta key="og-title" property="og:title" content={title} data-react-helmet="true" />, | |
title && <meta key="twitter-title" property="twitter:title" content={title} data-react-helmet="true" />, | |
image && <meta key="og-image" property="og:image" content={image} data-react-helmet="true" />, | |
image && <meta key="twitter-image" property="twitter:image" content={image} data-react-helmet="true" />, | |
description && <meta key="og-description" property="og:description" content={description} data-react-helmet="true" />, | |
description && <meta key="twitter-description" property="twitter:description" content={description} data-react-helmet="true" />, | |
description && <meta key="description" name="description" content={description} data-react-helmet="true" />, | |
url && <meta key="og-url" property="og:url" content={url} />, | |
styles && renderStyles(styles), | |
scripts && renderScripts(scripts) | |
]); | |
export default class Meta extends Component { | |
render() { | |
return ( | |
<Helmet> | |
{staticMeta(this.props)} | |
</Helmet> | |
) | |
} | |
} | |
// router.js | |
import React from 'react' | |
import { Route, Switch } from 'react-router-dom' | |
import Layout from 'src/components/layout'; | |
import Article, { loadData as articleLoadData } from 'src/pages/article'; | |
import Root, { loadData as rootLoadData } from 'src/pages/root'; | |
import NoMatch from 'src/components/no-match'; | |
export const routes = [ | |
{ | |
path: '/', | |
key: 'index', | |
exact: true, | |
component: Root, | |
loadData: (...args) => rootLoadData(...args), | |
}, | |
{ | |
path: '/articles/:alias', | |
key: 'article', | |
exact: true, | |
component: Article, | |
loadData: (...args) => articleLoadData(...args), | |
}, | |
{ | |
path: '*', | |
exact: true, | |
key: 'no found', | |
component: NoMatch, | |
} | |
]; | |
const Routes = props => { | |
return ( | |
<Layout> | |
<Switch> | |
{routes.map(route => ( | |
<Route {...route} /> | |
))} | |
</Switch> | |
</Layout> | |
); | |
}; | |
export default Routes; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment