本文使用到nextjs@10作為項(xiàng)目開發(fā),使用lru-cache插件作為直出結(jié)果緩存工具。本文所述的ssr緩存效果可以在獵豹影院看到。
如果大家只想知道如何實(shí)現(xiàn),可以直接跳到最后看實(shí)現(xiàn)源碼,當(dāng)然如果大家想知道nextjs直出緩存的相關(guān)細(xì)節(jié)可以以此往下閱讀。
getInitialProps or getServerSideProps
nextjs直出本身不存在緩存功能,我們需要先拿到直出的html內(nèi)容,然后將直出內(nèi)容緩存在服務(wù)器中。nextjs提供了一個(gè)renderToHTML api供我們獲取直出的html,我們可以通過下面的方式來(lái)調(diào)用:
const next = require('next');
const app = next({ dev: isDev });
// 獲取直出的html
app.renderToHTML(...);
nextjs也提供了getRequestHandlerapi,來(lái)獲取自動(dòng)處理頁(yè)面請(qǐng)求的函數(shù),const handle = app.getRequestHandler()。
在nextjs直出環(huán)境下,頁(yè)面組件中我們可以通過getInitialProps和getServerSideProps這兩個(gè)api來(lái)獲取記錄頁(yè)面渲染所需的數(shù)據(jù)。nextjs會(huì)將從這兩個(gè)api中拿到的數(shù)據(jù)寫入到id為__NEXT_DATA__的script標(biāo)簽中。
我們?cè)谶M(jìn)行業(yè)務(wù)開發(fā)的時(shí)候getInitialProps和getServerSideProps這兩個(gè)api只有一個(gè)有效,當(dāng)我們這兩個(gè)函數(shù)都定義以后,項(xiàng)目構(gòu)建中會(huì)報(bào)警。一般情況下,我們優(yōu)先使用getServerSideProps,這也是官方所推崇的。但是,在做直出數(shù)據(jù)緩存的時(shí)候我們需要使用getInitialProps api。關(guān)于這兩個(gè)api的使用,大家可以在nextjs官方文檔中看到,這里就不再贅述。
在調(diào)用renderToHTML渲染頁(yè)面的時(shí)候,使用getInitialProps和getServerSideProps這兩個(gè)api進(jìn)行數(shù)據(jù)獲取時(shí),渲染表現(xiàn)會(huì)有一定的出入。
-
getInitialProps:調(diào)用renderToHTML函數(shù),會(huì)返回直出的html,開發(fā)者需要手動(dòng)調(diào)用res.end將結(jié)果返回給客戶端。 -
getServerSideProps:調(diào)用renderToHTML函數(shù),renderToHTML內(nèi)部在獲得直出的html后,會(huì)去判斷當(dāng)前是否使用的getServerSideProps來(lái)獲取的直出數(shù)據(jù),如果是,就會(huì)直接調(diào)用res.end將數(shù)據(jù)緩存,然后將renderToHTML函數(shù)返回的直出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獲取直出數(shù)據(jù),調(diào)用renderToHtml函數(shù)時(shí)無(wú)法拿到html,并且直出結(jié)果會(huì)在renderToHtml函數(shù)中調(diào)用res.end響應(yīng)給客戶端,在調(diào)用renderToHtml后就沒法再調(diào)用res.end。
相對(duì)于getServerSideProps,getInitialProps作為頁(yè)面獲取數(shù)據(jù)的方式更加可控,我們不僅可以拿到直出的html,還可以控制如何響應(yīng)當(dāng)前請(qǐng)求。因此后面將使用getInitialProps作為直出數(shù)據(jù)獲取的方式。
直出緩存代碼
// 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原生請(qǐng)求處理函數(shù)
const handle = app.getRequestHandler();
// 緩存工具初始
const ssrCache = new LRUCache({
max: 100,
maxAge: 1 * 60 * 60 * 1000, // 1小時(shí)緩存
});
// 使用請(qǐng)求的url作為緩存key
function getCacheKey (req) {
return `${req.url}`
}
function renderAndCache (req, res, pagePath, queryParams) {
const key = getCacheKey(req)
// 如果緩存中有直出的html數(shù)據(jù),就直接將緩存內(nèi)容響應(yīng)給客戶端
if (ssrCache.has(key)) {
res.send(ssrCache.get(key));
return
}
// 如果沒有當(dāng)前緩存,調(diào)用renderToHTML生成直出html
app.renderToHTML(req, res, pagePath, queryParams)
.then((html) => {
if(res.statusCode === 200) {
// 使用緩存工具將html存放
ssrCache.set(key, html);
}else{
ssrCache.del(key);
}
// 響應(yīng)直出內(nèi)容
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()得到的原生資源處理函數(shù),靜態(tài)資源請(qǐng)求、直出請(qǐng)求這個(gè)函數(shù)都能正常處理
server.get('*', (req, res) => handle(req, res));
}
main();
// package.json
{
// ...
"scripts": {
"dev": "cross-env NODE_ENV=development node server.js",
}
}
// 頁(yè)面代碼
export default function Home() {}
Home.getInitialProps = async () => {
reutrn {
// 直出所需數(shù)據(jù)
}
}