本文是next.js 的服务端渲染机制(一)的后续
server/render.js
这个模块是服务端渲染的核心模块,它主要完成了三个环节:
- URL path 到组件的文件路径的匹配;
- 调用 react 的服务端渲染方法,拼接出完整的 html 字符串;
- document 请求应答。
承接next.js 的服务端渲染机制(一),先看renderToHTML()
方法,我们定位到它调用了一个doRender()
函数。
async function doRender(
req,
res,
pathname,
query,
{
err,
page,
buildId,
buildStats,
hotReloader,
assetPrefix,
availableChunks,
dir = process.cwd(),
dev = false,
staticMarkup = false,
nextExport = false,
} = {},
) {
page = page || pathname;
await ensurePage(page, { dir, hotReloader });
const dist = getConfig(dir).distDir;
// 引入当前url指定path的page
let [Component, Document] = await Promise.all([
requireModule(join(dir, dist, 'dist', 'pages', page)),
requireModule(join(dir, dist, 'dist', 'pages', '_document')),
]);
Component = Component.default || Component;
Document = Document.default || Document;
const asPath = req.url;
// ctx传入源
const ctx = { err, req, res, pathname, query, asPath };
// 执行getInitialProps函数
const props = await loadGetInitialProps(Component, ctx);
// the response might be finshed on the getinitialprops call
if (res.finished) return;
const renderPage = (enhancer = Page => Page) => {
// 生成用App包裹的page
const app = createElement(App, {
Component: enhancer(Component),
props,
router: new Router(pathname, query),
});
const render = staticMarkup ? renderToStaticMarkup : renderToString;
let html;
let head;
let errorHtml = '';
try {
// 服务端渲染页面组件
html = render(app);
} finally {
head = Head.rewind() || defaultHead();
}
// 获取到当前需要动态加载的模块的列表
const chunks = loadChunks({ dev, dir, dist, availableChunks });
if (err && dev) {
errorHtml = render(createElement(ErrorDebug, { error: err }));
}
return { html, head, errorHtml, chunks };
};
// 执行document的getInitialProps
const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage });
// While developing, we should not cache any assets.
// So, we use a different buildId for each page load.
// With that we can ensure, we have unique URL for assets per every page load.
// So, it'll prevent issues like this: https://git.io/vHLtb
const devBuildId = Date.now();
if (res.finished) return;
if (!Document.prototype || !Document.prototype.isReactComponent)
throw new Error('_document.js is not exporting a React element');
// 生成document对应的元素
const doc = createElement(Document, {
__NEXT_DATA__: {
props,
pathname,
query,
buildId: dev ? devBuildId : buildId,
buildStats,
assetPrefix,
nextExport,
err: err ? serializeError(dev, err) : null,
},
dev,
dir,
staticMarkup,
...docProps,
});
return '<!DOCTYPE html>' + renderToStaticMarkup(doc);
}
这段代码很长,我分段讲述。首先它完成了一个我们一直存疑的环节——路由到组件路径的匹配。通过一个简单的 require 模块,动态地引入 page component,并在同时将 page 目录下的_document
组件也引入进来。
获取到对应的 page component 之后,next 显示地调用了这个组件的getInitialProps()
方法。我们知道,getInitialProps()
方法是 next 对react 组件生命周期的拓展,是一个只会在服务端执行的 hook 函数,页面首屏需要的数据信息一律都在这个钩子函数中作接口获取。而 ctx 正是我们在getInitialProps()
中获取到的传参。
紧接着,next 定义了一个在后边执行的函数,这个函数的主要作用是利用 react 提供的 createElement()
方法和renderToStaticMarkup() / renderToString()
方法,将组件渲染成字符串,并且生成文档头部和获取到当前页面依赖的动态模块的chunk
,一并返回回去。
这里边第一是用到了一个包裹组件——lib / app.js
。它是业务组件外裹的第一层,主要用于: 1、将路由信息和 router 的一些方法聚合到一个对象上并挂载在组件的 props 中; 2、模拟浏览器实现对 hash 值的定位处理。
其次、loadchunks()
用于获取当前需要加载的动态模块,而我们知道,动态模块是通过 next 提供的 dynamic 方法引入的,形如:
import dynamic from 'next/dynamic'
const DynamicComponent = dynamic(import('../components/hello'))
其机理是通过 dynamic 引入的模块,其逻辑代码会被 webpack 打包到另外的chunk,如果模块在当前服务端渲染中被需要时,dynamic 首先会把对应的 html 补充到前边的 page component 中,然后登记它的chunkName
,而在这里就通过loadChunks
这个方法把所有动态模块的chunk
收集起来。
跟着,next 调用 Document 组件的getInitalProps()
方法,并将获取到的
props,连同其它一些信息作为 Document 组件的 props,传入并实例化这个组件,最后执行该组件的服务端渲染。这个组件是 page component 最外层的组件,用于补充文档头部、script和样式,并填充渲染完的 content HTML,拼接成完整的 document。
最后,补充DOCTYPE
,返回整个文档字符串。
相比,渲染完 HTML 字符串后执行的
sendHTML()
就显得很简单了。tag的生成和更新,缓存有效性判定,http header 的设置,请求应答,完事。next 的整个服务端渲染流程就大概是这样子,大多为自己摸索,有错误还烦请指出。