第三章 实战案例:构建一个音乐播放器

本文所有源码均在:github.com/Sunny-117/e...

本文收录在《Electron桌面客户端应用程序开发入门到原理》掘金专栏

本文介绍

同样是通过不断迭代的方式,一步一步完善音乐播放器。

  • 原生网页版音乐播放器
  • AmplitudeJS迭代音乐播放器
  • Electron版本音乐播放器
  • Vite、Vue、Electron搭建一个项目,继续迭代音乐播放器
  • Electron-Vite 迭代音乐播放器

AmplitudeJS

AmplitudeJS 是一个轻量级和开源的 JavaScript 库,它建立在 HTML5 音频 API 之上,提供了一套易于使用的接口来管理音频播放、播放列表、音量控制等。

官网地址:521dimensions.com/open-source...

AmplitudeJS 具有以下特点:

  1. 无依赖:AmplitudeJS 不依赖于任何其他 JavaScript 库或框架,这使得它在任何项目中都很容易集成。
  2. 自定义 UI:开发者可以自由设计和实现音频播放器的用户界面,使其与应用的风格一致。AmplitudeJS 不强加任何样式,它只提供功能性的接口。
  3. 丰富的 API:AmplitudeJS 提供了广泛的 API,使得开发者可以通过编程的方式控制播放器的行为,如播放、暂停、跳转到特定时间点等。
  4. 播放列表和歌曲管理:它支持创建和管理多个播放列表,以及播放列表内歌曲的动态添加和删除。

快速上手案例

下面是一个关于 AmplitudeJS 的一个快速入门案例:

js 复制代码
// index.js
Amplitude.init({
  songs: [
    {
      name: "Gotta Have You",
      url: "./music/Gotta Have You.mp3",
    },
    {
      name: "K歌之王",
      url: "./music/K歌之王 - 陈奕迅.mp3",
    },
    // ...
  ],
  volume: 50,
});

首先需要在 html 中引入 amplitude.js。引入之后会提供一个 Amplitude 的对象,上面会有大量的方法,这里我们暂时只用到了 init 方法。该方法用于初始化我们要播放的歌曲,以及一开始的音量大小。

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>Music player</h1>
    <button class="amplitude-play">播放</button>
    <button class="amplitude-pause">暂停</button>
    <div class="amplitude-play-pause">播放/暂停</div>
    <script src="./amplitude.js"></script>
    <script src="./index.js"></script>
  </body>
</html>

接下来就是 html 部分的代码,首先需要引入 amplitude.js 以及刚才我们自己所写的 index.js。

之后在 html 元素上挂相应的样式类就可以了,例如挂上一个 amplitude-play 类,那么这个元素就是一个"播放"按钮,挂上一个 amplitude-pause 类,那么这个元素就是一个"暂停"按钮。这使得我们不再需要去编写和 audio 元素相关的"播放"或者"暂停"这部分逻辑。而样式部分,则完全交给用户,让用户来自定义。

另外,在 AmplitudeJS 中定义了 4 个不同的级别来控制音频的播放:

  • 全局(Global):全局元素控制任何正在播放的音频,无论范围如何。
html 复制代码
<!-- 全局播放/暂停按钮 -->
<button class="amplitude-play-pause" id="global-play-pause"></button>
  • 播放列表(Playlist):播放列表级别的元素只影响特定播放列表中的音频。
html 复制代码
<!-- 特定播放列表的播放/暂停按钮 -->
<button class="amplitude-play-pause" data-amplitude-playlist="playlist_key" id="playlist-play-pause"></button>
  • 歌曲(Song):歌曲级别的元素仅影响或显示单个歌曲,不考虑它是否属于某个播放列表。
html 复制代码
<!-- 控制特定歌曲的播放/暂停按钮 -->
<button class="amplitude-play-pause" data-amplitude-song-index="song_index" id="song-play-pause"></button>
  • 播放列表中的歌曲(Song In Playlist):这些元素影响或显示播放列表中的特定歌曲。
html 复制代码
<!-- 控制播放列表中特定歌曲的播放/暂停按钮 -->
<button class="amplitude-play-pause" data-amplitude-song-index="song_index_in_playlist" data-amplitude-playlist="playlist_key" id="song-in-playlist-play-pause"></button>

常用类记录

  • amplitude-prev:上一曲

  • amplitude-next:下一曲

  • amplitude-play-pause:播放和暂停

  • amplitude-current-minutes:当前播放的分钟数

  • amplitude-current-seconds:当前播放的秒数

  • amplitude-duration-minutes:总的分钟数

  • amplitude-duration-seconds:除开总分钟数后的剩余秒数

  • amplitude-song-slider:播放进度条

  • amplitude-mute:静音操作

  • amplitude-volume-slider:控制音量大小

  • data-amplitude-song-info:获取歌曲信息,这个歌曲信息来源于在使用 Amplitude.init 方法初始化歌曲时传入的歌曲信息

    • data-amplitude-song-info="name" :就是获取歌曲的名称

    集成现代框架

这里我们会集成两个东西:

  • Vue3
  • Vite

首先第一步,我们需要使用 Vite 来搭建一个基于 Vue 的项目。命令如下:

bash 复制代码
npm create vite@latest <项目名> -- --template vue

项目搭建完毕后,接下来需要安装 electron:

bash 复制代码
npm install electron -D

之后,我们需要创建我们的主进程代码,代码如下:

js 复制代码
// src/main/mainEntry.js
// 和主进程相关的代码
import { app, BrowserWindow } from "electron";

let mainWindow = null; // 存储窗口实例

app.whenReady().then(() => {
  mainWindow = new BrowserWindow({});
  mainWindow.loadURL(process.argv[2]);
});

process.argv 拿到的是一个数组,数组里面会包含启动 Node.js 进程时传递给它的命令行参数。

  • process.argv[0] :Node.js 的路径
  • process.argv[1] :正在执行的 JavaScript 文件的路径
  • process.argv[2] :从 2 开始,也就是数组的第三项开始,蚕食实际命令行传递给这个脚本的第一个实际参数,该参数回头会对应一个 URL

开发 Vite 插件

接下来,我们需要针对 Vite 编写一个插件:

js 复制代码
import esbuild from "esbuild";
import { spawn } from "child_process";
import electron from "electron";

export const devPlugin = () => {
  return {
    name: "dev-plugin", // 插件的名称
    // 一个异步的方法,用于配置服务器的
    // 接收一个参数,该参数就是 Vite 开发服务器的实例
    async configureServer(server) {
      // 首先第一步,咱们需要使用 esbuild 去同步的构建项目
      esbuild.buildSync({
        entryPoints: ["./src/main/mainEntry.js"], // 对象项目的入口文件
        bundle: true, //  启用打包,将依赖一起打包为一个文件
        platform: "node", // 指定平台为 node,主要是为了 Electorn 主进程服务
        format: "esm", // 模块的格式
        outfile: "./dist/mainEntry.js", // 输出文件的路径
        external: ["electron"], // 外部依赖,避免被打包进去
      });
      // 接下来,我们需要监听服务器的 listening 事件
      // 这个 listening 事件会在服务器开始监听端口时触发
      server.httpServer.listen("listening", () => {
        // 当触发 listening 事件的时候,我们需要启动 electron 进程

        // 获取服务器的地址信息,包括 IP 和端口
        const addressInfo = server.httpServer.address();
        // 构造服务器的 HTTP 地址字符串
        const httpAddress = `http://${addressInfo.address}:${addressInfo.port}`;

        // 启动 electron 进程
        const electronProcess = spawn(
          electron,
          ["./dist/mainEntry.js", httpAddress],
          {
            cwd: process.cwd(), // 子进程 electron 当前的工作目录
            stdio: "inherit", // 继承父进程的标准输入输出
          }
        );

        // 监听 electron 的 close 事件
        electronProcess.on("close", () => {
          server.close(); // 关闭 Vite 开发服务器
          process.exit(); // 退出当前进程
        });
      });
    },
  };
};

这里介绍一下关于 spawn。这个 spawn 是 child_process 模块里面的一个方法,该方法用于异步的创建一个新的子进程。

spawn 方法接收这么一些参数:

  • command:要运行的命令
  • args:命令的参数列表,是一个字符串数组
  • options:可选的配置对象,可以配置:
    • cwd:子进程当前的工作目录
    • env:环境变量键值对
    • shell:对应的是一个布尔值或者字符串
      • 如果是布尔值,例如是 true ,代表在 shell 中运行命令
      • 如果是字符串,代表的就是具体的要执行的 shell 命令

插件编写完毕之后,接下来就是使用插件:

js 复制代码
// vite.config.js

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { devPlugin } from "./plugins/devPlugin";

// https://vitejs.dev/config/
export default defineConfig({
  // 使用刚才我们所编写的插件
  plugins: [devPlugin(), vue()],
});

集成现代框架

这里我们会集成两个东西:

  • Vue3
  • Vite

首先第一步,我们需要使用 Vite 来搭建一个基于 Vue 的项目。命令如下:

bash 复制代码
npm create vite@latest <项目名> -- --template vue

项目搭建完毕后,接下来需要安装 electron:

bash 复制代码
npm install electron -D

之后,我们需要创建我们的主进程代码,代码如下:

js 复制代码
// src/main/mainEntry.js
// 和主进程相关的代码
import { app, BrowserWindow } from "electron";

let mainWindow = null; // 存储窗口实例

app.whenReady().then(() => {
  mainWindow = new BrowserWindow({});
  mainWindow.loadURL(process.argv[2]);
});

process.argv 拿到的是一个数组,数组里面会包含启动 Node.js 进程时传递给它的命令行参数。

  • process.argv[0] :Node.js 的路径
  • process.argv[1] :正在执行的 JavaScript 文件的路径
  • process.argv[2] :从 2 开始,也就是数组的第三项开始,蚕食实际命令行传递给这个脚本的第一个实际参数,该参数回头会对应一个 URL

开发 Vite 插件

接下来,我们需要针对 Vite 编写一个插件:

js 复制代码
import esbuild from "esbuild";
import { spawn } from "child_process";
import electron from "electron";

export const devPlugin = () => {
  return {
    name: "dev-plugin", // 插件的名称
    // 一个异步的方法,用于配置服务器的
    // 接收一个参数,该参数就是 Vite 开发服务器的实例
    async configureServer(server) {
      // 首先第一步,咱们需要使用 esbuild 去同步的构建项目
      esbuild.buildSync({
        entryPoints: ["./src/main/mainEntry.js"], // 对象项目的入口文件
        bundle: true, //  启用打包,将依赖一起打包为一个文件
        platform: "node", // 指定平台为 node,主要是为了 Electorn 主进程服务
        format: "esm", // 模块的格式
        outfile: "./dist/mainEntry.js", // 输出文件的路径
        external: ["electron"], // 外部依赖,避免被打包进去
      });
      // 接下来,我们需要监听服务器的 listening 事件
      // 这个 listening 事件会在服务器开始监听端口时触发
      server.httpServer.listen("listening", () => {
        // 当触发 listening 事件的时候,我们需要启动 electron 进程

        // 获取服务器的地址信息,包括 IP 和端口
        const addressInfo = server.httpServer.address();
        // 构造服务器的 HTTP 地址字符串
        const httpAddress = `http://${addressInfo.address}:${addressInfo.port}`;

        // 启动 electron 进程
        const electronProcess = spawn(
          electron,
          ["./dist/mainEntry.js", httpAddress],
          {
            cwd: process.cwd(), // 子进程 electron 当前的工作目录
            stdio: "inherit", // 继承父进程的标准输入输出
          }
        );

        // 监听 electron 的 close 事件
        electronProcess.on("close", () => {
          server.close(); // 关闭 Vite 开发服务器
          process.exit(); // 退出当前进程
        });
      });
    },
  };
};

这里介绍一下关于 spawn。这个 spawn 是 child_process 模块里面的一个方法,该方法用于异步的创建一个新的子进程。

spawn 方法接收这么一些参数:

  • command:要运行的命令
  • args:命令的参数列表,是一个字符串数组
  • options:可选的配置对象,可以配置:
    • cwd:子进程当前的工作目录
    • env:环境变量键值对
    • shell:对应的是一个布尔值或者字符串
      • 如果是布尔值,例如是 true ,代表在 shell 中运行命令
      • 如果是字符串,代表的就是具体的要执行的 shell 命令

插件编写完毕之后,接下来就是使用插件:

js 复制代码
// vite.config.js

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { devPlugin } from "./plugins/devPlugin";

// https://vitejs.dev/config/
export default defineConfig({
  // 使用刚才我们所编写的插件
  plugins: [devPlugin(), vue()],
});

目前,我们已经成功集成了 electron、vite 以及 vue 这几个模块。但是现在存在一个问题,就是在**渲染进程**下面是无法使用 Node.js 的模块以及 electron 自身的模块,即便我们在主进程中创建窗口的时候,已经明确的指明了要集成 node.js 以及关闭上下文隔离,在渲染进程中仍然无法使用。究其原意,是因为 vite 主动屏蔽了这些模块,如果开发者强行要引入这些所屏蔽的模块,那么就会出现诸如下面的错误:

rust 复制代码
Module "xxxx" has been externalized for browser compatibility and cannot be accessed in client code.

要解决这个问题,我们就需要安装一个插件:vite-plugin-optimizer

该插件会为你创建一个临时的目录:node_modules.vite-plugin-optimizer

然后会将类似于:

js 复制代码
const fs = require('fs'); export {fs as default}

这样的代码写入到临时目录的 fs.js 文件中。

之前我们在渲染进程里面执行这样的代码 import fs from "fs"; 是找不到的,但是现在执行这样的代码的时候,就会请求临时目录下的 fs.js 模块,从而达到了在渲染进程中引入 Node.js 内置模块的目录。

接下来在 vite 中新增一个插件,叫做 getReplacer,对应的代码如下:

js 复制代码
export const getReplacer = () => {
  // 在这个插件里面,我们主要要做的事情就是替换工作
  // 这里的替换工作包含两个方面:
  // 1. Node.js 常见的模块替换,比如 path、fs、os 等
  // 2. Electron 相关的内置模块,比如 clipboard,ipcRenderer 等

  // 该数组存放了一些 Node.js 下常用模块
  let externalModels = [
    "os",
    "fs",
    "path",
    "events",
    "child_process",
    "crypto",
    "http",
    "buffer",
    "url",
    "better-sqlite3",
    "knex",
  ];
  // 该对象用于存储最终的替换结果
  let result = {};
  for (let item of externalModels) {
    result[item] = () => ({
      find: new RegExp(`^${item}$`),
      code: `const ${item} = require('${item}'); export { ${item} as default }`,
    });
  }

  // 处理 electron 对应的模块,处理的思路和上面的 node.js 的处理思路是一样。
  result["electron"] = () => {
    let electronModules = [
      "clipboard",
      "ipcRenderer",
      "nativeImage",
      "shell",
      "webFrame",
    ].join(",");
    return {
      find: new RegExp(`^electron$`), // 使用该正则去匹配 electron 模块
      code: `const { ${electronModules} } = require('electron'); export { ${electronModules} }`, // 要生成的代码片段
    };
  };

  return result;
};

该方法会返回一个对象,该对象类似于:

js 复制代码
{
  electron: `const { ipcRenderer } = require('electron'); export { ipcRenderer };`,
  fs: () => ({
    find: /^(node:)?fs$/,
    code: `const fs = require('fs'); export { fs as default }`;
  }),
}

最终,我们在 vite.config.js 中使用该插件:

js 复制代码
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { devPlugin, getReplacer } from "./plugins/devPlugin";
import optimizer from "vite-plugin-optimizer";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [optimizer(getReplacer()), devPlugin(), vue()],
});

这样就大功告成了,可以在渲染进程中使用 node.js 以及 electron 内置的模块了。

electron-vite介绍

前面我们从零开始使用 vite + vue3 搭建了一个 electron 的开发环境,并且迭代了前面我们所写的音乐播放器,这一节课给大家介绍一个比较有名的脚手架:electron-vite

地址:cn.electron-vite.org/

我们直接使用如下的命令搭建一个项目出来:

bash 复制代码
npm create @quick-start/electron

具体操作如下图所示:

之后安装依赖,然后 npm run dev 把项目跑起来即可。

目录结构

到目前为止都很轻松,接下来我们需要熟悉这个项目,那么就从项目的目录结构开始熟悉。

这里我们主要关注这么几个目录

  • src 目录:这是我们主要的开发目录

    • main:主进程相关代码

    • preload:预加载脚本

    • renderer:渲染进程相关代码,使用 vue 相关技术

  • build 目录:构建后的目录,存放构建后的文件

  • out 目录:打包后的目录,打包后的文件就存放于此目录中,electron 实际上加载的是此目录里面的内容

  • resources 目录:公共资源目录,如果你有图标、可执行程序、wasm 文件等资源,可以将它们放在这个目录中。

    • 公共目录中的所有资源都不会复制到输出目录。所以在打包 app 的时候,公共目录应该一起打包。
    • 渲染进程中的公共资源处理不同于主进程和预加载脚本。
      • 默认情况下,渲染进程的工作目录位于 src/renderer,因此需要在该目录下创建公共资源目录。默认的公共目录名为 public,也可以通过 renderer.build.publicDir 指定。
      • 渲染进程的公共资源将被复制到输出目录。
  • electron-builder.yml 文件:和打包相关的配置文件,里面配置了不同操作系统,打包成不同产物的配置

热加载

很多时候,我们希望主进程或预加载脚本模块发生变化时,能够快速重新构建并重启 Electron 程序。

使用 CLI 选项的 -w 或者 --watch 即可,这是首选方式,它更加灵活。

json 复制代码
"scripts": {
  ...
  "dev": "electron-vite dev --watch",
  ...
},

本文所有源码均在:github.com/Sunny-117/e...

「❤️ 感谢大家」

如果你觉得这篇内容对你挺有有帮助的话: 点赞支持下吧,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。觉得不错的话,也可以阅读 Sunny 近期梳理的文章(感谢掘友的鼓励与支持 🌹🌹🌹):

我的博客:

Github: https://github.com/sunny-117/

前端八股文题库: sunny-117.github.io/blog/

前端面试手写题库: github.com/Sunny-117/j...

手写前端库源码教程: sunny-117.github.io/mini-anythi...

热门文章

专栏

相关推荐
麒麟而非淇淋32 分钟前
AJAX 入门 day1
前端·javascript·ajax
2401_8581205334 分钟前
深入理解MATLAB中的事件处理机制
前端·javascript·matlab
阿树梢39 分钟前
【Vue】VueRouter路由
前端·javascript·vue.js
随笔写2 小时前
vue使用关于speak-tss插件的详细介绍
前端·javascript·vue.js
史努比.2 小时前
redis群集三种模式:主从复制、哨兵、集群
前端·bootstrap·html
快乐牌刀片882 小时前
web - JavaScript
开发语言·前端·javascript
miao_zz3 小时前
基于HTML5的下拉刷新效果
前端·html·html5
Zd083 小时前
14.其他流(下篇)
java·前端·数据库
藤原拓远3 小时前
JAVAWeb-XML-Tomcat(纯小白下载安装调试教程)-HTTP
前端·firefox
重生之我在20年代敲代码3 小时前
HTML讲解(一)body部分
服务器·前端·html