Next.js 提供了 Fast-Refresh 能力,它可以為您對 React 元件所做的編輯提供即時反饋。
但是,當你通過 Markdown 檔案提供網站內容時,由於 Markdown 不是 React 元件,熱更新將失效。
解決該問題可從以下幾方面思考:
約定: 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 通知瀏覽器更新頁面。
瀏覽器需要知道被更新的檔案與當前頁面所在路由是否有關,因此,伺服器端傳送給瀏覽器的訊息應至少包含當前
更新檔案對應的頁面路由。
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
元件,負責監聽來自伺服器端的訊息,並熱實現頁面更新。元件滿足以下要求:
開發模式
下工作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,讀取 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/