現(xiàn)代 web 應(yīng)用開發(fā)的特征一:數(shù)據(jù)驅(qū)動
前端框架發(fā)展到今天,已經(jīng)出現(xiàn)了很多被廣泛認(rèn)同的理念,也可以叫作它們的特征。在所有的特征中,最具代表性的特征之一即是數(shù)據(jù)驅(qū)動。
用戶交互的對象——界面
不論 web 應(yīng)用的內(nèi)部邏輯如何組織,最終與用戶產(chǎn)生交互的仍然是界面(User Interface,簡稱 UI)。對 web 應(yīng)用來說,界面則主要由 DOM 元素來呈現(xiàn)。因此,歸根到底,為了讓用戶通過 web 應(yīng)用完成操作,DOM 元素需要根據(jù)實(shí)際需要來不斷變化。
例如,用戶通過我們的頁面來查詢當(dāng)天蘋果的價格,頁面上有一個“查詢”按鈕,還有一個顯示蘋果價格的區(qū)域,此外,在查詢過程中,還有一個加載中的提示。
<button>查詢</button>
<div class="price">今日蘋果價格為 10.00/千克</div>
<div class="loading">加載中</div>
在查詢并顯示結(jié)果的過程中,DOM 實(shí)際上會經(jīng)歷這樣幾個狀態(tài):
- 鼠標(biāo)移動到按鈕上時,按鈕的樣式改變(通過 CSS
:hover
或者通過 JS)。 - 按鈕按下時,按鈕的樣式改變(通過 CSS
:active
或者通過 JS)。 - 清空價格區(qū)域的顯示內(nèi)容。
- 將加載中的提示顯示出來。
- 待查詢到數(shù)據(jù)后,將數(shù)據(jù)拼裝成文字“今日蘋果價格為 10.00/千克”,放入價格區(qū)域中。
- 將加載中的提示隱藏。
命令式編程實(shí)現(xiàn)界面變化
如果將 1 和 2 使用 CSS 來處理,那么早期我們的 JavaScript 代碼大概是這樣:
// 3. 清空價格區(qū)域的顯示內(nèi)容
price.innerHTML = '';
// 4. 將加載中的提示顯示出來
loading.style.display = 'block';
// 查詢價格
queryPrice(function(data){
// 5. 待查詢到數(shù)據(jù)后,將數(shù)據(jù)拼裝成文字“今日蘋果價格為 10.00/千克”,放入價格區(qū)域中
price.innerHTML = '今日蘋果價格為 ' + data.price + '/千克';
// 6. 將加載中的提示隱藏
loading.style.display = 'none';
});
即使是引入 jQuery 之類的庫,整個過程仍然不會有實(shí)質(zhì)性的變化,主要靠每一行 JavaScript 代碼命令式地修改 DOM 元素來達(dá)到修改界面的目的。
數(shù)據(jù)驅(qū)動界面改動
既然需要不斷修改界面,而命令式修改界面又如此繁瑣,是否有別的方法來修改界面呢?數(shù)據(jù)驅(qū)動的概念應(yīng)運(yùn)而生。它的基本思想是:使用數(shù)據(jù)來描述應(yīng)用的狀態(tài),將界面的修改與數(shù)據(jù)的修改綁定起來,從而實(shí)現(xiàn)數(shù)據(jù)的任何修改都直接反映到界面的修改。
如果用一個公式來表示的話,可以寫成UI=F(state)
,UI 即指用戶界面,state
指應(yīng)用的狀態(tài),而F
則是應(yīng)用狀態(tài)與用戶界面的映射關(guān)系定義。它最直觀的理解是“應(yīng)用的任何狀態(tài),都可以通過一種確定的映射關(guān)系,反映到界面的某種狀態(tài)上”,因此只要狀態(tài)發(fā)生變化,界面也會發(fā)生變化。
上述例子使用 Vue 來編寫:
<template>
<button @click="query">查詢</button>
<div class="price">今日蘋果價格為 {{price}}/千克</div>
<div class="loading" v-show="isLoading">加載中</div>
</template>
<script>
export default {
data(){
return {
price: 0,
isLoading: false
}
},
methods: {
query(){
this.isLoading = true;
queryPrice((data) => {
this.price = data.price;
this.isLoading = false;
});
}
}
}
</script>
在上述代碼中,應(yīng)用的狀態(tài)就是data
,包括兩個值price
和isLoading
。我們的邏輯代碼只需要對這兩個值進(jìn)行操作,即可以完成整個應(yīng)用功能的界面變化。
而狀態(tài)和界面的映射則由<template>
來定義,例如今日蘋果價格為 {{price}}/千克
表示狀態(tài)中的price
變量需要顯示在此處,v-show="isLoading"
則表示是否顯示加載中完全由變量值isloading
決定。
事實(shí)上當(dāng)前前端主流的幾大框架,在界面的渲染上都不約而同選擇了數(shù)據(jù)驅(qū)動的方式,深入理解這一模式有助于我們更好地理解前端框架。
數(shù)據(jù)驅(qū)動帶來的好處
數(shù)據(jù)驅(qū)動界面變更能迅速被廣泛接受,也可以從側(cè)面說明它確實(shí)為開發(fā)者帶來了一些好處。
首先,因?yàn)殚_發(fā)者僅需要管理數(shù)據(jù),使得關(guān)于界面細(xì)節(jié)控制的代碼不再需要開發(fā)者編寫。同時,由于狀態(tài)被抽象出來,同一個變量值在界面上的多處變化全部由映射關(guān)系來決定,而不需要開發(fā)者手工修改每一處變化。這兩者結(jié)合起來使得開發(fā)者的心智負(fù)擔(dān)大大減少,需要關(guān)注的代碼量也大大減少,從而使得開發(fā)效率得以大幅提升,出現(xiàn) bug 的概率也大大減少。
其次,專門將應(yīng)用狀態(tài)抽象出來,使得開發(fā)者必須認(rèn)真思考代碼的組織方式,而因?yàn)榻缑嫦嚓P(guān)細(xì)節(jié)的消失,大部分的代碼都變成了邏輯代碼,使得傳統(tǒng)編程中的模式都可以被應(yīng)用到前端代碼中,從而使得前端代碼能夠支持更大規(guī)模的應(yīng)用,也能更好地組織前端代碼本身,使得代碼更容易閱讀和維護(hù)。
狀態(tài)的抽象也使得開發(fā)者可以精準(zhǔn)地保存和還原任意一個界面狀態(tài)。因?yàn)榻缑娴拿恳粫r刻的界面表現(xiàn)都是由這一時刻的應(yīng)用狀態(tài)決定,因此只要能夠?qū)⒋藭r的應(yīng)用狀態(tài)進(jìn)行保存,就能在另一個時間、空間中重現(xiàn)應(yīng)用此時的界面表現(xiàn)。
這個特性在某一些場景下非常好用,例如線上 bug 的排查。如果我們有辦法取到用戶的當(dāng)前狀態(tài),就有辦法完全還原用戶的界面表現(xiàn),從而快速復(fù)現(xiàn)應(yīng)用碰到的 bug,而不用再苦苦和用戶溝通詳細(xì)的操作步驟,一點(diǎn)點(diǎn)地確認(rèn)應(yīng)用可能是哪里出了問題。除此之外,這個特性還可以用于實(shí)現(xiàn)“時間旅行”效果,即應(yīng)用界面的回放。我們只需要將狀態(tài)的變更都記錄下來,就能看到應(yīng)用從初始化一直到最終狀態(tài)中間發(fā)生的完整事情。它本身可以作為一個效果來使用,也可以用來支持一些功能(例如撤銷/重做)。
因?yàn)閼?yīng)用界面完全由應(yīng)用狀態(tài)決定,而狀態(tài)映射到界面的操作一般由框架來幫忙我們完成,因此在測試的時候,就有機(jī)會將重點(diǎn)放在狀態(tài)的測試上。即在很多情況下,我們只需要測試邏輯和數(shù)據(jù),確保應(yīng)用狀態(tài)是正確的,即可大概認(rèn)為界面是正確的。
因?yàn)榻缑鏈y試的成本要遠(yuǎn)高于邏輯和數(shù)據(jù)測試,如果我們能在不做界面測試的情況下也保證應(yīng)用邏輯和狀態(tài)是正確的,將大大提升測試效率。
小結(jié)
時至今日,數(shù)據(jù)驅(qū)動已經(jīng)成為絕大部分前端開發(fā)者的共識,Vue 也是這一理論的實(shí)踐者,后面我們將看到 Vue 是如何實(shí)現(xiàn)這一重要特征的。