一、本文适合对象
- 对 RxJS 感兴趣,有探索欲望
- 熟悉函数式编程和面向对象范式
- 对 RxJS 有基础了解
- 熟悉 RxJS 操作符
二、rxjs 处理复杂业务
有时候我们要把 RxJS 理解为一个中 编程语言
, 因为有自己的编程范式。数据必须是要在客观对象的中包裹或者pipe函数中处理的。
三、为什么会复杂?
之所复杂其实就是 RxJS 处理复杂的过程逻辑与面向对象和过程式有很大的不同,需要 RxJS 化,思考方式需要流的知识点支持,需要 RxJS 基于管道函数式编程思想的支持与实践。
四、普通程序复杂
- 单/多数据,在普通的程序中,数据通常就是变量中保存。
- 处理异步,异步处理方式:callback/promise/async-await。
- 数据转换,根据不同业务需求,需要进行不同的转换
- 条件判断,不同的条件干不同的事情
- 复杂逻辑抽象函数,将复杂的内容抽象化,防止代码复杂
- 复用代码,抽象出重复的代码,方便维护
- 错误处理,出错处理往往与业务结合。
五、数据流向
数据流变化:可观察对象倾向与使用 push
方式处理数据,有一个数据源,但订阅之后,使用 push方式获取数据。
ts
function handle() {
return {}
}
const data = handle()
data 在 handle 函数发生调用之后, 被
拉取到。而 RxJS 是数据根据订阅推送的
流
一个可观察对象就是一个数据流:
- 静态数据流:
of(1)
/from(['a'])
- promise 流
- ...
组合数组流
场景:当单个数据流,携带的数据不能满足需求,此时就需要组合流,组合的方式有很多种,这里着重介绍以下几种
combineLatest([ob$1, ob$2])
: 组合最新的,特点是有新的值就发出一组数据。forkJoin()
: 支持数组,对象
等形式,特点式,发出流中的最后一个,如果其中一个组合一致没有发出值,就会被挂起,此时可能需要额外的处理。
流中数据处理
- map系列:转换
- filter:过滤
- 数学计算
- CRUD
- 组合
切换流
当我们完成第一个流任务,然后需要进入下一个流,此时我们就需要切换流。
- switchMap 就是一个用具抓换数据并切换的操作符。有了流的切换,意味着我们就具有:
创建 RxJS 无限流可能,一个 RxJS 流可完成我们想要的众多任务
。
条件判断
在流中数据返回可能需要我们进行判断:
- iif 就是一个判断操作符,第一参数是判断函数,需要返回 boolean 值,第二个和第三个都是可观察对象,分别是对错流。
错误处理
- 捕获错误:catchError
- 抛出错误:throwError
错误处理必要的,throwError 和 catchError 都需要接收函数,catchError 用于捕获当前 pipe 中的错误。
调试困难
在 RxJS 中由于函数占据主流,箭头函数使得编程变得简单,但是箭头函数在返回一个内容时不利于调试。在 pipe 管道中等更加不好直接调试。这样使得编程时候增加了难度。
处理一个复杂的逻辑
以下是一个登录流程:
- 获取 session 并校验是否登录
- 登录 dto 数据
- 校验 dto(成功-失败处理)
- 数据库查询(基于 prisma)
- 对比密码
- 根据对比结果条件判断
- 写入登录日志
- 重定向到 dashboard
以上大概包含了 7 显示的任务,如果使用 rxjs 以下面的形式完成:
post 方法要求返回一个 Response/null/...,我们这在处理错误或者路由跳转都将其封装到函数,当 RxJS 数据完成时,调用函数即可。
ts
static async post({ request, params }: ActionFunctionArgs) {
const session$ = from(getSession(request.headers.get("Cookie")));
const lang$ = of(params?.lang).pipe(defaultIfEmpty(defaultLang));
const dataDto$ = from(request.json());
const crreateErrorHandle = (message?: string) => () => {
return respUtils.respFailJson(
{},
message ?? "登录失败,用户名或密码错误!",
);
};
const redirectToDashboard =
(url: string, cookie: string, lang: string) => () => {
return redirect(`/${lang}/admin/dashboard`, {
headers: {
"Set-Cookie": cookie,
},
});
};
const user$ = dataDto$.pipe(
switchMap((dataDto) => findByUserName$(dataDto.username)),
catchError((e) => throwError(crreateErrorHandle(e ?? "未注册"))),
);
const loginResult$ = forkJoin([dataDto$, user$, session$]).pipe(
switchMap((v) => {
const [dataDto, user, session] = v;
return iif(
() => dataDto === null,
from([]).pipe(
switchMap(() => {
session.flash("error", "Invalid username/password");
return throwError(
crreateErrorHandle("Invalid username/password"),
);
}),
),
of([dataDto, user]).pipe(
tap(() => {
session.set("userId", String(user?.id));
}),
),
);
}),
map((v) => ({
user: v[1],
passwordMatch: comparePassword(v[0].password, v[1].password),
})),
switchMap(({ user, passwordMatch }) => {
return iif(
() => passwordMatch,
of(user),
throwError(crreateErrorHandle("Invalid username/password")),
);
}),
switchMap((user) =>
from(getLoginInfo(request)).pipe(
map((loginLog) =>
loginLogSchema.parse({ ...loginLog, name: user.name }),
),
switchMap((validateLoginLog) =>
from(createLoginLog({ ...validateLoginLog })),
),
switchMap(() => of(user))
),
),
);
const url$ = forkJoin([loginResult$, lang$]).pipe(
map((v) => `/${v[1]}/admin/dashboard?${v[0].username}`),
);
const result$ = url$.pipe(
switchMap((url) =>
from(session$).pipe(
switchMap((session) =>
forkJoin([of(url), from(commitSession(session)), lang$]),
),
map((data) => ({ url: data[0], cookie: data[1], lang: data[2] })),
map(({ url, cookie, lang }) => {
return redirectToDashboard(url, cookie, lang!);
}),
),
),
catchError((e) => {
return throwError(crreateErrorHandle(e.message));
}),
);
const handleOver = await lastValueFrom(result$);
return handleOver();
}
这是一个复杂的流,想想从 async-await 转向 rxjs, 其实挺复杂的,在一个函数里面完成众多,当然时最能体现 RxJS 处理业务的能力,当然这个 RxJS 可能还可以够简化,这里就不在探索了。
小结
有时候我们需要将RxJS 看成一门编程语言,因为他有自己的流,似乎与主流的方向格格不入,但是在具备响应式和函数式特点,风格统一。使用 RxJS 编程需要我们在原来过程式或者面向对象式方式,跳转出来。在 RxJS 的流中切换和计算。同时需要与原始 JS 数据结构与来行进行转换,满足显示需求。是否有更好的 RxJS 使用编程使用,可以交流一下。