Vue两大核心思想:组件化和数据驱动,组件可重复使用,数据驱动让DOM随着数据的变化自然而然的变化。

整体结构

Vue 运行流程和结构

image-20190306154013538

index.html –> main.js –> App.vue –> router/index.js

1.index.html (主页)

index.html如其他html一样,但一般只定义一个空的根节点,在main.js里面定义的实例将挂载在根节点下,内容都通过vue组件来填充

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>vue-test</title>
</head>
<body>
<div id="app">
<router-view></router-view>
</div>
</body>
</html>

2.main.js(入口文件)

main.js主要是引入vue框架,根组件及路由设置,并且定义vue实例

1
2
3
4
5
6
7
8
9
10
11
12
import Vue from 'vue'
import App from './App'
import router from './router'

Vue.config.productionTip = false

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

3.App.vue(根组件)

一个vue页面通常由三部分组成:模板(template)、js(script)、样式(style),样式通过style标签包裹,默认是影响全局的,如需定义作用域只在该组件下起作用,需在标签上加scoped,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div id="app">
<img src="./assets/logo.png">
<router-view></router-view>
</div>
</template>

<script>
export default {
name: 'App'
}
</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>

4.router–index.js(路由组件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div id="app">
<img src="./assets/logo.png">
<router-view/>
</div>
</template>

<script>
export default {
name: 'App'
}
</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>

vue-router

vue-router是vue的核心插件,使用vue-router,我们可以将组件映射到路由,然后告诉vue-router在哪里渲染它们

基础语法

模板语法

数据绑定

文本数据插入

内容跟数据同步更新

1
<span>{{msg}}</span>

通过v-once指令实现首次加载,内容不再同步;

1
`<span v-once>这个将不会改变: {{ msg }}</span>`
html代码插入 v-html

慎用。

1
<p>Using v-html directive: <span v-html="rawHtml"></span></p>

标签属性绑定 v-bind:

1
<div v-bind:id="dynamicId"></div>

缩写:

1
<div :id="dynamicId"></div>

指令标签属性

v-if

根据布尔值控制内容显示

1
`<p v-if="seen">现在你看到我了</p>`
v-on

监听DOM事件,:click为指令参数,表示事件名为click

1
`<a v-on:click="doSomething">...</a>`

缩写:

1
<a @click="doSomething">...</a>

计算属性 VS 方法

计算属性 computed

计算属性会缓存之前的计算结果,只有依赖的数据改变时才会重新计算,有利于减少不必要的操作;同时当数据变化时,计算属性也会同步更新。

1
2
3
4
<div id="example">
<p>Original message: "{{ message }}"</p>
<p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
1
2
3
4
5
6
7
8
9
10
11
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: { // 计算属性的 getter
reversedMessage: function () {
return this.message.split('').reverse().join('') // `this` 指向 vm 实例
}
}
})

但是,如果是return Date.()之类非响应式依赖,就不会更新了。

方法 methods

每次访问都会再次执行函数:

1
2
3
4
5
6
// 在组件中
methods: {
reversedMessage: function () {
return this.message.split('').reverse().join('')
}
}

侦听器 watch

当数据变化时触发函数

1
2
3
4
5
6
7
<div id="watch-example">
<p>
Ask a yes/no question:
<input v-model="question">
</p>
<p>{{ answer }}</p>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
<script>
var watchExampleVM = new Vue({
el: '#watch-example',
data: {
question: '',
answer: 'I cannot give you an answer until you ask a question!'
},
watch: { // 如果 `question` 发生改变,这个函数就会运行
question: function (newQuestion, oldQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.debouncedGetAnswer()
}
},
created: function () {
this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
//_.debounce 是通过 Lodash 限制操作频率的函数。我们希望限制访问 yesno.wtf/api 的频率,AJAX 请求直到用户输入完毕才会发出。参考:https://lodash.com/docs#debounce
},
methods: {
getAnswer: function () {
if (this.question.indexOf('?') === -1) {
this.answer = 'Questions usually contain a question mark. ;-)'
return
}
this.answer = 'Thinking...'
var vm = this
axios.get('https://yesno.wtf/api')
.then(function (response) {
vm.answer = _.capitalize(response.data.answer)
})
.catch(function (error) {
vm.answer = 'Error! Could not reach the API. ' + error
})
}
}
})
</script>

条件渲染

v-if

1
2
3
4
5
6
7
8
9
10
11
12
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>

通过key管理复用元素

下面每次切换时,输入框都将被重新渲染,如果没有key,则切换时输入框会保留之前的内容;

1
`<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>`

v-show

不管初始条件是什么,元素总是会被渲染,只是简单地切换元素的 CSS 属性 display

1
<h1 v-show="ok">Hello!</h1>

v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show较好;如果在运行时条件很少改变,则使用 v-if 较好。

列表渲染 v-for

数组循环

1
2
3
4
5
<ul id="example-2">
<li v-for="(item, index) in items"> //index 参数可选
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
</ul>
1
2
3
4
5
6
7
8
9
10
var example2 = new Vue({
el: '#example-2',
data: {
parentMessage: 'Parent',
items: [
{ message: 'Foo' },
{ message: 'Bar' }
]
}
})
1
2
3
<div v-for="item in items" :key="item.id">
<!-- 内容 -->
</div>

注意:Vue 不能检测数组的以下变动:

  1. 使用索引修改值,例如:vm.items[indexOfItem] = newValue
  2. 修改数组的长度时,例如:vm.items.length = newLength

可用以下方法实现:

1
`// Vue.setVue.set(vm.items, indexOfItem, newValue)`
变异方法

这些方法会触发视图更新:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

非变异 (non-mutating method) 方法,例如:filter(), concat()slice() 。这些不会改变原始数组,但总是返回一个新数组

对象循环

1
2
3
4
5
6
7
8
9
10
11
<div v-for="(value, key, index) in object" :key="item.id"> //建议使用 v-for 时提供key,以便它能跟踪每个节点的身份
{{ index }}. {{ key }}: {{ value }}
</div>

//利用带有 v-for 的 <template> 渲染多个元素
<ul>
<template v-for="item in items" :key="item.id">
<li>{{ item.msg }}</li>
<li class="divider" role="presentation"></li>
</template>
</ul>
1
2
3
4
5
6
7
8
9
10
new Vue({
el: '#v-for-object',
data: {
object: {
firstName: 'John',
lastName: 'Doe',
age: 30
}
}
})

注意:Vue 不能检测对象属性的添加或删除,例如添加属性:vm.obj.id=1。可用以下方法实现:

1
`Vue.set(vm.userProfile, 'age', 27)`

Vue.nextTick 的原理

在Vue中数据的修改并不会立即更新到DOM视图上,而是等当前所有的同步代码执行完后,才会执行异步队列,DOM的更新就属于异步操作。因此当数据改变后,需要立即操作或获取最新DOM的数据,就必须在DOM更新后进行操作,而Vue.nexTick的作用就是把其中的代码放到DOM更新操作的后面执行

知乎上的例子:

1
2
3
4
5
6
7
8
9
10
//改变数据
vm.message = 'changed'

//想要立即使用更新后的DOM。这样不行,因为设置message后DOM还没有更新
console.log(vm.$el.textContent) // 并不会得到'changed'

//这样可以,nextTick里面的代码会在DOM更新后执行
Vue.nextTick(function(){
console.log(vm.$el.textContent) //可以得到'changed'
})

img

组件通讯

父->子组件间的数据传递

prop

子->父组件间的数据传递

$emit

子组件中用$emit自定义事件,在父组件中监听这个事件并执行回调函数

1
2
3
4
// 父组件监听子组件定义的事件
<editor :inputIndex="index" @editorEmit='editorEmit'></editor>
// 子组件需要返回数据时执行,并可以传递数据
this.$emit('editorEmit', data)
$refs

调用子组件实例中数据,或函数(将父组件的数据传给子组件中的函数)

1
2
3
4
5
6
7
8
methods: {
delCtag() {
let currentData = this.$refs.tree.getCurrentNode()
let parentData = this.$refs.tree.getNode(currentData).parent.data
this.$refs.tree.remove(currentData) // 删除标签
this.$refs.tree.setCurrentKey(parentData.id) // 删除后选中父标签
}
}

兄弟组件间的数据传递

Bus通信

建立一个Vue对象做为通信的媒介

1
2
3
4
//main.js
let bus = new Vue()
Vue.prototype.$bus = bus
bus.$emit('reload-file', 'from dad')
1
2
3
4
5
6
7
8
9
//任意子孙组件中
mounted: function() {
let that = this
this.$bus.$on('reload-file', function(val) {
console.log('reload-file: ' + val)
that.currentChange(that.currentCtag) //调用当前组件的方法和数据
})
}
//结果:reload-file: from dad

也可以在组件之外定义一个bus.js作为组件间通信的桥梁,适用于比较小型不需要vuex的项目

参考:http://hjaiim.github.io/2018/11/06/vue-eventBus/

Vuex集中状态管理

vuex最简单、最详细的入门文档

组件深层嵌套,祖先组件与子组件间的数据传递

provide/inject
$attrs

父组件传向子组件传的,子组件不prop接受的数据都会放在$attrs中,子组件直接用this.$attrs获取

在子组件中添加v-bind=’$attrs’

$children/$parent

进阶

前端路由

路由,引导、指路之意;

前端路由让客户端浏览器可以不依赖服务端,根据不同的URL渲染不同的视图页面。

单页应用的概念是伴随着 MVVM 出现,单页应用不仅仅是在页面交互是无刷新的,连页面跳转都是无刷新的,为了实现单页应用,所以就有了前端路由。就是一个网站只有一个html页面,但是点击不同的导航显示不同的内容,对应的url也会发生变化,这就是前端路由做的事。

路由就是用来跟后端服务器进行交互的一种方式,通过不同的路径,来请求不同的资源,请求不同的页面是路由的其中一种功能。

前端路由的实现本质上是检测 url 的变化,截获 url 地址,然后解析来匹配路由规则。

服务器端路由的好处是安全性更高,但会增加了服务器端的负荷;前端是毫无安全性可言的。用户可以肆意修改代码来进入不同的流程。

参考

1前端路由的前生今世及实现原理

0前端路由一探

数组元素赋值状态不更新

Vue之所以能够监听Model状态的变化,是因为JavaScript语言本身提供了Proxy或者Object.observe()机制来监听对象状态的变化。但是,对于数组元素的赋值,却没有办法直接监听,因此,如果我们直接对数组元素赋值:

1
2
3
4
vm.todos[0] = {
name: 'New name',
description: 'New description'
};

会导致Vue无法更新View。

正确的方法是不要对数组元素赋值,而是更新:

1
2
vm.todos[0].name = 'New name';
vm.todos[0].description = 'New description';

或者,通过splice()方法,删除某个元素后,再添加一个元素,达到“赋值”的效果:

1
2
3
var index = 0;
var newElement = {...};
vm.todos.splice(index, 1, newElement);

Vue可以监听数组的splicepushunshift等方法调用,所以,上述代码可以正确更新View。

vue-cli 2

在终端进入对应项目目录,执行构建

1
2
3
4
5
6
7
8
9
10
11
12
13
# 如果你没有vue-cli的话需要全局安装
npm install -g vue-cli
# 然后使用vue-cli来安装electron-vue的模板
vue init simulatedgreg/electron-vue my-project

# 安装依赖
cd my-project
yarn # or npm install
# 进入开发模式
yarn run dev # or npm run dev

# 强制退出开发模式
ctrl+c

vue cli 3

选项

PWA:渐进式网页应用,具有native 体验,即时更新,链接访问无需安装、可被搜索引擎检索、可离线使用、快速响应、自适应机型、可放到桌面,安全,支持推送通知;

dart-sass vs node-sass:dart-sass为最新重写版本,处理速度更快,功能更新更容易,为后续主推版本;

vue-router:默认hash模式,URL改变时页面不会重新加载(选择),history模式则会重新加载;

vue-loader

vue-loader 是一个webpack的loader;可以将vue文件转换为JS模块;

特性

  1. 支持ES2015
  2. 允许对VUE组件的组成部分使用其他webpack loader,比如对< style >使用SASS(编译CSS语言),对< template >使用JADE(jade是一个高性能的模板引擎,用JS实现,也有其他语言的实现—php,scala,yuby,python,java,可以供给node使用)
  3. .vue文件中允许自定义节点,然后使用自定义的loader处理他们
  4. 对< style >< template >中的静态资源当做模块来对待,并且使用webpack loaders进行处理
  5. 对每个组件模拟出CSS作用域
  6. 支持开发期组件的热重载

使用预处理器SCSS

安装:

1
npm install -D sass-loader node-sass

在 webpack 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
module: {
rules: [
// ... 忽略其它规则

// 普通的 `.scss` 文件和 `*.vue` 文件中的
// `<style lang="scss">` 块都应用它
{
test: /\.scss$/,
use: [
'vue-style-loader',
'css-loader',
'sass-loader'
]
}
]
},
// 插件忽略
}
共享全局变量

sass-loader 也支持一个 data 选项,这个选项允许你在所有被处理的文件之间共享常见的变量,而不需要显式地导入它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack.config.js -> module.rules
{
test: /\.scss$/,
use: [
'vue-style-loader',
'css-loader',
{
loader: 'sass-loader',
options: {
// 你也可以从一个文件读取,例如 `variables.scss`
data: `$color: red;`
}
}
]
}

Mixin

Mixin允许你封装一块在不同组件中复用的代码,可以更加dry (don’t repeat yourself)

1
2
3
4
5
6
7
8
9
10
11
12
13
//文件:./mixins/toggle.js 中定义共用的代码
export const toggle = {
data() {
return {
isshowing: false
}
},
methods: {
toggleShow() {
this.isShowing = !this.isShowing
}
}
}
1
2
3
4
5
6
7
8
9
10
11
//组件 Modal.vue中引入
import Child from './Child'
import { toggle } from './mixins/toggle'

export default {
name: 'modal',
mixins: [toggle],
components: {
appChild: Child
}
}

生命周期钩子合并

Mixin中的生命周期的钩子也同样是可用的,当组件与Mixin有相同的生命周期钩子,默认Mixin上会首先被注册,组件上的接着注册,这样我们就可以在组件中按需要重写Mixin中的语句,再调用。组件拥有最终发言权。

自定义指令

拖动边框调大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
<div>
<div class="box-wrap" ref="dialog__wrapper">
<div class="line" v-dialogDragWidth="$refs"></div>
</div>
</div>
</template>

<script>
export default {
data() {
return {}
},
methods: {},
mounted() {}
}
</script>

<style>
.box-wrap {
position: relative;
top: 10px;
left: 10px;
width: 100px;
height: 100px;
background: #dadada4d;
}
//可拖动区域
.line {
width: 4px;
top: 2px;
right: -1px;
height: calc(100% - 2px);
cursor: e-resize;
border-right: 1px solid #333;
position: absolute;
z-index: 999998;
}
</style>

在main.js中引入以下自定义指令import './directives.js'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//directives.js
import Vue from "vue"

// v-dialogDragWidth: 弹窗宽度拖大 拖小
Vue.directive("dialogDragWidth", {
inserted(el, binding) {
console.log("el", el, binding)
const dragDom = binding.value.dialog__wrapper
console.log('dragDom', dragDom)

el.onmousedown = e => {
// 鼠标按下,计算当前元素距离可视区的距离
console.log(e)
const disX = e.clientX - el.offsetLeft;
console.log('disX', disX)
document.onmousemove = function (e) {
e.preventDefault() // 移动时禁用默认事件

// 通过事件委托,计算移动的距离
const l = e.clientX - disX
console.log('l', l)
dragDom.style.width = `${l}px` //改变对象宽度
}
document.onmouseup = function () {
document.onmousemove = null
document.onmouseup = null
}
}
}
})

技巧

禁止文本选择和右键

document.onselectstart

缺点:鼠标仍是文本选择的光标;

vue和普通的html页面不太一样。vue的生命周期必须加载完才可以操作dom,生命周期这里不再叙述,自行学习,或者也在mounted和created的时候使用this.$nextTick方法,来使方法在dom生成以后再进行操作。

1
2
3
4
5
6
7
8
created() {
this.$nextTick(() => {
// 禁用右键
document.oncontextmenu = new Function('event.returnValue=false')
// 禁用选择
document.onselectstart = new Function('event.returnValue=false')
})
}

通过css实现

1
2
3
4
5
6
7
.el-table__row { 
-moz-user-select: none; /*火狐*/
-webkit-user-select: none; /*webkit浏览器(chrome)*/
-ms-user-select: none; /*IE10*/
-khtml-user-select: none; /*早期浏览器*/
user-select: none;
}

hover

1
2
3
<el-button type="primary" icon="el-icon-search" plain round 
@mouseenter.native="knowledge = '即将推出,敬请期待'"
@mouseleave.native="knowledge = '知识库搜索答案'">{{knowledge}}</el-button>

添加.native修饰符,就可以把事件绑到组件的根元素上

避免input中@keyup.enter和@blur触发两次

1
2
@blur="editDone(row)"
@keydown.enter.native="$event.target.blur"

vue-cli 3使用

create 创建项目报错

1
error: unknown option `--registry'

在~/.vuerc, 将useTaobaoRegistry改为false