如何創建可擴展和可維護的前端架構

{"type":"doc","content":[{"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":"link","attrs":{"href":"https:\/\/dev.to\/kevtiq\/interfacing-your-ui-components-5c52","title":null,"type":null},"content":[{"type":"text","text":"可重用的 UI 組件"}]},{"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":"必須改變。我可以提高搜索技能,或者更熟練地使用 Visual Studio Code。但我並不是唯一在前端工作的人。所以,我們需要對前端項目進行設置。要讓它們變得更易於維護和擴展。那意味着我們可以對當前特性進行修改,但也可以更快地添加新特性。"}]},{"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":"link","attrs":{"href":"https:\/\/en.wikipedia.org\/wiki\/Software_design_pattern","title":null,"type":null},"content":[{"type":"text","text":"架構模式"}]},{"type":"text","text":"。"},{"type":"link","attrs":{"href":"https:\/\/martinfowler.com\/bliki\/BoundedContext.html","title":null,"type":null},"content":[{"type":"text","text":"領域驅動開發"}]},{"type":"text","text":"(domain-driven development,DDD)和"},{"type":"link","attrs":{"href":"https:\/\/en.wikipedia.org\/wiki\/Separation_of_concerns","title":null,"type":null},"content":[{"type":"text","text":"關注點分離"}]},{"type":"text","text":"(separation of concerns,SoC)是目前使用的兩個概念。這兩個概念給前端開發帶來了巨大價值。在 DDD 中,你試着把相似的特性分組合起來,並儘量使它們和其他組(比如模塊)解耦。而在 SoC 中,例如,我們可以分離邏輯、試圖和數據模型(例如,使用 MVC 或 MVVM 設計模式)。"}]},{"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":"希望現代的前端應用程序能完成越來越多的繁重工作。當複雜度增加時,Bug 也會變得更加頻繁。由於用戶和前端的交互,我們需要一個既可維護又可擴展的可靠架構。在這一點上,我的首選架構是模塊化和領域驅動的。記住,我的想法也許會改變,但這是我此刻首選的方式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/1e\/1ec192923c89154cd4cf513f0a205aa3.jpeg","alt":"image.png","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":"當用戶與我們的應用交互時,應用將路由引導用戶到正確的模塊。每一個模塊都被完全包含。然而,如果用戶想要使用一個應用,而非幾個小應用,就會有一些藕合。該耦合存在於特定的特性或業務邏輯中。有幾個特性可以在模塊間共享。你可以將該邏輯放在應用層。也就是說,每個模塊可以選擇與應用層進行交互。例如,需要通過客戶端的 API 連接到後端,或者設置 API 網關。"}]},{"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":"app"}]},{"type":"text","text":" 目錄下。並且所有的模塊都有一個目錄,位於 "},{"type":"codeinline","content":[{"type":"text","text":"modules"}]},{"type":"text","text":" 目錄下。不依賴業務邏輯的可重複使用的 UI 組件(如表格)在 "},{"type":"codeinline","content":[{"type":"text","text":"components"}]},{"type":"text","text":" 目錄下。"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"app\/\nassets\/\ncomponents\/\nlib\/\nmodules\/\nstyles\/"}]},{"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":"assets"}]},{"type":"text","text":"(如圖片)或者"},{"type":"codeinline","content":[{"type":"text","text":"lib"}]},{"type":"text","text":" 中的輔助函數。輔助函數可以非常簡單。它們可以將某些東西轉換爲某種格式,或者幫助處理對象。但更復雜的代碼可以存放於 "},{"type":"codeinline","content":[{"type":"text","text":"lib"}]},{"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":"CSS-in-JS"}]},{"type":"text","text":" 或"},{"type":"link","attrs":{"href":"https:\/\/www.styled-components.com\/","title":null,"type":null},"content":[{"type":"text","text":"樣式組件"}]},{"type":"text","text":"之類的東西,但是我更喜歡普通的 CSS。爲什麼呢?無需 JavaScript,我們可以使用 CSS 和 HTML 解決很多 UI 問題。當我們應用 SoC 的概念時,這會變得更加容易。此外,在一個地方維護 CSS 使你更容易維護,因爲你可以減少重複的工作。它要求一個穩定的 CSS 架構。儘管我會在另一篇博文中討論這個問題,但是我的 CSS 架構是基於 "},{"type":"link","attrs":{"href":"https:\/\/csswizardry.com\/2018\/11\/itcss-and-skillshare\/","title":null,"type":null},"content":[{"type":"text","text":"Harry Roberts 的 ITCSS"}]},{"type":"text","text":"。"}]},{"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":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/3b\/3b3ae92028cddc675f9f05ead7d9bb80.jpeg","alt":"image.png","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":"應用層由兩部分組成:存儲和客戶端 API。存儲是我們的全局應用狀態。這個狀態保存着不同模塊在同一時間可以存取的數據。即使在屏幕上不需要這些數據,它也會持續存在於存儲中。正如你所看到的,每一個發送到存儲的更新請求都可以通過一連串的邏輯。這就是我們所說的中間件。這是 "},{"type":"link","attrs":{"href":"https:\/\/redux.js.org\/advanced\/middleware","title":null,"type":null},"content":[{"type":"text","text":"Redux"}]},{"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":"有時候,需要通過外部服務中的數據對存儲的傳入請求進行增強。在 Redux 中,我們使用 "},{"type":"codeinline","content":[{"type":"text","text":"Promise"}]},{"type":"text","text":" 處理這個調用。它可能是後端服務,但是它也可能是公共的第三方 API。有些情況下,只需使用瀏覽器 "},{"type":"codeinline","content":[{"type":"text","text":"fetch"}]},{"type":"text","text":" API 就可以實現單一目的的 REST 調用。如果希望使用同一個 API 來執行不同的調用,那麼創建 API 客戶端定義是個不錯的想法。"}]},{"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":"基本的 API 客戶端處理外部請求、響應和錯誤。你甚至可以讓它爲你提供有關請求狀態的信息(例如,加載)。不過,更復雜的 API 客戶端可以處理更多的事情。有些 API 通過 web-socket 連接甚至是 GraphQL API。在這種情況下,你將擁有更多的配置選項,如下圖所示。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/5b\/5bc3ab6705fadbb10b6039a47517089d.jpeg","alt":"image.png","title":"","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":"對於更加複雜的 API 客戶端,我們可以通過中間件修改所有發出的請求(例如,添加認證頭)。響應可以由後件修改(比如更改數據結構)。更改響應之後,我們將其存儲在客戶端的緩存中,這就像應用存儲一樣。有什麼不同嗎?緩存只處理傳入的 API 數據,而我們可以把任何數據放入應用存儲裏。"}]},{"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":"link","attrs":{"href":"https:\/\/kubernetes.io\/","title":null,"type":null},"content":[{"type":"text","text":"Kubernetes"}]},{"type":"text","text":" 集羣之上的 API 網關,還是一個單一的單體後端。但是有時候我們需要連接到不同的外部服務。使用這種架構,我們可以創建大量的 API 客戶端。每個 API 客戶端都有緩存、中間件和後件。我們應用的不同部分應該能夠與這些 API 客戶端中的每一個進行交互。"}]},{"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":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"app\/ \n api\/\nconfig\/ \n store\/\npubsub\/ \n schemas\/ \n index.js"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"app"}]},{"type":"text","text":" 中的兩個目錄現在聽起來應該很熟悉:"},{"type":"codeinline","content":[{"type":"text","text":"api"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"store"}]},{"type":"text","text":"。這兩個目錄保存了與前面描述的用例有關的所有內容。"},{"type":"codeinline","content":[{"type":"text","text":"config"}]},{"type":"text","text":" 存放靜態定義和配置(比如常量),用於整個應用。"},{"type":"codeinline","content":[{"type":"text","text":"schemas"}]},{"type":"text","text":" 描述了 JavaScript 對象的特定數據結構。這在使用 TypeScript 或 JavaScript 時都可以使用。應用的所有通用模式都存儲在 "},{"type":"codeinline","content":[{"type":"text","text":"schemas"}]},{"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":"codeinline","content":[{"type":"text","text":"pubsub"}]},{"type":"text","text":" 是一個很好的例子,它可以擴展前端的基本架構。"},{"type":"codeinline","content":[{"type":"text","text":"pubsub"}]},{"type":"text","text":" 可以用於模塊通信或管理預定作業。因爲它對於應用的核心很重要,所以它位於 "},{"type":"codeinline","content":[{"type":"text","text":"app"}]},{"type":"text","text":" 目錄內。最後,我們得到了 "},{"type":"codeinline","content":[{"type":"text","text":"index.js"}]},{"type":"text","text":" 文件。通過這個文件,我們可以添加 "},{"type":"codeinline","content":[{"type":"text","text":"app"}]},{"type":"text","text":" 目錄中的所有函數和常量。這就是說,這個文件的功能是進入應用邏輯的入口點。"}]},{"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":"介紹了應用層之後,就剩下模塊了。詳細的架構圖已經顯示了一個模塊的內部結構。如果應用的路由指向一個特定的模塊時,這個模塊就會決定路由應該如何繼續。模塊的路由決定哪個頁面應該顯示。一個頁面包括許多 UI 組件,也就是用戶在屏幕上看到的內容。"}]},{"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":"在本例中,頁面與 UI 組件沒有任何區別。它是一個大的 UI 組件。然而,其他模塊可以與組件(和動作)交互,但不能與頁面交互。只有使用嵌套路由才能使來自不同模塊的頁面相互作用。這就是說,你將模塊的路由放在不同模塊的頁面中。"}]},{"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":"組件通過動作與應用層交互。這些動作可能表現爲各種形式。它們可以是普通的 JavaScript 函數、Redux 相關函數或者 React Hooks。有時候,你有一些小的實用函數專門用於某些模塊。如果是這樣,你可以將它們放到 "},{"type":"codeinline","content":[{"type":"text","text":"actions"}]},{"type":"text","text":" 目錄下,也可以爲模塊創建一個專門的 "},{"type":"codeinline","content":[{"type":"text","text":"utils"}]},{"type":"text","text":" 目錄。下面顯示了項目的模塊結構:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"users\/ \n actions\/ \n components\/\nconfig\/ \n constants.js \n routes.js \n tables.js \n forms.js \n pages\/\ngql\/ \n schemas\/ \n index.js"}]},{"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":"config"}]},{"type":"text","text":"或 "},{"type":"codeinline","content":[{"type":"text","text":"schema"}]},{"type":"text","text":" 目錄下。在使用 GraphQL 時,可以有查詢和變異的定義。這些應該放在 "},{"type":"codeinline","content":[{"type":"text","text":"gql"}]},{"type":"text","text":" 目錄下(或者一個具有相似用途的目錄)。添加 "},{"type":"codeinline","content":[{"type":"text","text":"interface.js"}]},{"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":"codeinline","content":[{"type":"text","text":"index.js"}]},{"type":"text","text":" 作爲 "},{"type":"codeinline","content":[{"type":"text","text":"app"}]},{"type":"text","text":" 目錄的 "},{"type":"codeinline","content":[{"type":"text","text":"index.js"}]},{"type":"text","text":"。在這裏,我們描述了供他人訪問的所有的組件、動作和常量。"}]},{"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":"並不是每個模塊都需要擁有上述所有的目錄和文件。比如,有些模塊不需要頁面,因爲它們只包括組件和動作。“files”模塊就是一個很好的例子。這個模塊結合了組建和動作來查看和上傳文件。一個例子是一個拖放文件的區域,將結果上傳到一個 blob 存儲。它可以成爲可重複使用的組件。但是,文件的實際上傳取決於我們能夠使用的服務。我們通過將 UI 組件和上傳文件的實際動作結合起來,創建了一個小的包含模塊。將組件與業務邏輯結合在一起時,我們將其轉換爲模塊。"}]},{"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":"index.js"}]},{"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":"UserDropdown"}]},{"type":"text","text":" 組件。現在我們只在用戶周圍更改一個組件時更改。因此有時候我們需要選擇公開的內容:動作或組件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/5f\/5f7a80025790e35456b6973339d6c553.jpeg","alt":"image.png","title":"","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":"codeinline","content":[{"type":"text","text":"pubsub"}]},{"type":"text","text":"。在這個模式下,不能共享組件,但是我們可以共享數據。上面的圖片說明了它的工作原理。再一次強調一下,這是一種高級模式,僅當你想要走微型前端路線或者需要的時候。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"UI 組件剖析"}]},{"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":"還缺少最後一個細節層面,那就是 UI 組件的架構。我在以前的博文中已經對此進行過描述。你可以從這種解剖圖中看到一些我們已經廣泛應用的概念。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/93\/937e6c53fffa428e957267ab932c53dd.jpeg","alt":"image.png","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":"前端是用戶的第一個入口點。當前端項目特性增加時,我們也引入了更多的Bug。但是我們的用戶期望沒有Bug,新特性也會更快。那不可能。但是,只要有了一個好的架構,我們就能儘可能達到這個目標。"}]},{"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":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Kevin Pennekamp,富有創意的前端工程師。喜歡CSS,並遵循一些基本的工程原則。"}]},{"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":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/dev.to\/crinklesio\/how-to-create-a-scalable-and-maintainable-front-end-architecture-4f47","title":null,"type":null},"content":[{"type":"text","text":"https:\/\/dev.to\/crinklesio\/how-to-create-a-scalable-and-maintainable-front-end-architecture-4f47"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章