如何更好地进行 Android 组件化开发(一)实战篇

4,711 阅读7分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

前言

我们通常会在一个项目工程 (Project) 里开发,该工程包含了 App 的所有业务。最开始我们会在一个 app 模块里实现业务功能,之后随着业务增加,代码量越来越多,编译时间越来越长。可能会抽取一些业务代码到新的模块,但是模块之间还是存在着错综复杂的依赖关系,因为有跳转页面、传递数据等需求,耦合度很高,这会导致以下痛点:

  • 任何修改都要编译整个项目工程,经常编译一次需要非常久。项目越大,编ProcessOn译的时间越长。
  • 业务模块耦合度很高,导致业务功能难以复用,即使把整个模块代码导入到别的项目工程也很难编译通过。
  • 多人协作开发容易相互影响,代码合并经常冲突,使得协作开发的效率很低。
  • 容易牵一发而动全身,导致维护成本变高。

这些代码耦合度高导致的问题都可以用组件化解决,本文会给大家介绍组件化的优势及其应用,以及更多的实战技巧。

相关系列文章:

组件化的优势

组件是业务单一的功能模块,每个组件都可以独立运行,也可以集成到其他组件中运行。组件化相对于前面说的模块化,耦合度更低,能有效解决上述所讲的痛点。

  • 开发时可以独立编译调试一个业务模块,无需编译整个工程,提高编译效率。
  • 业务模块的耦合度降低,代码更加独立,更容易复用。
  • 每个业务组件都有相应的负责人,大家的开发互不打扰,代码质量的好坏也只会影响到自己的业务模块,减少代码冲突,提高协作开发效率。
  • 如果业务功能有问题,通常只需要修改对应业务组件,维护成本更低。

组件化架构

以下是个人理解的组件化架构方案:

组件化架构图.png

从上到下分成了四层,只有上层模块才能依赖下层。讲一下每一层的作用:

  1. 基础层,是最基础的开发框架,包含了基础开发所需的基类、工具类、第三方库等。依赖该模块就能快速进行开发。
  2. 中间层,包含了路由的功能,可以和业务组件进行交互。由于依赖了基础开发框架,也算是一个增强版的组件化开发框架。
  3. 业务层,依赖于中间层,包含了各个业务功能的组件。每个业务组件都能运行出一个小型的 App 进行调试,也可能给其它模块进行复用。
  4. 应用层,也就是俗称的 App 壳,可以集成各种所需的业务组件,组合出不同 App。

如果是需要复用组件或者开发组件,就依赖中间层。如果 App 功能比较简单,根本用不上组件,那就依赖基础层。

如何组件化

统一依赖版本

随着业务功能不断增加,模块会越来越多,容易出现依赖版本不一致的情况,需要统一版本。常见的做法有两种。第一种是在 gradle 文件添加 Extra 属性,比如在工程根目录的 build.gradle 添加 ext {} 代码块声明变量,这些变量能在其它 gradle 文件直接使用。这样虽然能统一版本号和依赖,但是不能自动补全代码,不支持跳转。

所以个人推荐使用 buildSrc 的方式,实现起来也简单,在工程根目录添加一个 buildSrc 文件夹,并在文件夹创建一个 build.gradle.kts 文件,文件内容如下:

plugins {
  `kotlin-dsl`
}

repositories {
  gradlePluginPortal()
}

之后在 buildSrc\src\main\kotlin 目录下添加 Kotlin 常量,比如:

object Versions {
    const val COMPILE_SDK = 32
    const val TARGET_SDK = 32
    const val MIN_SDK = 23
    const val VERSION_CODE = 1
    const val VERSION_NAME = "1.0.0"

    const val ANDROID_GRADLE_PLUGIN = "7.1.2"
    const val KOTLIN = "1.7.20"

    const val APPCOMPAT = "1.5.1"
    const val APP_STARTUP = "1.1.0"
    const val CONSTRAINT_LAYOUT = "2.1.4"
    const val CORE_KTX = "1.8.0"
    const val ESPRESSO_CORE = "3.4.0"
    const val EXT_JUNIT = "1.1.3"
    const val JUNIT = "4.13.2"
    const val MATERIAL = "1.6.1"
    // ...
}
object Libs {
    const val APPCOMPAT = "androidx.appcompat:appcompat:${Versions.APPCOMPAT}"
    const val APP_STARTUP = "androidx.startup:startup-runtime:${Versions.APP_STARTUP}"
    const val CONSTRAINT_LAYOUT = "androidx.constraintlayout:constraintlayout:${Versions.CONSTRAINT_LAYOUT}"
    const val CORE_KTX = "androidx.core:core-ktx:${Versions.CORE_KTX}"
    const val ESPRESSO_CORE = "androidx.test.espresso:espresso-core:${Versions.ESPRESSO_CORE}"
    const val EXT_JUNIT = "androidx.test.ext:junit:${Versions.EXT_JUNIT}"
    const val JUNIT = "junit:junit:${Versions.JUNIT}"
    const val MATERIAL = "com.google.android.material:material:${Versions.MATERIAL}"
    // ...
}
object ClassPaths {
    const val ANDROID_GRADLE_PLUGIN = "com.android.tools.build:gradle:${Versions.ANDROID_GRADLE_PLUGIN}"
    const val KOTLIN_GRADLE_PLUGIN = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.KOTLIN}"
    const val KOTLIN_SERIALIZATION = "org.jetbrains.kotlin:kotlin-serialization:${Versions.KOTLIN}"
    // ...
}

Sync Project 之后就能在 build.gradle 使用这些常量了。

buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath ClassPaths.ANDROID_GRADLE_PLUGIN
        classpath ClassPaths.KOTLIN_GRADLE_PLUGIN
        classpath ClassPaths.KOTLIN_SERIALIZATION
    }
}
plugins {
    id 'com.android.library'
    id 'org.jetbrains.kotlin.android'
}

android {
    compileSdk Versions.COMPILE_SDK

    defaultConfig {
        minSdk Versions.MIN_SDK
        targetSdk Versions.TARGET_SDK

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }
    // ...
}

dependencies {
    api Libs.CORE_KTX
    api Libs.APPCOMPAT
    api Libs.MATERIAL
    api Libs.CONSTRAINT_LAYOUT
    // ...
}

buildSrc 支持自动补全,显示语法高亮且可以点击跳转。稍微有点小小不足是不能像第一种方案那样提示依赖有新版本,不过瑕不掩瑜。

独立调试与集成调试

通过设置 Gradle 的 plugin 可以配置 module 的类型,如果配置的是 com.android.application 插件,该模块就能打包运行。如果配置的是 com.android.library 插件,该模块就能被其它模块依赖使用。

所以当页面组件需要独立运行时就改成 com.android.application 插件,当需要集成调试被依赖时就改成 com.android.library 插件。通常会用一个调试开关变量来控制模块类型,所以我们在 buildSrc 模块里添加一个 DEBUG_MODULE 常量。

object Plugins {
    const val DEBUG_MODULE = false
}

然后判断调试变量的值去设置不同的插件,这就只需修改变量的值并 Sync 一下就能切换模块类型。

if (Plugins.DEBUG_MODULE) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

apply plugin: 'xxxx' 是老的插件写法了,现在新建项目一般是新的 plugins DSL 写法:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

个人研究了下在 id 后面加 apply() 函数可以控制是否应用该插件,所以改成下面的写法。

plugins {
    id 'com.android.application' apply(Plugins.DEBUG_MODULE)
    id 'com.android.library' apply(!Plugins.DEBUG_MODULE)
    id 'org.jetbrains.kotlin.android'
}

看似好像没什么问题,但是 Sync 一下就会有报错。

image.png

报错的意思是参数列表必须是一个字面的 boolean 值,也就是必须写 true 或者 false,目前看来写变量是不行的,那该怎么解决呢?个人想了很久实在没办法了,只好新老写法一起使用。

plugins {
    id 'org.jetbrains.kotlin.android'
}

if (Plugins.DEBUG_MODULE) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

注意 plugins {} 代码块要在最上面,这样编译才没问题。

当然只配置 application 插件还是不足以让一个模块运行起来的,我们还需要添加 applicationId,如果是 library 类型则不需要 applicationId。

并且 AndroidManifest 要做区分,想独立运行时在 AndroidManifest.xml 的 <appclication/> 节点要有图标、主题等信息,还要声明启动的 Activity。 如果业务模块已有的页面不适合做启动页,那么可以写个简单的测试页面。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.dylanc.componentization.account.impl">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.ComponentizationSample">
        <activity
            android:name=".ui.SignInActivity"
            android:exported="false" />
        <activity
            android:name=".ui.TestActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

而作为 library 的时候只需把其它模块需要跳转的 Activity 声明即可,由于需要两份不同的配置,这里在 src/main/manifest 文件夹新建了另一个 AndroidManifest.xml 文件。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.dylanc.componentization.account.impl">

    <application>
        <activity
            android:name=".ui.SignInActivity"
            android:exported="false"/>
    </application>

</manifest>

在业务组件的 build.gradle 根据开关变量的值设置 applicationId 和 AndroidManifest.xml 的路径。

android {
    defaultConfig {
        if (Plugins.DEBUG_MODULE) {
            applicationId "com.dylanc.componentization.account"
        }
        // ...
    }

    sourceSets {
        main {
            if (Plugins.DEBUG_MODULE) {
                manifest.srcFile 'src/main/AndroidManifest.xml' // 独立调试
            } else {
                manifest.srcFile 'src/main/manifest/AndroidManifest.xml' // 集成调试
            }
        }
    }
}

创建组件模块时建议选择 Phone & Tablet 新建一个 application 类型的模块,然后再更改 build.gradle 配置支持切换成 library 类型。

image.png

如果反过来选择 Android Library 新建一个 library 类型模块,想支持 application 类型来独立运行还要增加启动页、图标、App 名称、主题等不少代码,比较麻烦。

再分享点小技巧,当我们把前面的 DEBUG_MODULE 改为 true 时就可以独立调试各个模块,但是有的组件还会依赖于其它组件。比如社区发帖前肯定需要先登录,那么社区组件会依赖登录组件。当独立调试社区组件的时候,登录组件的模块也是 application 类型,此时是不能被依赖的。那么可以增加另一个变量来单独控制登录组件为 library 类型,这样就能正常调试社区组件了。

object Plugins {
    const val DEBUG_MODULE = true
    const val DEBUG_ACCOUNT_MODULE = false
}

还见过有人给每一个组件都添加变量,可能也是为了应对这种需要被依赖的情况,但是没有必要所有组件都一一对应一个调试变量,只需给独立调试时需要被依赖的组件添加调试变量即可。

代码隔离

独立调试和集成调试其实只是切换 module 的类型,解耦问题还得靠代码隔离。我们要尽量避免组件之间直接引用,除了依赖基础开发模块之外,不应该直接依赖于任何业务组件,这样才能保证脱离了其它业务模块后也能编译通过。

比如很多业务功能都需要登录后才能使用,那么一些业务组件需要依赖于账户组件。如果直接依赖了账户组件,可能会有意无意地访问到该组件的代码,增加了耦合度。可能有一天要在另一个 APP 使用新的账户系统,同事写好了另一套账户组件,返回的账户信息都没有变。本来是把老账户组件的依赖换成新账户组件就可以,但是由于存在耦合,换依赖后可能会编译不过,这就没法独立调试了。

所以为了保证在各种情况下都正常独立调试组件,实现代码隔离是非常必要的。这就可以用到 runtimeOnly 的依赖方式,这样依赖的模块只在运行时可用,我们开发的时候是不能访问到该模块的代码的,这就能实现代码隔离。

dependencies {
    // ...
    runtimeOnly project(path: ':module-account')
}

通过代码隔离就能降低业务组件间的耦合度,保证在各种情况下都正常独立调试或集成调试组件。不过这样就访问不到组件的代码了,组件之间要怎么交互又是个新的问题。

页面导航跳转

实现代码隔离后就不能访问到该模块下的类,那怎么跳转页面呢?可以用 Android 的隐式 Intent 的方式,但是隐式 Intent 需要通过 AndroidManifest 集中管理,协作开发比较麻烦,所以通常是使用路由框架来实现业务组件间的页面跳转。

本文使用的是经典的路由框架 ARouter,还可以使用 TheRouterWMRouter、等路由框架,用法和实现原理都是类似的。

根据 ARouter 的官方文档介绍,ARouter 是一个用于帮助 Android App 进行组件化改造的框架,支持模块间的路由、通信、解耦。下面介绍下用法:

在 Kotlin 项目使用 ARouter 需要在 build.gradle 添加以下配置和依赖。

plugins {
    // ...
    id 'kotlin-kapt'
}

kapt {
    arguments {
        arg("AROUTER_MODULE_NAME", project.getName())
    }
}

dependencies {
    implementation "com.alibaba:arouter-api:1.5.2"
    kapt "com.alibaba:arouter-compiler:1.5.2"
}

其中的 arouter-compiler 依赖会通过 APT 的方式生成代码,而生成代码的时候需要知道是在哪个模块,这是读取了 arguments 中的 AROUTER_MODULE_NAME 参数,所以还需要给 AROUTER_MODULE_NAME 设置为 project.getName()。简单来说就是上面的两段 kapt 代码必须一起使用。

在 Application 初始化 ARouter:

class App : Application() {

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) {
            ARouter.openLog()
            ARouter.openDebug()
        }
        ARouter.init(this)
    }
}

给 Activity 添加 @Route 注解,这样类的信息就和 path 字符串建立映射关系。

@Route(path = "/account/sign_in")
class SignInActivity : BaseActivity<AccountActivitySignInBinding>() {

    // ...
}

path 由两部分组成,前面的 /account 是分组,为什么要加分组呢?如果只声明 sign_in,那多个组件都有 sign_in 路由的话,路由框架就不知道到底要用哪个了,加上分组就能规避这个问题。注意一个模块内只能有一个分组,否则编译会不通过。

之后我们就能用路由跳转到指定 path 的 Activity。

ARouter.getInstance().build("/account/sign_in").navigation()

调用 withXXXX() 函数可以传递参数,如果需要用 startActivityForResult(intent) 的方式跳转路由页面,要在 navigation() 函数加上 Activity 和 requestCode 参数。

ARouter.getInstance().build("/account/sign_in")
    .withString("email", email)
    .navigation(this, REQUEST_CODE_SIGN_IN)

现在 startActivityForResult(intent) 标记为弃用了,官方推荐用新的 ActivityResult API,但是目前的路由框架基本都不支持,确实不太好适配,后面会单独发篇文章来讲讲怎么给路由框架适配 ActivityResult API。

获取 Fragment

跳转 Activity 是解决了,我们开发中还会经常用到 Fragment,在访问不了 Fragment 类的情况下怎么得到 Fragment 实例呢?通常路由框架还会支持获取 Fragment。

我们给 Fragment 添加 @Route 注解。

@Route(path = "/account/me")
class MeFragment : BaseFragment<AccountFragmentMeBinding>() {

    // ...
}

之后就能通过路由去实例化 Fragment 对象。

val meFragment = ARouter.getInstance().build("/account/me").navigation() as? Fragment

navigation() 函数是有返回值的,如果 path 对应的是一个 Activity 类,就会直接跳转页面并返回 null,如果 path 对应的是 Fragment 类,就会返回实例化的 Fragment 对象。不过返回的是 Object 类型,我们需要强转成可空的 Fragment 类型。传递参数给 Fragment 也是同样调用 withXXXX() 函数。

组件间通信

实现代码隔离后访问不到组件各个类的代码,怎么进行组件间的通信?比如获取数据、调用方法、实现监听等。其实也能用路由框架解决。

比如我们需要用账户组件判断是否登录,还有点击设置里的注销按钮时退出登录,先定义一个 AccountService 接口提供对应的方法,注意接口需要继承 IProvider

interface AccountService : IProvider {
  val isSignIn: Boolean
  fun signOut()
}

在账户组件添加该接口的实现类,并用 @Route 注解添加路由。

@Route(path = "/account/service")
class AccountServiceProvider : AccountService {
    override val isSignIn: Boolean
        get() = AccountRepository.isSignIn

    override fun signOut() {
        AccountRepository.signOut()
    }

    override fun init(context: Context) = Unit
}

之前就能通过路由获取对应 path 的接口实例,用法类似 Fragment,强转一下返回值。

val accountService = ARouter.getInstance().build("/account/service").navigation() as? AccountService
if (accountService?.isSignIn == true) {
    // ...
}

如果服务接口只有一个实例类,还可以用另一种获取方式,不传 path 字符串,直接传 Class 对象。这么用的时候最好将返回值声明为一个可空的类型,后面才不会忘了判空操作。

val accountService: AccountService? = ARouter.getInstance().navigation(AccountService::class.java)
if (accountService?.isSignIn == true) {
    // ...
}

还有一个很重要的问题,组件通信的接口放在哪里?通常会用个单独的组件通信模块来存放所有组件路由接口,每个组件都依赖该模块实现自己业务的接口并提供路由。按照前面的架构图,我们是放在中间层的 module-common 模块,这也是网上组件化较为常见的做法。

不过这样会存在中心化问题,所有组件模块都依附于一个组件通信的 module-common 模块上。随着业务的不断膨胀,module-common 模块的代码会越来越臃肿,不仅仅只有组件通信的接口,当需要获取组件的数据时会添加所需的 Bean 类,当需要监听时会添加对应的 Listener 类等。这样 module-common 模块会越来越杂乱,也很难知道每个组件对哪些组件接口有依赖,所以有必要去中心化。

去中心化

将组件通信的 module-common 模块拆分成各个组件的 api 模块,比如有一个 module-account 组件,就会有对应的通信模块 module-account-api。我们修改一下前面架构图的中间层:

组件化架构图-去中心化.png

对中间层解耦后,使用一个组件都要添加两个依赖。

dependencies {
    // ...
    implementation project(path: ':module-account-api')
    runtimeOnly project(path: ':module-account')
}

虽然这么使用会稍微有点麻烦,但是职责更加清晰。我们可以把对应的 api 模块作为这个组件的协议,比如 module-account-api 模块有以下的类。

object AccountPaths {
    private const val GROUP = "/account"
    const val SERVICE = "$GROUP/service"
    const val FRAGMENT_ME = "$GROUP/me"
    const val ACTIVITY_SIGN_IN = "$GROUP/sign_in"
}
interface AccountService : IProvider {
    val isSignIn: Boolean
    val user: User
    fun signOut()
}

我们通过 module-account-api 模块的代码就能知道能和 module-account 组件做些什么交互,能跳转哪些 Activity 能跳转,能获取哪些 Fragment。不需要查询组件文档或问同事就能完成开发,减少使用和沟通成本。如果发现该组件没有自己所需的功能,再反馈给对应的开发同事。

微信有个 api 方案,在组件添加 .api 后缀的文件,通过 gradle 脚本生成 xxxx-api 模块。这个思路蛮有意思的,不过个人觉得和手动 New Module 差不了多少,会增加些学习成本,需要同事额外了解一些开发规范,并不是很有必要,如果感兴趣的可以自行了解一下。

组件初始化

不管是独立调试用到各组件的 Application,还是集成调试用到 App 壳的 Application,都需要保证所涉及组件的初始化逻辑能正常执行。这里推荐使用 Jetpack 的一个组件 —— App Startup。App Startup 是用于应用程序在启动时初始化组件。

有不少开源库实现自动初始化会借助 ContentProvider,虽然很巧妙,但是会增加许多额外的耗时。因为 ContentProvider 是 Android 四大组件之一,即使我们的初始化操作很轻量,依赖 ContentProvider 后就变成了一个重量级的操作。官方测试一个空的 ContentProvider 大约会占用 2ms 的耗时,如果很多第三方库都用 ContentProvider 自动初始化,就会增加不少耗时。

所以官方推出了 App Startup,App Startup 也会创建一个 ContentProvider,不过提供了一套初始化的标准。官方希望第三方库都基于这套标准进行初始化,这样就能减少 ContentProvider 的数量,减少启动的耗时。

App Startup 是用于自动初始化,那也能很好地运用到组件化项目中。接下来讲下怎么使用,首先在 build.gradle 添加 App Startup 的依赖。

implementation "androidx.startup:startup-runtime:1.1.0"

写一个类继承 Initializer,在 create() 方法里实现初始化的逻辑。其中 dependencies() 函数可以控制在哪些 Initializer 之后再初始化,没有要求就返回个空列表。

class AccountInitializer : Initializer<Unit> {

    override fun create(context: Context) {
        // 初始化
    }

    override fun dependencies() = emptyList<Class<out Initializer<*>>>()
}

在业务模块的 AndroidManifest.xml 中添加 provider,这样就能实现自动初始化了。

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <meta-data
        android:name="com.dylanc.componentization.account.impl.AccountInitializer"
        android:value="androidx.startup" />
</provider>

上面的 <provider/> 一个固定的模版,需要修改的只有 android:name,改成对应 Initializer 实现类的全包名即可。

使用 App Startup 做初始化还有个好处是支持自定义的初始化顺序,在重写的 dependencies() 函数返回其它 Initializer 的 Class 列表,这样就能在这些 Initializer 之后才初始化。

由于我们做了代码隔离访问不到 Initializer 的代码,可能需要用 Class.forName(name) 得到 Class 对象,这么获取有点不好是字符串不会和全包名同步,要人为保证一致。不过这个类一般不会动,其实也还好。

我们可以在 api 模块定义一个属性获取 Initializer 的 Class,如果找不到类抛个异常提示一下。

@Suppress("UNCHECKED_CAST")
val accountInitializerClazz by lazy {
    (Class.forName("com.dylanc.componentization.account.AccountInitializer") as? Class<out Initializer<*>>)
        ?: throw IllegalStateException("Please depend on module-account.")
}

在其它模块的 Initializer 的 dependencies() 函数中返回该属性就能修改初始化顺序。

class NewsInitializer : Initializer<Unit> {

    override fun create(context: Context) {
        // ...
    }

    // 在 AccountInitializer 之后初始化
    override fun dependencies() = listOf(accountInitializerClazz)
}

如果不希望业务组件自动初始化,我们也能改成手动初始化。首先要在 provider 配置移除 Initializer,在 AndroidManifest.xml 添加以下配置:

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <meta-data
        android:name="com.dylanc.componentization.account.impl.AccountInitializer"
        tools:node="remove" />
</provider>

关键代码是 tools:node="remove",这样在 merge 所有模块的 AndroidManifest.xml 时,会把对应的 meta-data 节点全部删除,该 Initializer 就不会自动初始化了。

之后我们只需在特定的时机再手动初始化即可,同样是用服务接口去获取 Initializer 的 Class 对象。

val accountService = ARouter.getInstance().navigation(AccountService::class.java)
    ?: throw IllegalStateException("Please depend on account-impl module.")
AppInitializer.getInstance(this).initializeComponent(accountService.initializerClazz)

总结

本文介绍了单一工程开发的缺点和组件化的优势,了解了组件化开发需要解决的问题和具体的解决方案,如何独立和集成调试、实现代码隔离、页面跳转、获取 Fragment、组件通信、组件初始化等。其中有些是用路由框架来解决,本文使用的是 ARouter,大家也可以选择其它路由框架,用法都是大同小异。

其实组件化开发的步骤和原理并不难,但实际开发中还会遇到更多问题,比如怎么划分组件、有多套 UI 需求怎么处理等。下一篇文章会和大家分享更多个人在实际开发中总结的经验,帮助大家更好地进行组件化开发。

示例代码:待补充。

参考文献

关于我

一个兴趣使然的程序“工匠”  。有代码洁癖,喜欢封装,对封装有一定的个人见解,有不少个人原创的封装思路。GitHub 有分享一些帮助搭建开发框架的开源库,有任何使用上的问题或者需求都可以提 issues 或者加我微信直接反馈。