Skip to content

Instantly share code, notes, and snippets.

@jsanta
Created Mar 21, 2022
Embed
What would you like to do?
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