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

本文所有源码均在: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...

热门文章

专栏

相关推荐
崔庆才丨静觅12 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606112 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了12 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅12 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅13 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅13 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment13 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅14 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊14 小时前
jwt介绍
前端
爱敲代码的小鱼14 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax