• Next.js 热更新 Markdown 文件变更


    🚀 优质资源分享 🚀

    学习路线指引(点击解锁)知识定位人群定位
    🧡 Python实战微信订餐小程序 🧡进阶级本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
    💛Python量化交易实战💛入门级手把手带你打造一个易扩展、更安全、效率更高的量化交易系统

    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)
        });
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    通知浏览器

    服务端通过 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 }))
            })
        })
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    浏览器连接服务器

    新建一个 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('http://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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    数据预览 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
    ) {
        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)
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    更新页面

    页面 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} - Gauliangtitle>
                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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    启动脚本

    更新 package.jsondev 脚本:

    "scripts": {
        "dev": "node scripts/watch.js & \n next dev"
    },
    
    
    • 1
    • 2
    • 3
    • 4

    总结

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

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

  • 相关阅读:
    手把手改进yolo训练自己的数据(坑洼路面识别)
    小程序用vue编写,保存表单出错
    动态树的第2大值
    IIC驱动OLED
    pytorch-损失函数-分类和回归区别
    “那天开始,我拒绝了油盐糖”
    【MicroPython ESP32】1.8“tft ST7735驱动3Dcube图形显示示例
    Python采集天气数据,做可视化分析【附源码】
    46、TCP的“三次握手”
    java代码审计-SpEL表达式注入
  • 原文地址:https://blog.csdn.net/qq_43479892/article/details/125457995