不用任何框架開發(fā) Web 應(yīng)用程序,可能嗎?
點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)
過去流行的是 Angular,然后是 React,現(xiàn)在是 Vue.js……其他的像 Ember、Backbone 或 Knockout 什么的幾乎都快消失了。一些標(biāo)準(zhǔn),例如 Web Components,則很少被使用。似乎每年都會(huì)發(fā)布一些新框架,比如 Svelte、Aurelia,而且每個(gè)框架在服務(wù)器端都有對(duì)應(yīng)的對(duì)象(開頭那些框架對(duì)應(yīng)的 NestJS、NextJS 或 Nuxt,Svelte 對(duì)應(yīng)的 Sapper,等等)。非 JavaScript Web 框架(如 Django、Spring、Laravel、Rails 等)就更不用說(shuō)了。甚至還有框架之上的框架(Quasar、SolidJS)、為框架生成組件代碼的框架(Stencil、Mitosis),以及 NCDP(無(wú)代碼開發(fā)平臺(tái),No-Code Development Platform)。
這種多樣性讓想知道哪種技術(shù)值得學(xué)習(xí)的開發(fā)人員和技術(shù)選型決策者感到困惑。
網(wǎng)絡(luò)上經(jīng)常會(huì)出現(xiàn)一些比較這些框架的文章,好像是在幫助我們解開這種困惑。但大多數(shù)作者通常是帶有偏見的,因?yàn)樗麄兛赡堋坝眠^這個(gè)框架”,但“只嘗試了一些其他的框架”。偏見程度較低的作者總是得出“這取決于具體情況”的結(jié)論(取決于性能、工具、支持、社區(qū)等),這實(shí)際上是一種非結(jié)論性的結(jié)論。
即使一些基準(zhǔn)測(cè)試基于同一個(gè)應(yīng)用程序?qū)Σ煌目蚣苓M(jìn)行了比較,也很難獲得真實(shí)的結(jié)果,因?yàn)檫@種基準(zhǔn)測(cè)試受限于被測(cè)試的應(yīng)用程序(比如待辦事項(xiàng)應(yīng)用程序)。
框架看起來(lái)就像是宗教(或者說(shuō)是政治):每一個(gè)框架都假裝自己為開發(fā)者提供了解決方案,但每一個(gè)又都不一樣。它們每一個(gè)都聲稱可以為應(yīng)用程序提供最好的前景,但關(guān)于哪一個(gè)真正名副其實(shí)的爭(zhēng)論又不絕于耳。每一個(gè)框架都要求你遵循特定的規(guī)則,它們之間可能有相似之處,但要從一個(gè)框架轉(zhuǎn)換到另一個(gè)框架總是很難。
現(xiàn)在,讓我們來(lái)看看框架的“無(wú)神論”方法:不使用框架。
我有超過 25 年的專業(yè)軟件開發(fā)經(jīng)驗(yàn),除此之外,本文還將以構(gòu)建真實(shí)純 JS Web 應(yīng)用程序(前端和后端)的經(jīng)驗(yàn)為基礎(chǔ)。
實(shí)際上,這個(gè)想法還很新。早在 2017 年,Django Web 框架聯(lián)合創(chuàng)始人 Adrian Holovaty 就談到了他的框架“疲勞”,以及他為什么離開 Django 去構(gòu)建自己的純 JS 項(xiàng)目。
有人可能會(huì)問,為什么會(huì)有人想要在不使用框架的情況下開發(fā) Web 應(yīng)用程序?為什么不在其他人花了數(shù)年時(shí)間和精力的成果的基礎(chǔ)上做開發(fā)?或者是因?yàn)?NIH(Not Invented Here)綜合癥導(dǎo)致人人都想構(gòu)建定制的框架?
開發(fā)人員并不會(huì)比一般人更傾向于自找麻煩,實(shí)際上,他們可能比任何人都懶:他們只會(huì)想寫更少的代碼(這樣他們就可以更少犯錯(cuò)),想要自動(dòng)化(以避免人為錯(cuò)誤)……

但他們又想要敏捷,也就是能夠輕松、快速地解決問題。
雖然“快速”似乎是框架承諾的東西(為你搭建腳手架,并增加可靠性),但它不是免費(fèi)的:它們想讓你簽署合同,同意支付“稅”費(fèi),并將你的代碼放入“孤井”(“稅和孤井”的說(shuō)法來(lái)自 IBM Carbon 系統(tǒng)設(shè)計(jì)團(tuán)隊(duì)負(fù)責(zé)人 Akira Sudhttps://github.com/carbon-design-system/carbon-web-components#readme)。
使用框架是需要付出成本的:
遵循它們的 API 規(guī)則,這樣它們就可以向你提供服務(wù)。這就是框架的工作方式:你的代碼必須遵守某些規(guī)則,包括或多或少的樣板代碼。你每天要想的不是“如何做這件事”,而是“如何讓框架做(或不做)這件事”。如果你規(guī)避了這些約束,風(fēng)險(xiǎn)就由你自己承擔(dān):如果你通過直接調(diào)用底層 API 來(lái)繞過框架,就不要指望它們能理解你的意圖,也不要指望它們的行為能保持一致。所以,框架會(huì)讓你“專注于業(yè)務(wù)”是一個(gè)虛假的承諾:實(shí)際上,框架的事情你也沒少操心。
如果你想要以下這些東西,就不得不強(qiáng)制進(jìn)行升級(jí):
1) 想要一個(gè)新功能(即使你不需要所有功能,也必須升級(jí)所有東西);
2) 想要修復(fù)一個(gè) bug;
3) 不想失去框架的支持(隨著新版本的發(fā)布,你的應(yīng)用程序所依賴的版本將會(huì)被棄用)。
如果框架出現(xiàn)了 bug,但沒有明確的計(jì)劃修復(fù)日期,這會(huì)讓你感到非常沮喪(可能還會(huì)讓項(xiàng)目面臨風(fēng)險(xiǎn))。第三方提供的框架庫(kù)(如小部件)或插件也不例外,如果你一直使用舊版本,它們與你的應(yīng)用程序的兼容性會(huì)越來(lái)越差。對(duì)于框架維護(hù)者來(lái)說(shuō),維護(hù)向后兼容性已經(jīng)成為一件非常麻煩的事情。他們發(fā)現(xiàn),開發(fā)自動(dòng)升級(jí)代碼的工具(Angular 的 ng-update、React 的原生升級(jí)助手、Facebook 的 jscodesshift 等)會(huì)更有利可圖。
需要學(xué)習(xí)如何使用它們(它們能做或不能做什么、它們的概念、API、生態(tài)系統(tǒng)、工具),包括了解在新版本中可能發(fā)生的變化。如果你選擇的是當(dāng)前最流行的框架,這可能會(huì)容易些,但你不可能了解一個(gè)框架的方方面面。此外,炒作也從來(lái)不會(huì)消停:如果你決定在一個(gè)新應(yīng)用程序中使用另一個(gè)框架(或者更糟的是,從一個(gè)框架遷移到另一個(gè)框架),那么你在舊框架上所有的投入都將歸零。這就是為什么很多企業(yè)項(xiàng)目會(huì)缺乏活力,即使每個(gè)項(xiàng)目都可能與前一個(gè)項(xiàng)目不一樣。已故的 David Wheeler 曾經(jīng)說(shuō)過:“保持兼容性意味著有意重復(fù)別人的錯(cuò)誤”。
將控制權(quán)委托給框架,這是對(duì)框架缺陷的妥協(xié):你可能無(wú)法做任何你想做的事(或防止框架做你不希望它們做的事情)或者你也許不能獲得你想要的性能(因?yàn)轭~外的分層、普適性、更大的代碼體積或向后兼容性需求)。
技能零散化。很多開發(fā)人員要么不太了解底層 API(因?yàn)樗麄兛偸鞘褂每蚣芴峁┑臇|西),要么活在過去(只知道過時(shí)的知識(shí),不知道最新的改進(jìn)和功能)?!肮ぞ叻▌t”常常導(dǎo)致過度設(shè)計(jì),為簡(jiǎn)單的問題構(gòu)建復(fù)雜的解決方案,而構(gòu)建簡(jiǎn)單解決方案的知識(shí)逐漸零散化。在指南的指導(dǎo)下,我們失去了(或者沒有獲得)好的軟件設(shè)計(jì)(原則、模式)文化,并失去(或者沒有獲得)構(gòu)建重要工程的經(jīng)驗(yàn)。就像 CSS 框架(Bootstrap、Tailwind 等)的用戶缺乏 CSS 技能一樣,Web 框架的用戶也注定缺乏現(xiàn)代 Web API 和軟件設(shè)計(jì)經(jīng)驗(yàn)。

一旦你把錢放入框架,就很難把它拿出來(lái)。
除了必須支付“稅”費(fèi)來(lái)獲得框架的好處之外,如果框架沒有標(biāo)準(zhǔn)化,它們還會(huì)帶來(lái)另一個(gè)問題。
因?yàn)樗鼈儚?qiáng)制要求你遵循框架規(guī)則——而且每一條規(guī)則都不一樣——這意味著你的應(yīng)用程序?qū)⑴c一個(gè)專有的生態(tài)系統(tǒng)綁定在一起,也就是用專有 API(及其升級(jí)過程)鎖定你的應(yīng)用程序代碼。這對(duì)于你的項(xiàng)目來(lái)說(shuō)是一個(gè)冒險(xiǎn)的賭注,正如它們所暗示的那樣:
沒有可移植性:將代碼遷移到另一個(gè)框架(或者一個(gè)有重大變化的新版本,甚至是不使用框架)將是非常昂貴的,包括可能需要進(jìn)行重新培訓(xùn)的成本;
你的代碼與其他框架運(yùn)行時(shí)或你想要使用的其他框架組件庫(kù)沒有互操作性:由于規(guī)則不同,大多數(shù)框架彼此之間很難實(shí)現(xiàn)互操作。
當(dāng)然,在項(xiàng)目剛開始時(shí),你可以選擇最流行的框架。對(duì)于一個(gè)短期的項(xiàng)目來(lái)說(shuō),這可能是可以接受的,但對(duì)于長(zhǎng)期項(xiàng)目來(lái)說(shuō)則不然。

框架來(lái)來(lái)去去。從 2018 年開始,每年都有 1 到 3 個(gè)新框架取代舊框架。
不過,標(biāo)準(zhǔn)框架并不存在孤井。在 Web 平臺(tái)(即瀏覽器框架)上,使用標(biāo)準(zhǔn) Web API 可以降低你的投入風(fēng)險(xiǎn),因?yàn)樗鼈兛梢栽诖蠖鄶?shù)瀏覽器上運(yùn)行。即使不是所有的瀏覽器都支持,仍然可以通過 polyfill 來(lái)彌補(bǔ)。
例如,現(xiàn)在的 Web 組件既可移植(幾乎可以在所有瀏覽器中使用),又可互操作(可以被任何代碼使用,包括專有框架),因?yàn)樗鼈兛梢员环庋b成任意的 HTML 元素。不僅具備更好的性能,它們的運(yùn)行時(shí)(自定義元素、陰影 DOM、HTML 模板)還作為瀏覽器的一部分運(yùn)行,所以它們已經(jīng)在那里(不需要下載),并且是原生的。

很少會(huì)有開發(fā)者試圖逃離框架孤井。
如果是為實(shí)現(xiàn)應(yīng)用程序邏輯而創(chuàng)建自己的框架,那就不能說(shuō)框架是不好的:任何應(yīng)用程序都需要實(shí)現(xiàn)自己的業(yè)務(wù)規(guī)則。
如果符合以下這些情況,框架就是好的:
是應(yīng)用程序特有的:任何應(yīng)用程序最終都會(huì)設(shè)計(jì)自己的“業(yè)務(wù)”框架。
成為標(biāo)準(zhǔn),例如,Web 平臺(tái)就是一個(gè)標(biāo)準(zhǔn)的 Web 框架,而 Web 組件框架(lit、stencil、skatejs 等)最終構(gòu)建的組件都符合這個(gè)標(biāo)準(zhǔn)。
添加一些其他解決方案(包括其他框架)所缺少的獨(dú)特價(jià)值。對(duì)于這種情況,你幾乎沒有選擇,這些附加價(jià)值證明了隱含的鎖定成本是合理的。例如,一個(gè)特定于操作系統(tǒng)的框架遵循了操作系統(tǒng)的標(biāo)準(zhǔn),除此之外沒有其他方式可以獲得能夠滿足需求的應(yīng)用程序或擴(kuò)展。
用于構(gòu)建非關(guān)鍵(短期、低質(zhì)量預(yù)期,并且可以接受“稅費(fèi)”和“孤井”)應(yīng)用程序。例如,使用 Bootstrap 構(gòu)建原型、MVP 或內(nèi)部工具。
簡(jiǎn)單地說(shuō),避免使用框架來(lái)構(gòu)建應(yīng)用程序的目標(biāo)是:
通過避免框架的“一刀切”約束來(lái)最大化靈活性。此外,去掉規(guī)則的約束,提升應(yīng)用程序的創(chuàng)造力。大多數(shù)使用 Bootstrap 開發(fā)的 Web 應(yīng)用程序都屬于此類,因?yàn)樗鼈兒茈y擺脫預(yù)定義組件和樣式,最終將很難從其他角度思考問題。
盡量減少對(duì)炒作過度的框架的依賴。不被框架鎖定,才能夠避免可移植性和互操作性方面的問題。
只在需要時(shí)進(jìn)行最細(xì)粒度的操作(例如,不依賴框架的刷新周期),并減少依賴項(xiàng),只使用一些必需的輕量級(jí)庫(kù),以此來(lái)最大化性能。
當(dāng)然,我們的目標(biāo)也不能是“重新發(fā)明輪子”。我們來(lái)看看該怎么做。
那么,如何在沒有框架的情況下開發(fā)應(yīng)用程序呢?
首先,我們必須明確一個(gè)反目標(biāo):不要將“不使用框架構(gòu)建應(yīng)用程序”與“取代框架”混淆起來(lái)了??蚣苁且环N用于托管任意應(yīng)用程序的通用技術(shù)解決方案,所以它們的目標(biāo)并非你的應(yīng)用程序,而是所有的應(yīng)用程序。相反,脫離框架才有可能讓你更專注于你的應(yīng)用程序。

不使用框架開發(fā)應(yīng)用程序并不意味著要重新實(shí)現(xiàn)框架。
要評(píng)估在不使用框架的情況下構(gòu)建應(yīng)用程序的難度,我們要明白:它不像構(gòu)建框架那么困難,因?yàn)橐韵逻@些不是我們的目標(biāo):
構(gòu)建專有的組件模型(實(shí)現(xiàn)特定組件生命周期的容器);
構(gòu)建專有的插件或擴(kuò)展系統(tǒng);
構(gòu)建一個(gè)奇特的模板語(yǔ)法(JSX、Angular HTML 等);
實(shí)現(xiàn)通用的優(yōu)化(變更檢測(cè)、虛擬 DOM);
特定于框架的工具(調(diào)試擴(kuò)展、UI 構(gòu)建器、版本遷移工具)。
因此,構(gòu)建一個(gè)普通的應(yīng)用程序并不是一項(xiàng)艱巨的“重新發(fā)明輪子”的任務(wù),因?yàn)檫@個(gè)“輪子”主要是關(guān)于 API/ 合約、實(shí)現(xiàn)、通用引擎和相關(guān)的優(yōu)化、調(diào)試能力等。放棄通用目標(biāo),專注于應(yīng)用程序的目標(biāo),這意味著你可以擺脫大部分目標(biāo),而這才是真正的“專注于你的應(yīng)用程序”。
那么,我們?cè)撊绾卧O(shè)計(jì)和實(shí)現(xiàn)一個(gè)普通的應(yīng)用程序?因?yàn)榇蠖鄶?shù)應(yīng)用程序都是使用框架構(gòu)建的,所以如果沒有這些熟悉的工具,確實(shí)很難設(shè)計(jì)出一種方法來(lái)實(shí)現(xiàn)類似的結(jié)果。你必須:
改變你的想法:不要使用特定于框架的服務(wù)。對(duì)于一個(gè)普通的應(yīng)用程序來(lái)說(shuō),你可能不需要這些服務(wù)。不需要變更檢測(cè),直接更新 DOM 即可……
用其他技術(shù)替代方案來(lái)執(zhí)行原先使用框架執(zhí)行的常見任務(wù)(更新 DOM、延遲加載等)。
一些作者,如 Jeremy Likness 或 Chris Ferdinandi(被稱為“JS 極客”)也提到過這個(gè)話題。但是,根據(jù)定義,任何一個(gè)普通的應(yīng)用程序都可以選擇(或不選擇)使用其中的一種技術(shù),具體視需求而定。例如,MeetSpace 的作者只需要使用標(biāo)準(zhǔn) API 就足以。
接下來(lái),讓我們來(lái)看看一些常見的“解法”。
標(biāo)準(zhǔn) API 屬于“好的框架”,因?yàn)樗鼈儯?/p>
具備可移植性:它們?cè)谌魏蔚胤蕉伎捎?,如果不可用,可以通過 polyfill 的方式實(shí)現(xiàn)。
具備互操作性:它們可以與其他標(biāo)準(zhǔn)交互,并被用在專有代碼中。
長(zhǎng)期存在:由多個(gè)行業(yè)參與者設(shè)計(jì),而不只是一個(gè)。它們被設(shè)計(jì)得很好,一旦發(fā)布就會(huì)一直存在,使用它們的風(fēng)險(xiǎn)較小。
在大多數(shù)情況下在瀏覽器中都是立即可用的,避免了下載過程。在某些情況下,你可能需要下載 polyfill。但是,與專有框架(注定會(huì)越來(lái)越不流行)不一樣的是,它們的可用性會(huì)越來(lái)越高(逐漸降低下載的必要性)。
在選擇編程語(yǔ)言時(shí),我們要著重考慮標(biāo)準(zhǔn)。JavaScript 經(jīng)過多年的發(fā)展,現(xiàn)在也包含了在其他編程語(yǔ)言中出現(xiàn)的特性,比如 class 關(guān)鍵字和通過 JSDoc 注釋(如 @type)提供有限的類型檢查支持。
很多編程語(yǔ)言可以被編譯成 JavaScript:TypeScript、CoffeeScript、Elm、Kotlin、Scala.js、Haxe、Dart、Rust、Flow 等。它們都為你的代碼添加了不同的價(jià)值(類型檢查、額外的抽象、語(yǔ)法糖)。普通的應(yīng)用出現(xiàn)應(yīng)該使用它們嗎?為了回答這個(gè)問題,讓我們來(lái)看看它們是否隱含了與框架相同的缺點(diǎn):
遵循語(yǔ)法:大多數(shù)編程語(yǔ)言都強(qiáng)制要求這么做(CoffeeScript、Elm、Kotlin 等)。但需要注意的是,它們是 JavaScript 的超集(TypeScript、Flow),你仍然可以用純 JavaScript 編寫你選擇的某些部分。
如果你使用的是非常舊的編程語(yǔ)言(包括 JavaScript)版本,就需要升級(jí),但升級(jí)頻率比框架低很多。
需要學(xué)習(xí)它們的語(yǔ)法。不過,你可以循序漸進(jìn)地學(xué)習(xí)超集編程語(yǔ)言,因?yàn)槟愕拇a的某些部分可以繼續(xù)使用傳統(tǒng) JS。
對(duì)于非超集編程語(yǔ)言來(lái)說(shuō),離散化技能確實(shí)是一個(gè)風(fēng)險(xiǎn)。因?yàn)樗鼈兊木幾g具有普適性,可能不是最優(yōu)的,而你可能沒有意識(shí)到這一點(diǎn)。也許你可以使用更簡(jiǎn)單和高效的 JS 代碼來(lái)完成同樣的操作。
需要對(duì)缺點(diǎn)做出妥協(xié),因?yàn)槲覀儫o(wú)法改變轉(zhuǎn)譯成 JS(或者使用 tsconfig.json 做一點(diǎn)定制)或編譯成 WebAssembly 的過程。有些語(yǔ)言可能還會(huì)忽略 JS 的一些概念。
具備可移植性,因?yàn)橥ǔ4a可以轉(zhuǎn)譯到 ES5(但有時(shí)你不得不妥協(xié),即使你想要轉(zhuǎn)譯到 ES6)。WebAssembly 很新,所有現(xiàn)代瀏覽器都支持它。
提供與其他 JS 代碼的互操作性。例如,Typescript 可以被配置為支持 JS。
在一個(gè)普通的應(yīng)用程序中,我們要小心謹(jǐn)慎地使用非超集語(yǔ)言,因?yàn)樗鼈兓蚨嗷蛏俣茧[含了一些約束。超集語(yǔ)言(TypeScript、Flow)通過避免“要么全有要么全無(wú)”來(lái)最小化這些約束,我們應(yīng)該在它們可以帶來(lái)價(jià)值的地方使用它們。
需要注意的是,在 JavaScript 之上構(gòu)建的語(yǔ)言層意味著我們的工具鏈中又增加了一層復(fù)雜性,可能會(huì)因?yàn)槟承┰蛘兄率。ㄒ娤挛模4送?,在?jīng)過編譯或轉(zhuǎn)譯之后,開發(fā)階段的好處也會(huì)消失(通常在運(yùn)行時(shí)不會(huì)強(qiáng)制執(zhí)行類型或可見性約束檢查)。
基于不“重寫框架”的假設(shè),就會(huì)得出普通的 JS 應(yīng)用程序不應(yīng)該使用開發(fā)庫(kù)的結(jié)論。這是完全錯(cuò)誤的?!爸匦掳l(fā)明輪子”,即從頭開始重寫一切,并不是一個(gè)明智的目標(biāo)。我們的目標(biāo)是消除框架(而不是開發(fā)庫(kù))中隱含的約束,請(qǐng)不要將其與“自己編寫一切”的教條混淆在一起。
因此,如果你自己不能編寫某些代碼(可能是因?yàn)闆]有時(shí)間,或者因?yàn)樾枰嗟膶I(yè)知識(shí)),使用開發(fā)庫(kù)并沒有什么錯(cuò)。你只需要關(guān)心:
模塊化:如果你只需要一小部分功能,就要避免依賴整個(gè)大開發(fā)庫(kù);
避免冗余:在沒有標(biāo)準(zhǔn)的情況下才使用開發(fā)庫(kù),并優(yōu)先選擇實(shí)現(xiàn)了標(biāo)準(zhǔn)的開發(fā)庫(kù);
避免鎖定:不要直接使用開發(fā)庫(kù)的 API,而是把它們包裝在應(yīng)用程序 API 中。
需要注意的是,不要被那些聲稱它們不是框架的文檔或文章所迷惑(因?yàn)樗鼈儭皼]有被明確定義”成框架,或者沒有定義一個(gè)“完整的應(yīng)用程序”):只要隱含了約束,它們就是框架。
Holovaty 說(shuō),只是應(yīng)用模式(不使用框架)來(lái)構(gòu)建軟件是不夠的。
模式是眾所周知的東西,不特定于某種開發(fā)過程。它們本身是自我文檔化的,因?yàn)樗鼈兛梢员挥薪?jīng)驗(yàn)的開發(fā)人員快速識(shí)別出來(lái)。
這里僅舉幾個(gè)例子:
模型、視圖和控制器模式(MVC);
根據(jù)配置創(chuàng)建對(duì)象的工廠模式;
簡(jiǎn)化反應(yīng)式編程的觀察者模式;
用于遍歷集合的迭代器模式;
用于延遲加載、安全檢查的代理模式;
用于封裝操作(可能基于上下文被觸發(fā))的命令模式。
這樣的模式有很多:你可以自由地用它們來(lái)滿足你的需求。如果一個(gè)模式為你的應(yīng)用程序的一個(gè)典型問題提供了典型的解決方案,你一定要用它。更寬泛地說(shuō),任何符合 SOLID 原則和具有良好內(nèi)聚力的東西都有利于應(yīng)用程序的靈活性和可維護(hù)性。
在面試開發(fā)者時(shí),當(dāng)被問及在構(gòu)建一個(gè)普通應(yīng)用程序時(shí)他們主要會(huì)擔(dān)心哪些東西時(shí),他們大多數(shù)會(huì)回答:實(shí)現(xiàn)復(fù)雜的模型變化檢測(cè)和后續(xù)的“視圖”更新。這是典型的“工具法則”效應(yīng),它會(huì)讓你按照框架的思路思考問題,但實(shí)際上你的一些簡(jiǎn)單的需求根本不需要用到框架:
“視圖”只是 DOM 元素。你當(dāng)然可以對(duì)它們進(jìn)行抽象(你也應(yīng)該這樣做),但最終它們也只是抽象而已。
更新它們只是調(diào)用 viewElement.replaceChild(newContent) 的問題,不需要更新更大范圍的 DOM,也不需要重畫或滾動(dòng)。更新 DOM 的方法有好多種,可以插入文本,也可以操作實(shí)際的 DOM 對(duì)象,只要選一個(gè)適合你的就行了。
在普通應(yīng)用程序中,“檢測(cè)”什么時(shí)候需要更新視圖通常是沒有必要的。因?yàn)樵诖蠖鄶?shù)情況下,你只知道在一個(gè)事件之后需要更新什么,然后你直接執(zhí)行這個(gè)命令就可以了。當(dāng)然,在某些情況下,你可能需要通過反轉(zhuǎn)依賴和通知觀察者(見下文)來(lái)進(jìn)行一般性的更新。
開發(fā)人員不希望缺失的另一個(gè)特性是編寫帶有動(dòng)態(tài)部分或監(jiān)聽器的 HTML 片段。
首先,DOM API(如 document.createElement("button"))并不是那么難,而且實(shí)際上比任何模板語(yǔ)言都更強(qiáng)大,因?yàn)槟憧梢匀嬖L問這些 API。編寫很長(zhǎng)的 HTML 片段可能很乏味,如果它們真的很長(zhǎng),可以將它們拆分成更細(xì)粒度的組件。
不過,將這些元素視為模板確實(shí)可以提高可讀性。那么該如何管理它們呢?這里有多種方法:
現(xiàn)在可以在瀏覽器中使用HTML模板了(實(shí)際上從2017年就可以了)。它們提供了構(gòu)建可重用的HTML 片段的能力。這實(shí)際上是Web組件的一部分。
JavaScript從ES6(2015)開始支持模板字面量,你可以很輕松地將值嵌入到字符串中。你可以嵌入原始類型(數(shù)字、字符串,包括其他HTML代碼等),但不能嵌入更復(fù)雜的元素,例如注冊(cè)了監(jiān)聽器的DOM元素。
我們可以借助標(biāo)記模板字面量函數(shù)將復(fù)雜的值(如DOM節(jié)點(diǎn))嵌入到模板中。ObservableHQ已經(jīng)設(shè)計(jì)了一個(gè)非常方便的工具,可以用它編寫html`
${stringOrNode} 這樣的代碼,或者實(shí)現(xiàn)更復(fù)雜的模板,比如html`- ${items.map(item => `
- ${item.title} }
模板中的條件或循環(huán)語(yǔ)句該怎么辦?且不說(shuō)這可能從來(lái)都不是一個(gè)好主意(UI 中不應(yīng)該包含邏輯),你可以(也應(yīng)該)只用 JS 來(lái)實(shí)現(xiàn)邏輯,然后使用上面的技術(shù)將結(jié)果插入到模板中。
現(xiàn)在,我們有了基本的模板,那么該如何將事件綁定到 DOM 節(jié)點(diǎn)呢?這里也有幾種選擇:
HTML事件處理器代碼(
事件處理器API(button.addEventListener("click", myClickHandler))可用于所有通過DOM API或HTML標(biāo)記模板字面量函數(shù)創(chuàng)建的節(jié)點(diǎn)。
那么定制或業(yè)務(wù)事件該怎么辦?如果我需要對(duì)應(yīng)用程序的某個(gè)組件觸發(fā)的一些事件作出反應(yīng)該怎么辦?這里也有多種處理方式:
自定義事件:你可以通過擴(kuò)展 EventTarget 來(lái)創(chuàng)建自己的事件類,并派發(fā)或監(jiān)聽它們,就像“標(biāo)準(zhǔn)”事件一樣。
理論上說(shuō),使用 EventEmitter 也是一種辦法(存在于 Node 中,在瀏覽器中作為庫(kù)存在),但它很少被使用。
觀察者模式:你可以構(gòu)建自己的觀察者,也可以考慮使用 RxJs,它是這方面的標(biāo)準(zhǔn)。你只需要構(gòu)建一個(gè) Subject,并在發(fā)生事件時(shí)通知所有訂閱者,讓訂閱者對(duì)事件做出反應(yīng)。
雖說(shuō)開發(fā)普通的應(yīng)用程序不同于開發(fā)復(fù)雜的基礎(chǔ)設(shè)施(也就是用于托管組件的容器),但如果一些東西在系統(tǒng)中會(huì)多次出現(xiàn),那么將它們?cè)O(shè)計(jì)成可重用組件(與上下文無(wú)關(guān))仍然是一個(gè)好主意。無(wú)論你使用何種技術(shù),也無(wú)論是業(yè)務(wù)還是技術(shù),一定程度粒度的抽象仍然是有用的:將與同一業(yè)務(wù)概念相關(guān)的數(shù)據(jù)和規(guī)則封裝成一個(gè)可重用的對(duì)象,或者構(gòu)建可以在應(yīng)用程序多個(gè)地方進(jìn)行實(shí)例化的小部件,總歸是個(gè)好主意。
創(chuàng)建組件的方法有很多,具體視自己的需求而定。早在 2017 年,Mev-Rael 就提出了很多技巧,用于處理 JavaScript 組件的狀態(tài)、自定義屬性和視圖。當(dāng)然,我們不要拘囿于別人推薦的技術(shù),而是要先考慮自己的需求,然后再選擇合適的技術(shù)。
除了標(biāo)準(zhǔn)的小部件組件(通常是標(biāo)準(zhǔn)的 Web 組件),任何一個(gè)組件都應(yīng)該能夠:
將邏輯和視圖拆分開(通常會(huì)使用 MVC 模式)。把它們混合在一起通常會(huì)導(dǎo)致代碼不易于維護(hù),還會(huì)降低靈活性(例如,如果你想同時(shí)以詳情或表格的形式顯示一條記錄,你的 RecordComponent 只需要使用 DetailRecordView 或 RowRecordView)。
參數(shù)化組件的行為或視圖。
通過觸發(fā)事件的形式通知訂閱者組件中發(fā)生了某些事件(通常是在發(fā)生用戶交互之后)。
同步:如果發(fā)生一些事件,組件應(yīng)該能夠進(jìn)行重繪。這個(gè)使用反應(yīng)式開發(fā)庫(kù)(如 RxJS)可以很容易實(shí)現(xiàn)。
在任何情況下,無(wú)論你選擇了什么樣的設(shè)計(jì)策略,你的組件(或者更具體地說(shuō),它的相關(guān)“視圖”)都必須能夠提供一些 HTML 渲染結(jié)果。你可以使用包含 HTML 代碼的字符串,但 HTMLElement(或 Element)通常是更好的選擇(可讀性高,直接更新,可以綁定事件處理器),而且性能更好(不需要解析)。
此外,你可能希望使用來(lái)自第三方的外部組件。由于專有框架的流行程度較高,它們可以更大程度地利用社區(qū)開發(fā)的庫(kù)和組。它們中的大多數(shù)實(shí)際上與純 JS 實(shí)現(xiàn)的特性(比如 JQuery)并沒有太大不同,但問題是,它們?nèi)狈ゲ僮餍?,所以到最后你?huì)發(fā)現(xiàn)自己需要的其實(shí)是純 JS 或 Web 組件。
所幸的是,這樣的庫(kù)確實(shí)存在,比如 Vanilla JS Toolkit,盡管可能不太常見。在 Web 組件方面,webcomponents.org 列出了 2000 多個(gè)元素。甚至還有普通的 Web 組件,只是它們與我們要討論的不太相關(guān)(更多的是關(guān)注輕量級(jí)實(shí)現(xiàn),而不是互操作性)。
在 SPA 中管理路由需要使用 Web History API。雖然這并不復(fù)雜,但你仍然可能希望將其委托給簡(jiǎn)單的路由器庫(kù),如 Navigo。
你所要做的就是在路由時(shí)用一個(gè) DOM 元素替換另一個(gè) DOM 元素(使用 replaceChildren() 或 replaceWith() 方法)。
按需加載 JavaScript 代碼是任何一個(gè) Web 應(yīng)用程序都需要考慮的問題。你一定不希望為了顯示一個(gè)登錄界面而加載全部的應(yīng)用程序代碼。
早在 2009 年,在 Web 框架出現(xiàn)之前,James Burke(Dojo 開發(fā)者)就發(fā)布了 RequireJS(最開始叫“RunJS”)來(lái)解決這個(gè)問題。從那時(shí)起,隨著模塊化的出現(xiàn),出現(xiàn)了更多的技術(shù)。從 ES6(2015)開始,我們可以動(dòng)態(tài)加載代碼。在 Node 中可以,在瀏覽器中也可以:
{WelcomeModule} = await import("./welcome/ModuleImpl")module = new WelcomeModule()
那么如何將模塊分拆到單獨(dú)的文件中?打包器(如 Webpack)可以為你做這些工作。
需要注意的是,在導(dǎo)入路徑里你應(yīng)該只使用常量,否則打包器就無(wú)法猜到你想要加載什么,就會(huì)將所有可能的文件都打包在一個(gè)文件中。例如,await import(./welcome/${moduleName}) 將把所有東西都打包到指定的目錄中,因?yàn)榇虬鞑恢雷兞?moduleName 在運(yùn)行時(shí)會(huì)是什么。
越來(lái)越多的框架為原生平臺(tái)(如 React Native)提供了運(yùn)行、遷移或編譯應(yīng)用程序的方法,以便將它們作為獨(dú)立應(yīng)用程序部署到 Android 或 iOS 移動(dòng)系統(tǒng)上。
除了考慮開發(fā)真正的原生應(yīng)用程序之外,更普遍的解決方案是將 Web 應(yīng)用程序嵌入到原生容器中,比如之前的 PhoneGap(現(xiàn)已停止維護(hù))或 Apache Cordova,現(xiàn)在的 NativeScript(它支持框架,如 Angular,也支持普通的應(yīng)用程序),或者像 Electron 這樣的原生 Web 應(yīng)用程序包裝器,或者 Electron 的輕量級(jí)后繼者 Tauri。
很多框架在前端和后端運(yùn)行的代碼是相似的,這樣更容易實(shí)現(xiàn)對(duì) SEO 友好的服務(wù)器端渲染(SSR)。
這可能是一個(gè)又酷又便利的特性,但需要注意的是,它也可能導(dǎo)致服務(wù)器鎖定。因此,在向應(yīng)用程序引入框架鎖定之前,你需要考慮它對(duì)項(xiàng)目、基礎(chǔ)設(shè)施、客戶端技術(shù)等方面的影響。
所幸的是,你也可以在不使用框架的情況下實(shí)現(xiàn)這個(gè)特性。
采用普通的實(shí)現(xiàn)方案在一開始看起來(lái)很簡(jiǎn)單:不就是返回 HTML 嗎?是的,你已經(jīng)有現(xiàn)成的組件了,但是:
你還需要一個(gè)服務(wù)器端 DOM API,因?yàn)槟J(rèn)情況下,服務(wù)器端不提供 DOM API(由 Domenic Denicola 負(fù)責(zé)維護(hù)的 JSDOM 或經(jīng)過優(yōu)化的 Happy DOM 就是很好的選擇)。
你的渲染組件不能假設(shè)是 DOM 是在客戶端或服務(wù)器端,也就是說(shuō),不要使用全局 DOM,因?yàn)樵诜?wù)器端,每個(gè)請(qǐng)求都需要一個(gè) DOM。要做到這一點(diǎn),你需要從(客戶端或服務(wù)器)應(yīng)用程序上下文中選擇 DOM 對(duì)象(windowdocument 和類型,如 Node、HTMLElement、NodeFilter),而不是直接獲取。
在客戶端和服務(wù)器應(yīng)用程序之間共享渲染組件有多種辦法,比如將其發(fā)布在包存儲(chǔ)庫(kù)中,但最靈活的應(yīng)該是讓應(yīng)用程序包引用 monorepo 中的模塊。
然而,一旦 HTML 元素被轉(zhuǎn)換成字符串,在這些元素上設(shè)置的所有事件處理器都丟失了。為了恢復(fù)交互性,你需要一些“補(bǔ)水”步驟,也就是注入腳本,讓它們?cè)诳蛻舳藞?zhí)行。框架因其普適性很難做到這一點(diǎn)。就拿影子 DOM 來(lái)說(shuō),它們不斷嘗試改進(jìn)算法,希望能夠以最聰明的方式做到這一點(diǎn),但如果我們把問題縮小到應(yīng)用程序?qū)用?,就?huì)變得簡(jiǎn)單很多。
當(dāng)然,在普通的服務(wù)器應(yīng)用程序中做到這一點(diǎn)也意味著需要將 JS 腳本注入到響應(yīng)消息中(通過引用或內(nèi)聯(lián),具體取決于你想要怎樣的“漸進(jìn)”程度,比如將 Web 組件所需的代碼嵌入到 HTML 響應(yīng)中,讓它們?cè)诳蛻舳藞?zhí)行)。
普通的解決方案讓你可以控制在哪里、什么時(shí)候以及附加哪些東西:你可以先只發(fā)送 HTML,再加載基本的交互性 JavaScript,然后加載更多(取決于用戶的操作),等等。
這比本文中提到的任何一個(gè)東西都簡(jiǎn)單,因?yàn)樗鼈兪菓?yīng)用程序代碼,而不是通用的框架代碼。
多年來(lái),國(guó)際化問題都是通過庫(kù)來(lái)處理的(最終也被集成到框架中)。要自己集成這些庫(kù)也很容易,但你也可以選擇自己實(shí)現(xiàn)一個(gè),因?yàn)榕c通用庫(kù)相比,自己的實(shí)現(xiàn)可以支持更簡(jiǎn)單、更有效的消息類型。
就是這么簡(jiǎn)單:
interface WelcomeMessages {title: stringgreetings(user: string, unreadCount: number): string}class WelcomeMessage_en implements WelcomeMessage {title = "Welcome !",??greetings?=?(user,?unreadCount)?=>?`Welcome?${user},?you?have?${unreadCount}?unread?messages.`}class?WelcomeMessage_fr?implements?WelcomeMessage?{??title?=?"Bienvenue?!",??greetings?=?(user,?unreadCount)?=>?`Bienvenue?${user},?vous?avez?${unreadCount}?nouveaux?messages.`}
這里為你提供了:
類型檢查:每個(gè)消息都有一個(gè)靜態(tài)類型(和幾個(gè)翻譯實(shí)現(xiàn)),所以 IDE 可以檢查你是否使用了有效的消息屬性,并為你提供自動(dòng)補(bǔ)全功能。
翻譯完整性檢查:在為所有消息鍵提供所有語(yǔ)言的翻譯之前,無(wú)法通過編譯。
你所需要做的就是(加載和)實(shí)例化與用戶語(yǔ)言環(huán)境相關(guān)的消息類。通用庫(kù)不會(huì)提供這種特定于業(yè)務(wù)的消息類型。
如果你想要擺脫對(duì)強(qiáng)約束性軟件技術(shù)棧的依賴,那你很可能也想擺脫對(duì)工具的依賴:你不希望只有靠著它們(它們的局限性、性能、錯(cuò)誤、版本)才能向前走。你不希望被一個(gè)你無(wú)法解決的構(gòu)建問題(或者需要數(shù)小時(shí)或數(shù)天才能解決)所困擾(特別是如果你使用的是最近構(gòu)建的版本,而它們還沒有經(jīng)過充分的實(shí)戰(zhàn)測(cè)試)。
話雖如此,你仍然很難避免使用這些工具。大多數(shù)情況下,你的產(chǎn)品代碼必須以某種方式打成包,包括縮小體積、混淆、代碼拆分、搖樹優(yōu)化、延遲加載、包含樣式等。毫無(wú)疑問,現(xiàn)有的打包工具如 Webpack、Parcel、ESBuild 或 Vite 會(huì)做得比你更好。
你所能做的是:
盡可能少用轉(zhuǎn)譯。例如,使用 TypeScript 可能是件好事,但它會(huì)帶來(lái)額外的復(fù)雜性,你的工具鏈中必須有相應(yīng)的工具來(lái)處理這種復(fù)雜性。CSS 也一樣,特別是最新版本,不值得你用預(yù)處理器(如 Sass)來(lái)處理它們。
盡可能少用工具。你用的工具越多,就越有可能出問題或無(wú)法滿足你的需求。
如果確實(shí)需要使用工具,請(qǐng)選擇最流行的工具,因?yàn)樗鼈兘?jīng)過實(shí)戰(zhàn)測(cè)試,更有可能滿足你的需求(這樣你就不會(huì)陷入“改變需求或更換工具”的困境)。過早使用最新的打包工具可能會(huì)為你節(jié)省幾秒鐘的構(gòu)建時(shí)間,但這些時(shí)間很可能都不夠用來(lái)理解工具文檔、處理 bug 或處理因缺乏支持而導(dǎo)致的問題。
說(shuō)到底,最大的挑戰(zhàn)不是技術(shù)上的,而是關(guān)于人的:
你要走出舒適區(qū)。希望你終將能夠明白,使用普通的解決方案并不是那么困難,框架的復(fù)雜性比它們帶來(lái)的好處要大得多。此外,你可能會(huì)看到更多新的 API(WebComponents、ES6 模塊、代理、MutationObserver……),而且 Web 比你想象的更現(xiàn)代、更強(qiáng)大。
至于其他人,你可以嘗試說(shuō)服他們。他們可能不愿意這么做,因?yàn)槿魏稳硕疾辉敢忾_啟自己從未嘗試過的旅程。
其他人可能會(huì)跟你說(shuō):
“你要開發(fā)自己的框架”:不,我們要開發(fā)的是應(yīng)用程序,而不是框架。
“你要寫更多的代碼”:也許,但也許不會(huì)太多(取決于用了多少開發(fā)庫(kù)),因?yàn)檫@需要與框架的樣板代碼進(jìn)行比較。但不管怎樣,需要加載的代碼都會(huì)更少。
“你將不斷地重新發(fā)明輪子”:當(dāng)然不是。不使用框架是為了不遵循它們預(yù)定義的規(guī)則(配置、生命周期管理、刷新機(jī)制等),但我們并沒有忘記 DRY 原則,我們?nèi)匀豢梢裕ú⑶覒?yīng)該)使用經(jīng)過實(shí)戰(zhàn)測(cè)試的第三方庫(kù)。
“你需要為每一個(gè)功能寫更多的代碼”:不,你可以遵循自己的規(guī)則,而不是使用框架樣板代碼。
“沒有文檔可看”:肯定不會(huì)有框架文檔(因?yàn)楦揪蜎]有框架),但你仍然需要寫應(yīng)用程序文檔。值得一提的是,使用模式有助于你自動(dòng)文檔化你的軟件設(shè)計(jì)。你只需要關(guān)心應(yīng)用程序的代碼文檔,而如果你多使用一個(gè)框架,就需要多看一份文檔。
“不會(huì)有約束或模式來(lái)指導(dǎo)開發(fā)人員”:不,如果你確實(shí)需要約束,沒有什么能阻止你(你只需要定義契約就行了)。
“你會(huì)錯(cuò)過性能提升”,比如曾經(jīng)被大肆炒作的虛擬 Dom(如今受到了挑戰(zhàn),包括來(lái)自 Svelte 或 Aurelia 框架的挑戰(zhàn)):不,因?yàn)樾枰@些“性能提升”的是框架本身(為了通用性),而不是應(yīng)用程序。相反,通用框架更有可能錯(cuò)過一些可以通過自定義代碼實(shí)現(xiàn)的性能提升。
你遇到這個(gè)問題是因?yàn)槟銢]有使用框架。每一個(gè)問題(包括漏洞、延遲、招募等)都會(huì)被歸咎于因?yàn)闆]有使用框架。因?yàn)榇蠖鄶?shù)開發(fā)人員的經(jīng)驗(yàn)是,所有正常運(yùn)行的東西都使用了框架,默認(rèn)情況下,不使用它們將被認(rèn)為是有風(fēng)險(xiǎn)的。一旦出現(xiàn)問題,無(wú)論是否與不使用框架有關(guān),這個(gè)假設(shè)都會(huì)被認(rèn)為是正確的。他們忘記了在使用框架時(shí)也會(huì)遇到類似的問題。
“我們找不到開發(fā)者”:他們會(huì)說(shuō)很難找到能夠?qū)懠?JS 代碼的開發(fā)者。這句話是對(duì)的,也是錯(cuò)的。因?yàn)楹芏嚅_發(fā)者(且不說(shuō)管理者)會(huì)發(fā)現(xiàn)自己更習(xí)慣于使用框架。如果他們從來(lái)沒有使用過或不了解基本的 Web API,那么他們可能會(huì)對(duì)從零開始構(gòu)建一個(gè) Web 應(yīng)用程序感到害怕。但是,如果你想要開發(fā)高質(zhì)量的應(yīng)用程序,就不應(yīng)該去找這種類型的開發(fā)者。當(dāng)然,現(xiàn)在找 React 開發(fā)者很容易,但你需要的不只是 React 開發(fā)者,而是優(yōu)秀的開發(fā)者。
“你無(wú)法獲得與框架相同的代碼質(zhì)量”。當(dāng)然,框架或開發(fā)庫(kù)通常是由行業(yè)里有經(jīng)驗(yàn)的開發(fā)者編寫的。但是,框架的代碼主要與框架特定的活動(dòng)相關(guān)(組件生命周期、通用的刷新機(jī)制和優(yōu)化、工具,等等),與你的應(yīng)用程序無(wú)關(guān)。此外,即使使用了框架,你仍然可能做出糟糕的設(shè)計(jì),寫出糟糕的代碼。應(yīng)用程序的質(zhì)量總是更多地取決于團(tuán)隊(duì)的質(zhì)量,而不是因?yàn)槿鄙倏蚣堋?/p>
“你無(wú)法獲得與框架相同的性能”:不,我們可以獲得更好的性能。行業(yè)里關(guān)于框架采用了可以“提升性能”的復(fù)雜技術(shù)的說(shuō)法就不在這里討論了,因?yàn)樗鼈兛赡苤饕挥脕?lái)解決框架通用解決方案的性能缺陷(比如虛擬 DOM)。

毫無(wú)疑問,性能最好的框架是那些在普通代碼之上添加層數(shù)較少的框架??蚣艿摹皟?yōu)化”更多的是為了彌補(bǔ)框架本身的開銷。
不使用框架構(gòu)建 Web 應(yīng)用程序并非意味著要自己構(gòu)建框架,它是關(guān)于在不使用通用引擎的情況下開發(fā)應(yīng)用程序,目的是:
避免散失控制和被隱含約束(鎖定、升級(jí)成本等);
可以進(jìn)行優(yōu)化(性能、體積、設(shè)計(jì))。
也就是只編寫特定于應(yīng)用程序的代碼(業(yè)務(wù)和技術(shù)),包括使用開發(fā)庫(kù)。你真正應(yīng)該關(guān)注的框架是你自己的框架,也就是那個(gè)特定于應(yīng)用程序的框架。這是真正的“專注于業(yè)務(wù)”,也是最有效的。
這并沒有你想象的那么難,特別是有了現(xiàn)代標(biāo)準(zhǔn)的加持(在必要時(shí)主流瀏覽器可以通過 polyfill 來(lái)支持新特性)。
原文鏈接:
https://javarome.medium.com/design-noframework-bbc00a02d9b3
? ? ?
往 期 推 薦
1、SpringBoot + Elasticsearch7.6 實(shí)現(xiàn)查詢及高亮分詞查詢,超級(jí)詳細(xì)! 2、MyBatis 二級(jí)緩存 關(guān)聯(lián)刷新實(shí)現(xiàn) 3、一個(gè)很酷的圖床系統(tǒng)(自帶鑒黃功能) 4、用了 HTTPS 就一定安全嗎? 5、單點(diǎn)登錄系統(tǒng)用幾張漫畫就解釋了 。。。 點(diǎn)分享
點(diǎn)收藏
點(diǎn)點(diǎn)贊
點(diǎn)在看





