為什么要使用服務(wù)器端渲染?
創(chuàng)建應(yīng)用的SSR版本有三個主要原因。
- 促進(jìn)網(wǎng)絡(luò)爬蟲(SEO)
- 提高移動和低功耗設(shè)備的性能
- 配合Angular 模塊懶加載 快速顯示首屏
詳細(xì)教程參考官網(wǎng),我做的時候也是基本上跟著官網(wǎng)一步一步走。
問題一:window,document,navigator,或location等瀏覽器對象報錯:如
ReferenceError: window is not defined
項目在服務(wù)器端渲染(這里用的是express),在node環(huán)境下是沒有以上對象的。解決方案有幾種:
1、在server.ts中添加
import { join } from 'path';
const domino = require('domino');
const fs = require('fs');
const DIST_FOLDER = join(process.cwd(), 'dist');
//我的項目代碼 在運(yùn)行npm run build:ssr 后生生成在 dist/browser 目錄下,請根據(jù)實際路徑修改
const template = fs.readFileSync(join(DIST_FOLDER, 'browser', 'index.html')).toString();
const win = domino.createWindow(template);
// global 對象中添加 對應(yīng)window的對象
global['window'] = win;
global['document'] = win.document;
global['navigator'] = win.navigator;
global['history'] = win.history;
2、利用angular module懶加載,首屏加載的時候不要寫瀏覽器對象相關(guān)的邏輯
3、在生命周期函數(shù)ngAfterViewInit(): void 中寫調(diào)用瀏覽器對象的邏輯,ReferenceError: localStorage is not defined 如localStorage等無法被domino創(chuàng)建的window模擬的。
4、通過判斷代碼運(yùn)行平臺寫不同邏輯
import { PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
constructor(@Inject(PLATFORM_ID) private platformId: Object) { ... }
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
// Client only code.
...
}
if (isPlatformServer(this.platformId)) {
// Server only code.
...
}
}
問題二:SSR時候需要使用絕對路徑發(fā)送請求,這時可以使用express的反向代理中間件, api請求設(shè)置:
import * as proxyMiddleWare from 'http-proxy-middleware';
import * as express from 'express';
const proxyPath = "http://0.0.0.0:80";//目標(biāo)后端服務(wù)地址
const proxyOption ={target:proxyPath,changeOrigoin:true};
// Express server
const app = express();
// use the proxy middleware
app.use("/api",proxyMiddleWare(proxyOption));
問題三,SSR國際化配置 和客戶端使用有一點不同,這里用的是ngx-translate庫。項目多語言切換的時候會出現(xiàn)頁面已經(jīng)返回,但是語言包還沒返回的情況。在頁面初次加載時,模板資源的請求要先于語言包文件的請求,所以在頁面在客戶端渲染時,語言包資源實際還沒就緒,因此在那一瞬間填寫在模板中的語言包鍵名便直接被渲染在了頁面中。
既然Angular的服務(wù)端渲染本身無法實現(xiàn)首次刷新無毛刺的效果,那么我們稍微變換一下思路,能否將語言包資源與模板同時返回給客戶端呢?答案是肯定的。通過Angular提供的狀態(tài)轉(zhuǎn)移功能,我們可以在服務(wù)端獲取語言包,并將其與模板一同返回給客戶端,如此客戶端在渲染模板時便能直接獲取到鍵值對應(yīng)的文本,從而避免鍵值直接渲染在頁面中的問題。
解決這個問題的核心技術(shù)就是Angular的TransferState,除此之外我們還需要結(jié)合ngx-translate的自定義loader功能。
我們這里為服務(wù)端新建一個loader:
//translate-server-loader.service.ts
import { Observable } from "rxjs";
import { TranslateLoader } from '@ngx-translate/core';
declare var require: any;
declare var process: any;
import { makeStateKey, StateKey, TransferState } from '@angular/platform-browser';
const path = require('path');
const fs = require('fs');
export class TranslateServerLoader implements TranslateLoader {
constructor(
private prefix: string = 'i18n',
private suffix: string = '.json',
private transferState: TransferState) {
}
public getTranslation(lang: string): Observable<any> {
return Observable.create(observer => {
const assets_folder = path.join(process.cwd(), 'dist', 'checkout-on-board', this.prefix);
const jsonData = JSON.parse(fs.readFileSync(`${assets_folder}/${lang}${this.suffix}`, 'utf8'));
// Here we save the translations in the transfer-state
const key: StateKey<number> = makeStateKey<number>('transfer-translate-' + lang);
this.transferState.set(key, jsonData);
observer.next(jsonData);
observer.complete();
});
}
}
修改服務(wù)端 app.server.module.ts
import { TranslateServerLoader } from './translate-server-loader.service'
import { TransferState } from '@angular/platform-browser';
export function translateFactory(transferState: TransferState) {
return new TranslateServerLoader('/assets/i18n', '.json', transferState);
}
@NgModule({
imports: [
...
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: translateFactory,
deps: [TransferState]
}
})
...
],
providers:[
TransferState
],
})
參考:
小談Angular SSR項目的國際化
ngx-translate/core issue #754 中的@peterpeterparker 與 @ocombe,@ocombe指出了問題的根本原因,@peterpeterparker則貼出了完整的代碼示例。