Next.js 提供了 Fast-Refresh 能力,它可以為您對 React 組件所做的編輯提供即時反饋。 但是,當你通過 Markdown 文件提供網站內容時,由於 Markdown 不是 React 組件,熱更新將失效。 怎麼做 解決該問題可從以下幾方面思考: 伺服器如何監控文件更新 伺服器 ...
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<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()
及 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"
},
總結
上述內容,整體概述了大致的實現邏輯。具體項目落地時,還需考慮一些細節信息,
如:文件更新時希望能夠在命令行提示更的文件名、針對個性化的路由信息調整文件與路由的匹配邏輯等。
Next.js 博客版原文:https://gauliang.github.io/blogs/2022/watch-markdown-files-and-hot-load-the-nextjs-page/
識微見遠 格物致知