nextjs ssr資料快取
本文使用到nextjs@10作為專案開發,使用lru-cache外掛作為直出結果快取工具。本文所述的ssr快取效果可以在獵豹影院看到。
如果大家只想知道如何實現,可以直接跳到最後看實現原始碼,當然如果大家想知道nextjs直出快取的相關細節可以以此往下閱讀。
getInitialProps or getServerSideProps
nextjs直出本身不存在快取功能,我們需要先拿到直出的html內容,然後將直出內容快取在伺服器中。nextjs提供了一個renderToHTML
api供我們獲取直出的html,我們可以通過下面的方式來呼叫:
const next = require('next'); const app = next({ dev: isDev }); // 獲取直出的html app.renderToHTML(...);
nextjs也提供了getRequestHandler
api,來獲取自動處理頁面請求的函式,const handle = app.getRequestHandler()
。
在nextjs直出環境下,頁面元件中我們可以通過getInitialProps
和getServerSideProps
這兩個api來獲取記錄頁面渲染所需的資料。nextjs會將從這兩個api中拿到的資料寫入到id為__NEXT_DATA__
的script標籤中。
我們在進行業務開發的時候getInitialProps
和getServerSideProps
這兩個api只有一個有效,當我們這兩個函式都定義以後,專案構建中會報警。一般情況下,我們優先使用getServerSideProps
getInitialProps
api。關於這兩個api的使用,大家可以在nextjs官方文件中看到,這裡就不再贅述。
在呼叫renderToHTML
渲染頁面的時候,使用getInitialProps
和getServerSideProps
這兩個api進行資料獲取時,渲染表現會有一定的出入。
getInitialProps
:呼叫renderToHTML
函式,會返回直出的html,開發者需要手動呼叫res.end
將結果返回給客戶端。getServerSideProps
:呼叫renderToHTML
函式,renderToHTML
內部在獲得直出的html後,會去判斷當前是否使用的getServerSideProps
res.end
將資料快取,然後將renderToHTML
函式返回的直出html至空,原始碼如下:
// https://github.com/vercel/next.js/blob/canary/packages/next/next-server/server/next-server.ts#L1842
if (
!isResSent(res) &&
!isNotFound &&
(isSSG || isDataReq || hasServerProps)
) {
if (isRedirect && !isDataReq) {
await handleRedirect(pageData)
} else {
sendPayload(...)
}
resHtml = null
}
// https://github.com/vercel/next.js/blob/canary/packages/next/next-server/server/send-payload.ts#L38
export function sendPayload(
req: IncomingMessage,
res: ServerResponse,
payload: any,
type: 'html' | 'json',
{
generateEtags,
poweredByHeader,
}: { generateEtags: boolean; poweredByHeader: boolean },
options?: PayloadOptions
): void {
// ...
if (!res.getHeader('Content-Type')) {
res.setHeader(
'Content-Type',
type === 'json' ? 'application/json' : 'text/html; charset=utf-8'
)
}
res.setHeader('Content-Length', Buffer.byteLength(payload))
res.end(req.method === 'HEAD' ? null : payload)
}
通過上面我們瞭解到通過getServerSideProps
api獲取直出資料,呼叫renderToHtml
函式時無法拿到html,並且直出結果會在renderToHtml
函式中呼叫res.end
響應給客戶端,在呼叫renderToHtml
後就沒法再呼叫res.end
。
相對於getServerSideProps
,getInitialProps
作為頁面獲取資料的方式更加可控,我們不僅可以拿到直出的html,還可以控制如何響應當前請求。因此後面將使用getInitialProps
作為直出資料獲取的方式。
直出快取程式碼
// server.js
const express = require('express');
const next = require('next');
const LRUCache = require('lru-cache');
const port = parseInt(process.env.PORT, 10) || 3000;
const isDev = process.env.NODE_ENV === 'development';
const app = next({ dev: isDev });
// nextjs原生請求處理函式
const handle = app.getRequestHandler();
// 快取工具初始
const ssrCache = new LRUCache({
max: 100,
maxAge: 1 * 60 * 60 * 1000, // 1小時快取
});
// 使用請求的url作為快取key
function getCacheKey (req) {
return `${req.url}`
}
function renderAndCache (req, res, pagePath, queryParams) {
const key = getCacheKey(req)
// 如果快取中有直出的html資料,就直接將快取內容響應給客戶端
if (ssrCache.has(key)) {
res.send(ssrCache.get(key));
return
}
// 如果沒有當前快取,呼叫renderToHTML生成直出html
app.renderToHTML(req, res, pagePath, queryParams)
.then((html) => {
if(res.statusCode === 200) {
// 使用快取工具將html存放
ssrCache.set(key, html);
}else{
ssrCache.del(key);
}
// 響應直出內容
res.send(html);
})
.catch((err) => {
app.renderError(err, req, res, pagePath, queryParams)
})
}
async function main() {
await app.prepare();
const server = express();
server.listen(port, (err) => {
if (err) throw err;
console.log(`> Ready on http://localhost:${port}`);
});
server.get('/', (req, res) => renderAndCache(req, res, '/'));
// app.getRequestHandler()得到的原生資源處理函式,靜態資源請求、直出請求這個函式都能正常處理
server.get('*', (req, res) => handle(req, res));
}
main();
// package.json
{
// ...
"scripts": {
"dev": "cross-env NODE_ENV=development node server.js",
}
}
// 頁面程式碼
export default function Home() {}
Home.getInitialProps = async () => {
reutrn {
// 直出所需資料
}
}