搭建音乐播放器桌面应用--前端篇

强大的JavaScript似乎无所不能了,除了PC端的单页面实现,可以用RN或weex完成兼容Android端和iOS端的APP,也可以用Electron搭建兼容Mac或Windows的桌面应用了。

这篇文章主要讲一个播放器的从零开始的实现过程,包括前后端的架构和选型分析;采用前后端分离,前后端如何分工合作;数据的获取与存储;如何抓包。

源码

Musicer

最终效果

效果图

前端架构

  • React: 基于虚拟DOM实现单向响应式数据流的UI框架。
  • Redux: 提供单向,可预测的数据流。
  • Electron: 使用 JavaScript, HTML 和 CSS 等 Web 技术创建原生程序的框架。

    选型

    React

  • 基于v-dom的UI框架: 采用virtual dom render + diff进行必要的DOM渲染。这样的好处是我们可以采用一种比重置innerHtml 更高效的方法更新DOM元素。在过往我们写的DOM元素是需要调出DOM然后再对它操作,因此导致页面reflow或repaint,而react是所有元素置于v-dom,即完完全全在JavaScript中曲操作DOM,这样开销就小了很多。

  • 高度组件化:虽然前端应用复杂度越来越大,我们传统的做法是多个HTML和js文件,而新时代赋予了一种新兴的科技即组件。我们可以将通用或不通用的模块分成一个个组件,分别放在components和containers中,结合ES6或CommonJS的模块加载方案实现组件加载。

Electron

  • 跨平台: 兼容Mac, Windows 和 Linux
  • 浏览器兼容: PC端的前端攻城狮经常需要考虑浏览器的兼容性,而当我们把呈现在页面上的内容转移到应用上时,我们可以直接根据Chrome浏览器编程。
  • 快速上手: 这可以直接省去大量学习成本,只要搭建好基础框架,就可以像写页面一样去写应用。

Redux

  • 优秀之处: Redux是一个管理全局state的容器,它的优秀之处在于将所有状态从所有模块组件中抽身而出的状态集合,可以理解为每个组件中的一个变量或一个对象,如果没有Redux,我们可能需要通过React中state,props去层层传值,但redux提供给我们的是全局的状态,所有状态集合store,通知作用的action和执行官reducer。这样很好地减少数据耦合,我们可以站在一个地方观览所有数据。
  • 可恨之处: 每次添加新状态都需要在store,action,reducer添加新行为,这让代码看起来非常冗余,似乎写了特别多重复性代码,如果追求代码简洁,redux会让你泪奔~~o(>_<)o ~~

前端实现方案

思路

系统中的组件

组件

以上是播放器的组件结构,从Root入口开始。

  • Main:处理初始化状态,如登录状态
  • Home: 界面翻转切换逻辑,歌曲切换逻辑
  • Login,Account,Share,Lyric:提供切换界面

决定采用React就是看中其高度组件化的优势,这些组件从开始至今被更改了好几次,累加简化再累加再简化,抽离成目前的状态。我们本来可以在Home中处理所有逻辑,再将状态一一扔给action去通知再处理,而后来我在Home之前加入一个包围一个Main,这样做的目的是将初始化和进行中分成两个时间维度,我们可以在开始之前先预知这一次用户的状态,再呈现处理。

后面的四小组件也是功能组件,各司其职,如Login是用户登录界面,我们只在里面处理登录更改事物,再将最后的状态通知到action再做处理;同理Lyric是歌词组件,获取当前歌曲再做展示。
可在项目中/public/Components查看各个组件
组件

Main组件中的初始化

初始化
首先Main组件会查看缓存中是否有用户token信息存在,若无则将登录状态设为未登录;若有,则在前端获取信息查看信息是否过期,获取的信息如下:

1
2
3
4
5
6
7
"data": {
"access_token": "25a64943770fca55d46995",
"douban_user_name": "abigaleyu",
"douban_user_id": "1688842",
"expires_in": 7775999,
"refresh_token": "5f2388c502a0799f2f9"
}

以上expires_in即为从获取信息时间的7775999秒后将会过期,因此我们需要将当下时间和有效期做下对比。
若已过期,将缓存信息清除后将登录状态设为未登录,否则则通过登录,然后将本次登录状态设为true,在之后如需获取个人信息,点赞歌词等个人相关操作时,我们将需要用到该token(即个人身份令牌)去操作。

登录逻辑

登录逻辑

这是当用户切入登录界面时,我们已在Main获取到登录状态,它将通知到全局,而我们又可以在全局获取到该状态,当为true时,则直接用token去访问后端要些个人信息(用户账号,用户ID,用户头像等),当登录状态为false时,则进入以上流程,用户凭输入的用户名和密码去请求API(界面出现用户名和密码的输入框),通过时则返回token,不通过时会出现多种情况:登录多次错误需要验证码;账号错误;密码错误;账号不存在等,我们这里简化成两种状态:需要验证码(界面会多出验证码输入框和验证码图片)和 其他不需要验证码的情况。当再次输入正确或验证码也通过时,则获取到token。注意以上流程我们只有一个目的:获取token。这是一个身份令牌,我们只有拥有它就有权限去干个人相关的事情。至于通过就获取到个人信息
,我们和用户操作归到一起,都是只拿token去操作。

获取个人信息

获取个人信息
这便是拿到token之后的事情,拿到token就意味着已登录完毕,如没获取到token,会返回到登录界面去要求用户登录。
当我们手持token时,先会在前端验证token的有效期,这样做的意义是避免网络开销,前后端验证开销,如果不这么做,当我们拿着已经过期的token去向后端要个人信息时,后端返回告诉我们token无效的,前端再告诉用户需要重新登录。因此这一步会显得很有必要。
若前端验证为有效期内的token时,这才向后端获取有效的个人信息,当然,这个token后端也会做一次判断,如果前端是乱传过去的呢~

快速创建示例仓库并安装运行

  • 克隆库
    git clone https://github.com/electron/electron-quick-start
  • 进入路径
    cd electron-quick-start
  • 安装依赖库并启动
    npm install && npm start

启动入口文件

在package.json中可以看到启动的入口文件为./public/main.js

代码均为文件节选代码块,详见示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let mainWindow
/**
* createWindow 创建前端界面窗口。
*/
function createWindow() {
mainWindow = new BrowserWindow({ width: 195, height: 230, frame: false,transparent: true})
mainWindow.loadURL(url.format({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file:',
slashes: true
}))
}
app.on('ready', createWindow)
const PORT = process.env.PORT || 8082;
const server = express();
server.listen(PORT, () => {
console.log(`The server has been set up at 0.0.0.0:${PORT}`);
});

入口文件主要实现

  • 窗口的创建,关闭等事件
  • 启动后端服务

前端窗口我们重点关注窗口内业务代码实现,至于窗口如何与PC交互,Electron已经帮我们解决了。

以上示例中 BrowserWindow 类主要定义窗口的基本元素,width和height定义宽高。frame定义是否需要边框。transparent定义背景是否透明。

示例中 mainWindow.loadURL方法定义入口UI模板index.html

Electron框架结合React

Electron提供平台框架,相当于浏览器。
添加react库

安装React npm install react

新建脚本entry.js,实现前端脚本逻辑。这里我们使用webpack打包并嵌入入口UI模板index.html

数据存储

前端跟后端不一样,不能有DB的存在。但我们可以造伪DB,可以用cookie,localStorage和sessionStorage,或者将数据存在本地文件夹,本例中采用localStorage

踩过的小坑

  • 父子组件通信:父组件向子组件通信时,可以通过子组件属性传递,而子组件向父组件传递时,可调用父级方法。
  • 组件中获取video属性:嵌入的音乐播放器video,但比如暂停,获取歌曲长度等,在虚拟DOM都不可直接获取,但可以通过video的各个方法,如onTimeUpdate,根据时间的变化执行函数去获取当前位置。