En este artículo, exploraremos la creación de cero de un proyecto de Angular con SSR y veremos qué configuraciones nuevas nos trae. En el siguiente tutorial veremos cómo mejorar la configuración por defecto y añadir otras herramientas de desarrollo.
Creación proyecto
El primer paso va a ser crear un proyecto de Angular de cero, usando el siguiente comando:
npm init @angular ng-future
Marcamos la opción “Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)”, que ya está lista para entornos productivos.
Con la opción SSR habilitada Angular creará dos entornos de ejecución: cliente y servidor. La parte novedosa es que ya está integrado totalmente dentro del CLI de Angular. Con el SSR se mejora el performance y otras métricas importantes sin tener que instalar el paquete de Angular Universal, que este quedaría deprecado.
Estructura del proyecto
Si indagamos en la estructura de proyecto veremos que ha creado una serie de ficheros de configuración para la parte cliente como la parte servidora. Aquí tienes un resumen:
Cliente | Servidor |
---|---|
main.ts | main.server.ts |
app.config.ts | app.config.server.ts |
~ | server.ts |
server.ts
En este fichero es donde se inicializa un servidor de NodeJS con Express con el siguiente contenido:
import { APP_BASE_HREF } from '@angular/common'
import { CommonEngine } from '@angular/ssr'
import express from 'express'
import { fileURLToPath } from 'node:url'
import { dirname, join, resolve } from 'node:path'
import bootstrap from './src/main.server'
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express()
const serverDistFolder = dirname(fileURLToPath(import.meta.url))
const browserDistFolder = resolve(serverDistFolder, '../browser')
const indexHtml = join(serverDistFolder, 'index.server.html')
const commonEngine = new CommonEngine()
server.set('view engine', 'html')
server.set('views', browserDistFolder)
// Serve static files from /browser
server.get(
'*.*',
express.static(browserDistFolder, {
maxAge: '1y',
}),
)
// All regular routes use the Angular engine
server.get('*', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req
commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
})
.then(html => res.send(html))
.catch(err => next(err))
})
return server
}
function run(): void {
const port = process.env['PORT'] || 4000
// Start up the Node server
const server = app()
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`)
})
}
run()
Se hace boostrap
de la aplicación del servidor y se exponen los assets estáticos (CSS, HTML, fuentes, imágenes, etc) con el siguiente código:
server.get(
'*.*',
express.static(browserDistFolder, {
maxAge: '1y',
}),
)
Aquí se podría integrar un CDN como CloudFront para servir los ficheros estáticos y que estén cacheados.
main.server.ts
En este fichero tenemos una función bootstrap
exportada que se usa en el server.ts
para lanzar la aplicación en el lado servidor. Se carga también el componente raíz AppComponent
. Como podemos observar no hay ninguna mención de los Módulos de Angular, ya que se usan los nuevos Standalone Components (profundizaremos en esto en otra sección):
import { bootstrapApplication } from '@angular/platform-browser'
import { AppComponent } from './app/app.component'
import { config } from './app/app.config.server'
const bootstrap = () => bootstrapApplication(AppComponent, config)
export default bootstrap
main.ts
En este fichero tendremos el código responsable de lanzar la aplicación en el cliente. Es muy parecido al main.server.ts
salvo que la configuración es distinta, usa el app.config
:
import { bootstrapApplication } from '@angular/platform-browser'
import { appConfig } from './app/app.config'
import { AppComponent } from './app/app.component'
bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err))
app.config
La configuración que está en el appConfig
tendrá un nuevo provider que une la brecha entre el cliente y el servidor: el provideClientHydration()
. Con este provider nuevo nos aseguraremos que el cliente y el servidor no duplican su trabajo.
import { ApplicationConfig } from '@angular/core'
import { provideRouter } from '@angular/router'
import { routes } from './app.routes'
import { provideClientHydration } from '@angular/platform-browser'
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideClientHydration(),
],
}
app.config.server.ts
Por último tenemos el app.config.server.ts
, que carga un nuevo provider: provideServerRendering()
:
import { ApplicationConfig, mergeApplicationConfig } from '@angular/core'
import { provideServerRendering } from '@angular/platform-server'
import { appConfig } from './app.config'
const serverConfig: ApplicationConfig = {
providers: [provideServerRendering()],
}
export const config = mergeApplicationConfig(appConfig, serverConfig)
¿Dónde están los NgModules?
Angular quiere empujar a los desarrolladores a que eviten el uso de módulos, decisión que creo que es correcta. El principal problema de los módulos es que introducen mucho boilerplate, es fácil no incluir una dependencia necesaria haciendo que la aplicación se rompa de manera sustancial y es fácil incluir dependencias de más que no harán más que añadir lastre a un bundle que ya es bastante pesado para un “Hola mundo”.
Conclusión
Como hemos visto en este tutorial hay configuraciones nuevas respecto a versiones anteriores, simplificando y reduciendo el número de ficheros que se crean con el CLI de Angular a la hora de comenzar un nuevo proyecto.