
在 ArkTS 和 ArkUI 開發中一個頁面通常包含三部分內容頁面展示用戶交互數據請求和業務處理。如果所有代碼都寫在一個Component中隨著功能增加組件會變得越來越臃腫。MVC、MVP 和 MVVM 的作用就是將頁面、業務和數據進行合理拆分。本文使用同一個“登錄頁面”來理解這三種架構。一、三種架構共同解決的問題假設登錄頁面需要完成以下功能獲取用戶輸入的賬號和密碼校驗賬號和密碼調用登錄接口顯示加載狀態顯示登錄成功或失敗登錄成功后跳轉頁面。如果這些邏輯全部寫在頁面組件中EntryComponentstruct LoginPage{Stateaccount:stringStatepassword:stringStateloading:booleanfalseStatemessage:stringprivateasynclogin():Promisevoid{// 參數校驗// 網絡請求// 錯誤處理// 頁面狀態修改// 頁面跳轉}build(){// 頁面布局}}隨著功能增加LoginPage會同時負責UI 布局狀態管理輸入校驗網絡請求數據轉換錯誤處理頁面跳轉。這類組件通常被稱為“胖組件”。MVC、MVP、MVVM 的核心目標就是讓不同對象承擔不同職責。二、公共的 Model 層無論采用哪種架構Model 的職責基本一致。Model 負責數據實體網絡請求數據庫存儲業務數據處理底層服務能力。例如classUser{id:numbername:stringconstructor(id:number,name:string){this.ididthis.namename}}classLoginService{asynclogin(account:string,password:string):PromiseUser{if(accountadminpassword123456){returnnewUser(1,管理員)}thrownewError(賬號或密碼錯誤)}}LoginService只負責完成登錄業務不關心頁面中使用的是Text Button TextInput LoadingProgressModel 不應該依賴具體 UI。三、MVCController 直接協調 Model 和 ViewMVC 分為Model View Controller其中Model負責數據和業務能力View負責界面展示Controller接收用戶事件調用 Model并更新 View。在 ArkUI 中Component經常同時承擔 View 和 Controller 的職責build() → View 事件處理方法 → ControllerMVC 示例EntryComponentstruct LoginMvcPage{Stateaccount:stringStatepassword:stringStateloading:booleanfalseStatemessage:stringprivateloginService:LoginServicenewLoginService()privateasynconLoginClick():Promisevoid{if(this.account.length0){this.message請輸入賬號return}if(this.password.length6){this.message密碼不能少于 6 位return}this.loadingtruethis.messagetry{constuserawaitthis.loginService.login(this.account,this.password)this.message歡迎你${user.name}}catch(error){this.message賬號或密碼錯誤}finally{this.loadingfalse}}build(){Column({space:16}){TextInput({placeholder:請輸入賬號,text:this.account}).onChange((value:string){this.accountvalue})TextInput({placeholder:請輸入密碼,text:this.password}).onChange((value:string){this.passwordvalue})Button(this.loading?登錄中...:登錄).enabled(!this.loading).onClick((){this.onLoginClick()})if(this.loading){LoadingProgress()}Text(this.message)}.padding(20)}}MVC 的數據流用戶點擊 ↓ View 觸發事件 ↓ Controller 處理邏輯 ↓ Controller 調用 Model ↓ Model 返回結果 ↓ Controller 修改頁面狀態 ↓ View 重新渲染可以概括為View → Controller → Model View ← Controller ← ModelMVC 的特點MVC 最大的特點是Controller 直接讀取 View 的數據也直接修改 View 的狀態。例如this.loadingtruethis.message登錄失敗這種方式簡單直接適合業務較少的頁面。但頁面復雜后Controller 邏輯容易全部堆積在組件中最終形成“胖組件”。四、MVPPresenter 通過接口命令 ViewMVP 分為Model View PresenterPresenter 可以理解為從 Controller 中抽離出來的業務協調者。它負責獲取用戶輸入校驗數據調用 Model處理業務結果命令 View 更新頁面。MVP 的核心是Presenter 不直接操作具體 UI而是通過 View 接口控制頁面。定義 View 接口interfaceLoginViewContract{getAccount():stringgetPassword():stringshowLoading():voidhideLoading():voidshowMessage(message:string):voidshowLoginSuccess(user:User):void}這個接口規定登錄頁面必須具備哪些能力。Presenter 只認識這個接口并不知道頁面具體使用的是Text、Toast 還是 Dialog。Presenter 實現classLoginPresenter{privateview:LoginViewContractprivateloginService:LoginServiceconstructor(view:LoginViewContract,loginService:LoginService){this.viewviewthis.loginServiceloginService}asynclogin():Promisevoid{constaccountthis.view.getAccount()constpasswordthis.view.getPassword()if(account.length0){this.view.showMessage(請輸入賬號)return}if(password.length6){this.view.showMessage(密碼不能少于 6 位)return}this.view.showLoading()try{constuserawaitthis.loginService.login(account,password)this.view.showLoginSuccess(user)}catch(error){this.view.showMessage(賬號或密碼錯誤)}finally{this.view.hideLoading()}}}Presenter 中沒有出現任何 ArkUI 控件Component State TextInput Button Text因此 Presenter 可以脫離頁面獨立測試。MVP 頁面EntryComponentstruct LoginMvpPage{Stateaccount:stringStatepassword:stringStateloading:booleanfalseStatemessage:stringprivatepresenter:LoginPresenter|nullnullaboutToAppear():void{constview:LoginViewContract{getAccount:():string{returnthis.account},getPassword:():string{returnthis.password},showLoading:():void{this.loadingtrue},hideLoading:():void{this.loadingfalse},showMessage:(message:string):void{this.messagemessage},showLoginSuccess:(user:User):void{this.message歡迎你${user.name}}}this.presenternewLoginPresenter(view,newLoginService())}build(){Column({space:16}){TextInput({placeholder:請輸入賬號,text:this.account}).onChange((value:string){this.accountvalue})TextInput({placeholder:請輸入密碼,text:this.password}).onChange((value:string){this.passwordvalue})Button(this.loading?登錄中...:登錄).enabled(!this.loading).onClick((){this.presenter?.login()})if(this.loading){LoadingProgress()}Text(this.message)}.padding(20)}}MVP 的數據流用戶點擊 ↓ View 將事件交給 Presenter ↓ Presenter 獲取 View 輸入 ↓ Presenter 調用 Model ↓ Model 返回結果 ↓ Presenter 調用 View 接口 ↓ View 修改頁面狀態 ↓ 頁面重新渲染可以概括為View ? Presenter ? ModelMVP 的核心特征Presenter 會調用this.view.showLoading()this.view.showMessage(登錄失敗)this.view.showLoginSuccess(user)這些代碼表達的是View請顯示加載狀態 View請顯示錯誤信息 View請顯示登錄成功所以 MVP 的本質是Presenter 輸出的是行為和命令。Presenter 雖然不認識具體控件但它知道 View 可以執行哪些操作。五、MVVMViewModel 提供頁面狀態MVVM 分為Model View ViewModelViewModel 負責保存頁面狀態接收頁面輸入處理輸入校驗調用 Model轉換頁面數據提供頁面可以直接使用的狀態。MVVM 的核心是ViewModel 不命令 View 做什么只修改自身狀態View 根據狀態重新渲染。ViewModel 實現ObservedclassLoginViewModel{account:stringpassword:stringloading:booleanfalsemessage:stringprivateloginService:LoginServiceconstructor(loginService:LoginService){this.loginServiceloginService}getloginEnabled():boolean{returnthis.account.length0this.password.length6!this.loading}setAccount(account:string):void{this.accountaccount}setPassword(password:string):void{this.passwordpassword}asynclogin():Promisevoid{if(this.account.length0){this.message請輸入賬號return}if(this.password.length6){this.message密碼不能少于 6 位return}this.loadingtruethis.messagetry{constuserawaitthis.loginService.login(this.account,this.password)this.message歡迎你${user.name}}catch(error){this.message賬號或密碼錯誤}finally{this.loadingfalse}}}ViewModel 中沒有Text Button TextInput LoadingProgress它只維護頁面狀態。MVVM 頁面EntryComponentstruct LoginMvvmPage{StateviewModel:LoginViewModelnewLoginViewModel(newLoginService())build(){Column({space:16}){TextInput({placeholder:請輸入賬號,text:this.viewModel.account}).onChange((value:string){this.viewModel.setAccount(value)})TextInput({placeholder:請輸入密碼,text:this.viewModel.password}).onChange((value:string){this.viewModel.setPassword(value)})Button(this.viewModel.loading?登錄中...:登錄).enabled(this.viewModel.loginEnabled).onClick((){this.viewModel.login()})if(this.viewModel.loading){LoadingProgress()}Text(this.viewModel.message)}.padding(20)}}MVVM 的數據流用戶輸入或點擊 ↓ View 調用 ViewModel ↓ ViewModel 調用 Model ↓ Model 返回結果 ↓ ViewModel 修改自身狀態 ↓ View 讀取新狀態 ↓ 頁面重新渲染可以概括為View ? ViewModel ? ModelMVVM 的核心特征ViewModel 只會修改this.loadingtruethis.message登錄失敗它不會調用this.view.showLoading()this.view.showMessage()View 根據狀態決定頁面如何顯示if(this.viewModel.loading){LoadingProgress()}Text(this.viewModel.message)因此 MVVM 的本質是ViewModel 輸出的是狀態和數據。六、MVP 和 MVVM 的核心區別MVP 和 MVVM 都把業務邏輯從頁面組件中抽離但它們與 View 的通信方式不同。MVPPresenter 命令 Viewthis.view.showLoading()this.view.showMessage(登錄失敗)Presenter 的意思是View你現在顯示加載。 View你現在顯示錯誤。Presenter 輸出的是行為。MVVMViewModel 修改狀態this.loadingtruethis.message登錄失敗ViewModel 的意思是我現在處于加載狀態。 我現在有一條錯誤信息。View 根據這些狀態決定如何渲染。ViewModel 輸出的是狀態。因此可以記住Presenter 輸出行為 ViewModel 輸出狀態七、Model 和 ViewModel 的區別Model 表示真實業務數據。例如classUser{firstName:stringlastName:stringage:number0vip:booleanfalse}這些是服務器或數據庫中的原始數據。但頁面可能需要顯示張三 18歲 VIP會員ViewModel 可以將 Model 轉換成適合頁面直接展示的數據classUserViewModel{displayName:stringageText:stringvipText:stringupdate(user:User):void{this.displayName${user.firstName}${user.lastName}this.ageText${user.age}歲this.vipTextuser.vip?VIP會員:普通用戶}}所以Model真實業務數據 ViewModel頁面需要的數據和狀態八、三種架構的核心對比架構中間層業務邏輯位置UI 更新方式MVCControllerComponent 或 ControllerController 直接修改狀態MVPPresenterPresenterPresenter 通過接口命令 ViewMVVMViewModelViewModelView 根據 ViewModel 狀態渲染進一步理解問題MVCMVPMVVM誰處理點擊事件ControllerPresenterViewModel誰執行輸入校驗ControllerPresenterViewModel誰調用 ModelControllerPresenterViewModel誰修改 UIControllerViewView中間層輸出什么直接操作行為、命令狀態、數據是否依賴狀態綁定不強制不強制通常依賴測試難度相對較高較低較低九、ArkTS 項目應該如何選擇簡單頁面使用 MVC例如關于頁面靜態詳情頁簡單設置頁只有少量狀態和交互的頁面。這類頁面直接在 Component 中處理邏輯即可沒必要創建大量額外對象。流程型頁面適合 MVP例如多步驟注冊實名認證權限申請復雜表單提交分步驟業務流程。這類頁面經常需要明確控制 View顯示下一步 隱藏當前區域 彈出錯誤提示 進入成功頁面Presenter 輸出行為會比較直觀。狀態復雜的頁面適合 MVVM例如下載頁面播放器頁面聊天頁面購物車頁面股票行情頁面多狀態列表頁面。這些頁面通常有大量狀態加載中 加載成功 加載失敗 空數據 刷新中 是否可點擊 下載進度 播放進度 未讀數量MVVM 可以把這些狀態集中放在 ViewModel 中由 View 聲明式渲染。十、為什么 MVVM 與 ArkUI 很契合ArkUI 是聲明式 UI。開發者不是一步一步命令頁面創建 Loading 添加 Loading 隱藏按鈕 修改文字 移除 Loading而是描述頁面在某種狀態下應該是什么樣子if(this.viewModel.loading){LoadingProgress()}Button(登錄).enabled(this.viewModel.loginEnabled)Text(this.viewModel.message)當狀態變化時ArkUI 會重新計算和渲染相關 UI。這與 MVVM 的狀態驅動思想天然一致ViewModel 管理狀態 View 描述狀態對應的界面十一、任何架構都可能變得臃腫MVC 可能出現Massive ComponentMVP 可能出現Massive PresenterMVVM 可能出現Massive ViewModel如果 ViewModel 同時負責網絡請求數據庫存儲JSON 解析頁面跳轉權限申請埋點文件下載緩存處理頁面狀態那么它同樣會非常臃腫。更合理的結構可以繼續拆分為View ↓ ViewModel / Presenter ↓ UseCase / Service ↓ Repository ↓ 網絡和數據庫架構的重點不是類名而是職責是否清晰。十二、最終總結MVC、MVP 和 MVVM 都是在解決頁面代碼職責混亂的問題。MVCView 觸發事件 → Controller 調用 Model → Controller 直接修改 View一句話總結Controller 直接協調 Model 和 View。MVPView 把事件交給 Presenter → Presenter 調用 Model → Presenter 通過接口命令 View一句話總結Presenter 不直接操作控件但會命令 View 做什么。MVVMView 把輸入交給 ViewModel → ViewModel 調用 Model → ViewModel 修改狀態 → View 根據狀態重新渲染一句話總結ViewModel 不命令 View只提供 View 需要的狀態。最終可以壓縮為三句話MVCController 直接修改 View。 MVPPresenter 通過接口命令 View。 MVVMViewModel 修改狀態View 根據狀態自己更新。再進一步壓縮MVC 直接協調 MVP 行為驅動 MVVM 狀態驅動