基于Hilt的依赖注入跨模块导航框架的Kotlin实现

news2024/11/24 11:51:14

前提:

        GitHub链接:GitHub - savelove123/AwesomeNavigation: 基于Hilt的Android页面导航框架

        在使用清洁架构+MVVM+组件化重构现有的项目的时候,为了实现跨模块页面导航,需要使用一个支持跨模块的页面导航组件。原本使用的是阿里巴巴的ARouter路由框架,但是ARouter这两年并没有更新,没有适配新版本的AGP,最主要原因是使用kapt的时候使用ARouter无法编译AndroidTest下的代码

       

        基于URI的路由框架,确实很便利,但是其实和目前的项目的实践理念不太合适,基于URI的导航框架,每一个导航都像一次RPC的调用,像一次GET请求,好处是简单、易用,但是它的表现行为更像胶水,好比各种Bus.这些工具没有提供一个规范为页面行为进行封装。同时我也希望能支持协程来获取导航后的结果,并且能和ViewModel结合使用,方便编写测试代码的路由框架。所以Bus和URI的框架,相对而言缺少一些规范和设计,决定弃用。

        目前有很多基于URI的框架,入门简单,易使用,但是带来的问题也是存在不规范性的隐患,测试比较困难,在建立APP质量体系的时候,编写测试代码是代码质量和App质量的可靠保障,考虑到开源项目可能更新慢而自己没那么大精力维护的情况,因此后面选用的工具优先用官方和轻量级的工具且不考虑基于URI的路由框架。

        所以目标很明确,足够轻量、可测试、支持封装、依赖解耦,这样的代码容易改动,毕竟一个活跃的项目一定是不断调整的项目,响应需求的变化和技术的迭代,且能支持不断地持续重构才是好的架构的目标。

思考与探索:

       在长期的项目实践中,基于软件的扩展性、健壮性、复用性的选择上,我更喜欢软件的业务代码的扩展性、健壮性大于复用性,简而言之,我更倾向于敏捷的软件架构,基于此我采用了清洁架构的思想,其实这也是目前一些大牛们推荐的最佳实践,在MVVM的基础上对业务逻辑再一次分层,并且使用Hilt来做依赖注入实践依赖注入依赖反转

        使用Hilt而不是使用Koin的原因是Hilt更注重类之间的关联关系,并且在编译期就可以检查错误、运行效率高、官方支持且对Android系统的生命周期的支持非常好。

        而Koin的话,它是运行时使用一种服务定位器模式,优点是它简单,并且更灵活,可以在运行时委托一些代理,便捷实现一些功能,但是它不是谷歌官方的,其次,它性能和类型安全检查都不如Hilt.所以我最终选择Hilt作为依赖注入的工具。

基于依赖注入的导航

        Hilt是一个基于Dagger(依赖注入框架)的扩展库,在项目原本的组件化实践中,我已经将各个Feature模块分离为Interface和Implemation,组件之间的关联关系都引用到Interface,而Implemation都使用Hilt注入到对应的依赖中。而原本的路由框架使用的ARouter,我也封装了ARouter的相关路由表结构到Interface模块。那么只需要继续这个思路,把导航的声明和实现逻辑分开,再通过实现获取具体的导航行为就可以更清晰地完成导航逻辑。

        除了一些URI的Router框架、Navigation框架,还有一些方案是采用Deep Link的方案来做,但是在这个实践中可能会存在由于项目越来越庞大,那么各个模块之间的DeepLink也越来越多,要维护许多DeepLink( 比如 path,intent-filter,action等等),但是这些DeepLink的意义比较难读,而且,往往不适用于对外提供。

        首先导航应该是简单、便捷的,其实Googlde 的Navigation的设计就挺好的,但是Navigation对跨模块的Activity支持得不是很好,第二个要编写额外的XML文件,但是这里面的NavGraph的思想非常值得借鉴。    

导航框架功能定义 

基于上面所说和实际项目的情况,我们希望页面导航框架是下面这样的:

  • 支持每个功能组件导航的时候不直接依赖其他功能组件的具体实现
  • 每个功能组件声明一个接口模块,这个接口模块描述了它声明了一组期望的目的地,实现了这个接口模块的功能组件导航到目的地。
  • 每个功能组件自己实现导航跳转的逻辑,不是在app模块实现,也不是在公共模块实现
  • 每个功能组件的实现允许依赖多个不同组件的接口模块
  • 导航到目的地的具体行为由功能实现组件内部维护,对外不可见
  • 导航的行为是一个抽象的行为,并且和清洁架构中的domain层解耦,导航不需要关注Domain层次的信息,只需要关注输入的参数信息,然后根据参数做出正确的行为即可。
  • 导航支持的目的地是可以灵活扩展的:Activity,Fragment,Dialog
  • 支持使用协程获取结果。
  • 只依赖官方的组件,不需要额外插件

导航框架的元素

基于导航的概念,我定义了框架中的四个基本元素

  • NavComponent 导航的逻辑组件,通常是ViewModel
  • Navigable 导航的载体,通常是Activity,Fragment
  • NavDestination 导航的目标点,可能是个Intent,Fragment,Dialog...
  • NavCommand 导航的行为 navigateTo,back,finish,toPrevious    

 首先,我们希望,我们能通过依赖注入获取到导航节点对应的实际节点,比如我们要去上海,那么在我们填写目的地为上海后输入我们的条件,能得到一个去上海的具体路线。

        所以我们这个过程就是,在导航的组件(NavComponent)中通过依赖注入获取一个包含我们目标节点和条件的导航(NavDestination)的实现(NavDestination Implementation),然后在导航的组件中,通过导航的载体(Navigable)得到具体的导航(NavDestination)的结果(Screen),最终在导航的载体处理导航的结果。

导航节点

        导航节点就是一组接口,声明与实现分开,然后返回一个目标点如下

interface ScreenDestination : NavDestination {
    val navContext: NavDestinationContext
        get() = EmptyNavContext
    fun toScreen(params:Parcelable?,
                 navigable: Navigable): Screen
}

导航组件  

因此我们期待的导航组件(NavComponent)执行的代码如下:        

@Inject
lateinit var anyDestination :NavDestination

...

//没有参数的时候去导航
anyDestination.navigateTo()

//如果有参数的时候去导航
anyDestination.injectParams( someParams ).navigateTo()

        在执行完navigateTo()后,我们期望得到一个导航的具体目标,可能是一个Intent,可能是一个Fragment等

导航载体       

那么我们在得到导航的目标以后,我们还需要使用这个导航目标,可能是跳转,也可能是把某个Fragment添加到容器中,这个时候,Navigable就承载了使用导航目标的功能。

        所以Navigable中我们需要有这么个函数:       

fun navigateTo(direction: NavDirection){
    isFragment{
        ...
    }
    isIntent{
        ...
    }
    isDialog{
        ...
    }
    other{
        ...
    }
}

        也就是说,NavComponent得到导航目标后,Navigable要能自动观察导航目标,然后处理导航目标。导航一般可以分为,页面导航和其他的导航,这里,我们先实现页面的导航。

导航的行为

页面的导航的行为可以分为下面几个:

  • 不带返回结果 
    fun navigate()
  • 可以带有返回结果 
    suspend fun navigateForResult():NavResult?
  • 返回到之前某一个页面
    fun navigateToPrevious( naviDestination:NaviDestination)
  • 关闭本页面返回上一页 
    fun back()
  • 结束本次页面,且带有结果
    fun finish( result:Any?=null )

关键代码设计

而在导航的目标页面,比如Activity,Fragment,我们可以通过如下的函数获取到导航的参数        

class SampleActivity:AppCompatActivity{
    //在onCreate函数或者之后调用
    val params = requireNavParam<ParamsType>()
}

然后我们获取到参数后,可以注入到ViewModel,那么我们可以得到目标的代码如下:

        

class SampleActivity:AppCompatActivity{
     
    private val sampleViewModel :SampleViewModel by lazyNavViewModel {
        injectVMParams(requireNavParam())
    }
}

而在我们的页面的viewModel,我们希望快捷获取到参数,那么我们的目标代码如下:

class SampleViewModel:NavViewModel<NavDirection>{
    fun onAttach( saveInstanceState:Bundle?){
        //要在viewModel初始化后调用
        requireVMParams()
    }
}

简单的Demo实践:

       比如我们有一个登录模块和一个用户模块,登录模块,有个简单的逻辑是,登录模块包含登录、注册、手机号验证,注册后,需要前往设置用户信息页面,而用户模块在修改用户信息的时候需要跳转到登录模块验证然后获取验证的结果。

        那么我们首先把登录模块拆分为接口和实现2个模块:
 

然后用户模块也拆分为接口与实现:

 然后我们编写登录模块的导航节点在auth模块中:


interface AuthDestination:ScreenDestination{

    @Parcelize
    data class Params(private val authType:AuthType, val phone:String?=null):Parcelable{
        val type :AuthType get() = run {
            if (authType != AuthType.Login && authType != AuthType.Register && phone.isNullOrEmpty()) {
                AuthType.Login
            } else {
                authType
            }
        }
    }
}

 然后再在auth_impl模块中编写登录模块导航节点的实现:

class AuthDestinationImpl: AuthDestination{
    override fun toScreen( params: Parcelable?, navigable: Navigable): Screen {
        return Intent(navigable.toContext(), AuthActivity::class.java)
    }
}

然后我们再编写用户信息页面和修改手机号码的导航节点:

interface UserInfoDestination:ScreenDestination{
    @Parcelize
    data class Params( val requireFragment:Boolean=false): Parcelable
}
interface ChangePhoneDestination:ScreenDestination, Parameterized<ChangePhoneDestination.Params> {
    @Parcelize
    data class Params(val phone:String,val token:String ): Parcelable
}

以及这两个导航节点的实现,我们可以尝试把导航节点的实现和业务逻辑相关联,实现一个页面统一的拦截的功能,比如UserInfoDestinationImpl所做的,但是我不推荐这样做,一个是这样编写出来的粒度加大了,页面跳转与业务逻辑不应该关联。


class UserInfoDestinationImpl constructor(
    private val authRepository: AuthRepository,
    private val userRepository: UserRepository,
    private val authDestination: AuthDestination,
    ) : UserInfoDestination,
    Parameterized<UserInfoDestination.Params> {

    override val navContext: NavDestinationContext
        get() = run {
            authNavContext?:super.navContext
        }

    var authNavContext:NavDestinationContext? = null

    override fun toScreen(params: Parcelable?, navigable:Navigable): Screen {
        if( !authRepository.isAuthorized()  ){
            val loginParams =  AuthDestination.Params( if( userRepository.userInfo().value!= null ) AuthType.Login else AuthType.Register)
            authNavContext = NavDestinationContext( loginParams)
            return authDestination.injectParams(loginParams )
                .toScreen( navigable )
        }

        return if( params.reifiedOrNull()?.requireFragment == true  ){
            UserInfoFragment()
        }else {
            Intent( navigable.toContext(), UserInfoActivity::class.java )
        }
    }

}

像下面这样,根据参数来判断页面的导航,我个人比较推荐。

class ChangePhoneDestinationImpl @Inject constructor(): ChangePhoneDestination {

    override fun toScreen( params: Parcelable?, navigable: Navigable): Screen {
        requireNotNull(params)
        return Intent( navigable.toContext(), ChangePhoneActivity::class.java )
    }
}

然后我们在Hilt增加相关的Provides函数

@Module
@InstallIn(SingletonComponent::class)
object AuthModule {

    @Provides
    @Singleton
    fun provideAuthRepository( @AppScope appScope: CoroutineScope):AuthRepository = AuthRepositoryImpl(appScope)

    @Provides
    fun provideAuthDestination( ): AuthDestination = AuthDestinationImpl()

}
@Module
@InstallIn(SingletonComponent::class)
object UserModule {

    @Provides
    @Singleton
    fun provideUserRepository(): UserRepository = UserRepositoryImpl(authRepository, scope)

    @Provides
    fun provideChangePhoneDestination(
        userRepository: UserRepository,
        authDestination: AuthDestination,
    ): ChangePhoneDestination = ChangePhoneDestinationImpl()

    @Provides
    fun provideUserInfoDestination(
        authRepository: AuthRepository,
        userRepository: UserRepository,
        authDestination: AuthDestination,
    ): UserInfoDestination = UserInfoDestinationImpl( authRepository,userRepository,authDestination)


}

然后我们就实现了页面节点支持自动注入了。

接下来我们编写Activity和ViewModel的代码:

@AndroidEntryPoint
class AuthActivity: BaseActivity<ActivityAuthBinding,AuthViewModel>() ,Navigable{

    private val authViewModel :AuthViewModel by lazyNavViewModel {
        observeNavigation()
        injectVMParams(requireNavParam())
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewDataBinding.title.setTitle( when( requireNavParam<AuthDestination.Params>().type){
            AuthType.Register->"注册"
            AuthType.Login->"登录"
            AuthType.ResetPassword->"重置密码"
            AuthType.ChangePhone->"修改手机号"
        })
    }

    override fun getLayoutId() = R.layout.activity_auth

    override fun getBindingVM() = authViewModel
    override fun getVariableId() = BR.vm

}
sealed class AuthNextDestination:NavDestination

@HiltViewModel
class AuthViewModel @Inject constructor(
    application: Application,
    private val authRepository: AuthRepository,
    val setPasswordDestination: SetPasswordDestination,
    val setUserInfoDestination: SetUserInfoDestination,
    val privacyDestination: PrivacyDestination,
    val homeDestination: HomeDestination
    ):NavViewModel<AuthNextDestination>( application = application ),Parameterized<AuthDestination.Params>{


    val phone = MutableLiveData("")
    val verifyCode = MutableLiveData("")

    override fun attachViewModel(savedInstanceState: Bundle?) {
        super.attachViewModel(savedInstanceState)

        if( requireVMParams().type == AuthType.Register ){
            viewModelScope.launch {
                val results :PrivacyDestination.Results? = privacyDestination.injectParams(PrivacyDestination.Params( PrivacyDestination.Action.CONFIRM ))
                    .navigateForResult().let {
                        NavigationResult( it.code,it.data)
                    }.getParcelable()
                if(results?.accepted != true ){
                    navigateBack()
                }
            }

        }
    }

    fun observePhone() = createTextWatcher {
        phone.value = it?.toString() ?: ""
    }

    fun observeVerifyCode() = createTextWatcher {
        verifyCode.value = it?.toString() ?: ""
    }

    fun verify() = createActionDoneListener{
        viewModelScope.launch {
            authRepository.verifyPhone( phone.value!! , verifyCode.value!! , requireVMParams().type )
                .fold( onSuccess = {
                    when( requireVMParams().type ){
                        AuthType.Register->{
                            authRepository.setAuthState( it  as AuthResult.AuthState)
                            SetUserInfo().navigate()
                        }
                        AuthType.ResetPassword->{
                            SetPassword()
                                .injectParams( SetPasswordDestination.Params(( it as AuthResult.CheckOperation).token))
                                .navigate()
                        }
                        AuthType.ChangePhone->{
                            finish( it )
                        }
                        AuthType.Login->{
                            authRepository.setAuthState( it  as AuthResult.AuthState)
                            navigateToPrevious( Home() )
                        }
                    }
                    navigateBack()
                }, onFailure = {
                    Toast.makeText( getApplication() , it.message , Toast.LENGTH_SHORT ).show()
                })
        }


    }

    inner class SetPassword : AuthNextDestination(),SetPasswordDestination by setPasswordDestination
    inner class SetUserInfo : AuthNextDestination(),SetUserInfoDestination by setUserInfoDestination
    inner class Home:AuthNextDestination(),HomeDestination by homeDestination

}

可以注意到,我这边使用了AuthNextDestination这个密封类,用户定义AuthViewModel这个导航组件可以导航的节点类型,同时我对AuthViewModel使用了Parametrized接口标记,表示AuthViewModel是可以注入参数的,如果AuthViewModel不需要外部参数,则不必实现这个接口。

设置密码和设置手机号码使用了不同的逻辑,设置密码是耦合了后面的业务功能,这个我个人不推荐这样的做法,而修改手机号使用了finish把结果返回给上一个页面,上一个页面可以通过协程获取,代码如下:

fun changePhone(){
        viewModelScope.launch {
            val result :AuthResult.CheckOperation? = authDestination.injectParams( AuthDestination.Params(
                AuthType.ChangePhone,
                authRepository.authState().value.phoneNumber) ).navigateForResult().getParcelable()
            result?.let {
                changePhoneDestination.injectParams(it )
                    .navigate()
            }
        }
    }

上面👆🏻这种做法我们可以结构化地异步获取结果,个人推荐这样使用。

基于此我们得到的相关导航相关的架构和流程图如下:

优点:契合清洁架构的理念,实现了导航的依赖注入以及依赖反转,轻量级,编写的代码更具备规范性、由于使用了依赖注入因此易于编写测试代码。

缺点:目前默认只实现了页面导航、只支持kotlin,扩展功能需要掌握kotlin,由于代码粒度低需要编写额外的类。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/786514.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

图说 SQL 的 JOIN

JOIN 用于多表联查&#xff0c;分为&#xff1a; OUTER JOIN FULL OUTER JOIN LEFT OUTER JOIN RIGHT OUTER JOIN INNER JOIN OUTER 通常省略&#xff0c;所以我们在查询语句中看到的大都是 FULL JOIN、LEFT JOIN、RIGHT JOIN。 为了便于验证说明&#xff0c;下面我在 tem…

每日一题——判断链表中是否有环

题目 判断给定的链表中是否有环。如果有环则返回true&#xff0c;否则返回false。 数据范围&#xff1a;链表长度 0≤n≤10000&#xff0c;链表中任意节点的值满足 ∣val∣<100000 要求&#xff1a;空间复杂度 O(1)&#xff0c;时间复杂度 O(n) 输入分为两部分&#xff0c…

关于tensorflow和pytroch安装的一些坑坑

主要是自己总结记录一下 首先tensorflow的安装 主要先参照的官网 https://docs.anaconda.com/free/anaconda/applications/tensorflow/之后我的项目是需要用到keras的&#xff0c;然后我又去安装keras&#xff0c;出现了Cannot import name ‘dtensor’ from ‘tensorflow.co…

物联网阀控水表计量准确度如何?

物联网阀控水表是一种新型的智能水表&#xff0c;它采用了先进的物联网技术&#xff0c;可以通过远程控制和监测水表的运行情况&#xff0c;实现更加精准的水量计量和费用结算。那么&#xff0c;物联网阀控水表的计量准确度如何呢&#xff1f;下面我们将从以下几个方面进行详细…

树状数组笔记

数组、前缀和、树状数组的区别&#xff1a; 数组&#xff1a;修改某点O&#xff08;1&#xff09;&#xff0c;求区间O&#xff08;n&#xff09; 前缀和&#xff1a;修改某点O&#xff08;n&#xff09;&#xff0c;求区间O&#xff08;1&#xff09; 树状数组&#xff1a;修改…

机器学习实战:Python基于GBM梯度提升机进行预测(十四)

这篇干货很硬&#xff0c;喜欢的小伙伴点个赞/收藏&#xff0c;持续更新&#xff01; 文章目录 1.前言1.1 GBM的介绍1.2 GBM的应用 2. scikit-learn实战演示2.1 分类问题2.2 回归问题 3. GBM超参数3.1 决策树数量&#xff08;n_estimators&#xff09;3.2 样本数量&#xff08…

网络设备中的配置文件管理

建立强大网络的第一步是为灾难和网络中断做好准备&#xff0c;许多企业在中断期间遭受损失&#xff0c;因为他们缺乏备份计划并且配置管理不达标&#xff0c;使用配置文件管理工具进行适当的配置文件管理不仅有助于处理网络中断&#xff0c;还有助于优化网络性能。 使用配置文…

Cilium

Cilium是一个开源的、面向Kubernetes和容器环境的网络插件&#xff0c;用于提供高级的网络和安全功能。它是一个用于容器网络和网络层四、七层安全的项目&#xff0c;旨在简化网络和安全层的管理&#xff0c;并提供高性能和低延迟的数据包处理。Cilium通过BPF&#xff08;Berke…

Windows 使用批处理脚本 kill 进程

使用 jenkins 构建 SpringBoot 项目时&#xff0c;需要增加 kill 进程的功能&#xff0c;否则再次启动时会失败&#xff0c;提示端口被占用。 Windows 平台的脚本 命令 for /f "tokens5" %%p in (netstat /anop tcp ^| findstr /i 8007 ^| findstr /i listening) do…

2023 7.24~7.30 周报 (VelocityGAN)

目录哟 0 上周回顾1 本周计划1.1 论文背景 2 完成情况2.1 背景简述2.2 网络结构: 生成器 (Generator)2.3 网络结构: 判别器 (Discriminator)2.4 损失函数: Loss2.5 OpenFWI中的VelocityGAN与它的核心代码2.5.1 判别器loss2.5.2 生成器loss2.5.3 训练的配置顺序 2.6 复现结果 3 …

【lesson3】Linux基本指令2

文章目录 echo重定向输出重定向>(输出重定向)>>(追加重定向) <(输入重定向) 生成10000行内容到file.txt命令行moremoremore -n(行数) lesslessless / 字符串 ctrl cheadheadhead -n tailtailtial -n |(管道)wcdatedatedate 其它命令 calcalcal 年份cal -1cal …

Python爬虫基础知识点有哪些

目录 Python爬虫基础知识点 Requests库 Beautiful Soup库 正则表达式 数据存储 防止被反爬虫策略 爬虫调度和任务管理 认识robots.txt文件 反爬虫法律与道德 示例代码 Requests库 Beautiful Soup库 正则表达式 数据存储 防止被反爬虫策略 结语 网络世界中信息的…

Ant Design Vue Modal 模态框位置调整

问题描述 有一个功能已经实现的需求&#xff0c;是点击了一个按钮&#xff0c;弹出了如下模态框&#xff1a; 这里看到的就是点击按钮之后用户看到的效果&#xff0c;了保持模态框在用户视野范围内&#xff0c;我钻研如何调整显示的位置。 实现步骤 Ant Design of Vue的官方…

音视频——封装格式原理

视频解码基础 一、封裝格式 ​ 我们播放的视频文件一般都是用一种封装格式封装起来的&#xff0c;封装格式的作用是什么呢&#xff1f;一般视频文件里不光有视频&#xff0c;还有音频&#xff0c;封装格式的作用就是把视频和音频打包起来。 所以我们先要解封装格式&#xff0…

【Android Framework系列】第8章 事件分发你真了解吗?

1 事件分发基本认知 1.1 事件分发的”事件“是指什么 1.2 事件处理中涉及到的点 1.3 Android 事件处理的三个流程 在Android中&#xff0c;Touch事件的分发分服务端和应用端。在服务端由WindowManagerService&#xff08;借助InputManagerService&#xff09;负责采集和分发的…

高校vr元宇宙虚拟禁毒体验推动社会戒毒工作的深入开展

元宇宙是指一个虚拟的、全球性的、可交互的虚拟世界&#xff0c;深度融合了VR虚拟现实、AR增强现实和ai等技术。将元宇宙应用于戒毒安全教育平台&#xff0c;具有以下现实意义&#xff1a; 创造安全的学习环境 戒毒安全教育需要让人们了解毒品的危害和如何预防&#xff0c;但直…

水环境综合治理监测系统:筑牢城市水生态安全屏障

水是生命之源&#xff0c;是人类赖以生存的基础。然而&#xff0c;随着工业化、城市化的快速发展&#xff0c;水污染问题日益凸显&#xff0c;给居民的环境卫生以及用水安全带来了巨大的威胁。因此&#xff0c;加强水环境综合治理&#xff0c;保护水资源和维护生态平衡&#xf…

vue之ReadIdcard(身份证读取组件)

组件功能 读取二代身份证信息组件,包含无效身份证验证,过期身份证验证,是否满16周岁验证 #界面 #界面输入项 序号输入项输入形式是否必输是否可配置备注1

CentOS 安装Oracle11g

一、方式一&#xff08;亲测&#xff09; https://blog.csdn.net/zw521cx/article/details/108550215 遇到问题解决&#xff1a; 1.执行 dbca -silent -responseFile /home/oracle/response/dbca.rsp 报错 解决办法&#xff1a; a.全局查找 [rootVM-0-8-centos ~]# locate S…

Vue全局事件总线

main.js //引入Vue import Vue from vue //引入App import App from "./App"; //关闭Vue的生产提示 Vue.config.productionTip false // const Demo Vue.extend({}) // const d new Demo() // Vue.prototype.x d//创建vm new Vue({el:#app,render:h>h(App),b…