Vue.js学习

Vue.js 是用于构建交互式的 Web 界面的库。
Vue.js 提供了 MVVM 数据绑定和一个可组合的组件系统,具有简单、灵活的 API。

其实和Jquery一样,VueJs就是一个Js库,但是是面向前端的库,具体来讲叫做MVVM(Model-View-ViewModel)库.
也就是说,有部分功能和Jquery是差不多的,Vuejs能做的,Jquery也能做。这下我们就放心了,Jquery多简单啊,令人发指的是Vuejs在实现相同功能的时候更简单(不然用你干嘛啊);

理解Vuejs最关键的一句话叫做“数据驱动视图”,比如用Jquery来做一个列表,这个列表的数据是从Laravel来的,那么我们要遍历这个数据,然后把列表的html元素加到dom里面去, 要删除一个列表项的时候,先要在找到列表项在dom的位置,然后去除这个节点。Vuejs不用,数据在的时候,列表就在,数据减一,列表项就自动实时相应减一。也就是说,你只要操作数据就够了,不用管dom。这基本就是Vuejs的中心思想。

一、基础学习

1.MVVM模式

下图不仅概括了MVVM模式(Model-View-ViewModel),还描述了在Vue.js中ViewModel是如何和View以及Model进行交互的。

clipboard.png

ViewModel是Vue.js的核心,它是一个Vue实例。Vue实例是作用于某一个HTML元素上的,这个元素可以是HTML的body元素,也可以是指定了id的某个元素。

当创建了ViewModel后,双向绑定是如何达成的呢?

首先,我们将上图中的DOM Listeners和Data Bindings看作两个工具,它们是实现双向绑定的关键。
从View侧看,ViewModel中的DOM Listeners工具会帮我们监测页面上DOM元素的变化,如果有变化,则更改Model中的数据;
从Model侧看,当我们更新Model中的数据时,Data Bindings工具会帮我们更新页面中的DOM元素。

1.2 Vue基本语法

先来看一个简单的示例,Hello,World!

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vue学习</title>
<script src="http://static.runoob.com/assets/vue/1.0.11/vue.min.js"></script>
</head>
<body>
<div id="app">
  {{ message }}
</div>
<!-- JavaScript 代码需要放在尾部(指定的HTML元素之后) -->
<script>
new Vue({
    el:'#app',
    data: {
        message:'Hello World!'
    }
});
</script>
</body>
</html>

示例详解:

1.2.1 data属性和方法

每个Vue实例都会代理其data对象中的所有属性:

var data = { a: 1 }
var vm = new Vue({
    data: data
})

vm.a === data.a // -> true

// setting the property also affects original data
vm.a = 2
data.a // -> 2

// ... and vice-versa
data.a = 3
vm.a // -> 3

需要注意的是只有代理属性是反应式的,如果在实例创建之后添加一个新的属性到实例上,将不会触发任何视图更新。关于这一点我们将在后续反应系统中讨论。

除了数据属性之外,Vue实例还提供了许多有用的实例属性和方法,这些属性和方法都以$开头以便和代理数据属性进行区分。例如:

var data = { a: 1 }
var vm = new Vue({
    el: '#example',
    data: data
})

vm.$data === data // -> true
vm.$el === document.getElementById('example') // -> true

// $watch is an instance method
vm.$watch('a', function (newVal, oldVal) {
    // this callback will be called when `vm.a` changes
})

1.2.2 实例生命周期

每个Vue实例在创建时都会经历一系列实例化步骤,例如,需要设置数据观察、编译模板、以及创建必要的数据绑定。在这个过程中,还会调用生命周期钩子,从而方便我们执行自定义逻辑,例如,created钩子会在实例创建后调用:

var vm = new Vue({
    data: {
        a: 1
    },
    created: function () {
        // `this` points to the vm instance
        console.log('a is: ' + this.a)
    }
})
// -> "a is: 1"

还有一些钩子会在实例生命周期的不同阶段调用,例如compiled、readydestroyed,所有被调用的生命周期钩子通过this指向调用它的Vue实例,一些用户可能会疑惑在Vue.js的世界中有没有“控制器”的概念,答案是没有。组件的自定义逻辑会被分割到这些生命周期钩子中。

1.2.3 props 用法

props将数据从父作用域传到子组件。在 Vue.js 中,父子组件的关系可以总结为 props down, events up 。父组件通过 props 向下传递数据给子组件,子组件通过 events 给父组件发送消息。

<div id="app-7">
  <ol>
    <!-- 现在我们为每个todo-item提供待办项对象    -->
    <!-- 待办项对象是变量,即其内容可以是动态的 -->
    <todo-item v-for="item in groceryList" v-bind:todo="item"></todo-item>
  </ol>
</div>


Vue.component('todo-item', {
  props: ['todo'],
  template: '<li>{{ todo.text }}</li>'
})
var app7 = new Vue({
  el: '#app-7',
  data: {
    groceryList: [
      { text: '蔬菜' },
      { text: '奶酪' },
      { text: '随便其他什么人吃的东西' }
    ]
  }
})

clipboard.png
这只是一个假设的例子,但是我们已经设法将应用分割成了两个更小的单元,子单元通过 props 接口实现了与父单元很好的解耦。我们现在可以进一步为我们的 todo-item 组件实现更复杂的模板和逻辑的改进,而不会影响到父单元。

组件实例的作用域是孤立的。这意味着不能(也不应该)在子组件的模板内直接引用父组件的数据。要让子组件使用父组件的数据,我们需要通过子组件的props选项。

子组件要显式地用props 选项声明它期待获得的数据:

Vue.component('child', {
  // 声明 props
  props: ['message'],
  // 就像 data 一样,prop 可以用在模板内
  // 同样也可以在 vm 实例中像 “this.message” 这样使用
  template: '<span>{{ message }}</span>'
})

然后我们可以这样向它传入一个普通字符串:

<child message="hello!"></child>

结果:

hello!

1.2.4 data属性对象和函数返回对象的区别

我们先来看一下这个比较经典的问题,当初在学Vue的时候也犯过这样的迷惑,不知道何时传递data对象,何时传递data函数 。Vue.js的data是要一个对象还是一个function?

Vue 实例的数据对象。Vue.js 会递归地将它全部属性转为 getter/setter,从而让它能响应数据变化。这个对象必须是普通对象:原生对象,getter/setter 及原型属性会被忽略。不推荐观察复杂对象。

在实例创建之后,可以用 vm.$data 访问原始数据对象。Vue 实例也代理了数据对象所有的属性。

在定义组件时,同一定义将创建多个实例,此时 data 必须是一个函数,返回原始数据对象。如果 data 仍然是一个普通对象,则所有的实例将指向同一个对象!换成函数后,每当创建一个实例时,会调用这个函数,返回一个新的原始数据对象的副本。

简单说, 在实例中data是对象, 在组件中data就得是函数返回对象。

组件中的data写法示例:

<div id="example-2">
  <simple-counter></simple-counter>
</div>

var data = { counter: 0 }
Vue.component('simple-counter', {
  template: '<button v-on:click="counter += 1">{{ counter }}</button>',
  // 技术上 data 的确是一个函数了,因此 Vue 不会警告,
  // 但是我们返回给每个组件的实例的却引用了同一个data对象
  data: function () {
    return data
  }
})
new Vue({
  el: '#example-2'
})

由于这三个组件共享了同一个 data , 因此增加一个 counter 会影响所有组件!这不对。我们可以通过为每个组件返回全新的 data 对象来解决这个问题:

data: function () {
  return {
    counter: 0
  }
}

更多详情请参考:官网组件数据data传递说明(Component)

1.2.5 组件写法需注意的几个问题

一个组件下只能有一个并列的 div,可以这么写,所以复制官网示例的时候只要复制 div 里面的内容就好。

clipboard.png

但是不能这样写:

clipboard.png

第二。数据要写在 return 里面而不是像文档那样子写

clipboard.png

错误的写法:

clipboard.png

组件使用 :
firstComponent.vue

<template>
  <div id="firstcomponent">
    <h1>I am a title.</h1>
    <a> written by {{ author }} </a>
  </div>
</template>

<script type="text/javascript">
export default {
  data () {
    return {
      author: "微信公众号 jinkey-love"
    }
  }
}
</script>

<style>
</style>

其他Vue问题,可以参考该博文:Vue2.0 新手完全填坑攻略——从环境搭建到发布

2.Vue.js的常用指令请参考官方文档

Vue官方文档

1.v-model,v-for,v-on

在了解了vue的基本用法(数据绑定、指令、缩写、条件渲染等)后,来看一个完整示例,动态添加/删除元素:

<!DOCTYPE html>
<html>

    <head>
        <meta charset="UTF-8">
        <title></title>
        <link rel="stylesheet" href="styles/demo.css" />
    </head>

    <body>
        <div id="app">

            <fieldset>
                <legend>
                    Create New Person
                </legend>
                <div class="form-group">
                    <label>Name:</label>
                    <input type="text" v-model="newPerson.name"/>
                </div>
                <div class="form-group">
                    <label>Age:</label>
                    <input type="text" v-model="newPerson.age"/>
                </div>
                <div class="form-group">
                    <label>Sex:</label>
                    <select v-model="newPerson.sex">
                    <option value="Male">Male</option>
                    <option value="Female">Female</option>
                </select>
                </div>
                <div class="form-group">
                    <label></label>
                    <button @click="createPerson">Create</button>
                </div>
        </fieldset>
        <table>
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Age</th>
                    <th>Sex</th>
                    <th>Delete</th>
                </tr>
            </thead>
            <tbody>
                <tr v-for="person in people">
                    <td>{{ person.name }}</td>
                    <td>{{ person.age }}</td>
                    <td>{{ person.sex }}</td>
                    <td :class="'text-center'"><button @click="deletePerson($index)">Delete</button></td>
                    <td><button @click="greet">Greet</button></td>
                </tr>
            </tbody>
        </table>
        </div>
    </body>
    <script src="js/vue.js"></script>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                newPerson: {
                    name: '',
                    age: 0,
                    sex: 'Male'
                },
                people: [{
                    name: 'Jack',
                    age: 30,
                    sex: 'Male'
                }, {
                    name: 'Bill',
                    age: 26,
                    sex: 'Male'
                }, {
                    name: 'Tracy',
                    age: 22,
                    sex: 'Female'
                }, {
                    name: 'Chris',
                    age: 36,
                    sex: 'Male'
                }]
            },
            methods:{
                createPerson: function(){
                    this.people.push(this.newPerson);
                    
                    // 添加完newPerson对象后,重置newPerson对象
                    this.newPerson = {name: '', age: 0, sex: 'Male'}
                },
                deletePerson: function(index){
                    // 删一个数组元素
                    this.people.splice(index,1);
                    alert(index);
                },

                greet:function(){
                    alert(this.newPerson.sex);
                }
            }
        })
    </script>

</html>

clipboard.png

Demo
Github示例源码

实战示例:

添加、删除表单数据

<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>Vue js</title>
<link rel="stylesheet" type="text/css" href="../assets/css/bootstrap.min.css">
</head>
<body>
  <header class="navbar navbar-fixed-top navbar-inverse">
    <div class="container">
    <h1>Vue开发</h1>
    </div>
  </header>

<div class="container" id="app" style="margin:100px;">
  <div class="row">
    <div class="col-md-offset-2 col-md-8">
      <div class="panel panel-default">
        <div class="panel-heading">
          welcome to Vue.js
        </div>
        <div class="panel-body">
          <h1>{{ message }}</h1>
          <!--
          <input type="text" class="form-control" v-model="message" />
          -->

          <ul class="list-group">
            <li class="list-group-item"
            v-for="(todo,index) in todos">
                {{ todo.id }} {{ todo.title }}
                <button class="btn btn-danger btn-sm pull-right"
                 v-on:click="deleteTodo(index)"
                >删除</button>
            </li>
          </ul>

          <form v-on:submit.prevent="addTodo(newTodo)">
            <div class="form-group">
                <input type="text" v-model="newTodo.title" class="form-control" placeholder="Add a list" />
            </div>
            <div class="form-group">
                <button class="btn btn-success">Add to do</button>
            </div>
          </form>

        </div>
      </div>
    </div>
  </div>
</div>
</body>

<!-- 注意:此处的引入文件需要放在body的后边,需要DOM加载完后,才能获取到#app-->
<script src="../assets/js/vue.js"></script>
<script>
  new Vue({
    el:'#app',
    data:{
      message:"Hello,Vue",
      todos:[
        {id:1,title:'Jack Chan'},
        {id:2,title:'Jet Lee'}
      ],
      newTodo:{id:null,title:""}
    },
    methods:{
      addTodo(newTodo){
        this.todos.push(newTodo)
        this.newTodo = {id:null,title:""}
      },
      deleteTodo(index){
        this.todos.splice(index,1)
      }
    }
  })

</script>
</html>

clipboard.png

注意:在输入框输入数据添加到列表时,需要使用v-on:submit.prevent="addTodo(newTodo)方法对表单提交进行阻止,并使用v-model进行数据双向绑定,当输入框里的数据变化时,Vue实例中的newTodo:{id:null,title:""}属性数据也跟着变化,这样就可以将数据塞入到todos数组中。

2.计算属性computed,v-bind

  <!--  模板不再简单和清晰,所以,这里引入computed,增加代码可读性
          <h1>My todos {{todos.length}}</h1>
          -->
          <h1>My todos {{todosCount}}</h1>

JS中的用法:

 computed:{
      todosCount(){
        return this.todos.length;
      }

完整代码:

<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>Vue js</title>
<link rel="stylesheet" type="text/css" href="../assets/css/bootstrap.min.css">
<style>
 .completed{
   color:#5c5b5c;
   text-decoration: line-through;
 }
</style>
</head>


<body>
  <header class="navbar navbar-fixed-top navbar-inverse">
    <div class="container">
    <h1>Vue开发</h1>
    </div>
  </header>

<div class="container" id="app" style="margin:100px;">
  <div class="row">
    <div class="col-md-offset-2 col-md-8">
      <div class="panel panel-default">
        <div class="panel-heading">
          welcome to Vue.js
        </div>
        <div class="panel-body">
          <!--  模板不再简单和清晰,所以,这里引入computed,增加代码可读性
          <h1>My todos {{todos.length}}</h1>
          -->
          <h1>My todos {{todosCount}}</h1>

          <ul class="list-group">
            <li class="list-group-item"
            v-bind:class="{ 'completed' : todo.completed }"
            v-for="(todo,index) in todos">
                {{ todo.id }} {{ todo.title }}
                <button class="btn btn-warning btn-xs pull-right"
                v-bind:class="[todo.completed ? 'btn-danger' : 'btn-success']"
                 v-on:click="toggleCompletion(todo)"
                >
                {{ todo.completed ? 'undo' : 'complete' }}
                </button>

                <button class="btn btn-danger btn-xs pull-right"
                 v-on:click="deleteTodo(index)"
                >删除</button>
            </li>
          </ul>

          <form v-on:submit.prevent="addTodo(newTodo)">
            <div class="form-group">
                <input type="text" v-model="newTodo.title" class="form-control" placeholder="Add a list" />
            </div>
            <div class="form-group">
                <button class="btn btn-success">Add to do</button>
            </div>
          </form>

        </div>
      </div>
    </div>
  </div>
</div>
</body>

<!-- 注意:此处的引入文件需要放在body的后边,需要DOM加载完后,才能获取到#app-->
<script src="../assets/js/vue.js"></script>
<script>
  new Vue({
    el:'#app',
    data:{
      message:"Hello,Vue",
      todos:[
        {id:1,title:'Jack Chan',completed:true},
        {id:2,title:'Jet Lee',completed:false}
      ],
      newTodo:{id:null,title:""}
    },
    computed:{
      todosCount(){
        return this.todos.length;
      }

    },
    methods:{
      addTodo(newTodo){
        this.todos.push(newTodo)
        this.newTodo = {id:null,title:""}
      },
      deleteTodo(index){
        this.todos.splice(index,1)
      },
      toggleCompletion(todo){
        todo.completed = !todo.completed;
      }
    }
  })

</script>


</html>

二、组件

组件(Component)是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素, Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以是原生 HTML 元素的形式,以 is 特性扩展。

组件化的好处:
增加了代码的可读性,更重要的是增加了代码的可重用性。

1)、全局组件

先注册,然后再使用

<div id="example">
  <my-component></my-component>
</div>

// 注册
Vue.component('my-component', {
  template: '<div>A custom component!</div>'
})
// 创建根实例
new Vue({
  el: '#example'
})

渲染:

<div id="example">
  <div>A custom component!</div>
</div>

注意事项:组件在注册之后,便可以在父实例的模块中以自定义元素 <my-component></my-component> 的形式使用。要确保在初始化根实例 之前 注册了组件。

2)、局部注册

不必在全局注册每个组件。通过使用组件实例选项注册,可以使组件仅在另一个实例/组件的作用域中可用:

var Child = {
  template: '<div>A custom component!</div>'
}
new Vue({
  // ...
  components: {
    // <my-component> 将只在父模板可用
    'my-component': Child
  }
})

对上边的实战示例进行组件化封装处理:
本案例,完成两个组件化工作:
1:对列表进行组件封装,注意在组件模板属性参数的v-bind:todos="todos"传递
2:对表单进行了组装,注意data为function返回

注意在 JavaScript对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。

<!DOCTYPE html>

<!--
组件化的好处:
增加了代码的可读性,更重要的是增加了代码的可重用性。

本案例,完成两个组件化工作:
1:对列表进行组件封装,注意在组件模板属性参数的v-bind:todos="todos"传递
2:对表单进行了组装,注意data为function返回

注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。
-->
<head>
<meta charset="UTF-8">
<title>Vue js</title>
<link rel="stylesheet" type="text/css" href="../assets/css/bootstrap.min.css">
<style>
 .completed{
   color:#5c5b5c;
   text-decoration: line-through;
 }
</style>
</head>


<body>
  <header class="navbar navbar-fixed-top navbar-inverse">
    <div class="container">
    <h1>Vue开发</h1>
    </div>
  </header>

<div class="container" id="app" style="margin:100px;">
  <div class="row">
    <div class="col-md-offset-2 col-md-8">
      <div class="panel panel-default">
        <div class="panel-heading">
          welcome to Vue.js
        </div>
        <div class="panel-body">
          <!--  模板不再简单和清晰,所以,这里引入computed,增加代码可读性
          <h1>My todos {{todos.length}}</h1>
          -->
          <h1>My todos {{todosCount}}</h1>

          <!-- 这里使用组件化封装ul列表,这里需要给组件绑定一个数据属性,v-bind: 可以简写为一个冒号:-->
          <todo-items v-bind:todos="todos"></todo-items>
          <todo-form :todos="todos"></todo-form>

        </div>
      </div>
    </div>
  </div>
</div>
</body>

<!-- 列表组件 -->
<script type="text/x-template" id="todo-items-template">
  <ul class="list-group">
    <li class="list-group-item"
    v-bind:class="{ 'completed' : todo.completed }"
    v-for="(todo,index) in todos">
        {{ todo.id }} {{ todo.title }}
        <button class="btn btn-warning btn-xs pull-right"
        v-bind:class="[todo.completed ? 'btn-danger' : 'btn-success']"
         v-on:click="toggleCompletion(todo)"
        >
        {{ todo.completed ? 'undo' : 'complete' }}
        </button>

        <button class="btn btn-danger btn-xs pull-right"
         v-on:click="deleteTodo(index)"
        >删除</button>
    </li>
  </ul>
</script>

<!-- 表单组件 -->
<script type="text/x-template" id="add-form-template">
  <form v-on:submit.prevent="addTodo(newTodo)">
    <div class="form-group">
        <input type="text" v-model="newTodo.title" class="form-control" placeholder="Add a list" />
    </div>
    <div class="form-group">
        <button class="btn btn-success">Add to do</button>
    </div>
  </form>
</script>

<!-- 注意:此处的引入文件需要放在body的后边,需要DOM加载完后,才能获取到#app-->
<script src="../assets/js/vue.js"></script>
<script>

// 组件化-列表
Vue.component('todo-items',{
  template:'#todo-items-template',
  props:['todos'],   // 定义一个属性
  methods:{
    deleteTodo(index){
      this.todos.splice(index,1)
    },
    toggleCompletion(todo){
      todo.completed = !todo.completed;
    }
  }
})

// 组件化-表单
Vue.component('todo-form',{
  template:'#add-form-template',
  props:['todos'],

  // data为function返回
  data(){
    return{
      newTodo:{id:null,title:"",completed:false}
    }
  },
  methods:{
    addTodo(newTodo){
      this.todos.push(newTodo)
      this.newTodo = {id:null,title:"",completed:false}
    }
  }
})

  new Vue({
    el:'#app',
    data:{
      message:"Hello,Vue",
      todos:[
        {id:1,title:'Jack Chan',completed:true},
        {id:2,title:'Jet Lee',completed:false}
      ],
    },
    computed:{
      todosCount(){
        return this.todos.length;
      }

    }
  })

</script>
</html>

三、vue-cli脚手架

vue-cli脚手架Github地址:https://github.com/vuejs/vue-cli

1、node安装vue-cli脚手架

// 这里使用淘宝的镜像cnpm
$ cnpm install -g vue-cli  

安装好之后,可以通过vue命令查看:
clipboard.png

2、命令安装webpack项目

➜  Code vue init webpack vuejs-cli

clipboard.png

然后再执行上边给出的提示命令:

cd vuejs-2.0-cli
npm install
npm run dev

执行完上边的命令后,会打开浏览器的http://localhost:8080/#/页面

clipboard.png

我们看一下这个目录下边的文件:

clipboard.png

3.vue-router

vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,适合用于构建单页面应用。vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来。传统的页面应用,是用一些超链接来实现页面切换和跳转的。在vue-router单页面应用中,则是路径之间的切换,也就是组件的切换。

vue-router 快速入门
Vue路由的使用

<div class="list-group">
    <a class="list-group-item" v-link="{ path: '/home'}">Home</a>
    <a class="list-group-item" v-link="{ path: '/about'}">About</a>
</div>

4.JavaScript ES6中export及export default的区别

相信很多人都使用过export、export default、import,然而它们到底有什么区别呢? 在JavaScript ES6中,export与export default均可用于导出常量、函数、文件、模块等,你可以在其它文件或模块中通过import+(常量 | 函数 | 文件 | 模块)名的方式,将其导入,以便能够对其进行使用,但在一个文件或模块中,export、import可以有多个,export default仅有一个。

具体事例,请看下边的原文章:
JavaScript ES6中export及export default的区别

四、Vue请求api之vue-axios使用

axios github官方地址
vue-axios 扩展包GitHub地址

我们在学习vue的API请求,所以,我们用第二个特定的包vue-axios来安装https://github.com/imcvampire...

1.安装

npm install --save axios vue-axios

2.使用

安装好之后,引入到使用的文件中

import Vue from 'vue'
import axios from 'axios'
import VueAxios from 'vue-axios'

Vue.use(VueAxios, axios)

示例:

Vue.axios.get(api).then((response) => {
  console.log(response.data)
})

this.axios.get(api).then((response) => {
  console.log(response.data)
})

五、laravel做后端API提供数据

这里使用axios请求接口,会出现跨域的问题,不过,我们可以通过安装https://github.com/barryvdh/laravel-cors库来解决这个问题。

六、Vuex学习

1、什么是Vuex?

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

2、state理解

state 这样概念初次接触的时候可能会感觉到有点模糊,简单来说就是将 state 看成我们项目中使用的数据的集合。然后,Vuex 使得 组件本地状态(component local state)应用层级状态(application state) 有了一定的差异。

  • component local state:该状态表示仅仅在组件内部使用的状态,有点类似通过配置选项传入 Vue 组件内部的意思。

  • application level state:应用层级状态,表示同时被多个组件共享的状态层级。

假设有这样一个场景:我们有一个父组件,同时包含两个子组件。父组件可以很容易的通过使用 props 属性来向子组件传递数据。

但是问题来了,当我们的两个子组件如何和对方互相通信的? 或者子组件如何传递数据给他父组件的?在我们的项目很小的时候,这个两个问题都不会太难,因为我们可以通过事件派发和监听来完成父组件和子组件的通信。

然而,随着我们项目的增长:

  • 保持对所有的事件追踪将变得很困难。到底哪个事件是哪个组件派发的,哪个组件该监听哪个事件?

  • 项目逻辑分散在各个组件当中,很容易导致逻辑的混乱,不利于我们项目的维护。

  • 父组件将变得和子组件耦合越来越严重,因为它需要明确的派发和监听子组件的某些事件。

这就是 Vuex 用来解决的问题。 Vuex 的四个核心概念分别是:

  • The state tree:Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。至此它便作为一个『唯一数据源(SSOT)』而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。

  • Getters:用来从 store 获取 Vue 组件数据。

  • Mutators:事件处理器用来驱动状态的变化。

  • Actions:可以给组件使用的函数,以此用来驱动事件处理器 mutations

如何你暂时还不太理解这个四个概念,不用着急,我们将在后面的项目实战中详细的解释。

Vuex 应用中数据的流向(Vuex 官方图)
clipboard.png

上边的流程图简单解释下:

Vuex 规定,属于应用层级的状态只能通过 Mutation 中的方法来修改,而派发 Mutation 中的事件只能通过 action。

从左到又,从组件出发,组件中调用 action,在 action 这一层级我们可以和后台数据交互,比如获取初始化的数据源,或者中间数据的过滤等。然后在 action 中去派发 Mutation。Mutation 去触发状态的改变,状态的改变,将触发视图的更新。

注意事项

  • 数据流都是单向的

  • 组件能够调用 action

  • action 用来派发 Mutation

  • 只有 mutation 可以改变状态

  • store 是响应式的,无论 state 什么时候更新,组件都将同步更新

3、Vuex目录结构

我们来看一下创建的Vuex项目的目录结构:

clipboard.png

  • components/ 文件夹用来存放我们的 Vue 组件

  • vuex/ 文件夹存放的是和 Vuex store 相关的东西(state object,actions,mutators)

  • build/ 文件是 webpack 的打包编译配置文件

  • config/ 文件夹存放的是一些配置项,比如我们服务器访问的端口配置等

  • dist/ 该文件夹一开始是不存在,在我们的项目经过 build 之后才会产出

  • App.vue 根组件,所有的子组件都将在这里被引用

  • index.html 整个项目的入口文件,将会引用我们的根组件 App.vue

  • main.js 入口文件的 js 逻辑,在 webpack 打包之后将被注入到 index.html 中

注:本博客Vuex部分内容转自该博文:使用 Vuex + Vue.js 构建单页应用,博文作者对Vuex理解的比较透彻,所以转过来学习下。

4、将上边实战列表表单例子用Vuex重构

clipboard.png

先看Vuex的目录结构:

clipboard.png

主要是在 src目录下做组件重构:

先看列表组件Todos.vue:

<template>
  <div id="todos">

  <ul class="list-group">
    <li class="list-group-item"
    v-bind:class="{ 'completed' : todo.completed }"
    v-for="(todo,index) in todos">
        {{ todo.id }}
        <router-link :to="{ name: 'todo', params: { id: todo.id }}">{{ todo.title }}</router-link>
        <button class="btn btn-warning btn-xs pull-right margin-right-10"
        v-bind:class="[todo.completed ? 'btn-danger' : 'btn-success']"
         v-on:click="toggleCompletion(todo)"
        >
        {{ todo.completed ? 'undo' : 'complete' }}
        </button>

        <button class="btn btn-danger btn-xs pull-right margin-right-10"
         v-on:click="deleteTodo(todo, index)"
        >删除</button>
    </li>
  </ul>

  <todo-form></todo-form>

</div>
</template>

<style>
  .completed{
    color:#5c5b5c;
    text-decoration: line-through;
  }
  .margin-right-10{
    margin-right: 10px;
  }
</style>

<script>
import TodoForm from './TodoForm';
  export default{
    name:'todos',
  //  props:['todos'],   // 定义一个属性
  computed: {
   todos() {
     return this.$store.state.todos
   }
 },
    methods:{
      deleteTodo(todo, index){
        this.$store.dispatch('removeTodo', todo, index)
      },
      toggleCompletion(todo){
        this.$store.dispatch('completeTodo', todo)
      }
    },
    components:{
        TodoForm
      }
  }
</script>

表单组件TodoForm.vue

<template>
  <form v-on:submit.prevent="addTodo(newTodo)">
     <div class="form-group">
         <input type="text" v-model="newTodo.title" class="form-control" placeholder="Add a list" />
     </div>
     <div class="form-group">
         <button class="btn btn-success" type="submit">Add to do</button>
     </div>
   </form>
</template>

<style>
  .completed{
    color:#5c5b5c;
    text-decoration: line-through;
  }
  .margin-right-10{
    margin-right: 10px;
  }
</style>

<script>
  export default{
    //  props:['todos'],
     // 子组件数据属性
     /*   Vuex 写法,将 newTodo 放入到 state 中
     data(){
       return{
         newTodo:{id:null,title:"",completed:false}
       }
     },*/
     computed: {
      newTodo() {
        return this.$store.state.newTodo
      }
    },
     methods:{
       addTodo(newTodo){
         // this.todos.push(newTodo)
         // this.newTodo = {id:null,title:"",completed:false}
           this.$store.dispatch('saveTodo', newTodo)
       }
     }
  }
</script>

App.vue总组件:

<template>
  <div id="app">
    <img src="./assets/logo.png">
    <!--
    <todos :todos="todos"></todos>
  -->

  <!-- 路由出口 -->
  <!-- 路由匹配到的组件将渲染在这里 -->
  <router-view></router-view>
  </div>
</template>

<script>
import Hello from './components/Hello';
import Todos from './components/Todos';
export default {
  name: 'app',
  /*
  data(){
    return {
      todos:[
        {id:1,title:'Jack Chan',completed:true},
        {id:2,title:'Jet Lee',completed:false}
      ]
    }
  },
  */
  // 异步请求
  mounted(){
    /*
    this.axios.get('http://baidu.com').then(response =>{
      console.log(response.data)
    })*/
    this.$store.dispatch('getTodos')
  },
  computed:{
    todosCount(){
      return this.$store.todos.length;
    }
  },
  components: {
    Hello
  }
}
</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>

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'

// 引入axios请求API
import axios from 'axios'
import VueAxios from 'vue-axios'
Vue.use(VueAxios, axios)

// 引入路由
import VueRouter from 'vue-router'
Vue.use(VueRouter)

// 引入vuex
import Vuex from 'vuex'
Vue.use(Vuex)

import Todos from './components/Todos';
import Todo from './components/Todo';

const routes = [
  { path: '/', component: Todos },
  { path: '/todo/:id', component: Todo, name:'todo'}
]

const router = new VueRouter({
  routes // (缩写)相当于 routes: routes
})


/* ====================== Vuex说明 ==================== //
  store:可以理解为一个大的容器
  state: 相当于一个全局变量数据属性
  mutations:里边的方法专门操作state里边的全局变量
  actions: 模型组件操作actions,获取数据,然后触发mutations
  小结:Vuex的核心,主要是理解其应用的场景,如果理清了这些思路,就可以很容易进行开发了。
 另外我们也可以将store这变量拆分为一个store.js文件,然后引用进来即可。
====================================================== */

// Vuex example
// store 可以理解为一个大的容器
const store = new Vuex.Store({

  state: {    // state 可以理解为全局的变量,可以在任意组件中使用
    todos: [],
    newTodo:{id:null, title:"", completed:false}
  },

  // mutations里边的方法用来修改state里的数据
  mutations: {
    get_todo_list(state, todos) {
      state.todos = todos;
    },
    complete_todo(state, todo){
      todo.completed = ! todo.completed
    },
    delete_todo(state, index){
      state.todos.splice(index, 1)
    },
    add_todo(state, todo){
      state.todos.push(todo)
    }
  },
  // actions 主要用来获取客户端的数据,然后将数据传给 mutations 的方法
  actions:{
    getTodos(store){
      store.commit('get_todo_list', [{id : 1, title:"hello,world!"}])
    },
    completeTodo(state, todo){
      store.commit('complete_todo', todo)
    },
    removeTodo(store, todo, index){
      store.commit('delete_todo', index)
    },
    saveTodo(store, todo){
      store.commit('add_todo', todo)
      store.state.newTodo = {id:null,title:"",completed:false}
    },


  }
})

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

主页面 index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>vuejs-2.0-cli</title>
    <link rel="stylesheet" href="https://cdn.static.runoob.com/libs/bootstrap/3.3.7/css/bootstrap.min.css">
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

本项目GitHub地址:
https://github.com/corwien/vu...


更多有关Vue.js学习的文章,请看这里:
Vue.js——60分钟快速入门
Vue.js——基于$.ajax实现数据的跨域增删查改
Vuejs2.0 文档攻略-介绍
使用 Vuex + Vue.js 构建单页应用
使用Vue.js和Vuex实现购物车场景
Vuex学习

阅读 2.5k

推荐阅读
Corwien
用户专栏

为者常成,行者常至!

960 人关注
290 篇文章
专栏主页