RFC: Provide express server for SSR
nicolassenechal opened this issue · 13 comments
Summary
The current SSR implementation implies that users have to implement their own server. We should provide an express server handling this kind of functionality.
Motivation
This is a required stepping stone for further improvements, such as routing and server-side API resolution.
Detailed design
- Add "scripts/server.js" to the project
- In this file, create and start an express server
- Expose this file through sku methods
- Allow extra middlewares to be passed in through the sku config and consumed by the express app
How We Teach This
Readme should be updated to explain how to use and extend the express server.
Drawbacks
That extra configuration option might make sku configs more complex and harder to understand.
Alternatives
We could let users duplicate the logic in their applications.
Unresolved questions
- This does not cover extra middleware we will want to include at some stage (hot module reloading for instance)
- There might be some confusion for end users between "sku start" and "sku server"
The big question for me on this is, what does your project's server.js
file export, if anything? How does sku make use of what you provide?
The "detailed design" section is a little sparse at the moment. Could you go into a little more detail about the API?
I had two designs in mind:
- Create the server, apply default middlewares, export server and let user add their own middlewares and start the server in their project. Export the server
- Accept user middlewares and port as a sku config options, create the server, apply default middlewares, apply user middlewares, start the server. Don't export anything
I am leaning towards the second approach as it works out of the box.
Thinking more about the second one—I wonder if server.js
could work like this:
export default app => {
app.get('*', () => { ... });
}
We pass you an express app, you add your stuff to it?
Could work too. According to the docs, you can consume another express app as any other middleware. I have never tried it but it feels like it would achieve the same result.
There's obviously a big tradeoff here, whether or not we couple ourselves to a particular library like this. What's the advantage? The alternative is that we could do something like this:
import express from 'express';
export default ({ port }, callback) => {
const app = express();
app.get(...);
app.listen(port, callback);
};
Looking into webpack dev middleware...
Makes me think coupling to express isn't a bad idea. It means we'd have the ability to hook your express server into webpack on your behalf, start listening on the right port for you, omit the dev middleware when building for production, all while keeping your code nice and clean.
A more future proof version of this API might look like this, since it means we can easily pass you things other than just app
:
export default ({ app }) => {
app.get('*', () => { ... });
}
@markdalgleish we forgot to cover how the consuming app can extend the app object with extra middlewares. Got two versions in mind:
//sku code (server.js)
import express from 'express';
export default( { port = 8080, renderCallback, middlewares = [] }) => {
const app = express();
app.get('*', renderCallback);
app.use(middlewares);
app.listen(port, () => {
console.log(`App started on port ${port}`);
});
};
//Consuming code
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App/App';
import middleware from 'middleware';
import server from 'sku/server.js';
const renderCallback = (req, res) => {
res.send(renderToString(<html><App /></html>));
};
server({renderCallback, middlewares: [middleware]});
//sku code (server.js)
import express from 'express';
export default( { renderCallback }) => {
const app = express();
app.get('*', renderCallback);
return app;
};
//Consuming code
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App/App';
import middleware from 'middleware';
import server from 'sku/server.js';
const renderCallback = (req, res) => {
res.send(renderToString(<html><App /></html>));
};
const app = server({renderCallback});
app.use(middleware);
app.listen('8080', () => {
console.log(`App started on port ${port}`);
});
I prefer the first solution as it is self-containing, starts the server for the user. The second one does very little at this stage (it's really a placeholder for hmr)
In your consuming code examples, where is the server
function coming from?
Overlooked this, thanks. Updated, but I think I need to rethink the way we are starting the server. The logic might be inverted
I'd probably export a function that is passed a reference to the things it needs to render, rather than importing a file from sku
, e.g.
export default ({ server }) => {
server(...);
};
instead of this:
import server from 'sku/server.js';
server(...);
Going to summarise a few discussions and realign the proposal with a more viable solution
Summary
Package an express server for use with server-side rendering
How to consume this
The consuming application needs to create a server entry point similar to this:
import render from './render.js';
import middlewares from './middlewares';
export default {
renderCallback: (req, res) => {
res.send(render());
},
middlewares: middlewares
};
middlewares
is optional. renderCallback
is compulsory.
sku.config nees to contain a link to that server configuration file using the existing syntax:
module.exports = {
entry: {
client: 'src/client.js',
server: 'src/server.js'
},
public: 'src/public',
target: 'dist',
port: 8081
}
The user can now run sku build
. This will generate dist/server.js
. The server can be run using node ./dist/server.js
Detailed implementation
- Add an alias to the webpack server build:
resolve: {
alias: isRender ? {} : {
'serverEntry': paths.serverEntry
}
},
- Update the entry point for server-side rendering to a static file inside sku:
entry: {
render: paths.renderEntry || path.join(__dirname, '../server/server.js')
},
- Add the port used for the server to the environment variables
- Create the static file for server-side rendering along the lines of:
const express = require('express');
const {renderCallback, middlewares} = require('serverEntry').default;
const port = process.env.PORT || 8080;
const app = express();
app.get('*', renderCallback);
app.use(middlewares);
app.listen(port, () => {
console.log(`App started on port ${port}`);
});
Implemented 👍