tauri播放器开发日记-1 - Shiina's Blog

tauri播放器开发日记-1

2023 年 5 月 5 日 - 04:24:26 发布
5.6K 字 19分钟
本作品采用 CC BY-NC-SA 4.0 进行许可。
本文距离首次发布已经过去了 584 天,请注意文章的时效性!

tauri播放器开发日记-1

写在前面

这是一个继承了之前 React 播放器的坑,当时计划要做桌面端,而且是一个完整项目

现在想想选择用 tauri 还是有点冲动了,啥都不清楚就开始做,行动力太高也有坏处

项目其实三月份就在计划了,当时也有了提交,但是为什么现在才写文章,是因为太忙了,四月份准备了蓝桥杯 web 比赛,还有算法的天梯杯,天梯杯就不吐槽了,不好评价;蓝桥杯会写一篇题解扔博客上面。就这一两周写完。后面跟着一个中软杯比赛,希望能两周搞定。

项目地址:https://github.com/QingXia-Ela/MonsterSirenPlayer/

如果不嫌弃可以先给个star :)

软件目标是先从本地播放器开始,集成塞壬唱片接口,然后为每个平台编写一个 rust 包,用于后端处理

或者是成本低点,直接 pkg 打包到应用里头,不过大小肯定不会少

然后就是网页端,如果本人有能力的话就会考虑兼容网页端

现在就先总结一些遇到的小坑,后面一直踩坑就会一直记录

顺便吐槽一下:rust 太抽象辣

环境搭建

首先是 windows 环境与 sdk:https://tauri.app/v1/guides/getting-started/prerequisites

这里就看一下我自己选择能用的依赖列表

依赖列表

然后是 rust 的安装,当时直接用的 winget 安装的,好像问题也不是很大

安装基本就是这些

打包处理

说一个点,打包的时候可能会遇到以下报错:

参照 issue 解决的思路是这样:在 tauri.conf.json 的 bundle 部分下这样设置:

"bundle": {
  "windows": {
    "wix": {
      "language": "zh-CN"
    }
  }
}

language 要设置为 zh-CN 才可以正常打包,如果还是不行就把 src-tauri/target 文件夹删了,重新编译一下依赖

坑-tauri

React 等调试工具在 webview2 中无法使用

webview2 中的浏览器调试控制台是不支持安装调试工具扩展的,这个问题在 2020 年就被诟病了:

https://github.com/MicrosoftEdge/WebView2Feedback/issues/98

后面经过一波折腾后发现 React 调试工具是有一个外置版本:

https://www.npmjs.com/package/react-devtools

安装过程会比较久,因为它本身是一个 electron 程序

随后就是下载安装,然后自己编写了一个启动脚本:

https://github.com/QingXia-Ela/MonsterSirenPlayer/blob/main/bin/dev.js

把 dev 启动指令改为通过 node 执行该脚本即可

还有一个解决方案是这样:https://github.com/MicrosoftEdge/WebView2Feedback/issues/190

似乎是通过远程链接调试,这样可以直接在浏览器环境中处理,也能使用所有的扩展

希望这个扩展问题能在未来得到解决

扩展跨域控制

跨域这个问题在浏览器是没办法处理的,但是在 tauri 跨域可以手动关闭,当然最好是只在能信任的 url 上进行跨域:

"tauri": {
  "allowlist": {
    "http": {
      "all": true,
      "request": true,
      "scope": [
        "https://monster-siren.hypergryph.com/api/**",
        "https://web.hycdn.cn/**"
      ]
    },
  }
}

详细见:https://tauri.app/v1/api/js/http#security

其次是重写请求方法,在这里我写了个跨平台请求处理:https://github.com/QingXia-Ela/MonsterSirenPlayer/blob/main/src/utils/request.ts

这个请求会根据不同平台执行不同请求方法

坑 - react

react-custom-scrollbars + virtuoso + DND 组合

DND - react-beautiful-dnd 组件,目前比较流行的拖拽组件,可扩展空间很大

这个算是困扰了我最久的一个问题,自定义滚动条在虚拟列表组件内不生效的问题很早也有过讨论:

https://github.com/petyosi/react-virtuoso/discussions/743

大家都没有给出一个相对较好的解决方案,之前有看过低版本 overlay-scrollbars + virtuoso 的解决方案,但是测试后仍然不生效,当时发现的问题核心是虚拟列表组件监听不到滚动视口的滚动事件。当时尝试过如下思路:

  • 数据重绘,改变引用,这个在 DND 存在的情况下是有效的,但是没了 DND 仍然没用,因为 id 没变
  • custom-scrollbars 的 renderview 改为 Virtuoso - 报错,找不到元素,原因未知
  • 用 Virtuoso 内置的 Scroller API - 同样监听不到滚动事件
  • 魔改源码强制暴露更新 API - 学习时间成本有点高,放弃了
  • 还有三四个记不太清楚了…

处理了 114514 天后,最终决定了如下方案:

  • Scrollbars 核心组件内部填充一个全空白的 div,高度与 Virtuoso 列表相同
  • Virtuoso 用绝对定位保持与 Scrollbars 相同的相对位置
  • 互相监听滚动事件并控制对方滚动位置,节流 33 毫秒

虽然不是最好但是能用,以后再想想有没有更好的处理方法

做的时候顺便把之前的答辩代码重构了一下,现在舒服多了

后面又发现在样式那里有个 bug:列表样式采用的是斑马纹,虚拟列表每次移动超过一个 item 就会更新,导致斑马纹样式会不断变化闪烁,很难受

这个 bug 会继续想办法处理

tauri DOM 布局偏移处理性能

起因是想尝试一下右键菜单,发现 mui 有个示例:https://mui.com/material-ui/react-menu/#context-menu

然后把代码直接复制到 tauri 里面用,出现了菜单关闭时附带半秒的卡顿,十分难受,体验直接下降一截

后面通过观察发现,他会直接在 body 内一把梭,全部的内容直接插入 body

这个在浏览器内几乎体验不出来,非常流畅,但是到了 tauri 直接痛苦面具

body插入

最后决定指定容器元素,避免插入 body 导致布局发生微小改变而造成的卡顿:

<BlackMenu
  open={contextMenu !== null}
  onClose={handleClose}
  anchorReference="anchorPosition"
  anchorPosition={
    contextMenu !== null
      ? { top: contextMenu.mouseY, left: contextMenu.mouseX }
      : undefined
  }
// prevent context menu change dom cause page choked in app
container={document.querySelector(`#BottomListContextMenuWrapper`)}
>
  <BlackMenuItem onClick={handleClose}>Copy</BlackMenuItem>
  <BlackMenuItem onClick={handleClose}>Print</BlackMenuItem>
  <BlackMenuItem onClick={handleClose}>Highlight</BlackMenuItem>
  <BlackMenuItem onClick={handleClose}>Email</BlackMenuItem>
</BlackMenu>
组件内插入

后面应该还有更好的性能提升方法,就是专门弄个空的 DOM 节点来给这些元素插入内容

不要在 DOM 节点中使用类似 Array.map 来构造节点

这个问题是从上一个问题引申出来的,因为在上一个问题以为是什么东西重复渲染占用了大量的资源,于是在 devtools 中找了一下,偶然发现了每次开关右键菜单都会导致 Scrollbars 内的内容更新,明明变量也没发生改变,但就是一直在触发更新

列表反复更新

下面是 devtools 截图,可以发现是 children 改变导致的:

列表反复更新

后面注意了一下,发现代码是这么写的:

items.length < 200 ? items.map(
  (d) => (
    <NormalListItem style={NormalListItemStyle} SmallScaleNum={0.99} key={d.id}>
      <SingleItem name={"Operation Pyrite"} author={"塞壬唱片-MSR"} album={"危机合约"} time={"01:14"} tags={d.tags} />
    </NormalListItem>
  )
) : null      

这也就意味着每次组件触发更新时,这里会返回一个全新的引用

所以我们要做的就是对其进行缓存,通过 useMemo 处理:

const ItemsNode = useMemo(() => {
  return items.map(
    (d) => (
      <NormalListItem style={NormalListItemStyle} SmallScaleNum={0.99} key={d.id}>
        <SingleItem name={"Operation Pyrite"} author={"塞壬唱片-MSR"} album={"危机合约"} time={"01:14"} tags={d.tags} />
      </NormalListItem>
    )
  )
}, [items])

顺便感叹一下,如果是 vue 会自动帮你优化,因为是静态编译,所以 vue 就只需要看 items 本身是否发生变化,而不是每次都重新触发渲染

优化前,可以注意到几乎每个节点都进行了一次 diff 算法:

优化前

优化后,舍去了多余的对比:

优化后
个人信息
avatar
Shiinafan
文章
45
标签
53
归档
4
导航