前面讨论了环境的搭建和导航页面以及路由的配置,今天我们讨论下如何开发一个拥有表单和表格功能的页面。先上开发完的效果图: 

可以看出页面非常的简单,其中上半部分是表单搜索和查询,下半部分是用于展示数据的表格。如果按照传统的开发思路,其实非常简单,只要用两个div,第一个div放置表单,第二个div放置表格即可。但是,我们今天要介绍的,是这个页面的另一种写法,也是vue作为一个优秀的前端框架的核心功能,也就是组件化的写法。

什么是组件化?

某搜索引擎告诉我们,组件化是指解耦复杂系统时将多个功能模块拆分、重组的过程,有多种属性、状态反映其内部特性。以我们这次要编写的页面为例,组件化就是要将这个页面里面的表格和表单分开成两个不同的组件,每个组件有它自己的属性和状态,既互不干扰又可以互相通信。

为什么要组件化?

从上文中的定义我们也可以看出,组件化的主要目的是解耦。当然,还有其他的目的,比如组件复用,按需引入等。具体的细节我们可以先往下看。

开始之前

为了规范工程的层级,我们把原先与Navi文件夹同级的Page1,Page2和Page3.vue文件删掉,重新建立三个名为Page1,Page2和Page3的文件夹,并分别在三个新建立的文件夹中建立Page1.vue,Page2.vue和Page3.vue。当然,结束之后同样要修改vue-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作为表单和表格组件的父组件。

  1. 在页面中表单内输入数据
  2. 表单组件通过调用student模块的action->mutation,将表单内的数据同步到state中
  3. 点击搜索按钮时,表单组件通过action->mutation,将state中的搜索flag(初始化为false)置于true
  4. Page1.vue中设置一个局部变量,将这个局部变量computed为state中的搜索flag
  5. 将步骤4中的局部变量通过父组件->子组件方式传值至表格组件中
  6. 表格组件中对这个接收到的值进行watch,当且仅当这个值由false变为true时,以state中的表单数据为搜索条件,向服务器发送请求,获取数据并渲染
  7. 最后一步千万不要忘了,表格组件还要通过调用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文件夹用于存放共用的组件。


想要整理更多的碎片知识,扫码关注下面的公众号,让我们在哪里接着唠!

Logo

前往低代码交流专区

更多推荐