Angular Universal 快速入門、常見重點整理
前言
很早就知道SSR這東西,因平常用不到,對他不怎麼熟悉
最近需要把一專案轉成SSR,花了些時間研究
單純只看官方文件總還是會有滿滿的疑問,不清楚該怎麼繼續
故整理了幾個快速重點,方便初次學習有個大方向的理解
之後依需求再去查閱又臭又長的官方文章或Google關鍵字應該會是比較有效率的作法XD
而且最近Google又大改了SEO
(參考新聞 -> Google 搜尋結果大洗牌?除了關鍵字關聯性,用戶體驗將更被重視)
想必對於用框架做出來的網頁,SSR的重要性又更加提升了
快速重點
- HTTP URL 必須是絕對路徑 (官方文件)
- Angular Universal 12開始,可以直接使用 proxy-config 重新導向api
- 承上,很容易會誤解成production也可以使用proxy-conf來導流API,實際上是不行的。proxy-conf是給你開發使用的,請見第1點,API都要使用絕對路徑- 若是在既有的Angular專案額外增加Universal的話,可以使用interceptor填補url (參考程式碼詳見下方註1)
- angular.json有關打包後使用- proxy-conf參數,你怎麼設定都會報錯。寫在- serve:ssr --proxy-conf=proxy.conf.prod.json也是無用的
 
- 在既有的Angular專案增加Universal是很簡單的,只須下指令就行 (作法見下方註2)- Angular 12專案增加Universal,執行dev:ssr時會有問題,請見下方註3解決此問題
- 同樣的指令,在Angular 11是正常的@@
 
- Angular 12專案增加Universal,執行
- 若在Service有比較複雜的業務邏輯,該邏輯會在Client執行
- 操作到cookie、localStorage、window這種只有 browser 才有的東西,官方作法是注入platformId判斷目前環境=browser時才執行。但可以透過三方套件在 Server 填補缺少的,以達到不改程式就可以使用(見下方註4)
- 不論用哪個框架開發前端,只要用到SSR,現行都得透過Node.Js host 前端 Server
- 若全新專案起步就要用SSR,強烈推薦使用nestjs做為BASE開發,原因如下:- GitHub: nestjs/ng-universal
- nestjs預設底層就是- express,與Angular官方解法一致
- nestjs風格完全與- Angular一致,對純前端的Angular開發者來說,可說是無痛上手
- 讓前端 Server 擁有相似Spring Boot、.Net Core這種有DI系統的後端架構。若真的有什麼小需求要在前端Server自己處理掉時,前端自己有一個完整的後端架構是很有幫助的
- 若是僅自用side project的爬蟲小服務,想要用框架開發但又不想再搞Server,可以直接將爬蟲功能寫在nestjs裡的!別忘了SSR就是Server Side Render,很容易忘記: 使用SSR的純前端網站,其Node.Js Runtime Server 是一個真正的Server!
- 承上,對於這種自用小服務,帶來的極大優勢就是:部署在雲端主機(如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填補
- 在Server填補Window:npm / @ntegral/ngx-universal-window
- 在Server填補localStorage:npm / localstorage-polyfill。npm文件說明較少,用法可參考 -> LocalStorage Is Not Defined In Angular Universal?
- 在Server填補cookies:npm / ngx-universal-cookies
 若用量小的話,還是建議使用Angular官方作法:isPlatformBrowser(this.platformId)來包覆。
避免用太多三方套件,增加未來版更的困難度