Open jaredpalmer opened 7 years ago
@jaredpalmer can you share how you did that?
// server.js
import 'source-map-support/register';
import express from 'express';
import compression from 'compression';
import path from 'path';
import React from 'react';
import { withAsyncComponents } from 'react-async-component';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { renderStatic } from 'glamor/server';
import template from './template';
import App from '../components/App';
import configureStore from '../store';
import { render } from 'rapscallion';
/* eslint-disable */
const clientAssets = require(KYT.ASSETS_MANIFEST);
/* eslint-enable */
const server = express();
// Remove annoying Express header addition.
server.disable('x-powered-by');
// Compress (gzip) assets in production.
server.use(compression());
// Setup the public directory so that we can server static assets.
server.use(express.static(path.join(process.cwd(), KYT.PUBLIC_DIR)));
// Setup server side routing.
server.get('*', (request, response) => {
// First create a context for <StaticRouter>, which will allow us to
// query for the results of the render.
const reactRouterContext = {};
const store = configureStore({
sourceRequest: {
protocol: request.headers['x-forwarded-proto'] || request.protocol,
host: request.headers.host,
},
});
const ReactRoot = (
<StaticRouter location={request.url} context={reactRouterContext}>
<Provider store={store}>
<App />
</Provider>
</StaticRouter>
);
withAsyncComponents(ReactRoot)
.then(({ appWithAsyncComponents, state, STATE_IDENTIFIER }) => {
const html = template({
root: renderStatic(() => render(appWithAsyncComponents)),
initialState: store.getState(),
jsBundle: clientAssets.main.js,
cssBundle: clientAssets.main.css,
});
if (reactRouterContext.url) {
response.writeHead(302, { Location: reactRouterContext.url });
response.end();
return;
}
html.toStream().pipe(response);
})
.catch(e => {
console.log(e);
response.status(500).json({ error: e.message, stack: e.stack });
});
});
server.listen(parseInt(process.env.PORT || KYT.SERVER_PORT, 10), err => {
if (err) {
throw err;
}
console.log('> started');
});
// template.js
/* eslint-disable prefer-template, max-len */
import { template } from 'rapscallion';
export default vo => template`
<!DOCTYPE html>
<html lang="en">
<head>
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta charSet='utf-8' />
<title>Universal React</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#584095" />
<meta name="mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-title" content="NKBA"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
<link id="favicon" rel="shortcut icon" href="/kyt-favicon.png" sizes="16x16 32x32" type="image/png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icon/apple-touch-icon.png"/>
<link rel="manifest" href="/manifest.json"/>
<meta name="msapplication-tap-highlight" content="no"/>
<meta name="msapplication-TileImage" content="/icon/ms-touch-icon-144x144-precomposed.png"/>
<meta name="msapplication-TileColor" content="#F2F2F2"/>
<meta name="theme-color" content="#673AB8"/>
<style type="text/css">${() => vo.root.css}</style>
<script type="text/javascript" async>
!function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="4.0.0";
analytics.load("XXXXXXXXXXXXX");
}}();
</script>
</head>
<body>
<div id="root"><div>${vo.root.html}</div></div>
<script src="${() => vo.jsBundle}" defer></script>
<script type="text/javascript">window._initialState = ${() =>
JSON.stringify(vo.initialState) ||
{}}; window._glam =${() => JSON.stringify(vo.root.ids)};</script>
</body>
</html>
`;
// client.js
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { withAsyncComponents } from 'react-async-component';
import { Provider } from 'react-redux';
import configureStore from '../store';
import ReactHotLoader from './RHL';
import App from '../components/App';
import { rehydrate } from 'glamor';
rehydrate(window._glam);
const container = document.querySelector('#root');
const store = configureStore(window._initialState || {});
function renderApp(TheApp) {
const app = (
<ReactHotLoader>
<Provider store={store}>
<BrowserRouter>
<TheApp />
</BrowserRouter>
</Provider>
</ReactHotLoader>
);
// We use the react-async-component in order to support super easy code splitting
// within our application. It's important to use this helper
// @see https://github.com/ctrlplusb/react-async-component
withAsyncComponents(app).then(
({ appWithAsyncComponents }) => render(appWithAsyncComponents, container),
);
}
// // The following is needed so that we can support hot reloading our application.
if (process.env.NODE_ENV === 'development' && module.hot) {
// Accept changes to this file for hot reloading.
module.hot.accept('./index.js');
// Any changes to our App will cause a hotload re-render.
module.hot.accept(
'../components/App',
() => renderApp(require('../components/App').default),
);
}
// Execute the first render of our app.
renderApp(App);
@aweary still working through the checksum. currently has a double flicker, but the styles are there and 85/100
lighthouse on now.sh ain't too shabby.
@aweary if i add
document.querySelector("#root").setAttribute("data-react-checksum", "${vo.root.html.checksum()}")
to template.js
(right after the initialState
declaration), I get the following error:
Error: Renderer#checksum can only be invoked for a renderer converted to node stream.
at Renderer.checksum (/Users/jared/workspace/XXXX/node_modules/rapscallion/src/renderer.js:31:13)
at exports.default (/Users/jared/workspace/XXXX/build/server/webpack:/src/server/template.js:41:89)
at /Users/jared/workspace/XXXX/build/server/webpack:/src/server/index.js:53:20
@jaredpalmer I think there may still be issues with the order of evaluation of the template segments. I'll reproduce this example this evening and make any edits that seem necessary. In particular, glamor
's renderStatic
looks a bit funky (it relies on a global singleton and assumes synchronous render) and will have to be worked around. But it's definitely doable.
I'm also thinking through how I want to document all of this. It might become cumbersome to document how-tos for individual React libraries. But these are definitely questions that people will be asking, so a FAQ that covers some of these common patterns might be really useful.
As cumbersome as it might be to do recipes for individual libs, it would drive adoption if things were copy and paste-ready though.
That's very true. The problem with full examples in the docs is that they're hard to keep up-to-date. Blog posts don't usually have that problem - people expect that they may be incorrect after awhile.
On the other hand, it might be worth including well-commented examples in the repo. They can be rendered in CI, and that way we'll have some guarantees regarding correctness. Do you think that would suffice @jaredpalmer?
I think there are really 4 big recipes:
match() {....}
StaticRouter
etc.You could probably combine the Redux and CSS-in-JS into one.
Also, injecting stuff in <head/>
with e.g. react-helmet, for example meta tags and document title.
I'd really love to see such demos, still wondering how to make this working with apollo and fela!
I wrote a transforming stream that takes streamed html and inlines (glamor) css at the precise points its used.
Uncovered a bug in rapscallion in that it doesn't read glamor rules attached via data attribs. classnames work fine though. Will file separate issue for it.
If you're adventurous, using glamor with classnames, copy paste this file somewhere, and use as -
import inline from './path/to/file'
// ...
render(<App/>).pipe(inline()).pipe(res)
as a bonus, this method has the benefit of not needing to inline css and ids at any point, and should "just work".
So you inject relevant style tags whenever you encouter a new class?
Can you do that in all places in html, e.g. inside tables?
Does this not break the rendering checksum?
On Mon, Mar 13, 2017, 11:49 PM Sunil Pai notifications@github.com wrote:
I wrote a transforming stream that takes streamed html and inlines (glamor) css at the precise points its used.
Uncovered a bug in rapscallion in that it doesn't read glamor rules attached via data attribs. classnames work fine though. Will file separate issue for it.
If you're adventurous, using glamor with classnames, copy paste this file https://github.com/threepointone/rakt/blob/master/packages/rakt/src/inline-css.js somewhere, and use as -
import inline from './path/to/file'// ...render(
).pipe(inline()).pipe(res) as a bonus, this method has the benefit of not needing to inline css and ids at any point, and should "just work".
— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/FormidableLabs/rapscallion/issues/39#issuecomment-286268116, or mute the thread https://github.com/notifications/unsubscribe-auth/AADWlg7Mozhr1C1RP0bZuojF0FzP_uw5ks5rlcgDgaJpZM4MCFSO .
Pretty much, yes. I have naive detection for classnames, and prepend a style tag before its first occurrence.
Unsure which tags it would break with, I'll try out tables and such.
It does break checksums. React 16 doesn't show the warnings tho 🤷♀️
It appears the checksums break only when used with rapscallion. I might be integrating wrong. I'll try some variations and get back here.
For people who are wondering how to use rapscallion and styled-components (also a css-in-js lib) for SSR, this is how I'm doing it: https://github.com/ismay/ismaywolff.nl/blob/develop/src/server/handleRender.jsx#L18
Works like a charm.
Just making sure: This first renders the entire app and then streams it, right? So streaming won't make a difference in time-to-first-byte?
Since the styles are in the
One solution for this would be to embed the styles just before the first component using them, but that's only really feasible with fragments.
On Wed, May 10, 2017 at 12:55 PM ismay notifications@github.com wrote:
For people who are wondering how to use rapscallion and styled-components (also a css-in-js lib) for SSR, this is how I'm doing it: https://github.com/ismay/ismaywolff.nl/blob/develop/src/server/handleRender.jsx#L18
Works like a charm.
— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/FormidableLabs/rapscallion/issues/39#issuecomment-300447174, or mute the thread https://github.com/notifications/unsubscribe-auth/AADWlg4KOiZKvNTRNi00R5lxvaa4Mdc6ks5r4ZeDgaJpZM4MCFSO .
Just making sure: This first renders the entire app and then streams it, right?
@wmertens Hmm, I hope not. I've based it on the example from the readme: https://github.com/FormidableLabs/rapscallion#example. So I hope I'm not doing anything stupid and losing all of rapscallions benefits..
The way I intended this to work is for it to start streaming immediately, and while it's doing that render <App />
. Only thing it should be waiting for is for the promises from fetch
to resolve and for each previous expression to be evaluated. Which is also how I understood the example from the readme to work, but I might be misunderstanding that.
But yeah, since the styles are only available after rendering <App />
you'll have to insert them in the <head />
clientside. Just like a couple of other things need to be handled clientside (like setting meta tags, checksum, etc.).
@ismay I don't think you'll be able to get the styles as well while streaming. Related: https://github.com/styled-components/styled-components/issues/600#issuecomment-298732201
@siddharthkp, That's ok, as long as I can get them afterwards and then insert them in the head clientside.
⚠️ Probably not the right issue for streaming + css-in-js discussion, let me know if I should create another issue.
@ismay Are you saying you will get the styles at the end of the streaming? If yes, the true benefit of stream is not realised.
Are you saying you will get the styles at the end of the streaming? If yes, the true benefit of stream is not realised.
@siddharthkp Currently yes. Anything else isn't possible with styled-components v2's api as far as I know. It'd be nice if the styles could be attached right away yeah, but there are still benefits to streaming nonetheless. I can start sending a response right away which enables the client to start downloading assets (etc.) earlier than if I'd render everything on the server and then start sending. The styles might be missing, but there's still other content besides that.
@ismay we could potentially place the style tags at the end of the body to bring streaming back properly, right?
@philpl, I'm not sure about that. I imagine the real power is rendering above the fold content first and then follow it up by rest. that would include styles. styles coming at the end is not optimal
(above-the-fold rendering is awesome for users, bad for search engines. Only do it when you detect a regular visitor)
Styles coming at the end means that while the html is loading, the browser renders everything unstyled.
I wonder if styled-components could hijack styled DOM elements that can
stand it (everything except table and list tags) to inject a <style>
tag
as the first child?
On Wed, May 10, 2017 at 7:09 PM Siddharth Kshetrapal < notifications@github.com> wrote:
@philpl https://github.com/philpl, I'm not sure about that. I imagine the real power is rendering above the fold content first and then follow it up by rest. that would include styles. styles coming at the end is not optimal
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/FormidableLabs/rapscallion/issues/39#issuecomment-300549453, or mute the thread https://github.com/notifications/unsubscribe-auth/AADWln4GU-brbHpBTezh9h-tqHSoNuj0ks5r4e80gaJpZM4MCFSO .
we could potentially place the style tags at the end of the body to bring streaming back properly, right?
@philpl I'm not sure if that's valid. Browsers might parse it but I believe only the scoped
attribute allows style tags in the body and scoped
has been deprecated. Placing them in the head seems the most safe to me. Please correct me if I'm wrong.
And as @siddharthkp said, when streaming the most desirable implementation would be to stream html that includes styles as well so that there's no unstyled content. @wmertens' suggestion would be a way to achieve that, if it's possible.
(above-the-fold rendering is awesome for users, bad for search engines. Only do it when you detect a regular visitor)
@wmertens That got me thinking, hope I'm not derailing the issue (and let me know if I am), but I was thinking about how to handle this. Maybe set a cookie, and:
That would mean three different approaches for rendering. Maybe a good subject for a new issue where we can discuss adapting rendering to the request (and adding that to the readme).
@jaredpalmer @wmertens Would be great to see how you can stream react markup and inject into the head.
I tried a similar approach to @threepointone - inlinePipe and was able to extract the head tag data generated by helmet however I would require to write some additional functionality to modify the top down order of the template renderer. Not sure if this is the correct approach, would be nice to use some type of callback or promise on the functions to sync the stream. Similar to your renderer. Specifically interested in putting data in the head (react-helmet) and retaining the benefit of a stream.
I got rapscallion working with
glamor/server
. Will submit a pr soon.