
前端开发
CSS单位大比较:rem、em、px、%、vh、vw、rpx的区别与应用场景
2024年5月22日
Astro作为一个现代化的静态站点生成器,凭借其"海岛架构"(Islands Architecture)、零JS默认配置和出色的内容处理能力,成为构建个人网站的理想选择。本文将分享一系列使用Astro开发个人网站的最佳实践,帮助你构建高性能、易于维护且功能丰富的个人网站。
一个组织良好的Astro项目应该具有清晰的目录结构:
project/
├── public/ # 静态资源目录
│ ├── images/ # 图片资源
│ ├── fonts/ # 字体文件
│ └── content/ # 内容文件(如markdown博客)
├── src/ # 源代码目录
│ ├── components/ # UI组件
│ │ ├── common/ # 通用组件
│ │ └── layout/ # 布局相关组件
│ ├── layouts/ # 页面布局模板
│ ├── pages/ # 页面组件(会自动生成路由)
│ │ └── blog/ # 博客相关页面
│ ├── styles/ # 样式文件
│ ├── utils/ # 工具函数
│ └── env.d.ts # 类型声明
├── astro.config.mjs # Astro配置文件
├── tailwind.config.js # Tailwind配置(如果使用)
├── tsconfig.json # TypeScript配置
└── package.json # 项目依赖
将配置项存储在环境变量中,避免硬编码敏感信息:
// src/utils/config.ts
export const CONFIG = {
site: {
title: import.meta.env.PUBLIC_SITE_TITLE || "我的个人网站",
description: import.meta.env.PUBLIC_SITE_DESCRIPTION,
url: import.meta.env.PUBLIC_SITE_URL
},
// GitHub等集成的API密钥
github: {
token: import.meta.env.GITHUB_TOKEN
}
};
配合.env
文件使用(记得将它添加到.gitignore
中):
# 公共变量(可以暴露给客户端)
PUBLIC_SITE_TITLE=Yu的个人网站
PUBLIC_SITE_DESCRIPTION=分享技术与经验的个人空间
PUBLIC_SITE_URL=https://example.com
# 私有变量(仅在服务端可用)
GITHUB_TOKEN=your_github_token
Astro的海岛架构允许你只在需要交互的组件上使用JavaScript:
---
import StaticHeader from '../components/StaticHeader.astro';
import InteractiveCounter from '../components/InteractiveCounter.jsx';
import StaticFooter from '../components/StaticFooter.astro';
---
<StaticHeader />
<!-- 只有这个组件会发送JavaScript到浏览器 -->
<InteractiveCounter client:visible />
<StaticFooter />
Astro提供多种客户端加载策略,应根据组件特性选择合适的指令:
client:load
- 立即加载组件JSclient:visible
- 组件进入视口时加载client:idle
- 浏览器空闲时加载client:media
- 满足媒体查询条件时加载client:only
- 跳过服务端渲染<!-- 关键交互组件 -->
<LoginForm client:load />
<!-- 页面滚动后才需要交互的组件 -->
<CommentSection client:visible />
<!-- 非关键交互组件 -->
<ThemeToggle client:idle />
<!-- 仅在桌面设备上交互的组件 -->
<ComplexDashboard client:media="(min-width: 1024px)" />
使用Astro内置的图片优化功能:
---
import { Image } from 'astro:assets';
import myImage from '../assets/my-image.jpg';
---
<!-- 自动优化图片 -->
<Image
src={myImage}
alt="优化的图片"
width={800}
height={600}
format="webp"
quality={80}
/>
使用<link>
标签预加载关键资源:
---
// Layout.astro
---
<head>
<!-- 预加载关键CSS -->
<link rel="preload" href="/styles/critical.css" as="style">
<!-- 预加载自定义字体 -->
<link rel="preload" href="/fonts/custom-font.woff2" as="font" type="font/woff2" crossorigin>
<!-- 预加载首屏图片 -->
<link rel="preload" href="/images/hero-image.webp" as="image">
</head>
Astro 2.0+提供了内容集合API,用于类型安全的内容管理:
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
// 定义博客集合及其模式
const blogCollection = defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
date: z.date(),
author: z.string(),
image: z.string().optional(),
tags: z.array(z.string()).default([])
})
});
// 定义项目集合及其模式
const projectsCollection = defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
image: z.string().optional(),
github: z.string().optional(),
demo: z.string().optional(),
technologies: z.array(z.string()).default([])
})
});
export const collections = {
'blog': blogCollection,
'projects': projectsCollection
};
然后在页面中使用:
---
// src/pages/blog.astro
import { getCollection } from 'astro:content';
// 获取所有博客文章
const allBlogPosts = await getCollection('blog');
// 按日期排序
const sortedPosts = allBlogPosts.sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime()
);
---
<ul>
{sortedPosts.map(post => (
<li>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
</li>
))}
</ul>
对于内容丰富的个人网站,充分利用Markdown和MDX:
---
// src/pages/blog/[...slug].astro
import { getCollection } from 'astro:content';
import BlogLayout from '../../layouts/BlogLayout.astro';
// 生成所有博客页面的路径
export async function getStaticPaths() {
const blogEntries = await getCollection('blog');
return blogEntries.map(entry => ({
params: { slug: entry.slug },
props: { entry }
}));
}
// 获取特定博客文章的内容
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<BlogLayout frontmatter={entry.data}>
<Content />
</BlogLayout>
对于需要非技术用户编辑内容的网站,可以集成Decap CMS等无头CMS:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import decapCmsOauth from 'astro-decap-cms-oauth';
export default defineConfig({
integrations: [
// ...其他集成
decapCmsOauth()
]
});
Astro与Tailwind CSS集成良好,可以实现优雅的响应式设计:
---
// 安装@astrojs/tailwind集成后
---
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="bg-white dark:bg-gray-800 p-4 rounded-lg shadow">
<!-- 卡片内容 -->
</div>
<!-- 更多卡片 -->
</div>
---
// ThemeToggle.jsx (React组件)
---
<script>
// 纯JavaScript实现的主题切换
document.addEventListener('DOMContentLoaded', () => {
const themeToggle = document.getElementById('theme-toggle');
// 获取当前主题
const getTheme = () => {
if (localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};
// 应用主题
const applyTheme = (theme) => {
document.documentElement.classList.toggle('dark', theme === 'dark');
localStorage.setItem('theme', theme);
};
// 初始化主题
applyTheme(getTheme());
// 切换主题
themeToggle.addEventListener('click', () => {
const currentTheme = getTheme();
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
applyTheme(newTheme);
});
});
</script>
<button id="theme-toggle" aria-label="切换主题" class="p-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 dark:hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<!-- 月亮图标(白天模式显示) -->
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 hidden dark:block" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<!-- 太阳图标(暗黑模式显示) -->
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</button>
创建一个动态生成的站点地图:
---
// src/pages/sitemap.xml.ts
import { getCollection } from 'astro:content';
import type { APIContext } from 'astro';
export async function GET({ site }: APIContext) {
// 获取所有内容
const blogPosts = await getCollection('blog');
const projects = await getCollection('projects');
// 生成站点地图
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<!-- 主页 -->
<url>
<loc>${site}${import.meta.env.BASE_URL}</loc>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<!-- 博客页面 -->
<url>
<loc>${site}${import.meta.env.BASE_URL}blog</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<!-- 博客文章 -->
${blogPosts.map(post => `
<url>
<loc>${site}${import.meta.env.BASE_URL}blog/${post.slug}</loc>
<lastmod>${post.data.date.toISOString()}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
`).join('')}
<!-- 项目页面 -->
<url>
<loc>${site}${import.meta.env.BASE_URL}projects</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<!-- 个别项目 -->
${projects.map(project => `
<url>
<loc>${site}${import.meta.env.BASE_URL}projects/${project.slug}</loc>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
`).join('')}
</urlset>`;
return new Response(sitemap, {
headers: {
'Content-Type': 'application/xml'
}
});
}
---
Astro支持多种输出模式,选择最适合你的:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel';
export default defineConfig({
// 静态站点生成(默认)
output: 'static',
// 或服务端渲染
// output: 'server',
// adapter: vercel()
});
帮助使用键盘导航的用户:
---
// src/layouts/Layout.astro
---
<div class="skip-to-content">
<a href="#main-content">跳转到主要内容</a>
</div>
<!-- 网站导航 -->
<main id="main-content">
<slot />
</main>
<style>
.skip-to-content a {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: white;
padding: 8px;
z-index: 100;
transition: top 0.2s;
}
.skip-to-content a:focus {
top: 0;
}
</style>
以上最佳实践将帮助你利用Astro框架的优势构建高性能、易于维护且功能丰富的个人网站。通过Astro的"按需注入JavaScript"的理念,你可以在不牺牲交互性的前提下获得静态站点的性能优势。
随着你的Astro开发经验增长,你会发现更多适合自己项目的优化技巧。持续学习,不断实验,与社区分享你的发现,将帮助你成为更出色的Astro开发者。