Vue 朝花夕拾(基础与组件)

# 在本文中,笔者又提炼了以下几个重点

  1. 补偿双向数据绑定 Vue.$set
  2. 数据侦听 Vue.$watch
  3. 表单绑定修饰符
  4. 动态组件
  5. 基础组件的自动化全局注册
  6. Vue.$emit参数,及与 v-on 事件命名规范
  7. Prop传递数据时防脏
  8. 插槽及高复用组件

# 补偿双向数据绑定 Vue.$set

  官网说受JS限制,笔者觉得讲的太浅了。相信了解双向数据绑定原理的朋友都知道,Vue 2是依赖原生JS中Object.defineProperty()方法的存取操作符set即数据劫持实现数据实时更新。然而对于一些引用类型的数据,如果操作不是发生在已经定义好的数据结构本身,Vue 无法探测普通的新增属性 (比如 this.myObject.newProperty = 'hi'),我们称它为不是响应式的。如:

(1) 对象属性的添加和删除时: 见案例
(2) 利用索引直接设置数组的一个项时: vm.items[2] = 'red'
(3) 修改数组长度时: vm.items.length = newLen

data () {
  return {
    user: { name: 'zfs', age: 25  }
  }
},
mounted: {
  this.user = { name: 'borui' }  // 改变对象本身,触发setter
  this.user.tall = 178  // 新增属性,未触发setter,视图不更新
}

  为了解决这个问题,尤大大重写了set方法,提供了$setAPI接口。注意不要写成vm.$set(key, value)形式,这种错误就略低级了。对比一下原生和API接口

原生方法:Object.defineProperty(object, 'key’, descriptor)
API 接口: vm.$set(vm.Obj, 'key', 'value')

  很容易发现,该接口原理将描述符descriptor设置为set, 将输入的新值value作为参数传递给set调用从而手动触发数据劫持。因此,上述案例需要改成:

mounted () {
  this.$set(this.student, "tall",  178)
}

  但是问题又来了,如果需要一次性增加多个新的响应式属性,显然多次调用$set方法不是个很好的选择。通常我们会使用Object.assign()_extend()来实现。Vue建议创建一个新的对象来存放两个合并对象的所有属性(通常用空对象{}),然后再赋值给目标元素。而不是直接合并到目标元素上。做法即:

vm.user = Object.assign({}, vm.user, {
  hobby: 'basketball',
  favoriteColor: 'Green'
})

# 数据侦听 Vue.$watch

watch提供了观察和响应实例上数据变动的办法,当有一些数据需要跟随其他数据变化而变化时,如子组件某个数据依赖来自于父组件的prop计算。很直观的会想到计算这功能和计算属性十分类似。Vue建议用户使用计算属性,除非如下情况:
(1)当要执行的操作是异步操作时,
(2)相应事件是开销较大的操作时。

watch: {
  question: function (newVal, oldVal) {
    this.answer = 'Waiting for you to stop typing...'
    axios.get('https://yesno.wtf/api')
        .then(function (response) {
          vm.answer = _.capitalize(response.data.answer)
        })
        .catch(function (error) {
          vm.answer = 'Error! Could not reach the API. ' + error
        })
    }
  }
}

  当观察的值发生改变时, 观察者会接收到两个参数:(1) 新值,(2)原先的值。 值得注意的是, watch在组件第一次被挂载时不会触发, 只有值被改变时才触发。

watch: {
  selectedVal ( newVal, oldVal ) {
    console.log(newVal)
  }
}

# data选项为什么是一个函数?

  Vue官网第一课描述的data选项就是一个对象,为什么在编写组件的时候却要定义成一个函数?

  我们知道对象是引用类型,而组件最大的特性就是可复用性,当一个组件被多次复用却指向同一个引用类型数据,组件间将无独立性而言。因此,将data选项定义成一个函数,是为了利用函数的私有作用域特性实现不同组件间数据私有的效果

# 计算属性缓存 及 get()/set()方法

  一个需要计算的数据,通常有: (1)计算属性获取,(2)定义一个方法实现。虽然实现结果相同,但前者优势在于计算属性是基于它们的依赖进行缓存的。也就是说:

(1)计算属性依赖不改变,计算就不会触发,改变了才重新触发计算;而调用方法总会再次执行函数
(2)当依赖不是响应式依赖时, 计算属性将永远不会触发计算。如

computed: {
  now () {
    return Date.now()
  }
}

  计算属性默认只有 getter,常规用法其实是调用了计算属性的getter方法。
什么情况下使用setter?一般计算属性都是根据依赖来计算自身的值,如果计算属性自身需要手动传入值时,就需要提供一个setter。例如:将一个计算属性绑定给v-model
  提供get()set()的计算属性, 需要调整为一个对象。

<template>
  <input v-model="name" />
</template>
computed: {
 reserve : {
    get () {
      return this. $store.state.name
    },
    set (val) {
      return this.$store.state.name = `李${val}`
    }
  }
}

# v-if 惰性、缓存 及 使用 <template>

  我们知道,v-if能决定DOM结构存不存在,而v-show只是控制了DOM元素的display属性,当页面切换频率不高时,Vue建议使用v-if
  所谓的惰性,就是当遇到条件为非真时直接跳过,只有第一次遇到真值才开始渲染条件块。
  而缓存,官网给出解释如下:

Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。

  也就是说,假设页面原本渲染了一个input标签,而状态改变后也有一个input标签,Vue检查到新老标签标签名和属性列表都相同。将保留已渲染的标签继续使用。
  这种缓存机制是Vue默认的,想修改这种动作,只要给标签加上具有唯一值的key属性即可,

<input placeholder="Enter your username" key="username-input">

  正常情况下,v-if会被设置在一个标签元素内使用,当遇到前后两个或多个兄弟标签都需要使用相同状态值来判断是否渲染时,可以一个无状态不可见标签<template>来包裹,Vue在构建DOM时会将其丢弃,并正确的将v-if作用到相应的标签上。

<template v-if="real">
  <div>实体车位</div>
  <div>实体车辆</div>
</template>

# v-if 与 v-for 优先级

  根据Vue的风格指南,不建议将v-ifv-for放在一起使用,我们来探索一下为什么.

它们一起使用的场景无非就有两个
(1)希望通过v-if控制v-for代码块是否显示。这种情况下一般v-if变量是个状态量,与v-for循环变量无关。
(2)希望通过循环变量中的某个属性的真假值,来控制该项是否应该被循环渲染出来

这两种用法有什么问题?在Vue语法中有个规则:循环体中,v-for属性优先级高于其他属性。也就是说:
场景(1): v-if的渲染会发生在循环之后,列表优先生成,这就无法提前阻止循环列表的渲染。这与我们初衷想要决定循环块是否渲染产生冲突。解决办法是:使用<template>标签包裹并在这里设置v-if控制
场景(2): 如果存在不该被渲染的项,这个项就不应该出现在循环变量中,Vue建议使用计算属性过滤数组。因此也不再需要v-if

# v-for 作用于对象

  循环不止作用于数组,同样可作用于所有可迭代类型变量中。

在遍历对象时,通常是按 Object.keys() 的结果遍历,但是不能保证它的结果在不同的 JavaScript 引擎下是一致的。

// 对象遍历,第一个是值,第二个是键,第三个才是索引
<div v-for="(value, key, index) in object" :key="index">
  {{ key }} : {{ value }}
</div>

# v-for渲染后的数组缓存替换规则

  Vue 包含一组观察数组的变异方法(mutation method),它们会触发视图更新。包括: push()、pop()、shift()、unshift()、splice()、sort()、reserve()等。这些方法都会改变原数组。
  同样还包含非变异方法,如filter()、concat()、slice()。他们不改变原数组,而是返回一个新数组。

  如果我们对已渲染过后的数组进行非变异方法操作,直觉上列表会重新渲染,其实不然。

Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的、启发式的方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。

exa.items = exa.items.filter(function (item) {
  return item.message.match(/Foo/)
})

# 冻结双向数据绑定

  如果初始渲染后不想让视图层响应模型层变化, 可以使用v-once标签属性, 告知被包含在该标签内部的所有数据绑定不要响应视图更新

<span v-once>这个数据不会发生改变: {{ message }}</span>

# 绑定一段 HTML

  Vue在html部分, 无论是利用双大括号{{ }}还是v-model绑定的值都会被解释为普通文本。如果需要绑定一段 HTML,可以使用v-html

<p v-html="htmlCode"></p>

# 修饰符

(1) .prevent / .stop / .passive

  如果你遇到过在页面执行一个Click事件,触发了两次函数调用,你则需要检查一下是否由事件冒泡引起的。 在DOM2级, DOM3级事件标准中, 浏览器接受一个点击交互后, 产生事件流会有两个过程,捕获和冒泡。 过程如下:

  为解决该问题,Vue提供了修饰符.prevent 可以告诉v-on指令对于触发的事件调用event.preventDefault() 来阻止浏览器的默认行为。 .stop则是调用event.stopPropagation() 来阻止目标元素的冒泡事件

.passive不能和.prevent一同使用,它会屏蔽.prevent的冒泡效果。.passive主要使用在移动端,它能提高其性能

(2)键盘修饰符 .enter / .tab / .delete ...

  Vue提供监听键盘按键键值的办法,方便我们监听键盘事件。一般情况下,直接使用键值修饰,如enter键的键值为13,则使用办法为:

<input @click.13="handleClick"></input>

  Vue为方便记忆,绑定了常用键名与键值的关系可直接使用键名绑定

<input @click.enter="handleClick"></input>

常用的有:.enter.tab.delete.esc.space.up.down.left.right也可以用通过config.keyCodes 对象自定义按键修饰符别名

// 可以使用 `v-on:keyup.f1`
Vue.config.keyCodes.f1 = 112
(3)鼠标修饰符

  鼠标修饰符限制处理函数仅响应特定的鼠标按钮,包括.left.right.middle

# 动态样式绑定 :class

  Vue允许动态切换一个样式, 支持两种语法: 对象形式 | 数组形式

  • 对象办法:键表示样式类名,值为 Truthy 表示添加该样式
<div class="wrap" :class="{ borderTop: boolean, active: isActive }"></div>

(2)给:class传递一个数组,表示应用一组样式

<div :class="[ classA, classB ]"></div>

v-bind:style 使用需要添加浏览器引擎前缀的 CSS 属性时,如 transform,Vue.js 会自动侦测并添加相应的前缀。

# 事件绑定传参

  如下,前者使用监听事件,而后者是内联处理器

<div id="example">
  <button @click="handleSubmit">提交</button>
  <button @click="say('Hi')">问候</button>
</div>

# 表单输入绑定

  对于普通元素如<div> {{ message }} </div>等并没有真正表现出Vue双向数据绑定的魅力,其只展现了从ViewModel层发生变化后反馈到View层的单方面特性。而表单输入的双向数据绑定还增加了用户交互使得View层发生改变并响应到ViewModel层,真正体现了“双向”功能。

v-model可以在表单元素<input>, <textarea><select>上创建双向数据绑定。Vue会根据空间类型自动选取正确的方法更新元素。值得注意的是,v-model会忽略所有表单元素的value, chekcd, selected特性的初始值而总是将Vue实例的数据data选项作为数据来源。也就是说,不能通过特性自身赋值绑定到v-model上,而需要在data中手动赋初始值

  1. 对于单行多行输入框,经v-model绑定过后的元素在文本区域中插值并不会生效,Vue只读绑定中的内容。如
<textarea>{{text}}</textarea>
  1. 单个复选框v-model绑定到布尔值;而多个复选框则绑定到同一个数组
# 只有一个checkbox则v-model输出true/false
<input type="checkbox" id="jack" value="Jack" v-model="checkedName">
<label for="jack">{{checkedName}}</label>   // checkedName: true / false
# 若在此基础上,再增加一个,则输出选中的数组
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
# 输出checkedName: [jack, mike]
  1. 单选按钮,绑定到同一个字符串,其值是value所对应的值
<input type="radio" id="one" value="One" v-model="picked">
<label for="one">One</label>    // 选中时输出对应value值: One
  1. 下拉选择菜单,单选时绑定到一个值上,多选时绑定到一个数组
# 单选下拉框去掉 multiple属性
<select v-model="selected" multiple style="width: 50px;">
  <option v-for="opt in options" v-bind:value="opt .value">
    {{ opt .text }}
  </option>
</select>
<span>Array: {{ selected }}</span>

# 表单绑定修饰符

  • .lazy:将input触发的更新延迟至change触发
<input v-model.lazy="msg" >
  • .number:将用户输入的内容转化为数字,否则总是返回字符串。设置type属性移动端可以调起数字键盘。如果这个值无法被 parseFloat()解析,则会返回原始的值
<input v-model.number="age" type="number">
  • .trim:自动过滤用户输入的首尾空白字符
<input v-model.trim="msg">

# Vue.$emit参数,及与 v-on 事件命名规范

  在刚开始开发时可能会思考为什么prop没有子向父传递。不幸运的是,prop的逆向会给数据流向带来巨大的维护和理解困难,这也是为什么Vue封装了$emit的模式 触发事件来取而代之的原因

this.$emit('method-name', param)

第一个参数是抛出的事件名,对应父级v-on事件名,第二个参数是要带出的数据,该数据使用$event捕获

<Children @click="$emit('enlarge-text', 0.1)"> Enlarge text </Children >
<blog @enlarge-text="postFontSize += $event"></blog>

通常父组件中会绑定给一个属性,该属性定义为一个方法且它的第一个参数就是被带出来的数据

<blog @enlarge-text="enlargeText"></blog>

methods: {
  enlargeText  (num) {
    this.postFontSize += num
  }
}

注意】不同于组件和prop,经$emit抛出的事件名不会被用作一个JavaScript变量名或属性名,所以就没有理由使用camelCase(驼峰式)或PascalCase(短线式)。因为HTML大小写不明感因素,v-on事件监听器在DOM模板中实质上会被自动转换为全小写,如此一来,原本计划通过驼峰式转换成的短线式的监听事件名也不可能被触发了,所以如果$emit使用驼峰式命名规则那么你的监听事件也需要驼峰式命名
  Vue建议使用短线式或全小写,特别是前者

this.$emit('my-event', params)  // 发起
<my-component v-on:my-event="handleEmit"></my-component>  // 接收

# 动态组件

  比如我们有一个tab栏,其中有三个tab页,点击不同tab页需切换至不同的组件下,此时非常适合使用is来指定不同的组件达到动态组件效果,如下。 完整示例

<component :is="currentTabComponent"></component>

  通过切换不同的tab能够实现不同组件的渲染。注意当你每次切换新标签的时候,Vue都创建了一个新的currentTabComponent实例,因此,他不会保留切换前用户停留的那个页面状态。通常来说,重新创建实例的行为是符合预期的,但也会有需要保留状态的时候,就像是缓存下来一般

>> 使用keep-alive保留状态
// 注意使用了keep-alive的组件必须要有name属性
<keep-alive>
  <component :is="currentTabComponent"></component>
</keep-alive>

# 基础组件的自动化全局注册

  经常为了美化页面效果,我们会对HTML元素做一层封装,成为基础组件,可能是一个输入框、一个按钮又或者是别的。对于这些组件,Vue建议使用具有语义化的规范命名风格,如以Base开头,BaseButtonBaseIcon, BaseInput等。引入这些组件往往占据了大量代码空间
import好几行,components: {}又有好几行,但是他们又只是模板中的很小的一部分。
  在 Vue CLI 3+ 中提供了require.context 通过全局注册这些非常通用的基础组件,允许你在应用入口文件(src/main.js)全局导入它们

import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camlCase from 'lodash/camelCase'

const requireComponent = require.context(
  // 其组件目录的相对路径
  './components',
  // 是否查询其子目录
  false,
  // 匹配基础组件文件名的正则表达式
  /Base[A-Z]\w+\.(vue|js)$/
)

requireComponent.keys().forEach(fileName => {
  // 获取组件配置
  const componentConfig = requireComponent(fileName)
  // 获取组件的 PascalCase 命名
  const componentName = upperFirst(
    camelCase(
      // 剥去文件名开头的 `./` 和结尾的扩展名
      fileName.replace(/^\.\/(.*)\.\w+$/, '$1')
    )
  )

  // 全局注册组件
  Vue.component(
    componentName,
    // 如果这个组件选项是通过 `export default` 导出的,
    // 那么就会优先使用 `.default`,否则回退到使用模块的根。
    componentConfig.default || componentConfig
  )
})

这里有一个真实案例

# Prop传递数据防脏

  所有的 prop都使得其父子组件形成一个单向下行绑定,父级prop的更新会流动到子组件中,但反过来不行。这种设计办法是为了防止子组件意外改变父组件的状态,从而导致你的应用的数据流向难以理解。另外,如果该数据还被其他子组件使用,也将受影响,产生泛洪式灾难。因此不应该在子组件中设计修改prop数据的操作。

在Javascript中对象和数组都是通过引用传入的,因此对于引用类型的prop来说,在子组件中修改数据本身将直接改变父级的数据。

  常见的试图改变prop的操作有一下两种情形:

  1. 接收的prop作为一个初始值,这个子组件接下来希望将其作为一个本地的prop数据来使用。这种情况下应该使用子组件中的data来拷贝一份prop数据数据
prop: [ 'initialNum' ],
data () {
  return {
    num: this.initialNum
  }
}
  1. 接收的prop作为原始的值需要进行格式转换。这种情况下,应该使用计算属性来实现
props: ['size'],
computed: {
  normalizedSize () {
    return this.size.trim().toLowerCase()
  }
}

  当不需要对prop做改变只是进行使用时可以不用data拷贝,但也需要注意使用,曾经遇到将 == 写成 =,花了不少时间找bug。当系统比较庞大时这种问题不好找,所以大家一定要细心实在不行就多做个data拷贝。

# prop自定义检查函数

  Vue允许在进行prop传值时对值进行验证,type可以验证数据类型,default可以设置当未传入时的默认值。除此之外,还允许开发者们自定义验证函数

function CheckName (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}

  验证办法如下

prop: {
  userName: CheckName
}

# Prop与自身属性重名问题

  当使用ElementUIBootstrap这些第三方插件时,往往他们定义有自己的属性,如果开发者们自定义的prop属性与其发生重名时,Vue在大多数情况下,从外部提供给组件的值会替换掉组件内部设置好的值。

  即假设存在传入type="text" 就会替换掉本身type="date" 类型,原来的就会被破坏。庆幸的是, classstyle会智能一些,即两边的值会合并起来

# Prop实现‘双向绑定’效果 .sync

  父组件中的某个属性值需要根据自身利用prop传递给子组件,然后在子组件中做一些操作后响应回给父组件来更新这样的需求时,除了使用$emitAPI,不妨试试.sync
  作为一种语法糖存在,.sync修饰在v-bind上,可以替代prop传递数据时v-on:updata:title="titleName"这种写法,(给属性增加update:是Vue在这种需求下的推荐用法),后者还需要使用$emit来回传值this.$emit('update:title', newTitle).sync则显得更加简便
  需要注意的是,.sync修饰的属性不能和表达是一起使用,如doc.title + "!"

<text-document v-bind:title.sync="doc.title"></text-document

# 将原生事件绑定到组件上

  通常都是在原生的标签上使用事件的绑定,但有时候,你可能想要在一个组件的根元素上直接监听一个原生事件,如使用CubeUI(一般UI库自身会提供原生的继承方法)或自定义组件上。这时,你可以使用 v-on.native 修饰符:

<tab-item @click.native=""></tab-item>

# Vue 插槽

  Vue插槽非常重要,笔者为其特意编写了一个专题,详情阅读Vue插槽,高复用组件

# $ref

  有时候需要直接访问一个子组件或子元素,此时可以为他赋予一个ref作为唯一标识,通过$refs来访问

<self-input ref="nameInput"></self-input>

访问时使用 this.$refs.nameInput,这样就可以自由访问其内部数据和方法了。这种办法同样适用于元素上。

<input ref="innerInput"></input>

比如我们想在父组件中控制子组件中的input框自动获取焦点,可以这么做

method: {
  focus: function() {
    this.$refs.innerInput.focus()
  }
}

refv-ror一起使用时,得到的结果是包含了对应数据源的这些子组件的数组。另外,需要注意的是,$refs只会在组件渲染完成之后生效,并且不是响应式的。它并不适用与计算属性

# $root & $parent

  在每个vue实例中,提供了根实例和父实例的数据和方法,这只是一种访问数据的实例,对于小型应用来说很方便,跟建议使用Vuex的状态管理机制。

  • $root 访问根实例的数据和方法,包括计算属性等
  • $parent 访问父实例的数据和方法,包括计算属性等。修改父组件容易导致难以查找数据变更源

# 依赖注入provide & inject

$root$parent只能实现根级实例访问和父级实例访问,然而对于跨级的组件间数据交互,虽然可以通过$parent一层层传递,但这不是一个好办法。依赖注入有了用武之地,通过两个新的选项:provideinject

provide允许我们在当前组件中指定想要提供给后代组件的数据和方法,表现形式很像data选项

provide () {
  return {
    getMsg: this.getMsg
  }
}

它就像是一个大范围的prop,后代组件都可以使用inject选项俩注入它。

inject: [ 'getMsg' ]

通过依赖注入的数据也是非响应式的,同样不适用与计算属性

# 后语

  本文内容大部分来自官网,作为提炼和融入笔者的一点思考,如有不对和不理解的地方欢迎与笔者交流和提出质疑

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

推荐阅读更多精彩内容