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:
parent
182642b2ea
commit
b9da1741cb
|
@ -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).
|
|
@ -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 |
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
|
@ -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;
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
body {
|
||||||
|
margin: 10px;
|
||||||
|
padding: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
|
||||||
|
import App from './components/App';
|
||||||
|
|
||||||
|
render(<App assets={window.assetManifest} />, document);
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue