Day 11 最後,我們把選單從單純的字串:
const menuItems = [
'首頁',
'服務',
'關於我們'
]
慢慢改成:
const menuItems = [
{ id: 1, name: '首頁' },
{ id: 2, name: '服務' },
{ id: 3, name: '關於我們' }
]
當時其實已經偷偷碰到一個新的東西:
{
id: 1,
name: '首頁'
}
這個 {} 到底是什麼?
如果只是背一句:
這叫 Object,中文叫物件。
大概過幾天又忘了。
所以這次不從定義開始,直接拿一個實務上很常碰到的東西來看:會員系統。
而且今天不只停在 Object。
我也想先偷看一下:如果未來會員資料真的放進 Supabase,資料到底怎麼一路跑到 React 前端?中間為什麼不一定需要再架一個 Node.js 後端,我覺得也可以稍微說一下,雖然也不是什麼大問題。
假設網站現在有一個會員:
姓名:Anthony
Email:anthony@example.com
角色:member
是否登入:true
最直覺的做法,可以全部拆成變數:
const userName = 'Anthony'
const userEmail = 'anthony@example.com'
const userRole = 'member'
const isLoggedIn = true
這樣不是不能用。
但問題是,這四個東西其實都在描述同一個人。
如果未來還要加:
會員編號
頭像
電話
註冊時間
權限
方案
變數就會愈來愈多。
這時候通常比較想要的是:
能不能把「Anthony 這個會員」相關的資料包在一起?
JavaScript 的 Object 就很適合做這件事。
const user = {
id: 1,
name: 'Anthony',
email: 'anthony@example.com',
role: 'member',
isLoggedIn: true
}
現在不是五個散落的變數。
而是有一個:
user
裡面放著這個會員的不同資料。
可以先把 Object 想成:
一筆有欄位名稱的資料。
name: 'Anthony' 到底怎麼看?例如:
const user = {
name: 'Anthony',
role: 'member'
}
裡面有兩組:
name → Anthony
role → member
JavaScript 寫成:
key: value
所以:
name: 'Anthony'
可以先讀成:
name 這個欄位 它的值是 Anthony
而:
role: 'member'
就是:
role 這個欄位 它的值是 member
如果:
const user = {
id: 1,
name: 'Anthony',
email: 'anthony@example.com',
role: 'member'
}
我要拿名字,可以寫:
user.name
結果是:
Anthony
拿角色:
user.role
結果是:
member
拿 Email:
user.email
結果就是:
anthony@example.com
所以這個 . 可以先把它念成人話:
user.name
就是:
user 的 name。
而:
user.role
就是:
user 的 role。
這樣其實比先背「dot notation」好記很多。
Day 11 的 Array:
const menuItems = [
'首頁',
'服務',
'關於我們'
]
比較像:
一串有順序的資料。
所以可以靠位置拿:
menuItems[0]
menuItems[1]
menuItems[2]
但 Object:
const user = {
name: 'Anthony',
role: 'member'
}
不是靠「第幾個」。
而是靠欄位名稱:
user.name
user.role
可以先這樣區分:
Array
→ 我有一串資料
→ 常常在意順序
→ 用 [0]、[1] 取資料
Object
→ 我有一筆資料
→ 裡面有很多欄位
→ 用 .name、.role 取資料
因為會員通常不會只有一個。
假設有三個會員:
const users = [
{
id: 1,
name: 'Anthony',
role: 'member'
},
{
id: 2,
name: 'Amy',
role: 'admin'
},
{
id: 3,
name: 'Tom',
role: 'member'
}
]
這時候外面:
[
...
]
是 Array。
裡面的每一筆:
{
id: 1,
name: 'Anthony',
role: 'member'
}
是一個 Object。
所以整體可以想成:
users Array
│
├─ user Object
│ ├─ id
│ ├─ name
│ └─ role
│
├─ user Object
│ ├─ id
│ ├─ name
│ └─ role
│
└─ user Object
├─ id
├─ name
└─ role
這時候 Day 11 的 map() 就又回來了。
users.map((user) => {
return user.name
})
每一輪的 user 分別會是:
{ id: 1, name: 'Anthony', role: 'member' }
{ id: 2, name: 'Amy', role: 'admin' }
{ id: 3, name: 'Tom', role: 'member' }
所以:
user.name
會依序拿到:
Anthony
Amy
Tom
Day 11 的:
map()
跟今天的:
Object
就在這裡接起來了。
例如現在有:
const user = {
id: 1,
name: 'Anthony',
email: 'anthony@example.com',
role: 'member'
}
React 可以直接寫:
function MemberCard() {
const user = {
id: 1,
name: 'Anthony',
email: 'anthony@example.com',
role: 'member'
}
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>{user.role}</p>
</div>
)
}
畫面就會得到類似:
Anthony
anthony@example.com
member
這裡其實沒有什麼神奇的 React 魔法。
只是:
user.name
user.email
user.role
把 Object 裡的值拿出來,再放進 JSX。
真的會員網站通常還會有選單。
例如:
const menuItems = [
{
id: 1,
name: '首頁',
url: '/',
requiresLogin: false
},
{
id: 2,
name: '會員中心',
url: '/member',
requiresLogin: true
}
]
這次每一筆 menu 不只有名字。
還知道:
它叫什麼
網址在哪
需不需要登入
也就是:
item.name
item.url
item.requiresLogin
React 就可以根據這些資料決定要畫什麼。
例如先做一個非常簡化的版本:
function Header() {
const user = {
id: 1,
name: 'Anthony',
isLoggedIn: true
}
const menuItems = [
{
id: 1,
name: '首頁',
url: '/',
requiresLogin: false
},
{
id: 2,
name: '會員中心',
url: '/member',
requiresLogin: true
}
]
return (
<nav>
{menuItems.map((item) => {
if (item.requiresLogin && !user.isLoggedIn) {
return null
}
return (
<a key={item.id} href={item.url}>
{item.name}
</a>
)
})}
</nav>
)
}
這段開始有點像真的網站了。
但今天先不要一次拆太多。
現在只要先看懂:
item.requiresLogin
是在讀 Object 裡的欄位。
user.isLoggedIn
也是在讀 Object 裡的欄位。
至於:
&&
!
return null
之後再說好了。
dist 嗎?我原本有一個疑問:
如果真的做會員系統,這些東西是放在
dist裡,還是components裡?
現在可以先建立一個很重要的分界。
平常自己寫的 React 原始碼,主要是在:
src/
例如未來可能長成:
src/
├─ components/
│ ├─ Header.jsx
│ └─ MemberCard.jsx
│
├─ pages/
│ ├─ Login.jsx
│ └─ Member.jsx
│
├─ App.jsx
└─ main.jsx
components/ 只是 src/ 裡的一部分。
像:
Header
MemberCard
Button
Sidebar
這種可以重複使用的小區塊,很適合放 components/。
但整個會員系統不會全部塞在一個 component 裡。
而:
dist/
不是平常寫程式的地方。
通常是執行:
npm run build
之後,Vite 幫我們產生的部署成品。
可以先記:
src/
→ 我寫的原始碼
npm run build
↓
dist/
→ Vite 整理、打包後準備部署的成品
所以不要跑去 dist 裡手改會員系統。
到目前為止,我們都是自己寫:
const user = {
id: 1,
name: 'Anthony',
role: 'member'
}
這當然只是練習。
真正的網站裡,會員資料通常會放在資料庫。
後面我們使用 Supabase,大概會開始看到這種資料流:
PostgreSQL
↓
Supabase Data API
↓
supabase-js
↓
React
↓
畫面
假設資料庫的 profiles table 裡有:
id | name | role
1 | Anthony | member
2 | Amy | admin
前端之後可能會寫出類似:
const { data, error } = await supabase
.from('profiles')
.select('*')
這段今天先不用會寫。
先注意 data 回到 JavaScript 世界之後,概念上可能長得像:
[
{
id: 1,
name: 'Anthony',
role: 'member'
},
{
id: 2,
name: 'Amy',
role: 'admin'
}
]
咦?
這不就是:
Array
↓
裡面很多 Object
又回到 Day 11、Day 12 了。
所以前面學:
user.name
或:
users.map((user) => {
return user.name
})
不只是拿來操作我們自己手寫的假資料。
之後從後端 API 拿回來的資料,也常常會變成這種 JavaScript Array / Object,再交給 React 顯示。
使用 Supabase 時,不一定。
Supabase 本身已經幫我們提供一層可以存取 PostgreSQL 的 Data API,而且 supabase-js 可以直接從瀏覽器呼叫它。
所以一個簡單的 React + Supabase 專案,可以先長成:
Browser
↓
React
↓
supabase-js
↓ HTTPS
Supabase Data API
↓
PostgreSQL
也就是:
我不一定要先自己寫一個 Express / Node.js API Server,React 才能拿到資料庫資料。
這也是 Supabase 很方便的地方之一。
它已經幫我們做掉很多傳統後端會先處理的基礎能力,例如資料 API、登入驗證、資料庫與檔案儲存等。
看到這裡下一個直覺問題大概就是:
等等,如果瀏覽器可以直接呼叫 Supabase,那使用者打開 DevTools,不就也看得到這些請求?
對,所以正式系統不會因為「可以從前端直接呼叫」就把所有資料全部開放。
後面會碰到一個很重要的東西:
RLS
Row Level Security
它會負責限制:
這個登入者
到底可以讀哪些 row
可以改哪些 row
例如:
Anthony
→ 可以讀自己的 profile
→ 不代表可以亂讀所有會員資料
RLS 今天先停在這一句就好。
現在如果直接開始寫 policy、auth.uid()、權限角色,Day 12 又會瞬間變成另外一門課。
Day 11 我們有:
Array
↓
map()
↓
JSX
Day 12 加入 Object 之後:
Array
↓
很多 Object
↓
map()
↓
user.name / user.role
↓
JSX
如果再稍微偷看未來的 Supabase:
PostgreSQL
↓
Supabase Data API
↓
supabase-js
↓
JavaScript Array / Object
↓
map()
↓
React JSX
↓
畫面
這時候 Object 就不再只是一本 JavaScript 教科書裡的語法啦!(撒花
它開始站在真正前後端資料流的中間。
資料庫裡的一列:
id | name | role
1 | Anthony | member
來到 JavaScript 世界,很可能就變成:
{
id: 1,
name: 'Anthony',
role: 'member'
}
React 再去拿:
user.name
user.role
把資料畫出來。
我們只是拿會員系統當例子,把幾個原本分開的世界第一次接起來:
JavaScript Object
↓
React
↓
Supabase Data API
↓
PostgreSQL
現在至少可以先回答:
一個會員
→ 可以用一個 Object 表示
很多會員
→ 可以是 Array 裡很多 Object
React 怎麼顯示
→ 讀 Object 欄位,再產生 JSX
真正資料從哪裡來
→ 之後可以從 Supabase / PostgreSQL 查回來
中間一定要自己的 Node.js 嗎
→ 不一定,Supabase 可以提供前端直接呼叫的 Data API
那安全怎麼辦
→ 後面再學 RLS
目前我們寫:
const user = {
name: 'Anthony',
isLoggedIn: true
}
它從程式一開始就是登入狀態。
可是正常會員系統應該比較像:
一開始沒有登入
↓
user 可能是 null
↓
使用者輸入帳號密碼
↓
按下登入
↓
user 變成一個會員 Object
↓
畫面跟著改變
問題來了:
JavaScript 裡的資料變了,React 怎麼知道畫面也要跟著變?
這個問題,後面就會把我們帶到 state。
但在進 state 以前,我還想先把 Object 怎麼傳進 Component,以及畫面怎麼從設計稿拆成 Component 看清楚。
所以接下來,這個「假會員」會慢慢長成真的會員系統。
每天都好累...寶寶睡了、太太睡了,然後再整理筆記是現在晚上的日常 Zz 晚安