简易的Vue程序
在vscode中新建一个html文件,输入html5快捷生成代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
</html>
添加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">
<title>Document</title>
</head>
<body>
<div id="app">
{{message}} {{message + message}}
<div :id="message"></div>
<ul>
<li v-for="item in list">
<span v-if="!item.del">{{item.title}}</span>
<span v-else style="text-decoration: line-through">{{item.title}}</span>
<button v-show="!item.del">删除</button>
</li>
</ul>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
message: 'hello world',
list: [{
title: '课程1',
del: false
}, {
title: '课程2',
del: true
}],
}
})
</script>
</body>
</html>
组件
为了使用小型、独立和可复用的模块构建大型应用我们引入了组件的概念,来看一下基于上述内容的组件第一次抽象,我们抽离出了todo-list组件和todo-item组件:
<!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">
<title>Document</title>
</head>
<body>
<div id="app">
{{message}} {{message + message}}
<div :id="message"></div>
<!-- <ul>
<todo-item v-for="item in list" :title="item.title" :del="item.del"></todo-item>
</ul> -->
<todo-list></todo-list>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.component('todo-item', {
props: {
title: String,
del: {
type: Boolean,
default: false,
},
},
template: `
<li>
<span v-if="!del">{{title}}</span>
<span v-else style="text-decoration: line-through">{{title}}</span>
<button v-show="!del">删除</button>
</li>
`,
data: function() {
return {}
},
methods: {
},
})
Vue.component('todo-list', {
template: `
<ul>
<todo-item v-for="item in list" :title="item.title" :del="item.del"></todo-item>
</ul>
`,
data: function() {
return {
list: [{
title: '课程1',
del: false
}, {
title: '课程2',
del: true
}],
}
}
})
var vm = new Vue({
el: '#app',
data: {
message: 'hello world',
}
})
</script>
</body>
</html>
属性
思考:子组件为何不可以修改父组件传递的prop,如果修改了,Vue是如何监控到属性的修改并给出警告的?
事件
@click.stop 阻止冒泡
思考题:this.$emit的返回值是什么?如果在上层组件,return一个值,在$emit能不能接受到呢?
代码中的@click是绑定原生组件的方法,@delete是绑定自定义组件的方法,我们在handleClick中通过this.$emit('delete', this.title)抛出事件,在handelDelete中就能拿到对应的参数了。
<!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">
<title>Document</title>
</head>
<body>
<div id="app">
{{message}} {{message + message}}
<div :id="message"></div>
<!-- <ul>
<todo-item v-for="item in list" :title="item.title" :del="item.del"></todo-item>
</ul> -->
<todo-list></todo-list>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.component('todo-item', {
props: {
title: String,
del: {
type: Boolean,
default: false,
},
},
template: `
<li>
<span v-if="!del">{{title}}</span>
<span v-else style="text-decoration: line-through">{{title}}</span>
<button v-show="!del" @click="handleClick">删除</button>
</li>
`,
data: function() {
return {}
},
methods: {
handleClick(e) {
console.log('点击删除按钮')
this.$emit('delete', this.title)
}
},
})
Vue.component('todo-list', {
template: `
<ul>
<todo-item @delete="handleDelete" v-for="item in list" :title="item.title" :del="item.del"></todo-item>
</ul>
`,
data: function() {
return {
list: [{
title: '课程1',
del: false
}, {
title: '课程2',
del: true
}],
}
},
methods: {
handleDelete(val) {
console.log('handleDelete', val)
}
}
})
var vm = new Vue({
el: '#app',
data: {
message: 'hello world',
}
})
</script>
</body>
</html>
诸如冒泡等方法也可以在vue中通过修饰符来进行实现。
插槽
普通插槽 和 作用域插槽
思考:相同名称的插槽是合并还是替换?
在上面的代码中,我们的todo-item是直接写死在todo-list里面,这样其实是不是很合理的,我们希望能往todo-list中传入todo-item这样就能够自动进行渲染。
但是当我们把todo-item从todo-list中取出来,todo-list中就只剩下了ul标签,那么todo-item应该放置在什么位置呢?在这个基础上引入了插槽的概念。
插槽分为匿名插槽、具名插槽和作用域插槽,在作用域插槽中,template绑定:value = "value" ,父组件通过v-slot:pre-icon="{value}"来拿到value值。
<!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">
<title>Document</title>
</head>
<body>
<div id="app">
{{message}} {{message + message}}
<div :id="message"></div>
<!-- <ul>
<todo-item v-for="item in list" :title="item.title" :del="item.del"></todo-item>
</ul> -->
<todo-list>
<todo-item @delete="handleDelete" v-for="item in list" :title="item.title" :del="item.del">
<template v-slot:pre-icon="{value}">
<span>前置图标 {{value}}</span>
</template>
</todo-item>
</todo-list>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.component('todo-item', {
props: {
title: String,
del: {
type: Boolean,
default: false,
},
},
template: `
<li>
<slot name="pre-icon" :value="value"></slot>
<span v-if="!del">{{title}}</span>
<span v-else style="text-decoration: line-through">{{title}}</span>
<slot name="suf-icon">😄</slot>
<button v-show="!del" @click="handleClick">删除</button>
</li>
`,
data: function() {
return {
value: Math.random()
}
},
methods: {
handleClick(e) {
console.log('点击删除按钮')
this.$emit('delete', this.title)
}
},
})
Vue.component('todo-list', {
template: `
<ul>
<slot></slot>
</ul>
`,
data: function() {
return {
}
},
})
var vm = new Vue({
el: '#app',
data: {
message: 'hello world',
list: [{
title: '课程1',
del: false
}, {
title: '课程2',
del: true
}],
},
methods: {
handleDelete(val) {
console.log('handleDelete', val)
}
}
})
</script>
</body>
</html>
单文件组件
上述方式在很多中小规模的项目中运作的很好,在这些项目里 JavaScript 只被用来加强特定的视图。但当在更复杂的项目中,或者你的前端完全由 JavaScript 驱动的时候,下面这些缺点将变得非常明显:
- 全局定义 (Global definitions) 强制要求每个 component 中的命名不得重复
- 字符串模板 (String templates) 缺乏语法高亮,在 HTML 有多行的时候,需要用到丑陋的
- 不支持 CSS (No CSS support) 意味着当 HTML 和 JavaScript 组件化时,CSS 明显被遗漏
- 没有构建步骤 (No build step) 限制只能使用 HTML 和 ES5 JavaScript,而不能使用预处理器,如 Pug (formerly Jade) 和 Babel 文件扩展名为 .vue 的 single-file components (单文件组件) 为以上所有问题提供了解决方法,并且还可以使用 webpack 或 Browserify 等构建工具。
将代码改成单文件组件的形式: github.com/geektime-ge…
双向绑定
什么是双向绑定?
什么是单向数据流?
双向绑定or单向数据流
-
Vue是单向数据流,不是双向绑定
-
Vue的双向绑定不过是语法糖
-
Object.defineProperty是用来做响应式更新的,和双向绑定没关系
思考:扩展PersonallInfo Demo对手机号做非空且合法校验,如不合法,则给出错误提示
众所周知,vue实现了双向绑定的功能,当我们的数据变化时我们的视图同步的更新,当我们的视图更新之后,我们的数据也会更新。在Vue中通过v-model实现双向绑定的功能。
v-model的本质仅仅是语法糖,它本质上是value和input的简写形式。
所以vue的双向绑定实际上还是单项数据流。
注意:v-model会在内部为不同的输入元素使用不同的属性并抛出不同的事件,比如text和textarea使用value属性和input事件,checkbox和radio使用checked和change事件等。
虚拟DOM与key属性
为了尽可能减少对真实dom节点的更新,提出了虚拟dom的概念。 看一下几个虚拟dom比较的方法:
场景一:只需要移动C节点即可
场景二:C节点删除,新建新节点E和F
场景三:删除C节点,E和F节点同步被删除。G节点新建,E和F节点新建
场景四:B2和B1节点移动,E和F节点删除后新建
场景五:有key属性之后,就和场景一一致了,只是节点的移动
场景六:插入节点B4
组件更新
我们都知道vue是数据驱动的,只有在数据改变的时候,我们的视图才会改变。所以任何直接修改dom的行为都是在作死。
数据来源(单向的):
- 来自父元素的属性
- 来自组件自身的状态data
- 来自状态管理器,vuex,Vue.observable
状态data与属性props
- 状态时组件自身的数据
- 属性时来自父组件的数据
- 状态的改变未必会触发更新
- 属性的改变未必会触发更新
vue的响应式更新
可以看到,在vue进行实例化的时候我们会对data下面的数据进行一些getter和setter的转化,我们在render的时候会对render到的数据进行watcher,只有被watcher的数据才会在改变时触发组件的更新。
思考:数组有哪些方法支持响应式更新,如不支持如何处理,底层原理如何实现的?
计算属性和侦听器
计算属性 computed
- 减少模板中的计算逻辑
- 数据缓存
- 依赖固定的数据类型(响应式数据) 看下示例代码:
<template>
<div>
<p>Reversed message1: "{{ reversedMessage1 }}"</p>
<p>Reversed message2: "{{ reversedMessage2() }}"</p>
<p>{{ now }}</p>
<button @click="() => $forceUpdate()">forceUpdate</button>
<br />
<input v-model="message" />
</div>
</template>
<script>
export default {
data() {
return {
message: "hello vue"
};
},
computed: {
// 计算属性的 getter
reversedMessage1: function() {
console.log("执行reversedMessage1");
return this.message
.split("")
.reverse()
.join("");
},
now: function() {
return Date.now();
}
},
methods: {
reversedMessage2: function() {
console.log("执行reversedMessage2");
return this.message
.split("")
.reverse()
.join("");
}
}
};
</script>
我们通过this.$forceUpdate()刷新数据,我们点击按钮的时候,可以看到只有reversedMessage2方法被调用了。
而当我们在输入框中输入数据时,reversedMessage1和reversedMessage2会同时被执行。
侦听器watch
- 更加灵活、通用
- watch中可以执行任何逻辑,如函数节流,ajax异步获取数据,甚至操作DOM
看下示例代码:
<template>
<div>
{{ $data }}
<br />
<button @click="() => (a += 1)">a+1</button>
</div>
</template>
<script>
export default {
data: function() {
return {
a: 1,
b: { c: 2, d: 3 },
e: {
f: {
g: 4
}
},
h: []
};
},
watch: {
a: function(val, oldVal) {
this.b.c += 1;
console.log("new: %s, old: %s", val, oldVal);
},
"b.c": function(val, oldVal) {
this.b.d += 1;
console.log("new: %s, old: %s", val, oldVal);
},
"b.d": function(val, oldVal) {
this.e.f.g += 1;
console.log("new: %s, old: %s", val, oldVal);
},
e: {
handler: function(val, oldVal) {
this.h.push("😄");
console.log("new: %s, old: %s", val, oldVal);
},
deep: true
},
h(val, oldVal) {
console.log("new: %s, old: %s", val, oldVal);
}
}
};
</script>
这里涉及到一个嵌套监听的概念,注意:如果在e中设置deep为false,那么对b.d的监听中更改e值不会涉及到e的handler事件的触发。
computed vs watch
- computed 能做的,watch都能做,反之则不行
- 能用computed的尽量用computed
看下以下用computed和watch实现的fullName的更新:
computed
<template>
<div>
{{ fullName }}
<div>firstName: <input v-model="firstName" /></div>
<div>lastName: <input v-model="lastName" /></div>
</div>
</template>
<script>
export default {
data: function() {
return {
firstName: "Foo",
lastName: "Bar"
};
},
computed: {
fullName: function() {
return this.firstName + " " + this.lastName;
}
},
watch: {
fullName: function(val, oldVal) {
console.log("new: %s, old: %s", val, oldVal);
}
}
};
</script>
watch
<template>
<div>
{{ fullName }}
<div>firstName: <input v-model="firstName" /></div>
<div>lastName: <input v-model="lastName" /></div>
</div>
</template>
<script>
export default {
data: function() {
return {
firstName: "Foo",
lastName: "Bar",
fullName: "Foo Bar"
};
},
watch: {
firstName: function(val) {
this.fullName = val + " " + this.lastName;
},
lastName: function(val) {
this.fullName = this.firstName + " " + val;
}
}
};
</script>
这两段实现的功能是一样的,但是区别时一个时用计算属性实现的,一个是用侦听器实现的。
思考:对watch1 Demo进行防抖改造 即直到用户停止输入超过500毫秒后,才更新fullName
生命周期的应用场景和函数式组件
生命周期
看下生命周期的各个阶段:创建阶段、更新阶段和销毁阶段
来详细看下各个阶段做的事情:
再mounted之后vue并不承诺子组件的DOM挂载到真实的DOM上,所以要时候需要this.nextTick()来获取子组件更新后的DOM值(this.ref[‘refName’])。
注意在此处更改数据会导致死循环(一旦更改就执行更新阶段,直到浏览器爆掉)。
函数式组件
- fuctional: true
- 无状态、无实例、没有this上下文、无生命周期
看一下示例,通过函数式组件实现临时变量的功能(虽然计算属性能帮我们搞定大部分的问题,但是计算属性更多的时针对一些被监听的变量的,有时候还是要用到临时变量。)
TempVar为一个函数式组件:
TempVar.js
export default {
functional: true,
render: (h, ctx) => {
return ctx.scopedSlots.default && ctx.scopedSlots.default(ctx.props || {});
}
};
index.vue
<template>
<div>
<a-tabs>
<a-tab-pane key="clock" tab="时钟">
<button @click="destroyClock = !destroyClock">
{{ destroyClock ? "加载时钟" : "销毁时钟" }}
</button>
<Clock v-if="!destroyClock" />
</a-tab-pane>
<a-tab-pane key="Functional" tab="函数式组件">
<Functional :name="name" />
<TempVar
:var1="`hello ${name}`"
:var2="destroyClock ? 'hello vue' : 'hello world'"
>
<template v-slot="{ var1, var2 }">
{{ var1 }}
{{ var2 }}
</template>
</TempVar>
</a-tab-pane>
</a-tabs>
</div>
</template>
<script>
import Clock from "./Clock";
import Functional from "./Functional";
import TempVar from "./TempVar";
export default {
components: {
Clock,
Functional,
TempVar
},
data() {
return {
destroyClock: false,
name: "vue"
};
}
};
</script>
我们在index文件中使用作用域插槽,能够拿到var1和var2,并作为变量在下面的内容中进行使用。
思考:设计一个秒杀倒计时组件(尤其是在电商公司面试时候会有)
指令
vue提供了14种内置指令:
v-cloack在当前的单文件页面中不起作用 自定义指令:
看下示例:
<template>
<div>
<button @click="show = !show">
销毁
</button>
<button v-if="show" v-append-text="`hello ${number}`" @click="number++">
按钮
</button>
</div>
</template>
<script>
export default {
directives: {
appendText: {
bind() {
console.log("bind");
},
inserted(el, binding) {
el.appendChild(document.createTextNode(binding.value));
console.log("inserted", el, binding);
},
update() {
console.log("update");
},
componentUpdated(el, binding) {
el.removeChild(el.childNodes[el.childNodes.length - 1]);
el.appendChild(document.createTextNode(binding.value));
console.log("componentUpdated");
},
unbind() {
console.log("unbind");
}
}
},
data() {
return {
number: 1,
show: true
};
}
};
</script>
这个append-text指令实现的是往当前的结点文本内容之后插入内容的指令,看下运行效果图:
初始化时触发bind和insert,点击按钮触发update和componentUpdated。
Vue中v-if和v-show的使用场景(面试题)
思考:查看组件生命周期和指令周期钩子的运行顺序
provide/inject
provider和inject是为了解决组件通信时层层需要传递的问题。 我们看下示例:
上图展示了各个节点之间的层级,我们按照这个层级书写代码:
看下A节点的代码:
<template>
<div class="border">
<h1>A 结点</h1>
<button @click="() => changeColor()">改变color</button>
<ChildrenB />
<ChildrenC />
<ChildrenD />
</div>
</template>
<script>
import ChildrenB from "./ChildrenB";
import ChildrenC from "./ChildrenC";
import ChildrenD from "./ChildrenD";
export default {
components: {
ChildrenB,
ChildrenC,
ChildrenD
},
provide() {
return {
theme: {
color: this.color
}
};
},
// provide() {
// return {
// theme: this
// };
// },
data() {
return {
color: "blue"
};
},
methods: {
changeColor(color) {
if (color) {
this.color = color;
} else {
this.color = this.color === "blue" ? "red" : "blue";
}
}
}
};
</script>
A节点通过provide提供了一个theme属性。
E节点代码:
<template>
<div class="border2">
<h3 :style="{ color: theme.color }">E 结点</h3>
<button @click="handleClick">改变color为green</button>
</div>
</template>
<script>
export default {
components: {},
inject: {
theme: {
default: () => ({})
}
},
methods: {
handleClick() {
if (this.theme.changeColor) {
this.theme.changeColor("green");
}
}
}
};
</script>
E节点通过inject注入theme。
F节点(运用别名):
<template>
<div class="border2">
<h3 :style="{ color: theme1.color }">F 结点</h3>
</div>
</template>
<script>
export default {
components: {},
inject: {
theme1: {
from: "theme",
default: () => ({})
}
}
};
</script>
I节点(函数式组件):
<template functional>
<div class="border2">
<h3 :style="{ color: injections.theme.color }">I 结点</h3>
</div>
</template>
<script>
export default {
inject: {
theme: {
default: () => ({})
}
}
};
</script>
跨层级组件实例
方案:封装了一个组件,使用回调,缓存机制
我们可以通过ref获取组件实例:
但是当我们层级多的时候就显得不合适了,递归查找的代码会繁琐,性能会低效。我们看一下callback ref的概念:
思考:v-ant-ref指令回调中能否对更改响应式数据?为什么?更改了会发生什么事情?
如果e节点更新以后能够调用a节点的钩子函数,主动通知a节点,那么层级的调用就显得容易很多了。 看一下实现的形式,依旧是沿用上一次的节点结构:
<template>
<div class="border">
<h1>A 结点</h1>
<button @click="getEH3Ref">获取E h3 Ref</button>
<ChildrenB />
<ChildrenC />
<ChildrenD />
</div>
</template>
<script>
import ChildrenB from "./ChildrenB";
import ChildrenC from "./ChildrenC";
import ChildrenD from "./ChildrenD";
export default {
components: {
ChildrenB,
ChildrenC,
ChildrenD
},
provide() {
return {
setChildrenRef: (name, ref) => {
this[name] = ref;
},
getChildrenRef: name => {
return this[name];
},
getRef: () => {
return this;
}
};
},
data() {
return {
color: "blue"
};
},
methods: {
getEH3Ref() {
console.log(this.childrenE);
}
}
};
</script>
A节点通过provide提供主动获取通知的钩子函数。
子节点D:
<template>
<div class="border1">
<h2>D 结点</h2>
<ChildrenG />
<ChildrenH v-ant-ref="c => setChildrenRef('childrenH', c)" />
<ChildrenI />
</div>
</template>
<script>
import ChildrenG from "./ChildrenG";
import ChildrenH from "./ChildrenH";
import ChildrenI from "./ChildrenI";
export default {
components: {
ChildrenG,
ChildrenH,
ChildrenI
},
inject: {
setChildrenRef: {
default: () => {}
}
}
};
</script>
Vuex
Why Vuex
provide和inject虽然能够实现层层传递的数据管理,但对于一个大的管理系统而言会显得有些繁琐,我们需要一个大型的状态管理系统。
如果没有异步操作,是可以直接commit到mutation的
思考:vuex是通过什么方式提供响应式数据的? 答:new Vue({})
How Vuex
一个简单的计数器的例子,在main.js中引入Vuex
import Vue from 'vue'
import Vuex from 'vuex'
import App from './App.vue'
Vue.use(Vuex)
Vue.config.productionTip = false
const store = new Vuex.Store({
state: {
count: 0,
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
increment({commit}) {
setTimeout(()=>{
// state.count++ // 不要对state进行更改操作,应该通过commit交给mutations去处理
commit('increment')
}, 3000)
}
},
getters: {
doubleCount(state) {
return state.count * 2
}
}
})
new Vue({
store,
render: h => h(App),
}).$mount('#app')
APP.vue中, $store.dispatch对应的是action的定义,$store.commit对应的是mutations 的定义:
<template>
<div id="app">
{{count}}
<br>
{{$store.getters.doubleCount}}
<button @click="$store.commit('increment')">count++</button>
<button @click="$store.dispatch('increment')">count++</button>
</div>
</template>
<script>
export default {
name: 'app',
computed: {
count() {
return this.$store.state.count
}
}
}
</script>
<style>
</style>
$store 是如何是如何挂载到实例 this 上的?
通过new Vue中放置store
Vuex的核心概念和底层原理
思考:扩展简化版的min-vuex,实现getters,并实现vuex的方式注入$store
答案:
计算属性computed实现getters缓存
before中混入$router的获取方式
min-vuex实例:我们尝试实现一个简单的min-vuex:
import Vue from 'vue'
const Store = function Store (options = {}) {
const {state = {}, mutations={}} = options
this._vm = new Vue({
data: {
$$state: state
},
})
this._mutations = mutations
}
Store.prototype.commit = function(type, payload){
if(this._mutations[type]) {
this._mutations[type](this.state, payload)
}
}
Object.defineProperties(Store.prototype, {
state: {
get: function(){
return this._vm._data.$$state
}
}
});
export default {Store}
在这个简易的min-vuex中,我们定义了state、mutation、commit、get等属性,可以将上述的case中的vuex替换为我们的min-vuex,计时器也是可以跑起来的。
main.js文件
import Vue from 'vue'
import Vuex from './min-vuex'
import App from './App.vue'
Vue.use(Vuex)
Vue.config.productionTip = false
const store = new Vuex.Store({
state: {
count: 0,
},
mutations: {
increment(state) {
state.count++
}
},
// actions: {
// increment({commit}) {
// setTimeout(()=>{
// // state.count++ // 不要对state进行更改操作,应该通过commit交给mutations去处理
// commit('increment')
// }, 3000)
// }
// },
// getters: {
// doubleCount(state) {
// return state.count * 2
// }
// }
})
Vue.prototype.$store = store
new Vue({
// store,
render: h => h(App),
}).$mount('#app')
App.vue文件
<template>
<div id="app">
{{count}}
<button @click="$store.commit('increment')">count++</button>
</div>
</template>
<script>
export default {
name: 'app',
computed: {
count() {
return this.$store.state.count
}
}
}
</script>
<style>
</style>
扩展简化版的 min-vuex,实现 getters,并实现 Vuex 的方式注入$store
-
计算属性computer实现getters缓存
-
beforeCreate中混入$store的获取方式
看一下扩展后的min-vuex
let Vue;
function install (_Vue) {
Vue = _Vue;
function vuexInit () {
var options = this.$options;
// store injection
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store;
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store;
}
}
Vue.mixin({ beforeCreate: vuexInit });
}
const Store = function Store (options = {}) {
const {state = {}, mutations={}, getters={}} = options
const computed = {}
const store = this
store.getters = {};
for (let [key, fn] of Object.entries(getters)) {
computed[key] = function () { return fn(store.state, store.getters); };
Object.defineProperty(store.getters, key, {
get: function () { return store._vm[key]; },
});
}
this._vm = new Vue({
data: {
$$state: state
},
computed,
})
this._mutations = mutations
}
Store.prototype.commit = function(type, payload){
if(this._mutations[type]) {
this._mutations[type](this.state, payload)
}
}
Object.defineProperties(Store.prototype, {
state: {
get: function(){
return this._vm._data.$$state
}
}
});
export default {Store, install}
Vuex的最佳实践
前面提到的五个核心概念的取值,vuex提供了很多简写的方式:
我们可以用常量代替Mutation事件类型:
Module:
- 开启命名空间 namespaced: true
- 嵌套模块不要过深,尽量扁平化
- 灵活应用 createNamespacedHelpers
购物车示例
看下代码运行页面:
示例的逻辑还是比较清晰的,我们可以将产品添加到清单中,添加完产品可以将清单提交到后台去购买,我们看一下我们的代码逻辑。
入口文件main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false
new Vue({
store,
render: h => h(App),
}).$mount('#app')
入口文件还是一样,只是将store相关的数据都放在了store文件中,入口文件没有什么特别之处,我们接下来看一下App.vue
<template>
<div id="app">
<h1>购物车示例</h1>
<p>账号: {{email}}</p>
<hr>
<h2>产品</h2>
<ProductList/>
<hr>
<ShoppingCart/>
</div>
</template>
<script>
import { mapState } from 'vuex'
import ProductList from './components/ProductList.vue'
import ShoppingCart from './components/ShoppingCart.vue'
export default {
computed: mapState({
email: state => state.userInfo.email
}),
components: { ProductList, ShoppingCart },
}
</script>
这个页面展现了页面的主题结构,有购物车示例、账号等信息,我们来看下ProductList 和ShoppingCart
ProductList.vue
<template>
<ul>
<li
v-for="product in products"
:key="product.id">
{{ product.title }} - {{ product.price }}
<br>
<button
:disabled="!product.inventory"
@click="addProductToCart(product)">
加入购物车
</button>
</li>
</ul>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
computed: mapState({
products: state => state.products.all,
}),
// computed: {
// products(){
// return this.$store.state.products.all
// }
// },
methods: mapActions('cart', [
'addProductToCart'
]),
// methods: {
// addProductToCart(product){
// this.$store.dispatch('cart/addProductToCart', product)
// }
// },
created () {
this.$store.dispatch('products/getAllProducts')
}
}
</script>
注意:上述注释的代码等价于未注释的代码。
ShoppingCart.Vue
<template>
<div class="cart">
<h2>清单</h2>
<p v-show="!products.length"><i>请添加产品到购物车</i></p>
<ul>
<li
v-for="product in products"
:key="product.id">
{{ product.title }} - {{ product.price }} x {{ product.quantity }}
</li>
</ul>
<p>合计: {{ total }}</p>
<p><button :disabled="!products.length" @click="checkout(products)">提交</button></p>
<p v-show="checkoutStatus">提交 {{ checkoutStatus }}.</p>
</div>
</template>
<script>
import { mapGetters, mapState } from 'vuex'
export default {
computed: {
...mapState({
checkoutStatus: state => state.cart.checkoutStatus
}),
...mapGetters('cart', {
products: 'cartProducts',
total: 'cartTotalPrice'
}),
// ...mapGetters({
// products: 'cart/cartProducts',
// total: 'cart/cartTotalPrice'
// })
},
// computed: {
// checkoutStatus(){
// return this.$store.state.cart.checkoutStatus
// },
// products() {
// return this.$store.getters['cart/cartProducts']
// },
// total() {
// return this.$store.getters['cart/cartTotalPrice']
// }
// },
methods: {
checkout (products) {
this.$store.dispatch('cart/checkout', products)
}
},
}
</script>
我们来看一下store的内容:
index.js文件中:
import Vue from 'vue'
import Vuex from 'vuex'
import cart from './modules/cart'
import products from './modules/products'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
userInfo: {
email: "xxxxxx@qq.com"
}
},
modules: {
cart,
products
},
})
在这里我们把cart和products模块引入注册到modules中。
看一下modules/products.js模块
import shop from '../../api/shop'
import {PRODUCTS} from '../mutation-types'
// initial state
const state = {
all: []
}
// getters
const getters = {}
// actions
const actions = {
getAllProducts ({ commit }) {
shop.getProducts(products => {
commit(PRODUCTS.SET_PRODUCTS, products)
})
}
}
// mutations
const mutations = {
[PRODUCTS.SET_PRODUCTS] (state, products) {
state.all = products
},
[PRODUCTS.DECREMENT_PRODUCT_INVENTORY] (state, { id }) {
const product = state.all.find(product => product.id === id)
product.inventory--
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
其中shop就是去模拟一下ajax请求:
/**
* Mocking client-server processing
*/
const _products = [ {"id": 1, "title": "华为 Mate 20", "price": 3999, "inventory": 2}, {"id": 2, "title": "小米 9", "price": 2999, "inventory": 0}, {"id": 3, "title": "OPPO R17", "price": 2999, "inventory": 5}]
export default {
getProducts (cb) {
setTimeout(() => cb(_products), 100)
},
buyProducts (products, cb, errorCb) {
setTimeout(() => {
// simulate random checkout failure.
Math.random() > 0.5
? cb()
: errorCb()
}, 100)
}
}
cart的代码会稍微复杂一点:
import shop from '../../api/shop'
import { CART, PRODUCTS } from '../mutation-types'
// initial state
// shape: [{ id, quantity }]
const state = {
items: [],
checkoutStatus: null
}
// getters
const getters = {
cartProducts: (state, getters, rootState) => {
return state.items.map(({ id, quantity }) => {
const product = rootState.products.all.find(product => product.id === id)
return {
title: product.title,
price: product.price,
quantity
}
})
},
cartTotalPrice: (state, getters) => {
return getters.cartProducts.reduce((total, product) => {
return total + product.price * product.quantity
}, 0)
}
}
// actions
const actions = {
checkout ({ commit, state }, products) {
const savedCartItems = [...state.items]
//先将购物车的状态置为空
commit(CART.SET_CHECKOUT_STATUS, null)
// empty cart
commit(CART.SET_CART_ITEMS, { items: [] })
shop.buyProducts(
products,
() => commit(CART.SET_CHECKOUT_STATUS, 'successful'),
() => {
commit(CART.SET_CHECKOUT_STATUS, 'failed')
// rollback to the cart saved before sending the request
commit(CART.SET_CART_ITEMS, { items: savedCartItems })
}
)
},
addProductToCart ({ state, commit }, product) {
commit(CART.SET_CHECKOUT_STATUS, null)
if (product.inventory > 0) {
const cartItem = state.items.find(item => item.id === product.id)
if (!cartItem) {
commit(CART.PUSH_PRODUCT_TO_CART, { id: product.id })
} else {
commit(CART.INCREMENT_ITEM_QUANTITY, cartItem)
}
// remove 1 item from stock
commit(`products/${PRODUCTS.DECREMENT_PRODUCT_INVENTORY}`, { id: product.id }, { root: true })
}
}
}
// mutations
const mutations = {
[CART.PUSH_PRODUCT_TO_CART] (state, { id }) {
state.items.push({
id,
quantity: 1
})
},
[CART.INCREMENT_ITEM_QUANTITY] (state, { id }) {
const cartItem = state.items.find(item => item.id === id)
cartItem.quantity++
},
[CART.SET_CART_ITEMS] (state, { items }) {
state.items = items
},
[CART.SET_CHECKOUT_STATUS] (state, status) {
state.checkoutStatus = status
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
扩展购物车示例,提供单次添加 1-N 的数量到购物车的功能: github.com/geektime-ge…
Vue Router
原理解析:1.vue Router是如何响应路由变化的? 2.next如何做服务端渲染的?
Why Vue Router
传统开发模式下,每个url都对应着一个html页面,每次切换url的时候会引起页面的重新加载,在这种情况下诞生了单页面(spa)开发模式,用户在切换url的时候不在是执行页面的变化,而是根据我们的逻辑进行执行,返回数据。
看一下Vue Router解决的问题:
- 监听 URL 的变化,并在变化前后执行相应的逻辑
- 不同的 URL 对应不同的不同的组件
- 提供多种方式改变 URL 的 API(URL 的改变不能导致浏览器刷新)
它的使用方式:
- 提供一个路由配置表,不同 URL 对应不同组件的配置
- 初始化路由实例 new VueRouter()
- 挂载到 Vue 实例上
- 提供一个路由占位,用来挂载 URL 匹配到的组件
看一下Vue Router的使用实例:
在main.js中引入路由
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App.vue'
import routes from './routes'
Vue.config.productionTip = false
Vue.use(VueRouter)
const router = new VueRouter({
mode: 'history',
routes,
})
new Vue({
router,
render: h => h(App),
}).$mount('#app')
在main.js中完成路由的注册,接下来看App.vue这个文件:
<template>
<div id="app">
<h2>router demo</h2>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'app',
components: {
},
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
在里面加入了<router-view></router-view>组件。
在路由配置列表中则进行如下配置:
import RouterDemo from './components/RouterDemo'
import RouterChildrenDemo from './components/RouterChildrenDemo'
const routes = [
{ path: '/foo', component: RouterDemo, name: '1' },
{ path: '/bar', component: RouterDemo, name: '2' },
// 当 /user/:id 匹配成功,
// RouterDemo 会被渲染在 App 的 <router-view /> 中
{ path: '/user/:id',
component: RouterDemo,
name: '3',
props: true,
children: [
{
// 当 /user/:id/profile 匹配成功,
// RouterChildrenDemo 会被渲染在 RouterDemo 的 <router-view/> 中
path: 'profile',
component: RouterChildrenDemo,
name: '3-1'
},
{
// 当 /user/:id/posts 匹配成功
// RouterChildrenDemo 会被渲染在 RouterDemo 的 <router-view/> 中
path: 'posts',
component: RouterChildrenDemo
}
]
},
{ path: '/a', redirect: '/bar' },
{ path: '*', component: RouterDemo, name: '404' }
]
export default routes
SPA 的缺点有哪些,如何解决?
缺点:
- 不利于SEO
- 首屏渲染时间长
解决:
-
首屏渲染
-
ssr
路由类型及底层原理
路由类型:
- hash模式 丑,无法使用锚点定位 hashchange
- History模式 需要后端配合,IE9不兼容(可使用强制刷新处理) 我们只需要在声明router的时候把mode改成history的模式就可以了。
history.pushState API
看一下路由的底层原理图:
路由通过Vue.util.defineReactive_route 这样一个api把router的信息变为一个响应式的,我们通过router-link, $router.push, a href, 浏览器的前进后退以及手动更改URL来触发updateRouter方法,由updateRouter来改变响应式数据,updateRouter触发以后再来更改我们的router-view的更新。 对于动态内容,如果不使用SSR,如何做SEO
- 使用无头浏览器(phantomjs、headlessChrome)——效率比较低
Nuxt
Nuxt解决的问题
我们都知道spa单页面的缺点有以下几种:
- 不利于SEO(搜索引擎爬取单页面是没有内容的,它不会出现再搜索的结果中)
- 首屏渲染时间长
针对这两个缺点,我们会有一些方案来解决:
- 服务端渲染SSR
- 预渲染Prerendering
Prerendering预渲染
- 主要适用于静态站点
SSR
- 动态渲染
- 配置繁琐
那么针对这些问题Nuxt就是去做这些操作
- 静态站点
- 动态渲染
- 简化配置
思考:对于动态内容,如果不使用SSR,如何做SEO?
答:使用无头浏览器(phantomjs、headlessChrome)
Nuxt的核心原理
UI组件库对比
常用开发工具
Vetur
- 语法高亮
- 标签补全、模板生成(快速生成template)
- Lint 检查
- 格式化
ESLint
- 代码规范
- 错误检查
对后期维护非常有利。
Prettier
- 格式化
Vue DevTools
- 集成Vuex
- 可远程调试
- 性能分析
单元测试
使用方式:
-
jest或mocha
-
@vue/test-utils
-
sinon
看一下单测jest的配置(jest.config.js )
module.exports = {
moduleFileExtensions: ["js", "jsx", "json", "vue"],
transform: {
"^.+\\.vue$": "vue-jest",
".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$":
"jest-transform-stub",
"^.+\\.jsx?$": "babel-jest"
},
transformIgnorePatterns: ["/node_modules/"],
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1"
},
snapshotSerializers: ["jest-serializer-vue"],
testMatch: [
"**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"
],
testURL: "http://localhost/"
};
transform中配置不同的类型处理不同的文件
moduleNameMapper 指定快捷路径
snapshotSerializers 用来做快照的格式化
testMatch 来匹配那些需要做单元测试
testURL 是给jest去使用的
书写单测代码:
import { mount } from "@vue/test-utils";
import HelloWorld from "@/components/HelloWorld.vue";
describe("HelloWorld.vue", () => {
it("renders props.msg when passed", () => {
const msg = "new message";
const wrapper = mount(HelloWorld, {
propsData: { msg }
});
expect(wrapper.text()).toMatch(msg);
});
});
expect是一个断言,我希望wrapper.text()和msg是匹配的。
看一个复杂一点的单元测试,这是一个计数器的单元测试:
import { mount } from "@vue/test-utils";
import Counter from "@/components/Counter.vue";
import sinon from "sinon";
describe("Counter.vue", () => {
const change = sinon.spy();
const wrapper = mount(Counter, {
listeners: {
change
}
});
it("renders counter html", () => {
expect(wrapper.html()).toMatchSnapshot();
});
it("count++", () => {
const button = wrapper.find("button");
button.trigger("click");
expect(wrapper.vm.count).toBe(1);
expect(change.called).toBe(true);
button.trigger("click");
expect(change.callCount).toBe(2);
});
});