next.js 源码解析 - getStaticProps、getStaticPaths 篇
<blockquote><p>😂 好久前写了关于 <code>getStaticProps</code> 和 <code>getStaticPaths</code> 的内容,然而半年过去了源码解析就一直忘记了,不久前有人提醒才想起来,补下坑。</p>
</blockquote>
<p>本文主要是解读下 <code>getStaticProps</code>、<code>getStaticPaths</code> 相关的源码,不了解这两个 <code>API</code> 的建议先看下之前的文章再看。👀</p>
<h2 id="getstaticprops">getStaticProps</h2>
<p>首先 <code>getStaticProps</code> 是应用于 <code>SSG</code> 场景,我们先看下 <code>packages/next/server/render.tsx</code> 中相关的代码:</p>
<pre><code class="language-ts">const isSSG = !!getStaticProps;
const pageIsDynamic = isDynamicRoute(pathname);
if (isSSG && !isFallback) {
let data: UnwrapPromise<ReturnType<GetStaticProps>>;
try {
data = await getStaticProps!({
...(pageIsDynamic ? { params: query as ParsedUrlQuery } : undefined),
...(isPreview ? { preview: true, previewData: previewData } : undefined),
locales: renderOpts.locales,
locale: renderOpts.locale,
defaultLocale: renderOpts.defaultLocale
});
} catch (staticPropsError: any) {
// ....
}
// ...
}
</code></pre>
<p><code>isFallback</code> 可以先不管。可以看到 <code>getStaticProps</code> 同样可以为异步函数,而是否为 <code>SSG</code> 就是由是否存在 <code>getStaticProps</code> 函数来决定的,<code>SSG</code> 场景下的 <code>pageIsDynamic</code> 则必须配合 <code>getStaticPaths</code> 使用,可以看到 <code>getStaticProps</code> 会接收几个参数:</p>
<ul>
<li><code>params</code> 是在动态页面的路由参数</li>
<li><code>previewData</code> 和 <code>preview: preview</code> 模式的相关数据</li>
<li><code>locales, locale</code> 和 <code>defaultLocale</code> 多语言相关参数</li>
</ul>
<p>执行完成后 <code>getStaticProps</code> 的返回值会被放入 <code>pageProps</code> 中。</p>
<p>再看看 <code>invalidKeys</code> 相关部分,除了 <code>revalidate</code>、<code>props</code>、<code>redirect</code> 和 <code>notFound</code> 外别的属性都会被视为非法。</p>
<pre><code class="language-ts">const invalidKeys = Object.keys(data).filter(
key => key !== 'revalidate' && key !== 'props' && key !== 'redirect' && key !== 'notFound'
);
if (invalidKeys.includes('unstable_revalidate')) {
throw new Error(UNSTABLE_REVALIDATE_RENAME_ERROR);
}
if (invalidKeys.length) {
throw new Error(invalidKeysMsg('getStaticProps', invalidKeys));
}
</code></pre>
<p>然后还有关于 <code>notFound</code> 和 <code>redirect</code> 的处理:</p>
<pre><code class="language-ts">if ('notFound' in data && data.notFound) {
if (pathname === '/404') {
throw new Error(`The /404 page can not return notFound in "getStaticProps", please remove it to continue!`);
}
(renderOpts as any).isNotFound = true;
}
if ('redirect' in data && data.redirect && typeof data.redirect === 'object') {
checkRedirectValues(data.redirect as Redirect, req, 'getStaticProps');
if (isBuildTimeSSG) {
throw new Error(
`\`redirect\` can not be returned from getStaticProps during prerendering (${req.url})\n` +
`See more info here: https://nextjs.org/docs/messages/gsp-redirect-during-prerender`
);
}
(data as any).props = {
__N_REDIRECT: data.redirect.destination,
__N_REDIRECT_STATUS: getRedirectStatus(data.redirect)
};
if (typeof data.redirect.basePath !== 'undefined') {
(data as any).props.__N_REDIRECT_BASE_PATH = data.redirect.basePath;
}
(renderOpts as any).isRedirect = true;
}
</code></pre>
<p><code>notFound</code> 会使用 <code>renderOpts.isNotFound</code> 来标识,而 <code>redirect</code> 则会在 <code>props</code> 中通过 <code>__N_REDIRECT</code> 相关的参数来进行标识。</p>
<p>当然这里省略很多的校验,比如 <code>getStaticProps</code> 和 <code>getServerSideProps</code> 冲突、<code>getStaticPaths</code> 的检查、<code>notFound</code> 和 <code>redirect</code> 不能同时存在等。</p>
<pre><code class="language-ts">props.pageProps = Object.assign({}, props.pageProps, 'props' in data ? data.props : undefined);
</code></pre>
<p>然后其中还包含了一部分与 <code>revalidate</code> 相关的内容,主要是一些检测和值的处理,主要与 <code>ISR</code> 相关的此处先跳过。</p>
<h2 id="getstaticpaths">getStaticPaths</h2>
<p><code>getStaticPaths</code> 的相关的调用源码主要在 <code>packages/next/build/utils.ts</code> 文件中的 <code>buildStaticPaths</code> 中,<code>buildStaticPaths</code> 会在两个时候被调用,一个是 <code>next.js</code> 构建的时候,第二个是 <code>next.js</code> 的 <code>devServer</code> 中。在 <code>next.js</code> 遇到动态路由时,会按照 <code>buildStaticPaths</code> 和 <code>getStaticProps</code> 来决定是否启用 <code>SSG</code> 模式,启用则会调用 <code>buildStaticPaths</code> 获取该动态路由所对应的需要构建的所有静态页面。</p>
<pre><code class="language-ts">if (getStaticPaths) {
staticPathsResult = await getStaticPaths({ locales, defaultLocale });
}
if (!staticPathsResult || typeof staticPathsResult !== 'object' || Array.isArray(staticPathsResult)) {
throw new Error(
`Invalid value returned from getStaticPaths in ${page}. Received ${typeof staticPathsResult} ${expectedReturnVal}`
);
}
const invalidStaticPathKeys = Object.keys(staticPathsResult).filter(key => !(key === 'paths' || key === 'fallback'));
if (invalidStaticPathKeys.length > 0) {
throw new Error(
`Extra keys returned from getStaticPaths in ${page} (${invalidStaticPathKeys.join(', ')}) ${expectedReturnVal}`
);
}
if (!(typeof staticPathsResult.fallback === 'boolean' || staticPathsResult.fallback === 'blocking')) {
throw new Error(`The \`fallback\` key must be returned from getStaticPaths in ${page}.\n` + expectedReturnVal);
}
const toPrerender = staticPathsResult.paths;
if (!Array.isArray(toPrerender)) {
throw new Error(
`Invalid \`paths\` value returned from getStaticPaths in ${page}.\n` +
`\`paths\` must be an array of strings or objects of shape { params: : string }`
);
}
</code></pre>
<p>在 <code>buildStaticPaths</code> 第一部分是获取 <code>getStaticPaths</code> 的返回值,并对其返回值进行检查:</p>
<ol>
<li><code>getStaticPaths</code> 可以为 <code>async</code> 方法</li>
<li><code>getStaticPaths</code> 接受两个参数:<code>locales</code> 和 <code>defaultLocale</code></li>
<li>返回值必须为 <code>{paths: Array, fallback: boolean | 'blocking'}</code> 结构</li>
</ol>
<p>而在拿到 <code>toPrerender</code> 之后,<code>next.js</code> 会将其转换为 <code>prerenderPaths</code> 和 <code>encodedPrerenderPaths</code>,这两个 <code>set</code> 的数据集基本一致,只是一个 <code>path</code> 为已经被解码,一个没有,猜测是为了性能考虑空间换时间。</p>
<pre><code class="language-ts">toPrerender.forEach(entry => {
if (typeof entry === 'string') {
entry = removeTrailingSlash(entry);
const localePathResult = normalizeLocalePath(entry, locales);
let cleanedEntry = entry;
if (localePathResult.detectedLocale) {
cleanedEntry = entry.slice(localePathResult.detectedLocale.length + 1);
} else if (defaultLocale) {
entry = `/${defaultLocale}${entry}`;
}
const result = _routeMatcher(cleanedEntry);
if (!result) {
throw new Error(`The provided path \`${cleanedEntry}\` does not match the page: \`${page}\`.`);
}
// If leveraging the string paths variant the entry should already be
// encoded so we decode the segments ensuring we only escape path
// delimiters
prerenderPaths.add(
entry
.split('/')
.map(segment => escapePathDelimiters(decodeURIComponent(segment), true))
.join('/')
);
encodedPrerenderPaths.add(entry);
} else {
// ...
}
});
</code></pre>
<p>针对 <code>string</code> 类型的 <code>entry</code>,简单的处理下语言、路径即可。</p>
<pre><code class="language-ts">const _validParamKeys = Object.keys(_routeMatcher(page));
if (typeof entry === 'string') {
// ...
} else {
const invalidKeys = Object.keys(entry).filter(key => key !== 'params' && key !== 'locale');
if (invalidKeys.length) {
throw new Error('...');
}
const { params = {} } = entry;
let builtPage = page;
let encodedBuiltPage = page;
_validParamKeys.forEach(validParamKey => {
const { repeat, optional } = _routeRegex.groups;
let paramValue = params;
if (
optional &&
params.hasOwnProperty(validParamKey) &&
(paramValue === null || paramValue === undefined || (paramValue as any) === false)
) {
paramValue = [];
}
if ((repeat && !Array.isArray(paramValue)) || (!repeat && typeof paramValue !== 'string')) {
throw new Error('...');
}
let replaced = `[${repeat ? '...' : ''}${validParamKey}]`;
if (optional) {
replaced = `[${replaced}]`;
}
builtPage = builtPage
.replace(
replaced,
repeat
? (paramValue as string[]).map(segment => escapePathDelimiters(segment, true)).join('/')
: escapePathDelimiters(paramValue as string, true)
)
.replace(/(?!^)\/$/, '');
encodedBuiltPage = encodedBuiltPage
.replace(
replaced,
repeat
? (paramValue as string[]).map(encodeURIComponent).join('/')
: encodeURIComponent(paramValue as string)
)
.replace(/(?!^)\/$/, '');
});
if (entry.locale && !locales?.includes(entry.locale)) {
throw new Error('...');
}
const curLocale = entry.locale || defaultLocale || '';
prerenderPaths.add(`${curLocale ? `/${curLocale}` : ''}${curLocale && builtPage === '/' ? '' : builtPage}`);
encodedPrerenderPaths.add(
`${curLocale ? `/${curLocale}` : ''}${curLocale && encodedBuiltPage === '/' ? '' : encodedBuiltPage}`
);
}
</code></pre>
<p>而对于 <code>Object</code> 类型的 <code>entry</code>,则会先检查确保是 <code>{params, locale}</code> 结构,然后使用 <code>params</code> 对动态路由进行替换拼接。 <code>_validParamKeys</code> 是该动态路由页面中的参数的 <code>key</code> 数组。然后一样是路径和语言的处理。最终的返回值如下:</p>
<pre><code class="language-ts">return {
paths: [...prerenderPaths],
fallback: staticPathsResult.fallback,
encodedPaths: [...encodedPrerenderPaths]
};
</code></pre>
<p>当需要时 <code>next.js</code> 就会使用这里的 <code>paths</code> 来生成对应的静态页面,从而实现动态路由的 <code>SSG</code>。</p>
<h2 id="总结">总结</h2>
<p><code>getStaticProps</code>、<code>getStaticPaths</code> 相关的源码其实大部分都是在处理关于数据检查、处理这类的事情,因为这两个 <code>API</code> 的指责也都很简单:<code>getStaticPaths</code> 负责为动态路由的 <code>SSG</code> 场景提供页面列表,<code>getStaticProps</code> 则为 <code>SSG</code> 页面提供对应的页面数据。</p><br><br>
来源:https://www.cnblogs.com/zxbing0066/p/17632619.html
頁:
[1]