您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 订阅
  捐助
Vuex2.0小米便签项目实例
 
  2452  次浏览      17
  2018-11-26
 
编辑推荐:
本文来自segmentfault,文章主要从构建一个小米便签应用来学习怎么使用Vuex等相关内容。

在这个教程中,我们会通过构建一个小米便签应用来学习怎么使用Vuex,开始我会简单的介绍Vuex的一些基础内容,什么时候使用以及用Vuex怎么组织代码,然后一步一步的把这些概念应用到小米便签应用里面。

废话不多说,先给大家看一下小米便签应用的截图:

你可以从GitHub上下载源码,这里是项目源代码的地址和在线预览地址,安装成功后推荐使用chrome的设备模式查看效果更佳。

Vuex概述

Vuex 是一个专门为 Vue.js 应用所设计的集中式状态管理架构,它借鉴了 Flux 和 Redux 的设计思想,但简化了概念,并且采用了一种为能更好发挥 Vue.js 数据响应机制而专门设计的实现。

如果你不太理解 Vue.js 应用里的状态是什么意思的话,你可以想象一下你此前写的 Vue 组件里面的 data 字段。Vuex 把状态分成组件内部状态和应用级别状态:

组件内部状态:仅在一个组件内使用的状态(data 字段)

应用级别状态:多个组件共用的状态

举个例子:比如说有一个父组件,它有两个子组件。这个父组件可以用 props 向子组件传递数据,这条数据通道很好理解。

那如果这两个子组件相互之间需要共享数据呢?或者子组件需要向父组件传递数据呢?这两个问题在应用体量较小的时候都好解决,只要用自定义事件即可。

但是随着应用规模的扩大:

追踪这些事件越来越难了。这个事件是哪个组件触发的?谁在监听它?

业务逻辑遍布各个组件,导致各种意想不到的问题。

由于要显式地分发和监听事件,父组件和子组件强耦合。

Vuex 要解决的就是这些问题,Vuex 背后有四个核心的概念:

State: 包含所有应用级别状态的对象

Getters: 在组件内部获取 store 中状态的函数

Mutations: 修改状态的事件回调函数

Actions: 组件内部用来分发 mutations 事件的函数

下面这张图完美地解释了一个 Vuex 应用内部的数据流动:

这张图的重点:

数据流动是单向的

组件可以调用 actions

Actions 是用来分发 mutations 的

只有 mutations 可以修改状态

store 是反应式的,即,状态的变化会在组件内部得到反映

搭建项目

项目结构:

项目主要文件存放于src目录下:

assets/公共图片,css文件

components/包含所有组件

libs/扩展文件

router/路由文件

store/vuex相关文件(state,actions,getters,mutation)

App.vue根组件

main.js应用总入口

新建项目:

使用vue-cli脚手架,可用于快速搭建大型单页应用。该工具为现代化的前端开发工作流提供了开箱即用的构建配置。只需几分钟即可创建并启动一个带热重载、保存时静态检查以及可用于生产环境的构建配置的项目:

# 安装vue
npm install vue
# 全局安装 vue-cli
npm install --global vue-cli
# 创建一个基于 webpack 模板的新项目
vue init webpack notepad-xiaomi
# 安装依赖,走你
cd notepad-xiaomi
# 安装依赖
npm install muse-ui vue-awesome --save
# 安装vuex
npm install vue vuex --save
# 运行
npm run dev

使用vue-cli脚手架创建项目时,一定要安装vue-router插件。

安装依赖后再main.js中引用

创建Vuex Store

在store文件夹下创建第一个index.js:

import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import mutations from './mutation'
import * as getters from './getters'
import * as actions from './actions'

Vue.use(Vuex)

export default new Vuex.Store({
state,
mutations,
getters,
actions
})

现在我用下面这张图把应用分解成多个组件,并把组件内部需要的数据对应到 store.js 里的 state。

App根组件,第一幅图中的红色盒子

Header头部组件,第一幅图中的绿色盒子

NoteList列表组件,第一幅图中的橙色盒子

ToolBar工具栏组件,第一幅图中的蓝色盒子(包括删除和移动按钮)

Editor编辑组件,第二幅图,

NoteFolder便签夹组件,第三幅图

TrashHeader废纸篓头部组件,第四幅图蓝色盒子

TrashNoteList废纸篓列表组件,第四幅图灰色盒子

TrashToolBar废纸篓工具栏组件,第四幅图黄色盒子

state.js里面的状态对象会包含所有应用级别的状态,也就是各个组件需要共享的状态。

笔记列表(notes: [])包含了 NodesList 组件要渲染的 notes 对象。当前便签(activeNote: {})则包含当前编辑的便签对象,多个组件都需要这个对象。

聊完了状态state,我们来看看 mutations, 我们要实现的 mutation 方法包括:

添加标签到notes数组中

编辑选中便签

删除便签

便签布局

勾选便签

全部/取消勾选便签

保存便签

勾选废纸篓便签

全部/取消勾选废纸篓便签

恢复废纸篓便签

mutation-types中用于将常量放在单独的文件中,方便协作开发。

export const NEW_NOTE = 'NEW_NOTE'
export const EDIT_NOTE = 'EDIT_NOTE'
export const TOGGLE_NOTE = 'TOGGLE_NOTE'
export const CANCEL_CHECK = 'CANCEL_CHECK'
export const ALL_CHECK = 'ALL_CHECK'
export const DELETE_NOTE = 'DELETE_NOTE'
export const BACK_SAVE = 'BACK_SAVE'
export const TOGGLE_TRASHNOTE = 'TOGGLE_TRASHNOTE'
export const CANCEL_TRASHCHECk = 'CANCEL_TRASHCHECk'
export const ALL_TRASHCHECK = 'ALL_TRASHCHECK'
export const DELETE_TRASHNOTE = 'DELETE_TRASHNOTE'
export const RECOVERY_NOTE = 'RECOVERY_NOTE'

首先,创建一条新的便签,我们需要做的是:

新建一个对象

初始化属性

push到state.notes数组中

[types.NEW_NOTE](state) {
let newNote = {
id: +new Date(),
date: new Date().Format('yyyy-MM-dd hh:mm'),
content: '',
done: false
}
state.notes.push(newNote)
}

然后,编辑便签需要用笔记内容 content 作参数:

[types.EDIT_NOTE](state, note) {
state.activeNote = note;
}

剩下的这些 mutations 很简单就不一一赘述了。整个 store/mutation.js 如下:

import Format from '../libs/dateFormat'
import * as types from './mutation-types';

const mutations = {
[types.NEW_NOTE](state) {
let newNote = {
id: +new Date(),
date: new Date().Format('yyyy-MM-dd hh:mm'),
content: '',
done: false
}
state.notes.push(newNote)
},
[types.EDIT_NOTE](state, note) {
state.activeNote = note;
},
[types.TOGGLE_NOTE](state, note) {
state.notes.map((item, i) => {
if (item.id == note.id) {
item.done = !note.done;
}
})
if (note.done) {
state.deleteNotes.push(note);
} else {
state.deleteNotes.splice(state.deleteNotes.
indexOf(note), 1);
}
},
[types.CANCEL_CHECK](state) {
state.notes.map((item, i) => {
item.done = false;
})
state.deleteNotes = [];
state.isCheck = false;
},
[types.ALL_CHECK](state, done) {
state.deleteNotes = [];
state.notes.map((item, i) => {
item.done = done;
if (done) {
state.deleteNotes.push(item);
} else {
state.deleteNotes = [];
}
})
},
[types.DELETE_NOTE](state) {
state.deleteNotes.map((item, i) => {
item.done = false;
state.notes.splice(state.notes.indexOf(item), 1);
state.trashNotes.push(item)
})
state.isCheck = false;
state.deleteNotes = [];
},
[types.BACK_SAVE](state, note) {
if (note.content != '') return;
state.notes.splice(state.notes.indexOf(note), 1);
},
[types.TOGGLE_TRASHNOTE](state, note) {
state.trashNotes.map((item, i) => {
if (item.id == note.id) {
item.done = !note.done;
}
})
if (note.done) {
state.deleteTrashNotes.push(note);
} else {
state.deleteTrashNotes.splice(state.
deleteTrashNotes.indexOf(note), 1);
}
},
[types.CANCEL_TRASHCHECk](state) {
state.trashNotes.map((item, i) => {
item.done = false;
})
state.deleteTrashNotes = [];
state.isTrashCheck = false;
},
[types.ALL_TRASHCHECK](state, done) {
state.deleteTrashNotes = [];
state.trashNotes.map((item, i) => {
item.done = done;
if (done) {
state.deleteTrashNotes.push(item);
} else {
state.deleteTrashNotes = [];
}
})
},
[types.DELETE_TRASHNOTE](state) {
state.deleteTrashNotes.map((item, i) => {
state.trashNotes.splice(state.trashNotes.indexOf(item), 1);
})
state.deleteTrashNotes = [];
state.isTrashCheck = false;
},
[types.RECOVERY_NOTE](state) {
state.deleteTrashNotes.map((item, i) => {
item.done = false;
state.notes.unshift(item)
state.trashNotes.splice(state.trashNotes.indexOf(item), 1);
})
state.deleteTrashNotes = [];
state.isTrashCheck = false;
}
}

export default mutations;

接下来聊 actions, actions 是组件内用来分发 mutations 的函数。它们接收 store 作为第一个参数。比方说,当用户点击 Toolbar 组件的添加按钮时,我们想要调用一个能分发NEW_NOTE mutation 的 action。现在我们在 store/文件夹下创建一个 actions.js 并在里面写上 newNote函数:

// 创建新便签
export const newNote = ({ commit }) => {
commit(types.NEW_NOTE)
}

其他的这些actions都类似,整个store/actions.js如下:

import * as types from './mutation-types';

//创建新便签
export const newNote = ({ commit }) => {
commit(types.NEW_NOTE)
}

//编辑便签
export const editNote = ({ commit }, note) => {
commit(types.EDIT_NOTE, note)
}

//勾选便签
export const toggleNote = ({ commit }, note) => {
commit(types.TOGGLE_NOTE, note)
}

//取消勾选便签
export const cancelCheck = ({ commit }) => {
commit(types.CANCEL_CHECK)
}

//全部勾选
export const allCheck = ({ commit }, done) => {
commit(types.ALL_CHECK, done)
}

//删除便签
export const deleteNote = ({ commit }) => {
commit(types.DELETE_NOTE)
}

//返回自动保存
export const backSave = ({ commit }, note) => {
commit(types.BACK_SAVE, note)
}

//勾选废纸篓便签
export const toggleTrashNote = ({ commit }, note) => {
commit(types.TOGGLE_TRASHNOTE, note)
}

//取消勾选废纸篓便签
export const cancelTrashCheck = ({ commit }) => {
commit(types.CANCEL_TRASHCHECk)
}

//全选废纸篓便签
export const allTrashCheck = ({ commit }, done) => {
commit(types.ALL_TRASHCHECK, done)
}

//删除废纸篓便签
export const deleteTrashNote = ({ commit }) => {
commit(types.DELETE_TRASHNOTE)
}

//恢复便签
export const recoveryNote = ({ commit }) => {
commit(types.RECOVERY_NOTE)
}

最后说一下getters,在Store仓库里,state就是用来存放数据,若是对数据进行处理输出,比如数据要过滤,一般我们可以写到computed中。但是如果很多组件都使用这个过滤后的数据,比如饼状图组件和曲线图组件,我们是否可以把这个数据抽提出来共享?这就是getters存在的意义。我们可以认为,getters是store的计算属性

// 搜索过滤便签
export const filterNote = (state) => {
if (state.search != '' && state.notes.length > 0) {
return state.notes.filter(note => note.content.indexOf(state.search) > -1) || {}
} else {
return state.notes || {}
}
}
// 当前编辑的便签
export const activeNote = (state) => {
return state.activeNote
}
// 便签列表布局
export const layout = state => state.layout
// 便签选中状态
export const isCheck = state => state.isCheck
// 废纸篓便签选中状态
export const isTrashCheck = state => state.isTrashCheck

这样,在 store文件夹里面要写的代码就都写完了。这里面包括了 state.js 中的 state 和 mutation.js中的mutations,以及 actions.js 里面用来分发 mutations 的 actions,和getters.js中的处理输出。

构建Vue组件

最后这个小结,我们来实现四个组件 (App, Header,Toolbar, NoteList 和 Editor) 并学习怎么在这些组件里面获取 Vuex store 里的数据以及调用 actions。

创建根实例 - main.js

main.js是应用的入口文件,里面有根实例,我们要把 Vuex store 加到到这个根实例里面,进而注入到它所有的子组件里面:

// 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 './store/index'

/* 第三方插件 */
import MuseUI from 'muse-ui'
import 'muse-ui/dist/muse-ui.css'
import 'muse-ui/dist/theme-teal.css'
import Icon from 'vue-awesome/components/Icon'
import 'vue-awesome/icons/flag'
import 'vue-awesome/icons'


Vue.use(MuseUI)
Vue.component('icon', Icon);
Vue.config.productionTip = false


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

App - 根组件

根组件 App 作为总的路由入口:

<template>
<div id="app">
<router-view/>
</div>
</template>

<script>
export default {
name: 'App'
}
</script>

Notepad

Notepad 组件会 import 其余三个组件:Header,NoteList和ToolBar:

<template>
<div class="notepad">
<Header />
<NoteList />
<ToolBar />
</div>
</template>

<script>
import Header from './Header'
import NoteList from './NoteList'
import ToolBar from './ToolBar'
export default {
name: 'Notepad',
data () {
return {
}
},
components:{
Header,
NoteList,
ToolBar,
}
}
</script>

Header

Header组件提供搜索和便签勾选和取消,并统计勾选数量功能,如图:

对于Header组件来说,搜索框中输入查询内容时,需要对便签列表中的数据进行过滤,在创建state.js的时候就添加了search字段,用于存储搜索内容,而在getters.js中通过filterNote方法对便签列表进行过滤,筛选出符合条件的便签并返回,这时候我们在NoteList组件中就直接遍历filterNote方法就可以实现搜索功能。

store/getters中实现filterNote方法

// 搜索过滤便签
export const filterNote = (state) => {
if (state.search != '' && state.notes.length > 0) {
return state.notes.filter(note => note.content.indexOf(state.search) > -1) || {}
} else {
return state.notes || {}
}
}

NoteList组件中遍历filterNote

<li v-for="note in filterNote" :key="note.id" @mousedown="gtouchstart(note)" @mouseup="gtouchend(note)" @touchstart="loopstart(note)" @touchend="clearLoop">
<h4>{{note.date}}</h4>
<p>{{note.content}}</p>
<mu-checkbox label="" v-model="note.done" class="checkbox" v-show="isCheck"/>
</li>

Header组件:

...mapGetters中的...是es6的扩展运算符,不懂的可以查阅es6文档

<template>
<header class="header" :class="{visible:isVisible}">
<mu-flexbox class="headerTool" :class="{visible:isVisible}">
<mu-flexbox-item order="0" class="flex">
<mu-raised-button v-if="isCheck" label="取消" @click="cancelCheck" class="raised-button"/>
<span v-else class="icon" @click="openFolder"><icon name="folder-open"></icon></span>
</mu-flexbox-item>
<mu-flexbox-item order="1" class="flex" style="text-align:center">
<span v-if="isCheck">{{checkTitle}}</span>
<span v-else>{{title}}</span>
</mu-flexbox-item>
<mu-flexbox-item order="2" class="flex" style="text-align:right">
<mu-raised-button v-if="isCheck" :label="checkBtnTxt" @click="allCheck(!allChecked)" class="raised-button"/>
<span v-else>
<span class="icon" v-if="layout=='grid'" @click="changeLayout"><icon name="list"></icon></span>
<span class="icon" v-else @click="changeLayout"><icon name="th-large"></icon></span>
</span>
</mu-flexbox-item>
</mu-flexbox>
<div class="search">
<div class="icon"><icon name="search"></icon></div>
<input type="text" v-model="searchTxt" @keyup="search" @focus="searchFocus" @blur="searchBlur"/>
</div>
</header>
</template>

<script>
import { mapActions,mapGetters } from 'vuex'
export default {
name: 'Header',
data(){
return {
title:'便签',
checkBtnTxt:'全选',
searchTxt:'',
isVisible:false
}
},
computed:{
...mapGetters([
'layout',
'isCheck'
]),
//获取便签勾选状态
allChecked(){
return this.$store.state.notes.every(note => note.done)
},
//便签选中数量提示
checkTitle(){
return `已选择${this.$store.state.deleteNotes.length}项`
}
},
methods:{
//显示搜索框
searchFocus(){
this.isVisible = true;
},
//隐藏搜索框
searchBlur(){
this.isVisible = false;
},
//搜索
search(){
this.$store.state.search = this.searchTxt
},
//切换布局
changeLayout(){
if(this.$store.state.layout == 'list'){
this.$store.state.layout = 'grid'
}else{
this.$store.state.layout = 'list'
}

},
//取消勾选
cancelCheck(){
this.$store.dispatch('cancelCheck')
},
//全选切换
allCheck(done){
this.checkBtnTxt = done?'取消全选':'全选'
this.$store.dispatch('allCheck',done)
},
//打开便签夹
openFolder(){
this.$router.push({path:'noteFolder'})
}
}
}
</script>

NoteList

NotesList 组件主要有三个功能:

渲染便签列表

对便签进行勾选或取消

点击编辑便签

<template>
<ul class="noteList" :class="layout">
<li v-for="note in filterNote" :key="note.id" @mousedown="gtouchstart(note)" @mouseup="gtouchend(note)" @touchstart="loopstart(note)" @touchend="clearLoop">
<h4>{{note.date}}</h4>
<p>{{note.content}}</p>
<mu-checkbox label="" v-model="note.done" class="checkbox" v-show="isCheck"/>
</li>
</ul>
</template>

<script>
import { mapGetters,mapActions } from 'vuex'
export default {
name: 'NoteList',
data(){
return {
timeOutEvent: 0,
Loop:null
}
},
computed:{
...mapGetters([
'filterNote',
'layout',
'isCheck'
])
},
methods:{
//编辑&选中
editNote(note){
if(this.isCheck){
this.$store.dispatch('toggleNote',note);
}else{
this.$store.dispatch('editNote',note);
this.$router.push({path:'/editor'})
}

},
//鼠标按下,模拟长按事件
gtouchstart(note){
var _this = this;
this.timeOutEvent = setTimeout(function(){
_this.longPress(note)
},500);//这里设置定时器,定义长按500毫秒触发长按事件,时间可以自己改,个人感觉500毫秒非常合适
return false;
},
//鼠标放开,模拟长按事件
gtouchend(note){
clearTimeout(this.timeOutEvent);//清除定时器
if(this.timeOutEvent!=0){
//这里写要执行的内容(尤如onclick事件)
this.editNote(note);
}
return false;
},
longPress(note){
this.timeOutEvent = 0;
this.$store.state.isCheck = true;
this.$store.dispatch('toggleNote',note);
},
//手按住开始,模拟长按事件
loopstart(note){
var _this = this;
clearInterval(this.Loop);
       this.Loop = setTimeout(function(){
     _this.$store.state.isCheck = true;
_this.$store.dispatch('toggleNote',note);
      },500);
},
//手放开结束,模拟长按事件
clearLoop(){
clearTimeout(this.Loop);
}
}
}
</script>

ToolBar

Toolbar组件提供给用户三个按钮:创建便签,编辑便签和移动便签(移动便签功能还没有做):

<template>
<div class="toolBar">
<div class="toolBtn" v-if="isCheck">
<span class="icon" @click="deleteNote"><icon name="trash-alt"></icon></span>
<span class="icon"><icon name="dolly"></icon></span>
</div>
<div class="addNote" v-else>
<div class="float-button mu-float-button" @click="addNote"><icon name="plus"></icon></div>
</div>
<mu-dialog :open="dialog" title="删除便签" @close="close">
您确定删除所选便签吗?
<mu-flat-button slot="actions" @click="close" primary label="取消"/>
<mu-flat-button slot="actions" primary @click="deleteConfirm" label="确定"/>
</mu-dialog>
</div>
</template>

<script>
import { mapGetters,mapActions } from 'vuex'
export default {
name: 'ToolBar',
data(){
return {
dialog: false
}
},
computed:{
...mapGetters([
'isCheck'
])
},
methods:{
//添加便签
addNote(){
this.$store.dispatch('newNote');
this.$router.push({path:'editor'});
},
//删除便签
deleteNote(){
this.dialog = true;
},
//关闭窗口
close () {
this.dialog = false;
},
//确定删除
deleteConfirm(){
this.dialog = false;
this.$store.dispatch('deleteNote');
}
}
}
</script>

Editor

Editor 组件是最简单的,它只做两件事:

从 store 获取当前笔记activeNote,把它的内容展示在 textarea

在用户更新笔记的时候,调用 editNote() action

以下是完整的 Editor.vue:

<template>
<div class="edit-panel">
<div class="edit-tool">
<span class="back-list" @click="backList"><icon name="angle-left"></icon></span>
<span class="date" v-text="activeNote.date"></span>
<span class="saveNote" v-show="isShow" @click="backList">完成</span>
</div>
<textarea v-focus class="edit-area" v-model="activeNote.content" @keyup="editorNote"></textarea>
</div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
name: 'Editor',
data(){
return {
content:'',
isShow:false
}
},
created(){
this.content = this.activeNote.content
},
computed:{
//获取正在操作的便签
...mapGetters([
'activeNote'
])
},
directives:{
focus:{
inserted(el){
el.focus();
}
}
},
methods:{
//返回便签列表
backList(){
this.$router.push({path:'/'})
this.$store.dispatch('backSave',this.activeNote)
},
//完成按钮显示&隐藏
editorNote(){
if(this.content != this.activeNote.content){
this.isShow = true;
}else{
this.isShow = false;
}
}
}
}
</script>

这就是一个小米便签的创建和编辑,还有删除以及废纸篓功能这里就不多说了,功能都很简单不明白的地方可以看源代码,然后自己实战操作一番,如有写的不对的地方大家提出来,互相学习互相帮助嘛,谢谢!

相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程

   
2452 次浏览       17