以前工作在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語句)