介绍Dubbo-Desktop-Manager 简称 ddm是我开发的 app最早是用来管理测试 dubbo 框架服务的现在的方向是基于npm仓库的开放插件平台。目前已经内置的 dubbo 插件能力多注册中心zookeeper、nacos、etcd、dubbo-admin、dubbo-admin相同协议的服务服务管理提供者列表、消费者列表、 服务启用、服务禁用、编辑配置服务测试服务测试目前已支持Telnet、dubbo、http 等协议、invoke命令生成、参数填充、响应对比为什么要内置 npm原来 ddm 是All in One软件包。哪怕只改一个小功能用户也得重新下载完整安装包。用户都是很“懒”的旧版本能用就不想重新下载安装。加上没“给果子交钱”没有经过 Apple 签名和公证macOS 安装时困难重重。所以我把软件升级为插件模式app 只提供稳定的宿主能力Dubbo 等功能由插件实现。插件可以独立安装和升级体积小也不用每次更新功能都重新发布本体。插件包使用 npm 分发很合适。它已经解决了发布、版本、依赖和镜像等问题但新的问题也随之出现怎样执行npm install安装插件呢从 PicGo 的spawn方案开始做插件系统前我先阅读了 PicGo 作者 MARKSZ 的 Electron 系列博客之后又研究了 PicGo 和 PicGo Core 的源码。它们对我的插件系统帮助很大在此特别感谢 PicGo 作者。PicGo Core 使用cross-spawn启动系统 npmconstnpmspawn(npm,args,{cwd:pluginDirectory})我最初也尝试了这种方式。它简单、完整但依赖用户安装 Node.js/npm。ddm的未来定位是任何人可用易用的插件工具平台未必他们会正确的安装Node.js所以这个方案不符合我们的要求。尝试 npm 6 的编程接口我后续又了解到npm 6 可以作为项目依赖直接调用importnpmfromnpmnpm.load(config,err{npm.commands[command](modules,callback)})这种方案不依赖系统 npm但只能配合很低版本的 npm 使用。低版本 npm 功能不足部分插件安装后无法正常使用。后来我看到 Rubick 在 de0e9ed8 中删除了同样的 npm 依赖调用改为cross-spawn让用户自行安装 Node.js。我和 Rubick 作者交流了一下确认原因同样是低版本 npm 的限制阅读 npm 8 源码既然npm6可用那么npm8也应该是可以的我开始查看npm8.19.4的源码。npm 8 直接执行require(npm)会进入根index.js并抛出The programmatic API was removed in npm v8.0.0但继续查看源码可以发现npm CLI 仍然是运行在 Node.js 上的 JavaScript 程序核心Npm类位于lib/npm.js。Electron 主进程就是 Node.js。因此我的最终方案是把完整的 npm 8 打进应用绕过根index.js直接加载lib/npm.js然后调用load()和exec()。项目固定 npm 版本并通过 Electron Builder 复制完整目录{dependencies:{npm:8.19.4},build:{extraResources:[./node_modules/npm]}}NpmUtils 完整实现NpmUtils负责加载内置 npm、指定安装目录、选择镜像并转发安装日志// 开发和生产环境使用不同的 npm 路径constAPPLICATION_NPM_PATH_PREFIXIS_DEVELOPMENT?path.join(process.cwd(),node_modules/npm/):path.join(process.resourcesPath,node_modules/npm/)classNpmUtils{asyncexecCommand(command,args,where){if(!command){thrownewError(command is a must);}process.argv[0]process.argv[1]dist_electronprocess.argv[2]commandfor(leti0;iargs.length;i){process.argv.push(args[i]);}constNpmrequire(${APPLICATION_NPM_PATH_PREFIX}/lib/npm.js)classMyNpmextendsNpm{constructor(localPrefix){super()this.localPrefixlocalPrefix;}/** 安装依赖时会修改title可能会影响应用的title所以重写忽略 */settitle(t){}}constnpmnewMyNpm(where)conststdoutWriteprocess.stdout.write;conststderrWriteprocess.stderr.write;try{process.stdout.write(...msg){npm.output(...msg)}process.stderr.write(...msg){npm.outputError(...msg)}awaitnpm.load()// 可以设置更快的仓库npm.config.set(registry,https://registry.npmmirror.com/)constcmdnpm.argv.shift()awaitnpm.exec(cmd,npm.argv)}finally{process.stdout.writestdoutWrite;process.stderr.writestderrWrite;}returnnpm.message;}}exportdefaultnewNpmUtils();使用案例安装插件awaitnpmUtils.execCommand(install,[ddm-plugin-dubbo-support1.0.0,--loglevel,info],pluginDirectory)卸载插件awaitnpmUtils.execCommand(uninstall,[ddm-plugin-dubbo-support,--loglevel,info],pluginDirectory)真实代码参见PluginInstaller.jsNpmUtils.js