博客 / 技术笔记

从零搭建 Astro 博客踩坑实录(三):Astro 组件开发经验

前言

Astro 的「零 JS」哲学很香,但组件开发还是有些地方需要注意。这篇聊聊开发中总结的一些经验。

一、样式作用域的坑

Astro 组件中的 <style> 默认是 scoped(作用域),只对当前组件生效。

Header.astro
<style>
.nav-links {
color: blue;
}
</style>

编译后会变成:

.nav-links[data-astro-cid-xxx] {
color: blue;
}

如果需要全局样式

<style is:global>
.global-class {
color: red;
}
</style>

或者在 src/styles/global.css 中定义。

二、组件复用:Hero 的条件渲染

首页需要 Hero 全屏背景,博客页不需要。通过 props 控制:

Header.astro
---
interface Props {
hideHero?: boolean;
}
const { hideHero = false } = Astro.props;
---
{hideHero === false && (
<section class="hero">
<div class="hero-overlay"></div>
<div class="hero-content">
<h1 class="hero-title">
<span id="typing-title"></span>
<span class="typing-cursor">|</span>
</h1>
</div>
</section>
)}
---
// 首页
import Header from '../components/Header.astro';
---
<Header /> <!-- 显示 Hero -->
// 博客页
<Header hideHero={true} /> <!-- 隐藏 Hero -->

三、打字机效果实现

const titleEl = document.getElementById('typing-title');
if (titleEl) {
const lines = ['安静且有趣的日子', '是大家最期盼的平淡'];
let lineIndex = 0;
let charIndex = 0;
function typeWriter() {
if (lineIndex < lines.length) {
const currentLine = lines[lineIndex];
charIndex++;
currentText = lines.slice(0, lineIndex).join('\n') + '\n' + currentLine.substring(0, charIndex);
titleEl.textContent = currentText;
if (charIndex < currentLine.length) {
setTimeout(typeWriter, 120);
} else {
lineIndex++;
charIndex = 0;
if (lineIndex < lines.length) {
setTimeout(typeWriter, 300); // 行间停顿更长
}
}
}
}
setTimeout(typeWriter, 500);
}

光标闪烁用 CSS 动画:

.typing-cursor {
display: inline-block;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}

四、Header 滚动行为

滚动超过 Hero 一半时:

  • 背景色变化(透明 → 白色)
  • 向下滚动时隐藏 Header
  • 向上滚动时显示 Header
let lastScroll = 0;
const header = document.querySelector('.header-nav');
const hero = document.querySelector('.hero');
const threshold = hero ? hero.clientHeight * 0.5 : 0;
window.addEventListener('scroll', () => {
const currentScroll = window.pageYOffset;
if (currentScroll > threshold) {
header?.classList.add('scrolled');
if (currentScroll > lastScroll) {
header?.classList.add('hidden'); // 向下滚,隐藏
} else {
header?.classList.remove('hidden'); // 向上滚,显示
}
} else {
header?.classList.remove('hidden', 'scrolled');
}
lastScroll = currentScroll;
}, { passive: true });

五、主题切换

const themeToggle = document.querySelector('.theme-toggle');
const html = document.documentElement;
// 读取保存的主题
const savedTheme = localStorage.getItem('theme') || 'light';
html.setAttribute('data-theme', savedTheme);
themeToggle?.addEventListener('click', () => {
const current = html.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
});

配合 CSS 变量:

:root {
--bg-primary: #ffffff;
--text-primary: #1a1a1a;
}
[data-theme="dark"] {
--bg-primary: #1a1a1a;
--text-primary: #ffffff;
}

总结

  1. 样式作用域:小样式放组件内,大样式放 global.css
  2. 条件渲染:通过 props 控制组件行为
  3. 零 JS 理念:只在需要交互的地方写 JS
  4. passive 监听:滚动事件加上 { passive: true } 提升性能

Astro 的开发体验还是很舒服的,特别是静态生成 + 组件化的组合。