大家都知道JAVAScript無論是在瀏覽器中運行、還是在Node.js中運行都是單線程運行的,所以并不適合在處理一些CPU密集型任務。但是Node.js允許開發者使用C、C++等語言開發像普通的Node.js模塊一樣通過require()函數加載的原生模塊。因為Electron內置Node.js,這樣就使得Electron同樣具備了相同的能力。
在實際業務場景中,也有一些現成的C/C++項目,在Node.js項目中直接復用可以節約很多開發成本。
本文將探討如何在Electron應用中開發原生模塊,以擴展應用的功能和性能。
搭建原生模塊開發環境
在目前的原生模塊開發中,一般都是基于Node-Api進行開發。
Node-Api是什么呢?
Node-API(Node Application Programming Interface)是一個用于編寫跨平臺原生插件的封裝層。它提供了一組穩定的 C/C++ 函數,使開發者可以編寫與 Node.js 運行時環境兼容的原生插件。通過使用 Node-API,開發者可以消除由于 Node.js 版本變化而引起的插件不兼容的問題,并且能夠更方便地編寫和維護跨平臺的原生模塊。
理解一下就是我們無需為不同版本的Node.js編譯不同版本的原生模塊。不同版本的 Node.js 使用同樣的接口為原生模塊提供服務,這些接口是 ABI 化的,只要 ABI 的版本號一致,編譯好的原生模塊就可以直接使用,而不需要重新編譯。
ABI 化是指將軟件接口轉化為應用程序二進制接口(Application Binary Interface)的過程。在編程中,ABI 化的目標是確保在不同編譯器、操作系統以及硬件平臺之間的二進制兼容性。通過將接口規范化為二進制標準,不同模塊或程序可以相互調用和交互,而無需關心具體實現細節和底層平臺差異。Node-API 的設計就是為了實現跨版本和跨平臺的 ABI 兼容性,以便 C/C++ 模塊能夠在不同的 Node.js 環境中無需重新編譯即可運行。
Node-API有哪些開發方式?
基于 Node-API 的原生模塊開發可以使用 C 語言或者基于 node-addon-api 項目使用 C++ 語言的兩種方式。
- 基于C語言開發:由于受眾為前端開發者,C語言的編程復雜度高、開發效率較低,開發過程可能較為繁瑣。
- 基于 node-addon-api項目開發:相對于純 C 語言開發,C++ 提供了更多的高級特性和工具,開發效率相對較高。node-addon-api 項目提供了一組方便的 C++ API 封裝,簡化了與 Node-API 的交互過程,減少了部分底層操作。
接下來我們就基于這個項目來開發一個 Electron 的原生模塊。
- 安裝 Node.js:首先,確保您已經安裝了 Node.js,可以從 Node.js 官網下載并安裝適合您操作系統的版本。
- 需要全局安裝 node-gyp,它是專門為構建開發、編譯原生模塊環境而生的跨平臺命令行工具。
npm install -g node-gyp
- 創建空項目目錄:創建一個新的項目目錄,作為原生模塊的開發目錄。
- 初始化 npm 項目:進入項目目錄,打開終端,并運行以下命令初始化 npm 項目。
npm init
- 根據提示,設置項目的名稱、版本等信息。
- 安裝 node-addon-api:運行以下命令安裝 node-addon-api 模塊:
npm install node-addon-api --save-dev
以上就已經搭建好了基本的原生模塊開發環境,接下來通過一個簡單的例子,來實現一個訪問系統文件的原生模塊帶大家了解一下開發流程。
開發原生模塊訪問系統文件并讀取文件內容
開發原生模塊需要熟悉C++編程和Node.js的C/C++擴展開發,涉及一些底層的編程和跨平臺的知識。這里的例子較為簡單,方便大家理解。
- 創建 C++ 文件:在項目目錄下創建一個 C++ 源文件,例如 filesystem.cpp,并添加以下代碼內容:
#include <napi.h>
#include <IOStream>
#include <fstream>
Napi::String ReadFile(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
// 讀取文件路徑參數
std::string filePath = info[0].As<Napi::String>().Utf8Value();
// 打開文件并讀取內容 std::ifstream file(filePath);
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
// 將內容轉換為 Napi::String 返回
return Napi::String::New(env, content);
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("readFile", Napi::Function::New(env, ReadFile));
return exports;
}
NODE_API_MODULE(addon, Init)
上述代碼定義了一個 ReadFile 函數,它接受一個文件路徑參數,并返回文件的內容。
NODE_API_MODULE是Node-API 提供的一個重要的宏,用于在 C++ 中定義 Node.js 原生模塊的入口點,創建一個模塊初始化函數,并將此函數暴露給 Node.js 運行時。
使用 NODE_API_MODULE 定義原生模塊的入口點,可以讓開發者以 C++ 的方式編寫模塊的初始化、導出函數、屬性等,并與 Node.js 運行時進行交互。并可以在 Node.js 中加載和使用。
Init 函數是模塊的初始化函數,用于在模塊加載時注冊和導出相應的函數、屬性等。然后,通過 NODE_API_MODULE 將 Init 函數暴露給 Node.js 運行時,并指定模塊的名字為 "addon"。
- 創建 binding.gyp 文件:在項目目錄中創建一個名為 binding.gyp 的文件,并添加以下內容:
{
"targets": [
{
"target_name": "filesystem",
"sources": ["filesystem.cpp"]
}
}
binding.gyp 是一個用于配置 Node.js 原生模塊構建過程的項目文件。它采用了 JSON 格式,并使用特定的語法來定義編譯選項、依賴項和源文件等信息。通過編輯 binding.gyp 文件,可以指定編譯器和鏈接器的選項,添加所需的依賴庫,并確定要編譯的源文件。
編譯原生模塊
在構建編譯原生模塊時,需要使用 node-gyp ,它會讀取 binding.gyp 文件并根據其內容進行編譯操作。node-gyp 提供了一個簡化的構建流程,使得開發人員能夠輕松地配置和構建原生模塊。
使用以下命令構建該模塊。
$ node-gyp configure
$ node-gyp build
運行上述命令將生成一個名為 build/Release/filesystem.node 的編譯好的原生模塊文件。
接下來,就可以在任何 Node.js 文件中使用該模塊:創建一個名為 app.js 的 JavaScript 文件,并添加以下代碼
const addon = require('./build/Release/filesystem.node');
const filePath = '/path/to/file.txt';
const content = addon.readFile(filePath);
console.log(content);
上述代碼使用 require 導入原生模塊,然后調用 readFile 函數讀取指定文件的內容,并輸出到控制臺。
總結
在 Electron 應用中,使用和開發原生模塊可以為前端開發同學提供更廣闊的可能性;能夠利用操作系統級別的功能來提升應用的性能。
那么有同學就會有疑問,除了自己開發原生模塊還有什么方案可以給Electron應用提供更廣泛的功能擴展,包括底層系統功能的訪問、高性能計算呢?
當然有,在日常開發中,可以使用動態鏈接庫(Dynamic Link Library,DLL)進行擴展功能的模塊化。使用動態鏈接庫可以使用更多的語言和框架進行開發,適合不同開發者的需求。比如,一個go開發者也可以給我們提供一個動態鏈接庫供Electron調用,也可以將go代碼打包成不同平臺的文件供其他平臺調用,更適合獨立功能各個平臺使用的場景。原生模塊則更專注于為 Node.js 和 Electron 應用程序提供特定功能的開發。
在實際應用中,可根據具體需求和開發團隊的技術棧來選擇合適的方式,結合動態鏈接庫和原生模塊來擴展 Electron 應用程序的功能。
參考鏈接
- Electron桌面應用開發:https://juejin.cn/book/7152717638173966349?enter_from=course_center&utm_source=course_center
- node-gyp 實現 nodejs 調用 C++:https://juejin.cn/post/6844903971220357134?searchId=20231214152519329167E9F0AB744365BF