最近在研究vue的相关知识,最好的学习方法莫过于本身开发一个SPA,这样带着问题来学习,进步天然飞速。因而边查边写差很少花了2周写完了一个todo-list,功能不够完备,可是麻雀虽小,却也是五脏俱全,基本功能是能够知足的了。话很少说,直接来看项目吧。
以上。css
接下来就是代码分析了。html
这一个步骤没什么好说的,网上教程一大堆,随便找一个照着走就行了。
完成后,你应该有一个项目的文件夹,里面应该有这几个文件:
README.md、build、config、index.html、package.json、src、static
嗯,就这样。vue
先安装依赖,命令行到对应根目录文件夹执行以下命令(推荐VS code,自带命令行输入,方便!)node
npm install
稍等片刻完成(若是太慢,推动啊淘宝镜像的cnpm安装)
安装好以后,继续安装:jquery
npm install vuex vue-router bootstrap --save
安装完成后,须要配置如下文件,确保可以使用。
打开:xx(项目文件夹)-src-main.js
以下:webpack
import Vue from 'vue' import VueRouter from 'vue-router' import App from './App' import 'bootstrap/dist/css/bootstrap.css' Vue.use(VueRouter) const routes=[ { path:'/', component:Home }, { path:'/todolist', component:todolist } ]; const router=new VueRouter({routes}); /* eslint-disable no-new */ const app=new Vue({ router, el:'#app', render:h=>h(App) //ES6语法 })
这里配置了vue-router和bootstrap,项目中可使用了,接着咱们还须要配置vuex和jQuery。git
首先在根目录建立一个文件夹,命名为vuex,在里面建立一个store.js文件,
配置以下:github
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex); const state={ } const getters={ } const mutations={ } export default new Vuex.Store({ state, getters, mutations })
const是ES6的语法,这里getters,state,mutations都不急着用,先配置好。
配置好store.js,回到main.js继续配置。
增长一些内容:
import Vue from 'vue' import VueRouter from 'vue-router' import store from './vuex/store' import App from './App' import Home from './components/Home.vue' import todolist from './components/todolist.vue' import 'bootstrap/dist/css/bootstrap.css' Vue.use(VueRouter) const routes=[ { path:'/', component:Home }, { path:'/todolist', component:todolist } ]; const router=new VueRouter({routes}); /* eslint-disable no-new */ const app=new Vue({ router, store, el:'#app', render:h=>h(App) //ES6语法 })
好了,vuex就配置完了。接着咱们配置JQuery,由于bootstrap依赖JQuery,因此这里也必须放上去。
老规矩,先用npm安装JQuery。web
npm install jquery --save
打开xx-build-webpack-base.conf.js,在module.exports里面添加以下代码:vue-router
plugins:[ new webpack.optimize.CommonsChunkPlugin('common.js'), new webpack.ProvidePlugin({ jQuery: "jquery", $: "jquery" }) ]
打开main.js配置JQuery和bootstrap的动效。
添加一点内容:
import $ from 'jquery' import 'bootstrap/dist/js/bootstrap.min.js'
OK,至此,全部的前期配置就完成了,能够开始正式的代码书写了。
Vue最碉堡的地方就是它的组件式开发,因此这个思想是咱们在写代码式要时刻注意的,如何合理的划分本身的组件,是一件很须要思考的事,接下来我将详细介绍个人组件内容和实现的功能。
下面是个人组件结构:
在src文件夹里,有一个主组件:app.vue,有一个组件文件夹:conponents,在这里面我放了4个组件,以下:
Home.vue ———— 首页
todolist.vue ———— todolist 应用主页面
sidebar.vue ———— todolist任务列表
editor.vue ———— todolist任务编辑
我会一个个介绍功能。
在首页里,咱们会用bootstrap写一个导航,经过vue-router的路由导航到不一样的应用。
代码以下:
<template> <div id="app"> <!--nav start--> <nav class="navbar navbar-default"> <div class="container-fluid"> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#"><i class="glyphicon glyphicon-home"></i></a> </div> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav"> <li><router-link to="/todolist">Todo List</router-link></li> <li><a href="#">开发中...</a></li> </ul> </div><!-- /.navbar-collapse --> </div><!-- /.container-fluid --> </nav> <!--nav end--> <!--content--> <router-view></router-view> </div> </template> <script> export default { name: 'app', data(){ return{ } } } </script> <style> </style>
首页的上部分是一个导航,导航的UI和样式用的是bootstrap,导航用路由实现连接到不一样的应用,要注意的是,不一样的应用咱们用不一样的组件封装,好比这个待办事项的应用,咱们用的是todolist.vue。还要注意的是,这些组件的注册和路由连接都须要在main.js中配置。不要忘记了。
返回查看main.js看看代码是怎么写的。
配置完后,咱们的主页面上只有一个导航。接着咱们配置主页。
咱们在xx-src-components文件夹里建立一个新的组件:Home.vue。这个组件是咱们的首页内容,这里我放了一张图,和一句话:欢迎!这里有你须要的App。这里一样用到了bootstrap的栅格系统,这样就能够兼容移动端了。
看代码:
<template> <div class="Home"> <div class="container"> <div class="col-sm-8"> <div class="jumbotron"> <img src="../assets/home-l-img.jpg"> </div> </div> <div class="col-sm-4"> <div class="jumbotron"> <h2>欢迎你!</h2> <p>这里有你须要的app</p> </div> </div> </div> </div> </template> <script> export default { name: 'Home', data () { return { } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .col-sm-8 .jumbotron{ padding: 0; } .jumbotron img{ width: 100%; } </style>
代码不复杂,就不解释了。接下来就是重头戏了。
这是这个应用的主组件,在这个组件里还会包含两个子组件:sidebar.vue和editor.vue,因此在这个组件里,咱们要实现的是一个新建任务的功能。新建的任务会显示在左边的任务列表sidebar.vue组件里,而后点击某个任务的编辑按钮,咱们能够在右边的任务编辑editor.vue组件里修改。这就是这个应用的架构思路。下面来看看代码:
<template> <div class="todolist"> <div class="container addInput"> <input type="text" v-model="taskText" class="form-control col-sm-12" placeholder="新建一个任务" v-on:keyup.enter="addTask"> </div> <div class="container"> <div class="col-sm-6"> <sidebar></sidebar> </div> <div class="col-sm-6"> <editor></editor> </div> </div> </div> </template> <script> import sidebar from './sidebar.vue' import editor from './editor.vue' export default { name: 'todolist', data(){ return{ taskText:'' } }, components: { sidebar, editor }, methods:{ addTask(){ if(this.taskText==''){ alert('请输入具体任务内容!') }else{ this.$store.commit('addTask',this.taskText); this.taskText='' } } } } </script> <style scoped> .addInput { margin-bottom: .75rem; } </style>
代码量不算大,除了一个输入框以外就是两个组件的标签<sidebar></sidebar>、<editor></editor>
了。这里会有一个commit(),它里面引号的内容是一个函数,这个函数在store.js里面的mutations里编写。
要注意的点:
1.两个子组件要在父组件里面注册引入才能使用。
2.这里开始涉及到了Vuex的功能,简单说明一下,咱们在输入任务后,这个任务的相关数据会被保存到状态管理store里面,而后经过mutations的操做,把输入的内容保存在子组件sidebar里。
显然设置完这里仍是没法使用新建任务的,咱们还须要两步操做。
第一步:设置sidebar.vue:
这个子组件是完成任务列表的渲染。看代码:
<template> <div class="sidebar"> <div class="panel panel-default"> <div class="panel-heading"> <h4 class="text-left"> 任务列表 <i class="glyphicon glyphicon-refresh pull-right"></i> </h4> </div> <div class="panel-body sidebar-context"> <div> <div v-for="(item,index) in items" class="panel panel-default"> <div class="panel-heading"> <h4> <input type="checkbox"> {{item.task}} <i class="glyphicon glyphicon-remove pull-right"></i> <i class="glyphicon glyphicon-edit pull-right"></i> </h4> </div> <div class="panel-body"> <p> {{item.setTime}}<br> {{item.details}} </p> </div> </div> </div> </div> </div> </div> </template> <script> export default { name: 'sidebar', data() { return { } }, computed:{ items(){ return this.$store.getters.items; } }, methods:{ } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> ul { list-style: none; } .sidebar-context{ max-height: 8rem; overflow: scroll; } i.glyphicon{ font-size: .275rem; cursor: pointer; margin-left: .25rem; } </style>
能够仔细考虑一下代码的书写,在<script>
里设置computed,不然保存在store.js的值没法输出到子组件sidebar.vue里。
第二步,设置store.js:
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex); const state={ items:[] } const getters={ items:state=>state.items } const mutations={ addTask(state,task){ var myDate=new Date(); var y=myDate.getFullYear(); var m, mm=myDate.getMonth()+1; if(mm<10){ m="0"+mm; }else{ m=mm; } var d, dd=myDate.getDate(); if(dd<10){ d="0"+dd; }else{ d=dd; } var currentTime=y+m+d; state.items.push({ task, isFinished:false, details:"this is a new task", setTime:currentTime }) } } export default new Vuex.Store({ state, getters, mutations })
这一段代码,咱们会把输入的值做为一项新建任务的task值保存,并渲染到sidebar上,固然,做为一项待办任务,只有名称是不够的,因此咱们为它添加了默认的描述details和默认的完成时间setTime,setTime是当前日期。
看到commit相关的函数了吗?
OK,至此,这个todolist应用最基本的新建任务就完成了。
接着,咱们完成编辑、删除、标记已完成任务的功能。编辑功能在editor.vue组件內操做,删除功能则在sidebar.vue里面完成便可。让咱们先编辑sidebar.vue。
下面这一段代码我会一次性增长删除编辑和编辑已完成的功能,你能够先敲出来,思考一下原理,也能够跳过这一段,跟着后面的步骤一步步添加功能。
<template> <div class="sidebar"> <div class="panel panel-default"> <div class="panel-heading"> <h4 class="text-left"> 任务列表 <i class="glyphicon glyphicon-refresh pull-right" v-on:click="reList"></i> </h4> </div> <div class="panel-body sidebar-context"> <div> <div v-for="(item,index) in items" class="panel panel-default"> <div class="panel-heading"> <h4> <input @click="itemCheck(index)" type="checkbox"> {{item.task}} <i class="glyphicon glyphicon-remove pull-right" v-on:click="deleteTask(index)"></I> <i class="glyphicon glyphicon-edit pull-right" v-on:click="clickTask(item)"></i> </h4> </div> <div class="panel-body"> <p> {{item.setTime}}<br> {{item.details}} </p> </div> </div> </div> </div> </div> </div> </template> <script> export default { name: 'sidebar', data() { return { } }, computed:{ items(){ return this.$store.getters.items; }, activeTask(){ return this.$store.getters.activeTask; } }, methods:{ deleteTask(index){ this.$store.commit('deleteTask',index); }, itemCheck(index){ this.$store.commit('toggleCheck',index); }, clickTask(item){ this.$store.commit('setActivetask',item); }, reList(){ this.$store.commit('reList') } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> ul { list-style: none; } .sidebar-context{ max-height: 8rem; overflow: scroll; } i.glyphicon{ font-size: .275rem; cursor: pointer; margin-left: .25rem; } </style>
在这里,我一次性把编辑、删除、标记已完成任务的功能三个功能都加上了(偷懒...)我会一个个来解释。
1.删除
看名字能看的出来,我在一个删除图标(仍是bootstrap啦~)上绑定了一个点击事件deleteTask,里面的内容是执行store.js里一个deleteTask的函数,同时传入一个index的参数。而在相关的store.js里,咱们在mutations里添加deleteTask函数相关的代码:
deleteTask(state,index){ state.items.splice(index,1) }
删除对应的数组数据。也就是删除指定index的相关数据。任务就搞定了。
2.编辑
这是一个难点。涉及到数据在兄弟组件之间的交换,咱们仍然使用vuex。
先说说思路,咱们点击sidebar组件里的编辑按钮(删除旁边的按钮),而后把这个任务设置为活动任务,而后显示在右边的编辑组件editor上。在editor上编辑完成后,点击完成按钮,修改的任务被更新到左边的任务列表sidebar上。
好了,看上面sidebar.vue的代码,咱们先要完成设置活动任务(activeTask):
computed里面须要设置方法:
activeTask(){ return this.$store.getters.activeTask; }
而后在编辑按钮上绑定点击事件,把当前的任务设置为活动任务:
clickTask(item){ this.$store.commit('setActivetask',item); }
sidebar.vue里的内容就编辑完了。接着咱们须要在components文件夹建立一个组件editor.vue(忘记前面有没有建立了...),开始写这一段的代码:
这里须要提早声明的是,下面这一段的代码是我最没把握的代码,由于我不肯定这一段是否是有效率的,可是它确定是能跑起来的。
<template> <div class="sidebar"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="text-left">任务编辑</h3> </div> <div class="panel-body"> <p>任务名称</p> <input type="text" class="form-control task-input" placeholder="任务名称" v-bind:value="task" v-on:input="saveTask"> <p>任务详情</p> <textarea class="form-control details-input" rows="3" placeholder="任务详情" v-bind:value="details" v-on:input="saveDetails"></textarea> <p>任务期限</p> <input type="text" class="form-control settime-input" placeholder="格式:20170606" v-bind:value="setTime" v-on:input="saveSettime"> <h3><i class="glyphicon glyphicon-ok pull-right" v-on:click="save"></i></h3> </div> </div> </div> </template> <script> export default { name: 'editor', data() { return { } }, computed:{ items(){ return this.$store.getters.items; }, task(){ this.taskInput=this.$store.getters.activeTask.task; return this.$store.getters.activeTask.task; }, details(){ this.detailsInput=this.$store.getters.activeTask.details; return this.$store.getters.activeTask.details; }, setTime(){ this.settimeInput=this.$store.getters.activeTask.setTime; return this.$store.getters.activeTask.setTime; } }, methods:{ saveTask(e){ this.taskInput=e.target.value; }, saveDetails(e){ this.detailsInput=e.target.value; }, saveSettime(e){ this.settimeInput=e.target.value; }, save(){ this.$store.commit('editTask',this.taskInput); this.$store.commit('editDetails',this.detailsInput); this.$store.commit('editSettime',this.settimeInput); this.$store.commit('clearAll'); } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> input,textarea{ margin-bottom: .3rem; } i{ cursor: pointer; margin-right: .2rem; } </style>
说明以下:
1.computed里的方法分别对应的是items任务数组(包含全部创建的任务数据)、task任务名称数据、details任务描述数据、setTime任务时间数据。这几个方法,除了第一个,其余的功能都是在对应的input表单显示活动任务(activeTask)的值。
2.methods里的方法,前三个都是在对应的input输入新值时触发事件,它会把新输入的值分别保存在一个地方。(好比task值就会保存在this.taskInput,taskInput是类名为task-input的input,其余两个以此类推)第四个save(),会把前面保存的三个值赋给活动任务。这也是我不敢肯定的地方,由于代码这样写,有多少个不一样的值就要用多少个函数,很不环保。
这些commit里的函数都放在store.js的mutations里,下面再说。
以上就把editor.vue的代码编辑完了,接着编辑store.js。
咱们添加一下内容:
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex); const state={ items:[], activeTask:{} } const getters={ items:state=>state.items, activeTask:state=>state.activeTask } const mutations={ addTask(state,task){ var myDate=new Date(); var y=myDate.getFullYear(); var m, mm=myDate.getMonth()+1; if(mm<10){ m="0"+mm; }else{ m=mm; } var d, dd=myDate.getDate(); if(dd<10){ d="0"+dd; }else{ d=dd; } var currentTime=y+m+d; state.items.push({ task, isFinished:false, details:"this is a new task", setTime:currentTime }) }, deleteTask(state,index){ state.items.splice(index,1) }, setActivetask(state,item){ state.activeTask=item }, editTask(state,task){ state.activeTask.task=task; for(let i in state.items){ if(i==state.activeTask){ i.task=task; } } }, editDetails(state,details){ state.activeTask.details=details; for(let i in state.items){ if(i==state.activeTask){ i.details=details; } } }, editSettime(state,settime){ state.activeTask.setTime=settime; for(let i in state.items){ if(i==state.activeTask){ i.setTime=settime; } } } } export default new Vuex.Store({ state, getters, mutations })
ok,主要是在state和getters里添加了活动任务,mutations里添加了前面提到的相关函数,对比一下看看有没有遗漏。
到这里,这个todolist最核心的任务就完成了。可是咱们还须要一些细节:
1.修改任务后,把右边的活动任务清空,回复初始状态。
2.修改任务的完成时间后,须要知道哪些任务更加紧急,须要让任务列表从新排序。
3.修改任务完成状态。
咱们来依次完成:
1.这个比较简单,咱们只要修改完后点击完成按钮时触发一个设置活动任务(activeTask)为空的方法就行了。
在editor.vue组件的save()方法添加:
this.$store.commit('clearAll');
在store.js的mutations添加:
clearAll(state){ state.activeTask={}; },
done!
2.咱们在左边的任务列表上放一个刷新按钮,点击时,会根据任务数据的setTime属性从新排序,时间近的在前面,时间远的在后面。
sidebar.vue的methods添加:
reList(){ this.$store.commit('reList') }
在store.js的mutations添加:
reList(state){ function compare(propertyName){ return function(obj1,obj2){ var value1=obj1[propertyName]; var value2=obj2[propertyName]; if(value2<value1){ return 1; }else if(value2>value1){ return -1; }else{ return 0; } } } state.items.sort(compare('setTime')); }
这里用到了一个基础的比较大小从新排序的功能。done!
3.这个也不复杂,咱们在任务列表组件sidebar.vue组件的多选框上添加点击事件,methods里添加:
itemCheck(index){ this.$store.commit('toggleCheck',index); },
在store.js的mutations添加:
toggleCheck(state,index){ state.items[index].isFinished=!state.items[index].isFinished },
由于在建立任务时,咱们会添加一个isFinished的属性,默认为false,经过以上方法,咱们就能够切换isFinished的值,也就是切换任务的是否完成状态。搞定。
到这里,这个todo-list基本就算完成了。
接下来还有一些须要调整的,首先是移动端的适配,咱们要先打开根目录的index.html,添加一下内容:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name = "viewport" content = "width = device-width, initial-scale = 1.0, maximum-scale = 1.0, user-scalable = 0" /> <title>todolist</title> </head> <body> <div id="app"></div> <!-- 下面是我添加的 --> <script> let html = document.documentElement; window.rem = html.getBoundingClientRect().width / 25 ; html.style.fontSize = window.rem + 'px'; </script> </body> </html>
添加meta和一段JS完成bootstrap的适应和rem方法自适应大小。
第二是localstorage实现存储。毕竟是一个todolist应用,每次打开都要从新建任务怎么行?
这里我用了一个vue的localstorage插件:地址
npm安装插件:
npm install vue-localstorage --save
store.js配置:
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex); import VueLocalStorage from 'vue-localstorage' Vue.use(VueLocalStorage) const STORAGE_KEY='myapp-todolist' const state={ items:JSON.parse(window.localStorage.getItem(STORAGE_KEY) || '[]'), activeTask:{} } const getters={ items:state=>state.items, activeTask:state=>state.activeTask } const mutations={ addTask(state,task){ var myDate=new Date(); var y=myDate.getFullYear(); var m, mm=myDate.getMonth()+1; if(mm<10){ m="0"+mm; }else{ m=mm; } var d, dd=myDate.getDate(); if(dd<10){ d="0"+dd; }else{ d=dd; } var currentTime=y+m+d; state.items.push({ task, isFinished:false, details:"this is a new task", setTime:currentTime }) }, deleteTask(state,index){ state.items.splice(index,1) }, toggleCheck(state,index){ state.items[index].isFinished=!state.items[index].isFinished }, setActivetask(state,item){ state.activeTask=item }, editTask(state,task){ state.activeTask.task=task; for(let i in state.items){ if(i==state.activeTask){ i.task=task; } } }, editDetails(state,details){ state.activeTask.details=details; for(let i in state.items){ if(i==state.activeTask){ i.details=details; } } }, editSettime(state,settime){ state.activeTask.setTime=settime; for(let i in state.items){ if(i==state.activeTask){ i.setTime=settime; } } }, clearAll(state){ state.activeTask={}; }, reList(state){ function compare(propertyName){ return function(obj1,obj2){ var value1=obj1[propertyName]; var value2=obj2[propertyName]; if(value2<value1){ return 1; }else if(value2>value1){ return -1; }else{ return 0; } } } state.items.sort(compare('setTime')); } } const localStoragePlugin = store=>{ store.subscribe((mutation,{items})=>{ window.localStorage.setItem(STORAGE_KEY,JSON.stringify(items)) }) } export default new Vuex.Store({ state, getters, mutations, plugins:[localStoragePlugin] })
要改的解放并很少,主要是把任务数据的数组转换成localstorage的格式。
完成了!撒花~~
这个todo-list实在是简陋不堪,可是基础很差的我也差很少花了2个星期才写完,在写这个应用的时候我是抱着诚恳的态度的,因此也确实花了不少的心血,包括学习命令行,npm、搭建项目脚手架、查找资料,中间的磨练和挫折也没必要多说,一切的辛苦都在看着这个应用可以运行的那一刻烟消云散了。
写了一篇这么长的,我尽可能用我认为最明白清楚的语言来表达,中间确定有不少错误和疏漏,欢迎大神拍砖、指点。写这篇blog的目的也就是从新梳理一下代码的思路,我是抱着学习的态度来写的。中间的代码有不少重复的地方,在大神看来,无疑是有着占篇幅的嫌疑,但也是但愿能更加清楚地表述,方便新人理解。我也是刚入此门,十分能理解代码不详细带来的困惑。
在起名字的时候,我没有直接说这是一个todo-list,而说是一个应用盒子,是由于我想把它做为一个个人表明项目,里面会集成许多的应用,todolist、音乐、计算器等等,一方面练手,一方面攒一个拿得出手的项目,方便之后求职。
说了这么多废话,下面是干货:
todo-list源码(个人github,欢迎star,fork)
Vue2.0 新手彻底填坑攻略(若是你还没学过Vue,先看这个入门)
vue2.0 构建单页应用最佳实战(bootstrap+vue架构一个todolist)
Vue.js+Vuex:一个简单的记事本(介绍vuex项目实战最清楚的教程)
固然,官方文档也是很是重要的,看着教程你能够写出须要的代码,可是只有看着文档,你才能知道为何要这样写。
vue官方文档
vuex官方文档
that's all~