SSR fixture (#9547)

Adds a server-rendering fixture based on create-react-app without ejecting.

This is using the full-page strategy because I wanted to flush out any issues with it. Turns out there's a lot of little things that are fixable and some non-fixable. This actually surfaced some differences between the current strategy and the one I had in mind.

This uses the asset manifest from webpack to pass the final URLs to the client. This ensures that we can render the same exact mark up on the client - including the URL to our own script that we're running in.

Doing full document renders work with 15.x as long as the checksum matches. However, with the patch up reviving strategy I had in mind it would end up removing the CSS tags that webpack injects before running our render call. This is a behavior change.

In dev mode the server runs a proxy in front of the normal CRA webpack server so that we can replace the HTML request for the root page.

I don't know what the best way to link in the react and react-dom packages. Each fixture has a different strategy so here's another one. Just add NODE_PATH=../../build/packages in front of all commands.
This commit is contained in:
Sebastian Markbåge 2017-04-27 21:03:32 -07:00 committed by GitHub
parent 182642b2ea
commit b9da1741cb
13 changed files with 5541 additions and 0 deletions

24
fixtures/ssr/README.md Normal file
View File

@ -0,0 +1,24 @@
# SSR Fixtures
A set of test cases for quickly identifying issues with server-side rendering.
## Setup
To reference a local build of React, first run `npm run build` at the root
of the React project. Then:
```
cd fixtures/ssr
npm install
npm start
```
The `start` command runs a webpack dev server and a server-side rendering server in development mode with hot reloading.
If you want to try the production mode instead run:
```
npm run start:prod
```
This will pre-build all static resources and then start a server-side rendering HTTP server that hosts the React app and service the static resources (without hot reloading).

25
fixtures/ssr/package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "react-fixtures-ssr",
"version": "0.1.0",
"private": true,
"devDependencies": {
"concurrently": "3.1.0",
"http-proxy-middleware": "0.17.3",
"react-scripts": "0.9.5"
},
"dependencies": {
"express": "^4.14.0",
"ignore-styles": "^5.0.1",
"import-export": "^1.0.1",
"node-fetch": "^1.6.3"
},
"scripts": {
"start": "concurrently \"npm run start:server\" \"npm run start:client\"",
"start:client": "NODE_PATH=../../build/packages PORT=3001 react-scripts start",
"start:server": "NODE_PATH=../../build/packages NODE_ENV=development node server",
"start:prod": "NODE_PATH=../../build/packages react-scripts build && NODE_PATH=../../build/packages NODE_ENV=production node server",
"build": "NODE_PATH=../../build/packages react-scripts build",
"test": "NODE_PATH=../../build/packages react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,13 @@
<!doctype html>
<html>
<body>
<script>
/*
This is just a placeholder to make react-scripts happy.
We're not using it. If we end up here, redirect to the
primary server.
*/
location.href = '//localhost:3000/';
</script>
</body>
</html>

View File

@ -0,0 +1,69 @@
require('ignore-styles');
const babelRegister = require('babel-register');
const proxy = require('http-proxy-middleware');
babelRegister({
ignore: /\/(build|node_modules)\//,
presets: ['react-app'],
});
const express = require('express');
const path = require('path');
const app = express();
// Application
if (process.env.NODE_ENV === 'development') {
app.get('/', function(req, res) {
// In development mode we clear the module cache between each request to
// get automatic hot reloading.
for (var key in require.cache) {
delete require.cache[key];
}
const render = require('./render').default;
res.send(render(req.url));
});
} else {
const render = require('./render').default;
app.get('/', function(req, res) {
res.send(render(req.url));
});
}
// Static resources
app.use(express.static(path.resolve(__dirname, '..', 'build')));
// Proxy everything else to create-react-app's webpack development server
if (process.env.NODE_ENV === 'development') {
app.use('/', proxy({
ws: true,
target: 'http://localhost:3001'
}));
}
app.listen(3000, () => {
console.log('Listening on port 3000...');
});
app.on('error', function(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
});

View File

@ -0,0 +1,21 @@
import React from 'react';
import {renderToString} from 'react-dom/server';
import App from '../src/components/App';
import assetManifest from '../build/asset-manifest.json';
let assets = assetManifest;
if (process.env.NODE_ENV === 'development') {
// Use the bundle from create-react-app's server in development mode.
assets = {
'main.js': '/static/js/bundle.js',
'main.css': '',
};
}
export default function render() {
var html = renderToString(<App assets={assets} />);
// There's no way to render a doctype in React so prepend manually.
// Also append a bootstrap script tag.
return '<!DOCTYPE html>' + html;
};

View File

@ -0,0 +1,17 @@
import React, { Component } from 'react';
import Chrome from './Chrome';
import Page from './Page';
export default class App extends Component {
render() {
return (
<Chrome title="Hello World" assets={this.props.assets}>
<div>
<h1>Hello World</h1>
<Page />
</div>
</Chrome>
);
}
}

View File

@ -0,0 +1,5 @@
body {
margin: 10px;
padding: 0;
font-family: sans-serif;
}

View File

@ -0,0 +1,27 @@
import React, { Component } from 'react';
import './Chrome.css';
export default class Chrome extends Component {
render() {
const assets = this.props.assets;
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" href="favicon.ico" />
<link rel="stylesheet" href={assets['main.css']} />
<title>{this.props.title}</title>
</head>
<body>
{this.props.children}
<script dangerouslySetInnerHTML={{
__html: `assetManifest = ${JSON.stringify(assets)};`
}} />
<script src={assets['main.js']} />
</body>
</html>
);
}
}

View File

@ -0,0 +1,3 @@
.bold {
font-weight: bold;
}

View File

@ -0,0 +1,24 @@
import React, { Component } from 'react';
import './Page.css';
export default class Page extends Component {
state = { active: false };
handleClick = (e) => {
this.setState({ active: true });
}
render() {
const link = (
<a className="bold" onClick={this.handleClick}>
Click Here
</a>
);
return (
<div>
<p>
{!this.state.active ? link : "Thanks!"}
</p>
</div>
);
}
}

View File

@ -0,0 +1,6 @@
import React from 'react';
import { render } from 'react-dom';
import App from './components/App';
render(<App assets={window.assetManifest} />, document);

5307
fixtures/ssr/yarn.lock Normal file

File diff suppressed because it is too large Load Diff