React下ECharts的數(shù)據(jù)驅(qū)動(dòng)探索

ECharts.gif

什么是數(shù)據(jù)驅(qū)動(dòng)?

使用過Vue React框架我們就知道,我們不再更改某個(gè)DOM的innertext和innerhtml屬性就能完成視圖的改變捶朵,兩者都是通過對(duì)狀態(tài)的改變刽严,喚起 virtualDOM 的diff方法,最終生成patch反應(yīng)到真實(shí)DOM上瑟慈。區(qū)別是Vue通過依賴收集觀測(cè)數(shù)據(jù)的變化漫谷,而React是通過調(diào)用setState方法仔雷,不要小看這個(gè)區(qū)別。在結(jié)合ECharts的過程中舔示,有著極大的不同碟婆。

盡管兩者都是數(shù)據(jù)驅(qū)動(dòng)的框架,不過它們僅僅改變的是DOM惕稻,不能直接喚起ECharts的改變(ECharts本身也是數(shù)據(jù)驅(qū)動(dòng)的竖共,通過適配不同的option,就能自動(dòng)進(jìn)行變換并且找到合理的動(dòng)畫過渡)俺祠。因此需要做一些適配公给。本文將淺談在React中,完成ECharts的數(shù)據(jù)驅(qū)動(dòng)所遇到的坑點(diǎn)

期待的效果

如最上面的gif動(dòng)圖展示的蜘渣,最終我們的ECharts要實(shí)現(xiàn)兩個(gè)效果

  1. 尺寸變化引起的重繪 resize淌铐,有兩種需要考慮的情況,第一個(gè)是頁(yè)面尺寸的變化蔫缸,即 window的 resize事件腿准;第二種是上面的toggle按鈕,導(dǎo)致容器的寬度發(fā)生變化捂龄。兩者都需要進(jìn)行 chart.resize
  2. 數(shù)據(jù)驅(qū)動(dòng),通過用戶觸發(fā)DOM事件加叁,讓chart 進(jìn)行重繪

resize

本身實(shí)現(xiàn)resize并不復(fù)雜倦沧,ECharts為我們提供了 ECharts.resize 這個(gè)API。關(guān)鍵是調(diào)用這個(gè)API的時(shí)機(jī)它匕。我們發(fā)現(xiàn)導(dǎo)致畫面產(chǎn)生變化的因素只有兩個(gè)展融。一個(gè)是 window.onresize 事件,另一個(gè)是toggle的點(diǎn)擊事件豫柬。關(guān)于前者很多人都是在創(chuàng)建ECharts實(shí)例后告希,在window上綁定了事件扑浸,監(jiān)聽到變化時(shí)調(diào)用API。而后者處理的人就比較少燕偶,因?yàn)榧词故遣惶幚硪材芸春仍搿_@當(dāng)然是追求完美的我不能滿足的。

僅僅從實(shí)現(xiàn)上來看指么,為每一個(gè)實(shí)例都 addEventlistener 不太劃算酝惧。先不說不少人在實(shí)例銷毀后忘記釋放導(dǎo)致內(nèi)存的占用。每一次都綁定一次也不符合 DRY 的原則伯诬。針對(duì)這個(gè)問題我做了如下處理

// 注冊(cè)一個(gè)事件中心
const eventCenter = new EventCenter()

class Base extends React.Component<IProps, any> {
    // 略掉不相關(guān)代碼
    public async componentDidMount () {
      // ......
      EventCenter.on('resize', this.handleDOMChange)
    }

    public componentWillUnmount () {
      // ......
      this.chart && this.chart.dispose()
      delete this.chart
      EventCenter.off('resize', this.handleDOMChange)
    }

    private handleDOMChange () {
      this.chart && this.chart.resize()
    }
}

我注冊(cè)了一個(gè)事件中心晚唇。在 ECharts 的基類Base中,每當(dāng)ECharts初始化以后盗似,我都在 EventCenter 中注冊(cè)了 resize 事件哩陕, 在 Base 將要銷毀的時(shí)候注銷這個(gè)事件,并且釋放 ECharts的相關(guān)資源赫舒。注意這個(gè) handleDOMChange 是在 EventCenter 中執(zhí)行的悍及,this 指向會(huì)改變。因此在 constructor 中執(zhí)行綁定 this.handleDOMChange = this.handleDOMChange.bind(this)

window.addEventListener('resize', () => {
    EventCenter.emit('resize')
})

handleToggle() {
    setTimeout(() => {
        EventCenter.emit('resize')
    }, 500)
}

在window和切換toggle中分別觸發(fā)resize事件号阿,這樣EventCenter就會(huì)執(zhí)行注冊(cè)了的ECharts的resize方法并鸵。因?yàn)?Base 基類中也包含了注銷事件,因此不會(huì)擔(dān)心同一個(gè)ECharts注冊(cè)多次導(dǎo)致內(nèi)存的占用扔涧。

值得注意的是园担,在handleToggle的時(shí)候我設(shè)置了一個(gè)延時(shí)。這是因?yàn)辄c(diǎn)擊了toggle按鈕枯夜,視圖并沒有立即更新弯汰,即使這個(gè)時(shí)候 ECharts進(jìn)行 resize 仍然取到的是不正確的寬度。應(yīng)該等到視圖更新完以后再進(jìn)行resize湖雹。更加準(zhǔn)確的是監(jiān)聽 AppMain(右側(cè)主體)的 'transiationEnd' 事件咏闪。因?yàn)?antd 設(shè)置的變化時(shí) .5s(CSS中的設(shè)置),此處就偷懶直接寫了500ms

// ANTD-PRO中的實(shí)現(xiàn)

// antd\src\components\GlobalHeader
  @Debounce(600)
  triggerResizeEvent() {
    const event = document.createEvent('HTMLEvents');
    event.initEvent('resize', true, false);
    window.dispatchEvent(event);
  }

// antd\src\components\Charts\Bar
  @Bind()
  @Debounce(400)
  resize() {
    if (!this.node) {
      return;
    }
    const canvasWidth = this.node.parentNode.clientWidth;
    const { data = [], autoLabel = true } = this.props;
    if (!autoLabel) {
      return;
    }
    const minWidth = data.length * 30;
    const { autoHideXLabels } = this.state;

    if (canvasWidth <= minWidth) {
      if (!autoHideXLabels) {
        this.setState({
          autoHideXLabels: true,
        });
      }
    } else if (autoHideXLabels) {
      this.setState({
        autoHideXLabels: false,
      });
    }
  }

在antd-pro中摔吏,他們沒有分開設(shè)置鸽嫂, toggle是模擬了一個(gè)resize事件≌鹘玻總的邏輯放在了 window.addEventListener('resize', this.resize) 這段代碼在每一個(gè)定義的圖表類中都有据某,有些重復(fù)。相比引入一個(gè) EventCenter 就能解決诗箍,這一點(diǎn)上我覺得我的做好更好些癣籽。當(dāng)然也可以像他們一樣加入節(jié)流,避免頻繁觸發(fā)帶來的重繪消耗

數(shù)據(jù)驅(qū)動(dòng)

技術(shù)選型

在討論數(shù)據(jù)驅(qū)動(dòng)之前,我要先講講我的技術(shù)選型筷狼。在React上能選擇的框架很多瓶籽,既靈活又容易踩坑。不同的技術(shù)方案對(duì)數(shù)據(jù)的處理是不一樣的埂材。我的選型主要參考了一下幾點(diǎn)

  1. 沒有使用antd-pro塑顺,雖然這套模板在對(duì)中后臺(tái)處理給的實(shí)例非常完善,基本上能做到開箱即用楞遏,改改參數(shù)就行茬暇。但是因?yàn)闆]有Typescript的模板,我要從JS改成TS成本太高
  2. 使用mobx而不是使用redux寡喝,因?yàn)槭呛笈_(tái)頁(yè)面糙俗,每個(gè)頁(yè)面的數(shù)據(jù)基本都是獨(dú)立的。因此不需要把所有狀態(tài)都集中到一起预鬓,我為每一個(gè)頁(yè)面單獨(dú)配置一個(gè)mobx驅(qū)動(dòng)store巧骚,這樣邏輯更加簡(jiǎn)潔,將來也能充分?jǐn)U展


    數(shù)據(jù)流向.png

這就是我最后的技術(shù)選項(xiàng)格二,通過mobx提供對(duì)數(shù)據(jù)的驅(qū)動(dòng)劈彪,父組件直接引用mobx配置的store實(shí)例,store中的數(shù)據(jù)發(fā)生變化時(shí)父組件就能自動(dòng)更新視圖顶猜。同樣也可以作為參數(shù)傳給子組件沧奴,子組件就能像正常的組件一樣響應(yīng)props的變動(dòng)

數(shù)據(jù)驅(qū)動(dòng)的嘗試

在進(jìn)行數(shù)據(jù)驅(qū)動(dòng)嘗試的時(shí)候,總共有以下4種方式

  1. state傳遞配置數(shù)據(jù) state傳遞變化數(shù)據(jù) setOption為的配置數(shù)據(jù)
  2. state傳遞配置數(shù)據(jù) EventCenter驅(qū)動(dòng) setOption為初始變動(dòng)的配置數(shù)據(jù)
  3. state傳遞配置數(shù)據(jù) mobx傳遞變化的數(shù)據(jù) setOption為 變化的數(shù)據(jù)
  4. state傳遞配置數(shù)據(jù) mobx傳遞變化的數(shù)據(jù) setOption為初始變動(dòng)的配置數(shù)據(jù)

其中有兩種涼涼了长窄,接下來依次講講每種方式的實(shí)現(xiàn)

// state傳遞配置數(shù)據(jù) state傳遞變化數(shù)據(jù) setOption為的配置數(shù)據(jù)
  interface IProps {
    width?: string | number
    height?: string | number
    theme?: object
    config?: InitConfig
    opt: ECharts.EChartOption | any
    series?: any[]
    dynamic?: boolean,
    diff?: any
    debug?: boolean
  }
  class Base extends React.Component<Props, any> {

    public chartDOM: HTMLDivElement | HTMLCanvasElement
    public chart: ECharts.ECharts
    public option: ECharts.EChartOption

    public async componentDidMount () {
      let { theme, config } = this.props
      
      theme = theme || {}
      config = config || {}

      this.option = this.props.opt
      
      // 延遲 500ms 等待外層 DOM 正確初始化
      setTimeout(() => {
        this.props.debug && console.log('mount')
        this.chart = ECharts.init(this.chartDOM, theme, config)
        this.chart.setOption(this.option)
      }, 500)


      EventCenter.on('resize', this.handleDOMChange)
      if (this.props.dynamic) {
        EventCenter.on('update', this.handleUpdate)
      }
    }

    public getSnapshotBeforeUpdate () {
      this.chart.setOption(this.option)
      return null
    }
  }

  class Parent extends React.Component {
    public state = {
      opt: {
        // 省略無關(guān)
        xAxis: {
          type: 'category',
          data: store.xAxis
        }
      }
    }

    public render() {
      return (
        <Base opt={this.state.opt} height="65vh" debug={true}/>
      )
    }
  }

這種方式通過保存初始傳入的配置項(xiàng), 之后每次改動(dòng)在 getSnapshotBeforeUpdate 中監(jiān)聽然后重新設(shè)置option滔吠。結(jié)果是涼涼,因?yàn)閭魅氲膐pt雖然內(nèi)部數(shù)據(jù)發(fā)生了變化挠日,但是子組件感知不到,因此沒有執(zhí)行g(shù)etSnapshotBeforeUpdate周期疮绷。我發(fā)現(xiàn)經(jīng)管this.option發(fā)生了變化,但是子組件沒有執(zhí)行生命周期嚣潜,因此我希望數(shù)據(jù)變化了能執(zhí)行冬骚,能夠執(zhí)行setOption,參考之前resize的方法懂算,做了如下改動(dòng)

class Base extends React.Component {
  public async componentDidMount () {

  EventCenter.on('resize', this.handleDOMChange)
  if (this.props.dynamic) {
     EventCenter.on('update', this.handleUpdate)
    }
  }
}

class Store {
  
  @observable
  public xAxis: any[] = []


  private week: string[] = ['2018/07/12', '2018/07/13', '2018/07/14', '2018/07/15', '2018/07/16', '2018/07/17', '2018/07/18', '2018/07/19']

  constructor() {
    this.today = GET_TIME()
    this.xAxis = this.today
  }

  @action
  public handleWeek() {
    this.xAxis = this.week
    EventCenter.emit('update')
  }

  @action
  public handleDay() {
    this.xAxis = this.today
    EventCenter.emit('update')
  }
}

我為每一個(gè)圖形組件注冊(cè)了一個(gè) update 事件(同樣在unmount里注銷)只冻。然而并沒有成功。盡管mobx傳遞給父組件的數(shù)據(jù)變化了计技,子組件接收的數(shù)據(jù)卻沒有發(fā)生變化喜德。具體的原因可以簡(jiǎn)化為

// A是父組件,B是子組件,B組件初始化時(shí)獲取了A.attr引用
const A = {
  attr: [1 ,2, 3]
}
const B.prop = A.attr
// 數(shù)據(jù)變化
A.attr = [3, 4, 5]
this.chart.setOption(B.prop) // B.prop === [1, 2, 3]

B.prop還保持著原來屬性的引用酸役,此時(shí)setOption并不能起作用住诸。這和在react中直接修改state并不會(huì)導(dǎo)致子組件的更新一樣,必須通過setState改變一樣涣澡。所以如果想要setOption生效贱呐,我們就不能直接替換原數(shù)組的應(yīng)用,而是保持引用修改內(nèi)部的值入桂。mobx為裝飾過的數(shù)組提供了這樣一個(gè)能力

class Store {
  @action
  public handleWeek() {
    this.xAxis.clear()
    for (let i of this.week) {
      this.xAxis.push(i)
    }
    EventCenter.emit('update')
  }

  @action
  public handleDay() {
    this.xAxis.clear()
    for (let i of this.week) {
      this.xAxis.push(i)
    }    
    EventCenter.emit('update')
  } 
}

我們通過清空原來的數(shù)組并保持組件中對(duì)數(shù)組的應(yīng)用奄薇,重新填入值。再通過EventCenter觸發(fā)ECharts的更新命令抗愁,這樣就能使的ECharts能夠正確修改馁蒂。但是我們?nèi)匀徊荒苷Mㄟ^子組件的生命周期來修改,因?yàn)閷?duì)于子組件來說蜘腌,它感知不到傳入數(shù)據(jù)發(fā)生了變化(React通過判斷淺引用來判斷需要不需要更新沫屡,數(shù)據(jù)變更前后傳入的 option都沒有發(fā)生變化,盡管內(nèi)部數(shù)據(jù)發(fā)生了改變撮珠,但是組件是不知道的)沮脖。

這樣的一種 hack 實(shí)現(xiàn)的并不優(yōu)雅,首先我們引入了 EventCenter 必須每次在變動(dòng)數(shù)據(jù)的時(shí)候觸發(fā) update 事件芯急,并且數(shù)據(jù)的修改還得時(shí)刻注意不能直接修改 子組件的引用勺届,比如不能 this.arr = newArr

回到最初的目標(biāo),我們究竟需要什么樣的數(shù)據(jù)驅(qū)動(dòng)娶耍?

我們希望子組件盡可能的抽象免姿,使得我們可以通過父組件傳參數(shù)給子組件,子組件再繪制出相應(yīng)的圖表榕酒。而不是針對(duì) Bar line map 每一個(gè)圖表類型都單獨(dú)生成類胚膊。并且我們還需要圖表能根據(jù)父組件傳遞數(shù)據(jù)的變化而進(jìn)行變化,并且是在子組件的生命周期執(zhí)行奈应。而不是額外指定澜掩。

上面兩個(gè)情況是我們實(shí)際的需求,前者我們可以通過父組件傳遞一個(gè) option 選項(xiàng)控制圖表的類型杖挣。后者我們希望在子組件的生命周期里完成肩榕,因此必須要讓子組件感知到數(shù)據(jù)的變化。

最佳實(shí)踐

class Store {
  // 省略無關(guān)代碼
  @computed
  public get diff() {
    return {
      xAxis: {
        data: this.xAxis
      }
    }
  }

  @action
  public handleWeek() {
    this.xAxis = this.week
  }

  @action
  public handleDay() {
    this.xAxis = this.today
  }
}

class Parent extends React.Component {
  public state = {
    opt: {
      // 省略無關(guān)代碼
      xAxis: {
        type: 'category',
        data: store.xAxis
      }
    }
  }
  public render() {
    return (
      <Base opt={this.state.opt} diff={store.diff} height="65vh" debug={true}/>
    )
  }
}

class Base extends React.Component<Props, any>{
  public getSnapshotBeforeUpdate () {
    this.props.debug && console.log('snapshot', this.props.diff)
    if (this.props.diff) {
      this.chart && this.chart.setOption(this.props.diff)
    }
    return null
  }
}

我們?nèi)匀煌ㄟ^父組件傳遞給子組件用來渲染正確的圖表惩妇,接著把需要變化的部分 diff 從 store 里單獨(dú)抽出來傳遞給子組件株汉。子組件通過 diff 屬性接收,這樣一旦 diff 發(fā)生了變化 store 便能傳遞給子組件歌殃,子組件也能監(jiān)聽到 props 的變化進(jìn)而在生命周期里執(zhí)行ECharts的更新操作乔妈。

需要注意的是 this.chart.setOption(option: ECharts.EChartsOption) 這個(gè) option 要實(shí)現(xiàn) ECharts.EChartsOption 接口,因此我們通過 mobx 提供的 computed 屬性直接將 diff 變?yōu)橐粋€(gè)符合該接口的實(shí)現(xiàn)氓皱。

為什么選擇 getSnapshotBeforeUpdate 這個(gè)生命周期路召?

因?yàn)樵?React16 中勃刨, componentWillMount, componentWillReceiveProps, componentWillUpdate 都被標(biāo)記為不安全的生命周期(和fiber算法有關(guān)), 而 getDerivedStateFromProps 是一個(gè)靜態(tài)生命周期,找不到 this.chart 這個(gè)實(shí)例股淡,因此這能選這個(gè)生命周期執(zhí)行ECharts的更新

總結(jié)

最后的最佳實(shí)踐是經(jīng)過前幾次的失敗以后嘗試出來的身隐,當(dāng)時(shí)真的很氣,ECharts各種不按照自己的預(yù)計(jì)進(jìn)行更新唯灵,當(dāng)然事后分析了行為贾铝,發(fā)現(xiàn)了子組件還保持著原來數(shù)據(jù)的引用導(dǎo)致失敗的。并且一直發(fā)現(xiàn)子組件的生命周期沒有更新埠帕,后來仔細(xì)發(fā)現(xiàn)垢揩,要想是的子組件數(shù)據(jù)發(fā)生變化執(zhí)行變化相關(guān)的鉤子,一定得父組件使用 setState 方法敛瓷, 直接更改 state 是沒有效果的叁巨,這一點(diǎn)又回到 React 數(shù)據(jù)驅(qū)動(dòng)的本質(zhì)。在嘗試將 diff 部分也通過 state 傳遞呐籽, 通過 setState 更新以后再嘗試的 mobx 的改造俘种。mobx的本質(zhì)就是將 setState 部分改為了 mobx 裝飾過后的數(shù)據(jù)通過代理驅(qū)動(dòng)。最后取得了成功

當(dāng)然之所以一開始就采取直接傳遞 option 的方法绝淡,來自于 vue 的使用經(jīng)驗(yàn)宙刘,具體參考Vue下使用ECharts,直接通過父組件傳遞 option 選項(xiàng)牢酵,因?yàn)?vue 有依賴收集悬包,因此直接在子組件的 updated 周期更新 ECharts 就行了。不得不說 Vue真香

源碼

Echarts基類
父組件
mobx裝飾的Store

以上都是我瞎編的馍乙,如果你喜歡我一本正經(jīng)的胡說八道布近,歡迎star我的 Github

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市丝格,隨后出現(xiàn)的幾起案子撑瞧,更是在濱河造成了極大的恐慌,老刑警劉巖显蝌,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件预伺,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡曼尊,警方通過查閱死者的電腦和手機(jī)酬诀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來骆撇,“玉大人瞒御,你說我怎么就攤上這事∩窠迹” “怎么了肴裙?”我有些...
    開封第一講書人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵趾唱,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我蜻懦,道長(zhǎng)鲸匿,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任阻肩,我火速辦了婚禮,結(jié)果婚禮上运授,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好啸罢,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開白布履磨。 她就那樣靜靜地躺著,像睡著了一般逗宜。 火紅的嫁衣襯著肌膚如雪雄右。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評(píng)論 1 291
  • 那天纺讲,我揣著相機(jī)與錄音擂仍,去河邊找鬼。 笑死熬甚,一個(gè)胖子當(dāng)著我的面吹牛逢渔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播乡括,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼肃廓,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了诲泌?” 一聲冷哼從身側(cè)響起盲赊,我...
    開封第一講書人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎敷扫,沒想到半個(gè)月后哀蘑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡葵第,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年递礼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片羹幸。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡脊髓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出栅受,到底是詐尸還是另有隱情将硝,我是刑警寧澤恭朗,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站依疼,受9級(jí)特大地震影響痰腮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜律罢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一膀值、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧误辑,春花似錦沧踏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至砰苍,卻和暖如春潦匈,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背赚导。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工茬缩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吼旧。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓寒屯,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親黍少。 傳聞我的和親對(duì)象是個(gè)殘疾皇子寡夹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350

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