|
|
|
@@ -28,6 +28,7 @@ import * as expressStaticGzip from 'express-static-gzip';
|
|
|
|
|
/* eslint-enable import/no-namespace */
|
|
|
|
|
|
|
|
|
|
import axios from 'axios';
|
|
|
|
|
import LRU from 'lru-cache';
|
|
|
|
|
import { createCertificate } from 'pem';
|
|
|
|
|
import { createServer } from 'https';
|
|
|
|
|
import { json } from 'body-parser';
|
|
|
|
@@ -54,6 +55,7 @@ import { APP_CONFIG, AppConfig } from './src/config/app-config.interface';
|
|
|
|
|
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
|
|
|
|
|
import { logStartupMessage } from './startup-message';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Set path for the browser application's dist folder
|
|
|
|
|
*/
|
|
|
|
@@ -67,6 +69,9 @@ const cookieParser = require('cookie-parser');
|
|
|
|
|
|
|
|
|
|
const appConfig: AppConfig = buildAppConfig(join(DIST_FOLDER, 'assets/config.json'));
|
|
|
|
|
|
|
|
|
|
// cache of SSR pages, only enabled in production mode
|
|
|
|
|
let cache: LRU<string, any>;
|
|
|
|
|
|
|
|
|
|
// extend environment with app config for server
|
|
|
|
|
extendEnvironmentWithAppConfig(environment, appConfig);
|
|
|
|
|
|
|
|
|
@@ -87,10 +92,12 @@ export function app() {
|
|
|
|
|
/*
|
|
|
|
|
* If production mode is enabled in the environment file:
|
|
|
|
|
* - Enable Angular's production mode
|
|
|
|
|
* - Enable caching of SSR rendered pages (if enabled in config.yml)
|
|
|
|
|
* - Enable compression for SSR reponses. See [compression](https://github.com/expressjs/compression)
|
|
|
|
|
*/
|
|
|
|
|
if (environment.production) {
|
|
|
|
|
enableProdMode();
|
|
|
|
|
enableCache();
|
|
|
|
|
server.use(compression({
|
|
|
|
|
// only compress responses we've marked as SSR
|
|
|
|
|
// otherwise, this middleware may compress files we've chosen not to compress via compression-webpack-plugin
|
|
|
|
@@ -186,7 +193,7 @@ export function app() {
|
|
|
|
|
* Serve static resources (images, i18n messages, …)
|
|
|
|
|
* Handle pre-compressed files with [express-static-gzip](https://github.com/tkoenig89/express-static-gzip)
|
|
|
|
|
*/
|
|
|
|
|
router.get('*.*', cacheControl, expressStaticGzip(DIST_FOLDER, {
|
|
|
|
|
router.get('*.*', addCacheControl, expressStaticGzip(DIST_FOLDER, {
|
|
|
|
|
index: false,
|
|
|
|
|
enableBrotli: true,
|
|
|
|
|
orderPreference: ['br', 'gzip'],
|
|
|
|
@@ -202,8 +209,11 @@ export function app() {
|
|
|
|
|
*/
|
|
|
|
|
server.get('/app/health', healthCheck);
|
|
|
|
|
|
|
|
|
|
// Register the ngApp callback function to handle incoming requests
|
|
|
|
|
router.get('*', ngApp);
|
|
|
|
|
/**
|
|
|
|
|
* Default sending all incoming requests to ngApp() function, after first checking for a cached
|
|
|
|
|
* copy of the page (see cacheCheck())
|
|
|
|
|
*/
|
|
|
|
|
router.get('*', cacheCheck, ngApp);
|
|
|
|
|
|
|
|
|
|
server.use(environment.ui.nameSpace, router);
|
|
|
|
|
|
|
|
|
@@ -215,60 +225,191 @@ export function app() {
|
|
|
|
|
*/
|
|
|
|
|
function ngApp(req, res) {
|
|
|
|
|
if (environment.universal.preboot) {
|
|
|
|
|
res.render(indexHtml, {
|
|
|
|
|
req,
|
|
|
|
|
res,
|
|
|
|
|
preboot: environment.universal.preboot,
|
|
|
|
|
async: environment.universal.async,
|
|
|
|
|
time: environment.universal.time,
|
|
|
|
|
baseUrl: environment.ui.nameSpace,
|
|
|
|
|
originUrl: environment.ui.baseUrl,
|
|
|
|
|
requestUrl: req.originalUrl,
|
|
|
|
|
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }]
|
|
|
|
|
}, (err, data) => {
|
|
|
|
|
if (hasNoValue(err) && hasValue(data)) {
|
|
|
|
|
res.locals.ssr = true; // mark response as SSR
|
|
|
|
|
res.send(data);
|
|
|
|
|
} else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
|
|
|
|
|
// When this error occurs we can't fall back to CSR because the response has already been
|
|
|
|
|
// sent. These errors occur for various reasons in universal, not all of which are in our
|
|
|
|
|
// control to solve.
|
|
|
|
|
console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client');
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('Error in SSR, serving for direct CSR.');
|
|
|
|
|
if (hasValue(err)) {
|
|
|
|
|
console.warn('Error details : ', err);
|
|
|
|
|
}
|
|
|
|
|
res.render(indexHtml, {
|
|
|
|
|
req,
|
|
|
|
|
providers: [{
|
|
|
|
|
provide: APP_BASE_HREF,
|
|
|
|
|
useValue: req.baseUrl
|
|
|
|
|
}]
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// Render the page to user via SSR (server side rendering)
|
|
|
|
|
serverSideRender(req, res);
|
|
|
|
|
} else {
|
|
|
|
|
// If preboot is disabled, just serve the client
|
|
|
|
|
console.log('Universal off, serving for direct CSR');
|
|
|
|
|
res.render(indexHtml, {
|
|
|
|
|
req,
|
|
|
|
|
providers: [{
|
|
|
|
|
provide: APP_BASE_HREF,
|
|
|
|
|
useValue: req.baseUrl
|
|
|
|
|
}]
|
|
|
|
|
console.log('Universal off, serving for direct client-side rendering (CSR)');
|
|
|
|
|
clientSideRender(req, res);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Render page content on server side using Angular SSR. By default this page content is
|
|
|
|
|
* returned to the user.
|
|
|
|
|
* @param req current request
|
|
|
|
|
* @param res current response
|
|
|
|
|
* @param sendToUser if true (default), send the rendered content to the user.
|
|
|
|
|
* If false, then only save this rendered content to the in-memory cache (to refresh cache).
|
|
|
|
|
*/
|
|
|
|
|
function serverSideRender(req, res, sendToUser: boolean = true) {
|
|
|
|
|
// Render the page via SSR (server side rendering)
|
|
|
|
|
res.render(indexHtml, {
|
|
|
|
|
req,
|
|
|
|
|
res,
|
|
|
|
|
preboot: environment.universal.preboot,
|
|
|
|
|
async: environment.universal.async,
|
|
|
|
|
time: environment.universal.time,
|
|
|
|
|
baseUrl: environment.ui.nameSpace,
|
|
|
|
|
originUrl: environment.ui.baseUrl,
|
|
|
|
|
requestUrl: req.originalUrl,
|
|
|
|
|
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }]
|
|
|
|
|
}, (err, data) => {
|
|
|
|
|
if (hasNoValue(err) && hasValue(data)) {
|
|
|
|
|
res.locals.ssr = true; // mark response as SSR (enables text compression)
|
|
|
|
|
// save server side rendered data to cache
|
|
|
|
|
saveToCache(getCacheKey(req), data);
|
|
|
|
|
if (sendToUser) {
|
|
|
|
|
// send rendered page to user
|
|
|
|
|
res.send(data);
|
|
|
|
|
}
|
|
|
|
|
} else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
|
|
|
|
|
// When this error occurs we can't fall back to CSR because the response has already been
|
|
|
|
|
// sent. These errors occur for various reasons in universal, not all of which are in our
|
|
|
|
|
// control to solve.
|
|
|
|
|
console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client');
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('Error in server-side rendering (SSR)');
|
|
|
|
|
if (hasValue(err)) {
|
|
|
|
|
console.warn('Error details : ', err);
|
|
|
|
|
}
|
|
|
|
|
if (sendToUser) {
|
|
|
|
|
console.warn('Falling back to serving direct client-side rendering (CSR).');
|
|
|
|
|
clientSideRender(req, res);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Send back response to user to trigger direct client-side rendering (CSR)
|
|
|
|
|
* @param req current request
|
|
|
|
|
* @param res current response
|
|
|
|
|
*/
|
|
|
|
|
function clientSideRender(req, res) {
|
|
|
|
|
res.render(indexHtml, {
|
|
|
|
|
req,
|
|
|
|
|
providers: [{
|
|
|
|
|
provide: APP_BASE_HREF,
|
|
|
|
|
useValue: req.baseUrl
|
|
|
|
|
}]
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Adds a Cache-Control HTTP header to the response.
|
|
|
|
|
* The cache control value can be configured in the config.*.yml file
|
|
|
|
|
* Defaults to max-age=604,800 seconds (1 week)
|
|
|
|
|
*/
|
|
|
|
|
function addCacheControl(req, res, next) {
|
|
|
|
|
// instruct browser to revalidate
|
|
|
|
|
res.header('Cache-Control', environment.cache.control || 'max-age=604800');
|
|
|
|
|
next();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Enable server-side caching of pages rendered via SSR.
|
|
|
|
|
*/
|
|
|
|
|
function enableCache() {
|
|
|
|
|
if (cacheEnabled()) {
|
|
|
|
|
// Initialize a new "least-recently-used" item cache.
|
|
|
|
|
// See https://www.npmjs.com/package/lru-cache
|
|
|
|
|
cache = new LRU( {
|
|
|
|
|
max: environment.cache.serverSide.max || 100, // 100 items in cache maximum
|
|
|
|
|
ttl: environment.cache.serverSide.timeToLive || 15 * 60 * 1000, // 15 minute cache
|
|
|
|
|
allowStale: true // If object is found to be stale, return stale value before deleting
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Adds a cache control header to the response
|
|
|
|
|
* The cache control value can be configured in the environments file and defaults to max-age=60
|
|
|
|
|
/**
|
|
|
|
|
* Return whether server side caching is enabled in configuration.
|
|
|
|
|
*/
|
|
|
|
|
function cacheControl(req, res, next) {
|
|
|
|
|
// instruct browser to revalidate
|
|
|
|
|
res.header('Cache-Control', environment.cache.control || 'max-age=60');
|
|
|
|
|
next();
|
|
|
|
|
function cacheEnabled(): boolean {
|
|
|
|
|
// Caching is only enabled is SSR is enabled AND
|
|
|
|
|
// "serverSide.max" setting is greater than zero
|
|
|
|
|
return environment.universal.preboot && environment.cache.serverSide.max && (environment.cache.serverSide.max > 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if the currently requested page is in our server-side, in-memory cache.
|
|
|
|
|
* Caching is ONLY done for SSR requests. Pages are cached base on their path (e.g. /home or /search?query=test)
|
|
|
|
|
*/
|
|
|
|
|
function cacheCheck(req, res, next) {
|
|
|
|
|
let cacheHit = false;
|
|
|
|
|
let debug = false; // Enable to see cache hits & re-rendering logs
|
|
|
|
|
|
|
|
|
|
// Only check cache if cache enabled & SSR is enabled
|
|
|
|
|
if (cacheEnabled()) {
|
|
|
|
|
const key = getCacheKey(req);
|
|
|
|
|
|
|
|
|
|
// Check if this page is in our cache
|
|
|
|
|
let cachedCopy = cache.get(key);
|
|
|
|
|
if (cachedCopy) {
|
|
|
|
|
cacheHit = true;
|
|
|
|
|
res.locals.ssr = true; // mark response as SSR (enables text compression)
|
|
|
|
|
if (debug) { console.log(`CACHE HIT FOR ${key}`); }
|
|
|
|
|
// return page from cache to user
|
|
|
|
|
res.send(cachedCopy);
|
|
|
|
|
|
|
|
|
|
// Check if cached copy is expired (in this sitution key will now be gone from cache)
|
|
|
|
|
if (!cache.has(key)) {
|
|
|
|
|
if (debug) { console.log(`CACHE EXPIRED FOR ${key} Re-rendering...`); }
|
|
|
|
|
// Update cached copy by rerendering server-side
|
|
|
|
|
// NOTE: Cached copy was already returned to user above. So, this re-render is just to prepare for next user.
|
|
|
|
|
serverSideRender(req, res, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tell Express to skip all other handlers for this path
|
|
|
|
|
// This ensures we don't try to re-render the page since we've already returned the cached copy
|
|
|
|
|
next('router');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If nothing found in cache, just continue with next handler
|
|
|
|
|
// (This should send the request on to the handler that rerenders the page via SSR)
|
|
|
|
|
if (!cacheHit) {
|
|
|
|
|
next();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a cache key from the current request.
|
|
|
|
|
* The cache key is the URL path (NOTE: this key will also include any querystring params).
|
|
|
|
|
* E.g. "/home" or "/search?query=test"
|
|
|
|
|
* @param req current request
|
|
|
|
|
* @returns cache key to use for this page
|
|
|
|
|
*/
|
|
|
|
|
function getCacheKey(req): string {
|
|
|
|
|
// NOTE: this will return the URL path *without* any baseUrl
|
|
|
|
|
return req.url;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Save data to server side cache, if enabled. If caching is not enabled, this is a noop
|
|
|
|
|
* @param key page key
|
|
|
|
|
* @param data page data to save to cache
|
|
|
|
|
*/
|
|
|
|
|
function saveToCache(key: string, data: any) {
|
|
|
|
|
// Only cache if caching is enabled and this path is allowed to be cached
|
|
|
|
|
if (cacheEnabled() && canCachePage(key)) {
|
|
|
|
|
cache.set(key, data);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Whether this path is allowed to be cached. Only public paths can be cached as the cache is shared across all users.
|
|
|
|
|
* @param key page key (corresponds to path of page)
|
|
|
|
|
* @returns true if allowed to be cached, false otherwise.
|
|
|
|
|
*/
|
|
|
|
|
function canCachePage(key: string) {
|
|
|
|
|
// Only these publicly accessible pages can be cached.
|
|
|
|
|
// NOTE: Caching pages which require authentication is NOT ALLOWED. The same cache is used by all users & user-specific data must NEVER appear in cache.
|
|
|
|
|
const allowedPages = [/^\/$/, /^\/home$/, /^\/items\//, /^\/entities\//, /^\/collections\//, /^\/communities\//, /^\/search[\/?]?/, /\/browse\//, /^\/community-list$/];
|
|
|
|
|
|
|
|
|
|
// Check whether any of these regexs match with the passed in key
|
|
|
|
|
return allowedPages.some(regex => regex.test(key));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|