Pre-renderizar una web de Angular con Angular-Universal

Ignacio Buioli
- 02/06/2017

Cierto es que las WebApps realizadas con Angular utilizan JavaScript para introducir el contenido en cada sitio. Esto si bien es altamente eficiente para la carga de una web, es un problema a la hora de indexar un sitio completo en buscadores como Google. El contenido presente en el index.html generado por Angular será, entonces, el único dato que podrá leer un bot. Todo contenido agregado mediante JavaScript no será visualizado por los bots de búsqueda, con lo cual una web en Angular tendrá una pobre indexación en los buscadores. Los más experimentados en el tema sospecharán que la solución va por el lado de la pre-renderización web, un sistema similar al que realiza PHP en los servidores. Para eso, la respuesta la otorga oficialmente Angular, con un paquete llamado Angular-Universal.

Angular-Universal es un paquete de Angular que permite el renderizado de los componentes de una WebApp del lado del servidor. Esto quiere decir que todas las páginas que integren nuestra WebApp se van a renderizar como contenido de texto, con la capacidad de ser indexado sin problemas por los bots de los buscadores. De este modo, Angular-Universal renderiza los contenidos en el servidor antes de mostrarlos en el cliente.

¿Qué métodos de integración existen? En principio, podemos recurrir al un quickstart de angular-universal. Para este caso, la integración se da solo con Angular, no se nos permite utilizar Angular-CLI, lo que significa que cualquier proyecto que tengamos realizado sobre Angular-CLI no puede utilizarse. En este breve articulo presentaremos una opción que permite utilizar Angular-Universal en un nuevo proyecto de Angular-CLI, y de este modo aprovechar todas las ventajas que nos permiten este tipo de tecnologías.

 

En principio vamos a necesitar tener instalado las versiones estables de node.js y npm, así como también el Angular-CLI de forma global:

 

npm install -g @angular/cli@latest

 

Acto seguido vamos a crear un nuevo proyecto de Angular-CLI y a instalar sus dependencias, pero a diferencia de lo que se suele hacer normalmente usaremos unas condiciones específicas para permitir la integración:

 

ng new --style=scss cli-universal-demo
cd cli-universal-demo
npm install -D ts-node
npm install -S @angular/platform-server @angular/animations

 

Con esto vamos a darle la dirección de utilizar como hoja de estilos SCSS (necesario para la pre-renderización), y al momento de instalar las dependencias vamos a agregar el ts-node, y las dependencias de Angular-Universal. A partir de acá vamos a tener que modificar y crear ciertos archivos:

 

Modificar src/app/app.module.ts:

 

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule.withServerTransition({appId: 'cli-universal-demo'}),
    FormsModule,
    HttpModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

 

Crear el archivo src/app/app.server.module.ts:

 

import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';

@NgModule({
  imports: [
    ServerModule,
    AppModule
  ],
  bootstrap: [AppComponent]
})
export class AppServerModule { }

 

Modificar src/tsconfig.app.json:

 

  {
"extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/app", "module": "es2015", "baseUrl": "", "types": [] }, "exclude": [ "server.ts", "test.ts", "**/*.spec.ts" ] }

 

Modificar tsconfig.json:

 

{
  "compileOnSave": false,
  "compilerOptions": {
    "outDir": "./dist/out-tsc",
    "baseUrl": "src",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es5",
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "es2016",
      "dom"
    ]
  },
  "angularCompilerOptions": {
    "genDir": "./dist/ngfactory",
    "entryModule": "./src/app/app.module#AppModule"
  }
}

 

Crear el archivo src/server.ts:

 

import 'reflect-metadata';
import 'zone.js/dist/zone-node';
import { platformServer, renderModuleFactory } from '@angular/platform-server'
import { enableProdMode } from '@angular/core'
import { AppServerModuleNgFactory } from '../dist/ngfactory/src/app/app.server.module.ngfactory'
import * as express from 'express';
import { readFileSync } from 'fs';
import { join } from 'path';

const PORT = 4000;

enableProdMode();

const app = express();

let template = readFileSync(join(__dirname, '..', 'dist', 'index.html')).toString();

app.engine('html', (_, options, callback) => {
  const opts = { document: template, url: options.req.url };

  renderModuleFactory(AppServerModuleNgFactory, opts)
    .then(html => callback(null, html));
});

app.set('view engine', 'html');
app.set('views', 'src')

app.get('*.*', express.static(join(__dirname, '..', 'dist')));

app.get('*', (req, res) => {
  res.render('index', { req });
});

app.listen(PORT, () => {
  console.log(`listening on http://localhost:${PORT}!`);
});

 

Finalmente, vamos a incluir un script en el package.json para poder ejecutar el pre-renderizado. En este caso también vamos a modificar el script de start:

 

"scripts": {
    "prestart": "ng build --prod && ngc",
    "start": "ts-node src/server.ts"
 }

 

Al hacer npm start y entrar al localhost:4000 vamos a ver la el típico mensaje “app works” de Angular. Si revisamos el código fuente de nuestra web, en lugar de encontrarnos con el mismo código de Angular podremos ver que se encuentra el contenido de ejemplo de la app.

Agradecimientos a Éverton Roberto Auler quien desarrolló el repositorio para poder utilizar dicha integración: https://github.com/evertonrobertoauler/cli-universal-demo