使用 Notion API 和 Next.js 构建动态网站

1,556 阅读11分钟

最近在学习 React 的过程中,我注意到官方网站推荐了 Next.js。虽然我之前一直知道 Next.js 是一个 SSR 框架,但我对 SSR 的了解并不深入。因此,我决定趁着学习 React 的机会,一起学习一下 SSR。

此前,我一直在使用 Notion 做笔记。偶然间,我发现了 Notion 的开发者文档。于是,我想结合 Next.js 和 Notion API 来创建自己的博客,也算是一个练手项目。


正文

源码地址 ,UI参考了这个和notion

在线地址(部署在vercel, 需要翻墙)

nextjs使用的是13.3, 用的是一个实验性的功能的App Router 一个全新的路有系统,具体文档地址在这里.

nextjs13的全新路由系统

💡 路由系统是用来映射 URL 到页面组件的。nextjs中的路由系统支持静态路由和动态路由。静态路由是指路由映射是固定的,而动态路由则是指路由映射是基于 URL 的参数动态生成的。例如,`/posts/1` 中的 `1` 就是一个动态参数。另外,nextjs还支持路由嵌套,可以在一个页面中嵌套另一个页面,形成父子页面的组合效果。

Next.js 13 中引入了一个全新的路由系统,叫做 App Router。这是一个实验性的功能,可以在 beta 版本中使用。默认情况下,在 app 文件夹中的 React 组件都是 Server Component,这意味着它们可以在服务器端渲染。app文件夹就相当于路由的根目录/, app文件夹下的page.tsx就是你的页面入口, 还有layout.tsxpage的布局组件.

// app/layout.tsx
export default function RootLayout({ children }: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
			// children就是page.tsx
      <body>{children}</body>
    </html>
  );
}

Next.js 13 引入了一个新的文件约定 loading.js,以帮助你使用 React Suspense 创建有意义的加载 UI。通过这个约定,当路由段的内容加载时,你可以从服务器显示即时的加载状态,一旦渲染完成,新的内容就会自动替换原来的内容。loading.tsx是一个可选组件,用于在页面加载数据时显示加载状态,以提高用户体验。当你的页面需要加载大量数据时,你可以使用这个组件来显示一个加载动画,让用户知道页面正在加载,以免用户认为网站崩溃了。

Untitled.png

在app目录中,文件夹用于定义路由。

每个文件夹表示一个路由段,映射到一个URL段。要创建嵌套路由,可以相互嵌套文件夹。

Untitled 1.png

在这个例子中,/dashboard/analytics URL路径不是公开可访问的,因为它没有相应的 page.js 文件。这个文件夹可以用来存储组件、样式表、图片或其他相关文件。

在app文件夹📁目录下的的react组件默认都是sever Component. 当然你也可以使用client Component.

当在文件顶部(在任何导入之前)使用”use client”指令时,组件将成为客户端组件。

这些组件(和文件)可以放置在应用程序中的任何位置。它们不需要特别放在app/目录下。

例如,您可能需要一个components/目录。但是,app/目录使您可以将组件与页面放在一起,而这在以前在pages/目录中是不可能的。

// app/Count.tsx
'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

只有在组件使用了客户端hooks,如useStateuseEffect时,才需要将组件标记为'use client'。最好将不依赖于客户端hooks的组件保留为无指令,这样当它们不被其他客户端组件导入时,它们可以自动呈现为服务器组件。这有助于确保最小量的客户端JavaScript。具体的查看这里

Static and Dynamic Rendering

在Next.js中,路由可以静态或动态渲染。

  • 在静态路由中,组件在构建时在服务器上渲染。工作结果被缓存并在后续请求中重复使用。
  • 在动态路由中,组件在请求时在服务器上渲染。

Static Rendering (Default)

静态渲染(默认)提高了性能,因为所有的渲染工作都是预先完成的,并可以从地理位置更接近用户的内容交付网络(CDN)提供服务。

您可以通过在布局或页面中使用动态函数或动态数据提取来选择动态渲染。这将导致 Next.js 在请求时动态地呈现整个路由。

此表总结了动态函数和静态数据获取(缓存)如何影响路由的呈现行为。

Data FetchingDynamic FunctionsRendering
CachedNoStatic
CachedYesDynamic
Not CachedNoDynamic
Not CachedYesDynamic

Static Data Fetching (Default)

默认情况下,Next.js会缓存未明确选择退出缓存行为的fetch()请求的结果。这意味着没有设置缓存选项的fetch请求将使用force-cache选项。

如果在路由中的任何fetch请求使用了revalidate选项,则在重新验证期间将静态重新呈现路由。

要了解有关缓存数据获取请求的更多信息,请参见Caching and Revalidating页面。

Fetching Data on the Server

尽可能地,我们建议在Server Components中获取数据。 Server Components始终在服务器上获取数据。这使您可以:

  • 直接访问后端数据资源(例如数据库)。
  • 通过防止敏感信息(例如访问令牌和API密钥)暴露给客户端来使您的应用程序更安全。
  • 获取数据并在同一环境中呈现。这减少了客户端和服务器之间的往返通信以及客户端主线程上的工作量。
  • 执行多个数据获取,而不是在客户端上进行多个单独的请求。
  • 减少客户端 - 服务器瀑布流。
  • 根据您的地区,数据获取也可能发生在更接近数据源的位置,从而减少延迟并提高性能。

了解有关Client and Server Components的更多信息。

app目录中,你有更多的选项可以探索:

第一,你可以使用loading.js在数据获取函数中从服务器端展示即时的加载状态,同时流式传输结果。

第二,你可以把数据获取的位置放在组件树的更低层,只阻止页面需要的部分渲染。例如,将数据获取移到一个特定的组件中,而不是在根布局处获取。

尽可能地,在使用数据的部分获取数据是最好的。这也允许你只为正在加载的页面部分显示加载状态,而不是整个页面。

notionApi

具体查看notion的开发文档,

项目中遇到的问题

使用next/image组件时报未设置域名报错

error - Error: Invalid src prop (https://www.notion.so/images/page-cover/met_william_morris_1878.jpg) on `next/image`, hostname "www.notion.so" is not configured under images in your `next.config.js`
See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host

如果您在使用next/image组件时看到错误消息“Invalid src prop (...) on next/image, hostname "..." is not configured under images in your next.config.js",这意味着您需要在next.config.js中配置您使用的图像域名。具体地,您需要在images属性下添加一个对象,该对象的键是您的图像域名,值是一个对象,其中包含您的域名配置。例如,如果您要使用example.com的图像,您的next.config.js应该包含以下内容:

module.exports = {
  images: {
    domains: ['example.com'],
  },
}

除了域名外,您还可以为特定图像提供其他配置。例如,您可以使用loader属性来指定图像加载程序,或使用path属性来指定图像的路径。有关更多信息,请参见官方文档中的next/image部分。

使用第三方包报错

“use client”指令是一个新的React功能,它是Server Components的一部分。由于Server Components非常新,生态系统中的第三方包才刚刚开始将其添加到使用useStateuseEffectcreateContext等客户端功能的组件中。

今天,许多npm包中使用客户端功能的组件还没有这个指令。这些第三方组件在您自己的Client Components中将按预期工作,因为它们本身有“use client”指令,但它们在Server Components中将无法工作。

import { Spinner } from 'flowbite-react';

export default function Page() {
  return (
    <div>
      <p>View pictures</p>

      {/* 🔴 Error: `useState` can not be used within Server Components */}
      <Spinner />
    </div>);
}

我在项目中使用了flowbite-reactSpinner组件, 由于它是第三方包, 且并没有使用支持nextjs客户端端渲染的“use client”指令, 所以我得再新建一个Spinner组件, 使用“use client”重新导出

// components/ui/Spinner
'use client'
export  { Spinner } from "flowbite-react";

然后这样引入就不会报错了


import { Spinner } from '@components/ui/Spinner';

export default function Page() {
  return (
    <div>
      <p>View pictures</p>

      {/* 🔴 Error: `useState` can not be used within Server Components */}
      <Spinner />
    </div>);
}

notion 内容获取

notion页面的内容都是基于block的,每一段都是一个block. 所以要获取notion内容都是通过获取block来获取的. notion APi提供了 获取block的接口, 要获取页面所有的bloks那这个block_id就是当前页面的pageid , 返 回包含在指定ID的块中的子块对象的分页数组。为了获得块的完整表示,您可能需要递归地检索子块的块子级。需要注意的是: 仅返回指定块的第一级子级。有关确定该块是否具有嵌套子级的详细信息,请参见block objects。笔者就遇到了查询table的问题. 只出现了table的格式化信息, 并没有出现内容. 遇到这个问题时你需要拿到tableblockid再去调用blockchildren的接口去获取table content内容.

next.js 启动报错

因为我升级next.js到了最新版本13.3导致以来有问题报如下错误:

error - Failed to load SWC binary for darwin/x64, see more info here: https://nextjs.org/docs/messages/failed-loading-swc

遇到这个报错删除.lock文件重新install就好了 failed-loading-swc | Next.js (nextjs.org)

动态渲染(dynamic) 报错: Dynamic server usage: searchParams

将布局或页面的动态行为更改为完全静态或完全动态。

  • 在静态路由中,组件在构建时在服务器上渲染。工作结果被缓存并在后续请求中重复使用。
  • 在动态路由中,组件在请求时在服务器上渲染。
// layout.tsx or page.tsx
export const dynamic = 'auto'
// 'auto' | 'force-dynamic' | 'error' | 'force-static'

'auto'(默认): 缓存尽可能多,同时不会阻止任何组件选择动态行为。

'force-dynamic': 强制动态渲染和动态数据获取布局或页面,通过禁用所有fetch请求的缓存并始终重新验证来实现。此选项相当于:

  • pages目录中的getServerSideProps()
  • 在布局或页面中将每个fetch()请求的选项设置为 { cache: 'no-store', next: { revalidate: 0 } }
  • 设置段配置为 export const fetchCache = 'force-no-store'

'error': 通过在任何组件使用动态函数或动态获取时导致错误,强制静态渲染和静态数据获取布局或页面。此选项相当于:

  • pages目录中的getStaticProps()
  • 在布局或页面中将每个fetch()请求的选项设置为 { cache: 'force-cache' }
  • 设置段配置为 fetchCache = 'only-cache', dynamicParams = false。 注意:dynamic = 'error'dynamicParams 的默认值从true更改为false。您可以手动将 dynamicParams 设置为true,以使动态呈现页面支持由 generateStaticParams 未生成的动态参数。

'force-static': 强制静态渲染和静态数据获取布局或页面,通过强制 cookies(), headers()useSearchParams() 返回空值实现。

因为我的页面中使用了useSearchParams 当我在开发模式使用时是没有错误的, 但是在生产就报错Dynamic server usage: searchParams

所以强制使用 'force-static' 就好了.

子组件使用async component时eslint报错

'ExampleAsyncComponent' cannot be used as a JSX component.
  Its return type 'Promise<Element>' is not a valid JSX element.
    Type 'Promise<Element>' is missing the following properties from type 'ReactElement<any, any>': type, props, keyts(2786)

解决方法:

  • 使用async的服务器组件会导致在使用它的地方出现'Promise<Element>' is not a valid JSX element类型错误。

  • 这是TypeScript的已知问题,并且正在进行修复。

  • 作为临时解决办法,您可以在组件上方添加{/* @ts-expect-error Async Server Component */}来禁用对其的类型检查。

    app/page.tsx

    import { ExampleAsyncComponent } from './ExampleAsyncComponent';
    export default function Page() {
      return (
        <>
          {/* @ts-expect-error Async Server Component */}
          <ExampleAsyncComponent />
        </>);
    }
    
    
  • 这不适用于Layout和Page组件。

Hydration failed because the initial UI does not match what was rendered on the server.

NextJS不能很好地处理p标签包装的div、section等,因此会显示“因为初始UI与在服务器上呈现的不匹配,因此整个初始请求已失败”。通过检查我的元素如何互相包装,我解决了这个问题。

<!-- 错误的 -->
<p>
   <div>...</div>
</p>
<!-- 正确的 -->
<div>
   <p>...</p>
</div>

stackoverflow

最后

还有一些问题和解决方案没有列全,我会继续更新这篇文章,如果您有什么建议或想法,欢迎随时告诉我。谢谢您的阅读!

源码地址