随着 Next.js 12.2 的发布,我们可以选择使用哪个运行时](https://nextjs.org/docs/advanced-features/react-18/switchable-runtime)在 Next.js 服务器上呈现页面或提供 API。

这意味着什么?好吧,在过去,Next.js 服务器只能在 Node.js 运行时中运行,这意味着它与Cloudflare Worker或几乎所有仅支持V8 运行时的无服务器边缘计算平台不兼容。你可以在这里阅读更多关于它们的区别。如果您对技术细节不感兴趣,可以节省 10 分钟的阅读时间——在 V8 运行时运行会更快、更便宜。 Node.js 运行时需要如此多的资源,以至于将它们部署在边缘是只有大型云提供商才能负担得起的。据我所知,唯一提供此服务的公司是亚马逊。

当然,您可以零配置在Vercel上部署您闪亮的 Next.js 网站,但这比 Cloudflare 贵得多。与大多数云提供商一样,Vercel 将根据消耗的带宽向您收费,并且坐稳了,它是 $40/100GB。实际上,考虑到netlify(它还支持以零配置部署 Next.js)将要求 55 美元,这并不是那么昂贵。在 Cloudflare 上,带宽始终是免费的。

不支持在 Cloudflare 页面上部署 Next.js 吗?

嗯,是的,也不是。

Next.js 有很多方法可以生成网页。您可以在构建网站时预渲染所有网页。这样,您的网站是完全静态的,您可以将其部署在Cloudflare Pages并且它是免费的(除非您正在疯狂地更新您的网站)。

但是,如果您的网页太多以至于构建它们需要几个小时,您可以使用增量静态再生 (ISR)。在这种模式下,服务器将在任何用户第一次访问它时生成网页,并缓存该页面以供将来请求。或者,如果您想提供动态页面,但出于 SEO 原因希望将其呈现在服务器上,同时仍为用户保留 SPA 体验,则可以使用服务器端呈现 (SSR)。

您不能使用静态生成来执行 ISR 或 SSR,您需要在某个地方运行代码以在用户访问它们时生成网页。而Cloudflare Pages 确实通过 Cloudflare Worker 提供了动态托管,您可以在其中在 v8 运行时运行 javascript 或 wasm,但不能在 Node.js 运行时运行。因此,部署具有完整功能的 Next.js 网站是不可能的(在某种程度上是)。

Next.js 的 Serverless 支持

既然 Next.js 现在可以使用 V8 运行时,我们可以使用 Cloudflare Pages 来部署动态 Next.js 网站,对吧?

理论上,是的。

Cloudflare Worker 遵循无服务器模式,其中没有长时间运行的进程处理请求。为每个请求启动一个新实例(可能被缓存)。

Next.js 框架本身官方只支持一种动态服务模式,通过运行next start来启动 Next.js 服务端,运行时间较长。

serverless目标和standalone输出

曾经有一个serverless目标可以为一个页面生成一个独立的 javascript 文件,因此我们可以将它们与轻量级包装器一起使用来生成页面,但是已弃用有利于新的输出文件跟踪功能。

有了这个新功能,Next.js 将跟踪每个页面的依赖关系。结合output: 'standalone'配置,Next.js 会将所有需要的依赖项复制到输出文件夹,并使用最小的server.js文件代替next start,因此部署起来更小。但这是一个完整的 Next.js 服务器,并且该服务器与 V8 运行时不兼容,它会扫描磁盘以查找配置和源文件,然后构建路由映射。

边缘运行时

那么新的边缘运行时呢?通过启用边缘运行时我们会得到什么?

为了更好地理解这一点,让我们首先尝试弄清楚“Next.js running in edge runtime”的真正含义。

看了一大堆源码,终于知道这意味着Next.js服务器会在edge runtime中执行API Routes(大部分是你写的代码)来处理所有请求。 Next.js 期望始终有一个 Next.js 服务器在运行,这只是 Next.js 服务器在什么运行时执行您的代码的问题。

那么我们如何在 Cloudflare Worker 上运行服务器呢?

好吧,我们不能,至少不能直接。

如果您看一下vercel或netlify如何进一步处理next build的构建工件以使其在其服务器上工作,您会发现这不是一项琐碎的任务。

让我们试着找出需要做什么。

以下所有内容均假设您具有使用 Next.js 和 Cloudflare Pages Function 的一定经验

注意我认为Next.js在文档中没有提供关于输出](https://nextjs.org/docs/deployment#nextjs-build-api)的[内容的任何细节,所以下面的大部分内容应该被认为是Next.js版本(12.2.3)的内部实现。 (最新的12.2.5有个小问题)

Next.js 构建工件是如何组织的

在以下位置查找源代码

https://github.com/zhuhaow/nextjs-cloudflare-page-demo

让我们首先创建一个包含 SSG、ISR、SSR 页面和边缘 API 路由的组合的演示网站。

请注意,该页面最终也将编译为边缘 API 路由。

pages
├── _app.tsx
├── api
│   └── hello.ts
├── dynamic
│   └── [page].tsx
├── index.tsx
└── static
    └── [page].tsx

我们有一个完全静态的页面index

带有后备的静态页面,因此我们可以测试 ISR。

import { GetStaticPaths, GetStaticProps } from "next";

interface Props {
    page: string;
}

export const getStaticProps: GetStaticProps<Props> = async (context) => {
    const page = context?.params?.page as string;

    return {
        props: {
            page
        },
    };
};

export const getStaticPaths: GetStaticPaths = async () => {
    return {
        paths: ["SSG"].map((page) => ({ params: { page } })),
        fallback: true,
    };
};

const Page = ({ page }: Props) => {
    return (
        <div>
            <h1>Page {page}</h1>
        </div>
    )
}

export default Page;

export const config = {
  runtime: 'experimental-edge',
};

和一个简单的动态页面

import { GetServerSideProps } from "next";

export const getServerSideProps: GetServerSideProps = async ({
    query,
}) => {
    const page = query.page as string;

    return { props: { page } };
};

const Page = ({ page }: { page: string }) => {
    return (
        <div>
            <h1>Page {page}</h1>
        </div>
    )
}

export default Page;

export const config = {
  runtime: 'experimental-edge',
};

和 API

import type { NextApiRequest } from 'next'

export default function handler(
  _req: NextApiRequest,
): Response {
  return new Response('Hello World');
}

export const config = {
  runtime: 'experimental-edge',
};

我们来看看next build生成了什么。

.next
├── BUILD_ID
├── build-manifest.json
├── export-marker.json
├── images-manifest.json
├── next-server.js.nft.json
├── package.json
├── pages
│   ├── api
│   │   └── hello.js.nft.json
│   ├── dynamic
│   │   └── [page].js.nft.json
│   └── static
│       └── [page].js.nft.json
├── prerender-manifest.json
├── react-loadable-manifest.json
├── required-server-files.json
├── routes-manifest.json
├── server
│   ├── chunks
│   │   ├── 675.js
│   │   ├── 783.js
│   │   └── font-manifest.json
│   ├── edge-chunks
│   │   ├── 566.js
│   │   └── 566.js.map
│   ├── edge-runtime-webpack.js
│   ├── edge-runtime-webpack.js.map
│   ├── font-manifest.json
│   ├── middleware-build-manifest.js
│   ├── middleware-manifest.json
│   ├── middleware-react-loadable-manifest.js
│   ├── pages
│   │   ├── 404.html
│   │   ├── 500.html
│   │   ├── _app.js
│   │   ├── _app.js.nft.json
│   │   ├── _document.js
│   │   ├── _document.js.nft.json
│   │   ├── _error.js
│   │   ├── _error.js.nft.json
│   │   ├── api
│   │   │   ├── hello.js
│   │   │   └── hello.js.map
│   │   ├── dynamic
│   │   │   ├── [page].js
│   │   │   └── [page].js.map
│   │   ├── index.html
│   │   ├── index.js.nft.json
│   │   └── static
│   │       ├── [page].js
│   │       └── [page].js.map
│   ├── pages-manifest.json
│   └── webpack-runtime.js
└── trace

我省略了cachestatic文件夹中的内容。

文件看似很多,但目的却很明确。

.next/static文件夹(在树中省略)包含应该按原样提供的资产,它们是由预处理器、捆绑器等生成的内容,而不是由 Next.js 服务器生成的。那里没有动态内容。

.next/server文件夹中,我们可以找到服务器生成的所有内容,包括运行next build时预先生成的index.html404.html等。

正如我们之前提到的,还有.nft.json文件(输出文件跟踪)跟踪每个 javascript 文件的依赖关系。

.next/server/pages下的 js 文件最重要,但是与完整的 Next.js 服务器相比,这些脚本的作用有多大?

答案不是很令人兴奋。

.next/server/pages中的 js 文件都是API Routes其中每个只是处理对一个 HTTP 端点的请求,仅此而已。

如果我们启动 Next.js 服务器(next start),它将加载这些文件和json文件,其中包含路由映射和服务器配置。但是服务器不是可以在 V8 运行时上运行的东西。

虽然技术上可以让无头 Next.js 服务器以无服务器模式在边缘运行,但 Next.js 可能不打算这样做。

在 Cloudflare 页面上运行 API 路由

让我们看看我们是否可以在没有 Next.js 服务器的情况下在 Cloudflare Pages 上启动并运行 API 路由。

如果我们打开.next/server/pages/static/[page].js,我们会看到一个由 webpack 生成的非常短的文件(<150 行),这表明这个文件不能独立工作。我们稍后将讨论的脚本中没有捆绑依赖项是有充分理由的。

Next.js 编译器使用 webpack 进行块拆分,现在,我们可以使用对应的nft文件来查找哪些文件应该是required 来启动和运行边缘函数。

我不确定这是否是使用 webpack 块的正确方法,但我找不到任何谈论这个的资源。所以我只是通过查看块的源代码来解决这个问题。_

让我们创建cf/functions/api/hello.js

global._ENTRIES = global._ENTRIES || {};
global.process = global.process || { env: { NODE_DEBUG: false } }

require("../../../.next/server/pages/api/hello");
require("../../../.next/server/edge-runtime-webpack");

export async function onRequest(context) {
    return (await global._ENTRIES["middleware_pages/api/hello"].default({ request: context.request })).response;
}

那么这里发生了什么?

首先我在cs/functions下创建文件,之后我们可以使用wranglercf本地启动一个dev Cloudflare Pages服务器进行测试。

接下来,我定义 webpack 将加载模块的_ENTRIES。我还填充了process,因为它在 V8 运行时中不可用但需要(请参阅此处了解 Next.js 的作用)。

然后我加载块模块api/hello,最后加载edge-runtime-webpack将所有模块加载到_ENTRIES中。

这样,如果我们有一个上下文来处理请求,我们就可以在这个上下文中加载我们想要处理的部分,而每个条目中没有重复的代码。这对于 Cloudflare 非常重要,因为 Pages 会将所有内容捆绑到一个文件中,如果入口文件已经捆绑,那么最终工作包中将会有很多重复。

经过一些测试和试验以找出导出函数的签名,然后我们有一个包装器,它简单地将 Cloudflare 请求转发到 Next.js API Route。

现在,如果我们在cf/中运行wrangler pages dev .,然后前往http://localhost:8788/api/hello,我们将看到可爱的“Hello World”。

然而,对页面做同样的事情,现在还行不通。

global._ENTRIES = global._ENTRIES || {};
global.process = global.process || { env: { NODE_DEBUG: false } }

require("../../../.next/server/edge-chunks/553");
require("../../../.next/server/pages/dynamic/[page]");
require("../../../.next/server/edge-runtime-webpack");

export async function onRequest(context) {
    return (await global._ENTRIES["middleware_pages/dynamic/[page]"].default({ request: context.request })).response;
}

由于 Cloudflare Pages 的流 API 存在问题,您将看到一个错误(请参阅此处的讨论)。但我希望它在 Cloudflare 端修复后能够工作。

在 Cloudflare 上运行完整的 Next.js 站点

因此,假设流 API 问题已得到修复,如果我们自动化此过程并将所有边缘功能映射到 Cloudflare Pages 功能,我们现在可以在 Cloudflare 上运行 Next.js 网站吗?

也许。这取决于您使用的 Next.js 功能。

请记住,我们没有 Next.js 服务器,我们只有 API Routes 处理程序以及 Cloudflare 提供的任何内容(例如基于文件层次结构的自动路由,因此api/hellofunctions/api/hello.js处理)。

为了完全支持所有 Next.js 功能,我们需要使用 Cloudflare Pages 中间件来完成服务器应该做的所有事情。

我会列出一些我能想到的开箱即用的东西。

工人规模

这可能是我能看到的最直接的问题。

当我在本地启动开发服务器时,有一行日志显示:

[pages:inf] Worker reloaded! (0.94MiB)

我不确定是否可以进行任何进一步的优化来缩小大小,但是worker 大小的限制是 1MB(Cloudflare Pages 会将所有功能组合到一个 worker 脚本中),我们还没有导入任何东西然而。

通过 Cloudflare 确认日志中显示的大小未缩小。我尝试缩小捆绑包,Next.js 依赖项的大小应该是 ~460KB。

图片

根据您打算如何使用它,这可能是也可能不是问题。

如果你只是禁用图像优化,那么你很高兴。

但是,如果您想要开箱即用的图像优化工作,Cloudflare Image Resizing需要 Pro 计划。免费计划没有配额。 (我敢让你在 10 分钟内在 Cloudflare 的网站上找到它的价格详情。)

还需要一些额外的代码来验证是否允许请求的图像。

请求对象

我们需要将请求对象扩展为NextRequest。 Cloudflare 已经提供了所有信息。

路由

静态文件应该是按原样提供的,并且默认具有更高的路由优先级,所以不用担心。

动态页面和 API 路由都放置在正确的位置,所以它应该可以正常工作。

但是,有一些陷阱。

页面过渡

当使用next/linknext/router转换到新页面时,页面不会刷新,而是请求包含呈现该页面所需的所有必要数据的json文件。例如,在我们的示例中,当我们从index过渡到dynamic/123时,浏览器实际上向/_next/data/[BUILD_ID]/dynamic/123.json?page=123发出请求。所以我们需要确保我们正在映射要由正确的处理程序处理的路径。

基本路径和语言环境

此类信息应通过填充NextRequest对象传递给处理程序。我们需要通过从 URL 中剥离这些部分来确保将请求映射到正确的处理程序。

缓存

Cloudflare Workers 支持缓存但不像 Next.js 服务器那样隐式支持,我们可以在包装器中完成它们,而且应该非常简单。但我不确定stale-while-revalidate是否可行。

中间件

Next.js 中间件需要由 Cloudflare Pages 中间件处理。我还没有调查过这个。但这应该是可能的。

next.config.js

Next.js 服务器的大部分行为是由next.config.js配置的。

标题

我们必须在中间件中实现它,因为它具有Cloudflare Pages config不支持的某些功能。

重写和重定向

我们必须使用中间件。 Next.js 重定向和重写比 Cloudflare 提供的配置更强大。

斜杠

中间件,再次。

结论

理论上,现在可以在 Cloudflare Pages 上托管完整的 Next.js 网站。

鉴于 API Routes 的包装器非常简单(可能我们应该将它们包装在中间件中,而不是让 wrangler 这样做),仍然缺少一个 Pages 中间件,它的作用与 Next.js 服务器完全相同,但可以在V8 运行时。

目前,在 Pages](https://discord.com/channels/595317990191398933/789155108529111069/1004452034780598284)上至少有 4 个支持 SSR 的框架[,但它们都是由框架所有者而不是 Cloudflare 提供的。鉴于 Next.js 的所有者是 vercel,而 Next.js 甚至不支持在 vercel 本身上部署,Next.js 不太可能正式支持在 Cloudflare(或任何其他平台)上部署。

我认为Cloudflare 尚未开始研究它。但好消息是ItsWendell 在不和谐中提到有一些进展。

希望它会很快准备好。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐