前言
本文是本系列的第三篇文章。在前两篇文章中我们介绍了关于搭建vue+elementUI开发环境的方法和编写导航页的方法。关于前两篇文章的内容,若读者有些记不清楚,可以点击下方链接快速查看:
从零开始搭建自己的VueJS2.0+ElementUI单页面网站(一、环境搭建)
从零开始搭建自己的VueJS2.0+ElementUI单页面网站(二、编写导航页)
由于最近比较忙,所以更新速度比较慢,请读者谅解。另外值得一提的是,ElementUI2.0版本已经推出beta版本,有一些新功能和新样式,感兴趣的读者可以访问Element2.0官网进行查阅。我本人最近也在研究,欢迎讨论。本文中仍使用1.X版本的ElementUI。
本文结尾附有当前工程的下载地址。
正文
下面进入正题。在这篇文章中我将对第一个非导航页面,也就是第一个有实质内容的页面进行设计和开发。这次我们要开发一个拥有表单和表格功能的页面。先上开发完的效果图:
效果图
可以看出页面非常的简单,其中上半部分是表单搜索和查询,下半部分是用于展示数据的表格。如果按照传统的开发思路,其实非常简单,只要用两个div,第一个div放置表单,第二个div放置表格即可。但是,我们今天要介绍的,是这个页面的另一种写法,也是vue作为一个优秀的前端框架的核心功能,也就是组件化的写法。
什么是组件化?
某搜索引擎告诉我们,组件化是指解耦复杂系统时将多个功能模块拆分、重组的过程,有多种属性、状态反映其内部特性。以我们这次要编写的页面为例,组件化就是要将这个页面里面的表格和表单分开成两个不同的组件,每个组件有它自己的属性和状态,既互不干扰又可以互相通信。
为什么要组件化?
从上文中的定义我们也可以看出,组件化的主要目的是解耦。当然,还有其他的目的,比如组件复用,按需引入等。具体的细节我们可以先往下看。
开始之前
为了规范工程的层级,我们把原先与Navi文件夹同级的Page1,Page2和Page3.vue文件删掉,重新建立三个名为Page1,Page2和Page3的文件夹,并分别在三个新建立的文件夹中建立Page1.vue,Page2.vue和Page3.vue。当然,结束之后同样要修改vue-router对于这三个组件的引用路径。如下图
修改后的router
接着,在刚才建立的Page1文件夹下,建立两个新vue组件:StudentForm.vue和StudentTable.vue。
这两个组件就是我们即将要编写的表单组件和表格组件。
接下来要介绍两种vue组件间传值的方法。对于父子组件的传值,网上有很多教程,这里不详述。对于其他类型的传值,我们这里要介绍vue的状态管理机制,vuex。
我们首先在src目录下新建一个名为vuex的文件夹,在vuex文件夹下建立一个index.js文件,作为vuex的配置文件。然后在vuex文件夹下再建立一个Modules文件夹,用于放置模块的状态文件。在Modules中新建一个Navi.js,用于存储Navi模块的状态;新建一个Student.js,用于存储我们即将要写的student模块的状态。
下面是代码
Navi.js
/*
* 导航页
*/
const state = {
//学生类型
studentTypeList:[],
}
const actions = {
//存入交通类型数据
changeStudentTypeListAction({commit}, payload) {
commit('changeStudentTypeListMutation', payload)
},
}
//mutations,真正用来修改state的方法集
const mutations = {
changeStudentTypeListMutation (state, payload) {
state.studentTypeList = payload
},
}
const getter = {
}
const moduleNavi = {
state: state,
mutations: mutations,
actions: actions,
getter: getter
}
export default moduleNavi;
可以看到我们导出的模块主要有四个部分:state,mutation,action和getter。state用于存储模块的状态,这个“状态”可以理解为在组件化开发下当前模块的全局变量,即需要进行通信的变量。action用于提交mutation,我们可以在action里进行异步操作。mutation是真正修改状态的函数。而getter类似于vue中的computed计算属性,这里我们用不到,所以暂时不添加内容。
下面是Student.js
/*
* 学生基本信息
*/
const state = {
//查询学生基本信息的表单
studentForm: {
id: '',
name: '',
type: '',
},
//是否进行查询
studentQueryFlag: false,
}
const actions = {
//存入搜索船舶基本资料form值
changeStudentFormAction({commit}, payload) {
commit('changeStudentFormMutation', payload)
},
//更改是否搜索标识
changeStudentQueryFlagAction ({commit}, payload){
commit('changeStudentQueryFlagMutation', payload)
},
}
//mutations,真正用来修改state的方法集
const mutations = {
changeStudentFormMutation (state, payload) {
state.studentForm = payload
},
changeStudentQueryFlagMutation (state, payload) {
state.studentQueryFlag = payload
},
}
const getter = {
}
const moduleStudent = {
state: state,
mutations: mutations,
actions: actions,
getter: getter
}
export default moduleStudent;
这个模块就是我们即将要编写的页面模块。这里面的state存储了两个变量:一个是查询所用到的表单,另一个是用于表示是否进行查询的标识flag。说到这,就不得不提到我们这次组件化开发,预计的程序运行的流程。这里我们用Page1.vue作为表单和表格组件的父组件。
- 在页面中表单内输入数据
- 表单组件通过调用student模块的action->mutation,将表单内的数据同步到state中
- 点击搜索按钮时,表单组件通过action->mutation,将state中的搜索flag(初始化为false)置于true
- Page1.vue中设置一个局部变量,将这个局部变量computed为state中的搜索flag
- 将步骤4中的局部变量通过父组件->子组件方式传值至表格组件中
- 表格组件中对这个接收到的值进行watch,当且仅当这个值由false变为true时,以state中的表单数据为搜索条件,向服务器发送请求,获取数据并渲染
最后一步千万不要忘了,表格组件还要通过调用student模块的action->mutation,将state中的搜索flag重新置为false。
可以看出这些步骤相对于非组件化编程来说很麻烦,但是它很好的解决了解耦的问题:表单组件不需要知道它的搜索请求发给了谁,而表格组件不需要知道是谁发起的搜索请求。如果你熟悉或使用过消息中间件,或是研究过订阅发布模式,你可以体会到相同的感觉。举个例子:我们一般会使用websocket或一些其他方式来进行服务端对客户端的消息推送。当我们从服务端推送“更新列表”的消息至客户端时,客户端的处理函数可以直接修改state中的搜索flag而达到效果,自始至终都与我们编写的表单组件不产生关系和耦合。
接下来是vuex中的index.js修改后的代码
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
import navi from './Modules/Navi'
import student from './Modules/Student'
export default new Vuex.Store({
modules: {
navi: navi,
student: student
}
})
接下来就是表格组件和表单组件,比较简单。
首先是表单组件
<template>
<div style="border-radius:5px;">
<div style="border:1px solid;background-color:#FFFFFF;box-shadow: 2px 2px 5px #888888;overflow: hidden;border-radius:5px;">
<div style="background-color:#20A0FF;padding:5px;color:white;">
学生资料查询
</div>
<br/>
<el-form ref="form" :model="form" :inline=true label-width="70px" label-position="left" style="margin-left: 5%">
<el-row :gutter="10">
<el-col :xs="24" :sm="7" :md="7" :lg="8">
<el-form-item label="名称" prop="name">
<el-input v-model="form.name"></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="7" :md="7" :lg="8">
<el-form-item label="id" prop="id">
<el-input v-model="form.id"></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="7" :md="7" :lg="8">
<el-form-item label="种类" prop="type">
<el-select v-model="form.type" clearable filterable placeholder="---请选择---" style="width:175px">
<el-option v-for="item in studentTypeList" :value="item.typeId" :label="item.typeName"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item style="float:right">
<el-button type="primary" @click="resetForm('form')">清空</el-button>
<el-button type="primary" @click="submitForm()">查询</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
data () {
return {
//提交的表单
form: {
name:'',
id:'',
type:''
},
}
},
methods: {
...mapActions({
saveFormVal: 'changeStudentFormAction',
search: 'changeStudentQueryFlagAction'
}),
//重置表单
resetForm(formName) {
this.$refs[formName].resetFields();
},
//提交表单
submitForm: function() {
this.search(true);
},
},
mounted () {
this.saveFormVal(this.form);
},
computed: {
studentTypeList(){
return this.$store.state.navi.studentTypeList;
}
}
}
</script>
<style>
</style>
值得说明的是,如果想调用state的action,需要引入mapActions,也就是js代码中的第一行
import { mapActions } from 'vuex'
并且在methods里用以下方式调用action
...mapActions({
saveFormVal: 'changeStudentFormAction',
search: 'changeStudentQueryFlagAction'
}),
注意…mapActions是固定方式,不要修改。对于函数体里面的参数,右侧是action的名称,也就是定义在vuex/Modules/XX.js中的action,而左侧是action在当前组件中的“引用”名。换句话说,
saveFormVal: 'changeStudentFormAction'
的意思是使saveFormVal和changeStudentFormAction这个action绑定,这样在当前组件中调用
this.saveFormVal({key: value})
实际上就是调用changeStudentFormAction({key: value})。
对于多个mapAction,用逗号隔开即可。
下面是表格组件。
<template>
<div style="box-shadow: 2px 2px 5px #888888;border-radius:5px;">
<div style="background-color:#20A0FF;padding:5px;color:white;overflow:hidden;border-radius:5px 5px 0 0">
<span class="demonstration" style="float:left;padding:5px">学生资料</span>
</div>
<div style="margin:1%">
<el-table
:data="tableData"
border
style="width: 100%"
:default-sort = "{prop: 'name', order: 'descending'}"
>
<el-table-column
prop="name"
label="姓名"
align="center"
sortable>
</el-table-column>
<el-table-column
prop="id"
label="id"
align="center"
sortable>
</el-table-column>
<el-table-column
prop="age"
label="年龄"
align="center"
sortable>
</el-table-column>
<el-table-column
prop="sex"
label="性别"
align="center"
sortable>
</el-table-column>
</el-table>
</div>
<div class="block" align="center">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 30, 40]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="totalNum">
</el-pagination>
</div>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
props:['searchflag'],
data () {
return {
//表格数据
tableData:[
{
id: 1,
name: '李小明',
sex: '男',
type: 0,
age: 22,
math: 97,
verbal: 78,
specialize: 82
},
{
id: 2,
name: '王小红',
sex: '女',
type: 0,
age: 21,
math: 80,
verbal: 90,
specialize: 84
},
{
id: 3,
name: '赵小刚',
sex: '男',
type: 0,
age: 24,
math: 94,
verbal: 99,
specialize: 97
},
{
id: 4,
name: '张小芸',
sex: '女',
type: 0,
age: 23,
math: 100,
verbal: 90,
specialize: 85
}
],
//详情页可见性
detailDialogVisible: false,
//被点击当前船舶信息
nowShipInfo:'',
//表格当前页
currentPage: 1,
//表格数据总量
totalNum: 0,
//每页显示数据数量
pageSize: 10,
}
},
methods: {
//加载表格ajax
loadData(){
var id = this.$store.state.student.studentForm.id;
var tabledata = [];
console.log(id)
if(id != ''){
this.tableData.forEach((item) => {
if(item.id == id)
tabledata.push(item)
})
this.tableData = tabledata;
}
else{
this.tableData=[
{
id: 1,
name: '李小明',
sex: '男',
type: 0,
age: 22,
math: 97,
verbal: 78,
specialize: 82
},
{
id: 2,
name: '王小红',
sex: '女',
type: 0,
age: 21,
math: 80,
verbal: 90,
specialize: 84
},
{
id: 3,
name: '赵小刚',
sex: '男',
type: 0,
age: 24,
math: 94,
verbal: 99,
specialize: 97
},
{
id: 4,
name: '张小芸',
sex: '女',
type: 0,
age: 23,
math: 100,
verbal: 90,
specialize: 85
}
]
}
this.totalNum = this.tableData.length;
},
//每页显示数据变更响应
handleSizeChange(val) {
this.pageSize = val;
this.loadData();
},
//换页响应
handleCurrentChange(val) {
this.currentPage = val;
this.loadData();
},
...mapActions({
search: 'changeStudentQueryFlagAction'
}),
},
mounted () {
this.loadData();
},
watch: {
searchflag(newval,oldval){
if(newval){
this.loadData();
this.search(false);
}
}
}
}
</script>
<style>
</style>
接下来修改Page1.vue,修改后的代码如下
<template>
<div>
<div style="border-radius:5px;">
<StudentForm></StudentForm>
</div>
<br/>
<div style="border:1px solid;margin-top:5px;background-color:#FFFFFF;border-radius:5px">
<StudentTable :searchflag="search"></StudentTable>
</div>
</div>
</template>
<script type="text/ecmascript-6">
import StudentForm from './StudentForm.vue'
import StudentTable from './StudentTable.vue'
export default {
data () {
return {
}
},
components: {
StudentForm: StudentForm,
StudentTable: StudentTable
},
computed: {
search(){
return this.$store.state.student.studentQueryFlag;
}
}
}
</script>
<style>
</style>
注意这里Page1.vue作为表格组件和表单组件的父组件,涉及到了与子组件传值的问题。可以看到
<div style="border:1px solid;margin-top:5px;background-color:#FFFFFF;border-radius:5px">
<StudentTable :searchflag="search"></StudentTable>
</div>
这段代码中,有一个
:searchflag="search"
这句话的意思是把子组件中的searchflag变量与当前组件中的search变量进行传值绑定。而当前组件中的search变量又是对于state中的搜索flag的计算属性,所以可以看出经过state和Page1两个“中间件”的传值,表单组件与表格组件进行了通信。
如果读者回看上文中的表格组件的代码,可以看到
props:['searchflag'],
这就是子组件从父组件中接收传值的方式。
至此我们这一篇文章的开发就结束了。看一下目录结构:
结束语
组件化开发除了可以做到解耦之外,在代码复用方面也有很大优势。比如,我们想在多个页面中都展示同一个表格,那么直接在其他页面中用import的方式引入表格组件即可。如果需要复用的组件较多,我们可以在components文件夹下单独创建一个common文件夹用于存放共用的组件。
注:由于篇幅原因,表格组件内只对id的搜索进行响应。