以前工作在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映射;設計了通用數據庫操作接口規範,能無縫的在多種數據庫之間切換