一个具有基本增删改查功能的通讯录,数据保存在本地的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=" 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