原来 RxJS 是这样处理复杂数据流

一、本文适合对象

  • 对 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 使用编程使用,可以交流一下。

相关推荐
鸿蒙自习室1 分钟前
鸿蒙UI开发——组件滤镜效果
开发语言·前端·javascript
m0_748250748 分钟前
高性能Web网关:OpenResty 基础讲解
前端·openresty
前端没钱34 分钟前
从 Vue 迈向 React:平滑过渡与关键注意点全解析
前端·vue.js·react.js
NoneCoder39 分钟前
CSS系列(29)-- Scroll Snap详解
前端·css
无言非影43 分钟前
vtie项目中使用到了TailwindCSS,如何打包成一个单独的CSS文件(优化、压缩)
前端·css
.生产的驴1 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
我曾经是个程序员1 小时前
鸿蒙学习记录
开发语言·前端·javascript
顽疲1 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端
机器之心1 小时前
AAAI 2025|时间序列演进也是种扩散过程?基于移动自回归的时序扩散预测模型
人工智能·后端
羊小猪~~1 小时前
前端入门之VUE--ajax、vuex、router,最后的前端总结
前端·javascript·css·vue.js·vscode·ajax·html5