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)
來包覆。
避免用太多三方套件,增加未來版更的困難度