我使用Next.js和TailwindCss构建了一个Markdown编辑器🔥 #
加入我,在这个项目中我们使用最新版的Nextjs构建一个在线Markdown编辑器。
目标 #
- 在Next.js项目中渲染Markdown
- 使用自定义组件
- 添加 Remark (opens new window) 和 Rehype (opens new window) 插件
- 学会从子组件中改变父组件的状态
- 玩得开心 🔥
点击这里 (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 (
...
这将用我们自定义的`CodeBlock`组件替换每个出现的`code`。 【可选】
BUG(🐛):您的代码组件周围可能会有一个奇怪的深色边框,这是由`pre`标签和tailwind样式引起的。
要解决这个问题,请返回到您的`Code.tsx`并添加以下代码,以从pre标签中删除tailwind样式。
export const Pre = ({ ...props }) => { return (
进入全屏模式 退出全屏模式
将其导入到您的`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 编辑器。 如果你喜欢这篇文章或从中获得了一些收获,请给它一个红心💖并关注我以获取更多内容。