新聞中心
導(dǎo)讀
2021 年,Web 開(kāi)發(fā)整體上仍然處于比較低效的狀態(tài),各種開(kāi)發(fā),部署工具仍未很好的收斂,開(kāi)發(fā)者仍然要面對(duì)選擇框架,選擇各種庫(kù),選擇部署方式,溝通前后端接口等,一個(gè)完整的 Web 應(yīng)用開(kāi)發(fā)會(huì)牽扯很多不同的工種,而不同分工之間的協(xié)作卻是很低效的,本文旨在能夠很好的梳理當(dāng)下 Web 開(kāi)發(fā)的 "困局",以及我們通過(guò)何種方式,能夠走出這些困局,解放生產(chǎn)力,希望能給未來(lái)的工具發(fā)展給出一定的預(yù)測(cè)和啟發(fā)。

創(chuàng)新互聯(lián)公司主要從事網(wǎng)站建設(shè)、網(wǎng)站設(shè)計(jì)、網(wǎng)頁(yè)設(shè)計(jì)、企業(yè)做網(wǎng)站、公司建網(wǎng)站等業(yè)務(wù)。立足成都服務(wù)太原,10年網(wǎng)站建設(shè)經(jīng)驗(yàn),價(jià)格優(yōu)惠、服務(wù)專(zhuān)業(yè),歡迎來(lái)電咨詢(xún)建站服務(wù):028-86922220
困境
設(shè)計(jì)/前端協(xié)作困境
在實(shí)際的 Web 開(kāi)發(fā)中,UI/UX 的工作與前端的工作事實(shí)上是在兩個(gè)完全割裂的環(huán)境中進(jìn)行的,比如,UI 會(huì)在 Figma 中完成頁(yè)面與組件的設(shè)計(jì),而前端則是根據(jù)設(shè)計(jì)好的原型圖,在代碼環(huán)境中去復(fù)現(xiàn)原型圖,這其中就出現(xiàn)了幾個(gè)協(xié)作問(wèn)題。我把它簡(jiǎn)要總結(jié)成四個(gè)問(wèn)題:
- 應(yīng)該先設(shè)計(jì)再開(kāi)發(fā),還是先開(kāi)發(fā)再設(shè)計(jì)?(Dev First or Design First?)
- 如果說(shuō)設(shè)計(jì)的原型圖是前端開(kāi)發(fā)的上游,那前端應(yīng)該如何更高效地獲取設(shè)計(jì)的上游更新?
- 設(shè)計(jì)圖中藏有很多可復(fù)用的概念與元素,如何很好的傳達(dá)給前端?
- 前端工程師是否有義務(wù)參與純樣式的開(kāi)發(fā)?既然 UI 已經(jīng)完成了樣式的設(shè)計(jì),為什么前端仍然需要重新實(shí)現(xiàn)一遍?
下面我們逐個(gè)討論這些問(wèn)題,之后給出可能的解決方案。
Dev First Or Design First
先來(lái)討論第一個(gè)問(wèn)題,這是一個(gè)困擾了我很久的問(wèn)題,在之前的工作經(jīng)驗(yàn)中,我的處理方式往往是,以功能為主的組件,先開(kāi)發(fā),再設(shè)計(jì)(比如富文本編輯器),以展示為主的,先設(shè)計(jì),再開(kāi)發(fā),但是實(shí)際的協(xié)作仍然會(huì)出現(xiàn)很多問(wèn)題:
Dev First:先開(kāi)發(fā)再設(shè)計(jì),往往前端程序員需返工 (比如原來(lái)調(diào)的現(xiàn)成組件現(xiàn)在不能直接用了,需要自己重新寫(xiě)一個(gè)),降低前端程序員工作體驗(yàn),而在設(shè)計(jì)圖不穩(wěn)定的時(shí)候,前端會(huì)反復(fù)地 follow 設(shè)計(jì)圖的改動(dòng),降低前端程序員的工作體驗(yàn),有時(shí)甚至?xí)l(fā)員工間的矛盾。
Design First:設(shè)計(jì)對(duì)組件邏輯理解較為模糊,難以涵蓋組件所有狀態(tài)的樣式,而為了枚舉或描述組件的所有可能狀態(tài),常常過(guò)于繁瑣,會(huì)有很多長(zhǎng)得很像的重復(fù)原型圖,而缺斤短兩的原型圖,也容易影響前端工程師的工作體驗(yàn),甚至?xí)嵘?zé)任推卸的可能性 (前端覺(jué)得有些組件狀態(tài)設(shè)計(jì)圖沒(méi)有給到,就停工了,將責(zé)任推卸給 UI)。
之后我們可以看到,如果不改變協(xié)作模式和工具,這個(gè) dilemma 是無(wú)法消除的。
前端應(yīng)該如何更高效地獲取設(shè)計(jì)的上游更新
設(shè)想這樣一個(gè)場(chǎng)景,公司有一個(gè)完整的設(shè)計(jì)團(tuán)隊(duì),它們有時(shí)會(huì)更新一些組件的圖標(biāo),當(dāng)這些圖標(biāo)得到更新后,設(shè)計(jì)團(tuán)隊(duì)可能會(huì)手動(dòng)通知前端工程師,前端工程師再下載到新的 icon 文件,將該文件放入倉(cāng)庫(kù)的 src/assets 下,再 push 代碼,觸發(fā)流水線(xiàn),部署完畢后,icon 得到更新。
上述的過(guò)程顯然是十分低效的,設(shè)想這樣一種情形,全公司有上百個(gè)網(wǎng)站,幾乎每個(gè)網(wǎng)站都用到了公司的 logo,但絕大多數(shù)網(wǎng)站都是將該 logo 放入 src/assets 這樣的形式來(lái)部署的,那么當(dāng)公司更新 logo 的時(shí)候,就需要所有代碼倉(cāng)庫(kù)都更新該 logo,浪費(fèi)很多團(tuán)隊(duì),很多人的時(shí)間,并且更重要的是,公司 logo 的全量更新成為了一個(gè)漫長(zhǎng)的過(guò)程。
除了上面這個(gè)例子外,還有很多例子,比如設(shè)計(jì)經(jīng)常會(huì)給原型圖做一些修改,每次修改后,如果我們希望足夠敏捷,那 UI 就會(huì)當(dāng)場(chǎng)通知前端,前端再打開(kāi) Figma 之類(lèi)的軟件和 VSCode 等開(kāi)發(fā)工具,完成了更改 (更多時(shí)候,前端還需要仔細(xì)檢查到底是哪里更改了,有時(shí)需要和 UI 進(jìn)行同步溝通),在這個(gè)場(chǎng)景下,前端像是被 UI 牽著鼻子走的工種,長(zhǎng)此以往,前端會(huì)覺(jué)得自己的工作沒(méi)有價(jià)值,引發(fā)更深層次的問(wèn)題。
那為了防止這樣的現(xiàn)象,我們索性犧牲敏捷性,每個(gè)月迭代一版,前端統(tǒng)一更新 UI,但這樣又拋棄了 Web 的優(yōu)勢(shì)之一:用戶(hù)使用的應(yīng)用永遠(yuǎn)都是最新的,在講究快速迭代的環(huán)境中,這種方式越來(lái)越少見(jiàn),一個(gè)例子就是現(xiàn)在越來(lái)越多的 Web 應(yīng)用忽略了 版本號(hào) 這個(gè)概念,因?yàn)橹灰軌蚝芎玫淖粉?commit history,并規(guī)范好 commit message,版本號(hào) 這個(gè)概念其實(shí)也已經(jīng)變得比較雞肋,它更多是桌面時(shí)代的產(chǎn)物。
設(shè)計(jì)圖中可復(fù)用的概念與元素,如何很好的傳達(dá)給前端
成熟的設(shè)計(jì)團(tuán)隊(duì),往往會(huì)給有自己的設(shè)計(jì)系統(tǒng),會(huì)在團(tuán)隊(duì)內(nèi)部沉淀一些復(fù)用的概念與元素出來(lái),比如,規(guī)定所有卡片組件的 box-shadow 都是同一種格式,規(guī)定調(diào)色板的基礎(chǔ)色號(hào)有哪幾種,字體大小有哪幾級(jí),但這些信息往往并不能在原型圖層面很好的展現(xiàn)出來(lái),而 UI 也很少會(huì)將這些概念很好的傳達(dá)給前端,前端也覺(jué)得自己沒(méi)有義務(wù)理解這些設(shè)計(jì)層面的概念,進(jìn)一步加深了兩個(gè)工種的分裂。
UI/UX 和 前端工程師之間的概念往往并不互通,而互相也覺(jué)得自己并沒(méi)有義務(wù)去了解對(duì)方專(zhuān)業(yè)中的知識(shí),但日常的協(xié)作又有極多的的深度交織,UI/UX 是最了解設(shè)計(jì)里面的邏輯和復(fù)用的,但真正實(shí)現(xiàn)邏輯和復(fù)用的卻是前端工程師。這種職能的錯(cuò)配和重疊是問(wèn)題的根源所在
設(shè)想這樣一個(gè)情景:一個(gè) Web 應(yīng)用起初的設(shè)計(jì),并未把主題色更換考慮在內(nèi),而 UI 團(tuán)隊(duì)內(nèi)部有基礎(chǔ)調(diào)色板,很多組件都共用一些基礎(chǔ)色號(hào),但并未在原型圖中展示這些信息,事實(shí)上前端工程師也不關(guān)心這些,這就導(dǎo)致了幾乎所有前端代碼中,組件的顏色都是 hardcoded 的,并未體現(xiàn)出邏輯性和復(fù)用性,然后過(guò)了兩個(gè)月,UI 團(tuán)隊(duì)決定支持主題色更換,于是前端團(tuán)隊(duì)又面臨著巨量的體力活。
既然 UI 已經(jīng)完成了樣式的設(shè)計(jì),為什么前端仍然需要重新實(shí)現(xiàn)一遍
設(shè)計(jì)師給出原型圖,前端再實(shí)現(xiàn)一遍,這很契合我們往常的經(jīng)驗(yàn),但是仔細(xì)思考會(huì)發(fā)現(xiàn)這是很荒誕的,這就像游戲行業(yè)的 建模師 建好了人物模型后,游戲開(kāi)發(fā)者竟然還需要在游戲中重新實(shí)現(xiàn)一遍模型。
設(shè)備的尺寸布局,responsive 排版方式,這些層面的設(shè)計(jì),按理來(lái)說(shuō)應(yīng)該由 UI/UX 來(lái)把控,可事實(shí)上卻是由前端工程師把控的,前端工程師似乎承擔(dān)了太多設(shè)計(jì)層面的 實(shí)現(xiàn)任務(wù)。
就像上文提到的,造成這四個(gè) UI/前端 的協(xié)作困局,根本原因在于 職能的錯(cuò)配和重疊,兩個(gè)工種深度耦合,互相牽制,用一個(gè)圖概括就是這樣:
在頁(yè)面樣式這部分,工程師與設(shè)計(jì)師都參與了進(jìn)來(lái),這部分就是兩個(gè)工種的 職能重疊部分,重疊的部分帶來(lái)了大量的重復(fù)勞動(dòng)與溝通成本。
為了徹底解決上面的問(wèn)題,我們首先將 職能重疊 的部分盡可能減少,從情理上講,頁(yè)面樣式 這塊工作應(yīng)該歸屬于 UI,而前端工程師只需要負(fù)責(zé) 功能邏輯 即可,這樣兩個(gè)工種的工作就正交了:
接下來(lái)我們需要思考的問(wèn)題是 為什么當(dāng)下的工具和生態(tài)不允許這樣的分工方式?
從工具角度來(lái)說(shuō),目前 UI 用的工具以 Sketch,F(xiàn)igma 為主,它們都是比較好用的圖形化設(shè)計(jì)軟件,都支持組件設(shè)計(jì),也支持一定的復(fù)用邏輯,原型圖也往往直接能夠看到元素的 css,這樣看起來(lái)似乎 前端工程師 只需要無(wú)腦復(fù)制粘貼 css,就可以復(fù)制出一個(gè)一模一樣的頁(yè)面了,
但真實(shí)情況并不是這么簡(jiǎn)單,一方面就像上文提到的,直接復(fù)制粘貼 css 無(wú)法在前端代碼層面體現(xiàn)出設(shè)計(jì)的復(fù)用邏輯,而且原型圖的 css 往往采用絕對(duì)定位,實(shí)際的 css 要考慮 responsive,多設(shè)備適配等問(wèn)題,并不能直接搬過(guò)來(lái)用,所以這條路事實(shí)上行不通。
根本原因在于 UI 工程師是在白板上進(jìn)行設(shè)計(jì),而不是在真實(shí)的組件上進(jìn)行設(shè)計(jì)。
UI 工程師往往都在 Sketch 等軟件提供的白板上用各種按鈕,圖形去拼接出圖,這個(gè)東西和前端的環(huán)境完全分離,做的工作完全不能應(yīng)用到實(shí)際的組件上,一方面我們沒(méi)有提供給 UI 工程師工具讓其將設(shè)計(jì)應(yīng)用于某組件上 (你總不能期望 UI 工程師打開(kāi) VSCode,從 git 拉代碼吧),一方面前端工程師的組件也往往是作為一個(gè) npm package 里面的一個(gè) submodule 存在的,因此,我們事實(shí)上需要一種 UI 工程師和前端工程師互通共享的工作環(huán)境,在這個(gè)環(huán)境中,前端工程師可以實(shí)現(xiàn)組件的邏輯,UI 工程師可以直接給組件骨架添加設(shè)計(jì)樣式。
也就是說(shuō),次世代的前端開(kāi)發(fā)工具,應(yīng)該是同時(shí)面向 設(shè)計(jì)師 和 工程師 的,而同時(shí)也是 面向組件 的。它應(yīng)該是一個(gè)對(duì)于設(shè)計(jì)師用戶(hù)友好的平臺(tái),可以在這個(gè)平臺(tái)上看到工程師已經(jīng)產(chǎn)出的,帶有邏輯骨架的組件,并且在平臺(tái)上為這些組件添加樣式,有了這樣的平臺(tái),對(duì)于組件的開(kāi)發(fā),前端工程師只需要關(guān)心邏輯即可,剩下的樣式工作可以全部交由設(shè)計(jì)師來(lái)完成,這樣就實(shí)現(xiàn)了兩個(gè)工種職責(zé)的隔離,雙方都只需要負(fù)責(zé)好自己的事情,不需要互相替對(duì)方去實(shí)現(xiàn)一些想法。
所以概括一下,我認(rèn)為未來(lái)的協(xié)作方式應(yīng)該是這樣:
上面這張圖中,前端和 UI 都共享一個(gè) Component Registry,也就是 組件注冊(cè)中心,它一方面向前端工程師暴露代碼接口,一方面又向設(shè)計(jì)師暴露設(shè)計(jì)面板,在這個(gè) 組件中心 里,一個(gè) team 可以共享的看到所有組件,這是個(gè)統(tǒng)一的協(xié)作平臺(tái)。
一點(diǎn)小想法:目前 AI 已經(jīng)可以幫我們?cè)O(shè)計(jì) Logo 了,日后也可以幫我們?cè)O(shè)計(jì)組件的樣式,AI 可以學(xué)習(xí)一套組件的風(fēng)格,并且將這種風(fēng)格自動(dòng)的應(yīng)用到其它組件上,,日后這樣的方式或許可以幫我們快速地得到設(shè)計(jì)統(tǒng)一美觀(guān)的組件庫(kù)。
對(duì)于前端來(lái)講,代碼的 commit history,版本控制,它們的 scope 都應(yīng)該是 組件,開(kāi)發(fā)是針對(duì)組件的,而 UI 則可以打開(kāi)一個(gè)組件,然后設(shè)計(jì)套件會(huì)自動(dòng)將組件的一些基本元素提取出來(lái),為 UI 提供圖形化的設(shè)計(jì)面板,完成設(shè)計(jì)后,轉(zhuǎn)化成 css in js 之類(lèi)的東西,最終轉(zhuǎn)化成一條 commit history 提交到組件中。
從圖中看到,前端和 UI 都是在向 組件中心 push changes,而設(shè)計(jì)也可以在設(shè)計(jì)平臺(tái)中定義一些全局變量,一些復(fù)用的樣式,組件也會(huì)依賴(lài)于設(shè)計(jì)團(tuán)隊(duì)的這些設(shè)計(jì)變量,于是設(shè)計(jì)團(tuán)隊(duì)可以通過(guò)統(tǒng)一更改變量來(lái)達(dá)到全局組件樣式風(fēng)格切換的效果,到這里為止,設(shè)計(jì)層面的復(fù)用和邏輯職能,甚至是 responsive design,多設(shè)備尺寸適配,都收斂到了設(shè)計(jì)師手里,因此設(shè)計(jì)師還需要學(xué)習(xí)流式布局,網(wǎng)格系統(tǒng)等概念,這從職能上看也更為合理,也更容易讓設(shè)計(jì)師來(lái)發(fā)揮更大的能力。
同時(shí),因?yàn)榻M件在功能邏輯層面,前端已經(jīng)做好了 composition,它們已經(jīng)實(shí)現(xiàn)了組件之間的相互依賴(lài)關(guān)系,所以 UI 無(wú)需關(guān)心組件之間的依賴(lài)關(guān)系,當(dāng)一個(gè)子組件的樣式更新后,在設(shè)計(jì)套件中,父組件也能看到更新后的子組件,組件邏輯的依賴(lài)關(guān)系,收斂到了前端手里。
這種協(xié)作模式,我稱(chēng)為 面向組件的研發(fā)模式,而目前,像 https://bit.dev/ 之類(lèi)的產(chǎn)品,已經(jīng)在實(shí)踐這個(gè)想法,但是它們只是給工程師提供了一個(gè)面向組件的研發(fā)平臺(tái),還未給設(shè)計(jì)師提供設(shè)計(jì)平臺(tái),這是這類(lèi)產(chǎn)品目前欠缺的地方,很可能也是它們未來(lái)的發(fā)展方向。
讀者可以回顧一下之前討論的四個(gè)問(wèn)題,我們會(huì)發(fā)現(xiàn)在新的協(xié)作模式下,四個(gè)問(wèn)題都得到了較好的解決。
組件這個(gè)層級(jí)的協(xié)作方式發(fā)生改變后,頁(yè)面級(jí)的組件 Composition 問(wèn)題,也就是由這些組件組合成一個(gè)完整的頁(yè)面時(shí),也可以同樣地在這套系統(tǒng)上完成協(xié)作,只需要把頁(yè)面當(dāng)成一個(gè)復(fù)合程度很高的組件即可,實(shí)際的頁(yè)面內(nèi)部會(huì)有很多數(shù)據(jù) fetch 和處理邏輯,這塊的處理方式,在下文中會(huì)有提到。
數(shù)據(jù)交互困境
桌面應(yīng)用誕生的最早期,客戶(hù)端是可以直接連接數(shù)據(jù)庫(kù)的,在當(dāng)時(shí),關(guān)于怎樣獲取數(shù)據(jù),怎樣存數(shù)據(jù)的邏輯,是放在客戶(hù)端負(fù)責(zé)的,之后 Web 的發(fā)展,讓更多的邏輯放在了后端,后端負(fù)責(zé)連接數(shù)據(jù)庫(kù),并且將更簡(jiǎn)單規(guī)范的 HTTP (以 Restful 為代表) 請(qǐng)求轉(zhuǎn)換成對(duì)應(yīng)的 SQL 等數(shù)據(jù)庫(kù)語(yǔ)句,在后端完成和數(shù)據(jù)庫(kù)的交互 (此處的數(shù)據(jù)庫(kù)指廣義的數(shù)據(jù)庫(kù),可能包含各種中間件,各種形式的存儲(chǔ)等),前端只需要消費(fèi)這些簡(jiǎn)單的接口即可,看起來(lái)是降低了前端的負(fù)擔(dān) (無(wú)需思考如何和后端的數(shù)據(jù)庫(kù)等服務(wù)交互,后端已經(jīng)封裝好了)。
但在實(shí)際的業(yè)務(wù)場(chǎng)景下,以 Restful[1] 為主的這種后端 API 思路仍然出現(xiàn)了很多問(wèn)題,我們發(fā)現(xiàn)實(shí)際的前端場(chǎng)景下,前端往往需要對(duì)數(shù)據(jù)有更精細(xì)的控制:
試想這樣一個(gè)情景:前端顯示一個(gè)評(píng)論列表,這里只用到了每個(gè)人的頭像和昵稱(chēng),但是后端提供的 profile 接口卻會(huì)連著其它的手機(jī)號(hào),個(gè)性簽名等等一系列信息全部返回了,這個(gè)時(shí)候后端就返回了很多無(wú)用信息。
上面的場(chǎng)景說(shuō)明,如果后端將用于操縱數(shù)據(jù)的接口封裝的抽象層級(jí)過(guò)高,會(huì)出現(xiàn)無(wú)法滿(mǎn)足前端的靈活使用的問(wèn)題,除此之外還有處理一對(duì)多關(guān)系,比如一個(gè)班級(jí)里面包含很多學(xué)生,students 是 class 的一個(gè)屬性,那使用 GET 請(qǐng)求請(qǐng)求 class 的時(shí)候,是否應(yīng)該返回它的子屬性 students 呢?如果前端希望能夠控制,那往往又需要引入新的 query parameter 來(lái)控制,這又增加了協(xié)商成本和文檔成本。
基于 Restful 的開(kāi)發(fā)模式,實(shí)際體驗(yàn)往往是前端仍然需要去看接口文檔,為了讓接口有靈活性,需要引入很多自定義的 query params,由于接口本身的靈活性差,導(dǎo)致前端程序員需要思考使用什么樣的順序和方式調(diào)用接口,才能實(shí)現(xiàn)一個(gè)功能,很多時(shí)候前端需要被迫拼接,堆積接口調(diào)用,甚至?xí)霈F(xiàn)在前端手動(dòng)遞歸調(diào)用后端接口獲得一個(gè)樹(shù)狀文件夾數(shù)據(jù)這種現(xiàn)象,可見(jiàn)這種方式是有很大問(wèn)題的。
除此之外,普通的后端 CRUD 接口,本身的實(shí)現(xiàn)很簡(jiǎn)單,但是由于前后端分離,語(yǔ)言也可能不同,導(dǎo)致前端遇到接口問(wèn)題時(shí),必須要和后端協(xié)商,后端再做出改動(dòng),真實(shí)情況是 讓前端去學(xué)習(xí)后端 CRUD,并且直接對(duì)后端做出改動(dòng),比前端和后端協(xié)商來(lái)的效率高,根本原因第一在于 Restful 本身的靈活性問(wèn)題,其次在于簡(jiǎn)單的后端查詢(xún)業(yè)務(wù)由于和前端的業(yè)務(wù)深度耦合,這部分工作應(yīng)該收斂到一個(gè)工種上,并且考慮到傳統(tǒng)的后端 CRUD 的代碼很大程度可以自動(dòng)生成,所以我們接下來(lái)要做的事可以總結(jié)為:把常規(guī)的后端業(yè)務(wù)實(shí)現(xiàn)的任務(wù)收斂到前端工種,并且通過(guò)更好的 API + SDK + P/F/SaaS 讓常規(guī)的后端業(yè)務(wù)盡可能自動(dòng)化+服務(wù)化,從而淘汰掉傳統(tǒng)的后端 CRUD 工種,提升整個(gè)系統(tǒng)的效率。
回過(guò)頭來(lái)想一下,如果后端希望暴露給前端一個(gè)安全的操縱數(shù)據(jù)的接口,使用 HTTP Path + Method 的這種方式顯然不夠強(qiáng),關(guān)于數(shù)據(jù)關(guān)系模型是一個(gè)關(guān)于集合的數(shù)學(xué)理論,它在數(shù)學(xué)中一開(kāi)始的描述方式是使用 關(guān)系代數(shù)[2] 這樣的語(yǔ)言描述的,基于樹(shù)狀關(guān)系(HTTP Path 是一種樹(shù)狀的命名空間) + 方法(Get Post Delete Put 等) 的描述方式過(guò)弱,遠(yuǎn)遠(yuǎn)無(wú)法支撐實(shí)際的數(shù)據(jù)操作。
但直接使用 SQL 語(yǔ)言,一方面是安全性的問(wèn)題(可以通過(guò)代理+一些權(quán)限驗(yàn)證方式解決,不是問(wèn)題的關(guān)鍵),一方面是 SQL 語(yǔ)言這種模式和前端的語(yǔ)言環(huán)境太過(guò)割裂,前端被迫進(jìn)行字符串拼接,與之相對(duì)的是 MongoDB 的查詢(xún)語(yǔ)言,其和 javascript 語(yǔ)言的貼合度較之 SQL 要好很多,前端程序員可以用很自然的方式寫(xiě)出一個(gè)查詢(xún)語(yǔ)句。
與此同時(shí),基于 Restful 這樣的模式,讓很多的后端代碼變成了非常簡(jiǎn)單的 CRUD 代碼,很多代碼就是為了將 restful 接口轉(zhuǎn)化成 SQL 語(yǔ)句,大量的時(shí)間被耗費(fèi)在了這些無(wú)聊的事情上,降低了開(kāi)發(fā)效率,這些簡(jiǎn)單的操作應(yīng)該被自動(dòng)化。
在這樣的困境下,GraphQL[3] 應(yīng)運(yùn)而生,它用一種更優(yōu)雅的方式實(shí)現(xiàn)了聲明式數(shù)據(jù)請(qǐng)求格式,相較于 Restful,它更像是 SQL 這種聲明式的語(yǔ)言,從 Restful 到 GraphQL 的轉(zhuǎn)變,對(duì)于前端來(lái)講則是命令式到聲明式的轉(zhuǎn)變,從思考 "what do I need to get I want" 到直接思考 "what I want"。
但是僅僅依靠 GraphQL 還是沒(méi)解決這兩個(gè)問(wèn)題:
- Restful 時(shí)代的 CRUD 代碼轉(zhuǎn)變成了 GraphQL 的 resolver 代碼,后端還是需要手動(dòng)寫(xiě),或者使用 codegen 工具來(lái)生成代碼,仍然沒(méi)擺脫樣板代碼的桎梏。
- 前端對(duì)數(shù)據(jù)的請(qǐng)求的狀態(tài)管理:重復(fù)請(qǐng)求問(wèn)題,數(shù)據(jù)依賴(lài)更新問(wèn)題。
先討論第二個(gè)問(wèn)題,在前端的業(yè)務(wù)場(chǎng)景下,數(shù)據(jù)依賴(lài)可以分為兩類(lèi):一類(lèi)是純前端的數(shù)據(jù)綁定,一類(lèi)則是涉及到后端數(shù)據(jù)的綁定,狀態(tài)的綁定也是前端這種響應(yīng)式系統(tǒng)和轉(zhuǎn)換式系統(tǒng)的最顯要的差別,這是 Web 要處理的最核心的問(wèn)題之一。
響應(yīng)式系統(tǒng)(reactive system) 和 轉(zhuǎn)化式系統(tǒng) (transformational system) 的最大區(qū)別在于,前者更像一個(gè)狀態(tài)機(jī),輸入與當(dāng)前的狀態(tài)才能決定輸出,而轉(zhuǎn)化式系統(tǒng)(典型例子如編譯器) 則更著重的是輸入和輸出。Statecharts:a visual formalism for complex systems - ScienceDirect[4]
我們可以看到幾乎所有前端框架,無(wú)論基于模板的,還是 jsx 的,都解決了一個(gè)核心問(wèn)題就是狀態(tài)之間的綁定,UI 狀態(tài)和內(nèi)部 js 變量的綁定等,數(shù)據(jù)之間是有一個(gè)依賴(lài)關(guān)系的,它們可以用一個(gè)依賴(lài)圖表示。
鑒于 Web 的特殊性,我將狀態(tài)的綁定分為 純前端綁定 和 前后端綁定,前者的綁定只發(fā)生在前端,比如一個(gè) js 內(nèi)部的變量和 的 value 的綁定,而后者則涉及到前端狀態(tài)與后端狀態(tài)的綁定,比如,一個(gè)評(píng)論列表與后端的評(píng)論數(shù)據(jù)做綁定。
(更多的時(shí)候,程序員并沒(méi)有把后者理解成一種綁定,因?yàn)槟壳暗墓ぞ哌€是將這個(gè)過(guò)程作為一種主動(dòng) fetch 的命令式做法,沒(méi)有像前端框架一樣提供了簡(jiǎn)單的聲明式綁定,這也是目前發(fā)展的不足之處,我認(rèn)為未來(lái)前后端的綁定,也應(yīng)該是像純前端綁定一樣簡(jiǎn)單,命令式,消除顯式的 http 請(qǐng)求代碼)
試想這樣一個(gè)場(chǎng)景:一篇文章下面有評(píng)論列表,你在評(píng)論框中添加了一條評(píng)論,這時(shí)按道理來(lái)說(shuō),前端的 UI 列表與后端的評(píng)論數(shù)據(jù)應(yīng)該是雙向綁定的關(guān)系,評(píng)論列表應(yīng)該立即得到更新,但這時(shí)前端程序員的做法很可能是顯式的寫(xiě)一個(gè)邏輯,當(dāng)新建評(píng)論后,手動(dòng)重新請(qǐng)求評(píng)論 api,然后得到更新,這種做法像極了使用原生 DOM 和 js 處理前端 UI 和數(shù)據(jù)的綁定關(guān)系,手動(dòng)維護(hù)狀態(tài),手動(dòng)調(diào)用 DOM 接口,從這個(gè)角度看,兩種綁定的發(fā)展路線(xiàn)是類(lèi)似的,只不過(guò) 前后端綁定 的相關(guān)工具是最近兩三年出現(xiàn)的。
處理前后端綁定的框架,代表性的有 React Query[5] (2019 年建倉(cāng)) 和 Apollo GraphQL Client[6] (2016 年建倉(cāng))。它們解決的問(wèn)題都是使用聲明式的語(yǔ)法,處理前后端綁定的場(chǎng)景,都基于 GraphQL,它們往往被人稱(chēng)作數(shù)據(jù)層的狀態(tài)管理工具,為了更好的理解本文意圖,我將其稱(chēng)為 前后端數(shù)據(jù)綁定工具。
在上文的 設(shè)計(jì)/前端協(xié)作困境 中提到的基于組件的協(xié)作流,只是對(duì)單組件的協(xié)作,而一個(gè)復(fù)合型應(yīng)用需要將這些組件組合起來(lái),填充到頁(yè)面中,這個(gè)場(chǎng)景下多了兩個(gè)要解決的問(wèn)題:
- 組件和后端數(shù)據(jù)的綁定問(wèn)題。
- 純前端綁定。
前后端綁定方式
拿一個(gè)最簡(jiǎn)單的例子,一個(gè)填寫(xiě)個(gè)人信息的表單,在傳統(tǒng)的 Web 思路中,如果使用框架的話(huà),會(huì)將一個(gè)組件內(nèi)部的 r 和表單的 的 value 屬性做綁定,當(dāng)用戶(hù)點(diǎn)擊 "提交" 按鈕時(shí),執(zhí)行一個(gè) onSubmit 方法,方法內(nèi)部將數(shù)據(jù)作為 body,一個(gè) POST 請(qǐng)求將數(shù)據(jù)傳給后端。
這樣做沒(méi)問(wèn)題,但它會(huì)阻擾我們理解問(wèn)題。使用"面向綁定"的方式理解這個(gè)問(wèn)題的時(shí)候,這個(gè)問(wèn)題其實(shí)變得很簡(jiǎn)單,我們將后端的個(gè)人信息數(shù)據(jù)與組件內(nèi)部的狀態(tài)做綁定,當(dāng)用戶(hù)點(diǎn)擊提交前,綁定處于 out of sync 的狀態(tài),點(diǎn)擊 "提交" 進(jìn)行同步,進(jìn)入 sync 狀態(tài)。這樣理解問(wèn)題,前后端的數(shù)據(jù)交互問(wèn)題就變得清晰明朗了起來(lái)。
那么與此同時(shí),因?yàn)轫?yè)面的右上角可能有你的頭像,那個(gè)頭像的組件也和你的個(gè)人信息的子屬性 avatar 綁定,這時(shí),當(dāng)表單進(jìn)行 sync 后,由于頭像組件也綁定了有依賴(lài)關(guān)系的數(shù)據(jù)源,所以數(shù)據(jù)層會(huì)自動(dòng)更新頭像:
上圖中,表單和 Profile 做了綁定,頭像和 Avatar 做了綁定,當(dāng)個(gè)人信息點(diǎn)擊提交后,數(shù)據(jù)狀態(tài)管理層會(huì)自動(dòng)檢測(cè)到 Avatar 組件所依賴(lài)的數(shù)據(jù)的父節(jié)點(diǎn)發(fā)生了 Mutation,從而自動(dòng)觸發(fā) refetch,獲得更新后的 Avatar。
如果我們能夠保證所有的數(shù)據(jù)源的請(qǐng)求都是以 GraphQL 的話(huà),那么我們可以使用 GraphQL Query 作為前后端數(shù)據(jù)綁定的聲明式語(yǔ)法,而根據(jù)對(duì) GraphQL + Endpoint 構(gòu)成的實(shí)時(shí)數(shù)據(jù)圖依賴(lài)分析,可以實(shí)時(shí)地解決數(shù)據(jù)的依賴(lài)變化問(wèn)題,這個(gè)過(guò)程可以直接在前端完成。在 React Query 中,由于其本身的設(shè)計(jì)是后端無(wú)關(guān)的,數(shù)據(jù)間的依賴(lài)關(guān)系是通過(guò)手動(dòng)維護(hù) query 的命名數(shù)組進(jìn)行的,尚未達(dá)成自動(dòng)解決依賴(lài)的問(wèn)題。
為了更好地在下面討論組件,我們先將組件從復(fù)用性的高低可以分為兩類(lèi),一類(lèi)是 通用型組件,一類(lèi)是 自治型組件。 前者盡可能地讓自己的能力通用化,自己內(nèi)部不維護(hù)網(wǎng)絡(luò)請(qǐng)求等信息,而是根據(jù)傳入的 props 來(lái)動(dòng)態(tài)地決定與后端數(shù)據(jù)的依賴(lài),以及數(shù)據(jù)源和組件內(nèi)部狀態(tài)的綁定關(guān)系,而自治型組件通??梢元?dú)立使用,其自己內(nèi)部實(shí)現(xiàn)了網(wǎng)絡(luò)請(qǐng)求等邏輯,但是通用型較差,通常只能實(shí)現(xiàn)特定功能,類(lèi)似于 iframe。
舉個(gè)例子,繼續(xù)拿表單組件為例,通用型表單組件從外界接受數(shù)據(jù)源,以及請(qǐng)求后的數(shù)據(jù)的屬性與組件內(nèi)部的對(duì)應(yīng)關(guān)系,除此之外,往往還需要提供一個(gè)數(shù)組用來(lái)生成相應(yīng)的表單列表,數(shù)據(jù)從哪來(lái),數(shù)據(jù)和表單怎么對(duì)應(yīng),表單的 validation 函數(shù),都從外界傳入,組件本身只實(shí)現(xiàn)邏輯框架和設(shè)計(jì)樣式。
而對(duì)于自治型表單組件,很可能是一個(gè)飛書(shū)投票組件,從飛書(shū)小程序中生成一個(gè)組件實(shí)例,就可以直接使用,它內(nèi)部實(shí)現(xiàn)了相應(yīng)的數(shù)據(jù)邏輯,用戶(hù)可以直接填寫(xiě),就可以在飛書(shū)投票后臺(tái)看到該組件收集的投票信息。
對(duì)于純前端的綁定來(lái)說(shuō),本質(zhì)上還是組件的樹(shù)狀結(jié)構(gòu)組合,很多頁(yè)面可以通過(guò)代碼的方式寫(xiě)成一個(gè)大組件,不管是單一功能型組件還是頁(yè)面,都可以統(tǒng)一的視為組件,使用統(tǒng)一的方式看待和處理。
再來(lái)討論上面的第一個(gè)問(wèn)題,如何解決后端程序員仍然要寫(xiě)很多 GraphQL Resovler 代碼浪費(fèi)很多時(shí)間,顯然,從數(shù)據(jù)庫(kù)的 schema 出發(fā),是可以生成一些默認(rèn)的 Rosovler 代碼的,但是需要程序員手寫(xiě)的原因在于,默認(rèn)生成的 Resolver 代碼往往缺一些和具體業(yè)務(wù)相關(guān)的東西,基于此可以使用約定大于配置的思路,為用戶(hù)提供默認(rèn)的 Resolver 能力,并給用戶(hù)提供自定義的 Custom Resolver 的接口,在這個(gè)方向上,Hasura[7] 已經(jīng)做了一些工作,它們也是從數(shù)據(jù)庫(kù)出發(fā),搭配權(quán)限驗(yàn)證策略,自動(dòng)生成統(tǒng)一的 GraphQL API 網(wǎng)關(guān),供各種形式的前端來(lái)調(diào)用,大幅簡(jiǎn)化傳統(tǒng)的 CRUD 代碼:
基于此,我設(shè)想未來(lái)的 Web App 的開(kāi)發(fā),可能會(huì)有很多的數(shù)據(jù)源提供提供商,比如像 OneGraph - Build Integrations 100x Faster[8] 中,將常用的公開(kāi) API 轉(zhuǎn)化成統(tǒng)一的 GraphQL API 網(wǎng)關(guān)供調(diào)用,這些數(shù)據(jù)源會(huì)像是一個(gè) Market,組件有組件市場(chǎng) (Component Market),而數(shù)據(jù)源也有數(shù)據(jù)源市場(chǎng) (Datasources Market)。
對(duì)于數(shù)據(jù)源市場(chǎng)來(lái)講,每個(gè)人都能在其上發(fā)布數(shù)據(jù)源,可以像 Github 一樣做私有付費(fèi)(Pay For Privacy) 的策略,和組件一樣可以供其它人消費(fèi),然后很多程序員可以快速借助統(tǒng)一的數(shù)據(jù)源和組件市場(chǎng)搭建出一個(gè)功能完整的現(xiàn)代 web app:
數(shù)據(jù)源平臺(tái)將會(huì)依托于 Serverless 提供的基礎(chǔ)能力,為泛開(kāi)發(fā)者提供一個(gè)類(lèi)似于 AWS Lambda 的函數(shù)注冊(cè)中心,提供幾種基本的數(shù)據(jù)交互場(chǎng)景的定制能力,開(kāi)發(fā)者可以直接使用云 IDE 完成一些輕量的函數(shù)開(kāi)發(fā)和更新,去填補(bǔ)單純根據(jù) Schema 生成的 GraphQL Endpoint 能力的不足,架構(gòu)設(shè)想圖如下:
數(shù)據(jù)源平臺(tái)會(huì)依托于基礎(chǔ)存儲(chǔ)服務(wù)和 Serverless 服務(wù),借助在線(xiàn)的 Schema Editor 來(lái)編輯關(guān)系型數(shù)據(jù) Schema,借由 Schema -> GraphQL Generator 自動(dòng)生成 GraphQL Endpoint,再由平臺(tái)提供的函數(shù)計(jì)算接口,在在線(xiàn) IDE 中完成函數(shù)的邏輯的開(kāi)發(fā),這些函數(shù)可以在數(shù)據(jù)源中扮演 middleware,trigger,intercepter 等的角色,為數(shù)據(jù)源能力提供一些補(bǔ)全和增強(qiáng)。
數(shù)據(jù)源平臺(tái)會(huì)作為現(xiàn)代 web 開(kāi)發(fā)的后臺(tái)的最高層次抽象,現(xiàn)代的業(yè)務(wù)側(cè)開(kāi)發(fā)者往往只需要在數(shù)據(jù)源平臺(tái)上進(jìn)行簡(jiǎn)單的操作即可配置出數(shù)據(jù)源供前端消費(fèi)。
有數(shù)據(jù)源 PaaS 的支持,傳統(tǒng)的 CRUD 工作可以很大程度消除,并且將一些剩余的任務(wù)收斂到前端(事實(shí)上相當(dāng)于前端與 CRUD 后端工種合并了,但由于 CRUD 的工作大部分被自動(dòng)化了,所以我們姑且仍然稱(chēng)這種具有全棧 Web App 開(kāi)發(fā)能力的工種叫做"前端")
上面這是一種思路,除此之外,還有將服務(wù)端與客戶(hù)端代碼放在一起的開(kāi)發(fā)模式,典型例子如 Blitz[9],它也遵循同樣的思路,代碼中沒(méi)有顯式的 http 調(diào)用,直接通過(guò)函數(shù)調(diào)用的方式直接對(duì)數(shù)據(jù)庫(kù)做 Query,這樣的優(yōu)勢(shì)是前后端合并,但是缺點(diǎn)還是有些明顯:由于沒(méi)有 GraphQL 這一層的轉(zhuǎn)換,可能會(huì)聲明過(guò)多的 Query 函數(shù),它將處理 Query 復(fù)用的責(zé)任遷移給了程序員,并且一個(gè)數(shù)據(jù)源 對(duì)接 多終端的場(chǎng)景不太合適,后端和前端的綁定過(guò)深,不易抽取出數(shù)據(jù)源,如果移動(dòng)端 App 和網(wǎng)頁(yè)都依賴(lài)這個(gè)數(shù)據(jù)源,使用這種方式不太好處理前后端的解耦,個(gè)人認(rèn)為這種方式在前后端聚合程度較高,且只有單一客戶(hù)端(比如只有 Web,沒(méi)有移動(dòng)端 app)的情形下比較適合,適用場(chǎng)景較窄。
構(gòu)建困境
DevOps 平臺(tái)是一個(gè)資源消耗大戶(hù):每當(dāng)應(yīng)用倉(cāng)庫(kù)的 release 分支發(fā)生 commit 的時(shí)候,往往就會(huì)觸發(fā)流水線(xiàn)的測(cè)試,構(gòu)建,部署等一系列運(yùn)維操作,而目前的生態(tài),前端的構(gòu)建涉及到依賴(lài)的拉取,依賴(lài)圖分析,打包依賴(lài),打包產(chǎn)物優(yōu)化等步驟,一次完整的構(gòu)建花費(fèi)的時(shí)間可能是分鐘級(jí)的:
上圖給出了目前 Web 應(yīng)用構(gòu)建所要經(jīng)歷的步驟,在敏捷開(kāi)發(fā)的場(chǎng)景下,如果 release 分支經(jīng)常得到更新的話(huà),流水線(xiàn)將經(jīng)常阻塞,而且如果是僅僅是更新了某個(gè)包的版本,或者更新了 readme,或者是修改了源碼中的變量命名,就需要全量的進(jìn)行上圖中繁重的工作的話(huà),這無(wú)疑是存在很大的算力和 I/O 浪費(fèi)的。
我們上文曾提到以組件為中心的協(xié)作方式,在那種協(xié)作方式下,我們注重組件的快速迭代,而一個(gè) web app 則會(huì)重度依賴(lài)上游的各種組件,總結(jié)一下,目前上圖的這種構(gòu)建/發(fā)布模式存在這幾個(gè)重大問(wèn)題:
- 修改一個(gè)文件中的一行代碼,觸發(fā)全量構(gòu)建,大量算力,I/O 浪費(fèi)。
- 上游的更新無(wú)法觸發(fā)下游流水線(xiàn)更新,或者說(shuō)下游無(wú)法 "觀(guān)察" 上游的更新。
對(duì)于第一個(gè)問(wèn)題剛才已經(jīng)解釋過(guò),第二個(gè)問(wèn)題可能更為嚴(yán)重,下面解釋一下:
包和包之間的依賴(lài),是一個(gè) 有向無(wú)環(huán)圖,在 npm package 這種管理模式下,一個(gè)包得到更新,往往依靠迭代新的版本號(hào)來(lái)解決,示意圖:
上圖中,Web App 依賴(lài)于 Form Widget 和 Sidebar Widget ,而這兩個(gè)組件又依賴(lài)于更基礎(chǔ)的 Button 組件,而當(dāng) Button 組件得到更新之后,比如版本從 3.2.1 遷移到了 3.2.2,這時(shí)候 Web App 應(yīng)用本身是不會(huì)收到這個(gè)通知的,它必須手動(dòng)重新運(yùn)行一次流水線(xiàn),才能將更新的依賴(lài) 3.2.2 打包進(jìn)構(gòu)建產(chǎn)物中。
在上面的 Button 更新的場(chǎng)景中,我們自然希望所有依賴(lài) Button 的 Web App,在 Button 得到更新后,立即能夠使用新的 Button。
npm 這種基于版本的發(fā)布更新方式,雖然 semantic version 本身能夠起到對(duì)包的兼容性等的基本管控,但它本質(zhì)上是一種君子協(xié)定,包不遵守也沒(méi)辦法,其實(shí),我在上文中曾提到現(xiàn)代的 Web App 傾向于 "無(wú)版本號(hào)化",只要源碼改動(dòng)能夠以極低的成本,極快的速度觸發(fā)產(chǎn)品更新,那版本號(hào)這種方案就可以廢棄,如果我們能夠很容易的追溯過(guò)去任何一個(gè)組件在任何一個(gè)時(shí)間點(diǎn)的狀態(tài),那所謂的版本號(hào)的意義只是用來(lái)聲明 break changes 的發(fā)生節(jié)點(diǎn)。
前端開(kāi)發(fā)在很多場(chǎng)景下被迫使用 monorepo,也是使用 semver (semantic version) 作為迭代的方式的失敗證明。若快速迭代一個(gè)包,則版本數(shù)爆炸增長(zhǎng),若想讓版本號(hào)慢速增長(zhǎng),則需要累計(jì)更新,又失去了敏捷性,這看起來(lái)是一個(gè)無(wú)法調(diào)和的矛盾 (關(guān)于 monorepo 和其它的替代方案的討論,會(huì)在下面一個(gè) section 深入討論)。
造成這種構(gòu)建困境的源頭,其實(shí)和歷史包袱有關(guān),那就是純?yōu)g覽器端的 module load 一直在過(guò)去都被認(rèn)為是一個(gè)還沒(méi)有得到良好覆蓋的 es6 特性,但是截至目前,es module 在除了 IE 之外的其它主流瀏覽器中,已經(jīng)得到了良好的覆蓋:
之前基于 webpack,rollup 等工具的生態(tài),是為了既能讓開(kāi)發(fā)側(cè)可以享受 module 帶來(lái)的好處,又能在瀏覽器側(cè)加載單文件提升加載速度和兼容性,如果我們不再考慮 es module 帶來(lái)的兼容性問(wèn)題,那么我們就可以開(kāi)始進(jìn)行對(duì) esm 的使用和驗(yàn)證,相關(guān)的工具已經(jīng)不斷涌現(xiàn),典型的例子如 Vite[10] ,Snowpack[11] 等,這類(lèi)構(gòu)建工具可以簡(jiǎn)稱(chēng)為 bundless build tool。
但是目前,拿 Vite 來(lái)說(shuō),它們僅僅是在開(kāi)發(fā)模式下啟用無(wú)打包模式,生產(chǎn)環(huán)境仍然使用打包,原因在于目前關(guān)于生產(chǎn)環(huán)境中使用 esm,一些測(cè)試結(jié)果表明仍然會(huì)影響性能,可汗學(xué)院曾嘗試進(jìn)行 esm 的全量遷移,即便是在 HTTP2 的加持下,加載速度仍然變慢了:Forgo JS packaging?Not so fast (khanacademy.org)[12]
但是同樣也有一些數(shù)據(jù)表明,在應(yīng)用本身的體量較小的情況下,全量使用 esm 是完全 OK 的:ES modules in production:my experience so far | Bryan Braun - Designer/Developer[13]
在可汗學(xué)院的博客中提到,全量 esm 的性能下降原因主要來(lái)源于 HTTP2 的一些加載 issue 和 多個(gè)小文件的解壓縮開(kāi)銷(xiāo)增大,最后的結(jié)果是使用 esm 使得資源下載時(shí)間從 0.6s 漲到了 1.7s,最后得出的結(jié)論是目前仍然推薦使用 bundle 用于生產(chǎn)。
但是其測(cè)試并未考慮 esm 能帶來(lái)的更多的優(yōu)勢(shì),在這些新的優(yōu)勢(shì)和網(wǎng)絡(luò)協(xié)議的發(fā)展下,esm 之后做到基本和 bundle 持平或者接近,個(gè)人認(rèn)為是完全有可能的。
esm 能帶來(lái)的潛在優(yōu)勢(shì)如下:
- 全局依賴(lài)緩存。
- 大幅降低流水線(xiàn)構(gòu)建的計(jì)算和 I/O 負(fù)擔(dān),甚至可以跳過(guò)構(gòu)建這個(gè)步驟。
- 上游更新后,用戶(hù)加載頁(yè)面時(shí),可以直接加載更新后的組件的代碼,達(dá)到了真正的敏捷更新。
可以看到后兩個(gè)問(wèn)題是我們上面提到過(guò)的,如果使用了 esm,由于代碼的加載是直接通過(guò) import 的方式,那么當(dāng)上游的一個(gè)組件更新后,瀏覽器側(cè)就可以直接加載到更新后的組件的代碼,完全不需要觸發(fā)任何依賴(lài)了該組件的項(xiàng)目的流水線(xiàn),而且當(dāng)應(yīng)用更新的時(shí)候,如果是打包模式,用戶(hù)需要全量加載新的 js 資源,但是在 esm 場(chǎng)景下,用戶(hù)只需要重新加載更新后的那一小部分即可。
很多 Bug 的發(fā)生都是局部的,比如 Bug 發(fā)生在組件內(nèi)部,當(dāng)修復(fù)了這些 Bug 后,工程師只需要將更新 push 到組件注冊(cè)中心,即可完成 Bug 修復(fù),無(wú)需觸發(fā)下游無(wú)數(shù)的 app 的流水線(xiàn)。
而下游如果想鎖定上游的版本,也可以直接用 commit hash 鎖定,這樣就不會(huì)加載到更新的組件版本,保證了下游的一致性。
第一條:全局依賴(lài)緩存 是指,不同的域名,應(yīng)用之間,有很多的包都是公用的,比如 react,這些包加載了一次之后,就不需要再次加載了,隨著用戶(hù)使用瀏覽器的增多,本地的緩存就會(huì)變得更多,用戶(hù)訪(fǎng)問(wèn)新網(wǎng)站后,需要加載的新依賴(lài)就會(huì)變得更少,而這的前提是這些網(wǎng)站都使用同樣的 CDN,這種 CDN 應(yīng)該專(zhuān)為瀏覽器側(cè)的 esm 做了優(yōu)化,支持 HTTP2/3 等新的協(xié)議, 這種全局依賴(lài)緩存的建立,會(huì)進(jìn)一步縮小 esm 和 bundle 之間的性能差距。
第一個(gè)將這個(gè)思路 built in mind 的,應(yīng)該是 deno,它原生支持 http import,為服務(wù)側(cè)基于 cdn 的 import 的開(kāi)發(fā)做了準(zhǔn)備,與之配套的就是相關(guān)的開(kāi)發(fā)套件,比如 VSCode 相關(guān)插件的支持,以及 CDN for module,對(duì)于這種 CDN,已經(jīng)有了類(lèi)似的產(chǎn)品:Skypack: search millions of open source JavaScript packages[14]。
個(gè)人大膽預(yù)測(cè)一下,五年之后的 web 開(kāi)發(fā),不管在 dev 還是 prod,不管是 server 還是 client,都會(huì)采用 CDN for module + http import 這種模式,帶來(lái)前端的新一輪敏捷革命。(現(xiàn)在已經(jīng)開(kāi)始了 )
代碼管理困境
沒(méi)有包管理的時(shí)代,人們的應(yīng)用都包含了全部代碼,有了包管理后,人們傾向于每個(gè)包都有自己獨(dú)立的 git 倉(cāng)庫(kù)來(lái)管理,但是有時(shí)候又想將一些包放在一起來(lái)開(kāi)發(fā),于是又有了 monorepo:
這樣搞來(lái)搞去其實(shí)沒(méi)什么意思,都沒(méi)有根本解決問(wèn)題,我們引入 Monorepo 是因?yàn)槲覀兿胍瑫r(shí)對(duì)一些包做改動(dòng),然后統(tǒng)一發(fā)布更新,如果分開(kāi),程序員需要每天在不同的倉(cāng)庫(kù)中輾轉(zhuǎn),并且需要不斷地 publish&update 才能在另外的包用到更新的包,但是引入了 Monorepo 后,commit history 就混入了各種包的 commit,不方便追蹤某個(gè)模塊的改動(dòng),與之相對(duì)應(yīng)的一種代碼管理方式是以 Git - Submodules (git-scm.com)[15] 為代表的子倉(cāng)庫(kù)模式,父?jìng)}庫(kù)可以依賴(lài)于其它的子 git 倉(cāng)庫(kù),在父?jìng)}庫(kù)做的 commit 不會(huì)進(jìn)入到子倉(cāng)庫(kù)中,同時(shí)在開(kāi)發(fā)父?jìng)}庫(kù)的時(shí)候,又可以修改子倉(cāng)庫(kù)的代碼,甚至進(jìn)行 commit,它很好的平衡了 作為依賴(lài)引入 和 想要隨時(shí)修改 的兩個(gè)需求,實(shí)測(cè)好用。
可惜的是在 npm 這樣的生態(tài)下,發(fā)布的東西和源碼可以不是一種東西,發(fā)布的包也不再是一個(gè) Git 倉(cāng)庫(kù),其它包引用某包的時(shí)候,先不談?dòng)袥](méi)有 push 某包的權(quán)限,本身就無(wú)法當(dāng)作 git submodule 來(lái)使用,即便把代碼 push 到源 repo 了,也不會(huì)觸發(fā) package 的更新,還需要手動(dòng)發(fā)布,這個(gè)和 Go 語(yǔ)言的基于 git 倉(cāng)庫(kù) + git registry 的依賴(lài)管理方式形成了鮮明的對(duì)比,個(gè)人認(rèn)為這也是 npm 設(shè)計(jì)的一大敗筆。
我認(rèn)為新的包管理模式,應(yīng)該是和 Git 倉(cāng)庫(kù)綁定的,參考 Go 語(yǔ)言,我們使用 Git 來(lái)進(jìn)行代碼的管理,發(fā)布到注冊(cè)中心的包,仍然是一個(gè) Git 倉(cāng)庫(kù),只不過(guò)是一個(gè) remote history,當(dāng)它作為依賴(lài)?yán)〉臅r(shí)候,會(huì)連并構(gòu)建產(chǎn)物一并拉取,(git clone --depth 1 拉取最新的 snapshot) 而如果想要即時(shí)修改該包并且 push 更新,則可以使用提供的新的命令行工具進(jìn)行 dependency 到 git submodule 模式的轉(zhuǎn)化,轉(zhuǎn)化后,會(huì)變成 git submodule,你就可以即時(shí)的修改這個(gè)包了。
而構(gòu)建步驟和 semver 相關(guān)的東西,可以深度集成流水線(xiàn)和一些 tag,下游使用上游依賴(lài)也仍然可以鎖定版本,這些都可以解決。
所以我認(rèn)為未來(lái)的包管理中心可能是:
所有的包在 registry 中都是作為一個(gè) git 倉(cāng)庫(kù)存在的,而本地開(kāi)發(fā)的時(shí)候,既可以將其作為依賴(lài),也可以將其一個(gè)命令轉(zhuǎn)化為 git submodule,這樣就可以靈活的協(xié)調(diào)依賴(lài)和快速修改反饋之間的矛盾了。
而這個(gè)包注冊(cè)中心,應(yīng)該和上面所說(shuō)的組件注冊(cè)中心其實(shí)是一個(gè)東西,每個(gè)組件也都是一個(gè) package,后者是前者的子集。
總結(jié)
把上面提到的技術(shù)設(shè)想畫(huà)成一張大圖:
網(wǎng)頁(yè)名稱(chēng):現(xiàn)代 Web 開(kāi)發(fā)困局
當(dāng)前URL:http://fisionsoft.com.cn/article/cdshees.html


咨詢(xún)
建站咨詢(xún)
