node.js基於 cmake-js 進行外掛開發實戰

zhoutk發表於2023-01-08

以前工作在node.js環境下,做微服務產品; 三年前轉回到C++環境,已經有一些程式碼積攢。我將以往基於node.js與C++的相關專案結合起來(C++程式碼以addon外掛嵌入),實現了一個微服務快速(rest api service)開發框架。該框架以關聯式資料庫為基礎,現在支援(mysql、sqlite3、postgres),同時支援windows, linux, macos。本文以該專案為藍本,來說明使用C++為node.js開發外掛的實踐經驗。

專案結構

  • addon : C++外掛封裝程式碼目錄,這是一個node.js與C++的介面卡,具體的C++功能都在thirds目錄中
  • src : node.js原始碼目錄,一套完整的智慧微服務程式碼,基於關係資料,提供標準的rest api service, 不需要寫一行程式碼,詳見 gels專案
  • test : 單元測試程式碼目錄,提供了全面測試,同時也是很好的示例程式碼
  • thirds : C++專案都放在這個目錄下

    |-- CMakeLists.txt
    |-- addon                       //c++外掛封裝
    |   |-- export.cc
    |   |-- index.cc
    |   `-- index.h
    |-- package.json
    |-- src                         //node.js核心原始碼
    |   |-- config                  //只列出了目錄
    |   |-- dao
    |   |-- db
    |   |-- inits
    |   |-- middlewares
    |   `-- routers
    |-- test                        //rest api 測試
    |   `-- test.js
    |-- thirds                      //依賴的c++專案
    |-- package.json
    `-- tsconfig.json

Nodejs擴充套件基本開發

編譯擴充套件,兩種方式

  • node-gyp
  • cmake-js

開發環境

因為,本人的C++專案都使用cmake進行專案管理的,所以我選擇使用cmake-js來進行node.js的擴充套件開發。開發環境:

  • windows : cmake > 3.18, node.js >= 16, visual studio >= 2019; 若使用vs2022, windows SDK 必須安裝10.的版本,只裝11版本的話,編譯會出錯
  • linux : cmake > 3.18, node.js >= 16, gcc >= 7.5
  • macOs : cmake > 3.18, node.js >= 16, clang >= 12

專案依賴

專案依賴,請參看package.json中相關小節,與外掛開發相關的主要是以下三個專案:

  • cmake-js
  • bindings
  • node-addon-api

CMakeLists.txt關鍵點說明

完整的程式碼請自行到專案中去獲取,我再這裡只是節選,並進行一些說明

c++版本指定,因為依賴庫Zjson最低需要c++17
set (CMAKE_CXX_STANDARD 17)                             
SET(CMAKE_CXX_FLAGS "-D_GLIBCXX_USE_CXX17_ABI=0")
windows必須增加如下的引數設定,必須將動態連結庫的記憶體與主程式融合
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} /MTd")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd")
linux下的特殊要求, 其它環境不設這個變數,在連結的時候就只有linux會加上 dl這個引數
set(dlLinkParam dl)
...
target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB} ${MysqlDll} ${pqName} ${sqliteName} ${dlLinkParam})
cmake-js最基本的編譯設定
set(NODE_LINK_LIBS "")
set(NODE_EXTERNAL_INCLUDES "")

FILE(GLOB_RECURSE SOURCE_FILES "./addon/*.cc") 
FILE(GLOB_RECURSE HEADER_FILES "./addon/*.h") 

add_library(${PROJECT_NAME} SHARED ${HEADER_FILES} ${SOURCE_FILES} ${CMAKE_JS_SRC})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")
message("-------- CMAKE_JS_INC -------" ${CMAKE_JS_INC})
# Include Node-API wrappers
target_include_directories(${PROJECT_NAME} PRIVATE  
    ${CMAKE_SOURCE_DIR}/node_modules/node-addon-api 
    ${CMAKE_SOURCE_DIR}/node_modules/node-addon-api/src
    ${CMAKE_JS_INC})
target_link_libraries(${PROJECT_NAME} PRIVATE ${CMAKE_JS_LIB})

注: sqlite3 必須以動態連結庫的形式接入,直接將.c和.h加入到主程式中,能編譯透過,也能執行,但查詢系統表的時候會出現異常

外掛開發程式碼解析

addon 目錄下是與C++專案適配的程式碼,C++的功能,先寫成cmake管理的專案,放到thirds目錄,再適配進addon外掛,這樣能做到相對的獨立
一般需要三個檔案:export.cc, index.h, index.cc

export.cc

#include "index.h"

//匯出介面
Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
  return Zorm::Init(env, exports);
}

NODE_API_MODULE(Zorm, InitAll)

index.h

#include <napi.h>           //node.js外掛開發標頭檔案
#include "Idb.h"            //資料庫通用介面標頭檔案

class Zorm : public Napi::ObjectWrap<Zorm>{
public:
    //匯出函式
    static Napi::Object Init(Napi::Env env, Napi::Object exports);
    static Napi::FunctionReference constructor;
    //建構函式,生成一個orm物件,儲存到 成員變數 db 中
    Zorm(const Napi::CallbackInfo& info);
    //公用的類方法,要實現資料庫通用介面的所有方法適配
    Napi::Value select(const Napi::CallbackInfo& info);
    ...
private:
    ZORM::Idb* db;   //成員變數
};

index.cc

初始化函式,定義所有成員方法
Napi::Object Zorm::Init(Napi::Env env, Napi::Object exports)
{
    Napi::HandleScope scope(env);
    Napi::Function func =
        DefineClass(env, "Zorm",            //除了這個函式,其它基本都是規定寫法
                    {                       //定義外部能呼叫的所有成員方法
                        InstanceMethod("select", &Zorm::select),  
                        ...
                    });

    constructor = Napi::Persistent(func);
    constructor.SuppressDestruct();

    exports.Set("Zorm", func);
    return exports;
}
建構函式適配

Zorm::Zorm(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Zorm>(info), db(nullptr)
{
    int len = info.Length();
    Napi::Env env = info.Env();
    if (len < 2 || !info[0].IsString()) {               //函式引數解析,json物件我都用字串進行傳遞;二進位制使用Napi::Array jsNativeArray接收C++的char*
        Napi::TypeError::New(env, "String expected").ThrowAsJavaScriptException();
    }
    std::string dbDialect = info[0].As<Napi::String>().ToString();
    std::string opStr = info[1].As<Napi::String>();
    ZJSON::Json options(opStr);
    db = new ZORM::DbBase(dbDialect, options);
}
成員方法示例
Napi::Value Zorm::select(const Napi::CallbackInfo& info)
{
    int len = info.Length();
    Napi::Env env = info.Env();
    if (len < 1 || !info[0].IsString()) {
        Napi::TypeError::New(env, "String expected").ThrowAsJavaScriptException();
    }
    std::string tableName = info[0].As<Napi::String>().ToString().Utf8Value();

    ZJSON::Json params;
    if(len >= 2){
        params.extend(ZJSON::Json(info[1].As<Napi::String>().ToString().Utf8Value()));
    }
    std::string fieldStr;
    if(len >= 3){
        fieldStr = info[2].As<Napi::String>().ToString().Utf8Value();
    }

    ZJSON::Json rs = db->select(tableName, params, ZORM::DbUtils::MakeVector(fieldStr));
    return Napi::String::New(info.Env(), rs.toString());
}

專案地址

https://gitee.com/zhoutk/zrest
或
https://github.com/zhoutk/zrest

安裝執行

  • 新建配置檔案,./src/config/configs.ts, 指定資料庫:

    export default {
        inits: {
            directory: {
                run: false,
                dirs: ['public/upload', 'public/temp']
            },
            socket: {
                run: false
            }
        },
        port: 12321,
        db_dialect: 'sqlite3',              //資料庫選擇,現支援 sqlite3, mysql, postgres
        db_options: {
            DbLogClose: false,              //是否顯示SQL語句
            parameterized: false,           //是否進行引數化查詢
            db_host: '192.168.0.12',
            db_port: 5432,
            db_name: 'dbtest',
            db_user: 'root',
            db_pass: '123456',
            db_char: 'utf8mb4',
            db_conn: 5,
            connString: ':memory:',         //記憶體模式執行
        }
    }
  • 在終端(Terminal)中依次執行如下命令

    git clone https://gitee.com/zhoutk/zrest
    cd ztest
    npm i -g yarn
    yarn global add typescript eslint nodemon
    yarn
    tsc -w          //或 command + shift + B,選 tsc:監視
    yarm configure  //windows下最低vs2019, gcc 7.5, macos clang12.0
    yarn compile    //編譯c++外掛, 若有問題,請參照 [Zorm](https://gitee.com/zhoutk/zorm) 文件,特別是最後的註釋
    yarn start      //或 node ./dist/index.js
    export PACTUM_REQUEST_BASE_URL=http://127.0.0.1:12321
    yarn test       //執行rest api介面測試,請仔細檢視測試檔案,其中有相當完善的使用方法
                    //修改配置檔案,可以切換不同的資料,執行測試;使用mysql或postgres時,請先手動建立dbtest資料,編碼使用Utf-8
  • 測試執行結果圖
    測試執行輸出

    專案日誌(包括請求和sql語句)

相關專案

  • Zrest node.js嵌入c++外掛專案,實現跨平臺多資料庫無縫切換的微服務開發框架
  • gels node.js專案,基於koa2實現的rest api服務框架,功能齊全; 以gels為入口,實現本專案,c++專案以外掛方式整合
  • Zjson c++專案,實現簡單高效的json處理
  • Zorm c++專案,以json物件為媒介,實現了一種ORM對映;設計了通用資料庫操作介面規範,能無縫的在多種資料庫之間切換

相關文章