對于MVVM,多一些思考總是沒差的

寫在前面

MVC,MVP,MVVM……移動端的開發(fā)可謂是在MVX的海洋中摸爬滾打!然而,V和M的概念不說,關(guān)于P,關(guān)于VM,它為什么叫Presenter,為什么叫ViewModel?我們實踐中的P,VM所做的事情真的和它們的概念對得上么?

本篇即基于MVVM在移動端的應(yīng)用這一話題做一些簡單的討論,希望大家可以借以回顧自己搭過的框架,碼過的代碼,能喚起一些有趣更有意義的思考!

第一篇:我對“MVVM”的初識

那是在2015年的時候,MVVM被炒的火熱,一日,面朝我已經(jīng)盡心竭力做好了概念分組的“巨型VC”,我無力地將頭專向小馬哥,“編輯這部分代碼我還是感到很難受……”

//某VC中
[aRequester sendPostReqWithUrl:aUrl paras:paraDic response:^(id respData, NSError *error) {
    [Error check code];
    NSDictionary *tmpDic = (NSDictionary *)respData;
    NSNumber *tmpNumber = [tmpDic objectForKey:@"boolVal"] ;  
    if (YES == [tmpNumber boolValue]) {
       _contentView.titleLable.text = [tmpDic objectForKey:@"title"] ; 
    } else {
       _contentView.titleLable.text = [tmpDic objectForKey:@"title2"] ;
    }}];

如上,很常見的,網(wǎng)絡(luò)請求后,錯誤檢查,解析數(shù)據(jù)并用數(shù)據(jù)更新視圖。
“如此清晰順暢無比的場景,還能如何優(yōu)化呢?‘感到難受’,這也沒辦法啦,什么都不能做了!”我們有這樣自我合理化的內(nèi)心os太正常不過了,但多年的經(jīng)驗不斷地讓我印證一個真理:事出反常必有鬼(感覺不爽,必可優(yōu)化)。果然!……

“那是因為你把視圖和邏輯耦合到一起了!”小馬哥回答。

Step1 可以將我們期望在回調(diào)中做的事情進(jìn)行簡單的概念分組

//某VC中
[aRequester sendPostReqWithUrl:aUrl paras:paraDic response:^(id respData, NSError *error) {
    /* Error check */
    /* Handle Parser */
    /* Update View */
}];

Step2 分別抽象出解析&更新視圖的具體處理

/* Handle Parser */
- (NSString *)parserTitle:(NSDictionary *respData) {
    NSString *tmpStr = nil;
    NSNumber *tmpNumber = [respData objectForKey:@"boolVal"] ;  
    if (YES == [tmpNumber boolValue]) {
        tmpStr = [respData objectForKey:@"title"] ; 
    } else {
        tmpStr = [respData objectForKey:@"title2"] ;
    }}];
    return tmpStr;
}

/* Update View */
- (void)updateViewWithTitle:(NSString *)title {
    _contentView.titleLable.text = title;
}

Step3 方法抽象封裝,并新建文件(logicModel)來承載功能簡單但實現(xiàn)復(fù)雜的邏輯代碼

//某VC中
[_logicModel requestTitleInfoWithResponse:^(NSString *title) {
    /* Update View */
}];

//封裝到logicModel文件中
- (void)requestUserInfoWithResponse:(void(^)(id userInfo, NSError *error))callback {
    [aRequester sendPostReqWithUrl:(NSString *)url paras:(NSDictionary *)paras response:(id respData, NSError *error) {
        /* Error check */
        /* Handle parser */
        /* Callback */
        callback(title);
    }];
}

有什么不一樣么?或許不經(jīng)過實踐操作中對代碼維護(hù)效率精益求精地追求,很難通過上面的例子直觀的理解抽象的好處。甚至?xí)幸恍└≡甑哪娣葱睦碜魉?,認(rèn)為這么做多此一舉。

抽象/封裝不一定都是好的,它們的應(yīng)用要權(quán)衡地考慮某個實現(xiàn)模塊的復(fù)雜性,從而選擇一個最合適的抽象層次。而抽象/封裝的最基本原則參考,我想應(yīng)當(dāng)是:概念合理。

上面的例子即是簡單地將“數(shù)據(jù)的處理”“視圖的更新”相互獨立起來,使視圖的更新更為純粹!簡言之,我們期望避免下面場景的出現(xiàn)

    if (YES == [tmpNumber boolValue]) {
       _contentView.titleLable.text = [tmpDic objectForKey:@"title"] ; 
    } else {
       _contentView.titleLable.text = [tmpDic objectForKey:@"title2"] ;
    }

然后,小馬哥告訴我,這就是MVVM,即當(dāng)前最流行的Model-View-ViewModel模式。

我上面起名為LogicModel的文件即為小馬哥所說的“VM”,它負(fù)責(zé)將網(wǎng)絡(luò)請求、請求解析和一些數(shù)據(jù)處理邏輯進(jìn)行封裝,從而使VC變得“輕”一些。

想象一下我們的思維走向:
1) 與視圖無關(guān)的數(shù)據(jù)邏輯問題,直接往從VM入手排查
2) 與視圖展示有關(guān)的邏輯問題,在VC到View的流程中一看數(shù)據(jù)對接,二看VM反饋的數(shù)據(jù)是否有誤。
可謂是結(jié)點清晰,定位問題毫無壓力!大贊!不愧是“MVVM”!

可是,VM就是邏輯封裝自然演化的一個“代號”么?

VM = View Model,是視圖的模型,“模型”一詞,從概念上傾向于一種“靜態(tài)”,而邏輯處理,網(wǎng)絡(luò)請求,信號接收這些趨向于一種異步的“動態(tài)”,而且好像和“視圖”的概念差別有些大。這種封裝固然有它的優(yōu)勢所在,但MVVM的設(shè)計者干嘛對它起名為VM呢?視圖的模型?叫LogicModel (邏輯模型),或者VCTool(VC工具)怎么都比VM合理吧?

只是一個名字而已嘛!然而,就我對“大?!钡睦斫?,他們對于某種概念的名稱擬定,是絕不會馬馬虎虎了事的!

VM的本源必然就是一個VM!一個視圖的模型!

第二篇:追溯MVVM的提出

或許是我對于VM的理解方向不對吧?畢竟從網(wǎng)上的眾多文章的分析說明,從同事的實踐中,我們對于MVVM又或MVP的在移動端的應(yīng)用實踐竟然出奇地一致?。ㄈ缦聢D)


image.png

翻閱了幾十篇相關(guān)的文章,每篇的說的頗有道理,很多文章還是分了上中下篇,并配以圖示,似頗為系統(tǒng)的對MVC,MVP,MVVM進(jìn)行介紹。不得不說,這些文章頗具指導(dǎo)意義,確實可以讓很多限于邏輯耦合深淵的朋友找到一盞明燈,讓他們的項目變得清晰而易于維護(hù)。但我還是任性地感覺,他們在打著MVVM的旗號在講VC減負(fù)——我想要做的,是接近MVVM的本源!

MVVM 最早于 2005 年被微軟的 WPF 和 Silverlight 的架構(gòu)師 John Gossman 提出,并且應(yīng)用在微軟的軟件開發(fā)中。我找到了那片博文,并進(jìn)行了翻譯和仔細(xì)的思考探究。

《Model/View/ViewModel pattern for building WPF apps》
John Gossman
譯文鏈接:
http://m.itdecent.cn/p/b0b80163782f
原文鏈接: https://blogs.msdn.microsoft.com/johngossman/2005/10/08/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps/

這篇博文中,有這樣這樣兩句有趣的話:

1)Model/View/ViewModel is a variation of Model/View/Controller (MVC) that is tailored for modern UI development platforms where the View is the responsibility of a designer rather than a classic developer.(譯:MVVM是MVC模式的一個演變,針對一個視圖的展示樣式,比起傳統(tǒng)的開發(fā)者,現(xiàn)在往往是設(shè)計師更為關(guān)注,MVVM正是為這種狀況而定制的一種模式。)

2)The term means "Model of a View", and can be thought of as abstraction of the view。(譯:它意為“視圖的模型”,可以將它想象成一個抽像化的視圖。)

那么,基于對MVVM本源的解讀,我們在一個“可以被想象成視圖的抽象”VM中添加大量網(wǎng)絡(luò),頁面跳轉(zhuǎn)等邏輯顯然不太合適了。(它是視圖,它是視圖,它是視圖,請這樣對自己洗腦!)同時,我們也想思考下關(guān)于“讓設(shè)計師去完成視圖展示”這個有趣的點。

第三篇:MVVM基于WPF的應(yīng)用(最初的應(yīng)用場景)

WPF即Windows Presentation Foundation,是微軟推出的基于Windows 的用戶界面框架。

第二篇的譯文中原作者舉例的應(yīng)用界面是Sparkle,我這邊則以類似的OmniGraffle(一款原型繪制軟件)的界面進(jìn)行說明。(沒什么特別的原因,因為我正在用OmniGraffle,更容易截圖:)

image.png

如果大家閱讀了第二篇提供的《Model/View/ViewModel pattern for building WPF apps》,你會發(fā)現(xiàn)文中舉例的Sparkle界面操作欄和我舉例的OmniGraffle界面圈紅的部分是很相似的。那么,參照文中的VM劃分方式,我們可以設(shè)計A部分對應(yīng)一個ViewModel A(當(dāng)然OmniGraffle不一定是這樣實現(xiàn)的),B部分對應(yīng)一個ViewModel B,然后B部分的“填充”,“筆畫”,“陰影”,“形狀”,“線條”亦可以分別對應(yīng)5個小的ViewModel……(如下圖)

image.png

一個有趣的點,View的層次疊加變成了VM的層次疊加!VM真如一個View的抽象一般!

同時,第二篇摘錄的另一段譯文引導(dǎo)的另一個問題:什么叫設(shè)計師更關(guān)注UI的頁面展示?不要小看我們UI同學(xué)哦,當(dāng)下很多的設(shè)計師都有css、html的開發(fā)經(jīng)驗,同時MVVM由微軟提出,記得微軟有一個自己的XAML吧?它正式一種搭建UI的語言。所以,基于MVVM的模式,我們至少可以從概念上將視圖完全剝離(甚至交給UI同學(xué)去渲染與實現(xiàn)),模型中只要有視圖中需要展示的元素的具體內(nèi)容數(shù)據(jù)即可,他不關(guān)心任何視圖的布局,渲染效果。

同事,針對視圖的布局和效果的動態(tài)改變,我們將這些改變抽象成狀態(tài),存放在VM當(dāng)中。至此,一個最最簡單的MVVM元組得以實現(xiàn)。

image.png

第四篇:MVVM基于APP的應(yīng)用

回到市面上較為流行的一種類“MVVM實踐模式”,它們以“MVC+VC減負(fù)+概念抽象封裝”作為基本的思路參考,讓VC作為VM和View溝通的主橋梁。

如下圖,一般是一個VC包含一個VM和一個主View,然后VM或許會處理少量“雙向綁定的任務(wù)”,同時也可能將更多的比如網(wǎng)絡(luò)答復(fù)的操作動作回調(diào)給VC去處理視圖更新

image.png

這種模式易于理解也確實可以實際的提高代碼的概念性和可維護(hù)性。但我們發(fā)現(xiàn),視圖的更新走了兩條長線
1) 介由VM的綁定實現(xiàn)模型更新視圖;
2) 借由VM的回調(diào)實現(xiàn)VC控制更新視圖。

這總讓我們感到不夠清爽:當(dāng)我希望將視圖中的一段文字由“我的領(lǐng)導(dǎo)是個壞人”改為“我的領(lǐng)導(dǎo)是個好人”時候,沒有明確的概念告訴我哪一條“線”是有決策力的“線”(可以成功進(jìn)行修改的線)。

“看代碼不就知道了?”
請記?。?br> 1 有思想的代碼幾乎不需要透過代碼來定位問題
2 維護(hù)代價的“積累”不是“疊加”而是“邏輯分支的疊乘”(每一次“選線”的猶豫,都是一層邏輯分支)

所以,看代碼當(dāng)然可以解決問題!甚至針對復(fù)雜的工程,你大可花費一個月將它的每個細(xì)節(jié)流程完全理透!然后心滿意足的大贊自我的耐心和代碼閱讀能力!不想,領(lǐng)導(dǎo)已經(jīng)看到了那句你還沒有來得及改掉的“我的領(lǐng)導(dǎo)是個壞蛋”……

言歸正傳,第三篇我們基于MVVM在WPF中的應(yīng)用分析貌似還蠻順暢的,但好像應(yīng)用再APP中,有什么地方有些……怪!根源在哪里?或許如下幾個問題可以作為我們的參考:
1) VC的地位到底更傾向于什么?是V?是C?是VM?
2) 網(wǎng)絡(luò)請求/視圖生命周期/路由跳轉(zhuǎn)這些在APP端大量出現(xiàn)的概念模塊,它們在MVVM中有著怎樣的概念歸屬?

4.1 MVVM基于APP的基礎(chǔ)架構(gòu)&模塊分工

我們嘗試一下下面的這套交互結(jié)構(gòu)

image.png

首先我們明確一個點,在一個MVC結(jié)構(gòu)中,即便拋開視圖后,模型和控制器處理的大部分業(yè)務(wù)邏輯,都是為視圖服務(wù)的。我們常說的“重VC”,很大一部分重在視圖相關(guān)的邏輯或是為之服務(wù)的邏輯。

所以,當(dāng)我們抽象一些視圖的基礎(chǔ)模型,并通過VM將視圖本身的(不需要與外界交互的)狀態(tài)變遷邏輯封裝在一個MVVM組的內(nèi)部,對外(對VC)只暴露必要的數(shù)據(jù)更新和消息回調(diào)接口。繁瑣的視圖邏輯就可以被限制在一個MVVM當(dāng)中(它確實也應(yīng)當(dāng)在那里)。這時留在VC中的邏輯,一般情況下就很少了。如果此刻的VC還讓你感到“重”的話,我們大可再對其抽象一個VC-Logic,將復(fù)雜的邏輯進(jìn)行封裝。

各個模塊所負(fù)責(zé)的主要工作可以參考下圖

image.png

如圖,VC中的“生命周期控制”,“網(wǎng)絡(luò)請求”,“路由”,View中的“視圖布局”,“控件效果”都很好理解,讓人一眼摸不清的概念主要存在所謂的VM當(dāng)中,我們來簡單說明:

1)什么是“處理視圖狀態(tài)”?
視圖可能根據(jù)不同的狀態(tài)有不同的展示內(nèi)容,甚至展示效果。我們常見的“cur”(current)前綴就適用于說明這種場景?!爱?dāng)前選擇的模塊”,“某個按鈕當(dāng)前的選擇狀態(tài)”,這些表示視圖狀態(tài)的操作變量的定義應(yīng)當(dāng)在VM當(dāng)中,相關(guān)的邏輯交互也應(yīng)當(dāng)在VM當(dāng)中。如果說View提供了視圖的所有展示元素;那么VM則可以確定某個視圖模塊某一時刻某一個狀態(tài)下的呈現(xiàn)內(nèi)容。

2)什么是“處理視圖協(xié)作”?
一個VM不一定只和一個View存在關(guān)聯(lián),它可能同時協(xié)調(diào)多個視圖。
我們以同程旅行的一個篩選界面作為參考場景進(jìn)行說明:

image.png

當(dāng)我們將“4.5分以上”后面的對號勾上的時候,上面的“4.5分以上”會被同步勾取,同時,“評分”后面會多出個小綠點,這表示評分這頁的篩選條件選擇的不是默認(rèn)的“不限”。很顯然,關(guān)鍵詞模塊、篩選分類模塊、篩選詳情模塊正常人都會分成3部分視圖繪制。這三個視圖間顯然是有交互關(guān)系的(即“篩選詳情模塊”的勾選觸發(fā)了“關(guān)鍵詞模塊”的高亮和“篩選分類模塊”的加點),而VM即是處理這種交互關(guān)系理想場所。

3)什么是“數(shù)據(jù)綁定”?
這邊特指將一個模型數(shù)據(jù)和視圖中的一個展示內(nèi)容進(jìn)行關(guān)聯(lián)綁定;
單向綁定一般指模型數(shù)據(jù)變化觸發(fā)對應(yīng)的視圖數(shù)據(jù)變化
雙向綁定指模型數(shù)據(jù),視圖數(shù)據(jù)任意一方變化,都會觸發(fā)另一方的同步變化。

4)什么是“數(shù)據(jù)轉(zhuǎn)換”?
我們不能企望所有的模型數(shù)據(jù)都能直接被視圖使用,比如模型中是一個BOOL(0/1)值,而對應(yīng)的視圖展示期望為“是”/“否”,類似這樣的數(shù)據(jù)轉(zhuǎn)化工作,交給VM吧!

4.2 MVVM基于APP的抽象討論

我們再來討論一下幾個觀點的理解:
1)VC是特殊的VM
很常見的,VC中除了主要的展示視圖外,還有一個導(dǎo)航條(NavigationBar),而我們又很常見導(dǎo)航條要根據(jù)主視圖的滾動而改變展示效果(比如隨著視圖滾動變得透明),這種視圖的交互顯然只能在VC中處理。這很正常,VC可以理解為特殊的VM,即它會負(fù)責(zé)一些類似VM的協(xié)調(diào)工作(協(xié)調(diào)本身也是C的職責(zé)),亦會負(fù)責(zé)VC的其他本職工作(如控制視圖生命周期等)。

2)VM的是可以存在類似View的層次的
寫視圖,Subview(子視圖)的概念是逃不掉的,而參照“將VM理解為視圖”的思路,復(fù)雜視圖中,VM的層次也是逃不掉的,像圖中一樣。

image.png

大家會發(fā)現(xiàn),我在VC下面標(biāo)明了“mainVM”,在一些MVVM下面標(biāo)明了“mini”,mainVM好理解,因為前面我們已經(jīng)引入了“VC是特殊的VM這一思路”,但是mini呢?

大多數(shù)場景,我們一個頁面的視圖交互不會特別復(fù)雜,所以,一般的多層視圖,只用一個VM管理就夠了。但有時我們會希望對視圖層中的小模塊進(jìn)行MVVM封裝,因為它是“通用”的(希望被復(fù)用的),通用的小視圖模塊往往是“簡單”的,這是mini的第一層含義。

同時,當(dāng)我們沒有引入VM概念的時候,View就單純地是View么?想想UIButton吧,可以設(shè)定選擇狀態(tài)不說,它還可以隨時獲取當(dāng)前按鈕的選擇狀態(tài)(selected),這不就是說UIButton保存了視圖狀態(tài)么!如果把UIButton進(jìn)行細(xì)致的概念拆分,不就變成了我們的MVVM組么!所以,我們很多系統(tǒng)的視圖控件,本來就可以理解為mini的MVVM。

VM從某種角度上講,就是一個視圖!

第五篇:RAC對iOS實踐MVVM的價值

很顯然,上面的講述中,我們只字未提到RAC(ReactiveCocoa),所以,RAC本身是和MVVM沒有本質(zhì)上的關(guān)聯(lián)的。但無可反駁的是,使用RAC確實能讓MVVM的實踐上顯得更加精巧。

5.1 快速綁定

我們在應(yīng)用中運用的視圖更新接口,block回調(diào),代理,通知,KVO,目標(biāo)動作對……都可以理解為廣義“綁定”所依賴的技巧RAC則將上述機(jī)制統(tǒng)一成“消息”,可以讓我們以更簡單的方式處理綁定動作。請看下面的例子。

1)常規(guī)方式:雙向綁定一個字符串和一個textField的text值

/* 1. 使textField中的text改變時,字符串textStr可以同步變化 */
[_textField addTarget:self action:@selector(valueChanged:) forControlEvents:UIControlEventEditingChanged];

- (void)valueChanged:(UITextField *)textField {
    _textStr = _textField.text;
}

/* 2. 使textStr改變時,textField中的text可以同步變化 */
[self addObserver:self
       forKeyPath:@"textStr"
          options:NSKeyValueObservingOptionNew
          context:nil];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    if (object == self && [keyPath isEqualToString:@"textStr"]) {
        _textField.text = _textStr;
    }
}

2)RAC方式:雙向綁定一個字符串和一個textField的text值

/* 1. 使textField中的text改變時,字符串textStr可以同步變化 */
RAC(self, textStr) = _textField.rac_textSignal;
/* 2. 使textStr改變時,textField中的text可以同步變化 */
RAC(self.textField, text) = RACObserve(self, textStr);

代碼的簡化是顯而易見的。

5.2 多元監(jiān)聽

iOS中對于代理的應(yīng)用場景還是很多的。而基本的代理模式中,某個模塊的代理者只能有一個。為了讓多個對象同時接收代理消息,我們不得不修改模塊結(jié)構(gòu),又或者自定制一個自以為很簡單完美的代理隊列,又或?qū)⒋?strong>改用通知?!(不想玩死自己的話,放棄在局部使用這種思路吧!)甚至,還有更奇葩的設(shè)計。
然而,在RAC中很簡單。

下面的代碼即實現(xiàn)了textStr和textStr2同時監(jiān)聽textField的text的變化
(處理代理一樣的簡單,因為RAC全部將其抽象成為了“消息”)

    RAC(self, textStr) = _textField.rac_textSignal;
    RAC(self, textStr2) = _textField.rac_textSignal;

但是,應(yīng)用中的意義呢?
將我們封裝的VM可以理解為一個模塊,對一個模塊而言,沒什么比輸入輸出接口的設(shè)計更加重要了。而互聯(lián)網(wǎng)時代,神奇的需求變動在很多場景下讓我們不得不對模塊進(jìn)行更新,甚至更新模塊的對外接口。多元監(jiān)聽的支持可以大大降低模塊對外接口更新的復(fù)雜性。(接口的更新很容易牽連整個模塊的基礎(chǔ)框架,更細(xì)節(jié)的分析在此不再贅述)

5.3 元組的引入

元組,并不是一個讓人感到陌生的概念,它即代表一組約定的有序數(shù)據(jù),該組數(shù)據(jù)中每個數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)不需要統(tǒng)一。
在MVVM中(其實普通的視圖設(shè)計中也是),為了方便視圖的展示,我們常常要約定一些輕量級的純視圖數(shù)據(jù)結(jié)構(gòu)。這時候Tuple或許會是最契合我們場景的概念。

為什么?
1)Tuple的概念定位不同于Array,tuple的長度一般是確定的,tuple組內(nèi)每個元素的類型不要求一致。
2)Tuple的概念亦不同于Dictionary,tuple無須將一個數(shù)組分為key,value兩個部分(繁瑣,麻煩~),同時,tuple是有序的(字典是無序的)。

當(dāng)然,介于tuple的靈活性特征,使用場景一定要控制在小范圍,需要定義對象的時候,還是要定義的!萬萬不可將tuple在不可控的大范圍使用。(一樣是玩死自己的行為)

5.4 沒有RAC不能應(yīng)用MVVM?

我不這么認(rèn)為:
1)如我們之前說過的,MVVM與RAC沒有本質(zhì)的關(guān)聯(lián)
2)如5.1~5.3,RAC可以使我們應(yīng)用MVVM的一些場景變得更為簡單優(yōu)雅,RAC針對MVVM優(yōu)化的問題在我們不使用MVVM時依然也存在(你用MVC是有些場景一樣要用KVO),而且,這些場景不算是決定性的(雙向綁定的實踐應(yīng)用場景其實很少)

結(jié)語

這篇文章的準(zhǔn)備在一個月前,中間因為主工作項目原因有各種間斷和擱置,但也慶幸有這樣的項目需求,可以讓我將其中部分的思路得以實踐和印證。相信這篇從MVVM的提出為出發(fā)點,經(jīng)過了反復(fù)思考印證,將模塊的概念分工多次推倒重組的文章,可以真正為大家對MVVM理解上提供有價值的思路參考,為實踐中遇到的一些讓人感到不舒服的代碼的優(yōu)化方向上提供有價值的思路參考,為MVVM在移動端的應(yīng)用實踐提供有價值的思路參考!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容