【Nuxt】 一個 Nuxt.js 搭配 VueX 的 CRUD 小栗子

本篇大綱:

  1. 簡介
  2. 前置作業
  3. Vuex 設定
  4. CRUD 功能

簡介

這是一個 Nuxt 搭配 Vuex 的 CRUD 栗子。

Source code
Live Demo


前置作業

新增 Nuxt 專案

1
npx create-nuxt-app crud-app

在輸入指令後會提供幾個項目的選擇,包括服務器端框架、UI 框架、測試框架、Nuxt 模式等。這裡的 UI 框架選擇的是 Vuetify.js.


準備資料

這裡使用了 JSONPlaceholder 提供的 API. 裡面提供的例子使用 fetch, 所以在這個例子裡,就可以直接 抄起來 使用. 要注意的是:這個 API 在發送新增 / 更新 / 刪除的 request 時並不會真的更改原本的資料。


設定 UI 部分

這裡需要的 UI 只有 form, card, button, 直接套用 vuetify 裡的模板。


Vuex 設定

Vuex 是類似 Redux 的 state 集中管理器。這裡的 state 比較單純,只有我們設定的 posts 一項,用來儲存每筆的資料。

這裡會用到 Vuex 的功能包含了: state, mutation, action. 因為 Nuxt 已經幫忙處理 Vuex 後面的東西,所以只要按照 文件 上的方式使用就可以了。

Vuex 的檔案會放在 store 底下。


State

這裡創造了一個 posts 為空 array, 用來儲存資料。

1
2
3
4
5
6
//filename : index.js

export const state = () => ({
posts: []
})


Action

要執行的動作會在 action 裡處理. Action 並不會直接改變 state, 而是會提交 mutation,由mutation 來改變 state.

Action 會通過 store.dispatch 被觸發,會在 nuxt 的 methods 中 dispatch。

1
2
3
4
5
6
7
8
9
//例子

methods: {
async createPost () {
await this.$store.dispatch('createPost', { title: this.title, body: this.body })
this.title = ''
this.body = ''
},
}

dispatch 的第二個參數是我們想要傳到 action 的參數。如果要傳多個參數就要將他們包成一個 object 往下傳,因為 action 只能接受 1 個參數。

action 在接收參數時候,要注意參數位置。第一個參數:內建的參數,如 commit / state;第二個參數:自己要往下傳的參數。

1
2
3
4
5
6
7
8
9
10
11
//錯誤例子

export const actions = {
async createPost ({ commit , title, body }) {
// 這裡會是兩個參數不是一個:
// async createPost ({commit}, {title, body})
·
·
·
}
}

mutation

Mutation 是最為底層的操作,比如說「把某個變數改成 true」、「把這筆資料塞到 array 裡」的操作。因此在命名上也要以改 function 做了什麼為標準,才能做到最大程度的重複使用和避免混淆。

CRUD 功能

CRUD 的功能包括增加 (Create), 讀取 (Read), 更新 (Update) 和 刪除 (Delete). JSONplaceholder 提供了這四個方法但因為發送的請求不會改變原本的資料,所以其實都是在操作自己的 array.


Read 讀取

在執行其他 function 前,會先將資料抓取下來,所以這個步驟會放在 Nuxt mouted() 裡。

1
2
3
4
5
6
//filename : index.vue

async mounted () {
await this.$store.dispatch('initPost')
},

Action 中的 init post 會發送請求,data 會是 100 筆的資料。

  • 第 4 行 : commit 是 action 自帶的參數
  • 第 7 行 : 每筆資料都需要 show 這個變數,用來控制是一般狀態 / 可編輯狀態
1
2
3
4
5
6
7
8
9
10
11
12
//filename : index.js

export const actions = {
async initPost ({ commit }) {
const res = await fetch('https://jsonplaceholder.typicode.com/posts')
const data = await res.json()
data.forEach((item) => { item.show = true })

commit('setPosts', data)
},
...
}
1
2
3
4
5
6
7
8
9
10
//filename : index.js
// 把 posts 設成 action 抓下來的 data

export const mutations = {
setPosts (state, data) {
state.posts = data
},
...
}


Create 增加

提供用戶輸入 title 和 body 兩個部分。需要把用戶輸入的 title 和 body 都往下傳。

1
2
3
4
5
6
7
8
9
10
//index.vue

methods: {
async createPost () {
await this.$store.dispatch('createPost', { title: this.title, body: this.body })
this.title = ''
this.body = ''
},
...
}

每則 comment 的 id 使用亂數產生。這裡發送的 request 並不會直接更新到原本的資料,只會把這筆資料加到 posts 裡。 與前面的相同,這裡需要為新增的資料加上 show 變數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//index.js

export const actions = {
...

async createPost ({ commit }, { title, body }) {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: JSON.stringify({
title,
body,
userId: 1
}),
headers: {
'Content-type': 'application/json; charset=UTF-8'
}
})

const post = await res.json()
post.show = true
post.id = Math.floor(Math.random() * 10000)
// console.log(post)
commit('pushPost', post)
},

...
}

這裡的動作就是將新增加的這筆資料加到 posts 裡。這裡的 post 存在 state 裡,所以要使用 state.posts 才能讀取。

1
2
3
4
5
6
7
8
9
10
11
//index.js

export const mutations = {
...

pushPost (state, post) {
state.posts.unshift(post)
},

...
}

Update 更新

更新資料一樣使用 JSONplaceholder 提供的 API. 更新資料的邏輯是設定兩個區塊( 一般顯示/ 顯示 form 欄位 ),使用 show 當做變數來操控:當 show 為 true 時,就顯示正常的狀況、反之則顯示 form 讓用戶更新資料。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//index.vue

methods: {
...

async updatePost (post, editedTitle, editedBody) {
await this.$store.dispatch('updatePost', {
post,
editedTitle: this.editedTitle,
editedBody: this.editedBody
})
},

editBtn (post) {
this.$store.dispatch('editBtn', post)
this.editedTitle = post.title
this.editedBody = post.body
},

...
}

因為這裡不會改變原本的資料,因此這裡的 URL 後面的 id 可以不用改。再者,因為在創造新資料的時候,創造新的 id 是使用亂數產生,這些亂數並不存在在原本的資料中,因此用新 id 去發送請求會出現 error.

在 form 出現的時候,submitcancel button 會一同出現,editdelete 就會隱藏起來。(會在 template 裡處理).

edit button 和 cancel button 都是使用 for loop 的方式去找導被點擊的卡片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
//index.js

export const actions = {
...
async updatePost ({ commit }, { post, editedTitle, editedBody }) {
const res = await fetch('https://jsonplaceholder.typicode.com/posts/1', {
method: 'PUT',
body: JSON.stringify({
id: 1,
title: post.title,
body: post.body,
userId: 1
}),
headers: {
'Content-type': 'application/json; charset=UTF-8'
}
})
await res.json()
commit('setPost', {
id: post.id,
title: editedTitle,
body: editedBody
})
commit('setShow', {
id: post.id,
show: true
})
},

editBtn ({ commit, state }, post) {
state.posts.forEach((item) => {
if (item.id !== post.id) {
commit('setShow', {
id: item.id,
show: true
})
}
})

commit('setShow', {
id: post.id,
show: !post.show
})
},

cancelBtn ({ commit, state }, post) {
const postIndex = state.posts.findIndex(item => item.id === post.id)

state.posts.forEach((item) => {
if (item.id === postIndex) {
commit('setShow', {
id: item.id,
show: true
})
}
})

commit('setShow', {
id: post.id,
show: true
})
},
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//index.js

export const mutations = {
...

setPost (state, { id, title, body }) {
const postIndex = state.posts.findIndex(item => item.id === id)

this._vm.$set(state.posts, postIndex, {
title,
body,
id,
show: true
})
},

setShow (state, { id, show }) {
const postIndex = state.posts.findIndex(item => item.id === id)
state.posts[postIndex].show = show
}
...
}

Delete 刪除

如前面所說的,發出去的 request 並不會真的改變原本的資料。因此這裡刪除的處理方式就是將被點到 comment 從 posts array 中移除。

1
2
3
4
5
6
7
// filename : index.vue
methods: {
...
deletePost (post) {
this.$store.dispatch('deletePost', post)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
//index.js

export const actions = {
...

deletePost ({ commit }, post) {
fetch(`https://jsonplaceholder.typicode.com/posts/${post.id}`, {
method: 'DELETE'
})

commit('removePost', post.id)
}
}
1
2
3
4
5
6
7
8
9
10
11
//index.js

export const mutations = {
...
removePost (state, id) {
const removeItem = state.posts.findIndex(item => item.id === id)
state.posts.splice(removeItem, 1)
},
...
}
}

其他小筆記

全局引入 CSS

這個例子其實沒有用到很多的樣式,但還是有一個 CSS 檔案需要引入。這裡選擇了最方便的方式:在 nuxt.config.js 中全局引入。

因為還沒有深入研究全局引入和其他方式引入有什麼優劣,因此這裡就是用我認為最方便的方法。

methods 裡的 function

methods 是一個 dictionary, 內容看起來不是常見的 key: value pair, 他其實是簡寫。原本長得是這個樣子,但如果後面接的是匿名函式就可以簡寫成 createPost() {...} .

1
2
3
4
5
6
7
//原本長這樣

methods: {
createPost: () => {
...
}
}

部署到 github page

這裡參考了這篇文章:28. Nuxt 靜態頁部署 ,就沒有在另外寫筆記啦。


小結

這次機緣巧合下需要接觸 Vue 和 Nuxt 所做出來的小栗子。當中有遇到一些參數傳遞的問題,以及一些元件沒有及時更新的問題,後者的問題在使用 Vuex 之後就解決了。其他遇到的坑或是需要留意的部分都記錄下來了。

心得就是不會就要問,卡太久就要問,問了就會記下來這樣。因為這是第一次接觸 Nuxt, 以前也沒有寫過 Vue, 弄起來還是有些吃力,背後的原理還是概念會再慢慢補起來,這裡就當做一個簡單記錄。

Comments