自 Angular 4.0 開始 Angular Universal 就已經正式併入 Angular 核心功能,本篇文章將示範如何將一個由 Angular CLI 建立的專案,加入 Angular Universal 伺服器渲染能力,只要透過 Node.js 知名的 Express 網站框架,即可快速實現 Angular 的伺服器渲染能力 ( SSR ) ( Server-side Rendering )。
1. 建立全新 Angular 4 專案
ng new demo1
2. 安裝 ts-node 套件 ( --save-dev )
由於我們的 Express 程式碼會以 TypeScript 撰寫,因此你必須額外安裝 ts-node 套件才能讓 Express + TypeScript 正確執行。
npm install --save-dev ts-node
3. 安裝 @angular/platform-server 套件
注意:自 npm 5.0 之後,npm install 不需要特別加上 --save 參數,只要專案下有 package.json 檔案,在 npm install 之後預設就會自動儲存套件到 package.json 定義檔中。
npm install @angular/platform-server
4. 更新 AppModule 模組 ( src/app/app.module.ts )
修正預設 AppModule 中 BrowserModule 的匯入方式
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'demo1' })
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
5. 建立一個全新的 AppServerModule 模組 ( 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 { }
6. 加入 ExpressJS / Node.js 主程式 ( src/server.ts )
由於 renderModuleFactory 函式需要透過 AOT (Ahead of Time) 編譯過的檔案,其傳入的第一個參數 AppServerModuleNgFactory 必須要先執行 ng server --prod 與 ngc 命令才能產生相關檔案,在這些檔案出現之前,你可能會在 Visual Studio Code 看見找不到參考檔案的錯誤訊息。
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}!`);
});
7. 更新 src/tsconfig.app.json 定義檔
我們要跳過 Express 程式 ( src/server.ts ),因為這段 Code 是 Node.js 的程式,會影響 Angular 本身的 TypeScript 編譯,因此需要排除在外。
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "es2015",
"baseUrl": "",
"types": []
},
"exclude": [
"server.ts",
"test.ts",
"**/*.spec.ts"
]
}
8. 更新 tsconfig.json 定義檔
請務必加入 "angularCompilerOptions" 設定到 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"
}
}
9. 更新 package.json
加入 "start" 與 "prestart" 到 "scripts" 區段中,因為在正式執行 src/server.ts 之前,必須先執行 ng build --prod 與 ngc 命令。
注意:專案內的 ngc 命令 (AOT 編譯工具) 位於 node_modules/.bin 目錄下。
"scripts": {
"ng": "ng",
"prestart": "ng build --prod && ngc",
"start": "ts-node src/server.ts",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
10. 執行 npm start 即可透過 ExpressJS 執行網站,預設網址為 http://localhost:4000/
如果查看 HTML 原始碼的話,就會發現 Angular Universal 的伺服器渲染機制已經成功啟動! ^__^
本篇文章已經將變更紀錄上傳至 GitHub,想看 Angular CLI 建立之後到底做出了哪些變化,可以參考 angular-universal-demo1 專案的版本變更紀錄。
相關連結