vue_basic

vue官网:https://cn.vuejs.org/

服务端渲染

sequenceDiagram
浏览器->>+服务器: http://xxx.com/news
Note right of 服务器: 组装页面(服务端渲染)
服务器->>-浏览器: 完整页面

前后端分离

sequenceDiagram
浏览器->>服务器: http://xxx.com/news
服务器->>浏览器: 无内容的html
activate 浏览器
浏览器-->>服务器: ajax
服务器-->>浏览器: 各种业务数据
Note left of 浏览器: 运行js,创建元素,渲染页面
deactivate 浏览器

单页应用

sequenceDiagram
浏览器->>服务器: http://xxx.com/news
服务器->>浏览器: 无内容的html
activate 浏览器
Note left of 浏览器: 运行js,创建元素,渲染页面
浏览器-->>服务器: ajax
服务器-->>浏览器: 各种业务数据
Note left of 浏览器: 跳转页面
浏览器-->>服务器: ajax
服务器-->>浏览器: 各种业务数据
Note left of 浏览器: JS重新构建页面元素
deactivate 浏览器

vue框架

sequenceDiagram
浏览器->>服务器: http://xxx.com/news
服务器->>浏览器: 无内容的html
activate 浏览器
Note left of 浏览器: 运行包含vue的js,使用框架渲染页面
浏览器-->>服务器: ajax
服务器-->>浏览器: 各种业务数据
Note left of 浏览器: 使用vue-router跳转页面
deactivate 浏览器

注入

image-20201110163548294

vue会将以下配置注入到vue实例:

  • data:和界面相关的数据
  • computed:通过已有数据计算得来的数据,将来详细讲解
  • methods:方法

模板中可以使用vue实例中的成员

虚拟DOM树

直接操作真实的DOM会引发严重的效率问题,vue使用虚拟DOM(vnode)的方式来描述要渲染的内容

vnode是一个普通的JS对象,用于描述界面上应该有什么,比如:

1
2
3
4
5
6
var vnode = {
tag: "h1",
children: [
{ tag: undefined, text: "第一个vue应用:Hello World"}
]
}

上面的对象描述了:

1
有一个标签名为h1的节点,它有一个子节点,该子节点是一个文本,内容为「第一个vue应用:Hello World」

vue模板并不是真实的DOM,它会被编译为虚拟DOM

1
2
3
4
<div id="app">
<h1>第一个vue应用:{{title}}</h1>
<p>作者:{{author}}</p>
</div>

上面的模板会被编译为类似下面结构的虚拟DOM

1
2
3
4
5
6
7
{
tag: "div",
children: [
{ tag: "h1", children: [ { text: "第一个vue应用:Hello World" } ] },
{ tag: "p", children: [ { text: "作者:袁" } ] }
]
}

虚拟DOM树会最终生成为真实的DOM树

image-20201106144536733

当数据变化后,将引发重新渲染,vue会比较新旧两棵vnode tree,找出差异,然后仅把差异部分应用到真实dom tree中

image-20201106145409844

可见,在vue中,要得到最终的界面,必须要生成一个vnode tree

vue通过以下逻辑生成vnode tree:

注意:虚拟节点树必须是单根的

挂载

将生成的真实DOM树,放置到某个元素位置,称之为挂载

挂载的方式:

  1. 通过el:"css选择器"进行配置
  2. 通过vue实例.$mount("css选择器")进行配置

完整流程

image-20200908051939745

组件的出现是为了实现以下两个目标:

  1. 降低整体复杂度,提升代码的可读性和可维护性
  2. 提升局部代码的可复用性

绝大部分情况下,一个组件就是页面中某个区域,组件包含该区域的:

  • 功能(JS代码)

  • 内容(模板代码)

  • 样式(CSS代码)

    要在组件中包含样式,需要构建工具的支撑

组件开发

创建组件

组件是根据一个普通的配置对象创建的,所以要开发一个组件,只需要写一个配置对象即可

该配置对象和vue实例的配置是几乎一样

1
2
3
4
5
6
7
8
9
//组件配置对象
var myComp = {
data(){
return {
// ...
}
},
template: `....`
}

值得注意的是,组件配置对象和vue实例有以下几点差异:

  • el
  • data必须是一个函数,该函数返回的对象作为数据
  • 由于没有el配置,组件的虚拟DOM树必须定义在templaterender

注册组件

注册组件分为两种方式,一种是全局注册,一种是局部注册

全局注册

一旦全局注册了一个组件,整个应用中任何地方都可以使用该组件

全局注册的方式是:

1
2
3
4
// 参数1:组件名称,将来在模板中使用组件时,会使用该名称
// 参数2:组件配置对象
// 该代码运行后,即可在模板中使用组件
Vue.component('my-comp', myComp)

在模板中,可以使用组件了

1
2
3
<my-comp />
<!-- 或 -->
<my-comp></my-comp>

但在一些工程化的大型项目中,很多组件都不需要全局使用。
比如一个登录组件,只有在登录的相关页面中使用,如果全局注册,将导致构建工具无法优化打包
因此,除非组件特别通用,否则不建议使用全局注册

局部注册

局部注册就是哪里要用到组件,就在哪里注册

局部注册的方式是,在要使用组件的组件或实例中加入一个配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 这是另一个要使用my-comp的组件
var otherComp = {
components:{
// 属性名为组件名称,模板中将使用该名称
// 属性值为组件配置对象
"my-comp": myComp
},
template: `
<div>
<!-- 该组件的其他内容 -->
<my-comp></my-comp>
</div>
`;
}

应用组件

在模板中使用组件特别简单,把组件名当作HTML元素名使用即可。

但要注意以下几点:

  1. 组件必须有结束

组件可以自结束,也可以用结束标记结束,但必须要有结束

下面的组件使用是错误的:

1
<my-comp>
  1. 组件的命名

无论你使用哪种方式注册组件,组件的命名需要遵循规范。

组件可以使用kebab-case 短横线命名法,也可以使用PascalCase 大驼峰命名法

下面两种命名均是可以的

1
2
3
4
5
6
var otherComp = {
components:{
"my-comp": myComp, // 方式1
MyComp: myComp //方式2
}
}

实际上,使用小驼峰命名法 camelCase也是可以识别的,只不过不符合官方要求的规范

使用PascalCase方式命名还有一个额外的好处,即可以在模板中使用两种组件名

1
2
3
4
5
var otherComp = {
components:{
MyComp: myComp
}
}

模板中:

1
2
3
<!-- 可用 -->
<my-comp />
<MyComp />

因此,在使用组件时,为了方便,往往使用以下代码:

1
2
3
4
5
6
7
8
9
var MyComp = {
//组件配置
}

var OtherComp = {
components:{
MyComp // ES6速写属性
}
}

组件树

一个组件创建好后,往往会在各种地方使用它。它可能多次出现在vue实例中,也可能出现在其他组件中。

于是就形成了一个组件树

向组件传递数据

大部分组件要完成自身的功能,都需要一些额外的信息

比如一个头像组件,需要告诉它头像的地址,这就需要在使用组件时向组件传递数据

传递数据的方式有很多种,最常见的一种是使用组件属性 component props

首先在组件中申明可以接收哪些属性:

1
2
3
4
5
6
7
8
9
10
var MyComp = {
props:["p1", "p2", "p3"],
// 和vue实例一样,使用组件时也会创建组件的实例
// 而组件的属性会被提取到组件实例中,因此可以在模板中使用
template: `
<div>
{{p1}}, {{p2}}, {{p3}}
</div>
`
}

在使用组件时,向其传递属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
var OtherComp = {
components: {
MyComp
},
data(){
return {
a:1
}
},
template: `
<my-comp :p1="a" :p2="2" p3="3"/>
`
}

注意:在组件中,属性是只读的,绝不可以更改,这叫做单向数据流

工程结构

见代码

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

vue-cli

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

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

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

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

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>

预编译

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

这样做的好处在于:

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

遇到的问题:

使用npm指令显示打开方式然后执行了 C:\WINDOWS\system32\npm这个可执行文件,方法:删除她

搭建vue :vue create [name]

npm run serve开发使用

面试题:计算属性和方法有什么区别?

1
2
3
4
5
6
计算属性本质上是包含gettersetter的方法
当获取计算属性时,实际上是在调用计算属性的getter方法。vue会收集计算属性的依赖,并缓存计算属性的返回结果。只有当依赖变化后才会重新进行计算。
方法没有缓存,每次调用方法都会导致重新执行。
计算属性的gettersetter参数固定,getter没有参数,setter只有一个参数。而方法的参数不限。
由于有以上的这些区别,因此计算属性通常是根据已有数据得到其他数据,并在得到数据的过程中不建议使用异步、当前时间、随机数等副作用操作。
实际上,他们最重要的区别是含义上的区别。计算属性含义上也是一个数据,可以读取也可以赋值;方法含义上是一个操作,用于处理一些事情。

完整的计算属性书写:

1
2
3
4
5
6
7
8
9
10
computed: {
propName: {
get(){
// getter,最好不要在这个里面用调用时间的数据,因为会缓存下来,后面不会因为时间的改变而调用
},
set(val){
// setter,不改变get中的数据,调用set的情况下不会使得get被调
}
}
}

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

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

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

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

具名插槽

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

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

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 v-slot:default>

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

vue-router 官网:https://router.vuejs.org/zh/

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

路由插件

1
2
# 为了保证和课程一致,请安装3.4.9版本
npm i [email protected]

路由插件的使用

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
})

基本使用

1
2
3
4
5
6
7
8
9
10
// 路由配置
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>

路由模式

路由模式决定了:

  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
  3. abstract:路由从内存中获取路径,改变路径也只是改动内存中的值。这种模式通常应用到非浏览器环境中。

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

导航

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

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

激活状态

默认情况下,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更改精确匹配的类名

命名路由

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

1
2
3
4
5
6
7
8
9
10
// 路由配置
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>

使用css module

需要将样式文件命名为xxx.module.ooo

xxx为文件名

ooo为样式文件后缀名,可以是cssless

得到组件渲染的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;
}

扩展vue实例

扩展vue实例

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

本节课内容和vue没有任何关系!

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

axios: https://github.com/axios/axios

mockjs:http://mockjs.com/

远程获取数据的意义

image-20201204145137500

开发环境有跨域问题

vuecli中webpack搭建的服务器,由浏览器请求后端服务器,端口号或者请求域名不一样

虽然可以得到数据,但是浏览器为了安全进行劫持

sequenceDiagram
浏览器->>前端开发服务器: http://localhost:8080/
前端开发服务器->>浏览器: 页面
浏览器->>后端测试服务器: ajax 跨域:http://test-data:3000/api/news
后端测试服务器->>浏览器: JSON数据
rect rgb(224,74,74)
Note right of 浏览器: 浏览器阻止数据移交
end

生产环境一般 没有跨域问题

sequenceDiagram
浏览器->>服务器: http://www.my-site.com/
服务器->>浏览器: 页面
浏览器->>服务器: ajax:http://www.my-site.com/api/news
服务器->>浏览器: JSON数据
sequenceDiagram
浏览器->>静态资源服务器: http://www.my-site.com/
静态资源服务器->>浏览器: 页面
浏览器->>数据服务器: ajax 跨域:http://api.my-site.com/api/news
数据服务器->>浏览器: [后端允许www.my-site.com]JSON数据

解决开发环境的跨域问题

脱离浏览器环境就解决了跨域问题,通过代理(在vue-cli config.js中查找 )

sequenceDiagram
浏览器->>前端开发服务器: http://localhost:8080/
前端开发服务器->>浏览器: 页面
浏览器->>前端开发服务器: ajax:http://localhost:8080/api/news
前端开发服务器->>后端测试服务器: 代理请求:http://test-data:3000/api/news
后端测试服务器->>前端开发服务器: JSON数据
前端开发服务器->>浏览器: JSON数据

为什么要Mock数据

后端服务器未搭建完,需要模拟数据,在浏览器内部拦截

sequenceDiagram
浏览器->>前端开发服务器: http://localhost:8080/
前端开发服务器->>浏览器: 页面
浏览器->>前端开发服务器: ajax:http://localhost:8080/api/news
前端开发服务器->>后端测试服务器: 代理请求:http://test-data:3000/api/news
后端测试服务器->>前端开发服务器: 404 (后端正在开发中)
前端开发服务器->>浏览器: 404
sequenceDiagram
participant 浏览器
participant 浏览器内的MockJS
participant 前端开发服务器
activate 浏览器内的MockJS
Note left of 浏览器内的MockJS: 定义ajax拦截规则
deactivate 浏览器内的MockJS
浏览器->>前端开发服务器: http://localhost:8080/
前端开发服务器->>浏览器: 页面
浏览器->>浏览器内的MockJS: ajax:http://localhost:8080/api/news
浏览器内的MockJS->>浏览器: 模拟的JSON数据

组件生命周期

image-20200908051939745 image-20201206132819263

常见应用

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

加载远程数据

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

直接操作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;
}
}

启动和清除计时器

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);
}
}

Home组件负责呈现整体效果

CarouselItem组件负责呈现单张轮播图

image-20201206153157145

Home组件负责:

  • 整体布局
  • 监听鼠标滚轮事件,切换轮播图
  • 提供上下按钮,切换轮播图
  • 提供指示器,切换轮播图

CarouselItem组件负责:

  • 单张轮播图的全部事务

定义指令

全局定义

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
10
<template>
<!-- 某个组件代码 -->
<div>
<MyComp v-mydirec1="js表达式" />
<div v-mydirec2="js表达式">
...
</div>
<img v-mydirec1="js表达式" />
</div>
</template>

局部定义

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

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

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
<template>
<!-- 某个组件代码 -->
<div>
<MyComp v-mydirec1="js表达式" />
<div v-mydirec2="js表达式">
...
</div>
<img v-mydirec1="js表达式" />
</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>

指令配置对象

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

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
// 指令配置对象
{
bind(el, binding){
// el 是被绑定元素对应的真实DOM
// binding 是一个对象,描述了指令中提供的信息
}
}

bingding 对象

image-20210104174229660

查看更多bingding对象的属性

配置简化

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

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

},
update(el, bingding){

}
}

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

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

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

利用上述知识,可满足大部分自定义指令的需求

更多的自定义指令用法见官网

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

image-20210105161811637

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

image-20210105162109015

具体的做法非常简单:

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);
}
}

混入并不复杂,更多细节参见官网

d1abce9ba039c63d4046725ce2acd07 6ae3f3429280843c5e8369d504b32d7-17150818905342

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

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

具名插槽

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

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

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 v-slot:default>

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

vue-router 官网:https://router.vuejs.org/zh/

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

路由插件

1
2
# 为了保证和课程一致,请安装3.4.9版本
npm i [email protected]

路由插件的使用

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
})

基本使用

1
2
3
4
5
6
7
8
9
10
// 路由配置
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>

路由模式

路由模式决定了:

  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
  3. abstract:路由从内存中获取路径,改变路径也只是改动内存中的值。这种模式通常应用到非浏览器环境中。

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

导航

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

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

激活状态

默认情况下,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更改精确匹配的类名

命名路由

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

1
2
3
4
5
6
7
8
9
10
// 路由配置
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>

使用css module

需要将样式文件命名为xxx.module.ooo

xxx为文件名

ooo为样式文件后缀名,可以是cssless

得到组件渲染的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;
}

扩展vue实例

扩展vue实例

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

本节课内容和vue没有任何关系!

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

axios: https://github.com/axios/axios

mockjs:http://mockjs.com/

远程获取数据的意义

image-20201204145137500

开发环境有跨域问题

sequenceDiagram
浏览器->>前端开发服务器: http://localhost:8080/
前端开发服务器->>浏览器: 页面
浏览器->>后端测试服务器: ajax 跨域:http://test-data:3000/api/news
后端测试服务器->>浏览器: JSON数据
rect rgb(224,74,74)
Note right of 浏览器: 浏览器阻止数据移交
end

生产环境没有跨域问题

sequenceDiagram
浏览器->>服务器: http://www.my-site.com/
服务器->>浏览器: 页面
浏览器->>服务器: ajax:http://www.my-site.com/api/news
服务器->>浏览器: JSON数据
sequenceDiagram
浏览器->>静态资源服务器: http://www.my-site.com/
静态资源服务器->>浏览器: 页面
浏览器->>数据服务器: ajax 跨域:http://api.my-site.com/api/news
数据服务器->>浏览器: [允许www.my-site.com]JSON数据

解决开发环境的跨域问题

sequenceDiagram
浏览器->>前端开发服务器: http://localhost:8080/
前端开发服务器->>浏览器: 页面
浏览器->>前端开发服务器: ajax:http://localhost:8080/api/news
前端开发服务器->>后端测试服务器: 代理请求:http://test-data:3000/api/news
后端测试服务器->>前端开发服务器: JSON数据
前端开发服务器->>浏览器: JSON数据

为什么要Mock数据

sequenceDiagram
浏览器->>前端开发服务器: http://localhost:8080/
前端开发服务器->>浏览器: 页面
浏览器->>前端开发服务器: ajax:http://localhost:8080/api/news
前端开发服务器->>后端测试服务器: 代理请求:http://test-data:3000/api/news
后端测试服务器->>前端开发服务器: 404 (后端正在开发中)
前端开发服务器->>浏览器: 404
sequenceDiagram
participant 浏览器
participant MockJS
participant 前端开发服务器
activate MockJS
Note left of MockJS: 定义ajax拦截规则
deactivate MockJS
浏览器->>前端开发服务器: http://localhost:8080/
前端开发服务器->>浏览器: 页面
浏览器->>MockJS: ajax:http://localhost:8080/api/news
MockJS->>浏览器: 模拟的JSON数据

组件生命周期

image-20200908051939745 image-20201206132819263

常见应用

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

加载远程数据

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

直接操作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;
}
}

启动和清除计时器

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);
}
}

定义指令

全局定义

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
10
<template>
<!-- 某个组件代码 -->
<div>
<MyComp v-mydirec1="js表达式" />
<div v-mydirec2="js表达式">
...
</div>
<img v-mydirec1="js表达式" />
</div>
</template>

局部定义

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

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

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
<template>
<!-- 某个组件代码 -->
<div>
<MyComp v-mydirec1="js表达式" />
<div v-mydirec2="js表达式">
...
</div>
<img v-mydirec1="js表达式" />
</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>

指令配置对象

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

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
// 指令配置对象
{
bind(el, binding){
// el 是被绑定元素对应的真实DOM
// binding 是一个对象,描述了指令中提供的信息
}
}

bingding 对象

image-20210104174229660

查看更多bingding对象的属性

配置简化

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

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

},
update(el, bingding){

}
}

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

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

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

利用上述知识,可满足大部分自定义指令的需求

更多的自定义指令用法见官网

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

image-20210105161811637

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

image-20210105162109015

具体的做法非常简单:

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);
}
}

混入并不复杂,更多细节参见官网

文章列表页逻辑

路由跳转逻辑

image-20210107140253824

组件逻辑

image-20210107142310757

BlogList

image-20210107153623557

BlogCategory

image-20210107154531659

知识

动态路由

我们希望下面的地址都能够匹配到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" }

动态路由的导航

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>

编程式导航

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

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

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

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
}
}
}

vue_basic
http://example.com/2024/05/03/vue基础知识/
作者
John Doe
发布于
2024年5月3日
许可协议