React一基础

代码下载

React 概述

React 是一个用于构建用户界面的javaScript库,起源于facebook的内部项目,后续在13年开源了出来。

特点:

  • 声明式——只需要描述UI看起来是什么样式,就跟写HTML一样,React负责渲染UI
  • 基于组件——组件时React最重要的内容,组件表示页面中的部分内容,组合、复用多个组件,可以实现完整的页面功能
  • 学习一次,随处使用——使用React可以开发Web应用,使用React可以开发移动端,可以开发VR应用

React 基本使用

1、安装 React,在终端执行命令 npm i react react-dom

  • react 包是核心,提供创建元素、组件等功能
  • react-dom 包提供 DOM 相关功能等

2、引入 react 和 react-dom 两个 js 文件:

    <script src="../node_modules/react/umd/react.development.js"></script>
    <script src="../node_modules/react-dom/umd/react-dom.development.js"></script>

3、创建 React 元素并渲染 React 元素到页面中:

<body>
    <div id="root"></div>
</body>

<script>
    const title = React.createElement('h2', null, 'React 基本使用')
    ReactDOM.createRoot(document.getElementById('root')).render(title)
</script>

React.createElement 方法说明:

  • 返回值:React元素
  • 第一个参数:要创建的React元素名称
  • 第二个参数:该React元素的属性
  • 第三个及其以后的参数:该React元素的子节点
const title = React.createElement('h2', null, 'hello, world!', React.createElement('a', { href: 'https://www.baidu.cn/' }, '百度'))

ReactDOM.createRoot(dom).render(el) 方法说明:要渲染的React元素渲染到页面中的指定位置。

React 脚手架

React脚手架意义:

  • 脚手架是开发现代Web应用的必备
  • 充分利用 Webpack,Babel,ESLint等工具辅助项目开发
  • 零配置,无需手动配置繁琐的工具即可使用
  • 关注业务,而不是工具配置

React 脚手架初始化项目:
1、初始化项目,命令:npx create-react-app my-app

npx 命令是 npm v5.2.0 引入的一条命令:

  • 目的:提升包内提供的命令行工具的使用体验
  • 原来:先安装脚手架包,再使用这个包中提供的命令
  • 现在:无需安装脚手架包,就可以直接使用这个包提供的命令

2、 启动项目,在项目根目录执行命令:npm start

补充

脚手架创建项目的方式:
1、推荐使用:npx create-react-app my-app

2、npm init react-app my-app

3、如果安装了 yarn,可以使用 yarn create react-app my-app

  • yarn 是 Facebook 发布的包管理器,可以看做是 npm 的替代品,功能与 npm 相同
  • yarn 具有快速、可靠和安全的特点
  • 初始化新项目:yarn init
  • 安装包: yarn add 包名称
  • 安装项目依赖项: yarn
  • 其他命令,请参考yarn文档

在脚手架中使用 React

1、导入 react 和 react-dom 两个包:

import React from 'react'
import ReactDOM from 'react-dom/client'

2、调用 React.createElement() 方法创建 react 元素,并使用 ReactDOM 渲染到页面中:

const title = React.createElement('h2', null, 'hello, world!')
ReactDOM.createRoot(document.getElementById('root')).render(title)

JSX

通过createElement()方法创建的React元素有一些问题,代码比较繁琐,结构不直观,无法一眼看出描述的结构,不优雅。

JSX是JavaScript XML 的简写,表示在JavaScript代码中写HTML格式的代码,JSX优势就是声明式语法更加直观,与HTML结构相同,降低了学习成本,提升开发效率。

使用 JSX 语法创建 react 元素,并使用 ReactDOM 渲染到页面中:

const hiJSX = <h2>hello JSX!</h2>
ReactDOM.createRoot(document.getElementById('hiJSX')).render(hiJSX)

思考,为什么在脚手架中可以使用JSX语法?

  • JSX 不是标准的ECMAScript语法,它是ECMAScript的语法拓展
  • 需要使用babel编译处理后,才能在浏览器环境中使用
  • create-react-app脚手架中已经默认有该配置,无需手动配置
  • 编译JSX语法的包: @bable/preset-react

使用 JSX 注意点:

  1. React元素的属性名使用驼峰命名法。
  2. 特殊属性名:class -> className、for -> htmlFor、tabindex -> tabIndex 。
  3. 没有子节点的React元素可以用 /> 结束 。
  4. 推荐:使用小括号包裹 JSX ,从而避免 JS 中的自动插入分号陷阱。
const note = (
  <div className='note' />
)
ReactDOM.createRoot(document.getElementById('note')).render(note)

JSX 中使用 JavaScript 表达式

JSX 嵌入 JS 表达式,语法是 { JavaScript表达式 },注意事项如下:

  • 单大括号中可以使用任意的 JavaScript 表达式
  • JSX 自身也是 JS 表达式
  • JS 中的对象是一个例外,一般只会出现在 style 属性中
  • 不能在 {} 中出现语句(比如:if/for 等)
const sayHi = () => {
  return 'hello, JSX'
}
const expression = (
  <div>
    <p>{1}</p>
    <p>{'a'}</p>
    <p>{1 + 7}</p>
    <p>1 大于 2 吗?{1 > 2 ? '大于' : '小于或等于'}</p>
    <p>{sayHi()}</p>
    {/* 错误示例 */}
    {/* <p>{{ a: 'a' }}</p> */}
    {/* <p>{if (true) {}}</p> */}
    {/* <p>{for (var i = 0; i < 8; i++) {}}</p> */}
  </div>
)
ReactDOM.createRoot(document.getElementById('expression')).render(expression)

JSX 条件渲染

根据条件渲染特定的 JSX 结构,可以使用if/else或三元运算符或逻辑与运算符来实现:

var flag = true
// if-else
const load = () => {
  if (flag) {
    return <p>loading……</p>
  } else {
    return <p>加载完成</p>
  }
}
// 三元表达式
const load1 = () => {
  return flag ? <p>loading……</p> : <p>加载完成</p>
}
// 逻辑与运算符
const load2 = () => {
  return flag && <p>loading……</p>
}
const condition = (
  <div>
    if-else 条件语句:
    {load()}
    三元表达式:
    {load1()}
    逻辑与运算符:
    {load2()}
  </div>
)
ReactDOM.createRoot(document.getElementById('condition')).render(condition)

JSX 列表渲染

如果要渲染一组数据,应该使用数组的 map() 方法:

  • 渲染列表时应该添加 key 属性,key 属性的值要保证唯一
  • map() 遍历谁,就给谁添加 key 属性
  • 尽量避免使用索引号作为 key
const data = [
  { id: 0, name: 'one' }, 
  { id: 1, name: 'two' }, 
  { id: 2, name: 'three' }, 
  { id: 3, name: 'four' }
]
const ul = (
  <ul>
    {data.map(item => <li key={item.id}>{item.name}</li>)}
  </ul>
)
ReactDOM.createRoot(document.getElementById('list')).render(ul)

JSX 的样式处理

1、行内样式 —— style,在style里面通过对象的方式传递数据,这种方式比较的麻烦,不方便进行阅读,而且还会导致代码比较的繁琐:

const lineStyle = (
  <div style={{ backgroundColor: 'red', width: '100px', height: '100px'}}></div>
)
ReactDOM.createRoot(document.getElementById('lineStyle')).render(lineStyle)

2、类名 —— className(推荐),先创建CSS文件编写样式代码,然后在js中进行引入并设置类名即可:

/* css */
.green {
  background-color: green;
  width: 100px;
  height: 100px;
}

// js
import './index.css'
const className = (
  <div className='green'></div>
)
ReactDOM.createRoot(document.getElementById('className')).render(className)

总结:React完全利用JS语言自身的能力来编写UI,而不是造轮子增强HTML功能。

React 组件基础

组件是React的一等公民,使用React就是在用组件。组件表示页面中的部分功能,组合多个组件实现完整的页面功能。特点是可复用、独立、可组合。

React 组件的两种创建方式

1、使用函数创建组件,使用 JS 的函数(或箭头函数)创建的组件

  • 函数名称必须以大写字母开头
  • 函数组件必须有返回值,表示该组件的结构
  • 如果返回值为 null,表示不渲染任何内容
// 渲染结构的函数组件
function FuncComOne() {
  return (
    <div>渲染结构的函数组件</div>
  )
}
ReactDOM.createRoot(document.getElementById('funcOne')).render(<FuncComOne></FuncComOne>)

// 不渲染结构的函数组件
function FuncComTwo() {
  return null
}
ReactDOM.createRoot(document.getElementById('funcTwo')).render(<FuncComTwo></FuncComTwo>)

// 使用箭头函数
const FuncComThree = () => <div>使用箭头函数创建渲染结构的函数组件</div>
ReactDOM.createRoot(document.getElementById('funcThree')).render(<FuncComThree></FuncComThree>)

渲染函数组件用函数名作为组件标签名,组件标签可以是单标签也可以是双标签。

说明:

  • 使用JS中的函数创建的组件叫做函数组件
  • 函数组件必须有返回值
  • 组件名称必须以大写字母开头, React 据此区分 组件 和 普通的React 元素
  • 使用函数名作为组件标签名

2、使用类创建组件就是使用 ES6 的 class 创建的组件

  • 类名称也必须以大写字母开头
  • 类组件应该继承 React.Component 父类,从而可以使用父类中提供的方法或属性
  • 类组件必须提供 render() 方法
  • render() 方法必须有返回值,表示该组件的结构
// 使用类创建组件
class ClassCom extends React.Component {
  render() {
    return (
      <div>渲染结构的类组件</div>
    )
  }
}
ReactDOM.createRoot(document.getElementById('classCom')).render(<ClassCom></ClassCom>)

抽离为独立 JS 文件

组件作为一个独立的个体,一般都会放到一个单独的 JS 文件中。

1、创建组件文件 SingleCom.js,在 SingleCom.js 中导入React创建组件(函数 或 类)并导出该组件:

import React from "react";

class SingleCom extends React.Component {
    render() {
        return (
            <div>把组件抽离为独立 JS 文件</div>
        )
    }
}

export default SingleCom

2、在 index.js 中导入 Hello 组件并渲染组件:

import SingleCom from './SingleCom'
ReactDOM.createRoot(document.getElementById('SingleCom')).render(<SingleCom></SingleCom>)

React 事件处理

React 事件绑定语法与 DOM 事件语法相似,语法是 on+事件名称={事件处理程序},比如:onClick={() => {}}

注意:React 事件采用驼峰命名法,比如:onMouseEnter、onFocus

事件对象,可以通过事件处理程序的参数获取到事件对象。React 中的事件对象叫做:合成事件(对象),兼容所有浏览器,无需担心跨浏览器兼容性问题。

// 函数组件事件处理
function FuncComEvent() {
  function clickHandle(e) {
    e.preventDefault()
    console.log('点我干嘛');
  }
  return (
    <a href='https://www.baidu.cn/' onClick={clickHandle}>点我</a>
  )
}
ReactDOM.createRoot(document.getElementById('funcComEvent')).render(<FuncComEvent></FuncComEvent>)

// 类组件事件处理
class ClassComEvent extends React.Component {
  clickHandle(e) {
    e.preventDefault()
    console.log('点我干嘛');
  }
  render() {
    return (
      <a href='https://www.baidu.cn/' onClick={this.clickHandle}>点我</a>
    )
  }
}
ReactDOM.createRoot(document.getElementById('classComEvent')).render(<ClassComEvent></ClassComEvent>)

有状态组件和无状态组件

函数组件又叫做无状态组件,类组件又叫做有状态组件,状态(state)即数据:

  • 函数组件没有自己的状态,只负责数据展示(静)
  • 类组件有自己的状态,负责更新 UI,让页面“动” 起来

状态(state)即数据,是组件内部的私有数据,只能在组件内部使用,state 的值是对象,表示一个组件中可以有多个数据:

  • 通过 this.state 来获取状态。
  • 状态是可变的,通过 this.setState({ 要修改的数据 }) 来修改状态,setState() 的作用是修改 state 并更新UI,此思想是数据驱动视图。

注意:不要直接修改 state 中的值,这是错误的!!!

JSX 中掺杂过多 JS 逻辑代码,会显得非常混乱。推荐将逻辑抽离到单独的方法中,保证 JSX 结构清晰。

注意:事件处理程序中 this 的值为 undefined。希望 this 指向组件实例(render方法中的this即为组件实例)。

class StateCom extends React.Component {
  // 构造方法
  // constructor() {
  //   super()

  //   this.state = {
  //     count: 0
  //   }
  // }

  // 简写,不使用构造方法
  state = {
    count: 0
  }
  // 事件处理程序
  onDecrease() {
    // this 为 undefined
    this.setState({ count: this.state.count -1 })
  }
  render() {
    // render 方法 this 为组件实例
    return (
      <div>
        <p>数据结果:{this.state.count}</p>
        <button onClick={() => {
          // 箭头函数 this 为定义时的 this 即为组件实例

          // 修改 state
          this.setState({ count: this.state.count + 1 })

          // 错误修改 state 的方法
          // this.state.count++
        }}>+1</button>

        {/* 从 JSX 中抽离事件处理程序 */}
        <button onClick={this.onDecrease}>-1</button>
      </div>
    )
  }
}
ReactDOM.createRoot(document.getElementById('stateCom')).render(<StateCom></StateCom>)

事件绑定 this 指向

1、利用箭头函数自身不绑定this的特点,render() 方法中的 this 为组件实例,可以获取到 setState()。

2、利用ES5中的bind方法,将事件处理程序中的this与组件实例绑定到一起。

3、利用箭头函数形式的class实例方法,该语法是实验性语法,但是由于babel的存在可以直接使用。

// 处理 事件中 this 指向
class EventThis extends React.Component {
  constructor() {
    super()

    this.state = {
      count: 0
    }

    // 2、事件绑定 this 指向
    this.onIncreaceTwo = this.onIncreaceTwo.bind(this)
  }
  onIncreaceOne() {
    this.setState({ count: this.state.count + 1 })
  }
  onIncreaceTwo() {
    this.setState({ count: this.state.count + 2 })
  }
  // 3、class 的实例方法
  onIncreaceThree = () => {
    this.setState({ count: this.state.count + 3 })
  }
  render() {
    return (
      <div>
        <p>结果:{this.state.count}</p>
        {/* 1、通过箭头函数 */}
        <button onClick={() => this.onIncreaceOne()}>+1</button>
        <button onClick={this.onIncreaceTwo}>+2</button>
        <button onClick={this.onIncreaceThree}>+3</button>
      </div>
    )
  }
}
ReactDOM.createRoot(document.getElementById('eventThis')).render(<EventThis></EventThis>)

推荐使用class的实例方法。

表单处理

受控组件

HTML 中的表单元素是可输入的,也就是有自己的可变状态,而 React 中可变状态通常保存在 state 中,并且只能通过 setState() 方法来修改。React 将 state 与表单元素值value绑定到一起,由 state 的值来控制表单元素的值。

受控组件就是其值受到 React 控制的表单元素,处理受控组件步骤:

  • 在 state 中添加一个状态,作为表单元素的value值(控制表单元素值的来源)
  • 给表单元素绑定 change 事件,将 表单元素的值 设置为 state 的值(控制表单元素值的变化)
// 受控组件
class ControlCom extends React.Component {
  constructor() {
    super()

    this.state = {
      title: '',
      content: '',
      city: 'bj',
      isChecked: 'true'
    }
  }

  // 处理 文本框、富文本框、下拉框、复选框 的变化
  titleChange = e => this.setState({ title: e.target.value })
  contentChange = e => this.setState({ content: e.target.value })
  cityChange = e => this.setState({ city: e.target.value })
  isCheckedChange = e => this.setState({ isChecked: e.target.checked })

  render() {
    return (
      <div>
        <input type='text' placeholder='请输入标题' value={this.state.title} onChange={this.titleChange}></input>
        <br/>
        <textarea placeholder='请输入内容' value={this.state.content} onChange={this.contentChange}></textarea>
        <br/>
        <select value={this.state.city} onChange={this.cityChange}>
          <option value='bj'>北京</option>
          <option value='sh'>上海</option>
          <option value='sz'>深圳</option>
          <option value='gz'>广州</option>
        </select>
        <br/>
        <input type='checkbox' checked={this.state.isChecked} onChange={this.isCheckedChange}></input>
      </div>
    )
  }
}
ReactDOM.createRoot(document.getElementById('controlCom')).render(<ControlCom></ControlCom>)

受控组件多表单元素优化

个表单元素都有一个单独的事件处理程序处理太繁琐,可以使用一个事件处理程序同时处理多个表单元素,步骤如下:

  • 给表单元素添加name属性,名称与 state 相同
  • 根据表单元素类型获取对应值
  • 在 change 事件处理程序中通过 [name] 来修改对应的state
// 受控组件
class ControlCom extends React.Component {
  constructor() {
    super()

    this.state = {
      title: '',
      content: '',
      city: 'bj',
      isChecked: true
    }
  }
  
  valueChage = e => {
    const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value
    this.setState({ [e.target.name]: value })
  }

  render() {
    return (
      <div>
        <p>优化受控组件</p>
        <input name='title' type='text' placeholder='请输入标题' value={this.state.title} onChange={this.valueChage}></input>
        <br/>
        <textarea name='content' placeholder='请输入内容' value={this.state.content} onChange={this.valueChage}></textarea>
        <br/>
        <select name='city' value={this.state.city} onChange={this.valueChage}>
          <option value='bj'>北京</option>
          <option value='sh'>上海</option>
          <option value='sz'>深圳</option>
          <option value='gz'>广州</option>
        </select>
        <br/>
        <input name='isChecked' type='checkbox' checked={this.state.isChecked} onChange={this.valueChage}></input>
      </div>
    )
  }
}
ReactDOM.createRoot(document.getElementById('controlCom')).render(<ControlCom></ControlCom>)

非受控组件

借助于 ref,使用原生 DOM 方式来获取表单元素值,ref 的作用就是获取 DOM 或组件。使用步骤

  • 调用 React.createRef() 方法创建一个 ref 对象
  • 将创建好的 ref 对象添加到文本框中
  • 通过 ref 对象获取到文本框的值
// 非受控组件
class UncontrolCom extends React.Component {
  constructor() {
    super()

    // 1、调用 React.createRef() 方法创建一个 ref 对象
    this.textRef = React.createRef()
  }
  render() {
    return (
      <div>
        {/* 2、将创建好的 ref 对象添加到文本框中 */}
        <input type='text' ref={this.textRef}></input>
        <button onClick={this.getText}>获取文本</button>
      </div>
    )
  }
  // 3、 通过 ref 对象获取到文本框的值
  getText= () => {
    console.log('text: ', this.textRef.current.value);
  }
}
ReactDOM.createRoot(document.getElementById('uncontrolCom')).render(<UncontrolCom></UncontrolCom>)

评论列表案例

class Comment extends React.Component {
  // 初始化状态
  state = {
    count: 10,
    comments: [
      { id: 1, name: 'jack', content: '沙发!!!' },
      { id: 2, name: 'rose', content: '板凳~' },
      { id: 3, name: 'tom', content: '楼主好人' }
    ],
    userName: '',
    userContent: ''
  }
  // 渲染评论列表:
  renderList() {
    const { comments } = this.state
    if (comments.length === 0) {
      return <div className='no-comment'>暂无评论,快去评论吧</div>
    }
    return comments.map(item => (
      <li key={item.id}>
        <h4>评论人:{item.name}</h4>
        <p>评论内容:{item.content}</p>
      </li>
    ))
  }
  // 处理表单元素
  formHandle = (e) => {
    const { name, value } = e.target
    this.setState({
      [name]: value
    })
  }
  // 添加评论
  addComment = () => {
    const { userName, userContent } = this.state
    if (userName.trim() === '' || userContent.trim() === '') {
      alert('请输入评论人和评论内容!')
      return
    }
    const newComments = [{
      id: Math.random(),
      name: userName,
      content: userContent
    }, ...this.state.comments]
    this.setState({ 
      comments: newComments,
      userName: '',
      userContent: ''
     })
  }
  render () {
    const { userName, userContent } = this.state
    return (
      <div className='comment'>
        <div>
          <input className='user' type='text' placeholder='请输入评论人' value={userName} name='userName' onChange={this.formHandle}></input>
          <br />
          <textarea className='content' cols='30' rows='10' placeholder='请输入评论内容' value={userContent} name='userContent' onChange={this.formHandle}></textarea>
          <br />
          <button onClick={this.addComment}>发表评论</button>
          {this.renderList()}
        </div>
      </div>
    )
  }
}
ReactDOM.createRoot(document.getElementById('comment')).render(<Comment></Comment>)

实现步骤:

1、渲染评论列表:

  • 在 state 中初始化评论列表数据
  • 使用数组的map方法遍历state中的列表数据
  • 给每个被遍历的li元素添加key属性
  1. 渲染暂无评论:
  • 判断列表数据的长度是否为0
  • 如果为0,则渲染暂无评论

3、获取评论信息:使用受控组件方式处理表单元素

4、发表评论:

  • 给按钮绑定单击事件
  • 在事件处理程序中,通过state获取评论信息
  • 将评论信息添加到state中,并调用 setState() 方法更新state
  • 边界情况:清空文本框
  • 边界情况:非空判断
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容