APP 动态化路由框架:TheRouter

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: `TheRouter` 是一个 Kotlin 编写,用于 Android 模块化开发的一整套解决方案框架。 Github 项目地址与使用文档详见https://github.com/HuolalaTech/hll-wp-therouter-android

hll.png

TheRouter 是一个 Kotlin 编写,用于 Android 模块化开发的一整套解决方案框架。
Github 项目地址与使用文档详见 https://github.com/HuolalaTech/hll-wp-therouter-android

TheRouter 核心功能具备如下能力:

  • 页面导航跳转能力(Navigator)
  • 跨模块依赖注入能力(ServiceProvider)
  • 单模块自动初始化能力(FlowTaskExecutor)
  • 动态化能力(ActionManager)
  • 模块AAR/源码依赖一键切换脚本

一、为什么要使用 TheRouter

路由是现如今移动端开发中必不可少的功能,尤其是企业级APP,可以用于将Intent页面跳转的强依赖关系解耦,同时减少跨团队开发的互相依赖问题。

对于大型 APP 开发,基本都会选用模块化(或组件化)方式开发,对于模块间解耦要求更高。 TheRouter 是一整套完全面向模块化开发的解决方案,不仅能支持常规的模块依赖解耦、页面跳转,同时提供了模块化过程中常见问题的解决办法。例如:完美解决了模块化开发后由于组件内无法获取 Application 生命周期与业务流程,造成每次初始化与关联依赖调用都需要跨模块修改代码的问题。

1.1 TheRouter 四大能力

Navigator:

  • 支持 ActivityFragment
  • 支持Path与页面多对一关系或一对一关系,可用于解决多端path统一问题
  • 页面Path支持正则表达式声明
  • 支持 json 格式路由表导出
  • 支持动态下发 json 路由表,降级任意页面为H5
  • 支持任意object跨模块传递(无需序列化,且能保证对象类型)
  • 支持页面跳转拦截处理
  • 支持自定义页面参数解析方式(例如将json解析为对象)
  • 支持使用路由跳转到第三方 SDK 中的Activity(Fragment)

ServiceProvider:

  • 支持跨模块依赖注入
  • 支持自定义注入项的创建规则,依赖注入可自定义参数
  • 支持自定义服务拦截,单模块mock调试
  • 支持注入对象缓存,多次注入 只会new一次对象

FlowTaskExecutor:

  • 支持单模块独立初始化
  • 支持懒加载初始化
  • 独立初始化允许多任务依赖(参考Gradle Task)
  • 支持编译期循环引用检测
  • 支持自定义业务初始化时机,可以用于解决隐私合规问题

ActionManager:

  • 支持全局回调配置
  • 支持优先级响应与中断响应
  • 支持记录调用路径,解决调试期观察者模式无法追踪Observable的问题

注: FlowTaskExecutorActionManager 后续会作为可选能力,提供可插拔单独使用的选项(预计10月份提供)。

二、路由方案

目前现有的路由基本上集中于两种能力的实现:页面跳转、跨模块调用,核心技术方案大体上如图:

1.png

  1. 开发阶段,对要使用路由的落地页或被调用方法添加注解标识。
  2. 编译期解析注解,生成一系列中间代码,待调用。
  3. 应用启动后调用中间代码完成路由的准备动作。大部分路由会额外通过 Gradle Transform,在编译期做一次聚合,以提升运行时准备路由表的效率。
  4. 发起路由跳转时,本质上就是一次路由表遍历,通过uri获取到对应的落地页或方法对象,进行调用。

TheRouter 的页面跳转、跨模块调用也是如此,但是在设计上会有一些细节处理。

2.png

TheRouter 会在编译期根据注解生成 RouteMap__开头的类,这些类中记录了当前模块的所有路由信息,也就是当前模块的路由表。

在最顶层的app模块中,通过Gradle插件,将所有aar、源码中的RouteMap__开头的类统一集中到TheRouterServiceProvideInjecter类中。

后续应用启动后,初始化路由时只需要执行TheRouterServiceProvideInjecter类的方法,就能没有任何反射的加载到全部的路由表了。

加载以后的路由表会被保存到一个支持正则匹配的 Map 中,这也是TheRouter允许多个path对应同一个落地页的原因。每当发生页面跳转时,通过跳转时的path,去Map中获取到对应的落地页信息,再正常调用startActivity()即可。

三、使用 TheRouter 页面跳转

3.1 声明路由项

如果一个页面(支持 Activity、Fragment)允许被路由打开,则需要使用注解 @Route 声明路由项,每个页面允许声明多个路由项,也就是一对多的能力,极大降低多端路由统一时的业务影响面。

参数释义

  • path: 路由path 【必传】。
    建议是一个url。path内支持使用正则表达式(为了匹配效率,正则必须包含反双斜杠\),允许多个path对应同一个Activity(Fragment)。
  • action: 自定义事件【可选】。
    一般用来打开目标页面后做一个执行动作,例如自定义页面弹出广告弹窗。
  • description: 页面描述【可选】。
    会被记录到路由表中,方便后期排查的时候知道每个path或Activity是什么业务。
  • params: 页面参数【可选】。
    自动写入intent中,允许写在路由表中动态下发修改默认值,或通过路由跳转时代码传入。

    @Route(path = "http://therouter.com/home", action = "action://scheme.com",

public class HomeActivity extends AppCompatActivity {
}


#### 3.2 发起页面跳转    

传入的参数可以是 `String` 和8种基本数据类型、也可以是`Bundle`、`Serializable`、
`Parcelable`对象,跟 `Intent` 传值规则一致。   
同时也支持为本次跳转的 `Intent` 添加`Flag/Uri/ClipData/identifier`等业务特殊参数。   

// 传入参数可以通过注解 @Autowired 解析成任意类型,如果是对象建议传json
// context 参数如果不传或传 null,会自动使用 application 替换
TheRouter.build("http://therouter.com/home")

    .withInt("key1", 12345678)
    .withString("key2", "参数")
    .withBoolean("key3", false)
    .withSerializable("key4", object)
    .withObject("object", any) // 这个方法可以传递任意对象,但是接收的地方对象类型需自行保证一致,否则会强转异常
    .navigation(context);

    // 如果传入 requestCode,默认使用startActivityForResult启动Activity
    .navigation(context, 123);

    // 如果要打开的是fragment,需要使用
    .createFragment();

#### 3.3 路由表生成规则  

如果两条路由的`path`、目标`className`完全相同,则认为是同一条路由,**不会考虑参数是否相同**。   
路由表生成规则:编译期按照如下顺序取**并集**。   

**覆盖规则**:   
根据如下顺序,如果相同,后者可以覆盖前者的路由表规则。   

1. 编译期解析注解生成路由表
2. 首先取 `业务模块 aar` 中的路由表
3. 再取 主`app module` 代码中的路由表
4. 最后取 `assets/RouteMap.json` 文件中声明的路由表。
  - 如果编译期没有这个文件,会生成一份默认路由表放在这个目录内;如果有,会将路由表合并。  
  - 路由表生成时可配置是否启用检查路由合法性,判断目标页面是否存在,(warning/error)级别。  
5. 运行时线上动态下发的路由表  
  - 路由表允许线上动态下发,将覆盖本地路由表,详见 【3.4 动态路由表的设计与使用】  
 
如果编译期没有这个文件,会生成一份默认路由表放在这个目录内;如果有,会将路由表合并,因此,对于没办法修改代码的第三方SDK内部,如果希望通过路由打开,只需要手动在`RouteMap.json`文件中声明,就能通过路由打开了。    


#### 3.4 动态路由表的设计与使用

`TheRouter` 的路由表是动态添加的,项目每次编译后,会在 apk 内生成一份当前 APP 的全量路由表,默认路径为:`/assets/therouter/routeMap.json`。这个路由表也可以后续通过远程下发的方式使用,例如远端可以针对不同的APP版本,下发不同的路由表达到配置目的。这样如果将来线上某些页面发生Crash,可以通过将这个页面的落地页替换为H5的方式,临时解决这类问题。    

有两种推荐的远程下发方式可供使用方选择:

1. 将打包系统与配置系统打通,每次新版本APP打包后自动将`assets/`目录中的配置文件上传到配置系统,下发给对应版本APP 。优点在于全自动不会出错。
2. 配置系统无法打通,线上手动下发需要修改的路由项,因为 `TheRouter` 会自动用最新下发的路由项覆盖包内的路由项。优点在于精确,且流量资源占用小。

注:一旦你设置了自定义的`InitTask`,原框架内路由表初始化任务将不再执行,你需要自己处理找不到路由表时的兜底逻辑,一种建议的处理方式见如下代码。

// 此代码 必须 在 Application.super.onCreate() 之前调用
RouteMap.setInitTask(new RouterMapInitTask() {

/** 
 * 此方法执行在异步
 */
@Override
public void asyncInitRouteMap() {
    // 此处为纯业务逻辑,每家公司远端配置方案可能都不一样
    // 不建议每次都请求网络,否则请求网络的过程中,路由表是空的,可能造成APP无法跳转页面
    // 最好是优先加载本地,然后开异步线程加载远端配置
    String json = Connfig.doHttp("routeMap");
    // 建议加一个判断,如果远端配置拉取失败,使用包内配置做兜底方案,否则可能造成路由表异常
    if (!TextUtils.isEmpty(json)) {
        List<RouteItem> list = new Gson().fromJson(json, new TypeToken<List<RouteItem>>() {
        }.getType());
        // 建议远端下发路由表差异部分,用远端包覆盖本地更合理
        RouteMap.addRouteMap(list);
    } else {
        // 在异步执行TheRouter内部兜底路由表
        initRouteMap()
    }
}

});


#### 3.5 高级用法

TheRouter同时支持更多页面跳转能力,详情可参考项目文档【[https://github.com/HuolalaTech/hll-wp-therouter-android/wiki/Navigator](https://github.com/HuolalaTech/hll-wp-therouter-android/wiki/Navigator)】:    

- 为第三方库里面的页面添加路由表,达到对某些页面降级替换的目的;  
- 延迟路由跳转(从Android 8开始,不能在后台启动页面);  
- 跳转过程拦截器(总共四层,可根据实际需求使用);  
- 跳转结果回调;

### 四、跨模块依赖注入 ServiceProvider 的设计  

对于模块化开发中跨模块的调用,我们推荐采用 [SOA(面向服务架构)](https://zh.m.wikipedia.org/zh-cn/%E9%9D%A2%E5%90%91%E6%9C%8D%E5%8A%A1%E7%9A%84%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84) 的设计方式,服务调用方与使用方完全隔离,调用模块外的能力不需要关注能力的提供者是谁。  
`ServiceProvider` 的核心设计思想也是这样的,目前服务间的调用协议采用接口的方式。当然,也可以兼容不通过接口下沉而是直接调用的情况。  


![4.jpeg](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6dbabea09faa4a598faf46ffd7462de9~tplv-k3u1fbpfcp-watermark.image?)

具体到 Android 侧就是 AIDL 类似的设计,只是要比AIDL开发简单很多:  

- 服务提供方负责提供服务,不需要关心调用方是谁会在何时调用自己。  
- 服务的使用方只关注服务本身,不需要关心这个服务是谁提供的,只需要只能服务能提供哪些能力即可。  

例如上面的图片:拉拉需要使用录音的服务,小货则向外提供一个录音的服务,由`TheRouter`的`ServiceProvider`负责撮合。  

#### 4.1 服务使用方:拉拉

她无需关心,`IRecordService`这个接口服务是谁提供的,他只需要知道自己需要使用这样的一个服务就行了。  
注:如果没有提供服务的提供方,`TheRouter.get()`可能返回`null`  

TheRouter.get(IRecordService::class.java)?.doRecord()



#### 4.2 服务提供方:小货  

服务提供方需要声明一个提供服务的方法,用`@ServiceProvider`注解标记。  

- 如果是 `java`,必须是 `public static` 修饰
- 如果是 `kotlin`,建议写成 top level 的函数
- 方法名不限

/**

  • 方法名不限定,任意名字都行
  • 返回值必须是服务接口名,如果是实现了服务的子类,需要加上returnType限定(例如下面代码)
  • 方法必须加上 public static 修饰,否则编译期就会报错

*/
@ServiceProvider
public static IRecordService test() {

return new IRecordService() {
    @Override
    public void doRecord() {
        String str = "执行录制逻辑";
    }
};

}

// 也可以直接返回对象,然后标注这个方法的服名是什么
@ServiceProvider(returnType = IRecordService.class)
public static RecordServiceImpl test() {

// xxx 

}


### 五、单模块自动初始化能力 FlowTaskExecutor 的设计  

前面讲过,`TheRouter`是完全面向模块化开发提供的一套解决方案。在模块化开发时,可能每个模块都有自己需要初始化的一些代码。以前的做法是把这些代码都在`Application`里声明,但是这样可能随着业务变动每次都需要修改`Application`所在模块。`TheRouter` 的单模块自动初始化能力就是为了解决这样的情况,可以只在当前模块声明初始化方法后,将会在业务场景时自动被调用。    

每个希望被自动初始化的方法,必须使用`public static`修饰,主要原因是这样子就能通过类名直接调用了。另外很多初始化代码都需要获取`Context`对象,所以我们将`Context`作为初始化方法的默认参数,会自动传入`Application`。其他的所在类名、方法名都没有限制,反正只要加上了 `@FlowTask` 注解,在编译期都能通过 APT 获取到。   


#### 5.1 FlowTaskExecutor 使用介绍

可以在当前模块中,任意类中声明一个任意方法名的方法,给方法添加上`@FlowTask` 的注解即可。   

`@FlowTask` 注解参数说明: 

- **taskName**:当前初始化任务的任务名,必须全局唯一,建议格式为:`moduleName_taskName`
- **dependsOn**:参考`Gradle` Task,任务与任务之间可能会有依赖关系。如果当前任务需要依赖其他任务先初始化,则在这里声明依赖的任务名。可以同时依赖多个任务,用英文逗号分隔,空格可选,会被过滤:dependsOn = "mmkv, config, login",默认为空,应用启动就被调用
- **async**:是否要在异步执行此任务,默认false。

/**

  • 将会在异步执行

*/
@FlowTask(taskName = "mmkv_init", dependsOn = TheRouterFlowTask.APP_ONCREATE, async = true)
public static void test2(Context context) {

System.out.println("异步=========Application onCreate后执行");

}

@FlowTask(taskName = "app1")
public static void test3(Context context) {

System.out.println("main线程=========应用启动就会执行");

}

/**

  • 将会在主线程初始化

*/
@FlowTask(taskName = "test", dependsOn = "mmkv,app1")
public static void test3(Context context) {

System.out.println("main线程=========在app1和mmkv两个任务都执行以后才会被执行");

}


#### 5.2内置初始化节点  

使用这个能力,在路由内部默认支持了两个生命周期类任务,可在使用时直接引用  

*  **TheRouterFlowTask.APP_ONCREATE**:当Application的onCreate()执行后初始化  
* **TheRouterFlowTask.APP_ONSPLASH**:当应用的首个Activity.onCreate()执行后初始化

同时,使用`TheRouter`的自动初始化依赖,也无需担心循环依赖造成的问题,框架会在编译期构建有向无环图,监测循环依赖情况,如果发现会在编译期直接报错,并且还会将发生循环引用的任务显示出来,用于排错。   


#### 5.3  实现原理

每个加了 `@FlowTask` 注解的方法,都会在编译期被解析,生成一个对应的 `Task` 对象,这个对象包含了初始化方法的相关信息,比如:是否异步执行、任务名、是否依赖其他任务先执行。   

当所有aar都编译完成,生成好全部的 `Task` 以后,会在主 app 中通过`Gradle`插件进行聚合,在这时会将所有的 `Task` 做一次检查,通过构建`有向无环图`来防止 `Task` 发生循环引用的情况。  

每次应用启动后,会在路由初始化时,将有向图中的全部`Task`,按照依赖关系按顺序加载。  


![3.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b400b35a3c0244479ef61ce6759b25a7~tplv-k3u1fbpfcp-watermark.image?)  

### 六、动态化能力 ActionManager 的设计   

`Action` 本质是一个全局的系统回调,主要用于预埋的一系列操作,例如:弹窗、上传日志、清理缓存。  
与 Android 系统自带的广播通知类似,你可以在任何地方声明动作与处理方式。并且所有`Action`都是可以被跟踪的,只要你愿意,可以在日志中将所有的动作调用栈输出,以方便调试使用,这样在一定程度上可以解决观察者模式带来的通病:**无法追踪`Observable`的问题**。

#### 6.1 Action 使用  

声明一个 Action:

// action建议遵循一定的格式
const val ACTION = "therouter://action/xxx"

@FlowTask(taskName="action_demo")
fun init(context: Context) =

TheRouter.addActionInterceptor(ACTION, object: ActionInterceptor() {
    override fun handle(context: Context, args: Bundle): Boolean {
        // do something
        return false
    }
})

执行一个 Action:

// action建议遵循一定的格式
const val ACTION = "therouter://action/xxx"

// 如果执行了一个没有被声明的Action,则不会有任何动作
TheRouter.build(ACTION).action()


#### 6.2 高级用法

每个`Action` 允许关联多个 `ActionInterceptor`进行处理,多个`ActionInterceptor`之间可以自定义拦截器优先级,同时允许终止接下来的低优先级拦截器的执行。   

最典型应用场景:首页可能会有多个弹窗,不同业务之间的弹窗是有优先级之分的,为了体验优化我们肯定不会在首页一次把所有弹窗全部弹出,可以通过`ActionInterceptor`为每个弹窗声明好优先级关系,假设需求是首页只能弹出3个弹窗,那么第三个弹窗处理完毕即可关闭当前事件,接下来的拦截器将不会被响应。

abstract class ActionInterceptor {

abstract fun handle(context: Context, args: Bundle): Boolean

fun onFinish() {}

/**
 * 数字越大,优先级越高
 */
open val priority: Int
    get() = 5

}


#### 6.3 客户端动态响应使用场景

**如果仅客户端使用**,常用的场景可能是:当用户执行某些操作(打开某个页面、H5点击某个按钮、动态页面配置的点击事件)时,将会自动触发,执行预埋的 Action 逻辑。   

**如果与服务端链路打通**,这个能力其实是需要整个公司的配合,比如有一套类似智慧大脑的方案,可以基于客户端过去的一些埋点数据,智能推断出用户下一步要做的事情,然后通过长连接直接向客户端下发指令做某些事情。那么通过客户端预埋的页面跳转、弹窗、清缓存、退出登录等等操作,就可以通过服务端指令进行操作,则就是一套完整的动态化方案。   

![TheRouter-ActionManager](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8bae49fec9d94ae2a3cdf2d07b39efa6~tplv-k3u1fbpfcp-zoom-1.image)  


### 七、一键切换源码与 AAR  

#### 7.1 模块化支持的 Gradle 脚本

在模块化开发过程中,如果没有采用分仓,或采用了分仓但依然使用 `git-submodule` 的方式开发,应该都会遇到一个问题。如果集成包采用源码编译,构建时间实在太久,大大降低开发调试效率;如果采用aar依赖编译,对于底层模块修改了代码,每次都要重新构建aar,在上层模块修改版本号以后,才能继续整包构建编译,也极大影响开发效率。  
`TheRouter` 中提供了一个 `Gradle` 脚本,只需要在开发本地的`local.properties`文件中声明要参与编译的`module`,其他未声明的默认使用aar编译,这样就能灵活切换源码与aar,并且不会影响其他人,如下节选代码可供参考使用:  

/**

  • 如果工程中有源码,则依赖源码,否则依赖aar

*/
def moduleApi(String compileStr, Closure configureClosure) {

String[] temp = compileStr.split(":")
String group = temp[0]
String artifactid = temp[1]
String version = temp[2]

Set<String> includeModule = new HashSet<>()
rootProject.getAllprojects().each {
    if (it != rootProject) includeModule.add(it.name)
}

if (includeModule.contains(artifactid)) {
    println(project.name + "源码依赖:===project(\":$artifactid\")")
    projects.project.dependencies.add("api", project(':' + artifactid), configureClosure)

// projects.project.configurations { compile.exclude group: group, module: artifactid }

} else {
    println(project.name + "依赖:=======$group:$artifactid:$version")
    projects.project.dependencies.add("api", "$group:$artifactid:$version", configureClosure)
}

}


在实际使用时,可以完全使用`moduleApi` 替换掉原有的`api`。当然, `implementation`也可以有一个对应的`moduleImplementation`,这样只需要注释或解注释`setting.gradle`文件内的`include`语句就可以达到切换源码、`aar`的目的了。

### 八、从其他路由迁移至 TheRouter

#### 8.1 迁移工具一键迁移

`TheRouter`提供了图形化界面的迁移工具,可以一键从其他路由迁移到`TheRouter`,目前仅支持`ARouter`,其他路由框架迁移也在开发中(GitHub下载,70M左右,请耐心等待):  

- Mac OS 迁移工具下载:[https://github.com/HuolalaTech/hll-wp-therouter-android/wiki/uploads/file/TheRouterTransfer-Mac.zip](https://github.com/HuolalaTech/hll-wp-therouter-android/wiki/uploads/file/TheRouterTransfer-Mac.zip) 
- Windows 迁移工具下载:[https://github.com/HuolalaTech/hll-wp-therouter-android/wiki/uploads/file/TheRouterTransfer-Windows.zip](https://github.com/HuolalaTech/hll-wp-therouter-android/wiki/uploads/file/TheRouterTransfer-Windows.zip)

如果项目中使用了ARouter的IProvider.init()方法,可能需要手动处理初始化逻辑。  
如下图:  

  
![1-2.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7f2cc2b3509f41ccaa11dc9617088033~tplv-k3u1fbpfcp-watermark.image?)

#### 8.2 与其他路由对比

|功能|TheRouter|ARouter|WMRouter|
|---|---|---|---|
|Fragment路由|✔️|✔️|✔️|
|支持依赖注入|✔️|✔️|✔️|
|加载路由表|无运行时扫描<br>无反射|运行时扫描dex<br>反射实例类<br>性能损耗大|运行时读文件<br>反射实例类<br>性能损耗中|
|注解正则表达式|✔️|✖️|✔️|
|Activity指定拦截器|✔️(四大拦截器可根据业务定制)|✖️|✔️|
|导出路由文档|✔️(路由文档支持添加注释描述)|✔️|✖️|
|动态注册路由信息|✔️|✔️|✖️|
|APT支持增量编译|✔️|✔️(开启文档生成则无法增量编译)|✖️|
|plugin支持增量编译|✔️|✖️|✖️|
|多 Path 对应同一页面(低成本实现双端path统一)|✔️|✖️|✖️|
|远端路由表下发|✔️|✖️|✖️|
|支持单模块独立初始化|✔️|✖️|✖️|
|支持使用路由打开第三方库页面|✔️|✖️|✖️|
|支持使用路由打开第三方库页面|✔️|✖️|✖️|
|对热修复支持(例如tinker)|✔️(未改变的代码多次构建无变动)|✖️(多次构建apt产物会发生变化,生成无意义补丁)|✖️(多次构建apt产物会发生变化,生成无意义补丁)|  


### 九、总结

`TheRouter` 并不仅仅是一个小巧灵活的路由库,而是一整套完整的 `Android` 模块化解决方案,能够解决几乎全部的模块化过程中会遇到的问题。  
对于现有的路由框架,我们也在最大限度支持平滑迁移,目前已完成`ARouter`的一键迁移工具,其他框架的迁移仍在开发中。你也可以在`Github` `issue`中提出需求,我们评估后会尽快支持,也欢迎任何人提供 `Pull Requests`。  

更多问题请访问:[详细沟通](https://kymjs.com/therouter/wx/)    
目录
相关文章
|
23天前
|
UED
|
2月前
|
开发框架 JavaScript 前端开发
uni-app x 跨平台开发框架
uni-app x 是一个强大的跨平台开发框架 uni-app x 是一个庞大的工程,它包括uts语言、uvue渲染引擎、uni的组件和API、以及扩展机制。
77 1
|
5月前
|
开发框架 移动开发 JavaScript
SpringCloud微服务实战——搭建企业级开发框架(四十七):【移动开发】整合uni-app搭建移动端快速开发框架-添加Axios并实现登录功能
在uni-app中,使用axios实现网络请求和登录功能涉及以下几个关键步骤: 1. **安装axios和axios-auth-refresh**: 在项目的`package.json`中添加axios和axios-auth-refresh依赖,可以通过HBuilderX的终端窗口运行`yarn add axios axios-auth-refresh`命令来安装。 2. **配置自定义常量**: 创建`project.config.js`文件,配置全局常量,如API基础URL、TenantId、APP_CLIENT_ID和APP_CLIENT_SECRET等。
216 60
|
3月前
|
Python Windows 内存技术
【Azure 应用服务】Azure App Service (Windows) 使用Flask框架部署Python应用,如何在代码中访问静态文件呢?如何设置文件路径?是相对路径还是绝对路径呢?
【Azure 应用服务】Azure App Service (Windows) 使用Flask框架部署Python应用,如何在代码中访问静态文件呢?如何设置文件路径?是相对路径还是绝对路径呢?
路由不跳转,只留在首页,写的样式写到了App.vue中,没使用router-view
路由不跳转,只留在首页,写的样式写到了App.vue中,没使用router-view
路由不跳转,只留在首页,写的样式写到了App.vue中,没使用router-view
|
5月前
|
开发框架 小程序 前端开发
uni-app前端应用开发框架
uni-app对做移动端开发的来说应该无人不知、无人不晓了吧?!从名字就能看出来这个框架要干啥,unify app——没错,就是统一前端应用开发,不管你是小程序、app、还是H5、公众号,用这个框架都能做。uni-app让开发者编写一套代码,就可以编译为安卓app、ios app、微信小程序、抖音小程序、支付宝小程序等十几个平台,而且马上支持纯血鸿蒙了,这简直是个人、开发工作室、小型开发公司的福音,开发一些常规的app、小程序,用这个框架足够了。
67 7
|
5月前
|
安全 测试技术 网络安全
APP攻防-资产收集篇&反证书检验&XP框架&反代理VPN&数据转发&反模拟器
APP攻防-资产收集篇&反证书检验&XP框架&反代理VPN&数据转发&反模拟器
|
5月前
|
安全 JavaScript 前端开发
kotlin开发安卓app,JetPack Compose框架,给webview新增一个按钮,点击刷新网页
在Kotlin中开发Android应用,使用Jetpack Compose框架时,可以通过添加一个按钮到TopAppBar来实现WebView页面的刷新功能。按钮位于右上角,点击后调用`webViewState?.reload()`来刷新网页内容。以下是代码摘要:
|
5月前
|
缓存 Android开发 Kotlin
【安卓app开发】kotlin Jetpack Compose框架 | 先用OKhttp下载远程音频文件再使用ExoPlayer播放
使用 Kotlin 的 Jetpack Compose 开发安卓应用时,可以结合 OkHttp 下载远程音频文件和 ExoPlayer 进行播放。在 `build.gradle` 添加相关依赖后,示例代码展示了如何下载音频并用 ExoPlayer 播放。代码包括添加依赖、下载文件、播放文件及简单的 Compose UI。注意,示例未包含完整错误处理和资源释放,实际应用需补充这些内容。