一、背景
目前移动端应用的版本更新, 最常见的方式是定期发版,无论是安卓还是iOS,都需要提交新的安装包到应用市场进行审核。审核通过后,用户在应用市场进行App的下载更新。而动态化, 就是不依赖更新程序安装包, 就能动态实时更新页面的技术。
相比动态化技术,定期发版更新应用的方式存在一些问题,比如:
- 审核周期长, 且可能审核不通过。 周期长导致发版本不够灵活, 紧急的业务需求不能及时上线。
- 线上出现急需修复的bug时,需要较长修复周期,影响用户体验。
- 安装包过大, 动辄几十兆几百兆的应用升级可能会让用户比较抗拒。
- 即使上线了,也无法达到全部用户升级, 服务端存在兼容多版本App的问题。
面对这些问题,如果能实现app增量、无感知更新,实现功能同步。无论是对公司还是用户都是非常好的体验。并且,动态化更新还能很好的解决以上问题:
- 随时实现功能升级。
- 线上bug可以实时修复,提高用户体验。
- 减小发版功能包体积。
- 发版后用户同步更新,不存在旧版本兼容问题。
经过团队近期的调研和学习,对业界Flutter动态化框架进行了如下总结:
- MXFlutter(腾讯): 使用js编写dart, 抛弃了 Dart 生态,维护困难。
- MTFlutter(美团): 布局,逻辑都使用Dart,增加语法解析和运行时, 由于其未开源,无从深入研究。
- Fair(58): 使用转换AST+JS的方式,通过下发bundle 和 js实现热更新,代表框架 Fair。
综上, MXFlutter官方已经停止更新, 而且需要使用js写Dart,而这种方式让开发效率受到极大影响; MTFlutter目前未开源,无从继续研究。 所以,我们开始着重调研和尝试使用 Fair。
二、Fair简介
Fair是为Flutter设计的动态化框架,通过Fair Compiler工具对原生Dart源文件的自动转化,使项目获得动态更新Widget Tree和State的能力。
创建Fair的目标是支持不发版(Android、iOS、Web)的情况下,通过业务bundle和JS下发实现更新,方式类似于React Native。与Flutter Fair集成后,您可以快速发布新的页面,而无需等待应用的下一个发布日期。Fair提供了标准的Widget,它可以被用作一个新的动态页面或作为现有Flutter页面的一部分,诸如运营位的排版/样式修改,整页面替换,局部替换等都可以使用。
Fair框架的架构如下图所示。
三、环境准备
构建 Fair 需要使用Flutter环境和CMAKE环境,请确保本地已经安装了相关的环境。
如果还没有安装Flutter,可以先下载Flutter,命令如下:
git clone https://github.com/flutter/flutter
然后,切换到 Fair 指定的 Flutter 版本。
git checkout
由于Fair用到了一些c++的库,所以需要安装 CMake & NDK,我们建议通过Android Studio进行安装,如下图。
四、Fair接入
这里参考了Fair团队出品的Fair 配套工具链介绍。通过使用Fair配套工具链,体验了完整的Fair工程的创建流程。
3.1 工具安装
安装 Faircli 命令行工具,命令如下:
dart pub global activate faircli
接下来,是安装 AS 插件。首先,打开AS,然后从/Android Studio/Preference/Plugins 搜索 "FairTemplate" 插件安装,如下图。
当然,也可以本地安装,FairTemplate-1.0.0.zip 插件下载。
3.2 Faircli-工程创建
工程分为载体工程及动态化工程。动态化的功能模块需要在动态化工程中开发。载体工程提供 bundle 下载、加载及基础能力支持。
首先,我们创建一个动态化工程,命令如下:
faircli create -n dynamic_project_name
其中,dynamic_project_name即为动态化工程名。接下来,我们再创建载体工程。
faircli create -k carrier -n carrier_project_name
其中,carrier_project_name即为载体工程名。
3.3 IDE插件-功能开发
使用AS打开上面创建的动态化工程,编译一下工程,如果拉取git的插件报错,可以将对应的插件代码拉到本地,然后使用本地依赖的方式进行依赖。
接下来,我们使用安装的FairTemplate模版来进行二次开发。
选择代码模板,生成对应的代码文件。
然后,我们就可以使用生成的模版代码进行二次开发。
同时,功能开发完成后,可使用 AS 插件进行一键打包。
3.4 启动本地热更新服务
打包完成之后,就可以启动本地热更新服务,进行开发功能预览。
当控制台打印如下信息,表示成功启动本地热更新服务。
接下来,我们运行载体工程,进入开发者选项页面进行热更新需要的一些配置。
然后,选择开发的模式为本地模式,输入host地址就可以加载 bundle 列表。选择对应的 bundle,进行功能预览。同时,我们还可以通过手机摇一摇触发重新加载功能。
3.5 IDE插件-上传 bundle 到线上环境
接下来,我们可以将生成的bundle包上传到线上环境,上传时同样需要使用到IDE插件。
3.6 使用效果
当使用faircli工具配置好本地的热更服务后,在移动设备上打开开发者选项,选择本地模式,输入电脑ip地址,就可以愉快的预览fair动态化效果啦。
五、Fair一键接入
5.1 安装插件
对于已有的Flutter项目,我们推荐使用pub形式进行集成。首先,在pubspec.yaml文件中添加依赖:
# add Fair dependency
dependencies:
fair: 3.0.0
# add build_runner and compiler dependency
dev_dependencies:
build_runner: ^2.0.0
fair_compiler: ^1.4.0
# switch "fair_version" according to the local Flutter SDK version
# Flutter SDK 3.3.x(3.3.0、3.3.1、3.3.2、3.3.3、3.3.4、3.3.5、3.3.6) -> flutter_3_3_0
# Flutter SDK 3.0.x(3.0.0、3.0.1、3.0.2、3.0.3、3.0.4、3.0.5) -> flutter_3_0_0
# Flutter SDK 2.10.x(2.10.0、2.10.1、2.10.2、2.10.3) -> flutter_2_10_0
# Flutter SDK 2.8.x(2.8.0、2.8.1) -> flutter_2_8_0
# Flutter SDK 2.5.x(2.5.0、2.5.1、2.5.2、2.5.3) -> flutter_2_5_0
# Flutter SDK 2.0.6 -> flutter_2_0_6
# Flutter SDK 1.22.6 -> flutter_1_22_6
dependency_overrides:
fair_version: 3.3.0
5.2 使用 Fair
以下是在App中接入Fair步骤如下:
5.2.1 注册FairApp
首先,将 FairApp 添加为需要动态化部分的顶级节点,常见做法是作为 App 的根节点,如果不是全局采用也可以作为子页面的根节点,如下所示。
void main() {
WidgetsFlutterBinding.ensureInitialized();
FairApp.runApplication(
_getApp(),
plugins: {
},
);
}
dynamic _getApp() => FairApp(
modules: {
},
delegate: {
},
child: MaterialApp(
home: FairWidget(
name: 'DynamicWidget',
path: 'assets/bundle/lib_src_page_dynamic_widget.fair.json',
data: {"fairProps": json.encode({})}),
),
);
5.2.2 添加动态组件
如果需要添加动态内容,那么每一个动态组件需要由一个FairWidget包裹,如下所示。
FairWidget(
name: 'DynamicWidget',
path: 'assets/bundle/lib_src_page_dynamic_widget.fair.json',
data: {"fairProps": json.encode({})}),
当然,根据使用场景诉求的不同,FairWidget还支持混合使用:
- 可以作为不同组件混合使用
- 一般作为一个全屏页面
- 支持嵌套使用,即可以局部嵌套在普通Widget下,也可以嵌套在另一个FairWidget下
参考代码:example
六、从0开始写一个Demo
下面我们通过改造Flutter官方的 counting 计数器示例说明如何集成 Fair 动态化。改造总共分为7步。
6.1 注册FairApp
首先,将 FairApp 添加为需要动态化部分的顶级节点。常见做法是作为 App 的根节点,如果不是全局采用也可以作为子页面的根节点。
void main() {
WidgetsFlutterBinding.ensureInitialized();
FairApp.runApplication(
_getApp(),
plugins: {},
);
}
dynamic _getApp() => FairApp(
modules: {},
delegate: {},
child: MyApp(),
);
6.2 添加动态组件
接下来,使用 @FairPatch() 注解标记需要动态化的 Widget。对于需要进行动态化改造的 Widget(无论是 StatefulWidget 还是 StatelessWidget),必须加上 @FairPatch() 注解。在 counting 示例中,我们想把 MyHomePage这个 StatefulWidget 变为一个动态页面,所以,我们需要为其加上 @FairPatch() 注解,如下所示。
@FairPatch()
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State createState() => _MyHomePageState();
}
需要说明的是,每一个 dart 文件里只能包含一个 @FairPatch。
6.3 外部参数
有时候,我们需要将一些参数传递给动态页面。对于这种场景,我们强烈建议通过一个 Map 来接收外部参数。由于 Map 类型的 value 值类型不确定,所以直接定义一个 dynamic 的变量即可,如下所示。
@FairPatch()
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, this.fairProps}) : super(key: key);
dynamic fairProps;
@override
State createState() => _MyHomePageState();
}
通常习惯上,我们将变量名定义为 fairProps。
6.4 定义与 JS 交互参数
在 _MyHomePageState类中,我们需要定义一个参数来与JS进行交互,并且需要使用@FairProps()进行标记。通常做法是也命名为 fairProps,当然,也可以自定义名称。
@FairProps()
var fairProps;
同时,fairProps的初始化需要在 initState()里进行。
@override
void initState() {
super.initState();
/// 需要将 widget.fairProps 赋值给 fairProps
fairProps = widget.fairProps;
}
如果需要在 build()方法里面的 UI Widget 需要使用到参数的话,统一通过 fairProps获取。
String getTitle() {
return fairProps['title'];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(getTitle()),
),
...
);
}
以下是_MyHomePageState的完整代码。
class _MyHomePageState extends State {
/// 定义与 JS 侧交互的参数,只支持 Map 类型的数据
///
/// 需要用 @FairProps() 注解标记
/// 变量名可以自定义,习惯上命名为 fairProps
@FairProps()
var fairProps;
int _counter = 0;
@override
void initState() {
super.initState();
/// 需要将 widget.fairProps 赋值给 fairProps
fairProps = widget.fairProps;
}
String getTitle() {
return fairProps['title'];
}
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(getTitle()),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
// 暂不支持 style: Theme.of(context).textTheme.headline4,
// 可替换成:
style: TextStyle(fontSize: 40, color: Color(0xffeb4237), wordSpacing: 0),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
需要说明的是,style: Theme.of(context).textTheme.headline4这样的语法暂不支持,可以使用 style: TextStyle(fontSize: 40, color: Color(0xffeb4237), wordSpacing: 0) 代替。
6.5 生成下发产物
接下来,我们运行一个 build_runner 命令,触发 Fair 的 Compiler 开始编译工作,生成下发产物了。
flutter pub run build_runner build
成功的提示:
产物的生成位置位于项目根目录/build/fair中。
编译成功后,在 build/fair 目录下可以找到与 @FairPatch() 所在的 dart 文件同名的资源,一般以 lib 开头。主要的产物有以下几个:
- fair.bin 格式为 release 产物
- fair.json 格式为 debug 产物
- fair.js 格式为逻辑转换为JS后的产物
- fair.metadata 格式为元数据,标记了源码与产物的关联信息
fair.json 主要是 debug 期间使用,因为 JSON 文件比较易读,便于排查错误。而 fair.bin主要是在 release 期间使用,它是使用 FlatBuffers 工具生成的一种二进制文件,好处是不用反序列化,大大的提升了 Fair 解析、加载资源的速度。
其中,我们将 JSON 文件(或 bin 文件)和 JS 文件合称为 bundle 资源。
我们建议你以 bin 文件和 JS 文件作为最终的 bundle 资源,因为 bin 文件无需反序列化,可以提升 Fair 的加载效率。而JSON 文件可以作为 debug 期间使用。
6.6 FairWidget 加载 bundle
bundle 资源生成好以后,我们可以先本地测试一下,先将 bundle 资源拷贝到 assets 目录下.然后,使用 FairWidget加载看效果(别忘了先在 yaml 中配置 assets 目录路径)。
使用 FairWidget 时,有两个主要的参数,分别是path和data,说明如下。
- path:bundle 资源的路径。
- data:传递给动态页面的参数。
其中,path 可以接受一个 assets 路径,如 'assets/bundle/lib_main.fair.json'。一般用来做本地调试的时候使用。path 也可以接受一个手机本地磁盘的路径,注意是绝对路径。比如将 bundle 资源托管到自己公司服务器上,运行期间下载存储到手机磁盘后,以 bundle 文件的磁盘路径作为 path。我们建议 Android 设备将 bundle 保存到 External Storage 目录,iOS 设备保存到 Application Documents 目录下。
第二个参数是 data,data 是传递给动态页面的参数,data 是一个 Map
data: {
/// 此处的 key 必须是 fairProps,不可以自定义
/// value 是一个 Map 类型的数据,最好是进行 jsonEncode() 操作
'fairProps': jsonEncode({'title': '你好'})
}
以下是一段完整代码:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// home: MyHomePage(title: 'Flutter Demo Home Page'),
/// FairWidget 是用来加载 bundle 资源的容器
///
/// path 参数:需要加载的 bundle 资源文件路径
/// data 参数:需要传递给动态页面的参数
home: FairWidget(
/// path 可以是 assets 目录下的 bundle 资源,也可以是手机存储
/// 里的 bundle 资源,如果是手机存储里的 bundle 资源需要使用绝对路径
path: 'assets/bundle/lib_main.fair.json',
data: {
/// 此处的 key 必须是 fairProps,不可以自定义
/// value 是一个 Map 类型的数据,最好是进行 jsonEncode() 操作
'fairProps': jsonEncode({'title': '你好'})
}));
}
}
然后,我们重新运行一下项目,效果如下。
6.7 将 bundle 托管到服务器
本地调试没有问题后,就可以将 bundle 资源(bin 文件 + JS 文件)上传到自己公司服务器上,或者直接使用58开源的服务器FairPushy。
FairPushy 是基于Flutter+Dart三端一体化动态更新平台,为Flutter Fair设计的动态化框架提供动态分发能力。主要有Flutter Web平台、Dart Server和移动端SDK组成,使用统一的技术Dart语言开发。
最后,贴一下改造后的 counting 计数器例子的完整代码:
import 'dart:convert';
import 'package:fair/fair.dart';
import 'package:flutter/material.dart';
void main() {
// runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized();
FairApp.runApplication(
_getApp(),
plugins: {},
);
}
dynamic _getApp() => FairApp(
modules: {},
delegate: {},
child: MyApp(),
);
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// home: MyHomePage(title: 'Flutter Demo Home Page'),
/// FairWidget 是用来加载 bundle 资源的容器
///
/// path 参数:需要加载的 bundle 资源文件路径
/// data 参数:需要传递给动态页面的参数
home: FairWidget(
/// path 可以是 assets 目录下的 bundle 资源,也可以是手机存储
/// 里的 bundle 资源,如果是手机存储里的 bundle 资源需要使用绝对路径
path: 'assets/bundle/lib_main.fair.json',
data: {
/// 此处的 key 必须是 fairProps,不可以自定义
/// value 是一个 Map 类型的数据,最好是进行 jsonEncode() 操作
'fairProps': jsonEncode({'title': '你好'})
}));
}
}
@FairPatch()
class MyHomePage extends StatefulWidget {
// const MyHomePage({Key? key, required this.title}) : super(key: key);
// final String title;
MyHomePage({Key? key, this.fairProps}) : super(key: key);
// 通常习惯上,我们将变量名定义为 fairProps
dynamic fairProps;
@override
State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
/// 定义与 JS 侧交互的参数,只支持 Map 类型的数据
///
/// 需要用 @FairProps() 注解标记
/// 变量名可以自定义,习惯上命名为 fairProps
@FairProps()
var fairProps;
int _counter = 0;
@override
void initState() {
super.initState();
/// 需要将 widget.fairProps 赋值给 fairProps
fairProps = widget.fairProps;
}
String getTitle() {
return fairProps['title'];
}
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(getTitle()),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
// 暂不支持 style: Theme.of(context).textTheme.headline4,
// 可替换成:
style: TextStyle(fontSize: 40, color: Color(0xffeb4237), wordSpacing: 0),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
七、配套开发工具
7.1 Faircli配套工具链
官方为了让开发者快速上手,降低接入门槛, 解决在接入过程中的痛点。 Fair团队开发了Faircli配套工具链,主要包含三个部分:
- 工程创建:快速搭建Fair载体工程及动态化工程。
- 模板代码:提供页面及组件模板。
- 本地热更新:线下开发使用,实现开发阶段快速预览Fair动态化功能。
在安装了工具链提供的dart命令行工具及AS插件后, 通过创建模板, 构建产物, 本地启服务,体验热更新功能,开发者可以轻松接入并体验Fair。
7.2 Fair语法检测插件
官方为了让开发者在Fair开发过程中,出现不正确或者不支持的语法问题。 开发了配套插件去提示用户使用Fair语法糖。
查看以下示例:
1,build方法下if的代码检测,及提示引导信息:
2, 点击 more action 或者 AS 代码提示快捷键
3, 根据提示点击替换
通过插件,在编写 fair 过程中,可以快速识别并解决不支持的语法问题。提高开发 Fair 效率。
八、FairPushy热更新平台
从初次接触Server开发者角度分析主要分为两个流程开发阶段和部署阶段。
8.1 同步FairPushy Server
首先,将FairPushy代码同步到本地,并且修改本地配置信息进行部署。
git clone https://github.com/wuba/FairPushy.git
8.2 修改数据库配置
FairPushy项目使用的数据库是基于MySql在远端部署,因此需要填写MySql表名、密码、端口等信息。切换到FairServer工程找到bin/config.dart修改以下信息:
const settingsYaml = '''
mysql_user:
mysql_password:
mysql_host:
mysql_port:
mysql_database:
''';
8.3 测试服务
FairPushy工程可以直接运行启动测试服务仅需在控制台输入以下命令:
dart run bin/server.dart
控制台打印:FairServer ready 证明服务已正常启动,在浏览器输入"获取资源文件接口"检查服务是否正常。
http://127.0.0.1:8080/app/patch
正常情况会收到以下提示:
{
"code": -2,
"data": null,
"msg": "bundleId==null"
}
8.4 表创建
由于部分远端DB限制,目前需要手动创建表,提供以下创建语句和表结构:
CREATE TABLE `app_info` (
`app_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`app_name` varchar(45) DEFAULT NULL COMMENT '项目名字',
`app_key` varchar(45) DEFAULT NULL COMMENT '项目唯一标识',
`app_description` varchar(300) DEFAULT NULL COMMENT '项目描述',
`app_pic_url` varchar(300) DEFAULT NULL COMMENT '项目图片',
`user_member` varchar(255) DEFAULT NULL COMMENT '项目成员',
`patch_list` varchar(255) DEFAULT NULL COMMENT '补丁列表',
`version_name` varchar(20) DEFAULT NULL COMMENT '版本名字',
`remark` varchar(300) DEFAULT NULL COMMENT '版本备注',
`create_name` varchar(45) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`app_id`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8 COMMENT='app项目信息';
CREATE TABLE `online_build` (
`buildId` int(11) NOT NULL AUTO_INCREMENT COMMENT '在线构建任务id',
`patchGitUrl` varchar(300) DEFAULT NULL COMMENT '补丁项目git地址',
`patchGitBranch` varchar(45) DEFAULT NULL COMMENT '补丁项目git分支',
`patchBuildName` varchar(300) DEFAULT NULL COMMENT '补丁项目构建成功后压缩包名称',
`flutterVersion` varchar(45) DEFAULT NULL COMMENT '构建项目时使用的flutter版本',
`buildStatus` int(11) NOT NULL COMMENT '在线构建任务状态 0:成功 1:失败 2:构建中',
`patchWosUrl` varchar(300) DEFAULT NULL COMMENT '在线构建任务成功后,资源上传到wos后生成的地址 默认为空传,buildStatus为0时该值有效',
`errorLogUrl` varchar(300) DEFAULT NULL COMMENT '在线构建任务失败的日志 默认为空传,buildStatus为1时该值有效',
`buildStartTime` datetime DEFAULT NULL COMMENT '构建任务开始时间',
`buildFinishTime` datetime DEFAULT NULL COMMENT '构建任务结束时间',
PRIMARY KEY (`buildId`)
) ENGINE=InnoDB AUTO_INCREMENT=93 DEFAULT CHARSET=utf8 COMMENT='在线构建';
CREATE TABLE `operation_record` (
`record_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`operation_time` datetime DEFAULT NULL COMMENT '操作时间',
`operator` varchar(45) DEFAULT NULL COMMENT '操作者',
`operatio_content` varchar(255) DEFAULT NULL COMMENT '操作内容',
`app_key` varchar(100) DEFAULT NULL COMMENT 'app唯一表',
`version_name` varchar(100) DEFAULT NULL COMMENT 'app版本',
PRIMARY KEY (`record_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='操作记录';
CREATE TABLE `patch_info` (
`bundle_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '补丁id',
`app_id` varchar(255) DEFAULT NULL COMMENT '项目id',
`patch_url` varchar(300) NOT NULL COMMENT '补丁地址url',
`status` varchar(10) DEFAULT NULL COMMENT '补丁状态:1下发中,2回滚',
`remark` varchar(255) DEFAULT NULL COMMENT '补丁备注',
`bundle_version` varchar(20) DEFAULT NULL COMMENT '版本名字',
`version_code` varchar(20) DEFAULT NULL COMMENT '版本号',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`bundle_name` varchar(20) NOT NULL DEFAULT '' COMMENT '补丁名字',
`patchGitUrl` varchar(300) DEFAULT NULL COMMENT '补丁项目git地址',
`patchGitBranch` varchar(45) DEFAULT NULL COMMENT '补丁项目git分支',
`flutterVersion` varchar(45) DEFAULT NULL COMMENT '构建项目时使用的flutter版本',
PRIMARY KEY (`bundle_id`)
) ENGINE=InnoDB AUTO_INCREMENT=136 DEFAULT CHARSET=utf8 COMMENT='补丁信息';
8.5 接口说明
项目中主要提供Web平台的接口和移动端SDK调用接口一共大概10个左右,主流程接口如下:
序号 | 接口地址 | 功能描述 |
---|---|---|
1 | /web/createApp | 创建项目 |
2 | /web/getAppList | 获取项目列表 |
3 | /web/module_patch | 补丁列表 |
4 | /web/create_patch | 上传补丁 |
5 | /web/operating_record | 操作记录 |