前面討論了環境的搭建和導航頁面以及路由的配置,今天我們討論下如何開發一個擁有表單和表格功能的頁面。先上開發完的效果圖:
可以看出頁面非常的簡單,其中上半部分是表單搜索和查詢,下半部分是用於展示數據的表格。如果按照傳統的開發思路,其實非常簡單,只要用兩個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文件夾用於存放共用的組件。