無限階層留言規劃,含資料表設計

這個議題算是只要有留言系統,並且想實作無限層級的留言都會碰到的。
如:Facebook的留言、wordpress的留言。
當然也有簡單類的留言,只有一層的,如:古早的無名小站,但這不是我們今天要探討的。
就單刀直入了,如何實作「無限階層的留言」。

資料表

只要一張comments表加上parent_id即可,如下。
再來就可以利用程式方式(遞迴或迴圈)去印出他。

程式部分

把comments當主表,撈出資料SQL:

1
2
3
4
5
6
7
select
users.name,
comments.id,
comments.comment_parent_id,
comments.title,
comments.content
from comments left join users on comments.user_id = users.id

把上面資料表進行關聯後撈出後的資料:

1
2
3
4
5
6
7
[
{id:1,comment_parent_id:null,title:'留言1',content:'內容',name:'路人A'},
{id:2,comment_parent_id:null,title:'留言2',content:'內容',name:'路人B'},
{id:3,comment_parent_id:2,title:'留言3',content:'內容',name:'路人C'},
{id:4,comment_parent_id:3,title:'留言4',content:'內容',name:'路人D'},
{id:5,comment_parent_id:3,title:'留言5',content:'內容',name:'路人E'}
]

再來就可以實作程式碼了,我們要先把上面SQL撈出來的「打平結構」變成「樹狀結構」。
這會需要使用到遞迴。

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
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
</head>
<body>
</body>
</html>
<script>
const commentList = [
{id:1,comment_parent_id:null,title:'留言1',content:'內容',name:'路人A'},
{id:2,comment_parent_id:null,title:'留言2',content:'內容',name:'路人B'},
{id:3,comment_parent_id:2,title:'留言3',content:'內容',name:'路人C'},
{id:4,comment_parent_id:3,title:'留言4',content:'內容',name:'路人D'},
{id:5,comment_parent_id:3,title:'留言5',content:'內容',name:'路人E'}
]

// 建立樹狀階層
function buildTree(commentList){
// 根節點
const rootNode = commentList.filter(it=> it.comment_parent_id === null)
// 增加子節點
return addNode(rootNode,commentList)
}

// 添加子節點
function addNode(children,commentList){
// 找不到子節點返回
if(children.length === 0){
return []
}
// 添加子節點
const arr = []
// 遍歷當前children節點
for(let i=0;i<children.length;i++){
// 查詢底下的子節點
const childrenNode = commentList.filter(it=>
it.comment_parent_id === children[i].id)
// 把底下子節點添加進陣列
arr.push({
id:children[i].id,
name:children[i].name,
title:children[i].title,
content:children[i].content,
children:addNode(childrenNode,commentList)
})
}
// 返回子節點陣列
return arr
}
// 印出結果
console.log(JSON.stringify(buildTree(commentList)));

</script>

轉成樹狀階層後的資料:

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
[
{
"id": 1,
"name": "路人A",
"title": "留言1",
"content": "內容",
"children": []
},
{
"id": 2,
"name": "路人B",
"title": "留言2",
"content": "內容",
"children": [
{
"id": 3,
"name": "路人C",
"title": "留言3",
"content": "內容",
"children": [
{
"id": 4,
"name": "路人D",
"title": "留言4",
"content": "內容",
"children": []
},
{
"id": 5,
"name": "路人E",
"title": "留言5",
"content": "內容",
"children": []
}
]
}
]
}
]

光轉成上面那串,就想了快3小時的遞迴,然後上網找些範例參考才組出來。
我當時的疑問就是:「怎麼讓遞迴去回傳整包陣列」,所幸有找到參考,然後小改自己程式就成功了。

再來就是把上面那階層式資料在網頁上印出來。
還是要用遞迴。

網頁上內容呈現

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
</head>
<body>

<div id="comment">
</div>

</body>
</html>
<script>
const commentList = [
{ id: 1, comment_parent_id: null, title: '留言1', content: '內容', name: '路人A' },
{ id: 2, comment_parent_id: null, title: '留言2', content: '內容', name: '路人B' },
{ id: 3, comment_parent_id: 2, title: '留言3', content: '內容', name: '路人C' },
{ id: 4, comment_parent_id: 3, title: '留言4', content: '內容', name: '路人D' },
{ id: 5, comment_parent_id: 3, title: '留言5', content: '內容', name: '路人E' }
]

// 建立樹狀階層
function buildTree(commentList) {
// 根節點
const rootNode = commentList.filter(it => it.comment_parent_id === null)
// 增加子節點
return addNode(rootNode, commentList)
}

// 添加子節點
function addNode(children, commentList) {
// 找不到子節點返回
if (children.length === 0) {
return []
}
// 添加子節點
const arr = []
// 遍歷當前children節點
for (let i = 0; i < children.length; i++) {
// 查詢底下的子節點
const childrenNode = commentList.filter(it =>
it.comment_parent_id === children[i].id)
// 把底下子節點添加進陣列
arr.push({
id: children[i].id,
name: children[i].name,
title: children[i].title,
content: children[i].content,
children: addNode(childrenNode, commentList)
})
}
// 返回子節點陣列
return arr
}

// 在網頁上呈現出結果
function generateCommentTemplate(children) {
let template = '<ul>'
for (let i = 0; i < children.length; i++) {
template += '<li>'
template += children[i].title + '<br>'
template += children[i].content + '<br>'
template += children[i].name + '<br>'

// 如果有子節點則遞迴再次呼叫
if (children[i].children.length !== 0) {
template += generateCommentTemplate(children[i].children)
}
template += '</li>'
}
template += '</ul>'
return template
}
// 建立樹狀結構
const tree = buildTree(commentList)
// 產生HTML模板
document.getElementById("comment").innerHTML = generateCommentTemplate(tree)
</script>

輸出結果:

總結

整篇大概花了5小時左右寫完,算是網頁系統裡最難的”遞迴”。
在網路上查,很難有一篇現成完整範例T_T,所以在這篇中就把整套都放上來。
同時練習對於樹的掌握。

流程如下:
1.規劃無限階層的留言 - 約10分鐘
2.樹的組合(遞迴) - 約3小時
3.轉譯成HTML模板(遞迴) - 約30分鐘

總之,有點成就感,因為真的不好寫。
如果後端一開始吐給你的是已經階層化的,那就很輕鬆了,因為那段的遞迴比較簡單。

ps.這留言的實作方法也能用來處理分類、類別、側邊欄,就內文改一下即可。

我有封裝了方法變成一隻.js函式庫,可以直接拿去用。
https://github.com/yuhsiang237/buildTree.js

參考資料
https://blog.51cto.com/dd118/2095671
https://kevintsengtw.blogspot.com/2013/07/aspnet-mvc-jquery-easy-ui-tree-json.html
https://ithelp.ithome.com.tw/articles/10028129
https://itw01.com/UBNZIEU.html