一次react-router + react-transition-group實現(xiàn)轉(zhuǎn)場動畫的探索

原文地址

1. Introduction

在日常開發(fā)中,頁面切換時的轉(zhuǎn)場動畫是比較基礎(chǔ)的一個場景死嗦。在react項目當中褒繁,我們一般都會選用react-router來管理路由犬庇,但是react-router卻并沒有提供相應(yīng)的轉(zhuǎn)場動畫功能俯渤,而是非常生硬的直接替換掉組件呆细。一定程度上來說,體驗并不是那么友好八匠。

為了在react中實現(xiàn)動畫效果絮爷,其實我們有很多的選擇,比如:react-transition-group臀叙,react-motion略水,Animated等等价卤。但是劝萤,由于react-transition-group給元素添加的enter,enter-active慎璧,exit床嫌,exit-active這一系列勾子,簡直就是為我們的頁面入場離場而設(shè)計的胸私⊙岽Γ基于此,本文選擇react-transition-group來實現(xiàn)動畫效果岁疼。

接下來阔涉,本文就將結(jié)合兩者提供一個實現(xiàn)路由轉(zhuǎn)場動畫的思路,權(quán)當拋磚引玉~

2. Requirements

我們先明確要完成的轉(zhuǎn)場動畫是什么效果捷绒。如下圖所示:

3. react-router

首先瑰排,我們先簡要介紹下react-router的基本用法(詳細看官網(wǎng)介紹)。

這里我們會用到react-router提供的BrowserRouter暖侨,Switch椭住,Route三個組件。

  • BrowserRouter:以html5提供的history api形式實現(xiàn)的路由(還有一種hash形式實現(xiàn)的路由)字逗。
  • Switch:多個Route組件同時匹配時京郑,默認都會顯示宅广,但是被Switch包裹起來的Route組件只會顯示第一個被匹配上的路由。
  • Route:路由組件些举,path指定匹配的路由跟狱,component指定路由匹配時展示的組件。
// src/App1/index.js
export default class App1 extends React.PureComponent {
  render() {
    return (
      <BrowserRouter>
        <Switch>
          <Route exact path={'/'} component={HomePage}/>
          <Route exact path={'/about'} component={AboutPage}/>
          <Route exact path={'/list'} component={ListPage}/>
          <Route exact path={'/detail'} component={DetailPage}/>
        </Switch>
      </BrowserRouter>
    );
  }
}

如上所示户魏,這是路由關(guān)鍵的實現(xiàn)部分兽肤。我們一共創(chuàng)建了首頁關(guān)于頁绪抛,列表頁资铡,詳情頁這四個頁面。跳轉(zhuǎn)關(guān)系為:

  1. 首頁 ? 關(guān)于頁
  2. 首頁 ? 列表頁 ? 詳情頁

來看下目前默認的路由切換效果:

4. react-transition-group

從上面的效果圖中幢码,我們可以看到react-router在路由切換時完全沒有過渡效果笤休,而是直接替換的,顯得非常生硬症副。

正所謂工欲善其事店雅,必先利其器,在介紹實現(xiàn)轉(zhuǎn)場動畫之前贞铣,我們得先學(xué)習如何使用react-transition-group闹啦。基于此辕坝,接下來就將對其提供的CSSTransition和TransitionGroup這兩個組件展開簡要介紹窍奋。

4.1 CSSTransition

CSSTransition是react-transition-group提供的一個組件,這里簡單介紹下其工作原理酱畅。

When the in prop is set to true, the child component will first receive the class example-enter, then the example-enter-active will be added in the next tick. CSSTransition forces a reflow between before adding the example-enter-active. This is an important trick because it allows us to transition between example-enter and example-enter-active even though they were added immediately one after another. Most notably, this is what makes it possible for us to animate appearance.

這是來自官網(wǎng)上的一段描述琳袄,意思是當CSSTransition的in屬性置為true時,CSSTransition首先會給其子組件加上xxx-enter的class纺酸,然后在下個tick時馬上加上xxx-enter-active的class窖逗。所以我們可以利用這一點,通過css的transition屬性餐蔬,讓元素在兩個狀態(tài)之間平滑過渡碎紊,從而得到相應(yīng)的動畫效果。

相反地樊诺,當in屬性置為false時仗考,CSSTransition會給子組件加上xxx-exit和xxx-exit-active的class。(更多詳細介紹可以戳官網(wǎng)查看)

基于以上兩點啄骇,我們是不是只要事先寫好class對應(yīng)的css樣式即可痴鳄?可以做個小demo試試,如下代碼所示:

// src/App2/index.js
export default class App2 extends React.PureComponent {

  state = {show: true};

  onToggle = () => this.setState({show: !this.state.show});

  render() {
    const {show} = this.state;
    return (
      <div className={'container'}>
        <div className={'square-wrapper'}>
          <CSSTransition
            in={show}
            timeout={500}
            classNames={'fade'}
            unmountOnExit={true}
          >
            <div className={'square'} />
          </CSSTransition>
        </div>
        <Button onClick={this.onToggle}>toggle</Button>
      </div>
    );
  }
}
/* src/App2/index.css */
.fade-enter {
  opacity: 0;
  transform: translateX(100%);
}

.fade-enter-active {
  opacity: 1;
  transform: translateX(0);
  transition: all 500ms;
}

.fade-exit {
  opacity: 1;
  transform: translateX(0);
}

.fade-exit-active {
  opacity: 0;
  transform: translateX(-100%);
  transition: all 500ms;
}

來看看效果缸夹,是不是和頁面的入場離場效果有點相似痪寻?

4.2 TransitionGroup

用CSSTransition來處理動畫固然很方便螺句,但是直接用來管理多個頁面的動畫還是略顯單薄。為此我們再來介紹react-transition-group提供的TransitionGroup這個組件橡类。

The <TransitionGroup> component manages a set of transition components (<Transition> and <CSSTransition>) in a list. Like with the transition components, <TransitionGroup> is a state machine for managing the mounting and unmounting of components over time.

如官網(wǎng)介紹蛇尚,TransitionGroup組件就是用來管理一堆節(jié)點mounting和unmounting過程的組件,非常適合處理我們這里多個頁面的情況顾画。這么介紹似乎有點難懂取劫,那就讓我們來看段代碼,解釋下TransitionGroup的工作原理研侣。

// src/App3/index.js
export default class App3 extends React.PureComponent {

  state = {num: 0};

  onToggle = () => this.setState({num: (this.state.num + 1) % 2});

  render() {
    const {num} = this.state;
    return (
      <div className={'container'}>
        <TransitionGroup className={'square-wrapper'}>
          <CSSTransition
            key={num}
            timeout={500}
            classNames={'fade'}
          >
            <div className={'square'}>{num}</div>
          </CSSTransition>
        </TransitionGroup>
        <Button onClick={this.onToggle}>toggle</Button>
      </div>
    );
  }
}

我們先來看效果谱邪,然后再做解釋:

對比App3和App2的代碼,我們可以發(fā)現(xiàn)這次CSSTransition沒有in屬性了庶诡,而是用到了key屬性惦银。但是為什么仍然可以正常工作呢?

在回答這個問題之前末誓,我們先來思考一個問題:

由于react的dom diff機制用到了key屬性扯俱,如果前后兩次key不同,react會卸載舊節(jié)點喇澡,掛載新節(jié)點迅栅。那么在上面的代碼中,由于key變了晴玖,舊節(jié)點難道不是應(yīng)該立馬消失读存,但是為什么我們還能看到它淡出的動畫過程呢?

關(guān)鍵就出在TransitionGroup身上窜醉,因為它在感知到其children變化時宪萄,會先保存住即將要被移除的節(jié)點,而在其動畫結(jié)束時才會真正移除該節(jié)點榨惰。

所以在上面的例子中,當我們按下toggle按鈕時静汤,變化的過程可以這樣理解:

<TransitionGroup>
  <div>0</div>
</TransitionGroup>

????????????????

<TransitionGroup>
  <div>0</div>
  <div>1</div>
</TransitionGroup>

????????????????

<TransitionGroup>
  <div>1</div>
</TransitionGroup>

如上所解釋琅催,我們完全可以巧妙地借用key值的變化來讓TransitionGroup來接管我們在過渡時的頁面創(chuàng)建和銷毀工作,而僅僅需要關(guān)注如何選擇合適的key值和需要什么樣css樣式來實現(xiàn)動畫效果就可以了虫给。

5. Page transition animation

基于前文對react-router和react-transition-group的介紹藤抡,我們已經(jīng)掌握了基礎(chǔ),接下來就可以將兩者結(jié)合起來做頁面切換的轉(zhuǎn)場動畫了抹估。

在上一小節(jié)的末尾有提到缠黍,用了TransitionGroup之后我們的問題變成如何選擇合適的key值。那么在路由系統(tǒng)中药蜻,什么作為key值比較合適呢瓷式?

既然我們是在頁面切換的時候觸發(fā)轉(zhuǎn)場動畫替饿,自然是跟路由相關(guān)的值作為key值合適了。而react-router中的location對象就有一個key屬性贸典,它會隨著瀏覽器中的地址發(fā)生變化而變化视卢。然而,在實際場景中似乎并不適合廊驼,因為query參數(shù)或者hash變化也會導(dǎo)致location.key發(fā)生變化据过,但往往這些場景下并不需要觸發(fā)轉(zhuǎn)場動畫。

因此妒挎,個人覺得key值的選取還是得根據(jù)不同的項目而視绳锅。大部分情況下,還是推薦用location.pathname作為key值比較合適酝掩,因為它恰是我們不同頁面的路由榨呆。

說了這么多,還是看看具體的代碼是如何將react-transition-group應(yīng)用到react-router上的吧:

// src/App4/index.js
const Routes = withRouter(({location}) => (
  <TransitionGroup className={'router-wrapper'}>
    <CSSTransition
      timeout={5000}
      classNames={'fade'}
      key={location.pathname}
    >
      <Switch location={location}>
        <Route exact path={'/'} component={HomePage} />
        <Route exact path={'/about'} component={AboutPage} />
        <Route exact path={'/list'} component={ListPage} />
        <Route exact path={'/detail'} component={DetailPage} />
      </Switch>
    </CSSTransition>
  </TransitionGroup>
));

export default class App4 extends React.PureComponent {
  render() {
    return (
      <BrowserRouter>
        <Routes/>
      </BrowserRouter>
    );
  }
}

這是效果:

App4的代碼思路跟App3大致相同庸队,只是將原來的div換成了Switch組件积蜻,而且還用到了withRouter。

withRouter是react-router提供的一個高階組件彻消,可以為你的組件提供location竿拆,history等對象。因為我們這里要用location.pathname作為CSSTransition的key值宾尚,所以用到了它丙笋。

另外,這里有一個坑煌贴,就是Switch的location屬性御板。

A location object to be used for matching children elements instead of the current history location (usually the current browser URL).

這是官網(wǎng)中的描述,意思就是Switch組件會用這個對象來匹配其children中的路由牛郑,而且默認用的就是當前瀏覽器的url怠肋。如果在上面的例子中我們不給它指定,那么在轉(zhuǎn)場動畫中會發(fā)生很奇怪的現(xiàn)象淹朋,就是同時有兩個相同的節(jié)點在移動笙各。。础芍。就像下面這樣:

這是因為TransitionGroup組件雖然會保留即將被remove的Switch節(jié)點杈抢,但是當location變化時,舊的Switch節(jié)點會用變化后的location去匹配其children中的路由仑性。由于location都是最新的惶楼,所以兩個Switch匹配出來的頁面是相同的。好在我們可以改變Switch的location屬性,如上述代碼所示歼捐,這樣它就不會總是用當前的location匹配了何陆。

6. Page dynamic transition animation

雖然前文用react-transition-group和react-router實現(xiàn)了一個簡單的轉(zhuǎn)場動畫,但是卻存在一個嚴重的問題窥岩。仔細觀察上一小節(jié)的示意圖甲献,不難發(fā)現(xiàn)我們的進入下個頁面的動畫效果是符合預(yù)期的,但是后退的動畫效果是什么鬼颂翼。晃洒。修肠。明明應(yīng)該是上個頁面從左側(cè)淡入鳖擒,當前頁面從右側(cè)淡出。但是為什么卻變成當前頁面從左側(cè)淡出颅崩,下個頁面從右側(cè)淡入呻疹,跟進入下個頁面的效果是一樣的吃引。其實錯誤的原因很簡單:

首先,我們把路由改變分成forward和back兩種操作刽锤。在forward操作時镊尺,當前頁面的exit效果是向左淡出;在back操作時并思,當前頁面的exit效果是向右淡出庐氮。所以我們只用fade-exit和fade-exit-active這兩個class,很顯然宋彼,得到的動畫效果肯定是一致的弄砍。

因此,解決方案也很簡單输涕,我們用兩套class來分別管理forward和back操作時的動畫效果就可以了音婶。

/* src/App5/index.css */

/* 路由前進時的入場/離場動畫 */
.forward-enter {
  opacity: 0;
  transform: translateX(100%);
}

.forward-enter-active {
  opacity: 1;
  transform: translateX(0);
  transition: all 500ms;
}

.forward-exit {
  opacity: 1;
  transform: translateX(0);
}

.forward-exit-active {
  opacity: 0;
  transform: translateX(-100%);
  transition: all 500ms;
}

/* 路由后退時的入場/離場動畫 */
.back-enter {
  opacity: 0;
  transform: translateX(-100%);
}

.back-enter-active {
  opacity: 1;
  transform: translateX(0);
  transition: all 500ms;
}

.back-exit {
  opacity: 1;
  transform: translateX(0);
}

.back-exit-active {
  opacity: 0;
  transform: translate(100%);
  transition: all 500ms;
}

不過光有css的支持還不行,我們還得在不同的路由操作時加上合適的class才行莱坎。那么問題又來了衣式,在TransitionGroup的管理下,一旦某個組件掛載后型奥,其exit動畫其實就已經(jīng)確定了瞳收,可以看官網(wǎng)上的這個issue。也就是說厢汹,就算我們動態(tài)地給CSSTransition添加不同的ClassNames屬性來指定動畫效果,但其實是無效的谐宙。

解決方案其實在那個issue的下面就給出了烫葬,我們可以借助TransitionGroup的ChildFactory屬性以及React.cloneElement方法來強行覆蓋其className。比如:

<TransitionGroup childFactory={child => React.cloneElement(child, {
  classNames: 'your-animation-class-name'
})}>
  <CSSTransition>
    ...
  </CSSTransition>
</TransitionGroup>

上述幾個問題都解決之后,剩下的問題就是如何選擇合適的動畫class了搭综。而這個問題的實質(zhì)在于如何判斷當前路由的改變是forward還是back操作了垢箕。好在react-router已經(jīng)貼心地給我們準備好了,其提供的history對象有一個action屬性兑巾,代表當前路由改變的類型条获,其值是'PUSH' | 'POP' | 'REPLACE'。所以蒋歌,我們再調(diào)整下代碼:

// src/App5/index.js
const ANIMATION_MAP = {
  PUSH: 'forward',
  POP: 'back'
}

const Routes = withRouter(({location, history}) => (
  <TransitionGroup
    className={'router-wrapper'}
    childFactory={child => React.cloneElement(
      child,
      {classNames: ANIMATION_MAP[history.action]}
    )}
  >
    <CSSTransition
      timeout={500}
      key={location.pathname}
    >
      <Switch location={location}>
        <Route exact path={'/'} component={HomePage} />
        <Route exact path={'/about'} component={AboutPage} />
        <Route exact path={'/list'} component={ListPage} />
        <Route exact path={'/detail'} component={DetailPage} />
      </Switch>
    </CSSTransition>
  </TransitionGroup>
));

再來看下修改之后的動畫效果:

7. Optimize

其實帅掘,本節(jié)的內(nèi)容算不上優(yōu)化,轉(zhuǎn)場動畫的思路到這里基本上已經(jīng)結(jié)束了堂油,你可以腦洞大開修档,通過添加css來實現(xiàn)更炫酷的轉(zhuǎn)場動畫。不過府框,這里還是想再講下如何將我們的路由寫得更配置化(個人喜好吱窝,不喜勿噴)。

我們知道迫靖,react-router在升級v4的時候院峡,做了一次大改版。更加推崇動態(tài)路由系宜,而非靜態(tài)路由照激。不過具體問題具體分析,在一些項目中個人還是喜歡將路由集中化管理蜈首,就上面的例子而言希望能有一個RouteConfig实抡,就像下面這樣:

// src/App6/RouteConfig.js
export const RouterConfig = [
  {
    path: '/',
    component: HomePage
  },
  {
    path: '/about',
    component: AboutPage,
    sceneConfig: {
      enter: 'from-bottom',
      exit: 'to-bottom'
    }
  },
  {
    path: '/list',
    component: ListPage,
    sceneConfig: {
      enter: 'from-right',
      exit: 'to-right'
    }
  },
  {
    path: '/detail',
    component: DetailPage,
    sceneConfig: {
      enter: 'from-right',
      exit: 'to-right'
    }
  }
];

透過上面的RouterConfig,我們可以清晰的知道每個頁面所對應(yīng)的組件是哪個欢策,而且還可以知道其轉(zhuǎn)場動畫效果是什么吆寨,比如關(guān)于頁面是從底部進入頁面的,列表頁詳情頁都是從右側(cè)進入頁面的踩寇∽那澹總而言之,我們通過這個靜態(tài)路由配置表可以直接獲取到很多有用的信息俺孙,而不需要深入到代碼中去獲取信息辣卒。

那么,對于上面的這個需求睛榄,我們對應(yīng)的路由代碼需要如何調(diào)整呢荣茫?請看下面:

// src/App6/index.js
const DEFAULT_SCENE_CONFIG = {
  enter: 'from-right',
  exit: 'to-exit'
};

const getSceneConfig = location => {
  const matchedRoute = RouterConfig.find(config => new RegExp(`^${config.path}$`).test(location.pathname));
  return (matchedRoute && matchedRoute.sceneConfig) || DEFAULT_SCENE_CONFIG;
};

let oldLocation = null;
const Routes = withRouter(({location, history}) => {

  // 轉(zhuǎn)場動畫應(yīng)該都是采用當前頁面的sceneConfig,所以:
  // push操作時场靴,用新location匹配的路由sceneConfig
  // pop操作時啡莉,用舊location匹配的路由sceneConfig
  let classNames = '';
  if(history.action === 'PUSH') {
    classNames = 'forward-' + getSceneConfig(location).enter;
  } else if(history.action === 'POP' && oldLocation) {
    classNames = 'back-' + getSceneConfig(oldLocation).exit;
  }

  // 更新舊location
  oldLocation = location;

  return (
    <TransitionGroup
      className={'router-wrapper'}
      childFactory={child => React.cloneElement(child, {classNames})}
    >
      <CSSTransition timeout={500} key={location.pathname}>
        <Switch location={location}>
          {RouterConfig.map((config, index) => (
            <Route exact key={index} {...config}/>
          ))}
        </Switch>
      </CSSTransition>
    </TransitionGroup>
  );
});

由于css代碼有點多港准,這里就不貼了,不過無非就是相應(yīng)的轉(zhuǎn)場動畫配置咧欣,完整的代碼可以看github上的倉庫浅缸。我們來看下目前的效果:

8. Summarize

本文先簡單介紹了react-router和react-transition-group的基本使用方法;其中還分析了利用CSSTransition和TransitionGroup制作動畫的工作原理魄咕;接著又將react-router和react-transition-group兩者結(jié)合在一起完成一次轉(zhuǎn)場動畫的嘗試衩椒;并利用TransitionGroup的childFactory屬性解決了動態(tài)轉(zhuǎn)場動畫的問題;最后將路由配置化哮兰,實現(xiàn)路由的統(tǒng)一管理以及動畫的配置化毛萌,完成一次react-router + react-transition-group實現(xiàn)轉(zhuǎn)場動畫的探索。

9. Reference

  1. A shallow dive into router v4 animated transitions
  2. Dynamic transitions with react router and react transition group
  3. Issue#182 of react-transition-group
  4. StackOverflow: react-transition-group and react clone element do not send updated props

本文所有代碼托管在這兒奠蹬,如果覺得不錯的朝聋,可以給個star

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末囤躁,一起剝皮案震驚了整個濱河市冀痕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌狸演,老刑警劉巖言蛇,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異宵距,居然都是意外死亡腊尚,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門满哪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來婿斥,“玉大人,你說我怎么就攤上這事哨鸭∶袼蓿” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵像鸡,是天一觀的道長活鹰。 經(jīng)常有香客問我,道長只估,這世上最難降的妖魔是什么志群? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮蛔钙,結(jié)果婚禮上锌云,老公的妹妹穿的比我還像新娘。我一直安慰自己吁脱,他們只是感情好宾抓,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布子漩。 她就那樣靜靜地躺著豫喧,像睡著了一般石洗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上紧显,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天讲衫,我揣著相機與錄音,去河邊找鬼孵班。 笑死涉兽,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的篙程。 我是一名探鬼主播枷畏,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼虱饿!你這毒婦竟也來了拥诡?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤氮发,失蹤者是張志新(化名)和其女友劉穎渴肉,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體爽冕,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡仇祭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了颈畸。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乌奇。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖眯娱,靈堂內(nèi)的尸體忽然破棺而出礁苗,到底是詐尸還是另有隱情,我是刑警寧澤困乒,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布寂屏,位于F島的核電站,受9級特大地震影響娜搂,放射性物質(zhì)發(fā)生泄漏迁霎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一百宇、第九天 我趴在偏房一處隱蔽的房頂上張望考廉。 院中可真熱鬧,春花似錦携御、人聲如沸昌粤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽涮坐。三九已至凄贩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間袱讹,已是汗流浹背疲扎。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留捷雕,地道東北人椒丧。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像救巷,于是被迫代替她去往敵國和親壶熏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容