跳至主要内容

Day17 - 前端資料操作的眉眉角角

這是一個有點嚴肅的話題 (並沒有)。

好啦,事情是這樣的。
已知前端工程師的職責就是把畫面美美地呈現給使用者,多數人就會覺得我們最主要是在刻畫面。
誒... 不能說錯,但真的寫畫面結構的時間可能佔開發時間裡的少部分,畢竟你懂得,嗯,現代框架 XD
其實大部分時間前端反而是把時間花在跟資料的對話上。

這裡不討論純靜態的網站,實務上大部分上線的網站,其資料基本都是來自後端的 api。
那就有趣了,前後端分離的情況下,後端規劃的 api response 結構長怎樣會很大幅度影響前端的開發。
如果後端的結構相當複雜,前端往往會需要花一些時間去處理資料。
相對的,前端處理使用者的 input 也會需要把這些資料轉成 api 可以吃進去的格式,也會再花一些時間。
不過,當然,實務上前後端各自的系統架構師會盡量提早溝通好,盡量避免這種前後端不協調的情況發生。
但也總是會有一些不得已的情況,比如日期的格式,就有可能在前後端各自的框架裡有不一致的需求,這時很難要求任一方去改,因為那是框架問題。
所以前端收到資料後對資料做處理是無可避免的。

而且以實務情況來說,前端開發常常會拆分各種可複用的元件,往往會為元件定義好需要的 props 類型。
但在使用方,不見得原始資料就一定符合這些 props 類型,這時也需要前端做一些資料轉換。
所以前端工程師其實工作很大一部分是在處理各種資料的轉換。

所以這篇就是想稍微說說前端在處理資料時的一些眉眉角角。

淺拷貝與深拷貝​

我們都知道 object 的拷貝有兩種寫法:

// 淺拷貝
const shallowCopy = { ...originalObject }

// 深拷貝
const deepCopy = JSON.parse(JSON.stringify(originalObject)) // 注意:這種深拷貝有其限制,僅適用於純資料物件
const deepCopy = structuredClone(originalObject) // 近年新出現的深拷貝方法,支援更多資料類型

淺拷貝雖然也是前端常用的手法,但它有一個問題,就是物件的傳址特性。
如果我們對原資料做淺拷貝後就去做資料處理,其實是會影響到原資料的!
如果這時剛好有其他地方也在使用原資料,可能就會造成意想不到的 bug。

所以實務上,在真的要處理資料前,一般是用深拷貝的方式來複製一份資料,然後才對複製的資料做處理。
淺拷貝的應用基本都是出現在深拷貝複製完資料之後的應用,或是很確定動到原始資料也沒關係的情況下才會使用。

一些框架也會提供自己的深拷貝 method,比如 Quasar 的 extend(),提供了用更簡單、便捷的語法來做深拷貝。
這也側面顯示了深拷貝在前端開發中的重要性。

其實深拷貝在測試中也很常用到。
因為在寫測試時,通常都不會實際 call api 或是填 form,往往都是直接用一份 mock 的資料來做測試。
在很多測試項目時,如果用淺拷貝就可能發生前一個測試改動了 mock 資料,進而影響了後一個測試的結果。
為了測試的獨立性,通常也會用深拷貝來確保每個測試都不會互相影響。

會改變原陣列的方法​

淺拷貝與深拷貝是 for object 的方式,但在實務上 array 也是相當常處理的資料結構。
甚至很多時候 array 都是跟 object 一起出現。

在操縱 array 的諸多 method 裡,我相信大家在學 JavaScript 時一定也有被提醒有些會影響到原陣列,有些不會。
可能剛學時沒太多想法,但實務上這個差異是很重要的。
在多人協作裡,通常會建議使用不改變原陣列的方法,以方便追蹤 state 流向,避免難以預測的 side effect。

比如 push()、pop()、shift()、unshift()、splice() 等等,這些都是會改變原陣列的。
而 concat()、slice()、map()、filter()、reduce() 等等,這些都是不會改變原陣列的。
最常搞混的就是 slice 跟 splice,因為他們真的長太像了。

一個實務的例子來看資料操作​

這裡用 Vue 來稍微簡單舉一個前後端資料交換的例子。 情境是前端有一個表單,使用者可以填寫資料,然後送出到後端。
然後這個表單同時也會用作編輯,所以也會從後端那裡 get 到已有資料來填充表單。

一般來說前端會建好一個 initialForm 物件,定義好空表單初始的樣子。
然後在表單元件初始化時,透過深拷貝將 initialForm 複製一份,然後再用這份複製的資料來填充表單。

const form = ref(deepCopy(initialForm))  // 假設特別寫了一支專門做深拷貝的函式

用深拷貝複製一份的用處是確保 initialForm 永遠都保持最乾淨的樣子,任何操作都不會影響到它。
由此保障每次使用此元件時都能有一個乾淨的表單初始狀態。
那我們在將 form 資料 POST 給後端時,做資料轉換時就可以很放心直接對 form 做處理,不用擔心會影響到 initialForm。

async function submit () {
try {
const payload = formatData(form.value) // 假設有個函式專門處理資料格式轉換
await api.post('/submit', payload)
} catch (error) {
console.error('Submit failed:', error)
}
}

對於編輯時,已知後端會回傳一份資料來填充表單,這時也會用深拷貝來把 response 的欄位資料複製到 form 裡。
然後才做資料轉換,確保 form 裡的資料格式符合前端的需求。

async function fetchData () {
try {
const response = await api.get('/data')
form.value = deepCopy(response.data) // 深拷貝 response 資料到 form
convertFormData(form.value) // 假設有個函式專門處理後端資料格式轉前端格式
} catch (error) {
console.error('Fetch data failed:', error)
}
}