跳转到内容

从 Next.js 迁移

这里有一些关键概念和迁移策略可以帮助你入门。利用我们文档的其余部分和我们的 Discord 社区 继续前进!

Next.js 和 Astro 有一些相似之处,这将有助于你迁移项目

在 Astro 中重建 Next.js 网站时,你会注意到一些重要的区别

  • Next.js 是一个 React 单页应用,使用 index.js 作为项目的根。Astro 是一个多页网站,index.astro 是你的主页。

  • .astro 组件不是作为返回页面模板的导出函数编写的。相反,你会将代码分成用于 JavaScript 的“代码围栏”和专用于生成 HTML 的主体部分。

  • 内容驱动:Astro 旨在展示你的内容,并允许你仅在需要时选择性地加入交互性。现有的 Next.js 应用可能是为高客户端交互性而构建的,可能需要高级的 Astro 技术来包含使用 .astro 组件难以复制的项目,例如仪表盘。

每个项目的迁移过程都会有所不同,但在从 Next.js 转换为 Astro 时,你会执行一些常见的操作。

使用你的包管理器的 create astro 命令来启动 Astro 的 CLI 向导,或从 Astro 主题展示中选择一个社区主题。

你可以向 create astro 命令传递一个 --template 参数,以使用我们的官方入门模板(例如 docsblogportfolio)之一来启动新的 Astro 项目。或者,你可以从 GitHub 上的任何现有 Astro 仓库开始一个新项目

终端窗口
# launch the Astro CLI Wizard
npm create astro@latest
# create a new project with an official example
npm create astro@latest -- --template <example-name>

然后,将你现有的 Next 项目文件复制到新 Astro 项目中 src 之外的一个单独文件夹中。

在将 Next 项目转换为 Astro 时,安装一些 Astro 的可选集成可能会很有用

  • @astrojs/react:在新 Astro 网站中重用一些现有的 React UI 组件,或继续使用 React 组件编写。

  • @astrojs/mdx:从你的 Next 项目中引入现有的 MDX 文件,或在你的新 Astro 网站中使用 MDX。

遵循 Astro 的项目结构

  1. 保留 Next 的 public/ 文件夹不变。

    Astro 和 Next 一样,使用 public/ 目录存放静态资源。此文件夹及其内容无需任何更改。

  2. 在重建你的网站时,复制或移动 Next 的其他文件和文件夹(例如 pagesstyles 等)到 Astro 的 src/ 文件夹中,并遵循 Astro 的项目结构

    与 Next 类似,Astro 的 src/pages/ 文件夹是一个用于基于文件的路由的特殊文件夹。所有其他文件夹都是可选的,你可以按任何你喜欢的方式组织 src/ 文件夹的内容。Astro 项目中其他常见的文件夹包括 src/layouts/src/componentssrc/stylessrc/scripts

Astro 在你的项目根目录下有一个名为 astro.config.mjs 的配置文件。它仅用于配置你的 Astro 项目和任何已安装的集成,包括 SSR 适配器

以下是将 Next .js 组件转换为 .astro 组件的一些提示

  1. 使用现有 Next.js 组件函数返回的 JSX 作为你 HTML 模板的基础。

  2. 将任何 Next 或 JSX 语法更改为 Astro 或 HTML Web 标准。例如,这包括 <Link><Script>{children}className

  3. 将任何必要的 JavaScript,包括导入语句,移动到“代码围栏”(---中。注意:在 Astro 中,用于条件渲染内容的 JavaScript 通常直接写在 HTML 模板内。

  4. 使用 Astro.props 来访问之前传递给你 Next 函数的任何附加属性。

  5. 决定是否需要将任何导入的组件也转换为 Astro。安装官方集成后,你可以在 Astro 文件中使用现有的 React 组件。但是,你可能希望将它们转换为 .astro 组件,特别是如果它们不需要交互性!

  6. import 语句或 import.meta.glob() 替换 getStaticProps() 来查询本地文件。使用 fetch() 获取外部数据。

查看一个 Next .js 文件逐步转换的示例

比较以下 Next 组件和相应的 Astro 组件

StarCount.jsx
import Header from "./header";
import Footer from "./footer";
import "./layout.css";
export async function getStaticProps() {
const res = await fetch("https://api.github.com/repos/withastro/astro");
const json = await res.json();
return {
props: { message: json.message, stars: json.stargazers_count || 0 },
}
}
const Component = ({ stars, message }) => {
return (
<>
<Header />
<p style={{
backgroundColor: `#f4f4f4`,
padding: `1em 1.5em`,
textAlign: `center`,
marginBottom: `1em`
}}>Astro has {stars} 🧑‍🚀</p>
<Footer />
</>
)
}
export default Component;

你可能会发现,从将 Next.js 的布局和模板转换为 Astro 布局组件开始会很有帮助。

Next 有两种不同的方法来创建布局文件,每种方法处理布局的方式都与 Astro 不同

每个 Astro 页面都明确要求存在 <html><head><body> 标签,因此在不同页面之间重用布局文件是很常见的。Astro 使用<slot />来放置页面内容,无需导入语句。注意标准的 HTML 模板和对 <head> 的直接访问

src/layouts/Layout.astro
---
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
</head>
<body>
<!-- Wrap the slot element with your existing layout templating -->
<slot />
</body>
</html>

你的 Next 项目可能有一个 pages/_document.jsx 文件,它导入 React 组件来自定义应用的 <head>

pages/_document.jsx
import Document, { Html, Head, Main, NextScript } from "next/document";
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
<link rel="icon" href="/favicon.ico" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
  1. 仅使用返回的 JSX 创建一个新的 Astro 布局文件。

  2. 将任何 React 组件替换为 <html><head><slot> 和其他 HTML 标准标签。

    src/layouts/Document.astro
    <html lang="en">
    <head>
    <link rel="icon" href="/favicon.ico" />
    </head>
    <body>
    <slot/>
    </body>
    </html>

Next.js 的 app/ 目录布局文件由两个文件创建:一个 layout.jsx 文件用于自定义 <html><body> 的内容,以及一个 head.jsx 文件用于自定义 <head> 元素的内容。

app/layout.jsx
export default function Layout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
app/head.jsx
export default function Head() {
return (
<>
<title>My Page</title>
</>
);
}
  1. 仅使用返回的 JSX 创建一个新的 Astro 布局文件。

  2. 用一个包含页面外壳(<html><head><body> 标签)和 <slot/>(而不是 React 的 {children} 属性)的 Astro 布局文件替换这两个文件

    src/layouts/Layout.astro
    <html lang="en">
    <head>
    <title>My Page</title>
    </head>
    <body>
    <slot/>
    </body>
    </html>

在 Next.js 中,你的文章要么在 /pages 目录下,要么在 /app/routeName/page.jsx

在 Astro 中,除非你使用内容集合,否则所有页面内容都必须位于 src/ 内部。

你现有的 Next JSX (.js) 页面需要从 JSX 文件转换为 .astro 页面。你不能在 Astro 中使用现有的 JSX 页面文件。

这些 .astro 页面必须位于 src/pages/ 内,并且会根据其文件路径自动生成页面路由。

Astro 内置了对 Markdown 的支持,并为 MDX 文件提供了可选的集成。你可以重用任何现有的 Markdown 和 MDX 文件,但它们可能需要对其 frontmatter 进行一些调整,例如添加 Astro 的特殊 layout frontmatter 属性。你将不再需要为每个 Markdown 生成的路由手动创建页面。这些文件可以放在 src/pages/ 中,以利用自动基于文件的路由。

或者,你可以在 Astro 中使用内容集合来存储和管理你的内容。你将自己检索内容并动态生成这些页面

由于 Astro 输出的是原始 HTML,因此可以使用构建步骤的输出来编写端到端测试。如果你能够匹配 Next 网站的标记,那么之前编写的任何端到端测试都可能直接可用。像 Jest 和 React Testing Library 这样的测试库可以导入并在 Astro 中使用,以测试你的 React 组件。

更多信息请参阅 Astro 的测试指南

将任何 Next 的 <Link to=""><NavLink> 等组件转换为 HTML <a href=""> 标签。

<Link to="/blog">Blog</Link>
<a href="/blog">Blog</a>

Astro 不使用任何特殊的链接组件,但欢迎你构建自己的 <Link> 组件。然后你可以像导入和使用任何其他组件一样使用这个 <Link> 组件。

src/components/Link.astro
---
const { to } = Astro.props;
---
<a href={to}><slot /></a>

更新任何文件导入,以精确引用相对文件路径。这可以通过使用导入别名或完整写出相对路径来完成。

请注意,.astro 和其他几种文件类型必须使用其完整的文件扩展名进行导入。

src/pages/authors/Fred.astro
---
import Card from "../../components/Card.astro";
---
<Card />

将任何 {children} 实例转换为 Astro 的 <slot />。Astro 不需要以函数属性的形式接收 {children},它会自动在 <slot /> 中渲染子内容。

src/components/MyComponent.astro
---
---
export default function MyComponent(props) {
return (
<div>
{props.children}
</div>
);
}
<div>
<slot />
</div>

传递多组子组件的 React 组件可以使用命名插槽迁移到 Astro 组件。

查看更多关于Astro 中 <slot /> 的具体用法

将任何 getStaticProps() 实例转换为 import.meta.glob()getCollection()/getEntry(),以便从项目源中的其他文件访问数据。要获取远程数据,请使用 fetch()

这些数据请求是在 Astro 组件的 frontmatter 中发出的,并使用顶层 await。

src/pages/index.astro
---
import { getCollection } from 'astro:content';
// Get all `src/content/blog/` entries
const allBlogPosts = await getCollection('blog');
// Get all `src/pages/posts/` entries
const allPosts = Object.values(import.meta.glob('../pages/posts/*.md', { eager: true }));
const response = await fetch('https://randomuser.me/api/');
const data = await response.json();
const randomUser = data.results[0];
---

查看更多关于使用 import.meta.glob() 导入本地文件、使用集合 API 查询获取远程数据的信息。

你可能需要将任何 CSS-in-JS 库(例如 styled-components)替换为 Astro 中其他可用的 CSS 选项。

如有必要,将任何内联样式对象 (style={{ fontWeight: "bold" }}) 转换为内联 HTML 样式属性 (style="font-weight:bold;")。或者,使用 Astro 的 <style> 标签来编写局部作用域的 CSS 样式。

src/components/Card.astro
<div style={{backgroundColor: `#f4f4f4`, padding: `1em`}}>{message}</div>
<div style="background-color: #f4f4f4; padding: 1em;">{message}</div>

安装 Tailwind Vite 插件后即可支持 Tailwind。无需对你现有的 Tailwind 代码进行任何更改!

查看更多关于在 Astro 中设置样式的信息。

将任何 Next 的 <Image /> 组件转换为 Astro 在 .astro.mdx 文件中的自有图像组件,或在你的 React 组件中适当地转换为标准的 HTML <img> / JSX <img /> 标签。

Astro 的 <Image /> 组件仅在 .astro.mdx 文件中有效。请查看其组件属性的完整列表,并注意其中一些属性与 Next 的属性有所不同。

src/pages/index.astro
---
import { Image } from 'astro:assets';
import rocket from '../assets/rocket.png';
---
<Image src={rocket} alt="A rocketship in space." />
<img src={rocket.src} alt="A rocketship in space.">

在 React (.jsx) 组件中,使用标准的 JSX 图像语法 (<img />)。Astro 不会优化这些图像,但你可以安装和使用 NPM 包以获得更大的灵活性。

你可以在图像指南中了解更多关于在 Astro 中使用图像的信息。

指导示例:将 Next 数据获取转换为 Astro

标题为“指导示例:将 Next 数据获取转换为 Astro”的部分

这是一个将 Next.js 的 Pokédex 数据获取转换为 Astro 的示例。

pages/index.js 使用 REST PokéAPI 获取并显示前 151 个宝可梦的列表。

以下是如何在 src/pages/index.astro 中重现该功能,用 fetch() 替换 getStaticProps()

  1. 识别 return() 中的 JSX。

    pages/index.js
    import Link from 'next/link'
    import styles from '../styles/poke-list.module.css';
    export default function Home({ pokemons }) {
    return (
    <>
    <ul className={`plain-list ${styles.pokeList}`}>
    {pokemons.map((pokemon) => (
    <li className={styles.pokemonListItem} key={pokemon.name}>
    <Link className={styles.pokemonContainer} as={`/pokemon/${pokemon.name}`} href="/pokemon/[name]">
    <p className={styles.pokemonId}>No. {pokemon.id}</p>
    <img className={styles.pokemonImage} src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${pokemon.id}.png`} alt={`${pokemon.name} picture`}></img>
    <h2 className={styles.pokemonName}>{pokemon.name}</h2>
    </Link>
    </li>
    ))}
    </ul>
    </>
    )
    }
    export const getStaticProps = async () => {
    const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151")
    const resJson = await res.json();
    const pokemons = resJson.results.map(pokemon => {
    const name = pokemon.name;
    // https://pokeapi.co/api/v2/pokemon/1/
    const url = pokemon.url;
    const id = url.split("/")[url.split("/").length - 2];
    return {
    name,
    url,
    id
    }
    });
    return {
    props: {
    pokemons,
    },
    }
    }
  2. 创建 src/pages/index.astro

    使用 Next 函数的返回值。将任何 Next 或 React 语法转换为 Astro,包括更改任何 HTML 全局属性的大小写。

    请注意

    • .map 仍然有效!

    • className 变为 class

    • <Link> 变为 <a>

    • 在 Astro 模板中不需要 <> </> 片段。

    • key 是一个 React 属性,在 Astro 中不是 li 的属性。

    src/pages/index.astro
    ---
    ---
    <ul class="plain-list pokeList">
    {pokemons.map((pokemon) => (
    <li class="pokemonListItem">
    <a class="pokemonContainer" href={`/pokemon/${pokemon.name}`}>
    <p class="pokemonId">No. {pokemon.id}</p>
    <img class="pokemonImage" src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${pokemon.id}.png`} alt={`${pokemon.name} picture`}/>
    <h2 class="pokemonName">{pokemon.name}</h2>
    </a>
    </li>
    ))}
    </ul>
  3. 添加任何需要的导入、属性和 JavaScript

    请注意

    • 不再需要 getStaticProps 函数。API 数据直接在代码围栏中获取。
    • 导入一个 <Layout> 组件并包裹页面模板。
    src/pages/index.astro
    ---
    import Layout from '../layouts/layout.astro';
    const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151");
    const resJson = await res.json();
    const pokemons = resJson.results.map(pokemon => {
    const name = pokemon.name;
    // https://pokeapi.co/api/v2/pokemon/1/
    const url = pokemon.url;
    const id = url.split("/")[url.split("/").length - 2];
    return {
    name,
    url,
    id
    }
    });
    ---
    <Layout>
    <ul class="plain-list pokeList">
    {pokemons.map((pokemon) => (
    <li class="pokemonListItem" key={pokemon.name}>
    <a class="pokemonContainer" href={`/pokemon/${pokemon.name}`}>
    <p class="pokemonId">No. {pokemon.id}</p>
    <img class="pokemonImage" src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${pokemon.id}.png`} alt={`${pokemon.name} picture`}/>
    <h2 class="pokemonName">{pokemon.name}</h2>
    </a>
    </li>
    ))}
    </ul>
    </Layout>

更多迁移指南

贡献 社区 赞助