Astro
Astro 是一个现代的静态站点生成器(SSG),它允许你使用多种前端框架(如React、Vue、Svelte等)构建网站,并且能够输出干净、轻量级的HTML文件,不含有客户端的 JavaScript。Astro 旨在为构建高性能网站提供最佳的开发体验和最优的加载性能。

查看更多相关内容
什么是 Astro 框架,它的核心特性和工作原理是什么?Astro 是一个现代化的静态站点生成器,它的核心理念是"零 JavaScript 默认"。这意味着 Astro 默认只输出纯 HTML,不会向浏览器发送任何客户端 JavaScript,除非你明确要求。
**核心特性:**
1. **岛屿架构(Islands Architecture)**:这是 Astro 的核心概念。页面上的每个组件都是一个"岛屿",默认情况下是静态的 HTML。只有当你明确使用 `client:*` 指令时,组件才会变成交互式的 JavaScript 岛屿。
2. **多框架支持**:Astro 允许你在同一个项目中混合使用 React、Vue、Svelte、Preact、SolidJS 等前端框架。你可以在一个 Astro 组件中使用 React,在另一个组件中使用 Vue。
3. **性能优化**:通过默认输出纯 HTML,Astro 网站具有极快的加载速度和优秀的 SEO 表现。
4. **内容优先**:Astro 特别适合内容驱动的网站,如博客、文档、营销网站等。
**工作原理:**
```astro
---
// 服务端代码区域(只在构建时运行)
const data = await fetch('https://api.example.com/data');
const posts = await data.json();
---
<!-- 客户端代码区域(输出到 HTML) -->
<h1>我的博客</h1>
<ul>
{posts.map(post => (
<li>{post.title}</li>
))}
</ul>
```
Astro 使用 `---` 分隔符将组件分为服务端代码和客户端模板。服务端代码在构建时执行,可以执行异步操作、获取数据等;客户端模板则被编译成 HTML。
**与其他框架的区别:**
- 与 Next.js 不同,Astro 默认不进行客户端水合(hydration),除非你明确要求
- 与传统的静态站点生成器(如 Jekyll、Hugo)不同,Astro 支持现代前端框架和组件化开发
- Astro 的构建输出是纯 HTML,而不是包含大量 JavaScript 的应用
这种设计使得 Astro 成为构建高性能、内容驱动网站的理想选择。
前端 · 2月21日 16:15
Astro 支持哪些渲染模式?静态生成(SSG)和服务端渲染(SSR)有什么区别?Astro 支持多种渲染模式,可以根据不同的使用场景选择最适合的渲染策略。理解这些渲染模式对于构建高性能的 Astro 应用至关重要。
**主要渲染模式:**
1. **静态生成(Static Generation - SSG)**:
- 默认模式
- 在构建时生成 HTML
- 适合内容不经常变化的页面
- 性能最佳,SEO 友好
```astro
// src/pages/index.astro
---
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
---
<h1>博客文章</h1>
{posts.map(post => (
<article>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
```
2. **服务端渲染(Server-Side Rendering - SSR)**:
- 每次请求时动态生成 HTML
- 适合需要实时数据的页面
- 需要配置适配器(Adapter)
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/server';
export default defineConfig({
output: 'server',
adapter: vercel(),
});
```
```astro
// src/pages/dashboard.astro
---
// 每次请求都会执行
const user = await getUserFromSession(Astro.request);
const data = await fetchUserData(user.id);
---
<h1>欢迎, {user.name}</h1>
<p>你的数据: {data}</p>
```
3. **混合渲染(Hybrid Rendering)**:
- 结合静态和动态渲染
- 可以为不同页面指定不同的渲染模式
- 提供最大的灵活性
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/server';
export default defineConfig({
output: 'hybrid',
adapter: vercel(),
});
```
```astro
// src/pages/static.astro
---
// 静态页面
export const prerender = true;
---
<h1>静态页面</h1>
```
```astro
// src/pages/dynamic.astro
---
// 动态页面
export const prerender = false;
---
<h1>动态页面</h1>
```
4. **客户端渲染(Client-Side Rendering)**:
- 使用 `client:only` 指令
- 完全在浏览器中渲染
- 适合需要大量客户端交互的组件
```astro
---
import InteractiveChart from '../components/InteractiveChart.jsx';
---
<InteractiveChart client:only="react" />
```
**预渲染(Prerender)配置:**
```astro
---
// 控制单个页面的预渲染行为
export const prerender = true; // 静态生成
export const prerender = false; // 服务端渲染
export const prerender = 'auto'; // 自动判断
---
```
**动态路由:**
```astro
---
// src/pages/blog/[id].astro
export async function getStaticPaths() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return posts.map(post => ({
params: { id: post.id },
props: { post },
}));
}
const { id } = Astro.params;
const { post } = Astro.props;
---
<h1>{post.title}</h1>
<p>{post.content}</p>
```
**适配器(Adapters):**
适配器将 Astro 项目部署到不同的平台:
```bash
# Vercel
npx astro add vercel
# Netlify
npx astro add netlify
# Cloudflare
npx astro add cloudflare
# Node.js
npx astro add node
```
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/server';
import netlify from '@astrojs/netlify/edge';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server',
adapter: vercel(), // 或 netlify(), cloudflare()
});
```
**选择渲染模式的指南:**
1. **使用静态生成(SSG)**:
- 博客文章
- 文档页面
- 营销页面
- 内容不经常变化的页面
2. **使用服务端渲染(SSR)**:
- 用户仪表板
- 需要认证的页面
- 实时数据展示
- 个性化内容
3. **使用混合渲染**:
- 大部分内容静态,部分动态
- 需要灵活性的大型应用
- 既有公开页面又有私有页面的应用
4. **使用客户端渲染**:
- 复杂的交互式组件
- 需要大量客户端状态管理
- 不需要 SEO 的功能
**性能优化技巧:**
1. 默认使用静态生成
2. 只为需要动态内容的页面启用 SSR
3. 使用 `client:*` 指令控制水合时机
4. 利用混合渲染平衡性能和功能
5. 合理使用适配器优化部署
理解 Astro 的渲染模式可以帮助你构建既快速又灵活的 Web 应用。
前端 · 2月21日 16:15
如何在 Astro 项目中集成和使用多个前端框架(React、Vue、Svelte)?Astro 支持在同一个项目中混合使用多个前端框架,这是其最强大的特性之一。你可以在一个 Astro 项目中同时使用 React、Vue、Svelte、Preact、SolidJS 等框架。
**集成步骤:**
1. **安装框架集成包**:
```bash
# 安装 React 集成
npx astro add react
# 安装 Vue 集成
npx astro add vue
# 安装 Svelte 集成
npx astro add svelte
```
2. **配置 astro.config.mjs**:
```javascript
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import vue from '@astrojs/vue';
import svelte from '@astrojs/svelte';
export default defineConfig({
integrations: [react(), vue(), svelte()],
});
```
**使用不同框架的组件:**
```astro
---
import ReactComponent from '../components/ReactComponent.jsx';
import VueComponent from '../components/VueComponent.vue';
import SvelteComponent from '../components/SvelteComponent.svelte';
import AstroComponent from '../components/AstroComponent.astro';
---
<div>
<AstroComponent />
<ReactComponent />
<VueComponent />
<SvelteComponent />
</div>
```
**框架特定配置:**
**React 配置:**
```javascript
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
export default defineConfig({
integrations: [
react({
// React 特定配置
experimentalDirectRender: false,
}),
],
});
```
**Vue 配置:**
```javascript
import { defineConfig } from 'astro/config';
import vue from '@astrojs/vue';
export default defineConfig({
integrations: [
vue({
// Vue 特定配置
template: {
compilerOptions: {
isCustomElement: (tag) => tag.includes('-'),
},
},
}),
],
});
```
**Svelte 配置:**
```javascript
import { defineConfig } from 'astro/config';
import svelte from '@astrojs/svelte';
export default defineConfig({
integrations: [
svelte({
// Svelte 特定配置
preprocess: [],
}),
],
});
```
**混合框架的最佳实践:**
1. **按需选择框架**:
- React:适合复杂的交互式 UI
- Vue:适合渐进式增强和快速开发
- Svelte:适合高性能组件
- Astro:适合静态内容和布局
2. **保持一致性**:
- 在同一功能模块中使用相同框架
- 避免在单个组件中混合多个框架
- 建立清晰的组件命名约定
3. **性能优化**:
- 只为需要交互的组件使用框架
- 使用 `client:*` 指令控制水合时机
- 考虑使用轻量级框架(如 Preact)替代 React
**示例:博客页面**
```astro
---
import Layout from '../layouts/Layout.astro';
import Header from '../components/Header.astro';
import PostList from '../components/PostList.astro';
import CommentSection from '../components/CommentSection.jsx'; // React
import LikeButton from '../components/LikeButton.vue'; // Vue
import ShareWidget from '../components/ShareWidget.svelte'; // Svelte
---
<Layout title="博客文章">
<Header />
<main>
<PostList />
<CommentSection client:visible />
<LikeButton client:idle />
<ShareWidget client:idle />
</main>
</Layout>
```
**框架间数据共享:**
虽然不同框架的组件不能直接共享状态,但可以通过以下方式共享数据:
1. **通过 Props 传递**:
```astro
---
const data = await fetch('/api/data').then(r => r.json());
---
<ReactComponent data={data} />
<VueComponent :data={data} />
```
2. **使用全局状态管理**:
- 通过 API 获取数据
- 使用 localStorage 或 sessionStorage
- 使用自定义事件系统
**注意事项:**
1. 每个框架的组件需要使用对应的文件扩展名(`.jsx`、`.vue`、`.svelte`)
2. 不同框架的组件不能直接相互引用
3. 确保所有框架的依赖都已正确安装
4. 某些框架可能需要额外的配置(如 Vue 的插件、React 的 Context)
Astro 的多框架支持让你能够根据项目需求选择最合适的工具,同时保持高性能和优秀的开发体验。
前端 · 2月21日 16:15
Astro 的图片优化功能是如何工作的?如何使用 `<Image>` 组件优化图片加载?Astro 提供了强大的图片优化功能,可以自动处理图片的响应式、格式转换、压缩等任务,显著提升网站性能。
**核心功能:**
1. **自动响应式图片**:自动生成多个尺寸的图片
2. **格式转换**:自动转换为现代图片格式(WebP、AVIF)
3. **懒加载**:自动实现图片懒加载
4. **压缩优化**:自动压缩图片减少文件大小
**基本用法:**
```astro
---
import { Image } from 'astro:assets';
import myImage from '../assets/my-image.jpg';
---
<!-- 基本使用 -->
<Image src={myImage} alt="描述文字" />
<!-- 指定宽度和高度 -->
<Image
src={myImage}
alt="描述文字"
width={800}
height={600}
/>
<!-- 指定格式 -->
<Image
src={myImage}
alt="描述文字"
format="webp"
/>
<!-- 指定质量 -->
<Image
src={myImage}
alt="描述文字"
quality={80}
/>
```
**高级配置:**
```astro
---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---
<Image
src={heroImage}
alt="Hero Image"
widths={[400, 800, 1200]}
sizes="(max-width: 768px) 100vw, 50vw"
formats={['avif', 'webp', 'jpeg']}
loading="lazy"
decoding="async"
priority={false}
/>
```
**配置选项说明:**
- `widths`:生成多个宽度的图片版本
- `sizes`:指定不同屏幕宽度下的图片显示尺寸
- `formats`:指定输出格式(按优先级)
- `loading`:加载策略("eager" 或 "lazy")
- `decoding`:解码策略("sync" 或 "async")
- `priority`:是否为优先加载的图片
**远程图片:**
```astro
---
import { Image } from 'astro:assets';
import { getImage } from 'astro:assets';
// 处理远程图片
const remoteImage = await getImage({
src: 'https://example.com/image.jpg',
alt: 'Remote Image',
width: 800,
height: 600,
format: 'webp',
});
---
<img {...remoteImage.attributes} />
```
**配置远程图片域名:**
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
export default defineConfig({
integrations: [react()],
image: {
// 允许的远程图片域名
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
},
{
protocol: 'https',
hostname: '**.cdn.example.com',
},
],
},
});
```
**图片服务(Image Service):**
Astro 支持多种图片服务:
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import sharp from 'astro/assets/services/sharp';
import imagetools from 'astro/assets/services/imagetools';
export default defineConfig({
image: {
// 使用 Sharp(默认,性能最佳)
service: sharp,
// 或使用 ImageTools(更多功能)
// service: imagetools,
},
});
```
**背景图片:**
```astro
---
import { Image } from 'astro:assets';
import backgroundImage from '../assets/bg.jpg';
const { src: bgSrc } = await getImage({
src: backgroundImage,
format: 'webp',
width: 1920,
});
---
<div
style={`background-image: url('${bgSrc}');`}
class="hero-section"
>
<h1>Hero Section</h1>
</div>
```
**图片画廊示例:**
```astro
---
import { Image } from 'astro:assets';
import { getCollection } from 'astro:content';
const gallery = await getCollection('gallery');
---
<div class="gallery">
{gallery.map(item => (
<figure>
<Image
src={item.data.image}
alt={item.data.title}
widths={[300, 600, 900]}
sizes="(max-width: 600px) 100vw, 50vw"
loading="lazy"
/>
<figcaption>{item.data.title}</figcaption>
</figure>
))}
</div>
<style>
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
figure {
margin: 0;
}
img {
width: 100%;
height: auto;
}
</style>
```
**性能优化技巧:**
1. **使用正确的格式优先级**:
```astro
<Image
src={image}
formats={['avif', 'webp', 'jpeg']}
/>
```
2. **合理的尺寸设置**:
```astro
<Image
src={image}
widths={[400, 800, 1200, 1600]}
sizes="(max-width: 768px) 100vw, 50vw"
/>
```
3. **优先加载关键图片**:
```astro
<Image
src={heroImage}
loading="eager"
priority={true}
/>
```
4. **懒加载非关键图片**:
```astro
<Image
src={image}
loading="lazy"
decoding="async"
/>
```
5. **控制图片质量**:
```astro
<Image
src={image}
quality={75}
/>
```
**与内容集合集成:**
```markdown
---
title: "我的文章"
image: ./hero.jpg
---
文章内容...
```
```astro
---
import { Image } from 'astro:assets';
import { getEntry } from 'astro:content';
const post = await getEntry('blog', 'my-post');
const { image } = post.data;
---
<Image
src={image}
alt={post.data.title}
widths={[800, 1200, 1600]}
/>
```
**最佳实践:**
1. 默认使用 `<Image>` 组件而不是 `<img>` 标签
2. 为所有图片提供有意义的 alt 文本
3. 根据图片用途设置合适的加载策略
4. 使用 `widths` 和 `sizes` 实现真正的响应式
5. 优先使用现代图片格式(AVIF、WebP)
6. 为远程图片配置域名白名单
7. 在构建时处理图片,避免运行时开销
Astro 的图片优化功能可以显著提升网站性能,改善用户体验,同时保持简单的开发体验。
前端 · 2月21日 16:15
Astro 有哪些性能优化策略?如何构建超快速的 Astro 网站?Astro 提供了多种性能优化策略和技术,帮助开发者构建超快速的网站。了解这些优化技巧对于构建高性能的 Astro 应用至关重要。
**核心性能优化策略:**
1. **零 JavaScript 默认**:
- Astro 默认只输出纯 HTML
- 只在需要时才加载 JavaScript
- 显著减少初始加载时间
2. **岛屿架构优化**:
- 只为交互式组件添加 `client:*` 指令
- 使用合适的 `client:*` 指令类型
- 延迟非关键交互的水合
**代码分割和懒加载:**
```astro
---
// 延迟加载组件
import { lazy } from 'astro';
const HeavyComponent = lazy(() => import('../components/HeavyComponent.jsx'));
---
<HeavyComponent client:visible />
```
**图片优化:**
```astro
---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---
<!-- 使用正确的格式和尺寸 -->
<Image
src={heroImage}
alt="Hero"
widths={[400, 800, 1200, 1600]}
sizes="(max-width: 768px) 100vw, 50vw"
formats={['avif', 'webp', 'jpeg']}
loading="eager"
priority={true}
/>
<!-- 懒加载非关键图片 -->
<Image
src={image}
alt="Gallery Image"
loading="lazy"
decoding="async"
/>
```
**CSS 优化:**
```astro
---
// 使用作用域样式
---
<style>
/* 作用域样式,不会影响其他组件 */
.container {
max-width: 1200px;
margin: 0 auto;
}
</style>
<style is:global>
/* 全局样式,谨慎使用 */
body {
font-family: system-ui, sans-serif;
}
</style>
```
**预加载关键资源:**
```astro
---
// src/layouts/Layout.astro
---
<html>
<head>
<!-- 预加载关键 CSS -->
<link rel="preload" href="/styles/critical.css" as="style" />
<!-- 预加载字体 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin />
<!-- 预连接到外部域名 -->
<link rel="preconnect" href="https://api.example.com" />
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="https://cdn.example.com" />
</head>
<body>
<slot />
</body>
</html>
```
**数据获取优化:**
```astro
---
// src/pages/blog/[slug].astro
import { getEntry } from 'astro:content';
// 并行获取数据
const [post, relatedPosts, comments] = await Promise.all([
getEntry('blog', Astro.params.slug),
fetchRelatedPosts(Astro.params.slug),
fetchComments(Astro.params.slug),
]);
// 使用缓存
const cachedData = await cache.get(`post:${Astro.params.slug}`);
if (cachedData) {
return cachedData;
}
const data = await fetchData();
await cache.set(`post:${Astro.params.slug}`, data, { ttl: 3600 });
---
<h1>{post.data.title}</h1>
<Content />
```
**构建优化:**
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
build: {
// 优化构建输出
inlineStylesheets: 'auto',
},
vite: {
build: {
// 代码分割
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'date-fns'],
},
},
},
},
},
});
```
**服务端渲染优化:**
```typescript
// src/middleware.ts
export const onRequest = async (context, next) => {
// 添加缓存头
const response = await next();
// 为静态资源添加长期缓存
if (context.url.pathname.match(/\.(js|css|png|jpg|jpeg|gif|webp|svg)$/)) {
response.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
}
// 为 API 响应添加短期缓存
if (context.url.pathname.startsWith('/api/')) {
response.headers.set('Cache-Control', 'public, max-age=60');
}
return response;
};
```
**使用适配器优化部署:**
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/server';
export default defineConfig({
output: 'server',
adapter: vercel({
// Vercel 特定优化
imageService: true,
edgeMiddleware: true,
}),
});
```
**性能监控和分析:**
```typescript
// src/lib/performance.ts
export function measurePerformance(name: string, fn: () => Promise<void>) {
return async () => {
const start = performance.now();
await fn();
const duration = performance.now() - start;
console.log(`${name} took ${duration.toFixed(2)}ms`);
};
}
// 使用
export async function GET(context) {
await measurePerformance('data-fetch', async () => {
const data = await fetchData();
return new Response(JSON.stringify(data));
});
}
```
**Web Vitals 优化:**
```astro
---
// src/layouts/Layout.astro
---
<html>
<head>
<script>
// 监控 Core Web Vitals
import { onCLS, onFID, onFCP, onLCP, onTTFB } from 'web-vitals';
onCLS(console.log);
onFID(console.log);
onFCP(console.log);
onLCP(console.log);
onTTFB(console.log);
</script>
</head>
<body>
<slot />
</body>
</html>
```
**减少第三方脚本:**
```astro
---
// 延迟加载分析脚本
---
<script>
// 只在生产环境加载
if (import.meta.env.PROD) {
window.addEventListener('load', () => {
const script = document.createElement('script');
script.src = 'https://analytics.example.com/script.js';
script.async = true;
document.head.appendChild(script);
});
}
</script>
```
**使用 Service Worker 缓存:**
```typescript
// public/sw.js
const CACHE_NAME = 'astro-v1';
const urlsToCache = ['/', '/styles/main.css'];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => response || fetch(event.request))
);
});
```
```astro
---
// 注册 Service Worker
---
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
}
</script>
```
**性能优化清单:**
1. **构建时优化**:
- 启用代码分割
- 压缩和优化资源
- 使用 Tree Shaking
- 优化图片和字体
2. **运行时优化**:
- 使用合适的 `client:*` 指令
- 实现懒加载
- 优化数据获取
- 使用缓存策略
3. **网络优化**:
- 使用 CDN
- 启用压缩(gzip、brotli)
- 优化 HTTP 请求
- 使用 HTTP/2 或 HTTP/3
4. **渲染优化**:
- 减少重绘和回流
- 使用 CSS 动画而非 JavaScript
- 优化 DOM 结构
- 避免强制同步布局
**性能测试工具:**
- Lighthouse
- WebPageTest
- Chrome DevTools Performance
- Astro 内置的构建分析
Astro 的性能优化策略可以帮助你构建超快速的网站,提供优秀的用户体验和 SEO 表现。
前端 · 2月21日 16:15
Astro 的中间件(Middleware)是如何工作的?有哪些常见的使用场景?Astro 的中间件(Middleware)是一个强大的功能,允许你在请求到达页面之前拦截和处理请求,实现认证、重定向、修改响应等功能。
**核心概念:**
中间件在服务器端运行,可以访问请求对象,并在请求处理链中执行自定义逻辑。
**创建中间件:**
```typescript
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
import type { MiddlewareResponseHandler } from 'astro';
export const onRequest: MiddlewareResponseHandler = async (context, next) => {
// 在这里处理请求
// 调用 next() 继续处理链
const response = await next();
// 可以修改响应
response.headers.set('X-Custom-Header', 'Custom Value');
return response;
};
```
**中间件上下文:**
```typescript
// src/middleware.ts
export const onRequest = async (context, next) => {
// context 包含以下属性:
console.log(context.request); // Request 对象
console.log(context.url); // URL 对象
console.log(context.cookies); // Cookies
console.log(context.locals); // 本地数据存储
console.log(context.site); // 站点配置
const response = await next();
return response;
};
```
**使用场景:**
1. **认证和授权**:
```typescript
// src/middleware.ts
export const onRequest = async (context, next) => {
const protectedRoutes = ['/dashboard', '/settings', '/profile'];
const currentPath = context.url.pathname;
if (protectedRoutes.some(route => currentPath.startsWith(route))) {
const token = context.cookies.get('auth-token');
if (!token) {
return context.redirect('/login');
}
// 验证 token
const user = await verifyToken(token.value);
if (!user) {
return context.redirect('/login');
}
// 将用户信息存储在 locals 中
context.locals.user = user;
}
return next();
};
```
2. **重定向管理**:
```typescript
// src/middleware.ts
export const onRequest = async (context, next) => {
const redirects = {
'/old-url': '/new-url',
'/old-page': '/new-page',
};
const currentPath = context.url.pathname;
if (redirects[currentPath]) {
return context.redirect(redirects[currentPath], 301);
}
return next();
};
```
3. **国际化(i18n)**:
```typescript
// src/middleware.ts
export const onRequest = async (context, next) => {
const supportedLocales = ['en', 'zh', 'ja'];
const defaultLocale = 'en';
// 从 URL 获取语言
const pathSegments = context.url.pathname.split('/');
const locale = pathSegments[1];
// 检查是否是支持的语言
if (supportedLocales.includes(locale)) {
context.locals.locale = locale;
return next();
}
// 从 Accept-Language 头获取语言
const acceptLanguage = context.request.headers.get('accept-language');
const browserLocale = acceptLanguage?.split(',')[0].split('-')[0] || defaultLocale;
const targetLocale = supportedLocales.includes(browserLocale) ? browserLocale : defaultLocale;
return context.redirect(`/${targetLocale}${context.url.pathname}`);
};
```
4. **日志记录**:
```typescript
// src/middleware.ts
export const onRequest = async (context, next) => {
const startTime = Date.now();
const response = await next();
const duration = Date.now() - startTime;
console.log(`${context.request.method} ${context.url.pathname} - ${duration}ms`);
return response;
};
```
5. **CORS 处理**:
```typescript
// src/middleware.ts
export const onRequest = async (context, next) => {
const response = await next();
response.headers.set('Access-Control-Allow-Origin', '*');
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
return response;
};
```
**条件中间件:**
```typescript
// src/middleware.ts
export const onRequest = async (context, next) => {
// 只对特定路径应用中间件
if (context.url.pathname.startsWith('/api/')) {
console.log('API request:', context.url.pathname);
}
return next();
};
```
**中间件优先级:**
Astro 支持在不同层级定义中间件:
1. **全局中间件**:`src/middleware.ts` - 应用于所有请求
2. **路由中间件**:在特定路由目录中定义 - 应用于该路由及其子路由
```typescript
// src/middleware.ts (全局)
export const onRequest = async (context, next) => {
console.log('Global middleware');
return next();
};
```
```typescript
// src/pages/dashboard/middleware.ts (路由特定)
export const onRequest = async (context, next) => {
console.log('Dashboard middleware');
return next();
};
```
**使用 locals 传递数据:**
```typescript
// src/middleware.ts
export const onRequest = async (context, next) => {
// 在中间件中设置数据
context.locals.user = await getUser(context);
context.locals.theme = 'dark';
const response = await next();
return response;
};
```
```astro
---
// 在页面中使用 locals
const user = Astro.locals.user;
const theme = Astro.locals.theme;
---
<h1>欢迎, {user?.name}</h1>
<p>当前主题: {theme}</p>
```
**错误处理:**
```typescript
// src/middleware.ts
export const onRequest = async (context, next) => {
try {
return await next();
} catch (error) {
console.error('Middleware error:', error);
return new Response('Internal Server Error', {
status: 500,
headers: { 'Content-Type': 'text/plain' },
});
}
};
```
**性能优化:**
```typescript
// src/middleware.ts
export const onRequest = async (context, next) => {
// 缓存频繁访问的数据
const cacheKey = `user:${context.cookies.get('user-id')?.value}`;
const cachedUser = await cache.get(cacheKey);
if (cachedUser) {
context.locals.user = cachedUser;
return next();
}
const user = await fetchUser(context.cookies.get('user-id')?.value);
await cache.set(cacheKey, user, { ttl: 3600 });
context.locals.user = user;
return next();
};
```
**最佳实践:**
1. 保持中间件简洁高效
2. 避免在中间件中执行耗时操作
3. 使用缓存优化性能
4. 合理使用 locals 传递数据
5. 实现适当的错误处理
6. 考虑中间件的执行顺序
7. 只在必要时使用重定向
Astro 的中间件功能为构建复杂的应用提供了强大的请求处理能力,特别适合需要认证、授权、国际化等功能的项目。
前端 · 2月21日 16:15
Astro 的岛屿架构(Islands Architecture)是如何工作的?client 指令有哪些类型?Astro 的岛屿架构(Islands Architecture)是一种创新的 Web 开发模式,它解决了传统单页应用(SPA)的性能问题。
**核心概念:**
岛屿架构将页面视为静态 HTML 的海洋,其中散布着交互式的 JavaScript"岛屿"。默认情况下,Astro 组件是静态的,只输出 HTML。只有当你明确使用 `client:*` 指令时,组件才会变成交互式的岛屿。
**client 指令类型:**
1. **`client:load`**:页面加载时立即水合组件
```astro
<InteractiveComponent client:load />
```
2. **`client:idle`**:浏览器空闲时水合组件(推荐用于非关键交互)
```astro
<InteractiveComponent client:idle />
```
3. **`client:visible`**:组件进入视口时才水合(适合滚动加载)
```astro
<LazyComponent client:visible />
```
4. **`client:media`**:匹配特定媒体查询时才水合
```astro
<ResponsiveComponent client:media="(max-width: 768px)" />
```
5. **`client:only`**:只在客户端渲染,不进行服务端渲染
```astro
<ClientOnlyComponent client:only="react" />
```
**性能优势:**
1. **减少 JavaScript 包体积**:只有交互式组件的 JavaScript 才会被发送到浏览器
2. **更快的首次内容绘制(FCP)**:静态 HTML 可以立即显示
3. **更好的 SEO**:搜索引擎可以直接索引静态内容
4. **渐进式增强**:核心内容立即可用,交互功能按需加载
**实际应用示例:**
```astro
---
import Header from '../components/Header.astro';
import BlogPost from '../components/BlogPost.astro';
import CommentSection from '../components/CommentSection.jsx';
import NewsletterForm from '../components/NewsletterForm.svelte';
---
<Header />
<main>
<BlogPost />
<CommentSection client:visible />
<NewsletterForm client:idle />
</main>
```
在这个例子中:
- Header 和 BlogPost 是静态的(无 JavaScript)
- CommentSection 在滚动到视口时才加载 JavaScript
- NewsletterForm 在浏览器空闲时才加载
**与传统 SPA 的对比:**
传统 SPA 需要下载整个应用的 JavaScript 并进行水合,而岛屿架构只下载和执行必要的交互部分。这使得 Astro 网站通常比同等的 SPA 快 2-3 倍。
**最佳实践:**
1. 默认不使用 `client:*` 指令
2. 只为真正需要交互的组件添加 `client:*`
3. 根据交互的紧急程度选择合适的指令
4. 使用 `client:visible` 处理滚动加载的组件
5. 使用 `client:idle` 处理非关键交互功能
岛屿架构让开发者能够构建既快速又具有丰富交互体验的网站。
前端 · 2月21日 16:15
如何在 Astro 中实现国际化(i18n)?如何配置多语言网站?Astro 的国际化(i18n)功能让开发者能够轻松构建多语言网站。了解如何配置和使用 Astro 的 i18n 功能对于面向全球用户的项目至关重要。
**基本配置:**
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import { i18n } from 'astro-i18next';
export default defineConfig({
integrations: [
i18n({
defaultLocale: 'en',
locales: ['en', 'zh', 'ja', 'es'],
fallbackLocale: 'en',
routing: {
prefixDefaultLocale: false,
},
}),
],
});
```
**使用 astro-i18next 集成:**
```bash
npm install astro-i18next i18next
```
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import i18next from 'astro-i18next';
export default defineConfig({
integrations: [
i18next({
defaultLocale: 'en',
locales: ['en', 'zh'],
routingStrategy: 'prefix',
}),
],
});
```
**翻译文件结构:**
```
src/
├── i18n/
│ ├── en/
│ │ ├── common.json
│ │ └── home.json
│ ├── zh/
│ │ ├── common.json
│ │ └── home.json
│ └── ja/
│ ├── common.json
│ └── home.json
```
**翻译文件示例:**
```json
// src/i18n/en/common.json
{
"nav": {
"home": "Home",
"about": "About",
"contact": "Contact"
},
"buttons": {
"submit": "Submit",
"cancel": "Cancel"
}
}
```
```json
// src/i18n/zh/common.json
{
"nav": {
"home": "首页",
"about": "关于",
"contact": "联系"
},
"buttons": {
"submit": "提交",
"cancel": "取消"
}
}
```
**在组件中使用翻译:**
```astro
---
import { useTranslation } from 'astro-i18next';
const { t } = useTranslation();
---
<nav>
<a href="/">{t('nav.home')}</a>
<a href="/about">{t('nav.about')}</a>
<a href="/contact">{t('nav.contact')}</a>
</nav>
<button>{t('buttons.submit')}</button>
```
**语言切换器:**
```astro
---
import { useTranslation, useLocale } from 'astro-i18next';
const { t } = useTranslation();
const locale = useLocale();
const locales = ['en', 'zh', 'ja', 'es'];
---
<div class="language-switcher">
{locales.map(loc => (
<a
href={`/${loc === 'en' ? '' : loc}`}
class={locale === loc ? 'active' : ''}
>
{loc.toUpperCase()}
</a>
))}
</div>
<style>
.language-switcher {
display: flex;
gap: 0.5rem;
}
.active {
font-weight: bold;
text-decoration: underline;
}
</style>
```
**动态路由国际化:**
```astro
---
// src/pages/[lang]/blog/[slug].astro
import { useTranslation, useLocale } from 'astro-i18next';
import { getCollection } from 'astro:content';
const { t } = useTranslation();
const locale = useLocale();
const { slug } = Astro.params;
const post = await getEntry('blog', slug);
const { Content } = await post.render();
---
<h1>{post.data.title}</h1>
<Content />
```
**内容集合国际化:**
```markdown
---
# src/content/en/blog/my-post.md
title: "My English Post"
publishDate: 2024-01-15
---
This is the English version of the post.
```
```markdown
---
# src/content/zh/blog/my-post.md
title: "我的中文文章"
publishDate: 2024-01-15
---
这是文章的中文版本。
```
```astro
---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';
import { useLocale } from 'astro-i18next';
const locale = useLocale();
const { slug } = Astro.params;
const post = await getEntry(`blog-${locale}`, slug);
const { Content } = await post.render();
---
<h1>{post.data.title}</h1>
<Content />
```
**日期和数字格式化:**
```astro
---
import { useTranslation } from 'astro-i18next';
const { t, i18n } = useTranslation();
const date = new Date();
const number = 1234567.89;
---
<p>Date: {date.toLocaleDateString(i18n.language)}</p>
<p>Number: {number.toLocaleString(i18n.language)}</p>
<p>Currency: {number.toLocaleString(i18n.language, { style: 'currency', currency: 'USD' })}</p>
```
**SEO 优化:**
```astro
---
import { useTranslation, useLocale } from 'astro-i18next';
const { t } = useTranslation();
const locale = useLocale();
---
<html lang={locale}>
<head>
<meta charset="UTF-8" />
<title>{t('meta.title')}</title>
<meta name="description" content={t('meta.description')} />
<!-- 语言切换链接 -->
<link rel="alternate" hreflang="en" href="/en" />
<link rel="alternate" hreflang="zh" href="/zh" />
<link rel="alternate" hreflang="ja" href="/ja" />
<link rel="alternate" hreflang="x-default" href="/" />
</head>
<body>
<slot />
</body>
</html>
```
**服务端渲染(SSR)国际化:**
```typescript
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware((context, next) => {
const url = new URL(context.request.url);
const pathSegments = url.pathname.split('/').filter(Boolean);
// 检测语言
const detectedLocale = detectLocale(context.request);
// 如果没有语言前缀,重定向到检测到的语言
if (!pathSegments[0] || !['en', 'zh', 'ja'].includes(pathSegments[0])) {
return context.redirect(`/${detectedLocale}${url.pathname}`);
}
// 存储语言到 locals
context.locals.locale = pathSegments[0];
return next();
});
function detectLocale(request: Request): string {
const acceptLanguage = request.headers.get('accept-language');
const browserLocale = acceptLanguage?.split(',')[0].split('-')[0] || 'en';
const supportedLocales = ['en', 'zh', 'ja'];
return supportedLocales.includes(browserLocale) ? browserLocale : 'en';
}
```
**RTL(从右到左)语言支持:**
```astro
---
import { useTranslation, useLocale } from 'astro-i18next';
const { t } = useTranslation();
const locale = useLocale();
const rtlLocales = ['ar', 'he', 'fa'];
const isRTL = rtlLocales.includes(locale);
---
<html lang={locale} dir={isRTL ? 'rtl' : 'ltr'}>
<head>
<style>
body {
direction: {isRTL ? 'rtl' : 'ltr'};
}
</style>
</head>
<body>
<slot />
</body>
</html>
```
**最佳实践:**
1. **翻译管理**:
- 使用专业的翻译工具(如 Crowdin、Locize)
- 保持翻译文件结构一致
- 定期审查和更新翻译
2. **性能优化**:
- 按需加载翻译文件
- 使用缓存减少重复请求
- 预加载常用语言
3. **用户体验**:
- 提供清晰的语言切换器
- 记住用户语言偏好
- 处理缺失的翻译
4. **SEO 考虑**:
- 为每种语言设置正确的 hreflang
- 使用适当的语言标签
- 避免重复内容问题
5. **开发流程**:
- 使用类型安全的翻译键
- 自动化翻译检查
- 集成到 CI/CD 流程
Astro 的国际化功能提供了灵活的多语言支持,帮助开发者构建面向全球用户的应用。
前端 · 2月21日 16:14
如何部署 Astro 应用到不同的平台(Vercel、Netlify、Node.js)?有哪些部署最佳实践?Astro 的部署方式取决于你选择的渲染模式(SSG、SSR 或混合模式)。了解不同的部署选项和最佳实践对于成功发布 Astro 项目至关重要。
**静态部署(SSG):**
对于纯静态站点,可以将构建输出部署到任何静态托管服务。
1. **Vercel 部署**:
```bash
# 安装 Vercel CLI
npm i -g vercel
# 部署
vercel
```
```javascript
// vercel.json
{
"buildCommand": "astro build",
"outputDirectory": "dist"
}
```
2. **Netlify 部署**:
```bash
# 安装 Netlify CLI
npm i -g netlify-cli
# 部署
netlify deploy --prod
```
```toml
# netlify.toml
[build]
command = "astro build"
publish = "dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
```
3. **GitHub Pages 部署**:
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
site: 'https://username.github.io',
base: '/repository-name',
});
```
```bash
# 构建并部署
npm run build
# 将 dist 目录内容推送到 gh-pages 分支
```
**服务端部署(SSR):**
对于需要服务端渲染的应用,需要使用适配器。
1. **Vercel SSR 部署**:
```bash
npx astro add vercel
```
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/server';
export default defineConfig({
output: 'server',
adapter: vercel(),
});
```
2. **Netlify Edge Functions**:
```bash
npx astro add netlify
```
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify/edge';
export default defineConfig({
output: 'server',
adapter: netlify(),
});
```
3. **Node.js 服务器**:
```bash
npx astro add node
```
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({
mode: 'standalone',
}),
});
```
```bash
# 构建并运行
npm run build
node ./dist/server/entry.mjs
```
4. **Cloudflare Pages**:
```bash
npx astro add cloudflare
```
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server',
adapter: cloudflare(),
});
```
**Docker 部署:**
```dockerfile
# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS runner
WORKDIR /app
COPY --from=builder /app/package*.json ./
RUN npm ci --production
COPY --from=builder /app/dist ./dist
EXPOSE 4321
CMD ["node", "./dist/server/entry.mjs"]
```
```bash
# 构建和运行 Docker 镜像
docker build -t astro-app .
docker run -p 4321:4321 astro-app
```
**环境变量配置:**
```bash
# .env.example
PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgresql://...
SECRET_KEY=your-secret-key
```
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
vite: {
define: {
'import.meta.env.PUBLIC_API_URL': JSON.stringify(process.env.PUBLIC_API_URL),
},
},
});
```
**CI/CD 配置:**
```yaml
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'
```
**性能优化部署:**
1. **启用压缩**:
```javascript
// astro.config.mjs
export default defineConfig({
compressHTML: true,
});
```
2. **配置 CDN**:
```javascript
// astro.config.mjs
export default defineConfig({
build: {
assets: '_astro',
},
});
```
3. **缓存策略**:
```javascript
// src/middleware.ts
export const onRequest = async (context, next) => {
const response = await next();
if (context.url.pathname.startsWith('/_astro/')) {
response.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
}
return response;
};
```
**监控和日志:**
```typescript
// src/lib/monitoring.ts
export function logDeploymentInfo() {
console.log({
version: import.meta.env.PUBLIC_VERSION,
buildTime: new Date().toISOString(),
environment: import.meta.env.MODE,
});
}
// 在入口文件中调用
logDeploymentInfo();
```
**最佳实践:**
1. **选择合适的部署平台**:
- 静态站点:Vercel、Netlify、GitHub Pages
- SSR 应用:Vercel、Netlify Edge、Node.js
- 边缘计算:Cloudflare Workers、Vercel Edge
2. **环境变量管理**:
- 使用 `.env` 文件进行本地开发
- 在部署平台配置生产环境变量
- 区分公开和私有变量
3. **自动化部署**:
- 使用 CI/CD 管道
- 自动运行测试
- 自动部署到生产环境
4. **监控和日志**:
- 设置错误追踪
- 监控性能指标
- 记录部署信息
5. **回滚策略**:
- 保留多个部署版本
- 快速回滚到稳定版本
- 使用功能标志
Astro 提供了灵活的部署选项,可以根据项目需求选择最适合的部署策略。
前端 · 2月21日 16:14
什么是 Astro 的内容集合(Content Collections)?如何使用它来管理博客文章或文档?Astro 的内容集合(Content Collections)是一个强大的功能,用于管理结构化内容,如博客文章、文档、产品目录等。它提供了类型安全、性能优化和开发体验提升。
**核心概念:**
内容集合允许你在 `src/content` 目录下组织内容,并通过类型安全的 API 访问这些内容。
**设置内容集合:**
1. **创建集合配置**:
```typescript
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content', // 使用 Markdown/MDX
schema: z.object({
title: z.string(),
description: z.string(),
publishDate: z.coerce.date(),
tags: z.array(z.string()),
image: z.string().optional(),
}),
});
const products = defineCollection({
type: 'data', // 使用 JSON/YAML
schema: z.object({
name: z.string(),
price: z.number(),
category: z.string(),
}),
});
export const collections = { blog, products };
```
2. **创建内容文件**:
```markdown
<!-- src/content/blog/my-first-post.md -->
---
title: "我的第一篇文章"
description: "这是文章描述"
publishDate: 2024-01-15
tags: ["astro", "tutorial"]
image: "/images/post-1.jpg"
---
这是文章的正文内容...
```
```json
// src/content/products/product-1.json
{
"name": "产品 1",
"price": 99.99,
"category": "电子"
}
```
**查询内容集合:**
```astro
---
import { getCollection } from 'astro:content';
// 获取所有博客文章
const allPosts = await getCollection('blog');
// 按日期排序
const posts = allPosts
.filter(post => post.data.publishDate <= new Date())
.sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf());
// 获取特定标签的文章
const astroPosts = await getCollection('blog', ({ data }) => {
return data.tags.includes('astro');
});
---
<h1>博客文章</h1>
{posts.map(post => (
<article>
<h2>{post.data.title}</h2>
<p>{post.data.description}</p>
<time>{post.data.publishDate.toLocaleDateString()}</time>
<a href={`/blog/${post.slug}`}>阅读更多</a>
</article>
))}
```
**获取单个条目:**
```astro
---
import { getEntry } from 'astro:content';
import type { CollectionEntry } from 'astro:content';
// 通过 slug 获取单个条目
const post = await getEntry('blog', 'my-first-post');
// 或者从 params 获取
const { slug } = Astro.params;
const post = await getEntry('blog', slug);
if (!post) {
return Astro.redirect('/404');
}
const { Content } = await post.render();
---
<h1>{post.data.title}</h1>
<p>{post.data.description}</p>
<Content />
```
**动态路由与内容集合:**
```astro
---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<h1>{post.data.title}</h1>
<Content />
```
**使用 MDX:**
```markdown
---
title: "使用 MDX 的文章"
description: "支持 JSX 的 Markdown"
publishDate: 2024-01-20
tags: ["mdx", "astro"]
---
import { Counter } from '../../components/Counter.jsx';
这是普通的 Markdown 内容。
<Counter />
你可以在这里使用任何组件!
```
**内容集合的优势:**
1. **类型安全**:
- 使用 Zod schema 定义内容结构
- TypeScript 自动推断类型
- 编译时验证
2. **性能优化**:
- 构建时处理内容
- 自动生成路由
- 避免运行时解析
3. **开发体验**:
- IDE 自动完成
- 类型检查
- 错误提示
4. **灵活性**:
- 支持 Markdown、MDX、JSON、YAML
- 自定义 schema
- 前置数据处理
**高级用法:**
1. **嵌套目录**:
```
src/content/
├── blog/
│ ├── 2024/
│ │ ├── january/
│ │ │ └── post.md
│ │ └── february/
│ │ └── post.md
│ └── 2023/
│ └── post.md
└── docs/
└── guide.md
```
2. **自定义渲染器**:
```typescript
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
}),
// 自定义渲染器
render: async ({ entry }) => {
// 自定义渲染逻辑
return entry.render();
},
});
```
3. **内容转换**:
```typescript
// src/content/config.ts
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
date: z.coerce.date(),
}),
transform: async (data, id) => {
// 转换数据
return {
...data,
slug: id.replace('.md', ''),
};
},
});
```
**最佳实践:**
1. 使用 Zod schema 定义清晰的内容结构
2. 为不同的内容类型创建单独的集合
3. 利用 TypeScript 获得类型安全
4. 使用 `getStaticPaths` 生成动态路由
5. 在构建时处理所有内容,避免运行时开销
内容集合是 Astro 处理结构化内容的最佳方式,特别适合博客、文档、产品目录等内容驱动的网站。
前端 · 2月21日 16:14