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官方提供並維護的,你能夠選擇用或不用,取決於實際狀況。

到這裏,概念性的章節就結束了。從下一章開始,咱們將進入實戰課程。

相關文章
相關標籤/搜索