单文件组件

在很多 Vue 项目中,我们使用 Vue.component 来定义全局组件,紧接着用 new Vue({ el: '#container '}) 在每个页面内指定一个容器元素。

这种方式在很多中小规模的项目中运作的很好,在这些项目里 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

文件扩展名为 .vuesingle-file components(单文件组件) 为以上所有问题提供了解决方法

1
2
new Vue({
}).$mount('#app')

等同于

1
2
3
new Vue({
el:'#app'
})

运行项目

1
npm run serve

初步使用

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
<!-- App.vue -->
<template>
<div>
hello vue
<input type="text" />
<button @click="handlerAdd()">add</button>

<ul>
<li v-for="data in datalist" :key="data">
{{ data }}
</li>
</ul>
</div>
</template>

<script>
export default {
data () {
return {
datalist: ['1', '2', '3', '4']
}
},
methods: {
handlerAdd () {
console.log('1111111111')
}
}
}
</script>
1
2
3
4
// main.js
new Vue({
render: h => h(App)
}).$mount('#app')
  • <template> html代码 , 最多可以包含一个
  • <script> js代码 , 最多可以包含一个
  • <style> css代码,可以包含多个 , src路径是相对的
    • 加上scoped属性 , css局部生效 (这样局部不会覆盖全局 , 全局也不会覆盖局部)
    • 加上lang=”scss” , 可以支持scss

scoped的本质 :

添加完scoped

1
<div data-v-19f8877c="" > sidebar </div>

发现多了一个属性 , 这个属性就是scoped添加的 . 也就是说 , 此时的component是根据这个唯一的属性设置的样式 , 此时的css选择器是

1
2
3
div[data-v-19f8877c]{

}

这样 , 局部不会影响全局 , 全局也不会影响局部

使用component

在component目录创建两个文件navibar和sidebar

1
2
3
4
5
<template>
<div>
navibar
</div>
</template>
1
2
3
4
5
<template>
<div>
sidebar
</div>
</template>

全局组件

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<navibar></navibar>
<sidebar></sidebar>
</div>
</template>

<script>
import navibar from './components/navibar'
import sidebar from './components/sidebar'
import Vue from 'vue'

Vue.component('navibar', navibar)
Vue.component('sidebar', sidebar)

export default {
}
</script>

局部组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
import navibar from './components/navibar'
import sidebar from './components/sidebar'

export default {
data () {
return {
datalist: ['1', '2', '3', '4']
}
},
methods: {
handlerAdd () {
console.log('1111111111')
}
},
components: {
'navibar': navibar,
'sidebar': sidebar
}
}
</script>

vue.config.js

vue.config.js 是一个可选的配置文件,如果项目的 (和 package.json 同级的) 根目录中存在这个文件,那么它会被 @vue/cli-service 自动加载。

See Configuration Reference.

devServer.proxy

支持跨域请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
devServer: {
proxy: {
'/api': {
target: '<url>',
ws: true,
changeOrigin: true
},
'/foo': {
target: '<other_url>'
}
}
}
}

上面的配置和nginx的差不多

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
devServer: {
proxy: {
'/ajax': {
target: 'http://m.maoyan.com/',
ws: false,
changeOrigin: true
},
}
}
}

将路径为/ajax代理到http://m.maoyan.com/

这样就可以使用反向代理了 :

1
axios.get('/ajax/movieOnInfoList').then(ret => { console.log(ret) })

Vue.router

功能 : 构建单页面应用(SPA)

初步使用

Vue.router是标准的MVC模式 :

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
24
25
26
27
28
29
30
31
32
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home'
import Film from '@/views/Film'

Vue.use(VueRouter)

const router = new VueRouter({
// mode: 'history',
// base: process.env.BASE_URL,
const router = new VueRouter({
// mode: 'history',
// base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/film',
name: 'file',
component: Film
},
{
path: '*',
redirect: '/film',
}
]
})

export default router

views/Home.vue , views/Film.vue

1
2
3
4
5
<template>
<div class="about">
<h1>This is an film page</h1>
</div>
</template>

App.vue

1
2
3
4
5
<template>
<div>
<router-view></router-view>
</div>
</template>

<router-view> 为路由容器

main.js

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

Vue.config.productionTip = false

new Vue({
router: router,
// store,
render: h => h(App)
}).$mount('#app')

接下来访问<http://localhost:8080/#/film> 即可访问firm页面

简单使用

HTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>

<div id="app">
<h1>Hello App!</h1>
<p>
<!-- 使用 router-link 组件来导航. -->
<!-- 通过传入 `to` 属性指定链接. -->
<!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>

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
31
// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)

// 1. 定义 (路由) 组件。
// 可以从其他文件 import 进来
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]

// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
routes // (缩写) 相当于 routes: routes
})

// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
router
}).$mount('#app')

// 现在,应用已经启动了!

通过注入路由器,我们可以在任何组件内通过 this.$router 访问路由器,也可以通过 this.$route 访问当前路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Home.vue
export default {
computed: {
username() {
// 我们很快就会看到 `params` 是什么
return this.$route.params.username
}
},
methods: {
goBack() {
window.history.length > 1 ? this.$router.go(-1) : this.$router.push('/')
}
}
}

url-for的反向解析

  • 声明式导航 : 使用<a><router-link>更换路径
  • 编程式导航 : 使用js的location.url='XXX' this.$router.push(XXX)更换路径
1
2
3
4
5
6
7
8
9
10
11
const router = new VueRouter({
routes: [
{
path: '/film',
name: 'file',
component: Film
}
]
})

export default router

中 :

1
<router-link to="/film">film</router-link>

等同于

1
<a href="/#/film">film</a>

<router-link>的额外功能

  • <router-link> 对应的路由匹配成功,将自动设置 class 属性值 .router-link-active

  • tag : 将这个组件渲染成某个标签

    1
    <router-link to="/film" tag="li" >film</router-link>

    将film这个组件渲染成li标签

  • activeClass : 自动添加激活class , 方便添加高亮显示

    1
    2
    3
    4
    5
    6
    7
    <router-link to="/film" activeClass ="myactiveClass" >film</router-link>

    <style>
    .myactiveClass {
    cloor: red;
    }
    </style>

二级路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const router = new VueRouter({
// mode: 'history',
// base: process.env.BASE_URL,
routes: [
{
path: '/film',
name: 'file',
component: Film,
children: [
{
// 放在Film目录下
path: 'Film/nowplaying',
component: Nowplaying,
},
{
path: 'Film/comingsoon',
component: Comingsoon
}
]
}
]
})

之后可以在views文件夹路创建一个Film文件夹 , 存放Nowplaying.vueComingsoon.vue

反向解析

1
<router-link to="/film/nowplaying">nowplayingr</router-link>

url重定向

访问/film的时候自动重定向到/film/nowplaying

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const router = new VueRouter({
routes: [
{
path: '/film',
name: 'file',
component: Film,
children: [
{
path: 'nowplaying',
component: Nowplaying,
},
{
path: 'comingsoon',
component: Comingsonn
},
{
path: '',
redirect: 'film/nowplaying'
}
]
}
]
})

动态路由

编程式导航

1
2
3
4
5
6
7
8
export default {
methods: {
changepage () {
// 跳转到`/home`页面
this.$router.push('/home')
}
}
}

所谓的动态路由就类似于

1
2
3
@project_router.route("/download/<string:record_type>", methods=['GET'])
def download_excel(args, record_type):
pass

中的<string:record_type>

detail_id 类似于 record_type

1
2
3
4
5
6
7
const router = new VueRouter({
routes: [
{
path: '/detail/:detail_id',
name: 'detail',
component: detail,
})

然后我们就可以在views/detail.vue里使用

1
2
3
4
5
6
7
8
export default {
methods: {
mounted () {
console.log('当前的router就是:',this.$route)
console.log('当前detail_id就是:',this.$route.params.detail_id)
}
}
}

命名路由

就是给一个name

1
2
3
4
5
6
7
const router = new VueRouter({
routes: [
{
path: '/film/:myid',
name: 'file',
component: Film,
})

所以我们就可以以更加方便的形式进行url的反向解析

1
2
3
4
5
6
7
8
9
10
export default {
methods: {
changepage (myid) {
// 使用路径跳转
this.$router.push('/film/${myid}')
// 使用name跳转
this.$rouer.push({name:'film',params:{id:id}})
}
}
}

history 模式

<http://localhost:8080/#/film> , 发现有一个很丑的/#/ , 如果想取消这个/#/ , 只要设置history模式即可

1
2
3
4
5
6
7
8
9
10
const router = new VueRouter({
mode: 'history',
routes: [
{
path: '/film',
name: 'file',
component: Film
}
]
})
  • vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。

  • 如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。

  • 对于vue这类渐进式前端开发框架,为了构建 SPA(单页面应用),需要引入前端路由系统,这也就是 Vue-Router 存在的意义。前端路由的核心,就在于改变视图的同时不会向后端发出请求

  • 简单来说:

    • hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页
    • 也就是说 hash 出现在 URL 中,但不会被包含在 http 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面;
    • 同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。

路由守卫(路由拦截)

比如说 : 访问个人中心页面 , 如果你未登录 , 就会拦截本次路由 , 将其重定向到注册页面.

全局

在router.index.js添加以下内容 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const router = new VueRouter({
routes: [
{
path: '/',
name: 'home',
component: Home
}
]
})

router.beforeEach((to, from, next) => {
if(to.path === '/user'){
if(已经登录){
next();
}else{
next('/login');
}
}
})

export default router
  • to: Route: 即将要进入的目标 路由对象
  • from: Route: 当前导航正要离开的路由
  • next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
    • next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
    • next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
    • next(‘/‘) 或者 next({ path: ‘/‘ }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置诸如 replace: true、name: ‘home’ 之类的选项以及任何用在 router-link 的 to prop 或 router.push 中的选项。
    • next(error): (2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。

局部(某个路由独享的守卫)

1
2
3
4
5
6
7
8
9
10
11
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})

这些守卫与全局前置守卫的方法参数是一样的。

组件内的守卫

在路由组件内直接定义以下路由导航守卫:

  • beforeRouteEnter
  • beforeRouteUpdate
  • beforeRouteLeave
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}

所以前面拦截进入用户中心的功能 , 可以在user.vue你添加

1
2
3
4
5
6
7
8
9
10
11
<script>
export default {
beforeRouteEnter (to, from, next) {
if(已经登录){
next();
}else{
next('/login');
}
}
}
</script>

beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。

完整的导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用离开守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫 。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

状态管理 Vuex

  • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式
  • 它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
  • 简单来说 , Vuex就类似于python的全局命名空间 , 用于存储各个变量 . 接着我们就可以管理这些变量了

state

使用Vuex, 需要创建一个store/store.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
state: {
// 自定义的共享状态
isTabbarShow: true
},
mutations: {
},
actions: {
},
modules: {
}
})

然后在main.js里初始化

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

Vue.config.productionTip = false

new Vue({
router: router,
store: store,
render: h => h(App)
}).$mount('#app')

最后我们就可以使用$store.state.isTabbarshow来获取了 , 这是属性是可读可写

Mutations

上面的操作有效 , 但是危险 . 因为state是全局的 , 而且是可读可写的 , 一旦有谁修改 , 就很难追踪 . 所以必须使用Mutations来间接修改 , 因为Mutations可以用Devtools来监控 .

vuex

所以 , 上面的操作可以修改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
state: {
isTabbarShow:true
},
mutations: {
getTabbarShow(state,data){
// 这里应该是修改状态唯一地方
state.isTabbarShow = data;
}
},
actions: {
},
modules: {
}
})

接着我们就可以调用this.$store.commit('getTabbarShow',false)来修改

actions

异步处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
state: {
isTabbarShow:true
},
mutations: {
getTabbarShow(state,data){
// 这里应该是修改状态唯一地方
state.isTabbarShow = data;
}
},
actions: {
GetComingListAction(store){
axios.get(...).then(store.commit('getTabbarShow',true))
}
},
modules: {
}
})

接着 , 就可以使用this.$store.dispatch('GetcomingListAction')来执行异步操作

目前看到 https://www.bilibili.com/video/av81318072?p=85 , 来年继续

常见问题及其解决方案

取消全局监听事件

因为是单页面应用 , 所以监听事件是全局的 , 如果希望监听事件只存在于某个router , 可以使用beforeDestory

1
2
3
4
5
6
mouted(){
window.onscroll = this.mymethod;
}
beforeDestory(){
window.onscroll = null
}

异步请求数据渲染错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<h1>{{ film.name }}</h1>

<script>
import axios from 'axios'

export default {
data () {
return {
film: null
}
},
methods: {
getdata () {
axios.get('/ajax/movieOnInfoList').then(ret => { this.film = ret })
}
}
</script>

上面操作会引发错误 , 一开始异步数据还没请求到 , 此时的film是null , 渲染{{film.name}}出错

解决方法 , 使用v-if

1
<h1 v-if="film">{{ film.name }}</h1>

在模块化开发使用中央事件总线

所有的router必须共用一个bus , 因此我们不能在APP.vue里定义bus .

正确的做法是创建一个bus/bus.js , 然后export bus , 有谁需要就导入谁