媚丶 發表於 2022-6-24 19:25:00

Next.js 热更新 Markdown 文件变更

<p>Next.js 提供了 Fast-Refresh 能力,它可以为您对 React 组件所做的编辑提供即时反馈。<br>
但是,当你通过 Markdown 文件提供网站内容时,由于 Markdown 不是 React 组件,热更新将失效。</p>
<h2 id="怎么做">怎么做</h2>
<p>解决该问题可从以下几方面思考:</p>
<ol>
<li>服务器如何监控文件更新</li>
<li>服务器如何通知浏览器</li>
<li>浏览器如何更新页面</li>
<li>如何拿到最新的 Markdown 内容</li>
<li>如何与 Next.js 开发服务器一起启动</li>
</ol>
<h2 id="监控文件更新">监控文件更新</h2>
<blockquote>
<p>约定: markdown 文件存放在 Next.js 项目根目录下的 <code>_contents/</code> 中</p>
</blockquote>
<p>通过 <code>node:fs.watch</code> 模块递归的监控 <code>_contents</code> 目录,当文件发生变更,触发 listener 执行。<br>
新建文件 <code>scripts/watch.js</code> 监控 <code>_contents</code> 目录。</p>
<pre><code class="language-js">const { watch } = require('node:fs');

function main(){
    watch(process.cwd() + '/_contents', { recursive: true }, (eventType, filename) =&gt; {
      console.log(eventType, filename)
    });
}
</code></pre>
<h2 id="通知浏览器">通知浏览器</h2>
<p>服务端通过 WebSocket 与浏览器建立连接,当开发服务器发现文件变更后,通过 WS 通知浏览器更新页面。<br>
浏览器需要知道被更新的文件与当前页面所在路由是否有关,因此,服务端发送给浏览器的消息应至少包含当前<br>
更新文件对应的页面路由。</p>
<h3 id="websocket">WebSocket</h3>
<p><code>ws</code> 是一个简单易用、速度极快且经过全面测试的 WebSocket 客户端和服务器实现。通过 <code>ws</code> 启动 WebSocket 服务器。</p>
<pre><code class="language-js">const { watch } = require('node:fs');
const { WebSocketServer } = require('ws')

function main() {
    const wss = new WebSocketServer({ port: 80 })
    wss.on('connection', (ws, req) =&gt; {
      watch(process.cwd() + '/_contents', { recursive: true }, (eventType, filename) =&gt; {
            const path = filename.replace(/\.md/, '/')
            ws.send(JSON.stringify({ event: 'markdown-changed', path }))
      })
    })
}
</code></pre>
<h2 id="浏览器连接服务器">浏览器连接服务器</h2>
<p>新建一个 <code>HotLoad</code> 组件,负责监听来自服务端的消息,并热实现页面更新。组件满足以下要求:</p>
<ol>
<li>通过单例模式维护一个与 WebSocekt Server 的连接</li>
<li>监听到服务端消息后,判断当前页面路由是否与变更文件有关,无关则忽略</li>
<li>服务端消息可能会密集发送,需要在加载新版本内容时做防抖处理</li>
<li>加载 Markdown 文件并完成更新</li>
<li>该组件仅在 <code>开发模式</code> 下工作</li>
</ol>
<pre><code class="language-tsx">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(() =&gt; {
      const instance = getInstance()
      instance.ws.onmessage = async (res: any) =&gt; {
            const data = JSON.parse(res.data)
            if (data.event === 'markdown-changed') {
                if (data.path === asPath) {
                  const post = await getPreviewData(params)
                  setPost(post)
                }
            }
      }
      return () =&gt; {
            instance.ws.CONNECTING &amp;&amp; instance.ws.close(4001, asPath)
      }
    }, [])
    return null
}

export function getPreviewData(params: {id:string[]}) {
    if (instance.timer) {
      clearTimeout(instance.timer)
    }
    return new Promise((resolve) =&gt; {
      instance.timer = setTimeout(async () =&gt; {
            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)=&gt;null

if(process.env.NODE_ENV === 'development'){
    console.log('development hot load');
    core = _HotLoad
}

export const HotLoad = core
</code></pre>
<h2 id="数据预览-api">数据预览 API</h2>
<p>创建数据预览 API,读取 Markdown 文件内容,并编译为页面渲染使用的格式。这里的结果<br>
应与 <code>[...id].tsx</code> 页面中 <code>getStaticProps()</code> 方法返回的页面数据结构完全一致,相关<br>
逻辑可直接复用。</p>
<p>新建 API 文件 <code>pages/api/preview.ts</code>,</p>
<pre><code class="language-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&lt;Data&gt;
) {
    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)
    }
}
</code></pre>
<h2 id="更新页面">更新页面</h2>
<p>页面 <code>pages/[...id].tsx</code> 中引入 <code>HotLoad</code> 组件,并传递 <code>setPostData()</code> 及 <code>params</code> 给 <code>HotLoad</code> 组件。</p>
<pre><code class="language-tsx">...
import { HotLoad } from '../../components/hot-load'

const Post = ({ params, post, prev, next }: Params) =&gt; {
    const = useState(post)
   
    useEffect(()=&gt;{
      setPostData(post)
    },)

    return (
      &lt;Layout&gt;
            &lt;Head&gt;
                &lt;title&gt;{postData.title} - Gauliang&lt;/title&gt;
            &lt;/Head&gt;
            &lt;PostContent post={postData} prev={prev} next={next} /&gt;
            &lt;BackToTop /&gt;
            &lt;HotLoad setPost={setPostData} params={params} /&gt;
      &lt;/Layout&gt;
    )
}

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
</code></pre>
<h2 id="启动脚本">启动脚本</h2>
<p>更新 <code>package.json</code> 的 <code>dev</code> 脚本:</p>
<pre><code class="language-json">"scripts": {
    "dev": "node scripts/watch.js &amp; \n next dev"
},
</code></pre>
<h2 id="总结">总结</h2>
<p>上述内容,整体概述了大致的实现逻辑。具体项目落地时,还需考虑一些细节信息,<br>
如:文件更新时希望能够在命令行提示更的文件名、针对个性化的路由信息调整文件与路由的匹配逻辑等。</p>
<p>Next.js 博客版原文:https://gauliang.github.io/blogs/2022/watch-markdown-files-and-hot-load-the-nextjs-page/</p>


</div>
<div id="MySignature" role="contentinfo">
    识微见远 格物致知<br><br>
来源:https://www.cnblogs.com/kelsen/p/watch-markdown-files-and-hot-load-the-nextjs-page.html
頁: [1]
查看完整版本: Next.js 热更新 Markdown 文件变更