1. 程式人生 > >基於Electron + nodejs + 小程式 實現彈幕小工具(上篇)

基於Electron + nodejs + 小程式 實現彈幕小工具(上篇)

前言

上一篇文章,大概講述我們即將要做的彈幕小工具是什麼樣的,將使用什麼樣的技術。那麼,從這一篇開始,我們將一步步把想法落地成程式碼。本文,我們將使用Electron實現接收端,讓我們的彈幕飛起來。

效果圖

如上圖所示,把放映PPT的同時,使用者可以通過掃描小程式二維碼,實時發表自己的想法,達到互動的效果。

Electron基本介紹

如果你已經對Electron有了一定的瞭解,可跳過這部分的內容。

Electron是由Github開發,用HTML,CSS和JavaScript來構建跨平臺桌面應用程式的一個開源庫。 Electron通過將Chromium和Node.js合併到同一個執行時環境中,並將其打包為Mac,Windows和Linux系統下的應用來實現這一目的。

在Electron中,主程序和渲染程序是一個很重要的概念。

主程序和渲染程序

一個程序是計算機程式執行中的一個例項。 Electron 應用同時使用了 main(主程序) 和一個或者多個 rendere(渲染程序) 來執行多個程式。

在 Node.js 和 Electron 裡面,每個執行的程序包含一個 process物件。 這個物件作為一個全域性的提供當前程序的相關資訊和操作方法。 作為一個全域性變數,它在應用內能夠不用 require() 來隨時取到。

主程序

主程序,通常是名為main.js 的檔案,是每個 Electron 應用的入口檔案。它控制著整個 App 的生命週期,從開啟到關閉。 它也管理著系統原生元素比如選單,選單欄,Dock 欄,托盤等。 主程序負責建立 APP 的每個渲染程序。而且整個 Node API 都整合在裡面。

每個 app 的主程序檔案都定義在 package.json 中的 main 屬性當中。這也是為什麼 electron. 能夠知道應該使用哪個檔案來啟動。

在Chromium中, 這個程序被稱為 "瀏覽器程序"。它在Electron被重新命名, 以避免與渲染器程序混淆。

渲染程序

渲染程序是你的應用內的一個瀏覽器視窗。與主程序不同的是,它能夠同時存在多個而且執行在不一樣的程序。而且它們也能夠被隱藏。

在通常的瀏覽器內,網頁通常執行在一個沙盒的環境擋住並且不能夠使用原生的資源。 然而 Electron 的使用者在 Node.js 的 API 支援下可以在頁面中和作業系統進行一些低級別的互動。

大體架構

從上面的效果圖可以看出,介面主要由彈幕介面和小程式二維碼組成。

結合上面說的主程序和渲染程序,我們可以將彈幕和二維碼分別放在兩個渲染程序中,主程序和nodejs服務端進行websocket通訊,並將接收到的資料分發到對應的渲染程序。

快速開始

官方有個快速開始的示例,我們可以通過這個示例來大概瞭解一下整個應用是怎麼跑的。

# 克隆示例專案的倉庫
$ git clone https://github.com/electron/electron-quick-start

# 進入這個倉庫
$ cd electron-quick-start

# 安裝依賴並執行
$ npm install && npm start
複製程式碼

我們在package.json可以看到,main屬性的值為main.js,沒錯,那就是主程序的入口檔案了。在這個入口檔案中,通過BrowserWindow建立了一個瀏覽器視窗,並載入了一個html檔案。是的,這就是所說的渲染程序。

這樣看來,我們要做的事情其實還是很簡單的,基本上和我們寫靜態頁面一樣。建立瀏覽器視窗的程式碼寫好之後,剩下的就是寫靜態頁面了。當然,還需要考慮資料通訊的方案。

開始開發

建立瀏覽器視窗

在主程序中,通過API BrowserWindow 建立兩個渲染程序,分別展示彈幕主介面和二維碼。

function createMainWindow() {
    mainWindow = new BrowserWindow({
        width: 1920,
        height: 1080,
        transparent: true,
        frame: false,
        resizable: false,
        alwaysOnTop: true,
        center: true,
        skipTaskbar: true,
        autoHideMenuBar: true,
        focusable: false
    });
    mainWindow.setAlwaysOnTop(true, 'pop-up-menu'); //一定要這樣設定 要不然在mac下全屏播放PPT的時候看不到

    mainWindow.maximize();//視窗最大化
    mainWindow.setIgnoreMouseEvents(true); //點選穿透
    mainWindow.loadURL(`file://${__dirname}/app/index.html`);
}

function createQrcodeWindow() {
    qrcodeWindow = new BrowserWindow({
        width: 200,
        height: 200,
        transparent: true,
        frame: false,
        resizable: false,
        minimizable: false,
        maximizable: false,
        alwaysOnTop: true,
        center: true,
        skipTaskbar: true,
        autoHideMenuBar: true
    });
    qrcodeWindow.setAlwaysOnTop(true, 'pop-up-menu'); //一定要這樣設定 要不然在mac下全屏播放PPT的時候看不到

    qrcodeWindow.loadURL(`file://${__dirname}/app/qrcode.html`);
}

複製程式碼

對於建立視窗的配置項,還是比較多的,大家可以大概通讀一遍官方文件,知道到底能做些什麼。

由於我們的彈幕是一直在最上面的,而且不能影響下層的操作,所以我們設定了透明、無邊框、且可忽略了滑鼠事件(點選可穿透),這樣在彈幕的同時,還可以看到下層視窗並進行相關操作。此外,我們還設定了禁止放大縮小,而且將彈幕視窗最大化。

系統托盤

由於不希望窗口出現在工作列裡,畢竟彈幕一直執行著,放在工作列中,佔地方且礙眼,我們更希望它出現在系統托盤中,如下圖:

上圖為Mac效果,Windows會出現在右下角。

我們可以通過API Tray設定系統托盤。

function initTrayMenu() {
    let iconPath = path.join(__dirname, 'ico/favicon.ico');
    if (os.type() === "Darwin") {
        iconPath = path.join(__dirname, 'ico/favicon.png');
    }
    const nimage = nativeImage.createFromPath(iconPath);
    tray = new Tray(nimage);
    tray.setToolTip('彈幕');
    const contextMenu = Menu.buildFromTemplate([
        {
            label: '顯示彈幕',
            type: 'radio',
            click: showMainWindow
        },
        {
            label: '關閉彈幕',
            type: 'radio',
            click: hideMainWindow
        },
        {
            type: 'separator'
        },
        {
            label: '顯示二維碼',
            type: 'radio',
            click: showQrcodeWindow
        },
        {
            label: '隱藏二維碼',
            type: 'radio',
            click: hideQrcodeWindow
        },
        {
            type: 'separator'
        },
        {
            label: '退出',
            type: 'normal',
            click: function () {
                app.quit();
            }
        }
    ]);
    tray.setContextMenu(contextMenu); //設定選單
    tray.on('click', handleToggleShowMainWindow);
}
複製程式碼

與nodejs服務端通訊

在我們的設計中,接受端和服務端之間的通訊,採用websocket協議。所以,除了建立渲染程序之外,主程序還有一個很重要的任務,就是和服務端建立websocket連線。

我們選擇使用ws模組來快捷建立連線:

const ws = require('ws');
const Socket = new ws('wss://danmu.xxx.com');//引數為socket服務地址
Socket.on('open', function open() {
    //在初始化的時候,傳送初始化訊息,並帶上接收客戶端ID,獲取小程式二維碼
    const initData = {
        type: 'INIT',
        clientId: 'xxxx' //客戶端唯一ID
    }
    Socket.send(JSON.stringify(initData));
});

Socket.on('message', function incoming(data) {
    // 對收到的資料進行分發
    try {
        const received_msg = JSON.parse(data);
        if (received_msg.type === 'qrcode') {
            qrcodeWindow.webContents.send('qrcodeBase64', received_msg.data);
        }else{
            mainWindow.webContents.send('new-message', received_msg);
        }
    } catch (error) {
        console.log(error);
    }
});

複製程式碼

在和伺服器建立連線之後,我們傳送了初始化的訊息,我們約定,服務端收到初始化訊息之後,會根據clientId生成帶引數的小程式二維碼,並將二維碼的base64資料返回來。所以,我們接收到新訊息的時候,對訊息型別進行判斷並轉發到對應的渲染程序。

上面提到,需要提供一個唯一的客戶端ID傳給nodejs服務端,用於生成唯一的小程式二維碼。我們可以使用以下方法生成:

function generateUUID() {
    return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
}
// 大家不妨思考一下,為什麼可以使用這個方法生成UUID呢?
複製程式碼

此外,我們希望第一次使用這個彈幕應用之後,這個ID就一直不變,畢竟二維碼變來變去,對使用者是很不友好的。所以我們需要將這個ID寫在本地檔案中。偷懶,我們也可以使用別人寫好的模組electron-store,原理就是把需要儲存在本地的資訊寫在一個json檔案中,各種特殊的路徑可以通過app.getPath(name)來獲取。

如app.getPath("appData")可以獲取當前使用者的應用資料資料夾,預設對應:

  • Windows:%APPDATA%
  • Linux:$XDG_CONFIG_HOME or ~/.config
  • macOS: ~/Library/Application Support

其他更多的路徑可檢視官方文件。

主程序和渲染程序之間通訊

這裡就要講到ipcMain、ipcRenderer以及webContents了,他們都是EventEmitter類的一個例項。

ipcMain: 當在主程序中使用時,它處理從渲染程序傳送出來的非同步和同步資訊。 從渲染程序傳送的訊息將被髮送到該模組。

ipcRenderer:你可以使用它提供的一些方法從渲染程序傳送同步或非同步的訊息到主程序。 也可以接收主程序回覆的訊息。

webContents:BrowserWindow物件的一個屬性,其send方法可以向渲染程序傳送非同步訊息,可以傳送任意引數。

看看二維碼的栗子:

//主程序中
ipcMain.on("qrcodeFinished", function () {
    // doSomething
});
qrcodeWindow.webContents.send('qrcodeBase64', received_msg.data);

//渲染程序
const ipcRenderer = require('electron').ipcRenderer;
ipcRenderer.on('qrcodeBase64', function (event, data) {
    const qrcode = document.getElementById('qrcode');
    qrcode.src = data;
    qrcode.style.display = 'block';
    ipcRenderer.send("qrcodeFinished");
});
複製程式碼

讓彈幕飛起來

上面講到,我們在主程序中建立了瀏覽器視窗,然後通過瀏覽器視窗例項去載入html檔案,就將我們的頁面展示了處理。所以,讓彈幕飛起來,就和我們寫靜態頁面沒什麼區別了,資料的來源就是接收主程序傳送過來的訊息。

怎麼讓彈幕動起來呢,這個大家應該都不陌生,就是不斷更新彈幕的位置就可以了。

這裡就不得不講到window.requestAnimationFrame()這一神器了。該方法告訴瀏覽器希望執行動畫並請求瀏覽器在下一次重繪之前呼叫指定的函式來更新動畫。該方法使用一個回撥函式作為引數,這個回撥函式會在瀏覽器重繪之前呼叫。

具體的實現在這裡就不贅述了。

打包應用

上面大概介紹了此應用開發的關鍵點,開發完成之後,就是打包階段了。

我們使用electron-builder可以很方便地進行應用打包。

安裝模組:

npm install electron-builder --save-dev
複製程式碼

在package.json中加入以下內容:

"scripts": {
    "start": "electron .",
    "pack:win": "electron-builder --win --ia32",
    "pack:mac": "electron-builder --mac"
},
"build": {
    "appId": "com.Barrage.app",
    "productName": "彈幕666",
    "copyright": "Copyright © 2018 ${author}",
    "electronVersion": "3.0.4",
    "mac": {
      "icon": "ico/favicon.icns",
      "artifactName": "${productName}_Setup_${version}.${ext}"
    },
    "win": {
      "target": "nsis",
      "icon": "ico/favicon.ico",
      "artifactName": "${productName}_Setup_${version}.${ext}"
    }
}
複製程式碼

打包命令:

npm run pack:win
//or
npm run pack:mac
複製程式碼

詳細使用可以看看官網文件。

其他

  • 自動更新:這也應該是不可或缺的一環,我們可以使用electron-updater給我們的應用配置自動更新功能。在這裡就不展開講了。
  • 錯誤日誌收集:一個完善的應用,應該要有錯誤日誌的記錄。
  • more...

總結

本文大概介紹了開發Electron接收端的關鍵點。事實上,每一個點展開講,花很大的篇幅也未必將所有細節講清楚。最好的方案,還是應該自己動手去實踐一下,遇到問題解決問題,勢必有所收穫。

感謝耐心閱讀。以上,如有錯漏,歡迎指正!

@Author: TDGarden