科學怪人遷移:與框架無關的方法(第 2 部分)
已發表: 2022-03-10在本文中,我們將按照上一部分的建議,通過執行應用程序的逐步遷移來測試所有理論。 為了使事情變得簡單,減少不確定性、未知性和不必要的猜測,對於遷移的實際示例,我決定在一個簡單的待辦事項應用程序上演示該實踐。
一般來說,我假設您對通用待辦事項應用程序的工作方式有很好的理解。 這種類型的應用程序非常適合我們的需求:它是可預測的,但所需的組件數量最少,可以展示科學怪人遷移的不同方面。 但是,無論您的實際應用程序的大小和復雜性如何,該方法都具有良好的可擴展性,並且應該適用於任何規模的項目。
對於本文,作為起點,我從 TodoMVC 項目中選擇了一個 jQuery 應用程序 — 很多人可能已經熟悉了這個示例。 jQuery 已經足夠老了,可能會反映您項目的真實情況,最重要的是,它需要大量維護和 hack 來支持現代動態應用程序。 (這應該足以考慮遷移到更靈活的東西。)
我們要遷移到的這個“更靈活”是什麼? 為了展示一個在現實生活中非常實用的案例,我不得不在當今最流行的兩個框架中進行選擇:React 和 Vue。 然而,無論我選擇哪個,我們都會錯過另一個方向的某些方面。
因此,在這一部分中,我們將運行以下兩個:
- 將 jQuery 應用程序遷移到React ,以及
- 將 jQuery 應用程序遷移到Vue 。
代碼庫
這裡提到的所有代碼都是公開的,您可以隨時訪問。 有兩個存儲庫可供您使用:
- 科學怪人 TodoMVC
此存儲庫包含不同框架/庫中的 TodoMVC應用程序。 例如,您可以在此存儲庫中找到vue
、angularjs
、react
和jquery
等分支。 - 科學怪人演示
它包含幾個分支,每個分支代表應用程序之間的特定遷移方向,可在第一個存儲庫中使用。 特別是像migration/jquery-to-react
和migration/jquery-to-vue
這樣的分支,我們將在稍後介紹。
這兩個存儲庫都在進行中,應定期向其中添加具有新應用程序和遷移方向的新分支。 (您也可以自由貢獻! )遷移分支中的提交歷史結構良好,可以作為附加文檔,其中包含比我在本文中介紹的更多細節。
現在,讓我們動手吧! 我們還有很長的路要走,所以不要指望它會一帆風順。 您可以決定如何閱讀本文,但您可以執行以下操作:
- 從 Frankenstein TodoMVC 存儲庫克隆
jquery
分支,並嚴格遵循以下所有說明。 - 或者,您可以打開一個專門用於從 Frankenstein Demo 存儲庫遷移到 React 或遷移到 Vue 的分支,並跟踪提交歷史記錄。
- 或者,您可以放鬆並繼續閱讀,因為我將在這裡突出顯示最關鍵的代碼,理解過程的機制比實際代碼更重要。
我想再提一次,我們將嚴格遵循本文理論第一部分中提出的步驟。
讓我們潛入水中!
- 識別微服務
- 允許主機到外星人訪問
- 編寫外星微服務/組件
- 圍繞 Alien 服務編寫 Web 組件包裝器
- 用 Web 組件替換主機服務
- 沖洗並重複所有組件
- 切換到外星人
1. 識別微服務
正如第 1 部分所建議的,在這一步中,我們必須將我們的應用程序構建為專用於一項特定工作的小型獨立服務。 細心的讀者可能會注意到,我們的待辦事項應用程序已經很小且獨立,可以單獨代表一個微服務。 如果這個應用程序存在於更廣泛的環境中,我會這樣對待它。 但是請記住,識別微服務的過程完全是主觀的,沒有一個正確的答案。
所以,為了更詳細地了解 Frankenstein Migration 的過程,我們可以更進一步,將這個 to-do 應用拆分成兩個獨立的微服務:
- 用於添加新項目的輸入字段。
該服務還可以包含應用程序的標頭,純粹基於這些元素的定位接近度。 - 已添加項目的列表。
該服務更高級,與列表本身一起,它還包含過濾、列表項的操作等操作。
提示:要檢查選擇的服務是否真正獨立,請刪除代表這些服務中的每一個的 HTML 標記。 確保其餘功能仍然有效。 在我們的例子中,應該可以從沒有列表的輸入字段將新條目添加到localStorage
(此應用程序用作存儲),而即使輸入字段丟失,列表仍會呈現來自localStorage
的條目。 如果您在刪除潛在微服務的標記時應用程序拋出錯誤,請查看第 1 部分中的“如果需要重構”部分,了解如何處理此類情況的示例。
當然,我們可以繼續將第二個服務和項目列表進一步拆分為每個特定項目的獨立微服務。 但是,對於此示例,它可能過於精細。 所以,現在,我們得出結論,我們的應用程序將有兩個服務; 他們是獨立的,每個人都朝著自己的特定任務努力。 因此,我們將應用程序拆分為微服務。
2. 允許主機到外星人訪問
讓我簡要地提醒你這些是什麼。
- 主持人
這就是我們當前應用程序的名稱。 它是用我們即將離開的框架編寫的。 在這種特殊情況下,我們的 jQuery 應用程序。 - 外星人
簡單地說,這是在我們即將遷移到的新框架上對 Host 的逐步重寫。 同樣,在這種特殊情況下,它是一個 React 或 Vue 應用程序。
拆分 Host 和 Alien 時的經驗法則是,您應該能夠在不破壞另一個的情況下開發和部署它們中的任何一個——在任何時間點。
保持主機和外星人彼此獨立對於科學怪人遷移至關重要。 然而,這使得安排兩者之間的溝通有點挑戰。 我們如何允許主機訪問外星人而不將兩者粉碎在一起?
將 Alien 添加為主機的子模塊
儘管有幾種方法可以實現我們需要的設置,但組織項目以滿足此標準的最簡單形式可能是 git 子模塊。 這就是我們將在本文中使用的內容。 我會讓你仔細閱讀 git 中的子模塊是如何工作的,以便了解這種結構的限制和陷阱。
帶有 git 子模塊的項目架構的一般原則應如下所示:
- Host 和 Alien 都是獨立的,並且保存在單獨的
git
存儲庫中; - Host 引用 Alien 作為子模塊。 在此階段,Host 選擇 Alien 的特定狀態(提交)並將其添加為 Host 文件夾結構中的子文件夾。
添加子模塊的過程對於任何應用程序都是相同的。 教授git submodules
超出了本文的範圍,並且與科學怪人遷移本身沒有直接關係。 因此,讓我們簡單地看一下可能的例子。
在下面的代碼片段中,我們以 React 方向為例。 對於任何其他遷移方向,將react
替換為來自 Frankenstein TodoMVC 的分支名稱或在需要時調整為自定義值。
如果您繼續使用原始的 jQuery TodoMVC 應用程序:
$ git submodule add -b react [email protected]:mishunov/frankenstein-todomvc.git react $ git submodule update --remote $ cd react $ npm i
如果您跟隨來自 Frankenstein Demo 存儲庫的migration/jquery-to-react
(或任何其他遷移方向)分支,Alien 應用程序應該已經作為git submodule
在那裡,並且您應該看到相應的文件夾。 但是該文件夾默認是空的,需要更新和初始化註冊的子模塊。
從項目的根目錄(您的主機):
$ git submodule update --init $ cd react $ npm i
請注意,在這兩種情況下,我們都會為 Alien 應用程序安裝依賴項,但這些依賴項會被沙箱化到子文件夾中,不會污染我們的主機。
將 Alien 應用程序添加為 Host 的子模塊後,您將獲得獨立的(就微服務而言)Alien 和 Host 應用程序。 但是,在這種情況下,Host 將 Alien 視為一個子文件夾,顯然,這允許 Host 毫無問題地訪問 Alien。
3. 編寫外星微服務/組件
在這一步,我們必須決定首先遷移什麼微服務,並在 Alien 端編寫/使用它。 讓我們按照我們在步驟 1 中確定的相同服務順序,從第一個開始:用於添加新項目的輸入字段。 然而,在我們開始之前,讓我們同意,除此之外,我們將使用更有利的術語組件,而不是微服務或服務,因為我們正在走向前端框架的前提,並且術語組件遵循幾乎任何現代的定義框架。
Frankenstein TodoMVC 存儲庫的分支包含一個結果組件,該組件表示第一個服務“用於添加新項目的輸入字段”作為 Header 組件:
- React 中的標頭組件
- Vue 中的頭部組件
在您選擇的框架中編寫組件超出了本文的範圍,也不屬於 Frankenstein Migration 的一部分。 但是,在編寫 Alien 組件時需要牢記幾件事。
獨立
首先,Alien 中的組件應該遵循同樣的獨立原則,之前在 Host 端設置:組件不應該以任何方式依賴於其他組件。
互操作性
由於服務的獨立性,主機中的組件很可能以某種成熟的方式進行通信,無論是狀態管理系統、通過某些共享存儲進行通信,還是直接通過 DOM 事件系統進行通信。 Alien 組件的“互操作性”意味著它們應該能夠連接到由 Host 建立的同一通信源,以發送有關其狀態更改的信息並偵聽其他組件的更改。 實際上,這意味著如果您的主機中的組件通過 DOM 事件進行通信,那麼不幸的是,僅在考慮狀態管理的情況下構建您的 Alien 組件對於這種類型的遷移將無法完美運行。
例如,看一下js/storage.js
文件,它是我們的 jQuery 組件的主要通信渠道:
... fetch: function() { return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); }, save: function(todos) { localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); var event = new CustomEvent("store-update", { detail: { todos } }); document.dispatchEvent(event); }, ...
在這裡,我們使用localStorage
(因為這個例子不是安全關鍵的)來存儲我們的待辦事項,一旦對存儲的更改被記錄下來,我們就會在document
元素上調度一個自定義 DOM 事件,任何組件都可以監聽。
同時,在 Alien 方面(比如說 React),我們可以根據需要設置複雜的狀態管理通信。 然而,為將來保留它可能是明智的:為了成功地將 Alien React 組件集成到 Host 中,我們必須連接到 Host 使用的相同通信通道。 在這種情況下,它是localStorage
。 為了簡單起見,我們只是將 Host 的存儲文件複製到 Alien 中,並將我們的組件連接到它:
import todoStorage from "../storage"; class Header extends Component { constructor(props) { this.state = { todos: todoStorage.fetch() }; } componentDidMount() { document.addEventListener("store-update", this.updateTodos); } componentWillUnmount() { document.removeEventListener("store-update", this.updateTodos); } componentDidUpdate(prevProps, prevState) { if (prevState.todos !== this.state.todos) { todoStorage.save(this.state.todos); } } ... }
現在,我們的 Alien 組件可以與 Host 組件使用相同的語言,反之亦然。
4. 圍繞 Alien 服務編寫 Web Component Wrapper
儘管我們現在才到第四步,但我們已經取得了很多成就:
- 我們已將 Host 應用程序拆分為獨立的服務,這些服務已準備好被 Alien 服務替換;
- 我們將 Host 和 Alien 設置為彼此完全獨立,但通過
git submodules
很好地連接; - 我們已經使用新框架編寫了第一個 Alien 組件。
現在是時候在 Host 和 Alien 之間建立一個橋樑,以便新的 Alien 組件可以在 Host 中運行。
第 1 部分的提醒:確保您的主機有可用的包捆綁器。 在本文中,我們依賴於 Webpack,但這並不意味著該技術不適用於 Rollup 或您選擇的任何其他捆綁器。 但是,我將 Webpack 的映射留給您的實驗。
命名約定
如前文所述,我們將使用 Web Components 將 Alien 集成到 Host 中。 在主機端,我們創建一個新文件: js/frankenstein-wrappers/Header-wrapper.js
。 (這將是我們的第一個 Frankenstein 包裝器。)請記住,最好將包裝器命名為與 Alien 應用程序中的組件相同的名稱,例如,只需添加“ -wrapper
”後綴。 你稍後會看到為什麼這是一個好主意,但是現在,讓我們同意這意味著如果 Alien 組件被稱為Header.js
(在 React 中)或Header.vue
(在 Vue 中),則在主機端應稱為Header-wrapper.js
。
在我們的第一個包裝器中,我們從註冊自定義元素的基本樣板開始:
class FrankensteinWrapper extends HTMLElement {} customElements.define("frankenstein-header-wrapper", FrankensteinWrapper);
接下來,我們必須為這個元素初始化Shadow DOM 。
請參閱第 1 部分以了解我們為什麼使用 Shadow DOM。
class FrankensteinWrapper extends HTMLElement { connectedCallback() { this.attachShadow({ mode: "open" }); } }
有了這個,我們已經設置了 Web 組件的所有基本部分,是時候將我們的 Alien 組件添加到組合中了。 首先,在我們的 Frankenstein 包裝器的開頭,我們應該導入負責 Alien 組件渲染的所有位。
import React from "../../react/node_modules/react"; import ReactDOM from "../../react/node_modules/react-dom"; import HeaderApp from "../../react/src/components/Header"; ...
在這裡,我們必須暫停一秒鐘。 請注意,我們不會從 Host 的node_modules
導入 Alien 的依賴項。 一切都來自位於react/
子文件夾中的 Alien 本身。 這就是為什麼第 2 步如此重要的原因,確保主機可以完全訪問 Alien 的資產至關重要。
現在,我們可以在 Web 組件的 Shadow DOM 中渲染 Alien 組件:
... connectedCallback() { ... ReactDOM.render(<HeaderApp />, this.shadowRoot); } ...
注意:在這種情況下,React 不需要其他任何東西。 但是,要渲染 Vue 組件,您需要添加一個包裝節點來包含您的 Vue 組件,如下所示:
... connectedCallback() { const mountPoint = document.createElement("div"); this.attachShadow({ mode: "open" }).appendChild(mountPoint); new Vue({ render: h => h(VueHeader) }).$mount(mountPoint); } ...
原因是 React 和 Vue 渲染組件的方式不同:React 將組件附加到引用的 DOM 節點,而 Vue 用組件替換引用的 DOM 節點。 因此,如果我們為 Vue 執行.$mount(this.shadowRoot)
,它本質上會取代 Shadow DOM。
這就是我們現在對包裝器要做的所有事情。 Frankenstein 包裝器在 jQuery 到 React 和 jQuery 到 Vue 遷移方向的當前結果可以在這裡找到:
- 用於 React 組件的 Frankenstein Wrapper
- Vue 組件的弗蘭肯斯坦包裝器
總結一下 Frankenstein 包裝器的機制:
- 創建自定義元素,
- 啟動 Shadow DOM,
- 導入渲染 Alien 組件所需的一切,
- 在自定義元素的 Shadow DOM 中渲染 Alien 組件。
但是,這不會自動在 Host 中渲染我們的 Alien。 我們必須用我們新的 Frankenstein 包裝器替換現有的 Host 標記。
係好安全帶,這可能不像人們想像的那麼簡單!
5. 用 Web 組件替換主機服務
讓我們繼續將新Header-wrapper.js
文件添加到index.html
並用新創建的<frankenstein-header-wrapper>
自定義元素替換現有的標題標記。
... <!-- <header class="header">--> <!-- <h1>todos</h1>--> <!-- <input class="new-todo" placeholder="What needs to be done?" autofocus>--> <!-- </header>--> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script type="module" src="js/frankenstein-wrappers/Header-wrapper.js"></script>
不幸的是,這不會那麼簡單。 如果您打開瀏覽器並檢查控制台,則會出現Uncaught SyntaxError
等著您。 根據瀏覽器及其對 ES6 模塊的支持,它要么與 ES6 導入相關,要么與 Alien 組件的渲染方式相關。 無論哪種方式,我們都必須對此做一些事情,但是對於大多數讀者來說,問題和解決方案應該是熟悉和清楚的。
5.1。 在需要的地方更新 Webpack 和 Babel
在集成我們的 Frankenstein 包裝器之前,我們應該使用一些 Webpack 和 Babel 魔法。 爭論這些工具超出了本文的範圍,但您可以查看 Frankenstein Demo 存儲庫中的相應提交:
- 遷移到 React 的配置
- 遷移到 Vue 的配置
本質上,我們在 Webpack 的配置中設置了文件的處理以及一個新的入口點frankenstein
,以便在一個地方包含與 Frankenstein 包裝器相關的所有內容。
一旦 Host 中的 Webpack 知道如何處理 Alien 組件和 Web 組件,我們就可以用新的 Frankenstein 包裝器替換 Host 的標記。
5.2. 實際組件的更換
組件的更換現在應該很簡單了。 在主機的index.html
中,執行以下操作:
- 將
<header class="header">
DOM 元素替換為<frankenstein-header-wrapper>
; - 添加一個新腳本
frankenstein.js
。 這是 Webpack 中的新入口點,其中包含與 Frankenstein 包裝器相關的所有內容。
... <!-- We replace <header class="header"> --> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script src="./frankenstein.js"></script>
而已! 如果需要,重新啟動您的服務器並見證集成到 Host 中的 Alien 組件的魔力。
然而,似乎仍然缺少一些東西。 Host 上下文中的 Alien 組件與獨立 Alien 應用程序的上下文中的外觀不同。 它只是沒有樣式。
為什麼會這樣? 組件的樣式不應該和 Alien 組件自動集成到 Host 中嗎? 我希望他們會,但在太多情況下,這取決於。 我們正在進入科學怪人遷移的具有挑戰性的部分。
5.3. 外星人組件樣式的一般信息
首先,具有諷刺意味的是,事情的運作方式沒有錯誤。 一切都按照它的設計工作。 為了解釋這一點,讓我們簡要介紹一下樣式化組件的不同方式。
全局樣式
我們都熟悉這些:全局樣式可以(並且通常是)在沒有任何特定組件的情況下分發並應用於整個頁面。 全局樣式影響所有具有匹配選擇器的 DOM 節點。
全局樣式的一些示例是index.html
中的<style>
和<link rel="stylesheet">
標記。 或者,可以將全局樣式表導入到某個根 JS 模塊中,以便所有組件也可以訪問它。
以這種方式為應用程序設置樣式的問題很明顯:為大型應用程序維護單一樣式表變得非常困難。 此外,正如我們在上一篇文章中看到的,全局樣式很容易破壞直接在主 DOM 樹中呈現的組件,就像在 React 或 Vue 中一樣。
捆綁樣式
這些樣式通常與組件本身緊密耦合,並且很少在沒有組件的情況下分發。 樣式通常與組件位於同一個文件中。 這種類型的樣式很好的例子是 React 或 CSS 模塊中的 styled-components 和 Vue 中單個文件組件中的 Scoped CSS。 然而,無論編寫捆綁樣式的工具種類繁多,其中大多數的基本原理都是相同的:這些工具提供了一種作用域機制來鎖定組件中定義的樣式,這樣樣式就不會破壞其他組件或全局樣式。
為什麼作用域樣式會很脆弱?
在第 1 部分中,當證明在 Frankenstein Migration 中使用 Shadow DOM 的合理性時,我們簡要介紹了作用域與封裝的主題)以及 Shadow DOM 的封裝與作用域樣式工具的不同之處。 但是,我們沒有解釋為什麼作用域工具為我們的組件提供如此脆弱的樣式,現在,當我們面對無樣式的 Alien 組件時,理解它變得至關重要。
現代框架的所有範圍工具都類似地工作:
- 你以某種方式為你的組件編寫樣式,而不考慮範圍或封裝;
- 您通過一些捆綁系統(如 Webpack 或 Rollup)使用導入/嵌入的樣式表運行組件;
- 捆綁器生成獨特的 CSS 類或其他屬性,為您的 HTML 和相應的樣式表創建和注入單獨的選擇器;
- 捆綁器在文檔的
<head>
中創建一個<style>
條目,並將組件的樣式與獨特的混合選擇器放入其中。
差不多就是這樣。 在許多情況下,它確實可以正常工作。 除非它不這樣做:當所有組件的樣式都存在於全局樣式範圍內時,很容易破壞它們,例如,使用更高的特異性。 這解釋了作用域工具的潛在脆弱性,但為什麼我們的 Alien 組件完全沒有樣式?
我們來看看當前使用 DevTools 的 Host。 例如,在使用 Alien React 組件檢查新添加的 Frankenstein 包裝器時,我們可以看到如下內容:
因此,Webpack 確實為我們的組件生成了獨特的 CSS 類。 偉大的! 那麼風格在哪裡呢? 好吧,樣式正是它們被設計的地方——在文檔的<head>
中。
所以一切正常,這是主要問題。 由於我們的 Alien 組件位於 Shadow DOM 中,並且如第 1 部分所述,Shadow DOM 提供了對來自頁面其餘部分和全局樣式的組件的完全封裝,包括那些新生成的組件樣式表,這些樣式表不能跨越陰影邊界和進入外星人組件。 因此,Alien 組件未設置樣式。 然而,現在解決問題的策略應該很清楚了:我們應該以某種方式將組件的樣式放在我們的組件所在的同一個 Shadow DOM 中(而不是文檔的<head>
)。
5.4. 修復外星人組件的樣式
到目前為止,遷移到任何框架的過程都是相同的。 然而,事情從這裡開始出現分歧:每個框架都有關於如何設置組件樣式的建議,因此解決問題的方法也不同。 在這裡,我們討論最常見的情況,但是,如果您使用的框架使用一些獨特的組件樣式化方式,您需要牢記基本策略,例如將組件的樣式放入 Shadow DOM 而不是<head>
。
在本章中,我們將介紹以下修復:
- 在 Vue 中與 CSS 模塊捆綁樣式(Scoped CSS 的策略是相同的);
- 在 React 中將樣式與樣式組件捆綁在一起;
- 通用 CSS 模塊和全局樣式。 我將這些結合起來是因為 CSS 模塊通常與全局樣式表非常相似,並且可以由任何組件導入,從而使樣式與任何特定組件斷開連接。
首先是約束:我們為修復樣式所做的任何事情都不應破壞 Alien 組件本身。 否則,我們將失去外星人和主機系統的獨立性。 因此,為了解決樣式問題,我們將依賴捆綁器的配置或 Frankenstein 包裝器。
Vue 和 Shadow DOM 中的捆綁樣式
如果您正在編寫 Vue 應用程序,那麼您很可能使用的是單文件組件。 如果你也在使用 Webpack,你應該熟悉vue-loader
和vue-style-loader
這兩個加載器。 前者允許您編寫這些單個文件組件,而後者將組件的 CSS 作為<style>
標記動態注入到文檔中。 默認情況下, vue-style-loader
將組件的樣式註入到文檔的<head>
中。 但是,這兩個包都接受配置中的shadowMode
選項,這使我們能夠輕鬆更改默認行為並將樣式(正如選項名稱所暗示的那樣)注入 Shadow DOM。 讓我們看看它的實際效果。
Webpack 配置
Webpack 配置文件至少應該包含以下內容:
const VueLoaderPlugin = require('vue-loader/lib/plugin'); ... module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { shadowMode: true } }, { test: /\.css$/, include: path.resolve(__dirname, '../vue'), use: [ { loader:'vue-style-loader', options: { shadowMode: true } }, 'css-loader' ] } ], plugins: [ new VueLoaderPlugin() ] }
在實際應用程序中,您的test: /\.css$/
塊將更加複雜(可能涉及oneOf
規則)以同時考慮 Host 和 Alien 配置。 但是,在這種情況下,我們的 jQuery 在index.html
中使用簡單的<link rel="stylesheet">
進行樣式設置,因此我們不會通過 Webpack 為 Host 構建樣式,並且只滿足 Alien 是安全的。
包裝器配置
除了 Webpack 配置之外,我們還需要更新我們的 Frankenstein 包裝器,將 Vue 指向正確的 Shadow DOM。 在我們的Header-wrapper.js
中,Vue 組件的渲染應該包括shadowRoot
屬性,該屬性導致我們的 Frankenstein 包裝器的shadowRoot
:
... new Vue({ shadowRoot: this.shadowRoot, render: h => h(VueHeader) }).$mount(mountPoint); ...
更新文件並重新啟動服務器後,您應該在 DevTools 中得到類似的內容:
最後,Vue 組件的樣式在我們的 Shadow DOM 中。 同時,您的應用程序應如下所示:
我們開始得到類似於我們的 Vue 應用程序的東西:與組件捆綁在一起的樣式被注入到包裝器的 Shadow DOM 中,但組件看起來仍然不像預期的那樣。 原因是在原始的 Vue 應用程序中,組件的樣式不僅使用捆綁樣式,而且部分使用全局樣式。 然而,在修復全局樣式之前,我們必須讓我們的 React 集成到與 Vue 相同的狀態。
React 和 Shadow DOM 中的捆綁樣式
因為可以通過多種方式設置 React 組件的樣式,所以在 Frankenstein Migration 中修復 Alien 組件的特定解決方案取決於我們首先為組件設置樣式的方式。 讓我們簡要介紹最常用的替代方案。
樣式化組件
styled-components 是最流行的 React 組件樣式之一。 對於 Header React 組件,styled-components 正是我們為它設置樣式的方式。 由於這是一種經典的 CSS-in-JS 方法,因此沒有像我們對.css
或.js
文件所做的那樣,可以將我們的捆綁器掛鉤到的具有專用擴展名的文件。 幸運的是,styled-components 允許在StyleSheetManager
幫助組件的幫助下將組件的樣式註入自定義節點(在我們的例子中為 Shadow DOM)而不是文檔的head
。 它是一個預定義的組件,與接受target
屬性的styled-components
包一起安裝,定義“用於注入樣式信息的備用 DOM 節點”。 正是我們需要的! 此外,我們甚至不需要更改 Webpack 配置:一切都取決於我們的 Frankenstein 包裝器。
我們應該使用以下幾行更新包含 React Alien 組件的Header-wrapper.js
:
... import { StyleSheetManager } from "../../react/node_modules/styled-components"; ... const target = this.shadowRoot; ReactDOM.render( <StyleSheetManager target={target}> <HeaderApp /> </StyleSheetManager>, appWrapper ); ...
在這裡,我們導入StyleSheetManager
組件(來自 Alien,而不是來自 Host)並用它包裝我們的 React 組件。 同時,我們發送指向我們的shadowRoot
的target
屬性。 而已。 如果你重新啟動服務器,你必須在你的 DevTools 中看到類似這樣的東西:
現在,我們組件的樣式在 Shadow DOM 而不是<head>
中。 這樣,我們的應用程序的渲染現在類似於我們之前在 Vue 應用程序中看到的。
同樣的故事: styled-components 只負責 React 組件樣式的捆綁部分,而全局樣式管理其餘部分。 在我們回顧了另一種樣式組件之後,我們稍後會回到全局樣式。
CSS 模塊
如果您仔細查看我們之前修復的 Vue 組件,您可能會注意到 CSS 模塊正是我們為該組件設置樣式的方式。 However, even if we style it with Scoped CSS (another recommended way of styling Vue components) the way we fix our unstyled component doesn't change: it is still up to vue-loader
and vue-style-loader
to handle it through shadowMode: true
option.
When it comes to CSS Modules in React (or any other system using CSS Modules without any dedicated tools), things get a bit more complicated and less flexible, unfortunately.
Let's take a look at the same React component which we've just integrated, but this time styled with CSS Modules instead of styled-components. The main thing to note in this component is a separate import for stylesheet:
import styles from './Header.module.css'
The .module.css
extension is a standard way to tell React applications built with the create-react-app
utility that the imported stylesheet is a CSS Module. The stylesheet itself is very basic and does precisely the same our styled-components do.
Integrating CSS modules into a Frankenstein wrapper consists of two parts:
- Enabling CSS Modules in bundler,
- Pushing resulting stylesheet into Shadow DOM.
I believe the first point is trivial: all you need to do is set { modules: true }
for css-loader
in your Webpack configuration. Since, in this particular case, we have a dedicated extension for our CSS Modules ( .module.css
), we can have a dedicated configuration block for it under the general .css
configuration:
{ test: /\.css$/, oneOf: [ { test: /\.module\.css$/, use: [ ... { loader: 'css-loader', options: { modules: true, } } ] } ] }
Note : A modules
option for css-loader
is all we have to know about CSS Modules no matter whether it's React or any other system. When it comes to pushing resulting stylesheet into Shadow DOM, however, CSS Modules are no different from any other global stylesheet.
By now, we went through the ways of integrating bundled styles into Shadow DOM for the following conventional scenarios:
- Vue components, styled with CSS Modules. Dealing with Scoped CSS in Vue components won't be any different;
- React components, styled with styled-components;
- Components styled with raw CSS Modules (without dedicated tools like those in Vue). For these, we have enabled support for CSS modules in Webpack configuration.
However, our components still don't look as they are supposed to because their styles partially come from global styles . Those global styles do not come to our Frankenstein wrappers automatically. Moreover, you might get into a situation in which your Alien components are styled exclusively with global styles without any bundled styles whatsoever. So let's finally fix this side of the story.
Global Styles And Shadow DOM
Having your components styled with global styles is neither wrong nor bad per se: every project has its requirements and limitations. However, the best you can do for your components if they rely on some global styles is to pull those styles into the component itself. This way, you have proper easy-to-maintain self-contained components with bundled styles.
Nevertheless, it's not always possible or reasonable to do so: several components might share some styling, or your whole styling architecture could be built using global stylesheets that are split into the modular structure, and so on.
So having an opportunity to pull in global styles into our Frankenstein wrappers wherever it's required is essential for the success of this type of migration. Before we get to an example, keep in mind that this part is the same for pretty much any framework of your choice — be it React, Vue or anything else using global stylesheets!
Let's get back to our Header component from the Vue application. Take a look at this import:
import "todomvc-app-css/index.css";
This import is where we pull in the global stylesheet. In this case, we do it from the component itself. It's only one way of using global stylesheet to style your component, but it's not necessarily like this in your application.
Some parent module might add a global stylesheet like in our React application where we import index.css
only in index.js
, and then our components expect it to be available in the global scope. Your component's styling might even rely on a stylesheet, added with <style>
or <link>
to your index.html
. 沒關係。 What matters, however, is that you should expect to either import global stylesheets in your Alien component (if it doesn't harm the Alien application) or explicitly in the Frankenstein wrapper. Otherwise, the wrapper would not know that the Alien component needs any stylesheet other than the ones already bundled with it.
Caution . If there are many global stylesheets to be shared between Alien components and you have a lot of such components, this might harm the performance of your Host application under the migration period.
Here is how import of a global stylesheet, required for the Header component, is done in Frankenstein wrapper for React component:
// we import directly from react/, not from Host import '../../react/node_modules/todomvc-app-css/index.css'
Nevertheless, by importing a stylesheet this way, we still bring the styles to the global scope of our Host, while what we need is to pull in the styles into our Shadow DOM. 我們如何做到這一點?
Webpack configuration for global stylesheets & Shadow DOM
First of all, you might want to add an explicit test to make sure that we process only the stylesheets coming from our Alien. In case of our React migration, it will look similar to this:
test: /\.css$/, oneOf: [ // this matches stylesheets coming from /react/ subfolder { test: /\/react\//, use: [] }, ... ]
In case of Vue application, obviously, you change test: /\/react\//
with something like test: /\/vue\//
. Apart from that, the configuration will be the same for any framework. Next, let's specify the required loaders for this block.
... use: [ { loader: 'style-loader', options: { ... } }, 'css-loader' ]
Two things to note. First, you have to specify modules: true
in css-loader
's configuration if you're processing CSS Modules of your Alien application.
Second, we should convert styles into <style>
tag before injecting those into Shadow DOM. In the case of Webpack, for that, we use style-loader
. The default behavior for this loader is to insert styles into the document's head. Typically. And this is precisely what we don't want: our goal is to get stylesheets into Shadow DOM. However, in the same way we used target
property for styled-components in React or shadowMode
option for Vue components that allowed us to specify custom insertion point for our <style>
tags, regular style-loader
provides us with nearly same functionality for any stylesheet: the insert
configuration option is exactly what helps us achieve our primary goal. Great news! Let's add it to our configuration.
... { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }
However, not everything is so smooth here with a couple of things to keep in mind.
樣式style-loader
的全局樣式表和insert
選項
如果您查看此選項的文檔,您會注意到,此選項在每個配置中採用一個選擇器。 這意味著,如果您有多個需要將全局樣式拉入 Frankenstein 包裝器的 Alien 組件,則必須為每個 Frankenstein 包裝器指定style-loader
器。 實際上,這意味著您可能必須依賴配置塊中的oneOf
規則來為所有包裝器提供服務。
{ test: /\/react\//, oneOf: [ { test: /1-TEST-FOR-ALIEN-FILE-PATH$/, use: [ { loader: 'style-loader', options: { insert: '1-frankenstein-wrapper' } }, `css-loader` ] }, { test: /2-TEST-FOR-ALIEN-FILE-PATH$/, use: [ { loader: 'style-loader', options: { insert: '2-frankenstein-wrapper' } }, `css-loader` ] }, // etc. ], }
不是很靈活,我同意。 不過,只要您沒有數百個要遷移的組件,這沒什麼大不了的。 否則,它可能會使您的 Webpack 配置難以維護。 然而,真正的問題是我們不能為 Shadow DOM 編寫 CSS 選擇器。
為了解決這個問題,我們可能會注意到insert
選項也可以採用函數而不是普通選擇器來指定更高級的插入邏輯。 有了這個,我們可以使用這個選項將樣式表直接插入到 Shadow DOM 中! 在簡化形式中,它可能看起來類似於:
insert: function(element) { var parent = document.querySelector('frankenstein-header-wrapper').shadowRoot; parent.insertBefore(element, parent.firstChild); }
很誘人,不是嗎? 但是,這不適用於我們的場景,或者遠非最佳。 我們的<frankenstein-header-wrapper>
確實可以從index.html
獲得(因為我們在步驟 5.2 中添加了它)。 但是當 Webpack 處理 Alien 組件或 Frankenstein 包裝器的所有依賴項(包括樣式表)時,Shadow DOM 尚未在 Frankenstein 包裝器中初始化:在此之前處理導入。 因此,將insert
直接指向 shadowRoot 將導致錯誤。
只有一種情況我們可以保證在 Webpack 處理我們的樣式表依賴之前初始化 Shadow DOM。 如果 Alien 組件本身不導入樣式表,而是由 Frankenstein 包裝器來導入它,我們可以在設置 Shadow DOM 後使用動態導入並導入所需的樣式表:
this.attachShadow({ mode: "open" }); import('../vue/node_modules/todomvc-app-css/index.css');
這將起作用:這種導入,結合上面的insert
配置,確實會找到正確的 Shadow DOM 並將<style>
標記插入其中。 然而,獲取和處理樣式表需要時間,這意味著您的用戶在連接速度較慢或設備速度較慢的情況下可能會在您的樣式表在包裝器的 Shadow DOM 中就位之前面臨無樣式組件的片刻。
所以總而言之,即使insert
接受函數,不幸的是,這對我們來說還不夠,我們不得不回退到像frankenstein-header-wrapper
這樣的普通 CSS 選擇器。 但是,這不會自動將樣式表放入 Shadow DOM,並且樣式表位於 Shadow DOM 之外的<frankenstein-header-wrapper>
中。
我們還需要一塊拼圖。
全局樣式表和 Shadow DOM 的包裝器配置
幸運的是,包裝器方面的修復非常簡單:當 Shadow DOM 被初始化時,我們需要檢查當前包裝器中是否有任何掛起的樣式表並將它們拉入 Shadow DOM。
全局樣式表導入的當前狀態如下:
- 我們導入一個必須添加到 Shadow DOM 中的樣式表。 樣式表可以在 Alien 組件本身中導入,也可以在 Frankenstein 包裝器中顯式導入。 例如,在遷移到 React 的情況下,導入是從包裝器初始化的。 但是,在遷移到 Vue 時,類似的組件本身會導入所需的樣式表,我們不必在包裝器中導入任何內容。
- 如上所述,當 Webpack 為 Alien 組件處理
.css
導入時,由於style-loader
的insert
選項,樣式表被注入到 Frankenstein 包裝器中,但在 Shadow DOM 之外。
Frankenstein 包裝器中 Shadow DOM 的簡化初始化,目前(在我們拉入任何樣式表之前)應該類似於以下內容:
this.attachShadow({ mode: "open" }); ReactDOM.render(); // or `new Vue()`
現在,為了避免無樣式組件的閃爍,我們現在需要做的是在 Shadow DOM 初始化之後、Alien 組件渲染之前拉入所有需要的樣式表。
this.attachShadow({ mode: "open" }); Array.prototype.slice .call(this.querySelectorAll("style")) .forEach(style => { this.shadowRoot.prepend(style); }); ReactDOM.render(); // or new Vue({})
這是一個包含很多細節的冗長解釋,但主要是將全局樣式表引入 Shadow DOM 所需的全部內容:
- 在 Webpack 配置中添加帶有
insert
選項style-loader
,指向所需的 Frankenstein 包裝器。 - 在包裝器本身中,在 Shadow DOM 初始化之後、Alien 組件渲染之前拉入“待定”樣式表。
實施這些更改後,您的組件應該擁有所需的一切。 您可能想要添加的唯一內容(這不是必需的)是一些自定義 CSS 來微調 Host 環境中的 Alien 組件。 在 Host 中使用時,您甚至可以為 Alien 組件設置完全不同的樣式。 它超出了本文的重點,但您可以查看包裝器的最終代碼,您可以在其中找到有關如何在包裝器級別覆蓋簡單樣式的示例。
- 用於 React 組件的 Frankenstein 包裝器
- Vue 組件的 Frankenstein 包裝器
您還可以在遷移的這一步查看 Webpack 配置:
- 使用樣式組件遷移到 React
- 使用 CSS 模塊遷移到 React
- 遷移到 Vue
最後,我們的組件看起來完全符合我們的預期。
5.5. Alien 組件的修復樣式總結
這是總結到目前為止我們在本章中學到的知識的好時機。 看起來我們必須做大量工作來修復 Alien 組件的樣式; 然而,這一切都歸結為:
- 修復使用 React 或 CSS 模塊中的樣式組件和 Vue 中的 Scoped CSS 實現的捆綁樣式就像 Frankenstein 包裝器或 Webpack 配置中的幾行代碼一樣簡單。
- 使用 CSS 模塊實現的固定樣式,只需在
css-loader
配置中的一行開始。 之後,CSS 模塊被視為全局樣式表。 - 修復全局樣式表需要在 Webpack 中使用
insert
選項配置style-loader
包,並更新 Frankenstein 包裝器以在包裝器生命週期的正確時刻將樣式表拉入 Shadow DOM。
畢竟,我們已經將樣式正確的 Alien 組件遷移到了 Host 中。 但是,根據您遷移到的框架,只有一件事可能會或可能不會打擾您。
首先是好消息:如果您正在遷移到 Vue ,那麼演示應該可以正常工作,並且您應該能夠從遷移的 Vue 組件中添加新的待辦事項。 但是,如果您正在遷移到 React並嘗試添加新的待辦事項,您將不會成功。 添加新項目根本不起作用,並且沒有條目添加到列表中。 但為什麼? 有什麼問題? 沒有偏見,但 React 對某些事情有自己的看法。
5.6. Shadow DOM 中的 React 和 JS 事件
不管 React 文檔告訴你什麼,React 對 Web Components 都不是很友好。 文檔中示例的簡單性經不起任何批評,任何比在 Web 組件中呈現鏈接更複雜的事情都需要一些研究和調查。
正如您在修復 Alien 組件樣式時所看到的,與 Vue 幾乎開箱即用的 Web 組件不同,React 還沒有為 Web 組件做好準備。 目前,我們已經了解瞭如何讓 React 組件在 Web 組件中至少看起來不錯,但還有一些功能和 JavaScript 事件需要修復。
長話短說:Shadow DOM 封裝事件並重新定位它們,而React 本身不支持 Shadow DOM 的這種行為,因此不會捕獲來自 Shadow DOM 內部的事件。 這種行為有更深層次的原因,如果你想深入了解更多細節和討論,React 的錯誤跟踪器中甚至還有一個未解決的問題。
幸運的是,聰明人為我們準備了解決方案。 @josephnvu 為解決方案提供了基礎,Lukas Bombach 將其轉換為react-shadow-dom-retarget-events
npm 模塊。 所以你可以安裝包,按照包頁面上的說明,更新你的包裝器代碼,你的 Alien 組件將神奇地開始工作:
import retargetEvents from 'react-shadow-dom-retarget-events'; ... ReactDOM.render( ... ); retargetEvents(this.shadowRoot);
如果您想讓它具有更高的性能,您可以製作包的本地副本(MIT 許可證允許這樣做)並限制要偵聽的事件數量,因為它在 Frankenstein Demo 存儲庫中完成。 對於此示例,我知道我需要重新定位哪些事件並僅指定這些事件。
有了這個,我們終於(我知道這是一個漫長的過程)完成了第一個樣式化和功能齊全的 Alien 組件的正確遷移。 給自己喝點好酒。 你應得的!
6. 沖洗並重複所有組件
遷移第一個組件後,我們應該對所有組件重複該過程。 然而,在 Frankenstein Demo 的情況下,只剩下一個:負責渲染待辦事項列表的那個。
新組件的新包裝器
讓我們從添加一個新包裝器開始。 按照上面討論的命名約定(因為我們的 React 組件稱為MainSection.js
),遷移到 React 的相應包裝器應該稱為MainSection-wrapper.js
。 同時,Vue 中類似的組件稱為Listing.vue
,因此遷移到 Vue 時對應的包裝器應該稱為Listing-wrapper.js
。 但是,無論命名約定如何,包裝器本身都將與我們已經擁有的幾乎相同:
- React 列表的包裝器
- Vue 列表的包裝器
我們在 React 應用程序的第二個組件中只介紹了一件有趣的事情。 有時,出於這個或其他原因,您可能希望在組件中使用一些 jQuery 插件。 對於我們的 React 組件,我們引入了兩件事:
- 使用 jQuery 的 Bootstrap 工具提示插件,
-
.addClass()
和.removeClass()
等 CSS 類的切換。
注意:使用 jQuery 添加/刪除類純粹是說明性的。 請不要在實際項目中將 jQuery 用於此場景——而應使用純 JavaScript。
當然,當我們從 jQuery 遷移出來時,在 Alien 組件中引入 jQuery 可能看起來很奇怪,但是您的 Host 可能與本示例中的 Host 不同——您可能會從 AngularJS 或其他任何東西中遷移出來。 此外,組件中的 jQuery 功能和全局 jQuery 不一定是一回事。
然而,問題是即使您確認該組件在 Alien 應用程序的上下文中運行良好,但當您將其放入 Shadow DOM 時,您的 jQuery 插件和其他依賴 jQuery 的代碼將無法運行。
影子 DOM 中的 jQuery
讓我們看一下隨機 jQuery 插件的一般初始化:
$('.my-selector').fancyPlugin();
這樣,所有帶有.my-selector
的元素都將由fancyPlugin
處理。 這種初始化形式假定.my-selector
存在於全局 DOM 中。 但是,一旦將這樣的元素放入 Shadow DOM 中,就像樣式一樣,陰影邊界會阻止 jQuery 潛入其中。 結果,jQuery 無法在 Shadow DOM 中找到元素。
解決方案是為選擇器提供一個可選的第二個參數,該參數定義了 jQuery 搜索的根元素。 這就是我們可以提供shadowRoot
的地方。
$('.my-selector', this.shadowRoot).fancyPlugin();
這樣,jQuery 選擇器和插件就可以正常工作。
請記住,Alien 組件旨在用於以下兩種情況:在沒有 shadow DOM 的 Alien 中和在 Shadow DOM 中的 Host 中。 因此,我們需要一個更統一的解決方案,默認情況下不會假設存在 Shadow DOM。
分析我們的 React 應用程序中的MainSection
組件,我們發現它設置了documentRoot
屬性。
... this.documentRoot = this.props.root? this.props.root: document; ...
因此,我們檢查傳遞的root
屬性,如果它存在,這就是我們用作documentRoot
的內容。 否則,我們回退到document
。
這是使用此屬性的工具提示插件的初始化:
$('[data-toggle="tooltip"]', this.documentRoot).tooltip({ container: this.props.root || 'body' });
作為獎勵,在這種情況下,我們使用相同的root
屬性來定義一個用於注入工具提示的容器。
現在,當 Alien 組件準備好接受root
屬性時,我們在相應的 Frankenstein 包裝器中更新組件的渲染:
// `appWrapper` is the root element within wrapper's Shadow DOM. ReactDOM.render(<MainApp root={ appWrapper } />, appWrapper);
就是這樣! 該組件在 Shadow DOM 中的工作與在全局 DOM 中一樣好。
多包裝器場景的 Webpack 配置
當使用多個包裝器時,令人興奮的部分發生在 Webpack 的配置中。 像 Vue 組件中的 CSS 模塊或 React 中的樣式組件這樣的捆綁樣式沒有任何變化。 但是,全局樣式現在應該有所改變。
請記住,我們說過style-loader
(負責將全局樣式表注入到正確的 Shadow DOM 中)是不靈活的,因為它的insert
選項一次只需要一個選擇器。 這意味著我們應該使用oneOf
規則或類似規則將 Webpack 中的.css
規則拆分為每個包裝器有一個子規則,如果您使用的是 Webpack 以外的捆綁器。
用一個例子來解釋總是更容易,所以這次我們來談談從遷移到 Vue 的那個(然而,在遷移到 React 的那個幾乎相同):
... oneOf: [ { issuer: /Header/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }, ... ] }, { issuer: /Listing/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-listing-wrapper' } }, ... ] }, ] ...
我已經排除了css-loader
,因為它的配置在所有情況下都是相同的。 讓我們來談談style-loader
。 在此配置中,我們將<style>
標記插入*-header-*
或*-listing-*
,具體取決於請求該樣式表的文件的名稱( issuer
中的發布者規則)。 但我們必須記住,渲染 Alien 組件所需的全局樣式表可能會在兩個地方導入:
- 外星人組件本身,
- 弗蘭肯斯坦包裝紙。
在這裡,我們應該了解包裝器的命名約定,如上所述,當 Alien 組件的名稱和相應的包裝器匹配時。 例如,如果我們有一個樣式表,導入一個名為Header.vue
的 Vue 組件中,它會得到正確的*-header-*
包裝器。 同時,如果我們改為在包裝器中導入樣式表,如果包裝器名為Header-wrapper.js
且配置沒有任何更改,則此類樣式表遵循完全相同的規則。 Listing.vue
組件及其對應的包裝器Listing-wrapper.js
也是如此。 使用這個命名約定,我們減少了捆綁器中的配置。
遷移所有組件後,就該進行最後一步的遷移了。
7.切換到外星人
在某些時候,您會發現您在遷移的第一步中確定的組件都被 Frankenstein 包裝器替換了。 沒有真正留下任何 jQuery 應用程序,而您所擁有的本質上是使用 Host 的方式粘合在一起的 Alien 應用程序。
例如,jQuery 應用程序中index.html
的內容部分——在遷移了兩個微服務之後——現在看起來像這樣:
<section class="todoapp"> <frankenstein-header-wrapper></frankenstein-header-wrapper> <frankenstein-listing-wrapper></frankenstein-listing-wrapper> </section>
此時,保留我們的 jQuery 應用程序是沒有意義的:相反,我們應該切換到 Vue 應用程序並忘記我們所有的包裝器、Shadow DOM 和花哨的 Webpack 配置。 為此,我們有一個優雅的解決方案。
讓我們談談 HTTP 請求。 我將在這裡提到 Apache 的配置,但這只是一個實現細節:在 Nginx 或其他任何東西中進行切換應該與在 Apache 中一樣簡單。
想像一下,您的站點從服務器上的/var/www/html
文件夾提供服務。 在這種情況下,您的httpd.conf
或httpd-vhost.conf
應該有一個指向該文件夾的條目,例如:
DocumentRoot "/var/www/html"
要在將 Frankenstein 從 jQuery 遷移到 React 後切換應用程序,您需要做的就是將DocumentRoot
條目更新為以下內容:
DocumentRoot "/var/www/html/react/build"
構建您的 Alien 應用程序,重新啟動您的服務器,您的應用程序直接從 Alien 的文件夾提供服務:React 應用程序從react/
文件夾提供服務。 但是,當然,對於 Vue 或您已遷移的任何其他框架也是如此。 這就是為什麼在任何時候保持主機和外星人完全獨立和正常運行如此重要的原因,因為在這一步你的外星人將成為你的主機。
現在,您可以安全地刪除 Alien 文件夾周圍的所有內容,包括所有 Shadow DOM、Frankenstein 包裝器和任何其他與遷移相關的工件。 有時這是一條艱難的道路,但您已經遷移了您的網站。 恭喜!
結論
在這篇文章中,我們確實經歷了一些崎嶇的地形。 然而,在我們開始使用 jQuery 應用程序之後,我們已經設法將它遷移到 Vue 和 React。 在此過程中,我們發現了一些意想不到且不那麼微不足道的問題:我們必須修復樣式,我們必須修復 JavaScript 功能,引入一些捆綁器配置等等。 但是,它讓我們更好地了解了實際項目中的預期。 最後,我們得到了一個現代應用程序,沒有任何來自 jQuery 應用程序的剩餘部分,儘管在遷移過程中我們有權對最終結果持懷疑態度。
科學怪人遷移既不是靈丹妙藥,也不應該是一個可怕的過程。 它只是定義的算法,適用於許多項目,有助於以可預測的方式將項目轉換為新的和健壯的東西。