一個具有基本增刪改查功能的通訊錄,數據保存在本地的localStorage中。
demo地址: https://junjunhuahua.github.io
1. 所用技術
js框架: vue2 https://cn.vuejs.org/
ui框架: elementUI http://element.eleme.io/#/zh-CN
腳手架: vue-cli
單頁: vue-router https://router.vuejs.org/zh-cn/
模塊打包: webpack
2. 腳手架搭建
build為構建項目所用的node代碼,config為構建時的一些配置項,dist為打包后(npm run build 用於發布)的代碼,node_modules為node模塊,src為開發時所用的代碼。
src目錄:

assets為全局css,圖片,以及一些工具類的js,components為vue的組件,router為路由配置,app.vue為主頁面的組件,config.js為目錄配置項,main.js為入口js
import Vue from 'vue' import App from './App.vue' import router from './router' import ElementUI from 'element-ui' import utils from './assets/utils.js' import 'element-ui/lib/theme-chalk/index.css' import './assets/normalize.css' Vue.use(ElementUI) Vue.use(utils) /* eslint-disable no-new */ new Vue({ el: '#app', router, ElementUI, template: '<App/>', components: { App } })
main.js的主要工作是引入一些框架,全局css,以及工具函數,還會處理vue組件的加載,最后實例化vue。
<template>
<div id="app">
<div class="app-left">
<el-row class="tac">
<el-col>
<el-menu :default-active="menuIndex" class="el-menu-vertical-demo"
background-color="#545c64" text-color="#fff" :unique-opened="menuUniqueOpen" :router="menuRouter"
active-text-color="#ffd04b">
<h3>我的應用</h3>
<template v-for="(item, index) in menuData">
<!-- 此處的index需顯示轉換為string,否則會報warn -->
<el-submenu :index="'' + (index + 1)">
<template slot="title">{{ item.name }}</template>
<template v-for="(subItem, i) in item.value">
<!-- 此處index格式為父級的index加上下划線加上當前的index,index都需加1 -->
<router-link tag="span" :to="subItem.path">
<el-menu-item :index="subItem.name">{{ subItem.title }}</el-menu-item>
</router-link>
</template>
</el-submenu>
</template>
</el-menu>
</el-col>
</el-row>
</div>
<div class="app-right">
<router-view></router-view>
</div>
</div>
</template>
<script>
import menuData from './config'
export default {
name: 'app',
data () {
return {
menuData,
menuIndex: '', // 菜單當前所在位置
menuUniqueOpen: true, // 菜單項是否唯一開啟
menuRouter: true // 是否開啟路由模式
}
},
mounted: function () {
...
},
watch: {
'$route' (to) {
this.menuIndex = to.name
}
}
}
</script>
Vue.use(VueRouter) let myRouter = new VueRouter({ routes: [ { path: '*', component: () => import('../components/NotFoundComponent.vue') }, { path: '/', redirect: '/contact' }, { path: '/contact', name: 'Contact', component: () => import('../components/contact/List.vue') }, { path: '/contact/edit', name: 'Contact', component: () => import('../components/contact/Edit.vue') }, { path: '/account', name: 'Account', component: () => import('../components/account/list.vue') } ] })
可以看到上面/contact和/contact/edit的name是相同的,這是為了讓在新增或者編輯聯系人頁面下,還能讓active狀態停留在左側我的聯系人上,可以看到App.vue中的代碼this.menuIndex = to.name就是進行的該操作,
雖然這樣vue會報一個warn告訴我別重名[捂臉],暫時能想到的就是這樣的操作方式了,有考慮過依靠判斷path來確定是否顯示高亮狀態,但是當目錄層級較深且較復雜的情況下,這樣就不是很靠譜了。
component這里為什么是這種形式,而不是直接用一個組件名呢,因為當路由開始多起來的時候,一下把所有的組件都加載進來會非常非常慢且會加載到許多當時並沒有用到的組件,通過import這種形式,可以讓webpack將路由變換時用到的組件分開打包,網頁會根據使用情況再進行
由於router是vue的組件,所以使用時記得要Vue.use一下。
7. 聯系人列表頁 --- contact/list.vue
<template>
<div class="contact-list">
<div class="contact-list-header">
<el-button @click="goToNew" type="primary">新增聯系人</el-button>
</div>
<div class="contact-list-content">
<template>
<div class="contact-list-wrap">
<h3>高級檢索</h3>
<el-form ref="contactSearch" :model="searchParams" :inline=true>
<el-form-item label="姓名">
<el-input v-model="searchParams.name" placeholder="請輸入需要檢索的姓名"></el-input>
</el-form-item>
</el-form>
<el-button type="primary" size="mini" round @click="contactSearch('contactSearch')">搜索</el-button>
</div>
<div class="contact-list-wrap">
<h3>聯系人列表</h3>
<el-table
:data="listNewData"
style="width: 100%"
@row-click="viewContact"
:default-sort="{prop: 'name', order: 'descending'}"
>
<el-table-column
label="姓名"
prop="name"
sortable
width="180">
</el-table-column>
...
<el-table-column
label="功能">
<template scope="scope">
<el-button size="mini" type="primary" @click.stop="editContact(scope)">編輯</el-button>
<el-button size="mini" @click.stop="deleteContact(scope)">刪除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
</div>
<contact-view ref="contactView" :viewData="curData" :viewShow.sync="viewShow"></contact-view>
</div>
</template>
<script>
import contactView from './View.vue'
export default {
data () { ... },
components: {
contactView
},
computed: {
listNewData: function () { ... },
mounted: function () {
this.listData = this.utils.getLocalStorage('vueContact')
},
methods: {
goToNew: function () {
this.$router.push('/contact/edit')
},
sexFormatter: function (row) { ... },
deleteContact: function (res) {
let data = res.row
this.$confirm('此操作將永久刪除該聯系人, 是否繼續?', '提示', {
confirmButtonText: '確定',
cancelButtonText: '取消',
type: 'warning',
callback: (action) => {
if (action === 'confirm') {
this.$delete(this.listData, data.id)
this.utils.setLocalStorage('vueContact', this.listData)
}
}
})
},
editContact: function (res) {
let data = res.row
this.$router.push({
path: '/contact/edit', query: {id: data.id}
})
},
viewContact: function (row) {
this.viewShow = true
this.curData = this.listData[row.id]
},
contactSearch: function () {
let data = this.utils.getLocalStorage('vueContact')
let newData = {}
for (let item in data) {
if (data[item].name.indexOf(this.searchParams.name) > -1) {
newData[item] = data[item]
}
}
this.listData = newData
}
}
}
</script>
list.vue相當於該模塊的主頁,新增與編輯頁面通過右上角的新建按鈕或者列表中的編輯按鈕進入,查看頁面通過引入View.vue作為一個彈窗放在列表頁中展示,不單獨設置路由。
列表展示所使用的是elementUI的table組件
刪除對象時一定要使用$delete,否則不會觸發視圖更新
view.vue代碼:
<template>
<div class="contact-view">
<el-dialog :before-close="closePop" ref="myDialog" :visible="viewShow">
<el-form :model="viewData" label-width="60px">
<el-form-item label="姓名" prop="name">
<el-input :readonly="true" v-model="viewData.name"></el-input>
</el-form-item>
...
<el-form-item label="備注">
<el-input :readonly="true" type="textarea" v-model="viewData.desc"></el-input>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
export default {
props: ['viewShow', 'viewData'],
methods: {
closePop: function () {
// 需手動關閉彈窗,找到父組件中調用的地方進行事件的觸發
this.$parent.$refs.contactView.$emit('update:viewShow', false)
}
}
}
</script>
這里有個比較值得注意的點,就是關閉查看彈窗,彈窗的開啟關閉狀態通過list也就是父級中的viewShow來控制,viewShow通過view也就是子級中的props流入到子級中,但是vue中的數據流向是默認是單向的,想要子級中修改父級屬性必須使用emit,詳見上面代碼。
這里原先使用elementUI的dialog組件的自己的關閉,會報錯,只能自己修改了。
ps: 為什么這里不用vuex處理父子組件的通信?因為如果是一個大型的后台管理系統,像這樣的情況會經常發生,如果都放在vuex中管理,那vuex的體積會非常龐大,反而不利於維護。
8. 聯系人編輯(新增)頁 --- edit.vue
<template>
<div class="contact-edit">
<el-form ref="contactForm" :model="form" :rules="rules" label-width="80px">
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="性別">
<el-select v-model="form.sex" placeholder="請選擇性別">
<el-option label="男" value="male"></el-option>
<el-option label="女" value="female"></el-option>
</el-select>
</el-form-item>
...
<el-form-item label="備注">
<el-input type="textarea" v-model="form.desc"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit('contactForm')">{{ btnName }}</el-button>
<el-button @click="cancelForm">取消</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data () {
var nameValid = (rule, value, callback) => {
if (!value) {
callback(new Error('姓名不能為空'))
} else {
callback()
}
}
var mobileValid = (rule, value, callback) => {
let phonePattern = /(^\s*$)|(^[1][3,4,5,7,8][0-9]{9}$)/
if (value && !phonePattern.test(value)) {
callback(new Error('手機號格式不正確'))
} else {
callback()
}
}
return {
type: '', // 控制是否是新建
...
rules: {
name: [{validator: nameValid, trigger: 'blur'}],
mobile: [
// {required: true, message: '手機號不能為空', trigger: 'blur'},
{validator: mobileValid, trigger: 'blur'}
]
}
}
},
// 組件加載后的鈎子
mounted: function () {
this.checkPageStatus(this.$route.query.id)
},
// 路由在組件中的鈎子
beforeRouteUpdate: function (to, from, next) {
this.checkPageStatus(to.query.id)
next()
},
methods: {
// 檢查頁面是新建還是編輯
checkPageStatus: function (id) { ... },
cancelForm: function () {
this.$router.push('/contact')
},
onSubmit: function (formName) { ... }
}
}
</script>
可以看到mounted與beforeRouteUpdate中的代碼有些重合,那是因為vue在路由僅僅只是參數變換的時候,是不會重新重新加載組件的,所以需要在beforeRouteUpdate中處理初始的數據。
nameValid與mobileValid為表單驗證的函數,el-form配置rules屬性名稱,然后data中相應的添加rules即可開啟表單驗證,但是有一點一定要注意el-form-item上一定要設置對應的prop屬性,rules才會生效。
9. 總結
非常簡單的一個項目,但是有幾個點一定要關注好:
模塊的划分,模塊划分要合理,盡量能保證模塊的復用性
狀態的管理,一定要明確什么東西要放vuex中,什么東西不用放,以免使項目的維護反而變得更復雜
如果是大型項目,路由中一定要讓.vue文件在需要時再引入,否則會加重初次加載的負擔
為了減少篇幅,刪減了很多不重要的代碼,需要查看源碼請移步,項目地址: https://github.com/junjunhuahua/vue-basic-demo
github上的項目已改為后台提供接口,不再使用localStorage操作數據,后台項目使用MongoDB+node實現,具體項目:https://github.com/junjunhuahua/mongodb-demo
