前言

最近一个月沉迷喜马拉雅无法自拔,听相声、段子、每日新闻,还有英语听力,摸鱼学习两不误。上班时候苦于没有桌面端,用网页版有些 bug,官方也不搞一个,只好自己动手了。样式参考了一下 Moon FM /t/555343,颜值还过得去,自我感觉挺好 ???

简介

Mob(モブ), 异能超能 100的男一号。

GitHub: zenghongtu/Mob

基于 Electron, Umi, Dva, Antd 构建

功能及 UI

目前实现的功能有这些:

  • 一个基本的音乐播放器
  • 每日必听
  • 推荐
  • 排行榜
  • 分类
  • 订阅
  • 听过
  • 下载声音
  • 搜索专辑

技术选型

技术栈:

  • Electron
  • Umi
  • Dva
  • Antd

之所以选择 Umi 是因为在之前项目中研究过其部分源码,开发体验感不错,而且 bug 也少。 还有一个原因是我在找模板的过程中,看到这个大佬的模板wangtianlun/umi-electron-typescript,就直接拿来用了,大大减少了我搭建开发环境的时间,在此表示感谢~

如果你对 Umi 和 Dva 不熟,墙裂建议去学一下,分分钟就可以上手,而且开发效率要提高的不要太多。

开发篇

React Hooks 使用问题

在开发中,所有组件、页面都是使用 React Hooks 进行开发的。而让我觉得最难以琢磨的一个 hooks 非 useEffect 莫属。

// ...
useEffect(() => {ipcRenderer.on("HOTKEY", handleGlobalShortcut);ipcRenderer.on("DOWNLOAD", handleDownloadStatus);return () => {ipcRenderer.removeListener("HOTKEY", handleGlobalShortcut);ipcRenderer.removeListener("DOWNLOAD", handleDownloadStatus);};
}, [volume]);
// ...
const handleGlobalShortcut = (e, hotkey) => {switch (hotkey) {case "nextTrack":handleNext();break;case "prevTrack":handlePrev();break;case "volumeUp":const volumeUp = volume > 0.95 ? 1 : volume + 0.05;handleVolume(volumeUp * 100);break;case "volumeDown":const volumeDown = volume < 0.05 ? 0 : volume - 0.05;handleVolume(volumeDown * 100);break;case "changePlayState":handlePlayPause();break;default:break;}
};
// ...
复制代码

为了减少渲染次数,我会设置了第二参数为 [volume],但这会导致一些出乎意料的情况,比如我触发了changePlayState,但却并没有得到意料中的值,这个时候设置为 [volume, playState] 就正常了。

原因很简单,因为playState不在依赖中,不会触发重新渲染

所以这条经验就是在使用 hook 遇到问题时,可以先试一下添加到`useEffect·中(如果有用到这个 hook 的话)

组件复用

先来看一下预览:

可以发现很多组件是相似的,如何提高他们的复用,这一个提高开发效率的途径。

在这个项目中我没有使用高阶组件,而是通过反正控制或者说是render props来进行复用,在组件的指定生命周期中进行调用。

共有三个组件在其他多个组件和页面中复用:

  • 页面内容加载组件
  • 专辑封面组件
  • 专辑列表组件

页面内容加载组件如下:

export interface Content<T, R> {render: (result: Result) => React.ReactNode;genRequestList: (params?: R[]) => Array<Promise<T>>;rspHandler: (rspArr: any, lastResult?: any) => Result;params?: R[];
}export default function({params, // api 的请求参数genRequestList, // 负责返回 api 请求列表,返回值会被`Content`调用请求数据,返回值给`rspHandler`rspHandler, // 处理请求返回的`Response`值,返回值给`render`render //  负责渲染结果,将值传递给`render`函数中的组件
}: Content<any, any>) {const [loading, setLoading] = useState(true);const [hasError, setError] = useState(false);const [result, setResult] = useState(null);useEffect(() => {(async () => {try {setLoading(true);setError(false);const rspArr = await Promise.all(genRequestList(params));setResult(rspHandler(rspArr, result));} catch (e) {setError(true);} finally {setLoading(false);}})();}, [params]);return (<div className={styles.contentWrap}>{loading && !result ? (<div className={styles.loading}><Loading /></div>) : hasError ? (<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />) : (render(result))}</div>);
}
复制代码

利用缓存提高体验度

对 axios 的 get 请求进行封装,对每个请求 url 生成唯一值,如果在白名单内,存入 session storage 中,默认过期时间是 3600s,在下次访问时,直接返回该值。

这样做的一个问题是无法获得最新数据,但对比体验感来说并不那么严重。

const request = ({ whitelist = [], expiry = DEFAULT_EXPIRY }) => ({...instance,get: async (url: string, config?: AxiosRequestConfig) => {if (config) {config.url = url;}const fingerprint = JSON.stringify(config || url);// 判断是否需要缓存const isNeedCache = !whitelist.length || whitelist.includes(url);// 生成唯一值const hashKey = hash.sha256().update(fingerprint).digest("hex");if (expiry !== 0) {const cached = sessionStorage.getItem(hashKey);const lastCachedTS: number = +sessionStorage.getItem(`${hashKey}:TS`);if (cached !== null && lastCachedTS !== null) {const age = (Date.now() - lastCachedTS) / 1000;// 如果没有过期,就直接返回该值if (age < expiry) {return JSON.parse(cached);}// 否则清除之前的旧值sessionStorage.removeItem(hashKey);sessionStorage.removeItem(`${hashKey}:TS`);}}const rsp = await instance.get(url, config);if (isNeedCache) {cacheRsp(rsp, hashKey);}return rsp;}
});export default request({ whitelist: [] });
复制代码

Flex justify-content: space-between 最后一行问题

在 flex 中设置justify-content: space-between后,在最后一行会出现让人不愉悦的情况。

对于这个问题,我的办法是通过计算然后填充空的div进去。

const DEFAULT_WIDTH = 130;
const DEFAULT_PAGE_COUNT = 130;
const DEFAULT_WINDOW_WIDTH = 1040;
export default function({siderWidth = SIDE_BAR_WIDTH,pageCount = DEFAULT_PAGE_COUNT,divWidth = DEFAULT_WIDTH
}) {const [fillCount, setFillCount] = useState(0);const handleResize = debounce(e => {let innerWidth: number;if (e) {innerWidth = e.target.innerWidth;}// 当前容器的宽度const containerWidth = innerWidth || DEFAULT_WINDOW_WIDTH - siderWidth;// 每一行可以放的个数const rowDivCount = Math.floor(containerWidth / divWidth);// 需要填充的个数const count = rowDivCount - (pageCount % rowDivCount);setFillCount(count);}, 100);useEffect(() => {handleResize();window.addEventListener("resize", handleResize);return () => {window.removeEventListener("resize", handleResize);};}, []);return (<>{fillCount? // 按照填充个数填进去Array.from({ length: fillCount }).map((_, idx) => {return (<divkey={idx}style={{ width: divWidth, height: 0 }}className={styles.filler}/>);}): null}</>);
}
复制代码

路由的前进与后退

umi或者说是react-router中,也只有memory-router可以判断是否可以前进或者后退。

只能自己记录一下 index,然后进行判断。

let lastHistoryLen = 0;
const NavBar = ({ history, isLogin }) => {const { length, action } = history;const [curIndx, setCurIndx] = useState(0);const [suggests, setSuggests] = useState(null);const [text, setText] = useState('');const [visible, setVisible] = useState(false);useEffect(() => {// 判断最后历史记录的长度是否大于当前历史记录长度,如果是的话,把 index 归零if (lastHistoryLen > length) {setCurIndx(0);}lastHistoryLen = length;});const fetchSuggests = debounce(async (kw) => {if (!kw) {setSuggests(null);return;}const {data: { result },}: { data: SuggestRspData } = await getSuggest({ kw });let suggests = [...result.albumResultList, ...result.queryResultList];if (suggests.length < 1) {suggests = null;}// todo (only support albumResult now)setSuggests(suggests);}, 200);// ...const handleArrowClick = (n) => {return () => {setCurIndx(curIndx + n);router.go(n);};};复制代码

如何登录

本来想着分析一下登录接口,但是这么做的话,如果还要加上扫码登录,要花不少时间。

于是乎想到了使用 webview 嵌入登录页面,在登录后,如果打开了个人页面就说明登录成功了。

const TARGET_URL = "www.ximalaya.com/passport/sync_set";
const COOKIE_URL = "https://www.ximalaya.com";
const WebView = ({ onLoadedSession }) => {const [isLoading, setLoading] = useState(true);useEffect(() => {const webview = document.querySelector("#xmlyWebView") as HTMLElement;const handleDOMReady = e => {if (webview.getURL().includes(TARGET_URL)) {// todo fix prevent redirecte.preventDefault();const { session } = webview.getWebContents();onLoadedSession(session, COOKIE_URL);webview.reload();}};const handleLoadCommit = () => {setLoading(true);};const handleDidFinishLoad = () => {setLoading(false);};webview.addEventListener("dom-ready", handleDOMReady);webview.addEventListener("load-commit", handleLoadCommit);webview.addEventListener("did-finish-load", handleDidFinishLoad);return () => {webview.removeEventListener("dom-ready", handleDOMReady);webview.removeEventListener("load-commit", handleLoadCommit);webview.removeEventListener("did-finish-load", handleDidFinishLoad);};}, []);const props = {id: "xmlyWebView",useragent:// tslint:disable-next-line:max-line-length"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36",src: `https://${TARGET_URL}`,style: { widht: "750px", height: "600px" }};return (<div><Spin tip="Loading..." spinning={isLoading}><webview {...props} /></Spin></div>);
};
复制代码

最后

希望这篇文章能对你有所帮助。

下载与体验

转载于:https://juejin.im/post/5cd917f6f265da03ac0d2af6

基于 Electron + React 的超高颜值喜马拉雅客户端 - Mob 诞生记相关推荐

  1. electron开发_基于Electron+React的跨平台应用程序基础开发框架

    介绍 Electron React Boilerplate是Github上超过12k+star的可扩展跨平台应用程序开发框架,Electron 是基于HTML+CSS+Javascript等 Web ...

  2. ElectronOCR:基于Electron+React+Tesseract的MACOS下的OCR工具

    Github Repo 地址 文章地址 MAXOS Darwin x64下载 笔者一直在MacOS上没找到太顺心的OCR工具,导致看书的时候很多东西只能手打,略烦.正好前段时间用了Tesseract, ...

  3. 【前端工具分享】Electron React Boilerplate(Electron+React项目模板,开箱即用)

    目的 桌面客户端开发,也已经是前端领域的一致分支了.跳出浏览器的我们,会获得更多能力. Electron,目前客户端开发最知名的框架,以VSCode为首的各种应用,都是基于他做的. React,最强大 ...

  4. 基于 electron 实现简单易用的抓包、mock 工具

    背景 经常我们要去看一些页面所发出的请求时,经常会用到 Charles 做为抓包工具来进行接口抓取,但一方面市面是很多抓包工具都是收费或者无法二次开发的.当前我们团队大多数用的也都是 Charles, ...

  5. 使用Squirrel创建基于Electron开发的Windows 应用安装包

    我们把自己开发的Electron应用发布之前,需要把app打包成简单的安装包,这样app更容易被获取,以此来发布我们的应用.我们可以参考Wix或其他的安装程序,但是对于Electron应用更好的打包程 ...

  6. Beaker:一个基于Electron的点对点Web浏览器

    Beaker是一个基于Electron.Chromium和Node.js的实验性.点对点Web浏览器.Beaker包含新的基于Dat的API,用于构建无主机应用程序,同时又保持与传统Web的兼容性. ...

  7. Today:基于 Electron 和 Vue.js 的 GTD 应用

    这是我的一个 side project.今天发布了第一个预览版本 v0.0.2,欢迎访问 GitHub 上面的 Repo 获取试用下载(目前仅为 Mac 用户提供 build),并提供你们的宝贵意见和 ...

  8. 基于Spring cloud Ribbon和Eureka实现客户端负载均衡

    前言 本案例将基于Spring cloud Ribbon和Eureka实现客户端负载均衡,其中Ribbon用于实现客户端负载均衡,Eureka主要是用于服务注册及发现: 传统的服务端负载均衡 常见的服 ...

  9. 【翻译】基于 Create React App路由4.0的异步组件加载(Code Splitting)

    基于 Create React App路由4.0的异步组件加载 本文章是一个额外的篇章,它可以在你的React app中,帮助加快初始的加载组件时间.当然这个操作不是完全必要的,但如果你好奇的话,请随 ...

最新文章

  1. 基于 Linux Bridge 的 Neutron 多平面网络实现原理
  2. java 独占锁_锁分类(独占锁、分拆锁、分离锁、分布式锁)
  3. Python稳基修炼的经典案例9(计算机二级、初学者必会turtle库例题)
  4. PHP踩坑:对象的引用
  5. java常见基础面试题
  6. 外研社php,外研社高中英语单词
  7. [工业互联-7]:工业控制电气自动化系统与主要元器件
  8. windows系统 cmd 下载python包的代理配置
  9. Blender 快捷键总结,一些子问题
  10. 维基百科怎么做_维基百科创建修改技巧分享!
  11. kafka数据保存时间问题与kafka的性能测试
  12. 银行管理系统java论文_基于java的银行账户管理系统的设计与实现毕业论文.doc
  13. 超级计算机中心建设方案,我校举办大连理工大学超算中心建设方案论证会
  14. 【论文排版术】学习笔记1
  15. html5页面弹窗,H5页面怎么设置弹窗
  16. 【npm】tunneling socket could not be established
  17. 【Hyperledger Fabric】学习笔记1—— 区块链介绍
  18. 《数据之美》读书笔记
  19. 200佳优秀的精美网页欣赏网站推荐(系列八)
  20. 基于jsp+mysql+Spring+SpringMVC+mybatis的ssm生鲜超市进销存管理系统

热门文章

  1. ie自动保存html,IE无法打开 本地保存的HTML文件,解决方法
  2. 问题解决:Weka打开csv文件出错
  3. linux下的I2c 和展锐8310下的I2c
  4. 北大计算机是理科还是工科,选北大还是清华?从理科和工科的差异说起!
  5. 雷塞卡回零,演示消息泵的用法
  6. 【GAMS与C++的交互】
  7. 百兆网和千兆网怎么接线
  8. 在eNSP模拟器上通过Dot1q终结子接口(单臂路由)实现VLAN间通信
  9. python自动出题_Python出题
  10. 对抗生成网络原理和作用