Created
March 21, 2022 14:37
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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