HarmonyOS Next IM实战:异步缺乏await导致的逻辑错误Bug处理
背景介绍
IMSDK开发过程中,逻辑特别复杂,拿消息拉取逻辑来说,通过序号拉取热消息列表,接着处理消息,遍历消息内容,根据不同消息类型进行不同处理,将最终消息插入到消息表,如果消息是新会话消息还需要将会话信息插入到会话表,将用户信息插入到用户表、用户成员表等,最后通知UI新消息更新。网络请求、数据库操作很多都是异步操作,在整个流程中很多异步操作都是需要等待执行完才执行下一步,很多时候稍不留神就会忘记使用await
导致出现逻辑错误的莫名其妙的Bug。
当出现Bug后,很难根据现象定位到原因,只能重新梳理逻辑,查看是否有漏写await的地方,但是这样不仅效率低下,而且难免还是会有遗漏,有什么好办法呢?
Code Linter 登场
这个时候可以借助Code Linter扫描代码帮助我们发现潜在的问题。每次代码提交前使用Code Linter检查可能存在的风险。
Code Linter 使用
在项目中根路径下创建code-linter.json文件,文件配置内容格式如下:
{
"files": [
"**/*.ets"
],
"ignore": [
"**/src/ohosTest/**/*",
"**/src/test/**/*",
"**/src/mock/**/*",
"**/node_modules/**/*",
"**/oh_modules/**/*",
"**/build/**/*",
"**/.preview/**/*"
],
"ruleSet": [
"plugin:@performance/recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/return-await": "error",
"@typescript-eslint/require-await": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/promise-function-async": "error"
}
}
这里面主要三个层级:
- files:指定检测哪些文件,一般是所有ets文件;
- ignore:忽略文件,一般会忽略构建产物、测试文件等;
- ruleSet:规则集合,本示例中配置了性能推荐和通用规则
- rules:具体规则。
配置完成后在IDE中右键要检测模块,依次选择Code Linter、Full Linter:
最后会在Code Linter输出检查结果:
我们根据检测结果修改不符合规范的代码即可。
异步相关规则介绍
这里我们用到三个异步相关的code linter规则。
promise-function-async
# @typescript-eslint/promise-function-async
规则要求任何返回Promise的函数或方法标记为async。确保每个功能只能:
- 返回被拒绝的 promise,或者
- 抛出一个 Error 对象。
相比之下,非 async
、Promise
返回函数在技术上可以做到这两者。处理这些函数结果的代码通常需要处理这两种情况,这可能会变得复杂。此规则的实践消除了创建代码来处理这两种情况的要求。
promise-function-async规则的主要目的是:
- 提高代码清晰度:明确标识返回 Promise 的函数
- 防止常见错误:避免忘记处理异步操作
- 统一代码风格:确保所有 Promise 返回函数使用一致的声明方式
- 简化错误处理:确保
async
函数的自动错误传播机制 - 增强类型安全:利用 TypeScript 类型系统验证异步行为
下面是正确使用示例,返回Promise的函数都被标记为async:
export const arrowFunctionReturnsPromise = async () => Promise.resolve('value');
export async function functionReturnsPromise() {
return Promise.resolve('value');
}
// An explicit return type that is not Promise means this function cannot be made async, so it is ignored by the rule
export function functionReturnsUnionWithPromiseExplicitly(
p: boolean
): string | Promise {
return p ? 'value' : Promise.resolve('value');
}
export async function functionReturnsUnionWithPromiseImplicitly(p: boolean) {
return p ? 'value' : Promise.resolve('value');
}
下面是使用不规范示例:
export const arrowFunctionReturnsPromise = () => Promise.resolve('value');
export function functionReturnsPromise() {
return Promise.resolve('value');
}
export function functionReturnsUnionWithPromiseImplicitly(p: boolean) {
return p ? 'value' : Promise.resolve('value');
}
具体说明参考:
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide_promise-function-async-V5
await-thenable
@typescript-eslint/await-thenable
不允许对不是“Thenable”对象的值使用await关键字(“Thenable”表示某个对象拥有“then”方法,比如Promise)。
正确使用代码如下:
async function test() {
await Promise.resolve('value');
}
export { test };
不符合规范代码示例如下:
async function test() {
await 'value';
}
export { test };
具体文档参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide_await-thenable-V5
require-await
这个是我们最需要关注的规则,@typescript-eslint/require-await
要求异步函数必须包含“await”。
规则的主要目的是:
- 防止误用:避免在非异步值上使用
await
,这通常是代码错误或误解 - 提高代码清晰度:确保
await
只用于真正的异步操作 - 优化性能:消除不必要的异步开销(微任务队列)
- 类型安全:利用 TypeScript 类型系统检测无效的
await
使用
比如下面代码:
async function doSomething(): Promise {
return Promise.resolve();
}
export async function foo() {
doSomething();
}
foo函数会被检测到少了await,最大程度的帮助我们发现了可能的漏洞。
具体文档参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide_require-await-V5
return-await
@typescript-eslint/return-await
要求异步函数返回“await”。
在 async
函数中,返回 promise 封装的值或直接返回值都是有效的,这两者最终都会产生具有相同履行值的 promise。返回值而不是 promise 封装的值可以有几个好处:
- 返回等待的 promise 改进堆栈跟踪信息。
- 当 return 语句在 try…catch 中时,等待 promise 可以捕获 promise 的拒绝,而不是将错误留给调用者。
- return await promise; 就是 至少与直接返回 promise 一样快。
return-await规则强制一致处理是否在返回 promise 之前等待 promise。
return-await规则有四种配置: - never :不允许等待任何返回的 promise。
- error-handling-correctness-only:在错误处理上下文中,规则强制必须等待返回的 promise。在普通上下文中,规则不会强制执行有关是否等待返回的 promise 的任何特定行为。
- in-try-catch:在错误处理上下文中,规则强制必须等待返回的 promise。在普通上下文中,规则强制 _ 不得 _ 等待返回的 promise。 */
- always:要求等待所有返回的 promise。
return-await规则中的选项区分 “普通上下文” 和 “错误处理上下文”。错误处理上下文是返回未等待的 promise 会导致与异常/拒绝有关的意外控制流的任何地方。
- 如果你在
try
块内返回一个 promise,则应该等待它以按预期触发后续的catch
或finally
块。 - 如果你在
catch
块内返回一个 promise,并且有一个finally
块,则应该等待它以按预期触发finally
块。 - 如果你在
using
或await using
声明与其范围结束之间返回一个 promise,则应该等待它,因为它的行为相当于封装在try
块中后跟finally
的代码。
普通上下文是可能返回 promise 的任何其他位置。在普通上下文中是否等待返回的 promise 的选择主要是风格上的。
选项 | 普通上下文 (风格偏好🎨) | 错误处理上下文 (捕获错误🐛) | 我应该使用此选项吗? |
---|---|---|---|
always | return await promise; | return await promise; | ✅ 是的! |
in-try-catch | return promise; | return await promise; | ✅ 是的! |
error-handling-correctness-only | 不关心 🤷 | return await promise; | 🟡 可以使用,但上述选项会更好。 |
never | return promise; | return promise; (⚠️ 这种行为可能有害⚠️) | ❌ 不。此选项已弃用。 |
如下代码会被要求return 后跟await:
export async function validInTryCatch1() {
try {
return Promise.resolve('try');
} catch (e) {
return Promise.resolve('catch');
}
}
最佳实践:
- 使用
"in-try-catch"
选项 - 这是最合理的默认设置 - 在 try-catch 块中总是使用
return await
- 在其他地方直接返回 Promise
- 避免在不需要错误处理的地方使用不必要的
await
具体文档参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide_return-await-V5
no-floating-promises
@typescript-eslint/no-floating-promises
要求正确处理Promise表达式。floating-promise是指在创建Promise时,没有使用任何代码来处理它可能引发的错误,这是一种不正确的使用方式。“floating” Promise 是在没有设置任何代码来处理它可能抛出的任何错误的情况下创建的。浮动 Promises 可能会导致几个问题,例如操作顺序不正确、忽略 Promise 拒绝等。
no-floating-promises规则将报告未按以下方式之一处理的 Promise 值语句:
- 使用两个参数调用其
.then()
- 使用一个参数调用其
.catch()
await
ing itreturn
ing itvoid
ing it
no-floating-promises规则还会报告何时创建包含 Promise 的数组且未正确处理。解决此问题的主要方法是使用 Promise 并发方法之一创建单个 Promise,然后根据上述过程进行处理。这些方法包括:
Promise.all()
Promise.allSettled()
Promise.any()
Promise.race()
反例介绍:
export async function bar() {
const promise = new Promise(resolve => {
resolve('value');
return 'finish';
});
promise;
Promise.reject('value').catch();
await Promise.reject('value').finally();
['1', '2', '3'].map(async x => x + '1');
}
规则的核心意图总结:
- 防止静默失败
- 确保 Promise 拒绝(rejection)不会被静默忽略
- 避免未捕获的 Promise 错误导致应用状态不一致
- 提升代码可靠性
- 强制开发者显式处理异步操作的结果
- 防止因忘记错误处理而导致的崩溃
- 改善调试体验
- 确保所有异步错误都能被追踪和记录
- 避免难以复现的竞态条件问题
- 增强代码可维护性
- 明确标识异步操作的消费点
- 使异步数据流更易于理解和追踪
参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide_no-floating-promises-V5
no-misused-promises
@typescript-eslint/no-misused-promises
规则旨在防止 Promise 在非异步上下文中被误用,确保 Promise 只在适合处理异步操作的场景中使用。这条规则是 TypeScript 项目中避免常见异步错误的关键防线。
规则的核心意图
- 防止条件语句中的误用
- 检测在条件判断中直接使用 Promise(而非其解析值)
- 避免因 Promise 对象始终为真值(truthy)导致的逻辑错误
- 防止逻辑表达式中的误用
- 禁止在逻辑操作符(&&, ||, ??)中直接使用 Promise
- 确保逻辑操作基于实际值而非 Promise 对象
- 确保回调函数正确使用
- 防止在期望同步返回值的回调中返回 Promise
- 避免在数组方法(如 forEach)中误用异步函数
- 保护 void 返回类型
- 禁止在期望 void 返回类型的位置返回 Promise
- 确保事件处理器等特殊场景的正确行为
问题场景分析
场景1:条件语句中的误用
// ❌ 错误:在条件判断中使用 Promise 对象
if (fetchUserData()) {
// 始终执行,因为 Promise 对象总是 truthy
console.log('正在获取用户数据...');
}
// ✅ 正确:使用 await 获取实际值
async function checkUser() {
const userData = await fetchUserData();
if (userData) {
console.log('用户数据已加载');
}
}
场景2:逻辑表达式中的误用
// ❌ 错误:在逻辑操作符中使用 Promise
const config = loadConfig() || defaultConfig; // 永远使用 loadConfig()
// ✅ 正确:先解析 Promise 再比较
async function getConfig() {
const loadedConfig = await loadConfig();
return loadedConfig ?? defaultConfig;
}
场景3:回调函数中的误用
// ❌ 错误:在数组方法中使用异步回调
[1, 2, 3].forEach(async (id) => {
await deleteRecord(id); // 可能导致意外并发
});
// ✅ 正确:使用 for...of 顺序处理
async function deleteRecords() {
for (const id of [1, 2, 3]) {
await deleteRecord(id);
}
}
// ✅ 正确:使用 Promise.all 并行处理
await Promise.all([1, 2, 3].map(id => deleteRecord(id)));
场景4:void 返回类型的误用
// ❌ 错误:在事件处理器中返回 Promise
const button = document.getElementById('submit');
button.addEventListener('click', () => {
return submitForm(); // 事件处理器应返回 void
});
// ✅ 正确:使用 void 明确忽略返回值
button.addEventListener('click', () => {
void submitForm(); // 明确表示忽略 Promise
});
// ✅ 正确:使用 async 并处理错误
button.addEventListener('click', async () => {
try {
await submitForm();
} catch (error) {
showError(error);
}
});
具体文档参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide_no-misused-promises-V5
总结
本文介绍了在HarmonyOS 开发中由于异步使用不当导致的问题,通过code linter中五个具体规则帮助我们最大程度规避异步await导致的问题。
##鸿蒙核心技术##鸿蒙开发工具##DevEco Studio##
##社交##