首页
/ Excalidraw 接入 Next.js:with-nextjs 示例中 App Router 与 Pages Router 的完整集成方案

Excalidraw 接入 Next.js:with-nextjs 示例中 App Router 与 Pages Router 的完整集成方案

2026-09-03 16:46:37作者:田桥桑Industrious

Excalidraw(手绘风格虚拟白板)提供官方的 with-nextjs 集成示例,演示了如何将 @excalidraw/excalidraw 库嵌入一个由 create-next-app 引导的 Next.js 项目。本文以该示例为骨架,从开发服务启动、客户端组件渲染、字体资源自托管,到构建产物与 Vercel 部署配置逐层拆解,读完后你可以在自己的 Next.js 应用(App Router 或 Pages Router)中正确接入 Excalidraw 组件,并理解 SSR 场景下必须处理的几个关键细节。

示例项目定位与目录结构

with-nextjs 是 Excalidraw monorepo 下 examples/* workspace 的成员之一(见根目录 package.json 中的 workspaces 字段)。与同仓库的 with-script-in-browser 示例 不同,它演示的是"以 npm 库形式引入 Excalidraw"的集成路径,而不是通过 script 标签加载打包产物。

示例的核心文件分布如下:

值得注意的一点:该示例的 React 侧代码复用了 with-script-in-browser 中的 ExampleApp 组件,它通过 cloneElement<Excalidraw /> 注入 excalidrawAPIinitialDataUIOptions 等属性,并演示了导出(exportToSvg/exportToBlob/exportToCanvas)、更新场景、协作头像列表等 API 用法。因此阅读 Next.js 示例时,实际交互逻辑要追溯到 ExampleApp.tsx 去看。

Getting Started:启动开发服务

README 的说明,可以在 examples/with-nextjs 目录下运行以下任一命令启动开发服务器:

npm run dev
# 或
yarn dev
# 或
pnpm dev
# 或
bun dev

但结合 package.json 的 scripts 可以看到,实际的 dev 脚本比 README 描述的多了两步前置工作:

"dev": "yarn build:workspace && next dev -p 3005",
"build:workspace": "yarn build:packages && yarn copy:assets",
"copy:assets": "cp -r ../../packages/excalidraw/dist/prod/fonts ./public"

即每次启动开发服务前,会先执行:

  1. build:packages:在仓库根目录按依赖顺序构建 commonfractional-indexinglaser-pointermathelementexcalidraw 六个 ESM 包(对应根 package.json 中的 build:packages 脚本);
  2. copy:assets:将 packages/excalidraw/dist/prod/fonts 下的字体文件复制到示例的 public/ 目录,为字体自托管做准备(下文详述)。

另外,README 正文写的是打开 http://localhost:3000,但 next dev -p 3005 实际把开发服务跑在 3005 端口(README 中的超链接指向的也正是 3005);生产模式则通过 yarn build 构建、yarn start3006 端口 启动。以当前仓库脚本的实际配置为准即可。

核心集成点一:用 dynamic + ssr: false 隔离浏览器 API

Excalidraw 组件依赖 Canvas、window 等浏览器 API,在 Next.js 的服务端预渲染阶段直接导入会失败。示例的解法是把所有 Excalidraw 相关代码收敛到一个显式声明 "use client" 的模块 excalidrawWrapper.tsx

"use client";
import * as excalidrawLib from "@excalidraw/excalidraw";
import { Excalidraw } from "@excalidraw/excalidraw";

import "@excalidraw/excalidraw/index.css";

import App from "../../with-script-in-browser/components/ExampleApp";

const ExcalidrawWrapper: React.FC = () => {
  return (
    <>
      <App
        appTitle={"Excalidraw with Nextjs Example"}
        useCustom={(api: any, args?: any[]) => {}}
        excalidrawLib={excalidrawLib}
      >
        <Excalidraw />
      </App>
    </>
  );
};

export default ExcalidrawWrapper;

然后在服务端页面里通过 next/dynamicssr: false 动态加载(见 page.tsx):

import dynamic from "next/dynamic";
import Script from "next/script";

import "../common.scss";

// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
// with ssr false
const ExcalidrawWithClientOnly = dynamic(
  async () => (await import("../excalidrawWrapper")).default,
  {
    ssr: false,
  },
);

export default function Page() {
  return (
    <>
      <a href="/excalidraw-in-pages">Switch to Pages router</a>
      <h1 className="page-title">App Router</h1>
      <Script id="load-env-variables" strategy="beforeInteractive">
        {`window["EXCALIDRAW_ASSET_PATH"] = window.origin;`}
      </Script>
      {/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */}
      <ExcalidrawWithClientOnly />
    </>
  );
}

这个分层是关键:服务端页面只持有 dynamic 的懒加载句柄,@excalidraw/excalidraw 的整包导入和 index.css 引入全部发生在 client 组件内部。如果直接在 app/page.tsximport { Excalidraw },预渲染阶段就会触及浏览器环境。

核心集成点二:字体自托管与 EXCALIDRAW_ASSET_PATH

copy:assets 把构建产物中的字体复制进 public/,而 <Script strategy="beforeInteractive"> 则在客户端模块加载之前提前设置:

window["EXCALIDRAW_ASSET_PATH"] = window.origin;

这个全局变量决定了 Excalidraw 从哪个基地址加载字体等资源。从源码结构看,字体加载逻辑位于 ExcalidrawFontFace.ts:当 window.EXCALIDRAW_ASSET_PATH 是字符串时按其构造字体 URL,是数组时依次作为多级 fallback 基地址;未设置时则走默认的 CDN 地址。这与 packages/excalidraw/README.md 的说明一致:自托管场景下应把 dist/prod/fonts 的内容复制到应用静态资源目录(如 public/),并把 EXCALIDRAW_ASSET_PATH 指向同一目录;该 README 同时提醒"不要刻意设置 window.EXCALIDRAW_ASSET_PATH,除非你确实要自托管字体/资源"。

示例选择 window.origin 作为取值,意味着开发/生产环境下字体都从站点自身路径读取——这正是 copy:assets 把字体放进 public/ 的原因,两者配套才构成完整的自托管链路。

App Router 与 Pages Router 的双路由演示

该示例同时覆盖 Next.js 的两种路由体系,两个页面通过链接互相切换:

  • App Router 页面 src/app/page.tsx 提供 "Switch to Pages router" 链接,并额外包含上述的 <Script> 字体变量注入;
  • Pages Router 页面 src/pages/excalidraw-in-pages.tsx 结构几乎相同,同样使用 dynamic(..., { ssr: false }) 懒加载 excalidrawWrapper,但没有注入 EXCALIDRAW_ASSET_PATH。从源码结构看,这意味着 Pages Router 页面上字体将按默认 CDN 策略加载,而不是本地 public/ 副本。

两种写法共享同一个客户端包装层,说明该集成方案对路由体系是正交的——差异只体现在页面层如何挂载动态组件。

构建与部署配置

next.config.js

next.config.js 做了三处针对性配置:

/** @type {import('next').NextConfig} */
const nextConfig = {
  distDir: "build",
  typescript: {
    // The ts config doesn't work with `jsx: preserve" and if updated to `react-jsx` it gets ovewritten by next js throwing ts errors hence I am ignoring build errors until this is fixed.
    ignoreBuildErrors: true,
  },
  // This is needed as in pages router the code for importing types throws error as its outside next js app
  transpilePackages: ["../"],
};

module.exports = nextConfig;
  • distDir: "build":把构建输出目录从默认的 .next 改为 build,与 Vercel 配置对接;
  • typescript.ignoreBuildErrors: true:注释说明是因为 jsx 配置与 Next.js 的类型检查存在冲突,暂时忽略构建期类型报错;
  • transpilePackages: ["../"]:Pages Router 下跨 workspace 导入 with-script-in-browser 的代码会因位于 Next.js 应用目录之外而报错,这里通过 transpilePackages 让 Next.js 一并转译该目录。

vercel.json

vercel.json 只有两个字段,但都与上面的 distDir 呼应:

{
  "git": {
    "deploymentEnabled": {
      "master": true,
      "**": false
    }
  },
  "outputDirectory": "build"
}

outputDirectory 指向 build(即 Next.js 的实际产物目录),git.deploymentEnabled 限定仅 master 分支触发 Vercel 部署。README 的 "Deploy on Vercel" 一节描述的正是这一部署路径:构建命令即 package.json 中的 yarn build(同样会先执行 build:workspace 完成依赖包构建与字体复制),产物目录由 outputDirectory 指定。

实践要点小结

README 的 Getting Started 流程与示例源码对照起来,在 Next.js 中接入 Excalidraw 的完整要点是:

  1. 隔离服务端渲染:把 @excalidraw/excalidraw 的导入与 index.css 放入 "use client" 模块,页面层用 dynamic(..., { ssr: false }) 加载;
  2. 字体自托管:构建前把 dist/prod/fonts 复制到 public/,并用 beforeInteractive 脚本设置 window.EXCALIDRAW_ASSET_PATH,两者缺一不可;
  3. 跨 workspace 引用:复用其他示例目录的代码时,用 transpilePackages 处理 Pages Router 的转译问题;
  4. 部署对齐distDir 与 Vercel 的 outputDirectory 保持一致。

README 中的 "Learn More" 部分指向的是 Next.js 官方文档与教程(外部资源,此处不附链接);而在仓库内部,若想继续深入 Excalidraw 组件本身的 API 与集成规范,可从 packages/excalidraw/README.md 入手,它包含了 EXCALIDRAW_ASSET_PATH 的完整取值说明(字符串或多基地址数组)以及自托管的注意事项。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
981
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384