Vue项目化

第6章  Vue项目化

从本章开始,笔者将介绍一些使用Vue生态中其余成员进行项目开发的内容。这些内容是整个Vue生态中十分主流且核心的部分,不只须要同窗们可以看懂,还须要同窗们在实战中可以驾轻就熟地去应用。固然,要想作到这些,须要同窗们跟着教程进行操做和练习。javascript

本文选自《Vue.js从入门到项目实战》 书籍详情连接:https://item.jd.com/12513015.htmlhtml

6.1  项目快速构建

当下潮流的作法通常采用先后端分离的方式进行Web架构,但同时也对前端开发环境的搭建提出了更高的要求。一个完整的前端开发环境应该具有预编译模板、注入依赖、合并压缩资源、分离开发和生产环境以及提供一个模拟的服务端环境等功能。前端

对于初学者来讲,可以理解这些概念的定义和应用已经十分不易,好在Vue为咱们提供了项目的快速构建工具——Vue Cli。vue

6.1.1  Vue Cli简介

想起之前和朋友杂谈技术时,一个作Ruby的家伙说“Ruby on Rails是一个很是强大的Scaffolding(脚手架),用它一个小时就能够写个博客网站”。笔者心中暗自一惊,回去查了下这个“Scaffolding”单词,译为“脚手架”,但久久不能理解是什么意思,也不知“Ruby on Rails”究竟是何方神圣。以后,偶然有次机会去学习一个“Ruby on Rails”的项目源码,才明白其意思,不过尔尔。java

Vue Cli也是一个“脚手架”,使用它5分钟就能够搭建一个完整的Vue应用。Vue Cli是Vue官方提供的构建工具,可用于快速搭建一个带有热重载(在代码修改后没必要刷新页面便可呈现修改后的效果)、lint代码语法检测以及构建生产版本等功能的单页应用。node

上面的内容牵扯到不少概念,初次接触的同窗也没必要担忧,没有必要对其刨根究底,一是每每它们做为一个名词,用于沟通而已,咱们只须要理解其做用和用法;二是每每它们太过抽象,须要结合实例进行理解。webpack

下面笔者将演示如何使用Vue Cli快速构建一个Vue项目。git

6.1.2  使用Vue Cli构建项目

1.打开控制台,输入:web

cnpm install vue-cli -gajax

安装Vue Cli,还没有安装cnpm的同窗能够输入:

npm install cnpm -g --registry=https://registry.npm.taobao.org

安装国内淘宝镜像源的cnpm)。

在命令执行结束以后,输入:

vue --version

。若是控制台打印出版本号,即表示安装成功。

2.在项目所要放置的文件目录下打开控制台,输入:

vue init webpack my-project

初始化项目(此处的my-project为项目名称)。

3.在模板下载完成后,Vue Cli将引导咱们进行项目配置,笔者的配置如图6.1所示。

图6.1  Vue Cli项目初始化配置

其中,“Set up unit tests”和“Setup e2e tests with Nightwatch”选择“No”,这部份内容与Vue没有直接关系,这里不予探讨。最后一项也选择“no”是由于npm的镜像源在国外,安装依赖的速度缓慢且容易出错,笔者建议使用cnpm安装依赖。

4.输入:

cnpm install

安装项目依赖。

5.输入:

npm start

构建项目的开发版本,并启动webpack-dev-server。

此时,在浏览器地址栏输入http://localhost:8080便可访问项目,项目页面如图6.2所示。

图6.2  Vue Cli项目初始页面

6.以后,另开一个控制台,输入:

npm run build

构建项目的生产版本。

6.1.3  项目目录介绍

打开初始化后的项目目录,能够发现里面已经存在了一些文件和文件夹,如图6.3所示。

图6.3  Vue Cli项目初始目录

目录主要内容的说明如表6.1所示。

表6.1  Vue Cli项目初始目录

名称

说明

build

开发和生产版本的构建脚本

config

开发和生产版本的部分构建配置

dist

由npm run build生成;项目的生产版本;项目完成后,交付该文件夹便可

src

项目开发的关键资源目录和主要工做空间

static

静态资源(如使用JS赋值图片的src时,该图片资源应放在static下)

.babelrc

babel的配置文件(babel,下一代JS的预编译器)

.eslintignore

ESLint代码语法检测的配置文件(应忽略的语法格式)

.eslintrc.js

ESLint代码语法检测的配置文件(应规范的语法格式)

.gitignore

应被Git版本控制工具忽略的文件

index.html

应被webpack注入资源的模板HTML文件

以后使用编辑器(笔者使用的是WebStorm)打开项目,查看src文件夹下的内容,目录如图6.4所示。

图6.4  src目录下的内容

其中,assets文件夹用于存放图片、音频、视频等资源;components文件夹用于存放咱们开发的单文件组件;router/index.js用于配置项目的前端路由(用到了Vue Router);App.vue是Vue Cli为咱们默认建立的项目的根组件;main.js则是webpack的入口文件。

下面,咱们先来看一下App.vue、main.js中的内容。

App.vue中的代码以下:

<template>
  <div id="app">
    <img src="./assets/logo.png">
    <!-- Vue Router的路由视图区
-->
    <router-view/>
  </div>
</template>

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

  • 这是一个单文件组件,包含HTML、JS和CSS三个部分。显然,Vue Cli采用关注点分离的开发方式,这种开发方式使得组件的内聚性更强,也更适合于组件化的开发。
  • script标签中的内容为Vue组件;template标签中的内容为组件的DOM结构;style标签中的内容为CSS样式表(在被赋予scoped属性以后,样式表的做用域仅限在当前组件中)。
  • export和import是ES 6语法中用于模块化管理的两个关键字,这里使用export导出Vue组件以供外部调用。
    main.js中的代码以下:

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

  • 这里使用import引入全局的Vue对象、App组件和Vue Router的配置。以后,建立了一个Vue实例,并将App组件和router注册到实例中。
  • 注释/*eslint-disable no-new*/用于告诉eslint忽略此处对new关键字的检测。
    在main.js中,实例的el选项绑定了id为app的DOM元素。可这个元素在哪里呢?彷佛App组件中有个id为app的div元素,是这个吗?
    下面来看一下根目录(my-project)下index.html中的代码:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>my-project</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

因为App组件是被注册在实例中的(做为实例的子组件),那么App组件中的元素固然不可能做为实例的挂载元素。那么,实例最终是被挂载在index.html中的div元素上了吗?其实也不是。

Vue Cli会将全部编译整理好的资源路径注入到以index.html为模板的镜像中,被注入后的镜像即生产版本中项目的入口文件,也就是dist文件目录下的index.html,这里的元素才是实例最终被挂载的地方。

在src目录下,还有一个重要的文件——使用Vue Router配置的router/index.js。有关Vue Router的内容笔者将放到下一小节中进行讲述。

6.2  前端路由

路由这个概念首先出如今后台。传统MVC架构的web开发,由后台设置路由规则,当用户发送请求时,后台根据设定的路由规则将数据渲染到模板中,并将模板返回给用户。所以,用户每进行一次请求就要刷新一次页面,十分影响交互体验。

ajax的出现则有效解决了这一问题。ajax(asynchronous javascript and xml),浏览器提供的一种技术方案,采用异步加载数据的方式以实现页面局部刷新,极大提高了用户体验。

而异步交互体验的更高版本就是SPA——单页应用,不只页面交互无刷新,甚至页面跳转之间也能够无刷新,前端路由随之应运而生。

6.2.1  前端路由的简单实现

广义上的前端路由是由前端根据URL来分发视图,其实现有两个核心操做,一是须要监听浏览器地址的变化,二是须要动态加载视图。

笔者分别使用Vue和原生的JS来模拟其实现,并用到Node.js建立服务端文件,服务端文件app.js的代码以下:

const http = require('http') // http模块

const fs = require('fs') // 文件处理模块

const hostName = '127.0.0.1'

const port = 3000

const server = http.createServer(function (req, res) { // 建立http服务

  let content = fs.readFileSync('index.html') // 读取文件

  res.writeHead(200, { // 设置响应内容类型

    'content-type': 'text/html;charset="utf-8"'

  })

  res.write(content) // 返回index.html文件内容

  res.end()

})

server.listen(port, hostName, function () { // 启动服务监听

  console.log(`Server is running here: http://${hostName}:${port}`)

})

这段代码用到了Node的http和fs模块,用以建立一个能够返回index.html页面的服务。想要启动服务,首先要到Node官网下载安装Node客户端(推荐使用8.11.3长期稳定版),以后在文件所处目录下输入命令:

node app.js // node + 文件名

当控制台显示“Server is running here:http://127.0.0.1:3000”时,即表示服务启动成功。

下面来看一下使用Vue实现前端路由的代码(index.html):

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>

<div id="app">

    <ul>

        <li><router-link to="/">Home</router-link></li>

        <li><router-link to="/about">About</router-link></li>

    </ul>

    <router-view></router-view>

</div>

<script type="text/javascript">

  let Home = {

    template: '<h1>This is Home!</h1>'

  }

  let About = {

    template: '<h1>This is About!</h1>'

  }

  let routes = [ // 定义路由规则

    {

      path: '/',

      component: Home

    },

    {

      path: '/about',

      component: About

    }

  ]

  let RouterLink = {

    props: ['to'],

    template: '<a :href="to"><slot name="default"></slot></a>'

  }

  let RouterView = {

    data () {

      return {

        url: window.location.pathname // 获取浏览器地址

      }

    },

    computed: {

      ViewComponent () { // 根据浏览器地址返回相应组件

        return routes.find(route => route.path === this.url).component

      }

    },

    render (h) {

      return h(this.ViewComponent)

    }

  }

  /* eslint-disable */

  let vm = new Vue({

    el: '#app',

    components: { RouterLink, RouterView }

  })

</script>

在这段代码中,笔者声明了Home、About、RouterLink和RouterView四个组件。Home和About为待分发的视图组件;RouterLink为触发视图切换的组件;RouterView为挂载动态视图的组件。以后,笔者在vm实例中经过监测window.location.pathname的变化来动态分发视图。

这种方式虽然实现了前端路由,但其实视图切换仍是由页面刷新来执行的,这并非一个单页应用。

使用原生JS实现前端路由的代码以下(index.html):

<div>

  <ul>

    <li><a href="#/">Home</a></li>

    <li><a href="#/about">About</a></li>

  </ul>

  <!-- 动态视图被挂载的元素 -->

  <div id="view"></div>

</div>

<script type="text/javascript">

  let Home = '<h1>This is Home!</h1>' // 视图模板 Home

  let About = '<h1>This is About!</h1>' // 视图模板 About

  let Router = function (el) { // 定义路由类

    let view = document.getElementById(el)

    let routes = [] // 路由规则列表

    let load = function (route) { // 加载视图

      route && (view.innerHTML = route.template)

    }

    let redirect = function () { // 分发视图

      let url = window.location.hash.slice(1) || '/'

      for (let route of routes) {

        url === route.url && load(route)

      }

    }

    this.push = function (route) { // 添加路由规则

      routes.push(route)

    }

    window.addEventListener('load', redirect, false) // 页面加载时

    window.addEventListener('hashchange', redirect, false) // URL变化时

  }

  let router = new Router('view') // 实例化路由

  router.push({ // 添加路由规则

    url: '/',

    template: Home

  })

  router.push({

    url: '/about',

    template: About

  })

</script>

在这段代码中,笔者为浏览器的内置对象window在页面加载和URL变化时添加了监听器以分发视图。细心的同窗能够发现,笔者在a标签的href中写入了“#”符号,这个符号能够阻止页面刷新(实现了单页应用),但也会在URL中加入该符号,所以笔者在redirect函数中并无直接取window.location.hash的值,而是先slice(1),将“#”去掉。

两种实现的运行结果的初始页面均如图6.5所示。

图6.5  前端路由的简单实现(1)

当点击About连接时,页面如图6.6所示。

图6.6  前端路由的简单实现(2)

虽然两种实现的视图表现看似相同,但其实效果却截然不同,同窗们能够结合实例体会一下。

6.2.2  Vue中的前端路由

Vue Router是Vue.js官方提供的路由管理器,它与Vue.js的核心深度集成,且随着Vue.js版本更新而更新,致力于简化单页应用的构建。

Vue Router的功能十分丰富且强大,笔者无心于枚举一些抽象的概念,下面笔者将经过几个简单的示例(须要运行在服务端上)来演示一下它的用法。

1.基础路由

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

  <ul>

    <li><router-link to="/">Home</router-link></li>

    <li><router-link to="/about">About</router-link></li>

  </ul>

  <router-view></router-view>

</div>

<script type="text/javascript">

  let Home = { template: '<h1>This is Home!</h1>' } // Home组件

  let About = { template: '<h1>This is About!</h1>' } // About组件

  let routes = [ // 定义路由规则, 每个路由规则应该映射一个视图组件

    { path: '/', component: Home },

    { path: '/about', component: About }

  ]

  let router = new VueRouter({ // 建立VueRouter实例, 并传入routes配置

    routes

  })

  let app = new Vue({

    router

  }).$mount('#app')

</script>

RouterLink和RouterView是Vue Router提供的两个内置组件。RouterLink默认会被渲染成一个<a>标签,其to属性用于指定跳转连接;RouterView将负责挂载路由匹配到的视图组件。

2.动态路由

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

  <ul>

    <li><router-link to="/">Home</router-link></li>

    <li @click="add">

      <!-- 2. 参数num由实例传入路由 -->

      <router-link :to="'/about/' + num">About</router-link>

    </li>

  </ul>

  <router-view></router-view>

</div>

<script type="text/javascript">

  let Home = { template: '<h1>This is Home!</h1>' } // Home组件

  let About = { // About组件

    template: '<div>' +

    '<h1>This is About!</h1>' +

    '<p>num: {{ $route.params.num }}</p>' + // 3. 在组件中显示参数 num

    '</div>'

  }

  let routes = [ // 定义路由规则, 每个路由规则应该映射一个视图组件

    { path: '/', component: Home },

    { path: '/about/:num', component: About } // 1. 定义了参数 num, 格式如 /:num

  ]

  let router = new VueRouter({ // 建立VueRouter实例, 并传入routes配置

    routes

  })

  let app = new Vue({

    data () {

      return { num: 0 }

    },

    methods: { // 当点击About时, num值自增1

      add () { this.num++ }

    },

    router

  }).$mount('#app')

</script>

咱们可使用动态路由参数将匹配某种模式的全部路由映射到同一个组件(致敬RESTful)。如上述示例中,Vue Router将全部匹配/about/:num的路径全都映射到About组件中(图6.7),并将num做为组件中的一个参数。

图6.7  动态路径路由

路径参数应用冒号“:”标记,可是在使用时应注意设计的规则是否合理,好比:

routes: [

  { path: '/:any', component: Home} // 能够匹配路径为/about的路由,’about’将做为any的值

]

将会把全部路径都匹配到Home组件中。

当动态路径被匹配时,咱们能够在组件中使用this.$route.params来获取参数的值。

3.嵌套路由

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

  <ul>

    <li><router-link to="/">Home</router-link></li>

    <li>

      <div><router-link to="/about">About</router-link></div>

      <ul>

        <!-- 3. 使用嵌套路由 -->

        <li><router-link to="/about/author">About - Author</router-link></li>

        <li><router-link to="/about/email">About - Email</router-link></li>

      </ul>

    </li>

  </ul>

  <router-view></router-view>

</div>

<script type="text/javascript">

  let Home = { template: '<h1>This is Home!</h1>' } // Home组件

  let About = { // About组件

    template: '<div>' +

    '<h1>This is About!</h1>' +

    '<router-view></router-view>' +  // 1. 嵌套的动态视图区

    '</div>'

  }

  let Author = { template: '<p>Author: lonelydawn</p>' } // Author组件

  let Email = { template: '<p>Email: lonelydawn@sina.com</p>' } // Email组件

  let routes = [ // 定义路由规则, 每个路由规则应该映射一个视图组件

    { path: '/', component: Home },

    {

      path: '/about',

      component: About,

      children: [ // 2. 嵌套子路由

        { path: 'author', component: Author },

        { path: 'email', component: Email }

      ]

    }

  ]

  let router = new VueRouter({ // 建立VueRouter实例, 并传入routes配置

    routes

  })

  let app = new Vue({

    router

  }).$mount('#app')

</script>

  • 嵌套路由能够实如今动态视图中嵌套动态视图。
  • 这里有个问题,多层的动态视图是否可使用Vue的内置组件component来实现呢?固然能够。不过使用component切换的视图会在页面刷新后回到初始状态,而使用路由分发的视图在页面刷新后会保持当前路径对应的视图,并在浏览器的history中留下记录。

4.编程式路由

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

  <ul>

    <!-- 默认字符串为路径参数 -->

    <li @click="redirectByPath('/')">Home</li>

    <li>

      <!-- 指定参数为路径 -->

      <div @click="redirectByPath('/about')">About</div>

      <ul>

        <!-- 嵌套路由-->

        <li @click="redirectByPath('/about/author')">About - Author</li>

        <!-- 嵌套路由, 动态路由, 当使用path时, params参数不生效 -->

        <li @click="redirectByPath('/about/email', { email: lonelydawn@sina.com' })">About - Email</li>

        <!-- 嵌套路由, 动态路由, 能够直接将参数写入path -->

        <li @click="redirectByPath('/about/email/lonelydawn@sina.com')">About - Email</li>

        <!-- 嵌套路由, 动态路由, 使用命名路由跳转视图 -->

        <li @click="redirectByName('Email', { email: 'singledawn@sina.com' })">About - Email</li>

      </ul>

    </li>

  </ul>

  <router-view></router-view>

</div>

<script type="text/javascript">

  let Home = { template: '<h1>This is Home!</h1>' } // Home组件

  let About = { // About组件

    template: '<div>' +

    '<h1>This is About!</h1>' +

    '<router-view></router-view>' +  // 嵌套的动态视图区

    '</div>'

  }

  let Author = { template: '<p>Author: lonelydawn</p>' }

  let Email = { template: '<p>Email: {{ $route.params.email }}</p>' }

  let routes = [ // 定义路由规则, 每个路由规则应该映射一个视图组件

    { path: '/', component: Home },

    {

      path: '/about',

      component: About,

      children: [ // 嵌套子路由

        { name: 'Author', path: 'author', component: Author },

        { name: 'Email', path: 'email/:email', component: Email }

      ]

    }

  ]

  let router = new VueRouter({ // 建立VueRouter实例, 并传入routes配置

    routes

  })

  let app = new Vue({

    methods: {

      redirectByPath (path, params) {

        this.$router.push({ path, params })

      },

      redirectByName (name, params) {

        this.$router.push({ name, params })

      }

    },

    router

  }).$mount('#app')

</script>

  • 这里并无使用RouterLink组件,而是在JS中使用router.push方法跳转视图。
  • 咱们能够经过路由的path跳转视图,还能够赋予路由name属性,而后经过name跳转视图。
  • 动态参数应放在params中,当使用path时,params参数不生效,此时应将参数值直接写进path中。

5.使用Vue Cli快速构建的项目中的router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    }
  ]
})

  • 这里使用Vue.use安装Vue Router插件。
  • 这里使用export返回路由规则。默认只有当路径为/时,渲染HelloWorld组件。
    关于Vue Router的知识点有不少,笔者在这里并无一一列举,而是介绍了其最多见的几种用法。实际上,掌握这些已经能在实战中应对大部分的状况了。想要深研的同窗能够查阅官网文档,并欢迎随时邮件与笔者进行交流。

6.3  状态管理

对于小型应用来讲,彻底没有必要引入状态管理,由于这会带来更多的开发成本。然而当应用的复杂度逐渐升高,状态管理的重要性也愈加重要起来。

对于组件化开发来讲,大型应用的多个状态每每跨越多个组件和交互间。在多层嵌套的父子组件之间传递状态已经十分麻烦,而Vue更是没有为兄弟组件提供直接共享数据的办法。基于这个问题,许多框架提供了解决方案——使用全局的状态管理器,将全部分散的共享数据交由状态管理器保管,Vue也不例外。

Vue官网提供的状态管理器名为Vuex,本节将介绍有关Vuex的概念与用法。

6.3.1  对象引用

在了解Vuex以前,咱们先来看一下对象引用的概念。

下面这两段代码将输出什么(先不要看答案,本身思考一下)?

// 代码1

let state = {
  msg: 'welcome'
}
let copy = state
state.hello = "world"
console.log(Object.keys(copy)) // Object.keys用于获取对象的键名

// 代码2

let state = {
  msg: "welcome"
}
let copy = state
state = {
  hello: "world"
}
console.log(Object.keys(copy))

答案以下:

//-> ["msg", "hello"]

//-> ["msg"]

在代码1中,当state对象被定义时,浏览器会为其分配一个地址;当使用state赋值copy对象时,copy将引用state的地址;所以,当state改变时,copy也随之改变。

在代码2中,笔者在为copy引用state的地址以后,从新定义了state对象;此时,state将引用一个新的地址,而copy仍引用原来的地址,因此copy并没有任何变化。

理解了这个概念,将对咱们学习和使用Vuex大有裨益。

6.3.2  状态管理器Vuex

Vuex,用于管理分散在Vue各个组件中的数据。

每个Vuex应用的核心都是一个store(仓库),你也能够理解它是一个“非凡的全局对象”。与普通的全局对象不一样的是,基于Vue数据与视图绑定的特色,当store中的状态发生变化时,与之绑定的视图也会被从新渲染。

这是一个单向的过程,由于store中的状态不容许被直接修改。改变store中的状态的惟一途径就是显式地提交(commit)mutation,这可让咱们方便地跟踪每个状态的变化。(在大型的复杂应用中,若是没法有效地跟踪到状态的变化,将会对理解和维护代码带来极大的困扰。假如你能很好地理解使用Vuex进行状态管理的原因,你就应该尽力遵循“显式”的原则,即便你能够跳过这一过程)。

Vuex中有5个重要的概念:State、Getter、Mutation、Action、Module。

State用于维护全部应用层的状态,并确保应用只有惟一的数据源(SSOT, Single Source of Truth)。

State的用法以下:

new Vuex.Store({ // 建立仓库
  state: {
    count: 1
  }
})

在组件中,咱们能够直接使用$store.state.count(前提是store已被注册到实例中),也能够先用mapState辅助函数将其映射下来,代码以下:

import { mapState } from 'vuex'
export default {
  computed: {
    ...mapState(['count']) // ...是ES6中的对象展开运算符

  }
}

Getter维护由State派生的一些状态,这些状态随着State状态的变化而变化。与计算属性同样,Getter中的派生状态在被计算以后会被缓存起来,当重复调用时,若是被依赖的状态没有变化,那么Vuex不会从新计算派生状态的值,而是直接采用缓存值。

Getter的用法以下:

new Vuex.Store({ // 建立仓库
  state: {
    count: 1
  },
  getters: {
    tenTimesCount (state) { // Vuex
为其注入state对象
      return state.count * 10
    }
  }
})

在组件中,咱们能够直接使用$store.getters.tenTimesCount,也能够先用mapGetters辅助函数将其映射下来,代码以下:

import { mapGetters } from 'vuex'
export default {
  computed: {
    ...mapGetters(['tenTimesCount']) // ...是ES6中的对象展开运算符

  }
}

Mutation提供修改State状态的方法。

Mutation的用法以下:

new Vuex.Store({ // 建立仓库
  state: {
    count: 0
  },
  mutations: {
    addCount (state, num) {
      state.count += num || 1
    }
  }
})

在组件中,咱们能够直接使用store.commit来提交mutation,代码以下:

methods: {
  addCount () {
    this.$store.commit('addCount') // store被注入到Vue实例中后可以使用
this.$store
  }
}

也能够先用mapMutation辅助函数将其映射下来,代码以下:

import { mapState, mapMutations } from 'vuex'
export default {
  computed: {
    ...mapState(['count']) // ...是ES6中的对象展开运算符

  },
  methods: {
    ...mapMutations(['addCount']),
    ...mapMutations({ //
为mutation赋别名,注意冲突;不经常使用
      increaseCount: 'addCount'
    })
  }
}

Action相似于Mutation,不一样在于:

  • Action不能直接修改状态,只能经过提交mutation来修改。
  • Action能够包含异步操做。
    Action的用法以下:

new Vuex.Store({ // 建立仓库
  state: {
    count: 0
  },
  mutations: {
    addCount (state, num) {
      state.count += num || 1
    }
  },
  actions: {
    // context
具备和store实例相同的属性和方法
    // 能够经过context获取state和getters中的值,或者提交mutation和分发其余的action
    addCountAsync (context, num) {
      setInterval(function () {
        if (context.state.count < 2000) {
          context.commit('addCount', num || 100)
        }
      }, num || 100)
    }
  }
})

在组件中,咱们能够直接使用store.dispatch来分发action,代码以下:

methods: {
  addCountAsync (num) {
    this.$store.dispatch('addCountAsync', num)
  }
}

或者使用mapActions辅助函数先将其映射下来,代码以下:

import { mapState, mapActions } from 'vuex'
export default {
  computed: {
    ...mapState(['count']) // ...是ES6中的对象展开运算符

  },
  methods: {
    ...mapActions(['addCountAsync']),
    ...mapActions({ //
为action赋别名,注意冲突;不经常使用
      increaseCountAsync: 'addCountAsync'
    })
  }
}

因为使用单一状态树,当项目的状态很是多时,store对象就会变得十分臃肿。所以,Vuex容许咱们将store分割成模块(Module),每一个模块拥有独立的State、Getter、Mutation和Action,模块之中还能够嵌套模块,每一级都有着相同的结构。

Module的用法以下:

// 定义模块

const counter = {
  namespaced: true, // 定义为独立的命名空间

  state: {
    count: 0
  },
  getters: {
    //
在模块中,计算方法还会具备rootState、rootGetters参数以获取根模块中的数据
    tenTimesCount (state, getters, rootState, rootGetters) {
      console.log(state, getters, rootState, rootGetters)
      return state.count * 10
    }
  },
  mutations: {
    addCount (state, num) {
      state.count += num || 1
    }
  },
  actions: {
    // context
具备和store实例相同的属性和方法
    // 能够经过context获取state和getters中的值,或者提交mutation、分发action
    // 在模块中,context还会具备rootState和rootGetters属性以获取根模块中的数据

    addCountAsync (context, num) {
      setInterval(function () {
        if (context.state.count < 2000) {
          context.commit('addCount', num || 100)
        }
      }, num || 100)
    }
  }
}

// 建立仓库

new Vuex.Store({
  modules: { // 注册模块

    counter
  }
})

在组件中,模块的使用方法以下:

import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
  computed: {
    // 辅助函数的第一个参数为模块的名称

    ...mapState('counter', ['count']),
    ...mapGetters('counter', ['tenTimesCount'])
  },
  methods: {
    ...mapMutations('counter', ['addCount']),
    ...mapActions('counter', ['addCountAsync'])
  }
}

最后,结合Vuex用于管理分散在各个组件中的状态和追踪状态变动的初衷,笔者简单总结了一下这些概念。做为一个状态管理器,首先要有保管状态的容器——State;为了知足衍生数据和数据链的需求,从而有了Getter;为了能够“显式地”修改状态,因此须要Mutation;为了能够“异步地”修改状态(知足ajax等异步数据交互),因此须要Action;最后,若是应用有成百上千个状态,放在一块儿会显得十分庞杂,因此分模块管理(Module)也是必不可少的。

Vuex的用法如上,应该并不难于理解。那么如何将Vuex集成到项目中去呢?笔者将在下一小节中进行介绍。

6.3.3  在项目中使用Vuex

首先,咱们打开以前构建好的项目my-project,在命令行中输入:

cnpm install vuex --save-dev

安装插件。

以后,在src目录下建立store、store/index.js、store/modules、store/modules/counter.js,建立好的文件路径如图6.8所示。

图6.8  集成Vuex的文件目录

其中,store是咱们进行Vuex仓库开发的工做目录;store/index.js是仓库的输出文件;store/modules目录用于放置各个模块;store/modules/counter.js文件是一个加数器模块。

store/modules/counter.js中的代码以下:

export default {
  namespaced: true, // 定义为独立的命名空间

  state: {
    count: 0
  },
  getters: {
    //
在模块中,计算方法还会具备rootState、rootGetters参数以获取根模块中的数据
    tenTimesCount (state, getters, rootState, rootGetters) {
      console.log(state, getters, rootState, rootGetters)
      return state.count * 10
    }
  },
  mutations: {
    addCount (state, num) {
      state.count += num || 1
    }
  },
  actions: {
    // context
具备和store实例相同的属性和方法
    // 能够经过context获取state和getters中的值,或者提交mutation、分发action
    // 在模块中,context还会具备rootState和rootGetters属性以获取根模块中的数据

    addCountAsync (context, num) {
      setInterval(function () {
        if (context.state.count < 2000) {
          context.commit('addCount', num || 100)
        }
      }, num || 100)
    }
  }
}

store/index.js中的代码以下:

import Vue from 'vue'
import Vuex from 'vuex'
import counter from './modules/counter' // 引入加数器模块


Vue.use(Vuex) // 安装插件

export default new Vuex.Store({ // 实例化Vuex仓库
  modules: {
    counter
  }
})

在这两个文件中,笔者实例化了一个Vuex仓库并构建了一个加数器模块。以后,咱们要在Vue实例中引入这个仓库,这还须要修改两个文件:webpack的入口文件main.js和单组件文件components/HelloWorld.vue。

修改后的main.js的代码以下:

import Vue from 'vue'
import App from './App'
import router from './router' // 引入
router
import store from './store' // 引入
store

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({ // Vue实例

  el: '#app',
  router, //
注册router
  store, // 注册
store
  components: { App },
  template: '<App/>'
})

在这里,笔者将仓库注册到了Vue实例中。

修改后的components/HelloWorld.vue的代码以下:

<template>
  <div class="hello">
    <h2>count: {{ count }}</h2>
    <h2>ten times: {{ tenTimesCount }}</h2>
    <button @click="addCountAsync(50)">add Count</button>
    <button @click="addCount(20)">add Count2</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
  computed: {
    // 辅助函数的第一个参数为模块的名称

    ...mapState('counter', ['count']),
    ...mapGetters('counter', ['tenTimesCount'])
  },
  methods: {
    ...mapMutations('counter', ['addCount']),
    ...mapActions('counter', ['addCountAsync'])
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

在这里,笔者将仓库中的状态与视图进行了绑定。

项目的初始页面如图6.9所示。

图6.9  加数器的初始视图

当点击“add Count2”按钮以后,页面如图6.10所示。

图6.10  增长数值以后的加数器

Vuex并非Vue应用开发的必选项,在使用时,应先考虑项目的规模和特色,有选择地进行取舍,盲目地选用只会带来更多的开发成本。

Vuex为开发者提供了多种写法,不过笔者并不推荐过多的尝试和写法上的变换,毕竟保持一致的风格也是高质量代码的一种表现,除非这种变化是一种进步。笔者所认同的是,对于花里胡哨作到心中有数,对于本身作到大道至简和从一而终。

本章所介绍的工具和插件均是由Vue官方提供并维护的,你能够选择用或不用,取决于实际状况。

到这里,概念性的章节就结束了。从下一章开始,咱们将进入实战课程。

相关文章
相关标签/搜索