[Angular] 使用 window.postMessage() 實作跨網域傳值
前陣子專案上遇到個需求,客戶希望在後台編輯表單資料時,按下預覽按鈕可以開新分頁顯示預覽結果,此時的資料因尚未完成定案所以不會先進行存檔至資料庫。
在需求上我們有個地方要注意"開新分頁"這個部分,在 Angular 中資料傳遞我們可以建立 ShareDataService 方式達成元件之間資料分享,但新分頁就算是同一個 domain,整個應用程式生命週期其實也是新的,所以 ShareDataService 這招不管用,而且我們可能也會想要把資料傳遞給另一個網站使用,在 google 後我們找到了 Window.postMessage() 這個 API。
Window.postMessage() 可以在 iframe 或是新頁面上將原本頁面資料內容傳遞過去,其特點在於可跨網域傳遞,如這次的情境是後台只負責編輯資料,預覽的 html 和 css 都交給前台處理,只要指定網域無須其他設定就能安全地將資料傳送給對方,接下來以 Angular 示範如何使用。
首先我們先建立一個新專案後接著新增兩個 component,接著設定路由,如下
app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { PreviewComponent } from './preview/preview.component';
const routes: Routes = [
{
path: '', component: HomeComponent
},
{
path: 'preview', component: PreviewComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
為了畫面簡潔,調整 app.component.html ,只留下 router-outlet
接著進行以下操作
home.component.html
<p>home works!</p>
<button (click)="openWindow()">新分頁開啟</button>
<button (click)="postData()">傳送資料</button>
home.component.ts
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
private previewWindow: Window; // 記錄開啟的 window 物件
constructor() { }
ngOnInit(): void {
}
public openWindow(): void {
// 開啟目標視窗,如視窗未完成開啟前即執行 postMessage() 會傳送無效
this.previewWindow = window.open('preview', '_blank');
}
public postData(): void {
const foo = {
foo1: 'aaa',
foo2: 123
};
if (this.previewWindow) {
// 第二個參數 targetOrigin 為了示範使用故不指定,實務上應設定信任網域防止資訊外洩
this.previewWindow.postMessage(foo, '*');
}
}
}
preview.component.html
<p>preview works!</p>
<!-- 印出資料 -->
{{passData | json}}
preview.component.ts
import { Component, HostListener, OnInit } from '@angular/core';
@Component({
selector: 'app-preview',
templateUrl: './preview.component.html',
styleUrls: ['./preview.component.scss']
})
export class PreviewComponent implements OnInit {
passData: any;
constructor() { }
@HostListener('window:message', ['$event'])
onMessage(event: MessageEvent): void {
this.passData = event.data;
}
ngOnInit(): void {
}
}
這樣即完成了一個簡單的跨視窗傳值範例,而如果想要開啟後自動載入資料可以依照下面範例調整。
home.component.html
<p>home works!</p>
<button (click)="openWindow()">新分頁開啟</button>
home.component.ts
import { Component, HostListener, OnInit } from '@angular/core';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
private previewWindow: Window; // 記錄開啟的 window 物件
constructor() { }
@HostListener('window:message', ['$event'])
onMessage(event: MessageEvent): void {
// 子視窗通知準備完成
if (event.data === 'isReady') {
const foo = {
type: 'preview',
data: {
foo1: 'aaa',
foo2: 123
}
};
this.previewWindow.postMessage(foo, '*');
}
}
ngOnInit(): void {
}
public openWindow(): void {
// 開啟目標視窗,如視窗未完成開啟前即執行 postMessage() 會傳送無效
this.previewWindow = window.open('preview', '_blank');
}
}
preview.component.ts
import { Component, HostListener, OnInit, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-preview',
templateUrl: './preview.component.html',
styleUrls: ['./preview.component.scss']
})
export class PreviewComponent implements OnInit, AfterViewInit {
passData: any;
constructor() { }
@HostListener('window:message', ['$event'])
onMessage(event: MessageEvent): void {
// 僅接受自訂資料內容
if (event.data.type === 'preview') {
this.passData = event.data.data;
}
}
ngOnInit(): void {
}
ngAfterViewInit(): void {
const w = window.opener as Window; // 目前視窗之父視窗的參考
w.postMessage('isReady', '*'); // 通知父視窗
}
}
安全性
本文章為方便展示,呼叫 window.postMessage() 的 targetOrigin 參數設定為不指定(*),實務上應設定指定目標網域,接收端也應判斷來源端網域是否為已知可信任網域再進行後續操作。
// 傳送端 <http://example.com:8080> const foo = { foo1: 'aaa', foo2: 123 }; window.postMessage(foo,'http://example.org')
//接收端 <http://example.org> @HostListener('window:message', ['$event']) onMessage(event: MessageEvent): void { if (event.origin !== 'http://example.com:8080') { return; } // 驗證資料內容... }
哦哦Jed 一定要給這篇一個讚....