记一次误用顶层await导致的路由渲染错误

文章讲述了顶层await的概念,允许在模块顶层使用await,但需注意其必须在模块的顶层。作者在Vue的<scriptsetup>中尝试使用顶层await,导致组件渲染失败,因为<scriptsetup>相当于在setup方法内,这违反了顶层await的使用规则。错误提示表明需要在<Suspense>组件内使用异步setup。最后,作者提醒在使用新特性时要注意其使用条件。

背景:顶层 await

Async 异步函数能将 Promise 的链式调用的形式,改为同步的形式,对于编写和阅读代码都非常友好。但一直以来都有一个限制,就是 async 和 await 这两个关键字必须成对出现。这就导致了一个问题,想使用 await,必须要将其定义在一个 async 函数中,再调用此函数。所以 ECMAScript 中一直有一个提案,叫做顶层 await,它 支持在 async 函数以外使用 await,但是只能在一个模块的顶层中使用。这个提案于2022年正式成为 ES 标准语法。

过去的写法是这样的:

async function fn() {
  const value = await 10; // await 不仅可以接Promise,也可以将普通值
  console.log(value)
}

main();

使用顶层await 之后,就可以这样写了:

const value = await 10;
console.log(value)

但是一定有一个前提,必须处于模块的顶层。我就是没有注意这一点,导致出现了一个问题。

bug 出现

有一次开发时突发奇想,想用一下这个顶层await。于是写下了类似下面的代码:

<script setup>

const res = await axios(url)

</script>

然后过了一会,等去调试页面时才发现已经白屏了。于是打开调试工具,在元素面板中看到了整个页面的路由都没有渲染出来,只剩下一个空注释节点了:

image.png

在控制台中看到了如下警告:

[Vue warn]: Component <Anonymous>: setup function returned a promise, but no <Suspense> boundary was found in the parent component tree. A component with async setup() must be nested in a <Suspense> in order to be rendered.

起初按照直觉,以为是路由表配置出了问题,导致路由渲染不出来,反复调试路由配置,始终不奏效。然后搜索这条警告信息,果然有不少朋友都遇到过,无一例外是在 <script setup> 中使用了顶层 await。那我就纳闷了,说好的顶层await怎么不好用了呢?

盯着代码想了一会,突然想起 <script setup> 只是语法糖啊!那么上面的代码就相当于在 setup 方法中使用了 await:

<script>
export default {
  setup() {
      const res = await axios(url)
  }
}
</script>

这自然不是在模块顶层中使用了,也就导致了在解析上出现问题,导致组件不能正确渲染,最终导致对应的路由视图没有渲染出来。

小结

起初由于空页面,空节点,误解是路由的问题。其实 <router-view> 之所以被渲染成注释节点,看似是路由组件没有正确渲染,也有可能是组件本身出了问题。另外,在使用一些新特性要特别注意使用条件。

JavaScript 中的 `await` 关键字通常用于等待一个异步操作完成,它只能在 `async` 函数内部使用。然而,在模块系统中(如 ES6 模块或 Node.js 的 ESM),引入了“顶层 `await`”的概念,允许在模块的顶层作用域直接使用 `await`。这一特性简化了异步代码的编写,但也带来了一些潜在的问题,尤其是缓存行为和副作用。 ### 顶层 `await` 的缓存行为 当模块首次被加载时,其内容会被解析并执行一次,之后的结果将被缓存。这意味着如果模块中包含顶层 `await` 表达式,并且该表达式依赖于某些外部状态(例如网络请求、动态配置等),那么这些值仅在首次加载模块时被解析,后续的导入操作将不会重新执行这些异步操作,而是直接使用缓存的结果[^2]。 例如: ```javascript // config.js const response = await fetch('https://api.example.com/config'); const config = await response.json(); export default config; ``` 当其他模块通过 `import config from './config.js'` 导入时,只有第一次导入会实际执行 `fetch` 请求,后续导入将直接使用已缓存的 `config` 值。这可能导致模块返回的是过期的数据,尤其是在需要频繁更新配置的场景中。 ### 解决方案与最佳实践 #### 1. **避免在模块中使用顶层 `await` 获取动态数据** 如果模块中的数据需要频繁更新,建议不要在模块中直接使用顶层 `await` 来获取这些数据。可以改为导出一个异步函数,由调用者显式地调用该函数来获取最新数据: ```javascript // config.js export async function fetchConfig() { const response = await fetch('https://api.example.com/config'); return await response.json(); } ``` 调用方式: ```javascript import { fetchConfig } from './config.js'; (async () => { const config = await fetchConfig(); console.log(config); })(); ``` 这样每次调用 `fetchConfig()` 都会重新发起请求,从而获取最新的数据[^2]。 #### 2. **使用模块缓存控制机制** 在某些环境中(如 Node.js 的 ESM),可以通过手动清除模块缓存来强制重新加载模块。虽然这不是一种推荐的常规做法,但在某些调试或特定需求下可能有用: ```javascript import { promises as fs } from 'fs'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); delete require.cache[require.resolve('./config.js')]; const config = require('./config.js'); ``` 这种方式适用于希望绕过缓存并重新执行模块逻辑的情况。 #### 3. **采用中间层封装异步初始化逻辑** 对于需要异步初始化的模块,可以设计一个“初始化”函数,由主程序决定何时触发初始化过程,而不是依赖模块自动加载: ```javascript // database.js let connection; export async function initDatabase() { connection = await connectToDatabase(); // 假设这是异步连接数据库的函数 } export function getDatabaseConnection() { if (!connection) throw new Error('Database not initialized'); return connection; } ``` 主程序中: ```javascript import { initDatabase, getDatabaseConnection } from './database.js'; (async () => { await initDatabase(); const db = getDatabaseConnection(); })(); ``` 这种方法确保了模块的状态是可控的,并且可以在适当的时候重新初始化。 --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

昆吾kw

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值