以前工作在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
#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; //成员变量
};
初始化函数,定义所有成员方法
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语句)
相关项目
挺好的实践。有意思
好奇 sqllite 直接编译进去为啥会报错