目录

32.部署篇静态导出

前言

我们正常部署 Next.js 应用都是需要在服务器上起一个 server 来实现,但其实也可以不这样做。Next.js 也支持类似于静态网站或者单页应用(SPA)的形式。这就是本章要讲解的静态导出(Static Exports)功能。

它的效果是这样的:当你执行构建(npm run build)后,Next.js 会为每一个路由生成一个单独的 HTML 文件,以及相关使用的 CSS、JavaScript、图片等资源,这些内容会放到你指定的文件夹下,你可以将这个文件夹下的内容直接部署使用。

但效果跟传统的静态网站不一样的是,Next.js 生成的网站效果类似于 SPA,即路由虽然发生变化,但页面不会加载刷新。

让我们看看怎么实现静态导出吧!

1. 配置

要启用静态导出,修改 next.config.js 的导出模式:

// next.config.js
/**
 * @type {import('next').NextConfig}
 */
const nextConfig = {
  output: 'export',
  // 可选: 默认导出目录为 out,distDir 可以更改这个目录名 `out` -> `dist`
  // distDir: 'dist',
}
 
module.exports = nextConfig

运行 next build 后,Next.js 会创建一个名为 out 的文件夹包含该应用所需的 HTML、CSS、JS 等资源。

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0575099c35f3432bac5e2af8734dca71~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=3290\&h=634\&s=336366\&e=png\&b=1d1f20

2. 行为

为了支持静态导出,Next.js 的核心部分都进行了改造,让我们了解一下这些核心部分在静态导出的时候的行为和特性吧:

2.1. 服务端组件

当配置静态导出运行 next build 的时候,app 目录下的服务端组件会在构建期间运行,这个过程类似于传统的静态站点生成。

这些组件会渲染成静态的 HTML 文件(用于初始化页面加载)和客户端路由导航之间的静态 payload。当使用静态导出时,服务端组件不需要进行任何更改,除非它们使用了动态服务端函数,下文会讲到在静态导出中不支持的功能。

// app/page.jsx
export default async function Page() {
  // 在 `next build` 的时候 fetch 请求会执行
  const res = await fetch('https://jsonplaceholder.typicode.com/posts/1')
  const data = await res.json()
 
  return <main>{data.title}</main>
}

编译变成 HTML 文件后:

<!DOCTYPE html>
<html>

<head>
  <meta charSet="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
	///...
</head>

<body>
  <main>sunt aut facere repellat provident occaecati excepturi optio reprehenderit</main>
  //...
</body>

</html>

2.2. 客户端组件

页面不一定总是静态资源,有点时候,也需要在页面打开或者发生交互的时候获取数据,此时就需要使用客户端组件。如果要在客户端获取数据,可以使用带有 SWR 的客户端组件记忆化请求:

'use client'
// app/other/page.js
import useSWR from 'swr'
 
const fetcher = (url) => fetch(url).then((r) => r.json())
 
export default function Page() {
  const { data, error } = useSWR(
    `https://jsonplaceholder.typicode.com/posts/1`,
    fetcher
  )
  if (error) return 'Failed to load'
  if (!data) return 'Loading...'
 
  return data.title
}

因为路由导航发生在客户端,其行为类似于传统的 SPA。举个例子:

// app/page.js
import Link from 'next/link'
 
export default function Page() {
  return (
    <>
      <h1>Index Page</h1>
      <p>
        <Link href="/other">Other Page</Link>
      </p>
    </>
  )
}

现在我们运行 npm run build 执行构建,然后对导出的 out 文件夹起一个服务(VSCode 可以用 Live Server 这个插件),你会发现它的表现类似于 SPA:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/088c675b6c0f483d83cadd3ae7425cc6~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=421\&h=309\&s=19314\&e=gif\&f=17\&b=fefefe

这是为了避免在客户端加载不必要的 JavaScript 代码,从而减小 bundle 的大小,实现更快的页面加载。

但它跟传统的 SPA 还不一样。因为传统 SPA 的 HTML 是一个“空的”,只有一个可以挂载的根节点比如这样:

<div id='root'></div>
<script src="app.js" />

但 Next.js 构建出来的无论是服务端组件还是客户端组件,都是有 HTML 内容的。

2.3. 图片优化

在使用静态导出的时候,并不能使用带有默认 loader 的 next/image 组件,举个例子:

// app/page.js
import Image from 'next/image'
import profilePic from './me.png'
 
export default function Page() {
  return (
    <Image
      src={profilePic}
      alt="Picture of the author"
    />
  )
}

开发模式下会出现错误提示:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b87cea08bfec4f12bd0243cf2a50e401~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1898\&h=320\&s=98953\&e=png\&b=ffffff

你可以自定义配置一个 loader,比如使用 Cloudinary(提供基于云的图像和视频管理服务。用户能够上载,存储,管理,操纵和交付用于网站和应用程序的图像和视频)。

首先配置 next.config.js

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',
  images: {
    loader: 'custom',
    loaderFile: './my-loader.js',
  },
}
 
module.exports = nextConfig

其次,添加自定义 loader 的代码:

// my-loader.js
export default function cloudinaryLoader({ src, width, quality }) {
  const params = ['f_auto', 'c_limit', `w_${width}`, `q_${quality || 'auto'}`]
  return `https://res.cloudinary.com/demo/image/upload/${params.join(
    ','
  )}${src}`
}

现在,你就可以使用 next/image 组件:

// app/page.js
import Image from 'next/image'
 
export default function Page() {
  return <Image alt="turtles" src="/turtles.jpg" width={300} height={300} />
}

此时图片能够正确展示:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/526da7f547d8452a80140affc5571e77~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1790\&h=742\&s=838758\&e=png\&b=2b2b2b

你可能会想:“好麻烦!我就想简单展示个图片,还要去找个图片服务吗?”,其实你也可以直接使用 <img> 标签,但对应会失去 next/image 组件带来的优化。比如把图片放在 /public 下后使用 img 标签读取也可以正常展示:

// app/page.js
export default function Page() {
  return <img width="300" src="/image.png" />
}

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2e84d8d5821148ac9c6e6d0a550953ce~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1796\&h=526\&s=439599\&e=png\&b=2c2b2b

2.4. 路由处理程序

路由处理程序在运行 next build 的时候会渲染一个静态的响应。只有在 GET 请求被支持。这可以用于生成静态的 HTML、JSON、TXT 或者其他文件。举个例子:

// app/data.json/route.js
export async function GET() {
  return Response.json({ name: 'Lee' })
}

app/data.json/rout.js 会在 next build 的时候渲染成一个静态的名为 data.json的文件:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/16c4393ca4b24e448fee96ce1292254c~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1306\&h=586\&s=140151\&e=png\&b=1e2022

如果你需要从传入的请求中读取动态值,那就不能使用静态导出了。

2.5. 浏览器 API

在运行 next build 的时候,客户端组件会被预渲染成 HTML。因为 Web APIs 像 windowlocalStoragenavigator 在服务端是不可用的,所以你需要保证仅在浏览器中运行的时候才访问这些 API,举个例子:

'use client';
 
import { useEffect } from 'react';
 
export default function ClientComponent() {
  useEffect(() => {
    // 现在可以访问 `window`
    console.log(window.innerHeight);
  }, [])
 
  return ...;
}

3. 不支持的功能

需要 Nodejs server 的功能或者在构建过程中需要计算的动态逻辑都是不支持的,具体有:

next dev 的时候尝试使用这些功能都会导致错误。

4. 部署

使用静态导出,Next.js 可以部署和托管在任何能处理 HTML、CSS 、JS 静态资源的 Web 服务器上。

运行 next build 的时候,Next.js 会生成静态文件到 out 文件夹下,举个例子,假如你有这些路由:

  • /
  • /blog/[id]

运行 next build后,Next.js 会生成以下文件:

  • /out/index.html
  • /out/404.html
  • /out/blog/post-1.html
  • /out/blog/post-2.html

post-1.html、post-2.html 这些文件是因为定义了 generateStaticParams,不使用该函数也无法静态导出。

但此时路由跳转的时候会有一个问题,就比如从 /跳转到 /other,第一次没有问题,因为页面类似于 SPA,但是刷新 /other就会导致错误,原本的 /other 被编译成了 other.html,访问 /other.html 才会正常访问。让我们看下演示:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/541d355a7bd3484295dca0d94edd7160~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=590\&h=302\&s=80542\&e=gif\&f=52\&b=fefefe

为了解决这个问题,如果你使用了比如 Nginx,那你可以配置一个从传入请求到正确文件的重写:

# nginx.conf
server {
  listen 80;
  server_name acme.com;
 
  root /var/www/out;
 
  location / {
      try_files $uri $uri.html $uri/ =404;
  }
 
  # 当配置 `trailingSlash: false`这是必要的
  # 当配置 `trailingSlash: true`可以省略
  location /blog/ {
      rewrite ^/blog/(.*)$ /blog/$1.html break;
  }
 
  error_page 404 /404.html;
  location = /404.html {
      internal;
  }
}

参考链接

  1. https://nextjs.org/docs/app/building-your-application/deploying/static-exports