Skip to content
This repository has been archived by the owner on Oct 11, 2022. It is now read-only.

Server-side rendering #1394

Merged
merged 36 commits into from Sep 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
0af5058
Make client server-side rendering compatible
mxstbr Aug 21, 2017
f3d3ff7
Make frontend Redux setup server-side renderable
mxstbr Aug 21, 2017
8d06e4a
First working version of serving HTML
mxstbr Aug 21, 2017
7197b16
Use actual index.html from client, implement react-helmet SSR
mxstbr Aug 23, 2017
5f33a9b
Refactor
mxstbr Aug 23, 2017
0645bf3
Fix src env variable
mxstbr Aug 23, 2017
3a5f56c
Fix comment
mxstbr Aug 23, 2017
1c91323
Modularization
mxstbr Aug 23, 2017
036aff7
Maybe implement rehydration?
mxstbr Aug 23, 2017
74e1852
Make it work after building
mxstbr Aug 23, 2017
c31d96c
Swithc to localstorage memory
mxstbr Aug 24, 2017
73e6297
localstorage noop
mxstbr Aug 24, 2017
26e4cdc
Merge branch 'master' into sweet-sweet-seo
mxstbr Aug 31, 2017
84c2c2d
Ignore vim .swp files
mxstbr Aug 31, 2017
0830fc6
Fix <Redirect />s
mxstbr Aug 31, 2017
208b984
Add rel="nofollow"
mxstbr Aug 31, 2017
1e5b619
Force fetch delay on client
mxstbr Sep 1, 2017
d8a3484
Fix some issues with SSR
mxstbr Sep 1, 2017
2411a44
Fix window.innerHeight bug
mxstbr Sep 1, 2017
63c53ad
Work around react-router bug to make SSR work
mxstbr Sep 1, 2017
45da4a4
Share apollo client options
mxstbr Sep 2, 2017
17ca386
Convert shared options to ES5
mxstbr Sep 2, 2017
f192bd1
Fix meta tags in SSR
mxstbr Sep 2, 2017
eb0d44e
Sanitize injected state properly
mxstbr Sep 2, 2017
8f31eca
Merge branch 'master' into sweet-sweet-seo
mxstbr Sep 2, 2017
fd85845
Fix thread loading
mxstbr Sep 4, 2017
1b1f08a
Merge branch 'master' into sweet-sweet-seo
mxstbr Sep 6, 2017
5c945fe
Merge branch 'master' into sweet-sweet-seo
mxstbr Sep 7, 2017
57f2358
Merge branch 'master' into sweet-sweet-seo
mxstbr Sep 7, 2017
fb6b421
Enable ssr in dev by default
mxstbr Sep 7, 2017
1c274ab
Add docs about developing SSR
mxstbr Sep 7, 2017
0fa3f74
Fix .babelrc
mxstbr Sep 7, 2017
e55abe8
Update server-side-rendering.md
brianlovin Sep 7, 2017
11d04dc
Fix babel
mxstbr Sep 7, 2017
761d1a5
Add meta tags to notifications
mxstbr Sep 7, 2017
6428c42
Remove unused mutation file
brianlovin Sep 7, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .babelrc
Expand Up @@ -15,6 +15,8 @@
]
],
"plugins": [
"babel-plugin-transform-class-properties",
["styled-components", { "ssr": true }],
"transform-flow-strip-types",
"transform-object-rest-spread",
"babel-plugin-transform-react-jsx",
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -14,3 +14,4 @@ build-hermes
package-lock.json
.vscode
dump.rdb
*.swp
59 changes: 28 additions & 31 deletions build-chronos/main.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion config-overrides.js
Expand Up @@ -8,6 +8,7 @@ const rewireStyledComponents = require('react-app-rewire-styled-components');
const swPrecachePlugin = require('sw-precache-webpack-plugin');
const fs = require('fs');
const match = require('micromatch');
const WriteFilePlugin = require('write-file-webpack-plugin');

const isServiceWorkerPlugin = plugin => plugin instanceof swPrecachePlugin;
const whitelist = path => new RegExp(`^(?!\/${path}).*`);
Expand All @@ -29,5 +30,6 @@ const setCustomSwPrecacheOptions = config => {

module.exports = function override(config, env) {
setCustomSwPrecacheOptions(config);
return rewireStyledComponents(config, env);
config.plugins.push(WriteFilePlugin());
return rewireStyledComponents(config, env, { ssr: true });
};
23 changes: 23 additions & 0 deletions docs/backend/iris/server-side-rendering.md
@@ -0,0 +1,23 @@
# Server-side rendering

In production we server our React-based frontend (`src/`) server-side rendered, meaning we do an initial render on the server and send down static HTML, then rehydrate with the JS bundle.

## Developing SSR

When you develop Spectrum you're usually running two processes, `yarn run dev:client` for the frontend and `yarn run dev:server` for Iris, the GraphQL API. This means in your browser you access `localhost:3000`, which is a fully client-side React app, and that then fetches data from `localhost:3001/api`.


## When developing SSR features directly:
To test server-side rendering locally just load Spectrum from `localhost:3001` (instead of `:3000`), which will request the HTML from Iris rather than `webpack-dev-server`.

The upside of this setup is that we get the best development experience locally with hot module reloading etc, and in production we only have one SSR process.

The only downside is that when you're testing SSR and changing the frontend those changes won't be reflected. To get changes from the frontend reflected when you're requesting `localhost:3001` you have to:

1. Stop the `yarn run dev:client` process
2. Run `yarn run dev:client`
3. Wait for the first compilation to complete
4. Restart the `yarn run dev:server` process by stopping and then starting it again

## When doing all other client and Iris development:
Just stick to the normal workflow of `yarn run dev:client` and enjoy the hot reloading at `localhost:3000`!
4 changes: 1 addition & 3 deletions iris/authentication.js
Expand Up @@ -3,9 +3,7 @@
const env = require('node-env-file');
const IS_PROD = process.env.NODE_ENV === 'production';
const path = require('path');
if (!IS_PROD) {
env(path.resolve(__dirname, './.env'), { raise: false });
}
env(path.resolve(__dirname, './.env'), { raise: false });
// $FlowFixMe
const passport = require('passport');
// $FlowFixMe
Expand Down
44 changes: 11 additions & 33 deletions iris/index.js
Expand Up @@ -10,6 +10,7 @@ import fs from 'fs';
import { createServer } from 'http';
//$FlowFixMe
import express from 'express';
import * as graphql from 'graphql';

import schema from './schema';
import { init as initPassport } from './authentication.js';
Expand Down Expand Up @@ -38,38 +39,12 @@ app.use('/api', apiRoutes);
import stripeRoutes from './routes/stripe';
app.use('/stripe', stripeRoutes);

// In production use express to serve the React app
// In development this is done by react-scripts, which starts its own server
if (IS_PROD) {
const { graphql } = require('graphql');
// Load index.html into memory
var index = fs
.readFileSync(path.resolve(__dirname, '..', 'build', 'index.html'))
.toString();
app.use(
express.static(path.resolve(__dirname, '..', 'build'), { index: false })
);
app.get('*', function(req, res) {
getMeta(req.url, (query: string): Promise =>
graphql(schema, query, undefined, {
loaders: createLoaders(),
user: req.user,
})
).then(({ title, description, extra }) => {
// In production inject the meta title and description
res.send(
index
// Replace "Spectrum" with proper title, but make sure to not replace the twitter site:name
// (which is set to Spectrum.chat)
.replace(/Spectrum(?!\.chat)/g, title)
// Replace "Where communities live." with proper description for page
.replace(/Where communities live\./g, description)
// Add any extra meta tags at the end
.replace(/<meta name="%OG_EXTRA%">/g, extra || '')
);
});
});
}
// Use express to server-side render the React app
const renderer = require('./renderer').default;
app.use(
express.static(path.resolve(__dirname, '..', 'build'), { index: false })
);
app.get('*', renderer);

import type { Loader } from './loaders/types';
export type GraphQLContext = {
Expand All @@ -90,4 +65,7 @@ server.listen(PORT);

// Start database listeners
listeners.start();
console.log(`GraphQL server running at port ${PORT}!`);
console.log(`GraphQL server running at http://localhost:${PORT}/api`);
console.log(
`Web server running at http://localhost:${PORT}, server-side rendering enabled`
);
10 changes: 8 additions & 2 deletions iris/migrations/20170825220615-clean-recurring-payments.js
Expand Up @@ -38,9 +38,15 @@ exports.up = function(r, conn) {
return Promise.all([
cleanSubscriptions,
// delete all the old records in recurringPayments table
r.table('recurringPayments').delete().run(conn),
r
.table('recurringPayments')
.delete()
.run(conn),
// also create a new index against communityId for faster isPro lookups on communities
r.table('recurringPayments').indexCreate('communityId').run(conn),
r
.table('recurringPayments')
.indexCreate('communityId')
.run(conn),
]);
})
.then(([cleanSubscriptions]) => {
Expand Down
5 changes: 4 additions & 1 deletion iris/migrations/20170829233734-userid-index-on-invoices.js
Expand Up @@ -2,7 +2,10 @@

exports.up = function(r, conn) {
return Promise.all([
r.table('invoices').indexCreate('userId').run(conn),
r
.table('invoices')
.indexCreate('userId')
.run(conn),
]).catch(err => {
console.log(err);
throw err;
Expand Down
10 changes: 8 additions & 2 deletions iris/migrations/20170831163211-invoice-data-model-update.js
Expand Up @@ -32,13 +32,19 @@ exports.up = function(r, conn) {
return Promise.all([
cleanInvoices,
// delete all the old records in recurringPayments table
r.table('invoices').delete().run(conn),
r
.table('invoices')
.delete()
.run(conn),
]);
})
.then(([cleanInvoices]) => {
// insert each new clean record into the table
return cleanInvoices.map(invoice => {
return r.table('invoices').insert(invoice).run(conn);
return r
.table('invoices')
.insert(invoice)
.run(conn);
});
});
};
Expand Down
4 changes: 3 additions & 1 deletion iris/models/channel.js
Expand Up @@ -280,7 +280,9 @@ const deleteChannel = (channelId: string): Promise<Boolean> => {
};

const getChannelMemberCount = (channelId: string): number => {
return db.table('channels').get(channelId)('members').count().run();
return db.table('channels').get(channelId)('members')
.count()
.run();
};

module.exports = {
Expand Down
16 changes: 12 additions & 4 deletions iris/models/community.js
Expand Up @@ -51,7 +51,11 @@ const getCommunitiesByUser = (userId: string): Promise<Array<Object>> => {
.zip()
// ensure we don't return any deleted communities
.filter(community => db.not(community.hasFields('deletedAt')))
.filter(row => row('isMember').eq(true).or(row('isOwner').eq(true)))
.filter(row =>
row('isMember')
.eq(true)
.or(row('isOwner').eq(true))
)
.run()
);
};
Expand Down Expand Up @@ -471,9 +475,13 @@ const userIsMemberOfCommunity = (
communityId: string,
userId: string
): Promise<Boolean> => {
return db.table('communities').get(communityId).run().then(community => {
return community.members.indexOf(userId) > -1;
});
return db
.table('communities')
.get(communityId)
.run()
.then(community => {
return community.members.indexOf(userId) > -1;
});
};

const userIsMemberOfAnyChannelInCommunity = (
Expand Down
10 changes: 8 additions & 2 deletions iris/models/invoice.js
Expand Up @@ -3,11 +3,17 @@ import { db } from './db';
import { addQueue } from '../utils/workerQueue';

export const getInvoice = (id: string): Promise<Array<Object>> => {
return db.table('invoices').get(id).run();
return db
.table('invoices')
.get(id)
.run();
};

export const getInvoicesByCommunity = (id: string): Promise<Array<Object>> => {
return db.table('invoices').getAll(id, { index: 'communityId' }).run();
return db
.table('invoices')
.getAll(id, { index: 'communityId' })
.run();
};

export const getInvoicesByUser = (id: string): Promise<Array<Object>> => {
Expand Down
5 changes: 4 additions & 1 deletion iris/models/message.js
Expand Up @@ -10,7 +10,10 @@ import type { PaginationOptions } from '../utils/paginate-arrays';
export type MessageTypes = 'text' | 'media';

const getMessage = (messageId: string): Promise<Object> => {
return db.table('messages').get(messageId).run();
return db
.table('messages')
.get(messageId)
.run();
};

const getMessages = (threadId: String): Promise<Array<Object>> => {
Expand Down