news 2026/9/24 20:40:13

Android大型项目Clean Architecture落地:模块划分、依赖注入与分层实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android大型项目Clean Architecture落地:模块划分、依赖注入与分层实践

1. 内容整体设计与架构思路拆解

1.1 Clean Architecture到底在解决什么痛点

很多Android项目做到中期,代码就开始“拧巴”了。业务逻辑散落在Activity里、接口回调嵌套成山、一个需求的改动要牵连七八个文件、测试代码根本没法写……这种时候团队往往会想起Clean Architecture。但真到了动手落地,又会发现一个问题:网上的文章讲概念的很多,讲怎么在一个真实的大型项目里把Clean Architecture跑起来的却很少。

先花点时间说清楚Clean Architecture的本质。它最核心的东西不是那五个同心圆,而是依赖规则:源代码的依赖关系只能从外层指向内层,内层完全不知道外层的存在。换句话说,业务核心不该知道自己跑在Android上,也不该知道数据来自网络还是本地数据库,更不该知道UI用的是Compose还是XML。这就是为什么Robert Martin在提出这套架构时反复强调:你用的是Framework,不是Framework用你。

这句话放到Android项目里,翻译得直白一点:你的登录逻辑、购物车计算、订单状态流转,这些应该是一个个纯粹的业务模块,不依赖Activity生命周期,不依赖Context,不依赖Retrofit的注解,也不依赖Room的实体类。好处非常直接——业务模块可以单测,可以脱离Android环境跑,换UI框架不影响核心逻辑,换数据源也不会把业务代码搅得一团糟。

用一个生活化的例子来类比:Clean Architecture就像一家分工明确的餐厅。后厨的核心工作是把菜做好,至于这道菜是装进餐盒供外卖、还是摆盘端给堂食顾客,后厨不关心;食材是当天采购还是冷链配送,后厨也不用管。对应过来就是:业务逻辑(后厨)不关心UI(装盘方式),也不关心数据来源(食材渠道)。听起来很理想,但真落地的时候,层级怎么拆、模块怎么建、依赖怎么注入、模型怎么映射,每一个问题都能把项目拖进泥潭。接下来我会结合自己维护的一个长期迭代的Android项目,把这些细节逐一讲透。

1.2 为什么大型项目比中小项目更需要它

不是所有项目都需要Clean Architecture的,这个得先说明白。对于一个只有三五个页面、一两个月的短期项目,用Clean Architecture相当于用杀鸡刀宰牛,类的数量翻一倍,开发速度明显变慢,纯属自找麻烦。但是项目一旦跨过某个规模阈值,这套架构的收益就会开始体现出来,而且项目越大、活得越久,收益越明显。

我自己判断是否需要引入的标准有三个:第一,业务逻辑是否复杂到需要独立测试,这里的复杂不是说代码量多少,而是分支多、状态多、规则变动频繁;第二,团队是否有多人并行开发,如果几个人同时改一个Activity文件,git冲突会让人崩溃;第三,项目是否有长期迭代的计划,比如规划了一年以上的版本路线。这三条只要中了两条,Clean Architecture就值得认真考虑。

大型项目的核心痛点在于变化的方向太多。UI可能从XML换到Compose,网络库可能从OkHttp换到别的方案,数据库可能从SQLite换到Room,本地缓存策略可能从磁盘换到内存。如果这些变化都直接耦合在业务逻辑里面,每一次技术栈更换都是一次大手术。Clean Architecture的思路就是把这些“会变的东西”赶到外层,把“不太会变的东西”留在内层,通过稳定的接口把它们隔开。这样短期看是多写了几层胶水代码,但长期看每一个技术栈的替换都只是外层局部的修改,风险可控、范围清晰。

当然,它也是有代价的。最大的代价是初期结构设计要花心思,代码量明显增加,入门的同学上手成本变高,而且如果团队成员对架构边界理解不一致,很容易出现“看起来分层了、实际耦合在一起”的半吊子状态。所以用Clean Architecture一定要配合代码评审和依赖约束工具,光靠自觉是不行的。

1.3 和MVVM、MVI到底什么关系

经常有人把Clean Architecture和MVVM、MVI拿来比较,其实它们是不同维度的东西。Clean Architecture是整个应用的宏观架构,回答的是“业务逻辑放在哪里、依赖往哪个方向走”;MVVM和MVI是表现层的模式,回答的是“UI和数据怎么绑定、事件怎么流转”。两者完全不冲突,反而是很好的互补。

在Android生态里,最主流的组合方式是:外层使用MVVM或MVI作为表现层模式,中层使用UseCase承载业务用例,内层使用Repository接口隔离数据来源。ViewModel和StateFlow管理UI状态,UseCase处理具体业务规则,Repository实现类决定数据从网络来还是从缓存来。这样每一层各司其职,不会出现“一个ViewModel里堆了300行业务逻辑”的灾难现场。

还有个容易混淆的概念是Domain层。有人觉得小项目不需要Domain层,直接让ViewModel调Repository就行了;有人则坚持必须有三层。我的观点是,是否引入Domain层取决于业务逻辑的复杂程度。如果业务逻辑就是简单的“取数据填到UI上”,那确实不需要Domain层,硬加一个UseCase就是过度设计;但一旦出现跨数据源的业务规则,比如“计算购物车总价并应用优惠券”、“判断用户是否有权限执行某操作”,这些逻辑放到ViewModel里会污染表现层,放到Repository里又会让数据层变臃肿,这时候独立出来的Domain层就会很舒服。

2. 工程结构落地:模块划分与依赖方向

2.1 包结构怎么分才不容易乱

说到工程结构,第一个绕不开的问题就是:单模块还是多模块。在大型项目里,我强烈建议使用多模块结构,而且要把Clean Architecture的层级边界和Gradle模块边界对齐。这样做的核心价值在于:依赖方向由Gradle层面的模块依赖关系强制约束,违反架构规则的代码连编译都过不了,不依赖代码评审时的人工审查

拿我现在的项目举例,整体模块结构大概是这样的:

  • :app:Application入口、全局配置、导航宿主
  • :core:domain:业务模型、Repository接口、UseCase
  • :core:data:Repository实现、网络数据源、本地数据源、缓存策略
  • :core:common:通用工具、扩展函数、基础UI组件
  • :feature:login:登录模块的UI层(Activity、ViewModel)
  • :feature:home:首页模块的UI层
  • :feature:order:订单模块的UI层

从依赖方向上看,feature模块依赖core:domaincore:datacore:data依赖core:domaincore:domain不依赖任何其他模块和Android框架。这里有一个关键约束:core:domain模块一定不能依赖core:data和任何UI框架的代码,否则整个依赖规则就被破坏了。

有些项目为了图省事,把domain和data放同一个模块里,只在包层面做了区分。这种做法在中小型项目里问题不大,但在大型项目里很容易失控。因为包层面的分层是“软约束”,程序员今天可以把一个接口实现放到domain包里,明天可以把一个网络请求写进UseCase里,时间一长边界就模糊了。模块层面的隔离是“硬约束”,你想越界,Gradle编译就直接报错。

2.2 每个Layer的职责边界到底怎么画

分层架构最有争议的就是各层职责的划分,尤其在实际项目中,边界不可能像理论那么清晰。根据我自己的经验,下面这套职责划分在Android大型项目中是踩过很多坑之后沉淀下来的:

Domain层(最内层):只放业务模型、Repository接口、UseCase。业务模型应该是纯Kotlin数据类,不继承任何Android框架的类。Repository接口用suspend函数或Flow定义数据操作的能力,但不暴露任何Retrofit、Room相关的类型。UseCase是一个类封装一个业务场景,比如LoginUseCaseGetUserOrdersUseCase。这里最容易犯的错是在接口方法里直接使用DTO或数据源框架的类型,一旦这么做了,Domain层的纯洁性就被破坏了。

Data层(中间层):实现Domain层定义的接口,负责网络请求、数据库读写、本地缓存、文件上传等具体的数据获取工作。数据来源可以有多个,比如RemoteDataSource和LocalDataSource,Repository实现类负责协调它们。这里要注意的是,data层对外暴露的是Domain层的模型,不是DTO也不是数据库实体。API返回的UserDto要在Repository实现内部转换成User,数据库的OrderEntity也要在Repository实现内部映射成Order。这个模型转换工作看起来繁琐,但它是保证业务层不被技术细节污染的关键一堵墙。

Presentation层(最外层):Activity、Fragment、ViewModel、Adapter这些UI相关的东西都放在这层。ViewModel负责把Domain层返回的数据转换成UI状态,处理Loading、Error、Empty这些UI状态,以及处理用户点击事件。UI层可以直接依赖Domain层的模型和接口,但它不应该依赖Data层的具体实现。这样以后就算把网络库从Retrofit换成其他方案,UI层一行代码都不用改。

2.3 Hilt依赖注入如何配合三层结构

依赖注入在Clean Architecture里不只是简化代码,更是依赖方向的控制手段。没有依赖注入的话,想在Domain层的接口和Data层的实现之间加一个“运行时才决定谁是谁”的中间层,操作会非常别扭。

我用的方案是Hilt,因为它在Google生态里支持最好,和ViewModel、Room、Retrofit的集成都很顺滑。但在配置的时候有个很容易踩的坑:依赖注入的绑定关系要创建在正确的地方,不要把所有Provider堆在同一个Module里

标准的做法是给每层定义自己的Module。Data模块里的Module负责绑定Repository接口和实现类的关系,比如:

@Module @InstallIn(SingletonComponent::class) abstract class RepositoryModule { @Binds @Singleton abstract fun bindLoginRepository(impl: LoginRepositoryImpl): LoginRepository }

Presentation层只看到LoginRepository这个接口,完全不知道LoginRepositoryImpl的存在。Hilt在编译期生成依赖图的时候,会把LoginRepositoryImpl注入到需要LoginRepository的地方。这样依赖的方向就从“高层依赖低层”反转成“低层被高层通过接口调用”,实现了传统的控制反转。

用Hilt还有一个需要注意的点:Domain层不要放Hilt注解。前面说了Domain层要保持纯净,如果你在UseCase里写了@Inject,其实问题还不大,因为Hilt注解本身不是Android框架的类;但如果你在Domain层里引入了@Module@Provides这些注解,就相当于把依赖注入框架绑定到了最内层,以后想换DI框架或者做单元测试,Domain层就变得不干净了。正确做法是在Application或其他地方的Module里构造UseCase,或者直接在ViewModel里用构造函数注入。

3. 实操过程:从一个登录流程走通整个架构

3.1 用户场景如何从UI层抵达Domain层

光说不练是学不会的,我拿一个登录功能来完整走一遍流程。这个功能看起来简单,但刚好能覆盖UI、ViewModel、UseCase、Repository、数据源五层,再加上错误处理和状态流转,可以很清楚地看到Clean Architecture是怎么运作的。

先定义业务场景:用户输入手机号和验证码,点击登录按钮,系统先做本机校验,再调服务端接口换取用户信息,最后把用户信息存到本地。如果验证码错误,要给出错误提示。这个流程的关键是:“本机校验”和“服务端换取用户信息”这两个步骤属于不同的数据来源,在传统写法里很容易揉成一团,但在Clean Architecture里它们各自有明确归属。

UI层是一个LoginFragment,它的职责只有一个:把用户输入传给ViewModel,然后观察ViewModel暴露出来的UI状态。Fragment本身不做逻辑判断,不校验手机号格式,也不决定是否弹出错误提示,这些全丢给ViewModel处理。

class LoginViewModel @Inject constructor( private val loginUseCase: LoginUseCase ) : ViewModel() { private val _uiState = MutableStateFlow(LoginUiState()) val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow() fun onLoginClicked(phone: String, code: String) { viewModelScope.launch { _uiState.update { it.copy(isLoading = true) } val result = loginUseCase.execute(phone, code) when (result) { is Result.Success -> _uiState.update { it.copy(isLoading = false, isLoginSuccess = true) } is Result.Error -> _uiState.update { it.copy(isLoading = false, errorMessage = result.message) } } } } }

注意ViewModel里只做了三件事:调用UseCase、把结果转成UI状态、把状态暴露给View。校验手机号格式的逻辑不在ViewModel里,登录成功之后要不要跳转页面也不在ViewModel里。

3.2 UseCase封装的业务规则从哪来

接下来是Domain层的LoginUseCase。它的工作是对外提供一个干净的接口,把“登录”这个业务动作完整表达出来,同时把“怎么获取数据”这个细节完全隐藏在Repository后面。

这里有一个设计问题值得单独说:UseCase到底要不要执行“手机号格式校验”。我见过两种做法,一种是把校验放在ViewModel,一种是把校验放在UseCase。我的看法是,只要校验规则是业务规则(而不是单纯的UI交互规则),就应该放UseCase里。手机号格式校验显然属于业务规则,因为服务端也会校验、其他端也要同样的规则,它不能因为UI换了个样式就消失。所以UseCase内部会调用一个校验器,或者把校验逻辑直接写在UseCase里,然后返回一个校验失败的结果。

class LoginUseCase( private val repository: LoginRepository ) { suspend fun execute(phone: String, code: String): Result<User> { if (!PhoneNumberValidator.isValid(phone)) { return Result.Error("手机号格式不正确") } return repository.login(phone, code) } }

注意这里我用了Result<User>作为返回值,User是Domain层定义的业务模型。整个UseCase里没有任何Android依赖,不出意外的话,单测可以直接在JVM上跑,不需要模拟器。这在大型项目里是一个极其重要优势:业务逻辑的单测速度快、稳定、CI可以实时跑,不会因为UI自动化测试不稳定而折磨人。

3.3 Repository实现与数据源切换策略

现在看看Data层的LoginRepositoryImpl。它要做的事情包括:调服务端接口获取用户信息、把获取到的DTO转成Domain模型、把结果写到本地数据库或缓存。在这个过程中涉及一个很经典的问题:错误处理在哪个层级捕获

很多项目会在Repository里直接捕获异常然后封装成自定义的ApiException,这种做法本身没问题,但要注意不要让异常类型泄漏到Domain层。Domain层和UseCase层只应该看到统一的业务类型,比如Result.Error里带一个错误码和消息。网络超时、404、500这种技术相关问题,应该在Repository实现内部转换成业务相关的描述,比如“网络连接不稳定,请检查网络设置”。

class LoginRepositoryImpl( private val remoteDataSource: LoginRemoteDataSource, private val localDataSource: UserLocalDataSource, private val mapper: UserModelMapper ) : LoginRepository { override suspend fun login(phone: String, code: String): Result<User> { return try { val userDto = remoteDataSource.login(phone, code) val user = mapper.fromDto(userDto) localDataSource.cacheUser(user) Result.Success(user) } catch (e: ApiException) { when (e.code) { ERROR_CODE_INVALID_CODE -> Result.Error("验证码错误") ERROR_CODE_PHONE_NOT_REGISTERED -> Result.Error("该手机号尚未注册") else -> Result.Error("登录失败:${e.message}") } } } }

这里要专门说一下耗时操作的问题。登录接口是网络请求,必须在IO线程执行,但Repository内部不需要自己处理线程切换。为什么?因为上游的UseCase或ViewModel已经把调用包在协程里了,而且Retrofit本身在封装时已经把网络请求放到了IO线程池里。如果每个Repository都自作主张地加withContext(Dispatchers.IO),嵌套切换会搞得很难看。记住一个原则:线程调度尽量集中在上层,数据源框架自己会处理耗时操作

3.4 Model映射:最容易做脏的地方

模型映射是我见过的最容易做“脏”的环节。有些人图省事直接在Repository实现里把DTO往业务模型里硬塞,有些人干脆让UseCase直接依赖DTO,美其名曰“减少代码量”,这些我都不推荐。大型项目一定要把模型映射独立出来,用一个Mapper类或者Kotlin扩展函数维护清晰的转换逻辑。

DTO、Domain模型、UIModel三层模型是常态。为什么不能只用一个模型撑到底?因为三者的关注点完全不同。API返回的LoginResponseDto可能包含refreshTokenexpiresIn这些服务端特有的字段;Domain层的User只关心业务上的用户名、头像、手机号;UI层的LoginUiState可能还需要一个isAgreedPrivacyPolicy来表示用户是否勾选了隐私协议。如果让一个模型承担所有职责,结果是UI一改接口就改,接口一改数据库就改,改来改去全是雷。

在实际代码里,我习惯让Mapper统一处理模型间的转换:

object UserModelMapper { fun fromDto(dto: LoginResponseDto): User { return User( id = dto.userId, name = dto.nickname, avatarUrl = dto.avatar, phone = dto.mobile ) } }

这样的Mapper看起来很简单,但它的价值是把所有“字段重命名”和“类型不一致”的地方集中到一处管理,一旦API字段变了,只改这一个文件,不影响Domain层和UI层。

4. 常见问题与排查技巧实录

4.1 UseCase数量爆炸了怎么办

Clean Architecture最常见的落地问题就是UseCase泛滥。有人严格按照“一个业务动作一个UseCase”的教条来设计,结果一个小型功能页面上来了十好几个UseCase类,看着就头疼,维护成本反而上去了。

我自己在实践过程中逐步调整了策略。原则是:写操作和复杂业务校验独立成UseCase,简单读操作允许合并。比如获取用户资料的请求,如果只是一次简单查询,没有复杂的业务规则,直接让ViewModel依赖Repository接口就行,不需要中间再包一层GetUserProfileUseCase。但如果是“获取订单列表并且根据订单状态做聚合统计”这种有逻辑的场景,就值得单独建一个UseCase。

另外还有一个技巧:把UseCase定义成Kotlin的接口,然后再写实现类,这样在测试时才能方便地mock。但如果项目不大,UseCase本身可以直接用class,因为UseCase通常已经设计成无状态的了,不太需要mock一个接口。

4.2 Repository被“架空”了怎么办

第二种常见问题是Repository实现类退化成了网络接口的纯转发层,里面一行逻辑都没有,只是把RemoteDataSource的返回值原封不动传出去。出现这种情况说明Repository的设计失去了意义——它的价值在于封装数据获取策略,而不只是转发。

真正的Repository应该有策略、有判断、有合并。举个例子:我们项目里用户打开首页时,希望优先展示本地缓存的数据,让页面秒开,然后在后台拉取最新数据,等新数据到达后自动刷新界面。这个逻辑就是Repository的核心价值所在:

class HomeContentRepositoryImpl( private val remoteDataSource: HomeRemoteDataSource, private val localDataSource: HomeLocalDataSource ) : HomeContentRepository { override fun getHomeContent(): Flow<HomeContent> { return flow { localDataSource.getCachedContent()?.let { emit(it) } val latest = remoteDataSource.fetchHomeContent() localDataSource.updateCache(latest) emit(latest) } } }

这种Cache-Aside策略拆分到其他地方都不合适,放在Repository里反而非常自然。所以在写Repository的时候,多想想这个接口能不能体现“多个数据来源协调”的价值,如果只是单纯的透传,那不如砍掉这个中间层。

4.3 架构边界被无意间破坏了怎么办

再好的架构设计也扛不住“人人都觉得自己可以改一下”的团队。最常见的一种破坏方式:有人在Domain层的接口里加了@POST注解,直接把Retrofit的注解带进业务层;有人为了图快在UseCase里直接调用网络请求的静态方法;还有人把Room的Entity直接当Domain模型用。

靠代码评审来防止这种问题,效果有限。更好的办法是用Gradle模块依赖配合lint或者编译期检查来强制约束。比如在core:domain模块的build.gradle里,确保它不依赖任何Android框架的库以及Data层的库:

dependencies { implementation project(":core:common") // 只能依赖纯工具模块 // 严禁 implementation project(":core:data") // 严禁 implementation "com.squareup.retrofit2:retrofit:..." }

如果core:domain的代码依赖了Retrofit,编译时就会直接报错,这种“物理性”的约束比“人治”可靠得多。另外一个还要注意的点是依赖反过来也不行:feature层不能直接依赖core:data的具体实现类,要用接口注入。如果UI层直接new一个LoginRepositoryImpl,架构约束也名存实亡了。

4.4 模块间编译变慢、开发效率下降怎么办

多模块化配合Clean Architecture会带来一个实际成本:编译时间上升。每次改一行Domain层代码,所有依赖这个模块的上层模块都要重新编译,这在大型项目里可能就是好几分钟的等待。

我的处理经验有几个。第一,尽量做增量编译,AS的Instant Run和Kotlin的增量编译配合好;第二,不要在Domain层塞太多代码,Domain层的修改影响范围是最大的,把一些纯UI状态的封装放到feature模块里,减少Domain层变更的频率;第三,善用模块间的“空debounce”,比如本地开发时用@Preview直接调试UI,不跑真机编译;第四,如果项目真的很大,可以考虑把feature模块和core模块做成独立的Gradle Project,通过二进制依赖来加快构建速度。

5. 团队落地与后续扩展

5.1 新成员如何快速理解这套架构

架构落地最大的阻力往往不是技术问题,而是人的习惯问题。新加入的同事如果以前习惯了在Activity里写逻辑,突然让他在五层结构里加个功能,他会不知道代码该写在哪。

我自己的做法是在项目根目录维护一份简短的架构文档,不写大道理,就写清楚三件事:每层放什么、不能放什么、依赖方向长什么样。尤其要写清楚“反例”,比如“不要在ViewModel里直接调用网络库”、“不要在Repository里写UI逻辑”,这种反例比正向说明更直观。另一个手段是代码评审。前几个月新成员写的Merge Request我会特别关注分层边界,一旦发现问题,直接指出来比事后帮助文档印象深刻得多。

5.2 这套架构为后续演进留了哪些空间

Clean Architecture最让我觉得值得的地方,是它为项目后续演进留足了空间。我们团队在半年内干了两件大事:一是把UI从XML迁移到Jetpack Compose,按传统写法这基本等于重写表现层,但在Clean Architecture的结构下,我们只改了feature模块和少量ViewModel的数据绑定方式,Domain层和Data层的代码几乎没动;二是换了网络层的数据解析方式,从Gson换到了Kotlin Serialization,涉及到的也只是Data层内部的数据源实现和Mapper调整。

如果你的项目正处于一个业务快速扩张、团队不断扩大的阶段,我的建议是:架构设计可以适当激进一点,但“激进”指的是严格遵守分层边界,而不是为了过度设计而设计。我见过有些团队为了证明自己用了Clean Architecture,引入了大量不必要的模式,结果类数量暴涨,开发效率严重下降,最后架构被Team集体抵制。架构还是要服务于业务和团队,它只是手段,不是目的。

最后再分享一个我个人的体会:Clean Architecture的价值不是一朝一夕能看出来的,而是在项目经历了几次“伤筋动骨”的变更之后,你才会真正感激当初那个坚持分层、坚持接口隔离的自己。每当你发现换一个网络库、换一种UI方案、加一套缓存策略都不需要触碰核心业务代码的时候,你就知道这套架构带来的长期收益,已经远大于当初那一点点代码量和设计成本了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 20:40:07

威联通NAS数据治理实战:从存储设备到医疗器械受控数据底座

去年质量体系审核那天&#xff0c;检查老师坐在会议室&#xff0c;要求我们调出去年7月某批次产品的全部检验原始数据。质量部同事登录威联通&#xff0c;打开检验记录共享区&#xff0c;按日期筛出文件夹&#xff0c;导出PDF&#xff0c;连同文件属性里的创建时间、修改历史和…

作者头像 李华
网站建设 2026/9/24 20:40:03

OpenCV运动物体检测实战:背景减除算法选型与参数调优指南

做运动物体检测这件事&#xff0c;我用Python和OpenCV前前后后折腾了两个多月&#xff0c;踩了不少坑&#xff0c;也摸索出一套可以直接上手的方案。如果你正好也在研究这个&#xff0c;或者准备做一个人脸识别、安防报警、人流统计之类的项目&#xff0c;那我这篇应该能帮你少…

作者头像 李华
网站建设 2026/9/24 20:39:33

OpenCV运动物体检测实战:背景建模、代码调参与避坑指南

前阵子有个朋友问我&#xff0c;能不能给老家院子里的摄像头加个功能&#xff1a;一有人进院子&#xff0c;手机就收到提醒。这需求听着很玄乎&#xff0c;但拆开来看&#xff0c;核心就一件事——用Python和OpenCV把画面里“动起来”的区域找出来。运动物体检测在安防监控、无…

作者头像 李华
网站建设 2026/9/24 20:39:05

CodeIgniter 4实战:轻量PHP框架的安装、MVC与安全开发指南

1. 为什么还在谈CodeIgniter&#xff1f;——框架定位与上手前的准备工作聊到PHP框架&#xff0c;很多人第一反应是Laravel、Symfony这些主流选手&#xff0c;但CodeIgniter在我心里一直有个特殊位置。它体积小、起步快、文档清晰&#xff0c;不需要命令行工具也能跑起来&#…

作者头像 李华
网站建设 2026/9/24 20:34:49

UniApp实战:美妆教程小程序从开发到上线的完整方案

做个美妆教程小程序&#xff0c;是我今年开春接到的一个比较完整的商业项目。甲方要的不是简单的内容展示&#xff0c;而是一个集视频教程、图文专栏、社区晒妆、课程购买于一体的平台。技术栈当时锁死&#xff1a;微信小程序 UniApp。花了两周时间搭完基础版&#xff0c;又用…

作者头像 李华