DOM(Document Object Model)把 HTML 文件轉成「節點(Node)樹」,你可以用 JavaScript/TypeScript 操作它:查找元素、改文字、改屬性、改樣式、監聽事件等等。
querySelector
、querySelectorAll
textContent
、innerHTML
(本篇盡量用 textContent
)getAttribute
、setAttribute
、hasAttribute
、removeAttribute
classList.add/remove/toggle
、style.xxx
dataset
(如 data-category
)addEventListener('click', handler)
、preventDefault()
、stopPropagation()
event.target
判斷實際點擊者<header>
裡放一顆切換按鈕)<button id="theme-toggle" type="button" aria-pressed="false">切換主題</button>
建議 Day 2 的 CSS/SCSS 有用到顏色變數(或 root 變數),這裡只示範最小掛鉤:當 有 data-theme="dark" 就套用深色主題。
/* 你可以在 Day 2 的樣式檔中加入 */
:root {
--bg: #ffffff;
--fg: #2c3e50;
}
html[data-theme="dark"] {
--bg: #1f2937;
--fg: #f3f4f6;
}
body { background: var(--bg); color: var(--fg); }
main.ts
)// 主題切換:把狀態存在 <html data-theme="..."> 與 localStorage
const htmlEl = document.documentElement;
const themeToggleBtn = document.querySelector<HTMLButtonElement>('#theme-toggle');
function applyTheme(theme: 'light' | 'dark') {
htmlEl.setAttribute('data-theme', theme);
if (themeToggleBtn) {
themeToggleBtn.setAttribute('aria-pressed', String(theme === 'dark'));
themeToggleBtn.textContent = theme === 'dark' ? '切換為亮色' : '切換為暗色';
}
localStorage.setItem('theme', theme);
}
// 初始化:讀取上次選擇
const saved = (localStorage.getItem('theme') as 'light' | 'dark') || 'light';
applyTheme(saved);
// 綁定按鈕
themeToggleBtn?.addEventListener('click', () => {
const next = htmlEl.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
applyTheme(next as 'light' | 'dark');
});
data-category
)<section id="skills">
<h2>技能 Skillset</h2>
<!-- 篩選器 -->
<div id="skill-filters" role="tablist" aria-label="技能分類">
<button role="tab" data-filter="all" aria-selected="true">全部</button>
<button role="tab" data-filter="frontend">前端</button>
<button role="tab" data-filter="backend">後端</button>
<button role="tab" data-filter="tools">工具</button>
</div>
<ul id="skill-list">
<li data-category="frontend">HTML / CSS / SCSS</li>
<li data-category="frontend">TypeScript</li>
<li data-category="frontend">Angular / React / Vue</li>
<li data-category="backend">Node.js / Express</li>
<li data-category="tools">Git / GitHub / Docker</li>
</ul>
</section>
type SkillCategory = 'all' | 'frontend' | 'backend' | 'tools';
const filters = document.querySelector<HTMLDivElement>('#skill-filters');
const skillList = document.querySelector<HTMLUListElement>('#skill-list');
function applySkillFilter(cat: SkillCategory) {
if (!skillList) return;
const items = Array.from(skillList.querySelectorAll<HTMLLIElement>('li'));
items.forEach(li => {
const c = (li.dataset.category || 'frontend') as SkillCategory;
li.style.display = (cat === 'all' || c === cat) ? '' : 'none';
});
}
filters?.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (target.matches('button[data-filter]')) {
const cat = target.getAttribute('data-filter') as SkillCategory;
// 視覺/a11y 狀態
filters.querySelectorAll('[aria-selected="true"]').forEach(el => el.setAttribute('aria-selected', 'false'));
target.setAttribute('aria-selected', 'true');
// 套用篩選
applySkillFilter(cat);
}
});
// 預設顯示全部
applySkillFilter('all');
<section id="about">
<h2>關於我</h2>
<img id="avatar" src="me-formal.jpg" alt="Chiayu 的正式照片" width="200"
data-alt-src="me-casual.jpg">
<p>嗨,我是 Chiayu,一名前端工程師...</p>
<button id="photo-toggle" type="button">切換照片</button>
</section>
這裡使用 data-alt-src 存放替代圖片路徑,日後在框架中也很好綁定。
const img = document.querySelector<HTMLImageElement>('#avatar');
const photoToggle = document.querySelector<HTMLButtonElement>('#photo-toggle');
photoToggle?.addEventListener('click', () => {
if (!img) return;
const current = img.getAttribute('src') || '';
const altSrc = img.dataset.altSrc || '';
if (!altSrc) return;
// 交換 src 與 data-alt-src
img.setAttribute('src', altSrc);
img.dataset.altSrc = current;
// 同步替代文字(若兩張照性質不同)
const isFormal = /formal/.test(altSrc);
img.alt = isFormal ? 'Chiayu 的正式照片' : 'Chiayu 的生活照片';
});
完成以上三段後,你的履歷網站具備:
data-category
為依據,不用改 HTML 結構就能擴充。直接用 innerHTML
填入未消毒字串
錯誤:
titleEl.innerHTML = userInput; // 可能造成 XSS
正確:
titleEl.textContent = userInput; // 文字內容請用 textContent
忘了考慮元素可能為 null
錯誤:
document.querySelector('#x')!.addEventListener('click', fn);
正確:
const el = document.querySelector<HTMLButtonElement>('#x');
if (el) el.addEventListener('click', fn);
為每一個子元素都綁監聽,造成效能浪費
錯誤:
document.querySelectorAll('#skill-filters button')
.forEach(b => b.addEventListener('click', onClick));
正確:事件委派(監聽父元素)
filters?.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (target.matches('button[data-filter]')) { /* ... */ }
});
用行內樣式硬改所有外觀,導致樣式與行為耦合
錯誤:
el.style.display = 'none';
el.style.color = 'red';
正確:
el.classList.add('is-hidden'); // 把樣式定義在 CSS 中
濫用 dataset
當狀態倉庫
錯誤:
el.dataset.state = JSON.stringify({ aLotOfState: true }); // 難以維護
正確: dataset
存放輕量、與 DOM 強相關的設定值(如類別、替代路徑);複雜狀態請放 JS/TS 內部結構。
input
事件 → includes
過濾)scrollIntoView({ behavior: 'smooth' })
)明天我們會做一個 原生 HTML/CSS/TS 的一頁式自我介紹頁(小專案),把 Day1–Day4 的知識整合起來,形成「無框架也能交付」的最小可用作品。
接著 Day 6 起,我們會把同樣的資訊架構搬進 Angular,正式開始框架實戰。