vue基础

vue 是一个 构建用户界面的渐进式的 javascript 框架

渐进式:逐渐增强,vue不强求你一次性在网站中运用学习所有的语法,可以学一点用一点

库:本质上是一些方法的集合。每次调用方法,实现一个特定的功能 如 axios

框架:是一套完整的解决方案。框架实现了大部分的功能,我们需要按照框架的规则写代码 如 vue

Vue是一个MVVM的框架 (MVVM:一种软件架构模式)

是一种简化用户界面事件驱动的编程方式

M:model数据模型(ajax获取到的数据)

V:view视图(页面)

VM:ViewModel 视图模型 (操作视图+模型) 是mvvm的核心,将数据和实图进行双向绑定

MVVM通过 数据双向绑定 让数据自动地双向同步

V(修改视图) -> M(数据自动同步)

M(修改数据) -> V(视图自动同步)

一方面修改,另一方面跟着修改,不用手动的操作dom元素

之前的思想,原生 dom驱动。无论修改什么页面内容,先找对象,操作dom。

现在的思想,vue 数据驱动。想更新视图,直接操作数据即可。数据变化,视图自动更新

vue组件化思想

组件化:也就是vue页面 一个组件会包含(HTML+CSS+JS) ,完整的页面可以拆分成多个组件

组件化的优点:

  1. 容易维护

  2. 便于复用(HTML+CSS+JS)

@vue/cli 脚手架的使用

  1. 传统开发模式:基于html/css/js文件开发 vue

  2. 工程化开发方式:在webpack环境中开发vue。(推荐,学习+项目)

vue脚手架

@vue/cli 也叫 vue脚手架, @vue/cli 是vue官方提供的一个全局命令工具

这个命令可以帮助我们快速的创建一个vue项目的基础架子

vue脚手架的好处

  1. 开箱即用

  2. 零配置(不用你配webpack)

  3. 内置babel等工具

vue脚手架的基本使用

  1. 全局安装:(一次安装之后直接创建文件)
    npm i @vue/cli -g 或 yarn global add @vue/cli

  2. 查看vue版本:
    vue --version

  3. 初始化一个vue项目:
    vue create 项目名(不能用中文)

  4. 启动项目, 打包项目

     npm run serve
    
     npm run build
    

我们在项目无法找到webpack.config.js文件,因为vue把它隐藏了

如果需要覆盖webpack的配置,可以修改vue.config.js文件,覆盖webpack配置

脚手架里的主要文件的作用

node_modules – 下载的第三方的包

public/index.html – 运行的浏览器网页

src/main.js – webpack打包的入口

src/App.vue – vue页面的入口

package.json – 依赖包列表文件

public/index.html不用动,提供一个最基础的页面

src/main.js不用动, 渲染了App.vue组件

src/App.vue默认有很多的内容,可以全部删除

assets 文件夹 和 components 中的可以直接删除

.vue文件

单文件组件:一个 .vue 文件就是一个组件

  1. 安装插件 (语法高亮)vetur

  2. 一个单文件组件由三部分组成
    template: 结构 (有且只能一个根元素)
    script: js逻辑
    style: 样式

  3. 让组件支持 less
    (1) style标签, lang=“less”,开启less功能

    (2) 装包: yarn  add  less-loader@7.2.1  less  -D
    

vue插值表达式

vue提供数据

  1. 通过 data 属性可以提供数据, data属性必须是一个函数

  2. 这个函数需要返回一个对象,这个对象就代表vue提供的数据

export default {
  data(){
    return {
       name : "张三"
    }
  }
}

使用插值表达式,可以在模板中渲染数据

<template>
<div>
    {{ name }}
</div>
</template>

插值表达式: 小胡子语法 {{ }}

  1. 作用: 使用 data 中的数据渲染视图(模板)

  2. 支持基本语法, 支持三元运算符 变量 对象.属性 方法

  3. 注意点:
    (1)使用数据在 data 中必须存在
    (2)能使用表达式,但是不能使用语句 if for ...
    (3)不能在标签属性中使用 {{ }} 插值

vue指令

vue指令: 特殊的 html 标签属性, 特点: v- 开头

每个 v- 开头的指令, 都有着自己独立的功能, 将来vue解析时, 会根据不同的指令提供不同的功能

vue指令-v-bind

说明:插值表达式不能用在html的标签属性上,想要动态的设置html标签属性,需要使用v-bind指令

作用: 动态的设置html的标签属性

语法: v-bind:属性名="值"

简写: :属性名="值"

<template>
<!-- template中只有一个根标签 -->
<div>
  <!-- vue 的指令 v-bind: 开头 -->
  <input type="text" v-bind:value = "value">
  <br>
  <input type="text" :value ="value">
  <br>
  <a v-bind:href = 'url'>点击跳转1</a>
  <br>
  <a :href = 'url'>点击跳转2</a>
</div>
</template>

<script>
export default {
  name : 'app', 
  // data必须是一个函数,并且必须要返回一个对象,数据就在返回的对象里
  data(){
    return{
      value : '请输入姓名',
      url : 'https://www.jianshu.com/'
    }
  }
}
</script>

vue指令-v-on

作用:注册事件

语法:

  1. v-on:事件名=“要执行的少量代码"

  2. v-on:事件名=“methods中的函数名"

  3. v-on:事件名=“methods中的函数名(实参)"

    注意:事件处理函数在methods中提供

简写:v-on 可以 简写 成 @

<template>
<!-- template中只有一个根标签 -->
<div>
  <!-- 注册事件 vue指令 v-on:事件名 = ‘要执行的代码’ -->
 <!-- 简写:v-on 可以 简写 成 @ -->
 <h3>{{ money }}</h3>
 <!-- 点击加10 -->
 <button v-on:click = 'money+=10'>+10</button>
 <!-- 简写 -->
 <button @click = 'money+=10'>+10简写</button>
 <br>
 <!-- 点击加100,使用方法 -->
 <button v-on:click = 'addmoney'>+100</button>
 <!-- 加100简写 -->
 <button @click = 'addmoney'>+100简写</button>
 <br>
 <!-- 多个不同的按钮调用方法 -->
 <button @click = 'moneys(200)'>+200</button>
 <button @click = 'moneys(500)'>+500</button>
 <button @click = 'moneys(1000)'>+1000</button>
 <button @click = 'moneys(10000)'>+10000</button>
</div>
</template>

<script>
export default {
  name : 'app', 
  // data必须是一个函数,并且必须要返回一个对象,数据就在返回的对象里
  data(){
    return{
      money : 10
    }
  },
  //   事件处理函数在methods中提供 
  methods: {
    // 在methods中引用data里的变量需要使用this
    addmoney(){
       this.money += 100
    },
    // 有参数的方法可以根据需求给多个标签调用
    moneys(num){
       this.money += num
    },
  }
}
</script>

vue中阻止默认事件

正常阻止默认事件

语法:事件对象.preventDefault()

vue中获取事件对象

(1) 没有传参, 通过形参接收 e

(2) 传参了, 通过$event指代事件对象 e

<template>
<div>
<!-- 阻止事件默认行为 -->
<!-- 没有参数时 -->
<a href="https://www.jianshu.com/" @click = 'fn'>没有参数</a>
<br>
<br>
<!-- 有参数时 -->
<a href="https://www.jianshu.com/" @click="fns(10,$event)">有参数</a>
</div>
</template>
<script>
 methods: {    
    // 没有传参时,直接设置事件对象,然后阻止默认行为
    fn(e){
      e.preventDefault()
      console.log(e)
    },
    // 有传参时,最后一个参数是事件对象,但是传参必须传 $event 固定语法 否则报错
    fns(num,e){
      e.preventDefault()
      console.log(num)
    }
  }
}
</script>

修饰符

事件修饰符

事件修饰符:vue提供事件修饰符,可以快速阻止默认行为或阻止冒泡

.prevent 阻止默认行为, .stop 阻止冒泡

.once 程序运行期间,只能触发一次事件处理函数

<template>
<!-- template中只有一个根标签 -->
<div>
<!-- 阻止事件默认行为 事件名.prevent -->
<a href="https://www.baidu.com" @click.prevent = 'fn'>点击跳转页面</a>
<!-- 事件修饰符 事件名.stop 阻止事件冒泡-->
<div @click = 'father'>父亲
  <div @click.stop = 'son'>孩子</div>
</div>
</div>
</template>

<script>
export default {
  // data必须是一个函数,并且必须要返回一个对象,数据就在返回的对象里
  data(){
    return{   
    }
  },
  //   事件处理函数在methods中提供 
  methods: {
    fn(){
      console.log('阻止跳转')
    },
    father(){
      console.log('父亲')
    },
    son (){
      console.log('儿子')
    }
  }
}
</script>

按键修饰符

在监听键盘事件时,我们经常需要判断详细的按键。可用按键修饰符

@keyup.enter – 只要按下回车才能触发这个键盘事件函数

@keyup.esc 监听返回键

vue中内置有 .delete .enter .esc .tab 等

<template>
<!-- template中只有一个根标签 -->
<div>
<!-- 按键修饰符 -->
<input type="text" placeholder="用户名" @keyup.esc = 'down'>
<!-- 设置按键弹起时,只有在设置的输入框获取焦点时按才有效 -->
<input type="password" placeholder="密码" @keyup.enter = 'fn'>
<button>登录</button>
</div>
</template>

<script>
export default {
  name : 'app', 
  // data必须是一个函数,并且必须要返回一个对象,数据就在返回的对象里
  data(){
    return{   
    }
  },
  //   事件处理函数在methods中提供 
  methods: {
    down (){
      console.log('esc 返回')
    },
    fn () {
      console.log('enter弹起')
    }
  }
}
</script>

案例 :逆转世界

当点击时字符串翻转

使用: 插值表达式

<template>
<!-- template中只有一个根标签 -->
<div>
<!-- 逆转世界 -->
<!-- 先静态后动态 -->
<h1>{{ msg }}</h1>
<button @click="reverseWorld">逆转世界</button>
</div>
</template>

<script>
export default {
  data(){
    return {
      msg : 'Hello,World!'
    }
  },
  methods : {
     reverseWorld (){
      // 1.先将字符串分割  split
    const arr = this.msg.split('')
    // 2.再将分割形成的数组倒转  reverse
    arr.reverse()
    // 3.最后将倒转的数组重新拼接成字符串然后赋值给msg
    this.msg = arr.join('')
     }
  }
}
</script>

盒子的显示隐藏

v-show 和 v-if 可以控制盒子的显示隐藏

  1. v-show
    语法: v-show="布尔值" (true显示, false隐藏)

    原理: 实质是在控制元素的 css 样式, display: none;

  2. v-if
    语法: v-if="布尔值" (true显示, false隐藏)

    原理: 实质是在动态的创建 或者 删除元素节点

    应用场景:

     1. 如果是频繁的切换显示隐藏, 用 v-show   
         (v-show, 只是控制css样式,而v-if, 频繁切换会大量的创建和删除元素, 消耗性能)
    
    1. 如果是不用频繁切换, 要么显示, 要么隐藏的情况, 适合于用 v-if

      (v-if 是惰性的, 如果初始值为 false, 那么这些元素就直接不创建了, 节省一些初始渲染开销)

vue指令 - v-else 和 v-else-if

<template>
<!-- template中只有一个根标签 -->
<div>
 <!-- 显示隐藏  v-show  v-if   值都为布尔值 true显示  false 隐藏-->
 <!-- v-show 的值为false时,就是往其标签的样式上加display = none -->
 <div v-show="age >= 18">成年人的世界</div>
 <div v-show="age < 18">未成年人的世界</div>
 <!-- v-if 的值为true时 创建一个新的标签,为false时就删除 -->
 <div v-if="age >= 18">我想要</div>
 <div v-if="age < 18">孤勇者</div>
 <!-- v-if  v-else-if   v-else  用法和js中的if判断一样 -->
 <div v-if="num < 60">不及格</div>
 <div v-else-if="num < 80">及格</div>
 <div v-else-if="num < 90">优秀</div>
 <div v-else-if="num < 100">顶尖</div>
 <div v-else>金色传说</div>
</div>
</template>

<script>
export default {
  data(){
    return {
      age : 16,
      num : 12
    }
  },
  methods : {
    
  }
}
</script>

v-model

v-model作用:给表单元素使用, 双向数据绑定

  1. 数据变化了, 视图会跟着变

  2. 视图变化了, 数据要跟着变

    语法: v-model='值'

vue指令 - v-model - 处理其他表单元素

例如:
select 绑定的是选中的选项值
checkbox 绑定的是是否选中 的一个布尔值
textarea 绑定的是value属性 用户输入的值

注意:v-model 会 忽略掉表单元素原本的value, checked等初始值

v-model修饰符

语法: v-model.修饰符="Vue数据变量"

.number 转数字,以parseFloat转成数字类型
.trim 去除首尾空白字符
.lazy 在change时触发而非inupt时

<template>
<!-- template中只有一个根标签 -->
<div>
<!-- v-model 修饰符  -->
<!-- .number转数字,以浮点型转成数字类型然后赋值给变量 -->
<input type="text" v-model.number="age" placeholder="数字类型">
<br>
<!-- .trim 将用户输入的数据前后空格去除然后赋值给变量 -->
<input type="text" v-model.trim="add" placeholder="去除前后空格">
<br>
<!-- .lazy 在输入框失去焦点时才触发,将用户输入的数据赋值给变量 -->
<input type="text" v-model.lazy="sall" placeholder="失去焦点时触发">
</div>
</template>

<script>
export default {
  data(){
    return {
      age : "",
      add : "",
      sall : "",
    }
  }
}
</script>

vue指令-v-text和v-html

作用:更新元素的innerText/innerHTML

语法:

v-text="值"
v-html="值"

区别:
v-text 不解析标签
v-html 解析标签

使用v-text和v-html的标签元素中不应写内容,会被覆盖

<template>
<!-- template中只有一个根标签 -->
<div>
<!-- v-text和 v-html -->
<!-- v-text 相当于 innerText  textContent -->
<!-- v-html相当于 innerHTML -->
<!-- v-text 只能解析变量中的文本,会将变量的数据以字符串的方式显示在标签元素中 -->
<!-- 使用v-text和v-html之后  标签元素中的插值会被覆盖 -->
<h1 v-text="msg">{{666}}</h1>
<!-- v-html 可以解析标签 ,会将解析的html标签显示在标签元素中 -->
<h1 v-html="msg"></h1>
</div>
</template>

<script>
export default {
  data(){
    return {
     msg : "<button>按钮</button>"
    }
  }
}
</script>

综合案例--文章标题编辑

<template>
  <div>
    <!-- 标题部分 -->
    <!-- 2.当点击编辑时,标题和编辑隐藏,编辑内容显示  给编辑绑定点击事件 -->
    <!-- 3.当点击编辑时,标题隐藏 -->
    <h1 class="title" v-show="!show">
      {{ title }}
      <span class="tag">{{ option }}</span>
      <!--5.点击编辑的时候,要初始化表单显示  -->
      <a class="edit-btn" @click="fn">编辑</a>
    </h1>

    <!-- 编辑内容 -->
    <!-- 1.在最开始时,下面编辑框会隐藏 -->
    <div v-show="show">
      <div class="input-group">标题:<input type="text" v-model="edtitle" /></div>
      <div class="input-group">
        频道:<select v-model="edoption">
          <option value="前端">前端</option>
          <option value="测试">测试</option>
          <option value="Java">Java</option>
        </select>
      </div>
      <div class="operation">
        <!-- 4.点击取消或者确认回到原来的状态 -->
        <button class="cancel" @click="show = false">取消</button>
        <button class="confirm" @click="ret">确认</button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data(){
    return {
      show : false,
      title : '从入门到崩溃',
      option : "前端",
      edtitle : '',
      edoption : '',
    }
  },
  methods: {
    // 点击编辑的事件
    fn(){
      this.show = true
      // 将title的值赋值给编辑输入框
      this.edtitle = this.title
      this.edoption = this.option
    },
    // 点击确认要执行的
    ret(){
      this.show = false
      // 点击确认之后将将修改后的edtitle赋值给title
      this.title = this.edtitle
      this.option = this.edoption
    }
  }
};
</script>

<style>
.title {
  display: flex;
  align-items: center;
}

.tag {
  margin-left: 8px;
  padding: 3px 6px;
  border-radius: 8px;
  background-color: green;
  font-size: 0.6em;
  color: #fff;
}

.edit-btn {
  margin-left: 32px;
  font-size: 0.8em;
  color: lightskyblue;
}

.input-group {
  margin-top: 16px;
  font-size: 28px;
}

.input-group input,
.input-group select {
  width: 300px;
  border-color: #ddd;
  font-size: 28px;
}

.operation {
  margin-top: 16px;
}

.cancel {
  margin-left: 85px;
  background-color: #ddd;
}

.cancel,
.confirm {
  padding: 8px 24px;
  border: 0;
  border-radius: 4px;
}

.confirm {
  margin-left: 16px;
  background-color: #006699;
  color: #fff;
}
</style>

v-for

作用:可以遍历 数组 或者 对象 或者数字,用于渲染结构 可以遍历循环结构

遍历数组语法:

v-for="item in 数组名"

v-for="(item, index) in 数组名"

遍历对象语法:

v-for = "(value, key) in 对象名"

遍历数字

v-for = "item in 数字"

 v-for优先级高于v-if
    如果同时出现v-for和v-if,无论判断条件是否成立,都会执行一遍v-for循环,这样浪费性能,所以要尽可能的避免两者一起使用。

v-for就地更新

vue会尽量复用上一次渲染的dom

当需要重新渲染时,vue会先对比缓存在内存里的的dom结构,然后只更新变化的结构

新旧DOM产生后对比, 然后决定是否复用真实DOM/更新内容

Vue会尽可能的就地(同层级,同位置), 对比虚拟dom,复用旧dom结构,进行差异化更新

可以复用旧的dom结构,更新高效

虚拟dom

html 渲染出来的 真实dom树,是个树形结构(复杂)。每个标签,都只是树的某个节点

虚拟dom:本质就是 保存节点信息, 描述真实dom的 JS 对象

一个缓存在内存的js对象,可以用最少的属性结构,描述真实的dom

可以更高效的对比变化的地方,只更新改变的地方,减少dom操作

v-for中的key属性

在没有提供key的时候或是使用索引作为key的值时就使用就地更新

在使用key时,就根据key的值进行对比,提示对比和更新性能

key的值的要求是:唯一不重复的字符串或数字

key值 : 有id用id 没有用索引

可以配合虚拟DOM提高更新的性能

动态修改样式

style

用 v-bind 动态设置标签的 style 行内样式

语法 :style="对象/数组"

对象

:style = '{属性名:"属性值"}' // 属性值带引号

数组

:style = '[]' // 数组里可以是多个对象,多个值

样式的属性名带横线

1.使用小驼峰命名

2.给属性名加上引号

class

用 v-bind 动态设置标签的 class 类名

语法 :class="对象/数组"

    对象:如果键值对的值为true,那么就有这个类,否则没有这个类

   对象中可以使用表达式判断

类名有横线 : 加引号

    数组:数组中所有的类,都会添加到盒子上

    数组中的所有元素都要加上引号        

v-bind 对于类名操作的增强, 注意点 :class 不会影响到原来的 class 属性

计算属性

计算属性默认情况下只能获取,不能修改。

要给计算属性赋值,就必须写计算属性的完整写法!

作用: 根据一些数据算出来的属性 (用的时候不加括号)

声明在computed对象中 computed和data methods 同级

简写 : 声明为一个函数

computed : {
   age(){
     
   }
}

完整写法:

computed : {
 age: {
      get() {
        // 获取计算属性的值
        const date = new Date();
        return date.getFullYear() - this.year;
      },
      set(newAge) {
        //  更新计算属性的值
        // 本质上就是更新变量的值 改变之后触发set属性
        const date = new Date();
        this.year = date.getFullYear() - newAge;
      },
    },
}

set里的参数是age的状态或数据,当age更新时触发set,然后执行set里面的代码,将新的数据更新给变量,然后又触发get

侦听器

watch: 可以侦听到 data/computed 属性值的改变

简写

watch:{
  被侦听的属性名(newVal,oldVal){
   newVal 是新值
   oldVal 是旧值
}

完整写法

可以用于监听数据变化,然后同步到本地

 // 侦听数据的变化
  // 侦听器完整写法  相当于input时间
  // 可以侦听复杂数据类型 也可以立即执行
  watch: {
    list: {
      // immediate: true,   立即执行一次
      deep: true, // 深度侦听 常用于数组或对象
      handler(newVal) {  // 数据变化触发的函数
        console.log(newVal.name);
      },
    },
  },
};

组件

组件概念

组件是可复用的 Vue 实例, 封装标签, 样式和JS代码

组件化 :封装的思想,把页面上 可重用的部分 封装为 组件,从而方便项目的 开发 和 维护

一个页面, 可以拆分成一个个组件,一个组件就是一个整体

每个组件可以有自己独立的 结构 样式 和 行为(html, css和js)

前端组件化开发的好处:

1.提高了 复用性和灵活性

2.提升了 开发效率 和 后期可维护性

组件的注册使用

App.vue 是根组件, 这个比较特殊, 是最大的一个根组件。其实里面还可以注册使用其他小组件

使用组件的四步:
1.创建组件, 封装要复用的标签, 样式, JS代码
2.引入组件
3.注册组件
全局注册 – main.js中 – 语法如图
局部注册 – 某.vue文件内 – 语法如图
4.使用组件:组件名当成标签使用即可
<组件名></组件名>

注意点:组件名不能和内置的html名同名

创建组件

创建一个vue文件

封装要复用的结构 ,样式 , 和js代码

引入组件

import 组件对象  from  vue文件路径
import {组件对象} from  vue文件路径

当导入部分或导入非原组件默认导出的组件时,需要加大括号

导入默认导出的组件或者将整个模块作为单一的对象进行导入,不需要加大括号,该模块的所有导出都会作为对象的属性存在

注册组件

全局注册

Vue component('组件名',组件对象)

局部注册

export default {
  components : {
   '组件名' : 组件对象
  }
}

使用组件

<组件名></组件名>

组件名的命名方式

在进行组件的注册时,定义组件名的方式有两种

  1. 注册使用短横线命名法,例如 hm-header 和 hm-main

使用时 <hm-button> </hm-button>

Vue.component('hm-button',HmButton)
  1. 注册使用大驼峰命名法,例如 HmHeader 和 HmMain

使用时 <HmButton> </HmButton><hm-button> </hm-button> 都可以

Vue.component('HmButton',HmButton)

推荐1: 定义组件名时, 用大驼峰命名法, 更加方便

推荐2: 使用组件时,遵循html5规范, 小写横杠隔开(可选)

组件在 开发者工具中 显示的名字,可以通过name进行修改

在注册组件期间,除了可以直接提供组件的注册名称之外,还可以把组件的 name 属性作为注册后组件的名称

<script>
    export default {
        name : 'HmButton'
    }
</script>

name写在要组册的组件中

注册可以全局注册或者局部注册

Vue.component(HmButton.name,HmButton)

组件之间的样式冲突 scoped

默认情况下,写在组件中的样式会 全局生效,因此很容易造成多个组件之间的样式冲突问题。

  1. 全局样式: 默认组件中的样式会作用到全局

  2. 局部样式: 可以给组件加上 scoped 属性, 可以让样式只作用于当前组件

scoped原理

(1)当前组件内标签都被添加 data-v-hash值 的属性

(2)css选择器都被添加 [data-v-hash值] 的属性选择器

最终效果: 必须是当前组件的元素, 才会有这个自定义属性, 才会被这个样式作用到

scoped会在元素上添加唯一的属性(data-v-x形式),css编译后也会加上属性选择器,从而达到限制作用域的目的

组件通信--父传子

父组件给子组件传值

被引入的组件是子组件

父组件 : App.vue

子组件:被引入的组件

父传子语法:

  1. 父组件通过给子组件加属性传值
    <Son price="100" title="不错" :info="msg"></Son>

  2. 子组件中, 通过props属性接收

     props: ['price', 'title', 'info']
    

父组件

<template>
  <div>
    <h1>父组件</h1>
    <a-package :strs="str"></a-package>
  </div>
</template>

<script>
import A from "./components/a-package.vue";
export default {
  components: {
    "a-package": A,
  },
  data() {
    return {
      str: "这是父组件传给子组件的值",
    };
  },
};
</script>

<style>
</style>

子组件

<template>
  <div>
    <h1>{{ strs }}</h1>
  </div>
</template>

<script>
export default {
  props: ["strs"],
};
</script>

<style>
</style>

注意点:

1.父传子之前需要先将子组件引入父组件中使用

2.给子组件传值的属性等于值需要写在子组件在父组件的标签中

3.子组件需要使用props接收父组件传过来的值,传过来的值要用引号

4.props中接收的是写在父组件中子组件标签中的属性 ,所对应的值可以直接在子组件中使用,但不能改变

v-for遍历组件

父组件

<template>
  <div>
    <h1>v-for遍历组件</h1>
    <a-package
      v-for="item in list"
      :key="item.id"
      :username="item.name"
      :ages="item.age"
    ></a-package>
  </div>
</template>

<script>
import A from "./components/a-package.vue";
export default {
  components: {
    "a-package": A,
  },
  data() {
    return {
      list: [
        { id: 1, name: "唐僧", age: 20 },
        { id: 1, name: "悟空", age: 600 },
        { id: 1, name: "八戒", age: 1800 },
        { id: 1, name: "沙僧", age: 1500 },
      ],
    };
  },
};
</script>

<style>
</style>

子组件

<template>
  <div>
    <h1>{{ username }}</h1>
    <h2>{{ ages }}</h2>
  </div>
</template>

<script>
export default {
  props: ["username", "ages"],
};
</script>

<style>
</style>

单向数据流

在vue中需要遵循单向数据流原则: (从父到子的单向数据流动, 叫单向数据流)

  1. 父组件的数据变化了,会自动向下流动影响到子组件

  2. 子组件不能直接修改父组件传递过来的 props, props是只读的

组件通讯--子传父

子传父的基本语法:

  1. 子组件可以通过 this.$emit('自定义事件名', 参数1, 参数2, ...) 触发事件的同时传参的
  2. 父组件可以给子组件注册对应的自定义事件
  3. 父组件并提供对应的函数接收参数

子组件触发事件之后的方法给父组件传值

this.$emit('ok',参数)

父组件在子组件标签上监听该事件,当子组件触发该事件时,调用方法

<子组件 @ok = 'fn'></子组件>

<script>
export default {
   methods: {
      fn(e){
        console.log(e)
      }
   }
}
</script>

当子组件传参只有一个时,父组件监听的事件触发的方法默认第一个就是子组件的传参

当父组件也有传参时,$event则是子组件的默认传参

<子组件 @ok = 'fn(item,$event)'></子组件>

<script>
export default {
   methods: {
      fn(item,e){
        console.log(item)
        console.log(e)
      }
   }
}
</script>

props校验

props 是父传子, 传递给子组件的数据, 为了提高 子组件被使用时 的稳定性, 可以进行props校验, 验证传递的数据是否符合要求

默认的数组形式, 不会进行校验, 如果希望校验, 需要提供对象形式的 props

props: {
    // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组默认值必须从一个工厂函数获取
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }

v-model语法糖

语法糖:v-model本质上是 value属性和input事件的一层包装

v-model的作用:提供数据的双向绑定

1.数据发生了改变,页面会自动变 v-bind:value

2.页面输入改变 , 数据会自动变化 v-on:input

v-model流程

1.先是在父组件中声明数据

2.然后在父组件中的子组件的标签使用v-model 等于 数据

3.子组件在props中接收父组件传过来的参数必须为value

4.在子组件中使用value 然后在触发事件时将要改变的数据作为参数传给父组件 传参定义的事件默认为input

5.父组件因为使用v-model ,默认监听input事件,所有传过来的数据默认修改 ,然后传给子组件

以上是v-model实现组件数据的双向绑定的步骤

父组件

<template>
  <div>
    <a-package v-model="num"></a-package>
  </div>
</template>

<script>
import A from "./components/a-package.vue";
export default {
  data() {
    return {
      num: 10,
    };
  },
  components: {
    "a-package": A,
  },
};
</script>

<style>
</style>

子组件

<template>
  <div>
    <button @click="$emit('input', value - 1)">--</button>
    {{ value }}
    <button @click="$emit('input', value + 1)">++</button>
  </div>
</template>

<script>
export default {
  props: ["value"],
};
</script>

<style>
</style>

注意:

当子组件传数据给父组件时

在script中使用 this.$emit('自定义事件名',参数)

在template中不需要使用this

ref和$refs

作用:利用 ref 和 $refs 可以用于 获取 dom 元素, 或者组件实例

1.获取dom标签

给要获取的标签添加ref属性

然后通过this.$refs.属性值 ,获取目标标签

<h1 ref='my'><h1>
console.log(this.$refs.my)

2.获取组件实例

给Demo组件目标组件, 添加ref属性-名字随意

恰当时机, 通过 this.$refs.xxx 获取组件对象, 可调用组件对象里方法等

<组件名 ref= 'my'></组件名>
this.$refs.my.方法或属性

$nextTick

$nextTick:等DOM更新后, 才会触发执行此方法里的函数体

methods:{
   this.show = true
   this.$nextTick(() =>{
     console.log(this.$refs.my)
   })
}

小案例

点击按钮,显示输入框获取焦点

<template>
  <div>
    <input type="text" v-show="show" ref="inp" />
    <button v-show="!show" @click="fn">点击显示</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      show: false,
    };
  },
  methods: {
    fn() {
      this.show = !this.show;
      console.log(this.$refs.inp);
      this.$nextTick(() => {
        this.$refs.inp.focus();
      });
    },
  },
};
</script>

<style>
</style>

动态组件

动态组件是可以改变的组件,可以解决多组件同一位置,切换显示的问题

基本语法

component 组件(位置) + is 属性 (哪个组件)

修改 is 属性绑定的值 => 切换组件

  1. 准备被切换的 3个组件, 并引入注册

  2. 准备变量来承载要显示的"组件名"

  3. 设置挂载点<component>, is属性设置要显示的组件 ( component + is )

  4. 点击按钮 – 修改name变量里的"组件名" ( 修改 is 的值)

父组件

<template>
  <div>
    <button @click="name = 'a-package'">老虎</button>
    <button @click="name = 'b-package'">🦌</button>
    <button @click="name = 'c-package'">猫</button>
    <!-- <a-package></a-package>
    <b-package></b-package>
    <c-package></c-package> -->
    <component :is="name"></component>
  </div>
</template>

<script>
import A from "./components/a-package.vue";
import B from "./components/b-package.vue";
import C from "./components/c-package.vue";
export default {
  components: {
    "a-package": A,
    "b-package": B,
    "c-package": C,
  },
  data() {
    return {
      name: "a-package",
    };
  },
};
</script>

<style>
</style>

子组件中分别是三种对应按钮的图片

当点击对应的按钮时显示对应的图片

注意:当使用动态组件时,不要再使用子组件标签

只需要切换:is的值就可以切换组件

使用v-if 的显示隐藏也可以达到类似的效果

自定义指令

自定义指令:自己定义指令, 封装dom操作,扩展额外功能

局部注册

color是指令名

// 局部注册自定义指令
  directives: {
    color: {
      //  当指令所在的标签插入到dom树中,下面方法会被执行
      inserted(dom) {
        dom.style.color = "red";
      },
    },
  },

全局注册

bold是指令名

// 全局注册自定义指令
Vue.directive("bold", {
  inserted(dom) {
    dom.style.fontSize = "50px";
  },
});

自定义指令传参

语法:在绑定指令时,可以通过“等号”的形式为指令绑定具体的参数值

通过 binding.value 可以拿到指令值,指令值修改会触发 update 函数

自定义指令主要是对dom的操作来做一些额外操作

App.vue

<template>
  <div>
    <p v-color="red" v-bold="'100px'">自定义指令</p>
    <p v-color v-bold>自定义指令</p>
    <p v-color v-bold>自定义指令</p>
    <button @click="fn">点击</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      red: "red",
      blue: "blue",
      pink: "pink",
    };
  },
  // 局部注册自定义指令
  directives: {
    color: {
      //  当指令所在的标签插入到dom树中,下面方法会被执行
      inserted(dom) {
        dom.style.color = "red"; // value = red
      },
      // update 在指令的值改变时触发,将修改后的值传参然后重新赋值修改
      // binding.value是传给自定义指令的值
      update(dom, binding) {
        dom.style.color = binding.value;
      },
    },
  },
  methods: {
    fn() {
      this.red = "green";
    },
  },
};
</script>

<style>
</style>

main.js

import Vue from "vue";
import App from "./App.vue";

Vue.config.productionTip = false;

// 全局注册自定义指令
Vue.directive("bold", {
  inserted(dom) {
    dom.style.fontSize = "50px";
  },
  update(dom, binding) {
    dom.style.fontSize = binding.value;
  },
});

new Vue({
  render: (h) => h(App),
}).$mount("#app");

keep-alive--缓存组件

keep-alive是缓存组件,不会重复创建销毁

    <button @click="names = 'a-name'">a-a</button>
    <button @click="names = 'b-name'">b-b</button>
    <keep-alive>
      <component :is="names"></component>
    </keep-alive>

两个生命周期

export default {
  //在缓存激活的时候触发
  activated() {
    console.log("b激活了");
  },
  //在缓存失活的时候触发
  deactivated() {
    console.log("b失活了");
  },
};

插槽

默认插槽

插槽基本语法:

  1. 组件内用<slot></slot>占位
  2. 使用组件时<组件名></组件名>夹着的地方, 传入标签替换slot

父组件

<template>
  <div>
    <slot-a>
      <img
        src="https://ts3.cn.mm.bing.net/th?id=OIP-C.S3QOsgnlzrpbpAdOXF4YMAHaEo&w=316&h=197&c=8&rs=1&qlt=90&o=6&dpr=1.25&pid=3.1&rm=2"
        alt=""
      />
    </slot-a>
  </div>
</template>

<script>
import slotA from "./components/slot-a.vue";
export default {
  components: {
    "slot-a": slotA,
  },
};
</script>

<style>
</style>

子组件

<template>
  <div>
    <h1>插槽</h1>
    <slot></slot>
    <p>这是通过插槽传递结构</p>
  </div>
</template>

<script>
export default {};
</script>

<style>
</style>

插槽 - 后备内容(默认值)

插槽后备内容:封装组件时,可以为预留的 <slot> 插槽提供后备内容(默认内容)

语法: 在<slot>标签内放置内容, 作为默认显示内容

效果:

外部使用组件时,不传东西,则slot会显示后备内容 (slot标签内的结构)

外部使用组件时,传东西了,则slot整体会被换掉

<slot>
    <p>这是默认内容</p>
</slot>

插槽 - 具名插槽

一个组件内有多处,需要外部传入标签,进行定制

语法:

多个slot使用name属性区分名字 <slot name="content"></slot>

template配合v-slot:名字来分发对应标签,根据对应的名字传给不同的插槽对应的结构或内容

v-slot:可以简化成#

父组件

<template>
  <div>
    <slot-a>
      <template #header>
        <p>这是插槽的标题</p>
      </template>
      <template #body>
        <p>这是插槽的内容</p>
      </template>
      <template #footer>
        <p>这是插槽的底部</p>
      </template>
    </slot-a>
  </div>
</template>

<script>
import slotA from "./components/slot-a.vue";
export default {
  components: {
    "slot-a": slotA,
  },
};
</script>

<style>
</style>

子组件

<template>
  <div>
    <p>
      <slot name="header"><button>标题</button></slot>
    </p>
    <p>
      <slot name="body"><button>内容</button></slot>
    </p>
    <p>
      <slot name="footer"><button>底部</button></slot>
    </p>
  </div>
</template>

<script>
export default {};
</script>

<style>
</style>

插槽 - 作用域插槽

作用域插槽: 定义 slot 插槽的同时, 是可以传值的。给插槽上可以绑定数据,将来使用组件时可以用

基本步骤:

1.给slot标签以添加属性的方式传值

2.所有添加的属性都会被添加到一个对象中,然后传给父组件的对应的地方

3.在template中 ,接收子组件传过来的对象

匿名的就是 v-slot =‘自定义的对象名’

具名的就是 v-slot= 插槽名 = ‘自定义的对象名’

匿名插槽传参

父组件

<template>
  <div>
    <!-- 接收子组件传过来的对象 -->
    <slot-a v-slot="obj">
      <!--  子组件插槽传过来的值  obj:{user:{name:'张三',age:18}} -->
      <h1>{{ obj.user.name }}</h1>
      <h1>{{ obj.user.age }}</h1>
    </slot-a>
  </div>
</template>

<script>
import slotA from "./components/slot-a.vue";
export default {
  components: {
    "slot-a": slotA,
  },
};
</script>

<style>
</style>

子组件

<template>
  <div>
    <slot :user="user"></slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: {
        name: "张三",
        age: 18,
      },
    };
  },
};
</script>

<style>
</style>

具名插槽传参

父组件

<template>
  <div>
    <slot-a>
      <template #head="obj">
        <p>{{ obj.title }}</p>
      </template>
      <template #body="obj">
        <p>{{ obj.content }}</p>
      </template>
    </slot-a>
  </div>
</template>

<script>
import slotA from "./components/slot-a.vue";
export default {
  components: {
    "slot-a": slotA,
  },
};
</script>

<style>
</style>

子组件

<template>
  <div>
    <slot name="head" :title="user.title">标题</slot>
    <slot name="body" :content="user.content">内容</slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: {
        title: "这是一个标题",
        content: "这是内容",
      },
    };
  },
};
</script>

<style>
</style>

生命周期

vue组件生命周期:从创建 到 销毁 的整个过程就是 – Vue实例的 - 生命周期

生命周期-钩子函数概述

生命周期函数:是由 vue 框架提供的内置函数,会伴随着组件的生命周期,自动按次序执行

作用: 特定的时间点,执行特定的操作

比如: 组件创建完毕后,可以在created 生命周期函数中发起Ajax 请求,从而初始化 data 数据

分类: 三大阶段,8个方法

  • 组件初始化阶段的生命周期函数
  • 组件运行阶段的生命周期函数
  • 组件销毁阶段的生命周期函数

八大生命周期钩子函数:

1.beforeCreate:data数据初始化之前,组件还没有数据

2.created: data数据初始化之后,可以获取到组件的数据

3.beforeMount:DOM渲染之前,DOM还没渲染

4.mounted:DOM渲染之后,可以操作DOM了

5.beforeUpdate: 数据更新,DOM更新前

6.updated: 数据更新,DOM更新后

7.beforeDestroy: 组件销毁前

8.destroyed: 组件销毁后

路由

单页面应用程序

SPA - Single Page Application

单页面应用(SPA): 所有功能在一个html页面上实现 例如网易云音乐

(多页面应用程序MPA)

优点:

不整个刷新页面,每次请求仅获取需要的部分,用户体验更好

数据传递容易, 开发效率高

缺点:

开发成本高(需要学习专门知识 - 路由)

首次加载会比较慢一点。不利于seo

路由

路由就是路径地址和组件的映射关系

vue的路由是 vue-router ,是vue官方提供的一个插件,专门负责出来路由

其中v3 对应的是vue2.0

   v4 对应的是vue3.0

vue的组件分类说明

组件分类: .vue文件分2类, 一个是页面组件, 一个是复用组件

src/views文件夹

页面组件 - 页面展示 - 配合路由用

src/components文件夹

复用组件 - 展示数据 - 常用于复用

配置路由的步骤

1.下包

npm i vue-router@3   // @后面是对应的版本

2.引入路由

import VueRouter from "vue-router";

3.创建组件

4.引入组件

5.导入 注册路由

Vue.use(VueRouter); //使用Vue注册之前要先引入Vue

6.定义路由

const routes = [
  {
    path: "/head-name",  // 地址
    component: headName, // 组件
  },
]

7.实例化路由

routes这个是固定的 属性名 改变后路由地址改变无法跳转页面

const router = new VueRouter({
  routes, //缩写
});

8.导出路由

export default router;

9.在main.js中引入路由,然后挂载到vue实例上面去

// 引入路由
import router from "./router/index";

new Vue({
  // 将路由挂载到vue实例上
  router, //简写
  render: (h) => h(App),
}).$mount("#app");

10.在App.vue上使用 <router-view></router-view> 来渲染路由

声明式导航

组件router-link 替代 a标签,能跳转,能高亮

  1. vue-router提供了一个全局组件 router-link

  2. router-link实质上最终会渲染成a链接 to属性等价于提供 href属性(to无需#)

  3. router-link提供了声明式导航高亮的功能(自带类名)

<template>
  <div>
    <router-link to="/head-name">1</router-link><span>|</span>
    <router-link to="/body-name">2</router-link><span>|</span>
    <router-link to="/foot-name">3</router-link>
    <router-view></router-view>
  </div>
</template>

<script>
export default {};
</script>

<style scoped>
.router-link-active {
  color: red;
}
</style>

声明式导航 - 两个类名

router-link 会自动给当前导航添加两个类名

router-link-active: 激活的导航链接 模糊匹配

可以匹配开始一致的路径

router-link-exact-active: 激活的导航链接 精确匹配

只能匹配完全一致的路径

可以修改默认高亮的类名

const router = new VueRouter({
  routes,
  linkActiveClass = 'aa'
  linkExactActiveClass = 'bb'
});

声明式导航 - 跳转传参

跳转路由时, 可以给路由对应的组件内传值

在router-link上的to属性传值, 语法格式如下

/path?参数名=值 当参数对页面不是必须的,通常放在问号后面

/path/值 – 需要路由对象提前配置 path: “/path/:参数名” 参数对应页面来说是必须的,就将参数嵌套在url里面

对应页面组件接收传递过来的值

在对应的组件中创建后可以接收到

this.$route 代表当前的页面信息

查询参数

this.$route.query.参数名 问号后面

this.$route.params.参数名 嵌套url

1.问号后面非必要传参

<!-- 对象属性,path是要跳转的路径,query是问号后面的参数 -->
<router-link :to="{ path: '/bar', query: { name: 'jack', age: 18 } }">bar</router-link>

在对应页面可以查到对应的参数信息

created() {
    console.log(this.$route.query.name);
  },

2.内嵌url 对页面必要性传参

当需要传递内嵌到路径里的参数的时候

/路径/:参数名 来声明内嵌式参数 在定义路径和组件的映射关系中

路由文件

 {
    path: "/foot-name/:id",
    component: footName,
  },

App.vue

<router-link :to="`/foot-name/${user.id}`">3</router-link>

对应的组件可以查到传入的参数

export default {
  created() {
    console.log(this.$route.params.id);
  },
};

想要用对象的方式传参, path不能和params一起传参 ,如果非要用对象的方式传参,需要给对应的路由取一个名字,用name和params一起传参

{
    name: "aa",
    path: "/body-name/:id",
    component: bodyName,
  },
<router-link :to="{ name: 'aa', params: { id: 66 } }">2</router-link>

对应组件一样可以查看传的参数

export default {
  created() {
    console.log(this.$route.params.id);
  },
};

重定向

重定向:匹配path后, 强制跳转path路径

网页打开url默认hash值是/路径

redirect是设置要重定向到哪个路由路径

// 重定向
  {
    path: "/", // 默认根组件
    // 当为根地址是重定向为新的地址
    redirect: "/head-name",
  },

vue路由--404

404:当找不到路径匹配时,给个提示页面

路由最后, path匹配*(任意路径) – 前面不匹配就命中最后这个

// 模糊匹配要放在任意一个
  {
    // *代表任意路径
    path: "*",
    //组件使用路由先要引用
    component: noneName,
  },

路由模式设置 - 修改路由,在地址栏的模式

hash路由例如: http://localhost:8080/#/home

history路由例如: http://localhost:8080/home (以后上线需要服务器端支持)

const router = new VueRouter({
  routes, //缩写
  // 路由模式
  mode: "history",  // 这就是设置路由模式
});

编程式导航

编程式导航:用JS代码来进行跳转

this.$router 有push方法,可以推入页面,压下上一个页面,也可以传参

1.可以直接传路径

2.可以传对象,里面有路径和参数 path和query,或者name和params (path和params不能在一起)

<button @click="$router.push('/head-name')">编程式导航</button>

编程式导航 - 路由传参

query传:$route.query.xxx 接收

params传:$route.params.xxx 接收

区别:

params传参:是在内存中传参,刷新会丢失

query传参:是在地址栏传参,刷新还在

<button @click="$router.push({ path: '/head-name', query: { age: 55 } })">
<button @click="$router.push({ name: 'aa', params: { id: 2983193 } })">

过滤器--vue3.0已经废弃

和methods方法一样,filters

只能用在插值表达式和动态绑定属性中,用 | 表明

<template>
  <div>
    <h1>{{ str | split | res | join }}</h1>
  </div>
</template>

<script>
export default {
  data() {
    return {
      str: "123456",
    };
  },
  filters: {
    split(str) {
      return str.split("");
    },
    res(str) {
      return str.reverse();
    },
    join(str) {
      return str.join("");
    },
  },
};
</script>

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