跳转到内容

从 NuxtJS 迁移

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

本指南指的是 Nuxt 2,而不是较新的 Nuxt 3。虽然一些概念是相似的,但 Nuxt 3 是该框架的更新版本,可能需要为你的迁移部分采取不同的策略。

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

当你在 Astro 中重建你的 Nuxt 站点时,你会注意到一些重要的区别

  • Nuxt 是一个基于 Vue 的 SPA(单页应用)。Astro 站点是使用 .astro 组件构建的多页应用,但也支持 React、Preact、Vue.js、Svelte、SolidJS、AlpineJS 和原始 HTML 模板。

  • 页面路由:Nuxt 使用 vue-router 进行 SPA 路由,并使用 vue-meta 管理 <head>。在 Astro 中,你将创建独立的 HTML 页面路由,并直接控制页面的 <head>,或者在布局组件中进行控制。

  • 内容驱动:Astro 的设计初衷是为了展示你的内容,并允许你仅在需要时选择性地添加交互性。现有的 Nuxt 应用可能是为高客户端交互性而构建的。Astro 具有处理内容的内置功能,例如页面生成,但可能需要高级 Astro 技术来包含那些使用 .astro 组件难以复制的项目,例如仪表盘。

每个项目的迁移过程都会有所不同,但在从 Nuxt 转换为 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>

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

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

  • @astrojs/vue:在你的新 Astro 站点中重用一些现有的 Vue UI 组件,或者继续使用 Vue 组件编写。

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

  1. 移动 Nuxt 的 static/ 文件夹内容到 public/

    Astro 使用 public/ 目录存放静态资源,类似于 Nuxt 的 static/ 文件夹。

  2. 复制或移动 Nuxt 的其他文件和文件夹(例如 pageslayouts 等)到 Astro 的 src/ 文件夹中。

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

这里有一些将 Nuxt .vue 组件转换为 .astro 组件的技巧

  1. 使用现有 NuxtJS 组件函数的 <template> 作为你 HTML 模板的基础。

  2. 将任何 Nuxt 或 Vue 语法更改为 Astro 语法或 HTML Web 标准。例如,这包括 <NuxtLink>:class{{variable}}v-if

  3. <script> JavaScript 移入“代码栅栏”(---)中。将组件的数据获取属性转换为服务器端 JavaScript - 见 Nuxt 数据获取转换为 Astro

  4. 使用 Astro.props 来访问之前传递给你 Vue 组件的任何额外 props。

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

查看一个从 Nuxt 应用逐步转换的示例

比较下面的 Nuxt 组件和对应的 Astro 组件

Page.vue
<template>
<div>
<p v-if="message === 'Not found'">
The repository you're looking up doesn't exist
</p>
<div v-else>
<Header/>
<p class="banner">Astro has {{stars}} 🧑‍🚀</p>
<Footer />
</div>
</div>
</template>
<script>
import Vue from 'vue'
export default Vue.extend({
name: 'IndexPage',
async asyncData() {
const res = await fetch('https://api.github.com/repos/withastro/astro')
const json = await res.json();
return {
message: json.message,
stars: json.stargazers_count || 0,
};
}
});
</script>
<style scoped>
.banner {
background-color: #f4f4f4;
padding: 1em 1.5em;
text-align: center;
margin-bottom: 1em;
}
<style>

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

每个 Astro 页面都明确要求存在 <html><head><body> 标签。你的 Nuxt layout.vue 和模板将不包含这些。

注意标准的 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" />
<title>Astro</title>
</head>
<body>
<!-- Wrap the slot element with your existing layout templating -->
<slot />
</body>
</html>

你可能还希望重用来自 Nuxt 页面的 head 属性的代码来包含额外的站点元数据。请注意,Astro 既不使用 vue-meta 也不使用组件的 head 属性,而是直接创建 <head>。你可以导入和使用组件,甚至在 <head> 内部,来分离和组织你的页面内容。

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

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

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

在 Nuxt 中,你的动态页面使用下划线来表示一个动态页面属性,然后该属性会传递给页面生成

  • 目录pages/
    • 目录pokemon/
      • _name.vue
    • index.vue
  • nuxt.config.js

要转换为 Astro,请将这个带下划线的动态路径属性(例如 _name.vue)更改为用一对中括号包裹(例如 [name].astro

  • 目录src/
    • 目录pages/
      • 目录pokemon/
        • [name].astro
      • index.astro
  • astro.config.mjs

Astro 对 Markdown 有内置支持,并为 MDX 文件提供了可选集成。你可以重用任何现有的 Markdown 和 MDX 页面,但它们可能需要对其 frontmatter 进行一些调整,例如添加 Astro 的特殊 layout frontmatter 属性

你将不再需要为每个 Markdown 生成的路由手动创建页面,或使用像 @nuxt/content 这样的外部包。这些文件可以放在 src/pages/ 中,以利用自动化的基于文件的路由。

当作为内容集合的一部分时,你将从内容条目中动态生成页面

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

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

要在 Astro 组件的 HTML 中使用局部变量,请将双大括号改为单大括号

src/components/Component.astro
---
const message = "Hello!"
---
<p>{{message}}</p>
<p>{message}</p>

要在 Astro 组件中绑定属性或组件属性,请将此语法更改为以下内容

src/components/Component.astro
---
---
<p v-bind:aria-label="message">...</p>
<!-- Or -->
<p :aria-label="message">...</p>
<!-- Also support component props -->
<Header title="Page"/>
<p aria-label={message}>...</p>
<!-- Also support component props -->
<Header title={"Page"}/>

将任何 Nuxt <NuxtLink to=""> 组件转换为 HTML <a href=""> 标签。

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

Astro 不使用任何特殊的链接组件,但欢迎你构建自定义链接组件。然后你可以像导入和使用任何其他组件一样导入和使用这个 <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 />

在 Nuxt 中,要生成动态页面,你必须

在 Astro 中,你同样有两个选择

将 Nuxt 中的 generate 函数转换为 Astro 中的 getStaticPaths 函数。

标题为“将 Nuxt 中的 generate 函数转换为 Astro 中的 getStaticPaths 函数。”的部分

要生成多个页面,请将 nuxt.config.js 中用于创建路由的函数替换为直接在动态路由页面本身内部的 getStaticPaths()

nuxt.config.js
{
// ...
generate: {
async routes() {
// Axios is required here unless you're using Node 18
const res = await axios.get("https://pokeapi.co/api/v2/pokemon?limit=151")
const pokemons = res.data.results;
return pokemons.map(pokemon => {
return '/pokemon/' + pokemon.name
})
}
}
}
src/pages/pokemon/[name].astro
---
export const getStaticPaths = async () => {
const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151")
const resJson = await res.json();
const pokemons = resJson.results;
return pokemons.map(({ name }) => ({
params: { name },
}))
}
// ...
---
<!-- Your template here -->

Nuxt 有两种获取服务器端数据的方法

在 Astro 中,在页面的代码栅栏内获取数据。

迁移以下内容

pages/index.vue
{
// ...
async asyncData() {
const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151")
const resJson = await res.json();
const pokemons = resJson.results;
return {
pokemons,
}
},
}

到一个没有包装函数的代码栅栏

src/pages/index.astro
---
const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151")
const resJson = await res.json();
const pokemons = resJson.results;
---
<!-- Your template here -->

Nuxt 利用 Vue 的组件样式来生成页面的样式。

pages/index.vue
<template>
<!-- Your template here -->
</template>
<script>
// Your server logic here
</script>
<style scoped>
.class {
color: red;
}
</style>

类似地,在 Astro 中,你可以在页面的模板中放入一个 <style> 元素,为组件提供作用域内的样式。

src/pages/index.vue
---
// Your server logic here
---
<style>
.class {
color: red;
}
</style>

在 Astro 中,<style> 标签默认是作用域内(scoped)的。要使 <style> 标签变为全局的,请用 is:global 属性标记它

src/pages/index.vue
<style is:global>
p {
color: red;
}
</style>

Astro 支持最流行的 CSS 预处理器,只需将它们作为开发依赖项安装即可。例如,要使用 SCSS

终端窗口
npm install -D sass

完成此操作后,你就可以在 Vue 组件中使用 .scss.sass 样式,无需修改。

src/layouts/Layout.astro
<p>Hello, world</p>
<style lang="scss">
p {
color: black;
&:hover {
color: red;
}
}
</style>

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

将任何 Nuxt 的 <nuxt-img/><nuxt-picture/> 组件转换为Astro 自己的图像组件(在 .astro.mdx 文件中),或在你的 Vue 组件中酌情转换为标准的 HTML <img><picture> 标签。

Astro 的 <Image /> 组件仅在 .astro.mdx 文件中有效。请参阅其组件属性的完整列表,并注意其中有几个属性与 Nuxt 的属性不同。

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.">

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

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

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

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

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

  1. 在 Vue SFC 中识别 <template><style>

    pages/index.vue
    <template>
    <ul class="plain-list pokeList">
    <li v-for="pokemon of pokemons" class="pokemonListItem" :key="pokemon.name">
    <NuxtLink class="pokemonContainer" :to="`/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>
    </NuxtLink>
    </li>
    </ul>
    </template>
    <script>
    import Vue from 'vue'
    export default Vue.extend({
    name: 'IndexPage',
    layout: 'default',
    async asyncData() {
    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 {
    pokemons,
    }
    },
    head() {
    return {
    title: "Pokedex: Generation 1"
    }
    }
    });
    </script>
    <style scoped>
    .pokeList {
    display: grid;
    grid-template-columns: repeat( auto-fit, minmax(250px, 1fr) );
    gap: 1rem;
    }
    /* ... */
    </style>
  2. 创建 src/pages/index.astro

    使用 Nuxt SFC 的 <template><style> 标签。将任何 Nuxt 或 Vue 语法转换为 Astro。

    请注意

    • <template> 被移除

    • <style>scoped 属性被移除

    • v-for 变为 .map

    • :attr="val" 变为 attr={val}

    • <NuxtLink> 变为 <a>

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

    src/pages/index.astro
    ---
    ---
    <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>
    <style>
    .pokeList {
    display: grid;
    grid-template-columns: repeat( auto-fit, minmax(250px, 1fr) );
    gap: 1rem;
    }
    /* ... */
    </style>
  3. 添加任何需要的导入、props 和 JavaScript

    请注意

    • asyncData 函数不再需要。API 数据直接在代码栅栏中获取。
    • 导入一个 <Layout> 组件,并用它包裹页面模板。
      • 我们的 head() Nuxt 方法被传递给 <Layout> 组件,然后作为属性传递给 <title> 元素。
    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 title="Pokedex: Generation 1">
    <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>
    <style>
    .pokeList {
    display: grid;
    grid-template-columns: repeat( auto-fit, minmax(250px, 1fr) );
    gap: 1rem;
    }
    /* ... */
    </style>

更多迁移指南

贡献 社区 赞助