angular

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

周志衠 Jed Jhou 2020/10/26 17:00:33
3535

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

在需求上我們有個地方要注意"開新分頁"這個部分,在 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; } // 驗證資料內容... }

 

周志衠 Jed Jhou
韓子彥 Jason Han
2021/01/24 21:54:00

哦哦Jed 一定要給這篇一個讚....