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

2024-03-29 23:42:04 +08:00
 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 ),在下先行拜谢!

2776 次点击
所在节点    程序员
28 条回复
epiloguess
2024-04-01 08:23:52 +08:00
好消息是,经过扒了一夜的源码,我成功搞定了这个 bug 可以怎么被修复.
坏消息是我好困

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

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

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

记得保存

---如何验证

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

不出意外的话,第二个组件会被部分预渲染
lazyczx
2024-04-01 16:39:07 +08:00
@epiloguess

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

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

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

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

如果可以,希望听到你的感想!
epiloguess
2024-04-01 19:43:12 +08:00
注意,以下内容包含大量代码,为了良好的阅读体验.建议复制到 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
2024-04-02 19:28:07 +08:00
@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
2024-04-02 19:31:09 +08:00
@epiloguess

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

https://airportal.cn/519534/Fwr351S2E5

密码是 519534
epiloguess
2024-04-02 21:16:26 +08:00
@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
2024-04-02 23:15:58 +08:00
更正:
倒数第二个水平线
---
那我们思考一下访问会访问什么

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

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

---

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

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

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

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

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

不过可能还要几天,毕竟总结的越早,错误的地方也就越多.
lazyczx
2024-04-02 23:48:24 +08:00
@epiloguess

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

再来一次

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

123456

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

https://ex.noerr.eu.org/t/1028274

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

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

© 2021 V2EX