蓝狮官网Next.js 热更新 Markdown 文件变更

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

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

服务器如何监控文件更新
服务器如何通知浏览器
浏览器如何更新页面
如何拿到最新的 Markdown 内容
如何与 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 组件,负责监听来自服务端的消息,并热实现页面更新。组件满足以下要求:

通过单例模式维护一个与 WebSocekt Server 的连接
监听到服务端消息后,判断当前页面路由是否与变更文件有关,无关则忽略
服务端消息可能会密集发送,需要在加载新版本内容时做防抖处理
加载 Markdown 文件并完成更新
该组件仅在 开发模式 下工作
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
数据预览 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)
}
}
更新页面
页面 pages/[…id].tsx 中引入 HotLoad 组件,并传递 setPostData() 及 params 给 HotLoad 组件。


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.json 的 dev 脚本:

“scripts”: {
“dev”: “node scripts/watch.js & \n next dev”
},
总结
上述内容,整体概述了大致的实现逻辑。具体项目落地时,还需考虑一些细节信息,
如:文件更新时希望能够在命令行提示更的文件名、针对个性化的路由信息调整文件与路由的匹配逻辑等。

0 Comments
Leave a Reply