seek-oss / sku

Front-end development toolkit

Home Page:https://seek-oss.github.io/sku

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

RFC: Provide express server for SSR

nicolassenechal opened this issue · 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

  1. Add an alias to the webpack server build:
resolve: {
            alias: isRender ? {} : {
                'serverEntry': paths.serverEntry
            }
        },
  1. Update the entry point for server-side rendering to a static file inside sku:
entry: {
          render: paths.renderEntry || path.join(__dirname, '../server/server.js')
        },
  1. Add the port used for the server to the environment variables
  2. 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 👍