node.js基于 cmake-js 进行插件开发实战
发布于 21 天前 作者 zhoutk 717 次浏览 来自 分享

以前工作在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
    
  • 测试运行结果图
    测试运行输出
    test_result_01.jpg
    项目日志(包括请求和sql语句)
    test_result_02_.jpg

相关项目

  • Zrest node.js嵌入c++插件项目,实现跨平台多数据库无缝切换的微服务开发框架
  • gels node.js项目,基于koa2实现的rest api服务框架,功能齐全; 以gels为入口,实现本项目,c++项目以插件方式集成
  • Zjson c++项目,实现简单高效的json处理
  • Zorm c++项目,以json对象为媒介,实现了一种ORM映射;设计了通用数据库操作接口规范,能无缝的在多种数据库之间切换
2 回复

挺好的实践。有意思

好奇 sqllite 直接编译进去为啥会报错

回到顶部