為何前端要使用框架?(小白向)

sea_ljf發表於2017-08-26

親愛的觀眾老婆們好~最近在團隊裡組織了一次內容關於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層的套路差不多,通過自執行函式結合閉包儲存之後要操作的節點,對外暴露由兩個方法組成的物件,分別是initrenderinit用於初始化話頁面,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呢?這時候就該使用框架了。框架較大程度上能約束與規範每位開發者的行為,不按照框架的規定很可能就會報錯,這樣多人協作就有了基本的保證。

但是,不是說使用框架就是最佳實踐。當專案不復雜的時候(比如一次性的活動頁),我們有足夠能力去掌握專案的細節,那麼使用框架反而不是好的選擇。畢竟再好的框架在效能上都會有損失,而被框架的條條框框約束著,也總是令人不喜的。

簡單地說:腦子不夠,框架來湊;自己組織不好程式碼,靠框架來給我們組織。閱讀至此殊為不易,感謝各位看官,希望本文對你有一點幫助,謝謝!

參考資料

JavaScript 設計模式

相關文章