Angular Universal 快速入門、常見重點整理

前言

很早就知道SSR這東西,因平常用不到,對他不怎麼熟悉
最近需要把一專案轉成SSR,花了些時間研究
單純只看官方文件總還是會有滿滿的疑問,不清楚該怎麼繼續

故整理了幾個快速重點,方便初次學習有個大方向的理解
之後依需求再去查閱又臭又長的官方文章或Google關鍵字應該會是比較有效率的作法XD

而且最近Google又大改了SEO
(參考新聞 -> Google 搜尋結果大洗牌?除了關鍵字關聯性,用戶體驗將更被重視)

想必對於用框架做出來的網頁,SSR的重要性又更加提升了

快速重點

  1. HTTP URL 必須是絕對路徑 (官方文件)
  2. Angular Universal 12開始,可以直接使用 proxy-config 重新導向api
  3. 承上,很容易會誤解成production也可以使用proxy-conf來導流API,實際上是不行的。proxy-conf是給你開發使用的,請見第1點,API都要使用絕對路徑
    1. 若是在既有的Angular專案額外增加Universal的話,可以使用interceptor填補url (參考程式碼詳見下方註1)
    2. angular.json有關打包後使用proxy-conf參數,你怎麼設定都會報錯。寫在serve:ssr --proxy-conf=proxy.conf.prod.json也是無用的
  4. 在既有的Angular專案增加Universal是很簡單的,只須下指令就行 (作法見下方註2)
    1. Angular 12專案增加Universal,執行dev:ssr時會有問題,請見下方註3解決此問題
    2. 同樣的指令,在Angular 11是正常的@@
  5. 若在Service有比較複雜的業務邏輯,該邏輯會在Client執行
  6. 操作到cookielocalStoragewindow這種只有 browser 才有的東西,官方作法是注入platformId判斷目前環境=browser時才執行。但可以透過三方套件在 Server 填補缺少的,以達到不改程式就可以使用(見下方註4)
  7. 不論用哪個框架開發前端,只要用到SSR,現行都得透過Node.Js host 前端 Server
  8. 若全新專案起步就要用SSR,強烈推薦使用nestjs做為BASE開發,原因如下:
    1. GitHub: nestjs/ng-universal
    2. nestjs預設底層就是express,與Angular官方解法一致
    3. nestjs風格完全與Angular一致,對純前端的Angular開發者來說,可說是無痛上手
    4. 讓前端 Server 擁有相似Spring Boot.Net Core這種有DI系統的後端架構。若真的有什麼小需求要在前端Server自己處理掉時,前端自己有一個完整的後端架構是很有幫助的
    5. 若是僅自用side project的爬蟲小服務,想要用框架開發但又不想再搞Server,可以直接將爬蟲功能寫在nestjs裡的!別忘了SSR就是Server Side Render,很容易忘記: 使用SSR的純前端網站,其Node.Js Runtime Server 是一個真正的Server!
    6. 承上,對於這種自用小服務,帶來的極大優勢就是:部署在雲端主機(如Heroku, GCP, AWS…),我只需要啟一個服務,甚至免費方案就足夠使用!

註1: 使用Interceptor填補API絕對路徑 參考程式碼

environment.prod參數

開發環境可以使用proxy-conf重新導向
或著不使用,一律透過Interceptor變成絕對路徑

export const environment = {
  production: true,
  apiUrlBase: 'https://YOURAPI.com',
};

Interceptor

@Injectable()
export class AbsoluteApiInterceptor implements HttpInterceptor {
    constructor() { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        request = request.clone({
          url: `${environment.apiUrlBase}/${request.url}`
        });
        
        return next.handle(request);
    }
}

AppModule全域啟用Interceptor

providers增加Interceptor

@NgModule({
  declarations: [
    // ...略...
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'serverApp' }),
    // ...略...
  ],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AbsoluteApiInterceptor, multi: true },
    ],
  bootstrap: [AppComponent]
})
export class AppModule { }

註2: 在已有的Angular專案,要增加Angular Universal

已有的Angular專案,要增加Angular Universal時,現在已變的很簡單
直接使用ng add @nguniversal/express-engine
若執行後沒有自動產生相關程式碼,再執行一次即會建立

註3: 解決Configuration 'development' is not set in the workspace.

避免篇幅太過冗長,請參考我之前的文章:
Angular 12 Universal 解決Configuration ‘development’ is not set in the workspace.

註4: 使用Angular Universal提供的platformId判斷目前環境

import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Injectable({
  providedIn: 'root'
})
export class DemoService {

  constructor(@Inject(PLATFORM_ID) private platformId: string) { }

  useLocalStorage() {
    console.log(this.platformId); // 若在browser,會單純印出`browser`字串
    // 框架直接提供`isPlatformBrowser`
    // 千萬不要寫成 this.platformId === 'browser' 這種醜陋的程式碼
    // 若下方程式碼未包起來於browser執行,在Server端會噴錯說找不到localStorage
    if (isPlatformBrowser(this.platformId)) {
      
      const token = localStorage.getItem('token');
      if (token) {
        console.log(`從localStorage取得token = ${token}`);
      }
    }
  }
}

在Server填補缺少的browser物件,以避免大改現有程式碼

若現有專案對於browser才有的東西使用量太大(如Window)
不想要在現有程式碼增加太多isPlatformBrowser(this.platformId)來包覆邏輯的話
可以參考下方幾個套件於Server填補

  1. 在Server填補Window:npm / @ntegral/ngx-universal-window
  2. 在Server填補localStorage:npm / localstorage-polyfill。npm文件說明較少,用法可參考 -> LocalStorage Is Not Defined In Angular Universal?
  3. 在Server填補cookies:npm / ngx-universal-cookies

若用量小的話,還是建議使用Angular官方作法:isPlatformBrowser(this.platformId)來包覆。
避免用太多三方套件,增加未來版更的困難度

參考資料

Angular Universal:Angular 統一平臺簡介