摘要:這里引出了一個(gè)概念,就是數(shù)據(jù)流這個(gè)概念,在項(xiàng)目中我將所有數(shù)據(jù)的操作都成為數(shù)據(jù)的流動(dòng)。
最近重構(gòu)了一個(gè)項(xiàng)目,一個(gè)基于redux模型的react-native項(xiàng)目,目標(biāo)是在混亂的代碼中梳理出一個(gè)清晰的結(jié)構(gòu)來(lái),為了實(shí)現(xiàn)這個(gè)目標(biāo),首先需要對(duì)項(xiàng)目的結(jié)構(gòu)做分層處理,將各個(gè)邏輯分離出來(lái),這里我是基于典型的MVC模型,那么為了將現(xiàn)有代碼重構(gòu)為理想的模型,我需要做以下幾步:
拆分組件
邏輯處理
抽象、聚合數(shù)據(jù)
組件化這是一個(gè)老生常談的問(wèn)題了,從16年起前端除了構(gòu)建工具,討論的最多的就是組件化了,把視圖按照一定規(guī)則切分為若干模塊過(guò)程就是組件化,那么組件化的重點(diǎn)就是那個(gè)規(guī)則。
那么這個(gè)規(guī)則又是什么呢?
按功能?按樣式?
我之前的項(xiàng)目里多數(shù)這兩種情況都存在,舉個(gè)簡(jiǎn)單的例子,對(duì)于app的登錄模塊來(lái)說(shuō)就是一個(gè)典型的按功能分組,而對(duì)于一個(gè)列表就是一個(gè)明顯的按樣式去組件化,他們兩個(gè)對(duì)應(yīng)著兩種完全不同的寫法,因?yàn)樗麄円粋€(gè)是充血模型,一個(gè)是貧血模型。在redux中,明顯的區(qū)別是貧血組件中一切的狀態(tài)全部外置,組件自身不去管理自己的狀態(tài),統(tǒng)統(tǒng)放到reducer;而在充血組件中,一部分狀態(tài)由全局的store去管理,一部分有自身的state控制。
// 充血組件 // 貧血組件 組件A | 組件B | 組件C 組件A | 組件B | 組件C 邏輯A | 邏輯B | 邏輯C --------------------- 數(shù)據(jù)A | 數(shù)據(jù)B | 數(shù)據(jù)C 邏輯層 ------------------- --------------------- 全局邏輯 數(shù)據(jù)層
在我重構(gòu)的過(guò)程中更傾向于將組件內(nèi)的狀態(tài)都放在reducer中,這樣View就可以更純粹的去渲染了,這樣的View在我看來(lái)會(huì)更加簡(jiǎn)潔、更加清晰,對(duì)于組件的替換更是駕輕就熟。但狀態(tài)全外置這種實(shí)踐帶來(lái)的代價(jià)也是很大的。因?yàn)橐粋€(gè)帶交互的組件,勢(shì)必需要一些事件的處理,生命周期的觸發(fā)等等操作,這會(huì)帶來(lái)一些問(wèn)題:
這種組件提煉出來(lái)的狀態(tài)只和自己有關(guān),強(qiáng)制被放在Store中就會(huì)帶來(lái)Store復(fù)雜度的上升,如果你的組件足夠多,那么全局的Store會(huì)膨脹的特別明顯,更重要的是如果你的狀態(tài)是和組件成樹形對(duì)應(yīng)的話,Store中將會(huì)冗余很多重復(fù)的數(shù)據(jù)。
描述組件的狀態(tài)被轉(zhuǎn)移到外部,導(dǎo)致操作組件的成本變高,對(duì)于組件內(nèi)的一些簡(jiǎn)單操作將變得復(fù)雜繁瑣。
對(duì)于后一點(diǎn)我認(rèn)為并沒(méi)有很大的問(wèn)題,得益于分層和純渲染的設(shè)計(jì),組件將控制自身的行為交出后可以將這些邏輯抽象為更加通用的邏輯,從而方便有類似需求的組件使用,因?yàn)檫壿嫅?yīng)該只出現(xiàn)在一個(gè)地方,而不應(yīng)分散在多個(gè)地方。例如控制一批組件的顯示或隱藏,將組件內(nèi)部控制顯示的邏輯交出來(lái)反而會(huì)省去更多的重復(fù)代碼。
而我更擔(dān)心的是由于組件中私有狀態(tài)的轉(zhuǎn)移導(dǎo)致的Store膨脹的問(wèn)題,為了避免這個(gè)問(wèn)題首先做的便是盡可能的提取公用有相似作用的狀態(tài),例如控制顯示/隱藏、多個(gè)列表的頁(yè)數(shù)/條數(shù);等這些有著相似功能的字段。走到這一步就引出了另外一個(gè)問(wèn)題了,對(duì)于組件的狀態(tài)描述是樹形的還是平行的。
樹形結(jié)構(gòu)
這種結(jié)構(gòu)的特點(diǎn)是將一個(gè)組件的狀態(tài)通過(guò)一個(gè)樹的形式記錄下來(lái),頁(yè)面是如何嵌套的,那么狀態(tài)樹就是如何嵌套的,這樣做的好處是組件接收到狀態(tài)后直接遞歸的顯示就行了,對(duì)于組件來(lái)說(shuō)這是最簡(jiǎn)單,效率最高的展現(xiàn)形式。但這樣做的問(wèn)題就是如果有多個(gè)相似的組件就會(huì)造成Store中冗余大量重復(fù)數(shù)據(jù),最終造成Store的膨脹。
平行結(jié)構(gòu)
這種結(jié)構(gòu)和上面的樹形結(jié)構(gòu)恰恰相反,可以最大程度的避免冗余數(shù)據(jù)的產(chǎn)生,將每一類數(shù)據(jù)拍平保存,但這種形式對(duì)于組件的展示卻很不友好,組件需要自己去消化多處數(shù)據(jù)源帶來(lái)的格式化操作,在redux中connect方法就是用來(lái)處理這種多數(shù)據(jù)源聚合用的。
那么上面兩種結(jié)構(gòu)改如何取舍呢?我個(gè)人推薦第二種平行結(jié)構(gòu),既然選擇了平行結(jié)構(gòu),那么該如何去處理數(shù)據(jù)聚合的問(wèn)題呢?在這里我推薦利用管道的思路來(lái)解決,這借鑒了 Angular 2 Pipe的概念,當(dāng)然熟悉Linux的同學(xué)對(duì)于 | 操作符一定也不會(huì)陌生。在我們的項(xiàng)目中,數(shù)據(jù)是流動(dòng)的,如同一個(gè)管道中的水一樣,Store就是一個(gè)水庫(kù),匯集了各種各樣的數(shù)據(jù)(水),而頁(yè)面組件就如同需要灌溉的田,而從水庫(kù)到田間這段距離就需要水管的幫助了。同樣的,利用pipe我們可以將保存在Store中的數(shù)據(jù)轉(zhuǎn)換成期望看到的結(jié)構(gòu),而這一切操作都是在數(shù)據(jù)的流動(dòng)中完成的,而不是放在數(shù)據(jù)已經(jīng)傳遞到組件之后去處理了。
這里引出了一個(gè)概念,就是數(shù)據(jù)流這個(gè)概念,在項(xiàng)目中我將所有數(shù)據(jù)的操作都成為數(shù)據(jù)的流動(dòng)。舉個(gè)例子,當(dāng)用戶在登錄框輸入了用戶名和密碼并點(diǎn)擊提交之后,這兩個(gè)input中的value就變成了兩個(gè)數(shù)據(jù)流:
input => merge(name, password) => filter(校驗(yàn)合法性) => post(服務(wù)器)
這個(gè)行為變成了一條流水線,先不管post輸出的結(jié)果如何,在上面的demo中我們的輸入行為被抽象成了兩個(gè)參數(shù),最后通過(guò)合并、過(guò)濾、發(fā)送,最終到達(dá)服務(wù)器,這不是一個(gè)新概念,在很多的框架中都有體現(xiàn):
在Cycle.js它被稱為 Intent(負(fù)責(zé)從外部的輸入中,提取出所需信息),Intent實(shí)際上做的是action執(zhí)行過(guò)程的高級(jí)抽象,提取了必要的信息。由于View是純展示的,所以包括事件監(jiān)聽(tīng)在內(nèi)的行為統(tǒng)統(tǒng)被Intent抽象成數(shù)據(jù)源,這在RxJs中很常見(jiàn):
var clicks = Rx.Observable.fromEvent(document, "click"); clicks.subscribe(x => console.log(x)); // 結(jié)果: // 每次點(diǎn)擊 document 時(shí),都會(huì)在控制臺(tái)上輸出 MouseEvent 。
相比于從View中發(fā)出的同步數(shù)據(jù)源,我們遇到更多的是從HTTP中獲取的異步數(shù)據(jù)源。在redux中我們常用redux-thunk來(lái)處理異步操作,那么在流中呢?
邏輯處理在之前的業(yè)務(wù)中我們有很多方式去處理異步操作,比如說(shuō)最常用的redux-thunk(回調(diào))、promise、async/await。現(xiàn)在很多人更愿意用async/await操作符去寫異步邏輯,因?yàn)樗尨a顯得更加“同步”,我之前也很喜歡這種方式,但現(xiàn)在在數(shù)據(jù)流的概念中,同步/異步已經(jīng)被“模糊”了,它們都是數(shù)據(jù)源,它們都是“主動(dòng)”發(fā)出數(shù)據(jù)的,那么同步還是異步就顯得不那么重要了,還是上面的例子,如果用戶名變成了一個(gè)異步獲取的過(guò)程,而不是用戶主動(dòng)輸入的了:
input => merge(async(name), password) => filter(校驗(yàn)合法性) => post(服務(wù)器)
這種情況下在RxJs中可以通過(guò)zip來(lái)等待全部的數(shù)據(jù)流
let age$ = Observable.of(27, 25, 29); let name$ = Observable.of ("Foo", "Bar", "Beer"); let isDev$ = Observable.of (true, true, false); Observable .zip(age$, name$, isDev$, (age: number, name: string, isDev: boolean) => ({ age, name, isDev })) .subscribe(x => console.log(x)); // 輸出: // { age: 27, name: "Foo", isDev: true } // { age: 25, name: "Bar", isDev: true } // { age: 29, name: "Beer", isDev: false }
通過(guò)這樣的鏈?zhǔn)讲僮鳎覀兛梢院芊奖愕目刂坪瞳@取數(shù)據(jù)流,這是對(duì)于數(shù)據(jù)的獲取,那么數(shù)據(jù)的分發(fā)呢?在redux中,我們通常會(huì)多次dispatch,在redux-thunk中我們會(huì)這樣寫:
const getInfo = (params) => async (dispatch, getState) => { // TODO... dispatch(actionaA); // TODO... dispatch(actionaA); }
而在redux-observable中:
const somethingEpic = (action$, store) => action$.ofType(SOMETHING) .switchMap(() => ajax("/something") .do(() => store.dispatch({ type: SOMETHING_ELSE })) .map(response => ({ type: SUCCESS, response })) );
但是我認(rèn)為到處dispatch是一個(gè)不好的行為,這會(huì)讓一個(gè)流變得混亂,因?yàn)槟阍诹鞯淖詈蟛粫?huì)得完整的結(jié)果(在過(guò)程中有一部分就已經(jīng)派發(fā)出去了),這會(huì)讓邏輯看起來(lái)很散亂,所以我推薦應(yīng)該寫成這樣的形式:
const somethingEpic = action$ => action$.ofType(SOMETHING) .switchMap(() => ajax("/something") .mergeMap(response => Observable.of( { type: SOMETHING_ELSE }, { type: SUCCESS, response } )) ); // 上面這兩段demo來(lái)著redux-observable的文檔
結(jié)束了異步的處理,我們的流模型也完成了input->output的完整閉環(huán)了。在這里沒(méi)有詳細(xì)說(shuō)output是因?yàn)榛趓edux,我任然是通過(guò)redux的connect方法將Store分發(fā)注入到組件的props中去的,因此如果你熟悉redux那么會(huì)很習(xí)慣現(xiàn)在的改變。
在處理完了同步/異步之后我們就來(lái)聊聊業(yè)務(wù)的邏輯該如何處理了。在redux中邏輯被分在了兩個(gè)地方,action和reducer中,一個(gè)是做數(shù)據(jù)的聚合,一個(gè)是做數(shù)據(jù)的格式化。上面提到了Intent 是action的高階抽象,其實(shí)是對(duì)action的拆分,剝離了action中獲取數(shù)據(jù)的部分邏輯,那么剩下的就是數(shù)據(jù)處理的部分了,這部分在我的實(shí)踐中被叫做Service。
這是一個(gè)單例的實(shí)例,整個(gè)項(xiàng)目中一個(gè)服務(wù)只會(huì)有一個(gè)實(shí)例,不必將相同的代碼復(fù)制一遍又一遍,只需要?jiǎng)?chuàng)建一個(gè)單一的可復(fù)用的數(shù)據(jù)服務(wù),并且把它注入到需要它的那些組件中。并且使用多帶帶的服務(wù)可以保持組件足夠的精簡(jiǎn),同時(shí)也更容易對(duì)組件進(jìn)行單元測(cè)試。同樣reducer中的數(shù)據(jù)格式化邏輯也遷到了服務(wù)中去處理,在redux中reducer兼顧著數(shù)據(jù)的格式化和數(shù)據(jù)的保存這兩個(gè)功能,現(xiàn)在我們將徹底剝離出數(shù)據(jù)的處理部分,剩下的reducer將只做數(shù)據(jù)的保存,這就又引出了另一個(gè)概念Model,這一層我們一會(huì)討論,接著業(yè)務(wù)處理來(lái)看,在數(shù)據(jù)流獲取到數(shù)據(jù)并處理分發(fā)到Model中之后,input這一步基本算是結(jié)束了,接下來(lái)就是由Model到View的output了。
上文中我說(shuō)道了我推薦使用平行模式,那么在平行模式到View這種樹型結(jié)構(gòu)該如果轉(zhuǎn)化呢?這是output中最重要的一步,在CycleJS中這一步通常由filter去完成,而在Angular中則是由Pipe去處理,無(wú)論它叫什么,它們都是這條流程上的一環(huán),就像水管中的一節(jié)一樣,所有從Model通向View的數(shù)據(jù)都會(huì)進(jìn)過(guò)這一環(huán),從而被格式化。在代碼中我更推薦大家嘗試使用Decorator去過(guò)濾數(shù)據(jù)源:
@UserInfoPipe({ name: "Model.UserInfo.name" }) class LoginDemo extends Component { constructor(props) { super(props); } render(){ return (抽象、聚合數(shù)據(jù)); } } {this.props.name}
現(xiàn)在整體的骨架已經(jīng)有了,剩下的就是該如何更好的抽象整合項(xiàng)目中的數(shù)據(jù)了。
第一階段
最一開始的項(xiàng)目由于為了方便,我就按照API的結(jié)構(gòu)去設(shè)計(jì)Store,那個(gè)時(shí)候一個(gè)頁(yè)面對(duì)應(yīng)一個(gè)接口或者很少的幾個(gè)接口,這時(shí)候我將API返回的結(jié)構(gòu)與本地的狀態(tài)一一對(duì)應(yīng),這在初期非常的方便,不需要我做過(guò)多的轉(zhuǎn)換,然而接下來(lái)為了應(yīng)付接口的各種異常,不得不寫很多防御性的代碼(字段判空、屬性變更、接口數(shù)據(jù)拼裝),最后這些代碼變得臃腫不堪,在其它同學(xué)介入修改的時(shí)候總是一頭霧水,總是改了這里,那里出又出了問(wèn)題。并且這其中也存在不少冗余的數(shù)據(jù)。
第二階段
后來(lái)我發(fā)現(xiàn)既然數(shù)據(jù)都是最終給View去用的,那么我就按View的需求去設(shè)計(jì)Store好了,這個(gè)Store對(duì)于展示的組件來(lái)說(shuō),使用起來(lái)非常方便,當(dāng)前應(yīng)用處于哪種狀態(tài),就用對(duì)應(yīng)狀態(tài)的數(shù)組類型的數(shù)據(jù)渲染,不用做任何的中間數(shù)據(jù)轉(zhuǎn)換。不過(guò)這也同樣造成數(shù)據(jù)冗余的問(wèn)題,并且如果我需要改動(dòng)頁(yè)面的某個(gè)字段的話,需要在很多地方去修改,因?yàn)檫@個(gè)Store樹變得很深枝葉很多。
第三階段
那么我現(xiàn)在該如何設(shè)計(jì)狀態(tài)呢?作為一個(gè)曾經(jīng)做過(guò)一段時(shí)間后端的我來(lái)說(shuō),我決定模仿數(shù)據(jù)庫(kù)的結(jié)構(gòu)去設(shè)計(jì)狀態(tài)樹。把Store當(dāng)成一個(gè)數(shù)據(jù)庫(kù),每個(gè)種類的狀態(tài)看做數(shù)據(jù)庫(kù)中的一張表,狀態(tài)中的每一個(gè)字段對(duì)應(yīng)表的一個(gè)字段。
那么設(shè)計(jì)一個(gè)數(shù)據(jù)庫(kù),應(yīng)該要遵循哪些原則呢?
數(shù)據(jù)按照域分類,存在不同的表中,每張表存儲(chǔ)的字段不重復(fù)
每張表中每條數(shù)據(jù)都有一個(gè)唯一主鍵
表中除了主鍵外其它列,相互不存在依賴關(guān)系
而基于上面這三條原則,我們?cè)趺丛O(shè)計(jì)Store呢?
把整個(gè)項(xiàng)目按照一定模型去分離為若干子狀態(tài),這些子狀態(tài)之間不存在重復(fù)冗余的數(shù)據(jù)。
怎么理解這件事呢?舉個(gè)例子,我有一個(gè)長(zhǎng)列表,每當(dāng)我點(diǎn)擊列表中的某一列時(shí)就會(huì)有一個(gè)紅框出現(xiàn)包裹住這列,而這個(gè)列表中真正展示的數(shù)據(jù)應(yīng)該是另外一個(gè)子狀態(tài),它們的關(guān)系類似:
{ activeLine: 1, list: [ { name: "test1", }, { name: "test2", }, { name: "test3", }, { name: "test4", }, ] }
以鍵值對(duì)的結(jié)構(gòu)存儲(chǔ)數(shù)據(jù),用key/ID作為記錄的索引,記錄中的其他字段都依賴于索引。
有了唯一的key做主鍵,我們就可以很方便的去遍歷/處理數(shù)據(jù)。更進(jìn)一步的,如果我們想去判斷一條數(shù)據(jù)有沒(méi)有變化,我們可以單純的去判斷主鍵是否一致,在一些情況下,這是一個(gè)不錯(cuò)的思路,這避免了多層判斷,或者深拷貝帶來(lái)的復(fù)雜度和性能問(wèn)題(這個(gè)可以參考immutable)。
狀態(tài)樹中不保存可以通過(guò)已有數(shù)據(jù)計(jì)算出來(lái)的數(shù)據(jù),也就是這些數(shù)據(jù)都是相互獨(dú)立的,都可以被稱為原子數(shù)據(jù)。
什么是原子數(shù)據(jù)?頁(yè)面中使用到的數(shù)據(jù)都是由這些原子數(shù)據(jù)通過(guò)計(jì)算、拼裝得到的(注意:這里只有拼裝,沒(méi)有拆分,因?yàn)樵邮亲钚〉膯挝唬允遣豢刹鸱值模贿@就保持了數(shù)據(jù)源的統(tǒng)一,不會(huì)出現(xiàn)一份一樣的數(shù)據(jù)來(lái)自多出數(shù)據(jù)源的問(wèn)題了,這會(huì)避免很多不必要的問(wèn)題,如多處數(shù)據(jù)源不同步導(dǎo)致的頁(yè)面展示異常等問(wèn)題。
好了,數(shù)據(jù)層也設(shè)計(jì)完了,這樣一個(gè)完整的結(jié)構(gòu)就清晰的擺在面前了,最終總結(jié)一下這個(gè)過(guò)程:
按照貧血模型分離組件
通過(guò)訂閱的形式采集數(shù)據(jù)源
通過(guò)數(shù)據(jù)庫(kù)的形式去保存數(shù)據(jù)
通過(guò)流的方式去處理和分發(fā)數(shù)據(jù)
通過(guò)流的形式去格式化數(shù)據(jù)
經(jīng)過(guò)以上幾步,我們就初步的完成了一個(gè)業(yè)務(wù)從input到output的完整閉環(huán)。
已上這些便是我這次重構(gòu)總結(jié)的一些經(jīng)驗(yàn),肯定不全對(duì)、不完善、不準(zhǔn)確,但是這個(gè)大方向我覺(jué)得是值得去探索的。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/107467.html
摘要:面向?qū)ο笫亲约航M裝電腦,硬件已生產(chǎn)完畢。面向過(guò)程吃狗屎面向?qū)ο蠊烦允捍_切的講是一種軟件設(shè)計(jì)規(guī)范,早在年的理念就已經(jīng)誕生。后期的維護(hù)成本會(huì)減少很多。減輕了開發(fā)人員的負(fù)擔(dān),也減少了操作邏輯導(dǎo)致業(yè)務(wù)邏輯混亂的可能性。 什么是MVC,什么是MVVM? 面向過(guò)程 --> 面向?qū)ο?--> MVC --> MV* 面向過(guò)程: 開發(fā)人員按照需求邏輯順序開發(fā)代碼邏輯,主要思維模式在于如何實(shí)現(xiàn)。先細(xì)節(jié),...
摘要:上次的訪談,介紹了下可愛(ài)的依云醬,回憶傳送門。這里簡(jiǎn)單地介紹下龍女仆,全名小林家的龍女仆,為什么介紹這部劇呢因?yàn)樵O(shè)計(jì)獅顏值同學(xué)也安利了這部。劇情簡(jiǎn)介在獨(dú)身又勞累的小林劃重點(diǎn)一名程序員身邊突然出現(xiàn)的穿著女仆服裝的美少女托爾。 showImg(https://segmentfault.com/img/bVR6p5?w=900&h=385); 上次的訪談,介紹了下可愛(ài)的依云醬,回憶傳送門。不...
摘要:上次的訪談,介紹了下可愛(ài)的依云醬,回憶傳送門。這里簡(jiǎn)單地介紹下龍女仆,全名小林家的龍女仆,為什么介紹這部劇呢因?yàn)樵O(shè)計(jì)獅顏值同學(xué)也安利了這部。劇情簡(jiǎn)介在獨(dú)身又勞累的小林劃重點(diǎn)一名程序員身邊突然出現(xiàn)的穿著女仆服裝的美少女托爾。 showImg(https://segmentfault.com/img/bVR6p5?w=900&h=385); 上次的訪談,介紹了下可愛(ài)的依云醬,回憶傳送門。不...
摘要:今天這篇文章,我們會(huì)介紹幾種常見(jiàn)的方法和其中存在的問(wèn)題,并提出如何基于請(qǐng)求攔截,快速解決跨域和代理問(wèn)題的方案。因?yàn)闆](méi)有修改該請(qǐng)求,只是延遲發(fā)送,這樣就保持了原請(qǐng)求與業(yè)務(wù)服務(wù)器之間的所有鑒權(quán)等相關(guān)信息,由此解決了跨域訪問(wèn)無(wú)法攜帶的問(wèn)題。 近幾年,隨著 Web 開發(fā)逐漸成熟,前后端分離的架構(gòu)設(shè)計(jì)越來(lái)越被眾多開發(fā)者認(rèn)可,使得前端和后端可以專注各自的職能,降低溝通成本,提高開發(fā)效率。 在前后端...
摘要:在這里統(tǒng)一說(shuō)開發(fā),可能有失頗偏,畢竟我后端一直都是用實(shí)現(xiàn)的,沒(méi)用過(guò)也沒(méi)用過(guò),但我想大體都是一樣都,我就此闡述一下我所認(rèn)為的程序數(shù)據(jù)結(jié)構(gòu)算法。這套的想法主要目的是把復(fù)雜程序盡量做簡(jiǎn)化,并以數(shù)據(jù)和算法的思想去思考程序本身。 在這里統(tǒng)一說(shuō)Web開發(fā),可能有失頗偏,畢竟我后端一直都是用PHP實(shí)現(xiàn)的,沒(méi)用過(guò).net也沒(méi)用過(guò)java,但我想大體都是一樣都,我就此闡述一下我所認(rèn)為的程序=數(shù)據(jù)結(jié)構(gòu)+算...
閱讀 3581·2021-10-11 10:59
閱讀 1598·2021-09-29 09:35
閱讀 2267·2021-09-26 09:46
閱讀 3780·2021-09-10 10:50
閱讀 958·2019-08-29 12:17
閱讀 827·2019-08-26 13:40
閱讀 2442·2019-08-26 11:44
閱讀 2111·2019-08-26 11:22