Astro 现代 Web 全栈框架

30 阅读6分钟

项目标题与描述

Astro 是一个现代化的全栈 Web 框架,旨在构建快速的网站。它将强大的开发者体验与轻量级输出相结合,允许您从任何地方拉取内容并部署到任何地方,并由您喜爱的 UI 组件和库驱动。

Astro 的核心价值在于其“岛屿架构”(Islands Architecture),该架构允许您有选择性地激活页面上的交互式组件,从而显著减少发送到浏览器的 JavaScript,确保页面加载迅速且运行流畅。

功能特性

  • 岛屿架构: 默认情况下,Astro 发送零客户端 JavaScript。您可以通过 client:* 指令有选择性地激活页面上的交互式组件(“岛屿”),从而获得快速的页面加载和交互。
  • 服务器优先渲染: 页面在服务器上渲染为静态 HTML,提供出色的 SEO 性能和首次加载速度。
  • 多框架支持: 在同一个项目中无缝使用 React、Preact、Vue、Svelte、Solid 等流行的 UI 框架组件。
  • 基于文件的路由: 直观的文件系统路由,使得创建页面和布局变得简单。
  • 内容集合: 类型安全的内容管理,用于组织、验证和查询博客文章、文档等。
  • 视图过渡: 内置的客户端导航和页面过渡动画,提供类似应用的用户体验。
  • 内置图像优化: 自动优化图片,支持响应式图片和现代格式。
  • Markdown 和 MDX 支持: 编写内容并直接在 Markdown 文件中嵌入组件。
  • 环境变量和操作: 安全地管理服务器端和客户端环境变量,并提供类型安全的服务器操作。
  • 国际化 (i18n): 轻松构建多语言网站,支持路由和内容本地化。
  • 基准测试套件: 项目内置了全面的性能基准测试工具(如渲染、内存、服务器压力测试),确保框架持续优化。

安装指南

快速开始(推荐)

使用以下命令创建一个新的 Astro 项目:

npm create astro@latest

该命令会引导您完成项目设置,包括选择模板和配置。

手动安装

您也可以在现有项目中手动安装 Astro:

npm install --save-dev astro

先决条件

  • Node.js: >=18.20.8
  • 包管理器: 建议使用 pnpm (^10.21.0) 进行开发。可以通过 Corepack 启用。

本地开发设置

  1. 克隆仓库:
    git clone <repository-url>
    cd astro
    
  2. 安装依赖(使用根目录的 pnpm install):
    pnpm install
    
  3. 构建项目:
    pnpm run build
    

注意: Astro 使用 pnpm 工作区,请务必从顶级项目目录运行 pnpm install

使用 GitHub Codespaces

点击此链接在基于 Web 的 VS Code 中创建此仓库的代码空间,所有开发依赖项将预安装。

使用说明

创建新页面

src/pages/ 目录下创建 .astro.md.mdx 文件即可创建新页面。

示例:src/pages/index.astro


<!-- 组件模板 (HTML + JSX) -->
<html lang="zh">
  <head>
    <title>{title}</title>
  </head>
  <body>
    <h1>欢迎来到 {title}</h1>
    <p>这是一个静态渲染的页面。</p>
  </body>
</html>

添加交互式组件(岛屿)

您可以使用 client:* 指令激活框架组件。

示例:使用 React 计数器


<!-- 这个组件现在将在客户端交互 -->
<MyReactCounter client:load />

内容集合

定义内容模式并查询内容。

示例:src/content/config.ts

import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  schema: z.object({
    title: z.string(),
    date: z.date(),
    draft: z.boolean().optional(),
  }),
});

export const collections = { blog };

在页面中查询:


<ul>
  {blogPosts.map((post) => (
    <li><a href={`/blog/${post.slug}`}>{post.data.title}</a></li>
  ))}
</ul>

运行基准测试

Astro 内置了性能基准测试套件。

# 运行所有基准测试
pnpm run benchmark

# 运行特定测试,例如内存测试
pnpm run benchmark memory

# 针对特定项目运行测试
pnpm run benchmark memory --project render-default

可用的基准测试命令包括 memory(内存和构建速度)、render(渲染速度)、server-stress(服务器压力测试)和 cli-startup(CLI 启动速度测试)。

核心代码

以下展示了 Astro 项目中几个核心模块的代码,这些代码体现了其架构和功能。

1. 基准测试运行器主脚本 (bench/index.js)

此脚本是 astro-benchmark CLI 的核心,负责调度和执行不同的性能基准测试。

import fs from 'node:fs/promises';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import mri from 'mri';
import { makeProject } from './bench/_util.js';

const args = mri(process.argv.slice(2));

if (args.help || args.h) {
    // 显示帮助信息
	console.log(`\
astro-benchmark <command> [options]

Command
  [empty]         Run all benchmarks
  memory          Run build memory and speed test
  render          Run rendering speed test
  server-stress   Run server stress test
  cli-startup     Run CLI startup speed test

Options
  --project <project-name>       Project to use for benchmark, see benchmark/make-project/ for available names
  --output  <output-file>        Output file to write results to
`);
	process.exit(0);
}

// 定义可用的基准测试模块
const commandName = args._[0];
const benchmarks = {
	memory: () => import('./bench/memory.js'),
	render: () => import('./bench/render.js'),
	'server-stress': () => import('./bench/server-stress.js'),
	'cli-startup': () => import('./bench/cli-startup.js'),
};

if (commandName && !(commandName in benchmarks)) {
	console.error(`Invalid benchmark name: ${commandName}`);
	process.exit(1);
}

/**
 * 获取输出文件路径
 * @param {string} benchmarkName
 */
export async function getOutputFile(benchmarkName) {
	let file;
	if (args.output) {
        // 如果用户指定了输出文件,则使用该路径
		file = pathToFileURL(path.resolve(args.output));
	} else {
        // 否则,生成默认的结果文件路径
		file = new URL(`./results/${benchmarkName}-bench-${Date.now()}.json`, import.meta.url);
	}
	// 确保输出目录存在
	await fs.mkdir(new URL('./', file), { recursive: true });
	return file;
}

// 执行单个或所有基准测试
if (commandName) {
	// 运行单个基准测试
	const bench = benchmarks[commandName];
	const benchMod = await bench();
	const projectDir = await makeProject(args.project || benchMod.defaultProject);
	const outputFile = await getOutputFile(commandName);
	await benchMod.run(projectDir, outputFile);
} else {
	// 运行所有基准测试
	for (const name in benchmarks) {
		const bench = benchmarks[name];
		const benchMod = await bench();
		const projectDir = await makeProject(args.project || benchMod.defaultProject);
		const outputFile = await getOutputFile(name);
		await benchMod.run(projectDir, outputFile);
	}
}

2. 渲染性能基准测试 (bench/render.js)

此模块测量 Astro 页面的服务器端渲染时间,是性能评估的关键部分。

import fs from 'node:fs/promises';
import http from 'node:http';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { markdownTable } from 'markdown-table';
import { waitUntilBusy } from 'port-authority';
import { exec } from 'tinyexec';
import { renderPages } from '../make-project/render-default.js';
import { astroBin, calculateStat } from './_util.js';

const port = 4322;
export const defaultProject = 'render-default';

/**
 * 运行渲染基准测试的主要函数
 * @param {URL} projectDir 要测试的项目目录
 * @param {URL} outputFile 结果输出文件
 */
export async function run(projectDir, outputFile) {
	const root = fileURLToPath(projectDir);

	console.log('Building...');
    // 1. 构建项目
	await exec(astroBin, ['build'], {
		nodeOptions: {
			cwd: root,
			stdio: 'inherit',
		},
		throwOnError: true,
	});

	console.log('Previewing...');
    // 2. 启动预览服务器
	const previewProcess = exec(astroBin, ['preview', '--port', port], {
		nodeOptions: {
			cwd: root,
			stdio: 'inherit',
		},
		throwOnError: true,
	});

	console.log('Waiting for server ready...');
	await waitUntilBusy(port, { timeout: 5000 });

	console.log('Running benchmark...');
    // 3. 执行基准测试
	const result = await benchmarkRenderTime();

	console.log('Killing server...');
	if (!previewProcess.kill('SIGTERM')) {
		console.warn('Failed to kill server process id:', previewProcess.pid);
	}

    // 4. 保存结果
	console.log('Writing results to', fileURLToPath(outputFile));
	await fs.writeFile(outputFile, JSON.stringify(result, null, 2));

    // 5. 在控制台输出格式化结果
	console.log('Result preview:');
	console.log('='.repeat(10));
	console.log(`#### Render\n\n`);
	console.log(printResult(result));
	console.log('='.repeat(10));

	console.log('Done!');
}

/**
 * 核心测试逻辑:获取每个页面的渲染时间
 * @param {number} portToListen 服务器端口
 * @returns {Promise<Record<string, Stat>>} 页面路径到统计数据的映射
 */
export async function benchmarkRenderTime(portToListen = port) {
	/** @type {Record<string, number[]>} */
	const result = {};
    // 对预定义的页面列表进行测试
	for (const fileName of renderPages) {
		// 每个页面渲染100次以获取稳定数据
		for (let i = 0; i < 100; i++) {
			const pathname = '/' + fileName.slice(0, -path.extname(fileName).length);
			const renderTime = await fetchRenderTime(`http://localhost:${portToListen}${pathname}`);
			if (!result[pathname]) result[pathname] = [];
			result[pathname].push(renderTime);
		}
	}
	/** @type {Record<string, import('./_util.js').Stat>} */
	const processedResult = {};
    // 计算平均值、标准差和最大值
	for (const [pathname, times] of Object.entries(result)) {
		processedResult[pathname] = calculateStat(times);
	}
	return processedResult;
}

/**
 * 将结果格式化为 Markdown 表格
 * @param {Record<string, import('./_util.js').Stat>} result
 */
function printResult(result) {
	return markdownTable(
		[
			['Page', 'Avg (ms)', 'Stdev (ms)', 'Max (ms)'],
			...Object.entries(result).map(([pathname, { avg, stdev, max }]) => [
				pathname,
				avg.toFixed(2),
				stdev.toFixed(2),
				max.toFixed(2),
			]),
		],
		{
			align: ['l', 'r', 'r', 'r'],
		},
	);
}

/**
 * 简单的 HTTP 请求工具,获取由 `@benchmark/timer` 适配器返回的渲染时间(纯文本)
 * @param {string} url 要请求的页面 URL
 * @returns {Promise<number>} 渲染耗时(毫秒)
 */
function fetchRenderTime(url) {
	return new Promise((resolve, reject) => {
		const req = http.request(url, (res) => {
			res.setEncoding('utf8');
			let data = '';
			res.on('data', (chunk) => (data += chunk));
			res.on('error', (e) => reject(e));
			res.on('end', () => resolve(+data)); // 将响应文本转换为数字
		});
		req.on('error', (e) => reject(e));
		req.end();
	});
}

3. 工具函数:计算统计数据 (bench/_util.js)

此模块提供了基准测试中常用的工具函数,例如计算统计指标。

import { createRequire } from 'node:module';
import path from 'node:path';

// 解析 astro 主包的路径,用于获取可执行文件
const astroPkgPath = createRequire(import.meta.url).resolve('astro/package.json');
export const astroBin = path.resolve(astroPkgPath, '../astro.js');

/** @typedef {{ avg: number, stdev: number, max: number }} Stat 统计数据类型定义 */

/**
 * 计算一组数字的平均值、标准差和最大值
 * @param {number[]} numbers 输入的数字数组
 * @returns {Stat} 包含平均值、标准差和最大值的对象
 */
export function calculateStat(numbers) {
    // 计算平均值
	const avg = numbers.reduce((a, b) => a + b, 0) / numbers.length;
    // 计算标准差
	const stdev = Math.sqrt(
		numbers.map((x) => Math.pow(x - avg, 2)).reduce((a, b) => a + b, 0) / numbers.length,
	);
    // 计算最大值
	const max = Math.max(...numbers);
	return { avg, stdev, max };
}

/**
 * 根据名称创建或准备基准测试项目
 * @param {string} name 项目名称
 * @returns {Promise<URL>} 项目目录的 URL
 */
export async function makeProject(name) {
	console.log('Making project:', name);
	const projectDir = new URL(`../projects/${name}/`, import.meta.url);

    // 动态导入对应的项目生成脚本并执行
	const makeProjectMod = await import(`../make-project/${name}.js`);
	await makeProjectMod.run(projectDir);

	console.log('Finished making project:', name);
	return projectDir;
}

1dDUwxC45ELRatHf3TeyfhGwlsb3hwnamiAbjKU99UM=