关于 Next.js 最新的特性, Partial Pre-Rendering 和 SSR 之间的问题

268 天前
 lazyczx

我以前写过 React ,现在想学 Next.js 。跟着官方教程在学它的那个教程

学到 chapter 11: Partial Pre-rendering 越学越迷茫,我不懂它说的某些功能的表现是怎么样的,虽然我看得懂它教程里说的内容,这个框架是挺美好的,但是实际用起来真的很奇怪。

  1. 教程里说使用 unstable_noStore() ,给一个服务端组件的 API 层,可以禁用 cache ,这样可以让一些用户间不共享的信息,在用户请求的时候可以实时更新。问题是我就算吧这个 noStore 注释了,每次我刷新页面的时候,对应的 API 方法又重新调了。这就和我理解的冲突了,不是说服务端渲染好的页面,只要不是 dynamic 的(用了 noStore 就是 dynamic ),就不会那么快实时更新吗?而且我怕只看 API 方法重新调不合适,说不定它框架里做了从调用上看不出来的缓存。。然后我跑去 vercel 上 update 了数据库,好家伙,一刷新页面,修改了的数据立马反映出来了。。那是不是说明这个服务端根本不是静态渲染的,而是默认动态的,那我加不加这个 noStore 不是一摸一样吗?我去外网和官方文档上找,它说这个就是加了个 http header 。。具体有用没用好像是看网页客户端的,但是我这个不是服务器请求服务器吗?哪来的网页客户端,不是 nodejs ,或者这里是因为 nodejs 模拟了浏览器内核的缘故吗?

  2. 这个 Next.js 是不是有什么问题?我在测试这个缓存过没过期的时候,随便搜就搜到了这个帖子,它贴的官方文档说 dynamic 是 5s ,static 是 30s ,然后我又觉得这个缓存不起效可能和我一直在点刷新有关,我是不是应该在应用内点跳转链接,然后机缘巧合之下,我在等 30s 之后,开始点页面内的 navigation ,好家伙每点一下,API 方法就被调用一次。。下面是我弄的计算方法执行时间的 log:

    cost time: 773ms
     Fetching revenue data...
     cost time: 699ms
     Fetching revenue data...
     cost time: 213ms
     Fetching revenue data...
     cost time: 231ms
     Fetching revenue data...
     Fetching revenue data...
     cost time: 216ms
     Fetching revenue data...
     cost time: 215ms
     Fetching revenue data...
     Fetching revenue data...
     cost time: 246ms
     Fetching revenue data...
     cost time: 647ms
    

    而且我在 vercel 的[云服务上也复现]( https://nextjs-dashboard-kappa-amber-64.vercel.app/dashboard )了,有兴趣的可以等个三十秒,然后疯狂点那个 home 的按钮实时,每点一下就来一个网络请求,但是不知道这个网络请求是干嘛的,但是我本地的 dev 确实打这些 log 了。而且刚刚刷新,进去网站的时候,你就算疯狂点,也只会加载一次( api 方法也只调了一次),等个 30s 就这样了,是不是有什么问题。

写的有点长了感觉,但是这个 next.js 我是真越学越晕啊,各位如果有兴趣可以试一试,另球球前端大佬指点(我又来白嫖网友力量了嘿嘿 OWO ),在下先行拜谢!

2163 次点击
所在节点    程序员
28 条回复
epiloguess
265 天前
好消息是,经过扒了一夜的源码,我成功搞定了这个 bug 可以怎么被修复.
坏消息是我好困

一个简单的修复方法,现在就可以尝试.
首先确定你装的是 canary

打开你的 node_module/next/dist/server/app-render/dynamic-rendering.js
找到 markCurrentScopeAsDynamic 函数

在 调用 postponeWithTracking(store.prerenderState, expression, pathname); 这一行的上面
加上 store.isUnstableNoStore = false;

记得保存

---如何验证

第一个组件设置 noStore
第二个组件什么都不设置
两个组件都有 db 操作

不出意外的话,第二个组件会被部分预渲染
lazyczx
265 天前
@epiloguess

我实验了很久,总体感觉这个 cache 和 nostore 一起在 ppr 里用就是会有很多预期之外的问题,我有次尝试过把三个组件的 cache 都用上,然后各自加 nostore 发现只有特定的两个组件加上 nostore ,或者全部加上才不会报错,当然这个可能在这个网页的功能角度来看里是没意义的。后来我尝试用功能实现的角度去真正使用这个 ppr ,我把 cache 全部去掉,然后在一个组件上使用 nostore ,报错了,而我的预期是没加 nostore 的两个静态渲染,加 nostore 的就动态,但是。。反正是报错了。

然后我最后发现,要成功,只能把两个想做静态的组件在原来的基础上用上 cache ,这样页面才会如预期,只有一个是动态渲染的,其他俩静态(最终只有一个地方用了 noStore ),这应该就是你说的 cache 一把梭了。。。

然后我试了你的那个改源码的办法,出意外了。。。没办法通过改源码,把我上面的 俩 cache 给省掉啊。不加 cache 还是会报错。

不过你是真滴强,熬夜看源码。。我感觉面对这种 beta 的功能,连钻研源码的兴趣也没有,甚至我昨天之前都不知道前端项目咋看源码,我 ctrl 点进去,全是 ts 定义文件,昨天还问了下 chatgpt ,它推荐我去下代码仓库下来看。。我学习这些,都想的是有一个相对正确的 overview 就可以了,然后会使用,因为我觉得面向接口编程大多数情况下是挺好的,毕竟别人写的代码是无限多的,如此细粒度的学习仿佛让我感觉我在虚度时间一样,总之我觉得钻研细枝末节的实现的成本好大,但是这也导致我碰到问题了,如果没有他人/搜索引擎帮助,很难自己钻进去,然后获得一些成果。但是总体上,我觉得会看源码是个很厉害的事情,能通过看源码,针对性地找到自己想要的答案,这件事很厉害。但是我对于怎么做到这样,怎么样让这件事变高效很迷茫,也对做这件事情的“边界”感到迷茫,是一旦有好奇心就去看,看的话要看多少,要看哪些之类的。。在这样的状态下,更不用说让我去看一些功能还未成熟的源码。。。然后看到你还会熬夜看源码,属于是感到一个天一个地的差距了~_~

如果可以,希望听到你的感想!
epiloguess
265 天前
注意,以下内容包含大量代码,为了良好的阅读体验.建议复制到 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
lazyczx
264 天前
@epiloguess

我看了老半天那个 pr ,不知道的东西太多了,还好有 chatGPT 的语境分析和解释( openAI 真的牛),我大概知道了,就是这个 postpone 是 react 用来解决 infinite promise 的,然后是通过抛出错误解决的,用来处理静态组件到动态组件的退化(我感觉这个描述和我们发现的问题高度相关)。。

我昨天试了一下你说的(一个组件上 noStore ,其他的组件 noStore + cache )把 .next 删了重新 build ,确实可以,后来我也发现我之前 build 成功的结果,是因为本地已经有缓存了,所以也许那个 cache 直接就没执行,因此也轮不到莫名其妙哪里来的 validate 0 和静态组件发生冲突了。

你后面的源码分析里面是不是说是因为那个动态的组件的缘故,然后导致数据库的请求全部默认 validate 0 了,也就是不缓存的意思,这样和静态组件冲突了?

确实就像你说的,静态的部分如果用了 db 就必须 noStore + cache 才可以。

我看了你的对源码的分析之后,今天去尝试了一下怎么 debug 本地的 npm 仓库(就是在仓库里打断点),我以前在 Java 的项目里,这个是很简单的。

我做的时候,用的是 npm link ,下了 nextjs 仓库然后 link 替换掉了 dashboard 项目里的 next ,但是运行命令的时候提示 next 找不到,于是我全局安装了 next ,但是运行各种命令都报错,报什么 m...interface 什么找不到,我去 next 里 npm i 了也没用(虽然好像默认是不需要自己跑过去 npm i 的吧?)。

然后搞了半天最后还是失败了,只能做到 debug 自己写的代码,不能 debug 库里的,用 symlink 连接 next 直接跑不起来,我现在都怀疑这个方法是不是根本不可行。

你知道怎么 debug 公共库里的代码吗?就是在公共库的代码上加断点这种,比如 debug next 的代码,或者 react 的代码。

然后,我重新 i 了 canary 50 ,然后 build 的时候提示出来的是 52 版本,然后我昨天试过的代码根本没变,就 build 失败了,还是那个 validate 0 ,should bail out 的错误,我这里先考虑是版本有问题,先把版本弄正确了再说。

然后我把 node_modules 删了,再把 global 的 canary 删了,再试了一下 build (确认原先安装的 next 已经删除),然后看到命令找不到之后,又 npm i 了结果出来的还是 52 ,然后删了好几次 node_modules ,npm i 下下来的都是 52 ,而我的 package.json 里的版本是 50 ,但是去 node_modules 里看的还是 52 。

然后我把版本改成 49 ,这次下的是 49 了,然后又改成 50 ,就成功把包改成 50 了,这什么情况,好诡异。。。。。。
lazyczx
264 天前
@epiloguess

老哥聊了那么多,可否给个微信好友位,为了不过度暴露自己的个人信息,我弄了只能下载一次的一个文件,哈哈反正个帖子应该也没什么人看到这么后面!

https://airportal.cn/519534/Fwr351S2E5

密码是 519534
epiloguess
264 天前
@lazyczx 这个文件下不了呀

关于 debug,因为我一直用的都是 pnpm,node_module 里面都是 pnpm 处理的硬链接
如果是 npm,每个项目都不共用吧,除非装 global

不管怎么说,理论上,直接修改本地文件应该就可行吧,比如说你 import 了 noStore 这个函数,这个函数在本地就是以 js 格式存在的,是源码处理过后的,在源码中是以 ts 的格式存在的

> 我做的时候,用的是 npm link ,下了 nextjs 仓库然后 link 替换掉了 dashboard 项目里的 next ,但是运行命令的时候提示 next 找不到,于是我全局安装了 next ,但是运行各种命令都报错,报什么 m...interface 什么找不到,我去 next 里 npm i 了也没用(虽然好像默认是不需要自己跑过去 npm i 的吧?)。

所以我不太懂你这一步 ,npm link 是干嘛的...

如果想快速 debug,最方便的应该就是直接修改你当前项目里面 node_module 目录下的 next 包里面的函数值,这种方式一般也叫 heck

如果想从源码 debug,需要 git clone next 的仓库,一边修改一边测试,修改完之后 build,再替换你本地用的 next 包.

这样就比较麻烦了,特别是如果你不熟悉项目的情况下..

---
最后补充一下我昨天的内容
下面是我打的一部分断点,我查了一下 patch-fetch 的 commit,勉强搞明白了到底是怎么回事.
这是没有 noStore 的时候的断点
我们都知道 nextjs 扩展了 fetch,具体实现,
next build 的时候,首先就会对每个路由,patch 原生的 fetch,它并不在乎路由里面有没有 fetch 的调用,这样就比较麻烦了
实现的效果就是,以后在渲染这些路由的时候,在调用 fetch 的时候,调用的就是 patch 过的 fetch,而不是原生的 fetch


call patchFetch from entry-base
Function patchFetch at patch-fetch.js being called [Function: g]
urlPathname from patchFetch Begining / // 所以断点日志的前三部分,就是不同的 urlPathname
revalidate from patchFetch Begining undefined
isUnstableNoStore from patchFetchBegin false

call patchFetch from entry-base
Function patchFetch at patch-fetch.js being called [Function: g]
urlPathname from patchFetch Begining /_not-found
revalidate from patchFetch Begining undefined
isUnstableNoStore from patchFetchBegin false


call patchFetch from entry-base
Function patchFetch at patch-fetch.js being called [Function: g]
urlPathname from patchFetch Begining /dashboard
revalidate from patchFetch Begining undefined
isUnstableNoStore from patchFetchBegin false



// 这是 fetch 函数在 next 中的定义
// fetch(`https://...`, { next: { revalidate: false | 0 | number } })
// 完成上面的工作之后,next/webpack,开始渲染页面/页面中的组件

rendering...RevenueChart component
Fetching revenue data... //已经在 try...catch 里面了,下一步就是获取 data

using patchFetch fetch //调用 patch 过的 patch
// 这时候我们能看到 input 和 init,分别是 fetch 的 url 和 option,option 也就是第二个参数
// 这里也就是说,@vercel/postgre 中的 sql`` 是通过 fetch 调用的
// 值得注意的是,如果没有配置 option 里面的 option.next.revalidate ,这个值默认是 undefined,sql``是没有配置的
input https://ep-blue-star-a4zoprb8-pooler.us-east-1.aws.neon.tech/sql
init {
method: 'POST',
body: '{"query":"SELECT * FROM revenue","params":[]}',
headers: {
'Neon-Connection-String': 'xxxx',
'Neon-Raw-Text-Output': 'true',
'Neon-Array-Mode': 'true'
}
}

isRequestInput false
curRevalidate undefined
fetchCacheMode undefined
isUsingNoStore false
revalidate undefined
isUsingNoStore false
cacheReason
revalidate false
cacheReason auto cache



rendering...RevenueChart component
Fetching revenue data...
input https://ep-blue-star-a4zoprb8-pooler.us-east-1.aws.neon.tech/sql
init {
method: 'POST',
body: '{"query":"SELECT * FROM revenue","params":[]}',
headers: {
'Neon-Connection-String': 'xxxx',
'Neon-Raw-Text-Output': 'true',
'Neon-Array-Mode': 'true'
}
}
using patchFetch fetch
isRequestInput false
curRevalidate undefined
fetchCacheMode undefined
isUsingNoStore false
revalidate undefined
isUsingNoStore false
cacheReason
revalidate false
cacheReason auto cache


Fetched revenue data...
Fetched revenue data...


---

所以错误是怎么产生的?
你会发现,RevenueChart 被渲染了两次,假设我们现在有另一个组件,其中调用了 noStore()
一开始,为路线 patchFetch
然后渲染组件 RevenueChart,调用 patch 过的 fetch,没有问题
然后渲染组件 with noStore(),noStore()函数会调用 markCurrentScopeAsDynamic,也就是说,会标记当前 scope 作为动态渲染,因此,有 noStore 的组件,在 build 过程中,不会调用后面的 try..catch 块的里面的 await sql``


---
这里补充一下 scope 的知识
关于 https://nextjs.org/docs/messages/ppr-caught-error
里面提到过
> Alternatively, insert unstable_noStore() before the try/catch.

try..catch 就是一个独立的 scope,同理,unstable_cache 也一样
unstable_noStore 不期望在这些 scope 中被调用,否则会错误
因为 markCurrentScopeAsDynamic 期望 mark 到一个 suspense 边界,如果 unstable_noStore 在页面顶层/或者一个没有被 suspense 包裹的组件内,(本质上一样)被调用,外面没有 suspense,那会发生什么?
答案很简单,整个页面都会被 suspense,这一点参考
> https://nextjs.org/docs/app/api-reference/file-conventions/loading

整个世界就是一个大大的佩拉(误)
整个页面就是一个大大的 suspense

--
先回顾一下上上部分最后一行.

但是,noStore 函数调用的时候,会 store.isUnstableNoStore = true;这个 store 是路线共用的
所以在第二次渲染 RevenueChart 的时候,isUnstableNoStore 会变为 true,其中我记得有个简单的逻辑
if(revalidate === undefined){
if(isUsingNoStore/重命名了){
{revalidate = 0}
cache reason = 'noStore call'
}else{
cache reason = 'auto cache'
}

正是这一步,导致我们以同样的操作第二次渲染 RevenueChart 的时候,revalite 为 undefined 变成了 0
导致后面没有调用 trackFetchMetric 而是 trackDynamicFetch 并最终把 revalite0 送到了 postpone 手里,产生了报错

---

所以我上次给的临时解放方案,在 markCurrentScopeAsDynamic 之后 isUnstableNoStore = false
才会奏效

但是,其实这并没有解决根本问题

---
那么,为什么会这样?为什么会有这个逻辑
if(isUsingNoStore/重命名了){
{revalidate = 0}
cache reason = 'noStore call'
}else{
cache reason = 'auto cache'
}

这个逻辑是在这时候被添加的 https://github.com/vercel/next.js/pull/60630

他的解释

```
When you're using noStore() with fetch it's currently saying "auto cache" in cache missed reason, adding "noStore call" here to show it's caused by using with unstable_noStore
当您使用 noStore() with fetch 时,它当前在缓存错过原因中显示“自动缓存”,在此处添加“noStore 调用”以表明它是由使用 with unstable_noStore 引起的

GET /no-store 200 in 4069ms
│ GET https://next-data-api-endpoint.vercel.app/api/random?another-no-cache 200 in 257ms (cache: SKIP)
│ │ Cache missed reason: (noStore call)

```

如果不添加这个逻辑,不在 noStore 里设置 isUnstableNoStore 为 true
会发生什么?
直接走到了 else 的最后一步,revalidate 为 undefined 的 ,cache reason 设置为 auto cache,这适用于
fetch 的时候不写 revalidate 的函数,组件会被默认预渲染
而 noStore 这个不会被预渲染的,也用的是相同的 cache Reason,显然不符合不对的,所以这个 commit 就这么被提交了

---

总结和补充一些内容

1.每个路线上的 fetch 都会被 patch,组件,会被渲染两次,关于这个渲染两次,官方 doc 里面也有提过,你就理解成在服务器上模拟客户端的操作?
2.revalidate 为 undefined 的,没有 noStore 的,会正常渲染
3.有 noStore 的组件,会被标记为动态渲染,从而不调用组件里的 fetch?那什么时候调用?当然是访问页面的时候,build,start 以后访问页面就能看见

那我们思考一下访问会访问什么

会调用 patch 过的 fetch,传入的 revalidate 是 0,走到刚才那个逻辑,如果没有提前设置 isUnstableNoStore 为 true,就会 else{
cache reason = 'auto cache'
}
发现了吗,不正确的 cache reason

---

加上的这一步会影响什么?


if(isUsingNoStore){
revalidate = 0
cache reason = 'noStore call'
}

使用了 noStore 的组件是舒服了
但是没使用 noStore 组件却被 revalidate = 0 给坑了,错误就是这么来的

---
所以我昨天的解决方法是解决了我自己的问题,但没解决那个 commit 想解决的问题

可以说,干脆就把 noStore 里面赋值那一行删掉就可以解决问题

不过我的问题比较关键好吧,他的问题不过是调试才会发现的问题,根本不影响使用...


---





@lazyczx
epiloguess
264 天前
更正:
倒数第二个水平线
---
那我们思考一下访问会访问什么

会调用 patch 过的 fetch,传入的 revalidate 是 0,走到刚才那个逻辑,如果没有提前设置 isUnstableNoStore 为 true,就会 else{
cache reason = 'auto cache'
}
--

传入的是 revalidate undefined ,不是 0 写错了

---

关于 debug npm link 应该也可以 不过我没怎么用过

毕竟我不是天天 debug..............

你也可以不用这么麻烦,毕竟你都用 canary,直接修改本地某个版本的包就行,反正注意一下别的项目不再用了就可以,比如只修改.50

---
翻了一下最开始的评论,还行,对的比错的多,笑死.

计划整理一下所有回答的内容,可以给后来者一点明确的参考,毕竟我们这帖子太乱了.

不过可能还要几天,毕竟总结的越早,错误的地方也就越多.
lazyczx
264 天前
@epiloguess

把取件码搞错成密码了。。

再来一次

https://airportal.cn/81745/WQIRw8VCNn 复制链接到浏览器打开

123456

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1028274

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX