Flutter 热更新无侵入方案(插件中心)

3,420 阅读6分钟

第一篇 Flutter 热更新无侵入方案(探讨)

第二篇 Flutter 热更新无侵入方案(生成运行时库)

第三篇 Flutter 热更新无侵入方案(生成注册表)

目前我将开发放在了社区 flutter_runtime 希望有志向小伙伴一起开发

开发进度

  • 新增配置页面可以自定义修复生成的运行库

  • 修改生成分析配置可以支持分析缓存调用提升分析效率

  • 生成运行时中心库可以通过这个库动态调用

  • 添加命令行插件系统的支持

  • 分析运行代码成 JSON

  • 实现自定义解析器

  • 插件后台服务支持

  • 发布v0.1.0 版本

添加了插件中心系统,整个插件系统还需要后续逐步的完善,比如完善插件服务器支持,其他自定义插件支持。

更新日志如下

  • 🟢 新增插件页面可以安装/创建插件/激活插件/重装/安装其他版本等
  • 🟢 新增.active_plugins.json 可以获取和保存当前工程允许激活的插件
  • 🟢 新增.version.json 可以允许用户设置当前工程路径依赖的版本 为后面统一版本调用插件修复功能做准备
  • 🟢 新增插件可以开启开发模式,允许可以直接调用修改的代码进行运行 不过允许修改代码的工程存在于$HOME/.dcm 的安装目录
  • 🟢 新增生成代码之前会启动安装的插件进行修复
  • 🟢 新增关闭插件开发模式会自动进行重新编译
  • 🟡 修改 flutter 本地依赖获取版本号按照当前版本为准
  • 🔴 修改 flutter_runtime 的依赖修改为 Git 依赖,修复了后续打包成 app 存在报错的问题

实现和设计思路

整体插件中心的设计是从一个之前废掉 Flutter 低代码 IDE项目抠出来的功能,移植进来进行了改动。

之前为了设计插件系统,也是费劲了脑汁,研究了很多方案,最终确定了按照命令行程序为基准,通过本地文件进行通信系统。

对于插件的管理主要依赖自研的DCM命令行工具,通过这个可以对插件进行一系列的管理,目前可以支持如下功能。

Dart Cli Version Manager

Usage: dcm <command> [arguments]

Global options:
-h, --help    Print this usage information.

Available commands:
  create           创建命令行模版工程
  generated        根据输入JSON和Mustache进行输出
  get_all_branch   获取安装命令下面的所有支持分支
  get_all_tag      获取安装命令下面的所有Tag
  install          install a dart command
  list             list all dart commands
  local            通过本地路径安装
  print_db_path    打印数据库的路径!
  rebuild          重新编译
  run              run a dart command
  uninstall        uninstall a command

Run "dcm help <command>" for more information about a command.

其中 generatedprint_db_path已经没有用了。

创建DCM 来统一管理插件的目的是可以在终端走代理,这样执行起来会非常快。第二通过命令行和系统其他的命令交互没有权限的问题。

这个 DCM 命令行目前还在不停的根据需要进行调整功能和新增功能中。

操作界面

image-20230727162854991

插件中心从右上角的图标点击进入,UI 很丑可以忽略,毕竟功能优先。

为什么插件中心的入口要放置在进入工程详情页面里面,是因为后续插件中心的很多功能都是基于当前打开的工程进行定制化显示的。

image-20230727163425642

对于安装插件和新建插件我推荐用 DCM 在终端操作,毕竟如果依赖需要代理才可以的,就可能在这个软件上面等待很长,或者链接超时。

image-20230727164046505

安装插件

image-20230727164227730

安装插件需要安装的地址可以是一个本地的地址,也可以是一个 git 地址。同时需要知道安装的版本,支持分支/tag/commit。

是否本地仓库,是为了支持本地的插件,目前设计本地的也要是 git 工程,后续可能要移除本地仓库的选项,可以合并功能。

是否覆盖安装是为了解决同样的分支,但是更新了代码,可以将最新的代码插件安装到本地。

if (isLocal) {
  var command = 'local -p $url';
  if (isOverwrite) {
    command += ' -f';
  }
  commandRun = CommandRun('dcm', command);
} else {
  var command = 'install -p $url@$ref';
  if (isOverwrite) {
    command += ' -f';
  }
  commandRun = CommandRun('dcm', command);
}

这里如果是本地就执行dcm local否则就执行dcm install进行安装。

  1. 判断

安装本地插件

首选会判断 url 是否是http开头,如果是,则提示报错。随后会默认通过 git branch --show-current获取当前的分支当做当前安装的版本,如果获取不到则默认为__local__作为版本。

随后调用IntallMixindcm install共用一套安装逻辑。

安装 Git 的远程插件

会判断一下是否$url@$ref的 格式组成,并且url是否带有http前缀。

  1. 路径生成目录

不管是 git 的网络地址还是本地路径,怎么能保证在本地安装目录是唯一的呢,并且路径可以存在的。

这里我使用的将路径的特殊字符/换成_来解决,并且去掉.git的后缀生成的字符串来生成作为插件在本地的文件夹。

String packageName(Uri uri) {
  return [uri.host, ...uri.path.split("/")].join('_').replaceAll(".git", "");
}

写在这里,我意识到一个问题所在,这个固然在 macOS 运行的很好,但是在 window s 上面确实无法运行,毕竟在 windows 上面的路径符号为\。这样这里就存在问题了,为了支持后续可以发布 windows 版本,我们可以换成Platform.pathSeparator来支持 windows。

比如 Users/king/Documents/ide_plugins/fix_analyzer_runtime变成了下面的文件夹

__Users_king_Documents_ide_plugins_fix_analyzer_runtime

  1. 不同版本不同路径存放版本源码

对于同一个插件,可以安装不同的版本,比如不同工程需要对应的依赖版本不一样,则修复时候的代码逻辑是不一样的,因此支持可以安装不同的版本。

那么 ref 值就是每一个版本存放源代码的地方。

image-20230727174046182

比如上图所示,分别安装了fix_runtimemain插件的两个版本。

对于本地路径的插件代码,这个很好弄,只需要执行copy方法将代码复制到指定的位置即可。

await copyPath(path, refPath);

但是对于https的链接就比较麻烦,需要先进行clone下载到本地。

'$git clone ${uri.toString()} ${refDirectory.path}'

下一步将分支切换到我们需要安装的版本,毕竟我们支持安装的版本可以包括分支/tag/commit

下一步就是我们进行切换分支,或者创建新的分支了。但是有一个问题,如果本地分支存在我们使用git switch就会报错。

这就需要我们通过git branch -l获取本地分支,如果不存在就使用git switch -c $ref来换分支。

  1. 安装

不管是通过本地路径还是通过git clone我们已经将源代码保存到本地了,这么就需要安装了。安装?安装什么东西。

其实我们可以通过dart run直接运行我们的插件的,但是为什么还要安装?

一是为了可以在运行时间不用每次执行编译节省大量执行时间,二是编译成.exe 文件可以后续做成第三方插件支持二进制的下发,屏蔽源码功能。

安装需要通过pubspec.yaml获取到插件的名称,这里做的逻辑是不允许插件的名称有重复,虽然可能存在有重复的名称,但是目前设计就是不能有重复名称。

获取到插件的名称,需要通过名称和版本查询本地已安装的目录是否存在。

对于缓存插件的安装信息,之前我本来是采用realm数据库管理,但是对于这种命令行的用起来不是很方便。所以后面还是换成直接.json进行管理,毕竟本地数量级还是很少的。

// 安装路径
$HOME/.dcm/plugin.json

对于安装信息只保留了一些重要的信息

class Cli {
  /// 安装命令的地址
  late String url;

  /// 安装引用
  late String ref;

  /// 命令名字
  late String name;

  /// 是否本地路径安装
  late bool isLocal = false;

  /// 安装时间
  late int date;

  /// 安装在本地的路径
  late String installPath;
 }
  • url 为了后续可以支持重新安装和支持切换其他版本
  • ref 安装的版本,为了放置意外覆盖还有支持重装
  • name 插件的名称
  • isLocal 是为了在重装过程中走不同的命令
  • date 安装时间为了给插件进行排序等操作
  • installPath 源码安装在什么位置,可以后续进行进行开启开发调试

如果通过名称和版本在本地已安装的列表查询到,如果用户添加-f参数则运行覆盖重装,否则提示用户已存在不允许安装。

5 编译

最后一步就是编译 exe,会在源码目录执行pub get拉取最新的依赖下来,之后执行dart compile exe命令将../bin/$name.dart的执行文件编译成exe的可执行文件。

随后创建$HOME/.dcm/bin/$name/$ref/$name.exe的软链接,后续可以直接操作这个exe文件进行运行命令。

已安装的插件列表

获取本地已经安装的插件列表,通过调用封装的dcm list -j可以将列表转换为 json 进行输出。

但是本地安装了很多插件,并且一个插件可能还存在安装了多个版本,每一个项目使用的插件和版本都是不一样的。

所以就要做出根据项目做出筛选,在当前项目.active_plugins.json文件里面存储对应激活的插件信息。

class ActivePluginInfo {
  late String name;
  late String ref;
}
  • name 代表插件名称
  • ref 代表插件版本

新建插件

image-20230728164006325

新建插件需要提供插件名称插件模板地址插件模板分支

现在对于修复代码的插件地址 github.com/flutter-run… 可以找到

比如创建基于Analyzer的修复代码插件可以这样创建

dcm create -n $name -u https://github.com/flutter-runtime/plugin_template.get -r fix_runtime -d "修复 Analyzer 代码插件"

name 这个是插件的名字,推荐使用`fix_packageName_runtime,比如对于Analyzer的修复插件的名字叫做fix_anzlyzer_runtime`

只需要在下面的代码文件根据提供的信息修改里面的模型值即可

image-20230728164631403

插件的配置

image-20230728164715243

pubspec.yaml这个文件存在commands的字段,可以配置多个支持调用的命令,比如fix_runtime就是固定字符串,是支持修复生成代码的命令。name代表需要修复的库的名称,version代表需要修复库的对应版本。

对于一些本地路径采用的是md5加密的方式,不同用户的本地路径不一样,所以这个md5的值肯定是不一样的,但是怎么才能让修复的插件共用呢。

image-20230728165045509

我们在每隔依赖库的地方,都添加了一个修改的图标,用户可以自定义对应的版本,来代替默认生成的md5的值。

这样做也是为了支持团队合作,毕竟开发工程或者插件需要修复,只需要团队一个人新增插件即可。

这个自定义版本的配置文件放在工程.versions.json里面。

插件的卸载

image-20230728165256780

既然能安装插件,自然也能卸载插件。这个比较简单只需要调用dcm unstall即可。这样会自动删除引用/bin/$name/$ref/$.exe文件还有存放源代码的/packages/$path/$name/$ref路径。

插件重装

image-20230728165458733

对于我们一些插件做了修复或者变更,比如本地的路径安装的,或者根据分支安装的。但是插件在之前已经安装了,单不是最新的代码,就可以调用进行重装。

重装的逻辑也是十分的简单,第一步卸载之前存在的插件,第二步则是重新安装对应的插件和版本。

安装插件其他版本

image-20230728165707621

这个需求主要提现在比如发现最新的版本有问题,想回退版本,就可以点击安装其他版本。

image-20230728165752265

是支持选择不同分支,或者不同 tag 或者输入 commit 进行安装的。

插件的执行

image-20230728165856086

这里显示插件支持的所有命令,可以点击执行。对于一些被程序写死的点击是没有反应的。但是后续为了做到自定义的插件,可以支持用户直接点击就可以执行。

插件的开发调试

对于平时开发中,可能不确定自己写的代码是否真的能修复。就需要一边写一边进行测试。如果不停的重装,会很浪费时间,所以新增了一个开关是否支持调试模式。

image-20230728170214041

如果打开了调试模式,调用命令则会直接调用代码文件dart run ../packages/$path/$name/$ref/bin/$name.dart进行运行,可以做到随时改,随时查看结果。

这个就导致本体工程和修改的工程分开了,所以强烈的要求本地的工程支持 git 管理。

后续的计划

对于推荐的插件还没有实现,这个需要借助一个插件中心服务器,可以根据当前的依赖库自动安装对应修复的插件,可以根据安装的数量展示推荐的插件。

对于一些自定义插件,比如根据一些内容的系统变量,来实现一些功能。或者根据一些配置自动生成 UI 让用户输入来做到个性化很高的插件能力。