摘要:進階教程原文保持更新寫在前面相信您已經看過簡明教程,本教程是簡明教程的實戰化版本,伴隨源碼分析用的是編寫,看到有疑惑的地方的,可以復制粘貼到這里在線編譯總覽在的源碼目錄,我們可以看到如下文件結構打醬油的,負責在控制臺顯示警告信息入口文件除去
Redux 進階教程
§ Redux API 總覽原文(保持更新):https://github.com/kenberkele...
寫在前面相信您已經看過 Redux 簡明教程,本教程是簡明教程的實戰化版本,伴隨源碼分析
Redux 用的是 ES6 編寫,看到有疑惑的地方的,可以復制粘貼到這里在線編譯 ES5
在 Redux 的源碼目錄 src/,我們可以看到如下文件結構:
├── utils/ │ ├── warning.js # 打醬油的,負責在控制臺顯示警告信息 ├── applyMiddleware.js ├── bindActionCreators.js ├── combineReducers.js ├── compose.js ├── createStore.js ├── index.js # 入口文件
除去打醬油的 utils/warning.js 以及入口文件 index.js,剩下那 5 個就是 Redux 的 API
§ compose(...functions)⊙ 源碼分析先說這個 API 的原因是它沒有依賴,是一個純函數
/** * 看起來逼格很高,實際運用其實是這樣子的: * compose(f, g, h)(...arg) => f(g(h(...args))) * * 值得注意的是,它用到了 reduceRight,因此執行順序是從右到左 * * @param {多個函數,用逗號隔開} * @return {函數} */ export default function compose(...funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } const last = funcs[funcs.length - 1] const rest = funcs.slice(0, -1) return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args)) }
這里的關鍵點在于,reduceRight 可傳入初始值:
// 由于 reduce / reduceRight 僅僅是方向的不同,因此下面用 reduce 說明即可 var arr = [1, 2, 3, 4, 5] var re1 = arr.reduce(function(total, i) { return total + i }) console.log(re1) // 15 var re2 = arr.reduce(function(total, i) { return total + i }, 100) // <---------------傳入一個初始值 console.log(re2) // 115
下面是 compose 的實例(在線演示):
控制臺輸出:
func1 獲得參數 0 func2 獲得參數 1 func3 獲得參數 3 re1:6 =============== func1 獲得參數 0 func2 獲得參數 1 func3 獲得參數 3 re2:6§ createStore(reducer, initialState, enhancer) ⊙ 源碼分析
import isPlainObject from "lodash/isPlainObject" import $$observable from "symbol-observable" /** * 這是 Redux 的私有 action 常量 * 長得太丑了,你不要鳥就行了 */ export var ActionTypes = { INIT: "@@redux/INIT" } /** * @param {函數} reducer 不多解釋了 * @param {對象} preloadedState 主要用于前后端同構時的數據同步 * @param {函數} enhancer 很牛逼,可以實現中間件、時間旅行,持久化等 * ※ Redux 僅提供 appleMiddleware 這個 Store Enhancer ※ * @return {Store} */ export default function createStore(reducer, preloadedState, enhancer) { // 這里省略的代碼,到本文的最后再講述(用于壓軸你懂的) var currentReducer = reducer var currentState = preloadedState // 這就是整個應用的 state var currentListeners = [] // 用于存儲訂閱的回調函數,dispatch 后逐個執行 var nextListeners = currentListeners // 【懸念1:為什么需要兩個 存放回調函數 的變量?】 var isDispatching = false /** * 【懸念1·解疑】 * 試想,dispatch 后,回調函數正在乖乖地被逐個執行(for 循環進行時) * 假設回調函數隊列原本是這樣的 [a, b, c, d] * * 現在 for 循環執行到第 3 步,亦即 a、b 已經被執行,準備執行 c * 但在這電光火石的瞬間,a 被取消訂閱!!! * * 那么此時回調函數隊列就變成了 [b, c, d] * 那么第 3 步就對應換成了 d!!! * c 被跳過了!!!這就是躺槍。。。 * * 作為一個回調函數,最大的恥辱就是得不到執行 * 因此為了避免這個問題,本函數會在上述場景中把 * currentListeners 復制給 nextListeners * * 這樣的話,dispatch 后,在逐個執行回調函數的過程中 * 如果有新增訂閱或取消訂閱,都在 nextListeners 中操作 * 讓 currentListeners 中的回調函數得以完整地執行 * * 既然新增是在 nextListeners 中 push,因此毫無疑問 * 新的回調函數不會在本次 currentListeners 的循環體中被觸發 * * (上述事件發生的幾率雖然很低,但還是嚴謹點比較好) */ function ensureCanMutateNextListeners() { // <-------這貨就叫做【ensure 哥】吧 if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } } /** * 返回 state */ function getState() { return currentState } /** * 負責注冊回調函數的老司機 * * 這里需要注意的就是,回調函數中如果需要獲取 state * 那每次獲取都請使用 getState(),而不是開頭用一個變量緩存住它 * 因為回調函數執行期間,有可能有連續幾個 dispatch 讓 state 改得物是人非 * 而且別忘了,dispatch 之后,整個 state 是被完全替換掉的 * 你緩存的 state 指向的可能已經是老掉牙的 state 了!!! * * @param {函數} 想要訂閱的回調函數 * @return {函數} 取消訂閱的函數 */ function subscribe(listener) { if (typeof listener !== "function") { throw new Error("Expected listener to be a function.") } var isSubscribed = true ensureCanMutateNextListeners() // 調用 ensure 哥保平安 nextListeners.push(listener) // 新增訂閱在 nextListeners 中操作 // 返回一個取消訂閱的函數 return function unsubscribe() { if (!isSubscribed) { return } isSubscribed = false ensureCanMutateNextListeners() // 調用 ensure 哥保平安 var index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) // 取消訂閱還是在 nextListeners 中操作 } } /** * 改變應用狀態 state 的不二法門:dispatch 一個 action * 內部的實現是:往 reducer 中傳入 currentState 以及 action * 用其返回值替換 currentState,最后逐個觸發回調函數 * * 如果 dispatch 的不是一個對象類型的 action(同步的),而是 Promise / thunk(異步的) * 則需引入 redux-thunk 等中間件來反轉控制權【懸念2:什么是反轉控制權?】 * * @param & @return {對象} action */ function dispatch(action) { if (!isPlainObject(action)) { throw new Error( "Actions must be plain objects. " + "Use custom middleware for async actions." ) } if (typeof action.type === "undefined") { throw new Error( "Actions may not have an undefined "type" property. " + "Have you misspelled a constant?" ) } if (isDispatching) { throw new Error("Reducers may not dispatch actions.") } try { isDispatching = true // 關鍵點:currentState 與 action 會流通到所有的 reducer // 所有 reducer 的返回值整合后,替換掉當前的 currentState currentState = currentReducer(currentState, action) } finally { isDispatching = false } // 令 currentListeners 等于 nextListeners,表示正在逐個執行回調函數(這就是上面 ensure 哥的判定條件) var listeners = currentListeners = nextListeners // 逐個觸發回調函數。這里不緩存數組長度是明智的,原因見【懸念1·解疑】 for (var i = 0; i < listeners.length; i++) { listeners[i]() } return action // 為了方便鏈式調用,dispatch 執行完畢后,返回 action(下文會提到的,稍微記住就好了) } /** * 替換當前 reducer 的老司機 * 主要用于代碼分離按需加載、熱替換等情況 * * @param {函數} nextReducer */ function replaceReducer(nextReducer) { if (typeof nextReducer !== "function") { throw new Error("Expected the nextReducer to be a function.") } currentReducer = nextReducer // 就是這么簡單粗暴! dispatch({ type: ActionTypes.INIT }) // 觸發生成新的 state 樹 } /** * 這是留給 可觀察/響應式庫 的接口(詳情 https://github.com/zenparsing/es-observable) * 如果您了解 RxJS 等響應式編程庫,那可能會用到這個接口,否則請略過 * @return {observable} */ function observable() {略} // 這里 dispatch 只是為了生成 應用初始狀態 dispatch({ type: ActionTypes.INIT }) return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable } }
【懸念2:什么是反轉控制權? · 解疑】
在同步場景下,dispatch(action) 的這個 action 中的數據是同步獲取的,并沒有控制權的切換問題
但異步場景下,則需要將 dispatch 傳入到回調函數。待異步操作完成后,回調函數自行調用 dispatch(action)
說白了:在異步 Action Creator 中自行調用 dispatch 就相當于反轉控制權
您完全可以自己實現,也可以借助 redux-thunk / redux-promise 等中間件統一實現
(它們的作用也僅僅就是把 dispatch 等傳入異步 Action Creator 罷了)
§ combineReducers(reducers) ⊙ 應用場景拓展閱讀:阮老師的 Thunk 函數的含義與用法
題外話:您不覺得 JavaScript 的回調函數,就是反轉控制權最普遍的體現嗎?
簡明教程中的 code-7 如下:
/** 本代碼塊記為 code-7 **/ var initState = { counter: 0, todos: [] } function reducer(state, action) { if (!state) state = initState switch (action.type) { case "ADD_TODO": var nextState = _.deepClone(state) // 用到了 lodash 的深克隆 nextState.todos.push(action.payload) return nextState default: return state } }
上面的 reducer 僅僅是實現了 “新增待辦事項” 的 state 的處理
我們還有計數器的功能,下面我們繼續增加計數器 “增加 1” 的功能:
/** 本代碼塊記為 code-8 **/ var initState = { counter: 0, todos: [] } function reducer(state, action) { if (!state) return initState // 若是初始化可立即返回應用初始狀態 var nextState = _.deepClone(state) // 否則二話不說先克隆 switch (action.type) { case "ADD_TODO": // 新增待辦事項 nextState.todos.push(action.payload) break case "INCREMENT": // 計數器加 1 nextState.counter = nextState.counter + 1 break } return nextState }
如果說還有其他的動作,都需要在 code-8 這個 reducer 中繼續堆砌處理邏輯
但我們知道,計數器 與 待辦事項 屬于兩個不同的模塊,不應該都堆在一起寫
如果之后又要引入新的模塊(例如留言板),該 reducer 會越來越臃腫
此時就是 combineReducers 大顯身手的時刻:
目錄結構如下 reducers/ ├── index.js ├── counterReducer.js ├── todosReducer.js
/** 本代碼塊記為 code-9 **/ /* reducers/index.js */ import { combineReducers } from "redux" import counterReducer from "./counterReducer" import todosReducer from "./todosReducer" const rootReducer = combineReducers({ counter: counterReducer, // <-------- 鍵名就是該 reducer 對應管理的 state todos: todosReducer }) export default rootReducer ------------------------------------------------- /* reducers/counterReducer.js */ export default function counterReducer(counter = 0, action) { // 傳入的 state 其實是 state.counter switch (action.type) { case "INCREMENT": return counter + 1 // counter 是值傳遞,因此可以直接返回一個值 default: return counter } } ------------------------------------------------- /* reducers/todosReducers */ export default function todosReducer(todos = [], action) { // 傳入的 state 其實是 state.todos switch (action.type) { case "ADD_TODO": return [ ...todos, action.payload ] default: return todos } }
code-8 reducer 與 code-9 rootReducer 的功能是一樣的,但后者的各個子 reducer 僅維護對應的那部分 state
其可操作性、可維護性、可擴展性大大增強
Flux 中是根據不同的功能拆分出多個 store 分而治之
而 Redux 只允許應用中有唯一的 store,通過拆分出多個 reducer 分別管理對應的 state
下面繼續來深入使用 combineReducers。一直以來我們的應用狀態都是只有兩層,如下所示:
state ├── counter: 0 ├── todos: []
如果說現在又有一個需求:在待辦事項模塊中,存儲用戶每次操作(增刪改)的時間,那么此時應用初始狀態樹應為:
state ├── counter: 0 ├── todo ├── optTime: [] ├── todoList: [] # 這其實就是原來的 todos!
那么對應的 reducer 就是:
目錄結構如下 reducers/ ├── index.js <-------------- combineReducers (生成 rootReducer) ├── counterReducer.js ├── todoReducers/ <--------- combineReducers ├── index.js ├── optTimeReducer.js ├── todoListReducer.js
/* reducers/index.js */ import { combineReducers } from "redux" import counterReducer from "./counterReducer" import todoReducers from "./todoReducers/" const rootReducer = combineReducers({ counter: counterReducer, todo: todoReducers }) export default rootReducer ================================================= /* reducers/todoReducers/index.js */ import { combineReducers } from "redux" import optTimeReducer from "./optTimeReducer" import todoListReducer from "./todoListReducer" const todoReducers = combineReducers({ optTime: optTimeReducer, todoList: todoListReducer }) export default todoReducers ------------------------------------------------- /* reducers/todosReducers/optTimeReducer.js */ export default function optTimeReducer(optTime = [], action) { // 咦?這里怎么沒有 switch-case 分支?誰說 reducer 就一定包含 switch-case 分支的? return action.type.includes("TODO") ? [ ...optTime, new Date() ] : optTime } ------------------------------------------------- /* reducers/todosReducers/todoListReducer.js */ export default function todoListReducer(todoList = [], action) { switch (action.type) { case "ADD_TODO": return [ ...todoList, action.payload ] default: return todoList } }
無論您的應用狀態樹有多么的復雜,都可以通過逐層下分管理對應部分的 state:
counterReducer(counter, action) -------------------- counter ↗ ↘ rootReducer(state, action) —→∑ ↗ optTimeReducer(optTime, action) ------ optTime ↘ nextState ↘—→∑ todo ↗ ↘ todoListReducer(todoList,action) ----- todoList ↗ 注:左側表示 dispatch 分發流,∑ 表示 combineReducers;右側表示各實體 reducer 的返回值,最后匯總整合成 nextState
看了上圖,您應該能直觀感受到為何取名為 reducer 了吧?把 state 分而治之,極大減輕開發與維護的難度
⊙ 源碼分析無論是 dispatch 哪個 action,都會流通所有的 reducer
表面上看來,這樣子很浪費性能,但 JavaScript 對于這種純函數的調用是很高效率的,因此請盡管放心
這也是為何 reducer 必須返回其對應的 state 的原因。否則整合狀態樹時,該 reducer 對應的鍵名就是 undefined
僅截取關鍵部分,畢竟有很大一部分都是類型檢測警告
function combineReducers(reducers) { var reducerKeys = Object.keys(reducers) var finalReducers = {} for (var i = 0; i < reducerKeys.length; i++) { var key = reducerKeys[i] if (typeof reducers[key] === "function") { finalReducers[key] = reducers[key] } } var finalReducerKeys = Object.keys(finalReducers) // 返回合成后的 reducer return function combination(state = {}, action) { var hasChanged = false var nextState = {} for (var i = 0; i < finalReducerKeys.length; i++) { var key = finalReducerKeys[i] var reducer = finalReducers[key] var previousStateForKey = state[key] // 獲取當前子 state var nextStateForKey = reducer(previousStateForKey, action) // 執行各子 reducer 中獲取子 nextState nextState[key] = nextStateForKey // 將子 nextState 掛載到對應的鍵名 hasChanged = hasChanged || nextStateForKey !== previousStateForKey } return hasChanged ? nextState : state } }
§ bindActionCreators(actionCreators, dispatch)在此我的注釋很少,因為代碼寫得實在是太過明了了,注釋反而影響閱讀
作者 Dan 用了大量的 for 循環,的確有點不夠優雅
⊙ 源碼分析這個 API 有點雞肋,它無非就是做了這件事情:dispatch(ActionCreator(XXX))
/* 為 Action Creator 加裝上自動 dispatch 技能 */ function bindActionCreator(actionCreator, dispatch) { return (...args) => dispatch(actionCreator(...args)) } export default function bindActionCreators(actionCreators, dispatch) { // 省去一大坨類型判斷 var keys = Object.keys(actionCreators) var boundActionCreators = {} for (var i = 0; i < keys.length; i++) { var key = keys[i] var actionCreator = actionCreators[key] if (typeof actionCreator === "function") { // 逐個裝上自動 dispatch 技能 boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) } } return boundActionCreators }⊙ 應用場景
簡明教程中的 code-5 如下:
<--! 本代碼塊記為 code-5 -->
我們看到,調用 addTodo 這個 Action Creator 后得到一個 action,之后又要手動 dispatch(action)
如果是只有一個兩個 Action Creator 還是可以接受,但如果有很多個那就顯得有點重復了(其實我覺得不重復哈哈哈)
這個時候我們就可以利用 bindActionCreators 實現自動 dispatch:
§ applyMiddleware(...middlewares)綜上,這個 API 沒啥卵用,尤其是異步場景下,基本用不上
Redux 中文文檔 高級 · Middleware 有提到中間件的演化由來
首先要理解何謂 Middleware,何謂 Enhancer
⊙ Middleware說白了,Redux 引入中間件機制,其實就是為了在 dispatch 前后,統一“做愛做的事”。。。
諸如統一的日志記錄、引入 thunk 統一處理異步 Action Creator 等都屬于中間件
下面是一個簡單的打印動作前后 state 的中間件:
/* 裝逼寫法 */ const printStateMiddleware = ({ getState }) => next => action => { console.log("state before dispatch", getState()) let returnValue = next(action) console.log("state after dispatch", getState()) return returnValue } ------------------------------------------------- /* 降低逼格寫法 */ function printStateMiddleware(middlewareAPI) { // 記為【錨點-1】,中間件內可用的 API return function (dispatch) { // 記為【錨點-2】,傳入原 dispatch 的引用 return function (action) { console.log("state before dispatch", middlewareAPI.getState()) var returnValue = dispatch(action) // 還記得嗎,dispatch 的返回值其實還是 action console.log("state after dispatch", middlewareAPI.getState()) return returnValue // 繼續傳給下一個中間件作為參數 action } } }⊙ Store Enhancer
說白了,Store 增強器就是對生成的 store API 進行改造,這是它與中間件最大的區別(中間件不修改 store 的 API)
而改造 store 的 API 就要從它的締造者 createStore 入手。例如,Redux 的 API applyMiddleware 就是一個 Store 增強器:
import compose from "./compose" // 這貨的作用其實就是 compose(f, g, h)(action) => f(g(h(action))) /* 傳入一坨中間件 */ export default function applyMiddleware(...middlewares) { /* 傳入 createStore */ return function(createStore) { /* 返回一個函數簽名跟 createStore 一模一樣的函數,亦即返回的是一個增強版的 createStore */ return function(reducer, preloadedState, enhancer) { // 用原 createStore 先生成一個 store,其包含 getState / dispatch / subscribe / replaceReducer 四個 API var store = createStore(reducer, preloadedState, enhancer) var dispatch = store.dispatch // 指向原 dispatch var chain = [] // 存儲中間件的數組 // 提供給中間件的 API(其實都是 store 的 API) var middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } // 給中間件“裝上” API,見上面 ⊙Middleware【降低逼格寫法】的【錨點-1】 chain = middlewares.map(middleware => middleware(middlewareAPI)) // 串聯各個中間件,為各個中間件傳入原 store.dispatch,見【降低逼格寫法】的【錨點-2】 dispatch = compose(...chain)(store.dispatch) return { ...store, // store 的 API 中保留 getState / subsribe / replaceReducer dispatch // 新 dispatch 覆蓋原 dispatch,往后調用 dispatch 就會觸發 chain 內的中間件鏈式串聯執行 } } } }
最終返回的雖然還是 store 的那四個 API,但其中的 dispatch 函數的功能被增強了,這就是所謂的 Store Enhancer
⊙ 綜合應用 ( 在線演示 )控制臺輸出:
dispatch 前:{ counter: 0 } dispatch 后:{ counter: 1 } dispatch 前:{ counter: 1 } dispatch 后:{ counter: 2 } dispatch 前:{ counter: 2 } dispatch 后:{ counter: 1 }
實際上,上面生成 store 的代碼可以更加優雅:
/** 本代碼塊記為 code-10 **/ var store = Redux.createStore( reducer, Redux.applyMiddleware(printStateMiddleware) )
如果有多個中間件以及多個增強器,還可以這樣寫(請留意序號順序):
重溫一下 createStore 完整的函數簽名:function createStore(reducer, preloadedState, enhancer)
/** 本代碼塊記為 code-11 **/ import { createStore, applyMiddleware, compose } from "redux" const store = createStore( reducer, preloadedState, // <----- 可選,前后端同構的數據同步 compose( // <------------ 還記得嗎?compose 是從右到左的哦! applyMiddleware( // <-- 這貨也是 Store Enhancer 哦!但這是關乎中間件的增強器,必須置于 compose 執行鏈的最后 middleware1, middleware2, middleware3 ), enhancer3, enhancer2, enhancer1 ) )
為什么會支持那么多種寫法呢?在 createStore 的源碼分析的開頭部分,我省略了一些代碼,現在奉上該壓軸部分:
/** 本代碼塊記為 code-12 **/ if (typeof preloadedState === "function" && typeof enhancer === "undefined") { // 這里就是上面 code-10 的情況,只傳入 reducer 和 Store Enhancer 這兩個參數 enhancer = preloadedState preloadedState = undefined } if (typeof enhancer !== "undefined") { if (typeof enhancer !== "function") { throw new Error("Expected the enhancer to be a function.") } // 存在 enhancer 就立即執行,返回增強版的 createStore <--------- 記為【錨點 12-1】 return enhancer(createStore)(reducer, preloadedState) } if (typeof reducer !== "function") { throw new Error("Expected the reducer to be a function.") } // 除 compose 外,createStore 竟然也在此為我們提供了書寫的便利與自由度,實在是太體貼了
如果像 code-11 那樣有多個 enhancer,則 code-12 【錨點 12-1】 中的代碼會執行多次
生成最終的超級增強版 store。最后,奉上 code-11 中 compose 內部的執行順序示意圖:
原 createStore ———— │ ↓ return enhancer1(createStore)(reducer, preloadedState, enhancer2) | ├———————→ createStore 增強版 1 │ ↓ return enhancer2(createStore1)(reducer, preloadedState, enhancer3) | ├———————————→ createStore 增強版 1+2 │ ↓ return enhancer3(createStore1+2)(reducer, preloadedState, applyMiddleware(m1,m2,m3)) | ├————————————————————→ createStore 增強版 1+2+3 │ ↓ return appleMiddleware(m1,m2,m3)(createStore1+2+3)(reducer, preloadedState) | ├——————————————————————————————————→ 生成最終增強版 store§ 總結
Redux 有五個 API,分別是:
createStore(reducer, [initialState])
combineReducers(reducers)
applyMiddleware(...middlewares)
bindActionCreators(actionCreators, dispatch)
compose(...functions)
createStore 生成的 store 有四個 API,分別是:
getState()
dispatch(action)
subscribe(listener)
replaceReducer(nextReducer)
至此,若您已經理解上述 API 的作用機理,以及中間件與增強器的概念/區別
本人將不勝榮幸,不妨點個 star 算是對我的贊賞
如您對本教程有任何意見或改進的建議,歡迎 issue,我會盡快予您答復
最后奉上 React + Redux + React Router 的簡易留言板實例:react-demo
拓展閱讀:中間件的洋蔥模型
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/80156.html
摘要:只要一個有,那無論用什么設備訪問,都會得到這個還原也是相當簡單把數據庫備份導入到另一臺機器,部署同樣的運行環境與代碼。純粹只是一個狀態管理庫,幾乎可以搭配任何框架使用上述例子連都沒用哦親下一章進階教程 Redux 簡明教程 原文鏈接(保持更新):https://github.com/kenberkele... 寫在前面 本教程深入淺出,配套 簡明教程、進階教程(源碼精讀)以及文檔注釋...
摘要:在函數式編程中,異步操作修改全局變量等與函數外部環境發生的交互叫做副作用通常認為這些操作是邪惡骯臟的,并且也是導致的源頭。 注:這篇是17年1月的文章,搬運自本人 blog... https://github.com/BuptStEve/... 零、前言 在上一篇中介紹了 Redux 的各項基礎 api。接著一步一步地介紹如何與 React 進行結合,并從引入過程中遇到的各個痛點引出 ...
閱讀 2902·2023-04-26 02:14
閱讀 3751·2019-08-30 15:55
閱讀 1843·2019-08-29 16:42
閱讀 2757·2019-08-26 11:55
閱讀 2846·2019-08-23 13:38
閱讀 480·2019-08-23 12:10
閱讀 1308·2019-08-23 11:44
閱讀 2791·2019-08-23 11:43