如何使用Vuex+Vue.js构建单页应用

如何使用Vuex+Vue.js构建单页应用,第1张

按照上面我们列出来的功能模块,我们在 Vuex/ 下面建立一个 store.js 文件

import Vue from 'vue'

import Vuex from 'vuex'

Vue.use(Vuex)

// 需要维护的状态

const state = {

notes: [],

activeNote: {},

show: ''

}

const mutations = {

// 初始化 state

INIT_STORE(state, data) {

state.notes = data.notes,

state.show = data.show

state.activeNote = data.activeNote

},

// 新增笔记

NEW_NOTE(state) {

var newNote = {

id: +new Date(),

title: '',

content: '',

favorite: false

}

state.notes.push(newNote)

state.activeNote = newNote

},

// 修改笔记

EDIT_NOTE(state, note) {

state.activeNote = note

// 修改原始数据

for (var i = 0i <state.notes.lengthi++) {

if(state.notes[i].id === note.id){

state.notes[i] = note

break

}

}

},

// 删除笔记

DELETE_NOTE(state) {

state.notes.$remove(state.activeNote)

state.activeNote = state.notes[0] || {}

},

// 切换笔记的收藏与取消收藏

TOGGLE_FAVORITE(state) {

state.activeNote.favorite = !state.activeNote.favorite

},

// 切换显示数据列表类型:全部 or 收藏

SET_SHOW_ALL(state, show){

state.show = show

// 切换数据展示,需要同步更新 activeNote

if(show === 'favorite'){

state.activeNote = state.notes.filter(note =>note.favorite)[0] || {}

}else{

state.activeNote = state.notes[0] || {}

}

},

// 设置当前激活的笔记

SET_ACTIVE_NOTE(state, note) {

state.activeNote = note

}

}

export default new Vuex.Store({

state,

mutations

})

创建 Vuex Actions

在 Vuex/ 下面建立一个 action.js,用来给组件使用的函数

function makeAction(type) {

return ({ dispatch }, ...args) =>dispatch(type, ...args)

}

const initNote = {

id: +new Date(),

title: '我的笔记',

content: '第一篇笔记内容',

favorite: false

}

// 模拟初始化数据

const initData = {

show: 'all',

notes: [initNote],

activeNote: initNote

}

export const initStore = ({ dispatch }) =>{

dispatch('INIT_STORE', initData)

}

// 更新当前activeNote对象

export const updateActiveNote = makeAction('SET_ACTIVE_NOTE')

// 添加一个note对象

export const newNote = makeAction('NEW_NOTE')

// 删除一个note对象

export const deleteNote = makeAction('DELETE_NOTE')

export const toggleFavorite = makeAction('TOGGLE_FAVORITE')

export const editNote = makeAction('EDIT_NOTE')

// 更新列表展示

export const updateShow = makeAction('SET_SHOW_ALL')

创建 Vuex Getters

在 vuex/ 下面建立一个 getter.js 文件,用来从 store 获取数据

// 获取 noteList,这里将会根据 state.show 的状态做数据过滤

export const filteredNotes = (state) =>{

if(state.show === 'all'){

return state.notes || {}

}else if(state.show === 'favorite'){

return state.notes.filter(note =>note.favorite) || {}

}

}

// 获取列表展示状态 : all or favorite

export const show = (state) =>{

return state.show

}

// 获取当前激活 note

export const activeNote = (state) =>{

return state.activeNote

}

以上就是我们 Vuex 的所有逻辑了,在定下了我们需要完成的功能之后,接下来就是只需要在组件中去调用 action 来实现对应的功能了。

路由配置

在这里我们将使用 vue-router 来做路由,引用 bootstrap 样式。

index.html

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

<title>vuex-notes-app</title>

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">

</head>

<body>

<div id="app"></div>

<!-- built files will be auto injected -->

</body>

</html>

所有的入口逻辑我们都将在 main.js 中编写

main.js

import Vue from 'vue'

import App from './App'

import VueRouter from 'vue-router'

import VueResource from 'vue-resource'

// 路由模块和HTTP模块

Vue.use(VueResource)

Vue.use(VueRouter)

const router = new VueRouter()

router.map({

'/index': {

component: App

}

})

router.redirect({

'*': '/index'

})

router.start(App, '#app')

根组件 App.vue

<template>

<div id="app" class="app">

<toolbar></toolbar>

<notes-list></notes-list>

<editor></editor>

</div>

</template>

<style>

html, #app {

height: 100%

}

body {

margin: 0

padding: 0

border: 0

height: 100%

max-height: 100%

position: relative

}

</style>

<script>

import Toolbar from './components/Toolbar'

import NotesList from './components/NotesList'

import Editor from './components/Editor'

import store from './vuex/store'

import { initStore } from './vuex/actions'

export default {

components: {

Toolbar,

NotesList,

Editor

},

store,

vuex: {

actions: {

initStore

}

},

ready() {

this.initStore()

}

}

</script>

在根组件中引用了三个子组件:Toolbar.vue, NotesList.vue, Editor.vue。

注意:我们在配置里面加入了 vuex 这么一个选项,这里用来将我们 action 里面定义的方法给暴露出来,我们在根组件中只做了一件事情,那就是初始化模拟数据,因此我们在组件生命周期的 ready 阶段调用了 actions 里面的 initStore 来初始化我们的 store 里面的 state

Toolbar.vue

<template>

<div id="toolbar">

<i class="glyphicon logo"><img src="../assets/logo.png" width="30" height="30"></i>

<i @click="newNote" class="glyphicon glyphicon-plus"></i>

<i @click="toggleFavorite" class="glyphicon glyphicon-star" :class="{starred: activeNote.favorite}"></i>

<i @click="deleteNote" class="glyphicon glyphicon-remove"></i>

</div>

</template>

<script>

import { newNote, deleteNote, toggleFavorite } from '../vuex/actions'

import { activeNote } from '../vuex/getters'

export default {

vuex: {

getters: {

activeNote

},

actions: {

newNote,

deleteNote,

toggleFavorite

}

}

}

</script>

<style lang="scss" scoped>

#toolbar{

float: left

width: 80px

height: 100%

background-color: #30414D

color: #767676

padding: 35px 25px 25px 25px

.starred {

color: #F7AE4F

}

i{

font-size: 30px

margin-bottom: 35px

cursor: pointer

opacity: 0.8

transition: opacity 0.5s ease

&:hover{

opacity: 1

}

}

}

</style>

在这里,我们用到了 Vuex 的一个案例就是我们需要知道当前的激活的笔记是否是收藏类别的,如果是,我们需要高亮收藏按钮,那么如何知道呢?那就是通过 vuex 里面的 getters 获取当前激活的笔记对象,判断它的 favorite 是否为 true。

始终牢记一个概念,vuex 中数据是单向的,只能从 store 获取,而我们这个例子中的 activeNote 也是始终都在 store.js 中维护的,这样子就可以给其他组件公用了

// 需要维护的状态

const state = {

notes: [],

activeNote: {},

show: ''

}

NotesList.vue

<template>

<div id="notes-list">

<div id="list-header">

<h2>Notes | heavenru.com</h2>

<div class="btn-group btn-group-justified" role="group">

<!-- all -->

<div class="btn-group" role="group">

<button type="button" class="btn btn-default"

@click="toggleShow('all')"

:class="{active: show === 'all'}">All Notes</button>

</div>

<!-- favorites -->

<div class="btn-group" role="group">

<button type="button" class="btn btn-default"

@click="toggleShow('favorite')"

:class="{active: show === 'favorite'}">Favorites</button>

</div>

</div>

</div>

<!-- 渲染笔记列表 -->

<div class="container">

<div class="list-group">

<a v-for="note in filteredNotes"

class="list-group-item" href="#"

:class="{active: activeNote === note}"

@click="updateActiveNote(note)">

<h4 class="list-group-item-heading">

{{note.title.trim().substring(0,30)}}

</h4>

</a>

</div>

</div>

</div>

</template>

<script>

import { updateActiveNote, updateShow } from '../vuex/actions'

import { show, filteredNotes, activeNote } from '../vuex/getters'

export default {

vuex: {

getters: {

show,

filteredNotes,

activeNote

},

actions: {

updateActiveNote,

updateShow

}

},

methods: {

toggleShow(show) {

this.updateShow(show)

}

}

}

</script>

笔记列表组件,主要有三个 *** 作

渲染笔记

切换渲染笔记

点击列表 title,切换 activeNote

我们通过 getters 中的 filteredNotes 方法获取笔记列表

// 获取 noteList,这里将会根据 state.show 的状态做数据过滤

export const filteredNotes = (state) =>{

if(state.show === 'all'){

return state.notes || {}

}else if(state.show === 'favorite'){

return state.notes.filter(note =>note.favorite) || {}

}

}

可以看到,我们获取的列表是依赖于 state.show 这个状态的。而我们的切换列表 *** 作恰好就是调用 actions 里面的方法来更新 state.show ,这样一来,实现了数据列表的动态刷新,而且我们对树的 *** 作都是通过调用 actions 的方法来实现的。

我们再看,在切换列表的时候,我们还需要动态的更新 activeNote 。 看看我们在 store.js 中是如何做的:

// 切换显示数据列表类型:全部 or 收藏

SET_SHOW_ALL(state, show){

state.show = show

// 切换数据展示,需要同步更新 activeNote

if(show === 'favorite'){

state.activeNote = state.notes.filter(note =>note.favorite)[0] || {}

}else{

state.activeNote = state.notes[0] || {}

}

}

触发这些 *** 作的是我们给两个按钮分别绑定了我们自定义的函数,通过给函数传入不同的参数,然后调用 actions 里面的方法,来实现对数据的过滤,更新。

Editor.vue

<template>

<div id="note-editor">

<div class="form-group">

<input type="text" name="title"

class="title form-control"

placeholder="请输入标题"

@input="updateNote"

v-model="currentNote.title">

<textarea

v-model="currentNote.content" name="content"

class="form-control" row="3" placeholder="请输入正文"

@input="updateNote"></textarea>

</div>

</div>

</template>

<script>

import { editNote } from '../vuex/actions'

import { activeNote } from '../vuex/getters'

export default {

vuex: {

getters: {

activeNote

},

actions: {

editNote

}

},

computed: {

// 通过计算属性得到的一个对象,这样子我们就能愉快的使用 v-model 了

currentNote: activeNote

},

methods: {

// 为什么这么做? 因为在严格模式中不允许直接在模板层面去修改 state 中的值

updateNote() {

this.editNote(this.currentNote)

}

}

}

</script>

在 Editor.vue 组件中,我们需要能够实时的更新当前的 activeNote 组件和列表中对应的我们正在修改的笔记对象的内容。

由于我们前面提到过,在组件中是不允许直接修改 store.js在里面的状态值的,所以在这里的时候,我们通过一个计算属性,将 store 里面的状态值赋值给一个对象,然后在自定义的 updateNotes() 方法中,去调用 action,同时传入 currentNote 对象。

在 store.js 中,我们是这么做的,找到对应的 id 的对象,重新赋值,因为前面提到过,我们的数据是响应式的,在这里进行了改变,对应的视图也将刷新改变,这样一来就实现了实时编辑,实时渲染的功能了。

// 修改笔记

EDIT_NOTE(state, note) {

state.activeNote = note

// 修改原始数据

for (var i = 0i <state.notes.lengthi++) {

if(state.notes[i].id === note.id){

state.notes[i] = note

break

}

}

},

pinia 目前已经是 vue 官方正式的状态库。适用于 vue2 和 vue3,本文只描述vue3的写法。

相对于以前的 vuex,pinia具有以下优势

创建一个 pinia 并传递给 vue 应用

store的定义是通过 defineStore 这个函数,

它需要一个唯一的名称,该名称可以作为第一个参数传递,也可以用 id 熟悉传递。

该 id 是必要的,主要是用于 vue devtools

上述代码中,useMainStore实例化后的,我们就可以在 store 上访问 state、getters、actions 等(pinia中没有mutations)。

该 store 是一个 reactive 对象,所以不需要 “.value”,也不能对其进行解构使用,否则失去响应性(类似 props)。

如果一定要对其进行解构使用,可以使用 storeToRefs ,类似 vue3 中的 toRefs

在 pinia 中,定义 state 是在函数中返回 state 初始状态

可以通过store 实例直接访问

也可以直接修改状态

虽然可以直接修改,但是出于代码结构来说,全局的状态管理还是不要直接在各个组件处随意修改状态,应放于 action 中统一方法修改(没有mutation了)

可以通过调用store 上的方法将状态重置为初始状态

修改state还可以通过使用 $patch 方法

$patch 可以同时修改多个值,举个例子

但是,这种写法的在修改数组时,例如我只想要把 userList 的中第一项"小明"的age 改为 20,也需要传入整个包括所有成员的数组,这无疑增加了书写成本和风险,于是一般都推荐使用以下的传入一个函数的写法

通过 store.$subscribe() 的方法,

该方法的第一个参数接受一个回调函数,该函数可以在 state 变化时触发

如上所示,该回调函数的两个参数

其中 state 是 mainStore 实例,而 mutation 打印如下

可以发现,打印结果的mutation对象主要包含三个属性

上面代码中,调用mainStore.$subscribe返回的值(即上方示例的 subscribe 变量)可以停止订阅

store.$subscribe() 的方法的第二个参数options对象,是各种配置参数,包括

detached属性,其值是一个布尔值,默认是 false, 正常情况下,当 订阅所在的组件被卸载时,订阅将被停止删除,如果设置detached值为 true 时,即使所在组件被卸载,订阅依然可以生效。

其他属性主要还有 immediate、deep、flush 等等,和 vue3 watch的对应参数效果一样。

getter 是 store 中的 state 计算值,以defineStore中的 getters 属性定义

getters属性的值是一个函数,该函数的第一个参数是 state

上面代码中,getters的值是箭头函数,当getters的值是普通函数时,可以通过 this 访问整个store实例(如下)

但是如果是普通函数,想要通过 this 获取state的值并希望this的类型能正确推断,同时希望函数的返回值类型正确推断,我们需要声明函数的返回类型。

action 是 store 中的 方法,支持同步或异步。

action 定义的函数可以是普通函数从而可以通过 this 访问整个store实例,同时该函数可以传入任意参数并返回任何数据

通过 store.$onAction() ,可以监听action的动作及结果等

该函数可以接收一个回调函数作为参数,回调函数的参数中有五个属性,具体如下

举个例子,

首先,定义一个store

然后在 setup 中使用

如上,在 setup 中,调用了 subscribeNormal 函数后,页面打印如下

调用了 subscribeError 函数后,页面打印如下

同样,可以通过调用 mainStore.$onAction 返回的值来手动停止订阅,在上面代码的例子中,即是

store.$onAction 默认在所在组件卸载时会被自动删除,可以通过传递第二个参数 true,来将action订阅和所在组件分开(即组件卸载时,订阅依然有效)

在组件中使用时,useStore() 在大多数情况下都可以在调用后开箱即用。

在其他地方使用时,需确保在 pinia 激活使用后( app.use(createPinia()) )才能使用 useStore()

例如在路由守卫中

在store中也可以访问其他store

pinia store 支持扩展,通过 pinia 插件我们可以实现以下

例如可以写一个简单的插件来给所有store添加一个静态属性

然后,在所有其他的store都可以访问到上面添加的 env 属性

从上方代码可以发现,pinia 插件是一个函数,这个函数有一个可选参数

context 打印出来主要有

通过 context 我们可以在 store 上设置属性

这样,在所有其他的store都可以访问到上面添加的 env 属性

pinia 的 store 是通过 reactive 包装的,可以自动解包它包含的任何 ref 对象

通过上面插件,访问store 的 env 时不需要 .value,就可以直接访问

当需要添加来自其他库或不需要响应式的数据时,应该用 markRaw() 包装传递的对象,例如

markRaw 来自 vue3,可以标记一个对象,使其永远不会转换为 proxy。返回对象本身。

当通过插件添加新属性时,可以扩展 PiniaCustomProperties 接口

可以用设置get,set或者简单声明值的类型,以此来安全地写入和读取新加的属性


欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/bake/11747558.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-05-18
下一篇2023-05-18

发表评论

登录后才能评论

评论列表(0条)

    保存