[Angular] 使用 window.postMessage() 實作跨網域傳值

Jed Chou
8 min readApr 15, 2021

--

前陣子專案上遇到個需求,客戶希望在後台編輯表單資料時,按下預覽按鈕可以開新分頁顯示預覽結果,此時的資料因尚未完成定案所以不會先進行存檔至資料庫。

在需求上我們有個地方要注意”開新分頁”這個部分,在 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

<router-outlet></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;
}

// 驗證資料內容...
}

--

--