Using Notion-like Text Editor with AI Autocomplete and Planetscale Database in Next.js
2024 年 2 月 5 日

her is Draft.js (opens new window). Both are great options for integrating a text editor into a React application.

However, in this article, I want to introduce a different approach - using Notion-like text editor with AI autocomplete and Planetscale database in Next.js using shadcn/ui. This combination brings together the user-friendly interface of Notion, the intelligent autocomplete feature, and the power of Planetscale database.

Notion-Like Text Editor #

The Notion-like text editor provides a familiar and intuitive interface for users. It allows them to create and edit documents easily, with features like formatting, inline code blocks, and lists. The editor also supports collaborative editing, so multiple users can work on a document simultaneously.

AI Autocomplete #

One of the standout features of this text editor is the AI autocomplete. This feature uses machine learning algorithms to suggest completions as you type. It can predict the next word or even the next sentence, making writing faster and more efficient. The autocomplete suggestions are context-aware and personalized, providing relevant suggestions based on the content of the document.

Planetscale Database #

To store and manage the documents created with the text editor, this solution uses Planetscale database. Planetscale is a highly scalable and distributed database that offers strong consistency and low latency. It is designed to handle large amounts of data and provide fast access to it. With Planetscale, you can ensure that your documents are securely stored and easily accessible.

Next.js Integration #

Next.js is a popular framework for building React applications. It provides server-side rendering, automatic code splitting, and route prefetching, among other features. By integrating the Notion-like text editor with AI autocomplete and Planetscale database into a Next.js project, you can create a powerful and efficient web application.

shadcn/ui #

shadcn/ui is a UI component library for Next.js. It provides a set of reusable and customizable UI components that can be easily integrated into your project. With shadcn/ui, you can quickly build a beautiful and functional user interface for your application.

In conclusion, by combining the Notion-like text editor with AI autocomplete and Planetscale database in Next.js using shadcn/ui, you can create a powerful and user-friendly web application. This solution offers an intuitive interface, intelligent autocomplete, and efficient data storage. Give it a try and enhance your React application with these powerful tools. 在这里是Blocknote (opens new window)。这两个项目都基于现有的开源项目,分别是Tip-Tap (opens new window)Prosemirror (opens new window)。然而,它们的使用要简单得多。从Blocknote的情况可以看出,我们可以使用以下几行代码将它们快速集成到我们的React应用中:

import { BlockNoteEditor } from "@blocknote/core";
import { BlockNoteView, useBlockNote } from "@blocknote/react";
import "@blocknote/core/style.css";

export default function App() {
  // 创建一个新的编辑器实例。
  const editor: BlockNoteEditor = useBlockNote();

  // 使用React组件渲染编辑器实例。
  return <BlockNoteView editor={editor} theme={"light"} />;
}

我们要构建什么? #

我们要构建的是什么呢? 好的,我们先不要急着往前走。让我们看看在这篇文章中我们将一起创建什么,以及我们将在途中了解到什么:

我们正在制作一个应用程序,您可以在其中记录笔记,并以列表和详细信息的只读模式查看它们,就像上面的图片所示。编辑界面配备了一个小型AI自动完成功能,该功能集成到基于开源Novel编辑器的Blocknote中。让我们看看我们将使用哪些主要技术来实现这一目标:

除此之外,我们还将学习如何通过Vercel提供的AI软件包以边缘函数的形式流式传输响应。我们还将研究如何以何种形式将格式化的文档保存到MySQL数据库(Planetscale),所以请一直陪我读到文章结束。

实现 #

创建项目 #

让我们开始项目,并逐步了解使用的工具。我们可以通过执行以下命令来创建名为'note-blocks'的项目:

npx create-next-app@latest

我们将使用以下配置(保持默认):
[图片描述 接下来,让我们快速“清理”在创建Next.js项目后留下的代码行。让我们从globals.css开始。如果我们只留下这个就足够了:

@tailwind base;
@tailwind components;
@tailwind utilities;

然后,进入app目录中的page.tsx文件。在那里,我们可以将整个文件转换为以下内容:

import Link from "next/link";

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
        <Link href="/new">
          New Note
        </Link>
      </div>
    </main>
  )
}
``` 首先,让我们创建一个“下一页”,当我们点击“新笔记”链接时会跳转到该页面。我们在“app”目录下创建一个名为“new”的文件夹,然后在其中创建一个名为`page.tsx`的文件,内容如下:

export default function Page() { return (

编辑器
) }


这还不算太多,也不太漂亮,但我们稍后会讨论。暂时来说,如果你点击“新笔记”链接,将会打开一个占位页面。

#### 添加和使用Blocknote编辑器

根据文章开始描述的方法,我们可以将Blocknote编辑器拖放到这个页面上。为此,我们需要使用npm install在我们的应用程序中安装它:
`npm install @blocknote/core @blocknote/react`

如果安装成功,那么创建一个名为`Editor.tsx`的组件,放在 "新建"文件夹,根据定义将是一个客户端组件,因为我们的编辑器界面不能在服务器端操作,在这方面,您可以阅读Blocknote文档的相关部分[这里](https://https/%20/www.blocknotejs.org/docs/nextjs)。

我们还可以将以下代码插入到Editor组件中:

"use client"; import { BlockNoteEditor } from "@blocknote/core"; import { BlockNoteView, useBlockNote } from "@blocknote/react"; import "@blocknote/core/style.css";

export default function Editor() { const editor: BlockNoteEditor | null = useBlockNote({}); return ; }


请确保文件顶部有`"use client"`,这表示这是一个客户端组件。

而page.tsx的转换如下:

import dynamic from "next/dynamic";

const Editor = dynamic(() => import("./Editor"), { ssr: false });

export default function Page() { // 页面内容 } ``` 在这里,我们以一种使其在客户端上表示的方式导入了我们的客户端编辑器组件('ssr: false')。因此,当我们完成后,我们已经可以在页面上看到我们的编辑器。让我们稍微玩一下,看看它能做什么。

经过观察和测试,我们很快意识到这个编辑器非常好,但我们该如何完成应用程序的其余部分呢? 让我们顺利进行。首先,让我们看看如何向我们的小编辑器添加AI自动完成功能,首先,让我们看看如何向我们的文本编辑器添加菜单项。当我们在编辑器中按下“/”或点击行首的“+”符号时,该菜单项将显示出来。因此,我们希望我们的工具栏看起来像这样: 实现魔术AI功能

我们需要修改Editor.tsx文件中的代码以实现所需的行为:

"use client";
import { BlockNoteEditor } from "@blocknote/core";
import { BlockNoteView, getDefaultReactSlashMenuItems, ReactSlashMenuItem, useBlockNote } from "@blocknote/react";
import "@blocknote/core/style.css";
import { ImMagicWand } from "react-icons/im";

const insertMagicAi = (editor: BlockNoteEditor) => {
  console.log('Magic AI insertion incoming!')
};

const insertMagicItem: ReactSlashMenuItem = {
  name: 'Continue with AI',
  execute: insertMagicAi,
  aliases: ['ai', 'magic'],
  group: 'Magic',
  icon: <ImMagicWand size={18} />,
  hint: 'Continue your idea with some extra in'
};

我们需要在Editor.tsx文件中的代码中进行修改以实现所需的行为。具体修改如下:

"use client";
import { BlockNoteEditor } from "@blocknote/core";
import { BlockNoteView, getDefaultReactSlashMenuItems, ReactSlashMenuItem, useBlockNote } from "@blocknote/react";
import "@blocknote/core/style.css";
import { ImMagicWand } from "react-icons/im";

const insertMagicAi = (editor: BlockNoteEditor) => {
  console.log('Magic AI insertion incoming!')
};

const insertMagicItem: ReactSlashMenuItem = {
  name: '使用AI继续',
  execute: insertMagicAi,
  aliases: ['ai', 'magic'],
  group: '魔术',
  icon: <ImMagicWand size={18} />,
  hint: '继续您的想法,并获得额外的帮助'
};

我们需要修改Editor.tsx文件中的代码以实现所需的行为。具体修改如下:

"use client";
import { BlockNoteEditor } from "@blocknote/core";
import { BlockNoteView, getDefaultReactSlashMenuItems, ReactSlashMenuItem, useBlockNote } from "@blocknote/react";
import "@blocknote/core/style.css";
import { ImMagicWand } from "react-icons/im";

const insertMagicAi = (editor: BlockNoteEditor) => {
  console.log('魔术AI插入即将到来!')
};

const insertMagicItem: ReactSlashMenuItem = {
  name: '继续使用AI',
  execute: insertMagicAi,
  aliases: ['ai', 'magic'],
  group: '魔术',
  icon: <ImMagicWand size={18} />,
  hint: '使用一些额外的帮助来继续您的想法'
};

我们需要修改Editor.tsx文件中的代码以实现所需的行为。具体修改如下:

"use client";
import { BlockNoteEditor } from "@blocknote/core";
import { BlockNoteView, getDefaultReactSlashMenuItems, ReactSlashMenuItem, useBlockNote } from "@blocknote/react";
import "@blocknote/core/style.css";
import { ImMagicWand } from "react-icons/im";

const insertMagicAi = (editor: BlockNoteEditor) => {
  console.log('魔术AI插入即将到来!')
};

const insertMagicItem: ReactSlashMenuItem = {
  name: '使用AI继续',
  execute: insertMagicAi,
  aliases: ['ai', 'magic'],
  group: '魔术',
  icon: <ImMagicWand size={18} />,
  hint: '继续您的想法,并获得额外的帮助'
};
``` ```jsx
import React from 'react';
import { BlockNoteView, useBlockNote, getDefaultReactSlashMenuItems } from 'blocknote-react';

const insertMagicItem = {
  label: 'Insert Magic',
  handler: () => {
    const magicText = 'Magic!';
    editor?.insertText(magicText);
  },
};

const customSlashMenuItemList = [
  insertMagicItem,
  ...getDefaultReactSlashMenuItems(),
];

export default function Editor() {
  const editor = useBlockNote({
    slashMenuItems: customSlashMenuItemList,
  });

  return <BlockNoteView editor={editor} theme="light" />;
}

这段代码的新增部分是将一个新项添加到"slashMenuItems"列表中,该项将负责AI自动完成操作。我们将如何实现这个功能呢?当然是调用OpenAI API。如果您尚未在此处注册(在2024年这是相当不太可能的),您可以在这里 (opens new window)获得API密钥,这对我们进一步开发该功能是必要的。

让我们开始吧。首先,我们需要一个能够流式传输OpenAI响应的端点。在这里,我们可以使用在Novel开源项目中找到的代码部分,这对我们非常完美。

import React from 'react';
import { BlockNoteView, useBlockNote, getDefaultReactSlashMenuItems } from 'blocknote-react';

const insertMagicItem = {
  label: '插入魔法',
  handler: () => {
    const magicText = '魔法!';
    editor?.insertText(magicText);
  },
};

const customSlashMenuItemList = [
  insertMagicItem,
  ...getDefaultReactSlashMenuItems(),
];

export default function Editor() {
  const editor = useBlockNote({
    slashMenuItems: customSlashMenuItemList,
  });

  return <BlockNoteView editor={editor} theme="light" />;
}

这段代码的新增部分是将一个新项添加到"slashMenuItems"列表中,该项将负责AI自动完成操作。我们将如何实现这个功能呢?当然是调用OpenAI API。如果您尚未在此处注册(在2024年这是相当不太可能的),您可以在这里 (opens new window)获得API密钥,这对我们进一步开发该功能是必要的。

让我们开始吧。首先,我们需要一个能够流式传输OpenAI响应的端点。在这里,我们可以使用在Novel开源项目中找到的代码部分,这对我们非常完美。 创建一个 "api" 文件夹,放在 "app" 目录下,然后在 "api" 文件夹中创建一个 "generate" 文件夹,并在其中添加一个 route.ts 文件。这个文件夹结构告诉 Next.js 这将是一个 API 端点,URI 为 /api/generate。下面的内容将添加到 "route.ts" 文件中。如果我们在一开始安装的时候没有安装将会用到的包,请不要惊慌,我们将会解释并安装所有需要的包。


import OpenAI from 'openai';
import { OpenAIStream, StreamingTextResponse } from 'ai';
import { kv } from '@vercel/kv';
import { Ratelimit } from '@upstash/ratelimit';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY || '',
});

export const runtime = 'edge';

export async function POST(req: Request): Promise<Response> {
  if (!process.env.OPENAI_API_KEY || process.env.OPENAI_API_KEY === '') {
    return new Response(
      'Missing OPENAI_API_KEY – make sure to add it to your .env file.',
      {
        status: 400,
      }
    );
  }
  if 如果(process.env.NODE_ENV != 'development' &&
process.env.KV_REST_API_URL &&
process.env.KV_REST_API_TOKEN
) {
  const ip = req.headers.get('x-forwarded-for');
  const ratelimit = new Ratelimit({
    redis: kv,
    limiter: Ratelimit.slidingWindow(50, '1 d'),
  });

  const { success, limit, reset, remaining } = await ratelimit.limit(
    `noteblock_ratelimit_${ip}`
  );

  if (!success) {
    return new Response('您已达到每天的请求限制。', {
      status: 429,
      headers: {
        'X-RateLimit-Limit': limit.toString(),
        'X-RateLimit-Remaining': remaining.toString(),
        'X-RateLimit-Reset': reset.toString(),
      },
    });
  }
}

let { prompt } = await req.json();

const response = await openai.chat.completions.create({
  model: 'gpt-3.5-turbo',
  messages: [
    {
      role: 'system',
      content:
        '您是一个AI写作助手,根据已有的文本继续写作。',
    },
    {
      role: 'user',
      content: prompt,
    },
  ],
}); 这个问题的答案是通过OpenAI API来实现的。在代码中,我们首先导入了所需的包,然后定义了一个函数`openaiCompletion`,该函数接收一个字符串参数`prompt`。

这个函数使用OpenAI的`openai.Completion.create()`方法来发送一个请求,该请求包含了输入参数和文本内容。在这个例子中,我们的输入参数包含了一个用户角色和一个内容。我们使用了一些默认值来设置温度、top_p、frequency_penalty和presence_penalty。

接下来,我们定义了一个`stream`变量,它将OpenAI的响应转换成一个流。最后,我们返回一个`StreamingTextResponse`对象,它将流返回给调用者。

这就是这个函数的主要逻辑。现在,让我们来看一下如何将这个函数集成到我们的应用程序中。首先,我们需要在应用程序中导入所需的包。

```javascript
const OpenAi = require('openai');
const { OpenAIStream, StreamingTextResponse } = require('@vercel/openai');

然后,我们可以使用openaiCompletion函数来处理用户的输入并返回一个响应。我们可以将这个函数放在一个HTTP端点中,以便我们的应用程序可以接收请求。例如,使用Express.js,我们可以这样做:

app.post('/api/completion', async (req, res) => {
  const { prompt } = req.body;
  const response = await openaiCompletion(prompt);
  response.pipe(res);
});

在这个例子中,我们将用户输入作为请求的正文传递给openaiCompletion函数,并将响应通过HTTP响应返回给客户端。

这就是如何使用OpenAI API来实现一个简单的文本补全功能。希望这对你有帮助! 安装这个:

npm install ai

然后,在接下来的两行导入语句中,我们还将从Vercel导入名为KV的无服务器Redis组件,然后还有一个速率限制器,它将负责确保不会有无尽的请求到达这里。我们还安装这些:

npm install @vercel/kv
npm install @upstash/ratelimit

进入全屏模式 退出全屏模式

一旦我们有了这个,我们可以看到在代码行中不再遇到任何错误(希望如此),并且我们可以清楚地看到我们的应用程序的“灵魂”正在做什么:

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY || '',
});

这一部分使用从环境变量process.env.OPENAI_API_KEY获取的API密钥初始化了OpenAI对象。您也可以从OpenAI开发者界面复制您自己的密钥,并将其以以下格式插入到应用程序根目录中创建的.env文件中:
`OPE NAI_API_KEY=my_secret_key

为了防止遗漏,下一行代码中我们检查是否定义了这个密钥:

if (!process.env.OPENAI_API_KEY || process.env.OPENAI_API_KEY === '') {
  return new Response(...);
}

接下来的部分使用用户的IP地址来控制请求,以防止滥用,如果您有正确的.env变量用于请求。我们稍后会创建这些.env变量,现在不用担心。

if (
    process.env.NODE_ENV != 'development' &&
    process.env.KV_REST_API_URL &&
    process.env.KV_REST_API_TOKEN
  ) {
  const ip = req.headers.get('x-forwarded-for');
  const { success, limit, reset, remaining } = await ratelimit.limit(
    `noteblock_ratelimit_${ip}`
  );
}

这行代码从请求中读取用户提供的提示(初始文本):

let { prompt } = await req.json();

以上是对Markdown内容的翻译和修改,已删除了一级标题和图片链接,并尽可能删除了Markdown格式错误和一些无用的段落,使整个文章读起来更加自然。 使用GPT-3.5-turbo模型生成答案:

const response = await openai.chat.completions.create({...});

进入全屏模式 退出全屏模式

响应以 stream (opens new window)(持续的数据流)的形式发送回用户,这可以实现更高效的数据传输,尤其对于较大的响应来说:

const stream = OpenAIStream(response);
return new StreamingTextResponse(stream);

进入全屏模式 退出全屏模式

现在我们已经对这个API端点的功能有了一个概述,接下来我们可以进入下一部分,即实际使用这个端点。我们返回到之前编辑过的 TextEditor.tsx 文件,在 insertMagic() 函数之前插入这个函数:

 const { complete } = useCompletion({
    id: 'hackathon_starter',
    api: '/api/generate',
    onResponse: (response) => {
      if (response.status === 429) {
        return;
      }
``` 如果(response.body){
  const reader = response.body.getReader();
  let decoder = new TextDecoder();
  reader.read().then(function processText({ done, value }) {
    if (done) {
      return;
    }
    let chunk = decoder.decode(value, { stream: true });
    editor?._tiptapEditor.commands.insertContent(chunk);
    reader.read().then(processText);
  });
} else {
  console.error('响应主体为空');
}
},
onError: (e) => {
  console.error(e.message);
},
});

进入全屏模式退出全屏模式

useCompletion() 必须从 Vercel 提供的 AI 模块中导入:

import { useCompletion } from "ai/react";

这里使用了一个自定义的 React hook,useCompletion,来处理 API 响应和错误。它发起了一个 API 调用,处理了速率限制的响应(状态码 429),并处理了响应流。如果响应包含内容,则将其插入到编辑器中。如果响应主体为空,则输出错误消息。 插入数据,它将解码并将此内容插入到我们的文本编辑器中。

一旦我们有了这个,按照以下方式添加insertMagicAi()函数:

const insertMagicAi = (editor: BlockNoteEditor) => {
    complete(
      getPrevText(editor._tiptapEditor, {
        chars: 5000,
        offset: 1,
      })
    );
  };

在这里,您可以看到我们调用了之前编写的complete()方法,然后是一个getPrevText()函数,我们尚未创建。顾名思义,该函数的任务将是从我们的编辑器中提取最后的文本内容(确切地说是5000个字符),并基于此继续支持AI生成文本。

如果我们现在在界面上测试功能,我们可以看到(如果我们做得正确),AI自动完成的工作。

使用数据库存储笔记

如果我们能够将这个笔记保存在数据库中会怎样?让我们来做吧!我们将使用Planetscale 在Planetscale 注册后,我们可以很容易地创建一个免费数据库,具体参数如下:

一旦我们拥有了数据库,我们将选择Prisma作为我们想要连接到数据库的框架。之后,我们可以查看我们的 DATABASE_URL,并将其逐一复制到我们的 .env 文件中。

然而,Prisma本身尚未安装。我们可以通过在根目录下输入以下命令来安装它:

npm install prisma --save-dev 

然后启动Prisma CLI:

npx prisma 模式初始化
Prisma目录已经被创建在根目录中,其中包含一个'schema.prisma'文件,其内容可以替换为如下内容:

datasource db {
  provider     = "mysql"
  url          = env("DATABASE_URL")
  relationMode = "prisma"
}

生成器客户端 {
  provider = "prisma-client-js"
}

在这个文件中,我们指定了我们将使用mysql以及我们的数据库访问路径。我们的下一个任务是创建模式,描述我们的数据库将如何显示。我们在同一个'schema.prisma'文件中插入以下模型:

模型笔记 {
  id        String   @id @default(cuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  document  Json?    @db.Json
} 这定义了一个模式,即一个笔记对象。在我们类似Notion的编辑器中,我们将以Json形式将文本存储在文档字段中。幸运的是,MySQL已经支持这一点。
要实现这些更改,请运行以下命令,将我们的模式模型“推送”到Planetscale数据库:

`npx prisma db push`

我们可以接受这样。我们已经走了很长的路,现在让我们看看还剩下什么来构建应用程序:

*  我们需要使用保存按钮将笔记保存到数据库中,在这里Server Action会很有用
*  我们需要在主页上列出我们的笔记
*  在外观方面,我们需要整理我们的网站 - TailwindCSS和Schadcn UI库将帮助我们

让我们首先创建保存函数。为此,我们需要能够从应用程序的任何地方访问Prisma客户端。在prisma文件夹中,我们还有一个`prisma.schema` ts文件,创建一个`client.tsx`文件,内容如下:

import { PrismaClient } from '@prisma/client' 全局声明{ 命名空间NodeJS{ 接口全局{} } } 接口CustomNodeJsGlobal扩展NodeJS.Global{ prisma: PrismaClient; } 全局常量global: CustomNodeJsGlobal;

const prisma = global.prisma || new PrismaClient();

if (process.env.NODE_ENV !== 'production') global.prisma = prisma;

export default prisma;


进入全屏模式退出全屏模式


#### [](#添加保存操作)添加保存操作

接下来,在根目录中创建一个名为 `actions.ts` 的文件。我们的服务器操作,负责保存笔记,将放在这里。

'use server' import prisma from "@/prisma/client";

type Note = { document: object } export async function createNote(note: Note) { return prisma.note.create({ data: note, }); }


让我们将这个操作与我们的编辑器集成,通过连接一个保存按钮。这需要一个按钮。多么令人惊讶,对吗?首先,让我们放一个非常简单的设计, 在界面上创建一个简单、流畅、自然的HTML按钮。我们将在`TextEditor.tsx`中进行操作。首先,在文件的顶部导入我们的服务器动作:

```js
import { createNote } from "@/app/actions";

在代码的底部添加以下内容:

const handleSubmitNote = async () => {
    const note = {
      document: editor.topLevelBlocks
    }
    await createNote(note)
}
return (
    <div>
      <BlockNoteView editor={editor} theme={"light"}/>
      <button onClick={() => handleSubmitNote()}>提交</button>
    </div>
);

handleSubmitNote()函数中,我们调用了服务器动作,允许我们保存给定的笔记。让我们测试一下这个函数。当我们点击"提交"后,我们看不到任何东西,因为我们还没有开发保存后的操作。为了检查我们的工作,让我们查看数据库。我们可以通过启动Prisma Studio来实现,输入以下命令到终端中: npx prisma studio

在一个端口上打开了一个数据库查看系统,当点击适当的模型时,表中的内容会被输入,这样我们就可以看到(如果我们做得好的话)会有一行包含我们的注释。

设计我们的应用程序

我们已经准备好了应用程序的主要功能。接下来,让我们让它变得更加漂亮一些。 为此 - 如我先前所述 - 我们将使用带有ShadCN的TailwindCSS。当项目初始化时,已经安装了Tailwind,现在要使用ShadCN。在项目的根目录下,我们必须执行以下命令:

npx shadcn-ui@latest init

我们将按照默认设置进行操作,除了主题,实际上可以是任何你喜欢的东西。

(删除图片链接)

重新修饰整篇文章,尽量删除Markdown格式错误和一些无用的段落,使文章读起来更加自然。 我们肯定需要这个组件库中的一个按钮 (opens new window)。我们可以通过以下方式安装它:

npx shadcn-ui@latest add button

然后在“主页”中,我们将能够在app/page.tsx中使用它,代码如下:

import Link from "next/link";
import { Button } from "@/components/ui/button"
export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <div className="z-10 max-w-5xl w-full font-mono text-sm lg:flex items-center justify-center">
        <Button>
          <Link href="/new">
            新的笔记
          </Link>
        </Button>
      </div>
    </main>
  )
}

进入全屏模式 退出全屏模式 可以看到,我们稍微修改了Tailwind布局,并添加了我们的UI组件。但是,由于优化的路由,我们还包含了Next的Link组件。
然后进入我们的笔记编辑器界面,在那里进行设计更改。我们将修改编辑器,所以进入该组件(TextEditor.tsx)并按如下所示修改HTML部分:

   <div className="flex flex-col items-center min-h-screen px-4 w-full">
      <div className="relative w-full max-w-4xl mx-auto mb-4">
        <Button
          className="absolute right-0 top-0 mt-4 mr-4 px-4 py-2"
          onClick={() => handleSubmitNote()}
        >
          提交
        </Button>
        <BlockNoteView
          className="w-full bg-white p-4 pt-16"
          editor={editor}
          theme={"light"}
        />
      </div>
    </div>

全屏模式进入退出全屏模式

...我们完成了应用程序的外观,并准备好核心功能,您可以 ## 部署!

现在让我们来部署我们的系统。为此,除了将代码上传到Github并拥有Vercel账户之外,不需要其他任何东西。在我们将项目上传到Github之后(或者如果我们有一个我分享的Github项目的fork即可),我们现在可以部署项目了。在Vercel仪表板上,点击Add New...按钮,选择Project,然后从我们的Github仓库中选择note-blocks。然后,我们会看到这个配置屏幕:

(删除图片链接) 感谢上帝,我们不需要做任何事,因为它会自动识别一切。我们只需要设置环境变量。我们可以从我们的.env文件中复制这些变量。如果我们对此感到满意,我们还可以按下部署按钮。之后,应用程序将被激活,我们的应用程序将公开可用。

让我们公开构建

哦,我错过了更多的东西,不是吗?我们还谈到了如何在网站上添加一个列表部分,以便我可以看到到目前为止的所有笔记。我将这个功能开放给那些想要fork我的Github项目并通过这个功能进行PR的人。这样你也可以练习Next.js服务器操作和服务器组件的运行方式!

还缺少的一点(尤其是在生产环境中)是正确配置速率限制器。为此,我们只需要将@vercel/kv包连接到我们的项目中。 让我们连接起来

如果您对本文章有任何疑问或者注意到任何问题,欢迎在评论区写下来或者通过 我的X账户 (opens new window) 联系我。祝您编程愉快! 😃