我使用Next.js和TailwindCss构建了一个Markdown编辑器🔥

我使用Next.js和TailwindCss构建了一个Markdown编辑器🔥 #

加入我,在这个项目中我们使用最新版的Nextjs构建一个在线Markdown编辑器。

目标 #

点击这里 (opens new window)查看完成的构建 #

1. 创建首页 #

我想要一个简单的布局,所以我将屏幕分为两部分;左侧是编辑器,右侧是Markdown渲染结果。

const Homepage = () => {
    return (
    <div className='h-screen flex justify-between'>
        // 输入Markdown
        <section className='w-full pt-5 h-full'>
          <textarea
            className='w-full . 删除标题)3. 删除标题

现在,让我们删除第一级标题。

```javascript
"use client"

import { useState } from "react"

const Homepage = () => {
    const [source, setSource] = useState('');
    return (
        <div className='...'>
        <section className='...'>
          <textarea
            className='w-full ... placeholder:opacity-80'
            placeholder='Feed me some Markdown 🍕'
            value={source}
            onChange={(e) => setSource(e.target.value)}
            autoFocus
          />
        </section>
        <div className='fixed ... border-dashed' />
        // Render the markdown
        <article className='w-full pt-5 pl-6'>
          Markdown lies here
        </article>
      </div>
    )
}

return Homepage

进入全屏模式 退出全屏模式 安装react-markdown (opens new window)@tailwindcss/typography (opens new window)来渲染Markdown和样式化Markdown。通过以下命令进行安装。

npm install react-markdown
npm install -D @tailwindcss/typography

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

现在导入并添加Markdown组件,并将source作为子组件传递。记得给Markdown组件添加prose类名。

import Markdown from 'react-markdown'

const Homepage = () => {
    return (
        ...
        <div className='fixed ... border-dashed' />
        // 渲染Markdown
        <article className='w-full pt-5 pl-6'>
          <Markdown
            className='prose prose-invert min-w-full'
          >
            {source}
          </Markdown>
        </article>
        ...
    )
}

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

现在,如果您键入任何Markdown,您仍然找不到任何更改。这是因为我们忘记将`@tailwindcss/typography`插件添加到tailwindcss配置中💀

将您的`tailwind.config.ts`更改为以下内容:

import type { Config } from 'tailwindcss'

const config: Config = { content: [ './src/pages//*.{js,ts,jsx,tsx,mdx}', './src/components//.{js,ts,jsx,tsx,mdx}', './src/app/**/.{js,ts,jsx,tsx,mdx}', ], // 在这里添加插件 plugins: [require('@tailwindcss/typography')] }

export default config


现在编写一些Markdown,您将看到实时更改🚀


## 4. 代码高亮和自定义组件

现在,我们需要安装`react-syntax-highlighter`包,以将代码高亮添加到我们的项目中。

npm i react-syntax-highlighter npm i --save @types/react-syntax-highlighter


现在我们要为代码高亮器创建一个**自定义组件**。

在`src`文件夹内创建一个名为`components`的文件夹。然后在components文件夹内创建一个名为`Code.tsx`的文件。

从[react-syntax-highlighter的文档](https://github.com/react-syntax-highlighter/react-syntax-highlighter#readme)中添加以下代码:

import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { materialOceanic } from 'react-syntax-highlighter/dist/cjs/styles/prism';

export const CodeBlock = ({ ...props }) => { return ( <SyntaxHighlighter language={props.className?.replace(/(?:lang(?:uage)?-)/, '')} style={materialOceanic} wrapLines={true} className='not-prose rounded-md' > {props.children} ) }


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

在这里,props包含一个以`lang-typescript`或 有时候我们使用一些正则表达式来除去除语言名称之外的所有内容,以便删除`language-typescript`。`not-prose`类名将会删除默认的排版样式。

现在回到主要的`page.tsx`文件,导入`CodeBlock`组件并将其传递给原始的`<Markdown />`组件。

import Markdown from 'react-markdown' import { CodeBlock } from '@/components/Code'

const Homepage = () => { const options = { code: CodeBlock } return ( ...                       {source}                   ... ) }


这将用我们自定义的`CodeBlock`组件替换每个出现的`code`。 【可选】
BUG(🐛):您的代码组件周围可能会有一个奇怪的深色边框,这是由`pre`标签和tailwind样式引起的。

要解决这个问题,请返回到您的`Code.tsx`并添加以下代码,以从pre标签中删除tailwind样式。

export const Pre = ({ ...props }) => { return (

{props.children}
) }


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

将其导入到您的`page.tsx`中,并将其添加到`options`变量中:

const Homepage = () => { const options = { code: CodeBlock, // 在这里添加 pre: Pre, } return ( ... ) }


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

这将删除该边框。

## 5. 添加 Rehype 和 Remark 插件

[Rehype](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md) 和 [Remark](https://github.com/remarkjs/remark/blob/main/doc/plugins.md) 是用于转换的插件 格式化和操作网站的HTML和Markdown内容,以增强其功能和外观。

我们将使用以下插件:

- `rehype-sanitize`:清理Markdown
- `rehype-external-links`:在链接上添加🔗图标
- `remark-gfm`:支持[GFM](https://github.github.com/gfm/)的插件(支持表格、脚注等)

安装插件:

```plaintext
npm i remark-gfm rehype-external-links rehype-sanitize

回到我们的page.tsx

import remarkGfm from 'remark-gfm'
import rehypeSanitize from 'rehype-sanitize'
import rehypeExternalLinks from 'rehype-external-links'

... 
<Markdown
  ...
  remarkPlugins={[remarkGfm]}
  rehypePlugins={[
    rehypeSanitize,
    [rehypeExternalLinks,
     { content: { type: 'text', value: '🔗' } }
    ],
  ]}
>{source}</Markdown>

将remark插件传递给remarkPlugins,将rehype插件传递给rehypePlugins。 如果任何插件需要任何自定义 (opens new window) ,请将它们放在方括号中,后面跟着插件名称和选项,使用以下语法:[veryCoolPlugin, { { options } }]

6. 使用Markdown按钮的标题 #

接下来,我们添加一个标题组件,其中包含在点击时插入特定Markdown元素的按钮。

首先,在“components”文件夹中创建一个Header.tsx文件,并编写以下代码:

const Header = () => {
  const btns = [
    { name: 'B', syntax: '**Bold**' },
    { name: 'I', syntax: '*Italic*' },
    { name: 'S', syntax: '~Strikethrough~' },
    { name: 'H1', syntax: '# ' },
  ]

  return (
    <header className="flex ... bg-[#253237]">
        {btns.map(btn => (
          <button
            key={btn.syntax}
            className="flex ...rounded-md"
          >
            {btn.name}
          </button>
        ))}
    </heade 将以下Markdown转换为中文并删除一级标题:

```javascript
import React from 'react'
import { useState } from 'react'
import CodeBlock from '@/components/CodeBlock'

const Header = () => {
  const [source, setSource] = useState('')

  return (
    <>
      <div className='py-4 px-8 bg-blue-500 text-white'>
        <input
          type='text'
          className='border border-gray-400 py-2 px-4 rounded'
          value={source}
          onChange={e => setSource(e.target.value)}
        />
        <button
          className='bg-white text-blue-500 py-2 px-4 rounded ml-2'
          onClick={() => feedElement(source)}
        >
          Add
        </button>
      </div>
    </>
  )
}

export default Header

在主要的page.tsx文件中导入它:

import Header from '@/components/Header'

const Homepage = () => {
  const options = { code: CodeBlock }

  return (
    <>
      <Header /> // 应该在顶部
      <div className='h-screen flex justify-between'>
        ...
      </div>
    </>
  )
}

现在的问题是,我们的状态位于父组件中,而Header是一个子组件。

如何在子组件中使用这些状态?最好的解决方案是在父组件中创建一个函数来改变状态,并将该函数传递给子组件。阅读这篇文章 (opens new window)

const Homepage = () => {
  const [source, setSource] = useState('');

  const feedElement = (syntax: string) => {
    return setSource(source + syntax)
  }
  
  return (
    ...
  )
}
``` <>
     <Header />
    ...
  )
}

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

Header.tsx 中,我们需要将函数作为参数接受,并将其添加到按钮的 onClick 属性中:

const Header = (
  { feedElement }: 
  { feedElement: (syntax: string) => void }
) => {
  const btns = [ ... ]

  return (
    ...
    <button
      key={btn.syntax}
      className="flex ...rounded-md"
      onClick={() => feedElement(btn.syntax)}
    >
      {btn.name}
    </button>
  )
}

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

回到 page.tsx,我们将 feedElement 函数传递给 Header

const feedElement = (syntax: string) => {
  return setSource(source + syntax)
}
  
return (
  <>
  <Header feedElement={feedElement} />
  ...
)

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

现在,每当您点击按钮时,您都应该获得以下 Markdown 元素。

结束语 #

就是这样。现在我们有了一个完全功能的 Markdown 编辑器。 如果你喜欢这篇文章或从中获得了一些收获,请给它一个红心💖并关注我以获取更多内容。

喜欢 (opens new window)