Go to comments

vue 六日集训营笔记

为什么要学 vue ?


以前的开发,

我们关心的是 dom 元素,

就是对 dom 元素的处理,怎么创建一个 dom 元素,怎么给一个 dom 元素注册事件,怎么移除一个 dom 元素,怎么改变一个 dom 元素的结构,

jQuery 只是简化的 dom 操作,对 dom 操作更加方便了而已,它没有改变开发方式,以前怎么开发还怎么开发


现在前端发展到单页面应用,

就是整个网站只有一个页面,或者是某一个功能块只有一个页面,这就是单页面应用程序(比如 新浪微博


面对单页面应用程序,很多的数据、dom 元素全部都要 js 来处理,

面对这种情况,用传统的开发就显得麻烦了,因此 vue 能解决了这个问题,它能够在复杂的系统里面降低项目的复杂度


vue 的特点

1. 渐进式,意思是 vue 的侵入性很少,因此使用 vue 可以与很多其他前端技术联用

2. 组件化,我们面对一个复杂的页面的时候,可以把页面划分为很多很多的区域,每一个区域做成一个组件,这样一个区域就没有那么复杂了

3. 响应式,意思是数据响应式,vue 会监控数据的变化,当数据发生变化时自动重新渲染页面


解释一下,渐进式的实现方式,

vue 只会控制我们指定的容器 #app,一个 vue 实例控制一个容器,控制挂载区域的容器,

页面中的其他内容和 vue 没有关系,

因为 vue 只控制它自己那块区域,所以 vue 可以和其他技术共存

<div>这些内容和vue共存</div>
<div id="app"></div>
<div>这些内容和vue共存</div>

<script>

  const app = new Vue(config);
  app.$mount("#app");

</script>


一、vue 的开发方式

关于创建 vue 工程有两种方式

1. 直接在页面上引用 vue.js

2. 使用脚手架 vue-cli 搭建工程。需要的前置知识 nodejs、webpack、sass、less、css-module、如果要彻底理解脚手架还学习命令行开发


用第一种方式,

先引入 vue.js 文件,再引入我们自己写的 main.js

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>商品仓库管理</title>
<style>
ul{list-style:none;padding:0;}
span{display:inline-block;}
.soldout{color: #008c8c;}
.stock{color:#f40;width:30px;text-align:center;}
[type="number"]{width:30px;}
</style>
</head>
<body>

  <div id="app"></div> <!-- #app元素会被vue的模板tempalte所替换掉 -->

  <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.js"></script>
  <script src="./main.js"></script>

</body>
</html>


main.js

下面使用 vue 实现效果

// 模板
const template = `
<div>
  <h1>{{title}}</h1>
  <div>
    商品名称:<input type="text" v-model="newProducts.name">
    商品数量:<input type="number" v-model.number="newProducts.stock">
    <button @click="add">添加</button>
  </div>
  <ul>
    <li v-for="(item, index) in products" :key="index">
      <span style="width:70px;">{{item.name}}</span>
      <button @click="changeStock(item, item.stock-1)">-</button>
      <span v-if="item.stock>0" class="stock">{{item.stock}}</span>
      <i v-else>售罄</i>
      <input type="number" min="0" v-model="item.stock"/>
      <button @click="changeStock(item, +item.stock+1)">+</button>
      <button @click="remove(index)"> del </button>
    </li>
  </ul>
</div>`;


// 配置对象
const config = {
  template,
  el: "#app",
  data: {
    title: "商品和库存",
    products: [
      {name:"HuaWei", stock: 10},
      {name:"MI", stock: 8},
      {name:"iPhone", stock: 11}
    ],
    newProducts: {
      name: "",
      stock: 0
    }
  },
  methods: {
    changeStock(prod, newStock){
      if(newStock < 0){
        newStock = 0;
      }
      prod.stock = newStock;
    },
    remove(index){
      this.products.splice(index, 1);
    },
    add(){
      this.products.push(this.newProducts);
      this.newProducts = {
        name: "",
        stock: 0
      };
    },
  },
}


// 创建一个vue实例(变量app接收了vue实例)
const app = new Vue(config);


1、vue 实例

我们想要使用 vue,先要创建一个 vue 对象,这个对象叫vue 的实例

1. 引入 vue.js 里面提供了一个构造函数 Vue

2. 通过  new Vue(config)  创建一个 vue 对象,得到的这个对象叫做“ vue 的实例”

3. 参数 config 是配置对象,就是要写一个对象来进行配置。配置 vue 里面要具有哪些功能,要做什么事情...各种配置


 app.title="修改标题" 

变量 app 接收了 vue 实例,

我们可以通过 vue 实例更改数据,数据改了页面就跟着改了,这就是我们说的响应式,数据的变化会导致界面重新渲染()


属性 title 在配置对象 config 的 data 属性里面,

 config.data.title = "修改title标题"   为什么不在 config.data.title 上修改,

 app.title = "修改标题"   而是在 vue 实例里面修改


打印 app 对象,

vue 实例里面有 title 属性,这个 title 属性怎么来的呢?


这涉及到响应式的原理

1. 当通过 new Vue(config)  创建 vue 实例的时候,会遍历 data 配置中的所有成员提升到 vue 实例里面

2. vue 实例里面的属性,全是通过属性描述符 Object.definedPropty() 来创建的,

    于是读取属性要经过 get 函数,属性赋值的要经过 set 函数,这样做是为了实现响应式

3. 因此使用 app.title 赋值的时候,vue 就知道修改了数据,应该重新渲染了


Ps:

vue 3 不在使用 Object.definedPropty() 方式的,使用 es6 里面的 proxy 对象代理


为什么在模板里面可以直接写 {{title}}  呢?

可以认为模板环境里面的 this 指向的就是 vue 实例,所以模板可以使用实例里面的所有东西


为什么实例里面有很多奇怪的属性名字?

由于配置里面的东西会提升到 vue 实例里面,为了防止命名冲突,vue 自身的成员名称前加上 $ 或 _

$ 开头是我们可以使用的

_ 开头是 vue 内部使用的


比如,data 里面有一个 children 属性,

vue 实例里面自带了 $children 属性,

如果不加 $,我们的 children 提升后就会把 $children 覆盖了


在我们创建 vue 实例的时候,会把下面的配置成员提升到 vue 实例里面

1. data 配置,提升是为了实现响应式

2. methods 配置,提升是为了在模板中方便使用

3. computed 计算属性

4. prop 属性,为了实现响应式


2、配置对象

配置对象 config 里面的配置

配置说明
template渲染的模板类型是字符串,字符串写的是什么就渲染什么或者说就展示什么,模板里面是要展示的东西
el配置要控制的元素写的是一个css选择器,就是控制那个元素的渲染
data管理的数据就是配置我们要控制的数据,该数据是响应式的 
methods配置方法方法中的 this 指向的是 vue 实例。不能使用箭头函数,会干扰  this 的绑定
render

computed计算属性


挂载的配置,挂载有两种方式

1. 通过 el: "#app" 进行配置

2. 使用 vue 实例中的 $mount 函数进行配置


模板的配置

1. 如果不配置 template 属性,可以把模板写到页面 #app 里面

2. 在 tempalte 配置中书写(常见)

3. 在 render 中手动用函数创建,render 函数的参数是一个创建虚拟 dom 的方法


render 函数的参数是一个函数,该函数帮我们创建元素,

比如,创建一个 h1 元素,我们配置的模板 template 失效了,页面变成 h1 元素了,也就是说真正起作用的是 render 配置

const template = `<div>
  <ul>
    <li v-for="(item, index) in products" :key="index">
      <span>{{item.name}}</span>
    </li>
  </ul>
</div>`;

const config = {
  template,
  el: "#app",
  data: {
    products: [
      {name:"HuaWei", stock: 10},
      {name:"MI", stock: 8},
      {name:"iPhone", stock: 11}
    ],
  },
  render(createElement){
    return createElement("h1", "hellow!!!");
  },
}

const app = new Vue(config);


我们没有写 render 配置,才会读取模板配置 template,然后帮我们生成 render

这也提醒我们,

render 函数的参数是一个创建虚拟 dom 对象的方法,该方法 createElement 创建的是虚拟 dom,

也就是说 template 匹配里面写的都不是真实的 dom,真实的 dom 里面是没有 v-for 等这些指令的


为什么需要虚拟 dom?

因为真实的 dom 操作特别慢,虚拟 dom 就是一个普通的 js 对象,


至少要知道,

template 模板里面不是真实的 dom,

template 模板里面的内容通过 createElement 方法生成虚拟 dom


3、模板 template

插值,

在模板元素内部使用 {{js表达式}} 大胡子语法,模板内部默认就是 vue 实例环境


指令,

通常作为元素的属性存在,名称上以 v- 开头

指令说明
v-for用于循环生成元素
v-on用于注册事件,@语法糖,比如,input文本改变事件,不断打字就会不断的触发
v-if用于判断该元素是否生成,可以和 v-else 联用,或是 v-if-else 联着用
v-show用于判断该元素是否显示,不显示的时候 dispaly:none
v-bind用于绑定属性,如果属性来自于js表达式,语法糖 :
v-model语法糖,用于实现双向绑定,实际上是自动绑定了 :value 属性值和注册了 @input 事件
v-html


注意,

vue2 只支持单个根元素,

否则会报错 Component template should contain exactly one root element.


数据 data 里面配置的商品数组 products: [...],每一个商品是一个对象,可以在模板里一个一个的手写的商品数据,而且数据具有响应式

// 模板
const template = `
<div>
  <h1>{{title}}</h1>
  <ul>
    <li>{{products[0].name}}</li>
    <li>{{products[1].name}}</li>
    <li>{{products[2].name}}</li>
  </ul>
</div>`;


// 配置对象
const config = {
  template,
  el: "#app",
  data: {
    title: "商品和库存",
    products: [
      {name:"HuaWei", stock: 10},
      {name:"MI", stock: 8},
      {name:"iPhone", stock: 11}
    ],
  },
}

// 创建一个vue实例
const app = new Vue(config);


数据是具有响应式的

 app.products[0] = {name: "锤子", stock: 1}    不能这样赋值,不能直接给 app.products[0] 赋值(下面会说为什么不能)

 app.products[0].name = "锤子"    可以修改 name 属性


v-for

一个一个的写太麻烦了,一般使用 v-for 循环商品的数据


循环生成 li 元素   v-for="变量名item  关键字in  数组products"   

1. v-for 语法的意思是循环 products 数组

2. 每循环一次把数组里面的数据,就是当前对应的对象拿出来放到变量 item 里面

3. 然后生成一个 li

const template = `
<div>
  <h1>{{title}}</h1>
  <ul>
    <li v-for="(item, index) in products" :key="index">
      <span>{{item.name}}</span>
      <span>{{item.stock}}</span>
    </li>
  </ul>
</div>`;


 app.products.push({name: "锤子", stock: 1}) 

数据是有响应式的,

增加一项更改了这个 products 数据,vue 会收到通知,然后会重新渲染出新的数据


但是这里有一个疑问的,

为了响应式 products 提升到实例里面了,

重新给属性 app.products 赋值时 vue 会收到通知,

但是这里没有给 app.products 重新赋值,我们是调用的是 js 的数组的 push 方法


vue 并不知道我们调用了 js 数组的 push 方法,

所以,vue 重写了数组里面很多的方法,对比一下两个 push 不是一个方法

 app.products.push === Array.prototype.push  返回 false


这个叫“ vue 的数组变异”,

所以对数组的各种操作的时候,vue 都能收到通知


回到上面,因为没有给 [0] 索引加上 definedPropty,所以使用索引给数组赋值的时候,虽然数据变了,但是 vue 收不到通知

 app.products[0] = {name: "红米手机", stock: 200} 


 app.products[0].name = 123 

但是可以更改对象的属性是没问题的,直接更改索引 vue 收不到通知


v-on

vue 里面通过指令 v-on,给元素注册事件,

现在我们不用写 dom 的操作,只关注数据就可以了,数据是响应式的,item.stock-- 数据变了,自然而然生成界面,

所以变的非常的简单,点击按钮改变数据  <button v-on:click="item.stock--">-</button> 

// 模板
const template = `
<div>
  <h1>{{title}}</h1>
  <ul>
    <li v-for="(item, index) in products" :key="index">
      <span>{{item.name}}</span>
      <button v-on:click="item.stock--">-</button>
      <span>{{item.stock}}</span>
      <button v-on:click="item.stock++">+</button>
    </li>
  </ul>
</div>`;


代码比较少写在行间,如果代码比较多,也可以写一个函数,

函数写在 methods 配置里面,

然后在模板的 v-on:click 这里可以调用了这个函数  @click="直接写函数名"

// 模板
const template = `
<div>
  <h1>{{title}}</h1>
  <ul>
    <li v-for="(item, index) in products" :key="index">
      <span>{{item.name}}</span>
      <button @click="changeStock(item, item.stock-1)">-</button>
      <span class="stock">{{item.stock}}</span>
      <button @click="changeStock(item, item.stock+1)">+</button>
    </li>
  </ul>
</div>`;


// 配置对象
const config = {
  template,
  el: "#app",
  data: {
    title: "商品和库存",
    products: [
      {name:"HuaWei", stock: 10},
      {name:"MI", stock: 8},
      {name:"iPhone", stock: 11}
    ],
  },
  methods:{
    changeStock(prod, newStock){
      if(newStock < 0){
        newStock = 0;
      }
      prod.stock = newStock;
    },
  },
}


为什么 methods 方法里面的属性,模板里也可以直接使用呢?

打印 vue 实例 app,

实例里面除了 products、title,还可以看到 changeStock 方法,

methods 配置里面的方法也会提升到 vue 实例中,目的是为了在模板中可以方便使用,方法提升是为了可以在模板中调用


另外,

方法里面的 this 指向的是 vue 实例(点击一下按钮,返回 true)

methods: {
  changeStock(prod, newStock){
    console.log(this === app); //true
  },
},


v-if

用于判断元素是否生成,v-if 和 v-else 连着用

v-if="item.stock > 0" 意思是条件满足,当大于 0 的时候,条件满足生成 span 元素

v-else 条件不满足,生成 i 元素

// 模板
const template = `
<div>
  <h1>{{title}}</h1>
  <ul>
    <li v-for="(item, index) in products" :key="index">
      <span style="width:70px;">{{item.name}}</span>
      <button @click="changeStock(item, item.stock-1)">-</button>
      <span v-if="item.stock > 0" class="stock">{{item.stock}}</span>
      <i class="soldout" v-else>售罄</i>
      <input type="number" min="0" v-model="item.stock"/>
      <button @click="changeStock(item, +item.stock+1)">+</button>
    </li>
  </ul>
</div>`;


v-show

要写两个 v-show 指令,才能实现上面 v-if 同样的效果

v-show="item.stock > 0"  大于 0 的时候显示 span 元素

v-show="item.stock === 0" 等于 0 的时候显示 i 元素

const template = `
<div>
  <h1>{{title}}</h1>
  <ul>
    <li v-for="(item, index) in products" :key="index">
      <span>{{item.name}}</span>
      <button @click="changeStock(item, item.stock-1)">-</button>
      <span v-show="item.stock>0" class="stock">{{item.stock}}</span>
      <i class="soldout" v-show="item.stock===0">售罄</i>
      <button @click="changeStock(item, item.stock+1)">+</button>
    </li>
  </ul>
</div>`;


v-if 和 v-show 的区别

v-show 条件不满足的时候 display: none

v-if 条件不满足,不生成元素


如果都是 span 元素,也可以写三目运算

<span>{{item.stock>0? item.stock : "售罄"}}</span>


v-bind

input 元素文本框显示库存,要设置 value 属性的值,

不能这样写  value="item.stock" ,这样设置的是 html 元素的属性,写什么就显示什么,

属性的值来源于 js 代码,要使用 v-bind 指令绑定属性 v-bind:value="item.stock",也可以使用语法糖 :value="item.stock"

<input type="number" min="0" :value="item.stock"/>


问题还没有完,

文本框的数字变了后,商品的库存也要跟着变,


也就是说,

还要注册 input 事件,只要不停的打字就会不断触发该事件

@input="handleInput"  绑定一个事件处理函数,在事件函数里面可以获取事件对象 e


事件函数有两种调用方式

1. 之前是调用的方式,可以传一些额外的参数,比如按钮上面的事件 @click="changeStock(item, item.stock-1)"

2. 现在直接写函数名 @input="handleInput",该方式会自动把原生的事件参数 e 带到事件函数里面


e 是事件对象,

e.target 是 dom 元素对象 

// 模板
const template = `
<div>
  <h1>{{title}}</h1>
  <ul>
    <li v-for="(item, index) in products" :key="index">
      <span>{{item.name}}</span>
      <button @click="changeStock(item, item.stock-1)">-</button>
      <span v-show="item.stock>0" class="stock">{{item.stock}}</span>
      <i v-show="item.stock===0">售罄</i>
      <input type="number" min="0" @input="handleInput" :value="item.stock"/>
      <button @click="changeStock(item, item.stock+1)">+</button>
    </li>
  </ul>
</div>`;

// 配置对象
const config = {
  template,
  el: "#app",
  data: {
    title: "商品和库存",
    products: [
      {name:"HuaWei", stock: 10},
      {name:"MI", stock: 8},
      {name:"iPhone", stock: 11}
    ],
  },
  methods:{
    changeStock(prod, newStock){
      if(newStock < 0){
        newStock = 0;
      }
      prod.stock = newStock;
    },
    handleInput(e){
      console.log(e); // e.target.value拿到新的库存
    }
  },
}

// 创建一个vue实例
const app = new Vue(config);


 <input type="number" min="0" @input="handleInput(item, $event)"  :value="item.stock" />

通过 e.target.value 拿到新的库存,然后赋值给对应的商品对象

1. @input="handleInput(item, $event)" 传两个参数,

    $event 表示是事件对象,通过事件源拿到新的库存

    item 当前商品的数据

2. 拿到新的库存后赋值给 item.stock  item.stock = +event.target.value

// 模板
const template = `
<div>
  <h1>{{title}}</h1>
  <ul>
    <li v-for="(item, index) in products" :key="index">
      <span>{{item.name}}</span>
      <button @click="changeStock(item, item.stock-1)">-</button>
      <span v-show="item.stock>0" class="stock">{{item.stock}}</span>
      <i v-show="item.stock===0">售罄</i>
      <input type="number" min="0" @input="handleInput(item, $event)" :value="item.stock"/>
      <button @click="changeStock(item, item.stock+1)">+</button>
    </li>
  </ul>
</div>`;

// 配置对象
const config = {
  template,
  el: "#app",
  data: {
    title: "商品和库存",
    products: [
      {name:"HuaWei", stock: 10},
      {name:"MI", stock: 8},
      {name:"iPhone", stock: 11}
    ],
  },
  methods:{
    changeStock(prod, newStock){
      if(newStock < 0){
        newStock = 0;
      }
      prod.stock = newStock;
    },
    handleInput(item, event){
      // console.log(item, event);
      item.stock = +event.target.value;
    }
  },
}

// 创建一个vue实例
const app = new Vue(config);


也可以在 input 元素行间完成  item.stock = $event.target.value

拿到 value 改变后的值,直接赋值给 item.stock 库存,

因为数据 item.stock 是响应式的,数据一变 value 绑定的值也跟着变 :value="item.stock" ,这样就完成了双向绑定

<input type="number" min="0"
 :value="item.stock"
 v-on:input="item.stock = $event.target.value"
/>


什么是双向绑定?

文本框的值来自于数据 :value="item.stock"

文本框一变化,数据 $event.target.value 也会跟着变化

数据决定了文本框显示什么,文本框的操作决定了我们的数据是什么,这是是双向绑定

界面影响数据,数据也会影响界面


v-model

双向绑定,平时用 v-model 指令简写,效果完全一样,

v-model="item.stock" 就是一个语法糖,1.绑定 :value 属性,2.并自动注册 @input 事件

<input type="number" min="0" v-model="item.stock" />


删除一个商品

从数组里面移除一项,this 指向 app 对象,所以 methods 方法里面的函数不能用箭头函数,会干扰 vue 绑定 this

remove(index){
  this.products.splice(index, 1);
},


添加商品

按照以前的想法,获取 input 元素 value 的值,然后往 products 商品数组中加一项,

想法就错了,

vue 的一切全是数据,

连添加的东西也是数据,在 data 配置里面要写一个 newProducts 对象,表示添加的新商品。一切都是数据,界面是根据数据来生成的

// 模板
const template = `
<div>
  <h1>{{title}}</h1>
  <div>
    商品名称:<input type="text" v-model="newProducts.name">
    商品数量:<input type="number" v-model.number="newProducts.stock">
    <button @click="add">添加</button>
  </div>
  <ul>
    <li v-for="(item, index) in products" :key="index">
      <span style="width:70px;">{{item.name}}</span>
      <button @click="changeStock(item, item.stock-1)">-</button>
      <span v-if="item.stock>0" class="stock">{{item.stock}}</span>
      <i v-else>售罄</i>
      <input type="number" min="0" v-model="item.stock"/>
      <button @click="changeStock(item, +item.stock+1)">+</button>
      <button @click="remove(index)"> del </button>
    </li>
  </ul>
</div>`;

// 配置对象
const config = {
  template,
  el: "#app",
  data: {
    title: "商品和库存",
    products: [
      {name:"HuaWei", stock: 10},
      {name:"XiaoMi", stock: 8},
      {name:"iPhone", stock: 11}
    ],
    newProducts: { // 每个商品都是一个对象,新添加的商品不例外,它也是一个对象
      name: "",
      stock: 0
    }
  },
  methods: {
    changeStock(prod, newStock){
      console.log(this === app); //true
      if(newStock < 0){
        newStock = 0;
      }
      prod.stock = newStock;
    },
    remove(index){
      this.products.splice(index, 1);
    },
    add(){
      this.products.push(this.newProducts);
      this.newProducts = {
        name: "",
        stock: 0
      };
    },
  },
}


修饰符

number 表示转成数字

trim 表示去掉收尾空格


v-html

vue 为了安全,会将元素内部的 {{插值}} 进行实体编码

<div id="app"></div>

<script src="https://20180416-1252237835.cos.ap-beijing.myqcloud.com/common/cdn/vue.min.js"></script>
<script>
  const template = `<div>{{html}}</div>`;

  new Vue({
    el: "#app",
    template,
    data:{
      html: '<p style="background:#f40;"><span style="color:#fff;">带标签的内容</span></p>'
    }
  });
</script>

为了防止用户输入一些标签,导致页面混乱,甚至加一些 js 代码,进行注入攻击(xss攻击),

vue 会在 {{插值}} 的位置自动进行实体编码,因此我们看到 span 元素是编码后的结果(右键 Edit as HTML)

<div>&lt;p style="background:#f40;"&gt;&lt;span style="color:#fff;"&gt;带标签的内容&lt;/span&gt;&lt;/p&gt;</div>


什么是插值?

插值是在模板的元素内部,使用花括号 {{js...}} 语法,插值花括号里面使用一个 js 表达式算出来的结果


如果可以信任这个数据,使用 v-html 指令,

v-html="html" 相当于 div.innerHTML = html,一般用在富文本框提交的内容

const template = `<div v-html="html"></div>`;

new Vue({
  el: "#app",
  template,
  data:{
    html: '<p style="background:#f40;"><span style="color:#fff;">带标签的内容</span></p>'
  }
});


4、计算属性 computed

通过 firstName 和 lastName 拼接全名

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://20180416-1252237835.cos.ap-beijing.myqcloud.com/common/vue.min.js"></script>
<title>计算属性</title>
</head>
<body>

<div id="app"></div>

<script>

const template = `<div>
    <p>姓:{{firstName }}</p>
    <p>名:{{lastName}}</p>
    <p>全名</p>
    <p>模板中写表达式:{{firstName + lastName}}</p>
    <p>调用方法:{{getFullName()}}</p>
  </div>`

const config = {
  template,
  el: "#app",
  data:{
    firstName: "莫",
    lastName: "尼卡"
  },
  methods:{
    getFullName(){
      console.log("方法调用了");
      return this.firstName + this.lastName;
    }
  }
}

var app = new Vue(config);

</script>
</body>
</html>


拼接全名有三种方法

1. {{firstName + lastName}}

直接在大胡子语法里面写表达式的好处是,修改了姓 app.firstName = "Mo" 全名也跟着改了,因为数据是响应式的,修改后会重新渲染


2. {{getFullName()}}

但是如果计算很复杂,在 methods 配置里面写一个 getFullName 方法,然后在模板里面可以调用 getFullName() 函数


methods 里面的函数是没有响应式的,但是 data 数据有响应式,

当修改了数据 app.firstName = "Mo" 模板要重新渲染,重新渲染就会重新调用 getFullName() 函数


3. 计算属性

在 computed 配置里面写一个 fullName 函数,该函数在模板里面当做属性来调用

const template = `<div>
    <p>姓:{{firstName }}</p>
    <p>名:{{lastName}}</p>
    <p>全名</p>
    <p>模板中写表达式:{{firstName + lastName}}</p>
    <p>调用方法:{{getFullName()}}</p>
    <p>计算属性:{{fullName}}</p>
    <p>{{n}}</p>
    <button @click="n++">加</button>
  </div>`

const config = {
  template,
  el: "#app",
  data:{
    firstName: "莫",
    lastName: "尼卡",
    n: 0
  },
  computed:{
    fullName(){
      console.log("属性重新计算了");
      return this.firstName + this.lastName;
    }
  },
  methods:{
    getFullName(){
      console.log("方法调用了");
      return this.firstName + this.lastName;
    }
  }
}

var app = new Vue(config);


关于计算属性

1. 计算属性里面的配置,也会提升到 vue 实例中,

    因此,在模板里面可以直接当做“属性”使用, 

    使用时,实际上调用的是对应的方法

2. 通常情况下,计算属性里面需要的数据,来自于“data”或其他的“计算属性计算”得到的数据(否则就尽量不要用计算属性了)


计算属性与方法的区别(面试容易问到的问题):

区别一

vue 会检查计算属性的依赖,当依赖没有发生变化时,vue 会直接使用之前缓存的结果,而不会重新计算,这是为了提高效率,

如果将来的计算比较复杂,代码很多,计算需要耗费一定时间,

只要 firstName 或 lastName 两个依赖的项不发生变化,就不会调用计算属性的函数


比如点击按钮,n++ 数据变了,模板中的 {{n}} 数据变了肯定要重新渲染

1. 计算属性只是一开始调用了一次,点击按钮重新渲染页面,但没有重新调用计算属性的函数,

2. 因为计算属性依赖的两数据没有变,两个依赖没有变换,计算属性就不会重新计算,

    如果修改了依赖  app.firstName = "Moo",计算属性重新运行了

3. 而每一次点击按钮,都会重新调用方法


所以说,除非处理事件,否则能用计算属性尽量用计算属性,因为计算属性的效率更高


计算属性的原理

1. 因为 data 里面的配置会提到 vue 实例里面,

    在实例里面这些提升的属性是用 definedPropty 定义的,读取属性的时候 get 函数会监听到

2. 于是当发现用计算属性 {{fullName}} 的时候,就知道调用了 computed 配置的 fullName 函数,

    这时候 vue 会做一张表进行缓存 fullName 属性

3. 在 get 函数里面,检查有没有调用 firstName 或 lastName 这两个依赖,

     如果没有调用,在检查缓存表里面有没有 fullName,有就会调用计算属性对应的函数,没有才会重学调用


区别

计算属性的读取函数,不可以有参数(比如读取属性值,参数也没有意义)

methods 方法可以有任意的参数


区别

计算属性可以配置 get 和 set,分别用于读取和设置


如果给计算属性赋值

1. 计算属性就不是函数了,需要配置是为一个对象

2. 对象里面有两个函数 set 和 get 

3. 读取计算属性的时候会运行 get 函数得到结果,get 函数必须要有 return 返回

4. set 函数需要一个参数 set(newVal),参数就是给计算属性赋的值

computed:{
  fullName:{
    get(){
      console.log("属性重新计算");
      return this.firstName + this.lastName; // 读取属性的时候必须要有return返回
    },
    set(newVal){
      // console.log(newVal);
      // 因为计算属性是根据依赖生成的,改全名改的也是依赖
      this.firstName = newVal[0]; // 取第一个字符
      this.lastName = newVal.substr(1); // 从第2个字符开始截取
    }
  },
},


v-model="fullName" 使用双向绑定计算属性,

只要文本框的内容一变化,v-model 指令内部会给计算属性重新赋值 app.fullName = "Mo尼卡"

const template = `<div>
  <p>姓:{{firstName }}</p>
  <p>名:{{lastName}}</p>
  <p>计算属性拼接:{{fullName}}</p>
  <input type="text" v-model="fullName"/>
</div>`

const config = {
  template,
  el: "#app",
  data:{
    firstName: "莫",
    lastName: "尼卡",
  },
  computed:{
    fullName:{
      get(){
        console.log("属性重新计算");
        return this.firstName + this.lastName; // 读取属性的时候必须要返回
      },
      set(newVal){
        // console.log(newVal);
        // 因为计算属性是根据依赖生成的,改全名改的也是依赖
        this.firstName = newVal[0]; // 取第一个字符
        this.lastName = newVal.substr(1); // 从第2个字符开始截取
      }
    },
  },
}

var app = new Vue(config);


 app.fullName = "Moo尼卡"   赋值的时候实际调用 set 函数,并把新的值传进去了 fullName.set("Moo尼卡") 

 app.fullName   读属性候直接调用的是 get 函数,没有参数 fullName.get()


5、面试题

1、vue实现数据响应式的原理


响应式的意思是,

界面的渲染,它要观测数据的变换,

数据变换的时候,页面自动完成渲染


怎么实现的响应式的呢?

把配置对象里面的东西用 Object.definedPropty() 函数定义到 vue 实例里面,

因此使用 vue 实例里面响应的属性时候,通过 Object.definedPropty() 监听到数据的读取和设置,通过这种方式实现响应式的。


2、vue为什么要在自身成员前面加上 $ 符号?


3、vue配置中的方法(methods),this指向谁?


4、render、template、在页面中直接写模板,这三种方式有什么区别,优先级是什么?


render、template 是写在配置对象里面

页面中写模板,直接是写在页面上真实 dom 里面


render 是最核心最底层的方法,

template、页面中写模板,他们最终都会转换成 render 方法,


优先级是,

如果配置了 render 一定运行它,而忽略掉 template、页面中写模板

如果没有写 render,写了 template 就使用它,不使用页面中写的模板

如果前面两个都没有写,就读取页面上的模板


二、组件

vue 认为在复杂的系统,也一定可以往小了划分,

每一个划分的块叫做组件,组件可以降低开发的复杂度


使用组件分三步,

组件的创建好后,还要注册后,注册后才能使用


1、组件的创建和注册

什么是组件?

组件是页面中的一个可复用的功能单页,组件可以重复使用


对于开发者而言,组件就是一个配置对象(pager组件),跟 new Vue({}) 的配置类似

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>创建组件</title>
</head>
<body>

  <div id="app"></div>

  <script src="https://20180416-1252237835.cos.ap-beijing.myqcloud.com/common/vue.min.js"></script>
  <script>

    const pager = {
      template: `<h3>这是组件的内容<h3>`
    }

    new Vue({
      el: "#app",
      template: `<div></div>`
    });

  </script>

</body>
</html>


组件创建好后,还要注册后,注册后才能使用


组件的注册有两种方式

1. 全局注册

2. 局部注册


对于目前,

直接在页面上引用 vue.js 的方式,两种组件注册的区别不大

用脚手架搭建工程时,全局注册和局部注册会有影响,会影响页面的响应速度,最终打包部署的时候会影响 js 文件的体积,


原则上推荐局部注册,不推荐全局注册,

除非是太通用的组件,就是很多页面都要用到这个组件,否则尽量使用局部注册


全局注册组件


 component ("组件名称",配置对象)    构造函数 Vue 里面提供了一个 component 方法

1. 组件库名称,名称是一个字符串(注意,组件名称不是组件配置对象的名称)

2. 组件配置对象

const page = {
  template: `<p>page content</p>`
}

Vue.component("MyPager", page); // 全局注册组件

const config = {
  template: `<div>
    <MyPager></MyPager>
  </div>`,
  el: "#app",
}

new Vue(config);


组件名称的命名规范,以下方式任选其一

1. 短横线命名

2. 大驼峰命名法

所以组件不能用小驼峰命名法的


一个单词

Vue.component("pager", page)   短横线命名,小写,因为只有一个单词没写短横线

Vue.component("Pager", page)   大驼峰命名,首字母大写

二个单词

Vue.component("my-pager", page)   短横线

Vue.component("MyPager", page)   两个单词首字母大写


局部注册


在使用的组件 或 vue 实例中,通过 components  注册,该配置是一个对象

属性名,MyPage 表示的是组件名

属性值,page 是组件的配置对象

const page = {
  template: `<p>page content</p>`
}

const config = {
  components: {
    MyPager: page, // 局部注册
  },
  template: `<div>
    <!-- 使用组件 -->
    <MyPager></MyPager>
    <my-pager></my-pager>
    <MyPager/>
    <my-pager/>
  </div>`,
  el: "#app",
}

new Vue(config);


组件的使用


把组件当做标签使用即可,标签名任选其一

1. 短横线命名法

2. 大驼峰命名法


注册组件时候,名字 MyPager 用的是大驼峰 ,使用组件的时可以用以下任意一种

<MyPager></MyPager>

<my-pager></my-pager>

<MyPager/>

<my-pager/>


命名规范不是强制性的要求,为什么官方要求用大驼峰命名法(Pascal Case)?

防止组件的名字和 html 元素名字重名,比如组件名 Li,用小驼峰写容易和 html 元素的 li 同名


2、补充知识,ES6模块化

为什么要使用模块化?


面对大型项目,传统开发的问题

1. 如何管理错综复杂的代码


传统工程里面代码在多,也不可能分太多的 js,因为会造成请求数量的增加,

图片、js... 这些都是资源,这些请求太多会造成页面卡顿,所以不能有太多的 js,所以代码就放到几个 js 里面,就特别不好维护,


2. 如何处理全局变量污染问题


以前的传统工程里面,一个大型页面一般 10~20 左右的 js,

这些 js 文件里面充斥着不同开发者写的代码,有一些避免不了的 var 变量或全局函数,因为要留给别人做接口,必须要做成全局变量,

全局变量会污染,污染就会导致重复。以前用一些命名规范来解决,这种解决是很弱的,稍不注意就会出问题


3. 如何管理复杂的依赖关系


比如 jQuery 相关的 jQueryUI 库就是依赖关系,

页面上要按照顺序导入这些 js 文件,如果库一旦多了 20 ~ 30 个 js 要管理这些依赖关系


以上这些问题要依托于模块化来解决,

模块化真正让前端起飞,让 js 成为一个像样的语言


实现模块化的方式有这样几种

  - CommonJS 跟 nodejs 环境绑在一起

  - AMD 

  - CMD

  - ES6 官方规范


ES6 实现模块化

1. script 元素加上  type="module"  浏览器会当做一个模块化来解析。模块中的变量是局部的,只能在模块内部使用

2. 模块导出   export default 导出的数据  每个模块只能导出一次,如果数据多用对象

3. 模块导入  import 变量名 from 模块路径  注意,在所有代码之前写导入


3、组件的嵌套

使用在页面引用 vue.js 的方式创建工程


工程结构

|- my-site 

  |- src

  |    |- assets

  |    |    |- vue.js

  |    |    |- index.css

  |    |

  |    |- app.js

  |    |- main.js

  |    |- movieList.js

  |    |- pager.js

  |

  |- index.html


indxe.html

我们要运行的页面,页面里面写一个 div#app

引入 vue.js 文件,它不是模块化的

引入 css 文件

模块化引入 mian.js 入口文件

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<link rel="stylesheet" href="./src/assets/style.css" />
</head>
<body>

  <div id="app"></div>
  <script src="./src/assets/vue.js"></script>
  <script src="./src/main.js" type="module"></script>

</body>
</html>


src

我们通常建一个 src 文件夹,vue 代码和我们自己写的代码放 src 目录下 


assets 

该文件夹叫嵌入式的资源,或者叫静态资源,

通常会放一些我们要引入的第三方库,或者是 css 代码等,

当然以后用构建工具时,它里面不会放第三方库了,一般放一些静态资源,图片、css 等


mian.js 启动文件

只负责创建一个 vue 实例(也叫启动 vue),配置就渲染一个根组件,其它什么都不做

// 该模块只负责启动vue和启动时的配置,所有的界面交给app.js来渲染
import App from "./app.js";

new Vue({
  template: `<App />`,
  components:{
    App,
  },
  el: "#app",
});


app.js 根组件

整个页面的内容靠根组件来完成,

导入 <MovieList/> 和 <Pager/> 两个组件,导出的是一个组件配置对象

import MovieList from "./movieList.js";
import Pager from "./pager.js";

const template = `<div>
  <MovieList/> <!-- 这里也可以用movie-list短横线 -->
  <Pager/>
</div>`;

export default {
  template,
  components:{
    MovieList,
    Pager
  }
}


js 跟模板的有两种写法

 html in  js   在 js 中写模板,React 使用的方案

 js in html    在模板里面写 js 代码,这是 vue 使用的方案


袁老师说:

当我们用 js 在 html 里面书写时候,

虽然可以用小驼峰,但组件名尽量用大驼峰 MovieList,不然会出问题的,避免跟 html 元素名重复


movieList.js

该组件模块主要负责渲染电影列表,里面渲染了很多电影,用到了 <Movie/> 组件

// 渲染电影列表
import Movie from "./movie.js";

const template = `<div>
    <h3>电影列表</h3>
    <Movie/>
    <Movie/>
    <Movie/>
  </div>`;

export default {
  template,
  components:{
    Movie,
  }
}


movie.js

电影列表里面要用到的单个电影组件

const template = `<div>单个电影</div>`;

export default{
  template,
}


pager.js

分页组件

const template = `<div>分页组件</div>`;

export default {
  template,
}


我们的整个工程的结构

顶层是 app 根组件

顶层 app 组件里面渲染了 MovieList 和 Pager 两个组件,

MovieList 渲染的时候,它又渲染了多个 Movie 组件


|- App

  |- MovieList

  |   |- Movie

  |   |- Movie

  |   |- Movie

  |

  |- Pager


组件可以嵌套重复使用,

因此,会形成一个树形结构,我们叫“组件树”,

树的根叫做“根组件”


三、组件间的数据通信

1. 分页组件

2. 组件的状态和属性(课程大纲的知识点)

3. 什么是单项数据流

4. 自定义事件(课程大纲的知识点)

5. 使用 v-model 指令进行组件通信

6. 在组件模板中使用 import 导入的数据 


1、分页组件

先看一个分页组件的示例

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://20180416-1252237835.cos.ap-beijing.myqcloud.com/common/vue.min.js"></script>
<style>
.pager{text-align:center;font-size:0;}
.pager .pager-item {display:inline-block;padding:5px 10px;border:1px solid #ccc;margin:0 5px;cursor:pointer;color:rgb(96, 96, 224);font-size:12px;user-select:none;}
.pager .pager-item.disabled{color:#ccc;cursor:not-allowed;}
.pager .pager-item.active {color:#f40;border-color:#f40;cursor:auto;background:rgba(255, 255, 0, 0.3);}
</style>
<title>分页</title>
</head>
<body>

<div id="app"></div>

<script>

// pager 组件
const Pager = {
  template: `
  <div class="pager">
    <a @click="changePage(1)" class="pager-item" :class="current === 1 ? 'disabled' : ''">首页</a>
    <a @click="changePage(current - 1)" class="pager-item" :class="{disabled: current === 1}">上一页</a>
    <a class="pager-item"
      @click="changePage(item)"
      :class="{active: item === current}"
      v-for="(item, i) in numbers" :key="i"
    >
      {{item}}
    </a>
    <a @click="changePage(current + 1)"class="pager-item" :class="{disabled: current === pageNumber}">下一页</a>
    <a @click="changePage(pageNumber)" class="pager-item" :class="{disabled: current === pageNumber}">尾页</a>
  </div>`,
  props: {
    current: {
      type: Number,
      default: 1
    },
    pageSize: {
      type: Number,
      default: 10
    },
    total: {
      type: Number,
      required: true
    },
    panelNumber: {
      type: Number,
      default: 5
    }
  },
  computed:{
    pageNumber(){
      return Math.ceil(this.total / this.pageSize);
    },
    numbers(){
      var min = this.current - Math.floor(this.panelNumber / 2);
      if(min < 1){
        min = 1;
      }
      var max = min + this.panelNumber - 1;
      if(max > this.pageNumber){
        max = this.pageNumber;
      }
      const arr = [];
      for (let i = min; i <= max; i++) {
        arr.push(i);
      }
      return arr;
    }
  },
  methods:{
    changePage(newPage){
      if(newPage < 1){
        newPage = 1;
      }else if(newPage > this.pageNumber){
        newPage = this.pageNumber;
      }
      /**
       * $this.current = newPage 不可以这样直接修改属性
       * 应该改变页码了!,但是由于数据不是我们的,我不能改,
       * 所以,只能触发事件,让使用 Pager 组件的父组件收到通知
       * 
       */
      this.$emit("change", newPage);
    }
  }
};

// App 组件
const App = {
  template:`<div>
  <Pager 
    :panelNumber="7"
    :current="current" 
    :total="total"
    :page-size="pageSize"
    @change="current = $event"
  /></div>`,
  components:{
    Pager,
  },
  data(){
    return {
      current: 1,
      total: 320,
      pageSize: 10
    }
  },
  methods:{
    
  }
}

new Vue({
  template: `<div>
    <h1 style="text-align:center;">App 组件</h1>
    <App/>
  </div>`,
  components:{
    App,
  },
  el: "#app"
});

</script>
</body>
</html>


绑定 class

v-bind 指令

简写 :

绑定 :class 主要有两种写法


1. 字符的写法

:class="这里面是 js 表达式" 会把绑定的结果 abc 追加到前面的 class 里面

<a class="pager-item" :class="'abc'">首页</a> 直接写字符串 'abc'


既然是绑定字符串,就可以用三目运算符

<a class="pager-item" :class="current === 1 ? 'disabled' : ''">首页</a>


2. 对象也是表达式,绑定里面也可以写对象

对象的属性名作为类样式的名称,属性值是布尔表达式,结果为 true 加 disabled,这样可以方便的控制多个类样式

<a class="pager-item" :class="{disabled: current === 1}">上一页</a>


指令修饰符

我们知道事件是一个 v-on 指令,

如果在 a 标签里面不小心写了 href 属性会导致页面刷新,点击事件可以加修饰符 .prenvent  阻止默认行为  v-on:click.prevent="changePage(current + 1)"

<a href="" @click.prevent="changePage(current + 1)" class="pager-item" :class="{disabled: current === pageNumber}">下一页</a>


修饰符是和指令联合使用的,vue 中不同的指令涉及到不同的修饰符,用修饰符可以增强或更改指令的某些功能

.prevent 用于 v-on 事件指令,表示阻止默认行为

.stop      也是用于 v-on 事件指令,表示阻止事件冒泡

.number  上面提到过,把用户表单填写的内容转换成数字类型

.native


中间数字页码显示的范围

范围取决与 current 当前页码,中间是当前页,当前页两边是范围,比如  4 5 6 7 8 范围是 4 - 8

还有 panelNumber 范围有多少个数字,

写一个计算属性 numbers 生成一个数组 [ 4 5 6 7 8 ]


先算最小值,最小值出来最大值自然就出来了


最小页码

panelNumbe 一共显示的数字是 5,5 / 2 向下取整为 2

当前页码是 6, 6 -2 最小页码是 4


最大页码

4 + 5 = 9   最小页码 4 加上一共显示多少页码数 panelNumbe

9 - 1 = 8    然后在减 1(因为包含最小页码 4 自身,所以要减1),最大页码是 8


计算属性 numbers,依赖 current 和 panelNumber,这两个其中一个变了就重新计算


然后从最小页码循环到最大页码,

每循环一次,往 push 数组里 pushe 一个数字


循环 numbers 生成中间的页码部分

v-for 循环数组 numbers,每循环一次把数字 {{item}} 拿出来

<a class="pager-item"
  @click="changePage(item)"
  :class="{active: item === current}"
  v-for="(item, i) in numbers" :key="i"
>
  {{item}}
</a>


:class="{active: item === current}"

绑定 :class 当前的页码的类样式 .active

循环生成的数字 item,是否等于当前页码 current,如果等于加上样式 .active


这说明 v-for 循环的优先级非常高,

它要先循环,在循环生成的过程中,才去确定 :class 绑定的类样式,不然都没有 item 会出问题


官方文档还强调了这一点

v-if 和 v-for 不要在同一个元素里面用,容易造成误解,

因为加上 v-if 后,我们可能觉得元素要么显示,要么不显示,

其实不是的,

元素是先进行循环,在每一次循环的时候决定元素是否要显示,循环的优先级非常高


:key 是内置属性,

当循环渲染自定义组件时候,

比如  <Movie v-for="item in moves" :key="item._id" />  几乎是必须使用该属性

并且提供唯一的值,通常是 id

以便 vue 提高渲染效率


2、组件的状态(data)和属性(props)

组件的数据就来自两个配置

1. props 属性,属性里面的数据组件自己不能改

2. data   状态,状态里面数据组件能改


data 组件的状态 


组件配置中的 data 是需要组件自身管理的数据,

我们把需要组件自身管理的数据 data,叫做组件的状态(component state)

这种叫法是从 react 里面来的,因为 vue 本身有很多地方在模仿 react,只不过做了很多改进


组件的状态的特点:

组件状态只能在组件内部使用,外部原则上不可以使用

组件状态 data(state 状态)它是属于组件内部的东西,只能在组件内部的使用,

跟外面的使用者没有任何关系,原则上外部不可以使用。外面也能通过 ref 得到,不过通常不会这样做


在组件里面配置 data,跟在 vue 实例里面配置 data 是有区别的


区域一,

在组件中 data 必须是一个函数,而 vue 实例中直接是一个对象

export default{
  template,
  data(){
    return {
      // 函数返回的对象是组件的状态
    }
  },
}


为什么组件的 data 不是一个对象?

因为组件会重复使用,

比如,<Movie/>  组件会多次重复使用,而且还有可能别的组件中也会使用到 <Movie /> 组件,

如果 data 配置成一个对象,会导致所有的 <Movie /> 组件共享一个对象地址,其中一个地方的 <Movie /> 组件变了,其他地方 <Movie /> 组件也会跟着变,

这是 vue 不希望看到的,vue 认为每一个组件是相互独立的,每个 <Movie/> 的数据是相互独立的,多个 <Movie /> 之间互不干扰


当组件中的 data 写成函数,

每调用一次组件,就会调用一次函数,得到组件的数据


为什么 vue 实例可以直接写成对象呢?

因为 vue 实例只有一个,而且一个 vue 实例对应到页面中的一个区域


区别二,

组件中可以有属性 props(component props),而 vue 实例中没有


props 组件的属性


组件的数据来源,

除了 data 之外还有一个属性 props


分页组件需要的属性(props)

1. current 当前页码

2. pageSize 页容量,每页显示的数据,比如新闻列表

3. total 数据总量,总共有多少条数据

知道上面这三条就可以做了分页了

4. panelNumber  页面显示的页码


通过 pageSize 页容量和 total 总页数,可以算出总页数,

写一个计算属性 pageNumber 计算总页数。两个依赖项任何一个变了,总页数 pageNumber 马上会重新计算重新渲染


组件属性(props )的命名

1. 声明组件的属性时,可以用小驼峰 pageSize 或短横线 page-size 命名

2. 传递组件属性时,也可以使用短横向或小驼峰


用数组的方式声明属性,

数组里面每一项是字符串,字符串里面的值就是属性的名称

props:["current", "pageSize", "total", "panelNumber"],


使用组件属性

1. 跟 html 元素里面传属性一样,使用的时候把属性传进去

2. 声明属性时用小驼峰,传递属性时可以短横线或小驼峰,这两个是互通的

<Pager :current="1" :total="100" :page-size="10" :panelNumber="5"/>


总结命名规范的区别

1. 组件  短横线或大驼峰,比如 App、MovieList、Pager

2. 属性(props)短横线或小驼峰,比如 curren、total、page-size、panelNumber

3. 状态(data)短横线或大驼峰(为了与 HTML 元素区别)


属性(props 还可以用对象的方式精细的控制

对象的属性名就是当前组件需要属性 props 名,然后对属性继续增加配置,对属性进行约束

props: {
  current: {
    type: Number, // 属性类型是数字
    default: 1 // 默认值
  },
  total: {
    type: Number,
    required: true // 必须传递
  },
  movies:{
    type: Array, // 属性类型是数组
    default: () => [] // 类型是数组或对象,默认值必须用一个函数生成,因为又是引用地址的问题
  }
}


如果属性定义的类型是 Number,传递的是字符串。会在控制台报错,这个叫开发错误,提醒的是写代码的人,不是给用户看的

[Vue warn]: Invalid prop: type check failed for prop "total". Expecte Number with value 100, got String with value "100"

type check failed 类型检查错误

Expecte Number 期望是一个 Number

got String with value "100"  但是传的是一个字符串 "100"


如果设置必填,没有填,会提示“你缺失了一个属性

[Vue warn]: Missing required prop "total"


使用属性(props)跟状态(data)是一样的,也是  this.属性 

说明属性(props)也会被提升到实例中,这个实例不是“vue实例”而是“vue组件实例”


vue 组件实例

1. 我们写的这个 export default {}  对象,我们习惯上叫组件,实际它不是组件,它叫做“组件配置对象”

2. 这个配置对象内部会帮我们生成一个“组件实例”,

    组件实例跟 new Vue() 一个“ vue 实例 ”差不多的意思

3. 组件实例里面也会挂载一些 data、motheds、props,所以在模板里面可以直接使用 {{total}}


[Vue warn]: Invalid default value for prop "movies": Prop with type Object/Array must use factory function to return the default value.

如果属性(props)是一个数组或一个对象,如果写默认值必须要用一个函数生成,

因为引用地址的问题,组件要用很多多次,如果直接写数组,默认值使用的就是同一个数组了


属性(props)是不允许组件自己修改的

[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "current"

这个报错非常重要,意思是应该避免直接更改属性

Avoid 避免

mutating 更改/变化

directly 直接

a prop directly 一个属性


  组件的属性是只读的,不允许更改。为什么有这样的理念呢?根本原因是要保证单向数据流  


因为组件是单项数据流,

只有数据的所有者才有权利修改这个数据,通过属性(props)传过来的数据,是不允许组件自己修改的


3、什么是单项数据流?

单项数据流,

就是数据从一个方向流入,从一个方向流出,就是单向数据流。单项数据流的的概念来自于函数式编程


比如,函数 sum 就是一个单项数据流

function sum(a, b){
  reurn a + b;
}

sum(2, 3);


当调用 sum 函数的时候

形参 a 和 b 不是数据,实参 2 和 3 是数据,

我们把数据传给函数后,

函数没有修改数据 2 和 3,只是 a + b 计算后返回一个新的数据,这就是单项数据流


什么情况下会破坏单项数据流呢?

实参传两个对象,两个数据传进去,没有更改任何数据,直接返回一个结果,这是单向数据流

function sum(obj1, obj2){
  reurn obj1.number + obj2.number;
}

sum({number:2}, {number:3});


如果在函数里面 obj1.number ++  操作了数据,就不是单向数据流了,这叫副作用操作

function sum(obj1, obj2){
  obj1.number ++; // 函数里面操作了数据,破坏了单向数据流
  reurn obj1.number + obj2.number;
}

sum({number:2}, {number:3});


副作用操作,

修改了参数 obj1.number ++,或动了外面的东西,或者使用异步,这些都可以叫做副作用


为什么不仅是 Vue,还有 React 都是要保证单向数据流呢?

因为单向数据流是最不容易出问题的,最容易被人类理解的,

以后我们开发的系统很复杂,组件会非常非常的多,组件的嵌套层次非常非常深,


可以这样理解一个函数调用另一个函数、这个函数又调用另一个函数,嵌套的层次非常非常深,

如果某一个数据出问题了,没有使用单向数据流,任何一个函数都有权利修改我们的数据,我们不知道是哪个函数把数据改了,关系错综复杂不好调试


如果是单项数据流,由于在函数里面、组件里面是不可能更改我的数据,

因为数据属于谁谁负责,数据错了,数据是你的,你负责,

这样非常容易调试,也非常容易被理解,


单向数据流是非常容易被人类理解的东西,

就是输出、输出,我给你一个东西,你给我一东西,是单向数据流


单项数据流,数据属于谁的谁负责

数据 current、tobal、pageSize 是属于App 组件的,是 App 组件使用 pager 组件传的数据,

pager 组件没有权利改属性 props,属性是别人(App)给的数据,pager 组件没有权利改别人的数据,

如果需要改,只能让 App 组件来改

<Pager 
  :current="current" 
  :total="total"
  :page-size="pageSize"
/>

data(){ // App组件的数据,来自于它的data配置
  return {
    current: 2,
    total: 100,
    pageSize: 10
  }
}


属性和跟状态最大的区别是,

属性是只读的,不允许更改,

如果要更改 pager 组件的属性,需要 pager 组件抛出事件,通知 App 父组件更改,因为分页组件是 App 组件里面使用的


假设分页组件已经的抛出了 change 事件,App 组件怎么用呢?

App 组件这里直接把事件参数 $event 赋值给它的数据 current 进行更改

<Pager 
  :current="current" 
  :total="total"
  :page-size="pageSize"
  @change="current=$event"
/>


4、自定义事件

 this.$emit("change", 事件参数) 

在组件中触发事件,并把事件参数传过去,让父组件收到通知

1. $emit() 是实例里面的内置成员,该函数就是用来触发事件的

1. change 是事件名,事件名可以小驼峰也可以短横线。这里写的是什么名称,App 组件那边就注册什么事件的名

2. 事件参数是新页码,可以有多个事件参数


袁老师说:

事件其实是一种回调模式,

就是说我知道发生了什么事,但是我不知道我要干嘛,这时候就需要回调了


比如,

this.$emit("change", newPage)  触发事件的时候,

Pager 组件知道一定有变页码的事情发生了,

但是该事情不是 Pager 组件来做的,所以只能触发事情,让别人来做这件事情


触发事件的源码是监听者模式

1. App 注册事件 @change="current = $event"

    这个 @change="current = $event" 会放到一个函数里面,然后把函数加入到一个数组

2. this.$emit("change", newPage)  触发事件这里,实际上是循环数组,然后运行每一个函数


this.$emit("change", newPage)  这行叫触发事件(也叫自定义事件),只有当运行到这行

App 组件才会运行 @change="current = $event" 叫注册事件


梳理一下组件的事件

1. App 组件里面使用了一个 <Pager /> 组件

App 自身的数据叫状态(data),比如 current,

然后 App 组件把状态 current 是作为属性传给 Pager 组件的,

所以 Pager 组件不能更改属性 current


2. 当用户点击了某一个分页时,Pager 组件发生了一件事,

这个时候数据 current 不是 Pager 组件的,它不能修改该数据,

于是触发一个 change 事件,把新页码做为事件参数 newPage 扔给 App 组件


3. App 父组件注册了 @change 事件后,

可以监听到这个 change 事件,会把事件参数 newPage 传递给关键字 $event

current = $event 父组件改变自己数据中的 current


4. 当 App 组件中的状态(data 里面的 current)发生变化时,该组件会重新渲染,因为数据是响应式的,

如果 App 上面还有组件,跟 App 上面的组件没关系,从 App 组件这个节点开始重新渲染


5. App 组件数据(data)更新后重新渲染,重新渲染的过程中  :current="current" ,

导致 Pager 组件的属性 current 也跟着变了,

Pager 组件的属性变了,也会重新渲染


当一个组件中的状态发生变化时,该组件会重新渲染,在渲染的过程中,可能导致其子组件的属性发生变化,而属性的变化也会导致组件重新渲染。但根本原因,是状态发生变化


也就是说一个组件重新渲染有两种情况

1. 要么是组件自己管理的状态 data 发生变化

2. 要么是组件的属性 props 发生变化


Ps:

this.$emit("change", newPage, 36, 6, 87, 5, 4 ) 如果是多个参数

App 组件就不能用这种方式了 @change="current = $event",$event 只能接收第一个参数

要写一个事件绑定的函数,在函数里面接收多个参数


点击分页后,

子组件 Pager  触发了 change 事件通知父组件,

父组件处理了该事件,也就是注册了 @change 事件,并且更改了自己的状态,状态变了组件重新渲染,

导致 Pager 子组件的属性也跟着变了,子组件的属性变了也会重新渲染


5、使用 v-model 指令进行组件通信

看一个有意思的事情

1. Pager 组件属性

    current 属性改成名 value,

    模板里面的 current 也改成 value

    触发事件的名字改成 input

2. App 父组件,把下面两行代码,换成语法糖 v-model="current"

    :value="current"

    input="current = $event"

3. 这是固定的用法,实现类似于双向绑定的效果,实际上还是单向数据流,仍然是要触发事件的,无非就是少些几行代码

    所以 v-model 的本质是一个语法糖,

    实际上是绑定 value 属性,同时监听 input 事件

// pager 组件
const Pager = {
  template: `
  <div class="pager">
    <a @click="changePage(1)" class="pager-item" :class="value === 1 ? 'disabled' : ''">首页</a>
    <a @click="changePage(value - 1)" class="pager-item" :class="{disabled: value === 1}">上一页</a>
    <a class="pager-item"
      @click="changePage(item)"
      :class="{active: item === value}"
      v-for="(item, i) in numbers" :key="i"
    >
      {{item}}
    </a>
    <a @click="changePage(value + 1)"class="pager-item" :class="{disabled: value === pageNumber}">下一页</a>
    <a @click="changePage(pageNumber)" class="pager-item" :class="{disabled: value === pageNumber}">尾页</a>
  </div>`,
  props: {
    value: { // current改成value
      type: Number,
      default: 1
    },
    pageSize: {
      type: Number,
      default: 10
    },
    total: {
      type: Number,
      required: true
    },
    panelNumber: {
      type: Number,
      default: 5
    }
  },
  computed:{
    pageNumber(){
      return Math.ceil(this.total / this.pageSize);
    },
    numbers(){
      var min = this.value - Math.floor(this.panelNumber / 2);
      if(min < 1){
        min = 1;
      }
      var max = min + this.panelNumber - 1;
      if(max > this.pageNumber){
        max = this.pageNumber;
      }
      // 解决了一个分页时候的bug
      if(max === this.pageNumber){
        min = this.pageNumber - this.panelNumber + 1;
      }
      const arr = [];
      for (let i = min; i <= max; i++) {
        arr.push(i);
      }
      return arr;
    }
  },
  methods:{
    changePage(newPage){
      if(newPage < 1){
        newPage = 1;
      }else if(newPage > this.pageNumber){
        newPage = this.pageNumber;
      }
      this.$emit("input", newPage); // 事件名改成input
    }
  }
};


// App 组件
const App = {
  template:`<div>
  <Pager 
    :panelNumber="9"
    :total="total"
    :page-size="pageSize"
    v-model="current"
  /></div>`,
  components:{
    Pager,
  },
  data(){
    return {
      current: 1,
      total: 365,
      pageSize: 10
    }
  },
  methods:{
    
  }
}


new Vue({
  template: `<div>
    <h1 style="text-align:center;">App组件</h1>
    <App/>
  </div>`,
  components:{
    App,
  },
  el: "#app"
});

</script>


6、在组件模板中使用 import 导入的数据

项目中的知识点

App.js

import MovieList from './components/movieList.js';
import Pager from './components/pager.js';
import mockList from "./services/movieService.js"; // 导入电影数据

const template = `<div>
  <MovieList :movies="pageMovies"/>
  <Pager v-model="current" :total="total" :pageSize="pageSize"/>
</div>`;

export default{
  template,
  data(){
    return {
      current: 1,
      total: mockList.length,
      pageSize: 2,
      mockList,
      allMovies: mockList
    }
  },
  components: {
    MovieList,
    Pager
  },
  computed:{
    pageMovies(){
      return this.mockList.slice((this.current - 1) * this.pageSize, this.current * this.pageSize)
    },
  }
}


 <MovieList :movies="mockList"/> 

导入的电影的数据 mockList 不能直接在模板里面用,报错说 mockList 没有定义

[Vue warn]: Property or method "mockList" is not defained on the instance but referencedduring render


为什么 "mockList" 没有定义呢?

vue 在解析 template 模板,我们把解析模板的过程叫编译模板, 

编译模板的过程中,template 的环境是 vue组件实例,该环境连 window 对象都没有,也没有导入的 "mockList",

外部的东西没有响应式,修改后 vue 不会知道,所以一定要用内部的东西,


什么是内部的东西呢?

可以在 data 里面写一个 allMovies,把导入的所有电影数据放到 allmovies 里面   allMovies: mockList 

 <MovieList :movies="allMovies"/>  然后就能在模板里面,给组件传过去了


分页显示电影数据

根据当前页码和页容量截取数组

[1, 2, 3, 4, 5, 6, 7, 8, 9]


分页的条件

pageSize: 2 每页取两条

current: 1  当前页码为 1

数组的 slice 方法

arr.slice(起始下标, 结束下标); // 不包含结束下标


公式

第一个起始下标的数字  (current当前页码 - 1) * pageSize页容量

第二个结束下标的数字  current当前页码 * pageSize


示例

(current - 1) * pageSize

(current * pageSize

arr.slice(0, 2)  实际取不到 2,取出来的结果是 0 ~ 1,形成一个新的数组 [0,1]


根据当前的页码和页容量计算的结果

current: 1 -> 从 0 取的到 2(2 取不到)

current: 2 -> 从 2 取的到 4(4 取不到)

current: 3 -> 从 4 取的到 6 ...


计算属性把分割的新数组返回,

计算属性依赖 current 和 依赖 pageSize,

只要 current 一变就会重新计算,

重新计算 <MovieList :movies="pageMovies"/> 界面自然就重新刷新

pageMovies(){
  return this.mockList.slice((this.current - 1) * this.pageSize, this.current * this.pageSize)
},


这个警告的意思是,

[Vue tip]: <Movie v-for="item in movies">: component lists rendred with v-for should have explicit keys.

当循环渲染生成自定义组件时候,需要加上 :key 属性,key 是一个内置属性

并提供给唯一的值,通常是id,以便 vue 提高渲染效率

<Movie v-for="(item, i) in movies" :data = "item" :key="item._id" />


四、组件实例的生命周期

一个组件运行的过程中,它什么时候出生,什么时候死亡?

比如,一个用了 v-if 的组件,

显示的时候就是组件出生了,

不显示的时候就把组件移了,移除就组件死亡了


一个组件从出生到死亡,

会经过一些按照先后顺序执行的函数,就是声明周期函数

如果写了这些函数的,这些函数会自动执行,

1. beforeCreate

组件实例刚刚创建好之后执行,它执行的非常非常早,此时,组件实例中还没有提升任何成员


data 里面的数据 current 还没有提升到实例,这个声明周期函数一般不会用,了解一下就行了

beforeCreate(){
  console.log(this.current); // undefined
}


2. created

组件实例中已经提升了该有的成员,但是此时还没有渲染页面的任何内容


这些配置 data、methods、computed、props 已经提升到示例里面了,但是页面还没有渲染获取不到元素

created(){
  console.log(this.current); // 1
  console.log(document.getElementById("myDiv")); // null
}


3. beforeMount

组件即将进行渲染,但是还没有进行渲染,此时,已经编译好模版 template,已经满足了所有的渲染条件,

和上面的 created 一样,仍然得不到页面的 dom 元素


Mount 挂载的意思

beforeMount 挂载之前


4. mounted [重要]

组件已经完成了渲染,生成真实的 dom,可以看见页面了


我们通常会在 mounted 函数里面处理很多事件,

因为代码的运行也需要时间,如果在前面那三个函数里面,写了写很多很多代码,会导致卡住,因为页面还没有渲染,

不能因为我们的操作,让用户看不到东西


mounted 函数里面页面已经渲染好了,用户至少看得到东西了,

有些代码在这里处理,


比如 ajax 请求写到个函数里面,这是非常常见的做法

1. mounted 函数也只执行一次(beforeCreate、created、beforeMount 这三个函数都只执行一次)

所以写一个 setMovies 方法把获取远程数据的代码封装一下,根据当前的 this.cuttent 页码重新设置电影数据

2. 不用 v-model 了,页码改变后重新获取远程数据,需要要把 v-model 展开

:value = "cuttent"

@input="current=$event"

3. input 事件重新换 handlePageChange 方法处理

更新的页码

运行 setMovies 方法,重新设置电影数据

import MovieList from '../components/movieList.js';
import Pager from '../components/pager.js';
import movieService from "../services/movieService.js"; // 导入电影数据
import Loading from "../components/loading.js"

const template = `<div>
  <MovieList :movies="mockList"/>
  <Pager 
    :value="current" 
    @input="handlePageChange"
    :total="total" 
    :pageSize="pageSize"
  />
  <Loading :show="isLoading"/>
</div>`;

export default{
  template,
  mounted(){
    // 1.远程请求数据
    // movieService.getMovies(this.current, this.pageSize).then(resp => {
    //   this.mockList = resp.datas;
    //   this.total = resp.tobal;
    // });
    this.setMovies();
  },
  data(){
    return {
      current: 1,
      total: 30,
      pageSize: 2,
      mockList: [],
      isLoading: false, // 是否正在远程获取数据
    }
  },
  components: {
    MovieList,
    Pager,
    Loading
  },
  computed:{
    pageMovies(){
      return this.mockList.slice((this.current - 1) * this.pageSize, this.current * this.pageSize)
    },
  },
  methods: {
    // 2.由于mounted函数只运行一次,所以这里封装一下
    setMovies(){
      this.isLoading = true; // 开始远程获取数据
      movieService.getMovies(this.current, this.pageSize).then(resp => {
        this.mockList = resp.datas;
        this.total = resp.tobal;
        this.isLoading = false; // 远程获取结束
      });
    },
    // 3.更改新页码,然后根据新页码重新请求数据
    handlePageChange(newPage){
      this.current = newPage;
      this.setMovies();
    }
  }
}


5. 此时,等待组件更新

已经完成渲染了,等待组件组件更新,什么时候更新?

当一个组件的属性或状态发生变化的时候,会自动重新渲染


6. beforeUpdate

当组件既将更新,但还没有更新(此刻已经完成渲染了,等待组件更新),

此时得到的数据是新的,但是界面是旧的


6. updated

组件已经完成更新,此时数据和界面都是新的


7. beforeDestroy

当组件即将被销毁时候


什么时候被销毁?

整个组件不显示了,通常情况下都是因为 v-if 不显示了


8. destroyed(过去完成时)

当组件已经被销毁后


destroyed 函数有点用,一般销毁一些附带的资源,

比如组件一开始的时候开启了一个 setInterval 计时器,组件销毁的时候要顺带把计时器清除 clearInterval

{
  mounted(){ // 一开始mounted的时候开启了一个计时器
    this.timer = setInterval(() => {}, 1000);
  },

  destroyed(){ // 组件销毁时候要清除掉这个计时器
    clearInterval(this.timer);
  }
}


五、组件的插槽

插槽的位置就是预留一个空间


插槽位置 <slot></slot> 放置的是,使用组件的时传递的元素内容,如果不写会使用默认内容

  <Modal> 组件里面写的内容,会放到插槽的位置 </Modal>  

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://20180416-1252237835.cos.ap-beijing.myqcloud.com/common/vue.min.js"></script>
<style>
*{margin:0;padding:0;}
.modal{position:fixed;left:0;top:0;bottom:0;right:0;background:rgba(0,0,0,.3);}
.modal .center{position:absolute;left:50%;top:50%;transform:translate(-50%, -50%);background:lightyellow;padding:10px;}
</style>
<title>slot</title>
</head>
<body>

<div id="app"></div>

<script>
/**
 * Modal蒙层组件
 * 该组件里面有插槽slot
 */
const Modal = {
  template: `<div class="modal">
    <div class="center">
      <slot>没有使用插槽,显示的默认内容</slot>
    </div
  </div>`,
}

/**
 * App根组件
 * 该里面使用了蒙层组件,并往插槽里面传html
 */
const App = {
  template: `<div class="app">
    <Modal>
      <p><input type="text"></p>
      <button>按钮</button>
    </Modal>
  </div>`,
  components:{
    Modal,
  }
}

/* 启动vue */
new Vue({
  template: `<App/>`,
  components:{
    App,
  }
}).$mount("#app");

</script>
</body>
</html>

<div class="app">

  <div class="modal">

    <div class="center">

      <p><input type="text"></p>

      <button>按钮</button>

    </div>

  </div>

</div>


具名插槽

<slot>默认插槽</slot>  没有名字的是默认插槽,比如上面用的是默认插槽

<slot name="abc"></slot>  给插槽命名的是具名插槽,具名插槽的优点是可以写多个

<template v-slot:abc><p>具名插槽</p></template>  使用具名插槽

const Modal = {
  template: `<div class="modal">
    <div class="center">
      <slot name="abc"></slot>
      <slot name="bcd"></slot>
      <slot>默认插槽</slot>
    </div>
  </div>`,
}

const App = {
  template: `<div class="app">
    <Modal>  
      <template v-slot:abc><p>具名插槽</p></template>
      <template v-slot:bcd><a>具名插槽可以有多个</a></template>
      <button>这是默认插槽的内容</button>
    </Modal>
  </div>`,
  components:{
    Modal,
  }
}


具名插槽主要是用来布局的

/**
 * 左右两边的内容不知道放什么,就设置两个插槽
 * 
 */
const template = `<div class="container">
    <div class="left">
      <slot name="left"></slot>
    </div>
    <div class="right">
      <slot name="right"></slot>
    </div>
  </div>`;


/**
 * 使用插槽
 */
const template = `<div class="app">
    <Modal>  
      <template v-slot:left>
        <p>左边的内容</p>
      </template>
      <template v-slot:right>
        <p>右边的Neri</p>
      </template>
    </Modal>
  </div>`;


六、vue-router

https://router.vuejs.org/zh/guide/

vue 路由,可以简单理解为,当访问某个地址时,渲染某个组件(在不同上下文环境里面,路由的概念是不一样的)


工程结构

|- my-site

   |- src

   |    |- services

   |    | - assets

   |    |     |- vue.js

   |    |     |- vue-router.js

   |    |     |- index.css

   |    |

   |    |- components  普通组件的文件夹

   |    |     |- loading.js

   |    |     |- modal.js

   |    |     |- movie.js

   |    |     |- movieList.js

   |    |     |- page.js

   |    |

   |    |- pages 页面组件的文件夹,根据路由切换的页面组件

   |    |     |- home.js

   |    |     |- moviePages.js

   |    |

   |    |- app.js

   |    |-main.js

   |

   |- index.html


1、使用路由

在 index.html 页面

首页先引入 vue.js (v2.6.10)

在引入路由 vue-router.js (v3.1.3),普通导入,不是模块化的导入


怎么使用路由呢?

在启动 main.js 文件里面配置

1. 使用构造函数 new VueRouter({}) 得到一个“路由对象”router

    参数叫“路由配置对象”

2. 将“路由对象”配置到,创建 vue 实例时候,配置对象的 router 属性中

import App from "./app.js";

const router = new VueRouter({ // 1.得到一个路由对象router
  // 根据路由配置对象创建路由
});

new Vue({
  template: `<App/>`,
  components:{
    App,
  },
  el: "#app",
  router // 2.将路由对象配置到创建实例时候的配置里(router:router 由于变量名和属性名一样使用简写)
});


2、根据配置对象创建路由

根据配置对象创建路由,最关键的有两个配置(对象里面有多配置

routes 路由规则配置,重点,该配置决定怎么切换组件

mode 模式配置


routes: [ ] 路由规则配置

配置一个数组,数组的每一项 {} 是一个配置规则

 path: 地址   当访问地址的时候

 component: 组件   渲染的组件 


mode: "hash/history" 路由模式配置有两种

hash 默认的模式,兼容性最好,地址出现在#号后面,切换地址面不会导致页面刷新

history 模式,使用的是 H5 的 History API,地址直接变化,并且页面不刷新


示例

import App from "./app.js";
import Home from "./pages/home.js";
import MoviePage from "./pages/moviePage.js";

const router = new VueRouter({
  routes: [
    // 当访问path地址的时候,渲染component的组件
    {path: "/", component: Home},
    {path: "/movie", component: MoviePage}
  ],
  mode: "hash"
});

new Vue({
  template: `<App/>`,
  components:{
    App,
  },
  el: "#app",
  router
});


3、<RouterView/> 组件

最后在合适的位置写上 <router-view></router-view> 组件

1. 意思是表示路由匹配到的组件渲染的位置,比如访问这个 "/movie" 地址,渲染 MoviePage 组件

2. 实际上是 vue-router 做好的一个组件,并且进行了全局注册,组件使用方式跟之前一样,可以大驼峰 <RouterView/> 或短横线

// app.js根组件,整个页码内容靠该组件完成

const template = `<div style="width:900px;margin:0 auto;text-align:center;">
  <router-link to="/">首页</router-link>
  <router-link to="/movie">电影列表</router-link>
  <router-view></router-view>
</div>`;

export default{
  template,
  components:{
    Header,
  }
}


4、router-link 导航

<router-link to="/movie">首页</router-link>

通常使用 router-link 切换页面,它自动使用 mode 配置中的模式,并且不会刷新页面。本质上,就是生成 a 元素

to 属性是连接地址


router-link 会自动给生成的 a 元素添加类样式名,如果当前地址是匹配 to="/movie" 的时候

1. 当前地址为 /movie 时有两个样式

    精确匹配 router-link-exact-active

    模糊匹配 router-link-active

2. 剩下地址,只要以 / 开头的,就只有一个 router-link-active 模糊匹配


5、编程式导航

当创建 Vue 实例时,如果配置了 router 对象,则 router 会出现在所有的 vue 实例和组件实例中,作为 $router 属性出现

import App from "./app.js";
import router from "./router.js"; // 导入router

const vm = new Vue({
  template: `<App/>`,
  components:{
    App,
  },
  el: "#app",
  router // 在vue实例里面配置导入的router
});

console.log(vm); // 在vue实例里面可以看到$router属性


用编程的方式跳转页面

1. 在组件里面,通过 this 拿到当前组件实例,然后找到实例里面的 $router 对象

2. $router 对象里面提供了很多方法,其中最常用的是 push 方法

3. push("页面地址") 方法里面写的是跳转的地址

<p @click="handleClick" style="cursor:pointer;">用编程的方式回到首页</p>

methods:{
  handleClick(){
    this.$router.push("/"); // 使用编程的方式跳转到首页
  }
}


this.$router.push("页面地址")

push 有点类似于往数组里面追加一项,

这里的真实意思是,往当前页面地址栈中加入一个地址(这是 HTML5 里面 history Api 的知识)


可以简单理解

页面是放到一个数组里面,点击浏览器工具栏 <- 前进或后退 -> 图标,页面可以后退,可以前进,

怎么知道之前的页面呢,就是通过数组往前面找


this.$router.push("页面地址") 就是往地址栈的末尾加入一个页面5,如果点击浏览器的后退,就后退到之前页面4

[ 页面1, 页面2, 页面3, 页面4 ]

[ 页面1, 页面2, 页面3, 页面4, 页面5 ]


this.$router.replace("页面地址") 

replace 方法也是跳转页面,只不过 replace 方式是替换当前页面地址栈中当前位置的页面,

replace 是把当前页面4替换成了页面5,这时候如果在点击浏览器的 <-后退,后退到页面3

[ 页面1, 页面2, 页面3, 页面4 ]

[ 页面1, 页面2, 页面3, 页面5 ]


this.$router.go(偏移量)

偏移量是数字,

根据当前地址栈中的位置,以及设置的偏移量,跳转页面 


比如,当前是页面3

[ 页面1, 页面2, 页面3, 页面4 ]

根据当前页面3在栈中位置,根据偏移量

this.$router.go(1)  向前方偏移一个,跳转到页面4

this.$router.go(-1) 跳转到页面2

this.$router.go(-2) 跳转到页面1


这两个方法没有参数

this.$router.back() 相当于 this.$router.go(-1) 针对返回上一页的功能

this.$router.forward() 相当于 this.$router.go(1)


Ps:

记录一个错误,

如果已经在首页,还点击了跳转到首页

Uncaught (in promise) NavigationDuplicated {_name: 'NavigationDuplicated', name: 'NavigationDuplicated', message: 'Navigating to current location ("/") is not allowed', stack: 'Error\n    at new NavigationDuplicated...

报错的解决方法

handleClick(){
  this.$router.push("/").catch(()=>{

  });
}


6、动态路由

在配置路由规则时,可以将规则字符串写为动态路由,动态路由使用“冒号 + 单词


动态路由规则  /defail/:id 

id 是命名是自定义的

它能够配置到这样的  /detail/???  路径

import Home from "./views/home.js";
import Article from "./views/article.js";
import Detail from "./views/detail.js"

const router = new VueRouter({
  routes: [
    {name: "home", path: "/", component: Home},
    {name: "article", path: "/article/:currentPage", component: Article},
    {name: "detail", path: "/detail/:id", component: Detail},
  ],
  mode: "hash"
});

export default router;


动态路由规则配置好

访问 http://127.0.0.1/detail/xxx

动态部分只要有东西,比如 xxx,就可以匹配到 Detail 组件,可以看到页面了


如果多个动态部分,依次写就可以了,只不过太多的时候用 query 的方式

/**
 * http://127.0.0.1:8848/detail/abc/1/2
 */ 

{path: "/detail/:id/:a/:b", component: Detail}

id = abc

a = 1

b = 2


当配置好路由后,除了会向所有实例增加了 $router 属性之外,还增加了一个 $route 属性来获取路由信息

$router 主要用来跳转页面

$route 主要用于获取路由信息


$route 路由信息

mounted(){
  console.log(this.$route);
}

http://127.0.0.1:8848/detail/9?id=9&a=1&b=2

{

  fullPath: "/defail/9?a=1&b=2", 完整路径包含 ? 问号后面的东西

  hash: "",

  matched: [{…}],

  meta: {},

  name: "defail",

  params: {id: 'd'}, 属性值是一个对象,对象里面的属性是动态路由

  path: "/defail/9", 路径

  query: {}, 获取附加信息,通常叫地址栏参数,就是 ? 号后面的部分

}


this.$route.params

params 获取的路由配置规则中匹配到的路由信息,通常称为路由参数(params是参数的意思)

这是路由规则  /detail/:id 

获取到路由规则中匹配到 id 的信息


获取路由的动态 id,然后远程获取电影数据?

在 created 里面远程请求,大多数时也不怎么会出问题,但对初学者而言在 mounted 函数里面是绝对不会出问题的

import movieService from "../services/movieService.js";
import Loading from "../components/loading.js";

const template = `<div class="detail-container">
  <div v-if="movie">
    <h2 class="title">{{movie.name}}</h2>
    <div class="attach">
      <span>英文名:{{movie.ename}}</span> 
      <span>类型:{{movie.type}}</span>
      <span>上映地区:{{movie.area}}</span> 
      <span>上映时间:{{movie.upDate}}</span> 
      <span>时长:{{movie.time}}</span>
    </div>
    <div class="poster"><img :src="movie.poster" alt=""></div>
    <div class="desc">{{movie.intro? movie.intro: desc}}</div>
    <p @click="handleClick" style="cursor:pointer;text-align:center;padding:10px 0;">用编程的方式返回上一页</p>
  </div>
  <Loading :show="isLoading"/>
</div>`;

export default{
  template,
  mounted(){
    this.isLoading = true;
    // 获取动态路由里面的id
    const id = this.$route.params.id;
    // 获取远程获取
    movieService.getMovie(id).then(resp => {
      this.movie = resp;
      this.isLoading = false;
    });
  },
  data(){
    return {
      movie: null,
      isLoading: false
    }
  },
  components: {
    Loading,
  },
  methods:{
    handleClick(){
      this.$router.back();
    }
  }
}


 :data 开始绑定的数据是 null 所以报错,解决办法加上一个 v-if="movie"

[Vue warn]: Error in render: "TypeError: Cannot read properties of null (reading 'poster')"

vue.js:1902  TypeError: Cannot read properties of null (reading 'poster')


动态绑定 router-link 标签的 :to 属性,拼接上 id

const template = `<div class="data">
  <div class="poster">
    <img :src="data.poster" alt="">
  </div> 
  <div class="words">
    <h2 class="title">
      <router-link :to="'/detail/' + data._id">{{data.name}}</router-link>
      <!--<a :href="'#/detail/'+data._id" @click="hanleClick($event, data._id)">{{data.name}}</a>-->
    </h2> 
    <div class="attach">
      <span>英文名:{{data.ename}}</span> 
      <span>类型:{{data.type}}</span> <br> 
      <span>上映地区:{{data.area}}</span> 
      <span>上映时间:{{data.upDate}}</span> 
      <span>时长:{{data.time}}</span>
    </div> 
    <div class="desc">{{data.intro? data.intro: desc}}</div>
  </div>
</div>`;

export default{
  template,
  props: {
    data: {
      type: Object,
      default: () => {}
    }
  },
  methods:{
    hanleClick(e, id){
      location.href="http://127.0.0.1:8848/05 my-site/#/detail/"+id;
      e.preventDefault();
    }
  }
}


七、vuex

官网


https://v3.vuex.vuejs.org/zh


工程结构

|- my-site

  |- src

  |   |- assets

  |   |   |- vue.js

  |   |   |- index.css

  |   |   |- vue-router.js

  |   |   |- vuex.js

  |   |

  |   |- services

  |   |   |- movieService.js

  |   |   |- loginService.js

  |   |

  |   |- components

  |   |   |- movieList.js

  |   |   |- pager.js

  |   |   |- loading.js

  |   |   |- movie.js

  |   |   |- modal.js

  |   |   |- header.js

  |   |

  |   |- views

  |   |   |- home.js

  |   |   |- moviePage.js

  |   |   |- defail.js

  |   |   |- login.js 登陆页

  |   |

  |   |- store

  |   |   |- index.js 共享数据

  |   |

  |   |- - router

  |   |   |- index.js

  |   |

  |   |- app.js  根组件

  |   |- main.js 负责启动

  |

  |- .htaccess

  |- index.html


路由配置的路径必须 / 斜杠开头

[vue-router] Non-nested routes must include a leading slash character. Fix the following routes: - login


在 login 组件登陆成功后,拿到用户数据 { loginId: 'admin', name: '超级管理员' }

在 header 组件里面要显示当前登陆的用户,

由于 login 组件没有使用 header 组件,不能通过属性(props)传递数据,这时候有一种最简单的办法叫做“状态提升


捕获.jpg


状态提升

1. 把登陆的状态数据提升 app 组件里面,

由于状态在 app 组件里面,就可以把状态数据作为属性(props)传给 header 组件了


2. 但是 app 组件的状态数据会变化,比如注销、登陆成功了等...都会变换,

但只有 login 组件才能让 app 组件的状态数据发生变化,

因为状态数据属于 app 组件了, 所以 login 组件登录成功引发(抛出)一个事件,登录或失败也引发一个事件后,

app 注册该事件就可以把状态数据修改了


3. 因为状态是响应式的,app 修改了状态,它要重新渲染

app 重新渲染,导致 header 的属性发送变化,它也重新渲染了


如果没有新的知识,目前只能是这种方案


实际组件之间的通信是有很多方式,这两个最核心的

1. $emit  通过 事件传递数据

2. props 通过属性传递数据


这两种方式只能做状态提升,

但是在复杂系统里面,要共享数据的情况很多


1、vuex

vuex 用于解决大量的、复杂的组件间共享数据的问题


首先要知道 vuex 出现的原因和作用,才能很好的去使用它,

不是一个小的系统就用 vuex 来解决,

实际上,一个小型系统直接用状态提升到 app 就很好的解决了,

vuex 是用来解决复杂系统大型应用的,在小型应用 vuex 反而觉得复杂


vuex 的核心理念

1. 提出一个单独的模块叫数据仓库,共享的数据全部放到这个仓库里面,然后把这个仓库放到 vue 实例里面

2. 我们要更改数据,更改的是数据仓库里面的数据

3. 每一个组件都可以共享仓库里面的数据


2、使用 vuex 

如何使用 vuex

1. 首页引入 vuex.js 文件(版本 v3.3.1

2. 新建一个模块文件 /src/store/index.js

 new Vuex.Store(配置对象)    创建一个仓库,并 export default  导出这个仓库,

创建的仓库在 main.js 文件里面用,通常一个 vue 应用,只对应一个仓库

export default new Vuex.Store({

});


3. 启动文件 main.js 里面导入仓库,然后把 store 配置到 vue 实例里面,

在 vue 实例里面,仓库的配置方式和路由是一样的,说明属性的名必须是 store

// main.js

import App from "./app.js";
import router from "./router.js";
import store from "./store/index.js"; // 导入仓库 

const vm = new Vue({
  template: `<App/>`,
  components:{
    App,
  },
  el: "#app",
  router,
  store // 属性名必须是store(store2这样不行)
});


3、仓库配置对象

配置对象中有那些内容?

1. state 仓库的默认状态

2. mutations 配置状态有哪些变化

3. actions 配置副作用操作

import loginService from "../services/loginService.js";

export default new Vuex.Store({
  state:{
    loginUser:{
      data: null,
      isLoading: false,
    },
  },
  mutations:{
    /**
    * 用于改变是否正在登陆的状态
    * state 表示当前的状态,当前的状态会自动传给我们
    * payload 附加信息
    * 
    */
    setIsLoading(state, payload){
      state.loginUser.isLoading = payload;
    },
    /**
     * 用于改变登陆的用户
     * mutations 里面的代码往往很简单,就是state里面数据的赋值
     */
    setUser(state, userObj){
      state.loginUser.data = userObj;
    }
  },
  actions:{
    /**
     * 登陆的副作用操作
     * context 几乎等同于整个仓库对象
     * payload 需要传的账号和密码 {loginId:xxx, loginPwd:xxx}
     */
    async login(context, payload){
      context.commit('setIsLoading', true);
      const resp = await loginService.login(payload.loginId, payload.loginPwd);
      if(resp){
        context.commit('setUser', resp);
        localStorage.setItem("loginUser", JSON.stringify(resp));
        context.commit('setIsLoading', false);
        return true;
      }
      context.commit('setIsLoading', false);
      // return false; 默认返回的是undefault是一样的效果
    },
    /**
     * 退出登陆
     * 状态发生变化,要把setUser设置为null
     * 清空本地存储,本地存储是副作用,上面状态变换setUser设置为null不是副作用
     */
    loginOut(context){
      context.commit("setUser", null);
      localStorage.removeItem("loginUser");
    },
    /**
     * 初始化时,同步本地存储
     * 因为状态的数据loginUser在内存里面,之前登录过,一刷新就全部没了,所以刷新的时候需要把本地存储同步到loginUser里面来
     * 不管是否登陆,在main.js里面要先初始化一次
     */
    syncLogin(context){
      const local = localStorage.getItem("loginUser");
      if(local){
        const user = JSON.parse(local); // 拿出本地存储的用户对象
        context.commit("setUser", user); // 同步到状态
      }
    },
  }
});


Vuex.Store() 创建仓库的时候忘记加上 new

Uncaught Error: [vuex] store must be called with the new operator.


1. state

仓库里面数据的默认状态(相当于组件里面的 data)


为什么不这样直接写呢?

export default new Vuex.Store({
  state: {
    data: null,
    isLoading: false
  },

});


嵌套一层对象是避免和其他状态冲突,比如电影里面也有 isLoading

export default new Vuex.Store({
  state:{
    loginUser:{ // loginUser表示当前登陆的共享状态
      data: null,
      isLoading: false,
    },
    movies:{ // 表示当前电影的共享状态
      datas:[],
      page:1,
      isLoading: false,
    }
  },

});


2. mutations 

配置状态有哪些变化


仓库的状态 state 有可能会改变,

mutations 配置的就是状态有那些变化,每一个变化是一个函数,

mutations 是状态变化的唯一原因,绝对不能用其他方式来改变状态,也就是说必须调用 mutations 里面的函数来改变状态


这样的好处在于,

如果将来状态出了问题,可以跟踪状态是经过了哪一个变化出的问题,

主要是为了便于调试,其实还有一个点是为了单向数据流(单向数据流说来话要很长很长)


mutations 里面函数的参数,比如 setIsLoading(state, payload){...}

1. state 名字是固定的,表示仓库的状态,会自动传过来

2. payload  该参数是可以选的表示额外的信息。名字可以自定义,payload 是负载的意思,可以理解为附加信息


如何调用 mutations 配置里面的 setIsLoading 函数呢

不可以直接调用,必须通过仓库对象的 commit  函数进行调用

commit 表示提交的意思,就是提交一次更改


这是仓库的对象,

导出的仓库 new Vuex.Store 是一个对象,

main.js 导入仓库对象  import store from "./store/index.js"   


在启动文件里面,用仓库对象调用 commit 函数,传递两个参数

参数1  mutations 名称,就是该配置里面的函数名

参数2  payload

store.commit("setIsLoading", true); // 提交一次更改


mutations 函数中不可以出现异步等副作用操作

mutations:{
  setIsLoading(state, payload){
    state.loginUser.isLoading = payload;
  },
  setUser(state, userObj){
    /**
     * mutations里面绝对不能用ajax请求异步代码
     * 这里面的代码往往非常简单,直接赋值就行了
     */
    state.loginUser.data = userObj;
  }
},


什么是副作用操作?要等一会才运行的都是副作用操作

1. 不能有 ajax,因为要过一段时间才能执行完

2. 不能有定时器

3. 不能给 dom 注册一个事件,事件函数里面操作 dom 元素

4. 不能 localStorage 设置本地存储

5. 不能改变外部变量里面的一些东西,比如全局有一个 var obj = {},mutations 里面都不能操作

6. 没有当前时间

7. 没有随机数


为什么不允许呢?

为了调试的时候跟踪状态,做一些时光旅行


3. actions 

专门配置副作用操作,比如 ajax 请求、本地存储等


每个 actions 是一个函数,比如登录函数    login(context, payload){}  

参数1,context 上下文对象,它几乎等同于仓库对象(mutations 是状态,这里是上下文对象)

参数2,payload 传入账号和密码,自行约定好的参数是一个对象


context 几乎等同与对象

但不能这样直接更改数据   context.state.loginUser.isLoading = true  

因为 mutations 是数据变换的唯一原因,

必须要提交 mutations   context.commit("setIsLoading", true) 


如何调用 action 里面的 login 方法?

不能直接调用 login,

仓库对象 store 里面不仅有 commit 还有 dispatch,

必须通过仓库对象的 dispatch 方式调用,


演示一下,

在启动文件里面 window.store = store 把仓库 store  对象放到 window 里面,方便在控制台调试

控制台   store.dispatch("login", {loginId:"admin", loginPwd:"123"})   该放手叫分发,actions 是分发出去的

然后可以看仓库里面的状态 store.state.loginUser.data 有值了


同步本地存储,

在最开始要触发一次,在 main.js 文件里分发,不需要参数 payload

import App from "./app.js";
import router from "./router.js";
import store from "./store/index.js";

window.store = store;
store.dispatch("syncLogin"); // 同步本地存储

const vm = new Vue({
  template: `<App/>`,
  components:{
    App,
  },
  el: "#app",
  router,
  store
});


仓库里面只考虑数据,不要考虑别的,

它只考虑数据的变化,有哪些变化,需要怎么来处理数据,完全专心致志的考虑数据,


在公司里面很可能有人专门开发仓库,

他不知道界面是什么,但是知道功能是什么,

不知道界面上是怎么登陆的,知道功能上有一个登陆,就可以把数据写出来,

也就是说仓库里面不依赖界面也不依赖路由等其他东西,仓库里面只处理数据


仓库是纯粹的数据处理,跟界面没有关系

state 初始化状态(保存数据)

mutations 状态怎么发生变化,有哪些发生变化(变化数据)

actions 处理副作用,在副作用的操作过程中提交 mutation,让状态发生变化


八、在组件中使用 vuex 

在启动文件中配置了 vuex 后,

vue 实例和所有的组件实例都会出现一个属性 $store


1、登录

使用仓库登陆,触发副作用操作 login 方法

this.$store.dispatch("login", {
  loginId: this.loginId, 
  loginPwd: this.loginPwd
});


如何实现登陆成功的提示效果?

actions 里面可以返回 true 或 false,然后通过返回可以做登陆成功的提示

actions:{
  async login(context, payload){
    context.commit("setIsLoading", true);
    const resp = await loginService.login(payload.loginId, payload.loginPwd);
    if(resp){
      context.commit("setUser", resp);
      localStorage.setItem("loginUser", JSON.stringify(resp));
      return true;
    }
    context.commit("setIsLoading", false);
    return false;
  }
}


$store.state.loginUser.isLoading 不是属性也不是状态

<Loading :show="$store.state.loginUser.isLoading"/>


如果需要使用仓库里面的数据,通常使用计算属性封装一下,这基本上是固定的模式

src/views/login.js

import Loading from "../components/loading.js";

const template = `<div>
  <div class="center">
    <p>
      <label>账号:</label>
      <input type="text" v-model="loginId"/>
    </p>
    <p>
      <label>密码:</label>
      <input type="password" v-model="loginPwd"/>
    </p>
    <p>
      <button @click="handleLogin">登陆</button>
    </p>
  </div>
  <Loading :show="isLoading"/>
</div>`;

export default{
  name:"login登陆组件",
  template,
  components:{
    Loading
  },
  data(){
    return {
      loginId: "",
      loginPwd: ""
    }
  },
  computed:{
    isLoading(){
      return this.$store.state.loginUser.isLoading;
    }
  },
  methods:{
    async handleLogin(){
      const result = await this.$store.dispatch("login", { // 返回触发的结果
        loginId: this.loginId, 
        loginPwd: this.loginPwd
      });
      if(result){ // 判断触发的结果,登陆成功跳转的首页
        this.$router.push("/");
      }else{
        alert("账号或密码错误");
      }
    },
  },
}


如果组件中使用仓库中的数据,需要计算属性封装,也可以使用 vuex 里面的 mapState 函数简化操作

computed:Vuex.mapState({
  isLoading: state => state.loginUser.isLoading
}),

仓库配置里面有 isLoading 属性,

会自动生成这样一个对象,生成的对象里面就有一个 isLoading(){} 属性

{

  isLoading(){

    return this.$store.state.loginUser.isLoading;

  }

}


退出登陆

src/components/header.js

const template = `<nav>
  <div class=""left>
    <router-link :to="{
      name: 'home',
    }" exact
    >首页</router-link>
    <router-link :to="{
      name: 'article',
      params:{page:1}
    }"
    >电影页</router-link>
  </div>
  <div class="right" v-if="loginUser">
    <span>{{loginUser.name}}</span>
    <button @click="loginOut">退出登陆</button>
  </div>
</nav>`;

export default{
  template,
  computed:{
    loginUser(){
      return this.$store.state.loginUser.data;
    }
  },
  methods:{
    loginOut(){
      this.$store.dispatch("loginOut");
      this.$router.push("/login");
    }
  }
}

退出登陆的过程中,

仓库的数据变了,

仓库的数据变了,计算属性的依赖项变了,

计算属性一变,就会重新渲染,v-if="loginUser" 的元素自然就消失了


2、鉴权

有些页面是不能直接访问的,比如电影页是不能直接访问的,

最简单的办法是在 mounted 里面判断一下,如果没有登陆跳转到 login 页面登陆,但是如果有很多页面

mounted(){
  if(!this.$store.state.loginUser.isLoading){
    this.$router.push("/login");
    return;
  }
  this.setMovies();
  this.setNavClass();
},


如果有很多页面需要登陆,会导致 mounted 里面写重复代码

因此可以使用导航守卫,登陆通常会和导航守卫配合使用,又回到导航的知识了


使用 vuex 和路由结合,实现页面的鉴权

在路由里面使用导航守卫,因为登录通常与导航守卫配合起来用


什么是导航守卫呢?

导航守卫是一些 router 的配置函数,不同函数在不同时候运行(有点像生命周期)


回到导航配置这里,

导航守卫里面有很多函数,其中一个函数 beforeEach( function() {}) 全局导航守卫,就像守卫一样要经过它

1. 参数传入的是一个函数,该函数会在每次进去页面之前运行

2. 一旦注册的该守卫,除非在守卫中调用 next 函数,否则不会改变地址

router.beforeEach(function(to, from, next){
  console.log(from, to)
})

from 表示之前从哪个页面来,跳转到这个 to 页面

{

  fullPath: "/article/1",

  hash: "",

  matched: [{…}],

  meta: {},

  name: "article",

  params: {page: '1'},

  path: "/article/1",

  query: {},

  [[Prototype]]: Object

}

to

{

  fullPath: "/",

  hash: "",

  matched: [],

  meta: {},

  name: null,

  params: {},

  path: "/",

  query: {},

  [[Prototype]]: Object

}


要判断 to 的页面是否需要登陆,

如何判断呢?

在导航配置里面加一些额外的信息 meta

import Home from '../pages/home.js';
import Msg from '../pages/msg.js';
import Project from '../pages/project.js';
import MoviePage  from '../pages/moviePage.js';
import Detail from '../pages/movieDetail.js';
import Login from '../pages/login.js';
import Store from "../store/index.js";

const router = new VueRouter({
  routes: [
    {path: "/", component: Home},
    {path: "/index.html", component: Home},
    {path: "/movie", component: MoviePage, 
      meta:{needLogin: true} // meta自定义的数据,该数据通常会被导航守卫使用
    },
    {path: "/detail/:id", component: Detail,
      meta:{needLogin: true}
    },
    {path: "/project", component: Project},
    {path: "/Msg", component: Msg},
    {path: "/login", component: Login},
  ],
  mode: "hash", 
  base: '/06 my-site',
})

// 注册全局导航守卫
router.beforeEach(function(to, from, next){
  if(to.meta && to.meta.needLogin){ // 跳转的页面有meta并且needLogin有值,是需要登陆的页面
    if(Store.state.loginUser.data){
      next();
    }else{
      next("/login");
    }
  }else{
    next();
  }
})

export default router;


1. 配置 meta

meta 是一个自定义的数据,叫原数据,它什么都可以配置,一般配置为一个对象

meta:true

meta:123

meta: {}


2. 判断 to 的页面,是否是需要登陆的页面,

跳转的页面有 meta,并且 needLogin 有值,是需要登陆的页面

if(to.meta && to.meta.needLogin)


3. 页面有没有登陆呢?

需要把 store 仓库导入进来,然后判断一下仓库里面的数据


导入 js 路径错误

Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "". Strict MIME type checking is enforced for module scripts per HTML spec.


九、仓库分模块

仓库里面的数据不仅有登陆用户,还有电影,

仓库里面管理的数据越多,代码会越来越多,所以仓库也可以分模块

| store

  |- index.js

  |- movie.js

  |- loginUser.js

  

直接导出一个对象

模块中通常都会配置 namespaced: true,除了在模块内部,外面触发 mutations 或 actions 时,必须添加模块名称(命名空间)

state 状态里面不用 loginUser 对象了,直接写两个数据

模块内部 mutations 里面也不能写 loginUser 了

其他地方没有变换

import loginService from "../services/loginService.js";

export default {
  namespaced: true, // 开启命名空间
  state:{
    data: null,
    isLoading: false,
  },
  mutations:{
    setIsLoading(state, payload){
      state.isLoading = payload;
    },
    setUser(state, userObj){
      state.data = userObj;
    }
  },
  actions:{
    async login(context, payload){
      context.commit('setIsLoading', true);
      const resp = await loginService.login(payload.loginId, payload.loginPwd);
      if(resp){
        context.commit('setUser', resp);
        localStorage.setItem("loginUser", JSON.stringify(resp));
        context.commit('setIsLoading', false);
        return true;
      }
      context.commit('setIsLoading', false);
    },
    loginOut(context){
      context.commit("setUser", null);
      localStorage.removeItem("loginUser");
    },
    syncLogin(context){
      const local = localStorage.getItem("loginUser");
      if(local){
        const user = JSON.parse(local);
        context.commit("setUser", user);
      }
    },
  }
};



modules 属性配置模块

import loginUser from "./loginUser.js";
import movie from "./movie.js";

export default new Vuex.Store({
  modules:{ // 配置模块
    // abc: loginUser 模块名是abc
    loginUser
    movie
  }
});


store.state 仓库状态里面有两个模块的名字

1. store.state.movie  仓库里面的属性名是 movie

2. store.state.loginUser 仓库里面的属性名是 loginUser


开启了命名空间后

外面要触发 actions  或 actions 必须要把模块名(命名空间)加上,以防止命名冲突,模块内部不用加命名空间

/**
 * src/components/header.js
 * 退出登陆这里加上模块名
 */
methods:{
  loginOut(){
    this.$store.dispatch("loginUser/loginOut");
    this.$router.push("/login");
  }
}


/**
 * main.js启动文件
 * 同步本地存储这里加上模块名
*/
store.dispatch("loginUser/syncLogin");


/**
 * src/views/login.js登陆文件
*/
methods:{
  async handleLogin(){
    const result = await this.$store.dispatch("loginUser/login", {
      loginId: this.loginId, 
      loginPwd: this.loginPwd
    });
    if(result){
      this.$router.push("/");
    }else{
      alert("账号或密码错误");
    }
  },
}


.htaccess 文件

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /myCode/blog/
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /myCode/blog/index.html [L]
</IfModule>



Leave a comment 0 Comments.

Leave a Reply

换一张