前端框架的復雜度最近一段時間頻頻遭到質(zhì)疑,引發(fā)了一些吐槽蔓搞,甚至有一篇文章提到:『前端所有主流的框架胰丁,都是在自欺欺人』。本文主要是向前端的初學者介紹前端框架的發(fā)展歷程及設(shè)計思想锦庸,比如為何要引入這樣那樣的“復雜度”?這樣『設(shè)計』有什么好處妻顶?是為了解決什么問題酸员?了解其背后的原因蜒车,我們或許就不會那么多抱怨了讳嘱。
目錄
1 前端框架怎么你了?
2 前端框架的發(fā)展史
3 前端框架為何要引入“復雜度”
4 為什么當下這么多主流框架酿愧?
5 知其然知其所以然
6 題外話:Typescript 引入復雜度了嗎
7 總結(jié)
01 前端框架怎么你了沥潭?
最近 Rich Harris 介紹了 Svelte 5 的新特性 —— runes。這引發(fā)了一位外國小伙的不滿嬉挡,小伙發(fā)表了一篇文章《前端所有主流的框架钝鸽,都是在自欺欺人》對各種主流前端框架進行了一番吐槽。
到底這個 Svelte 5 的 runes 是設(shè)計得多“反人類”庞钢?以至于外國小伙這么無法忍受拔恰。
為此,筆者仔細地閱讀了 Svelte 5 發(fā)布新特性的文章 《Introducing runes》基括。下面一個小節(jié)將講講這個 Svelte 5 的新特性颜懊。https://svelte.dev/blog/runes
1.1 探秘 Svelte 的新特性 runes
原來,這個 runes 的特性是 Svelte 的響應(yīng)式編程的強化版本风皿。眾所周知河爹, Svelte 是目前使用人數(shù)最多的編譯型前端框架。與 Vue桐款, React 基于運行時的數(shù)據(jù)管理方式不同咸这,Svelte 的響應(yīng)式是在編譯期間處理的,這么做讓 Svelte 的渲染性能有比較明顯的優(yōu)勢魔眨,跟原生 JS 性能接近媳维。
大致的原理是這樣的:Svelte 通過魔改了 JavaScript 編譯器,讓 JavaScript 的賦值語句帶有響應(yīng)式的能力遏暴。讓我們看看下面這段 Svelte 的代碼:
<script>
let count = 0;
const handleClick = () => {
count += 1;
}
</script>
<div> count: {count} <button on:click={handleClick}>inc</button></div>
上面的代碼侨艾,點擊按鈕就能讓 count 遞增,進而讓頁面顯示最新的 count 的值拓挥。這種讓賦值語句帶有響應(yīng)式的魔法唠梨,正是因為 Svelte 的編譯器識別了 “count += 1" 是一個賦值語句,為其生成了響應(yīng)式的邏輯侥啤。
但目前版本的 Svelte 框架還存在一些問題需要解決当叭。比如茬故,當我們把 "let count = 0;"放到一個函數(shù)內(nèi)部蚁鳖, Svelte 就不會給他加上響應(yīng)式的邏輯了磺芭。這就與 Svelte 一開始給我們的變量自動帶有響應(yīng)式的開發(fā)體驗相悖,導致了語句的歧義醉箕,從而提升了開發(fā)的心智負擔钾腺。我們在開發(fā) Svelte 要時刻提醒自己,只有把變量定義在最外層讥裤,才具備響應(yīng)式放棒。
而 Svelte 5 的 runes 實際上就是為了消除這些心智負擔而設(shè)計的。
在 Svelte 5 中己英,只要把 "let count = 0;" 改為 "let count = state 即可。這實際上是給開發(fā)者進行減負治拿,消除了響應(yīng)式定義的歧義摩泪。
1.2 外國小伙的槽點
而正因為這個改動,變成一個導火索劫谅,點燃了前文說到的外國小伙见坑,發(fā)表了一篇文章對所有主流的前端框架進行批判。
大致看了下外國小伙的文章同波,他有以下一些槽點:
- HTML 不是前端框架最佳的選項鳄梅;
- 前端框架引入了復雜度問題;
- 前端框架編造出的模板語法完全沒必要未檩,用 DOM API 更好戴尸;
- 不同框架的模板語法不統(tǒng)一。
這也是目前前端新人會吐槽的點冤狡,隨著前端不斷引入新的框架孙蒙、打包工具、概念等悲雳,很多人表示『學不動了』挎峦。
接下來我們重點從前端框架來看下復雜度增加的原因及利弊,在此之前我們先了解下前端框架的發(fā)展歷史合瓢,弄清楚前端為什么會發(fā)展到現(xiàn)在這樣坦胶。
02 前端框架的發(fā)展史
前端框架從無到有,其實是伴隨著前端開發(fā)的復雜度從簡單到復雜的發(fā)展歷程而演變的。
最早的網(wǎng)頁顿苇,由于瀏覽器功能的局限峭咒,在網(wǎng)頁端并沒有邏輯,是純展示纪岁。包含業(yè)務(wù)邏輯凑队,展示邏輯在內(nèi)的所有邏輯都在后端處理。這個時候前端的職責非常薄幔翰。
后來 Netscape 公司發(fā)明了革命性的 JavaScript 漩氨,讓網(wǎng)頁可以運行程序,這就讓程序員可以把一部分交互邏輯放到網(wǎng)頁端遗增。當時存在一個問題是叫惊,不同瀏覽器廠商對外暴露的 BOM 接口和 JavaScript 語法在細節(jié)上是有差異的,并且能力上并不對齊贡定。導致當時的 web 程序員在開發(fā)同一個邏輯時可都,不得不適配多個瀏覽器的接口缓待。
當時每個程序員都在重復這樣的工作。這個時候渠牲,jQuery 和 Prototype.js 呼之欲出旋炒。它們把底層對瀏覽器的接口調(diào)用都封裝了一層,在不同瀏覽器上可以采用同樣的寫法签杈,解決了程序員反復開發(fā)瀏覽器兼容代碼的問題瘫镇。
這應(yīng)該算得上是最早期的前端框架的形態(tài),但其更像是工具函數(shù)的封裝答姥,它的誕生主要是為了解決瀏覽器兼容性的問題铣除。
直到 DOM 標準, ECMAScript 標準的制定鹦付,各個瀏覽器內(nèi)核開始遵循標準尚粘,最終趨于統(tǒng)一,差異問題逐漸消除敲长,這類前端框架才退出歷史舞臺郎嫁。
隨著前端項目不斷復雜化,面臨著項目規(guī)模不斷變大帶來的模塊管理困難的問題祈噪,這時候支持模塊管理的工具庫應(yīng)運而生泽铛,比如 SeaJS 還有 RequireJS。后面又出現(xiàn)了基于編譯的模塊構(gòu)建工具辑鲤,如 fis盔腔、webpack、vite 等等,進一步優(yōu)化模塊加載弛随、分包等問題澈蝙。
同時通過 MV* (MVC,MVP撵幽,MVVM)設(shè)計模式降低復雜度的框架也不斷涌現(xiàn)灯荧,如 Backbone、Ember盐杂、Knockout逗载、Angular、React链烈、Vue 等等厉斟。
后面隨著瀏覽器能力不斷提升,前端被賦予的職責也越來越多强衡,而開發(fā)的復雜度也隨之提升擦秽。伴隨而來的是,復雜度產(chǎn)生的可維護性低問題漩勤「谢樱基于直接操作 DOM,BOM 的開發(fā)模式越败,沒有運用一定的設(shè)計模式触幼,必然會隨著需求的迭代凸顯維護性低的問題。
所以這個時候誕生的框架究飞,就是為了提升可維護性而產(chǎn)生的置谦。它們帶來了組件的概念,響應(yīng)式數(shù)據(jù)的概念亿傅,模板渲染的概念媒峡。這些設(shè)計模式,幫助我們開發(fā)出封裝性更好葵擎,復用性更強谅阿,隱藏了 DOM 的操作的底層細節(jié),這些特性都大幅降低了項目的復雜度坪蚁。
縱觀前端框架發(fā)展史奔穿,我們可以看到,每個框架的出現(xiàn)都是為了解決當下的一個痛點敏晤,當然框架本身會引入一定的復雜度贱田,但整體來說是利遠大于弊。
03 前端框架為何要引入“復雜度”
接下來嘴脾,我們聊聊現(xiàn)代前端框架帶來的“復雜度”男摧。為什么要引入這些“復雜度”蔬墩,以及這些設(shè)計帶來的好處是什么?
3.1 HTML 模板:隱藏實現(xiàn)細節(jié)耗拓,降低開發(fā)難度
我們知道現(xiàn)代的前端框架基本都采用了類似 HTML 的語法來開發(fā)界面拇颅。并或多或少對這種語法進行擴展,支持條件渲染乔询,循環(huán)渲染樟插,組件渲染等等「偷螅可能剛開始接觸會覺得稍微有點理解成本黄锤。
下文將對比原生的寫法,來找出這種設(shè)計的必要性食拜。
松散 VS 結(jié)構(gòu)化
我們知道直接使用 DOM 開發(fā)鸵熟,通常是使用 document.createElement,appendChild, removeChild 對 DOM 樹進行操作负甸,通過 setAttribue 修改 DOM 的屬性流强。
使用這種原始的 API,我們需要時刻關(guān)注很多 DOM 的增刪改查的細節(jié)呻待,處理起來比較繁瑣打月,也不夠優(yōu)雅。我們寫出來的带污,可能是一堆松散的 DOM API 調(diào)用僵控。
比如我們要實現(xiàn)這么一個功能:界面上有一個方塊和一個按鈕香到,每按下按鈕鱼冀,當方塊是顯示狀態(tài),則隱藏方塊悠就,當方塊是隱藏狀態(tài)千绪,則顯示方塊。
使用原生的 API 實現(xiàn)是這樣的:
<div class="block">a block</div>
<button class="toggle-button">toggle block</button>
<script>
const block = document.querySelector('.block');
const toggleButton = document.querySelector('.toggle-button');
let blockVisible = true;
toggleButton.addEventListener('click', () => {
blockVisible = !blockVisible;
block.style.display = blockVisible ? 'block' : 'none';
});
</script>
從代碼可以看到梗脾,我們需要對每個要操作的 DOM 定義類名荸型,方便我們拿到他們的引用,需要獲取對 DOM 節(jié)點的引用:document.querySelector('.block')
炸茧,對 DOM 事件進行綁定:toggleButton.addEventListener('click', () => {})
瑞妇。
我們開發(fā)過程中,不希望去關(guān)注這些重復的細節(jié)梭冠,我們需要更直觀的寫法辕狰。我們希望能直觀地從模板就看出我們這個程序的意圖,比如按鈕點擊了要去執(zhí)行什么邏輯控漠,某個 div 是否有顯示隱藏的狀態(tài)變化蔓倍。我們看看前端框架(Vue) 是怎么實現(xiàn):
<template>
<div v-if="blockVisible">a block</div>
<button @click="handleClick">toggle block</button>
</template>
<script setup>
const blockVisible = ref(true);
const handleClick = () => {
blockVisible.value = !blockVisible.value;
}
</script>
上面的代碼看起來就很簡潔了悬钳,也更結(jié)構(gòu)化了。
只需要改變 v-if 的值偶翅,Vue 就會幫我們處理了 DOM 節(jié)點的“顯示”和“隱藏”默勾。
在 DOM 版本代碼里的三個步驟,定義類名聚谁、獲取引用母剥、綁定事件,在 Vue 里只剩下綁定事件需要我們做形导,而 Vue 這種綁定事件的寫法也更加簡潔媳搪。
框架幫我們監(jiān)聽了狀態(tài)的變化,并自動更新了視圖骤宣,比如上面例子里 的 blockVisible秦爆,我們只要對它賦值,Vue 就會知道更新哪里的視圖憔披,不需要我們記住這個變量關(guān)聯(lián)了哪個 DOM 節(jié)點等限。
@click 幫我們綁定了事件,讓我們直觀的知道按鈕按下芬膝,就要去執(zhí)行一個叫 handleClick 的方法望门。
整個開發(fā)過程,我們不需要關(guān)注 DOM 節(jié)點是怎么操作的锰霜,符合對隱藏細節(jié)的封裝原則筹误。
設(shè)想如果我們在開發(fā)業(yè)務(wù)的過程,還要不斷地考慮怎么操作 DOM癣缅,DOM 和數(shù)據(jù)之間怎么關(guān)聯(lián)厨剪,其實是不符合職責的解耦原則,我們首要關(guān)注的是我們開發(fā)的業(yè)務(wù)邏輯友存, DOM 操作祷膳,UI 狀態(tài)流轉(zhuǎn)交給框架。
所以這個“復雜度”其實是降低了我們開發(fā)的難度屡立,是我們可以更加專注在業(yè)務(wù)邏輯直晨,而且代碼看起來更加結(jié)構(gòu)化了,使得代碼更易開發(fā)膨俐、維護成本都得到了大幅提升勇皇。
注:這里用 v-if 會直接掛載或刪除 DOM 節(jié)點,如果要一比一還原 DOM API 版本的代碼焚刺,只需要改為 v-show
3.2 組件:提升復用性
相信沒有一個現(xiàn)代的前端框架敛摘,能夠脫離組件的概念。那為何要引入組件這個“復雜度”呢檩坚?
直接用 DOM 鋪開來寫着撩,可不可以呢诅福?
答案當然是不行的。就跟我們寫通用的代碼邏輯一樣拖叙,必不可少的就是封裝氓润。如果我們不對重復的邏輯加以封裝,那代碼將會變得冗余薯鳍,導致難以維護咖气。
我們開發(fā)過程中,都會對重復的邏輯進行封裝挖滤,變成函數(shù)崩溪,或者類,通過不斷的拆分斩松、封裝伶唯、解耦,讓我們的代碼時刻保持在一個可維護的狀態(tài)惧盹。
但早期的 DOM 規(guī)范是沒有組件的概念的(注:直到 Web Component 的誕生)乳幸,所有組件復用的邏輯,都需要自己封裝钧椰。
比如我們需要編寫一個經(jīng)典的 Todo list粹断。如果我們使用原生 DOM,是這樣的:
<div class="todo-list">
<div>
new item 1
<button>X</button>
</div>
<div>
new item 2
<button>X</button>
</div>
</div>
<button>add item</button>
<script>
const todoList = document.querySelector('.todo-list');
const addButton = document.querySelector('button');
addButton.addEventListener('click', () => {
const todoItem = document.createElement('div');
const deleteButton = document.createElement('button');
todoItem.appendChild(deleteButton);
todoItem.innerText = 'new item';
todoList.appendChild(todoItem);
});
</script>
上面的代碼嫡霞,基本沒有什么復用性可言瓶埋, todoItem 的邏輯完全跟 todoList 耦合了。
或許我們可以封裝一下诊沪,讓 todoItem 不與 todoList 耦合养筒。于是我們把代碼改成這樣:
<div class="todo-list">
</div>
<button>add item</button>
<script>
class TodoItem {
constructor(content) {
const todoItem = document.createElement('div');
const deleteButton = document.createElement('button');
todoItem.appendChild(deleteButton);
todoItem.innerText = content;
this.dom = todoItem;
}
appendTo(parent) {
return parent.appendChild(this.dom);
}
}
const todoList = document.querySelector('.todo-list');
const addButton = document.querySelector('button');
new TodoItem('new item 1').appendTo(todoList);
new TodoItem('new item 2').appendTo(todoList);
addButton.addEventListener('click', () => {
new TodoItem().appendTo(todoList);
});
</script>
這樣可能會好一些,我們把跟 TodoItem 相關(guān)的邏輯都封裝到 TodoItem 類里娄徊。這樣闽颇,我們不僅可以把 TodoItem 用在 todoList 的場景,也可以用在其他場景寄锐。
如果用現(xiàn)有的前端框架,組件的功能已經(jīng)原生內(nèi)建了尖啡,我們可以開箱即用橄仆,編寫起來更簡潔更優(yōu)雅。
我們是可以按照上面原生的方式去封裝衅斩,但實際的情況遠沒有一個 demo 這么簡單盆顾,我們需要考慮樣式隔離,組件生命周期等等畏梆,現(xiàn)代框架 React您宪、Vue.js 非常好的解決了這些問題奈懒。
我們看看如果用 Vue 怎么寫同樣的邏輯:
todo-item.vue:
<template>
<div>
new item
<button>delete</button>
</div>
</template>
todo-list.vue:
<div class="todo-list">
<todo-item v-for="item in todoItems" />
</div>
<button @click="handleClick">add item</button>
<script setup>
import TodoItem from './todo-item.Vue';
const todoItems = [];
const handleClick = () => {
todoItems.push({});
};
</script>
這么實現(xiàn),代碼量直接減少 10 行宪巨,同時我們獲得了數(shù)據(jù)響應(yīng)式磷杏,DOM 節(jié)點復用(v-for),樣式隔離等等好處捏卓。
這里 Vue demo 單獨定義了 一個 todo-item.Vue 的組件极祸,可以直接在 todo-list 組件里引用,通過 v-for 語句怠晴,可以遍歷插入 todo-item 組件遥金。我們只需要修改 todoItems 數(shù)組的值,對應(yīng)的視圖就會更新蒜田。
在這些框架里稿械,我們可以把一個組件當成一個 “HTML 標簽”來使用,其實這也是 web components 的思想冲粤。
這樣寫出來的代碼溜哮,通過看 HTML 模板的代碼,就可以很清楚的看出組件的層級關(guān)系色解。
3.3 VDOM/ 編譯器機制:跨平臺
假如我們用原生的 DOM API 寫了一個網(wǎng)頁應(yīng)用茂嗓,但我們需要進一步開拓我們的應(yīng)用市場,我們的應(yīng)用需要作為一個獨立的 App 或者小程序進行發(fā)布科阎。這個時候我們第一時間想到的方法有兩種:第一是重新用 Java述吸,Swift 和小程序框架重寫我們的應(yīng)用,第二種是我們把網(wǎng)頁作為一個內(nèi)嵌頁锣笨,嵌入對應(yīng)的外殼里蝌矛。
第一種研發(fā)成本非常大,第二種無論性能還是體驗都比較一般错英。那是不是魚和熊掌不能兼得呢入撒?
既要研發(fā)成本低,又要性能體驗好椭岩,其實使用現(xiàn)代前端框架是一個合適的方案】蟊埃現(xiàn)代的前端框架折晦,底層基本都是大同小異的設(shè)計思路,不是虛擬 DOM 就是編譯機制。無論是怎么樣的形式抖拦,它們都做到把面向開發(fā)者的接口與面向底層的細節(jié)隔離開了导绷。這種設(shè)計的好處是弄捕,我們開發(fā)的代碼可以具備跨平臺的能力缩筛。
比如 React 有 React Native,Taro锌仅, Vue 有 Weex章钾,Uni-App 等原生運行時墙贱,我們幾乎可以用最小的改動成本,將我們面向網(wǎng)頁開發(fā)的應(yīng)用遷移到移動端或者小程序上面贱傀。
我們可以看看下面這個圖:
就像上面的示意圖惨撇,框架面向開發(fā)者提供了一套抽象的接口,開發(fā)者基于這套接口開發(fā)應(yīng)用窍箍,不會接觸到平臺底層的細節(jié)串纺。
比如,當我們使用 Vue 開發(fā)一個應(yīng)用時椰棘,我們通常是不需要關(guān)注如何操作 DOM纺棺,如何綁定樣式的。只要框架在內(nèi)部對接了多套平臺的 API邪狞,開發(fā)者開發(fā)的應(yīng)用就可以運行在多個平臺上祷蝌,并且做到一次開發(fā)到處運行。
其實我們不需要深入探討每個框架是怎么實現(xiàn)的帆卓,只需要知道巨朦,在框架的設(shè)計中,有這么一套對底層平臺的抽象:把 UI 元素的創(chuàng)建剑令,更新糊啡,刪除等接口抽象出來,然后再針對不同平臺實現(xiàn)對應(yīng)的操作吁津。
下面的偽代碼描述了框架是如何做到:
面向底層的抽象接口:
interface IUIOperations {
面向瀏覽器端的實現(xiàn):
interface IUIOperations {
createElement(type: ElementType): Element;
removeElement(ele: Element);
updateElement(ele: Element);
setProperty(ele: Element, propName: string; propValue: string);
removeProperty(...
}
面向原生平臺的實現(xiàn):
class DOMUIOperations implements IUIOperations {
createElement(type: ElementType): Element {
return new DOMElement(document.createElement(type.getName()));
}
removeElement(ele: Element) {
...
}
updateElement(ele: Element)
...
}
網(wǎng)上有一些文章說虛擬 DOM 的作用之一是提供這種跨平臺的特性棚蓄,實際上按照我們上面所畫的框架架構(gòu)圖,內(nèi)部實現(xiàn)是怎么樣的對是否能跨平臺其實并沒有太大影響碍脏,只要做好對底層平臺的抽象就可以了梭依。這也就是為什么現(xiàn)在編譯型的框架,雖然它并沒有虛擬 DOM典尾,照樣也能跨平臺役拴。
有了這個機制,就可以實現(xiàn)一套代碼同時跑在移動端 App钾埂、PC 應(yīng)用程序河闰、H5 頁面等多端。
3.4 數(shù)據(jù)響應(yīng)式:降低數(shù)據(jù)管理復雜度
早期勃教,我們使用 DOM 開發(fā)應(yīng)用時淤击,遇到數(shù)據(jù)與視圖之間的狀態(tài)同步場景,通常免不了手忙腳亂對 DOM 的一頓操作故源,開發(fā)過程中要時刻關(guān)注每個數(shù)據(jù)關(guān)聯(lián)的 DOM 節(jié)點。
而數(shù)據(jù)響應(yīng)式的誕生汞贸,讓我們開發(fā)中绳军,不需要關(guān)注這些細節(jié)印机。我們只需要操作數(shù)據(jù),框架可以讓視圖可以自動更新门驾。
假設(shè)我們需要在按鈕按下時射赛,將一段文本反轉(zhuǎn)過來,并顯示到頁面上奶是。
<div>
</div>
<button>reverse</button>
<script>
const div = document.querySelector('div');
const button = document.querySelector('button');
let msg = 'hello, world';
div.innerText = msg;
button.addEventListener('click', () => {
msg = msg.split('').reverse().join('');
div.innerText = msg;
});
</script>
每當需要往視圖上更新數(shù)據(jù)時楣责,我們都需要對 DOM 進行顯式的修改。
我們再看看如果通過框架的數(shù)據(jù)響應(yīng)式聂沙,上面的程序會是怎么寫的:
<div>
{msg}
</div>
<button on:click="handleClick">reverse</button>
<script>
let msg = 'hello, world';
const handleClick = () => {
msg = msg.split('').reverse().join('');
};
</script>
我們發(fā)現(xiàn)秆麸,我們并沒有看到哪一行代碼是顯式在修改視圖的,數(shù)據(jù)與視圖唯一有關(guān)聯(lián)的地方及汉,就是在視圖模板里加入了數(shù)據(jù)變量的引用:<div>{msg}<div>
當我們對數(shù)據(jù)修改時沮趣,框架就可以感知這種修改,并對數(shù)據(jù)所關(guān)聯(lián)的視圖進行刷新坷随。
具體是怎么做到的呢房铭?每個框架的實現(xiàn)都不盡相同。
這里以 Vue 的實現(xiàn)簡單說一下温眉,當 Vue 按照模板首次渲染時缸匪,會收集模板和數(shù)據(jù)變量的關(guān)聯(lián)關(guān)系,相當于視圖訂閱了數(shù)據(jù)變量變化的事件类溢,一旦數(shù)據(jù)發(fā)生變化凌蔬,就會根據(jù)這個關(guān)聯(lián)關(guān)系,找到對應(yīng)的視圖豌骏,并調(diào)用它的更新函數(shù)龟梦。
有了框架幫我們處理好數(shù)據(jù)和視圖的關(guān)聯(lián),我們就不需要自己顯式的管理和操作數(shù)據(jù)關(guān)聯(lián)的 DOM窃躲。我們的心智負擔進一步降低计贰,這讓我們有更多的精力去開發(fā)上層的交互和業(yè)務(wù)邏輯。
04 為什么當下這么多主流框架蒂窒?
上面講了現(xiàn)代框架引入復雜度的好處躁倒,那是否可以一個框架就夠了呢?這些框架做得都是大同小異的事洒琢,為何還需要重復造輪子呢秧秉?
有 React,Vue衰抑,Angular象迎,近期又有了 Svelte, solid,最近又出現(xiàn)了 Qwik砾淌,Astro啦撮。
其實每個框架的誕生也有其背景和其想解決的問題。
接下來我們重點聊下現(xiàn)代框架的發(fā)展歷程汪厨,及每個框架的設(shè)計哲學和對應(yīng)的受眾赃春。
4.1 React 誕生的意義
首先聊聊 React,2011 年前后劫乱, Facebook 的業(yè)務(wù)快速發(fā)展织中,產(chǎn)生大量需求。
這里需要注意一下當時的背景衷戈,這個時期主流的開發(fā)方式還是通過 jQuery 直接操作 DOM狭吼,手動管理 UI 的狀態(tài),自行確保視圖和數(shù)據(jù)之間的狀態(tài)同步脱惰。
隨著需求的不斷增多搏嗡,如果繼續(xù)采用這種傳統(tǒng)的開發(fā)方式開發(fā) UI 交互,必然會帶來后續(xù)維護困難的問題拉一。
在這種背景下采盒,要繼續(xù)疊加需求,只能通過不斷加入開發(fā)人員蔚润,不斷產(chǎn)生大量的冗余的交互邏輯和錯亂的狀態(tài)管理磅氨。這沒有根本解決問題,這個問題需要從設(shè)計層面上來優(yōu)化嫡纠。
React 就是在這樣的背景下誕生烦租,最初只是為了解決 Facebook 內(nèi)部開發(fā)的可維護性低問題。在內(nèi)部實踐取得成功后除盏,再逐步對外推廣叉橱,在確實解決了其他開發(fā)者同樣面臨的痛點后,最終才成為一個主流的框架者蠕。
React 框架的設(shè)計理念之一是極簡主義
從語法角度上看窃祝, React 在傳統(tǒng)的前端技術(shù)棧上,只引入了 jsx踱侣,用于表達虛擬 DOM 的構(gòu)造過程粪小,其他的一切都是原生的 JavaScript。這樣設(shè)計的好處是抡句,降低了使用者的學習成本和心智負擔探膊,讓框架的靈活性極高。比如待榔,我們可以使用原生 Javascript 的 if else 語句表達視圖的條件顯示逞壁,用 for,map 等表達視圖中的循環(huán)列表,而不需要使用特殊的語法猾担。
從庫的職責上看袭灯,React 的核心只有 UI刺下,不包含 store绑嘹,路由等功能,開發(fā)者可以自行選擇合適的第三方庫搭配使用橘茉。
React 的另一個設(shè)計理念是函數(shù)式編程
React 強調(diào)把視圖的渲染更新當做是一個純函數(shù)工腋,盡量在一部分組件里避免副作用。這樣帶來的好處是畅卓,在代碼組織上擅腰,組件的狀態(tài)管理更為內(nèi)聚清晰,在測試上翁潘,組件的可測性更強趁冈。
React 的設(shè)計理念讓 React 使用起來極為的靈活。靈活的好處就是可定制性強拜马,代價缺少約束渗勘。所以使用 React 的開發(fā)用戶要求更高,還有需要配套搭建前端工程化俩莽,建立適合自身述求的開發(fā)約束旺坠。
4.2 為何還有 Vue
既然 React 已經(jīng)解決了當下的問題,為什么 Vue 還有市場呢扮超?讓我們看看 Vue 是怎么走出一條自己的路來的取刃。
早在 2013 年,Vue 是作為尤雨溪的個人實驗作品誕生的出刷。發(fā)布之后璧疗,很快得到一些開發(fā)者的認可。比如馁龟,PHP 框架 Laravel 的作者 Taylor Otwell 表示崩侠,他學習 Vue 的原因是 React 太難學了, Vue 很好入門屁柏,使用起來也很簡單啦膜。這個觀點可以代表一部分最早接觸 Vue 的人。
正如前面所說淌喻,React 當時的設(shè)計理念是極簡主義僧家,大部分操作都通過 JavaScript 來編寫,并且不官方捆綁像狀態(tài)管理裸删,路由等配套的庫八拱。這對于初學者來說并不友好。
早期,web 分工很細肌稻,有專門切圖的清蚀,有專門寫 html 的,專門寫 css爹谭,還有專門寫 JavaScript 邏輯的枷邪。
所以 React 的設(shè)計對于一些不太熟悉 JavaScript 的 Web 開發(fā)者來說,并不友好诺凡,他們更愿意接受類似 HTML 的寫法东揣。
而 Vue 的設(shè)計正好符合他們的口味,他們從傳統(tǒng)的項目腹泌,過渡到 Vue 遠遠要比過渡到 React 來的簡單得多嘶卧。
這就要講到 Vue 的設(shè)計理念之一,漸進式的開發(fā)理念凉袱。大白話就是芥吟,讓框架的初學者更容易接受。
Vue 采用了跟傳統(tǒng) HTML 開發(fā)接近的語法专甩,在同一個文件里钟鸵,通過 template 標簽定義模板,script 標簽定義 JavaScript 邏輯配深,在 style 標簽內(nèi)定義樣式携添。初學者從傳統(tǒng) HTML 開發(fā)轉(zhuǎn)過來,開發(fā)思想的慣性得到了保持篓叶,開發(fā) Vue 就像在開發(fā) HTML 一樣烈掠。
再一個是,Vue 保留了前后端未分離時期缸托,后端模板渲染的那一套左敌,也就是在 HTML 的基礎(chǔ)上擴展條件渲染,循環(huán)渲染的語法俐镐。這讓從舊時代后端模板渲染的那些開發(fā)者感到格外親切矫限,也更容易接受。
Vue 的另一個設(shè)計理念佩抹,開箱即用叼风,俗稱 Vue 全家桶。
這是 Vue 的另外一個殺手锏棍苹,通過捆綁官方的狀態(tài)管理 Vuex无宿,路由 Vue-router,讓用戶免去這些功能的選型困擾枢里,做到開箱即用孽鸡。這樣做的好處是蹂午,讓一部分沒法自行做出合理技術(shù)選型的用戶,可以在官方的推薦下彬碱,被動做出正確的技術(shù)選型豆胸。
除此之外,Vue 還很貼心的設(shè)計了提供了數(shù)據(jù)響應(yīng)式的設(shè)計巷疼,使用者不需要關(guān)注數(shù)據(jù)驅(qū)動視圖的細節(jié)晚胡。官網(wǎng)提供非常完善友好的文檔,并翻譯成多國語言等等皮迟。
Vue 的核心設(shè)計理念可以總結(jié)為:初學者友好向的框架搬泥。正是 Vue 的設(shè)計者意識到,一部分框架雖然設(shè)計思想很先進伏尼,但學習成本卻比較高,阻擋了一部分用戶尉尾。所以 Vue 從易用性角度設(shè)計框架爆阶,不出意外獲得大批被其他框架勸退的開發(fā)者。
4.3 Svelte
隨著 React沙咏,Vue 的廣泛使用辨图,基于虛擬 DOM 構(gòu)建前端框架已經(jīng)成為一種主流的方式。早期對虛擬 DOM 的宣傳是肢藐,可以減少對 DOM 的操作次數(shù)故河,優(yōu)化渲染性能。現(xiàn)在這一說法被推翻了吆豹,按現(xiàn)在的說法是鱼的,虛擬 DOM 是封裝了 DOM 的操作細節(jié),降低開發(fā)的復雜度痘煤。
那虛擬 DOM 是唯一的抽象方式嗎凑阶?
答案是否定的。Svelte 就是那個推翻虛擬 DOM 壟斷的框架衷快。Svelte 創(chuàng)新的提出了基于編譯的方式宙橱,來解決對 DOM 操作的封裝。
為什么 Svelte 要采用編譯來解決這一問題呢蘸拔?
這里需要講到 Svelte 的設(shè)計理念之一:性能優(yōu)先师郑。正是因為 Svelte 設(shè)計的初衷就是做一個輕量級,注重性能的框架调窍,使它拋棄了虛擬 DOM 的方式宝冕。虛擬 DOM 需要重復生成虛擬 DOM 樹,進行 diff 比對陨晶,DOM patch 等操作猬仁,這些都是運行時的性能損耗帝璧。Svelte 的解決之道是,通過把這些操作提前到編譯期來處理湿刽,通過編譯的烁,生成對應(yīng)的命令式語句,直接對 DOM 進行更新诈闺,有效的把計算從運行時轉(zhuǎn)移到編譯期渴庆。在 Svelte 的內(nèi)部,為了追求性能雅镊,還通過位運算做變量的變更標記襟雷。由于 Svelte 沒有傳統(tǒng)意義上的運行時,其框架體積也非常小仁烹,有利于首屏加載耸弄。
Svelte 的另一個設(shè)計理念是降低心智負擔,具體體現(xiàn)在 Svelte 對數(shù)據(jù)響應(yīng)式的設(shè)計上卓缰。傳統(tǒng)的數(shù)據(jù)響應(yīng)式计呈,都需要利用到語言的一些 hack 方法來模擬,使用起來其實不太直觀征唬,存在一定的心智負擔捌显,比如 Vue3 的 ref,需要通過 .value 來取值总寒。而 Svelte 通過編譯技術(shù)扶歪,很好的規(guī)避了這個問題。在 Svelte 里摄闸,變量定義自然就會獲得數(shù)據(jù)響應(yīng)的能力善镰,這是因為,在編譯時贪薪,Svelte 會識別 JavaScript 的賦值語法媳禁,并針對這個語法額外生成響應(yīng)式的代碼。這樣設(shè)計的好處是画切,開發(fā)者可以開發(fā)符合他們認知的 JavaScript竣稽,并且額外獲得數(shù)據(jù)的響應(yīng)式,而背后的細節(jié)由 Svelte 框架幫忙處理霍弹,很好地轉(zhuǎn)移了復雜度毫别。
4.4 各有千秋
前端框架都有自己標榜的核心設(shè)計理念,比如 React 不斷在復用角度深挖典格,發(fā)明了 hooks 的概念岛宦。而 Vue 不斷在易用性的角度深挖,發(fā)明了 setup 寫法耍缴,讓定義響應(yīng)式數(shù)據(jù)砾肺,就跟編寫普通的 JavaScript 一樣簡單挽霉。而后起之秀 Svelte 和 Solid,則是改變了前端框架在處理 DOM 的常規(guī)手段变汪,提出了使用編譯的方式來處理數(shù)據(jù)的響應(yīng)式侠坎,來獲得比虛擬 DOM 方式更好的性能。
他們各有優(yōu)劣裙盾,都解決了一部分人的痛點实胸,大家可以結(jié)合自己團地和業(yè)務(wù)的實際情況,選擇適合自己的框架番官。
4.5 整體來看
前端框架之間的關(guān)系庐完,如果我們把它們合在一起,作為前端發(fā)展歷程來看徘熔,我們會發(fā)現(xiàn)门躯,它們并不是相互排斥的,而是相互借鑒近顷,共同進步的生音。從整體來看,它們是一個進化的共同體窒升,互相吸收彼此好的東西,摒棄自身不好的東西慕匠,最后發(fā)展是趨同的饱须。
在這個過程中,跟不上隊伍的那個台谊,只能被無情的拋棄蓉媳,而不斷創(chuàng)新的,不斷滿足開發(fā)者訴求锅铅,解決痛點的酪呻,會繼續(xù)進化下去。
前端框架除了解決軟件設(shè)計上的問題外盐须,也跟瀏覽器的發(fā)展密切玩荠。
那些炫酷特性的實現(xiàn),離不開瀏覽器的發(fā)展贼邓。比如阶冈,Vue 從最開始使用 defineProperty 來實現(xiàn)響應(yīng)式,到現(xiàn)在使用 proxy 塑径,讓響應(yīng)式的能力更強女坑。
如果一個前端框架僅僅滿足于彌補瀏覽器的不足而存在,那可能在瀏覽器快速發(fā)展的趨勢下统舀,會被很快遺忘匆骗。比如用來適配瀏覽器選擇器 API 的 jQuery劳景,又或者是某個只有組件封裝功能的前端框架,也會被 Web Component 所替代碉就。
只有具備自己的設(shè)計理念盟广,并且不滿足于這種底層基礎(chǔ)的封裝的前端框架,才能有機會加入前端框架的競爭之中铝噩。
05 知其然知其所以然
所以我們遇到任何事情衡蚂,都應(yīng)該知其然知其所以然,了解其背后的原因骏庸,了解其實現(xiàn)原理毛甲,這樣即可以提升我們的認知,也可以幫助我們更好的用好工具具被,讓各種前端框架為我們服務(wù)玻募,解決我們實際場景的問題。
比如知道了框架的原理一姿,可以讓我們寫出更健壯的代碼七咧。
很多時候,框架都會給出一些教程叮叹,如果我們只是學習了教程艾栋,確實是可以開始寫代碼干活了。但假設(shè)我們不知道框架的設(shè)計思想蛉顽,我們不會知道為何要這么寫蝗砾,為何不能那么寫。如果遇到教程里沒有的携冤,我們就無法變通悼粮。
只有深入去理解框架的設(shè)計思想,我們才能在開發(fā)中化繁為簡曾棕,輕松駕馭各種開發(fā)問題的解法扣猫。
06 題外話:Typescript 引入復雜度了嗎
最近一段時間,還有一個話題很熱翘地,就是探討 TypeScript 是否有必要申尤,是不是引入了過多的復雜度,甚至覺得寫類型比寫代碼還更難子眶。
TypeScript 確實引入了一定的復雜度瀑凝, 但卻是前端往嚴謹項目開發(fā)的必然趨勢。
TypeScript 通過給 JavaScript 加上了類型系統(tǒng)臭杰,將 JavaScript 中的語言中弱類型帶來的陷阱大部分都規(guī)避了粤咪,大幅提升了系統(tǒng)健壯性和可維護性。
通常我們使用 TypeScript 會有兩種場景渴杆,一種是開發(fā)業(yè)務(wù)需求寥枝,另一種是開發(fā)庫 / 框架宪塔。
那開發(fā)業(yè)務(wù)需求有必要引入 TypeScript 嗎?還是要看情況囊拜,如果是嚴謹正規(guī)的長周期維護項目某筐,建議是使用,可以避免大量的弱類型語言陷阱冠跷,大幅提升系統(tǒng)的可維護性南誊。如果是比較臨時,生命周期極短的項目蜜托,比如臨時開發(fā)的簡單小需求抄囚,不需要持久迭代的,短期內(nèi)就會下線的橄务,那可以不需要 幔托。
實際上,日常開發(fā)業(yè)務(wù)蜂挪,我們通常只會使用類型定義重挑,頂多用到泛型函數(shù),類型定義和簡單的類型推導棠涮,并不會使用到“Typescript 的類型體操”這種模板元編程的程度谬哀。如果因為學不會類型體操,而否定 Typescript 在項目里的作用严肪,就有些過了玻粪,它們并沒有因果關(guān)系。
再說說 Typescript 在開發(fā)庫 / 框架的場景诬垂,毋庸置疑,主流的項目基本都采用 Typescript 來開發(fā)了伦仍。庫 / 框架本身就是一種嚴謹?shù)捻椖拷峋剑汩_發(fā)的東西是要面向廣大的開發(fā)者的,你有必要保障項目的質(zhì)量充蓝∷矸悖可能有的人會說,也有的庫是用 JavaScript 寫的谓苟,用其他工具來靜態(tài)檢測不就可以了官脓。確實是可以,不過相比之下涝焙,Typescript 的類型系統(tǒng)足夠強大卑笨,開箱即用,不是很特殊的理由仑撞,是不建議去折騰其他的方案赤兴。
07 總結(jié)
本文因一篇國外的吐槽文而起妖滔,里面的觀點錯得比較普遍、典型桶良,筆者感覺有必要為前端框架做一下澄清座舍,于是寫了這篇文章。全文講述了筆者對前端框架的前世今生的發(fā)展歷程陨帆,特別重點闡述了現(xiàn)代前端框架的誕生的背景和設(shè)計理念曲秉,并說明其引入復雜度的原因及收益,如有不同觀點疲牵,歡迎交流探討承二。