《Vue》生态及实践

VueX的设计及使用

简介

VueX时一个专为Vue.js应用程序开发的状态管理模式,它采用集中式存储管理应用的所有组件状态,并以相应的规则,保证状态以一种可预测的方式发生变化;

换句话说,Vuex采用类似全局对象的形式来管理所有组件的公共数据,如果想修改这个全局对象的数据,得按照Vuex提供的方式来修改,不能随意用自己的方式修改;(通俗点讲:创建了一个集中的数据存储的store,这个store挂载在顶层的Vue上面,正因为挂载的地方是顶层,所以创建的所有组件都是其子组件,也因此所有的组件都可以访问这个store;),因为通过规定的样式进行操作,因此vuex的修改是可以通过devtools进行追踪的,这样大大方便了开发;

Vuex是响应式的,数据的变化会实时同步到组件中,而组件内数据的变化也会同步变更到UI上;

安装与使用

初始化项目时会有选择是否安装Vuex,如果初始化时未安装,那么之后再安装需要输入命令

npm install vuex --save
//全局使用的话必须通过Vue.use()安装
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

基础知识

VueX默认有五种基本对象,分别是:stategettersmutationsactionsmodules

state

用于存储数据,这个对象用来存储状态,数据的,state是Vuex中唯一的数据源,因此其他所有获取数据的方式都是从state中获取的数据,提交,修改也是将数据提交,修改state中对应的数据;

computed: {
  count () {
    return this.$store.state.count
  }
}

辅助函数mapState
辅助函数mapState可以再不影响功能的前提下减少写的代码量

import { mapState } from 'vuex'

computed: {
   ...mapState({
   	count2:state => state.count
   })
}

通过展开运算符直接将state中的count属性取出来,之后通过count2可以使用了,假如我们命名的计算属性名和state中的对象名是完全相同的,比如下例

import { mapState } from 'vuex'

computed: {
   ...mapState({
   	count:state => state.count
   })
}

计算属性中的属性名和state中的属性名都是count,那么就可以再简写一步

import { mapState } from 'vuex'

computed: {
   ...mapState(['count'])
}

getters

有时候需要从state中获取的数据是需要经过转换过滤的,那么就可以将过滤的逻辑写在getters中

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})

获取数据的时候就不再是从state中获取了,就可以直接从getters中获取筛选后的数据

computed: {
  count () {
    return this.$store.getters.doneTodos
  }
}

辅助函数mapGetters
用法和mapState基本一致,下例是计算属性的属性名和getters中的某各属性名完全相同时可以直接写

import { mapGetters } from 'vuex'

export default {
  // ...
  computed: {
  // 使用对象展开运算符将 getter 混入 computed 对象中
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}

上面是最简写的方式,将store中的getter映射到了计算属性

mutations

修改数据,只能包含同步操作的。更改Vuex的store中数据的唯一方法,就是提交mutation,使用方式接近事件

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 获取state中的count,并且自增1
      state.count++
    }
  }
})

使用这个mutation并不是直接调用函数,而是通过store.commit调用,比如

//调用store中的中的mutatins,increment
this.$store.commit('increment')

假如有多个参数,那么

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state,num) {
      state.count += num
    }
  }
})

使用的时候就需要多传入一个或者多个参数

//调用store中的中的mutatins,increment
this.$store.commit('increment',10)

辅助函数mapMutations
使用其实是差不多的

import { mapMutations } from 'vuex'

export default {
  // ...
  computed: {
  // 使用对象展开运算符将 mapMutations中的指定事件获取到
    ...mapMutations([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}

**

Actions

异步操作。因为mutations仅仅是同步操作,如果有时候是异步操作的,那么就可以使用actions,另外注意,actions并不是直接修改store中的数据,而是通过mutations提交的

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      //在actions中调用mutations提交数据到state中
      context.commit('increment')
    }
  }
})

在组件中使用actions和mutations类似,区别在于:mutations通过commit提交,而actions通过dispatch调用

this.$store.dispatch('increment')

还有一个区别就是Actions可以包含任意异步代码,而mutations则不行

actions: {
  incrementAsync ({ commit }) {
    //通过解构获取到context中的commit对象,并直接通过commit调用mutations中的对应事件
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

辅助函数mapActions
使用方式还是差不多

import { mapActions } from 'vuex'

export default {
  // ...
  computed: {
  // 使用对象展开运算符将 mapActions中的指定事件获取到
    ...mapActions([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}

modules

开发大型项目时,store对象就会变得非常臃肿,为了解决这个问题,Vuex允许将store分割成多个模块,每个模块可以拥有自己的state,getters,mutations,actions,甚至可以再对子模块进行更进一步的分割



const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}
import moduleA from 'xxx'
import moduleB from 'xxx'
              
const store = new Vuex.Store({
  modules: {
    moduleA,
    moduleB
  }
})

moduleA内部

export default {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

moduleB内部

const state = {};
const mutations = {};
const getters = {};
const actions = {};
export default {
  state,
  mutations,
  actions,
  getters
}

模块做的就是分割,方便对于store仓库的管理,使用的方式分割和不分割是完全一致的,该怎么用;

Vue Router的设计与使用

简介

简单的说,Vue Router是Vue官方推出的路由管理器,它和Vue深度集成,可以很好的管理路由

安装与使用

初始化项目时会有选择是否安装Vue-router,如果初始化时未安装,那么之后再安装需要输入命令

//当然,如果方便的话使用cnpm代替npm
npm install vue-router
//全局使用的话必须通过Vue.use()安装
import Vue form 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter)

起步

首先得明确,路由的使用离不开:输入和输出,无论是通过使用方法还是通过组件,都是要告诉Vue,需要跳转的路径是哪一个,之后路径对应的组件将有一个输出的位置,或者说路径对应的组件要有一个显示的位置,,下面是一个简单的示例

<div id="app">
  <h1>Hello App!</h1>
  <p>
    <!-- 使用 router-link 组件来导航. -->
    <!-- 通过传入 `to` 属性指定链接. -->
    <!-- <router-link> 默认会被渲染成一个 `` 标签 -->
    <router-link to="/foo">Go to Foo</router-link>
    <router-link to="/bar">Go to Bar</router-link>
  </p>
  <!-- 路由出口 -->
  <!-- 路由匹配到的组件将渲染在这里 -->
  <router-view></router-view>
</div>
// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)

// 1. 定义 (路由) 组件。
// 可以从其他文件 import 进来
const Foo = { template: '
foo
'
} const Bar = { template: '
bar
'
} // 2. 定义路由 // 每个路由应该映射一个组件。 其中"component" 可以是 const routes = [ { path: '/foo', component: Foo }, { path: '/bar', component: Bar } ] // 3. 创建 router 实例,然后传 `routes` 配置 const router = new VueRouter({ routes // (缩写) 相当于 routes: routes })

点击之后,将显示属性to中的路径,路径对应的组件就是下面定义的组件

动态路由

我们经常需要把某种匹配到的所有路由,全部映射到同一个组件中(比如:user组件,针对不同的ID,虽然加载的是同一个组件,但是内容却是不一样的),因此可以在vue-router的路由路径中使用“动态路由参数”(在路径中以冒号开头)来达到这个效果

var router = new VueRouter({
    routers:[
        {
            //动态路径参数,以冒号开头
            path:'/user/:id',
            component:User
        }
    ]
})

这样当路径匹配到的是:/user/具体的id,可以在组件User中获取到具体的id的值,之后向服务器请求当前id的具体内容,注意的是,如果切换不同id值,比如路径:/user/1 和 /user/2 这两个路径都会调用组件User,此时变化的仅仅是路由的参数,而组件是不会被销毁再创建的,因此也就代表组件的生命周期钩子函数不会被再次调用;如果想要有响应式的变化操作,可以使用导航守卫(此功能下面讲)

嵌套路由(敲黑板,重点)

实际的项目开发的应用界面,通常由多层嵌套的组件组合而成。同样的,URL中各段动态路径也按某种结构对应嵌套的各层组件。

举个后台的列子:实际开发中,登录页,注册页,主页等这是最顶层路由,在主页上有一个侧边导航栏,有一个侧边导航栏对应内容显示区域,这个时候的内容的跳转实际上就在对应的是在主页上进行子路由的跳转;

div id='app'>
    <router-view></router-view>
</div>

var router = new VueRouter({
    routers:[
        {
             //动态路径参数,以冒号开头
             path:'/user/:id',
            component:User,
            children:[
                 {
                    path:'/userMsg',
                    component:() => import('@/views/Home/users/UserManager.vue')
                  },
                  {
                    path:'/userLog',
                    component:() => import('@/views/Home/users/UserLog.vue')
                  }
            ]
        }
    ]
})

示例的代码是最高级的顶层出口,这个渲染的是最顶层路由匹配到的组件,同样的,假如被渲染的组件内同样包含自己的那么在路由配置中需要添加children属性来指定子路由的匹配;

换句话说,比如下例中的User组件中也包含了一个,当路由匹配到的路径是/userMsg和/userLog时,那么这两个组件的内容就会被渲染到User组件中的里;

编程式导航

先说一下,这个标签是vue官方提供的一个组件,其本质是标签,在这个标签上有一个属性to,当用户点击标签时,这个组件会在内部使用router.push()这个方法

<router-link to='...'></router-link>

router.push()

当使用router.push()的时候,这个方法会向history栈添加一个新纪录,因此当点击浏览器的后退按钮时,则回到之前的URL

//将路由路径跳转到/home,是一种简写
router.push('home);

//等同于上一种,将路由路径跳转到/home
router.push({path:'home'})//命名路由,params是post传参方式
router.push({ name: 'user', params: { userId: '123' }})// 带查询参数,变成 /register?plan=private,query是get的传参方式
router.push({ path: 'register', query: { plan: 'private' }})

router.replace()

用法和push()几乎一样,唯一的不同点就是它不会向history添加新纪录,而是跟它的方法名一样,替换掉当前的history记录,如果要在router-link上使用则需要加上replace属性;

<router-link to='...' replace></router-link>

**

router.go(n)

参数n是一个整数,意思是在当前的history记录中向前或者后退多少步

//返回上一个界面
router.go(-1)

//进入下一个界面
router.go(1)

命名路由

有时候,通过一个名称来标识一个路由显得更方便些,可以通过方法进行路由地址进行跳转或者通过

const router = new VueRouter({
  routes: [
    {
      path: '/user/:userId',
      name: 'user',
      component: User
    }
  ]
})

//通过参数重的name属性进行关联,当点击router-link的时候,根据name属性的值匹配到User组件,params是参数,会将123传递给动态路由
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>

//等同于的方式
router.push({ name: 'user', params: { userId: 123 }})

这两种方式都会将路由路径导航到/user/123

命名视图

有时候在一个界面里,也就是同级中会有多个渲染的出口,也就是由多个同级的,这个时候就需要命名视图了,如果没有命名,那么router-view将默认命名为default

比如,在首页里,有两个输出的router-view,分别加载了不同的组件,这个时候就需要通过给视图命名,来告诉vue,哪个组件对应哪个router-view了

<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
<router-view class="view three" name="b"></router-view>

const router = new VueRouter({
  routes: [
    {
      path: '/',
      components: {
          //因为上面有3个router-view,因此在这个路由地址打开的时候,需要对三个router-view,分别渲染指定的组件
        default: Foo,
        a: Bar,
        b: Baz
      }
    }
  ]
})

注意,这里是components,而不是component。

重定向和别名

重定向

重定向的意思就是比如,当路径导航到/a时,vue会自动把路径/a **替换 **成/b,然后回去匹配路径/b,而不再是/a;

举个后台例子,侧边导航栏中有一个下拉菜单,每个主菜单下都有几个副菜单,现在要求是,点击主菜单的时候,自动显示的是主菜单下第一个子菜单对应的组件,这种时候就需要用到重定向了

const router = new VueRouter({
  routes: [
    { path: '/a', redirect: '/b' }
  ]
})

//当然,重定向也可以是一个命名路由
const router = new VueRouter({
  routes: [
    { path: '/a', redirect: {name:'foo} }
  ]
})

//甚至是一个方法
const router = new VueRouter({
  routes: [
    { path: '/a', redirect: to => {
      // 方法接收 目标路由 作为参数
      // return 重定向的 字符串路径/路径对象
    }}
  ]
})

别名

别名的意思,实际上就是给当前的路由换了个名字,可以不需要使用path中指定的名字去使用

const router = new VueRouter({
  routes: [
    { path: '/user', component: A, alias: '/hello' }
  ]
})

例中,当使用router-link,或者push()等方法指定路由路径是/hello时,vue会自动在中按照使用匹配路径为/user时的方式去渲染组件A,并且在浏览器的地址中显示的URL地址为:xxxxxxx/hello,换句话说,/hello和/user的结果是完全一样的,唯一的区别就是名称不同;

路由传参

//使用router-link进行路由导航,传递参数
<router-link to='/user/123'></router-link>

//直接调用$router.push实现携带参数的跳转
const userId = '123'
router.push({ path: `/user/${userId}` }) // -> /user/123

//通过路由属性中的name来确定匹配的路由,
//通过params来传递参数(get的方式使用query,如果是post,使用params)
const userId = '123'
router.push({ name: 'user', params: { userId }}) // -> /user/123

导航守卫

导航守卫主要用来通过跳转或取消的方式守卫导航,比如:在一个网站的首页前往某个子页面时,发现这个子页面需要登录后才可以查看,那么就这里在前置守卫里做一个判断,假如没有登录停止跳转到子页面,将跳转路径改为登录页的路由路径;

导航解析流程

要使用导航守卫,必须先了解跳转路由时一个完整的导航解析流程:

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

全局前置守卫

可以使用 router.beforeEach 注册一个全局前置守卫,这个守卫是全局的,代表会在任意一个组件进入前生效

const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
  // ...
})

该守卫方法有3个参数:

  • to:即将要进入的 目标路由对象
  • form:当前导航正要离开的路由对象
  • next:这个是一个函数,在方法内部一定要调用这个方法才会继续跳转
const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
  //这里面可以判断是否复合条件,比如是否已经登录了,如果登录了执行next()等待
  
  //直接跳转到to的路由对象
  next()
  //中断了当前的导航
  next(false)
  //中断道歉的导航,并跳转到指定的路由对象,这个方法就是上面举例中说的可以终止跳转到子页面,转而跳转到登录页
  next('/')或者next({path:'/'})
})

全局后置守卫

其实和全局前置守卫的用法差不多,全局前置守卫是在路由参数to的目标生效前执行的方法,而全局后置守卫则是在当前这个路由被销毁前执行的方法,另外全局后置守卫没有next()

router.afterEach((to, from) => {
  // ...
})

组件内的守卫

这些守卫是定义在每个组件内部的,使用方式和生命周期钩子函数类似

const Foo = {
  template: `...`,
  beforeRouteEnter (to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
    
    //如果要在该守卫中给组件的设置数据,可以这样
    next(vm=>{
      //给data中的msg设置值为5
    	vm.msg = 5
    })
    //或者,执行methods中的方法,在方法中对数据进行更新,或者其他操作,这个方法中可以使用this
    next(vm=> vm.getData())
  },
  beforeRouteUpdate (to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
  },
  beforeRouteLeave (to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
  }
}

路由元信息

定义路由的时候可以配置 meta 字段:

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      name:'foo',
      children: [
        {
          path: 'bar',
          name:'bar',
          component: Bar,
          meta: { requiresAuth: true }
        }
      ]
    }
  ]
})

routers的配置中的每个路由对象,称作为:路由记录。路由记录是可以嵌套的,比如上例中的foo包含了一个子路由bar,当导航匹配到的路径为:/foo/bar时,会渲染Bar组件,同时,这个路由路径/foo/bar,也可以说是URL的记录会被保存到$router对象中的matched数组

//比如
matched:[
    {
        path:'/foo',
        ....
    },
    {
        path:'bar',
        meta:{requiresAuth: true},
        ...
    }
]

因此,比如我们就可以在导航守卫中对前往的路由进行判断,所有meta中带有属性requiresAuth并且值为true的路由对象,都要判断当前是否处于登录状态,如果没有登录直接跳转到登录页,如果已经登录了,那么就可以跳转

router.beforeEach((to, from ,next) => {
    const token = store.getters.userMsg
    if(to.matched.some(msg => msg.meta.requireAuth)){
        next()//如果路由中有meta的requireAuth,且为true,就直接跳转
    }else{
        if(token){
            next()
        }else{
            if(to.path==="/login"){
                next()
            }else{
                next({path:'/login'})
            }
        }
    }
    return
})

Histroy模式

Vue-router默认hash模式,如果觉得丑要去掉#号,可以使用history模式,只需要在路由中加上字段history,如下

const router = new VueRouter({
  mode: 'history',
  routes: [...]
})

你可能感兴趣的:(前端工程提升之路)