本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
前言
如果说,有一个 Node.js API 是 Next.js 路由和我们日常开发用到的多个 API 的核心,你猜是哪个 API?
答案是 AsyncLocalStorage。
这可不是一个新 API,早在 2020 年就进入了稳定阶段,在日常的开发中,也可放心使用。
本篇就为大家介绍下 AsyncLocalStorage 这个 API,此外本篇我还会写一个可运行的 Demo,用于展示 AsyncLocalStorage 如何在 Next.js 的 cookies()、headers() 等函数中发挥作用,帮助大家理解 cookies()、headers() 函数的实现原理。
Node.js AsyncLocalStorage 介绍
先让我们介绍下 AsyncLocalStorage 这个 API,简单的来说,这是一个解决异步操作中数据存储的 API。
像我们使用 Next.js、Express.js 等 Node.js 框架写路由处理程序时,一个请求中可能会连续嵌套调用多个函数:
1 | javascript复制代码app.get('/', async (req, res, next) => { |
如果我们在请求时声明一个值(就比如用于监控的 traceId),如何保证深层次的函数如上图的 two() 函数准确的获取这个值,而且保证各个请求之间相互独立,不会获取错乱呢?毕竟我们还用了 async/await、setTimeout 等异步方式调用,如果直接声明为全局变量,很容易就获取错误。
一个简单的方法就是将参数透传。用伪代码表示如下:
1 | javascript复制代码app.get('/', async (req, res, next) => { |
这样一层一层传递当然是可以的,就是不够优雅!
Node.js 直接提供了 AsyncLocalStorage 这个 API 用于处理异步操作中的数据存储问题,按照 Node.js 的说法,该 API 高性能且内存安全。而且于 Node.js v16 版本就已进入稳定阶段。所以可以放心使用。
AsyncLocalStorage 的用法也比较简单:
1 | javascript复制代码import { AsyncLocalStorage } from 'node:async_hooks'; |
在这个例子中,我们声明一个 AsyncLocalStorage 实例,调用 enterWith 方法存储值,调用 getStore 获取值。
输出的效果如下:
尽管我们用了 setTimeout,但每个请求都获取到了正确的值,而如果我们直接获取外层 id 变量,因为 setTimeout 的异步效果,每次打印的值都是 5。
除了用 enterWith,也可以使用 run 方法,Node.js 提供了官方示例代码:
1 | javascript复制代码import http from 'node:http'; |
run 的第一个参数是 store,表示要存储的值,第二个参数是回调函数,store 只能在回调函数内访问,回调函数内创建的任何异步操作都可以访问该 store。
在这个例子中,我们使用 AsyncLocalStorage 构建了一个简单的 HTTP 请求 traceId,虽然发出了两条请求,但每条请求的 traceId 都是相互独立的,只能在各自的请求中获取到。
但是 AsyncLocalStorage 到底是怎么实现的呢?归根到底还是使用了底层的 API,拿到了异步函数的调用(AsyncResource,每次异步调用,V8都会创建一个对应的 AsyncResource),将 store 存储到这个异步资源上,所以在异步函数中调用也能正常获取到 store。
此外,使用 AsyncLocalStorage 会带来一定的性能损失,但相比它带来的收益,依然是十分值得使用的。
Next.js cookies() 介绍
说完 AsyncLocalStorage,我们说说 Next.js 的 cookies 函数,这是 Next.js 提供的用于获取请求 Cookie 的 API,使用方式如下:
1 | javascript复制代码import { cookies } from 'next/headers' |
除了 cookies(),获取标头的做法也是类似的,只是改用 headers() 函数:
1 | javascript复制代码import { headers } from 'next/headers' |
但是,在使用 cookies()、header() 的时候,有没有想过,为什么可以这样获取呢?为什么调用一下 cookies() 函数就可以获取到请求的 cookie,而不会出现错乱呢?为什么不采用透传 req 的方式来实现呢,就比如这样写:
1 | javascript复制代码export default function Page(req) { |
cookeis()、headers() 的背后到底是怎么实现的呢?
这就要说到 AsyncLocalStorage。为了演示 cookies 的工作原理,我们顺便使用 Express 手写一个 React SSR,那就让我们开始吧!
AsyncLocalStorage 实现 cookies()
新建 next-cookies
项目目录,运行 npm init
初始化项目。
运行以下命令安装用到的依赖项:
1 | bash复制代码npm i tsx express react react-dom |
其中:
- tsx 用于编译运行 jsx 文件(当然你也可以用 bun 或者其他工具替代)
- express 用于构建服务
- react、react-dom 用于书写 React 代码
新建 index.tsx
,代码如下:
1 | javascript复制代码import express from "express"; |
这段代码并不复杂,我们来详细解释一下作用。
当访问 /xxx
的时候,首先调用 parseCookies 获取 req 中的 cookies 对象,当然我们也可以直接使用 cookies-parse 等中间件,但这里为了更直观的展示,我们手动读取了 cookies 标头并将其转为对象。
然后调用 cookiesStorage.run,将 parse 后的 cookies 作为 store 传入,这样我们就可以在回调函数中的任何地方获取到该 store。
而在回调函数中,我们调用 renderToPipeableStream 将 React 组件转为流的形式进行返回。
renderToPipeableStream 是标准的 React API, 将一个 React 组件树渲染为管道化(pipeable)的 Node.js 流,具体使用方式可以参考 React 官网,这里我们演示的是一个标准的 renderToPipeableStream 用法。
而在具体的 <User>
组件中,新建 user.tsx
,代码如下:
1 | javascript复制代码import React from 'react'; |
我们调用了 index.tsx 导出的 cookies 函数,而导出的 cookies 函数代码其实很简单:
1 | javascript复制代码export function cookies() { |
就是这样,我们调用 cookies() 获取了请求的 cookies 对象。归根到底还是因为调用 cookies() 函数的时候还是在 cookiesStorage.run 的回调函数中。
修改 package.json
,添加脚本命令:
1 | javascript复制代码{ |
最后运行 npm start
,效果如下:
是不是跟我们在 Next.js 使用 cookies()、headers() 的方式很类似了?
- 功能实现:Next.js Cookies 函数
- 仓库源码:github.com/mqyqingfeng…
- 下载代码:
git clone -b nextjs-cookies git@github.com:mqyqingfeng/next-app-demo.git
PS:学习 Next.js,欢迎入手小册《Next.js 开发指南》。基础篇、实战篇、源码篇、面试篇四大篇章带你系统掌握 Next.js!
参考链接
本文转载自: 掘金