比较 SSR 框架 Next.js 和 Nuxt.js 的语法

768 阅读4分钟

SSR: Next.js & Nuxt.js,Next.js > v13.0.0,Nuxt.js 2 & 3

Basic-Routes

Next.js

<!-- (.js .jsx .tsx)  -->
|- pages/
  |- index.js      → href="/"
  |- blog/index.js   → href="/blog"

Nuxt.js

|- pages/
  |- index.vue       → href="/"
  |- blog/index.vue  → href="/blog"

Dynamic-Routes

Next.js

|- pages/
  |- blog/[slug].js           → href="/blog/:slug" (eg. /blog/hello-world)
  |- [username]/[option].js   → href="/:username/:option" (eg. /foo/settings)
  |- post/[...all].js         → href="/post/*" (eg. /post/2020/id/title)

Nuxt.js

|- pages/
  |- blog/[slug].vue         → href="/blog/:slug" (eg. /blog/hello-world)
  |- _username/_option.vue   → href="/:username/:option" (eg. /foo/settings)

Link

Next.js

import Link from "next/link"; // https://nextjs.org/docs/app/api-reference/components/link

function Home() {
  return (
    <Link href="/">
      <a>Home</a>
    </Link>
  );
}

Nuxt.js

<template>
  <nuxt-link to="/">Home page</nuxt-link>
</template>

Layout

Next.js

./pages/_app.js: automatically apply to all pages

export default function MyApp({ Component, pageProps }) {
  return (
    <React.Fragment>
      <MyHeader />
      <Component {...pageProps} />
      <MyFooter />
    </React.Fragment>
  );
}

Nuxt.js

v2

layouts/with-header-footer.vue: create layout

<template>
  <div>
    <MyHeader />
    <nuxt />
    <MyFooter />
  </div>
</template>

pages/index.vue: apply layout

<template>
  <!-- Your template -->
</template>
<script>
  export default {
    layout: "with-header-footer",
  };
</script>

v3

layout/default.vue

<template>
  <div>
    <slot />
  </div>
</template>

~/app.vue

<template>
  <NuxtLayout>
    <NuxtPage></NuxtPage>
  </NuxtLayout>
</template>

Error-Page

Next.js

app/global-error.tsx 意外运行错误 UI

"use client"; // Error boundaries must be Client Components

export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
  return (
    // global-error must include html and body tags
    <html>
      <body>
        <h2>Something went wrong!</h2>
        <button onClick={() => reset()}>Try again</button>
      </body>
    </html>
  );
}

app/not-found.tsx 路由未找到 UI

import Link from "next/link";

export default function NotFound() {
  return (
    <div>
      <h2>Not Found</h2>
      <p>Could not find requested resource</p>
      <Link href="/">Return Home</Link>
    </div>
  );
}

Nuxt.js

v2

layouts/error.vue

<template>
  <div class="container">
    <h1 v-if="error.statusCode === 404">Page not found</h1>
    <h1 v-else>An error occurred</h1>
    <nuxt-link to="/">Home page</nuxt-link>
  </div>
</template>

<script>
  export default {
    props: ["error"],
    layout: "blog", // you can set a custom layout for the error page
  };
</script>

v3

通过在应用程序的源目录中添加 ~/error.vue(与 app.vue 并排)来自定义默认错误页面。

<template>
  <div>
    <h2>{{ error.statusCode }}</h2>
    <h3>{{ error.message }}</h3>
    <h4 v-html="error.stack"></h4>
    <button @click="handleError">Clear errors</button>
  </div>
</template>
<script setup lang="ts">
  import type { NuxtError } from "#app";

  const props = defineProps({
    error: Object as () => NuxtError,
  });

  const handleError = () => clearError({ redirect: "/" });
</script>

Meta-Tag

Next.js

import Head from "next/head";

function IndexPage() {
  return (
    <div>
      <Head>
        <title>My page title</title>
        <meta name="viewport" content="initial-scale=1.0, width=device-width" />
      </Head>
      <p>Hello world!</p>
    </div>
  );
}

Nuxt.js

v2

  • 全局默认

nuxt.config.js

export default {
  /*
   ** Headers of the page
   */
  head: {
    title: prdConfig.productName,
    meta: [
      {
        name: "description",
        content: prdConfig.description,
      },
      {
        name: "keywords",
        content: prdConfig.keywords,
      },
    ],
  },
};

~/app.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, viewport-fit=cover" />
    <link rel="icon" type="image/x-icon" href="favicon.ico" />
    <style></style>
    {{HEAD}} {{ ENV.PATH_TYPE === "production" ? `` : `
    <script defer src="//cdn.com/vconsole.min.js"></script>
    `}}
  </head>
  <body>
    {{APP}}
  </body>
</html>
  • 页面维度
<template>
  <h1>{{ title }}</h1>
</template>

<script>
  export default {
    data() {
      return {
        title: "Hello World!",
      };
    },
    head() {
      return {
        title: this.title,
        meta: [
          // To avoid duplicated meta tags when used in child component, set up an unique identifier with the hid key
          {
            hid: "description",
            name: "description",
            content: "My custom description",
          },
        ],
      };
    },
  };
</script>

v3

全局默认

export default defineNuxtConfig({
  app: {
    head: {
      meta: [
        { charset: "utf-8" },
        {
          name: "viewport",
          content:
            "width=device-width,height=device-height,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover",
        },
      ],
      link: [{ rel: "icon", type: "image/x-icon", href: "favicon.ico" }],
      style: [],
      script: scripts,
      title: "主标题",
    },
  },
});

页面维度

可以使用 useHead 或 useSeoMeta 钩子 自定义:

<script setup lang="ts">
  useHead({
    title: "My App",
    meta: [{ name: "description", content: "My amazing site." }],
    bodyAttrs: {
      class: "test",
    },
    script: [{ innerHTML: "console.log('Hello world')" }],
  });

  useSeoMeta({
    title: "My Amazing Site",
    ogTitle: "My Amazing Site",
    description: "This is my amazing site, let me tell you all about it.",
    ogDescription: "This is my amazing site, let me tell you all about it.",
    ogImage: "https://example.com/image.png",
    twitterCard: "summary_large_image",
  });
</script>

Assets

Next.js

public

/*
|- public/
|-- my-image.png
*/
import Image from "next/image"; // https://nextjs.org/docs/app/api-reference/components/image

export default function myImage() {
  return (
    <Image
      src="/my-image.png"
      alt="Picture of the author"
      // width={500} automatically provided
      // height={500} automatically provided
      // blurDataURL="data:..." automatically provided
      // placeholder="blur" // Optional blur-up while loading
    />
  );
}

assets

/*
|- assets/
|-- images/
|-----open-source.png
*/
import Image from "next/image"; // https://nextjs.org/docs/app/api-reference/components/image
import openSource from "/assets/images/open-source.png";

export default function myImage() {
  return (
    <Image
      src={openSource}
      alt="Picture of the author"
      // width={500} automatically provided
      // height={500} automatically provided
      // blurDataURL="data:..." automatically provided
      // placeholder="blur" // Optional blur-up while loading
    />
  );
}

Nuxt.js

assets

By default, Nuxt uses vue-loader, file-loader and url-loader for strong assets serving.

<!--
|- assets/
  |- image.png
-->
<img src="~/assets/image.png" alt="image" />

在 css 文件中,如果需要引用 assets 目录,请使用~assets/your_image.png (不带斜杠)

background: url("~assets/banner.svg");

处理动态图像时,您需要使用 require

<img :src="require(`~/assets/img/${image}.jpg`)" />

static

automatically served

<!--
|- static/
  |- image.png
-->
<img src="/image.png" alt="image" />

CSS

Next.js

全局样式

在  pages/_app.js  文件中导入(import)CSS 文件。

/* styles.css */
body {
  font-family: "SF Pro Text", "SF Pro Icons", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
  padding: 20px 20px 60px;
  max-width: 680px;
  margin: 0 auto;
}

首先创建一个  pages/_app.js  文件(如果不存在的话)。 然后  import 该  styles.css  文件。

import "../styles.css";

// 新创建的 `pages/_app.js` 文件中必须有此默认的导出(export)函数
export default function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

这些样式 (styles.css) 将应用于你的应用程序中的所有页面和组件。 由于样式表的全局特性,并且为了避免冲突,你应该 只在  pages/_app.js文件中导入(import)样式表

从 node_modules 目录导入(import)样式

对于全局样式表(例如 bootstrap 或 nprogress),你应该在 pages/_app.js 文件中对齐进行导入(import)。

对于导入第三方组件所需的 CSS,可以在组件中进行。

// components/ExampleDialog.js
import { useState } from "react";
import { Dialog } from "@reach/dialog";
import VisuallyHidden from "@reach/visually-hidden";
import "@reach/dialog/styles.css";

function ExampleDialog(props) {
  const [showDialog, setShowDialog] = useState(false);
  const open = () => setShowDialog(true);
  const close = () => setShowDialog(false);

  return (
    <div>
      <button onClick={open}>Open Dialog</button>
      <Dialog isOpen={showDialog} onDismiss={close}>
        <button className="close-button" onClick={close}>
          <VisuallyHidden>Close</VisuallyHidden>
          <span aria-hidden>×</span>
        </button>
        <p>Hello there. I am a dialog</p>
      </Dialog>
    </div>
  );
}

组件级 CSS

Next.js 通过 [name].module.css 文件命名约定来支持 CSS 模块 。

CSS 模块通过自动创建唯一的类名从而将 CSS 限定在局部范围内。 这使您可以在不同文件中使用相同的 CSS 类名,而不必担心冲突。

此行为使 CSS 模块成为包含组件级 CSS 的理想方法。 CSS 模块文件 可以导入(import)到应用程序中的任何位置。

例如,假设 components/ 目录下有一个可重用 Button 组件:

首先,创建 components/Button.module.css 文件并填入以下内容:

/* 您不必担心 .error {} 与任何其他 `.css` 或 `.module.css` 文件发生冲突! */
.error {
  color: white;
  background-color: red;
}

然后,创建 components/Button.js 文件,导入(import)并使用上述 CSS 文件:

import styles from "./Button.module.css";

export function Button() {
  return (
    <button
      type="button"
      // 请注意如何将error类作为导入的`styles`对象的属性进行访问className={styles.error}
    >
      Destroy
    </button>
  );
}

CSS 模块是一项 可选功能,仅对带有 .module.css 扩展名的文件启用

对 Sass 的支持: Next.js 允许你导入(import).module.scss 或 .module.sass 扩展名来使用 Sass。

Nuxt.js

v2

全局设置 CSS 文件/模块/库(包含在每个页面中)

export default {
  css: [
    // Load a Node.js module directly (here it's a Sass file)
    "bulma",
    // CSS file in the project
    "~/assets/css/main.css",
    // SCSS file in the project
    "~/assets/css/main.scss",
  ],
};

v3

全局样式

<!-- pages/index.vue-->
<script>
// Use a static import for server-side compatibility
import "~/assets/css/first.css";

// Caution: Dynamic imports are not server-side compatible
import("~/assets/css/first.css");
</script>
<style lang="scss">
@use "~/assets/scss/main.scss";
</style>
  • 使用预处理器
export default defineNuxtConfig({
  css: ["~/assets/scss/main.scss"],
});
  • 在预处理的文件中注入代码
export default defineNuxtConfig({
  vite: {
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: '@use "~/assets/_colors.scss" as *;',
        },
      },
    },
  },
});

组件级别 CSS

<template>
  <div class="example">hi</div>
</template>

<style lang="scss" scoped>
  /* 单独设置组件的样式 */
  .example {
    color: red;
  }
</style>
<style lang="scss">
  /* 全局 的 css */
  .dialog {
    color: red;
  }
</style>

Fetch-On-Server

Next.js

getInitialProps can only be used in the default export of every page

>= v9.3

SWR

推荐的客户端请求 React Hook:

import useSWR from "swr";

const fetcher = (url) => fetch(url).then((res) => res.json());

function Profile() {
  const { data, error } = useSWR("/api/user", fetcher);

  if (error) return <div>failed to load</div>;
  if (!data) return <div>loading...</div>;
  return <div>hello {data.name}!</div>;
}

getStaticProps

Next.js 将在构建时使用 getStaticProps 返回的 props 预渲染此页面 (SSG 静态渲染)

// posts will be populated at build time by getStaticProps()
function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  );
}

// This function gets called at build time on server-side.
// It won't be called on client-side, so you can even do
// direct database queries. See the "Technical details" section.
export async function getStaticProps() {
  // Call an external API endpoint to get posts.
  // You can use any data fetching library
  const res = await fetch("https://.../posts");
  const posts = await res.json();

  // By returning { props: { posts } }, the Blog component
  // will receive `posts` as a prop at build time
  return {
    props: {
      posts,
    },
  };
}

export default Blog;

getServerSideProps

Next.js 将使用 getServerSideProps 返回的数据在每个请求上预渲染此页面。(SSR 服务端渲染)

function Page({ data }) {
  // Render data...
}

export async function getServerSideProps() {
  const res = await fetch(`https://.../data`);
  const data = await res.json();

  return { props: { data } };
}

export default Page;

Nuxt.js

v2

<template>
  <div v-if="$fetchState.error">Something went wrong 😭</div>
  <div v-if="$fetchState.pending">Loading...</div>
  <div v-else>
    <h1>{{ post.title }}</h1>
    <pre>{{ post.body }}</pre>
    <button @click="$fetch">Refresh</button>
  </div>
</template>

<script>
  import fetch from "node-fetch";

  export default {
    data() {
      return {
        post: {},
      };
    },
    async fetch() {
      this.post = await this.$http.$get("xxx");
    },
    fetchOnServer: true,
  };
</script>

v3

客户端请求

$fetch 用户纯客户端渲染, 在 SSR 期间,数据会被获取两次,一次在服务器上,一次在客户端上。

<script setup lang="ts">
function contactForm() {
  $fetch("/api/contact", {
    method: "POST",
    body: { hello: "world " },
  });
}
</script>

<template>
  <button @click="contactForm">Contact</button>
</template>

服务端渲染

useFetch 和 useAsyncData 组合函数用于服务端和客户端同构的环境。

<script setup lang="ts">
  const { data } = await useFetch("/api/data");

  async function handleFormSubmit() {
    const res = await $fetch("/api/submit", {
      method: "POST",
      body: {
        // My form data
      },
    });
  }
</script>

<template>
  <div v-if="data == null">No data</div>
  <div v-else>
    <form @submit="handleFormSubmit">
      <!-- form input tags -->
    </form>
  </div>
</template>

State-Management

Next.js

TODO:

Nuxt.js

v2

Nuxt2 使用 fetch 方法获取数据,存储在 store 中。

store/website.js

export const state = () => ({
  config: {},
});

export const getters = {};

export const mutations = {};

export const actions = {
  async queryWebsiteData({ state, commit, rootState }, params) {
    try {
      const res = await import(`@/assets/json/website.json}`);
      state.config = res;
    } catch (err) {
      console.log(err);
    }
  },
};

pages/index.vue

export default {
  name: "Indexhome",
  data() {
    return {};
  },
  fetch({ app }) {
    return app.store.dispatch("website/queryWebsiteData", "server");
  },
};

v3

nuxtjs.org.cn/docs/gettin…

Nuxt 提供强大的状态管理库和 useState 组合式 API,用于创建响应式且支持服务器端渲染的共享状态。

useState 是一个支持服务器端渲染的 ref 替代方案。它的值将在服务器端渲染后(客户端水合期间)保留,并通过唯一的键在所有组件之间共享。

<!-- 大多数情况下,您希望使用异步解析的数据初始化状态。您可以使用带有 callOnce 工具函数的 app.vue 组件来实现。 -->
<script setup lang="ts">
  const websiteConfig = useState("config");

  await callOnce(async () => {
    websiteConfig.value = await $fetch("https://my-cms.com/api/website-config");
  });
</script>
  • 与 Pinia 结合使用

我们利用 Pinia 模块 创建全局存储并在整个应用程序中使用它。

stores/website.ts

export const useWebsiteStore = defineStore("websiteStore", {
  state: () => ({
    name: "",
    description: "",
  }),
  actions: {
    async fetch() {
      const infos = await $fetch("https://api.nuxt.com/modules/pinia");

      this.name = infos.name;
      this.description = infos.description;
    },
  },
});

app.vue

<script setup lang="ts">
  const website = useWebsiteStore();

  await callOnce(website.fetch);
</script>

<template>
  <main>
    <h1>{{ website.name }}</h1>
    <p>{{ website.description }}</p>
  </main>
</template>

Context

Next.js

如果你导出从页面 async 调用的函数 getStaticProps,Next.js 将在构建时使用返回的 props 预渲染此页面 getStaticProps。

export async function getStaticProps(context) {
  return {
    props: {}, // will be passed to the page component as props
  };
}

该 context 参数是一个包含以下键的对象:

  • params 包含使用动态路由的页面的路由参数。例如,如果页面名称为[id].js,则将 params 看起来像{ id: ... }。一般与 getStaticPaths 一起使用 。
  • preview 是 true 页面是否处于预览模式,undefined。
  • previewData 包含 设置的预览数据 setPreviewData。
  • locale 包含活动语言环境(如果您已启用国际化路由)。
  • locales 包含所有支持的语言环境(如果您已启用国际化路由)。
  • defaultLocale 包含配置的默认语言环境(如果您已启用国际化路由)。

getStaticProps 应返回一个具有以下内容的对象:

  • props- 一个可选对象,其中包含页面组件将接收的 props。它应该是一个可序列化的对象
  • revalidate-可选的秒数,在此秒数后可以重新生成页面。默认为 false。revalidate 这 false 意味着没有重新验证,因此页面将缓存为已构建,直到您下次构建。
  • notFound-可选布尔值,允许页面返回 404 状态和页面。
function Page({ data }) {
  // Render data...
}

export async function getStaticProps(context) {
  const res = await fetch(`https://.../data`);
  const data = await res.json();

  if (!data) {
    return {
      notFound: true,
    };
  }

  return {
    props: { data }, // will be passed to the page component as props
  };
}

getStaticProps 只能从页面导出。您无法从非页面文件导出它。

Nuxt.js

v2

nuxt.config.js

export default {
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [
    {
      src: "~/plugins/plugin/main.js",
      ssr: true,
    },
  ],
};

plugin/main.js

export default ({ app, store }, inject) => {
  // Inject $hello(msg) in Vue, context and store.
  inject("hello", (msg) => console.log(`Hello ${msg}!`));

  // vuex缓存
  !isServer() &&
    createPersistedState({
      key: `vuex_store`,
      storage: window.sessionStorage,
    })(store);
};

默认情况下存在的所有上下文:

function (context) { // Could be asyncData, nuxtServerInit, ...
  // Always available
  const {
    app,
    store,
    route,
    params,
    query,
    env,
    isDev,
    isHMR,
    redirect,
    error,
    $config
  } = context

  // Only available on the Server-side
  if (process.server) {
    const { req, res, beforeNuxtRender, beforeSerialize } = context
  }

  // Only available on the Client-side
  if (process.client) {
    const { from, nuxtState } = context
  }
}

pages/index.vue

export default {
  mounted() {
    this.$hello("mounted");
    // will console.log 'Hello mounted!'
  },
  asyncData(context) {
    // Universal keys
    const { app, store, route, params, query, env, isDev, isHMR, redirect, error, $hello } = context;
    // Server-side
    if (process.server) {
      const { req, res, beforeNuxtRender } = context;
    }
    // Client-side
    if (process.client) {
      const { from, nuxtState } = context;
    }

    $hello("asyncData");
    // If using Nuxt <= 2.12, use 👇
    app.$hello("asyncData");

    return { project: "nuxt" };
  },
};

v3

useNuxtApp 访问 Nuxt 应用程序的共享运行时上下文。

<!--
app.vue -->
<script setup lang="ts">
  const nuxtApp = useNuxtApp();

  nuxtApp.provide("hello", (name) => `Hello ${name}!`);

  console.log(nuxtApp.$hello("name")); // Prints "Hello name!"
</script>
// https://nuxt.com/docs/guide/going-further/internals#the-nuxtapp-interface

const nuxtApp = {
  vueApp, // the global Vue application: https://vuejs.org/api/application.html#application-api

  versions, // an object containing Nuxt and Vue versions

  // These let you call and add runtime NuxtApp hooks
  // https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/nuxt.ts#L18
  hooks,
  hook,
  callHook,

  // Only accessible on server-side
  ssrContext: {
    url,
    req,
    res,
    runtimeConfig,
    noSSR,
  },

  // This will be stringified and passed from server to client
  payload: {
    serverRendered: true,
    data: {},
    state: {}
  }

  provide: (name: string, value: any) => void
}

Source Map

Next.js

// next.config.js
module.exports = {
  productionBrowserSourceMaps: true,
};

Nuxt.js

v2

export default {
  build: {
    extend(config, { isClient }) {
      // 为 客户端打包 进行扩展配置
      if (isClient) {
        // config.devtool = "eval-source-map"; //需要时才放开
        // 非生产环境开启 source-map
        if (process.env.PATH_TYPE !== "production") {
          config.devtool = "source-map";
          Object.assign(config.output, {
            devtoolModuleFilenameTemplate: "yanyue404://[resource-path]",
          });
        }
      }
    },
  },
};

v3

export default defineNuxtConfig({
  // or sourcemap: true
  sourcemap: {
    server: true,
    client: true,
  },
});

Environment Variables

Next.js

nextjs.org/docs/app/bu…

以 env.$(NODE_ENV) 来加载环境变量,NODE_ENV 允许有以下几种值。

.env.development
.env.production
.env.test

自动加载

Js 内置支持将环境变量从. env* 文件加载到 process.env 中

// app/api/route.js

export async function GET() {
  const db = await myDB.connect({
    host: process.env.DB_HOST,
    username: process.env.DB_USER,
    password: process.env.DB_PASS,
  });
  // ...
}

手动加载

npm install @next/env
// envConfig.ts
import { loadEnvConfig } from "@next/env";

const projectDir = process.cwd();
loadEnvConfig(projectDir);

具体使用:

// orm.config.ts
import "./envConfig.ts";

export default defineConfig({
  dbCredentials: {
    connectionString: process.env.DATABASE_URL!,
  },
});

Nuxt.js

v2

package.json

{
  "scrips": {
    "serve": "cross-env PATH_TYPE=development nuxt",
    "generate": "cross-env PATH_TYPE=production nuxt generate"
  }
}

nuxt.config.js

export default {
  mode: "universal",
  env: {
    PATH_TYPE: process.env.PATH_TYPE,
  },
};

在 页面 js 文件中使用:

const isProduction = process.env.PATH_TYPE === "production";

v3

.env.development
.env.production
.env.test
# 指向要加载的另一个 .env 文件,相对于根目录。
npx nuxi dev --dotenv .env.local

package.json

{
  "scripts": {
    "serve": "nuxt dev --dotenv .env.dev",
    "generate:dev": "nuxt generate --dotenv .env.dev",
    "generate:production": "nuxt generate --dotenv .env.production",
    "build": "nuxt build",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  }
}

在开发模式下更新 .env 时,Nuxt 实例会自动重新启动以将新值应用于 process.env。

const env = import.meta.env;

在您的应用程序代码中,您应该使用 运行时配置 而不是普通环境变量。

要将配置和环境变量公开到应用程序的其余部分,您需要在您的 nuxt.config 文件中使用 runtimeConfig 选项定义运行时配置。

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // The private keys which are only available within server-side
    apiSecret: "123",
    // Keys within public, will be also exposed to the client-side
    public: {
      apiBase: "/api",
    },
  },
});

当将 apiBase 添加到 runtimeConfig.public 时,Nuxt 会将其添加到每个页面有效负载中。我们可以在服务器端和浏览器端普遍访问 apiBase。

const runtimeConfig = useRuntimeConfig();

console.log(runtimeConfig.apiSecret);
console.log(runtimeConfig.public.apiBase);

Reference