Vue 学习总结

本贴最后更新于 2042 天前,其中的信息可能已经时过境迁

第 1 章 课程介绍

1-1 课程简介

学习流程

知识点
学习前提:
有一些 css、js、es6、webpack、npm 等基础知识
学习收获

第 2 章 Vue 起步

2-1 学习方法

多看一下官网 api Vue 官网
务必把官网的小的 demo 自己敲一遍加深一下对语法糖的理解

2-2 hello world

那么初步认识一下 vue,那么从一个简单的 hello world 开始吧

<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Hello world</title> <script src="https://cdn.jsdelivr.net/npm/vue"></script> </head> <body> <div id="root">{{content}}</div> <script> var app = new Vue({ el: '#root', data: { content: 'hello world' } }); setTimeout(function () { app.$data.content = 'bye world' }, 2000) </script> </body> </html>

el:'#root' vue 实例化的数据只针对 id 为 root 内使用
{{content}} : 获取 vue 里面 data 里面的数据值

2-3 开发 TodoList(v-model、v-for、v-on)
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Hello world</title> <script src="https://cdn.jsdelivr.net/npm/vue"></script> </head> <body> <div id="root"> <div> <input type="text" v-model="inputValue"> <button v-on:click="handleAdd">提交</button> </div> <ul> <li v-for="(item,index) in list" v-on:click="handleRemove(index)">{{item}}</li> </ul> </div> <script> var app = new Vue({ el: '#root', data: { inputValue: '', list: [] }, methods: { handleAdd() { this.list.push(this.inputValue); this.inputValue = ''; }, handleRemove(index) { this.list.splice(index, 1); } } }); </script> </body> </html>

v-on:click="handleClick" 绑定点击事件
v-model="inputValue" 数据双向绑定
v-for="(item,index) in list" 数据循环

2-4 MVVM 模式

MVP (Model View Presenter)
Model:接口请求操作
View:页面展示
P:处理数据和页面(大量的 dom 操作)
MVP

MVVM
MVVM

而 Vue 则采用的是 mvvm 这种模式,它在乎的是数据的变化驱动 Ui 的变化,不用用户管 dom 的操作,vue 则扮演的是 VM 的操作,我们的重心是放在了 M 层也就是数据这一层

2-5 前端组件化

页面有好多部分组成,把页面切成一部分一部分,而拆分出来的部分,就是一个组件

2-6 使用组件改造 TodoList

1.全局组件的声明和使用

//Vue创建全局组件的方法 Vue.component('TodoItem', { props: ['content'], template: '<li>{{content}}</li>' }); <todo-item v-bind:content="item" v-for="item in list"></todo-item> ps: 数据的传递通过v-bind: 来定义传递的属性,后面跟上要传递的值 通过“props”来接受属性,再通过插值表达式来展示{{content}}

2.Vue 局部组件的创建和使用

//Vue局部组件的创建和使用 var TodoItem = { props: ['content'], template: '<li>{{content}}</li>' }; var app = new Vue({ el: '#root', components: { TodoItem: TodoItem } }) ps: 定义一个变量,值为对象,把属性值和模板作为属性的键值 在Vue实例化中通过components这个参数来调用这个定义好的变量(局部组件)
2-7 简单的组件间传值

父组件向子组件传值:
子组件通过定义一个属性:v-bind:content="item",将 item 值传给子组件的 content
子组件通过 props:['content'] 来接受父组件传过来的值,再通过插值表达式来展示{{content}}
子组件向父组件传值:
子组件通过定义一个方法或者一个事件 handleItemClick,在方法或者事件中,通过 this.$emit(''delete",this.index)方法给给父组件发送一个信号,
父组件监听这个信号:@delete="handleDeleleItem"
下面代码演示:

<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Hello world</title> <script src="https://cdn.jsdelivr.net/npm/vue"></script> </head> <body> <div id="root"> <div> <input type="text" v-model="inputValue"> <button v-on:click="handleAdd">提交</button> </div> <ul> <!--<li v-for="(item,index) in list" v-on:click="handleRemove(index)">{{item}}</li>--> <todo-item v-bind:content="item" v-bind:index="index" v-for="(item,index) in list" @delete="handleItemDelete" > </todo-item> </ul> </div> <script> //Vue创建全局组件的方法 // Vue.component('TodoItem', { // props: ['content'], // template: '<li>{{content}}</li>' // }); //Vue局部组件的创建和使用 var TodoItem = { props: ['content', 'index'], template: '<li @click="handleItemClick">{{content}}</li>', methods: { handleItemClick() { this.$emit('delete', this.index); } } }; var app = new Vue({ el: '#root', components: { TodoItem: TodoItem }, data: { inputValue: '', list: [] }, methods: { handleAdd() { this.list.push(this.inputValue); this.inputValue = ''; }, // handleRemove(index) { // this.list.splice(index, 1); // }, handleItemDelete(index) { console.log(index); this.list.splice(index, 1) } } }); </script> </body> </html>
2-8 Vue 的一些指令简写方法

v-on:click 等价于 @click //this.emit('delete') 接受也是 @delete
v-bind:content 等价于 :content

第 3 章 Vue 基础精讲

3-1 Vue 实例

vue 实例是根实例,组件也是 vue 实例,所以说页面是由很多 vue 实例组成的
data(): 以 $ 开头的指的是 vue 实例的属性或方法
vm.$destroy():用于销毁 vue 实例,但是之前的数据和方法并没有被销毁

var app = new Vue({ el:'#root', data:{ message:'hello world' }, methods: { handleClick(){}, }, watch:{ }, computed:{ } })
3-2 Vue 实例生命周期
//生命周期函数就是vue实例在某个时间点自动执行的函数 var app = new Vue({ el:'#root', data:{ inputValue:'' }, beforeCreate: function () { console.log('beforeCreate'); }, created: function () { console.log('created'); }, beforeMount: function () { console.log(this.$el); console.log('beforeMount'); }, mounted: function () { console.log(this.$el); console.log('mounted'); }, beforeDestroy: function () { //app.$destroy() console.log('beforeDestroy'); }, destroyed: function () { console.log('destroyed'); }, beforeUpdate: function () { //app.$data.inputValue = 1 console.log('beforeUpdate') }, updated: function () { console.log('updated') } })
3-3 Vue 的模版语法

插值表达式{{}} : 用{{输入的值}}
v-指令 写的是 js 表达式
v-text 就是 innertext 其实就是 {{}}
v-html 就是 innerhtml
v-指令 后面除了可以写 js 名称还可以加字符串,插值表达式也可以写字符串

var app = new Vue({ el: '#root', data: { name: 'hello', bigName: '<h1>hello</h1>' } }) {{name + ' world'}} <div v-text="name + ' world' "></div> <div v-html="name + ' world' "></div> ps:v-html 会对bigName进行转义,字体变成h1字体大小,而不会出现标签
3-4 计算属性、方法与侦听器

1.计算属性
computed 属性,因为他是属性,所以在用插值表达式取值的时候不用加括号
computed:内置变量缓存的功能,当 data 里面 age 变量更改时,如果不是计算属性内边的变量更改,那么他就不会渲染 computed 内部的变量

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script src="https://cdn.jsdelivr.net/npm/vue"></script> </head> <body> <div id="root"> {{fullName}} </div> <script> var app = new Vue({ el: '#root', data: { firstName: 'sunny', lastName: 'fan', age: 28 }, //计算属性:内置缓存(firstName、lastName) computed: { fullName: function () { console.log('计算了一次'); return this.firstName + " " + this.lastName } } }) </script> </body> </html>

2.methods 方法
因为是方法,所以插值表达式要用括号取值,
他不具有缓存变量的功能

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script src="https://cdn.jsdelivr.net/npm/vue"></script> </head> <body> <div id="root"> {{fullName()}} </div> <script> var app = new Vue({ el: '#root', data: { firstName: 'sunny', lastName: 'fan' }, methods: { fullName: function () { console.log("计算了一次") return this.firstName + " " + this.lastName } } }) </script> </body> </html>

3.侦听器

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>侦听器</title> <script src="https://cdn.jsdelivr.net/npm/vue"></script> </head> <body> <div id="root"> {{fullName}} {{age}} </div> <script> var app = new Vue({ el: '#root', data: { firstName: 'sunny', lastName: 'fan', fullName: 'sunny fan', age: 28 }, watch: { firstName: function () { console.log('计算了一次'); this.fullName = this.firstName + " " + this.lastName }, lastName: function () { console.log('计算了一次'); this.fullName = this.firstName + " " + this.lastName } } }) </script> </body> </html>

总结:我们可以通过 methods、computed、watch 来实现 fullName 显示的问题
computed 和 watch 都具备缓存的功能
但是从代码量的编写程度来看,computed 属性会更加方便和便捷一些。

3-5 计算属性的 getter 和 setter

computed 属性当中有两个方法,分别是:get 和 set

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>getter和setter</title> <script src="https://cdn.jsdelivr.net/npm/vue"></script> </head> <body> <body> <div id="root"> {{fullName}} {{age}} </div> <script> var app = new Vue({ el: '#root', data: { firstName: 'sunny', lastName: 'fan', age: 28 }, computed: { fullName: { get: function () { return this.firstName + " " + this.lastName }, set: function (value) { console.log(value); var arr = value.split(" "); this.firstName = arr[0]; this.lastName = arr[1]; } } } }) </script> </body> </body> </html>
3-6 Vue 中的样式绑定

1.class 的对象绑定

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>class的对象绑定</title> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <style> .activated { color: red; } </style> </head> <body> <body> <div id="root"> <div @click="handleChangeColor" :class="{activated:isActivated}"> hello world </div> </div> <script> var app = new Vue({ el: '#root', data: { isActivated: false }, methods: { handleChangeColor: function () { this.isActivated = !this.isActivated } } }) </script> </body> </body> </html>

2.class 的数组绑定

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>class的数组绑定</title> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <style> .activated-one { font-size: 20px; } .activated { color: red; } </style> </head> <body> <div id="root"> <div @click="handleChangeColor" :class="[activated,activatedOne]">hello world</div> </div> <script> var app = new Vue({ el: '#root', data: { activated: '', activatedOne: 'activated-one' }, methods: { handleChangeColor: function () { this.activated = this.activated === 'activated' ? "" : "activated" } } }) </script> </body> </html>

3.style 对象绑定

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>style对象绑定</title> <script src="https://cdn.jsdelivr.net/npm/vue"></script> </head> <body> <div id="root"> <div @click="handleChangeColor" :style="styleObj">hello world</div> </div> <script> var app = new Vue({ el: '#root', data: { styleObj: { color: 'black' } }, methods: { handleChangeColor: function () { this.styleObj.color = this.styleObj.color === 'black' ? 'red' : 'black' } } }) </script> </body> </html>

4.style 的数组绑定

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>style数组绑定</title> <script src="https://cdn.jsdelivr.net/npm/vue"></script> </head> <body> <div id="root"> <div @click="handleChangeColor" :style="[styleObj,{fontSize:'30px'},styleOne]">hello world</div> </div> <script> var app = new Vue({ el: '#root', data: { styleObj: { color: 'black' }, styleOne: { fontWeight: 'bold' } }, methods: { handleChangeColor: function () { this.styleObj.color = this.styleObj.color === 'black' ? 'red' : 'black' } } }) </script> </body> </html>
3-7 条件渲染

v-if 、v-else-if、v-else

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>V-if</title> <script src="https://cdn.jsdelivr.net/npm/vue"></script> </head> <body> <div id="root"> <h5>实例一:v-if</h5> <template v-if="isShow"> hello world </template> <button @click="handleChange">{{toggleText}}</button> <hr> <h5>实例二:v-else</h5> <div v-if="isShowTwo"> 要是我显示 </div> <div v-else> 要么你显示 </div> <button @click="handleChangeRole">切换显示</button> <hr> <h5>实例三:v-else-if</h5> <div v-if="status==='A'"> A </div> <div v-else-if="status==='B'"> B </div> <div v-else-if="status==='C'"> C </div> <div v-else> 其他 </div> </div> <script> var app = new Vue({ el: '#root', data: { isShow: false, toggleText: '显示', isShowTwo: true, status: 'A' }, methods: { handleChange: function () { this.isShow = !this.isShow; this.toggleText = this.toggleText === '显示' ? '隐藏' : '显示' }, handleChangeRole: function () { this.isShowTwo = !this.isShowTwo; } } }) </script> </body> </html>

key 管理可复用的元素
当切换两个 input 输入框的时候,为了不让 input 框的输入内容被占用,所以我们通过设置 input 的 key 值来解决这个问题

<template v-if="loginType === 'username'"> <label>Username</label> <input placeholder="Enter your username" key="userName-input"> </template> <template v-else> <label>Email</label> <input placeholder="Enter your email address" key="email-input"> </template>

demo 例子:https://codepen.io/sunnyfan/pen/JQjRry

v-show
v-show 很相似,只要设置值为 true 则显示

v-if 和 v-show 的区别

  • v-show 不能和 v-else 和 v-else-if 结合使用
  • v-show 不管是 ture 还是 fasle div 元素都会渲染出来(false style 的 display:none),如果如果有频繁的切换,我们会首选 v-show,减少对 dom 的频繁操作
3-8 Vue 列表渲染

1.v-for

# view 部分 <li v-for="(item,index) in list" :key="index">{{index}}--{{item}}</li> <li v-for="(item,key,index) of userInfo" :key="index">{{key}}-{{item}}-{{index}}</li> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>列表渲染</title> <script src="https://cdn.jsdelivr.net/npm/vue"></script> </head> <body> <div id="root"> <ul> <li v-for="(item,index) in list" :key="index">{{index}}--{{item}}</li> </ul> <ul> <li v-for="(item,key,index) of userInfo" :key="index">{{key}}-{{item}}-{{index}}-</li> </ul> </div> <script> var app = new Vue({ el: '#root', data: { list: [ 'hello', 'world' ], userInfo: { name: 'sunny', age: 29 } } }) </script> </body> </html>

template 可以当一个空标签做为 for 循环渲染,而这个 template 不会渲染到 dom 里面

<ul> <template v-for="item in items"> <li>{{ item.msg }}</li> <li class="divider" role="presentation"></li> </template> </ul>

为了防止子组件循环渲染出现 dom 结构不对的情况,我们一般会通过 is 来给子组件命名

//html <table> <tbody> <tr is="row" v-for="item in list" :title="item"></tr> //这个地方调用is属性 </tbody> </table> //js Vue.component('row', { props: ['title'], template: '<tr><td>{{title}}</td></tr>' }); var app = new Vue({ el: '#root', data: { list: [ 'hello', 'world' ] } })
更改数组值方法有哪些?

1.变异方法
push()、 pop()、 shift()、unshift()、splice()、sort()、reverse()
2.替换数组

当也可以创建一个新的数组,在通过
filter()、concat()、slice()
更改原始数组的值,再把更改后的数组替换旧的数组
3.set 或者 $set 方法

Vue.set(app.userInfo,'age',22) //或者 vm.$set(app.userInfo,'age',22)

4.Object.assign()或者_.extend()

vm.userInfo = Object.assign({},vm.userInfo,{ sex:'男', email:'fx35792@163.com' })

ps:不可以通过数组下标去更改数组的值

var vm = new Vue({ data: { items: ['a', 'b', 'c'] } }) vm.items[1] = 'x' // 不是响应性的 vm.items.length = 2 // 不是响应性的
3-9 Vue 事件处理

监听事件、方法处理、内联处理器中的方法

1.监听事件

通过 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>监听事件</title> <script src="https://cdn.jsdelivr.net/npm/vue"></script> </head> <body> <div id="root"> <button v-on:click="counter+=1">add 1</button> <p>The button above has been clicked {{counter}} times.</p> </div> <script type="text/javascript"> var app = new Vue({ el: '#root', data: { counter: 0 } }) </script> </body> </html>
2.事件处理方法

但是在开发的过程中,有时候直接把 JavaScript 代码写在 v-on 指令中是不可行的。因此 v-on 还可以接收一个需要调用的方法名称

<div id="root"> <button v-on:click="greet">greet</button> </div> <script> const app = new Vue({ el: '#root', data: { name: 'sunny' }, methods: { greet: function (event) { console.log(`hello ${this.name}`) if (event) { console.log(event.target.tagName) } } } }) </script>
3.内联处理器中的方法

除了直接绑定到一个方法,也可以在内联 JavaScript 语句中调用方法:

<button v-on:click="say('hi')">say hi</button> <button v-on:click="say('hello')">say hello</button> const app = new Vue({ el: '#root', data: { name: 'sunny' }, methods: { say: function (val) { console.log(`${this.name} ${val}`) } } })

方法总通过传递参数 $event,可以原始的 DOM

<button v-on:click="warning('please input number',$event)">warning</button> const app = new Vue({ el: '#root', data: {}, methods: { warning: function (val, event) { if (event) event.preventDefault(); console.log(val) } } })
4.事件修饰符

我们在开发个过程中经常调用一些
event.preventDefault() //阻止事件的默认行为
event.stopPropagation() //阻止冒泡的行为

而 vue 为了更好的让我们关注业务逻辑代码的编写,它封装了很多 v-on 修饰符来帮助我们完成上面那些工作。

  • stop //event.stopPropagation()
  • prevent //event.preventDefault()
  • capture
  • self
  • once
  • passive

第 4 章 深入理解 Vue 组件

4-1 使用组件细节点

1.is 的使用
当我们写循环组件的时候,经常给
table 中的 tr
select 中的 option
ul 中的 li 或者 ol 中的 li
等等定义组件的时候,我们经常用 is 来定义组件的名称,为了让浏览器成功的渲染正确的 dom 结构

<div id="root"> <table> <tbody> <tr is="row"></tr> <tr is="row"></tr> <tr is="row"></tr> </tbody> </table> <select name="" id=""> <option is="selectOption"></option> <option is="selectOption"></option> <option is="selectOption"></option> </select> <ul> <li is="ulLi"></li> <li is="ulLi"></li> <li is="ulLi"></li> </ul> </div> <script> Vue.component('row', { template: '<tr><td>this is a row</td></tr>' }); Vue.component('selectOption',{ template: '<option>this is option</option>' }); Vue.component('ulLi',{ template: '<li>this is li</li>' }); var app = new Vue({ el: '#root', data: {}, }) </script>

2.在子组件定义 data 的时候,必须是一个函数,而不能是一个对象,返回一个对象是为了每个子组件都能拥有一个独立的数据存储。这样子组件之间的数据不会互相影响
而在根组件中,data 可以是一个对象。

<div id="root"> <table> <tbody> <tr is="row"></tr> <tr is="row"></tr> <tr is="row"></tr> </tbody> </table> </div> <script> Vue.component('row', { data: function () {//返回的是一个函数 return { content: 'this is row' } }, template: '<tr><td>{{content}}</td></tr>' }); var app = new Vue({ el: '#root', data: {} }) </script>

3.有时候我们在开发过程中,因为一些业务的需求,少不了对 dom 的操作,那么我们就可以借助 ref 来实现

//实例一 <div id="root"> <div ref="hello" @click="handleClick">hello world</div> </div> <script> var app = new Vue({ el: '#root', data: {}, methods: { handleClick: function () { console.log(this.$refs.hello.innerHTML);//通过refs属性 获取当前节点的文本 } } }); </script> //案例二 counter求和 <div id="root"> <counter ref="one" @change="handleChange"></counter> <counter ref="two" @change="handleChange"></counter> <div>{{total}}</div> </div> <script> Vue.component('counter', { data: function () { return { number: 0 } }, template: '<div @click="handleClick">{{number}}</div>', methods: { handleClick: function () { this.number++; this.$emit('change');//触发一个监听器 } } }); var app = new Vue({ el: '#root', data: { total: 0 }, methods: { handleChange: function () { this.total = this.$refs.one.number + this.$refs.two.number //通过refs 来回去组件的值 } } }); </script>
4-2 父子组件之间的数据传递

父组件向子组件传值:是通过属性的方式
子组件向父组件传值:可以通过 $emit 来触发一个事件

vue 数据传递遵循的是单向数据流,
所以在下面的案例中我们并没有对 content 数据直接进行数据的累加,而是把 content 数据赋值给了 number,对 number 进行数据的累加操作。

<div id="root"> <counter :content="1" @inc="handleInc"></counter><!--父组件通过属性向子组件传值--> <counter :content="3" @inc="handleInc"></counter> <div>{{total}}</div> </div> <script> Vue.component('counter', { props: ['content'], data: function () { return { number: this.content //遵循单向数据流 } }, template: '<div @click="handleClick">{{number}}</div>', methods: { handleClick: function () { this.number += 2; //子组件通过方法向父组件传值 this.$emit('inc', 2); } } }); var app = new Vue({ el: '#root', data: { total: 4 }, methods: { handleInc: function (step) { this.total += step } } }) </script>
4-3 组件参数校验和非 props 特性

1.组件的的参数校验

<div id="root"> <child content="hello"></child> </div> <script> Vue.component('child', { props: { content: { type: String, required: true, default: 'default Value', validator: function (value) { return (value.length > 5) } } }, template: '<div>{{content}}</div>' }); var app = new Vue({ el: '#root', }) </script>

2.props 特性和非 props 特性的对比
props 特性:
父组件传递属性,子组件要接受该属性
props 属性不会显示在 dom 的标签之中
非 props 特性:
父组件传递属性,子组件没有去接受,而是直接调用
props 属性会显示在 dom 的标签之中

4-4 给组件绑定原生事件

通过 .native 属性来绑定原生事件

<div id="root"> <child @click.native="handleClick"></child> </div> <script> Vue.component('child', { template: '<div>child</div>' }) var app = new Vue({ el: '#root', methods: { handleClick: function () { console.log('click'); } } }) </script>
4-5 非父子组件间的传值

非父子组件间的传值
1.通过 vuex
2.通过发布订阅模式(Bus/总线/发布订阅模式/观察者模式/)

<div id="root"> <child content="sunny"></child> <child content="fan"></child> </div> <script> Vue.prototype.bus = new Vue();//定义bus Vue.component('child', { data: function () { return { newContent: this.content //保证单向数据流 } }, props: { content: String }, template: '<div @click="handleClick">{{newContent}}</div>', methods: { handleClick: function () { this.bus.$emit('change', this.newContent); //在bus上发布一个事件,并且传值 } }, mounted: function () {//通过这个钩子,来监听change的变化,通过回调拿到相对应的的值 var that = this; this.bus.$on('change', function (msg) { console.log(msg) that.newContent = msg//this 指向发生变更,所以上面要从新获取一下this的指向 }) } }); var app = new Vue({ el: '#root' })
4-6 在 vue 中使用插槽

插槽只能有一个
而剧名插槽可以有多个

<div id="root"> <body-content> <p slot="header">this is header</p> <p slot="footer">this is footer</p> </body-content> </div> <script> Vue.component('body-content',{ template: `<div> <slot name="header">default header</slot> //设置默认值 <p>this is content</p> <slot name="footer"></slot> </div>` }) var app = new Vue({ el:'#root' }) </script>
4-7 作用域插槽

父组件调用子组件的时候,给子组件传了一个插槽,这个插槽是一个作用域的插槽,这个插槽必须是一个 <template slot-scope="props">{{props.item}}</template>
那什么时候使用作用插槽呢?
1.当子组件做循环
2.或者当子组件的 dom 结构由外部传递进来,或者有外部决定的时候

<div id="root"> <child> <template slot-scope="props"> <li>{{props.item}}</li> </template> </child> </div> <script> Vue.component('child', { data: function () { return { list: [1, 2, 3, 4] } }, template: `<div> <ul> <slot v-for="item of list" :item=item></slot> </ul> </div>` }) var app = new Vue({ el: '#root' }) </script>
4-8 动态组件和 v-once 指令
<div id="root"> <component :is="type"></component> <!--这就是动态组件--> <child-one v-if="type==='child-one'"></child-one> <child-two v-if="type==='child-two'"></child-two> <button @click="hanleBtnClick">change</button> </div> <script> Vue.component('child-one', { template: '<div v-once>child-one</div>' }) Vue.component('child-two', { template: '<div v-once>child-two</div>' }) var app = new Vue({ el: '#root', data: { type: 'child-one' }, methods: { hanleBtnClick: function () { this.type = this.type === 'child-one' ? 'child-two' : 'child-one' } } }) </script>

第 5 章 表单

5-1 双向数据绑定 v-model
<div id="root"> <p> <label for="">请输入姓名</label> <input type="text" v-model="name" placeholder="请输入名字"> </p> <p> 你的名字是:{{name}} </p> </div> <script> var app = new Vue({ el: '#root', data() { return { name: '' } }, }) </script>
5-2 复选框(checkbox)相关的操作

1)、单个复选框的取反操作
2)、多个复选框的数组操作

<div id="root"> <p>单个复选框:</p> <p> <input type="checkbox" id="checkbox" v-model="checked"> <label for="checkbox">{{checked}}</label> </p> <p>多个复选框:</p> <p> <input type="checkbox" id="chinese" value="chinese" v-model="checkedNames"> <label for="chinese">chinese</label> <input type="checkbox" id="Math" value="Math" v-model="checkedNames"> <label for="Math">Math</label> <input type="checkbox" id="English" value="English" v-model="checkedNames"> <label for="English">English</label> </p> <p>选择的值为:{{checkedNames}}</p> </div> <script> var app = new Vue({ el: '#root', data: { checked: false, checkedNames: [] } }) </script>

效果

5-3 单选框(radio)相关的操作
<div id="root"> <p>单个复选框:</p> <p> <input type="radio" id="man" value="man" v-model="picked"> <label for="man">man</label> <input type="radio" id="female" value="female" v-model="picked"> <label for="female">female</label> </p> <p> 选中的值:{{picked}} </p> </div> <script> var app = new Vue({ el: '#root', data: { picked: 'man', } }) </script>

效果

5-4 选择框(select)相关的操作
<div id="root"> <p>选择框:</p> <select name="age" id="age" v-model="ages"> <option value="0-12">儿童</option> <option value="12-18">少年</option> <option value="18-30">青年</option> <option value="30-40">中年</option> <option value="40-50">壮年</option> <option value="50-">老年</option> </select> <p>你先则的值是:{{ages}}</p> </div> <script> var app = new Vue({ el: '#root', data: { ages: '0-12', } }) </script>

效果

5-5 表单中一些修饰符的操作(.lazy、.number、.trim)
<div id="root"> <p>.lazy(input事件同步输入看的值,通过lazy转为change事件中同步):</p> <input type="text" v-model.lazy="text"> <p>你输入的文本内容是:{{text}}</p> <p>.number(输入文本内容为数字):</p> <input type="number" v-model.number="number"> <p>输入的数字是:{{number}}</p> <p>.trim(去除输入框两端的空格):</p> <input type="text" v-model.trim="trimText"> <p>显示输入的内容:{{trimText}}</p> </div> <script> var app = new Vue({ el: '#root', data: { text: '', number: '', trimText: '' } }) </script>

效果

第 6 章 动画

6-1 Vue 中的 css 动画原理

我们给 transition name 属性定义的是 fade 所以是下面名称:
fade-enter fade-enter-to fade-enter-active
fade-leave fade-leave-to fade-leave-active
如果我们没有给 transition 定义 name 属性,用默认的那么就是:
v-enter v-enter-to v-enter-active
v-leave v-leave-to v-leave-active

进场动画原理

刚开始存在 fade-enter 和 fade-enter-active
紧接第二帧的时候,fade-enter 消失、fade-enter-to 出现
到最后的时候 fade-enter-to 消失、fade-enter-active 消失

离开动画原理

刚开始存在 fade-leave 和 fade-leave-active
紧接第二帧的时候,fade-leave 消失、fade-leave-to 出现
到最后的时候 fade-leave-to 消失、fade-leave-active 消失

//css动画效果(css过度动画效果) <style> .fade-enter { opacity: 0; } .fade-enter-active { transition: opacity 1s; } .fade-leave-to { opacity: 0; } .fade-leave-active{ transition: opacity 1s; } </style> <div id="root"> <transition name="fade"> <div v-if="show">hello world</div> </transition> <button @click="handleClick">toggle</button> </div> <script> var app = new Vue({ el: '#root', data: { show: true }, methods: { handleClick: function () { this.show = !this.show; } } }) </script>

ps:
显示操作
刚开始 fade-enter opacity 为 0 第二帧 fade-enter 消失 opacity 变为 1 这个过程一直在 fade-enter-active 监听 1 秒时间
隐藏操作
刚开始 fade-leave opacity 默认是 1 第二帧 fade-leave 消失 fade-leave-to 出现 opacity 变为 0 这个过程一直在 fade-leave-active 监听 1 秒时间后消失

第 7 章 路由

7-1.什么是路由

路由:就是我们通过不同的 URL 访问不同的内容。

7-2.Vue 路由的安装
npm install vue-router
7-3.Vue 路由的简单案例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="https://cdn.bootcss.com/vue-router/2.8.1/vue-router.min.js"></script> <title>路由</title> <style> ._active { color: red } ._activeTwo { color: pink } </style> </head> <body> <div id="root"> <h1>hello 路由!</h1> <!-- 使用 router-link 组件来导航. --> <!-- 通过传入 `to` 属性指定链接. --> <!-- <router-link> 默认会被渲染成一个 `<a>` 标签 --> <p> <router-link to="/header">go header</router-link> <router-link to="/footer">go footer</router-link> </p> <!-- <router-link> replace 和 append的应用 标签 --> <p> <router-link :to="{path:'/header'}" replace>go header(replace)</router-link> <router-link :to="{path:'footer'}" append>go footer(append)</router-link> </p> <!-- <router-link> tag 渲染 --> <p> <router-link :to="{path:'/header'}" tag="li">go header(tag)</router-link> </p> <!-- <router-link> exact-active-class 和 active-class 渲染 --> <p> <router-link :to="{path:'/header'}" exact-active-class="_active">go header(exact-active-class)</router-link> <router-link :to="{path:'/footer'}" active-class="_activeTwo">go header(active-class)</router-link> </p> <!-- <router-link> event渲染 --> <p> <router-link :to="{path:'/header'}" @click.native="mouseover">go header(event)</router-link> </p> <!-- 路由出口 --> <!-- 路由匹配到的组件将渲染在这里 --> <router-view></router-view> </div> <script> //0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用Vue.use(VueRouter) //1. 定义路由组件 //可以从其他文件 import 进来 const Footer = { template: '<div>footer</div>' }; const Header = { template: '<div>Header</div>' }; //2. 定义路由 //每个路由应该映射一个组件。其中”compoment“ 可以是: //通过Vue.extend() 创建的组件构造器 //或者,只是一个组件对象 const routes = [ { path: '/header', component: Header }, { path: '/footer', component: Footer } ] //3. 创建router实例,然后传 `routes` 配置 const router = new VueRouter({ routes }) var app = new Vue({ el: '#root', router, methods: { mouseover: function () { console.log(1111); } }, }) </script> </body> </html>

效果

7-4.router-link 的相关配置

1)、to 表示路由链接
当被点击后,内部会立即把 to 的值传到 router.push,所以这个值可以是一个 字符串 或者是描述目标位置的 对象

<!-- 字符串 --> <router-link to="home">Home</router-link> <!-- 渲染结果 --> <a href="home">Home</a> <!-- 使用 v-bind 的 JS 表达式 --> <router-link v-bind:to="'home'">Home</router-link> <!-- 不写 v-bind 也可以,就像绑定别的属性一样 --> <router-link :to="'home'">Home</router-link> <!-- 同上 --> <router-link :to="{ path: 'home' }">Home</router-link> <!-- 命名的路由 --> <router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link> <!-- 带查询参数,下面的结果为 /register?plan=private --> <router-link :to="{ path: 'register', query: { plan: 'private' }}">Register</router-link>

2)、replace
设置 replace 属性的话,当点击时,会调用 router.replace() 而不是 router.push(),导航后不会留下 history 记录。

<router-link :to="{ path: '/home'}" replace></router-link>

3)、tag
有时候想要 渲染成某种标签,例如

  • 。 于是我们使用 tag prop 类指定何种标签,同样它还是会监听点击,触发导航。

    foo
  • foo
  • 4)、active-class
    设置 链接激活时使用的 CSS 类名。可以通过以下代码来替代。

    <style> ._active{ background-color : red; } </style> <p> <router-link v-bind:to = "{ path: '/route1'}" active-class = "_active">Router Link 1</router-link> <router-link v-bind:to = "{ path: '/route2'}" tag = "span">Router Link 2</router-link> </p>

    注意这里 class 使用 active_class="_active"。

    5)、exact-active-class
    配置当链接被精确匹配的时候应该激活的 class。可以通过以下代码来替代。

    <p> <router-link v-bind:to = "{ path: '/route1'}" exact-active-class = "_active">Router Link 1</router-link> <router-link v-bind:to = "{ path: '/route2'}" tag = "span">Router Link 2</router-link> </p>

    6)、event
    声明可以用来触发导航的事件。可以是一个字符串或是一个包含字符串的数组。

    <router-link v-bind:to = "{ path: '/route1'}" event = "mouseover">Router Link 1</router-link>

    以上代码设置了 event 为 mouseover ,及在鼠标移动到 Router Link 1 上时导航的 HTML 内容会发生改变。
    7)、exact-active-class 和 active-class 的区别
    exact-active-class:路由严格匹配
    active-class:路由模糊匹配
    如果你访问的是:
    /article 或者 /article/1

    <router-link to="/article" active-class="router-active"></router-link>

    都会被渲染

    <a href="#/article" class="router-active" rel="nofollow"></a>

    <router-link to="/article" exact-active-class="router-active"></router-link>

    只有访问 /article/1
    才会被渲染

    <a href="#/article" class="router-active" rel="nofollow"></a>

    如果是 /article,class 不会被渲染出来

    <a href="#/article" rel="nofollow"></a>

    第 8 章 Vue 项目预热

    8-1.NodeJS 安装
    //验证node和npm 是否安装 以及安装的版本 node -v npm -v

    根据自己电脑是什么系统去安装:NodeJS 安装

    8-2.vue 脚手架安装
    //电脑全局安装 npm install --global vue-lci //a. 实例出一个项目 vue init webpack vue-travel //vue-travel 名称自己定义 //b. 如果你本地有一个git项目了,你想把这个vue脚手架放入到这个项目中 vue init webpack git-project //git-project 本地git项目名称
    8-3.运行脚手架项目
    cd vue-travel //or cd git-project npm run dev //or npm start

    浏览器访问 localhost:8080 即可

    8-4.如何更改端口号

    项目 config 文件件,我们打开其目录下的 index.js,就是端口号的最终设置的地方:

    dev: { // Paths assetsSubDirectory: 'static', assetsPublicPath: '/', proxyTable: {}, // Various Dev Server settings host: 'localhost', // can be overwritten by process.env.HOST port: 8081, //在这个地方进行端口号的更改 autoOpenBrowser: false, errorOverlay: true, notifyOnErrors: true, poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- ..... }
    8-5.如何通过 ip 来访问我们的网站呢
    http://localhost:8080 http://127.0.0.1:8080 http://自己电脑ip:8080 //如果手机和电脑在同一个网段,手机可以联调项目,查看手机效果

    第一种方法修改:package.json 文件
    在 dev 命令里面添加 --host 0.0.0.0

    "scripts": { "dev": "webpack-dev-server --host 0.0.0.0 --inline --progress --config build/webpack.dev.conf.js", "start": "npm run dev", "lint": "eslint --ext .js,.vue src", "build": "node build/build.js" },

    第二种方法修改:config/index.js 文件

    dev: { // Paths assetsSubDirectory: 'static', assetsPublicPath: '/', proxyTable: {}, // Various Dev Server settings host: '0.0.0.0', // 修改这个地方 将localhost 改为 0.0.0.0 port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined autoOpenBrowser: false, errorOverlay: true, notifyOnErrors: true, poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- ...... }

    重启 npm run dev 或者 npm start

    区别:
    修改 package.json 之后的结果(和之前没有什么区别):
    Your application is running here: http://localhost:8080
    修改 config/index.js 文件 之后的运行结果:
    Your application is running here: http://0.0.0.0:8080

    所以我推荐 方法一(纯粹个人意见)

    8-6.Vue 项目初始化的准备工作

    因为是手机端的 vue 项目,所以项目 index.html 我们要设置一下
    1.禁止缩放

    <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">

    2.reset.css

    @charset "utf-8";html{background-color:#fff;color:#000;font-size:12px} body,ul,ol,dl,dd,h1,h2,h3,h4,h5,h6,figure,form,fieldset,legend,input,textarea,button,p,blockquote,th,td,pre,xmp{margin:0;padding:0} body,input,textarea,button,select,pre,xmp,tt,code,kbd,samp{line-height:1.5;font-family:tahoma,arial,"Hiragino Sans GB",simsun,sans-serif} h1,h2,h3,h4,h5,h6,small,big,input,textarea,button,select{font-size:100%} h1,h2,h3,h4,h5,h6{font-family:tahoma,arial,"Hiragino Sans GB","微软雅黑",simsun,sans-serif} h1,h2,h3,h4,h5,h6,b,strong{font-weight:normal} address,cite,dfn,em,i,optgroup,var{font-style:normal} table{border-collapse:collapse;border-spacing:0;text-align:left} caption,th{text-align:inherit} ul,ol,menu{list-style:none} fieldset,img{border:0} img,object,input,textarea,button,select{vertical-align:middle} article,aside,footer,header,section,nav,figure,figcaption,hgroup,details,menu{display:block} audio,canvas,video{display:inline-block;*display:inline;*zoom:1} blockquote:before,blockquote:after,q:before,q:after{content:"\0020"} textarea{overflow:auto;resize:vertical} input,textarea,button,select,a{outline:0 none;border: none;} button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0} mark{background-color:transparent} a,ins,s,u,del{text-decoration:none} sup,sub{vertical-align:baseline} html {overflow-x: hidden;height: 100%;font-size: 50px;-webkit-tap-highlight-color: transparent;} body {font-family: Arial, "Microsoft Yahei", "Helvetica Neue", Helvetica, sans-serif;color: #333;font-size: .28em;line-height: 1;-webkit-text-size-adjust: none;} hr {height: .02rem;margin: .1rem 0;border: medium none;border-top: .02rem solid #cacaca;} a {color: #25a4bb;text-decoration: none;}

    在我们项目 assets 文件下面
    创建 styles 文件
    在 styles 文件下面添加 reset.css
    最后在项目的 main.js 引入 reset.css

    import './assets/styles/reset.css'

    3.border.css
    解决 1px 边框问题

    @charset "utf-8"; .border, .border-top, .border-right, .border-bottom, .border-left, .border-topbottom, .border-rightleft, .border-topleft, .border-rightbottom, .border-topright, .border-bottomleft { position: relative; } .border::before, .border-top::before, .border-right::before, .border-bottom::before, .border-left::before, .border-topbottom::before, .border-topbottom::after, .border-rightleft::before, .border-rightleft::after, .border-topleft::before, .border-topleft::after, .border-rightbottom::before, .border-rightbottom::after, .border-topright::before, .border-topright::after, .border-bottomleft::before, .border-bottomleft::after { content: "\0020"; overflow: hidden; position: absolute; } /* border * 因,边框是由伪元素区域遮盖在父级 * 故,子级若有交互,需要对子级设置 * 定位 及 z轴 */ .border::before { box-sizing: border-box; top: 0; left: 0; height: 100%; width: 100%; border: 1px solid #eaeaea; transform-origin: 0 0; } .border-top::before, .border-bottom::before, .border-topbottom::before, .border-topbottom::after, .border-topleft::before, .border-rightbottom::after, .border-topright::before, .border-bottomleft::before { left: 0; width: 100%; height: 1px; } .border-right::before, .border-left::before, .border-rightleft::before, .border-rightleft::after, .border-topleft::after, .border-rightbottom::before, .border-topright::after, .border-bottomleft::after { top: 0; width: 1px; height: 100%; } .border-top::before, .border-topbottom::before, .border-topleft::before, .border-topright::before { border-top: 1px solid #eaeaea; transform-origin: 0 0; } .border-right::before, .border-rightbottom::before, .border-rightleft::before, .border-topright::after { border-right: 1px solid #eaeaea; transform-origin: 100% 0; } .border-bottom::before, .border-topbottom::after, .border-rightbottom::after, .border-bottomleft::before { border-bottom: 1px solid #eaeaea; transform-origin: 0 100%; } .border-left::before, .border-topleft::after, .border-rightleft::after, .border-bottomleft::after { border-left: 1px solid #eaeaea; transform-origin: 0 0; } .border-top::before, .border-topbottom::before, .border-topleft::before, .border-topright::before { top: 0; } .border-right::before, .border-rightleft::after, .border-rightbottom::before, .border-topright::after { right: 0; } .border-bottom::before, .border-topbottom::after, .border-rightbottom::after, .border-bottomleft::after { bottom: 0; } .border-left::before, .border-rightleft::before, .border-topleft::after, .border-bottomleft::before { left: 0; } @media (max--moz-device-pixel-ratio: 1.49), (-webkit-max-device-pixel-ratio: 1.49), (max-device-pixel-ratio: 1.49), (max-resolution: 143dpi), (max-resolution: 1.49dppx) { /* 默认值,无需重置 */ } @media (min--moz-device-pixel-ratio: 1.5) and (max--moz-device-pixel-ratio: 2.49), (-webkit-min-device-pixel-ratio: 1.5) and (-webkit-max-device-pixel-ratio: 2.49), (min-device-pixel-ratio: 1.5) and (max-device-pixel-ratio: 2.49), (min-resolution: 144dpi) and (max-resolution: 239dpi), (min-resolution: 1.5dppx) and (max-resolution: 2.49dppx) { .border::before { width: 200%; height: 200%; transform: scale(.5); } .border-top::before, .border-bottom::before, .border-topbottom::before, .border-topbottom::after, .border-topleft::before, .border-rightbottom::after, .border-topright::before, .border-bottomleft::before { transform: scaleY(.5); } .border-right::before, .border-left::before, .border-rightleft::before, .border-rightleft::after, .border-topleft::after, .border-rightbottom::before, .border-topright::after, .border-bottomleft::after { transform: scaleX(.5); } } @media (min--moz-device-pixel-ratio: 2.5), (-webkit-min-device-pixel-ratio: 2.5), (min-device-pixel-ratio: 2.5), (min-resolution: 240dpi), (min-resolution: 2.5dppx) { .border::before { width: 300%; height: 300%; transform: scale(.33333); } .border-top::before, .border-bottom::before, .border-topbottom::before, .border-topbottom::after, .border-topleft::before, .border-rightbottom::after, .border-topright::before, .border-bottomleft::before { transform: scaleY(.33333); } .border-right::before, .border-left::before, .border-rightleft::before, .border-rightleft::after, .border-topleft::after, .border-rightbottom::before, .border-topright::after, .border-bottomleft::after { transform: scaleX(.33333); } }

    在 main.js 文件下面

    import './assets/styles/border.css'

    4.fastclick 解决 300 毫秒点击延迟问题
    在 main.js 文件下面

    import fastClick from 'fastclick' fastClick.attach(document.body)

    如果想预览项目整体的效果和配置项 git 地址:
    https://github.com/fx35792/vue-travel
    5.安装 stylus、stylus-loader 第三方依赖

    npm install stylus -S npm install stylus-loader -S

    那么在 vue 组件中如何使用呢?

    //1.设置 lang 为 `stylus` //2.如果只想样式对当前页面生效,而不污染到全局的其他的样式 可以加 scoped属性 //3.stylus 语法:支持嵌套,变量引用,省去冒号和花括号等等优点 <style lang="stylus" scoped> @import '~styles/varibles.styl'; .header display flex background $bgColor color #fff height .88rem line-height .88rem .header-left float left width .64rem text-align center padding 0 .1rem .back-icon font-size .48rem .header-content flex 1 margin-top .1rem height .68rem line-height .68rem background #ffffff border-radius .05rem color #e4e7ea .search-icon padding-left .2rem .header-right padding 0 .22rem </style>

    上面我们说道 style 里面的 scope 是只对当前页面生效,但是在开发过程中,我们可能需要引入第三方的样式,有时候为了满足 Ui 的样式变化我们需要 style 书写覆盖第三方依赖的样式,那么我们改如何操作呢?

    <style lang="stylus" scoped> /*这块的穿透写法 `>>>` 就可以覆盖swiper 当前活跃圆点的样式*/ .wrapper >>> .swiper-pagination-bullet-active { background: #fff !important; } .wrapper { overflow: hidden; width: 100%; height: 0; padding-bottom: 26.67%; background: #eee; .swiper-img { width: 100%; } } </style>
    8-7.Vue 项目如何给长目录定义变量

    在开发过程中我们经常要引入外部文件,比如 styles 文件、图片文件等等

    ../../../assets/styles/border.css

    像上面的例子我们肯定遇到过,如果有一个变量 styles 能直接代码 ../../../styles 那就好了。
    那在 vue-cli 的脚手架中应该怎么去配置呢?
    在 build/webpack.base.conf.js 中

    resolve: { extensions: ['.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src'), 'styles': resolve('src/assets/styles'), //这是我们添加的变量 } },

    配置好以后,我们需要重启一下项目 npm start or npm run dev
    那么上面例子长长的引用我们就可以改写为

    styles/border.css

    ps:如果我们在 vue 组件中引用的话我们需要注意在 styles 之前加个波浪符~

    @import '~styles/varibles.styl';
    8-7.Vue 项目如何引入本地图片呢?

    require

    require('@/assets/images/1.jpg')

    import

    //此处的@ 指的是src目录 import bannerImg1 from '@/assets/images/1.jpg'

    alias

    //webpack.base.conf.js resolve: { extensions: ['.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src'), 'styles': resolve('src/assets/styles'), 'images': resolve('src/assets/images'),//配置图片变量路径 } }, //页面调用 <img src="~images/1.jpg" class="swiper-img" alt="">
    <script> import bannerImg1 from '@/assets/images/1.jpg' export default { name: 'HomeSwiper', data () { return { swiperOption: { pagination: '.swiper-pagination', loop: true }, swiperList: [ { id: '0001', url: bannerImg1 }, { id: '0002', url: require('@/assets/images/2.jpg') }, { id: '0003', url: require('../../../assets/images/3.jpg') } ] } } } </script>

    第 9 章 Vue 项目开发之首页

    首页制作完的整体效果:
    效果图

    如果你想看首页整体开发效果,直接运行 master 分支即可:
    https://github.com/fx35792/vue-travel

    如果想看每部分的开发效果:

    9-1 Vue 项目开发首页之 header

    在开发这块的时候,主要是一个布局的设置,还有就是 icon 图标,这个 icon 图标用的是阿里 iconfont:https://www.iconfont.cn/
    1.创建一个 iconfont 账号
    2.登录后--> 图标管理--> 我的图标--> 新建项目
    3.去查找 UI 所需要的图标 icon,添加购物车--> 添加项目--> 选择自己新建的项目
    4.图标全部查找完以后可以把这个项目下载到本地
    第一种方法:如果是下载到本地,放入到项目中的操作
    程序引入字体样式文件
    在 main.js 引入

    import 'styles/iconfont.css' //页面上使用的话 <span class="iconfont search-icon">&#xe609;</span>

    第二种方法:不下载本地,用阿里 iconfont 的 cdn
    程序引入样式 cdn
    在 index.html 中添加

    <link rel="stylesheet" href="//at.alicdn.com/t/font_892705_hf5c19omqen.css">‘’ //页面上使用的话(和方法一是一样的) <span class="iconfont search-icon">&#xe609;</span>

    如果你是线上项目,为了保险起见,推荐使用方法一
    但是如果你平时自己做项目练习,你使用方法二就行

    9-2 Vue 项目开发首页之 Swiper 轮播图

    1.安装 https://github.com/surmon-china/vue-awesome-swiper

    npm install vue-awesome-swiper --save

    2.如何使用呢?
    因为项目中很多地方可能需要用到 swiper,所以我们打算把他放到 main.js 中

    import VueAwesomeSwiper from 'vue-awesome-swiper' import 'swiper/dist/css/swiper.css' Vue.use(VueAwesomeSwiper)

    3.具体代码调用

    //template <swiper :options="swiperOption" v-if="swiperShow"> <swiper-slide v-for="item of list" :key="item.id"> <img :src="item.imgUrl" alt class="swiper-img"> </swiper-slide> <div class="swiper-pagination" slot="pagination"></div> </swiper> //js <script> export default { name: 'HomeIcons', data () { return { swiperOption: { pagination: 'swiper-pagination',//是否显示轮播图下面的小圆点 autoPlay: false//是否循环自动播放 } } }, computed: { swiperShow: function () { return this.list.length//当有数据的时候,再去渲染swiper,不然显示的第一条数据是最后一条 } } } </script>
    9-3 Vue 项目开发首页之”热门推荐“和”周末去哪“

    这一小节主要是 UI 上布局制作,以及 data 里面模拟一些数据,渲染到页面上。具体细节看
    https://github.com/fx35792/vue-travel/tree/index-recommond
    看代码基本上都可以看明白的,在这咱们就不详细的赘述了

    9-4 Vue 项目开发首页之 ajax 数据请求

    我们通过 ajax 来实现接口请求的数据,但是呢?在开发过程中,很多时候都是我们前端自己 mock 数据,通过代理,最后映射到页面上数据的,随后等服务器接口开发好了,我们在把 mock 数据地址替换为服务器地址即可。
    1.那么如何设置代理呢?
    其实在 vue-cli 的脚手架中,已经办咱们配置好了设置,只需要你自己配置一下即可:

    //在config文件下面的index.js文件中: proxyTable: { '/api': { target:'http://localhost:8080',//因为数据在我项目本地,所以我配置的localhost,如果是服务器你配置后端伙伴发给你的服务器地址即可 pathRewrite: { '^/api':'/static/mock' //当你接口请求中,以`/api`开头的时候,会帮我们代理到我们本地的/static/mock目录下面数据文件 } } },

    2.安装 axios

    npm install axios -S

    3.在页面上使用

    import axios from 'axios' mounted () {//一般的异步请求,我们都会放在mounted的生命周期中 this.getHomeInfo()//这个我们定义了一个方法,而不是直接写,是为了重复使用这个方法 }, methods: { getHomeInfo () { //通过axios请求接口 //当我们遇到`/api`,代理直接会找到/static/mock/index.js文件 axios.get('/api/index.json').then(this.getHomeInfoSucc) }, getHomeInfoSucc (res) { const result = res.data if (result.ret && result.data) { const data = result.data console.log(data) this.city = data.city this.swiperList = data.swiperList this.iconList = data.iconList this.recommendList = data.recommendList this.weekendList = data.weekendList } } }
    9-4 Vue 项目开发首页之父子组件之间的传值

    在制作的首页过程中,我们将首页拆分成了五部分,分别是:
    header、banner 轮播、icon 轮播、热门推荐、周末去哪
    那么,为了减少 http 接口请求,后端小伙伴会把五部门的内容都放到一个接口中去,在咱们本地模拟数据中我是放到了 static/mock/index.json 中的
    所以在 Home.vue 中

    //Home.vue <template> <div> <home-header :city="city"></home-header> <home-swiper :list="swiperList"></home-swiper> <!-- 第四步 --> <home-icons :list="iconList"></home-icons> <home-recommend :list="recommendList"></home-recommend> <home-weekend :list="weekendList"></home-weekend> </div> </template> <script> import HomeHeader from './components/Header' import HomeSwiper from './components/Swiper' import HomeIcons from './components/Icons' import HomeRecommend from './components/Recommend' import HomeWeekend from './components/Weekend' import axios from 'axios' export default { name: 'Home', components: { HomeHeader, HomeSwiper, HomeIcons, HomeRecommend, HomeWeekend }, data () { return { city: '', swiperList: [],//第二步 iconList: [], recommendList: [], weekendList: [] } }, methods: { getHomeInfo () { axios.get('/api/index.json').then(this.getHomeInfoSucc)//第一步 }, getHomeInfoSucc (res) { const result = res.data if (result.ret && result.data) { const data = result.data console.log(data) this.city = data.city this.swiperList = data.swiperList//第三步 this.iconList = data.iconList this.recommendList = data.recommendList this.weekendList = data.weekendList } } }, mounted () { this.getHomeInfo() } } </script> <style> </style> //Swiper.vue <template> <div class="wrapper"> <swiper :options="swiperOption" v-if="swiperShow"> <swiper-slide v-for="item of list" :key="item.id"><!--第六步--> <img :src="item.imgUrl" alt class="swiper-img"> </swiper-slide> <div class="swiper-pagination" slot="pagination"></div> </swiper> </div> </template> <script> export default { name: 'HomeSwiper', props: { list: Array //第五步 }, data () { return { swiperOption: { pagination: '.swiper-pagination', loop: true } } }, computed: { swiperShow: function () { return this.list.length } } } </script> <style lang="stylus" scoped> .wrapper >>> .swiper-pagination-bullet-active { background: #fff !important; } .wrapper { overflow: hidden; width: 100%; height: 0; padding-bottom: 31.25%; background: #eee; .swiper-img { width: 100%; } } </style>

    在这里咱们主要讲讲,首页父子组件的传值,咱们拿一个 banner 轮播图例子来说,其他的四部分咱们就不在这里赘述了。你去 github 仓库看源码就很容易明白。
    第一步:接口请求拿到数据(axios.get('/api/index.json').then(this.getHomeInfoSucc)//第一步)
    第二步:在 data 中我们初始化这五部分数据(swiperList: [],.//第二步)
    第三步:把接口拿到的数据依次放入到 data 初始化的值中( this.swiperList = data.swiperList//第三步)
    第四步:在 Home.vue 父组件中定义一个属性,来给 Swiper.vue 子组件传值(:list="swiperList")
    第五步:在 Swiper.vue 子组件中接受父组件传来的值(props: {
    list: Array //第五步
    })
    第六步:子组件渲染出来父组件传递过来的数据()

    第 10 章 Vue 项目开发之城市

    10-1.city 页面路由配置

    1.添加路由配置

    // router/index.js文件 import City from '@/pages/city/City' export default new Router({ routes: [ { path: '/', name: 'Home', component: Home }, { path: '/city', name: 'City', component: City } ] })

    2.添加相对应的页面
    在 pages 文件下面添加 city 文件夹和 City.vue 文件
    效果图
    3.初始化 City.vue 页面

    <template> <div> city </div> </template> <script> export default { name: 'City' } </script> <style lang="stylus" scoped> </style>
    10-2.city-header 部分制作
    //city/City.vue <template> <div> <city-header></city-header> </div> </template> <script> import CityHeader from './components/Header' export default { name: 'City', components: { CityHeader } } </script> <style lang="stylus" scoped> </style> //city/components/header.vue <template> <div class="header"> 城市选择 <router-link to="/"> <div class="iconfont back-city">&#xe696;</div> </router-link> </div> </template> <script> export default { name: 'CityHeader' } </script> <style lang="stylus" scoped> @import '~styles/varibles.styl' .header position relative height $headHeight line-height $headHeight background $bgColor text-align center color #ffffff .back-city position absolute left 0 top 0 width .64rem text-align center padding 0 .1rem font-size .48rem color #fff </style>

    效果图

    10-3.city-search 部分制作

    上面咱们已经完成了头部的制作,这一节咱们来 city-search 的 ui 部分制作,随后等咱们把 city 列表制作完成后,咱们再来制作 city-search 相关的逻辑部分,代码如下

    //city/components/Search.vue <template> <div class="search"> <input class="search-input" type="text" placeholder="输入城市名称或者拼音" /> </div> </template> <script> export default { name: 'CitySearch' } </script> <style lang="stylus" scoped> @import '~styles/varibles.styl' .search height .722rem padding 0 .1rem background $bgColor .search-input box-sizing border-box width 100% height .62rem padding 0 .1rem line-height .62rem border-radius .06rem color #666 text-align center </style>

    city/City.vue,在 city 的主页面引入我们制作好的 city-search 模块
    image.png

    10-3 city-list、city-ajax 、city-vuex、city-search-logic 部分的制作

    城市整体效果图

    Ui 上面的制作,直接从 github 下载下来,git checkout 到不同的分支,就能看到代码了,总结嘛,不能能把所有的项目中的代码都展示出来,更多的是展示难点、思路、注意事项等等一些小细节地方。
    city-ajax 部分和 index-ajax 方式是一样,在这里咱们就不再次赘述了

    知识点 1:BetterScroll 的使用,让城市列表可以滚动起来

    //安装better-scroll npm install better-scroll -S

    在这个使用 better-scroll 的时候我们需要注意三点

    • dom 结构(要符合这种结构)
    <div class="wrapper"> <ul class="content"> <li>...</li> <li>...</li> ... </ul> <!-- you can put some other DOMs here, it won't affect the scrolling </div>
    • 样式(要滚动的 list 要脱离文档流)
    .list overflow: hidden; position: absolute; top: 1.6rem; left: 0; right: 0; bottom: 0;
    • 在 vue 中的调用和使用方法
    //dom部分 <div class="list" ref="wrapper"> ..... </div> //js部分 import BScroll from 'better-scroll' mounted () { this.scroll = new BScroll(this.$refs.wrapper) }

    知识点 2 兄弟组件数据传递
    我们知道:
    City.vue 是父组件
    components/List.vue 是一个子组件
    components/Alphabet.vue 也是一个子组件

    那么子组件(Alphabet.vue)如何和子组件(List.vue)进行通信呢?
    现在有这样的一个需求,就是当我们点击右侧的字母(代码在 Alphabet.vue 中),列表(List.vue)能自动滚动相对应的列表字母模块部分,那么这个过程就是一个子组件和子组件的通信(兄弟组件数据传递)

    思路:
    第一步:子组件(Alphabet.vue)点击字母的时候,通过 $emit 发送一个'change'的方法,并且把携带的点击入参传递给父组(City.vue)

    //dom部分 <li class="item" v-for="item of letters" :key="item" @click="handleLetterClick" //触发点击事件 >{{item}}</li> //js部分 methods: { handleLetterClick (e) { this.$emit('change', e.target.innerText) } }

    第二步:父组件(City.vue)通过属性来监听‘change’事件,同时创建一个新的方法,在此方法中来接受子组件传递过来的参数,随后把入参放入到 data 初始化的 letter 中,再然后,把 letter 获得入参以属性的方式传递给 city-list 组件

    //1)dom 来监听子组件发出来的change <city-alphabet :cities="cities" @change="handleLetterClick"></city-alphabet> //4)dom 父组件从子组件那拿来的数据(letter)传递给新的子组件 <city-list :cities="cities" :hotCities="hotCities" :letter="letter"></city-list> //2)初始化data中的letter值 用来存储子组件出来的入参 data () { return { letter: '' } }, //3)js change 创建的方法 来接受子组件传递过来的值,并把它存储到data里面 handleLetterClick (letter) { this.letter = letter }

    第三步:子组件(List.vue)通过属性 props 来接受父组件传过来的值

    //js props: { letter: String//接受父组件传递过来的值 }, //js 监听传过来值的变化 watch: { letter () { if (this.letter) { const element = this.$refs[this.letter][0] //通过获取字母的值 this.scroll.scrollToElement(element) //滚动到指定元素模块 } } } //dom 需要在字母模块添加ref属性 <div class="area" v-for="(item,key) of cities" :key="key" :ref="key"//这个key值刚好和兄弟组件传过来的值相同 > <div class="title border-topbottom">{{key}}</div> <div class="item-list"> <div class="item border-bottom" v-for="innerItem of item" :key="innerItem.id">{{innerItem.name}}</div> </div> </div>

    知识点 3 完成一个手指滑动右侧字母,左侧区域跟着滚动
    这部分咱们需要给右侧的字母绑定上三个事件:

    @touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="handleTouchEnd"

    为了只让在 touchmove 里面去触发这些操作,所以我们需要定义个开关(标示位),我们把这个标示位放在了 data 里面

    touchStatus: false //设置为false

    所以当我们开始滑动的时候我们把 touchStatus 设置为 true

    handleTouchStart () { this.touchStatus = true }

    当我们手指划出触发操作区域的时候,我们需要把标示为设置为 false

    handleTouchEnd () { this.touchStatus = false }

    所以只有当标示位为 true 的这种情况,我们采取进滑动字母相对应的操作

    handleTouchMove () { if (this.touchStatus) { //滑动过程中所对应的逻辑 } }

    思路
    在滑动这个过程中,最终我们在这个页面上下滑动的时候,我们需要知道你滑动的位置是第几个字母
    1、我们需要知道 A 字母距离顶部的距离
    2、我们需要知道手指滑动到当前字母距离顶部的的距离
    3、把上面两个做一个差值,那么我们就可以得到当前位置距离 A 字母之间的高度
    4、我们把得到这个差值高度除以每个字母的高度,那么我们就得到了是第几个字母了
    根据上面这个思路,我们需要得到这个字母的数组:

    computed: { letters () { const letters = [] for (let i in this.cities) { letters.push(i) } return letters } }

    通过计算属性,我们就可以把 dom 上的数据获取从父组件传递过来的 cities 改为 letters

    <li class="item" v-for="item of letters" //通过计算属性来获得字母值 :key="item" :ref="item" @click="handleLetterClick" @touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="handleTouchEnd" > {{item}} </li>

    根据上面的思路咱们开始来编写相对应逻辑

    handleTouchMove (e) { //标示位开始 if (this.touchStart) { const startY = this.$refs['A'].offsetTop //获取字母A距离顶部的距离 const touchY = e.touches[0].clientY - 79 //获取手机滑动当前字母距离顶部距离(79是header和搜索框的高度) const index = Math.floor((touchY-startY) / 20) //获得是第几个字母 if (index >= 0 && index < this.letters.length) { this.$emit('change', this.letters[index]) //在有效的索引里面去 查找是第几个字母 } } }

    其实写到这块我们的功能是完成了的,但是细想还有一些地方需要优化?
    初始化

    data () { return { startY: 0, timer: null } },

    优化一:每次都去求获取字母 A 距离顶部的距离?

    updated () { this.startY = this.$refs['A'][0].offsetTop },

    优化二:滑动字母的时候,需要做一下事件节流(通过一个定时器 timer)

    handleTouchMove (e) { if (this.touchStatus) { if (this.timer) { clearTimeout(this.timer) } this.timer = setTimeout(() => { const startY = this.startY const touchY = e.touches[0].clientY - 79 const index = Math.floor((touchY - startY) / 20) if (index >= 0 && index < this.letters.length) { this.$emit('change', this.letters[index]) } }, 16) } },

    知识点 4 实现一个城市搜索功能
    需求
    1.根据字母或者汉字可以进行检索想要的内容
    2.当搜索框没数据的时候,不显示搜索区域内容
    3.当搜索框有数据且数据不在搜索内容时,显示暂无搜索内容
    4.当搜索出来的内容比较多的时候,搜索内容可以进行滚动(better-scroll)

    第一步:获取从父组件传递过来的 cities 值

    props: { cities: Object },

    第二步:data 里面初始化 keyword、list、timer

    data () { return { keyword: '', list: [], timer: null } },

    第三步:watch 方法监听 keyword 的更改、其中这里面包含 timer 优化、list 数据获取、检索操作的逻辑

    watch: { keyword () { if (this.timer) { clearTimeout(this.timer) } if (!this.keyword) { this.list = [] return false } this.timer = setTimeout(() => { const result = [] for (let i in this.cities) { this.cities[i].forEach(value => { if (value.spell.indexOf(this.keyword) > -1 || value.name.indexOf(this.keyword) > -1) { result.push(value) } }) } this.list = result }, 100) } },

    第四步:数据处理好了,要铺到 Ui 上面
    为了可以滚动:一定要符合 better-scroll 的 dom 结构;search-content 样式要脱离文档流。
    只有当有关键字才会显示搜索内容;
    当关键字搜索没有数据的时候,显示”没有搜索到匹配内容“

    <div class="search-content" ref="search" v-show="keyword"> <ul> <li class="search-item border-bottom" v-for="item of list" :key="item.id">{{item.name}}</li> <li class="search-item border-bottom" v-show="hasNoData">没有搜索到匹配内容</li> </ul> </div>

    第五步:搜索数据有了,但是过多的时候也要可以滚动,better-scroll

    mounted () { this.scroll = new Bscroll(this.$refs.search) }

    知识点 5 vuex 实现数据共享
    如果学过 react 的同学肯定知道 redux,react 是处理 ui 层的,那么数据层就是通过 redux 来完成,方便我们不同页面之间的传值,一直值的更改等等
    同样在 vue 中,vue 也只负责 ui 部分,vuex 则是用来处理数据层的
    vuex 的原理图

    1.安装 vuex

    npm install vuex -S

    2.使用和调用 vuex
    因为 vuex 是处理数据模块的,所以我们在 src 目录下创建一个 store 目录,在 store 目录下面创建一个
    index.js

    import Vue from 'vue' import Vuex from 'vuex' export default new Vuex.Store({ state: { city: '北京' } })

    创建好之后,我们在 main.js 文件中去调用这个文件

    import store from './store' new Vue({ el: '#app', store,//根实例引入store router, components: { App }, template: '<App/>' })

    3.应用
    在咱们这个项目中,首页右上角的城市名称是通过后端返给我们,那么我们可以通过 vuex 来初始化一个城市,也可以通过 vuex 来更改城市这个值。
    在 store/index.js 其实我们已经做了 city 的初始化的值:北京
    那么在首页和城市页面我们如何获取 vuex 当中这个值呢?

    //pages/home/components/Header.vue {{this.$store.state.city}} //pages/city/components/List.vue 当前城市 {{this.$store.state.city}}

    点击热门城市或者点击城市搜索出来列表切换城市的显示,那么我们去如何更改 state 这个值呢?

    //点击热门城市事件 @click="handleCityClick(item.name)" methods: { handleCityClick (city) { //要调用store里面的dispatch方法 this.$store.dispatch('changeCity', city) } }

    上面我们已经触发了一个 dispatch 的方法,那么我们通过 actions 来接受这个方法
    store/index.js

    export default new Vuex.Store({ state: { city: '上海' }, actions: { changeCity(ctx, city) { //console.log(city) //那么action如何调用mutations呢?通过commit方法 ctx.commit('changeCity',city) } }, mutations: { changeCity (state, city) { state.city = city } } })

    从上面可以看出在我们发送 dispatch 的时候,并没有触发异步请求,或者批量的数据操作,所以上面操作,我们可以直接跳过 actions 这部分,不需要去触发 dispatch 操作,而是直接调用 commit 对 mutations 的操作
    所以上面的代码就可以改为:

    //点击热门城市事件 @click="handleCityClick(item.name)" methods: { handleCityClick (city) { //要调用store里面的dispatch方法 this.$store.commit('changeCity', city) //将dispatch 改为commit } } //store/index.js export default new Vuex.Store({ state: { city: '上海' }, //删除actions的相关操作 mutations: { changeCity (state, city) { state.city = city } } })

    讲到这里其实就实现了 vuex 的数据一个设置以及显示的一些操作,但是我们更具当前的产品需求我们还是需要完善一下页面跳转。
    之前我们实现页面跳转是通过
    1.router-link 的 to 属性来实现
    2.那么还有一种通过 js 来实现页面跳转的 $router.push
    那么我们希望我们在选择完城市后,能自动跳转到首页,那么

    this.$router.push('/')

    知识点 6 vuex 的高级使用以及 localStorage

    store/index.js 文件的拆分和 localStorage 的应用

    在上面使用 vuex 中我们给 city 设置了一个初始值:'上海',但是当我们切换完城市后,返回首页,如果我们刷新首页,那么我们选择的城市就又变回为了默认值:'上海',那么针对这种情况,我们需要引入本地缓存 localStorage,但是呢,有些浏览器会屏蔽 localStorage 的一些东西,为了程序的健壮性,减少没必要的浏览器异常,所以在对 localStorage 进行相关操作的时候,我们先进行一层 try catch 的操作

    //store/index.js let defaultCity = '上海' try { if (localStorage.city) { defaultCity = localStorage.city } } catch (e) {} export default new Vuex.Store({ state: { city: defaultCity }, mutations: { changeCity (state, city) { state.city = city try { localStorage.city = city } catch (e) {} } } })

    写到这里我们发现,将来如果我们业务比较复杂的话,store/index.js 会变的越来越庞大,那么这不是我们希望看到的,所以我们要对 store/index.js 进行拆分。
    那么如何进行拆分呢?
    store/index.js 只是一个总文件,而这个总文件包含很多部分:state、actions、mutations 等等,
    那么我们将可以将这些模块拆分成为:state.js、actions.js、mutations.js
    最后再把他们引入到 store/index.js 文件中
    那么,根据这个思路咱们接下来拆分一下 store/index.js

    //store/state.js let defaultCity = '北京' try { if (localStorage.city) { defaultCity = localStorage.city } } catch (e) { } export default { city: defaultCity } //store/mutions.js export default{ changeCity (state, city) { state.city = city try { localStorage.city = city } catch (e) {} } }

    那么 store/index.js 就变为了:

    import Vue from 'vue' import Vuex from 'vuex' import state from './state' import mutations from './mutations' Vue.use(Vuex) export default new Vuex.Store({ state, mutations })

    vuex 的高级应用以及针对项目当中的优化

    我们上面调用城市的时候是通过 {{this.$store.state.city}} 来实现的
    如果这么写的话,略显页面比较冗余。那么有没有其他方法会比较简单一些呢?
    vuex 帮我们封装了一些方法和 aip 有一个 mapState 的方法就可以帮我们实现,那么应该如何使用呢?

    import { mapState } from 'vuex' //第一种通过数组方法获取 computed: { ...mapState(['city']) //这样就把把store中的city值获取到 } //第二种通过对象方法获取(起一个别名) computed: { ...mapState({ currentCity: 'city' }) //这样就把把store中的city值获取到 } //如果是第一种方法获取的 将原来的 {{this.$store.state.city}} 改为 {{this.city}} //如果是第二种方法获取的 将原来的 {{this.$store.state.city}} 改为 {{this.currentCity}}

    获取 vuex 中 store 的数据我们可以通过 mapState 方法,那么设置 vuex 数据呢?
    我们可以通过 vuex 给我们提供的 mapMutations 方法,那么如何实现呢?

    import {mapMutations} from 'vuex' methods: { handleCityClick (city) { //this.$store.commit('changeCity', city) 改为下面: this.changeCity(city) this.$router.push('/') }, ...mapMutations(['changeCity']) }

    讲的这里我们使用了 vuex 给我们提供的 state、actions、mutations,我们登录 vue 官网,我们发现 vuex 还给我们提供了两个一个是 getter、另一个是 module
    那么我们来看一下 getter 的使用

    //store/index.js import Vue from 'vue' import Vuex from 'vuex' import state from './state' import mutations from './mutations' Vue.use(Vuex) export default new Vuex.Store({ state, mutations, getters: { doubleCity (state) { return state.city + ' ' + state.city } } })

    那么页面上应该如何使用或者调用呢?

    import { mapGetters } from 'vuex' computed: { ...mapGetters(['doubleCity']) } //页面上调用 {{this.doubleCity}}

    那么我们此时会想,这有什么用处呢?因为 mapState 就可以实现的方法,我为什么还要使用 mapGetters 来实现呢?
    其实呢,我们发现 getters 方法有点类似 vue 组件当中的 computed 方法,他可以把我们 state 值进行处理后返给我们一个新值,从来来避免一些数据的冗余。
    getter 讲完了,那么 module 我们在什么情况下去使用呢?
    因为我们在 store/index.js 中 只写了 city 相关的(state、actions、mutations)等等操作,当我们在实际开发的过程中,我们肯定不单单只有 city 这一个模块的,如果有很多页面的功能模块的话,我们拆分的 state.js、actions.js、mutations.js 会变得很臃肿的,这不是我们期盼看到的。
    所以我们通过 module 模块对我们的功能模块进行进一步的拆分,每个功能模块包含自己的(state、actions、mutations 等等)。如下面例子:

    const moduleA = { state: { ... }, mutations: { ... }, actions: { ... }, getters: { ... } } const moduleB = { state: { ... }, mutations: { ... }, actions: { ... } } const store = new Vuex.Store({ modules: { a: moduleA, b: moduleB } }) store.state.a // -> moduleA 的状态 store.state.b // -> moduleB 的状态

    知识点 7 使用 keep-alive 优化网页
    keep-alive 是个抽象组件(或称为功能型组件),实际上不会被渲染在 DOM 树中。它的作用是在内存中缓存组件(不让组件销毁),等到下次再渲染的时候,还会保持其中的所有状态,并且会触发 activated 钩子函数。因为缓存的需要通常出现在页面切换时,所以常与 router-view 一起出现:

    //app.vue <keep-alive> <router-view/> </keep-alive>

    如此一来,每一个在 router-view 中渲染的组件,都会被缓存起来。
    如果只想渲染某一些页面/组件,可以使用 keep-alive 组件的 include/exclude 属性。include 属性表示要缓存的组件名(即组件定义时的 name 属性),接收的类型为 string、RegExp 或 string 数组;exclude 属性有着相反的作用,匹配到的组件不会被缓存。假如可能出现在同一 router-view 的 N 个页面中,我只想缓存列表页和详情页,那么可以这样写:

    <keep-alive :include="['Home', 'City']"> <router-view /> </keep-alive>

    那么针对咱们这个项目,当我们增加上 keep-alive 属性后,当我们访问过的页面请求过后,再去请求的时候,那么就不会再去触发 ajax 请求,而在此项目中首页的数据变更是需要我们切换不同的城市来实现变更的,也就是当城市只要变更我们就需要对首页数据进行一次请求。那么我们应该如何更新首页的数据呢?

    //通过vuex的mapState属性我们可以获取 city的值 import { mapState } from 'vuex' computed: { ...mapState(['city']) }, //通过ajax的入参来请求不同城市的数据 getHomeInfo () { axios.get('/api/index.json?city=' + this.city).then(this.getHomeInfoSucc) }, //当我们触发了keep-alive属性后,那么就会多出一个activated的生命周期钩子 //通过城市的变更,我们在这个生命周期函数中再去请求接口,入参用最新的城市,那么怎么来区分城市的变更呢? //data里面定义一个新的字段lastCity data () { return { lastCity: '',//定义 swiperList: [], iconList: [], recommendList: [], weekendList: [] } }, //如果当前的city和最后一次请求的城市不一致,那么我们把最后一次请求的城市赋值为最新的城市,且用最新的城市作为接口请求的入参 activated () { if (this.lastCity !== this.city) { this.lastCity = this.city this.getHomeInfo() } }

    第 11 章 Vue 项目开发之详情

    11-1.详情页-动态路由和 banner 布局

    上一章节我们制作了城市页面相关的东西,那么这一节我们打算制作咱们最后一部分详情页面。

    1.详情页面动态路由的添加
    这一部分的代码在 git@github.com:fx35792/vue-travel.git 仓库的 detail-banner 分支上

    //router/index.js import Detail from '@/pages/detail/Detail' export default new Router({ routes: [ ... { path: '/detail/:id', //添加动态id name: 'Detail', component: Detail } ] })

    2.详情页面 banner 部分以及点击 banner 进入 gallery 组件的制作
    制作这一部分主要是一个 Ui 的布局以及 gallery 组件的封装
    Ui 布局咱们总结中就不在这个地方就多说了,咱们还是说一下这个 gallery 公共组件吧
    效果图

    公共组件目录结构是 src/common/gallery/Gallery.vue

    <template> <div class="container" @click="handleCloseGallery"> <div class="wrapper"> <swiper :options="swiperOption"> <swiper-slide v-for="(item,index) in imgs" :key="index"> <img :src="item" class="gallery-img"> </swiper-slide> <div class="swiper-pagination" slot="pagination"></div> </swiper> </div> </div> </template> <script> export default { name: 'CommomGallery', props: { imgs: { type: Array, default () { return [] } } }, data () { return { swiperOption: { pagination: '.swiper-pagination', paginationType: 'fraction', observeParents: true, observer: true, autoplay: false } } }, methods: { handleCloseGallery () { this.$emit('close') } } } </script> <style lang="stylus" scoped> .container >>> .swiper-container { overflow: inherit; } .container { display: flex; flex-direction: column; justify-content: center; position: fixed; z-index: 999; top: 0; left: 0; bottom: 0; right: 0; background: #000; color: #fff; .wrapper { height: 0; width: 100%; padding-bottom: 100%; .gallery-img { width: 100%; } .swiper-pagination { bottom: -1rem; } } } </style>

    在制作 gallery 过程中注意几点
    1.是布局,通过 flex 把图片水平垂直居中
    2.图片轮播,swiperOption 的配置

    swiperOption: { pagination: '.swiper-pagination',//显示轮播底部的点/数字 paginationType: 'fraction', //显示当前轮播和所有图片的个数(bullets’ 圆点(默认)、‘fraction’ 分式 、‘progress’ 进度条、‘custom’ 自定义) observeParents: true,//将observe应用于Swiper的父元素。当Swiper的父元素变化时,例如window.resize,Swiper更新。 observer: true,//当改变swiper的样式(例如隐藏/显示)或者修改swiper的子元素时,自动初始化swiper。 autoplay: false //关闭自动轮播 }

    3.数据要从父组件传递过来,设置数据的类型和默认值为空
    4.通过 $emit 给父组件传递数据

    11-2.详情页-Header 渐隐渐显效果

    这一部分的代码在 git@github.com:fx35792/vue-travel.git 仓库的 detail-header 分支上
    效果图

    <template> <div> <router-link tag="div" to="/" class="header-abs" v-show="showAbs"> <div class="iconfont header-abs-icon">&#xe696;</div> </router-link> <div class="header-fixed" v-show="!showAbs" :style="opacityStyle"> <router-link to="/"> <div class="iconfont header-fixed-icon">&#xe696;</div> </router-link>景点详情 </div> </div> </template> <script> export default { name: 'DetailHeader', data () { return { showAbs: true, opacityStyle: { opacity: 0 } } }, methods: { handleScroll () { const top = document.documentElement.scrollTop console.log('top', top) if (top > 60) { let opacity = top / 140 opacity = opacity > 1 ? 1 : opacity this.opacityStyle = { opacity } this.showAbs = false } else { this.showAbs = true } } }, activated () { //绑定监听事件 window.addEventListener('scroll', this.handleScroll) }, deactivated () { //卸载监听事件 window.removeEventListener('scroll', this.handleScroll) } } </script> <style lang="stylus" scoped> @import '~styles/varibles.styl'; .header-abs { position: absolute; z-index: 999; top: 0.2rem; left: 0.2rem; width: 0.8rem; height: 0.8rem; line-height: 0.8rem; border-radius: 50%; text-align: center; background: rgba(0, 0, 0, 0.8); .header-abs-icon { color: #fff; font-size: 0.46rem; } } .header-fixed { position: fixed; z-index: 999; top: 0; left: 0; right: 0; height: $headHeight; line-height: $headHeight; background: $bgColor; text-align: center; color: #fff; font-size: 0.32rem; .header-fixed-icon { position: absolute; top: 0; left: 0; width: 0.64rem; text-align: center; font-size: 0.4rem; color: #fff; } } </style>

    1.两快头部的布局很简单
    2.主要是一个是渐变效果:
    监听页面滚动的高度来控制两个头部的显示和隐藏
    监听滚动的高度动态来改变 apacity 的值,当 apacity 的值大于 1 的时候,始终让它的值等于 1
    3.window 绑定监听事件,离开页面要卸载掉监听事件

    11-3.详情页-使用递归组件实现详情列表

    递归组件是:可以自身调用的组件
    使用场景:当数据结构相同的形式嵌套实现的时候
    知识点:组件命名方式(DetailList==>)
    1:父组件调用子组件时候使用
    2:递归组件使用
    3:当使用 keep-alive 的时候,不想缓存某个页面时,也会用到 name

    //src/detail/components/List.vue <template> <div> <div class="item" v-for="(item,index) of list" :key="index"> <div class="item-title border-bottom"> <span class="item-title-icon"></span> {{item.title}} </div> <div v-if="item.children" class="item-chilren"> <detail-list :list="item.children"></detail-list> </div> </div> </div> </template> <script> export default { name: 'DetailList', props: { list: Array } } </script> <style lang="stylus" scoped> .item-title-icon { position: relative; left: 0.06rem; top: 0.06rem; display: inline-block; width: 0.36rem; height: 0.36rem; background: url('http://s.qunarzz.com/piao/image/touch/sight/detail.png') 0 -0.45rem no-repeat; // s.qunarzz.com/piao/image/touch/sight/detail.png) 0 -.45rem no-repeat margin-right: 0.1rem; background-size: 0.4rem 3rem; } .item-title { line-height: 0.8rem; font-size: 0.32rem; padding: 0 0.2rem; } .item-chilren { padding: 0 0.2rem; } </style> //src/detail/Detail.vue <template> <div> <detail-banner :sightName="sightName" :bannerImg="bannerImg" :gallaryImgs="gallaryImgs"></detail-banner> <detail-header></detail-header> <div style="height:50rem"> <detail-list :list="list"></detail-list> </div> </div> </template> <script> import DetailBanner from './components/Banner' import DetailHeader from './components/Header' import DetailList from './components/List' import axios from 'axios' export default { name: 'Detail', data () { return { sightName: '', bannerImg: '', gallaryImgs: [], list: [] } }, methods: { getDetailInfo () { axios .get('/api/detail.json', { params: { id: this.$route.params.id } }) .then(this.getDetailSucc) }, getDetailSucc (res) { res = res.data if (res.ret && res.data) { res = res.data console.log(res) this.sightName = res.sightName this.bannerImg = res.bannerImg this.gallaryImgs = res.gallaryImgs this.list = res.categoryList } } }, mounted () { this.getDetailInfo() }, components: { DetailBanner, DetailHeader, DetailList } } </script> <style lang="stylus" scoped></style>

    ajax 数据的获取和传递 咱们就不在这赘述了,咱们说说在制作这个过程的一些知识点:
    1.当我们从首页的列表页面点击进入详情页面的时候,会遇到一种这样的场景,首页很长的的时候,我们滚动到下面,点击进入详情页面,你会发现,进入的详情页面被滑动了距离,而不是最顶部预览这个页面的。
    那么我们应该如何解决呢?
    我们需要在路由的页面添加一段代码

    scrollBehavior (to, from, savedPosition) { return { x: 0, y: 0 } }

    官网给出我们的解决方案:https://router.vuejs.org/zh/guide/advanced/scroll-behavior.html
    2.因为我们在 router-view 上使用了 keep-alive,那么访问了的页面就会被缓存下来,也就会导致,我们第一次请求详情页面的时候会走 ajax 请求,之后就不会了,而这不是我们希望看到的,因为我们点击每个列表的数据,他们展示的详情页面都会不相同,都会把详情的 id 写到详情页面的 url 上面,所以我们期盼的结果就是,每次点击详情页面的时候,都会进行数据 ajax 请求。
    之前我们使用过一种方法,解决过切换不同的城市,首页进行 ajax 请求,当时我们处理的方法是在 activated 这个生命周期的钩子上比较上次城市和切换的数据是不是相等,来进行首页接口的请求的。
    那么今天咱们来通过第二种方法来避免 keep-alive 导致的 ajax 请求不能发送的问题。
    那就是设置一下 keep-alive 的一个属性值:exclude
    Detail 是 不需要缓存组件的 name 值

    <keep-alive exclude="Detail"> <router-view/> </keep-alive>
    11-4.详情页-公共组件渐隐渐现 FadeAnimation 组件的制作

    利用的是插槽(slot)和动画(transition)的知识点

    //src/common/fade/FadeAnimation.vue <template> <transition> <slot></slot> </transition> </template> <script> export default { name: 'FadeAnimation' } </script> <style lang="stylus" scoped> .v-enter, .v-leave-to { opacity: 0; } .v-enter-active, .v-leave-active { transition: opacity 0.5s; } </style>

    使用

    //src/pages/detail/components/Banner.vue <fade-animation> <common-gallery :imgs="gallaryImgs" v-show="galleryStatus" @close="handleClickClose"></common-gallery> </fade-animation> <script> import FadeAnimation from 'common/fade/FadeAnimation' export default { name: 'DetailBanner', ... components: { ..., FadeAnimation } } </script>

    第 12 章 Vue 项目的联调、测试与发布上线

    12-1 Vue 项目的联调测试上线--项目前后端联调

    当我们项目开发完成后,我们需要和后端进行联调,那么我们需要把我们项目目录下的 static/mock 文件删除掉。
    此时就需要我们自己更改一下我们本地的代理文件 config/index.js 中的 proxyTable 的配置信息
    场景一:链接自己电脑 php 或者 Nginx 启动的一个后端服务

    proxyTable: { '/api': { target:'http://localhost:8080',//这个是你本机电脑的ip或者localhost+(自己启动的端口号,如果是80则可以不写) pathRewrite: { '^/api':'/static/mock'//这个也是根据提供的api地址来进行相应的修改 } } },

    场景二:链接后端同事电脑开启的一个后端服务

    proxyTable: { '/api': { target:'http://192.168.20.22:8080',//这个是同事电脑ip(同一网段)+(同事电脑启动的端口号) pathRewrite: { '^/api':'/static/mock'//这个也是根据提供的api地址来进行相应的修改 } } },

    场景三:链接后端已经把接口服务发布到服务器

    proxyTable: { '/api': { target:'http://www.***.com:8080',//服务器ip或者域名地址+(服务器端口号) pathRewrite: { '^/api':'/static/mock'//这个也是根据提供的api地址来进行相应的修改 } } },

    更改完配置后,一定要重新启动一下项目 ctrl+c 结束运行的项目,npm start 重启项目

    12-2 Vue 项目的联调测试上线--真机测试

    那么如何在真机预览我们本地运行的制作效果呢?
    我们在浏览器运行的是:http://localhost:8080/#/
    在手机上我们通过 ip 来访问本地网址:http://192.168.20.17:8080/#/
    想要通过 ip 来访问需要我们做两点的准备:
    1.查看 ip

    window: ipconfig mac: ifconfig

    2.项目启动更改启动项命令,开启 ip 访问
    在 package.json 启动命令中增加:--host 0.0.0.0

    "scripts": { "dev": "webpack-dev-server --host 0.0.0.0 --inline --progress --config build/webpack.dev.conf.js", "start": "npm run dev", "lint": "eslint --ext .js,.vue src", "build": "node build/build.js" },

    当我们用真机测试项目的过程中我们可能会遇到这样的问题,就是 pc 上运行好好的,但是在 phone 上运行不正常,比如咱们这个项目中,城市页面,当我们滑动右侧字母的过程中,页面也被滑动,那么我们出发点就是阻止页面的 touchstart 的默认行为增加一个 prevent 的修饰符

    @touchstart.prevent="handleTouchStart"

    还有在有些手机上发现页面加载不出来,加载出来的页面是一个空白屏,那是因为一些手机不支持 es6 的一些语法比如 promise 等等,那么如何解决这个问题呢?
    我们通过一个 babel 解释器把 es6 编译成为浏览器识别的 es5 语法即可

    npm install babel-polyfill -S

    在 main.js 中引入 babel-polyfill

    import 'babel-polyfill'

    这样就顺利解决了一部分手机白屏或者加载不出页面的问题。

    12-3 Vue 项目的联调测试上线--打包上线

    运行下面指令,在我们的项目的根目录下就会生成一个 dist 文件

    npm run build

    如果运行结束,看到:Build complete,并且在根目录看到 dist 文件,那说说明我们打包成功了
    把这个 dist 文件夹给运维或者后端 就行了,让他们帮我们部署到服务器上即可

    但是有时候我们的项目并不是直接域名下的一个网址,而是一个文件下的项目,比如:
    http://www.***.com/travel 用这样的网址来访问

    那么我们在打包的时候,就需要配置一下打包的配置信息

    //config/index.js build: { ...., assetsPublicPath: '/travel', ... }

    配置好之后,再去执行 npm run build

    12-4 Vue 项目的联调测试上线--异步组件实现按需加载

    上面我们打包完以后我们可以查看 dist 文件夹

    static --js ---app.**.js //业务代码 ---manifest.**.js //打包配置文件 ---vendor.**.js //公共的包文件 --css ---app.**.css 所有的样式文件 index.html

    当项目比较小的时候,我们这样去打包没有什么大问题,但是当我们随着业务代码的开发增加,app.**.js 文件会越来越大
    这样会导致:初次记载的事件会越来越长,体检降低
    那么我们能不能做到,当项目比较大的时候,只加载当前所需要的业务代码呢?这样虽然请求次数增加了,但是不会导致首次加载时间过长。
    我们通过异步加载组件的方法来解决这个问题:
    1.路由我们可以通过异步加载
    通过箭头函数来引入路由:() => import('@/pages/detail/Detail')

    //src/router/index.js //同步写法: import Vue from 'vue' import Router from 'vue-router' import Home from '@/pages/home/Home' import City from '@/pages/city/City' import Detail from '@/pages/detail/Detail' Vue.use(Router) export default new Router({ routes: [ { path: '/', name: 'Home', component: Home }, { path: '/city', name: 'City', component: City }, { path: '/detail/:id', name: 'Detail', component: Detail } ], scrollBehavior (to, from, savedPosition) { return { x: 0, y: 0 } } }) //异步写法: import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) export default new Router({ routes: [ { path: '/', name: 'Home', component: () => import('@/pages/home/Home') }, { path: '/city', name: 'City', component: () => import('@/pages/city/City') }, { path: '/detail/:id', name: 'Detail', component: () => import('@/pages/detail/Detail') } ], scrollBehavior (to, from, savedPosition) { return { x: 0, y: 0 } } })

    2.子组件我们也可以通过异步来加载

    //同步加载 import HomeRecommend from './components/Recommend' export default { name: 'Home', components: { ..., HomeRecommend } } //异步组件加载 export default { name: 'Home', components: { ..., HomeRecommend: () => import('./components/Recommend') } }
    12-5 Vue 项目的联调测试上线--课程总结和后续学习指南

    写到这里 vue 的一些基础使用在这个项目中基本上都体现了,可以开发一些初级、中级的项目了。
    如果你还想了解更多关于 vue 一些知识,你可以去 vue 官网查看关于:
    Vue router
    vuex
    Vue 服务器端渲染
    更多 Vue 资源 awesome-vue

    • Vue.js

      Vue.js(读音 /vju ː/,类似于 view)是一个构建数据驱动的 Web 界面库。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。

      267 引用 • 666 回帖

    相关帖子

    欢迎来到这里!

    我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

    注册 关于
    请输入回帖内容 ...