大家好!今天要聊的主題其實是大家寫網頁、串 API 時的幾個老朋友:DOM、JSON、fetch、Promise。
DOM簡單來說,就是瀏覽器把 HTML 轉成樹狀結構,讓 JavaScript(JS) / TypeScript(TS) 可以去讀取、修改網頁內容。
在JS裡,常見的寫法:
const btn = document.querySelector('.btn');
btn.addEventListener('click', () => {
console.log('被點了!');
});
如果換成TS:
const btn = document.querySelector('.btn');
btn.addEventListener('click', () => { //Object is possibly 'null'
console.log('被點了!');
});
因為 querySelector的回傳型別是 Element | null,TS會提醒開發者這個元素有可能不存在,不檢查一下可能會報錯。
解法通常有兩種:
1. 加上型別斷言
const btn = document.querySelector('.btn') as HTMLButtonElement;
btn.addEventListener('click', () => {
console.log('被點了!');
});
2. 加上防呆判斷
const btn = document.querySelector<HTMLButtonElement>('.btn');
if (btn) {
btn.addEventListener('click', () => {
console.log('被點了!');
});
}
小提醒:querySelector<HTMLButtonElement>()這種寫法使用了泛型,可以告訴TypeScript我預期抓到的是哪種元素。和型別斷言相比,這種寫法仍然會保留null的可能性,因此比較不容易忘記處理元素不存在的情況。
JSON大概是現在串API最常見的資料格式了,長得跟JS物件很像,但其實是「字串」需要轉換:
JSON.stringify():物件 → JSON 字串JSON.parse():JSON 字串 → 物件interface User {
name: string;
age: number;
}
const user: User = { name: '小明', age: 20 };
// 物件轉字串
const jsonStr = JSON.stringify(user);
console.log(jsonStr); // '{"name":"小明","age":20}'
// 字串轉物件
const parsed = JSON.parse(jsonStr);
console.log(parsed.name); // 小明
這裡要特別注意一個TS常見的錯誤:JSON.parse() 的回傳型別是 any,也就是說TS完全不會幫忙檢查裡面到底有沒有name這個屬性。所以比較嚴謹的做法,會搭配「型別斷言」或是額外寫一個檢查函式:
const parsed = JSON.parse(jsonStr) as User;
不過要記得,as User 只是「安慰自己」的寫法,實際上TS並不會真的去驗證資料格式對不對,如果後端傳回的資料不符合 User,可能要等到實際使用這些資料時才發現問題,這點要注意。
fetch 是瀏覽器內建的API,用來發送網路請求,取代以前XMLHttpRequest。基本用法長這樣:
fetch('https://api.example.com/users')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('出錯啦:', error));
如果搭配async/await,會更好讀:
async function getUsers() {
try {
const response = await fetch('https://api.example.com/users');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('出錯啦:', error);
}
}
我們通常會搭配泛型或型別斷言,讓後面的程式碼有型別檢查可以用:
interface User {
id: number;
name: string;
}
async function getUsers(): Promise<User[]> {
const response = await fetch('https://api.example.com/users');
const data = (await response.json()) as User[];
return data;
}
這樣一來,之後在別的地方呼叫 getUsers(),編輯器就會知道回傳的是 User[],打錯屬性名稱馬上就會被抓出來。
還有一個常被忽略的小細節:fetch 只有在請求無法完成的某些情況下才會 reject,例如網路錯誤;如果伺服器回傳 404 或 500,fetch 本身並不會因此 reject。,如果伺服器回傳 404 或 500,fetch其實還是會「成功」,只是 response.ok 會是 false。所以嚴謹一點的寫法會這樣加判斷:
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP 錯誤:${response.status}`);
}
講到fetch就一定要提Promise,因為fetch本身回傳的就是一個Promise。Promise 是JS用來處理「非同步」操作的機制。
一個 Promise 有三種狀態:
.then()
.catch()
function delay(ms: number): Promise<string> {
return new Promise((resolve, reject) => {
if (ms < 0) {
reject(new Error('時間不能是負數啦'));
return;
}
setTimeout(() => {
resolve(`等了 ${ms} 毫秒後完成!`);
}, ms);
});
}
delay(1000)
.then(msg => console.log(msg))
.catch(err => console.error(err));
在TS中,我們可以透過 Promise<T> 這個泛型,清楚標明「這個 Promise 成功之後會拿到什麼型別的資料」,像上面的Promise<string>就代表成功時會拿到一個字串。這對維護大型專案超級重要。
當然,現在大家更常用async/await這種寫法:
async function main() {
const result = await delay(1000);
console.log(result);
}
這四個東西幾乎是每個前端專案都會用到的基本功,今天把它們串在一起複習一遍,感覺對之後要串真實 API 的章節會更有底氣。那我們明天見啦!