摘要:如果說傳統(tǒng)的前端開發(fā)是以頁面為中心來入手的話,那么現(xiàn)代的應(yīng)用開發(fā)就是以狀態(tài)為中心來著手設(shè)計和開發(fā)的。初步分析路由是怎么管理狀態(tài)的復(fù)雜的話題簡單說在中,應(yīng)用的每一個可能的狀態(tài)都是通過體現(xiàn)的。
引子
SPA(單頁面應(yīng)用)的核心是什么?
自該類型應(yīng)用誕生以來我最多思考的問題就是這個。現(xiàn)在前端 SPA 框架滿天飛,許多不是框架的也被稱作框架,究竟有什么代表性的層(layer)能讓一個系統(tǒng)稱得上是框架?
我的答案是路由,而路由的本質(zhì)就是一個狀態(tài)管理器。沒有路由機(jī)制的系統(tǒng)不能稱之為框架,而路由機(jī)制做得不好的框架也算不上好框架(但可以算是好的工具集合,比如 Angular——詳見我在 Ruby China 上曾經(jīng)吐過的槽)。
為什么這么說呢?我們都知道 HTML 是無狀態(tài)的(stateless),做一堆 HTML 頁面拼在一起那不叫“應(yīng)用”,頂多稱之為“內(nèi)容系統(tǒng)”;在以前,HTML 網(wǎng)站上的狀態(tài)管理是由后端的 Session 加前端的 Cookies 協(xié)作完成的,到了 SPA 的時代 Session 不是必須的了(盡管傳統(tǒng)的 Session 機(jī)制也是可用的),UI 上的狀態(tài)轉(zhuǎn)移到了前端由 JavaScript 完全管控(由于 SPA 前后分離的特點),所以前端工程師擔(dān)負(fù)起了更多的業(yè)務(wù)邏輯職責(zé),相應(yīng)的整個技術(shù)鏈上也必須有一個可靠的環(huán)節(jié)來幫助他們做狀態(tài)管理這件事情。
在前端框架的發(fā)展過程中路由的誕生是水到渠成的(基于一些新技術(shù)的成熟,比如 HTML5 的History API 等等),但是應(yīng)用開發(fā)工程師對于路由的理解和重視卻還遠(yuǎn)遠(yuǎn)不夠。如果說傳統(tǒng)的前端開發(fā)是以頁面為中心來入手的話,那么現(xiàn)代的 SPA 應(yīng)用開發(fā)就是以狀態(tài)為中心來著手設(shè)計和開發(fā)的。
Ember 就是一款非常重視路由組件的 SPA 框架,本文借由一個實現(xiàn) UI 布局的例子來談?wù)?UI 編程與路由的關(guān)系,盡管這只是涉及到路由特性的一部分卻也足夠說明一些問題了。希望這個例子能讓更多前端工程師認(rèn)識和理解路由的重要性,從而更好的設(shè)計與實現(xiàn) SPA 應(yīng)用的各種功能場景。
場景描述多數(shù)應(yīng)用都有如下所述的 UI 設(shè)計:
多數(shù)視圖在一個通用的布局內(nèi)呈現(xiàn),比如典型的 Header + Main 的布局
個別視圖需要一個特定的布局,比如登錄和注冊頁面不需要 Header 等等
對于這些場景來說,那些重復(fù)的 HTML 結(jié)構(gòu)(如 Header 和 Footer)肯定需要某種方式的抽象使得它們可以復(fù)用或者指定渲染還是不渲染。后端渲染技術(shù)使用了一些機(jī)制(如 helpers 等) 來幫助開發(fā)者在視圖層實現(xiàn)這些邏輯,等到返回給瀏覽器的時候已經(jīng)是完整的 HTML 了(當(dāng)然也有 Turbolinks 這樣融合了部分前端路由特性的新技術(shù),本文不做進(jìn)一步描述),這顯然是不適合前端應(yīng)用的場景的,因為對于 SPA 應(yīng)用來說用戶更換 URLs 時需要在瀏覽器端即時拼裝最終的完整視圖,并不存在“預(yù)先渲染好的頁面一起交付過來”這么一說。我們需要先思考一下高層設(shè)計,看看有什么機(jī)制可以利用的。
初步分析路由是怎么管理狀態(tài)的?復(fù)雜的話題簡單說:
In Ember.js, each of the possible states in your application is represented by a URL.
在 Ember.js 中,應(yīng)用的每一個可能的狀態(tài)都是通過 URL 體現(xiàn)的。
這是官方文檔里所總結(jié)的,我來試著舉例表述一下:
假設(shè)當(dāng)前有如下路由定義:
let Router = Ember.Router.extend() Router.map(function() { this.route("dashboard", { path: "/dashboard" }) this.route("signin", { path: "/signin" }) })
于是,當(dāng)用戶——
進(jìn)入 /dashboard URL 的時候,對應(yīng)的 dashboard 路由開始接管應(yīng)用的當(dāng)前狀態(tài)
進(jìn)入 /signin URL 的時候,對應(yīng)的 signin 路由開始接管應(yīng)用的當(dāng)前狀態(tài)
但更重要的是:所有的路由都有一個共有的頂級路由——application 路由,其重要性主要體現(xiàn)在:
它是唯一一個靠譜的可以用來管理全局范圍狀態(tài)的路由
它為所有子路由的視圖渲染提供了模板的入口(outlet)
接著問題來了:如果說狀態(tài)通過 URL 來體現(xiàn),那么 UI 布局的不同如何體現(xiàn)呢?比如:
進(jìn)入 /dashboard URL 的時候,我們需要 Header + Main 的布局
進(jìn)入 /signin URL 的時候,我們不需要 Header
無論何種情形,application 路由在其中的作用……?
第一次嘗試因為每一個路由都會渲染自己的模版,我們可以做一個最簡單的嘗試:
{{!app/pods/application/template.hbs}} {{outlet}}
{{!app/pods/dashboard/template.hbs}}... ... {{outlet}}
{{!app/pods/signin/template.hbs}}... {{outlet}}
雖然這么做可以奏效,然而問題也是顯而易見的:如果出現(xiàn)多個和 dashboard 一樣的布局結(jié)構(gòu),我們將不得不多次重復(fù)
如果我們可以把問題場景簡化為只有一種可能,例如“所有的視圖都用 Header + Main 的布局”,那么解決方案可以簡化為:
{{!app/pods/application/template.hbs}}... {{outlet}}
{{!app/pods/dashboard/template.hbs}} ... {{outlet}}
{{!app/pods/signin/template.hbs}} ... {{outlet}}
那么再次恢復(fù)原來的場景要求,問題變成了:“進(jìn)入 /signin 之后,如何隱藏 application 模版里的
隱藏模版里的片段,最簡單的方法可以這么做:
{{!app/pods/application/template.hbs}} {{#if showNavbar}}... {{/if}}{{outlet}}
我們知道模版內(nèi)可訪問的變量可以通過控制器來設(shè)置,但此時我不打算創(chuàng)建 ApplicationController,因為路由里有一個 setupController 的鉤子方法能幫我們設(shè)置控制器的(更重要的原因是很快 Routable Components 將取代現(xiàn)在的 route + controller + template 的分層體系,所以從現(xiàn)在開始最好盡可能少的依賴 controller),試試看:
// app/pods/application/route.js export default Ember.Route.extend({ setupController(controller) { this._super(...arguments) controller.set("showNavbar", true) }), })
現(xiàn)在所有的狀態(tài)都會顯示 header 部分了,那怎么讓 /signin 不顯示呢?或許這樣……?
// app/pods/signin/route.js export default Ember.Route.extend({ setupController() { this._super(...arguments) this.controllerFor("application").set("showNavbar", false) }), })
以下是測試結(jié)果(這里建議先寫 Acceptance Test,省時間且不易錯漏),在每次刷新頁面后:
從... | 到... | 結(jié)果 |
---|---|---|
/ | /dashboard | 成功 |
/dashboard | / | 成功 |
/ | /signin | 成功 |
/signin | / | 失敗 |
/dashboard | /signin | 成功 |
/signin | /dashboard | 失敗 |
/signin | /dashboard | 失敗 |
/dashboard | /signin | 失敗 |
我們在測試中增加了 /dashboard 的訪問,但是我們并沒有定義位于 DashboardRoute 里的 setupController 鉤子,這是因為我們期望 /dashboard 能夠繼承 / 的狀態(tài),否則所有的路由都要設(shè)置類似的 setupController 會把人累死,然而測試結(jié)果可能會讓初學(xué)者覺得摸不著頭腦,我們試著分析一下好了:
/ 和 /dashboard 都需要 showNavbar === true,所以正反都可以;
當(dāng)自 /signin 刷新頁面的時候,先執(zhí)行了 ApplicationRoute 然后才是 SigninRoute,等到進(jìn)入 / 的時候,setupController 不會再次執(zhí)行的;
同上;
同上。
問題分析這里最明顯的問題就是 ApplicationRoute#setupController 這個鉤子方法是不可靠的,你只能保證它的第一次運(yùn)行,一旦變成了在路由之間來回跳轉(zhuǎn)就無效了。
實際上,setupController 的作用是將 model 鉤子返回的結(jié)果綁定在對應(yīng)的控制器上的,你可以擴(kuò)展這個邏輯但也僅限于數(shù)據(jù)層面的設(shè)置。只有當(dāng)調(diào)用了 route#render() 且返回了與之前不同的 model 時 setupController 才會再次被調(diào)用。
于是問題又變成了:有哪一個鉤子方法能保證在路由發(fā)生變化的時候都可用?
路由的生命周期這是一個非常重要但又很無趣的主題,我不想在這里重復(fù)那些可以通過閱讀文檔和親測就可以得出的答案,不過我可以給出一份測試路由生命周期的完整代碼片段:
https://gist.github.com/nightire/f766850fd225a9ec4aa2
把它們放進(jìn)你的路由當(dāng)中然后仔細(xì)觀察吧。順便給你一些經(jīng)驗之談:
這個測試不要錯過 ApplicationRoute,因為它是最特殊的一個
其他的路由至少要同時測試兩個,比如 IndexRoute 和 TestRoute
不要只測試頁面刷新后的生命周期,還要嘗試各種路由之間的相互過渡
測試完之后,你就會對整個路由系統(tǒng)有一個非常全面的了解了,這些體驗會帶給你一個重要的技能,即是在將來你可以很容易的決斷出實現(xiàn)一個功能應(yīng)該從哪里入手。對于我們這個例子來說,比較重要的結(jié)論如下:
ApplicationRoute 是所有路由的共同先祖,當(dāng)你第一次進(jìn)入應(yīng)用程序——無論是從 / 進(jìn)入還是從 /some/complicated/state 進(jìn)入——ApplicationRoute 都是第一個實例化的路由,并且它 activated 就不會 deactivated 了(除非你手動刷新瀏覽器)。因此我們可以把 ApplicationRoute 作為一個特殊的永遠(yuǎn)激活的路由
如果你有應(yīng)用邏輯依存于 ApplicationRoute#setupController,那么第一次進(jìn)入就是唯一靠譜的機(jī)會——你不能指望這個鉤子會在路由來回切換的時候觸發(fā)
但是其他路由上的 #setupController 鉤子是會在每次過渡進(jìn)來的時候重新執(zhí)行的
第三次嘗試基于以上分析,我們可以調(diào)整我們的代碼了:
// app/pods/application/route.js export default Ember.Route.extend()
// app/pods/index/route.js and app/pods/dashboard/route.js export default Ember.Route.extend({ setupController() { this._super(...arguments) this.controllerFor("application").set("showNavbar", true) }, })
// app/pods/signin/route.js export default Ember.Route.extend({ setupController() { this._super(...arguments) this.controllerFor("application").set("showNavbar", false) }, })
我們把 ApplicationRoute#setupController 里的邏輯轉(zhuǎn)移到了 IndexRoute#setupController 里去,就是因為當(dāng)你訪問 / 的時候,ApplicationRoute#setupController 只會觸發(fā)一次(第一次刷新的時候),而 IndexRoute#setupController 則可以保證每次都觸發(fā)。現(xiàn)在,我們設(shè)想的場景可以實現(xiàn)了。
這個設(shè)定一開始看起來非常古怪,很多初學(xué)者都在這里被搞暈掉:“為什么要有 IndexRoute?為什么不直接用 ApplicationRoute?”
抽象路由當(dāng)我們剛開始接觸前端的路由機(jī)制時,我們很容易把 ApplicationRoute 和 / 關(guān)聯(lián)起來,可實際上真正和 / 關(guān)聯(lián)的是 IndexRoute。如果你沒有自行創(chuàng)建 IndexRoute,Ember 會幫你創(chuàng)建一個,但不管怎樣 IndexRoute 都是必不可少的。
那么 ApplicationRoute 到底扮演著一個什么樣的角色呢?
先記住這個結(jié)論:在路由系統(tǒng)中,路由樹中任何一個當(dāng)前激活的路徑都會至少包括兩個路由節(jié)點,并且其中一個必然是 ApplicationRoute,這也正是 ApplicationRoute 永遠(yuǎn)處于 activated 而永遠(yuǎn)不會 deactivate 的原因所在。
舉幾個例子:
當(dāng)訪問 "/" 時,路由樹中當(dāng)前激活的路徑為:application => index
當(dāng)訪問 "/users/new" 時,路由樹中當(dāng)前激活的路徑為:application => users => new
當(dāng)訪問 "/posts/1/comments/1" 時,路由樹中當(dāng)前激活的路徑為:application => post => index => comment => index,也可能是:application => posts => show => comments => show ——取決于你的路由規(guī)則的寫法
等等……
Ember 并沒有為這個特殊的 |41b8a0714e572ed059c0e52d0e3c676c91| 做一個明確的定義(但是|41b8a0714e572ed059c0e52d0e3c676c92|),不過在其他類似的路由系統(tǒng)里我們可以找到等價物——比如來自 |41b8a0714e572ed059c0e52d0e3c676c93|(Angular 生態(tài)圈里最優(yōu)秀的路由系統(tǒng))里的抽象路由(Abstract Route)。
Ember 的 ApplicationRoute 和 ui.router 的抽象路由非常相似,它們的共性包括:
都能夠擁有子路由
自身都不能被直接激活(不能位于路由樹中當(dāng)前激活路徑的頂點)
不能直接過渡,也就是 transition to;Ember 里會等價于過渡到 IndexRoute,ui.router 則會拋出異常
都有對應(yīng)的模版、控制器、數(shù)據(jù)入口、生命周期鉤子等等
當(dāng)其下的任意子路由被激活,作為父節(jié)點的抽象路由都會被激活
當(dāng)然,它們也有不同,比如說:你可以在 ui.router 的路由樹中任意定義抽象路由,不受數(shù)量和節(jié)點深度的限制,只要保證抽象路由不會位于某條路徑的頂點就是了;而 Ember Router 只有一個抽象路由(而且并沒有明確的定義語法,只是行為類似——典型的鴨子類型設(shè)計嘛)且只能是 ApplicationRoute,你可以手動創(chuàng)建別的路由來模擬,但是 Ember Router 不會阻止你過渡到這些路由,不像 ui.router 會拋出異常(這一點很容易讓初學(xué)者碰壁)
實際上當(dāng)你對 Ember Router 的理解日漸深入之后你會發(fā)現(xiàn)所有的嵌套路由(包括頂層路由)都是抽象路由,因為它們都會隱式的創(chuàng)建對應(yīng)的 |41b8a0714e572ed059c0e52d0e3c676c98| 作為該路徑的頂節(jié)點,訪問它們就等于訪問它們的 |41b8a0714e572ed059c0e52d0e3c676c99|。我認(rèn)為 Ember Router 的這個設(shè)計與 ui.router 相比有利有弊:
利:設(shè)計精巧簡單,可以避免大量的 boilerplate 代碼,路由的定義相對清晰簡潔
弊:對于初學(xué)者來說,由于不存在抽象路由的概念,很難深刻理解父子節(jié)點,特別是隱式 IndexRoute 的存在價值
這個方案足夠完美了嗎?不,還差一些。試想:當(dāng)我們需要很多路由來組織應(yīng)用程序的結(jié)構(gòu)時,類似的 #setupController 豈不是要重復(fù)定義很多次?如何抽象這一邏輯讓其變得易于復(fù)用和維護(hù)?
Thinking in Angular way(w/ ui.router)在開發(fā) Angular 應(yīng)用的時候,類似場景的路由定義一般是這樣的:
+----> layoutOne(with header) +----> childrenRoutes(like dashboard, etc.) | | application(root) -| | | +----> layoutTwo(without header) +----> childrenRoutes(like signin, etc.)
我們用 Ember Router 也可以模擬這樣的路由定義,實現(xiàn)同樣的結(jié)果,代碼類似:
// app/router.js let Router = Ember.Router.extend({ location: config.locationType, }) Router.map(function() { // provide layout w/this.route("layoutOne", { path: "/" }, function() { this.route("dashboard", { resetNamespace: true }) // ... }) // provide layout w/o this.route("layoutTwo", { path: "/" }, function() { this.route("signin", { resetNamespace: true }) // ... }) })
但是個人非常不喜歡也不推崇這么做,原因是:
這樣的路由定義寫多了會很惡心
為了避免類似 /layoutOne/dashboard 這樣的 URLs,不得不重復(fù)設(shè)定 path: "/" 來覆蓋
ui.router 解決此問題依靠的是 url pattern inheritence,由于每一個路由的定義都必須指明 url 屬性,所以也就習(xí)慣了
為了避免類似 layoutTwo.signin 這樣的路由名字,不得不重復(fù)設(shè)定 resetNamespace: true
ui.router 解決此問題依靠的是路由定義里的 parent 屬性,所以子路由是可以分開定義的,不用嵌套也就無需 resetNamespace
對比兩家的路由定義語法,各有優(yōu)缺點吧,但是 Ember Router 向來都是以簡明扼要著稱的,真心不喜歡為了這個小小需求而把路由定義寫得一塌糊涂
另外這樣的路由設(shè)計還會導(dǎo)致 application 這個模版變成一個廢物,除了 {{outlet}} 它啥也做不成,生成的 DOM Tree 里平白多一個標(biāo)簽看的人直惡心~
Thinking in Ember way既然問題的本質(zhì)是 #setupController 鉤子需要重復(fù)定義,那么有沒有 Ember 風(fēng)格辦法來解決這一問題呢?
首先我們來考量一下 Mixin,你可以這么做:
// app/mixins/show-navbar.js export default Ember.Mixin.create({ setupController() { this._super(...arguments) this.controllerFor("application").set("showNavbar", true) }, }) // app/mixins/hide-navbar.js export default Ember.Mixin.create({ setupController() { this._super(...arguments) this.controllerFor("application").set("showNavbar", false) }, })
// app/pods/index/route.js and app/pods/dashboard/route.js import ShowNavbarMixin from "../../mixins/show-navbar" export default Ember.Route.extend(ShowNavbarMixin, { // ... }) // app/pods/signin/route.js import HideNavbarMixin from "../../mixins/hide-navbar" export default Ember.Route.extend(HideNavbarMixin, { // ... })
這么做倒也不是不行,但是——明顯很蠢嘛——這和抽取兩個方法然后到處調(diào)用沒有什么本質(zhì)的區(qū)別,看起來我們需要的是某種程度上的繼承與重寫才對:
// somewhere in app/app.js Ember.Route.reopen({ // show navbar by default, can be overwriten when define a specific route withLayout: true, setupController() { this._super(...arguments) this.controllerFor("application").set( "showNavbar", this.get("withLayout") ) }, })
// app/pods/index/route.js and app/pods/dashboard/route.js // Do nothing if showNavbar: true is expected // app/pods/signin/route.js export default Ember.Route.extend({ withLayout: false, })
這樣就行了,不需要額外的路由體系設(shè)計,就用 Ember 的對象系統(tǒng)便足夠完美。本文所描述的這個例子其實非常簡單,我相信略有 Ember 經(jīng)驗的開發(fā)者都能做出來,但是我的重點不在于這個例子,而在于對路由系統(tǒng)的一些闡述和理解。這個例子來源自真實的工作,為了給同事解釋清楚最初的方案為什么不行著實費(fèi)了我好大功夫,于是我把整個梳理過程記錄下來,希望對初學(xué)者——特別是對 SPA 的核心尚未了解的初學(xué)者能有所助益吧。
基于事件的解決方案這個問題其實還有多種解法,基于事件響應(yīng)的解法我就在現(xiàn)實里演示了兩種,不過相比于上面的最終方案,它們還是略微糙了些。在這里我寫其中一種比較少見的,里面涉及到一些 Ember 的內(nèi)部機(jī)制,權(quán)當(dāng)是一個借鑒吧,思路我就不多解釋了。
// app/mixins/hide-navbar.js export default Ember.Mixin.create({ hideNavbar: function() { this.set("showNavbar", false) }.on("init"), })
// app/router.js let Router = Ember.Router.extend({ location: config.locationType, didTransition() { this._super(...arguments) let currentRoute = this.get("container") .lookup(`route:${this.get("currentRouteName")}`) this.get("container").lookup("controller:application").set( "showNavbar", _.isUndefined(currentRoute.get("showNavbar")) ) } })
// app/pods/signin/route.js import HideNavbarMixin from "../../mixins/hide-navbar" export default Ember.Route.extend(HideNavbarMixin, { // only use this mixin when you need to hide the Header })
原文首發(fā)于 Ruby China 社區(qū),轉(zhuǎn)載請注明。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/85962.html
摘要:今天這篇主要講講里關(guān)于樣式開發(fā)的一些前期準(zhǔn)備工作,主要是和??偟膩碚f就不要再用了,又大又笨而且連親爹都準(zhǔn)備放棄它了,未來將是小快靈組件協(xié)同工作的大趨勢,就是可以用來替代的組件庫。 今天這篇主要講講 Ember CLI 里關(guān)于樣式開發(fā)的一些前期準(zhǔn)備工作,主要是 Sass 和 Bootstrap。 Ember Addons 是尋找各種組件的絕佳場所,下文將要介紹的一些都可以在這里找到,沒事...
摘要:因為組件的存在范圍被限制在以內(nèi),這就是這種機(jī)制目前存在的意義所在。組件都是可以傳遞參數(shù)或外部作用域的,利用此機(jī)制進(jìn)行判斷來執(zhí)行可選行為,這是對用戶友好的舉措。 這一篇還是一個簡單的例子所引發(fā)的思考。 你看,如今的框架和庫,無論規(guī)模大小功能多少,它們在本質(zhì)上都朝著組件化的思路快速演進(jìn)著。Angular 有 directives,Angular 2應(yīng)該也還是這個叫法;Ember 從 Vie...
摘要:好,你用就用吧,各種問題自己也不會看文檔問谷歌,成天怨聲載道的不得不吐槽一下現(xiàn)在的年輕人。為什么使用有關(guān)和的糾結(jié)歷史可以去谷歌一下,此處不再啰嗦最根本的原因就是對的支持更好,更新和維護(hù)也更勤快。 Tips on Ember 2 對我來說是沒什么計劃性的寫作,我只是把它當(dāng)做是每天工作的總結(jié)日志,一個很重要的目的是為團(tuán)隊做一些技術(shù)事務(wù)的整理,以幫助一些新人快速成長起來。如果有些內(nèi)容不能滿足...
摘要:警告版本是很不穩(wěn)定的,并不推薦使用于要上線的應(yīng)用。如果你要嘗試新的特性,要么是新建一個測試用的,要么是你的應(yīng)用離正式上線還早并且你和你的團(tuán)隊折騰得起。在此功能正式發(fā)布之后應(yīng)該是不需要這段補(bǔ)丁代碼的,目前來說也不會影響使用。 Ruby China 的朋友大概都知道我很喜歡 Ember,然而我用 Ember 的經(jīng)歷其實遠(yuǎn)比不上 Angular 那么豐富(Ember 業(yè)余愛好,Angular...
摘要:正文在年,框架的選擇并不少。特別的,通過思考這些框架分別如何處理狀態(tài)變化是很有用的。本文探索以下的數(shù)據(jù)綁定,的臟檢查的虛擬以及它與不可變數(shù)據(jù)結(jié)構(gòu)之間的聯(lián)系。當(dāng)狀態(tài)產(chǎn)生變化時,只有真正需要更新的部分才會發(fā)生改變。 譯者言 近幾年可謂是 JavaScript 的大爆炸紀(jì)元,各種框架類庫層出不窮,它們給前端帶來一個又一個的新思想。從以前我們用的 jQuery 直接操作 DOM,到 Backb...
閱讀 2187·2021-11-18 10:02
閱讀 3289·2021-11-11 16:55
閱讀 2694·2021-09-14 18:02
閱讀 2426·2021-09-04 16:41
閱讀 2056·2021-09-04 16:40
閱讀 1165·2019-08-30 15:56
閱讀 2213·2019-08-30 15:54
閱讀 3161·2019-08-30 14:15