如何使用Vue 2制作带有本地存储的CRUD应用
Vue.js是一个渐进的、多功能的前端JavaScript框架。由于它的简单性和小捆绑规模,它的可采用性很高。本地存储是现代浏览器中的一个Web存储API,它允许我们在用户的浏览器上将数据存储为键值对的字符串。
有了这个,我们就可以在不与后端应用程序通信的情况下处理数据,这就增强了数据的持久性。与使用cookies时不同,它在客户端最多存储4kb的数据。它们在HTTP请求时被发送到服务器上,并且可以被服务器修改。
前提条件
要跟上本教程,读者将需要。
- Node.js 6.x或更新版本
- Npm 5.10或更新版本
- Vue CLI
- 对JavaScript、CSS和HTML有一定了解
用Vue CLI创建项目
要创建一个Vue.js项目,首先要检查Vue CLI是否已在你的电脑中全局安装。
使用终端运行。
$ vue –version
如果没有安装,运行下面的命令来安装它。
$ npm install -g @vue/cli
进入你的工作区文件夹,运行下面的命令,创建一个新的Vue.js应用程序。
$ vue create books-app
使用方向键选择。
❯ Default ([Vue 2] babel, eslint)
然后点击回车。创建后,导航到创建的文件夹books-app ,并通过运行命令为该应用程序提供服务。
$ cd books-app
$ npm run serve
然后,在浏览器中打开URL http://localhost:8080,查看该应用程序。
安装Vue.js DevTools
这是一个用于调试Vue.js应用程序的浏览器扩展。它可以检查组件、道具、路由、vuex等。
打开你的浏览器,并安装Mozilla或Chrome的Vue.js DevTools扩展。
要打开浏览器的DevTools,在Windows/Linux上按SHIFT + CTRL + J ,在MacOS上按Command + Option + j 。
创建书籍组件
这个应用程序将管理一个要阅读的书籍列表。用你选择的代码编辑器打开这个应用程序。在我们浏览器上显示的组件是位于src/components 文件夹中的HelloWorld 组件。
我们将删除它和它在App.vue 中的引用,包括图像标识。现在在components 文件夹中创建一个Books.vue 文件。将下面的代码添加到这个文件中。
<template>
<div>
<h2>My Books List</h2>
</div>
</template>
<script>
export default {
name: "Books"
}
</script>
<style scoped>
</style>
注意,在Vue 2中,组件模板应该只包含一个根元素。否则,它将抛出一个错误。然后我们可以将Books 组件导入到根组件中。App.vue 组件现在应该如下图所示。
data函数返回一个空的书籍数组,这些书籍我们以后会添加到数组中。
<template>
<div id="app">
<Books/>
</div>
</template>
<script>
import Books from "./components/Books";
export default {
name: 'App',
components: {
Books
},
data () {
return {
books: []
}
}
}
</script>
<style>
#app {
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
v-bind 是一个Vue指令,用于将数据附加到一个Vue组件。这将帮助我们绑定传递给 组件的数据。Books
对App.vue 进行修改,如下图所示。
<Books v-bind:books="books"/>
props 是用来将数据从父组件传递给子组件的。在这种情况下, 是父组件,而 是子组件。App.vue Books.vue
为了使用道具,编辑子组件的<script> ,如下图所示。
<script>
export default {
name: "Books",
props: ["books"]
}
</script>
创建 "BookItem "组件
在组件文件夹中创建一个文件BookItem.vue 。这个组件将代表一本书。BookItem.vue 的代码应该像下面显示的片段。
<template>
<div>
<p></p>
</div>
</template>
<script>
export default {
name: "BookItem"
}
</script>
现在,将BookItem 作为一个子组件导入到Books 组件中,并在组件对象中声明它。在这里,我们将循环浏览数据,并使用Vue指令v-for ,向用户显示BookItems 。
Books.vue 的代码现在应该如下所示。
<template>
<div>
<h2>My Books List</h2>
<div v-bind:key="book.id" v-for="book in books">
<BookItem v-bind:book="book"></BookItem>
</div>
</div>
</template>
<script>
import BookItem from "./BookItem";
export default {
name: "Books",
props: ["books"],
components: {
BookItem
}
}
</script>
注意v-bind:key 。这很重要,因为它给了Vue一个提示来追踪每个节点的身份。v-bind:book 将数据绑定到Vue组件上。
为了显示一本书,编辑BookItem.vue ,如下图所示。
<template>
<div>
<p>{{book.title}}</p>
</div>
</template>
<script>
export default {
name: "BookItem",
props: ["book"]
}
</script>
你可以在App.vue 中的书籍数组中添加你自己的数据,如下图所示,以显示数据到用户界面。
books: [
{
id:1,
title: "1000 Leagues Under the Sea"
},
{
id:2,
title: "The Scorpion"
},
]
创建 "AddBookItem "组件
现在你可以删除上面的JSON测试数据。在components 文件夹下,创建一个名为AddBook.vue 的文件。把它导入到App.vue ,并在脚本内的组件对象中声明它,如下图所示。
import Books from "./components/Books";
import AddBookItem from "./components/AddBookItem";
export default {
name: 'App',
components: {
Books,
AddBookItem
},
}
现在在AddBookItem.vue 中添加以下代码。
<template>
<div>
<form @submit="addBook">
<input type="text" name="title" v-model="title" placeholder="Add Book">
<button type="submit">Add Book</button>
</form>
</div>
</template>
<script>
export default {
name: "AddBookItem",
data () {
return {
title: ''
}
},
methods: {
addBook(e){
e.preventDefault();
const newBook = {
title: this.title,
id: Math.floor(Math.random() * 100)
};
if (newBook.title !== ''){
this.$emit('add-book-event', newBook);
}
this.title = ''
}
}
}
</script>
这段代码有一个表单,你可以用它来添加一本书。它还有一个方法addBook() 和一个vue-directivev-model ,在用户输入和Vue.js组件之间建立一个双向的绑定。对输入值的任何改变都会改变绑定的数据,反之亦然。在这种情况下,title 。你会看到一个添加书籍的表单。
每本书都需要一个唯一的ID。我们将使用JavaScript的Math.random() 方法来生成唯一的id。
$emit() 方法会发出一个事件add-book-event ,用来根据用户的动作从子组件向父组件传递数据。当用户添加一本书并提交时,这个事件会被发射到父组件。
为了让父组件(App.vue)能够监听来自子组件(AddBookItem.vue)的add-book-event 事件,我们创建了一个方法addBook() ,并将其分配给发出的事件。
对App.vue 进行修改,使其看起来像下面这个。让AddBookItem() 在模板中位于Book 组件的上方。
<template>
<div id="app">
<AddBookItem v-on:add-book-event="addBook" />
<Books v-bind:books="books"/>
</div>
</template>
就在data() ,使用下面的代码添加一个方法addBook() 。
methods: {
addBookItem(newBook){
this.books = [...this.books, newBook]
},
}
该方法在书籍数组中添加一个新的书籍,我们使用传播操作符,这将新的书籍添加到数组的末尾,而不创建一个新的数组。
将数据保存到本地存储
我们将使用Vue.js内置的方法watch()。这个方法会自动观察书籍数组中的变化,并将数据保存到本地存储。
watch() 方法有一个名为deep 的属性,它被设置为true,以通知Vue实例始终关注books数组中的变化。
watch() 在处理你的组件之外的数据时,如浏览器API或获取数据时,会用到这个属性。
在App.vue ,添加以下代码到<script> 。
watch: {
books: {
handler() {
localStorage.setItem('books',JSON.stringify(this.books))
},
deep: true
}
}
本地存储使用setItem() 方法将数据保存为键值对,数据必须是一个字符串,所以我们将JSON转换成字符串,以便从使用JSON.stringify()方法保存。
从本地存储加载数据
我们需要将保存的数据,从本地存储中显示给用户。我们将使用一个名为mounted()的生命周期钩子,在Vue实例被创建后执行。
在这个循环钩子中,我们使用方法localStorage.getItem('key') ,从本地存储中检索数据。我们用来存储的键和我们用来检索数据的键是一样的。
在App.vue 中的watch() 方法之后添加下面的代码。
mounted() {
if (localStorage.getItem("books")){
this.books = JSON.parse(localStorage.getItem("books"))
}
}
JSON.parse() 方法将一个字符串转换为一个JavaScript对象,因为数据在本地存储中只以字符串形式存储。
然后,该数据被设置为显示给用户的书籍数组。
现在你就可以看到添加后的书籍列表了。
从本地存储中删除数据
用以下代码更新BookItem.vue 。
<div class="float-left">
<span class="float-right">
{{book.title}}
<button>
<i class="glyphicon glyphicon-trash" @click="$emit('del-book-item', book.id)">delete</i>
</button>
</span>
</div>
我在index.html 文件中添加了一个bootstrap CSS CDN链接。你可以把你的样式设计得更好看。
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
当你点击删除按钮时,会发出一个事件,将书的ID传递给父级(Books.vue)。$emit() 是Vue将数据从子组件传递给父组件的方式。
在Books.vue 更新<BookItem/> ,如下所示。
<BookItem v-bind:book="book" v-on:del-book-item="delBookMethod" />
将来自子组件的事件分配给一个叫做delBookMethod() 的方法。把它添加到方法对象中,并向它的父组件(App.vue)发出一个事件,同时传递书的ID。
methods: {
delBookMethod(id){
//send to parent
this.$emit('del-book-event', id);
},
}
在父组件(App.vue)中,让我们做一些改变。
<Books v-bind:books="books" v-on:del-book-event="deleteBookItem" />
儿童事件被捕获并分配给一个方法deleteBookItem() 。这个方法将帮助我们删除被点击的图书项目。还记得我们从BookItem.vue 到Books.vue 再到App.vue 所传递的ID吗?
它将被用来删除这本书,使用JavaScriptfilter() 方法来创建书籍数组,其中不包括传递ID的书籍。我们将使用ES6的箭头函数,如下图所示,这将返回所有的书,除了传递ID的那本。
在AppVue ,将这个方法添加到methods 。
deleteBookItem(id){
this.books = this.books.filter(book => book.id !== id);
}
编辑数据
就像我们在删除数据时做的那样,对BookItem.vue ,添加一个编辑按钮,代码应该是这样的。
<template>
<div class="float-left">
<span class="float-right">
{{book.title}}
<button>
<i class="glyphicon glyphicon-pencil" @click="$emit('edit-book-item', book.id)">edit</i>
</button>
<button>
<i class="glyphicon glyphicon-trash" @click="$emit('del-book-item', book.id)">delete</i>
</button>
</span>
</div>
</template>
发出一个名为edit-book-item 的事件,并将书的ID传递给它的父类(Books.vue)。在Books.vue 中监听该事件并将其分配给一个名为editBookMethod() 的方法,如下所示。
<BookItem v-bind:book="book" v-on:del-book-item="delBookMethod" v-on:edit-book-item="editBookMethod" />
使用该方法,向它的父类(App.vue)发送一个事件,并将书的id与它一起传递。
将这个方法添加到Books.vue 中的methods 。
editBookMethod(id){
//send to parent (App.vue)
this.$emit('edit-book-event', id)
}
在父类中,进行修改以从Books 组件中捕获事件edit-book-event ,并将其分配给一个方法editBookItem() 。在data 中创建一个新的对象editBook ,它将保存被编辑的数据。
该对象应该有一个标题和一个ID。
两者都应该是空字符串。
data () {
return {
books: [],
editBook: {
title: '',
id: ''
}
}
}
现在在editBookItem 方法中,我们需要找到该对象的id的索引。我们使用JavaScript的findIndex() 方法来做这件事。我们通过书籍数组来找到与子组件传递的ID相匹配的书籍对象,并将其分配给一个变量objIndex 。
这个变量帮助我们从书籍数组中获取书的标题,并将其与ID一起分配给editBook 对象中的标题,如下图所示。
editBookItem(id){
//find the index of the book's id
var objIndex = this.books.findIndex(obj=> obj.id === id);
this.editBook.title = this.books[objIndex].title;
this.editBook.id = id;
},
我们仍然不能编辑一本书。我们在AddBookItem 组件中捕获先前的事件edit-book-event ,并将其分配给一个方法editBookItemEvent() 。
然后我们用v-bind 指令将editBook 属性绑定到组件上,并将其作为一个道具传递给子组件(AddBookItem ),如下图所示。
<AddBookItem v-model="editBook.title" v-on:add-book-event="addBookItem" v-bind:editBook="editBook"/>
让我们打开AddBookItem.vue 。我们从父节点接收editBook 数据对象作为道具。然后在数据函数中添加id 为空字符串,edit 为假。
name: "AddBookItem",
props: ['editBook'],
data () {
return {
title: '',
id: '',
edit: false
}
}
我们将使用这个edit 属性来决定是否编辑或添加一个新书。我们首先检查用户是否没有编辑。我们保存数据,否则我们将编辑数据。
如果是编辑,我们发出一个edit-book-event ,并将保存编辑过的数据的变量bookItem ,连同事件一起传递给父级。
我们还清除输入字段。现在更新addBookItem() 方法,使其看起来如下所示。
addBook(e){
e.preventDefault();
if (this.edit === false){
// add new book
const newBook = {
title: this.title,
id: Math.floor(Math.random() * 100)
};
if (newBook.title !== ''){
this.$emit('add-book-event', newBook);
}
this.title = ''
}else{
//edit book
const bookItem = {
title: this.title,
id: this.id
}
//send to parent (App.vue)
this.$emit('edit-book-event', bookItem)
// clear input field
this.title = '';
this.edit = false;
}
}
现在你可以点击编辑按钮,输入字段将被填充为书名。watch() 方法再次派上用场,以帮助我们观察 editBook 数据的任何变化。
我们设置deep:true 属性,让Vue实例持续观察变化。因此,在编辑一本书时,edit 属性将始终为真。
它还观察标题属性,如果它是空的,它就把edit 属性设置为假。
这里我们不需要deep 属性。
watch: {
editBook: {
handler() {
this.title = this.editBook.title;
this.id = this.editBook.id;
this.edit = true
},
deep: true
},
title: {
handler() {
if (this.title === ''){
this.edit = false;
}
}
}
}
回到App.vue ,在编辑完一个标题后,一个事件edit-book-event 被发送到App.vue 。我们将该事件分配给一个方法,以便将变化保存到本地存储。更新你的代码,使其看起来像下面所示。
<AddBookItem v-model="editBook.title" v-on:add-book-event="addBookItem" v-bind:editBook="editBook" v-on:edit-book-event="editBookItemEvent" />
现在我们创建一个editBookItemEvent() 方法来处理数据的保存。在这个方法中,我们找到id的对象的索引。
这个索引将被用来重新分配正在编辑的书的标题。如果你已经走到这一步,你就可以编辑一个书名了。
在App.vue ,添加下面的代码到methods 。
editBookItemEvent(bookItem){
//find the index of this id's object
let objIndex = this.books.findIndex(obj => obj.id === bookItem.id)
//update the item
this.books[objIndex].title = bookItem.title;
}
现在,你的App.vue 应该看起来像下面的代码。
<template>
<div id="app">
<AddBookItem v-on:add-book-event="addBookItem" v-on:edit-book-event="editBookItemEvent" v-bind:editBook="editBook"/>
<div>
<Books v-bind:books="books" v-on:del-book-event="deleteBookItem" v-on:edit-book-event="editBookItem" />
</div>
</div>
</template>
<script>
import Books from "./components/Books";
import AddBookItem from "./components/AddBookItem";
export default {
name: 'App',
components: {
Books,
AddBookItem
},
data () {
return {
books: [],
editBook: {
title: '',
id: ''
}
}
},
methods: {
addBookItem(newBook){
// console.log('newbook', newBook.title);
this.books = [...this.books, newBook];
// this.books.unshift(newBook)
},
deleteBookItem(id){
this.books = this.books.filter(book => book.id !== id);
},
editBookItem(id){
//find the index of the book's id
let objIndex = this.books.findIndex(obj=> obj.id === id);
this.editBook.title = this.books[objIndex].title;
this.editBook.id = id;
},
editBookItemEvent(bookItem){
//find the index of this id's object
let objIndex = this.books.findIndex(obj => obj.id === bookItem.id)
//update the item
this.books[objIndex].title = bookItem.title;
}
},
watch: {
books: {
handler() {
localStorage.setItem('books',JSON.stringify(this.books))
},
deep: true
}
},
mounted() {
if (localStorage.getItem("books")){
this.books = JSON.parse(localStorage.getItem("books"))
}
}
}
</script>
<style>
#app {
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
结论
我们刚刚完成了一个带有本地存储的CRUD Vue2应用程序的创建。你可以使用 materialize 组件或其他 UI 设计材料来改进你的应用程序的用户界面。