使用 `postMessage` 跨域名遷移 `localStorage`

Meathill發表於2023-02-09

朋友的網站有個需求:要從 A 域名遷移到 B 域名。所有內容不變,只是更改域名。這個需求不復雜,理論上改下配置然後 301 即可。但這個網站是純靜態網站,使用者資料都存在 localStorage 裡,所以他希望能夠自動幫使用者把資料也遷移到新域名。

我們知道,localStorage 是按照域名儲存的,B 網站無法訪問 A 網站的 localStorage。所以我們就需要一些特殊的手段來實現需求。經過一些調研,我們準備使用 postMessage() 來完成這個需求。

大體的方案如下:

首先,增加 migrate.html 頁面。 這個頁面不需要其它具體功能,只要偵聽 message 事件,並且把 localStorage 傳出即可。

<!-- migrate.html -->
<script>
  window.addEventListener('message', function (message) {
    const { origin, source } = message;
    // 驗證來源,只接受我們自己的域名發來的請求
    if (origin !== 'https://wordleunlimited.me') return;

    const local = localStorage.getItem('wordle');
    // `source` 是瀏覽器自動填充的,即呼叫 `postMessage` 的來源。作為跨域 iframe 裡的頁面,拿不到外層 window,只能透過這種方式往回傳遞資料。
    source.postMessage({
      type: 'migrate',
      stored: local,
    }, 'https://wordleunlimited.me');
  });
</script>

然後,在應用裡增加 <iframe>,因為我用 Vue3,所以這裡也用 Vue 元件的方式處理。

<!-- App.vue -->
<template lang="pug">
migrate-domain(v-if="needMigrate")
</template>

<script setup>
// 我會把遷移的狀態持久化,以免反覆提示打擾使用者
const needMigrate = location.hostname === 'wordleunlimited.me' && !store.state.migrated19;
</script>
<!-- migrate-domain.vue -->
<script lang="ts" setup>
import {ref} from "vue";
// 專案啟動得早,還在用 vuex
import {useStore} from "vuex";
import {key} from "@/store";

const store = useStore(key);
const migrateIFrame = ref<HTMLIFrameElement>();

// migrate.html 接到請求後,驗證來源,然後會把 localStorage 的資料發回。我們用這個函式接收。
window.addEventListener('message', message => {
  const { origin, data } = message;
  // 同樣驗證來源
  if (origin !== 'https://mywordle.org' && origin !== 'https://mywordgame.com') return;
  const { type, stored } = data;
  if (type !== 'migrate') return;
  if (stored) {
    // 遷移資料時,需加入特殊標記,用來標記“已遷移”狀態
    localStorage.setItem('wordle', stored.replace(/}$/, ', "migrated19": true}'));
    // 很奇怪,直接 reload 可能會遷移失敗,所以這裡稍微等一下
    setTimeout(() => {
      if (confirm('Data migrated, reload the page?')) {
        location.reload();
      }
    }, 100);
  }
});

// iframe 載入完即執行這段 JS,向 iframe 內的頁面傳遞遷移請求
function onLoad() {
  const contentWindow = migrateIFrame.value?.contentWindow;
  if (contentWindow) {
    contentWindow.postMessage('migrate', 'https://mywordle.org');
  } else {
    console.warn('no content window');
  }
}
</script>

<template lang="pug">
iframe(
  ref="migrateIFrame"
  src="https://mywordle.org/migrate.html"
  frameborder="no"
  width="0"
  height="0"
  @load="onLoad"
)
</template>

<style scoped>
iframe {
  width: 0;
  height: 0;
}
</style>

至此,功能完成。

如此一來,老使用者開啟網站後,會被跳轉到新域名。然後應用 JS 會檢查 localStorage 裡儲存的資料,如果沒有遷移過,就會使用 <iframe> 載入老域名下的 migrate.html。等待目標頁面載入完成之後,呼叫 postMessage() 傳送遷移請求。接下里,migrate.html 接到請求後,返回之前儲存的資料。新域名儲存之後,提示重新整理。

主要的坑在於 <iframe> 裡的頁面無法直接跟跨域頁面通訊,所以需要父頁面先找到子頁面,發起請求;然後子頁面再把資料回傳給父頁面。其它方面應該就是一般的 API 呼叫,以及體驗性問題。
希望本文對大家有幫助。如果各位對 postMessage() 或者其它相關技術有問題的話,歡迎留言交流。

本文參與了SegmentFault 思否寫作挑戰賽,歡迎正在閱讀的你也加入。

相關文章