1. 搭建工程

vue-cli: https://cli.vuejs.org/zh/

1-1. vue-cli

vue-cli是一个脚手架工具,用于搭建vue工程

它内部使用了webpack,并预置了诸多插件(plugin)和加载器(loader),以达到开箱即用的效果

除了基本的插件和加载器外,vue-cli还预置了:

  • babel
  • webpack-dev-server
  • eslint
  • postcss
  • less-loader

1-2. SFC

单文件组件,Single File Component,即一个文件就包含了一个组件所需的全部代码

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<!-- 组件模板代码 -->
</template>

<script>
export default {
// 组件配置
}
</script>

<style>
/* 组件样式 */
</style>

1-3. 预编译

vue-cli进行打包时,会直接把组件中的模板转换为render函数,这叫做模板预编译

这样做的好处在于:

  1. 运行时就不再需要编译模板了,提高了运行效率
  2. 打包结果中不再需要vue的编译代码,减少了打包体积
    预编译

2. computed(计算属性)

完整的计算属性书写:

1
2
3
4
5
6
7
8
9
10
computed: {
propName: {
get(){
// getter
},
set(val){
// setter
}
}
}

只包含getter的计算属性简写:

1
2
3
4
5
computed: {
propName(){
// getter
}
}

使用方式:

1
2
<!-- 当成数据直接使用,不需要加括号 -->
<div>{{ propName }}</div>

计算属性和方法有什么区别?

1
2
3
4
5
6
7
8
9
10
计算属性本质上是包含getter和setter的方法

当获取计算属性时,实际上是在调用计算属性的getter方法。vue会收集计算属性的依赖,并缓存计算属性的返回结果。只有当依赖变化后才会重新进行计算。
方法没有缓存,每次调用方法都会导致重新执行。

计算属性的getter和setter参数固定,getter没有参数,setter只有一个参数。而方法的参数不限。

由于有以上的这些区别,因此计算属性通常是根据已有数据得到其他数据,并在得到数据的过程中不建议使用异步、当前时间、随机数等副作用操作。

实际上,他们最重要的区别是含义上的区别。计算属性含义上也是一个数据,可以读取也可以赋值;方法含义上是一个操作,用于处理一些事情。

3. props (声明组件的属性、传值)

声明形式:数组和对象

基础形式:数组

1
props: ['title', 'likes', 'isPublished', 'commentIds', 'author']

规定属性类型:

1
2
3
4
5
6
7
8
9
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // or any other constructor
}

Prop 验证:

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
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组默认值必须从一个工厂函数获取
default: function () {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].includes(value)
}
}
}

4. v-if 和 v-show

v-if


v-show

v-if 和 v-show 有什么区别?

1
2
3
4
5
6
7
v-if能够控制是否生成vnode,也就间接控制了是否生成对应的dom。当v-if为true时,会生成对应的vnode,并生成对应的dom元素;当其为false时,不会生成对应的vnode,自然不会生成任何的dom元素。

v-show始终会生成vnode,也就间接导致了始终生成dom。它只是控制dom的display属性,当v-show为true时,不做任何处理;当其为false时,生成的dom的display属性为none。

使用v-if可以有效的减少树的节点和渲染量,但也会导致树的不稳定;而使用v-show可以保持树的稳定,但不能减少树的节点和渲染量。

因此,在实际开发中,显示状态变化频繁的情况下应该使用v-show,以保持树的稳定;显示状态变化较少时应该使用v-if,以减少树的节点和渲染量。

5. 组件事件 (props、event)

组件事件

抛出事件:子组件在某个时候发生了一件事,但自身无法处理,于是通过事件的方式通知父组件处理

事件参数:子组件抛出事件时,传递给父组件的数据

注册事件:父组件申明,当子组件发生某件事的时候,自身将做出一些处理

事件的调用的写法:

  1. @Click=”handleEvent”
    1. 方法参数为默认事件参数
  2. @Click=”handleEvent()”
    1. 参数自定义,如果不传则为空;
    2. 可以通过关键字 $event 表示默认事件参数
    3. 可以自定义参数数量

5-1. v-on 的高阶用法

可以直接将当前组件的父级数据传递给自己的子集(可以实现多个事件传递)

1
2
3
4
5
6
<Ele v-on="{submit: handleSubmit(){}}"></Ele>
//等效于
<Ele v-on:submit="handleSubmit"></Ele>

//推导出
<Ele v-on="$listeners"></Ele>

6. 如何测试组件效果?

https://cli.vuejs.org/zh/guide/prototyping.html

概述:快速原型开发
你可以使用 vue serve 和 vue build 命令对单个 *.vue 文件进行快速原型开发,不过这需要先额外安装一个全局的扩展:

安装:

1
npm install -g @vue/cli-service-global

使用:

1
vue serve ./src/components/Pager/test.vue

进阶:在package.json中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
{
...
"scripts": {
...
"test:Pager": "vue serve ./src/components/Pager/test.vue",
},
"dependencies": {
...
},
"devDependencies": {
...
}
}
1
npm run test:Pager

7. 插槽

问题:在某些组件的模板中,有一部分区域需要父组件来指定

1
2
3
4
5
6
7
8
<!-- message组件:一个弹窗消息 -->
<div class="message-container">
<div class="content">
<!-- 这里是消息内容,可以是一个文本,也可能是一段html,具体是什么不知道,需要父组件指定 -->
</div>
<button>确定</button>
<button>关闭</button>
</div>

7-1. 插槽的简单用法

此时,就需要使用插槽来定制组件的功能

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
<!-- message组件:一个弹窗消息 -->
<div class="message-container">
<div class="content">
<!-- slot是vue的内置组件 -->
<slot></slot>
</div>
<button>确定</button>
<button>关闭</button>
</div>

<!-- 父组件App -->
<Message>
<div class="app-message">
<p>App Message</p>
<a href="">detail</a>
</div>
</Message>

<!-- 最终的结果 -->
<div class="message-container">
<div class="content">
<div class="app-message">
<p>App Message</p>
<a href="">detail</a>
</div>
</div>
<button>确定</button>
<button>关闭</button>
</div>

插槽

7-2. 具名插槽

如果某个组件中需要父元素传递多个区域的内容,也就意味着需要提供多个插槽

为了避免冲突,就需要给不同的插槽赋予不同的名字

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
<!-- Layout 组件 -->
<div class="layout-container">
<header>
<!-- 我们希望把页头放这里,提供插槽,名为header -->
<slot name="header"></slot>
</header>
<main>
<!-- 我们希望把主要内容放这里,提供插槽,名为default -->
<slot></slot>
</main>
<footer>
<!-- 我们希望把页脚放这里,提供插槽,名为footer -->
<slot name="footer"></slot>
</footer>
</div>

<!-- 父组件App -->
<BaseLayout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>

<template v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>

<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</BaseLayout>

v-slot:xxx 可以简写为 #xxx

具名插槽

8. 路由

路由

  1. 如何根据地址中的路径选择不同的组件?
  2. 把选择的组件放到哪个位置?
  3. 如何无刷新的切换组件?

8-1. 路由插件

8-1-1. 安装

1
npm i vue-router

8-1-2. 路由插件的使用

1
2
3
4
5
6
7
8
9
10
11
12
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter); // Vue.use(插件) 在Vue中安装插件

const router = new VueRouter({
// 路由配置
})
new Vue({
...,
router
})

8-1-3. 路由的基本使用

1
2
3
4
5
6
7
8
9
// 路由配置
const router = new VueRouter({
routes: [ // 路由规则
// 当匹配到路径 /foo 时,渲染 Foo 组件
{ path: '/foo', component: Foo },
// 当匹配到路径 /bar 时,渲染 Bar 组件
{ path: '/bar', component: Bar }
]
})
1
2
3
4
5
6
7
8
9
10
11
<!-- App.vue -->
<div class="container">
<div>
<!-- 公共区域 -->
</div>
<div>
<!-- 页面区域 -->
<!-- vue-router 匹配到的组件会渲染到这里 -->
<RouterView />
</div>
</div>

8-2. 路由模式

路由模式决定了:

  1. 路由从哪里获取访问路径
  2. 路由如何改变访问路径

vue-router提供了三种路由模式:

  1. hash:默认值。路由从浏览器地址栏中的hash部分获取路径,改变路径也是改变的hash部分。该模式兼容性最好。

    1
    2
    http://localhost:8081/#/blog  -->  /blog
    http://localhost:8081/about#/blog --> /blog
  2. history:路由从浏览器地址栏的location.pathname中获取路径,改变路径使用的H5的history api。该模式可以让地址栏最友好,但是需要浏览器支持history api

    1
    2
    3
    http://localhost:8081/#/blog  -->  /
    http://localhost:8081/about#/blog --> /about
    http://localhost:8081/blog --> /blog

    history api

    1
    2
    3
    4
    //无刷新切换网页url (H5 API)
    history.pushState(null, null, '/blog');

    //注意:直接使用此api,vue无法监控到
  3. abstract:路由从内存中获取路径,改变路径也只是改动内存中的值。这种模式通常应用到非浏览器环境中。

    1
    2
    3
    内存: /         -->     /
    内存: /about --> /about
    内存: /blog --> /blog

8-3. 导航

vue-router提供了全局的组件RouterLink,它的渲染结果是一个a元素

1
2
3
4
5
6
7
8
<RouterLink to="/blog">文章</RouterLink>

<!-- mode:hash 生成 -->
<a href="#/blog">文章</a>

<!-- mode:history 生成 -->
<!-- 为了避免刷新页面,vue-router实际上为它添加了点击事件,并阻止了默认行为,在事件内部使用hitory api更改路径 -->
<a href="/blog">文章</a>

路由

路由

8-4. 激活状态

默认情况下,vue-router会用 当前路径 匹配 导航路径

  • 如果当前路径是以导航路径开头,则算作匹配,会为导航的a元素添加类名router-link-active
  • 如果当前路径完全等于导航路径,则算作精确匹配,会为导航的a元素添加类名router-link-exact-active

例如,当前访问的路径是/blog,则:

导航路径 类名
/ router-link-active
/blog router-link-active router-link-exact-active
/about
/message

可以为组件RouterLink添加bool属性exact,将匹配规则改为:必须要精确匹配才能添加匹配类名router-link-active

例如,当前访问的路径是/blog,则:

导航路径 exact 类名
/ true
/blog false router-link-active router-link-exact-active
/about true
/message true

例如,当前访问的路径是/blog/detail/123,则:

导航路径 exact 类名
/ true
/blog false router-link-active
/about true
/message true

另外,可以通过active-class属性更改匹配的类名,通过exact-active-class更改精确匹配的类名

8-5. 命名路由

使用命名路由可以解除系统与路径之间的耦合

1
2
3
4
5
6
7
8
9
// 路由配置
const router = new VueRouter({
routes: [ // 路由规则
// 当匹配到路径 /foo 时,渲染 Foo 组件
{ name:"foo", path: '/foo', component: Foo },
// 当匹配到路径 /bar 时,渲染 Bar 组件
{ name:"bar", path: '/bar', component: Bar }
]
})
1
2
<!-- 向to属性传递路由信息对象 RouterLink会根据你传递的信息以及路由配置生成对应的路径 -->
<RouterLink :to="{ name:'foo' }">go to foo</RouterLink>

8-6. 动态路由

问题:我们希望下面的地址都能够匹配到Blog组件

  • /article,显示全部文章
  • /article/cate/1,显示分类id1的文章
  • /article/cate/3,显示分类id3的文章

第一种情况很简单,只需要将一个固定的地址匹配到Blog组件即可

1
2
3
4
5
{
path: "/article",
name: "Blog",
component: Blog
}

但后面的情况则不同:匹配到Blog组件的地址中,有一部分是动态变化的,则需要使用一种特殊的表达方式:

1
2
3
4
5
{
path: "/article/cate/:categoryId",
name: "CategoryBlog",
component: Blog
}

在地址中使用:xxx,来表达这一部分的内容是变化的,在vue-router中,将变化的这一部分称之为params,可以在vue组件中通过this.$route.params来获取

1
2
3
4
// 访问 /article/cate/3
this.$route.params // { categoryId: "3" }
// 访问 /article/cate/1
this.$route.params // { categoryId: "1" }

8-7. 动态路由的导航

1
2
3
4
5
6
7
8
<router-link to="/article/cate/3">to article of category 3</router-link>

<router-link :to="{
name: 'CategoryBlog',
params: {
categoryId: 3
}
}">to article of category 3</router-link>

8-8. 编程式导航

除了使用<RouterLink>超链接导航外,vue-router还允许在代码中跳转页面

1
2
3
4
5
6
7
8
9
10
11
12
13
this.$router.push("跳转地址"); // 普通跳转
this.$router.push({ // 命名路由跳转
name:"Blog"
})

/* 参数与 <RouterLink>中属性to相同 */
this.$router.push({ // 命名路由跳转
name:"Blog",
params: {}, //动态路由对应接受参数
query:{} //携带参数,?后面的数据
})

this.$router.go(-1); // 回退。类似于 history.go

9. css module

什么是 CSS Module?

CSS Modules 不是官方规范或浏览器中的实现,而是构建步骤中的一个过程(在 Webpack 或 Browserify 的帮助下),它改变了类名和选择器的作用域(即有点像命名空间)。

目的:解决 CSS 中全局作用域的问题

使用:

  • 需要将样式文件命名为xxx.module.ooo
  • xxx为文件名
  • ooo为样式文件后缀名,可以是cssless

示例:

1
2
3
4
5
6
7
8
/* global.module.css */
.header {
...
}

.footer {
...
}
1
2
3
4
5
6
7
import styles from "./global.module.css";

export default function() {
let header = `<div class="${styles.header}"></div>`;
header.className = `${styles.footer}`;
return header;
}

实现原理:

产生局部作用域的唯一方法,就是使用一个独一无二的 class 的名字,不会与其他选择器重名。这就是 CSS Modules 的做法。

10. 得到组件渲染的Dom

1
2
3
4
5
6
7
8
9
10
/**
获取某个组件渲染的Dom根元素
*/
function getComponentRootDom(comp, props){
const vm = new Vue({
render: h => h(comp, {props})
})
vm.$mount();
return vm.$el;
}

11. 扩展vue实例

扩展vue示例

1
2
3
4
5
6
7
8
import Vue from "vue";

// 向实例注入成员
Vue.prototype.$sayHello = function() {
console.log("Hello!!!!");
};

// vue应用和组件中都可以直接使用(组建中: this.$sayHello() )

12. ref

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
<template>
<div>
<p ref="para">some paragraph</p>
<ChildComp ref="comp" />
<button @click="handleClick">查看所有引用</button>
</div>
</template>

<script>
import ChildComp from "./ChildComp"
export default {
components:{
ChildComp
},
methods:{
handleClick(){
// 获取持有的所有引用
console.log(this.$refs);
/*
{
para: p元素(原生DOM),
comp: ChildComp的组件实例
}
*/
}
}
}
</script>

通过ref可以直接操作dom元素,甚至可能直接改动子组件,这些都不符合vue的设计理念。
除非迫不得已,否则不要使用ref

13. 组件生命周期

组件声明周期

组件声明周期

13-1. 常见应用

注意: 不要死记硬背,要根据具体情况灵活处理

13-2. 加载远程数据

1
2
3
4
5
6
7
8
9
10
export default {
data(){
return {
news: []
}
},
async created(){
this.news = await getNews();
}
}

13-3. 直接操作DOM

1
2
3
4
5
6
7
8
9
10
11
12
export default {
data(){
return {
containerWidth:0,
containerHeight:0
}
},
mounted(){
this.containerWidth = this.$refs.container.clientWidth;
this.containerHeight = this.$refs.container.containerHeight;
}
}

13-4. 启动和清除计时器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default {
data(){
return {
timer: null
}
},
created(){
this.timer = setInterval(()=>{
...
}, 1000)
},
destroyed(){
clearInterval(this.timer);
}
}

14. 自定义指令

14-1. 定义指令

14-1-1. 全局定义

1
2
3
4
5
6
7
8
9
// 指令名称为:mydirec1
Vue.directive('mydirec1', {
// 指令配置
})

// 指令名称为:mydirec2
Vue.directive('mydirec2', {
// 指令配置
})

之后,所有的组件均可以使用mydirec1mydirec2指令

1
2
3
4
5
6
7
8
9
<template>
<!-- 某个组件代码 -->
<div>
<MyComp v-mydirec1="js表达式" />
<div v-mydirec2="js表达式">
...
</div>
</div>
</template>

14-1-2. 局部定义

局部定义是指在某个组件中定义指令,和局部注册组件类似。

定义的指令仅在该组件中有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<!-- 某个组件代码 -->
<div>
<MyComp v-mydirec1="js表达式" />
<div v-mydirec2="js表达式">
...
</div>
</div>
</template>
<script>
export default {
// 定义指令
directives: {
// 指令名称:mydirec1
mydirec1: {
// 指令配置
},
// 指令名称:mydirec2
mydirec2: {
// 指令配置
}
}
}
</script>

和局部注册组件一样,为了让指令更加通用,通常我们会把指令的配置提取到其他模块。

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>
<MyComp v-mydirec1="js表达式" />
<div v-mydirec2="js表达式">
...
</div>
<img v-mydirec1="js表达式" />
</div>
</template>

<script>
// 导入当前组件需要用到的指令配置对象
import mydirec1 from "@/directives/mydirec1";
import mydirec2 from "@/directives/mydirec2";
export default {
// 定义指令
directives: {
mydirec1,
mydirec2
}
}
</script>

14-2. 指令配置对象

没有配置的指令,就像没有配置的组件一样,毫无意义

vue支持在指令中配置一些钩子函数,在适当的时机,vue会调用这些钩子函数并传入适当的参数,以便开发者完成自己想做的事情。

常用的钩子函数:

1
2
3
4
5
6
7
8
9
10
11
12
// 指令配置对象
{
bind(){
// 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
},
inserted(){
// 被绑定元素插入父节点时调用。
},
update(){
// 所在组件的 VNode 更新时调用
}
}

查看更多的钩子函数

每个钩子函数在调用时,vue都会向其传递一些参数,其中最重要的是前两个参数

1
2
3
4
5
6
7
8
// 指令配置对象
// 钩子函数的参数 (即 el、binding、vnode 和 oldVnode)
{
bind(el, binding){
// el 是被绑定元素对应的真实DOM
// binding 是一个对象,描述了指令中提供的信息
}
}

14-2-1. bingding 对象

bingding 对象

  • binding:一个对象,包含以下 property:
    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-my-directive=”1 + 1” 中,绑定值为 2。
    • oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive=”1 + 1” 中,表达式为 “1 + 1”。
    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 “foo”。
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。

查看更多bingding对象的属性

14-3. 配置简化

比较多的时候,在配置自定义指令时,我们都会配置两个钩子函数

1
2
3
4
5
6
7
8
{
bind(el, bingding){

},
update(el, bingding){

}
}

这样,在元素绑定和更新时,都能运行到钩子函数

如果这两个钩子函数实现的功能相同,可以直接把指令配置简化为一个单独的函数:

1
2
3
function(el, bingding){
// 该函数会被同时设置到bind和update中
}

15. 组件的混入

有的时候,许多组件有着类似的功能,这些功能代码分散在组件不同的配置中。

组件的混入

于是,我们可以把这些配置代码抽离出来,利用混入融合到组件中。

组件的混入

具体的做法非常简单:

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
// 抽离的公共代码
const common = {
data(){
return {
a: 1,
b: 2
}
},
created(){
console.log("common created");
},
computed:{
sum(){
return this.a + this.b;
}
}
}

/**
* 使用comp1,将会得到:
* common created
* comp1 created 1 2 3
*/
const comp1 = {
mixins: [common] // 之所以是数组,是因为可以混入多个配置代码
created(){
console.log("comp1 created", this.a, this.b, this.sum);
}
}

当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先

更多细节参见官网

16. 组件递归

组件是可以在它们自己的模板中调用自身的。不过它们只能通过 name 选项来做这件事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
...
<RightList />
...
</template>

<script>
export default {
name: "RightList",
props: {
...
}
};
</script>

17. watch

利用watch配置,可以直接观察某个数据的变化,变化时可以做一些处理

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
export default {
// ... 其他配置
watch: {
// 观察 this.$route 的变化,变化后,会调用该函数
$route(newVal, oldVal){
// newVal:this.$route 新的值,等同 this.$route
// oldVal:this.$route 旧的值
},
// 完整写法
$route: {
handler(newVal, oldVal){},
deep: false, // 是否监听该数据内部属性的变化,默认 false
immediate: false // 是否立即执行一次 handler,默认 false
}
// 观察 this.$route.params 的变化,变化后,会调用该函数
["$route.params"](newVal, oldVal){
// newVal:this.$route.params 新的值,等同 this.$route.params
// oldVal:this.$route.params 旧的值
},
// 完整写法
["$route.params"]: {
handler(newVal, oldVal){},
deep: false, // 是否监听该数据内部属性的变化,默认 false
immediate: false // 是否立即执行一次 handler,默认 false
}
}
}

18. 事件修饰符

针对dom节点的原生事件vue支持多种修饰符以简化代码

详见:事件修饰符、按键修饰符、系统修饰符

19. $listeners

$listenersvue的一个实例属性,它用于获取父组件传过来的所有事件函数

1
2
<!-- 父组件 -->
<Child @event1="handleEvent1" @event2="handleEvent2" />
1
2
// 子组件
this.$listeners // { event1: handleEvent1, event2: handleEvent2 }

$emit$listeners通信的异同

相同点:均可实现子组件向父组件传递消息

差异点:

  • $emit更加符合单向数据流,子组件仅发出通知,由父组件监听做出改变;而$listeners则是在子组件中直接使用了父组件的方法。
  • 调试工具可以监听到子组件$emit的事件,但无法监听到$listeners中的方法调用。(想想为什么)
  • 由于$listeners中可以获得传递过来的方法,因此调用方法可以得到其返回值。但$emit仅仅是向父组件发出通知,无法知晓父组件处理的结果
    对于上述中的第三点,可以在$emit中传递回调函数来解决

父组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<Child @click="handleClick" />
</template>

<script>
import Child from "./Child"
export default {
components:{
Child
},
methods:{
handleClick(data, callback){
console.log(data); // 得到子组件事件中的数据
setTimeout(()=>{
callback(1); // 一段时间后,调用子组件传递的回调函数
}, 3000)
}
}
}
</script>

子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<button @click="handleClick">click</button>
</template>
<script>
export default {
methods:{
handleClick(){
this.$emit("click", 123, (data)=>{
console.log(data); // data为父组件处理完成后得到的数据
})
}
}
}
</script>

20. v-model

v-model指令实质是一个语法糖,它是value属性和input事件的结合体

1
2
3
<input :value="data" @input="data=$event.target.value" />
<!-- 等同于 -->
<input v-model="data" />

详见:表单输入绑定

21. 事件总线

事件总线

事件总线功能:

  1. 提供监听某个事件的接口
  2. 提供取消监听的接口
  3. 触发事件的接口(可传递数据)
  4. 触发事件后会自动通知监听者

21-1. 自己实现

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
const listeners = {};

// 事件总线
export default {
// 监听某一个事件
$on(eventName, handler) {
if (!listeners[eventName]) {
listeners[eventName] = new Set();
}
listeners[eventName].add(handler);
},
// 取消监听
$off(eventName, handler) {
if (!listeners[eventName]) {
return;
}
listeners[eventName].delete(handler);
},
// 触发事件
$emit(eventName, ...args) {
if (!listeners[eventName]) {
return;
}
for (const handler of listeners[eventName]) {
handler(...args);
}
},
};

21-2. 通过vue实现

1
2
3
4
import Vue from "vue";
// new Vue({}); 即可实现时间总线功能,事件名同上;
// 挂载到Vue原型上
Vue.prototype.$bus = new Vue({});

main.js文件中导入即可全局使用,当然也可局部引用;

1
2
3
4
5
6
7
// 入口文件
import Vue from "vue";
import App from "./App.vue";
import "./eventBus";
new Vue({
render: (h) => h(App),
}).$mount("#app");

22. Vuex 数据共享

Vuex官方文档

数据共享

vue中遇到共享数据,会带来下面的多个问题:

  • 如何保证数据的唯一性?
    • 如果数据不唯一,则会浪费大量的内存资源,降低运行效率
    • 如果数据不唯一,就可能出现不统一的数据,难以维护
  • 某个组件改动数据后,如何让其他用到该数据的组件知道数据变化了?
    • 事件总线貌似可以解决该问题,但需要在组件中手动的维护监听,极其不方便,而且事件总线的目的在于「通知」,而不是「共享数据」

一种比较容易想到的方案,就是把所有的共享数据全部提升到根组件,然后通过属性不断下发,当某个组件需要修改数据时,又不断向上抛出事件,直到根组件完成对数据的修改。

数据共享

这种方案的缺陷也非常明显:

  • 需要编写大量的代码层层下发数据,很多组件被迫拥有了自己根本不需要的数据
  • 需要编写大量的代码层层上抛事件,很多组件被迫注册了自己根本处理不了的事件

基于上面的问题,我们可以简单的设置一个独立的数据仓库

数据仓库

  • 组件需要什么共享数据,可以自由的从仓库中获取,需要什么拿什么。

  • 组件可以自由的改变仓库中的数据,仓库的数据变化后,会自动通知用到对应数据的组件更新

要实现这一切,可以选择vuex

22-1. 安装vuex

1
npm install vuex

22-2. 创建仓库

安装vuex后,可以通过下面的代码创建一个数据仓库,在大部分情况下,一个工程仅需创建一个数据仓库

1
2
3
4
5
6
7
8
9
10
11
12
// store.js
import Vuex from "vuex";
import Vue from "vue";
Vue.use(Vuex); // 应用vuex插件
const store = new Vuex.Store({
// 仓库的配置
state: { // 仓库的初始状态(数据)
count: 0
}
})

export default store;

仓库创建好后,你可以使用store.state来访问仓库中的数据
如果希望在vue中方便的使用仓库数据,需要将vuex作为插件安装

1
2
3
4
5
6
7
8
// main.js
import Vue from "vue";
import App from "./App.vue";
import store from "./store.js";
new Vue({
store, // 向vue中注入仓库
render: h => h(App)
}).$mount("#app");

之后,在vue组件中,可以通过实例的$store属性访问到仓库

Vuex会自动将配置的状态数据设置为响应式数据,当数据变化时,依赖该数据的组件会自动渲染。

22-3. 数据的变更

尽管可以利用数据响应式的特点直接变更数据,但这样的做法在大型项目中会遇到问题

如果有一天,你发现某个共享数据是错误的,而有一百多个组件都有可能变更过这块数据,你该如何知道是哪一步数据变更出现了问题?

为了能够更好的跟踪数据的变化,vuex强烈建议使用mutation来更改数据

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
const store = new Vuex({
// 仓库的配置
state: { // 仓库的初始状态(数据)
count: 0
},
mutations: {
/**
* 每个mutation是一个方法,它描述了数据在某种场景下的变化
* increase mutation描述了数据在增加时应该发生的变化
* 参数state为当前的仓库数据
*/
increase(state){
state.count++;
},
decrease(state){
state.count--;
},
/**
* 求n次幂
* 该mutation需要一个额外的参数来提供指数
* 我们把让数据产生变化时的附加信息称之为负荷(负载) payload
* payload可以是任何类型,数字、字符串、对象均可
* 在该mutation中,我们约定payload为一个数字,表示指数
*/
power(state, payload){
state.count **= payload;
}
}
})

当我们有了mutation后,就不应该直接去改动仓库的数据了
而是通过store.commit方法提交一个mutation,具体做法是

1
store.commit("mutation的名字", payload);

现在,我们可以通过vue devtools观测到数据的变化了

特别注意:

  1. mutation中不得出现异步操作

    在实际开发的规范中,甚至要求不得有副作用操作

    副作用操作包括:

    • 异步
    • 更改或读取外部环境的信息,例如localStorage、location、DOM
  2. 提交mutation是数据改变的唯一原因

mutation

22-4. 异步处理

如果在vuex中要进行异步操作,需要使用action

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
const store = new Vuex({
state: {
count: 0
},
mutations: {
increase(state){
state.count++;
},
decrease(state){
state.count--;
},
power(state, payload){
state.count **= payload;
}
},
actions: {
/**
* ctx: 类似于store的对象
* payload: 本次异步操作的额外信息
*/
asyncPower(ctx, payload){
setTimeout(function(){
ctx.commit("power", payload)
}, 1000)
}
}
})

使用示例:

1
2
3
4
5
6
7
8
9
10
export default {
methods: {
handleDecrease() {
this.$store.commit("decrease");
},
handleAsyncIncrease() {
this.$store.dispatch("asyncIncrease");
},
},
};

异步处理

22-5. 进阶用法(modules)

配置样例

index.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Vuex from 'vuex';
import Vue from 'vue';
import loginUser from './loginUser';
import counter from './counter';

Vue.use(Vuex);

const store = new Vuex.Store({
modules: {
loginUser,
counter
},
strict: true, // 开启严格模式后,只允许通过mutation改变状态
});

export default store;

loginUser.js:

1
2
3
4
5
6
7
8
export default {
state: {
loading: false,
user: null
},
mutations: {},
actions: {}
}

counter.js:

1
2
3
4
5
6
7
export default {
state: {
count:0
},
mutations: {},
actions: {}
}

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { mapState } from "vuex";

export default {
data() {
return {
loginId: "",
loginPwd: "",
};
},
//获取数据
// this.$store.state.loginUser.loading
computed: mapState("loginUser", ["loading"]),
methods: {
handleSubmit() {
//执行方法
this.$store.dispatch("loginUser/login", {
loginId: this.loginId,
loginPwd: this.loginPwd,
});
},
},
};

23. 打包结果分析

模式:

模式是 Vue CLI 项目中一个重要的概念。默认情况下,一个 Vue CLI 项目有三个模式:

  • development 模式用于 vue-cli-service servenpm run serve
  • test 模式用于 vue-cli-service test:unitnpm run build
  • production 模式用于 vue-cli-service buildvue-cli-service test:e2e

23-1. 分析打包结果

由于vue-cli是利用webpack进行打包,我们仅需加入一个webpack插件webpack-bundle-analyzer即可分析打包结果

23-1-1. 安装

1
npm install webpack-bundle-analyzer -D

23-1-2. 配置

为了避免在开发环境中启动webpack-bundle-analyzer,我们仅需使用以下代码即可

1
2
3
4
5
6
7
8
9
10
11
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
.BundleAnalyzerPlugin;
// vue.config.js
module.exports = {
...
// 通过 configureWebpack 选项,可对 webpack 进行额外的配置
// 该配置最终会和 vue-cli 的默认配置进行合并(webpack-merge)
configureWebpack: {
plugins: [new BundleAnalyzerPlugin()]
},
};

完场上述配置后,运行npm run build即可显示打包分析;

但是运行npm run server也会显示打包分析,这个开发时一般是不需要的;

23-1-3. 自定义动态配置

如果你需要基于环境有条件地配置行为,或者想要直接修改配置,那就换成一个函数 (该函数会在环境变量被设置之后懒执行)。该方法的第一个参数会收到已经解析好的配置。在函数内,你可以直接修改配置,或者返回一个将会被合并的对象:

1
2
3
4
5
6
7
8
9
10
// vue.config.js
module.exports = {
configureWebpack: config => {
if (process.env.NODE_ENV === 'production') {
// 为生产环境修改配置...
} else {
// 为开发环境修改配置...
}
}
}

当然也可以提取到其他文件导入使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//webpack.config.js
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
.BundleAnalyzerPlugin;

if (process.env.NODE_ENV === "production") {
module.exports = {
devtool: "none", //关闭源码地图
plugins: [new BundleAnalyzerPlugin()],
//配置不需要打包的库
externals: {
vue: "Vue",
vuex: "Vuex",
"vue-router": "VueRouter",
axios: "axios",
},
};
} else {
module.exports = {};
}
1
2
3
4
5
6
7
8
9
10
11
// vue-cli的配置文件
module.exports = {
devServer: {
proxy: {
"/api": {
target: "http://test.my-site.com",
},
},
},
configureWebpack: require("./webpack.config"),
};

23-2. 优化公共库打包体积

23-2-1. 使用CDN

CDN全称为Content Delivery Network,称之为内容分发网络

它的基本原理是:架设多台服务器,这些服务器定期从源站拿取资源保存本地,到让不同地域的用户能够通过访问最近的服务器获得资源

CDN

我们可以把项目中的所有静态资源都放到CDN上(收费),也可以利用现成免费的CDN获取公共库的资源

CDN

首先,我们需要告诉webpack不要对公共库进行打包

1
2
3
4
5
6
7
8
9
10
// vue.config.js
module.exports = {
configureWebpack: {
externals: {
vue: "Vue", //vue不需要打包,使用Vue全局变量
vuex: "Vuex", //vuex不需要打包,使用Vuex全局变量
"vue-router": "VueRouter", //vue-router不需要打包,使用VueRouter全局变量
}
},
};

然后,在页面中手动加入cdn链接,这里使用bootcn

1
2
3
4
5
6
7
8
9
10
<body>
<div id="app"></div>
<!-- 可以通过函数判断只有在生产环境从cdn加载自愿 -->
<% if(NODE_ENV === "production") { %>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.12/vue.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/vuex/3.5.1/vuex.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/vue-router/3.4.7/vue-router.min.js"></script>
<% } %>
<!-- built files will be auto injected -->
</body>

对于vuexvue-router,使用这种传统的方式引入的话会自动成为Vue的插件,因此需要去掉Vue.use(xxx)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// store.js
import Vue from "vue";
import Vuex from "vuex";

if(!window.Vuex){
// 没有使用传统的方式引入Vuex
Vue.use(Vuex);
}

// router.js
import VueRouter from "vue-router";
import Vue from "vue";

if(!window.VueRouter){
// 没有使用传统的方式引入VueRouter
Vue.use(VueRouter);
}

23-2-2. 启用现代模式

为了兼容各种浏览器,vue-cli在内部使用了@babel/present-env对代码进行降级,你可以通过.browserlistrc配置来设置需要兼容的目标浏览器

这是一种比较偷懒的办法,因为对于那些使用现代浏览器的用户,它们也被迫使用了降级之后的代码,而降低的代码中包含了大量的polyfill,从而提升了包的体积

因此,我们希望提供两种打包结果:

  1. 降级后的包(大),提供给旧浏览器用户使用
  2. 未降级的包(小),提供给现代浏览器用户使用

除了应用webpack进行多次打包外,还可以利用vue-cli给我们提供的命令:

1
vue-cli-service build --modern

这里运行打包后会在dist文件夹下生成两份打包文件:

  • app.xxx.js (为降级:体积小)
  • app-legacy.xxx.js (降级后、兼容性代码:体积大)

打包后html中已自动生成兼容引入方式无需在做修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<head>
...
<!-- rel="prefetch" 预提取,告诉浏览器加载下一页面可能会用到的资源 -->
<link href="/css/about.343abd82.css" rel="prefetch">
<!-- A: 预下载 as=“style” 当做样式文件处理 -->
<link href="/css/app.d7ea4d3b.css" rel="preload" as="style">
<!-- B: rel="modulepreload" 只有现代浏览器支持 -->
<link href="/js/app.5bc72638.js" rel="modulepreload" as="script">
<!-- A: 使用预下载的文件 -->
<link href="/css/app.d7ea4d3b.css" rel="stylesheet">
</head>
<body>
...
<!-- B: 使用预下载的文件、type="module"旧版本不支持忽略此代码-->
<script type="module" src="/js/app.5bc72638.js"></script>
<!-- C: 新版本看到 nomodule 忽略此代码-->
<script src="/js/app-legacy.abf7d130.js" nomodule></script>
</body>

link标签属性rel:

rel 属性规定当前文档与被链接文档之间的关系

  1. rel="preload"

    • 预加载,先下载下来,暂时不用,将来如果需要使用直接使用预下载的文件
    • as="style" 当做样式文件处理
    • 可以让浏览器今早下载将来要用的文件
  2. rel="modulepreload":

    • 只有现代浏览器支持
    • 预加载,先下载下来,暂时不用,将来如果需要使用直接使用预下载的文件
    • as="script" 把href文件当做ES Module处理
  3. rel="prefetch":

    • 表示预提取,告诉浏览器加载下一页面可能会用到的资源
    • 浏览器会利用空闲状态进行下载并将资源存储到缓存中

preload 和 prefetch 的区别:

  • preload 是告诉浏览器页面必定需要的资源,浏览器一定会预先加载这些资源
  • prefetch 是告诉浏览器下一个页面可能需要的资源,浏览器不一定会加载这些资源
  • 在VUE SSR生成的页面中,首页的资源均使用preload,而路由对应的资源,则使用prefetch
  • 对于当前页面很有必要的资源使用 preload,对于可能在将来的页面中使用的资源使用 prefetch

23-3. 优化项目包体积

这里的项目包是指src目录中的打包结果

23-3-1. 页面分包

默认情况下,vue-cli会利用webpacksrc目录中的所有代码打包成一个bundle

这样就导致访问一个页面时,需要加载所有页面的js代码

我们可以利用webpack动态import的支持,从而达到把不同页面的代码打包到不同文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
// routes.js
export default [
{
name: "Home",
path: "/",
component: () => import(/* webpackChunkName: "home" */ "@/views/Home"),
},
{
name: "About",
path: "/about",
component: () => import(/* webpackChunkName: "about" */"@/views/About"),
}
];

23-4. 优化首屏响应

首页白屏受很多因素的影响

vue页面需要通过js构建,因此在js下载到本地之前,页面上什么也没有

一个非常简单有效的办法,即在页面中先渲染一个小的加载中效果,等到js下载到本地并运行后,即会自动替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div id="app">
<style>
.loading-img {
position: fixed;
width: 200px;
height: 200px;
left: 50%;
top: 50%;
margin-left: -100px;
margin-top: -100px;
}
</style>
<img src="loading.gif" />
</div>

24. 异步组件

在代码层面,vue组件本质上是一个配置对象

1
2
3
4
5
6
var comp = {
props: xxx,
data: xxx,
computed: xxx,
methods: xxx
}

但有的时候,要得到某个组件配置对象需要一个异步的加载过程,比如:

  • 需要使用ajax获得某个数据之后才能加载该组件
  • 为了合理的分包,组件配置对象需要通过import(xxx)动态加载

如果一个组件需要通过异步的方式得到组件配置对象,该组件可以把它做成一个异步组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 异步组件本质上是一个函数
* 该函数调用后返回一个Promise,Promise成功的结果是一个组件配置对象
*/
const AsyncComponent = () => import("./MyComp")

var App = {
components: {
/**
* 你可以把该函数当做一个组件使用(异步组件)
* Vue会调用该函数,并等待Promise完成,完成之前该组件位置什么也不渲染
*/
AsyncComponent
}
}

异步组件的函数不仅可以返回一个Promise,还支持返回一个对象

详见:返回对象格式的异步组件

24-1. 应用

异步组件通常应用在路由懒加载中,以达到更好的分包

为了提高用户体验,可以在组件配置对象加载完成之前给用户显示一些提示信息

1
2
3
4
5
6
7
8
var routes = [
{ path: "/", component: async () => {
console.log("组件开始加载");
const HomeComp = await import("./Views/Home.vue");
console.log("组件加载完毕");
return HomeComp;
} }
]

推荐使用NProgress展现一个进度条

25. NProgress

25-1. 安装

1
$ npm install --save nprogress

25-2. 使用

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
//routes.js
import "nprogress/nprogress.css";
import { start, done, configure } from "nprogress";

configure({
trickleSpeed: 20,
showSpinner: false,
});

function getPageComponent(pageCompResolver) {
return async () => {
start(); //开始进度条
const comp = await pageCompResolver();
done(); //完成进度条
return comp;
};
};

export default [
{
name: "Home",
path: "/",
component: getPageComponent(() =>
import(/* webpackChunkName: "home" */ "@/views/Home")
),
},
...
]

__END__