如何实现Next.js中Markdown文件变更的热更新功能?

2026-04-18 02:024阅读0评论SEO教程
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计1334个文字,预计阅读时间需要6分钟。

如何实现Next.js中Markdown文件变更的热更新功能?

Next.js 提供了 Fast-Refresh 功能,为 React 组件的编辑提供即时反馈。然而,当使用 Markdown 文件提供网站内容时,由于 Markdown 不是 React 组件,热更新将失效。如何解决?

Next.js 提供了 Fast-Refresh 能力,它可以为您对 React 组件所做的编辑提供即时反馈。
但是,当你通过 Markdown 文件提供网站内容时,由于 Markdown 不是 React 组件,热更新将失效。

怎么做

解决该问题可从以下几方面思考:

  1. 服务器如何监控文件更新
  2. 服务器如何通知浏览器
  3. 浏览器如何更新页面
  4. 如何拿到最新的 Markdown 内容
  5. 如何与 Next.js 开发服务器一起启动
监控文件更新

约定: markdown 文件存放在 Next.js 项目根目录下的 _contents/

通过 node:fs.watch 模块递归的监控 _contents 目录,当文件发生变更,触发 listener 执行。
新建文件 scripts/watch.js 监控 _contents 目录。

const { watch } = require('node:fs'); function main(){ watch(process.cwd() + '/_contents', { recursive: true }, (eventType, filename) => { console.log(eventType, filename) }); } 通知浏览器

服务端通过 WebSocket 与浏览器建立连接,当开发服务器发现文件变更后,通过 WS 通知浏览器更新页面。
浏览器需要知道被更新的文件与当前页面所在路由是否有关,因此,服务端发送给浏览器的消息应至少包含当前
更新文件对应的页面路由。

WebSocket

ws 是一个简单易用、速度极快且经过全面测试的 WebSocket 客户端和服务器实现。通过 ws 启动 WebSocket 服务器。

const { watch } = require('node:fs'); const { WebSocketServer } = require('ws') function main() { const wss = new WebSocketServer({ port: 80 }) wss.on('connection', (ws, req) => { watch(process.cwd() + '/_contents', { recursive: true }, (eventType, filename) => { const path = filename.replace(/\.md/, '/') ws.send(JSON.stringify({ event: 'markdown-changed', path })) }) }) } 浏览器连接服务器

新建一个 HotLoad 组件,负责监听来自服务端的消息,并热实现页面更新。组件满足以下要求:

  1. 通过单例模式维护一个与 WebSocekt Server 的连接
  2. 监听到服务端消息后,判断当前页面路由是否与变更文件有关,无关则忽略
  3. 服务端消息可能会密集发送,需要在加载新版本内容时做防抖处理
  4. 加载 Markdown 文件并完成更新
  5. 该组件仅在 开发模式 下工作

import { useRouter } from "next/router" import { useEffect } from "react" interface Instance { ws: WebSocket timer: any } let instance: Instance = { ws: null as any, timer: null as any } function getInstance() { if (instance.ws === null) { instance.ws = new WebSocket('ws://localhost') } return instance } function _HotLoad({ setPost, params }: any) { const { asPath } = useRouter() useEffect(() => { const instance = getInstance() instance.ws.onmessage = async (res: any) => { const data = JSON.parse(res.data) if (data.event === 'markdown-changed') { if (data.path === asPath) { const post = await getPreviewData(params) setPost(post) } } } return () => { instance.ws.CONNECTING && instance.ws.close(4001, asPath) } }, []) return null } export function getPreviewData(params: {id:string[]}) { if (instance.timer) { clearTimeout(instance.timer) } return new Promise((resolve) => { instance.timer = setTimeout(async () => { const res = await fetch('localhost:3000/api/preview/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params) }) resolve(res.json()) }, 200) }) } let core = ({ setPost, params }: any)=>null if(process.env.NODE_ENV === 'development'){ console.log('development hot load'); core = _HotLoad } export const HotLoad = core 数据预览 API

创建数据预览 API,读取 Markdown 文件内容,并编译为页面渲染使用的格式。这里的结果
应与 [...id].tsx 页面中 getStaticProps() 方法返回的页面数据结构完全一致,相关
逻辑可直接复用。

新建 API 文件 pages/api/preview.ts

import type { NextApiRequest, NextApiResponse } from 'next' import { getPostData } from '../../lib/posts' type Data = { name: string } export default async function handler( req: NextApiRequest, res: NextApiResponse<Data> ) { if (process.env.NODE_ENV === 'development') { const params = req.body const post = await getPostData(['posts', ...params.id]) return res.status(200).json(post) } else { return res.status(200) } } 更新页面

页面 pages/[...id].tsx 中引入 HotLoad 组件,并传递 setPostData()paramsHotLoad 组件。

... import { HotLoad } from '../../components/hot-load' const Post = ({ params, post, prev, next }: Params) => { const [postData, setPostData] = useState(post) useEffect(()=>{ setPostData(post) },[post]) return ( <Layout> <Head> <title>{postData.title} - Gauliang</title> </Head> <PostContent post={postData} prev={prev} next={next} /> <BackToTop /> <HotLoad setPost={setPostData} params={params} /> </Layout> ) } export async function getStaticProps({ params }: Params) { return { props: { params, post:await getPostData(['posts', ...params.id]) } } } export async function getStaticPaths() { const paths = getAllPostIdByType() return { paths, fallback: false } } export default Post 启动脚本

更新 package.jsondev 脚本:

"scripts": { "dev": "node scripts/watch.js & \n next dev" }, 总结

上述内容,整体概述了大致的实现逻辑。具体项目落地时,还需考虑一些细节信息,
如:文件更新时希望能够在命令行提示更的文件名、针对个性化的路由信息调整文件与路由的匹配逻辑等。

如何实现Next.js中Markdown文件变更的热更新功能?

Next.js 博客版原文:gauliang.github.io/blogs/2022/watch-markdown-files-and-hot-load-the-nextjs-page/

识微见远 格物致知

本文共计1334个文字,预计阅读时间需要6分钟。

如何实现Next.js中Markdown文件变更的热更新功能?

Next.js 提供了 Fast-Refresh 功能,为 React 组件的编辑提供即时反馈。然而,当使用 Markdown 文件提供网站内容时,由于 Markdown 不是 React 组件,热更新将失效。如何解决?

Next.js 提供了 Fast-Refresh 能力,它可以为您对 React 组件所做的编辑提供即时反馈。
但是,当你通过 Markdown 文件提供网站内容时,由于 Markdown 不是 React 组件,热更新将失效。

怎么做

解决该问题可从以下几方面思考:

  1. 服务器如何监控文件更新
  2. 服务器如何通知浏览器
  3. 浏览器如何更新页面
  4. 如何拿到最新的 Markdown 内容
  5. 如何与 Next.js 开发服务器一起启动
监控文件更新

约定: markdown 文件存放在 Next.js 项目根目录下的 _contents/

通过 node:fs.watch 模块递归的监控 _contents 目录,当文件发生变更,触发 listener 执行。
新建文件 scripts/watch.js 监控 _contents 目录。

const { watch } = require('node:fs'); function main(){ watch(process.cwd() + '/_contents', { recursive: true }, (eventType, filename) => { console.log(eventType, filename) }); } 通知浏览器

服务端通过 WebSocket 与浏览器建立连接,当开发服务器发现文件变更后,通过 WS 通知浏览器更新页面。
浏览器需要知道被更新的文件与当前页面所在路由是否有关,因此,服务端发送给浏览器的消息应至少包含当前
更新文件对应的页面路由。

WebSocket

ws 是一个简单易用、速度极快且经过全面测试的 WebSocket 客户端和服务器实现。通过 ws 启动 WebSocket 服务器。

const { watch } = require('node:fs'); const { WebSocketServer } = require('ws') function main() { const wss = new WebSocketServer({ port: 80 }) wss.on('connection', (ws, req) => { watch(process.cwd() + '/_contents', { recursive: true }, (eventType, filename) => { const path = filename.replace(/\.md/, '/') ws.send(JSON.stringify({ event: 'markdown-changed', path })) }) }) } 浏览器连接服务器

新建一个 HotLoad 组件,负责监听来自服务端的消息,并热实现页面更新。组件满足以下要求:

  1. 通过单例模式维护一个与 WebSocekt Server 的连接
  2. 监听到服务端消息后,判断当前页面路由是否与变更文件有关,无关则忽略
  3. 服务端消息可能会密集发送,需要在加载新版本内容时做防抖处理
  4. 加载 Markdown 文件并完成更新
  5. 该组件仅在 开发模式 下工作

import { useRouter } from "next/router" import { useEffect } from "react" interface Instance { ws: WebSocket timer: any } let instance: Instance = { ws: null as any, timer: null as any } function getInstance() { if (instance.ws === null) { instance.ws = new WebSocket('ws://localhost') } return instance } function _HotLoad({ setPost, params }: any) { const { asPath } = useRouter() useEffect(() => { const instance = getInstance() instance.ws.onmessage = async (res: any) => { const data = JSON.parse(res.data) if (data.event === 'markdown-changed') { if (data.path === asPath) { const post = await getPreviewData(params) setPost(post) } } } return () => { instance.ws.CONNECTING && instance.ws.close(4001, asPath) } }, []) return null } export function getPreviewData(params: {id:string[]}) { if (instance.timer) { clearTimeout(instance.timer) } return new Promise((resolve) => { instance.timer = setTimeout(async () => { const res = await fetch('localhost:3000/api/preview/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params) }) resolve(res.json()) }, 200) }) } let core = ({ setPost, params }: any)=>null if(process.env.NODE_ENV === 'development'){ console.log('development hot load'); core = _HotLoad } export const HotLoad = core 数据预览 API

创建数据预览 API,读取 Markdown 文件内容,并编译为页面渲染使用的格式。这里的结果
应与 [...id].tsx 页面中 getStaticProps() 方法返回的页面数据结构完全一致,相关
逻辑可直接复用。

新建 API 文件 pages/api/preview.ts

import type { NextApiRequest, NextApiResponse } from 'next' import { getPostData } from '../../lib/posts' type Data = { name: string } export default async function handler( req: NextApiRequest, res: NextApiResponse<Data> ) { if (process.env.NODE_ENV === 'development') { const params = req.body const post = await getPostData(['posts', ...params.id]) return res.status(200).json(post) } else { return res.status(200) } } 更新页面

页面 pages/[...id].tsx 中引入 HotLoad 组件,并传递 setPostData()paramsHotLoad 组件。

... import { HotLoad } from '../../components/hot-load' const Post = ({ params, post, prev, next }: Params) => { const [postData, setPostData] = useState(post) useEffect(()=>{ setPostData(post) },[post]) return ( <Layout> <Head> <title>{postData.title} - Gauliang</title> </Head> <PostContent post={postData} prev={prev} next={next} /> <BackToTop /> <HotLoad setPost={setPostData} params={params} /> </Layout> ) } export async function getStaticProps({ params }: Params) { return { props: { params, post:await getPostData(['posts', ...params.id]) } } } export async function getStaticPaths() { const paths = getAllPostIdByType() return { paths, fallback: false } } export default Post 启动脚本

更新 package.jsondev 脚本:

"scripts": { "dev": "node scripts/watch.js & \n next dev" }, 总结

上述内容,整体概述了大致的实现逻辑。具体项目落地时,还需考虑一些细节信息,
如:文件更新时希望能够在命令行提示更的文件名、针对个性化的路由信息调整文件与路由的匹配逻辑等。

如何实现Next.js中Markdown文件变更的热更新功能?

Next.js 博客版原文:gauliang.github.io/blogs/2022/watch-markdown-files-and-hot-load-the-nextjs-page/

识微见远 格物致知