V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lazyczx
V2EX  ›  程序员

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

  •  
  •   lazyczx · 236 天前 · 2073 次点击
    这是一个创建于 236 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我以前写过 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 ),在下先行拜谢!

    28 条回复    2024-04-02 23:48:24 +08:00
    lazyczx
        1
    lazyczx  
    OP
       236 天前
    另外,我把代码仓库 public 了,想要看问题 2 的兄台走[这里]( https://github.com/dribble312/nextjs-dashboard)
    lazyczx
        2
    lazyczx  
    OP
       236 天前
    写的太长了,而且看的教程也是那么长的英文,估计很难得会有大佬有时间帮忙指点这种根基性的问题了,如果没有本贴权当一些记录和发泄了,相信有一天关于这些问题,会豁然开朗!
    SmiteChow
        3
    SmiteChow  
       236 天前
    请求是请求,渲染是渲染,两码事
    epiloguess
        4
    epiloguess  
       236 天前   ❤️ 2
    这个问题比较复杂,我一点一点回答你。
    先讨论正式版,再讨论部分预预渲染
    1.next dev 和 build 的渲染逻辑是不一样的,如果你把 data.ts 中 dashboard 相关的那三个组件的 noStore 注释掉,然后 build 一下,你会发现 /dashboard 生成的是静态页面,因为页面没有动态函数而且数据都 cached 了,但 dev 的时候情况可能不一样,数据可能没有全部 cache 完,或者说每次刷新的时候都会重新请求相关数据,这样其实更符合逻辑。注意,你加不加 suspense 都不会影响静态渲染
    2.在正式版中,如果你给那三个组件中任意一个加上了 noStore ,整个 dashboard 页面,包括那三个组件都会退出静态渲染,这一点也并不难理解,回顾官方定义,再考虑一下目前渲染路线其实就两种,组件又在页面之中,next 在 build 的时候遇到 noStore 就知道下一步该选什么渲染路线。
    > unstable_noStore 可用于以声明方式选择退出静态渲染并指示不应缓存特定组件。

    3.你希望达成什么?我想应该是不能够每次刷新都去查询数据,最好可以手动 revalidateTag 。
    有两个函数,可能可以帮到你,react cache 和 unstable_cache
    https://nextjs.org/docs/app/building-your-application/caching#react-cache-function
    https://nextjs.org/docs/app/api-reference/functions/unstable_cache

    你可以现在就试试,记得随便一个组件上加上 noStore,然后在 RevenueChart 组件上创建一个函数,
    import { unstable_cache} from 'next/cache';

    const getCachedRevenue = unstable_cache(
    async () => fetchRevenue(),
    ['Revenue']
    );
    组件内部开头删掉
    const revenue = await fetchRevenue()
    然后这样写,
    const revenue = await getCachedRevenue()

    先 build,start,然后进 dashboard ,在刷新的时候,你会发现,Revenue 组件会保持不变,另外两个出现了骨架屏,可以换个浏览器或者进隐私窗口,一样是秒开。
    4.关于你的第二点,没看太明白,这个 30s,5 分钟之类的,都是客户端缓存,你在开发者工具的网络选项卡把选一下禁用缓存,第三点的效果不会变,多看看文档这一节,https://nextjs.org/docs/app/building-your-application/caching#overview
    5.最后再来说一下部分预渲染,

    根据,https://nextjs.org/docs/app/api-reference/functions/unstable_noStore
    在 unstable_cache 内使用 unstable_noStore 不会选择退出静态生成。相反,它将根据缓存配置来确定是否缓存结果。

    这句话不太好懂,不过我实测的结果就是,如果你是正式版,那么无论你任何地方使用了 noStore ,在 build 的时候,路线都会变成动态,而如果你是 canary,想要使用部分预渲染,最好还是不要在 unstable_cache 缓存的函数内使用 noStore,参考 https://nextjs.org/docs/messages/ppr-caught-error
    > 确保您没有将选择动态渲染的 Next.js API 包装在 try/catch 块中。
    尽管官方建议,可以在 try...catch 前插入 noStore, 但后面实现缓存函数不太方便,所以我个人建议,可以在组件的第一行,也就是 const revenue = await getCachedRevenue()的上面一行,使用 noStore ,第二行用 cache,逻辑也比较清晰。


    同时,根据,https://nextjs.org/docs/app/api-reference/next-config-js/partial-prerendering
    你需要 npm install next@canary ,然后改 config (这个我看有注释),记得每个组件都要加上 noStore ,不然参考第一条,就会被当成静态内容一次生成哦(关于哪些内容被生成静态的了,你可以去开发者工具的网络选项卡,预览第一个送过来的 Document ),然后可以 build 了哦。

    祝好!

    其他参考(不分先后,我也没看(完),不过很值得看):
    - https://github.com/vercel-labs/next-partial-prerendering
    - https://github.com/orgs/vercel/discussions/4696
    - https://codedrivendevelopment.com/posts/rarely-known-nextjs-features
    - https://github.com/vercel/next.js/pull/56930
    - https://stackoverflow.com/questions/76829076/in-next-js-13-app-router-how-can-i-use-data-caching-when-not-using-fetch-but
    - https://github.com/vercel/next.js/discussions/54075
    epiloguess
        5
    epiloguess  
       236 天前 via Android
    这里补充一下,为什么 unstable_noStore 在正式版和 canary 里表现不一致,可能是因为这个 function 就是为了后续的功能开发的,当前版本可以通过配置路由段,或者 fetch 一个空数据加上 no-store 来退出静态渲染
    shizhibuyu2023
        6
    shizhibuyu2023  
       236 天前
    这个教程我也做了,我没复现你这个问题。noStore 是判定是否为 dynamic 的边界,你试试把所有 noStore 都注释了,可能是另一个 noStore 的边界包含了你注释的位置
    lazyczx
        7
    lazyczx  
    OP
       236 天前
    @epiloguess 太感谢了,你是我的神!!!

    抱歉看到之后过了很久才回复,因为我一边看你的回复,一边自己实验了相当一段时间!

    1. 那个 cache 真的很强大,被 cache 包裹的 api 方法调用就像是被锁到了另外一个空间,同页面的别的组件都不会影响到它,引起它的的 revalidation 。我看到一个 noStore() 标注的方法执行的时候,别的 API 方法也跟着被调用,但是作为 cache fallback 的方法不被调用。我觉得这个就是 ppr 了对吗? canary 的支持只不过让这个实现更加简单了一点而已:不需要 cache 方法包裹,而是没有使用 noStore 就默认为 cache 。

    2. 如果调用了 noStore 的方法没有 cache 作为 fallback ,而是直接调用了的话,会导致整个 page 直接变成 dynamic 的,此时就算别的 api 调用没有调用 noStore ,也会每次刷新都自动调方法。如果所有方法都没有“有效的” noStore ,则在 start 启动的时候直接变成 static 页面,而在 dev 的时候,方法会调用,但是请求出不去,整个方法的执行都在毫秒级完成。

    3. 我说的那个一直点,一直调请求的,还是会时不时复现,有段时间我一直没有成功复现,然后我同时打开了 local 的页面和 vercel 上托管的页面,立马就出现这个问题了,我甚至都怀疑是不是它们两个之间互相有什么影响,但是我不知道是什么原因,照理说域名都不一样,怎么会有互相影响呢。。但是我发现,如果页面是 static 的,肯定就不会有问题。但是我猜大概从表现的危害上来说也可以接受吧,毕竟页面都是 dynamic 的,让它每点一下就调一次又怎么样了呢,但是原因还是不清楚。。关于这个,我想到个问题,如果页面是 dynamic 的话,是不是最好把后端接口调用放在客户端(如果没有保密需求的话),换言之前端服务器做这种 proxying (不包括缓存)的事情,对前端服务器压力大吗?因为我想如果是 user 之间不共享的数据,那每个 user 都要从你前端服务器 proxy 出去,应该有很大的吞吐量,框架要处理这些,会不会在上面浪费很多性能。

    4. 关于第 5 点,我不知道你是怎么测试的,但是我测试的情况下,如果 API 方法里使用了 noStore ,然后把这个方法作为 cache 的 fallback 的话,确实不会退出静态生成。build 的时候显示的 tree 也显示页面仍然是 static ,如果不用 cache 的话,tree 马上变为动态 λ 的了。我感觉正式版里只要用 cache 了,noStore 就跟没有调用一样,好像 cache 这个缓存,会直接接管这整个方法调用的结果,然后选择是否缓存。

    再次感谢大佬,very informative answer ,让我学到很多。

    其实好久没写前端了,你说起 build start 的时候,我猛然想起,以前做前端项目的时候都是每次改代码,每次 build start 的哈哈。
    lazyczx
        8
    lazyczx  
    OP
       236 天前
    @shizhibuyu2023
    noStore 都注释了的话,就变成 static 页面了,static 页面确实永远都不会出现这个问题,这一点我在本地测试了。

    但是我感觉我碰到的这个表现,这个和时间有关就很扯哈哈,就有的时候刚点进去,navigation 切了几下,就不会调接口了。然后等了一段时间,就算在 home 狂点那个 home 也会一直调接口,但是刚刚进去页面的时候不会这样。你点过我部署的那个页面了吗?你不会遇到这个问题吗?就进去之后先在 home 和 invoices 之前切几下,可以看到切几下之后不管怎么切,network 都没有新的请求了,然后等个一分多钟,再去点 home ,每点一下,就出来一个新请求,这是在动态页面的情况下。主要是和时间有关,太奇怪了。
    lazyczx
        9
    lazyczx  
    OP
       236 天前
    @shizhibuyu2023

    不过就算是 dynamic 的,也有不复现的概率,我有几次就死活没法复现。有段时间就是会出现这个问题,我不确定是什么原因,总之很奇怪。

    喏,又复现了:

    https://imgse.com/i/pFTauGt

    而且这是在 dynamic 的情况下,意味着每次点这个 home 就调一次数据库:
    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
    但是页面刚打开的时候却没有这种问题,很奇怪。
    lazyczx
        10
    lazyczx  
    OP
       236 天前
    我整了个 discussion 到 next 的 github community 碰碰运气

    https://github.com/vercel/next.js/discussions/63889
    epiloguess
        11
    epiloguess  
       236 天前
    前提:canary 但是 config 里面注释掉 ppr = 正式版

    1.
    > 我看到一个 noStore() 标注的方法执行的时候,别的 API 方法也跟着被调用,但是作为 cache fallback 的方法不被调用。我觉得这个就是 ppr 了对吗? canary 的支持只不过让这个实现更加简单了一点而已:不需要 cache 方法包裹,而是没有使用 noStore 就默认为 cache 。

    ppr 指的是部分预渲染,相当于默认一切都是静态的,只要 suspense 里面没有 no store,但是这里有坑,最后说。

    在正式版/canary without ppr 中,
    这个 noStore 是不是 cache 的 callback,都没有关系,只要你这个路线中,任意一个地方出现了 noStore,next 在 build 的时候,就等价于路由段配置中的 force-dynamic 或者 fetch 中的 no-store,相当于退出静态渲染,改用动态。

    当你刷新的时候,为什么另外两个组件都去获取数据了,有 cache 的却没反应呢,因为 unstable_cache 它 cache 的函数的返回值。

    所以这跟 ppr 有什么关系?重要的是要把 ppr 和 cache 分开。

    你加 cache,只是为了优化,降低它查询数据库的频率。

    2.
    有一点非常值得注意的是,在 nextjs 中的 fetch,和 unstale_cache 表现是不一样的,

    fetch 缓存的是 fetch 请求的返回值,也就是说,为什么在没有 unstable_cache 的时候,你会看到你写的测试时间的方法被调用,因为整个请求数据+计算时间的函数都会被执行,只不过 await sql`` 的时候,直接从缓存中给你值了(这里也有坑)

    而 unstable_cache 缓存的是给定函数的返回值,(请求数据+计算时间)这个函数的返回值被缓存了,里面的计算时间自然不会被执行了。

    3.
    > 但是我发现,如果页面是 static 的,肯定就不会有问题。但是我猜大概从表现的危害上来说也可以接受吧,毕竟页面都是 dynamic 的,让它每点一下就调一次又怎么样了呢,但是原因还是不清楚。

    static 的时候,直接就是 html 了,肯定不会调用你写的计时方法。
    你需要确保的是,如果函数都加上 unstable_cache 了,你这个问题应该就不会出现了吧。


    4.
    > 关于第 5 点,我不知道你是怎么测试的,但是我测试的情况下,如果 API 方法里使用了 noStore ,然后把这个方法作为 cache 的 fallback 的话,确实不会退出静态生成。build 的时候显示的 tree 也显示页面仍然是 static ,

    你这个是在什么条件下测试的?

    > 如果不用 cache 的话,tree 马上变为动态 λ 的了。
    在正式版中,任意组件任意位置存在 noStore ,是的,路线就会退出静态渲染,跟 cache 没什么关系。

    > 我感觉正式版里只要用 cache 了,noStore 就跟没有调用一样,好像 cache 这个缓存,会直接接管这整个方法调用的结果,然后选择是否缓存。

    正如我前面所说,这事儿跟 cache 没什么关系,cache 只是为了降低 动态组件 查询数据库的频率,在静态路线里写不写 cache 没什么意义,是同一个结果

    5.一些坑以及一些猜测
    深入理解 nextjs 的缓存,
    我 fork 了你的项目,RevenueChart 没有 cache,另外两个有,build,start
    当你刷新页面的时候,你会发现 RevenueChart 在获取数据,你的终端上出现 fetching data,另外两个数据立刻就有了
    这中间发生了什么?

    > unstable_noStore can be used to declaratively opt out of static rendering and indicate a particular component should not be cached.
    > unstable_noStore 优于 export const dynamic = 'force-dynamic' ,因为它更细粒度并且可以在每个组件的基础上使用

    unstable_noStore 是用来配合部分预渲染,实现细粒度的控制组件的渲染方式,当你在另外两个组件中声明 noStore 的时候,就成了一个动态组件
    > unstable_noStore is equivalent to cache: 'no-store' on a fetch
    > no-store - Next.js 在每次请求时从远程服务器获取资源,而不查看缓存,并且不会使用下载的资源更新缓存。

    但是这并意味着我们就没有 data cache 了

    > Next.js 有一个内置的数据缓存(data cache),可以在传入的服务器请求和部署中保留数据获取的结果。这是可能的,因为 Next.js 扩展了本机 fetch API 以允许服务器上的每个请求设置自己的持久缓存语义。


    ---以下为猜测---
    假如我们的组件声明了 noStore,组件内部 fetch 了一个资源,没有理由这个 fetch 会退出 nextjs 的 data cache.

    我们声明了 noStore,只表达了这是个动态组件,它不是网页静态的一部分,当你访问/刷新网页的时候,你应该永远从服务器获取组件的内容(这里先不提客户端缓存)


    因为 @vervel/postgre 的 sql 也是基于 fetch 的
    这一点很奇怪,完全想不通,但是参考
    https://github.com/orgs/vercel/discussions/4696
    https://nextjs.org/docs/messages/ppr-caught-error
    > Database Error: NeonDbError: 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
    > As a convenience, it is not necessary to set the cache option if revalidate is set to a number since 0 implies cache: 'no-store' and a positive value implies cache: 'force-cache'.
    > 为方便起见,如果 revalidate 设置为数字,则无需设置 cache 选项,因为 0 隐含 cache: 'no-store' 且正值意味着 cache: 'force-cache'

    sql 的内部实现可能用的是 fetch(``, { next: { revalidate: 0 } })

    一个悲伤的事情是,正式版的静态渲染和 revalidate 0 配合工作良好,ppr 却不行,这也就意味着,对于带有数据库查询的组件,你并不能 ppr 它们变成完全静态的

    当然,这个问题是可以被解决的,配合 router handle,你其实可以用 fetch 获取某些数据库的信息(安全吗?)

    ---接下来让我们回到刷新后的渲染过程---

    为了解决 sql 的 revalidate0,我们引入了 unstable_cache 。

    当我们刷新页面的时候,服务器上已经有了那两个组件 cache 的 RSCP,直接就发送过来了,内容是立即出现的
    而 RevenueChart 没有 cache,并且由于 sql 的 revalidate0 ,需要重新查询数据库,终端上出现 fetching data

    为什么第一次慢后面快?所以给你一种查询没有发出去的错觉?
    猜测 1:可能确实没发出去,毕竟 revalidate0 是我的猜测
    猜测 2:发出去了,第一次慢是因为网络问题,vercel 在国外,tcp 慢启动,第二次第三次就快了

    这一点其实也很好验证,你一边刷新(可以用插件自动刷新),一边在 vercel 数据库中新建一个数据就可以判断了,交给你了,等你反馈

    ---客户端缓存/路由器缓存---

    > 当用户在路线之间导航时,Next.js 会缓存访问过的路线段并预取用户可能导航到的路线(基于视口中的 <Link> 组件)。
    > 导航之间不会重新加载整页,并且会保留 React 状态和浏览器状态。
    > 会话:缓存在整个导航过程中持续存在。但是,它会在页面刷新时被清除

    这一点也很好验证,当你刷新的时候,RevenueChart 会重新获取数据,当你点击左边导航随便一个再点回来,不会触发重新获取,终端上也不会有 fetching data


    ---最后---

    目前你这个例子有一点小,还都是获取 db ,可能无法明显看出 ppr 的优势

    对于 ppr 和 sql revalidate 0 的问题,如果真的有想要完全静态的组件还带有 db 查询,
    我的建议是 unstable_cache 梭哈,不设置过期时间,算是一种半静态吧,除了第一次比较慢,后面其他用户第二次访问就很快了
    epiloguess
        12
    epiloguess  
       236 天前
    忘了发 fork 地址了抱歉,https://github.com/epiloguess/nextjs-dashboard
    epiloguess
        13
    epiloguess  
       236 天前 via Android
    更正:
    1. 我看到一个 noStore() 标注的方法执行的时候,别的 API 方法也跟着被调用,但是作为 cache fallback 的方法不被调用。
    第一点讨论的有点多余,因为我把 fallback 看成 callback 了

    2.但是这并意味着我们就没有 data cache 了
    “并”改成“并不”
    lazyczx
        14
    lazyczx  
    OP
       235 天前
    @epiloguess

    关于第一点,我确实测试的是非 canary ,我以为这个 canary 是 unstable 的,就没想这个是正式版 haha 。。

    ---

    关于和 PPR 有什么关系:我当时就是感觉如果假设页面上只有两个调 API 的组件,其中一个 A 组件调 API 的时候用 cache ,另一个 B 调 API 的时候用 dynamic ,那就相当于 PPR ,因为 A 组件在不 revalidation 的情况下就一直是静态的了,而 B 组件会变化,这和我心里想的部分预渲染的定义相符。可能到教程里的例子,就是把除了 noStore 的,其他的 API 调用都用 cache 包起来,就相当于实现了 canary 里的 PPR 的功能了,即默认其他的都是静态。

    也可能是 cache 的文档里有 revalidation 这个术语,让我想起了教程里 chapter 10 有这句话(看,这里也有 revalidation ):

    > Partial Prerendering leverages React's [Concurrent APIs]( https://react.dev/blog/2021/12/17/react-conf-2021-recap#react-18-and-concurrent-features) and uses [Suspense]( https://react.dev/reference/react/Suspense) to defer rendering parts of your application until some condition is met (e.g. data is loaded).
    >
    > The fallback is embedded into the initial static file along with other static content. At build time (or during **revalidation**), the static parts of the route are *prerendered*, and the rest is *postponed* until the user requests the route.

    ​ 所以我以为它们也许是 通 的。

    ---

    > 你需要确保的是,如果函数都加上 unstable_cache 了,你这个问题应该就不会出现了吧。

    这个我觉得应该是不会了,毕竟我当时测试的时候,加上了 unstable cache 之后,如果没有设定 revalidation 的话,不管怎么操作,都不会 rerender 了,就像是一个动态页面里独立出来了一个静态的玩意儿。所以用了这个应该是绝对不会出来这个问题。

    ---

    > \> 关于第 5 点,我不知道你是怎么测试的,但是我测试的情况下,如果 API 方法里使用了 noStore ,然后把这个方法作为 cache 的 fallback 的话,确实不会退出静态生成。build 的时候显示的 tree 也显示页面仍然是 static ,
    >
    > 你这个是在什么条件下测试的?

    是非 canary 下测试的呀,就是 noStore 只加在 revenue api 方法上,然后用 `cache(()=>getRevenue(), ['revenue'])`。发现 router 树 dashboard 后面的符号是 圆圈。而且用页面的时候 revenue 也没有被调用(除非有 revalidation )。

    > 在正式版/canary without ppr 中,
    > 这个 noStore 是不是 cache 的 callback,都没有关系,只要你这个路线中,任意一个地方出现了 noStore,next 在 build 的时候,就等价于路由段配置中的 force-dynamic 或者 fetch 中的 no-store,相当于退出静态渲染,改用动态。

    我刚刚去把版本换成 canary without ppr 测试了一下,我用了 noStore 在 API 方法里,然后使用 cache 把 API 包裹起来,生成树还是静态的(注意我没有使用你上面回答里的推荐做法,即把 noStore 放在组件里,放在 cache 方法上方,不知道是不是这里有误解。。)。

    ```
    Route (app) Size First Load JS
    ┌ ○ / 226 B 98.9 kB
    ├ ○ /_not-found 871 B 87.8 kB
    ├ ○ /dashboard 293 B 92.2 kB
    ├ ○ /dashboard/customers 139 B 87 kB
    ├ ○ /dashboard/invoices 1.65 kB 95.3 kB
    └ ○ /dashboard/invoices/create 172 B 93.8 kB
    + First Load JS shared by all 86.9 kB
    ├ chunks/23-51b06a4d0afaaa6e.js 31.3 kB
    ├ chunks/fd9d1056-f593fbcc7b74c7aa.js 53.6 kB
    └ other shared chunks (total) 1.91 kB


    ○ (Static) prerendered as static content
    ```

    而且我测试的时候又遇到个怪问题(为什么我老是遇到怪问题。。。):我在用 canary 版本 build 我做到后面 chapter 的代码的时候,发现 /dashboard/invoices 这个 route 变成 dynamic 的了,然后我慢慢注释一些我觉得可疑的东西,想试试是什么把它变成 dynamic 的。

    结果我发现是这行代码。。。(下面有 CONFUSION 的):

    ```tsx
    export default async function Page({
    searchParams,
    }: {
    searchParams?: {
    query?: string;
    page?: string;
    };
    }) {
    // ! CONFUSION: why once use this line, '/dashboard/invoices' became dynamic ??
    const query = searchParams?.query || '';
    // const currentPage = Number(searchParams?.page) || 1;
    // const totalPages = await fetchInvoicesPages(query)
    return (
    <div className="w-full">
    <div className="flex w-full items-center justify-between">
    <h1 className={`${lusitana.className} text-2xl`}>Invoices</h1>
    </div>
    <div className="mt-4 flex items-center justify-between gap-2 md:mt-8">
    <Search placeholder="Search invoices..." />
    <CreateInvoice />
    </div>
    {/* <Suspense key={query + currentPage} fallback={<InvoicesTableSkeleton />}>
    <Table query={query} currentPage={currentPage} />
    </Suspense>
    <div className="mt-5 flex w-full justify-center">
    <Pagination totalPages={totalPages} />
    </div> */}
    </div>
    );
    }
    ```

    我真是惊了个呆 o_o ....,这个不是我自己定义的一个 prop 吗。为什么会把 route 变成 dynamic 的。。。

    关于上面两点,我 commit 了代码( commit message is: canary test ) push 了,你可以跑跑看。(另外我发现我之前没 push 数据库配置。。。我打开你 fork 的仓库跑起来直接数据出不来。。现在我 push 上去了)

    ---

    > \> Next.js 有一个内置的数据缓存(data cache),可以在传入的服务器请求和部署中保留数据获取的结果。这是可能的,因为 Next.js 扩展了本机 fetch API 以允许服务器上的每个请求设置自己的持久缓存语义。

    这部分不懂,动态组件是 PPR 的概念对吧?意思是动态组件也可以调用这个内置的数据缓存吗?如果是这样的话,那 sql 不也是基于 fetch 的吗?意思是通过恰当的配置,可以让一个动态组件每次读的是缓存的数据?

    动态组件的情况下,并不意味着我们不能用缓存吗?

    > https://github.com/orgs/vercel/discussions/4696
    >
    > Your response are cached because `@vercel/postgres` uses `fetch` for `sql` and `fetch` is cached by default in Next.js ( https://nextjs.org/docs/app/building-your-application/caching#data-cache). You can use [Segment Config Options]( https://nextjs.org/docs/app/building-your-application/caching#segment-config-options) to control how caching works.
    >
    > I have created an example at https://github.com/vercel-support/167147-postgres-cache-nextjs/tree/main/app/api where you can compare [the default (cached) response]( https://167147-postgres-cache-nextjs-nsnjr8w00.preview.vercel-support.app/api/default) with [one that's fully dynamic]( https://167147-postgres-cache-nextjs-nsnjr8w00.preview.vercel-support.app/api/no-cache) .

    没仔细看它里面贴的网站,但是他这里说 sql fetch 是默认缓存的(因此造成了不修改请求代码的情况下会返回 stale data ),这好像和下面的 revalidate 0 冲突啊,revalidate 0 不是 no-cache 的意思吗?

    > 一个悲伤的事情是,正式版的静态渲染和 revalidate 0 配合工作良好,ppr 却不行,这也就意味着,对于带有数据库查询的组件,你并不能 ppr 它们变成完全静态的

    我 pull 了你的仓库,试了一下,确实是你说的这样。。

    只要调了 sql ,而且没有 noStore 就会报错,除非使用 cache 。

    我这里也报关于 validate 0 的问题,我也认为应该是 ppr 和它冲突了。

    ---

    其实我就是想说 callback 的,我自己打错了,我看到你说这句,我回去看我写的那么多个 fallback 我也懵逼了,然后在 Suspense 组件那里看到同名 prop 了,疑似被记忆注入替换了 hoho 。
    lazyczx
        15
    lazyczx  
    OP
       235 天前   ❤️ 1
    @epiloguess

    想说点无关的话,希望不会 bother

    不知道是不是因为喜欢潜水久了,突然有解决不了的问题,然后被逼的开始和人交流,带来的感觉真的很不错,感觉自己现在甚至有点像什么人来疯那种心态了(开玩笑 hh )。之前也交流过后端的问题在 v2 ,也有经验者帮忙,感觉到自己真的处于一个互帮互助的社区,这种感觉真的很棒!而且很多问题问 gpt 感觉问不出所以然(可能语言组织还不够),尤其是这种比较新的问题( chatGPT 3.5 数据库才更新到去年 4 月份,然后国内 gpt 又感觉会有很多幻觉),还是交流起来开心哈哈哈。。虽然打字组织语言和想法真的挺累的,但是感觉这也是在锻炼自己薄弱的环节,这感觉也很好!!
    epiloguess
        16
    epiloguess  
       235 天前
    @lazyczx

    只是一个假设,方便测试,使用 canary 但是不使用 ppr,表现出的行为应该和正式版的行为是一样,如果每次测非 ppr 都要切换回正式版就很麻烦了。


    ---

    > 后备与其他静态内容一起嵌入到初始静态文件中。在构建时(或重新验证期间),路线的静态部分被预渲染,其余部分被推迟,直到用户请求路线。

    主要的问题在与,你要深刻理解 nextjs 中服务端组件的渲染机制

    动态组件,在客户端的服务端组件,客户端组件本身
    ppr 和静态+cache+dynamic
    静态导出/静态渲染
    这些概念不要搞混,这些并不相等

    > https://nextjs.org/docs/app/building-your-application/rendering/server-components
    > 使用静态渲染,路线在构建时渲染,或者在数据重新验证后在后台渲染。结果被缓存并可以推送到内容交付网络 (CDN)。此优化允许您在用户和服务器请求之间共享渲染工作的结果。

    在没有 ppr 之前,路线只有静态渲染,动态渲染,流式渲染
    这就意味者,一旦一条路线被确认为动态渲染,这条路线中的所有内容都不会在构建或者重新验证后在后台渲染,从而被缓存为 RSCP 和 html ,推送到 CDN,共享渲染结果
    而是会在请求的时候进行渲染,在请求的时候才利用 data cache (或者 unstable_cache)加速渲染结果

    这也是为什么我们需要 ppr,尽可能提前渲染更多的内容,加速体验


    > 在渲染过程中,如果发现动态函数或未缓存的数据请求,Next.js 将切换为动态渲染整个路由。下表总结了动态函数和数据缓存如何影响静态或动态渲染路由:

    这里是理解的重点,动态函数和未缓存的数据请求
    动态函数中就包括你后面的迷惑点 searchParams ,虽然它是你传的 props,但是只有在请求的时候才能被获取,页面也会因为变成 dynamic
    未缓存的数据请求,包括,
    > https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#opting-out-of-data-caching
    The cache: 'no-store' is added to fetch requests.
    The revalidate: 0 option is added to individual fetch requests.
    The fetch request is inside a Router Handler that uses the POST method.
    The fetch request comes after the usage of headers or cookies.
    The const dynamic = 'force-dynamic' route segment option is used.
    The fetchCache route segment option is configured to skip cache by default.
    The fetch request uses Authorization or Cookie headers and there's an uncached request above it in the component tree.

    其中数据库查询的背后用的就是 revalidate0,这一点,我给出那个 github 上的 discussion,那个信息已经过时,unstable_cache 在那之后发生了一次重构,本身也很合理,没理由一个数据库操作自带 cache,改成用 revalidate0 很合理

    官方文档这一部分也值得多阅读几次
    > https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#fetching-data-on-the-server-with-third-party-libraries

    ---
    > 加上了 unstable cache 之后,如果没有设定 revalidation 的话,不管怎么操作,都不会 rerender 了,就像是一个动态页面里独立出来了一个静态的玩意儿

    unstable cache 的缓存是存放在服务端的,在.next/cache 目录下,完全重新部署就会没有的,只是作为组件内 db 操作无法 ppr 的权宜之计,这一点后面再说,实际上,如果你这么使用,本质上和不用 ppr 没有区别,单就这个组件而言,表现出的效果是一样的


    ---

    > 我刚刚去把版本换成 canary without ppr 测试了一下,我用了 noStore 在 API 方法里,然后使用 cache 把 API 包裹起来,生成树还是静态的(注意我没有使用你上面回答里的推荐做法,即把 noStore 放在组件里,放在 cache 方法上方,不知道是不是这里有误解。。)。

    这一点我之前已经提到过了,不过就连我自己都忘了,借助这一点我们就可以 ppr 数据查询操作了,太棒了!
    > https://nextjs.org/docs/app/api-reference/functions/unstable_noStore
    > 在 unstable_cache 内使用 unstable_noStore 不会选择退出静态生成。相反,它将根据缓存配置来确定是否缓存结果。

    让我们简单回顾一下
    1.在正式版/canary without ppr 中,如果我们在页面中任意位置声明了 noStore ,路线就会退出静态渲染,因为这相当于在路线中出现了 未缓存的数据请求,等价与 force-dynamic 或者 no-store
    但是,如果不声明 noStore ,尽管我们猜测 db 操作背后是 revalidate0,它不会让路线退出静态渲染,
    关于这一点,或许我们就不应该在正式版中使用 noStore,这个 function 如我之前所说,是用来在 ppr 中细粒度的控制组件渲染方式的
    2.在 ppr,
    如果我们在 suspense 边界内部声明了 noStore,根据约定,suspense 内部应该是一个动态组件,内容不应该被 ppr
    如果我们的组件没有声明 noStore,却使用了 db 操作,我们猜测 db 操作背后是 revalidate0 ,nextjs 会表现出和正式版不一样的操作,它会很困惑,不知道我们到底要做什么,我觉得这里就是一切困惑的来源。ppr 无法和 revalidate0 共存,可以提 issue 。
    好在官方可能考虑过这一点了,如果你在 unstable_cache 缓存的函数中声明 noStore,那么在 nextjs 看来,组件仍然是可以被 ppr 的,不会受到 nosStore 和 revalidate0 影响了

    ---


    > 我真是惊了个呆 o_o ....,这个不是我自己定义的一个 prop 吗。为什么会把 route 变成 dynamic 的。。。

    上面已经提到了,searchParams 属于动态函数

    > 另外我发现我之前没 push 数据库配置。。。我打开你 fork 的仓库跑起来直接数据出不来。。现在我 push 上去了)

    你需要 clone 我 fork 的版本然后加上.env 啊,不然没有数据啊,这一点我忘了提了


    ---

    > 这部分不懂,动态组件是 PPR 的概念对吧?意思是动态组件也可以调用这个内置的数据缓存吗?如果是这样的话,那 sql 不也是基于 fetch 的吗?意思是通过恰当的配置,可以让一个动态组件每次读的是缓存的数据?

    > 动态组件的情况下,并不意味着我们不能用缓存吗?

    没错,是这样的,前面说过了,要分清楚动态组件和服务端组件的区别

    或许你应该把 ppr 换个角度,看成缓存了更多静态内容的动态页面

    动态组件也要经过服务器才能查询 db ,或者 fetch 相关数据,不可能把 db 相关操作放到客户端来进行

    这也就意味着他们可以使用
    1. fetch 带来的持久缓存
    2. 每次 req-res 周期中存在的 rect Request Memoization
    3.unstable-cache

    但是动态组件并不能使用 全路由缓存
    > https://nextjs.org/docs/app/building-your-application/caching#opting-out-2
    > 您可以选择退出完整路由缓存,或者换句话说,为每个传入请求动态渲染组件,方法是:


    ---

    不会觉得 bother

    我其实也是最近一个月才接触 nextjs,hh ,没有做过这个项目,因为我学东西喜欢从 doc 看起,doc 中其实 canary,ppr 的内容很少,几乎没有。很高兴多学习了一些知识,也加深了自己对 nextjs ,data fetch ,cache,render 的理解。

    码字组织语言有助于提高自己的逻辑表达能力,不过 v 站这个排版效果真是让我一言难尽哦
    lazyczx
        17
    lazyczx  
    OP
       235 天前
    @epiloguess

    概念讨论:

    因为很多文档我还没有读过(不过我后面会认真读的,尤其是你发出来的文档! owo ),所以我这里就是大胆秀一下我的理解,如果有不对的地方,希望能够获得宝贵的指正!

    动态组件是 PPR 开始才有的,之前是只有动态 route 。它是在静态渲染页面里面动态的部分,每次 request 都会重新渲染。

    在客户端的 RSC 和 RSC 本身,这个有点混淆不懂。。我感觉 RSC 这个东西(我没看过文档,根据感受瞎猜一下 hh )就是把以前应该在客户端的操作挪到服务端,由服务端做好之后,直接把结果( render 好的服务端组件)发回给客户端。而且最重要的是它可以和 客户端组件 配合,客户端组件变化的时候可以把变化传回给服务端,然后服务端组件 render 之后再返回给客户端,隐藏了数据的处理。至于在客户端的服务端组件,好像服务端组件,如果在客户端就无法使用很多客户端才能提供的东西,比如 cookies 。

    静态导出应该是整个网站都是静态站吧,每次想要更新网站的内容,只能 build ,而静态渲染因为有 revalidation ,所以不一定要 build 。

    在没有 ppr 之前,路线只有静态渲染,动态渲染,流式渲染

    静态渲染是在构建时和 revalidation 时渲染一次。

    动态渲染是 request 请求发过来就渲染一次。

    流式渲染是哪个部分先渲染好了,就先发过去哪个部分,让 HTML 不间断地更新,ready ,实现方法是分别在一个 <Suspense> 中包裹每个 RSC ,并且把 data fetching 扔到每个 RSC 内自己做,自己管理自己是否 ready 。

    ---

    另外,你说的这个:

    > 好在官方可能考虑过这一点了,如果你在 unstable_cache 缓存的函数中声明 noStore,那么在 nextjs 看来,组件仍然是可以被 ppr 的,不会受到 nosStore 和 revalidate0 影响了

    我去 canary with ppr 测试了:

    会报错的组合:

    1. 有 cache

    noStore: 在 cache 内 or 完全没有

    2. 无 cache

    noStore: 没有

    不报错的组合:

    1. 有 cache

    noStore: 在 cache 外,在 cache 前(**这里就是 PPR 动态组件使用 cache !**,这时候可以使用 Suspense 在 PPR 做 Stream 其他动态的内容,我在 revenue 那里使用 noStore 再使用 unstable_cache 缓存了 api 返回值,然后又使用 Suspense 包裹了一个空调了一下 revenue API 的组件,发现 revenue API 每次刷新只被调了一次,说明 PPR 里的缓存起效了!)

    2. 无 cache

    noStore: 在 API 方法内外均可,只要在 API 方法里 try catch 之前即可。

    ppr 下,有 revalidate 0 ,next 会不知道我们想要什么( next: 为什么你使用了 revalidate 0 却不显示声明 noStore 啊啊啊迷惑死,大概它是这样的心理 = =),因此必须在 cache 外(如果有 cache )使用 noStore ,明确告诉要使用 ppr 。

    因此我认为,应该要在 cache 语句之前声明 noStore 才有用,在 cache 内部使用 noStore 没用,因这样好像影响不到 cache callback 的外部的组件。

    ---

    排版问题,确实,v2 为啥不在回复里应用 markdown 引擎。。应该不麻烦吧这个。。难道就是鼓励简短的无结构化的回复??
    epiloguess
        18
    epiloguess  
       235 天前 via Android
    @lazyczx 你可能需要 update 一下你的 canary 到 50 版本

    这样的话,在 unstable_cache 缓存的函数中使用 noStore ,内容就会被静态渲染了,你去开发者工具里查看第一个接受的 document 就能看到

    官方是没有动态组件/dynamic component 这个概念的,抱歉误导了你,我用这个词指的是那些在 ppr 中需要动态渲染的组件

    我在整理这个帖子的内容,稍微回复你这个回复的其他内容
    lazyczx
        19
    lazyczx  
    OP
       235 天前
    @epiloguess

    > 这样的话,在 unstable_cache 缓存的函数中使用 noStore ,内容就会被静态渲染了,你去开发者工具里查看第一个接受的 document 就能看到

    这个是在 PPR 开启的情况下吗?

    你的意思是没有使用 "unstable_cache 前的 noStore()" 的情况下吗?

    那就是变成我说的会报错的 组合 1 了。

    我刚刚升级到 50 版本 ( from 49 )测试了,还是会报错呀。

    如果你说的是 ppr 未开启的情况下,那肯定是会开启静态渲染了,因为 cache 里的 noStore 似乎没有直接作用于 next route (不知道我的表述对不对)。

    使用 unstable_cache 的话,也没有在 unstable_cache 前使用 noStore ,在 PPR 下会直接报错( validate 0 和 unstable_cache 的冲突);不在 PPR 下的话,本来就会静态渲染呀,我好像没有否认过这一点。

    和这点有关的我先前的回复:

    > 我刚刚去把版本换成 canary without ppr 测试了一下,我用了 noStore 在 API 方法里,然后使用 cache 把 API 包裹起来,生成树还是静态的(注意我没有使用你上面回答里的推荐做法,即把 noStore 放在组件里,放在 cache 方法上方,不知道是不是这里有误解。。)。
    epiloguess
        20
    epiloguess  
       235 天前
    @lazyczx

    我发现一些奇怪的事情,我的评价是远离 canary...

    以下为一些发现,都指的是 ppr 开启的情况下
    ---

    ###第一个 bug.
    先不考虑 unstable_cache 。
    1. noStore 的位置没有影响。
    只要 suspense 边界内有 noStore,这个组件就不会被预渲染。
    在没有 unstable_cache,开启 ppr,路由会被部分渲染,从 dashboard 上面那个标题就能看出来

    2.db 操作不是 revalidate 0 ,这一点跟原来想得不一样

    在没有 unstable_cache,没有 noStore 的情况下,开不开 ppr,路由都会被渲染成静态的

    3.非常非常有意思的一点,
    假如现在有两个组件,第一个组件满足条件 1(只有 noStore),第二个组件满足条件 2(没有 noStore,没有 unstable_cache)

    然后,会报错!!!直接黑人问号???

    这时候你先后注释掉一个组件,你会发现,每一个都可以 build,不是 static,就是 partial,合一起就不行了。

    我直接 hhhhhhhhhhhhhhh,要疯了
    这个 bug 直接误导我们 db 操作是 revalidate0

    ---

    ### 第二个 bug
    在第一个 bug 的基础上,我们想探索出,如何让两者共存
    目标是,两个 db 查询的组件,一个是动态渲染的,一个是构建时渲染的
    动态渲染的那个同样先不考虑 unstable_cache,因为那属于优化体验的事情.
    直接考虑第二个组件,目前是什么都没有,没有 cache,noStore
    这时候我们走进了第二个坑,准确来说就我一个
    > 在 unstable_cache 内使用 unstable_noStore 不会选择退出静态生成。相反,它将根据缓存配置来确定是否缓存结果。
    接着我们尝试在第二个组件中,在 unstable_cache 缓存的函数内使用 noStore,我说行,你说不行,那到底行不行?
    答案是,有时候行,有时候不行

    如果你意外让你的本地缓存里出现了你想要缓存的数据,那就行.

    首先,删掉你的.next,然后 build,直接 errrrrrrrrrrrrrooor(感觉自己有点精神不稳定了)

    第二部,注释掉 ppr,删掉.next(如果有),然后 build 一下,访问一下 dashboard 不出意外,第一个组件是动态渲染的,第二个组件是秒加载的,
    值得注意的是,这时候 dashboard 路线是动态的,第二个组件在 build 的时候被执行过一次,得益于 unstable_cache(谢谢你啊我的 cache!!!!),你第二次(你以为你是第一次,实际不是)访问的时候,数据是秒加载的


    这一点很容易验证,你在第二个组件首行加上终端输出就会看到,每次刷新都会有终端输出


    最后,加上 ppr,然后 build,成功了????恩?恩?



    ---
    一些多余的总结,包括你已经知道了的,
    在 ppr 中,noStore 用来声明动态渲染组件,unstable_cache 可以用来缓存函数返回值,降低数据库压力
    在 ppr 中,在 unstable_cache 中使用 noStore 目前会报错,应该在 cache 外使用


    > 在 unstable_cache 内使用 unstable_noStore 不会选择退出静态生成。相反,它将根据缓存配置来确定是否缓存结果。
    这句话好像只对非 ppr 有效

    非 ppr 时,在 unstable_cache 中使用 noStore 确实不会退出静态生成
    比如说,我们把第一个组件注释掉,在第二个组件中如此使用,生成的是 static 的
    不过,为什么要在 unstable_cache 中使用 nostore 呢?明明不使用 unstable_cache,和 unstable_noStore,一样可以生成 static 啊(终端不会再有东西蹦出来了)


    梳理一下应对方案和注意事项
    假设你正在构造一个 ppr 的页面,除非你想让页面完全 static,那你所有的 db 组件可以什么都不用管,(不过那你还要 ppr 干啥呢)
    就算你只有一个想动态渲染组件,这个组件也应该加上 noStore,不然它会 static

    现在似乎没有办法在开启 ppr 的时候,只静态渲染某个 db 组件
    ---

    最后的最后

    我已经不想思考为什么了,好在我错的够多,聪明如我,已经在之前给出过答案了.

    > 我的建议是 unstable_cache 梭哈,不设置过期时间,算是一种半静态吧,除了第一次比较慢,后面其他用户第二次访问就很快了
    epiloguess
        21
    epiloguess  
       234 天前
    好消息是,经过扒了一夜的源码,我成功搞定了这个 bug 可以怎么被修复.
    坏消息是我好困

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

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

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

    记得保存

    ---如何验证

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

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

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

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

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

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

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

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

    https://airportal.cn/519534/Fwr351S2E5

    密码是 519534
    epiloguess
        26
    epiloguess  
       233 天前
    @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
        27
    epiloguess  
       232 天前
    更正:
    倒数第二个水平线
    ---
    那我们思考一下访问会访问什么

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

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

    ---

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

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

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

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

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

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

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

    再来一次

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

    123456
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2776 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 15:10 · PVG 23:10 · LAX 07:10 · JFK 10:10
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.