注意,以下内容包含大量代码,为了良好的阅读体验.建议复制到 vscode 或者其他 markdown 编辑器里查看
---
有的时候,一个 bug,它不会直接让你定位的到,它会表现出别的 bug 的形式来误导你,我想你应该已经感受到了
事实上,在 ppr 里,在 `unstable_cache` 里使用 `unstable_noStore` 没有任何问题,
因为什么都不会发生
```ts
// unstable_noStore.ts
export function unstable_noStore() {
...
else {
store.isUnstableNoStore = true
markCurrentScopeAsDynamic(store, callingExpression)
}
}
```
```ts
// dynamic-rendering.ts
export function markCurrentScopeAsDynamic(
store: StaticGenerationStore,
expression: string,
): void {
...
if (store.isUnstableCacheCallback) {
// inside cache scopes marking a scope as dynamic has no effect because the outer cache scope
// creates a cache boundary. This is subtly different from reading a dynamic data source which is
// forbidden inside a cache scope.
return;
}
}
```
除非是使用 `unstable_cache` 本身带来的其它问题
比如
```ts
// unstable-cache.ts
if (options.revalidate === 0) {
throw new Error(
`Invariant revalidate: 0 can not be passed to unstable_cache(), must be "false" or "> 0" ${cb.toString()}`,
);
}
```
不过应该不是这里的问题
---
> 然后我最后发现,要成功,只能把两个想做静态的组件在原来的基础上用上 cache ,这样页面才会如预期,只有一个是动态渲染的,其他俩静态(最终只有一个地方用了 noStore ),这应该就是你说的 cache 一把梭了。。。
> 然后我试了你的那个改源码的办法,出意外了。。。没办法通过改源码,把我上面的 俩 cache 给省掉啊。不加 cache 还是会报错。
我当时说的 `unstable_cache` 梭哈指的是全部使用 `unstable_noStore` + `unstable_cache` ,
放弃在其中一个组件开启 `unstable_noStore` 的时候,预渲染另一个带有 `db` 操作的组件的幻想
你的意思是,
- 在需要预渲染的组件里用 `unstable_cache` 缓存 db 函数,不使用 unstable_noStore
- 在需要动态渲染的组件里用 `unstable_noStore`
这样就可以了?
说实话我很质疑...不过 `unstable_cache` 的源码部分我没怎么看,因为没测试这一部分,主要精力直接放在不加 `unstable_cache` 就可以预渲染,
因为很明显,没理由在你需要预渲染的组件里加上 `unstable_cache`,不符合逻辑和语义
所以我觉得你可能碰上了我昨天说的第二个 `bug`,你的本地缓存里意外出现了你想缓存的数据,我建议你删掉 `.next` 重新 `build` 试试
---
真正需要搞明白的问题是,报错的 `{revalidate: 0}` 是怎么来的,db 操作是不是 `{revalidate: 0}`,是不是基于 `fetch`,有没有自带 `cache`
**到底发生了什么.**
---
很多问题,我暂时也还是一知半解,毕竟,源码真的很大还都是屎山,一个 `patch-fetch` 写 600 行..........
我只能先讲一下,我解决这个问题的思路
> Error connecting to database: Route /dashboard needs to bail out of prerendering at this point because it used revalidate: 0. React throws this special object to indicate where. It should not be caught by your own try/catch. Learn more:
https://nextjs.org/docs/messages/ppr-caught-error万恶之源是这个报错,应该很熟悉了吧,这个报错来自
```ts
//dynamic-rendering.ts
function postponeWithTracking(
prerenderState: PrerenderState,
expression: string,
pathname: string
): never {
const reason =
`Route ${pathname} needs to bail out of prerendering at this point because it used ${expression}. ` +
`React throws this special object to indicate where. It should not be caught by ` +
`your own try/catch. Learn more:
https://nextjs.org/docs/messages/ppr-caught-error`
...
React.unstable_postpone(reason)
}
```
关于 React 的 Postpone API 可以参考
[Add Postpone API by sebmarkbage · Pull Request #27238 · facebook/react · GitHub](
https://github.com/facebook/react/pull/27238)
预渲染的函数调用路线
> entry-base > patchFetch > trackFetchMetric >
动态渲染的函数调用路线
> entry-base > patchFetch > unstable_noStore > > markCurrentScopeAsDynamic > postponeWithTRracking
万恶之源报错时的错误路线
> entry-base > patchFetch > trackDynamicFetch > postponeWithTracking
---
nextjs 在 build 的时候,会用一个 `AsyncLocalStorage` 的实例保存某条路线的元信息
如果组件内部声明了 `unstable_noStore()`,那么,就由这个 `noStore` 函数负责后续操作,~~而且这个组件会先被处理~~
糟糕的地方在于 `unstable_noStore()`函数内部,直接获取 Storage ,然后用点号赋值,`store.isUnstableNoStore = true`,
更糟糕的地方在于这个 Storage 似乎是被单个路线所有组件共用的。
而且还有一个函数 `patchFetch`,每次渲染组件都会调用,根据你的 `store.isUnstableStore`,修改你的 `store.revalidate` 值( db 操作默认为 undefined )
```ts
// patch-fetch.ts/patchFetch
const isUsingNoStore = !!staticGenerationStore.isUnstableNoStore
...
if (typeof revalidate === 'undefined') {
...
if (isUsingNoStore) {
revalidate = 0
cacheReason = 'noStore call'
} else {
cacheReason = 'auto cache'
revalidate =
typeof staticGenerationStore.revalidate === 'boolean' ||
typeof staticGenerationStore.revalidate === 'undefined'
? false
: staticGenerationStore.revalidate
}
} else if (!cacheReason) {
cacheReason = `revalidate: ${revalidate}`
}
```
值得注意的是,使用 `unstable_noStore` 的组件不会走到这一步,只有没有声明 `noStore` 的才会
所以,一开始,单独 build 的时候
- 第一个组件是 `isUnstableStore` 为 `true`,有 `noStore` 函数,直接就走动态渲染
- 第二个组件是 `isUnstableStore` 为 `false`,但是`{cacheReason:auto cache,revalidate:false}`,数据是可以 cache 的,调用 `trackFetchMetric`,所以走预渲染
如果组件一起渲染的话
第二个组件被坑了,`isUnstableStore:true` ,但是`{ revalidate : 0,cacheReason : 'noStore call'}`,
由于 `revalidate === 0`
```ts
//patch-fetch.ts/patchFetch
if (revalidate === 0) {
trackDynamicFetch(staticGenerationStore, 'revalidate: 0');
}
```
会调用 `trackDynamicFetch`,而且 `prerenderState` 为 `true`,会导致我们调用 `postponeWithTracking`,并调用 `React.unstable_postpone(reason)`,最终报错
```ts
// dynamic-rendering.ts/trackDynamicFetch
export function trackDynamicFetch(
store: StaticGenerationStore,
expression: string,
) {
if (store.prerenderState) {
postponeWithTracking(store.prerenderState, expression, store.urlPathname);
}
}
```
注意,`store.sprerenderState` 表示我们在 ppr 模式下,处于构建中,参考
```ts
// dynamic-rendering.ts/markCurrentScopeAsDynamic
if (
// We are in a prerender (PPR enabled, during build)
store.prerenderState
) {
// We track that we had a dynamic scope that postponed.
// This will be used by the renderer to decide whether
// the prerender requires a resume
postponeWithTracking(store.prerenderState, expression, pathname);
}
```
---
为什么动态渲染最后也会调用 `postponeWithTracking`,却没有报错?
```ts
// dynamic-rendering.ts/postponeWithTracking
prerenderState.dynamicAccesses.push({
// When we aren't debugging, we don't need to create another error for the
// stack trace.
stack: prerenderState.isDebugSkeleton ? new Error().stack : undefined,
expression,
});
```
因为被 `unstable_noStore` 函数调用后的 `store,prerenderState` 长这样
```ts
// postponeWithTracking prerenderState
{
isDebugSkeleton: undefined,
dynamicAccesses: [
{ stack: undefined, expression: 'unstable_noStore()' },
{ stack: undefined, expression: 'unstable_noStore()' }
]
}
```
```ts
// unstable-no-store.ts
const callingExpression = 'unstable_noStore()';
markCurrentScopeAsDynamic(store, callingExpression);
```
而调用 `trackDynamicFetch` 的`store,prerenderState` 最终长这样
```ts
// postponeWithTracking prerenderState
{
isDebugSkeleton: undefined,
dynamicAccesses: [
{ stack: undefined, expression: 'unstable_noStore()' },
{ stack: undefined, expression: 'revalidate: 0' },
{ stack: undefined, expression: 'unstable_noStore()' },
{ stack: undefined, expression: 'revalidate: 0' }
]}
```
推测: `React.unstable_postpone(reason)`接收的 `reason` 里不能有`revalidate:0`
具体实现参考 `React/packages/react/src/ReactPostpone.js` 还有上面上面提过的 `github pull`
---
#### 解决方案
临时解决方案:
在 `dynamic-rendering.ts/markCurrentScopeAsDynamic`
调用 `postponeWithTracking(store.prerenderState, expression, pathname);` 之前加上 `store.isUnstableNoStore = false`
最终解决方案
我对 AsyncLocalStorage 还有 nextjs 整体的理解还有待加强,所以上面只能作为临时方案
#### 最后
看不看源码这个东西,主要是兴趣吧,React 的官方文档就提到过很多次,不希望开发者关注底层是如何实现,只要专注 UI 部分就行了,他们负责 DX
不过作为开发者本身,对我这种人,对 BUG 的热情还是挺高的,乐在其中,我觉得有所收获就挺好的,至于成本的问题,不可避免,开心最重要 hhh