iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 10
1
Modern Web

深入現代前端開發系列 第 10

Day10 有了 jQuery 為什麼要有xxx?

或許你我都會好奇,為什麼 jQuery 用得好好的,會有像是 Angular, React, Vue 等框架的出現?

網頁的互動越來越複雜

從網頁出現開始,最原始的目的是為了呈現資訊,例如小時候各種用 FrontPage 製作的網頁,幾乎都是純靜態的 HTML + CSS,頂多做一些瀏覽器的判斷或很簡單的動畫等等。

隨著網路速度提升,4G 普及,電腦的設備越來越高級,瀏覽器支援的東西越來越多,網頁的功能也越來越豐富,可以做動畫、拿來發文、上傳圖片等等,也讓網頁開發有更多的可能性。

然而,互動性變高,其中最大的挑戰就是我們要如何管理狀態以及資料流**。

一般以後端伺服器渲染的程式碼,我以 Ruby On Rails 的模板引擎 .erb 為例,大概會長得像這樣:

<div class="profile">
  <h2>
    <%= user.name %>
  </h2>
  <p>
    <%= user.introduction %>
  </p>
</div>

在寫 view 的時候,我們只要把後端傳進來模板的變數放在正確的地方渲染這樣就結束了。

隨著各種雲端服務興起,除了呈現資訊以外,網頁也能夠拿來發文、上傳檔案、看影片、聽音樂、通訊軟體、修照片各種廣泛的用途使用。

為了讓使用者更好的體驗,SPA 出現了,說出現有點奇怪,比較像是慢慢衍伸出來的架構。

所謂的 SPA 指得是是單頁應用,為了讓使用者可以感受到良好的體驗跟互動,將各種商業邏輯全部寫在 JavaScript 當中。仰賴 API 與後端伺服器互動,也因此需要考慮更多事情,不像以往可以只專注在一個頁面的實作。

JavaScript 的規模越來越大,當專案到達一定規模,需要一種手法讓複雜的操作變得簡單一些,而導致各種框架產生。

因此現有的架構很容易遇到幾個問題:

  • 為了復用一些現有的元件(頁面),我們需要將常用的檔案(元件)拆開來,現有的 JavaScript 並沒有辦法很容易做到這件事。
  • 為了確保彼此的相依性(例如載入 bootstrap modal 時,我們要確保 jQuery 載入),以往的做法是直接把所有相依性全部塞到網頁 <script> 中。
  • 在修改某幾筆資料時,希望所有用到這個資料的元件或頁面都可以自動更新。
  • 怎麼有效地管理各種狀態

那麼 jQuery 哪裡不夠用呢?雖然 jQuery 提供了相當良好的函數給開發者使用,但是要怎麼管理資料與狀態,還是得全權交給開發者實作。

我以渲染一個 User Profile 頁面當做例子,用 jQuery 可能會這樣寫:

function getUserById(id) {
  return fetch(`/api/user/${id}`);
}
<button data-id="myuserid" id="show-profile">
  Click to show profile
</button>
<div id="user-profile">
  <h2 class="name">default</h2>
  <span class="introduction">intro</span>
</div>
function getUserById(id) {
  return fetch(`/api/user/${id}`).then(res => res.json());
}

function renderProfile(user) {
  $('#user-profile > .name').text(user.name);
  $('#user-profile > .introduction').text(user.introduction);
}

$('.profile').on('click', e => {
	getUserById(e.target.getAttribute("data-id")).then(renderProfile);  
});

這裡我們做了幾件事:

  • 監聽 click 事件,在點擊時送 API 拿取 profile 資料
  • 呼叫 renderProfile 來渲染到對應的 DOM 上面

這樣子的確可以運作,不過一旦 UI 變得越複雜,不同的 UI 元件需要共享同一份資料源,或是資料改變的時候,這樣做就不是什麼太輕鬆的事情了,比如說:

function renderProfile(user) {
  $('#user-profile > .name').text(user.name);
  $('#user-profile > .introduction').text(user.introduction);
  
  $('.header > #user-banner').attr('src', user.avatar);
}

如果再繼續加下去,這個函數馬上會變得臃腫不堪,如果我只想更新有改變的資料呢?按照現在的寫法會導致全部資料重新渲染一次。

雖然 jQuery 寫起來相當直覺,乍看之下或許開發速度還比較快,但其實要把整個架構寫好並不容易。

用 pub-sub 模式解決:

一個老方法是,每當改變 user 的值的時候,就建立一個事件,假設叫做 value-change 好了。如果有 UI 需要監聽值的變更,那就加入監聽器裡頭。

function changeUser(user, name, intro) {
  user.name = name;
  user.intro = intro;
	event.emit('value-change', user);
}

function main() {
  getUserById().then(renderUserProfile(user));
  addListener('value-change', renderUserProfile);
}

如果每次 value-change 觸發都需要重新 render 的話,可能會造成效能上的影響。

再來因為我們可能有所謂的巢狀相依,例如這個值的變化是從另外兩個值的變化而來,或是這個值的變化會引起其他值變化等等,要處理這類的事件就需要額外處理。

Data Binding

要解決這個問題,另外一種常見的方式是 data binding。所謂的 data binding 就是我將 UI 顯示的值,與某個實際的 model(或資料源)綁定,當資料源有變化的時候,UI 做更新,顯示相對應的變化。

React 當初被設計時是只有單向資料流,你只能透過 model 來改變值,不能透過 UI 來控制 model。什麼意思呢?

function Profile({ name, introduction }) {
  return <div>
      <h2>{name}</h2>
      <p>{introduction}</p>
    	<input onChange={console.log} />
    </div>
}

這樣講就有點怪怪的,什麼叫做資料由上往下,難道資料可以從下往上嗎?

雙向資料流、單向資料流?

你可能常常聽到一些佈道師在宣傳,雙向資料流是垃圾,單向資料流才是正解,但追問下去卻發現他們講不出一個所以然。

不過單向資料流是什麼?雙向資料流又是什麼?

單向資料流

對我來說,單向資料限制了資料源,確保每次的資料源的變化都是同樣的。像是:

function Input() {
  const [value, setValue] = useState(null);
  
  return <input onChange={(e) => setValue(e.target.value)} value={value} />
}

每次觸發 onChange 的時候,都會去 setValue,進而改變 value 的值。這樣子叫做單向資料流,你只能透過 setValue 這個函數來改變值。

雙向資料流

雙向資料流並不限定資料源,可以從 model 而來,也可以從 input 而來,像是下面 vue 的程式碼:

<input v-model="message">
<p>{{message}}</p>

<script>
  Vue.Component('Input', {
    data: () => ({
      message: 'hello world'
    })
  })
</script>

我們可以發現,就算不加入 onChange,message 的值一樣會改變,並且更新 UI。這樣就叫做雙向綁定。其實這段程式碼是類似這樣的:

<input value="message" @change="message = value">
<p>{{message}}</p>

主要的原因在於,一旦設定了雙向綁定,資料來源就不確定了,如 vue 的例子,資料來源可能是由 data 函式,也可能是 UI input 對應的操作。

當然其好處在於程式碼簡潔,我們不需要再大費周章(恐怕也沒那麼大費周章)寫一個 onChange 函數,只要讓函式庫幫我們運作就好。

回歸老話一句,適用場景跟你想達成的目的,但大多數的時候可能只是因為個人偏好(逃)。

以上 Profile 的元件如果用 React 寫的話大概會像這樣:

import React from 'react';

class Profile extends React.Component {
  state = {
    user: null,
  };

  componentDidMount() {
    getUserById().then(res => res.json()).then(user => this.setState({ user }));
  }

  componentDidUpdate(prevProp, prevState) {
    if (prevState.user.id !== this.state.user.id) {
      getUserById().then(res => res.json()).then(user => this.setState({ user }));
    }
  }

  render() {
    return 
    <>
	    <div>
  	    <h2>{user.name}</h2>
   	    <p>{user.introduction}</p>
    	</div>
    </>
  }
}

其他選擇

你也可以參考 custom-element 來做到撰寫組件的概念,因為瀏覽器原生就能支援的關係(可能要考慮支援度),而且使用起來就像 HTML 的 tag 一樣。不過在實際使用上你可能要注意狀態管理以及元件之間如何溝通的問題。

為什麼 React、Vue 變得如此熱門?

一個函式庫變得熱門通常有許多原因,不妨思考一下:

  • React 提供了單向資料流的架構,來確保 data source 的流向。
  • Vue 提供了雙向資料的機制,能夠讓我們很輕易地操作資料
  • 我們不需要擔心因為重新 render 所造成的效能問題,因為 React || Vue 時做了一套機制來做這件事
  • 我們可以透過 jsx,vue 的 template 簡單地描述 UI,並且提供了一個統一的介面與 lifecycle。
  • React 跟 vue 的 devtool 實在太好用了,很難想像沒有這些 devtool 要怎麼開發大型應用才好。
  • 活躍的生態圈

從 React, vue 當中,可以觀察到幾個現象:

  • 兩者都提供了生命週期的操作幫助簡化與整理程式碼
  • 兩者都提供了狀態管理機制(如 setState, context, wather 等)
  • 兩者都使用了 component base 的方式來撰寫程式碼

小結

在這個章節,我們了解到了前端在開發時面臨的幾個問題與挑戰,以及可能的解決辦法。如果你能夠找出優雅的方式解決上述的問題,並且兼顧效能考量的話,不用 react 或前端框架,就算只靠 jQuery 也能夠開發出不錯的 app。

雖然這裡用 React 當作範例,但其實同樣的概念可以套用到其他函式庫。

後記

這邊的範例似乎沒辦法很完整描述清楚複雜性,而且涵蓋的內容有點多,之後再看看能不能修改吧。


上一篇
Day9 為什麼前端需要工程化? — Babel
下一篇
Day11 設計 UI 真的很簡單嗎?談 UI 狀態
系列文
深入現代前端開發32

尚未有邦友留言

立即登入留言