平時(shí)在項(xiàng)目中很少用到Vue的繼承特性,通常是封裝單文件組件,然后在其他組件中引入。繼承是面向?qū)ο蟪绦蛟O(shè)計(jì)的一個(gè)核心概念之一,通過(guò)它可以實(shí)現(xiàn)子類(lèi)對(duì)父類(lèi)的數(shù)據(jù)和功能的復(fù)用,同時(shí)還可以選擇性地覆寫(xiě)父類(lèi)的屬性和方法。那么,前端組件開(kāi)發(fā)如何利用這個(gè)特性呢?
最近剛好碰到一個(gè)應(yīng)用場(chǎng)景,某個(gè)業(yè)務(wù)模塊的各個(gè)頁(yè)面在結(jié)構(gòu)上幾乎完全一致,區(qū)別只在于里面的數(shù)據(jù)。首先想到的實(shí)現(xiàn)方式是封裝成一個(gè)組件,然后在各個(gè)頁(yè)面中引入,通過(guò)props傳遞不同的數(shù)據(jù)來(lái)區(qū)分不同行為。但這種方式需要在組件里做各種判斷,每多一個(gè)頁(yè)面類(lèi)型就需要增加一個(gè)條件分支,這樣就違背了面向?qū)ο笤O(shè)計(jì)里的“開(kāi)發(fā)-封閉”原則,不利于以后的擴(kuò)展。

該業(yè)務(wù)模塊是要展示多個(gè)統(tǒng)計(jì)指標(biāo)的趨勢(shì),頁(yè)面包含一個(gè)走勢(shì)圖和報(bào)表,以及一些條件篩選控件。每個(gè)指標(biāo)的頁(yè)面幾乎都是一樣的。使用繼承,剛好可以解決這個(gè)問(wèn)題。定義一個(gè)基類(lèi)頁(yè)面,包含頁(yè)面模板和公共屬性和方法,各數(shù)據(jù)指標(biāo)頁(yè)面作為子類(lèi)繼承該基類(lèi),這樣就擁有了同樣的頁(yè)面模板和事件綁定,只需要在子類(lèi)覆寫(xiě)事件處理方法就能實(shí)現(xiàn)不同統(tǒng)計(jì)指標(biāo)展示不同數(shù)據(jù)。
Vue 支持組件的繼承,在單文件組件里指定extends即可:
<!-- 某統(tǒng)計(jì)指標(biāo)報(bào)表 -->
<script>
import ReportBase from './ReportBase.vue';
export default {
extends: ReportBase,
data() {
return {
title: '數(shù)據(jù)指標(biāo)標(biāo)題',
tableTitle: '數(shù)據(jù)指標(biāo)表格標(biāo)題',
chartTitle: '數(shù)據(jù)指標(biāo)趨勢(shì)圖標(biāo)題',
columns: [
{
label: '日期',
prop: 'date',
},
{
label: '總數(shù)(臺(tái))',
prop: 'total',
},
{
label: '故障數(shù)量(臺(tái))',
prop: 'failure',
},
{
label: '占比(%)',
prop: 'ratio',
},
],
};
},
methods: {
// 覆寫(xiě)基類(lèi)的事件處理方法,根據(jù)該統(tǒng)計(jì)指標(biāo)請(qǐng)求不同的業(yè)務(wù)接口
onFactoryChange() {},
onChartTimeChange() {},
onTableDateChange() {},
},
};
</script>
<style lang="scss" scoped>
</style>
基類(lèi)組件代碼大致如下:
<!-- ReportBase.vue -->
<template>
<div class="report-page router-page widget">
<div class="widget-header">
<div class="widget-header-title">{{title}}</div>
</div>
<div class="widget-body">
<div class="filter flexbox">
<span class="filter-name">{{ $t('pool.farm') }}</span>
<el-radio-group class="plain" size="mini" v-model="factoryId" @change="onFactoryChange">
<el-radio-button
v-for="(item, index) in factoryList"
:label="item.id"
:key="index"
>{{ item.name }}</el-radio-button>
</el-radio-group>
</div>
<section class="chart-area section">
<div class="flexbox align-center">
<div class="chart-title">{{chartTitle}}</div>
<el-radio-group size="mini" v-model="timespan" @change="onChartTimeChange">
<el-radio-button label="week">周</el-radio-button>
<el-radio-button label="month">月</el-radio-button>
<el-radio-button label="year">年</el-radio-button>
</el-radio-group>
</div>
</section>
<section class="grid-area section">
<div class="flexbox space-bt">
<div class="grid-title">{{tableTitle}}</div>
<el-date-picker
v-model="month"
type="month"
placeholder="選擇月"
@change="onTableDateChange"
></el-date-picker>
</div>
<el-table :data="tableData" style="width: 100%">
<el-table-column v-bind="item" v-for="(item, index) in columns" :key="index"></el-table-column>
</el-table>
</section>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
data() {
return {
title: '',
factoryId: 0,
month: '',
tableTitle: '',
columns: [],
tableData: [],
timespan: 'week',
};
},
watch: {
factoryList(value) {
if (value.length > 0) {
this.factoryId = value[0].id;
this.onFactoryChange(this.factoryId);
}
},
},
computed: {
...mapGetters(['factoryList']),
},
methods: {
onFactoryChange() {},
onChartTimeChange() {},
onTableDateChange() {},
},
created() {
if (this.factoryList.length > 0) {
this.factoryId = this.factoryList[0].id;
this.onFactoryChange(this.factoryId);
}
},
};
</script>
<style lang="scss">
這樣,后續(xù)如果有更多的統(tǒng)計(jì)指標(biāo)頁(yè)面,只需要繼承和覆寫(xiě)方法就行,而基類(lèi)幾乎不用改動(dòng)。當(dāng)然,Vue組件的這種繼承方式也不是完美的,比如 HTML模板要么完全繼承,要么完全重寫(xiě),不能按需繼承某個(gè)部分。如果子類(lèi)在結(jié)構(gòu)上跟基類(lèi)有所差異,還是需要在基類(lèi)中做條件判斷。如果模板差異太大,可以重新定義子類(lèi)自己的template,至少還可以重用一部分業(yè)務(wù)邏輯代碼。像本文提到的應(yīng)用場(chǎng)景,就非常適合用繼承來(lái)實(shí)現(xiàn)。
各位看官如果有更好的思路,歡迎在評(píng)論中一起探討!