React Hooks 實用

復雜狀態(tài)處理: 如何保持狀態(tài)一致性

1: 保證狀態(tài)最小化

在保證 State 完整性的同時倒信,也要保證它的最小化: 某些數(shù)據(jù)如果能從已有的 State 中計算得到, 那么我們應該始終在用的時候去計算, 而不是把計算的結(jié)果存到某個 State 中, 這樣, 才能簡化我們的狀態(tài)處理邏輯

function FilterList({ data }) {
  // 設置關鍵字的 State
  const [searchKey, setSearchKey] = useState('');
  // 設置最終要展示的數(shù)據(jù)狀態(tài),并用原始數(shù)據(jù)作為初始值
  const [filtered, setFiltered] = useState(data);

  // 處理用戶的搜索關鍵字
  const handleSearch = useCallback(evt => {
    setSearchKey(evt.target.value);
    setFiltered(data.filter(item => {
      return item.title.includes(evt.target.value)));
    }));
  }, [filtered])
  return (
    <div>
      <input value={searchKey} onChange={handleSearch} />
      {/* 根據(jù) filtered 數(shù)據(jù)渲染 UI */}
    </div>
  );
}

// 一致性, 根據(jù) data 關鍵字, 來緩存 filter 的值
function FilterList({ data }) {
  const [searchKey, setSearchKey] = useState("");

  // 每當 searchKey 或者 data 變化的時候拂到,重新計算最終結(jié)果
  const filtered = useMemo(() => {
    return data.filter((item) =>
      item.title.toLowerCase().includes(searchKey.toLowerCase())
    );
  }, [searchKey, data]);

  return (
    <div className="08-filter-list">
      <h2>Movies</h2>
      <input
        value={searchKey}
        placeholder="Search..."
        onChange={(evt) => setSearchKey(evt.target.value)}
      />
      <ul style={{ marginTop: 20 }}>
        {filtered.map((item) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}

2. 避免中間狀態(tài), 確保唯一數(shù)據(jù)源

在有的場景下墅垮,特別是原始狀態(tài)數(shù)據(jù)來自某個外部數(shù)據(jù)源壶辜,而非 state 或者 props 的時候催植,冗余狀態(tài)就沒那么明顯窖贤。這時候你就需要準確定位狀態(tài)的數(shù)據(jù)源究竟是什么,并且在開發(fā)中確保它始終是唯一的數(shù)據(jù)源草雕,以此避免定義中間狀態(tài)

異步處理: 如何向服務器發(fā)送請求

  1. 實現(xiàn)自己的 API Client

無論大小項目巷屿,在開始實現(xiàn)第一個請求的時候,通常我們要做的第一件事應該都是創(chuàng)建一個自己的 API Client墩虹,之后所有的請求都會通過這個 Client 發(fā)出去嘱巾。而不是上來就用 fetch 或者是 axios 去直接發(fā)起請求憨琳,因為那會造成大量的重復代碼

可以對你需要連接的服務端做一些通用的配置和處理,比如 Token旬昭、URL篙螟、錯誤處理等等

  • 通用的 Header, 比如: Authorization Token
  • 服務器地址的配置
  • 請求未認證, 錯誤處理等
import axios from "axios"

// 定義相關的 endpoint
const endPoints = {
  test: "https://api.io/",
  prod: "https://prod.myapi.io/",
  staging: "https://staging.myapi.io/",
}

// 創(chuàng)建 axios 的實例
const instance = axios.create({
  // 實際項目中根據(jù)當前環(huán)境設置 baseURL
  baseURL: endPoints.test,
  timeout: 30000,
  // 為所有請求設置通用的 header
  headers: { Authorization: "Bear mytoken" },
})

// 聽過 axios 定義攔截器預處理所有請求
instance.interceptors.response.use(
  (res) => {
    // 可以假如請求成功的邏輯,比如 log
    return res
  },
  (err) => {
    if (err.response.status === 403) {
      // 統(tǒng)一處理未授權(quán)請求问拘,跳轉(zhuǎn)到登錄界面
      document.location = "/login"
    }
    return Promise.reject(err)
  }
)

export default instance
  1. 使用 Hooks 思考異步請求, 封裝遠程資源
  • Data: 請求成功后的數(shù)據(jù)
  • Error: 請求失敗, 錯誤信息
  • Pending: loading

上面三個狀態(tài), 我們可以在 UI 上做一些處理, 寫一個 Hook

import { useState, useEffect } from "react"
import apiClient from "./apiClient"

// 將獲取文章的 API 封裝成一個遠程資源 Hook
const useArticle = (id) => {
  // 設置三個狀態(tài)分別存儲 data, error, loading
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(null)
  useEffect(() => {
    // 重新獲取數(shù)據(jù)時重置三個狀態(tài)
    setLoading(true)
    setData(null)
    setError(null)
    apiClient
      .get(`/posts/${id}`)
      .then((res) => {
        // 請求成功時設置返回數(shù)據(jù)到狀態(tài)
        setLoading(false)
        setData(res.data)
      })
      .catch((err) => {
        // 請求失敗時設置錯誤狀態(tài)
        setLoading(false)
        setError(err)
      })
  }, [id]) // 當 id 變化時重新獲取數(shù)據(jù)

  // 將三個狀態(tài)作為 Hook 的返回值
  return {
    loading,
    error,
    data,
  }
}

多個 API 調(diào)用, 如何處理并發(fā)或串行請求?

例如: 需要顯示作者遍略、作者頭像,以及文章的評論列表, 需要發(fā)送三個請求 GetAvatar GetAuthor GetComments

Promise.all([fetch1, fetch2]) 傳統(tǒng)思路, React 函數(shù)組件是一個同步的函數(shù), 沒法直接使用 await, 而是要把請求通過副作用去觸發(fā)

從狀態(tài)變化的角度去組織異步調(diào)用, 通過不同的狀態(tài)組合骤坐,來實現(xiàn)異步請求的邏輯

import { useState, useEffect } from "react"
import apiClient from "./apiClient"

export default (id) => {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(null)
  useEffect(() => {
    // 當 id 不存在绪杏,直接返回,不發(fā)送請求
    if (!id) return
    setLoading(true)
    setData(null)
    setError(null)
    apiClient
      .get(`/users/${id}`)
      .then((res) => {
        setLoading(false)
        setData(res.data)
      })
      .catch((err) => {
        setLoading(false)
        setError(err)
      })
  }, [id])
  return {
    loading,
    error,
    data,
  }
}

import useArticle from "./useArticle"
import useUser from "./useUser"
import useComments from "./useComments"

const ArticleView = ({ id }) => {
  // article comments 并行
  const { data: article, loading, error } = useArticle(id)
  const { data: comments } = useComments(id)
  // 串行的請求
  const { data: user } = useUser(article?.userId)
  if (error) return "Failed."
  if (!article || loading) return "Loading..."
  return (
    <div className='exp-09-article-view'>
      <h1>
        {id}. {article.title}
      </h1>
      {user && (
        <div className='user-info'>
          <img src={user.avatar} height='40px' alt='user' />
          <div>{user.name}</div>
          <div>{article.createdAt}</div>
        </div>
      )}
      <p>{article.content}</p>
      <CommentList data={comments || []} />
    </div>
  )
}

函數(shù)組件設計模式:如何應對復雜條件渲染場景纽绍?

  1. 容器模式: 實現(xiàn)按條件執(zhí)行 Hooks

Hooks 必須在頂層作用域調(diào)用寞忿,而不能放在條件判斷、循環(huán)等語句中顶岸,同時也不能在可能的 return 語句之后執(zhí)行腔彰。換句話說,Hooks 必須按順序被執(zhí)行到辖佣。

但假如我們希望實現(xiàn)一下 Modal, 像下面代碼會報錯

import { Modal } from "antd"
import useUser from "useUser"

function UserInfoModal({ visible, userId, ...rest }) {
  // 當 visible 為 false 時霹抛,不渲染任何內(nèi)容
  if (!visible) return null
  // 這一行 Hook 在可能的 return 之后,會報錯卷谈!
  const { data, loading, error } = useUser(userId)

  return (
    <Modal visible={visible} {...rest}>
      {/* 對話框的內(nèi)容 */}
    </Modal>
  )
}

我們可以使用容器模式: 把條件判斷的結(jié)果放到兩個組件之中杯拐,確保真正 render UI 的組件收到的所有屬性都是有值的

// 定義一個容器組件用于封裝真正的 UserInfoModal
export default function UserInfoModalWrapper({
  visible,
  ...rest // 使用 rest 獲取除了 visible 之外的屬性
}) {
  // 如果對話框不顯示,則不 render 任何內(nèi)容
  if (!visible) return null
  // 否則真正執(zhí)行對話框的組件邏輯
  return <UserInfoModal visible {...rest} />
}

把判斷條件放到 Hooks 中去

const ArticleView = ({ id }) => {
  const { data: article, loading } = useArticle(id)
  let user = null
  // Hook 不能放到條件語句中世蔗,那我們應該如何做呢
  if (article?.userId) user = useUser(article?.userId).data
  // 組件其它邏輯
}

function useUser(id) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(null)
  useEffect(() => {
    // 當 id 不存在端逼,直接返回,不發(fā)送請求
    if (!id) return
    // 獲取用戶信息的邏輯
  })
}
  1. render props 模式重用 UI 邏輯

render props 就是把一個 render 函數(shù)作為屬性傳遞給某個組件污淋,由這個組件去執(zhí)行這個函數(shù)從而 render 實際的內(nèi)容顶滩。

在 Class 組件時期,render props 和 HOC(高階組件)兩種模式可以說是進行邏輯重用的兩把利器寸爆,但是實際上礁鲁,HOC 的所有場景幾乎都可以用 render props 來實現(xiàn)×薅梗可以說仅醇,Hooks 是邏輯重用的第一選擇。

舉例演示: 計數(shù)器, 演示純數(shù)據(jù)邏輯的重用, 就是重用的業(yè)務邏輯自己不產(chǎn)生任何 UI

import { useState, useCallback } from "react"

//  把計數(shù)邏輯封裝到一個自己不 render 任何 UI 的組件中
function CounterRenderProps({ children }) {
  const [count, setCount] = useState(0)
  const increment = useCallback(() => {
    setCount(count + 1)
  }, [count])
  const decrement = useCallback(() => {
    setCount(count - 1)
  }, [count])

  return children({ count, increment, decrement })
}

function CounterRenderPropsExample() {
  return (
    // children 這個特殊屬性魔种。也就是組件開始 tag 和結(jié)束 tag 之間的內(nèi)容析二,其實是會作為 children 屬性傳遞給組件
    <CounterRenderProps>
      {({ count, increment, decrement }) => {
        return (
          <div>
            <button onClick={decrement}>-</button>
            <span>{count}</span>
            <button onClick={increment}>+</button>
          </div>
        )
      }}
    </CounterRenderProps>
  )
}

在上面這種場景下, Hooks 更方便

import { useState, useCallback } from "react"

function useCounter() {
  // 定義 count 這個 state 用于保存當前數(shù)值
  const [count, setCount] = useState(0)
  // 實現(xiàn)加 1 的操作
  const increment = useCallback(() => setCount(count + 1), [count])
  // 實現(xiàn)減 1 的操作
  const decrement = useCallback(() => setCount(count - 1), [count])

  // 將業(yè)務邏輯的操作 export 出去供調(diào)用者使用
  return { count, increment, decrement }
}

事件處理: 如何創(chuàng)建自定義事件

  1. React 中使用原生事件: 約定使用駱駝體 (onMouseOver, onChange) 等
  2. React 原生事件的原理: 合成事件 由于虛擬 DOM 的存在, 在 React 綁定一個事件到原生的 DOM 節(jié)點, 事件也不會綁定在對應的節(jié)點上, 而是所有的事件都綁定在根節(jié)點上. 然后由 React 統(tǒng)一監(jiān)聽和管理, 代理模式, 分發(fā)到具體的虛擬 DOM 上
  3. React17 版本前: 綁定在 document, 之后, 綁定在整個 App 上的根節(jié)點上
    • 虛擬 DOM render 的時候, DOM 可能還沒有真實的 render 到頁面上, 所以無法綁定事件
    • React 屏蔽底層事件的細節(jié), 避免瀏覽器兼容問題

創(chuàng)建自定義事件

  • 原生事件是瀏覽器機制
  • 自定義事件是組件自己的行為, 本質(zhì)是一種回調(diào)機制
    • 通過 props 給組件傳遞一個回調(diào)函數(shù),然后在組件中的某個時機节预,比如用戶輸入叶摄,或者某個請求完成時漆改,去調(diào)用這個傳過來的回調(diào)函數(shù)就可以了
    • 習慣上以 onSomething 命名
import { useState } from "react"

// 創(chuàng)建一個無狀態(tài)的受控組件
function ToggleButton({ value, onChange }) {
  const handleClick = () => {
    onChange(!value)
  }
  return (
    <button style={{ width: "60px" }} onClick={handleClick}>
      按鈕
    </button>
  )
}

Hooks 封裝鍵盤事件

import { useEffect, useState } from "react"

// 使用 document.body 作為默認的監(jiān)聽節(jié)點
const useKeyPress = (domNode = document.body) => {
  const [key, setKey] = useState(null)
  useEffect(() => {
    const handleKeyPress = (evt) => {
      setKey(evt.keyCode)
    }
    // 監(jiān)聽按鍵事件
    domNode.addEventListener("keypress", handleKeyPress)
    return () => {
      // 接觸監(jiān)聽按鍵事件
      domNode.removeEventListener("keypress", handleKeyPress)
    }
  }, [domNode])
  return key
}

Form: Hooks 給 Form 處理帶來的那些新變化

  1. 受控組件 和 非受控組件
function MyForm() {
  const [value, setValue] = useState("")
  const handleChange = useCallback((evt) => {
    setValue(evt.target.value)
  }, [])
  // React 統(tǒng)一了表單組件的 onChange 事件
  return <input value={value} onChange={handleChange} />
}
// 非受控組件 表單元素的值不是由父組件決定的,而是完全內(nèi)部的狀態(tài)
import { useRef } from "react"

export default function MyForm() {
  // 定義一個 ref 用于保存 input 節(jié)點的引用
  const inputRef = useRef()
  const handleSubmit = (evt) => {
    evt.preventDefault()
    // 使用的時候直接從 input 節(jié)點獲取值
    alert("Name: " + inputRef.current.value)
  }
  return (
    <form onSubmit={handleSubmit}>
      <label>
        Name:
        <input type='text' ref={inputRef} />
      </label>
      <input type='submit' value='Submit' />
    </form>
  )
}
  1. 使用 Hooks 簡化表單處理

我們對每一個表單元素, 都是遵循這樣處理

  • 設置一個 state 綁定 value
  • 監(jiān)聽表單元素的 onChange 事件, 同步 value 到 state

維護表單組件的狀態(tài)邏輯: 核心

  • 字段的名字
  • 綁定 value 值
  • 處理 onChange 事件
import { useState, useCallback } from "react"

const useForm = (initialValues = {}) => {
  // 設置整個 form 的狀態(tài):values
  const [values, setValues] = useState(initialValues)

  // 提供一個方法用于設置 form 上的某個字段的值
  const setFieldValue = useCallback((name, value) => {
    setValues((values) => ({
      ...values,
      [name]: value,
    }))
  }, [])

  // 返回整個 form 的值以及設置值的方法
  return { values, setFieldValue }
}

;<input
  value={values.email || null}
  onChange={(evt) => setFieldValue("email", evt.target.value)}
/>
  1. 處理表單驗證
  • 如何定義這樣的錯誤狀態(tài)
  • 如何設置這個錯誤狀態(tài)
// 除了初始值之外,還提供了一個 validators 對象,
// 用于提供針對某個字段的驗證函數(shù)
const useForm = (initialValues = {}, validators) => {
  const [values, setValues] = useState(initialValues)
  // 定義了 errors 狀態(tài)
  const [errors, setErrors] = useState({})

  const setFieldValue = useCallback(
    (name, value) => {
      setValues((values) => ({
        ...values,
        [name]: value,
      }))

      // 如果存在驗證函數(shù)藤为,則調(diào)用驗證用戶輸入
      if (validators[name]) {
        const errMsg = validators[name](value)
        setErrors((errors) => ({
          ...errors,
          // 如果返回錯誤信息,則將其設置到 errors 狀態(tài)樊破,否則清空錯誤狀態(tài)
          [name]: errMsg || null,
        }))
      }
    },
    [validators]
  )
  // 將 errors 狀態(tài)也返回給調(diào)用者
  return { values, errors, setFieldValue }
}
function MyForm() {
  // 用 useMemo 緩存 validators 對象
  const validators = useMemo(() => {
    return {
      name: (value) => {
        // 要求 name 的長度不得小于 2
        if (value.length < 2) return "Name length should be no less than 2."
        return null
      },
      email: (value) => {
        // 簡單的實現(xiàn)一個 email 驗證邏輯:必須包含 @ 符號。
        if (!value.includes("@")) return "Invalid email address"
        return null
      },
    }
  }, [])
  // 從 useForm 的返回值獲取 errors 狀態(tài)
  const { values, errors, setFieldValue } = useForm({}, validators)
  // UI 渲染邏輯...
}

路由管理

路由管理唆铐,就是讓你的頁面能夠根據(jù) URL 的變化進行頁面的切換哲戚,這是前端應用中一個非常重要的機制

URL 的全稱是 Uniform Resource Locator,中文意思是“統(tǒng)一資源定位符”艾岂,表明 URL 是用于唯一的定位某個資源的

  1. 路由工作原理

在前端路由管理中顺少,則一般只在主內(nèi)容區(qū)域 Content 部分變化, Header 和 Sider 是不會變化的王浴。

實現(xiàn)路由機制的核心邏輯就是根據(jù) URL 路徑這個狀態(tài)脆炎,來決定在主內(nèi)容區(qū)域顯示什么組件, 示意代碼

const MyRouter = ({ children }) => {
  const routes = _.keyBy(
    children.map((c) => c.props),
    "path"
  )
  const [hash] = useHash()
  // 通過 URL 中的 hash,也就是“#”后面的部分來決定具體渲染哪個組件到主區(qū)域
  const Page = routes[hash.replace("#", "")]?.component
  // 如果路由不存在就返回 Not found.
  return Page ? <Page /> : "Not found."
}

// 定義了一個空組件 Route氓辣,來接收路由的具體參數(shù) path 和 component秒裕,從而以聲明式的方式去定義路由
const Route = () => null
function SamplePages {
  return (
    <div className="sample-pages">
      {/* 定義了側(cè)邊導航欄 */}
      <div className="sider">
        <a href="#page1">Page 1</a>
        <a href="#page2">Page 2</a>
        <a href="#page3">Page 3</a>
        <a href="#page4">Page 4</a>
      </div>
      <div className="exp-15-page-container">
        {/* 定義路由配置 */}
        <MyRouter>
          <Route path="page1" component={Page1} />
          <Route path="page2" component={Page2} />
          <Route path="page3" component={Page3} />
          <Route path="page4" component={Page4} />
        </MyRouter>
      </div>
    </>
  );
};

按需加載

import 語句,定義按需加載的起始模塊

按需加載钞啸,就是指在某個組件需要被渲染到頁面時几蜻,才會去實際地下載這個頁面,以及這個頁面依賴的所有代碼

// return promise
import(someModule)

// 演示使用 import 語句
function ProfilePage() {
  // 定義一個 state 用于存放需要加載的組件
  const [RealPage, setRealPage] = useState(null)

  // 根據(jù)路徑動態(tài)加載真正的組件實現(xiàn)
  import("./RealProfilePage").then((comp) => {
    setRealPage(Comp)
  })
  // 如果組件未加載則顯示 Loading 狀態(tài)
  if (!RealPage) return "Loading...."

  // 組件加載成功后則將其渲染到界面
  return <RealPage />
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末体斩,一起剝皮案震驚了整個濱河市梭稚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌絮吵,老刑警劉巖弧烤,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異源武,居然都是意外死亡扼褪,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進店門粱栖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人脏毯,你說我怎么就攤上這事闹究。” “怎么了食店?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵渣淤,是天一觀的道長赏寇。 經(jīng)常有香客問我,道長价认,這世上最難降的妖魔是什么嗅定? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮用踩,結(jié)果婚禮上渠退,老公的妹妹穿的比我還像新娘。我一直安慰自己脐彩,他們只是感情好碎乃,可當我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著惠奸,像睡著了一般梅誓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上佛南,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天梗掰,我揣著相機與錄音,去河邊找鬼嗅回。 笑死愧怜,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的妈拌。 我是一名探鬼主播拥坛,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼尘分!你這毒婦竟也來了猜惋?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤培愁,失蹤者是張志新(化名)和其女友劉穎著摔,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體定续,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡谍咆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了私股。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片摹察。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖倡鲸,靈堂內(nèi)的尸體忽然破棺而出供嚎,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布克滴,位于F島的核電站逼争,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏劝赔。R本人自食惡果不足惜誓焦,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望着帽。 院中可真熱鬧杂伟,春花似錦、人聲如沸启摄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽歉备。三九已至傅是,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蕾羊,已是汗流浹背喧笔。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留龟再,地道東北人书闸。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像利凑,于是被迫代替她去往敵國和親浆劲。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,947評論 2 355

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