vue 集训营笔记
以前的开发,
我们关心的是 DOM 元素的处理,jQuery 只是简化的 DOM 操作
现在前端发展到单页面应用,
就是整个网站只有一个页面,或者是某一个功能块只有一个页面,这就是单页面应用程序
面对单页面应用程序,很多的数据、DOM元素全部要要 js 来处理,
这种情况用传统的开发就显得麻烦了,
vue 解决了这个问题,它能够在复杂的系统里面降低项目的复杂度
Vue 的特点
1. 渐进式,意思是 vue 的侵入性很少,因此使用 vue 和很多其他前端技术联用
2. 组件化,面对一个复杂的页面时,可以把页面划分为很多很多的区域,每一个区域做成一个组件
3. 响应式,指的是数据响应式,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)搭建工程
示例,
先引入 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的模板替换掉 --> <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实例 const app = new Vue(config);
1、vue 实例
我们想要使用 vue
1. 先要通过 new Vue(config) 创建一个 vue 对象,这个对象就是 vue 的实例
2. 参数 config 是配置对象,配置对象就是配置 vue 里面具有哪些功能,要做什么事情等各种配置
app 是 vue 实例,我们可以通过 vue 实例更改数据,数据的变化会导致界面重新渲染,这就是响应式
app.title="修改标题"
属性 title 在配置对象 config 的 data 里面,
config.data.title = "修改title标题" 为什么不在 config.data 上修改
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:
vue3 不在使用 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、配置对象
对象里面的配置
配置名 | |
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表达式}}
指令:
通常作为元素的属性存在,名称上以 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.
一个一个手写商品的数据,可以显示并且具有响应式
// 模板 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].name = "锤子" 可以修改 name 属性
v-for
一般使用 v-for 循环商品的数据
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 提升到实例里面了,重新给 products 属性赋值时 vue 会收到通知,
但是这里没有给 app.products 重新赋值,我们是调用的是 js 的数组方法 push
vue 并不知道我们调用了数组的 push 方法,
所以,vue 重写了数组里面很多的方法,对比一下两个 push 不是一个方法
app.products.push === Array.prototype.push 返回 false
这个叫 vue 的数组变异,
所以对数组的各种操作 vue 都能收到通知
因为没有给 [0] 索引加上 definedPropty
app.products[0] = {name: "红米手机", stock: 200}
直接使用索引给数组赋值,虽然数据变了,但是vue收不到通知
app.products[0].name = 123
但是可以更改对象的属性是没问题的
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>`;
也可以写一个方法,
在模板里面直接写“方法的名称”就可以调用了
// 模板 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; }, }, }
app 实例里面不仅可以看到 products、title,
还可以看到 changeStock 方法,为了在模板中方便使用,methods 配置里面的方法也会提升到 vue 实例中,方法提升是为了可以在模板中调用
方法里面的 this 指向的是 vue 实例(点击一下增加或减少库存,触发事件函数返回 true)
methods: { changeStock(prod, newStock){ console.log(this === app); //true }, },
元素的显示和隐藏
v-if 可以和 v-else 连着用
v-show 要写两个才能实现同样的效果
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> <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>
文本框显示库存
要设置 value 属性的值,value 的值来自于 js 表达式,
不能这样写 value="item.stock" ,这样设置的是 html 元素的属性,写什么就显示什么,
要使用 v-bind:value 绑定属性,也可以使用语法糖 :value="item.stock"
<input type="number" min="0" :value="item.stock"/>
改变文本框的数字,库存也要跟着变,
还要注册文本改变的 input 事件,只要打字就会不断触发该事件,然后写一个事件函数处理
事件函数有两种调用方式,
1. 之前是调用的方式,可以传一些额外的参数,比如按钮上面的事件 <button @click="changeStock(item, item.stock-1)">
2. 现在直接写函数名 @input="handleInput",该方式会自动把事件参数 e 带到事件函数里面
打印事件对象 e
// 模板 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);
可以通过 e.target.value 拿到新的库存,然后赋值给对应的数据,但是这种场景应该使用双向绑定
双向绑定
使用调用事件函数的方式,完成一个双向绑定
<input type="number" min="0" @input="handleInput(item, $event)" :value="item.stock"/>
1. 传两个参数,
$event 表示是事件对象,通过事件源拿到新的库存
item 当前商品的数据
2. 拿到新的库存,然后赋值给对象的 item.stock 属性,
3. 数据是响应式的,数据一变 :value 绑定的 value 值也跟着变( :value="item.stock" ),这样就完成了双向绑定
// 模板 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);
也可以在行间完成双向绑定,
拿到 value 改变后的值,直接赋值给 item.stock 库存,对象的 stock 是响应式的,stock 变了表单的 value 也跟着变了
<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,效果完全一样,它就是一个语法糖
绑定 :value 属性,
并自动注册 @input 事件
<input type="number" min="0" v-model="item.stock" />
删除一个商品
从数组里面移除一项,this 指向 app 对象,所以 methods 方法里面的函数不能用箭头函数
remove(index){ this.products.splice(index, 1); },
添加商品
按照以前的想法,获取 input 元素 value 的值,然后往 products 商品数组中加一项,
想法就错了,
vue 的一切全是数据,连添加里面的东西也是数据
1.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 }; }, }, }
v-html 指令
vue 为了安全,会将元素内部的 {{插值}} 进行实体编码
什么是插值?
差值是在 div 元素内部,使用一个表达式 {{js...}} 算出来的结果
vue 会在 {{插值}} 的位置自动进行实体编码,因此我们看到 span 元素是编码后的结果(右键 Edit as HTML)
<p><span style="color:#f40">带标签的内容</span></p>
为了防止用户输入一些标签,进行注入攻击(xss攻击)
如果信任这个数据使用用 v-htlm 指令,一般用在富文本框提交的内容
<div v-html="html"></div> 相当于 div.innerHTML = '<p><span color="#f40">带标签的内容</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 方法,然后在模板里面可以调用函数
methods 里面的函数是没有响应式的,但是数据有响应式,
当修改了数据 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);
关于计算属性 computed
1. 计算属性里面的配置会提升到 vue 实例中,
因此,在模板里面可以直接当做“属性”使用, 使用时,实际上调用的是对应的方法
2. 通常情况下,计算属性里面都会使用“data”或来自其他的“计算属性”计算得到的数据(否则尽量不要用计算属性了)
计算属性与方法的区别(面试经常问的问题):
第一个区别
vue 会检查计算属性的依赖,当依赖没有发生变化时,vue 会直接使用之前缓存的结果,而不会重新计算,这是为了提高效率,
只要两个依赖的项不发生变化,就不会调用计算属性的函数,
比如,点击按钮,数据变了 n++,数字变了肯定要重新渲染
1. 计算属性只是一开始调用了一次,点击按钮重新渲染页面,但没有重新调用计算属性的函数,
因为计算属性依赖的两数据没有变,计算属性就不会重新计算,
如果修改了依赖 app.firstName = "Moo" ,计算属性重新运行了
2. 而每次点击按钮,都会重新调用方法
所以,除非处理事件,否则能用计算属性尽量用计算属性,因为计算属性的效率更高
计算属性实现这一点的原理
1. 因为 data 里面的东西会提到 vue 实例里面,
在实例里面这些提升的属性是用 definedPropty 定义的,读取属性的时候 get 函数会监听到
2. 于是当发现用计算属性 {{fullName}} 的时候,就知道调用了 computed 配置的 fullName 函数,
这时候 vue 会做一张表进行缓存 fullName 属性
3. 在 get 函数里面,检查有没有调用这两个依赖...
第二个区别
计算属性的读取函数,不能有参数(读取属性时,参数也没有意义)
方法可以有人任意的参数
第三个区别
计算属性可以配置 get 和 set,分别用于读取和设置
如果需要设置计算属性
1. 计算属性就不是函数了,要配置为一个对象
2. 对象里面有 set 和 get 两个函数
3. set 函数需要一个参数 set(newVal),参数就是给计算属性赋的值
const template = `<div> <p>姓:{{firstName }}</p> <p>名:{{lastName}}</p> <p>计算属性拼接:{{fullName}}</p> </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);
fullName = "Moo尼卡" 赋值的时候实际调用 set 函数,并把新的值传进去了 fullName.set("Moo尼卡")
fullName 读属性候直接调用的是 get 函数,没有参数 fullName.get()
v-model="fullName" 双向绑定计算属性
文本框的内容变化,就会给计算属性重新赋值 app.fullName = "Mo尼卡"
const template = `<div> <input type="text" v-model="fullName"/> </div>`
二、组件
什么是组件?
组件是页面中的一个可复用的功能单页,
对于开发者而言,组件就是一个配置对象,跟 new Vue({}) 的配置类似
组件的注册
1. 全局注册
2. 局部注册
原则上推荐局部注册,
不推荐全局注册,除非是太通用的组件
对于直接在页面上引用 vue.js 的方式,两种组件注册的区别不大
用脚手架搭建工程时,用全局注册,最后打包部署的时候 js 文件的体积大小,
除了全局通用的组件,尽量使用局部注册
全局注册组件
构造函数 Vue 里面提供了一个 component 方法
1. 组件库名称,名称是一个字符串(注意,组件名称不是组件配置对象的名称)
2. 组件配置对象
/** * page组件 * 对于开发者而言,组件就是一个配置对象 * */ 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("MyPager", page) 两个单词首字母大写
Vue.component("my-pager", page) 短横线
局部注册
配置一个 components 对象
属性名:表示的是组件名是 MyPage
属性值:是组件的配置对象 page
/** * 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 同名
创建 vue 工程:
使用在页面引用 vue.js 的方式创建工程
ES6 模块化
1. script 元素加上 type="module" 浏览器会当做一个模块化来解析
模块中的变量是局部的,只能在模块内部使用
2. 模块导出 export default 每个模块只能导出一次,如果数据多用对象
3. 模块导入 import 变量名 from 模块路径 ,注意,在所有代码之前导入
工程结构
|- my-site
|- src
| |- assets
| | |- vue.js
| | |- index.css
| |
| |- app.js
| |- main.js
| |- movieList.js
| |- pager.js
|
|- index.html
indxe.html
是我们要运行的页面,里面写一个 div#app
<!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 和我们自己写的代码
assets 文件夹叫嵌入式的资源,或者叫静态资源,
通常会放一些我们要引入的第三方库,或者是 css 代码等,
以后用构建工具时,就不会放第三方库了,一般放一些静态资源,图片、css等
mian.js 启动文件
// 只负责启动 vue 和启动时的配置,所有的界面交给 app.js 来渲染 import App from "./app.js"; new Vue({ template: `<App />`, components:{ App, }, el: "#app", });
app.js
根组件,整个页面的内容靠根组件来完成
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代码,
组件尽量用大驼峰 MovieList,不然会出问题的,避免跟html元素重复
movieList.js
负责渲染电影列表
// 渲染电影列表 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
|- MovieList
| |- Movie
| |- Movie
| |- Movie
|
|- Pager
顶层是 app 根组件
顶层组件里面渲染了 MovieList 和 Pager 两个组件,
MovieList 渲染的时候,又渲染了多个 Movie 组件
三、组件的状态和属性
组件的数据就来自两个配置
1. 一个是属性 props,属性里面的数据组件自己不能改
2. 一个是状态 data,状态里面数据组件能改
因为组件是单项数据流,
只有数据的所有者才有权利修改这个数据,
通过属性 props 传过来的数据,是不允许组件自己修改的
1、组件状态
组件配置中的 data,是需要组件自身管理的数据,叫做组件的状态(component state)
这种叫法是从 react 里面来的,因为 vue 本身有很多地方在模仿 react,只不过做了很多改进
组件的状态的特点:
组件状态 data(state 状态)是属于组件内部的,只能在组件内部的使用,
跟外面的使用者没有任何关系,原则上外部不可以使用,外面也能通过 ref 得到,不过通常不会这样做
组件和 vue 实例的区别:
1. 在组件中 data 必须是一个函数,而 vue 实例中直接是一个对象
data(){ return { // 返回的对象是组件的状态 } },
为什么组件 data 配置不是一个对象?
因为组件会重复使用,
比如,组件 <Movie/> 会重复使用,还有可能别的组件也会使用到 <Movie /> 组件,
如果 data 配置的是对象,会导致所有的组件 <Movie /> 共享一个对象地址,其中一个组件变了,其他组件也会跟着变,
这是 vue 不希望看到的,vue 认为每一个组件是相互独立的,多个 <Movie /> 之间互不干扰
组件 data 写成函数,每调用一次组件,就会调用一次函数,得到组件的数据
为什么 vue 实例可以直接写成对象呢?
因为 vue 实例只有一个,而且一个 vue 实例对应到页面中的一个区域
2. 组件中可以有属性(component props),而vue实例中没有
2、组件的属性
组件的数据来源,
除了 data 之外还有一个属性 props
声明组件的属性,就是组件需要哪些数据,可以用数组的方式声明
props:["current", "pageSize", "total", "panelNumber"],
使用组件属性
1. 使用的时候把属性传进去,跟 html 元素里面传属性一样
2. 声明属性时用小驼峰,传递属性时可以短横线或小驼峰
<Pager :current="1" :total="100" :page-size="10" :panelNumber="5"/>
组件属性的命名可以用(小驼峰方式也可以用短横线)
1. 短横线 page-size
2. 小驼峰 pageSize
3. 声明和使用时都可以使用短横线或小驼峰,这两个是互通的
命名的区别
1. 组件 短横线或大驼峰
2. 属性 短横线或小驼峰
3. 状态 短横线或大驼峰
还可以用对象的方式精细的控制属性
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 类型检查错误
期望是一个 Number,但是传的是一个字符串
如果设置必填,没有填,会提示你缺失了一个属性
[Vue warn]: Missing required prop "total"
属性会被提升到“vue 组件实例”中(注意,不是 vue 实例)
1. 我们写的这个 export default{} 对象,我们习惯上叫组件,实际它不是组件,它叫做“组件配置对象”
2. 这个配置对象内部会帮我们生成一个“组件实例”,
组件实例跟 new Vue 实例差不多的意思
3. 组件实例里面也会挂载一些 data、motheds、props,所以在模板里面可以直接使用 {{total}}
属性是不允许组件自己修改的
[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 一个属性
[Vue warn]: Invalid default value for prop "movies": Prop with type Object/Array must use factory function to return the default value.
属性是一个数组或一个对象,默认值必须要用一个函数生成,
因为引用地址的问题,组件要用很多多次,组件的 props 配置是统一的,如果直接写数组,默认值就是同一个数组了
组件的属性是只读的,不允许更改。为什么有这样的理念呢?根本原因是要保证单向数据流
3、分页组件需要的属性
1. current 当前页码
2. pageSize 页容量
3. total 数据总量
4. panelNumber 页面显示的页码
5. 通过页容量 pageSize 和总页数 total,写一个计算属性 pageNumber 计算总页数。两个依赖项任何一个变了,总页数 pageNumber 马上会重新计算重新渲染
分页 demo
<!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;margin-bottom:100px;} .pager .pager-item {display:inline-block;padding:5px 10px;border:1px solid #ccc;margin:8px;cursor:pointer;color: rgb(96, 96, 224);} .pager .pager-item.disabled{color:#ccc;cursor:not-allowed;} .pager .pager-item.active {color:#f40;border:none;cursor:auto;} </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>
v-bind 指令
简写 :
绑定 :class 主要有两种写法
1. 字符的写法
:class="js表达式" 会把绑定的结果追加到前面的 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>
指令修饰符
href 属性会导致页面刷新,事件 v-on 是指令,vue 中不同的指令涉及到不同的修饰符
<a href="" @click.prevent="changePage(current + 1)" class="pager-item" :class="{disabled: current === pageNumber}">下一页</a>
修饰符可以增强或更改指令的某些功能
.prevent 用于 v-on 事件指令,表示阻止默认行为
.stop 也是用于 v-on 指令,表示阻止事件冒泡
.number 把用户填写的东西转换成数字类型,<input type="number"/>
.native
计算属性 Numbers
依赖 current 和 panelNumber,这两个一变就重新计算
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 循环的优先级非常高,
它要先循环,在循环生成的 item 的过程中,才去确定 :class 绑定的类样式
官方文档还强调了这一点
v-if 和 v-for 不要在同一个元素里面用,容易造成误解,
因为加上 v-if 后,我们可能觉得元素要么显示,要么不显示,
其实不是的,
元素是先进行循环,在每一次循环的时候决定元素是否要显示,循环的优先级非常高
4、什么是单项数据流?
就是数据从一个方向流入,从一个方向流出,就是单向数据流
单项数据流的的概念来自于函数式编程
示例
函数 sum 就是一个单项数据流
function sum(a, b){ reurn a + b; } sum(2, 3);
当调用 sum 函数的时候
形参 a 和 b 不是数据,实参 2 和 3 是数据,
我们把数据传给函数后,
函数没有修改数据,只是计算后返回一个新的数据,这就是单项数据流
示例
什么情况下会破坏单项数据流呢?
两个数据传进去,没有更改任何数据,直接返回一个结果,这是单向数据流
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});
副作用操作
修改了参数,或动了外面的东西,或者使用异步,这些都叫做副作用
为什么不仅是 Vue,还有 React 都是要保证单向数据流呢?
因为单向数据流是最不容易出问题的,
以后我们开发的系统很复杂,组件会非常非常的多,组件的嵌套层次非常非常深,
可以这样理解一个函数调用另一个函数、一个函数调用另一个函数,嵌套的层次非常非常深,
如果某一个数据出问题了,没有使用单向数据流,任何一个函数都有权利修改我们的数据,我们不知道是哪个函数把数据改了,关系错综复杂不好调试
如果是单项数据流,在函数里面、组件里面不可能更改我的数据,
因为数据属于谁谁负责,数据错了,数据是你的,你负责,
这样非常容易调试,也非常容易被理解,
单向数据流是非常容易被人类理解的东西,
就是输出、输出,我给你一个东西,你给我一东西,是单向数据流
数据 current、tobal、pageSize 是属于app 组件的,
pager 组件没有权利改,
如果需要改只能由 app 组件来改
<Pager :current="2" :total="210" :page-size = "10" />
属性和跟状态最大的区别是,
属性是只读的,不允许修改,
如果要修改属性,需要 pager 组件抛出事件,通知 App 父组件修改
四、自定义事件
在组件中触发事件,并把事件参数传过去,让父组件收到通知,
事件名 change,事件名可以小驼峰也可以短横线
事件参数是新页码
this.$emit("change", 事件参数)
事件是一种回调模式,
就是说我知道发生了什么事,但是我不知道要干嘛,就需要回调了
比如,
触发事件的时候 this.$emit("change", newPage)
Pager 组件知道要变页码了,知道一定有事情发生了,
但是事情不是 Pager 组件来做的,所以只能触发事情,让别人来做这件事情
触发事件的源码是监听者模式
1. 注册事件 @change="current = $event"
这个 current = $event 会放到一个函数里面,然后把函数加入到一个数组
2. this.$emit("change", newPage) 触发事件这里是循环数组,运行每一个函数
App 组件的状态 current 是作为属性传给 Pager 组件
1. 点击了一个分页后,页码不是 Pager 组件的,它不能修改页码数据,
2. 于是触发一个 "change" 事件,把事件参数新页码 newPage 扔给 App 组件
3. App 父组件注册了 @change 事件后,可以监听到这个 change 事件(会把事件参数 newPage 传递给 $event)
然后父组件改变当前数据 current = $event
4. 当 App 组件中的状态(data 里面的 current)发生变化时,该组件会重新渲染,因为数据是响应式的,
如果 App 上面还有组件,跟上面的组件没关系
5. App 组件数据更新后重新渲染,重新渲染的过程中,导致 Pager 组件的属性 :current 也跟着变了,组件的属性的变了 Pager 组件也会重新渲染
当一个组件中的状态发生变化时,该组件会重新渲染,在渲染的过程中,可能导致其子组件的属性发生变化,而属性的变化也会导致组件重新渲染。但根本原因是状态的变化
也就是说一个组件重新渲染有两种情况
1. 组件自己管理的状态发生变化
2. 组件的属性发生变化
点击分页后,
Pager 子组件触发了 change 事件通知父组件,
父组件处理了该事件,也就是注册了 @change 事件,并且更改了自己的状态,状态变了组件重新渲染,
导致 Pager 子组件的属性也跟着变了,子组件的属性变了也会重新渲染
看一个有意思的事情
1. Pager 组件属性
current 属性改成名 value,
模板里面的 current 也改成 value
触发事件的名字改成 input
2. App 父组件,把下面两行代码,换成语法糖 v-model="current"
:value="current"
input="current = $event"
3. 这是固定的用法,实现类似于双向绑定的效果,实际上还是单向数据流,无非就是少些几行代码
// 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; } 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="7" :total="total" :page-size="pageSize" v-model="current" /></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" });
v-model 的本质是一个语法糖,
实际上是绑定 value 属性,同时监听 input 事件,依然是单向数据流
五、生命周期
一个组件运行的过程中,它什么时候出生,什么时候死亡?
比如,一个用了 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(Mount挂载的意思,挂载之前)
组件即将进行渲染,但是还没有进行渲染,此时已经编译好模版,已经满足了所有的渲染条件,
和上面的 created 一样,仍然得不到页面的 dom 元素
4. mounted [重要]
组件已经完成了渲染,生成真实的 dom,可以看见页面了
我们通常会在这个函数里面处理很多事件,
因为在前面那三个函数里面,如果代码要执行很多很多东西,会导致代码运行时间长,卡主,导致用户看不到东西,因为页面还没有渲染,
这个函数里面页面已经渲染好了,用户至少看得到东西了,ajax 请求写到这里
5. 此时,等待组件更新,
已经完成渲染了,等待组件组件更新
什么时候更新?
当一个组件的属性或状态发生变化的时候,会自动重新渲染
6. beforeUpdate
当组件既将更新,但还没有更新(此刻已经完成渲染了,等待组件更新),
此时得到的数据是新的,但是界面是旧的
6. updated
组件已经完成更新,此时数据和界面都是新的
7. beforeDestroy
当组件即将被销毁时候
什么时候被销毁?
整个组件不在显示了,通常情况下都是因为 v-if 不显示了
8. destroyed(过去完成时)
当组件已经被销毁后
一般销毁一些附带的资源,
比如组件一开始的时候开启了一个计时器,组件销毁的时候要顺带把计时器清除
{ mounted(){ // 一开始开启了一个计时器 this.timer = setInterval(() => {}, 1000); }, destroyed(){ // 组件销毁时候,要清除掉这个计时器 clearInterval(this.timer); } }
根据当前页码和页容量截取数组
[1,2,3,4,5,6,7,8,9]
条件
用数组的 slice 方法
arr.slice(起始下标, 结束下标);
pageSize: 2 每页取两条
current: 1 当前页码为1
公式
第一个数字是:(current当前页码 - 1) 乘 pageSize
第二个数字是:current当前页码 乘 pageSize
示例
(current - 1) * pageSize
(current * pageSize
arr.slice(0, 2) 实际取不到2,取出来的结果是0 - 1,形成一个新的数组 [0,1]
根据当前的页码和页容量计算
current: 1 -> 从 0 取的到 2(实际是 0-1,2 取不到)
current: 2 -> 从 2 取的到 4(实际是 2-3,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" />
六、插槽
插槽的位置就是预留一个空间
插槽位置 <slot></slot> 放置的是使用组件的时候传递的元素内容,如果不写会使用默认内容
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="https://20180416-1252237835.cos.ap-beijing.myqcloud.com/common/vue.min.js"></script> <style> .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%);color:#Fff;} </style> <title>slot</title> </head> <body> <div id="app"></div> <script> const Modal = { template: `<div class="modal"> <div class="center"> <slot>默认的内容</slot> </div </div>`, } const App = { template: `<div class="app"> <Modal> <p>正在加载...</p> <button>按钮</button> </Modal> </div>`, components:{ Modal, } } new Vue({ template: `<App/>`, components:{ App, } }).$mount("#app"); </script> </body> </html>
<div class="app"> 父组件
<div class="modal"> 子组件
<div class="center">
<p>正在加载...</p> 插槽部分
<button>按钮</button>
</div>
</div>
</div>
具名插槽
没有名字的是默认插槽,给插槽命名的是具名插槽,具名插槽可以写多个
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, } }
七、vue-router
vue 路径,可以简单理解为,当访问某个地址时,渲染某个组件
在 index.html 页面
首页先引入 vue.js
再引入路由 vue-router.js
1、使用路由
在启动 main.js 文件里面配置路由
1. 使用构造函数 VueRouter 创建一个路由,得到一个路由对象
参数是一个配置对象
2. 在创建 vue 实例时,将路由对象 router 配置到实例配置的中
import App from "./app.js"; import Home from "./pages/home.js"; import MoviePage from "./pages/moviePage.js"; // 1.得到一个路由对象router, const router = new VueRouter({ // 3.参数是配置对象 routes: [ // 当访问 path 地址的时候,渲染 component 组件 { path: "/", component: Home }, { path: "/movie", component: MoviePage } ], mode: "hash" }); new Vue({ template: `<App/>`, components:{ App, }, el: "#app", router // 2.将路由对象配置到实例配置的中(变量名和属性名一样使用简写) });
路由配置对象
1. routes 路由规则配置
2. mode 模式配置
routes 路由规则配置
import Home from "./pages/home.js"; import MoviePage from "./pages/moviePage.js"; { routes: [ // 当访问 path 地址的时候,渲染 component 组件 { path: "/", component: Home }, { path: "/movie", component: MoviePage } ], mode: "hash" // 默认就是hash } export default router;
2. mode 模式配置
hash 模式,兼容性最好,地址出现在#号后面,切换地址面不会导致页面刷新
history 模式,使用的是H5的 History API,地址直接变化,并且页面不刷新
3. 在合适的位置写上 <router-view></router-view> 组件
1. 它表示路由匹配到的组件渲染的位置,
2. 实际上是 vue-router 做好的一个组件,并且进行了全局注册
3. 使用方式可以大驼峰或短横线
当配置好路由后,向所有实例增加了两个属性
1. $router 主要用来跳转页面
2. $route 主要用于获取路由信息
动态路由
在配置路由规则时,可以将规则字符串写为动态路由,动态路由使用冒号加单词
配置动态路由规则,
动态路由是 /defail/:id
它能够配置到这样的 /defail/??? 路径
import Home from "./views/home.js"; import Article from "./views/article.js"; import Detail from "./views/defail.js" const router = new VueRouter({ routes: [ { name: "home", path: "/", component: Home }, { name: "article", path: "/article/:currentPage", component: Article }, { name: "defail", path: "/defail/:id", component: Detail }, ], mode: "hash" }); export default router;
多个动态部分
// http://127.0.0.1:8848/defail/9/1/2 { path: "/defail/:id/:a/:b", component: Detail }
$route 路由信息
mounted(){ console.log(this.$route); } // 地址 http://127.0.0.1:8848/defail/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是参数的意思)
:data 开始绑定的数据是 null 所以报错,解决办法加上一个 v-if="movie"
<Movie v-if="movie" :data="movie"/> data(){ return { movie: null, isLoading: false } }, mounted(){ // 在mouted里面远程获取 const id = this.$route.params.id; // 获取动态路由里面的id movieApi.getMovie(id).then(resp => { this.movie = resp; }); },
[Vue warn]: Error in render: "TypeError: Cannot read properties of null (reading 'poster')"
vue.js:1902 TypeError: Cannot read properties of null (reading 'poster')
2、导航
使用 router-link 切换页面,不会刷新页面,本质上就是生成 a 元素
1. 模式设置 history
2. to 作为连接地址 <router-link to="/movie"></router-link>
router-link 自动给生成的 a 元素添加样式,如果当前地址配置 to="/movie" 中的地址添加
1. 当前地址为 /movie 有两个样式
精确匹配 router-link-exact-active
模糊匹配 router-link-active
2. 地址以 / 开头的,只有一个模糊匹配 router-link-active
编程式导航
当创建 Vue 实例里面配置了 router 之后,所有的 vue 实例以及组件实例里面都会包含两个对象
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实例里面多了两个对象
输出的 vue 实例多了两个对象
1. $router 主要用来跳转页面
2. $route 主要用于获取路由信息
用编程的方式跳转页面
1. 通过 this 拿到当前组件实例,然后找到实例里面的 $router 对象
2. $router 对象里面提供了很多方法,其中最常用的是 push
3. push("页面地址") 方法里面写的是跳转的地址
<p @click="handleClick">电影页</p> methods:{ handleClick(){ this.$router.push("/movie"); // 使用编程的方式跳转页面 } }
如果已经在首页,还点击了跳转到首页报错的解决方法
handleClick(){ this.$router.push("/article").catch(()=>{ }); }
push 有点类似于往数组里面追加一项,
这里的真实意思是,往当前页面地址栈中加入一个地址(这是 HTML5 里面 history Api 的知识)
可以简单理解
1. 页面是放到一个数组里面
2. 当前页面(最新的页面)就是数组的最后一位的页面4
[ 页面1, 页面2, 页面3, 页面4 ]
$router.push("页面地址") 就是往地址栈的末尾加入一个页面5,如果点击浏览器的后退,就后退到之前页面4
[ 页面1, 页面2, 页面3, 页面4, 页面5 ]
this.$router.replace("页面地址")
也是跳转页面,只不过 replace 方式是替换当前页面地址栈中当前位置的页面,
比如,把页面4替换成了页面5,这时候如果在点击浏览器的后退,后退到页面3
[ 页面1, 页面2, 页面3, 页面4 ] [ 页面1, 页面2, 页面3, 页面5 ]
this.$router.go(偏移量)
偏移量是数字,
根据当前地址栈中的位置,以及设置的偏移量,跳转页面
比如,
当前是页面3
[ 页面1, 页面2, 页面3, 页面4 ]
this.$router.go(1) 根据当前页面3的位置,向前方偏移一个,跳转到页面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)
八、vuex
|- 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.hs 登陆页
| |
| |- store
| | |- index.js 共享数据
| |
| |- router.js
| |- app.js 根组件
| |- main.js 负责启动
|
|- index.html
PS:
路由配置的路径必须 / 斜杠开头
[vue-router] Non-nested routes must include a leading slash character. Fix the following routes: - login
login 组件登陆成功 { loginId: 'admin', name: '超级管理员' },
header 组件要显示当前登陆的用户,
由于 login 组件没有使用 header 组件,不能通过属性传递数据,这时候有一种最简单的办法叫做状态提升
状态提升
1. 把登陆的状态数据提升 app 组件里面
2. 状态在 app 里面,可以把状态数据作为属性传给 header 组件
但是 app 组件的状态数据会变化,比如注销、登陆成功...
3. 只有 login 组件才能让 app 组件的状态数据发生变化,
4. 因为状态数据属于 app 组件了,login 组件引发(抛出)一个事件,
5. app 注册事件,就可以把状态数据修改了
6. 因为状态是响应式的,app 修改了状态,它要重新渲染
7. app 重新渲染,导致 header 的属性发送变化,它也重新渲染了
组件之间的通信有很多方式,两个最核心的是
1. 通过事件传递数据
2. 通过 props 属性传递数据
这两种方式只能做状态提升,
但是复杂系统里面,共享数据的情况很多
vuex 用于解决大量的、复杂的组件间共享数据的问题
首先要知道 vuex 出现的原因和作用,才能很好的去使用它,
实际上,一个小型系统直接用状态提升到 app 就可以解决了,
vuex 是用来解决复杂系统大型应用的,在小型应用 vuex 反而觉得复杂
vuex 的核心理念
1. 提出一个单独的模块叫数据仓库,共享数据全部放到这个仓库里面
2. 然后把这个仓库放到 vue 实例里面
3. 我们要更改数据,更改的是数据仓库里面的数据
4. 每一个组件都可以共享仓库里面的数据
1、使用 vuex
1. 首页引入 vuex.js
2. 新建 src/store/index.js 文件,export default 导出一个东西,导出的东西在 main.js 文件里面用
3. main.js 里面导入仓库 import store from store/index.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: store 这样不行) });
2、创建一个仓库
new Vuex.Store(配置对象) 创建仓库,通常一个 vue 应用,只对应一个仓库
配置对象
import loginService from "../services/loginService.js"; export default new Vuex.Store({ state:{ loginUser:{ data: null, // 表示当前登陆的用户为空,登陆成功值是一个对象 isLoading: false, // 表示是否正在登陆 }, movies:{ datas:[], page:1, 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} */ login(context, payload){ context.commit("setIsLoading", true); loginService.login(payload.loginId, payload.loginPwd).then(resp => { if(resp){ context.commit("setUser", resp); // 登陆成功给data状态赋值 localStorage.setItem("loginUser", JSON.stringify(resp)); } context.commit("setIsLoading", true); }); }, /** * 退出登陆 * 状态发生变化,setUser设置为null * 清空本地存储,这是副作用,上面setUser设置为null不是副作用 */ loginOut(context){ context.commit("setUser", null); localStorage.removeItem("loginUser"); 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)
loginUser 表示当前登陆的用户,套一层对象是避免和其他状态冲突
2. mutations 配置状态有哪些变化
mutations 是状态变化的唯一原因
每一个变化是一个函数,必须调用 mutations 里面的函数来改变
让状态变化的原因唯一,这样的好处在于,
如果状态出了问题,可以跟踪状态是经过了哪一个变化出的问题,
主要是为了便于调试,其实还有一个点是为了单向数据流(单向数据流说来话要很长很长)
mutations 里面函数的参数
1. state 名字是固定的
2. payload 名字可以自定义,表示额外的信息,payload 是负载的意思,可以理解为附加信息,该参数是可以选的
如何调用 mutations 配置?
commit 表示提交的意思,就是提交一次更改
导出的仓库 new Vuex.Store 是一个对象,
main.js 导入 仓库对象
必须通过 仓库对象.commit 进行调用
参数1,mutations 的名称
参数2,payload
import store from "./store/index.js"; store.commit("setIsLoading", true); // 提交一次更改
mutations 函数中不可以出现异步等副作用操作
为了调试的时候跟踪状态,不允许出现副作用操作
1. 不能有 ajax,因为要过一段时间才能执行完
2. 不能有定时器
3. 不能给 dom 注册一个事件,事件函数里面操作 dom 元素
4. 不能 localStorage 设置本地存储
5. 不能改变外部变量里面的一些东西,比如全局有一个 var obj = {}
6. 没有当前时间
7. 没有随机数
3. actions 配置副作用操作
每个 actions 是一个函数,函数的参数
1. context 上下文对象,几乎等同于仓库对象
2. payload
如何调用
actions 是分发出去的,不能直接调用,必须通过 仓库对象.dispatch 调用
测试
window.store = store 把仓库对象放到 window 里面
store 仓库对象里面有 commit 还有 dispatch
在控制台
store.dispatch("login", {loginId:"admin", loginPwd:"123"});
可以看仓库里面的状态有值了
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,让状态发生变化
3、使用仓库登陆
在启动文件配置了 vuex 后,
vue 实例和所有的组件实例都会出现一个属性 $store
比如,使用仓库登陆,触发副作用操作 login
handleLogin(){ 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 函数简化操作
使用 vuex.mapState 函数简化操作,
仓库配置里面有 isLoading 属性,生成的对象里面就有一个 isLoading(){} 属性
computed:Vuex.mapState({ isLoading: state => state.loginUser.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" 的元素自然就消失了
4、鉴权
比如电影页是不能访问的,
最简单的办法是在 mounted 里面判断一下,如果没有登陆跳转到 login 页面登陆
mounted(){ if(!this.$store.state.loginUser.isLoading){ this.$router.push("/login"); return; } this.setMovies(); this.setNavClass(); },
如果有很多页面需要登陆,这样做法会导致很多重复代码,
因此可以使用导航守卫,登陆通常会和导航守卫配合使用,又回到导航的知识了
5、导航守卫
什么是导航守卫呢?
导航守卫是一些 router 的配置函数,不同函数在不同时机运行(有点像生命周期)
导航守卫有很多函数,其中一个函数 beforeEach 可以实现登陆,
import Home from "./views/home.js"; import Article from "./views/article.js"; import Detail from "./views/defail.js" import Login from "./views/login.js" import Store from "./store/index.js"; // 导入仓库 const router = new VueRouter({ routes: [ { name: "home", path: "/", component: Home }, { name: "article", path: "/article/:page", component: Article, meta: { // 自定义的数据,该数据通常会被导航守卫使用 needLogin: true } }, { name: "defail", path: "/defail/:id", component: Detail, meta: { // 自定义的数据,该数据通常会被导航守卫使用 needLogin: true } }, { name: "login", path: "/login", component: Login }, ], mode: "hash" }); // 注册全局导航守卫 // 传入的函数会在每次进去页面之前运行 // 一旦注册的该守卫,除非在守卫中调用next函数,否则不会改变地址 router.beforeEach(function(to, from, next){ // 跳转的页面有meta,并且needLogin有值,是需要登陆的页面 if(to.meta && to.meta.needLogin){ if(Store.state.loginUser.data){ next(); }else{ next("/login"); } }else{ next(); } }) export default router;
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,
第一步,配置meta
meta 叫原数据,它什么都可以配置,一般配置为一个对象
meta:true
meta:123
meta: {}
第二部,判断 to 的页面是否需要登陆
跳转的页面有 meta,并且 needLogin 有值,是需要登陆的页面
if(to.meta && to.meta.needLogin)
第三步,页面有没有登陆呢?
导入 store 仓库
导入路径错误
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.
6、仓库分模块
仓库里面的数据不仅有登陆用户,还有电影,
仓库里面管理的数据越多,代码会越来越多,所以仓库也可以分模块
| store
|- index.js
|- movie.js
|- loginUser.js
modules 模块中通常都会配置 namespaced: true,除了模块内部,外面触发 mutations 或 actions 时,必须添加模块名称(命名空间)
store.state 仓库状态里面有两个模块的名字
store.state.movie
store.state.loginUser 仓库里面的属性名是 loginUser
开启命名空间,
因为有很多个模块,mutations 和 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("账号或密码错误"); } }, }