1. 程式人生 > >Vue+ElementUI從零開始搭建自己的網站(三、元件間的通訊)

Vue+ElementUI從零開始搭建自己的網站(三、元件間的通訊)

前面討論了環境的搭建和導航頁面以及路由的配置,今天我們討論下如何開發一個擁有表單和表格功能的頁面。先上開發完的效果圖: 

可以看出頁面非常的簡單,其中上半部分是表單搜尋和查詢,下半部分是用於展示資料的表格。如果按照傳統的開發思路,其實非常簡單,只要用兩個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資料夾用於存放共用的元件。