React篇 - TodoApp程式 + Fetch/Ajax 於生命週期方法
今天的主題是要加入與伺服器資料互動的功能,一般我們所說的在網頁上的JavaScript應用與伺服器溝通,指的就是使用Ajax技術,傳送資料到伺服器或是與伺服器要資料。我們的範例程式主要會用在一開始載入資料、勾選為已完成的項目時,以及編輯現有的項目時這三個情況。
這個程式最後的呈現結果,就像下面的動態圖片這樣,重點是在於它會傳出與外部伺服器溝通的資料:
註: 本文章同步放置於Github庫的這裡,所有的程式碼也在裡面。
在解說之前,本篇使用的一些額外的工具與知識,在這篇文章不會再多作說明,這個就你需要進一步學習了,下面有附它們的連結:
json-server: 這是一個架在Node.js上的用於測試JSON格式資料的伺服器,而且是REST API,裡面用了lowdb,所以其實它是一個具有資料庫寫入、讀取、查詢的功能的伺服器。這個工具在Github上有1萬8千個星,如果你要開發像我們這種JavaScript應用,而且是用json交換資料的應用,學會用這個工具,保証讓你開發的日子輕鬆很多。
Postman: 這工具則是一個客戶端程式,也有Chrome的外掛擴充版本,這是用來模擬由客戶端發送各種要求(Request)用的,像POST、GET、PUT...等等。也就是例如在還沒開始寫程式前,先作測試,看你的資料傳到伺服器(json-server)上能不能正確查詢到資料,或是新增資料等等。當然它也有很多其他的功能,不過我只用這部份而已。
Fetch API: 這是一個API,是HTML5標準中的新特性,當然它是一個實作在JavaScript的新API,Fetch API是一個完全以為基礎的API,Fetch並不是一個單純的XHR擴充加強版或改進版本,它是一個用不同角度思考的設計,雖然是可以作類似的事情。此外,Fetch還是基於Promise語法結構的,而且它的設計足夠低階,這表示它可以依照實際需求進行更多彈性設定。相對於XHR的功能來說,Fetch已經有足夠的相對功能來取代它,但Fetch並不僅於此,它還提供更多有效率與更多擴充性的作法。
上面都是這個範例程式中有用到的部份,這些內容如果要整個解說完,可能光說這些30天都不夠用,我附的連結中的資料就請再自行學習了。
程式碼說明
本次的程式碼只有修改到App
元件,其他都不變,這也是因為App
元件裡有所有這個範例應用中所有的狀態,更動狀態的所有方法也在這裡面,所以與伺服器溝通就靠App
元件就行了。
我們在App
元件加了一個新方法handleServerItemsLoad
,它會載入所有記錄在現在伺服器的項目資料。使用的是fetch的Promise語法,最後在正確載到資料時,作this.setState
,但因為伺服器資料上並沒有isEditing
,所以會在setState
前作資料的整理,加上isEditing
欄位。另外排序用id
值,實際上id
值就是我們每個項目加入的時間(微秒值)。程式碼如下:
handleServerItemsLoad = () => {
fetch('http://localhost:5555/items?_sort=id&_order=DESC', {
method: 'GET'
})
.then((response) => {
//ok 代表狀態碼在範圍 200-299
if (!response.ok) throw new Error(response.statusText)
return response.json()
})
.then((itemList) => {
//加入{ isEditing: false }屬性
const items = itemList.map((item) => {
return Object.assign({}, item, { isEditing: false })
})
//載入資料,重新渲染
this.setState({
items,
})
})
.catch((error) => {
//這裡可以顯示一些訊息
//console.error(error)
})
}
然後在生命週期方法的呼叫這個方法,componentDidMount
在ES6類別的元件語法中,是內建就有的一個方法,它會在React元件"已經"掛載到真實網頁上執行裡面的程式碼。程式碼如下:
componentDidMount() {
// 元件"已經"載入,所以可以載入資料進來
this.handleServerItemsLoad()
}
註: 無狀態元件,也就是用函式語法寫的元件,是"不能"使用生命週期方法的。
另一個新增的方法稱為handleServerItemAdd
,它是用於在新增項目用的,REST API用的是POST
這個method值,因為我們在資料庫裡不需要isEditing
這個屬性,所以也有先作資料的整理工作。程式碼如下:
handleServerItemAdd = (aItem: Item) => {
//處理payload,不需要isEditing欄位
const { id, title, isCompleted } = aItem
const payload = { id, title, isCompleted }
//作POST
fetch('http://localhost:5555/items', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then((response) => {
//ok 代表狀態碼在範圍 200-299
if (!response.ok) throw new Error(response.statusText)
return response.json()
})
.then((item) => {
//這裡可以顯示一些訊息,或是結束指示動畫…
//console.log(item)
})
.catch((error) => {
//這裡可以顯示一些訊息
//console.error(error)
})
}
在原先的handleItemAdd
中加上呼叫這個handleServerItemAdd
方法,讓資料同時也會傳到伺服器上儲存:
//加入新資料到資料庫
this.handleServerItemAdd(aItem)
最後是這個handleServerItemUpdate
,用於資料更新用的,REST API用的是PUT
這個method值。大致上與上面的handleServerItemAdd
方法都一樣,差異只是你需要給定要更新哪一筆資料,所以要給id
值,這裡用了樣版字串的語法,像http://localhost:5555/items/${id}
這樣的寫法,它也很方便使用,可以組合變數與字串。程式程如下:
handleServerItemUpdate = (aItem: Item) => {
//處理payload,不需要isEditing欄位
const { id, title, isCompleted } = aItem
const payload = { id, title, isCompleted }
//作POST
fetch(`http://localhost:5555/items/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then((response) => {
//ok 代表狀態碼在範圍 200-299
if (!response.ok) throw new Error(response.statusText)
return response.json()
})
.then((item) => {
//這裡可以顯示一些訊息,或是結束指示動畫…
//console.log(item)
})
.catch((error) => {
//console.error(error)
})
}
當然,你也要在更新項目狀態用的handleStylingItem
來呼叫這個方法,讓項目完成狀態改變時,伺服器上的資料也一併會更新:
//更新某一筆資料
this.handleServerItemUpdate(newItems[index])
以上就是程式碼的所有更動說明。當然它並會太複雜,有些動態指示的處理,或是錯誤時要如何處理,這留給你可以再去寫了。
生命週期方法
在React元件的設計中,因為它是個虛擬DOM的概念,所以每個元件實際上都是有生命週期(Lifecycle)的設計,也就是一個元件從一開始掛載(Mounting)到網頁上,然後呈現在網頁上,在網頁上因為事件的觸發進行更新(Updating)或重新渲染(being re-rendered),最後從網頁上消失,稱為卸載(Unmounting)。
React為這些生命週期的階段,設計了一些方法讓開發者使用,在某個生命週期階段你可以利用這些方法,在裡面執行一些東西,實際上有兩個生命週期方法我們已經看過,也用到了,以下列出這些生命週期方法:
卸載(Unmounting)
- constructor()
- componentWillMount()
- render()
- componentDidMount()
更新(Updating)
- componentWillReceiveProps()
- shouldComponentUpdate()
- componentWillUpdate()
- render()
- componentDidUpdate()
卸載(Unmounting)
- componentWillUnmount()
其他的重要方法還有兩個,分別是setState()
與forceUpdate()
。
生命週期方法在要作某些工作時特別重要,例如說一開始載入資料(Fetch/Ajax)或是計時器(Timer),或是自訂的事件監聽等等。因為你不能在元件還沒呈現在網頁上作這些事,所以只能在componentDidMount
中這些事情。計時器的clearTimeout或clearInterval,或是自訂事件監聽的取消則是在componentWillUnmount
方法中作這些事情。
其實這個設計還滿簡單的,我後來有畫幾張圖來解說這些生命週期的階段,因為網路上找到的有些是亂畫或是畫得不清楚,有些是很舊的不太符合較新版本的React。
生命週期方法其實不需要太去記,主要是componentDidMount
與componentWillUnmount
這兩個,搭配計時器與一開始載入資料使用,大概一開始學React會容易看到的。當然componentWillReceiveProps
有可能也會在很多元件交互應用時看到。
json-server
這裡說明一些json-server的簡單用法。
首先當然是安裝它,直接用在命令列工具(終端機)中用npm工具就可以安裝,裝在全域即可:
npm install -g json-server
要啟動它也是用命令列工具(終端機)的指令,當然前提是你要先建立一個db.json
檔案,這個檔案中在我們上面這個範例中有附上:
json-server --watch --port 5555 db.json
這個指令代表要啟動一個在埠號為5555的json-server伺服器,之後就可以用瀏覽器打開"http://localhost:5555/items"
註: 我們開發測試React應用的是3000埠,json-server伺服器是5555埠,相當於在同一個電腦啟動了兩個不同的伺服器。
註: 上面的5555是隨便取的埠號,你要用8888或9999也可以,不過埠號有一定的範圍就是。
db.json
檔案格式就類似像下面這樣,它與JS中的物件格式很像,但它是個純文字檔而已。json-server伺服器的網址後加上items會自動只列出items裡面的json資料,它稱之為Routes(路由):
{
"items": [
{
"id": 1482513391121,
"title": "聽演唱會",
"isCompleted": true
}
]
}
那麼,因為json-server伺服器是個用REST API的伺服器,而且又有小型資料庫,又該如何新增、讀取、更新…裡面的資料?
首先你要先理解REST是什麼,也就是在傳資料時的method各自分別要作不同的事情之用,例如下面幾個:
- POST = 新增
- GET = 讀取
- PUT = 更新
- DELETE = 刪除
- PATCH = 取代部份資料
下面就幾種常用的要作某些事情的範例:
載入所有資料
載入所有資料,預設是用id由小至大(ASC)排序:
GET /items
用id排序,由大至小:
GET /items?_sort=id&_order=DESC
新增一筆資料
POST /items
資料範例:
{
"id": 4,
"title": "44444",
"isCompleted": false
}
更新一筆資料
PUT /items/4
資料範例:
{
"id": 4,
"title": "4321",
"isCompleted": false
}
刪除一筆資料
DELETE /items/4
結論
本章主要是展示了React元件在與外部伺服器的溝通交互應用時,要如何來寫這些功能。這個範例程式用了新的Fetch API,以及幾個常常用在開發這類JS應用時會用到的工具,或許你可能還不熟悉,但我會建議你可以學習,因為這些新式的API,雖然它們並不是ES6標準的東西,但也是現在很重要的、很常被使用的一些API。
另一個主題當然是React中的生命週期方法,其實這也不是很困難的一個主題,主要要你要知道它是有這設計的,在某些特定的時刻,你非得使用這些生命週期方法來作某些事情。而在範例中的state
與資料庫的資料是故意設計成不一樣的,因為state
中的資料有可能不相等於資料庫的資料,這一點你需要很清楚,state
是應用程式整體的狀態資料,它仍然有暫時存放的特性,在你重新載入網頁時,state
就回復到最初始的狀態,只有依靠像資料庫的永久儲存性的方式,才能真正保存資料。