一種簡單無副作用的同源跨頁面數據同步方案

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"背景"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"提起這個方案,還要從某個風和日麗的早晨說起。那日小編正忙着手上的各種需求,突然後端的親火急火燎的找到小編,說是有一個重要的用戶,在使用 Word 在線編輯文檔功能時,發現保存的文件被篡改了。一聽到這,我心想這下攤上事兒了,妥妥的線上故障,但還是故作鎮定的開始排查是什麼問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"經過了日以繼夜的排查後,小編髮現是由於用戶同時打開了兩個在線編輯頁面,並且在 A 頁面的在線編輯工具還未關閉的情況下,去 B 頁面也打開了在線編輯工具。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"說到這個在線編輯工具,它叫 "},{"type":"codeinline","content":[{"type":"text","text":"pageOffice"}]},{"type":"text","text":",當他在線被觸發啓動時,會在本地打開一個類似軟件的窗口,啓動一個相對獨立的服務。且這個服務前端通過 Web SDK 提供的 API 能進行控制的餘地非常小,唯一的通信方式只有 "},{"type":"codeinline","content":[{"type":"text","text":"pageOffice"}]},{"type":"text","text":" 中操作觸發頁面上的回調函數。在和 "},{"type":"codeinline","content":[{"type":"text","text":"pageOffice"}]},{"type":"text","text":" 的客服進行了一系列如同太極的溝通後,我們還是沒能解決如何知道用戶已經打開了 "},{"type":"codeinline","content":[{"type":"text","text":"pageOffice"}]},{"type":"text","text":" 並且阻止用戶在另一個頁面觸發打開工具的方法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"初探"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上文提到的, "},{"type":"codeinline","content":[{"type":"text","text":"pageOffice"}]},{"type":"text","text":" 打開以後就成爲了相對獨立的個體,於是乎,小編對它直接的各種軟磨硬泡都宣告失敗。進而小編放棄了探索對它的控制,轉而思考兩個頁面之間通信的控制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"平時咱們對一個方法是否運行過,最常用的方式就是 “狀態開關”。即存儲用一個變量,類似於  "},{"type":"codeinline","content":[{"type":"text","text":"ifOpen"}]},{"type":"text","text":" 之類的,將其設置爲 "},{"type":"codeinline","content":[{"type":"text","text":"ture"}]},{"type":"text","text":" 去記錄當前方法已運行,再在其運行結束時設置爲 "},{"type":"codeinline","content":[{"type":"text","text":"false"}]},{"type":"text","text":",即可完成一個閉環。而我們這次除了以上條件,還需要讓別的頁面也擁有這個變量,才能阻止別的頁面在這個方法運行時再次觸發這個方法。這聽起來有點繞,不過下面有一個小圖解來解釋我們這次問題的初步解決方案。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/95\/957247c7a0ef2f636f1b3abe5f9aa4d3.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"顯而易見的,此處應有一個跨頁面通信的方案,但是由於這是同一個頁面上的功能,所以我們可以選擇最簡便的方案。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"提到跨頁面數據存儲,聰明的你們肯定會想到本地存儲 "},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":",提到"},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 小編就會想起它的兄弟 "},{"type":"codeinline","content":[{"type":"text","text":"sessionStorage"}]},{"type":"text","text":",那就大致回顧一下它們兩的特性吧:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"localStorage:持久的,相同的協議、主機名、端口(同源)能增刪改查,數據不會自動清除;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"sessionStorage:臨時的,除了同源外還要在同一窗口下才能增刪改查,數據會在窗口關閉時自動清除。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看到這裏想必大家已經看出來,本地存儲 "},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 完全可以滿足上圖中描述的功能。但是回想一下題目中提到的 "},{"type":"codeinline","content":[{"type":"text","text":"副作用"}]},{"type":"text","text":" 一詞,大家是否心中暗想此事必不簡單。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"小編解釋一下:首先,由於 "},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 不會自動清除的特性,當用戶再次進入頁面時,之前保存的 "},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 裏的數據會還在;其次,之前提到過,pageOffice 打開後就獨立了,所以,這兩個條件結合後就存在這樣一個場景 —— 在 pageOffice 還在打開的時候,用戶先把頁面關閉了,之後再關閉 "},{"type":"codeinline","content":[{"type":"text","text":"pageOffice"}]},{"type":"text","text":",此時,頁面已經不存在了,所以 pageOffice 關閉時觸發的回調函數,此時已經通知不到頁面去改變存在 "},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 裏的變量。而再下一次打開頁面時,由於"},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 存的數據還是上次未關閉 "},{"type":"codeinline","content":[{"type":"text","text":"pageOffice"}]},{"type":"text","text":" 時的 "},{"type":"codeinline","content":[{"type":"text","text":"ifOpen = false"}]},{"type":"text","text":", 所以,如果用戶不自主清除本地緩存,將再也打不開 "},{"type":"codeinline","content":[{"type":"text","text":"pageOffice"}]},{"type":"text","text":",小編把這種關閉頁面在未來可能會造成負面影響的數據稱爲 "},{"type":"codeinline","content":[{"type":"text","text":"副作用"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"構思"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了清除上述方案帶來的副作用,小編廢寢忘食圍繞副作用刪除的時機想到了幾種方案:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"方案一:"},{"type":"text","text":"用 "},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 儲存一條有當前打開頁面 Id 的數組,當頁面關閉就過濾掉關閉頁面的 Id,關閉頁面直到最後數組長度爲 1,並且 Id 就是當前頁面的 Id 時,就清除掉"},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 中副作用的數據。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個方案的缺陷就是,我們無法確定頁面的關閉時機,現有的在頁面關閉時能觸發的事件是beforeunload,但是非常不理想的是,這個事件在頁面刷新的時候也會觸發,如果刷新頁面則會產生預期外的效果,這並不是我們想要的,即使在這個事件中區分當前觸發的是刷新還是關閉也是不太合理的,所有最後還是選擇更換別的方案。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"方案二:"},{"type":"text","text":"由於關閉頁面的時機無法確定,所以小編考慮將其轉存爲頁面上的變量或者換一種儲存方式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"查閱了和 "},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 有關的內容之後,發現現存有這麼一個神奇的事件叫做 "},{"type":"codeinline","content":[{"type":"text","text":"storage"}]},{"type":"text","text":" 事件,仔細閱讀關於這個事件的相關文獻後發現其有幾個特點:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,它需要在同一瀏覽器打開兩個同源的頁面"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其次,兩個頁面都註冊了這個事件,並且有 localStorage 的變化,事件在其他頁面返回最新變化的 localStorage 的 Key 和 Value"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,這個事件並不是用來監聽當前頁面自己的 localStorage 變化的"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看起來這個事件完全就是考慮到了我們轉存 "},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 準備的,小編心裏頓時就覺得怎麼會有這麼善解人意的事件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然有了這個事件的存在,但是我們該如何順利的幫助 "},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 轉型呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"回想起上文提到的 "},{"type":"codeinline","content":[{"type":"text","text":"sessionStorage"}]},{"type":"text","text":" 這個會話存儲,一想到它能夠在窗口關閉時自動清除,小編就想用它搞點事情。順便一提,頁面上的變量也是可以在頁面關閉時自動清除的,不過當沒有兩個頁面的時候,這種事件觸發的變量一刷新就會丟失,但是 "},{"type":"codeinline","content":[{"type":"text","text":"sessionStorage"}]},{"type":"text","text":" 刷新還是會保留在當前頁面存儲中,於是,小編就萌生了這樣一個 "},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"sessionStorage"}]},{"type":"text","text":" 聯合使用的想法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"實現"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個方案最終的目的就是要把 "},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 中的數據都轉到 "},{"type":"codeinline","content":[{"type":"text","text":"sessionStorage"}]},{"type":"text","text":",簡單來說也就是跨頁面的 "},{"type":"codeinline","content":[{"type":"text","text":"sessionStorage"}]},{"type":"text","text":" 的數據同步,而 "},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 就是我們跨頁面的一座橋樑。所以,方案基本的實現原理就是:當數據變化時,我們首先要做的就是把數據存在當前頁的 "},{"type":"codeinline","content":[{"type":"text","text":"sessionStorage"}]},{"type":"text","text":" 裏,並觸發一次 "},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 的變化即存一次數據到"},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 裏,通過 "},{"type":"codeinline","content":[{"type":"text","text":"storage"}]},{"type":"text","text":" 將數據運輸到另一個頁面。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"值得注意是,"},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 的轉型就是爲了刪除副作用,所以當把數據存入"},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 後,下一步就是直接清除存入 "},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 裏的數據。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這裏小編封裝了一個函數,數據傳的是一個對象,這樣就可以一次同步多個數據啦,先進入一下圖解環節,讓大家有個初步的理解。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/e4\/e4149912a35126bad1e153c1e61c6200.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原理函數:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 觸發事件,需要同步數據變化時的事件\nfunction setSessionStorage(payload) {\n const data = JSON.stringify(payload);\n \/\/ 同步當前頁面數據變化\n sessionStorage.setItem('setSessionStorage', data);\n \/\/ 觸發localStorage的change事件將數據同步到其他頁面\n localStorage.setItem('setSessionStorage', data);\n \/\/ 刪除副作用\n localStorage.removeItem('setSessionStorage');\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後就是我們的橋樑 storage 核心事件的實現:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於把數據存入 "},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 後,下一步就是直接清除存入 "},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 裏的數據,清除 "},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 也是會進入這個函數的,只要校驗此時的值爲空時不將數據同步即可。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 監聽的storage變化的事件\nfunction storageChange(e) {\n \/\/ 校驗null是爲了在清除localStorage時不產生效果\n const ifNull = e.newValue === null && e.newValue === 'null';\n \/\/ 獲取從別的頁面傳遞過來的數據,並將數據同步到當前頁\n if (e.key === 'setSessionStorage' && !ifNull) {\n sessionStorage.setItem('setSessionStorage', e.newValue);\n }\n\n \/\/ 頁面初始化時觸發一次change事件將數據同步到其他頁面\n if (e.key === 'getSessionStorage' && !ifNull) {\n \/\/ 獲取當前頁的sessionStorage\n const currentSessionStorage = sessionStorage.getItem('setSessionStorage');\n \/\/ 其他頁面初始化時,已存在的標籤頁會觸發getSessionStorage事件\n \/\/ 將sessionStorage儲存在localStorage並觸發其他頁面的change事件,同時傳遞參數\n localStorage.setItem('setSessionStorage', currentSessionStorage);\n localStorage.removeItem('setSessionStorage');\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏還有一點要注意的是,我們同源跨頁面的場景一般兩個頁面都不是同時開啓的,又由於我們刪掉了 "},{"type":"codeinline","content":[{"type":"text","text":"localStorage"}]},{"type":"text","text":" 裏的數據,所以,在另一個頁面打開時,我們需要進行一次數據的同步,這就是上文的 "},{"type":"codeinline","content":[{"type":"text","text":"storage"}]},{"type":"text","text":" 事件中下部分函數的功能。這部分可能會有點繞,所以小編還是貼心的準備了一份圖解供大家參考。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/fd\/fd8ab31879a73852b41c6633a7b4ce1d.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"初始化函數部分:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function init() {\n \/\/ 初始化監聽localStorage的change事件\n window.addEventListener('storage', storageChange);\n \/\/ 頁面初始化時觸發一次change事件將數據同步到其他頁面\n localStorage.setItem('getSessionStorage', 'any');\n \/\/ 刪除副作用\n localStorage.removeItem('getSessionStorage');\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,不管在頁面哪個地方,只要不關閉窗口,只需要一行獲取當前 sessionStorage 的代碼即可。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 當前sessionStorage儲存的數據\nconst currentSessionStorage = sessionStorage.getItem('setSessionStorage');\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣,一種簡單無副作用的同源跨頁面數據同步方法就實現啦~"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"頭圖:Unsplash"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者:凱譯"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:https:\/\/mp.weixin.qq.com\/s\/Q90HS8UWbBkFSSdkN8gkVA"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:一種簡單無副作用的同源跨頁面數據同步方案"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"來源:政採雲前端團隊 - 微信公衆號 [ID:Zoo-Team]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"轉載:著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章