All Projects → zhangzhenfei → vue2.0-tutorial

zhangzhenfei / vue2.0-tutorial

Licence: MIT License
一个vue2.0的教程,带你一步一步学习vue2.0相关技术栈,在这里,你可以学到vue2.0组件式开发、使用vue-router开发SPA应用、使用vuex管理应用状态等

Programming Languages

javascript
184084 projects - #8 most used programming language
Vue
7211 projects
HTML
75241 projects

Projects that are alternatives of or similar to vue2.0-tutorial

Vue-CAMP
VueJS
Stars: ✭ 16 (-72.41%)
Mutual labels:  vue-cli, vue-router
vue-for-my-english-app
this is a vue demo project for green hand
Stars: ✭ 80 (+37.93%)
Mutual labels:  vue-cli, vue-router
vue-automation
一款开箱即用的 Vue 项目模版,基于 Vue 2.x
Stars: ✭ 31 (-46.55%)
Mutual labels:  vue-cli, vue-router
sophia
👩‍⚖️‍ Sophia:哲学知识共享社区(Built by Vue)
Stars: ✭ 31 (-46.55%)
Mutual labels:  vue-cli, vue-router
vue-bootstrap-boilerplate
📦 Vue 2/3, Bootstrap 5, Vuex, Vue-Router, Sass/Scss, ESLint, Axios (switch to vue3 branch)
Stars: ✭ 86 (+48.28%)
Mutual labels:  vue-cli, vue-router
vue2.0-SellPosSystem
vue2.0实战项目——简单的快餐店系统
Stars: ✭ 35 (-39.66%)
Mutual labels:  vue-cli, vue-router
vue-cli-easy
this is a vue development environment。
Stars: ✭ 30 (-48.28%)
Mutual labels:  vue-cli, vue-router
Netease Music Demo
「网易云音乐」Demo。使用了 Vue2.0 全家桶来开发的。
Stars: ✭ 254 (+337.93%)
Mutual labels:  vue-cli, vue-router
aura-admin
Aura Admin is the Web App that helps you to mange the Tech Communities like GDGs, DSCs or any other tech communities with Aura
Stars: ✭ 58 (+0%)
Mutual labels:  vue-cli, vue-router
vue-element-admin
Vuejs实例-Vuejs2.0全家桶结合ELementUI制作后台管理系统http://www.cnblogs.com/weiqinl/p/6873761.html
Stars: ✭ 97 (+67.24%)
Mutual labels:  vue-cli, vue-router
huobi-PC
火币桌面客户端,基于electorn-vue开发
Stars: ✭ 56 (-3.45%)
Mutual labels:  vue-cli, vue-router
cloudMusic
(移动端)Vue2.0+Nodejs网易云音乐,网易云音乐api强力驱动,高音质破解(持续更新中)
Stars: ✭ 14 (-75.86%)
Mutual labels:  vue-cli, vue-router
easy-resume
🎉 A less is more online resume editor!
Stars: ✭ 116 (+100%)
Mutual labels:  vue-cli, vue-router
Code-VueWapDemo
“Vue教程--Wap端项目搭建从0到1”的源码
Stars: ✭ 19 (-67.24%)
Mutual labels:  vue-cli, vue-router
easy-reading
An e-book reader powered by Vue and Epubjs.
Stars: ✭ 10 (-82.76%)
Mutual labels:  vue-cli, vue-router
vux2.5-webapp
商城布局:Grid、Card、cell、panel、flexbox 省市区联动:x-address 状态切换:x-switch 弹出层:x-dialog 提示框:toast 选项卡:Tab 表单功能
Stars: ✭ 39 (-32.76%)
Mutual labels:  vue-router, vue-vuex
Web designer
网页设计器图形化工具,通过拖拽组件进行页面排版和生成页面代码
Stars: ✭ 219 (+277.59%)
Mutual labels:  vue-cli, vue-router
Vuemmerce
👉 Responsive ecommerce template 🛒 built with Vue.js and Nuxt.js
Stars: ✭ 223 (+284.48%)
Mutual labels:  vue-cli, vue-router
special-vue-series-code-analyzing
「Vue生态库源码系列」,Vue、Vue-router、Vuex、Vue-cli、Vue-loader、Vue-devtools等
Stars: ✭ 15 (-74.14%)
Mutual labels:  vue-cli, vue-router
capacitor-vue-ionicv4-app
sample app using capacitor vuejs and ionicv4 components
Stars: ✭ 70 (+20.69%)
Mutual labels:  vue-cli, vue-router

vue2.0-tutorial

一个vue2.0的教程,带你一步一步学习vue2.0相关技术栈,在这里,你可以学到vue2.0组件式开发、使用vue-router开发SPA应用、使用vuex管理应用状态等。

lesson01.vue-cli

vue-cli是vue官方提供的脚手架,可以快速根据配置生成项目,第一讲是vue2.0的入门,就从官方的vue-cli开始把,可以避免新手专注于学习vue相关语法,避免在环境配置方面浪费太多时间。

vue-cli github地址:vue-cli

接下来就根据github的文档来安装vue-cli: 首先把vue-cli安装为全局包

$ npm install -g vue-cli

安装完毕后,进入你要生成项目的路径,使用vue-cli命令生成项目,vue-cli的生成项目的语法为:

$ vue init <template-name> <project-name>

template-name可选项如下

  1. webpack - A full-featured Webpack + vue-loader setup with hot reload, linting, testing & css extraction.
  2. webpack-simple - A simple Webpack + vue-loader setup for quick prototyping.
  3. browserify - A full-featured Browserify + vueify setup with hot-reload, linting & unit testing.
  4. browserify-simple - A simple Browserify + vueify setup for quick prototyping.
  5. simple - The simplest possible Vue setup in a single HTML file

project-name为你生成项目的名称 举个栗子:

$ vue init webpack 01.vue-cli

回车后,项目生成的时候会提示你输入项目名称,是否需要安装vue-router等配置项,这里我们就按照默认项来,一路回车到底,项目就生成了,我们进入到项目01.vue-cli的目录

$ cd 01.vue-cli/

安装相关的依赖项,并运行项目

$ npm install
$ npm run dev

正常来说命令执行成功后,浏览器会自动打开http://localhost:8080/#/,显示模板项目的主页

让我们来在模板项目上加一点官网案例,熟悉一下vue语法以及应用,项目源码放在了src目录下,这里我们可以发现项目结构如下:

|src // 源码路径
|--assets // 静态资源
|--components // vue组件目录
|----Hello.vue 
|--router // 路由目录
|----index.js
|--App.vue // vue root components
|--main.js // 项目入口

分析一下App.vue文件

<template>
  <div id="app">
    <img src="./assets/logo.png">
    <router-view></router-view>
  </div>
</template>

可以发现Hello.vue作为App.vue的子组件,所以我们这里直接在hello.vue上写测试代码,这里我们实现一个小小的需求,界面上有两个元素,一个文本域,一个输入框,文本域的内容根据输入框的输入信息自动更新。

打开Hello.vue,尝试在<templete></templete>中的<div class="hello">...</div>节点后增加一个<div id="demo"></div> 界面立即响应变化,报错了,报错信息如下:

Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.

组件的templete不允许包含超过一个根元素,这个和react的jsx控制规则是一样的,所以我们把<div id="demo"></div>移动到<div class="hello">...</div>中,作为其子元素

<div id="hello">
...
    <div id="demo">
      <p>{{ message }}</p>
      <input type="text" v-model="message"/>
    </div>
</div>

在script中,我们加了一个message,使其数据可响应

export default {
  name: 'hello',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App',
      message: ''
    }
  }
}

so,我们就完成了这个简单的需求,是不是非常easy,vue帮我们处理了数据与DOM之间的更新与变化,界面上所有的东西都可以看做是数据的映射,只要操作数据,界面会根据组件的配置自动更新,使我们能把精力放在行为和业务上,大大提高了编码生产力,同时,vue会很聪明的处理更新的队列,把同一个事物里的变化只做一次提交,而且只修改DOM变化的部分。

好了,您也来试试把。

$ cd lesson/01.vue-cli
$ npm install
$ npm run dev

01.vue-cli

lesson02.vue-router

这一节中,我们将学习vue,vue-router的集成和应用,做一个简单的SPA。 首先说一下路由的概念,为什么要有前端路由呢,使用前端路由是为了把多页应用改造成单页应用,单页应用利用浏览器的地址模拟了多页应用不同地址渲染不同功能的需求,同时避免了路由跳转的时候重新请求服务端获取数据重新渲染页面。

这节课的需求是:在界面上定义两个链接,点击其中一个链接渲染对应的vue子组件页面 对于vue-router不熟的同学可以先看看概念:vue-router

现在我们来分析一下,我们需要定义链接的组件应该是根组件App.vue,偷懒一下,直接把官网的demo拷进去

<template>
  <div id="app">
    <img src="./assets/logo.png">
    <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>
</template>

界面定义好了,那么我们需要写两个组件:Foo&Bar,组件放在components下

// Bar.vue
<template>
  <div>{{msg}}</div>
</template>

<script>
export default {
  name: 'bar',
  data () {
    return {
      msg: 'bar page'
    }
  }
}
</script>
// Foo.vue
<template>
  <div>{{msg}}</div>
</template>

<script>
export default {
  name: 'foo',
  data () {
    return {
      msg: 'foo page'
    }
  }
}
</script>

组件也定义好了,怎么把组件和路由关联起来呢,打开router/index.js,把组件注册到路由中

import Vue from 'vue'
import Router from 'vue-router'
import Foo from '../components/Foo'
import Bar from '../components/Bar'
import Hello from '../components/Hello'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Hello',
      component: Hello
    },
    { path: '/foo', component: Foo },
    { path: '/bar', component: Bar }
  ]
})

打开项目,效果是不是出来啦

$ cd lesson/02.vue-router
$ npm install
$ npm run dev

02.vue-router

lesson03.vue-todos

这一节,将会一步一步从零开始,完成一个todos应用,这个也是官网的例子,当应用从简单变复杂,我们要怎么去实现呢,这里我们只使用vue和vue-router来实现todos的增删改查,使用vue-router实现todos的过滤功能。

初始化应用

$ cd lesson
$ vue init webpack 03.vue-todos
$ cd vue-todos
$ npm install
$ npm run dev

浏览器自动打开并显示主页说明初始化成功

首先分析一下todos这个功能,从功能上可以划分为三个部分

  1. 头部(Header)
  2. 内容展示部分(Todos) 2.1 列表项(todo-item)
  3. 脚部过滤条件部分(Footer)

components.png

framework.png

那么我们就先把组件定义出来 components/Header/Header.vue

<template>
  <header class="header">
    <h1>todos</h1>
    <input 
      class="new-todo" 
      autofocus autocomplete="off" 
      placeholder="What needs to be done?" >
  </header>

</template>

<script>
  export default {
    name: 'header',
    data () {
      return {
        tip: 'What needs to be done?'
      }
    }
  }
</script>

components/Todos/Todos.vue

<template>

  <section class="main">
    <input class="toggle-all" type="checkbox">
    <ul class="todo-list">
    </ul>
  </section>

</template>

<script>
  export default {
    name: 'todos'
  }

</script>

components/Footer/Footer.vue

<template>
  <footer class="footer">
    <span class="todo-count">
        2 items left
      </span>
    <ul class="filters">
      <li><a href="#/all">All</a></li>
      <li><a href="#/active">Active</a></li>
      <li><a href="#/completed">Completed</a></li>
    </ul>
    <button class="clear-completed">
        Clear completed
    </button>
  </footer>
</template>

<script>
  export default {
    name: 'footer',
    data () {
      return {
        remain: '2 items left'
      }
    }
  }
</script>

组件定义好之后,我们需要在App.vue顶级组件中组合起来 components/Todos/Todos.vue

<template>

  <section class="main">
    <input class="toggle-all" type="checkbox">
    <ul class="todo-list">
    </ul>
  </section>

</template>

<script>
  export default {
    name: 'todos'
  }

</script>

App.vue

<template>
  <div id="app" class="todoapp">
    <Header />
    <Todos />
    <Footer />
  </div>
</template>

<script>
  import Header from './components/Header/Header'
  import Footer from './components/Footer/Footer'
  import Todos from './components/Todos/Todos'
  export default {
    name: 'app',
    components: {
      Header, Footer, Todos
    }
  }

</script>

好了,我们运行一下项目看看效果

$ npm run dev

demo

引入TODOMVC-APP-CSS 官方TODOMVC样式

基本框架是出来了,但是缺少样式,非常难看,我们把官方的todos的样式安装一下

npm install --save todomvc-app-css

如果需要import css,并打包成js,需要安装下面三个loader

cnpm install style-loader --save-dev
cnpm install css-loader --save-dev
cnpm install file-loader --save-dev

然后在App.vue中加上

<style>
  @import '../node_modules/todomvc-app-css/index.css'
</style>

重新运行项目,发现官方的样式全都加上去啦,现在我们又学会了一种引入第三方css的方法 import

现在我们打开浏览器控制台,发现了几个报错

Do not use built-in or reserved HTML elements as component id:

组件名字不要使用内置的或保留HTML元素为组件id,让我们修改App.vue的script部分如下

import MyHeader from './components/Header/Header'
import MyFooter from './components/Footer/Footer'
import Todos from './components/Todos/Todos'
export default {
  name: 'app',
  components: {
    MyHeader, MyFooter, Todos
  }
}

父子组件通讯

现在我们开始思考Todos应用的状态数据,大概如下:

{
    todos: [], // 存储所有todos
    visibility: 'all' // 存储当前过滤条件
}

那么,我们就在App.vue中先把这些数据定义在data中

注意,vue2.0中,组件的data定义只接受 function

export default {
    name: 'app',
    data () {
      return {
        todos: [], // 存储所有todos
        visibility: 'all' // 存储当前过滤条件
      }
    },
    components: {
      MyHeader, MyFooter, Todos
    }
  }

那么我们要怎么监听MyHeader的输入框,并且获取数据放入todos中呢,根据vue组件间的通信规则:

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

首先改造Header组件,使其能够监听input的键盘回车时间,并且主动触发对应的自定义事件发送消息给父组件

<template>
  <header class="header">
    <h1>todos</h1>
    <input 
      class="new-todo" 
      autofocus 
      autocomplete="off" 
      placeholder="What needs to be done?" 
      v-model="newTodo"
      @keyup.enter="addTodo">
  </header>
</template>

<script>
  export default {
    name: 'header',
    data () {
      return {
        newTodo: '' // 使用v-model将input的vulue属性绑定到newTodo
      }
    },
    methods: {
      addTodo () {
        // 主动触发addTodoHandle方法,'addTodoHandle'为向父组件传递的数据
        this.$emit('addTodoHandle', this.newTodo) 
      }
    }
  }
</script>

我们需要把MyHeader组件绑定一个addTodoHandle事件,给App发送消息,App.vue

<template>
  <div id="app" class="todoapp">
    <MyHeader @addTodoHandle="addTodo"/>
    <Todos />
    <MyFooter />
  </div>
</template>

<script>
  import MyHeader from './components/Header/Header'
  import MyFooter from './components/Footer/Footer'
  import Todos from './components/Todos/Todos'
  export default {
    name: 'app',
    data () {
      return {
        todos: [], // 存储所有todos
        visibility: 'all' // 存储当前过滤条件
      }
    },
    methods: {
      addTodo (value) {
        console.log(value) // 输出看看子组件发送的消息内容
      }
    },
    components: {
      MyHeader, MyFooter, Todos
    }
  }

</script>

<style>
  @import '../node_modules/todomvc-app-css/index.css'
</style>

我们在template中加了@addTodoHandle="addTodo"监听了组件的addTodoHandle方法,绑定到App的methods中的addTodo方法,所以子组件使用this.$emit('addTodoHandle')会触发父组件的addTodo回调方法,addTodo方法中,我们将子组件emit的数据打印出来,现在运行程序,在input中输入一些内容,按回车可以看到控制台打印的数据。

addTodo

使用vue-router实现过滤功能

现在我们思考一下,如何用路由来实现todos的过滤,需求,当我们点击footer的a标签的时候,需要动态的改变Todos.vue的渲染内容。

那么根据第二节课程所学到的知识,我们可以把footer的标签当作一个router-link,作为路由的导航。

组件支持用户在具有路由功能的应用中(点击)导航。 通过 to 属性指定目标地址,默认渲染成带有正确链接的 标签,可以通过配置 tag 属性生成别的标签.。另外,当目标路由成功激活时,链接元素自动设置一个表示激活的 CSS 类名。

router-link组件最终编译出来的DOMa标签,所以,我们改造一下Footer.vue其中的filters部分。

<ul class="filters">
  <!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
  <li><router-link to="/All">All</router-link></li>
  <li><router-link to="/Active">Active</router-link></li>
  <li><router-link to="/Completed">Completed</router-link></li>
</ul>

然后修改App.vue,让其支持vue-router动态渲染Todos组件

<template>
  <div id="app" class="todoapp">
    <MyHeader @addTodoHandle="addTodo"/>
    <!-- 路由匹配到的组件将渲染在这里 -->
    <router-view></router-view>
    <MyFooter />
  </div>
</template>

<script>
  import MyHeader from './components/Header/Header'
  import MyFooter from './components/Footer/Footer'
  
  export default {
    name: 'app',
    data () {
      return {
        todos: [], // 存储所有todos
        visibility: 'all' // 存储当前过滤条件
      }
    },
    methods: {
      addTodo (value) {
        console.log(value)
      }
    },
    components: {
      MyHeader, MyFooter
    }
  }

</script>

<style>
  @import '../node_modules/todomvc-app-css/index.css'
</style>

接下来,修改router/index.js路由的注册,让其接受动态路由参数filter

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

Vue.use(Router)

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

这里说明一下为啥import Todos 的路径使用@/components/Todos/Todos,这里@是当前项目的根路径下的src文件夹,在项目的build/wepack.base.config.js中定义了别名。

resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src')
    }
  },

然后让Todos.vue测试一下,当点击footer的过滤条件的时候,watch路由的变化,并打印出来

  export default {
    name: 'todos',
    watch: {
      '$route': function (to, from) {
        console.log(to)
      }
    }
  }

filter

但是我们可以发现有一个问题,当我们点击footer过滤条件的时候,没有激活样式,经过翻阅todomvc-app-css的样式,发现激活样式是selected,而vue-router默认的激活样式如下

默认值: "router-link-active"

所以我们需要对Footer.vue做小小改动,在对应的router-link增加属性active-class="selected"支持激活样式

<li><router-link to="/All" active-class="selected">All</router-link></li>
<li><router-link to="/Active" active-class="selected">Active</router-link></li>
<li><router-link to="/Completed" active-class="selected">Completed</router-link></li>

还有一个问题,访问应用默认的主页localhost:8080的时候,没有默认的indexRoute主页路由,我们想达到这样的需求,当进入默认主页的时候,让浏览器请求localhost:8080/#/All,即使用默认的All过滤,这时候我们需要让App.vue判断路由状态并跳转,在App.vue中script中,增加created的生命钩子

created () {
  let filter = this.$route.params.filter
  if (!filter) {
    this.$router.replace({ path: 'All' })
  }
},

现在我们打开localhost:8080,发现会自动定向到localhost:8080/#/All,但是footerreact-link并没有支持,阅读官方文档,发现需要修改Footer.vue做以下改动,增加exact replace支持路由根据当前链接点亮

<li><router-link to="/All" active-class="selected" exact replace>All</router-link></li>
<li><router-link to="/Active" active-class="selected" exact replace>Active</router-link></li>
<li><router-link to="/Completed" active-class="selected" exact replace>Completed</router-link></li>

使用localStorage存储应用数据

现在我们程序已经支持根据路由来动态过滤todos,但是对应的新建todo,和历史todos还没实现,现在我们就来一步一步实现,首先在src下定义store文件夹,作为整个程序的状态存储库,src/store/index.js文件内容如下:

const STORAGE_FILTER_KEY = 'VUE_FILTER'
const STORAGE_TODOS_KEY = 'VUE_TODOS'
const MODEL = {
  FILTER: localStorage.getItem(STORAGE_FILTER_KEY) || 'All',
  TODOS: JSON.parse(localStorage.getItem(STORAGE_TODOS_KEY)) || []
}

export default {
  filter: {
    get: () => MODEL.FILTER,
    save: path => localStorage.setItem(STORAGE_FILTER_KEY, path)
  },
  todos: {
    fetch: () => MODEL.TODOS,
    save: () => {
      localStorage.setItem(STORAGE_TODOS_KEY, JSON.stringify(MODEL.TODOS))
    }
  }
}

store内容很简单,包括两个部分的数据存储,一个是当前filterget/set,另一个是todosget/set,都是使用localStorage作为数据的存储,现在我们需要改造App.vue支持新建todo

为什么需要store,因为App.vueTodos.vue无法直接通过router共享数据todos,所以我们把该部分数据抽离到store统一维护,避免后面组件增加的时候,组件和组件之间的数据交织

<script>
  import uuid from 'uuid'
  import MyHeader from './components/Header/Header'
  import MyFooter from './components/Footer/Footer'
  import Store from './store/index'
  export default {
    name: 'app',
    data () {
      return {
        todos: Store.todos.fetch() // 存储所有todos
      }
    },
    created () {
      let filter = this.$route.params.filter
      if (!filter) {
        this.$router.replace({ path: 'All' })
      }
    },
    methods: {
      addTodo (value) {
        value = value && value.trim()
        if (!value) {
          return
        }
        this.todos.push({
          id: uuid(),
          title: value,
          completed: false
        })
      }
    },
    // watch todos change for localStorage persistence
    watch: {
      todos: {
        handler: function (todos) {
          Store.todos.save()
        },
        deep: true
      }
    },
    components: {
      MyHeader, MyFooter
    }
  }

</script>

代码说明:

  1. 首先data钩子函数对所有todos进行观察
  2. 当Header触发了addTodo方法,将新建的todo压入todos中
  3. app中watch钩子函数对todos进行深度的观察,当todos发生了变化,调用Store.todos.save保存

注意,addTodo中使用到了uuid(),这个包如果没有安装,需要安装一下 npm i uuid --save

Todos.vue也需要从Store中获取最新的todos进行展示

<template>
  <section class="main">
    <input class="toggle-all" type="checkbox" >
    <ul class="todo-list">
      <li v-for="todo in filteredTodos"
        class="todo"
        :key="todo.id"
        :class="{ completed: todo.completed }">
        <div class="view">
          <input class="toggle" type="checkbox" v-model="todo.completed">
          <label>{{ todo.title }}</label>
          <button class="destroy"></button>
        </div>
      </li>
    </ul>
  </section>
</template>

<script>
  import Store from '@/store/index'
  // visibility filters
  const filters = {
    All: todos => todos,
    Active: todos => (todos.filter(todo => !todo.completed)),
    Completed: todos => (todos.filter(todo => todo.completed))
  }
  export default {
    name: 'todos',
    data () {
      return {
        todos: Store.todos.fetch(),
        filteredTodos: [] // 根据filter过滤后的todos
      }
    },
    created () {
      // 组件创建完后获取数据,
      // 此时 data 已经被 observed 了
      this.filterTodos()
    },
    watch: {
       // 如果路由有变化,会再次执行该方法
      '$route': function (to) {
        Store.filter.save(to.params.filter)
        this.filterTodos()
      },
      // 观察todos,如果todos有变化,重新执行过滤方法
      todos: {
        handler: function (todos) {
          Store.todos.save()
          this.filterTodos()
        },
        deep: true
      }
    },
    methods: {
      filterTodos () {
        let filter = this.$route.params.filter
        if (!filter) return
        const filterFun = filters[filter]
        if (!filterFun) return
        this.filteredTodos = filterFun(this.todos)
      }
    }
  }
</script>

现在我们需要改造一下Filter功能,我们希望默认跳转的不是All路由,而是用户上次点击的路由,这时候我们需要把当前路由信息保存下来,Todos.vue中对route进行观察

watch: {
  // 如果路由有变化,会再次执行该方法
  '$route': function (to) {
    Store.filter.save(to.params.filter)
    this.filterTodos()
  }
}

App.vue中判断路由状态进行跳转

created () {
  let filter = this.$route.params.filter
  if (!filter) {
    this.$router.replace({ path: Store.filter.get() })
  }
}

完善Todos剩余功能

显示剩余 Active Todos count

需求:在Footer左下角显示剩余未完成的todo个数,因为FooterApp的子组件,未完成的Todos count只能从父组件中计算获取 所以改造Footer.vue如下

<template>
  <footer class="footer">
    <span class="todo-count">
      <strong>{{ remaining }}</strong> {{ remaining | pluralize }} left
    </span>
    <ul class="filters">
      <!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
      <li><router-link to="/All" active-class="selected" exact replace>All</router-link></li>
      <li><router-link to="/Active" active-class="selected" exact replace>Active</router-link></li>
      <li><router-link to="/Completed" active-class="selected" exact replace>Completed</router-link></li>
    </ul>
    <button class="clear-completed">
        Clear completed
    </button>
  </footer>
</template>

<script>
  export default {
    name: 'footer',
    // 声明 props
    props: ['todos'],
    computed: {
      remaining: function () {
        return this.todos.filter(todo => !todo.completed).length
      }
    },
    filters: {
      pluralize: function (n) {
        return n === 1 ? 'item' : 'items'
      }
    }
  }
</script>

改造App.vue,传递todos到子组件

<MyFooter :todos="todos"/>

双击编辑 Todo

Todos.vue进行完善

  1. 首先我们需要一个data记录哪一个todo属于当前编辑状态,在data中增加editedTodo: null
  2. 在watchs增加以下四个方法,编辑Todo,移除Todo,完成编辑Todo,取消编辑Todo
editTodo: function (todo) {
  this.beforeEditCache = todo.title // 缓存编辑前title,在cancelEdit中会用到
  this.editedTodo = todo
},

removeTodo: function (todo) {
  this.todos.splice(this.todos.indexOf(todo), 1)
},

doneEdit: function (todo) {
  if (!this.editedTodo) {
    return
  }
  this.editedTodo = null
  todo.title = todo.title.trim()
  if (!todo.title) {
    this.removeTodo(todo)
  }
},

cancelEdit: function (todo) {
  this.editedTodo = null
  todo.title = this.beforeEditCache
}
  1. 修改templete部分
<template>
  <section class="main">
    <input class="toggle-all" type="checkbox" >
    <ul class="todo-list">
      <li v-for="todo in filteredTodos"
        class="todo"
        :key="todo.id"
        :class="{ completed: todo.completed, editing: todo == editedTodo }">
        <div class="view">
          <input class="toggle" type="checkbox" v-model="todo.completed">
          <label @dblclick="editTodo(todo)">{{ todo.title }}</label>
          <button class="destroy"></button>
        </div>
        <input class="edit" type="text"
          v-model="todo.title"
          @blur="doneEdit(todo)"
          @keyup.enter="doneEdit(todo)"
          @keyup.esc="cancelEdit(todo)">
      </li>
    </ul>
  </section>
</template>

测试一下,编辑功能已经正常工作了,但是程序还有一个小缺陷,当双击todo title的时候,进入编辑模式,但是输入框input没有获取焦点,需要用鼠标获取焦点,比较不方便,如何让输入框进入编辑的时候可以自己获取焦点呢,我们知道在editTodo中,是操作不到DOM的,有没有其他办法呢,这里我们使用自定义指令来完成,在Todos.vue中增加自定义指令如下:

directives: {
  'todo-focus': function (el, binding) {
    if (binding.value) {
      el.focus()
    }
  }
}

templeteinput中使用指令,条件是当todo是编辑状态

<input class="edit" type="text"
  v-model="todo.title"
  v-todo-focus="todo == editedTodo"
  @blur="doneEdit(todo)"
  @keyup.enter="doneEdit(todo)"
  @keyup.esc="cancelEdit(todo)">

删除 Todo

上一小节中我们已经完成对应的删除方法removeTodo,实现删除,我们直接绑定到对应的按钮即可,如下:

<button class="destroy" @click="removeTodo(todo)"></button>

快捷删除 Complete Todos

首先,我们分析一下,Footer.vue中的todos是父组件App.vue传过去的,所以子类不能直接删除,需要在父组件App.vue定义对应的删除方法 点击clear complete,首先需要定义一个删除的方法removeCompleted,这里需要注意的是,需要对原数组进行删除,对应的watch才会生效,如果直接替换this.todos,是不会触发todos的观察更新的

removeCompleted: function () {
  const completedTodos = this.todos.filter(todo => todo.completed)
  completedTodos.forEach(todo => this.todos.splice(this.todos.indexOf(todo), 1))
}

向子组件传递事件

<MyFooter @removeCompletedHandle="removeCompleted" :todos="todos"/>

修改子组件Footer.vue,以支持对应的事件接口

methods: {
  removeCompleted: function () {
    this.$emit('removeCompletedHandle')
  }
},

绑定方法到模板中,当有所有Todos大于未完成Todos时候显示v-show="todos.length > remaining"

<button class="clear-completed" @click="removeCompleted" v-show="todos.length > remaining">
  Clear completed
</button>

lesson04.vue-vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。

这一节教程中,我们基于03.vue-todos将把vue的状态管理插件vuex整合进来,复制项目03.vue-todos改名为04.vue-vuex-todos

加入vuex插件库和vuex-router-sync(把route路由的状态同步到vuex的state中,方便跟踪路由的状态)

cnpm i vuex --save
cnpm i vuex-router-sync --save

这一节不对vuex的api做深入解析,若遇到不理解的地方还请翻阅官方文档

为了不影响lesson03store实现,我们在src中新建了一个vuex-store文件夹,作为vuex的store存储库 src/vuex-store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import uuid from 'uuid'

Vue.use(Vuex)

export default new Vuex.Store({
  strict: process.env.NODE_ENV !== 'production',
  state: {
    todos: [] // 存储所有todos
  },
  mutations: {
    ADD_TODO (state, todoContent) {
      state.todos.push({
        id: uuid(),
        title: todoContent,
        completed: false
      })
    },
    REMOVE_TODO (state, todo) {
      var todos = state.todos
      todos.splice(todos.indexOf(todo), 1)
    },
    COMPLETE_TODO (state, todo) {
      todo.completed = !todo.completed
    }
  },
  actions: {
    addTodo ({ commit }, todoContent) {
      commit('ADD_TODO', todoContent)
    },
    removeTodo ({ commit }, todo) {
      commit('REMOVE_TODO', todo)
    },
    completeTodo ({ commit }, todo) {
      commit('COMPLETE_TODO', todo)
    }

  },
  getters: {
    todos: state => state.todos.filter((todo) => { return !todo.completed }),
    completedTodos: state => state.todos.filter((todo) => { return todo.completed })
  }

})

strict: process.env.NODE_ENV !== 'production' 在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。

Vuex 通过 store 选项,提供了一种机制将状态从根组件『注入』到每一个子组件中(需调用 Vue.use(Vuex))

src/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'
import store from './vuex-store/index'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  store,
  template: '<App/>',
  components: { App }
})
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].