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
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
以 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);