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作為表單和表格元件的父元件。
- 在頁面中表單內輸入資料
- 表單元件通過呼叫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資料夾用於存放共用的元件。