Este artículo es una guía rápida de puesta en marcha de Universal para Angular 5.0 en un proyecto generado desde cero con Angular CLI.
Te recomiendo que leas mi artículo anterior si tienes dudas o quieres saber más sobre qué es Angular Universal.
1 – Crear el proyecto
Como siempre con la CLI de Angular, usa el comando ng
para generar un nuevo proyecto.
# bash
ng new myNewProject
2 – Instalar las dependencias
Para poder renderizar Angular desde servidor, necesitas algunas dependencias adicionales.
- @angular/platform-server – Plataforma Angular en servidor.
- @nguniversal/module-map-ngfactory-loader – Para poder realizar enrutado lazy-loading desde el servidor.
- @nguniversal/express-engine – Adaptador de Angular Universal para el popular servidor de Node Express.
- ts-loader – Plugin de Webpack para transpilar la parte TS del servidor.
# bash
npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader @nguniversal/express-engine
3 – Modificar app.module.ts
Debes indicarle a Angular que tiene que rehidratar la aplicación «myNewProject» generada por el servidor. Para eso, solo actualiza el import de BrowserModule
en el módulo principal de la aplicación.
//src/app/app.module.ts
//...some stuff...
@NgModule({
//...some stuff...
imports: [
BrowserModule.withServerTransition({appId:'myNewProject'})
],
//...some stuff...
})
export class AppModule { }
Angular se encargará de asociar la app generada por Universal con este appId
, de modo que el proceso en cliente sea capaz de reemplazar una por otra.
Es interesante que inyectes además los servicios PLATFORM_ID
y APP_ID
. Así sabrás en tiempo de ejecución si estás en servidor o en cliente y podrás acceder al appId
anterior.
Esto lo puedes hacer pasándole dichos servicios al módulo principal:
//src/app/app.module.ts
//...some imports...
import { PLATFORM_ID, APP_ID, Inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
//...some stuff...
export class AppModule {
constructor(
@Inject(PLATFORM_ID) private platformId: Object,
@Inject(APP_ID) private appId: string
){
const platform = isPlatformBrowser(this.platformId) ? 'browser' : 'server';
console.log("I'm on the ", platform);
}
}
4 – Crea el módulo de servidor
Para ejecutar Angular en servidor necesitas un módulo que actúe como punto de entrada: src/app/app.server.module.ts.
Fíjate, es un módulo muy simple que entre otros, importa el módulo principal (AppModule
) como dependencia:
// src/app/app.server.module.ts
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
@NgModule({
imports: [
AppModule,
ServerModule,
ModuleMapLoaderModule
],
providers: [
// Add universal-only providers here
],
bootstrap: [ AppComponent ],
})
export class AppServerModule {}
5 – Crea un punto de entrada para el módulo servidor
La CLI de Angular crea un archivo main.ts, que sirve de punto de entrada del módulo principal. Vas a tener que hacer algo similar para el servidor, aunque mucho más simple. Crea un nuevo archivo main.server.ts, y exporta el módulo anterior:
// src/main.server.ts
export { AppServerModule } from './app/app.server.module';
6 – Configura Typescript para Angular Universal
Igual que con el módulo principal, el módulo Angular que se ejecuta en servidor necesita ser transpilado a Javascript plano.
Para eso, crea un archivo src/tsconfig.server.json con el siguiente contenido:
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
"module": "commonjs",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
],
"angularCompilerOptions": {
"entryModule": "app/app.server.module#AppServerModule"
}
}
Si lo comparas con el archivo src/tsconfig.app.json verás que son realmente similares, salvo por un par de cosas:
- Utiliza módulos tipo
commonjs
en lugar de ES6. -
añade la propiedad
angularCompilerOptions
, que le indica el punto de entrada al compilador AOT (con la sintaxispath/al/archivo#nombreDeArchivo
).
7 – Crea una nueva entrada en angular-cli.json
Cuando ejecutas ng build
para compilar tu proyecto, se escanea el archivo angular-cli.json en busca de aplicaciones Angular que compilar. En este caso, necesitamos que compile también el módulo de servidor, así que actualizaremos el archivo del siguiente modo:
// ...some stuff..
"apps": [
{
"root": "src",
"outDir": "dist/browser",
//...some stuff...
},
{
"platform": "server",
"root": "src",
"outDir": "dist/server",
"assets": [
"assets",
"favicon.ico"
],
"index": "index.html",
"main": "main.server.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.server.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"styles.css"
],
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
],
//...some more stuff...
Fíjate en un par de detalles:
- En la app que ya había creado, he cambiado el directorio de salida a dist/browser. Este cambio es básicamente para separar los recursos de navegador de los de servidor.
-
He creado una segunda app, completamente igual a la primera salvo por:
- La plataforma
- El directorio de salida
- El punto de entrada (
main.server.ts
) - La configuración TS (
tsconfig.server.json
)
Con estos cambios, consigo que la CLI de Angular pueda reconocer también el módulo de servidor para compilarlo (con el flag --app 1
, lo verás en el paso 10).
8 – Crea el servidor Universal
El Server Side Rendering, como su nombre indica, necesita ser ejecutado en un servidor. Cualquier tecnología de servidor debería valer, pero como estás usando Javascript, tiene sentido que lo hagas en Node (y básicamente es lo que te voy a mostrar).
En el directorio principal del proyecto, crea un archivo server.ts con este contenido:
// src/../server.ts
// These are important and needed before anything else
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { enableProdMode } from '@angular/core';
import * as express from 'express';
import { join } from 'path';
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();
// Express server
const app = express();
const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist');
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main.bundle');
// Express Engine
import { ngExpressEngine } from '@nguniversal/express-engine';
// Import module map for lazy loading
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
}));
app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));
// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));
// All regular routes use the Universal engine
app.get('*', (req, res) => {
res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req });
});
// Start up the Node server
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});
Este es un servidor muy sencillo de Node, a partir de Express Framework. No es mi intención explicarte hoy Express, eso lo dejo para otro día, pero voy a comentarte 4 puntos básicos.
- Has creado el servidor invocando la función
express()
, y se lo has asignado a la variableapp
. - Le pasas como motor de renderizado HTML, la promise que devuelve la función
ngExpressEngine
. Esta función es un wrapper que te simplifica las cosas. Contiene mucha magia:- Internamente llama a
renderModuleFactory
, que es quien se encarga de generar el HTML para las peticiones de cliente. - Su parámetro inicial es
AppServerModule
, que es el módulo que has implementado antes. - Puedes pasar providers adicionales con datos que solo puede obtener el actual servidor en ejecución.
- Internamente llama a
- Utilizas
app.get()
para filtrar las peticiones HTTP.- En este caso, indicas que para todos los archivos estáticos (urls que acaban con una extensión, de ahí la expresión
*.*
), se sirvan de la carpetadist/browser
. Webpack moverá ahí los archivos estáticos que se necesitan a nivel de front (JS, CSS, imgs) gracias al cambio en angular-cli.json que hemos hecho antes. - El resto de rutas, se las pasarás al servidor como si fuera simple navegación.
- En este caso, indicas que para todos los archivos estáticos (urls que acaban con una extensión, de ahí la expresión
- Con
app.listen
lanzas el servidor, escuchando en el puerto indicado.
9 – Transpila el servidor con Webpack
En el paso anterior has usado TS para crear un servidor de Express, a parte del código Angular. Por eso necesitas usar Webpack para compilarlo en algo que pueda entender Node.
En el directorio principal, crea el archivo webpack.server.config.js con el siguiente contenido:
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: { server: './server.ts' },
resolve: { extensions: ['.js', '.ts'] },
target: 'node',
// this makes sure we include node_modules and other 3rd party libraries
externals: [/(node_modules|main\..*\.js)/],
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
module: {
rules: [{ test: /\.ts$/, loader: 'ts-loader' }]
},
plugins: [
// Temporary Fix for issue: https://github.com/angular/angular/issues/11580
// for 'WARNING Critical dependency: the request of a dependency is an expression'
new webpack.ContextReplacementPlugin(
/(.+)?angular(\\|\/)core(.+)?/,
path.join(__dirname, 'src'), // location of your src
{} // a map of your routes
),
new webpack.ContextReplacementPlugin(
/(.+)?express(\\|\/)(.+)?/,
path.join(__dirname, 'src'),
{}
)
]
};
No voy a entrar en detalle con la configuración de Webpack, que es todo un mundo. Escribí una introducción a Webpack hace tiempo que puede serte de ayuda si todo esto te suena a chino.
En todo caso, lo que te interesa saber es que defines server.ts
como punto de entrada y se genera un bundle en dist/server.js
con todo el código JS que necesita el servidor para funcionar (incluidas las dependencias de Express).
10 – Arranca el servidor con scripts de npm
Tienes el servidor casi listo, pero te falta ejecutar webpack para compilarlo y lanzarlo. Lo más cómodo es que te crees algunos scripts en el archivo package.json
para realizar estas tareas:
"scripts": {
...
"build:universal": "npm run build:client-and-server-bundles && npm run webpack:server",
"serve:universal": "node dist/server.js",
"build:client-and-server-bundles": "ng build --prod && ng build --prod --app 1 --output-hashing=false",
"webpack:server": "webpack --config webpack.server.config.js --progress --colors"
...
}
Fíjate como en build:client-and-server-bundles
compilas la app original (cliente), así como la app Universal (indicada con el flag --app 1
).
11 – Compilar y ejecutar
Si has seguido correctamente los pasos, estás listo para ejecutar la versión más simple posible de Angular Universal.
Ves a terminal y ejecuta:
npm run build:universal
npm run serve:universal
El proceso de build debería funcionar sin problemas y al ejecutar el servidor tendrías que encontrarte el mensaje:
«Node server listening on http://localhost:4000»
Si navegas a http://localhost:4000
, te encontrarás que la página inicial del proyecto se ejecuta correctamente. Además, la salida por consola te tiene que decir I'm on the browser
.
Ojo, mira ahora el terminal donde has ejecutado el servidor de Universal… ¿qué dice?
Correcto: I'm on the server
. Justo lo que podías esperar. La página se ha renderizado inicialmente en el servidor (de ahí esta salida), y luego de nuevo en el navegador (de ahí el otro console.log).
Siguientes pasos
En esta guía has visto los puntos necesarios para lanzar una app básica con Angular Universal. El mundo real es algo más complejo, con llamadas a una API REST, por ejemplo.
En el próximo artículo explicaré como afrontar estas situaciones, y como evitar el parpadeo de la vista con el servicio TransferState.
Si te ha gustado este artículo, compártelo 😉
Muy buen post, gracias por tu trabajo.
Estoy intentando seguir tus indicaciones pero tengo problemas al realizar el build:universal me da los siguientes errores:
:\Users\ablancor\Documents\Proyectos\probando\myNewProject>npm run build:universal
> my-new-project@0.0.0 build:universal C:\Users\ablancor\Documents\Proyectos\probando\myNewProject
> npm run build:client-and-server-bundles && npm run webpack:server
> my-new-project@0.0.0 build:client-and-server-bundles C:\Users\ablancor\Documents\Proyectos\probando\myNewProject
> ng build –prod && ng build –prod –app 1 –output-hashing=false
Date: 2017-12-15T09:43:45.429Z
Hash: d5283a55ab3cc29cda92
Time: 2168ms
chunk {0} styles.d41d8cd98f00b204e980.bundle.css (styles) 0 bytes [initial] [rendered]
chunk {1} polyfills.3bc34265385d52184eab.bundle.js (polyfills) 86 bytes [initial] [rendered]
chunk {2} main.e402deade8b026b7d50e.bundle.js (main) 84 bytes [initial] [rendered]
chunk {3} inline.22b7623ed7c5ac6f9a35.bundle.js (inline) 1.45 kB [entry] [rendered]
ERROR in src\app\app.server.module.ts(11,5): Error during template compile of ‘AppServerModule’
Could not resolve @angular/platform-server relative to C:/Users/ablancor/Documents/Proyectos/probando/myNewProject/src/app/app.server.module.ts..
src/app/app.server.module.ts(2,30): error TS2307: Cannot find module ‘@angular/platform-server’.
src/app/app.server.module.ts(3,39): error TS2307: Cannot find module ‘@nguniversal/module-map-ngfactory-loader’.
No creo que sea un problema de rutas porque el lint de ts no me da errores.
Gracias.
Resuelto, problemas con el proxy…
No se instalaron las nuevas dependencias.
Tengo un problema cuando ejecuto el comando npm run build:universal
Es el siguiente :
> my-new-project@0.0.0 build:universal /home/hylek/Desktop/Documents/Proyectos/Desarrollo/pruebas/myNewProject
> npm run build:client-and-server-bundles && npm run webpack:server
> my-new-project@0.0.0 build:client-and-server-bundles /home/hylek/Desktop/Documents/Proyectos/Desarrollo/pruebas/myNewProject
> ng build –prod && ng build –prod –app 1 –output-hashing=false
An app without ‘main’ cannot use the build command.
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! my-new-project@0.0.0 build:client-and-server-bundles: `ng build –prod && ng build –prod –app 1 –output-hashing=false`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the my-new-project@0.0.0 build:client-and-server-bundles script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! A complete log of this run can be found in:
npm ERR! /home/hylek/.npm/_logs/2018-01-03T18_54_29_870Z-debug.log
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! my-new-project@0.0.0 build:universal: `npm run build:client-and-server-bundles && npm run webpack:server`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the my-new-project@0.0.0 build:universal script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! A complete log of this run can be found in:
npm ERR! /home/hylek/.npm/_logs/2018-01-03T18_54_29_951Z-debug.log
hola pudiste resolver tu problema para compilarlo? tengo el mismo problema..
[…] Angular 5 Universal: Guía rápida […]
Excelente artículo.
Me arroja el siguiente error Cannot find module ‘path’.
Saludos.
Resolviste? a mi tambien me da el mismo error
tuve el mismo problema : 1) el archivo server.ts debe colgar de myNewProyect
2) en server.ts remplaza donde dice require(‘./dist/server/main.bundle’); por require(‘./dist-server/main.bundle’);
Con esto me funciono.
saludos
Hola Enrique, (no es sobre angular universal… eso me queda grande)
En mi proyecto actual estoy en Ionic 3.19 que usa angular 5 .. hecho de menos un post de los tuyos de Breaking changes .. van geniales.. 😉
y aqui mi duda, que no acabo de entender.. ¿como se hace ahora para variables de environtment ? el Opaquetoken ya no está soportado y el InjectionToken no lo acabo de comprender .. un ejemplo de los tuyos me iría de perlas ;-p
Tengo el siguiente error, y no se que puede estar pasando ya lo he intentado todo.
src/server.ts(8,22): error TS2307: Cannot find module ‘path’.
src/server.ts(16,14): error TS2304: Cannot find name ‘process’.
src/server.ts(17,26): error TS2304: Cannot find name ‘process’.
src/server.ts(20,55): error TS2304: Cannot find name ‘require’.
Por favor si me ayudara, se lo agradeceria mucho, saludos.
Algún paso te has tenido que saltar 😉
lo pudiste resolver? me da el mismo error a mi
Lo pudiste resolver? a mi me da el mismo error :C
He verificado que no me he saltado ningún paso y a mi también me sale este error
El error tiene pinta de estar relacionado con la compilación TS de `server.ts`.
Para los que os estáis encontrando con este error, os recomiendo darle un vistazo a la guía actualizada (actualizada pero no tanto, es Angular 5) que tengo en el artículo de Universal + Firebase. Ahí explico como simplificar la transpilación de `server.ts` (sin webpack) para que solo se pase a JS el código imprescindible, y el resto (dependencias de node), las vaya a buscar directamente a node_modules.
A alguien le ha funcionado esta guia? esque tengo ya un tiempo tratando y no he podido lograrlo!
Hola buen día, este post esta muy claro, hay que modificar unas cosas para que funcione al 100%, pero nada dificil.
1) El archivo server.ts va en la raiz del proyecto.
2) En el archivo server.ts hay que cambiar esta linea
// All regular routes use the Universal engine
app.get(‘*’, (req, res) => {
res.render(join(DIST_FOLDER, ‘browser’, ‘index.html’), { req });
});
por esta
// All regular routes use the Universal engine
app.get(‘*’, (req, res) => {
res.render(join(DIST_FOLDER, ‘index.html’), { req });
});
ya que quiere ir a buscar el archivo index en la carpeta «browser» y no existe dicha carpeta.
Hola Jose,
Le daré un repaso a ver si me he dejado comentar algo. Debería ser read => code => have fun!
Buenas, me podrian ayudar con este error:
carlos@carlos-linux:~/Proyectos/website$ npm run serve:universal
> tbet-project@0.0.0 serve:universal /home/carlos/Proyectos/website
> node dist/server.js
Node server listening on http://localhost:4000
I’m on the server
ERROR { [Error]
__zone_symbol__currentTask:
ZoneTask {
_zone:
Zone {
_properties: [Object],
_parent: [Object],
_name: ‘angular’,
_zoneDelegate: [Object] },
runCount: 0,
_zoneDelegates: null,
_state: ‘notScheduled’,
type: ‘microTask’,
source: ‘Promise.then’,
data: undefined,
scheduleFn: undefined,
cancelFn: null,
callback: [Function],
invoke: [Function] } }
Hola una duda. puedo crear mi proyecto normal con Angular CLI y una vez terminado mi proyecto o antes de pasarlo a producción convertirlo a Angular Universal?
Si, no veo por que no. Igual te encontrarás con algún problemilla y tendrás que retocar alguna cosa, pero debería ser de fácil arreglo, a menos que te estés apoyando en alguna librería que no esté soportada actualmente por Universal (vamos, alguna librería que toque el DOM sin miramientos).
Muchas Gracias
ERROR in ./server.ts
Module build failed: TypeError: Cannot read property ‘afterCompile’ of undefined
at successfulTypeScriptInstance (C:\xampp\htdocs\meridiano\node_modules\ts-loader\dist\instances.js:167:28)
at Object.getTypeScriptInstance (C:\xampp\htdocs\meridiano\node_modules\ts-loader\dist\instances.js:51:12)
at Object.loader (C:\xampp\htdocs\meridiano\node_modules\ts-loader\dist\index.js:16:41)
npm ERR! code ELIFECYCLE
npm ERR! errno 2
npm ERR! meridiano@0.0.0 webpack:server: `webpack –config webpack.server.config.js –progress –colors`
npm ERR! Exit status 2
npm ERR!
npm ERR! Failed at the meridiano@0.0.0 webpack:server script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! A complete log of this run can be found in:
npm ERR! C:\Users\chivo\AppData\Roaming\npm-cache\_logs\2018-05-08T04_55_06_012Z-debug.log
npm ERR! code ELIFECYCLE
npm ERR! errno 2
npm ERR! meridiano@0.0.0 build:universal: `npm run build:client-and-server-bundles && npm run webpack:server`
npm ERR! Exit status 2
npm ERR!
npm ERR! Failed at the meridiano@0.0.0 build:universal script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! A complete log of this run can be found in:
npm ERR! C:\Users\chivo\AppData\Roaming\npm-cache\_logs\2018-05-08T04_55_06_104Z-debug.log
ayuda con este error porfavor!!!
Te está petando el plugin ts-loader al compilar el servidor con Webpack.
Te recomiendo que sigas esta versión actualizada de Angular 5 Universal, donde el servidor se compila directamente con TS. El tiempo de compilación del servidor es más rápido por que no genera un bundle, y da menos problemas cuando tienes dependencias.
Un saludo
Hola Enrique, ya lo hice de la otra forma y todo va bien hasta que entró a localhost:4000
Me arroja en consola dos errores uno dice que screen is not define y el otro dice que «$» is not define y eso último creo que es porque uso jQuery para algunas cosas y tengo en varios componentes «declare var $:any» para poder usar jquery
Hola a tiodos buenos días, me estoy iniciando en Angularjs y me gustaría saber si es posible utilizar este framework con Xampp, bajo WIndows 10????? gracias y feliz día