原文:
Next.js 提供 Fast-Refresh 功能,该功能可针对您对 React 组件所做的编辑提供即时反馈。
但是,当您通过 Markdown 文件提供网站内容时,由于 Markdown 不是 React 组件,因此热更新将失败。
怎么做
要解决这个问题,考虑以下几个方面:
服务器如何监控文件更新 服务器如何通知浏览器 浏览器如何更新页面 如何获取最新的 Markdown 内容 如何使用 Next.js 开发服务器开始监控文件更新
约定:markdown文件存放在Next.js项目根目录下的_contents/中
通过node:fs.watch模块递归监听_contents目录,文件变化时触发监听器执行。
新建一个文件 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 服务器的连接。监听服务器消息后,判断当前页面路由是否与更改的文件有关。如果不相关,服务器消息可能会被忽略并集中发送。加载新版本内容时需要这样做。去抖动加载 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 文件页面/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 (
{postData.title} - Gauliang
)
}
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 中的开发脚本:
"scripts": {
"dev": "node scripts/watch.js & n next dev"
},
总结
以上内容整体概括了大致的实现逻辑。在具体项目实施时,需要考虑一些细节,
比如文件更新时,希望命令行能提示更新后的文件名,根据个性化的路由信息调整文件与路由的匹配逻辑。
Next.js 博客版原创:
暂无评论内容