Skip to content

Instantly share code, notes, and snippets.

@jsanta
Created March 21, 2022 14:37
Show Gist options
  • Save jsanta/d39f275e93c426fea193b481bc3bc9cd to your computer and use it in GitHub Desktop.
Save jsanta/d39f275e93c426fea193b481bc3bc9cd to your computer and use it in GitHub Desktop.
Fast Angular Universal SSR server example with Redis, compression, map "security" (for curious developers) and API proxy endpoint
import 'zone.js/node';
import 'dotenv/config';
import '@ng-web-apis/universal/mocks';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';
import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
import { existsSync, readFileSync } from 'fs';
import { renderModule } from '@angular/platform-server';
import { enableProdMode, LOCALE_ID } from '@angular/core';
import * as url from 'url';
import * as proxy from 'express-http-proxy';
import * as Redis from 'ioredis';
import * as compression from 'shrink-ray-current';
// const dotenv = require('dotenv');
// dotenv.config();
// console.log('process.env: ', process.env);
enableProdMode();
const distFolder = join(process.cwd(), 'dist/myproject/browser');
const indexFile = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index.html';
const indexHtml = readFileSync(join(distFolder, indexFile), 'utf-8').toString();
const useRedisCache = process.env['USE_REDIS'] === 'true';
let redisClient: Redis.Redis;
if(useRedisCache) {
redisClient = new Redis({
port : parseInt(process.env['REDIS_PORT'] ?? '6379', 10),
host : process.env['REDIS_HOST'] || 'localhost',
username: process.env['REDIS_USR'] || 'default',
password: process.env['REDIS_PWD'] || 'docker',
db: 0,
});
}
function execRenderModule(renderUrl: string, res: express.Response, req: express.Request, useRedisCache: boolean) {
renderModule(AppServerModule, {
document: indexHtml,
url: renderUrl,
extraProviders: [
{ provide: APP_BASE_HREF, useValue: req.baseUrl },
{provide: LOCALE_ID, useValue: 'es-CL'}
]
})
.then(html => {
if(useRedisCache) {
redisClient.set(renderUrl, html, (err: any, result: any) => {
if(err) {
console.error('Error setting Redis cache: ', err);
}
if(result) {
console.log('Set Redis cache: ', renderUrl);
}
});
}
res.setHeader("Content-Type", "text/html")
res.status(200).send(html);
})
.catch(err => {
console.error(err);
res.sendStatus(500);
});
}
// The Express app is exported so that it can be used by serverless Functions.
export function app() {
const server = express();
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
server.set('view engine', 'html');
server.set('views', distFolder);
server.set('etag', 'strong');
server.set('x-powered-by', false);
server.use(
compression({
cache: () => true,
zlib: {
level: 2
},
brotli: {
quality: 4
}
} as any)
);
// // TODO: implement data requests securely
// server.get('/api/**', (req, res) => {
// res.status(404).send('data requests are not yet supported');
// });
console.log('SSR proxy:', 'http://localhost:' + (process.env['API_PORT'] || 3000) + '/api')
const ssrProxyHost = 'http://localhost:' + (process.env['API_PORT'] || 3000);
server.use('/api/*', (proxy as any)(ssrProxyHost, {
proxyReqPathResolver: (req: any): string | null => {
const targetUrl = url.parse(req.baseUrl).path;
console.log('SSR proxy:', req.originalUrl, targetUrl);
return targetUrl as string;
}
}));
server.use('/img/*', (proxy as any)(ssrProxyHost, {
proxyReqPathResolver: (req: any): string | null => {
const targetUrl = url.parse(req.baseUrl).path;
console.log('SSR proxy:', req.originalUrl, targetUrl);
return targetUrl as string;
}
}));
// Endpoint para la captura de las peticiones sobre archivos de extension .map que pueden eventualmente no existir
server.route(/(.*)\.(css|js)\.map$/).get((req, res, next) => {
console.log(`[${new Date().toISOString()}] - [${req.method}] - [${req.url}]: Curioso tratando de debuguear un archivo .map desde ${req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'] || req.connection.remoteAddress}`);
res.status(200).json({ version: 3, file: `${req.url || req.originalUrl}`, sources: [], names: [], mappings: [] });
});
// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));
// All regular routes use the Universal engine
server.get('*', (req, res) => {
console.log('GET *: ', req.url);
// res.setHeader("Content-Type", "text/html");
// res.render(indexFile, { req, res, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
const renderUrl: string = req.url;
console.log('Rendering SSR: ', renderUrl);
if(useRedisCache) {
const _cache = redisClient.get(renderUrl, (err: any, result: any) => {
if(err) {
console.log('Error getting Redis cache: ', err);
}
if(result) {
console.log('Got Redis cache: ', renderUrl);
res.setHeader("Content-Type", "text/html");
res.status(200).send(result);
} else {
console.log('No Redis cache: ', renderUrl);
execRenderModule(renderUrl, res, req, useRedisCache);
}
});
}
});
return server;
}
function run() {
const port = process.env['SSR_PORT'] || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export * from './src/main.server';
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment