親愛的觀眾老婆們好~最近在團隊裡組織了一次內容關於React的分享,然而後端有同學反映未能理解為何前端需要使用框架,框架究竟解決了什麼問題。
回想剛入坑前端的時候,也接觸了一些PHP,當了解到其實PHP可以生成HTML模板之後,對前端的信仰幾乎崩潰。既然後端就可以渲染前端的話,前端價值到底在哪?感覺頁面就是隨便切一下就行,根本沒必要用框架,當時著實迷茫了很久。
後來對前端領域接觸深了,再經過大神的指點,總算是理解為何前端需要使用框架。根據自己的理解,再結合分享時後端同學的疑問,於是有了這篇文章。本文相對小白向,只是通過虛擬一個專案說明問題,還望各位大神不吝賜教。
開始新專案
產品: 來來來,新專案來啦。最近發現使用者喜歡擼貓,我們來個線上雲擼貓!頁面要求有貓圖,點一下計數加一點當擼了一次!多久能上線??
這個簡單啊!
厲害的我連jQuery都不用,原生上!
<body>
<img src="1.jpg" alt="">
<p>0</p>
<script>
const img = document.querySelector('img');
const p = document.querySelector('p');
let count = 0;
img.addEventListener('click', function() {
count += 1;
p.innerHTML = count;
})
</script>
</body>複製程式碼
簡單易懂,對不對!
需求變更
產品: 線上大受歡迎,然而要改需求了爸爸!現在要兩隻喵,點選要分開算哦,各自顯示就好。你最棒了,我知道你肯定可以的!
看在叫了爸爸的份上,還是給他改一下吧。然而第一個版本多粗糙啊,各種汙染全域性變數,第二版我要寫得棒棒的!
<body>
<section class="catSection">
<img src="cat1.jpg" alt="" class="catImg">
<p class="catCount">0</p>
</section>
<section class="catSection">
<img src="cat2.jpg" alt="" class="catImg">
<p class="catCount">0</p>
</section>
<script>
(function() {
const catSection = document.querySelectorAll('.catSection');
catSection.forEach((section) => {
let count = 0;
const catImg = section.querySelector('.catImg');
const catCount = section.querySelector('.catCount');
catImg.addEventListener('click', () => {
count++;
catCount.innerHTML = count;
})
})
})()
</script>複製程式碼
沒有汙染全域性變數,還可以擴充套件需求,後續產品就算要求再多的貓我都能hold住!
再一次需求變更
產品: 父皇,你聽我說啊,就最後一次,真的最後一次。現在貓兩隻不夠啊,但太多的話展示又不好看。現在需求做一個列表,列表有N只貓,點那隻貓展示那隻貓,每次只展示意志,而且貓要有對應的名字哦,點選也要分開統計!其實跟以前差不多?就改改就好了。明天就要哦!
我封裝得好好的功能,你跟我說改需求?而且改得面目全非跟我說差不多?
然而,吐槽歸吐槽,需求還是要做的。而且需求明天就要,程式碼寫得再好也可能馬上改功能,程式碼還是實現功能就算了吧。
具體程式碼由各位看官自己實現(建議先停下來,動手去實現這個需求),這裡我就不再上程式碼了。很可能我們這次寫的程式碼,就不會太考慮什麼全域性變數汙染,也不考慮封裝的問題,逐漸趨向於實現功能就好了,因為需求太多了,時間和精力限制了去寫“好”程式碼。
當然了,產品經理說需求最後一次改,都是騙人的。下次可能會有點選到某個值時,自動切換貓,動態新增貓等等的新需求。我們現在這樣組織程式碼的形式,是典型的”義大利麵式“程式碼(簡單說,就是各種東西整合在一起,層級不分,難以維護)。這種程式碼寫起來直觀,但日後的維護是相當難的。寫出上述例子後,不妨隔兩天再去看看能否輕易理解那一份程式碼。自己寫的程式碼尚且如此,他人的更不在話下了。
那麼,這種隨著專案越來越複雜,邏輯越來越多,我們該怎麼寫程式碼呢?
更好地組織程式碼
前端其實有一種說法:我們現在的”新“東西,都是其他領域玩過的。雖然看起來很氣人,但這也是事實。當現狀不知如何處理時,不妨參考下其他領域的解決方案。離前端比較近的就是後端了,那麼後端是怎麼管理的呢?最典型的設計就是MVC了。那麼,前端能不能借鑑呢?
說幹就幹,以上面的需求為例,我們試著用MVC的方式組織一下程式碼,看下和你剛才寫的有什麼不同。
先來M
,也就是Model,資料層,對外提供介面可以獲取相關的資料。這麼組織的話,是不是蠻好懂的:
const model = (function() {
//相關資料
const _model = {
catLists: [
{
src: '1.jpg',
name: 'cat1',
count: 0
},
{
src: '2.jpg',
name: 'cat2',
count: 0
}
],
targetCatIndex: 0, //目前可被點選的是哪知喵在catLists中的索引
};
//獲取getCatLists
function getCatLists() {
return _model.catLists;
}
//獲取目標物件
function getTargetCatObj() {
return _model.catLists[_model.targetCatIndex];
}
//修改targetCatIndex
function setTargetCatIndex(name) {
_model.catLists.some((catObj, index) => {
if (catObj.name === name) {
_model.targetCatIndex = index;
return true
}
})
}
//目標物件點選數+1
function addTargetCatCount() {
const catObj = getTargetCatObj();
catObj.count += 1;
}
return {
getCatLists,
getTargetCatObj,
setTargetCatIndex,
addTargetCatCount
}
})();複製程式碼
在自執行函式中,設計了一個物件命名為_model
,通過閉包儲存它。自執行函式返回一個物件,其中包含四個函式。四個函式執行後,可以返回或修改_model
中對應的資料。通過註釋看其實還是挺清楚的。
跟著是V
,也就是view層,負責頁面渲染。這個可能複雜一點,但不想把它弄得太繁瑣,不如就兩個方法吧。就一個初始化的init()
和負責更新檢視的render()
方法就好啦。
先確定HTML模板:
<ul class="catList"></ul>
<section class="clickArea">
<img src="" class="catImage">
<p class="catCount"></p>
<p class="catName"></p>
</section>複製程式碼
再組織一下view層的程式碼:
const view = (function() {
//獲取各個需要操作的DOM節點
const img = document.querySelector('.catImage');
const name = document.querySelector('.catName');
const count = document.querySelector('.catCount');
//初始化頁面
function init(catLists, targetObj) {
const list = document.querySelector('.catList');
const fragment = document.createDocumentFragment();
//為ul新增對應的li
for (let i = 0, len = catLists.length; i < len; i++) {
const li = document.createElement('li');
const name = catLists[i].name;
li.innerHTML = name;
li.addEventListener('click', function() {
//之後會有controller相關的程式碼,其實就是換一隻可點選的喵
controller.changeTargetCat(name);
});
fragment.appendChild(li);
}
list.appendChild(fragment);
img.addEventListener('click', function() {
//之後會有controller相關的程式碼,其實就是計數+1
controller.addCount(name);
});
render(targetObj);
}
//重新渲染頁面
function render(targetObj) {
img.src = targetObj.src;
name.innerHTML = targetObj.name;
count.innerHTML = targetObj.count;
}
return {
init,
render
}
})();複製程式碼
view層的程式碼其實也很簡單的,和model層的套路差不多,通過自執行函式結合閉包儲存之後要操作的節點,對外暴露由兩個方法組成的物件,分別是init
與render
。init
用於初始化話頁面,render
用於重新渲染頁面。裡面呼叫了controller
,其實就是之後要介紹的controller。
最後是C
層,也就是controller,主要是用於邏輯相關的處理,算是整個設計裡面的大腦。不過由於這專案比較簡單,所以程式碼反而是最簡單的:
const controller = {
addCount(name) {
//通過model的介面增加目標物件的計數
model.addTargetCatCount(name);
controller.renderView();
},
changeTargetCat(name) {
//通過model的介面修改目標索引
model.setTargetCatIndex(name);
controller.renderView();
},
init() {
//通過model的介面獲取相關資料
const { getCatLists, getTargetCatObj } = model;
//傳參並命令view層初始化
view.init(getCatLists(), getTargetCatObj());
},
renderView() {
//傳參並命令view層重新渲染
view.render(model.getTargetCatObj());
}
};複製程式碼
controller的設計其實是比較簡陋的,只是一個包含了四個方法的物件。其中addCount
對應點選加一的操作,changeTargetCat
對應換貓的操作。上述兩個方法其實是改變了資料的,而只要資料發生了變化,一律呼叫renderView
重新渲染。
至此主要程式碼已經寫完了,之後呼叫一下controller.init();
,就可以開心的擼貓,完成需求了。
如果之前你動手實現了上述需求的話,不妨對比一下我們設計程式碼上的差別。也許你寫的程式碼還是之前那種“麵條式”程式碼,但它也是可用的啊,而且還不用這麼多程式碼呢,長得也還算能維護的樣子,為何要用這種繁瑣的方式去阻止程式碼呢?
然而,按照之前說的,產品可能提更多的需求,下次可能會有點選到某個值時,自動切換貓,動態新增貓等等的新需求時,你現在的程式碼組織形式能否很快地完成需求?當日後修改某些需求時,不小心觸發了潛藏的bug(經常有的情況),又是否能快速定位出問題並快速改好呢?
“麵條式”程式碼經常是資料、檢視與處理邏輯耦合起來的,很容易牽一髮而動全身,當業務相當複雜的時候,開發可能還好說,維護簡直是不可想象的。而你可能已經觀察到了,遵循MVC設計的程式碼,雖然繁瑣,但各層是完全分開的,儘管資料與檢視可以直接呼叫對方的介面進行互動,但都是必須通過控制層來做統一處理。資料、檢視與處理邏輯解耦之後,程式碼的可閱讀性與可維護性都是一個飛躍。通過犧牲空間(多寫程式碼)來換取程式碼的可維護性與可擴充性,這是一筆劃算的買賣。
小結
說了半天,好像還沒有說出為何前端需要使用框架。然而在複雜的專案中,你同意我通過MVC組織程式碼會比“麵條式”程式碼好嗎?如果同意的話,將我剛才程式碼中不變的部分抽象起來(元件通訊、報錯處理等),想方設法提高渲染的效能(使用Virtual Dom),如果認為前端和其他不一樣,資料和檢視還是可以進行受控的互動(即MVVM),那麼整合起來,不就是一個框架了嗎?
其實必須要承認,人的腦力是有限的,一款產品的需求可能是無限的。當這款產品已經讓你無法掌握每個細節,每位參與開發的同學只能掌握區域性細節,而其他部分只管呼叫是必然的事實。但是,如何確定其他部分可信,呼叫不會出bug呢?這時候就該使用框架了。框架較大程度上能約束與規範每位開發者的行為,不按照框架的規定很可能就會報錯,這樣多人協作就有了基本的保證。
但是,不是說使用框架就是最佳實踐。當專案不復雜的時候(比如一次性的活動頁),我們有足夠能力去掌握專案的細節,那麼使用框架反而不是好的選擇。畢竟再好的框架在效能上都會有損失,而被框架的條條框框約束著,也總是令人不喜的。
簡單地說:腦子不夠,框架來湊;自己組織不好程式碼,靠框架來給我們組織。閱讀至此殊為不易,感謝各位看官,希望本文對你有一點幫助,謝謝!