Windows应用开发有着较为丰富和多样的技术选型。C#/WPF 这种偏Native的闭源方案,目前开发人员相对比较小众了。C++/QT 的跨平台框架,C++对于GUI开发来说上手会更难。JavaScript/CEF/Electron 基于Chromium 的跨端框架,使用前端技术栈来构建桌面应用,性能会略低一些。总而言之各有所长,有一点可以确定的是,跨端能力成为了选型的重要考量。
在进一步探索和预演之后,通过Flutter的能力,可以很方便地将移动端的业务模块迁移至PC端,尽可能地实现一码多端,降低业务维护成本,以此为出发点,进行了Windows平台的接入。
Windows平台通过Plugin或FFI的方式提供相关能力,需要使用C++编写相关的平台代码。如果Plugin的代码可以自闭环,即所有C++代码都可以在Plugin内编写完成,那这个Plugin可以单独抽成一个Dart库。但是如果Plugin的代码需要复用其他Plugin或者主工程的C++代码,粗暴一点就是拷贝代码,或者通过CMakeLists来控制相互之间的依赖关系,通过find_package来完成头文件和库文件的链接。一旦依赖关系比较复杂,CMakeLists就会变得臃肿,依赖关系发生变化时,也会牵一发而动全身。随着系统复杂度的提升,开发人员的增加,模块之间相互耦合在一起,单一模块的修改都会影响到所有模块。
上述是一个登录模块的例子,Module 作为基类,定义了模块的一些生命周期方法。LoginModule是对外公开的业务接口,里面仅包含外部会用到的和登录业务相关的方法。LoginModuleImplV1类是登录逻辑的具体实现,不对外公开,里面的私有成员变量和方法对外部是隐藏的,同时实现了Module和LoginModule的接口。Provider用于创建和管理Module实例。
// LoginModuleProvider 通过宏自动生成 X_MODULE_PROVIDER_DEFINE_SINGLE(LoginModule, MIN_VERSION, MAX_VERSION); // LoginModuleImplV1Provider 通过宏自动生成 X_MODULE_DEFINE_SECONDARY_PROVIDER(LoginModuleImplV1, LoginModule);XModule的模版开发方式,会增加很多类文件,为了方便,通过宏来控制Provider类的自动生成。其中MIN_VERSION和MAX_VERSION是该Module接口能支持的最小和最大的版本范围,可以限制后期dll插件化加载时,不加载在版本之外的dll,避免产生冲突和错误,目前Provider的GetVersion使用的是MAX_VERSION。
// 由 X_MODULE_DEFINE_SECONDARY_PROVIDER 宏自动生成 class DLLEXPORT LoginModuleImplV1Provider : public LoginModuleProvider { public: LoginModule* Create() const { LoginModuleImplV1* p = new LoginModuleImplV1(); ((Module*)p)->OnCreate(); return p; } };LoginModuleImplV1Provider可以通过调用Create方法拿到对应的LoginModuleImplV1实例。
x_module::ModuleCenter* module_center = x_module::ModuleCenter::GetInstance(); module_center->AcceptProviderType<LoginModuleProvider>();ModuleCenter是所有Module的管理类,先通过x_module::ModuleCenter::GetInstance()拿到ModuleCenter的实例,它是一个跨dll的单例。然后要用之前的LoginModuleProvider去注册一个Module类型到ModuleCenter中。LoginModuleProvider中定义了支持的Module类型,以及最小版本和最大版本,如果后续扫描到的dll中提供的对应类型的Provider中GetVersion返回的值不在最大版本和最小版本之间,那么就不会被允许加载进来。
module_center->AddProvider(new LoginModuleImplV1Provider());通过这种方式,可以将LoginModuleImplV1Provider注册到ModuleCenter中,然后创建并管理LoginModuleImplV1的实例。但是这样就显式地依赖了LoginModuleImplV1Provider,违反了前面说过的依赖倒置原则,对开闭原则也不友好,因为这样就只能通过修改代码来实现扩展了。
#include <x_module/connector.h> #include "login_module/login_module_impl.h" X_MODULE_CONNECTOR bool XModuleConnect(x_module::Owner& owner) { owner.add(new LoginModuleImplV1Provider()); return true; }为了在加载dll时,来注册Provider,增加了一个connector.cc,添加一个XModuleConnect方法,让dll被加载之后,能够找到XModuleConnect这个符号方法,并进行调用,在XModuleConnect被调用的时候,会调用AddProvider将Provider进行注册。
std::string path = GetProgramDir(); module_center->Install(path, "login_module");由于目前login_module.dll是直接放在exe同目录的,所以这里直接获取了一下exe绝对路径,然后调用Install方法,将路径和dll名login_module传入进去,这样就完成了注册。
auto* p_login_module = module_center->ModuleFromProtocol<LoginModule, LoginModuleProvider>(); if (p_login_module == nullptr) { (*move_result)->Error("-100", "login module 为空"); return; } bool islogin = p_login_module->IsLogin();在使用时,只需要LoginModule和LoginModuleProvider这两个抽象,就能获取真实的LoginModuleImplV1这个实例,调用方仅需关心LoginModule所公开的API,完全屏蔽了对实现的依赖。后续底层扩展成了LoginModuleImplV2,只要LoginModule的公开API不变,对上层是无感知的。这种方式完全遵循了前面提到的设计原则,对团队内的多人维护以及后续的更新迭代都带来了稳定的保障。
├── CMakeLists.txt ├── LICENSE ├── cmake │ └── config.cmake.in ├── include │ └── fish_ffi_module.h ├── src │ ├── connector.cc │ ├── fish_ffi_module_impl_v1.cc │ └── fish_ffi_module_impl_v1.h ├── vcpkg-configuration.json └── vcpkg.jsonvcpkg-configuration.json配置了私有源,后面会讲到。vcpkg.json文件,声明了当前库所依赖的其他库,即vcpkg的依赖清单,其中"dependencies"字段声明了所使用的依赖名称。
{ "name": "fish-ffi-module", "version": "1.0.0", "description": "A fish-ffi module based on fish-ffi-sdk.", "homepage": "", "dependencies": [ "fish-ffi-sdk", "x-module", "flutter-sdk" ] }CMake工程最重要的就是CMakeLists文件了,里面配置了编译相关的设置,添加了相关的注释来帮助理解。
cmake_minimum_required(VERSION 3.15) # 仓库版本常量,升级时修改 set(FISH_FFI_MODULE_VERSION "1.0.0") project(fish-ffi-module VERSION ${FISH_FFI_MODULE_VERSION} DESCRIPTION "A fish-ffi module based on fish-ffi-sdk." HOMEPAGE_URL "" LANGUAGES CXX) option(BUILD_SHARED_LIBS "Build using shared libraries" ON) # vcpkg清单中添加依赖之后,通过find_package就能找到 find_package(fish-ffi-sdk CONFIG REQUIRED) find_package(flutter-sdk CONFIG REQUIRED) find_package(x-module CONFIG REQUIRED) # configure_package_config_file 生成config要用到 include(CMakePackageConfigHelpers) # install 安装要用到 include(GNUInstallDirs) # 当前库的头文件和源文件 aux_source_directory(include HEADER_LIST) aux_source_directory(src SRC_LIST) add_library(fish-ffi-module SHARED ${HEADER_LIST} ${SRC_LIST} ) # 设置别名 add_library(fish-ffi-module::fish-ffi-module ALIAS fish-ffi-module) # 设置动态库导出宏,PRIVATE为编译时,INTERFACE为运行时 if (BUILD_SHARED_LIBS AND WIN32) target_compile_definitions(fish-ffi-module PRIVATE "FISH_FFI_MODULE_EXPORT=__declspec(dllexport)" INTERFACE "FISH_FFI_MODULE_EXPORT=__declspec(dllimport)") endif () target_compile_features(fish-ffi-module PUBLIC cxx_std_17) # 添加头文件 target_include_directories(fish-ffi-module PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/> $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}> ) # 链接库文件 target_link_libraries(fish-ffi-module PRIVATE fish-ffi-sdk::fish-ffi-sdk) target_link_libraries(fish-ffi-module PRIVATE flutter-sdk::flutter-sdk) target_link_libraries(fish-ffi-module PRIVATE x-module::x-module) # 基于config.cmake.in的模板生成xxx-config.cmake的文件 configure_package_config_file( cmake/config.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/fish-ffi-module-config.cmake INSTALL_DESTINATION ${CMAKE_INSTALL_DATADIR}/fish-ffi-module NO_SET_AND_CHECK_MACRO) # 生成xx-config-version.cmake文件 write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/fish-ffi-module-config-version.cmake VERSION ${FISH_FFI_MODULE_VERSION} COMPATIBILITY SameMajorVersion) # 将上面生成的两个config文件,安装到share/fish-ffi-module下 install( FILES ${CMAKE_CURRENT_BINARY_DIR}/fish-ffi-module-config.cmake ${CMAKE_CURRENT_BINARY_DIR}/fish-ffi-module-config-version.cmake DESTINATION ${CMAKE_INSTALL_DATADIR}/fish-ffi-module) # 安装头文件 install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) # install target install(TARGETS fish-ffi-module EXPORT fish-ffi-module-targets RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) # 导出 install(EXPORT fish-ffi-module-targets NAMESPACE fish-ffi-module:: DESTINATION ${CMAKE_INSTALL_DATADIR}/fish-ffi-module)这里面最重要的一点是配置xx-config.cmake和xx-config-version.cmake的生成,vcpkg会在源码首次拉下来的时候进行编译,编译完在相应库的share目录生成上述两个文件,并且在CMake配置阶段执行,这样在使用find_package的时候就能获取到这个库以及对应版本号。总结一下就是,vcpkg帮助完成了代码的下载、编译和配置,然后就可以方便的链接三方库了。
. ├── ports │ ├── fish-ffi-module │ │ ├── portfile.cmake │ │ └── vcpkg.json │ └── x-module │ ├── portfile.cmake │ └── vcpkg.json ├── versions │ ├── f- │ │ └── fish-ffi-module.json │ └── x- │ │ └── x-module.json │ └──baseline.json └── LICENSEvcpkg里面对依赖库的定义叫port,这里定义了两个port,分别是fish-ffi-module和x-module。其中的文件说明如下:
{ "default-registry": { "kind": "git", "repository": "https://github.com/microsoft/vcpkg", "baseline": "f4b262b259145adb2ab0116a390b08642489d32b" }, "registries": [ { "kind": "git", "repository": "xxx.git", "baseline": "1ad54586a5a2fadb8c44d3f8f47754e849fc5a38", "packages": [ "x-module", "fish-ffi-sdk", "fish-ffi-module"] } ] }在versions文件夹下还有一个baseline.json的文件,这个文件主要是设置基线用的,不像其他的依赖管理工具,vcpkg主要是通过这个基线来设置当前所使用的版本号的。vcpkg可以胜任依赖管理的相关工作,综上所述只是一个简单使用,相比其他平台的依赖管理工具略显繁琐,除此之外还有很多其他能力,需要到vcpkg.io的官方文档里面探索了。