为2019秋季学期软件工程项目所需,初步学习Vue.js框架。为啥选择Vue呢?首先是因为项目官网好看啦,他们官方手册的设计都已经成了一种常见的电子书风格,docsify和typora都有Vue风格的主题,至于这个以后大概还会写文章去记录。
基于Vue.js的很棒的组件库有:

最后我选择了前者,毕竟比较完整。回到软工项目,我们做了一个方便学习委员收集电子档文件的平台,叫做HUSTPORT,后端是队友用Flask写的,自己也学了一下Flask但是没有认真做笔记。之后大概会写一篇文章记录一下HUSTPORT的开发过程。

说来有趣,后来我将hustport.com这个域名用到了自己弄的论坛上。欢迎访问HUSTPORT | 学术交流论坛。之后大概还会写下论坛的由来,挖的坑太多,以后慢慢补。

Vue.js的学习主要参考了官方手册和Bilibili上的两门网课:

那现在开始吧 :-P

Vue.js概述

Hello World

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Vue Demo</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="name" placeholder="Your name">
        <h1>Good morning, {{ name }}</h1>
    </div>
    <script src="https://unpkg.com/vue/dist/vue.min.js"></script>
    <script>
        var app = new Vue({
            el: '#app',
            data: {
                name: '',
            }
        })
    </script>
</body>
</html>

该程序实现了双向数据绑定:Vue实例数据中的name变量将依赖于input标签改变(由v-model指定),而花括号中的name显示Vue实例数据中的name变量。因此在渲染得到的网页输入框中输入文本时,h1标签对应的标题文本会自动改变。

id为app的元素为Vue实例app的根容器,可以在Vue实例中通过以下语句指定根容器:

el: document.getElementById('app')  //或者是'#app'

Vue实例中的data也可以通过传入对象myData来实现,这时候myData.name和app.name默认建立了绑定关系。

生命周期

Vue实例从创建到销毁会经历一系列的过程,为了方便在合适的时机执行一些逻辑,可以使用生命周期钩子。

  • created 实例创建完后调用,但el尚未挂载
  • mounted el挂载到实例上后调用
  • beforeDestroy 实例销毁之前调用
var app = new Vue({
    el: '#app',
    data: {
        name: '',
    },
    created: function() {
        console.log(this.name);
    },
    mounted: function() {
        console.log(this.$el);
    }
})

$el直接访问id为app的元素,因此会在控制台上输出一段HTML代码。

插值与表达式

双大括号{{ }}可以在HTML中显示Vue实例中的数据,括号内可以插入单个表达式,但是不支持语句或流控制,还可以使用过滤器进行文本的处理。

如果确实想要在HTML中输出{{ }},则可以使用v-pre跳过编译:

<span v-pre> {{ Hello }}</span>

如果想要通过插值在DOM中插入一段HTML代码可以使用v-html:

<span v-html="link"></span>
data: {
    link: "<a href='https://baidu.com/''>Baidu</a>"
}

还可以在大括号中调用Vue实例methods中的方法,这时候需要加括号进行调用。

v-bind 与 v-on

v-bind用于动态更新HTML元素上的属性:

<a v-bind:href="url">Link</a>

url是Vue实例中的数据,当url改变时,该HTML标签就会随之改变。

v-on用于绑定事件监听器:

<button v-on:click="handleClick">ClickMe!</button>

点击后就会调用handleClick方法。除了click还有dblclick、keyup、mousemove等监听器。

注意双引号内调用方法时不需要加括号。引号内除了调用方法还可以使用内联语句比如赋值语句等。

为了简化代码的书写,可以使用单个冒号" : "来代替"v-bind: ",用"@"来代替"v-on: "。

特别注意

  1. JavaScript变量的作用域带来的问题:JS没有块作用域,有方法作用域,因此在嵌套调用方法时,在第二层的方法外面可以使用如下语句避免因作用域变动导致this所指的对象发生变化:
var _this = this;
  1. 什么时候需要括号?什么时候不需要括号?在插值的大括号中:方法需要括号,计算属性和变量不需要括号。在标签属性中:方法和变量都不需要括号。两种情况都不需要分号。

计算属性与方法

计算属性和方法的定义方式一模一样,只不过计算属性放在computed对象中,而方法放在methods对象中。此外计算属性不能传递参数。既然方法的功能更灵活,那为什么还需要计算属性呢?

原因在于计算属性是依赖缓存的。对于methods来说,任何变量发生变化,methods中所有的方法都会被重新执行;对于computed来说,只有某个计算属性依赖的变量发生变化,它才会被重新计算。因此对于处理大量数据的情况来说,使用计算属性能优化性能。

动态绑定样式

回想v-bind可以动态绑定属性,而HTML元素的CSS类和样式也是属性,因此就可以利用v-bind来动态绑定元素的样式。

<div id="app">
    <div class="static" :class="{ 'active': isActive, 'error': isError }"></div>
</div>
<script>
    var app = new Vue({
        el: "#app",
        data: {
            isActive: ture,
            isError: false
        }
    })
</script>

内层div元素始终属于static类,其是否属于active和error类视isActive和isError变量而定。这里用了v-bind的简化写法,就是在class属性名前面加一个冒号。这里用对象来给class赋值,称为“对象语法”,另外还有“数组语法”,此处略去。上面的div标签渲染之后的结果为:

<div class="static active"></div>

当class的表达式过长或者逻辑复杂时可以绑定一个计算属性返回一个对象,也可以放在data中。

使用v-bind:style还可以绑定内联样式,同样也可以放在data和computed中。绑定内联样式同样也有数组语法。

<div id="app">
  <div :style="{ 'color': color, 'fontSize': fontSize + 'px' }">TEXT</div>
</div>
<script>
  var app = new Vue({
    el: '#app',
    data: {
      color: 'red',
      fontSize: 14
    }
  })
</script>

内置指令

基本指令

初始化一个网页时可能会出现带有双大括号的插入指令,这是因为网页加载需要时间,而最开始DOM还没被替换。为了解决这个问题可以在根容器中加上v-cloak,并添加样式:

<style>
    [v-cloak] {
        display: none;
    }
</style>

<div id="app" v-cloak>
    <h1>Good morning, {{ name }}</h1>
</div>

在标签中加上v-once可以指定该元素或组件只渲染一次,即指定其为静态组件。

条件渲染指令

v-if,v-else-if,v-else可以根据其后跟着的表达式的真假值判断是否要渲染当前标签元素,注意v-else-if一定要紧跟v-if,v-else一定要紧跟v-else-if或v-if。注意JavaScript里面的三个等号。

<div v-if="status === 1"><p>Hello</p></div>

Vue在渲染元素时会尽可能地复用元素,可以用key来指定元素从而决定是否要复用元素。详见Vue手册

v-show和v-if的用法基本一致,只不过v-show只是改变元素的CSS属性display来确定是否显示该元素。使用v-show时就算没有显示元素,在网页检查元素工具中仍然能看到该元素。总而言之,使用v-show时,元素总是会被编译。

v-if的开销较大,而v-show的开销较小,后者更适用于频繁切换的场景。

注意:v-show不能在template中使用。

列表渲染指令

v-for用于对数组或对象数据进行迭代处理,如下例中books是Vue实例中的数组数据,可以在HTML中使用v-for:

<ul>
  <li v-for="book in books">{{ book.name }}</li>
</ul>

book是当前数组元素的别名,in是分隔符,也可以用of来代替in作为分隔符。

支持一个可选参数作为当前项的索引:

<ul>
  <li v-for="(book, index) in books">{{ index }} - {{ book.name }}</li>
</ul>

v-for也可以用于template,对一组元素进行渲染。v-for可以用于对象,此时有两个可选参数,分别是键名和索引:

<ul>
  <li v-for="(value, key, index) in user">
    {{ index }} - {{ key }}: {{ value }}
  </li>
</ul>

数组更新等内容可参考链接中官方手册。

注意:v-for不要与v-if一同使用

事件监听

v-on指令用于事件监听。常用的事件句柄有:

属性 描述
onclick 单击事件
ondblclick 双击事件
onkeydown 某个键盘的键被按下
onkeypress 某个键盘的键被按下或按住
onkeyup 某个键盘的键被松开
onmousedown 某个鼠标键被按下
onmousemove 鼠标移动
onmouseout 鼠标从某元素移开
onmouseover 鼠标被移到某元素之上
onsubmit 提交按钮被点击

可以使用修饰符:

  • .stop 阻止事件
  • .prevent 阻止默认事件
  • .self 事件发生在该元素本身时触发
  • .once 只触发一次

监听键盘事件时可以使用按键修饰符:

<input @keyup.13="submit">

按键修饰符提供了一些快捷名称:

  • .enter
  • .tab
  • .delete
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

一下修饰符可以组合使用:

  • .ctrl
  • .alt
  • .shift
  • .meta (Mac中时Command, Windows下是窗口键)
<div @click.ctrl="handle">Handle</div>

表单输入绑定

v-model指令可以用来实现双向的数据绑定。参见表单输入绑定

组件概述

组件示例

组件是可复用的Vue实例,可以通过component来定义:

Vue.component('button-counter', {
    data: function() {
        return {
            count: 0
        }
    },
    template: "<button @click='count++'>You clicked me {{ count }} times.</button>"
})

定义之后可以在任何的new Vue创建的根实例对应的根容器中,把button-counter当作自定义组件来使用。由于组件也是Vue实例,因此也可以接受data、computed、methods和生命周期钩子等选项,el除外(根实例特有)。

要注意data不像在Vue的根实例中,它必须定义为函数。因为JS没有块级作用域,必须通过函数返回才能建立自己的作用域,从而在进行组件的复用时不会出现数据相互影响的情况。

组件注册

通过Vue.component可以进行全局注册,第一个参数就是组件的名称。全局注册后组件可以在任何新的Vue根实例中,也可以在其他的子组件中使用。但是全局注册会导致性能的降低,所以对于一些不常用的组件常使用局部注册。

var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }

new Vue({
  el: '#app',
  components: {
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})

局部注册的组件在其子组件中不可用。这时候ComponentA和ComponentB都能在根实例中使用,但是ComponentA不能在ComponentB中使用,必须进行注册。

var ComponentA = { /* ... */ }

var ComponentB = {
  components: {
    'component-a': ComponentA
  },
  // ...
}

模块系统

在使用webpack时(使用模块系统风格),在一个组件中对另一个组件进行注册的代码像这样:

import ComponentA from './ComponentA.vue'

export default {
  components: {
    ComponentA
  },
  // ...
}

注意在新的ES版本中在对象中放置变量名 ComponentA 实际上是 ComponentA: ComponentA 的缩写。这意味着ComponentA同时是用在模板中的自定义元素的名称包含了这个组件选项的变量名

在用Vue脚手架搭建工程时会在 src 目录下有 components 目录,用于存放各个组件,文件后缀为vue。于是可以在局部注册之前导入相应的组件:

import ComponentA from './ComponentA'
import ComponentC from './ComponentC'

export default {
  components: {
    ComponentA,
    ComponentC
  },
  // ...
}

在JavaScript中export用于导出模块。export和export default的区别在于:前者可以在文件中到处多个模块,而在引入模块时必须使用原有的名字;后者在文件中只能导出一个模块,该模块可以是匿名模块,引入时必须为其指定名称。

在Vue脚手架中由于添加了相关的配置,在导入模块时可以用"@"来代替"src",各个组件文件的后缀".vue"、 ".js"、 ".json"等可以不用加。参考这里。在Vue脚手架中,单个".vue"文件就代表一个组件,组件可以被分离成三个部分,分别代表HTML模板(也就是template),处理逻辑(写在script中),样式(写在style中)。例如:

<template>
  <div id="app">
    <img src="./assets/logo.png">
    <Ping/>
  </div>
</template>

<script>
import Ping from './components/Ping'

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

这个文件名为App.vue,此时Ping就是App的子组件,二者的关联表现在App.vue用import引入Ping组件,在template中又调用了Ping组件。

通过Prop向子组件传递数据

在上面的例子中,设想这样的情景:在父组件App的template中多次调用了子组件Ping,而每个Ping中都要展示相同的数据,或者同一个数据集合(比如数组)的不同部分。为了处理这种情况,可以将子组件共有的数据存放在父组件中,通过某种手段将数据从父组件传递到子组件。在Vue中这种手段就是Prop传值。注意:父组件可以是根实例。

比如注册了这样一个文章组件用于显示每篇文章的标题:

Vue.component('blog-post', {
  props: ['title'],
  template: '<h3>{{ title }}</h3>'
})

这里 template 中的 title 和 props 中的 title 对应。而文章数据列表存放在Vue根实例中:

new Vue({
  el: '#blog-post-demo',
  data: {
    posts: [
      { id: 1, title: 'My journey with Vue' },
      { id: 2, title: 'Blogging with Vue' },
      { id: 3, title: 'Why Vue is so fun' }
    ]
  }
})

因此想要在根容器(根实例对应的HTML模板)中调用blog-post,必须给它传递数据。那怎么样来传递数据呢?当然是在blog-post标签的尖括号内填入类似于属性的键值对实现传值。例如上面两段代码对应的template是:

<blog-post
  v-for="post in posts"
  v-bind:title="post.title"
></blog-post>

这里的posts对应的是根实例中data对象里面的数组posts,就是父组件(根实例)的数据;title则对应blog-post组件中props列表内的变量名;post.title对应根实例中相应的数据。注意这里一定要用"v-bind:"或":",因为其实质仍是动态绑定属性。一定要十分清楚变量之间的对应关系。可以到这里试一下,不过不能用"key",似乎会发生关键字冲突。

进一步了解Prop

props一般写成数组形式。

Prop既可以用来传递静态数据,也可以用来传递动态数据。不使用v-bind时传递的总是静态的字符串;如果要传递整型、数组、布尔值、对象等静态数据,则必须使用v-bind,不然会被认为是字符串;传递动态数据一定要使用v-bind。

参考这里

当父组件数据data中的数组、对象通过v-bind传递给子组件的时候传递的实际上是引用,而数字、字符串、布尔值传递的是值。对于传递引用的情况来说,如果在一个子组件中改变了传入的Prop,那么在另一个子组件中该Prop也会发生改变,这不一定是我们想要的效果。

为了解决这个问题,实现从父组件到子组件数据的单向流动,也就是实现单行向下绑定(父级 prop 的更新会向下流动到子组件中,但是反过来则不行),可以针对两种不同的应用场景,使用两种不同的方法。

第一种场景是Prop传来一个初始值,而子组件希望把它作为本地的数据自己使用。这时候可以将Prop进来的数据在data中用函数处理一下,得到一个本地的值,比如下面的例子把传进的initCounter转变成了本地的Counter:

props: ['initialCounter'],
data: function () {
  return {
    counter: this.initialCounter
  }
}

第二种场景是Prop传来一个初始值,本身必然就需要被处理。这时候Vue官方推荐使用计算属性:

props: ['width'],
template: '<div :style="style">Content</div>',
computed: {
  style: function () {
    return {
      width: this.width + 'px'
    }
  }
}

这里其实就是将Prop传入的数字width转为带px后缀的字符串,同样也是需要改变Prop(改变Prop传入的数据)。

这两种方法本质上都是利用JavaScript中的函数来形成局部的作用域,从而避免在子组件中改变数据引发父组件数据改变的后果。目前也不太清楚为什么这里要分两种情况来处理。

Prop-验证非Prop的特性可参考官方指南。

单个根元素

template中的必须只有单个根元素,可以用div将它们括起来。如果其中的内容比较多,可以使用模板字符串或折行转义字符避免报错。可以参考这里

监听子组件事件

考虑这样一种场景:子组件是按钮组件,需要通过子组件按钮来改变父组件data中变量的值并通过{{ }}显示出来。在这种场景下,需要将信息从子组件传递到父组件,这种信息是事件信息。在Vue中可以通过自定义事件来实现。

例如下面的代码中,子组件是两个按钮,分别对应数字的增减,总数存在父组件中。在子组件模板中监听点击事件,触发两个不同的函数handleIncrease和handleReduce,这两个函数能够改变子组件作用域内的counter,希望在调用函数的时候能通过自定义事件将该counter传递给父组件的total。可以用$emit来注册自定义事件increase和reduce,并将子组件内的this.counter作为参数传出去。一旦点击+1的按钮就会触发handleIncrease函数,从而触发父组件中监听的increase事件,父组件同时也收到了传入的参数(子组件的counter),这时候触发父组件的函数handleGetTotal,子组件的counter作为名为total的参数传入,最终改变了父组件中total的值。

<div id="app">
    <p>TotalNumber: {{ total }}</p>
    <my-component
        @increase="handleGetTotal"
        @reduce="handleGetTotal"></my-component>
</div>
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<script>
    Vue.component('my-component', {
        template: '\
        <div>\
            <button @click="handleIncrease">+1</button>\
            <button @click="handleReduce">-1</button>\
        </div>',
        data: function () {
            return {
                counter: 0
            }
        },
        methods: {
            handleIncrease: function () {
                this.counter++;
                this.$emit('increase', this.counter);
            },
            handleReduce: function () {
                this.counter--;
                this.$emit('reduce', this.counter);
            }
        }
    });

    var app = new Vue({
        el: '#app',
        data: {
            total: 0
        },
        methods: {
            handleGetTotal: function (total) {
                this.total = total;
            }
        }
    })
</script>

插槽与内容分发

定义这样一个子组件,在子组件中插入了slot插槽。

Vue.component('alert-box', {
  template: `
    <div class="demo-alert-box">
      <strong>Error!</strong>
      <slot></slot>
    </div>
  `
})

在调用子组件时,加入自定义的内容:

<alert-box>
  Something bad happened.
</alert-box>

最终渲染结果如下:

<div class="demo-alert-box">
  <strong>Error!</strong>
  Something bad happened.
</div>

以上演示的是默认插槽。如果有多个插槽,需要用到具名插槽,在slot中用name来指定。在模板中:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

在调用时使用v-slot来指定插槽名称:

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

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

为指定名称的全部放入默认的没有名称的插槽中。如果没有默认插槽,那么传入的未指定插槽名称的内容会被丢弃。最后的渲染结果如下:

<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

注意:v-slot只能放在template中,仅有一个例外,参见这里

路由基础

JavaScript中的箭头函数

ES6中的箭头函数实际上就是一种简洁的定义函数的方式。例如下面两种函数定义方法时等价的。箭头前面的括号填写参数,当参数只有一个时,可以省略括号。

const sum = function (num1, num2) {
  return num1 + num2
}
const sum = (num1, num2) => {
  return num1 + num2
}

对于函数体只有一行的函数,还可以简写成如下形式:

const sum = (num1, num2) => num1 + num2

一般什么时候会用箭头函数呢?在函数作为一个参数传入另一个函数时常常会用到箭头函数。

Vue项目的整体目录结构

在使用脚手架创建项目之后,主要有这样一些结构:

>build\ 存放webpack配置信息的目录

>config\ 存放其他配置信息的目录

>node_modules\ 存放库文件的目录

>src\ 资源目录,我们代码的编辑主要就在这里面进行

>static\ 静态资源,可以放图片之类的资源,build之后会整体打包到dist目录中

>index.html 网站主页

在src目录下可以安排这样的目录结构:

>assets\ 存放资源的目录,可以在里面创建css文件夹和img文件夹分别存放样式和图片

>components\ 存放组件(.vue),可以在这里存放公共组件

>views\ 存放组件(.vue),在这里存放主要视图比如主页、关于等

>router\ 存放路由相关配置

>App.vue 根组件

>main.js 根实例

路由与前端发展历程

路由实际上就是辅助进行信息转发的映射表。在早期的Web开发中,常用后端渲染和后端路由技术,后端渲染就是网页在服务器中已经渲染好,最终只有HTML和CSS发送到客户端浏览器,而后端路由就是后端来处理URL和网页文件的映射关系。后端路由有非常明显的缺点:前后端耦合严重,很多前端工作必须由后端人员来做,维护困难。

为了解决这个问题,Web开发进入了前后端分离阶段:后端只负责提供数据,不负责任何界面的内容。可以将整个Web系统分为四个模块:浏览器客户端、静态资源服务器、提供API接口的服务器、数据库。访问网页的整个过程如下:浏览器先通过URL访问静态资源服务器,这时候会获得HTML+CSS+JS,前两个直接渲染,最后一个执行的时候会遇见API的URL,从而进一步去提供API接口的服务器获得数据(Ajax请求),数据返回客户端后继续进行渲染。这个阶段最大的特点就是前端渲染,前后端划分清晰,后端专注数据处理,前端专注交互和可视化。这种情况下API接口能提供给不同客户端比如网页端、移动端,因为他们都用同一套接口,后端无需关注过多。

第三个阶段时单页面富应用阶段(SPA),就是在上一个阶段上添加了前端路由功能。这时候整个网站只有一个HTML页面,输入网址时会将网站的所有资源都下载下来,通过前端路由配置的映射关系来处理页面关系。一旦输入一个子URL,浏览器就会从资源中抽取对应的资源显示出来,而不用重新向服务器进行请求。在Vue中,这些页面就是一个个组件。

如何更改URL而使页面不进行刷新

页面的刷新就是浏览器重新向服务器请求页面资源,有两种方法可以更改URL而使页面不进行刷新。

一种是hash函数的方式:打开审查元素工具,在console中输入 location.hash = 'test' 即可更改页面的URL,其中的test可以是任何值。从Network可见此时没有请求新的资源。

另一种是HTML5下的history模式:在console中输入 history.pushState({}, '', 'test') 即可更改页面URL而不进行刷新,pushState实际上是利用栈结构进行处理,能记录多个URL。也可以输入history.replaceState({}, '', 'test') ,但这种方式并不是利用栈结构。可以用history.back()来回退,history.forward()来前进,这两个接口其实就是浏览器的后退前进功能。pushState处理后可以进行后退前进,但是用replaceState后就不行了。

vue-router的安装和使用

npm install vue-router --save

第一步:导入路由对象,调用Vue.use(VueRouter)

第二步:创建路由实例,并且传入路由映射配置

第三步:在Vue实例中挂载创建的路由实例

在创建Vue项目的时候选择router系统就会帮忙进行创建路由。在src下创建router文件夹,在文件夹下创建index.js文件,用来存放所有的路由配置。在index.js中:

import VueRouter from 'vue-router'
import Vue from 'vue'

//1.通过Vue.use()来安装插件
Vue.use(VueRouter)

//2.创建VueRouter对象
const routes = [

]

const router = new VueRouter({
  //配置路由和组件之间的映射关系
  routes  //ES6语法:为了代码整洁直接传入所需的数组,数组在外面定义
})

//3.将router对象挂载到Vue实例中
//需要在本文件中导出,在main.js中通过import导入,然后在Vue实例中通过router: router来调用
//回忆ES6语法,router: router又可以直接写成router
export default router

如何使用vue-router?

第一步:创建路由组件

第二步:配置路由映射:组件和路径映射关系

第三步:使用路由:通过

首先我们先创建路由组件,比如可以创建Home.vue:

<template>
  <div>
    <h1>I'm Home!</h1>
  </div>
</template>

<script>
  export default{
    name: "Home"
  }
</script>

<style>
</style>

类似地还可以创建About.vue。之后可以在router/index.js中配置路由映射,需导入组件并更改数组配置:

import Home from '../components/Home'
import About from '../components/About'

const routes = [    //数组中一个对象对应一个组件
  {
    path: '/home',
    component: Home
  },
  {
    path: '/about',
    component: About
  }
]

但现在这两个组件中的内容还不能显示,因为没有被调用,所以需要在App.vue组件中进行调用。为什么在App.vue中的组件能够显示呢?因为在main.js的根实例中有render: h => h(App),可见App组件会被渲染。在App.vue的template中:

<div id="app">
  <router-link to="/home">Home</router-link>
  <router-link to="/about">About</router-link>
  <router-view></router-view>
</div>

router-link和router-view是vue-router全局注册的组件,在任何地方都可以用。在上面的代码中如果只有router-link是不够的,他们只能更改URL,但是还没有指定组件的内容显示在什么地方。这时候需要用router-view来指定组件显示的位置。其占位功能类似于插槽。

路由细节处理

在上面得到的结果中访问网站时并不会出现首页视图内容,需要点击首页才行,这是因为还没有配置默认路由。可以在routes数组中配置默认路由:

{
    path: '/',
    redirect: '/home'
}

现在还有一个问题,由于现在的路由用的是hash模式所以会出现井号,需要在index.js文件中创建VueRouter实例时指定使用history模式:

const router = VueRouter({
  routes,
  mode: 'history'
})

router-link会默认被渲染成a标签,也可以对其设置tag属性渲染成button或者li等:

<router-link to='/home' tag='button'>Home</router-link>

router-link默认用pushState来更改URL,因此浏览器中可以后退前进。如果希望取消后退前进功能,可使用replace模式:

<router-link to='/home' tag='button' replace>Home</router-link>

还可以在router-link中添加active-class属性指定当前显示组件对应的导航项的样式。也可以在VueRouter实例中指定linkActiveClass属性来更改。

代码实现路由跳转

事实上也可以不用router-link标签来实现路由跳转,这样可以更灵活地自定义我们的导航。比如使用button进行跳转:

<button @click="homeClick">Home</button>
<button @click="aboutClick">About</button>

再实现监听调用的这两个函数(在export default中的methods中实现):

homeClick () {
  this.$router.push('/home')
}

这时候使用pushState来跳转的,可以在上面讲push改为replace即可实现取消后退前进。

基于axios请求

axios的基本使用

npm install axios --save

可以用下面的代码进行一个GET请求(默认是GET请求):

axios({
    url: 'https://jsonplaceholder.typicode.com/posts'
}).then(res => {
    console.log(res);
})

还可以在配置中指定请求的方式,如果GET有参数还可以指定参数(就是URL的问号后面部分):

axios({
    url: 'https://jsonplaceholder.typicode.com/posts',
  method: 'get',
  params: {
    type: 'pop',
    page: 1
  }
}).then(res => {
    console.log(res);
})

可以挂在到相应组件中生命周期的created钩子下面,可以附带catch捕捉错误:

export default {
  name: 'app',
  created () {
    axios({
      url: 'https://jsonplaceholder.typicode.com/posts'
    }).then(res => {
      console.log(res);
    }).catch(err => {
      console.log(err);
    })
  }
}

发送并发请求

axios.all([axios({
  url: 'https://jsonplaceholder.typicode.com/posts'
}), axios({
  url: 'https://jsonplaceholder.typicode.com/posts'
})]).then(res => {
  console.log(res);
})

当两个请求都拿到之后才会执行then。注意axios.all返回的是一个数组,可以用spread将其分开来,比如:

axios.all([axios({
  url: 'https://jsonplaceholder.typicode.com/posts'
}), axios({
  url: 'https://jsonplaceholder.typicode.com/posts'
})]).then(axios.spread((res1, res2) => {
  console.log(res1);
  console.log(res2);
}))

当然也可以用res[0]和res[1]来取得结果。

补充ES6的语法:对象和数组的解构。

const obj = {
  name: 'John',
  age: 20
}
const {name, age} = obj;

const names = ['John', 'Bob', 'Tom']
const [name1, name2, name3] = names;

axios的全局配置与实例创建

有些公共的参数还有baseURL等可以统一进行设置,可以放在main.js中。比如:

axios.default.baseURL = 'https://jsonplaceholder.typicode.com'
axios.default.timeout = 5000    //毫秒为单位

一旦设置了baseURL,那么在axios中可以使用'/posts'作为URL。

强调两点,在进行GET请求时,参数放入params对象中,而进行POST请求时,请求体放入data对象中。比如:

params: {
  id = 2
}
data: {
  key = 'aa'
}

在实际使用中一般不会用全局配置,因为那样的话就只能到同一个服务器中请求数据。可以创建axios实例,在实例中进行局部配置,并调用axios实例发送请求。

const instance1 = axios.create({
  baseURL: 'https://jsonplaceholder.typicode.com',
  timeout: 5000
})

instance1({
  url: '/posts'
}).then(res => {
  console.log(res);
})

axios的封装

一定不要在组件代码中直接引入axios并调用!如果自己的项目代码中很多组件都用了某个第三方组件,假设有一天该组件不再维护,则项目代码需要在很多地方改动,这是一个严重的后果。所以在任何时候,使用第三方组件都应该对其进行进一步的封装。在Vue项目的src文件夹下,可以创建network文件夹放置网络请求相关配置文件,可以创建request.js文件来放置我们对axios的封装。

由于封装的结果不止一个,类似于创建axios实例,我们不用export default,而使用export function。


Le vent se lève, il faut tenter de vivre.