1. 程式人生 > 程式設計 >解決vue單頁面多個元件巢狀監聽瀏覽器視窗變化問題

解決vue單頁面多個元件巢狀監聽瀏覽器視窗變化問題

需求

最近公司有個大屏展示專案(如下圖)

解決vue單頁面多個元件巢狀監聽瀏覽器視窗變化問題

頁面的元素需要做響應式監聽,圖表需要跟著視窗響應變化

問題

每一個圖表都被我寫成了一個元件,然後就在每一個元件裡寫了一串程式碼,監聽瀏覽器變化

結果只有父元件的程式碼生效

mounted(){
 window.onresize = () => { //當視窗發生改變時觸發
 //
 };
}

原因

經簡單測試後發現,同一個路由頁面只能註冊一次瀏覽器視窗監聽事件,第二次註冊的會覆蓋第一次註冊

下邊程式碼即可測試

mounted(){
 window.onresize = () => { //當視窗發生改變時觸發 
 console.log(1)
 };
 window.onresize = () => { //當視窗發生改變時觸發 (會覆蓋上一個函式)
 console.log(2)
 };
}

父子巢狀元件同理,子元件生命週期執行在父元件之前,父元件函式會覆蓋子元件函式

解決方案

1、只在父頁面寫個監聽,但是通過元件傳值的方式傳給子元件,並且子元件用watch監聽傳值的變化,響應改變

2、假如是多層元件巢狀,用vuex可能會更省力

補充知識:vue/元件巢狀/無限巢狀/巢狀元件訊息傳遞/巢狀父子元件傳值

目前接到一個需求,是我之前從來沒有實踐過的,正好趁此機會做一個深度剖析,並記錄下這次的成長,並分享給大家。

需求文件

一 、(一個廠商編號和一個版本號)唯一決定一個配置

二 、 配置內容支援無限巢狀

三、配置資料格式示例(配置包括項和模組):

{
 "vendorId": "IL03#sub_01","version": "9.0.0","config": {
 "module-1": {
  "property-1": "value-1","property-2": "value-2","property-3": "value-3","module-1_1": {
  "property-1_1": "value-1_1","property-1_2": "value-1_2","property-1_3": "value-1_3"
  }
 },"module-2": {
  "property-4": "value-4","property-5": "value-5"
 }
 }
}

四、配置成果物如下:

解決vue單頁面多個元件巢狀監聽瀏覽器視窗變化問題

需求分解

一個簡單的巢狀元件:

<template>
 <div>
 <span>{{data.content}}<span>
 <div>
 <nested :data="data.child"></nested>
 <div>
 </div>
</template>
<script>
export default {
 name: 'nested',
 props: ['data']
}
</script>

我們給最外層的元件(根巢狀元件)繫結形如

{
 "content": "value","child": {
 "content": "value-1"
 "child": {
 "content": "value-1_1"
 ......
 }
 }
}

的資料結構,就可以看見效果了,是不是和我們前面需求的資料結構很像?

開始動工

step1:最外層列表展示

這裡作為靜態路由頁面展示即可(分頁、查詢、刪除功能在這裡做)

<!-- 這裡使用了EL-UI -->
<template>
 <!-- 應用配置入口 -->
 <div class="app-config-wrap">
  <!-- 增 -->
  <div class="app-config-add">
  <el-button type="primary" size="mini" @click="handleClickAdd">新增配置</el-button>
  </div>
  <!-- 查 -->
  <div class="app-config-search">
   <div class="label" @click="isShowFilter = !isShowFilter">
    <span class="text">查詢App配置</span>
    <span class="el-icon-caret-right" v-if="!isShowFilter"></span>
    <span class="el-icon-caret-bottom" v-else></span>
   </div>
   <div class="clear-all" @click="handleClearAll" v-if="isShowFilter" title="點選清空所有查詢條件">
    <span class="text">清空條件</span>
   </div>
   <div class="form-wrap" v-show="isShowFilter">
    <div class="by-vendorId">
     <el-input type="text" size="mini" placeholder="按廠商編號查詢" clearable v-model.trim="vendorId">
     </el-input>
    </div>
    <div class="by-version">
     <el-input type="text" size="mini" placeholder="按版本號查詢" clearable v-model.trim="version">
     </el-input>
    </div>
    <div class="search-button">
     <el-button type="primary" size="mini" @click="handleClickSearch">查 詢</el-button>
    </div>
   </div>
  </div>
  <div class="app-config-main" :style="tableHeight">
  <el-table size="mini" height="100%" :data="configList" stripe @row-click="handleClickRow"
   highlight-current-row style="width: 100%;">
   <el-table-column type="index" label="No." width="60"></el-table-column>
   <el-table-column prop="vendorId" label="廠商編號" :show-overflow-tooltip="true"></el-table-column>
   <el-table-column prop="version" label="版本號" :show-overflow-tooltip="true"></el-table-column>
   <el-table-column prop="operation" label="操作">
    <template slot-scope="scope">
    <!-- 刪 -->
    	<el-button type="danger" size="mini" @click="handleClickDelete(scope.row.id)">刪除配置</el-button>
    </template>
   </el-table-column>
  </el-table>
  </div>
  <el-pagination class="pagination" v-if="total" background small :current-page="pageNum"
  :page-sizes="[10,20,40,60]" :page-size="pageSize" layout="total,sizes,prev,pager,next,jumper"
  :total="parseInt(total)" @current-change="changePageNo" @size-change="changePageSize">
  </el-pagination>
 </div>
</template>

<script>
export default {
 name: 'appConfig',components: {},props: [],data () {
 return {
  isShowFilter: false,vendorId: '',version: '',pageNum: 1,pageSize: 20,total: 0,configList: [{ // 假資料
  id: 1,vendorId: 'cjm',version: '10.0.0'
  }]
 }
 },computed: {
 tableHeight () {
  return this.isShowFilter ? { height: 'calc(100% - 129px)' } : { height: 'calc(100% - 90px)' }
 }
 },methods: {
 handleClearAll () {
  this.vendorId = ''
  this.version = ''
 },handleClickSearch () { // 這裡傳送查詢請求
 },changePageNo (val) { // 這裡傳送分頁請求
  this.pageNum = val
 },changePageSize (val) { // 這裡傳送分頁請求
  this.pageSize = val
 },handleClickDelete (id) { // 這裡傳送刪除請求
 },handleClickAdd () { // 使用路由方式跳轉到配置頁面(增加配置和修改配置同一頁面即可)
  this.$router.push({
  name: 'configData',query: {}
  })
 },handleClickRow (row) { // 通過id讓配置頁面初始化時請求資料,在跳轉頁面中watch即可
  this.$router.push({
  name: 'configData',query: {
   id: row.id
  }
  })
 }
 }
}
</script>
// 樣式我就不貼了,節省篇幅

step2:動態路由頁準備

由於配置頁面展示是根據廠商編號和版本號動態改變的,所以這裡用到

this.$router.push({
 name: 'configData',query: {
 id: row.id
 }
})

來實現動態路由頁面,這裡也需要引入我們的根巢狀元件(巢狀入口)。

<template>
 <div class="config-data-warp">
 <div class="config-head">
  <span class="text">廠商編號:</span>
  <el-input class="config-input" type="text" size="mini" placeholder="廠商編號"
  clearable v-model.trim="vendorId">
  </el-input>
 </div>
 <div class="config-head">
  <span class="text">版本號:</span>
  <el-input class="config-input" type="text" size="mini" placeholder="版本號"
  clearable v-model.trim="version">
  </el-input>
 </div>
 <div class="config-main">
  <config-module :data="config" :root="true"
  :commit="commit" @commit="handlerCommit"></config-module>
 </div>
 <el-button class="config-commit-btn" type="primary"
  size="mini" @click="commit = true">確認提交</el-button>
 </div>
</template>
<script>
import configModule from './configModule'
export default {
 name: 'configData',components: {configModule},data () {
 return {
  id: this.$route.id,commit: false,config: {} // 這裡放點假資料
 }
 },mounted () { // 如果id存在,就去請求資料
 	if (id) { ... }
 },methods: {
 handlerCommit (data) { // 這裡是彙總資料的地方,記下來,等下提到了好找
  console.log(data)
  this.commit = false
 }
 }
}
</script>

值得注意的是,確認提交按鈕只是將commit變數置為true,而commit繫結在我們的巢狀元件中。

<div class="config-children" v-for="(value,index) in configJsonChildren" :key="index + 'child' + moduleName">
 <config-module :index="index" @change="changeChildName" @commit="handlerCommit"
 :commit="commit" :data="{key: value[0],child: value[1]}"></config-module>
</div>

這是巢狀元件的部分程式碼,我們可以看到commit被原封不動的傳遞給了子巢狀元件,也就是說,這裡的commit變數起到通知所有巢狀元件執行了提交動作,這也是父元件控制子元件元件的唯一方式——傳遞資料變化props(或者使用vuex也可以)。

這裡還有一點,就是定義什麼是巢狀部分,什麼是巢狀外部分,顯然,廠商編號和版本號不屬於巢狀部分。

還有傳入的root變數,是為了控制根巢狀元件的名稱不可修改,所以傳個true就可以了。

step3:巢狀元件的實現(重點)

這裡梳理一下巢狀元件需要提供的功能點:

1、能夠做到傳入資料的展示

2、能夠動態新增項和模組

3,能夠將修改了的資料傳遞出去

傳入資料的展示

我們再回過頭看看後臺傳給我們的資料格式:

{
 "vendorId": "IL03#sub_01","property-5": "value-5"
 }
 }
}

從中我們是否可以提煉出每個巢狀元件的資料格式?

module: {
 property-1: value-1,property-2: value-2,......
 module-1: { ...... },mpdule-2: { ...... },......
}

而且,我們需要這個物件的key和value是可以被雙向繫結的。

可是我們想一下,物件的key可以雙向繫結嗎?顯然不能!

這也就是說,原始傳入的資料結構並不能用,需要進行處理:

<template>
 <div class="config-module-warp">
 <div class="config-head">
  <!-- 根元件固定模組名,或者說不需要模組名 -->
  <span class="config-header-item" v-if="root">配置:</span>
  <div class="config-header-item" v-else>
  <el-input class="config-module-name" type="text" size="mini"
   placeholder="模組名" clearable v-model="moduleName" @change="changeModuleName"></el-input>
  </div>
  <el-button class="config-header-item" type="primary" size="mini"
  @click="handleClickAddProperty">新增項</el-button>
  <el-button class="config-header-item" type="primary" size="mini"
  @click="handleClickAddModule">新增模組</el-button>
  <el-button v-if="root" class="config-btn" type="danger" size="mini"
  @click="handleClickClear">清空配置</el-button>
  <el-button v-else class="config-btn" type="danger" size="mini"
  @click="handleClickDeleteModule">刪除模組</el-button>
 <div class="config-property" v-for="(value,index) in configJsonProperty"
  :key="index + 'property'">
  <el-input class="config-property-value" type="text" size="mini"
  placeholder="key" clearable v-model="value[0]"></el-input> :
  <el-input class="config-property-value" type="text" size="mini"
  placeholder="value" clearable v-model="value[1]"></el-input>
  <el-button class="config-header-item" type="danger" size="mini"
  @click="handleClickDeleteProperty(index)">刪除該項</el-button>
 </div>
 <div class="config-children" v-for="(value,index) in configJsonChildren"
  :key="index + 'child'">
  <config-module :index="index" @change="changeChildName" @commit="handlerCommit"
  :commit="commit" :data="{key: value[0],child: value[1]}"></config-module>
 </div>
 </div>
</template>
...
data () {
 return {
 moduleName: '',// 綁定當前子模組名
 configJsonProperty: [],// 這裡是子模組的property
 configJsonChildren: [],// 這裡是子模組下的子模組(孖模組^-^)
 ...
 }
}
...
mounted () {
 if (this.data && this.root) {
 // 由於根節點是沒有模組名的,資料的解析結構是{key: moduleName,child: moduleValue},參上。
 this.classify({child: this.data})
 } else if (this.data) {
 this.classify(this.data)
 }
}
// 或者將引用根元件的地方改成下面這樣也可以:
// <config-module :data="{child: config}" :root="true" :commit="commit"
// @commit="handlerCommit"></config-module>
// _____________________________________
// mounted () {
// if (this.data) {
//  this.classify(this.data)
// }
// }
...
classify (prop) {
 let data = prop.child
 this.moduleName = prop.key
 for (let key in data) {
 if (typeof data[key] === 'object') {
  this.configJsonChildren.push([ // 這裡將陣列轉化為可以雙向繫結的二維陣列
  key,data[key]
  ])
 } else {
  this.configJsonProperty.push([
  key,data[key]
  ])
 }
 }
}

實現動態增加

只需要新增空項就行了,但由於模組是由父元件傳入的,所以改變模組名也需要同步改變父元件的模組名,而這裡就用到了props中的index,代表父元件中的位置。

handleClickAddProperty () {
this.configJsonProperty.push([
 '',''
 ])
},handleClickAddModule () {
 this.configJsonChildren.push([
 '',{}
 ])
},

changeModuleName (value) {
 this.$emit('change',this.index,value)
},changeChildName (index,name) {
 this.$set(this.configJsonChildren[index],name)
},

孿生兄弟:動態刪除

其實,增加資料和刪除資料無外乎就是,本地資料本地改,外部資料同步改:

handleClickClear () {
 // 如果本身就是空,就無需操作,防止誤操作,畢竟我挺討厭彈窗的
 if (!this.configJsonProperty.length && !this.configJsonChildren.length) {
 return
 }
 // 敏感操作給個彈窗
 this.$confirm('確定清空所有配置?','警告',{
 confirmButtonText: '確定',cancelButtonText: '取消',type: 'warning'
 }).then(() => {
 // 這個是本地觸發的哦!
 this.configJsonProperty = []
 this.configJsonChildren = []
 })
},handleClickDeleteProperty (index) { // 本地資料本地改
 this.configJsonProperty.splice(index,1)
},handleClickDeleteModule () {
 // 外部資料傳出改,由於是刪除操作,外部銷燬了會直接導致本地銷燬,本地無需再銷燬
 // 和改模組名不一樣
 // 改模組名時,雖然外部資料改變觸發了本地更新,但由於是push操作,並不會改變本地資料
 this.$emit('delete',this.index)
},deleteModule (index) {
 // 與handleClickDeleteProperty方法比較,一定要分清哪個是子元件觸發,哪個是父元件觸發
 this.configJsonChildren.splice(index,

重中之重:提取這個樹結構中的資料

資料在各個子元件中儲存,怎麼把它們提取出來呢?

聰明的你肯定馬上想到了我之前所說的commit變數吧,它將這個動作分發到了各個子元件。

所以,只要每個子元件聽從命令,把資料層層上報,是不是就完成了呢?

這就好比是公司總經理想要開發一個軟體,他就只要告訴各個部門:

哎,你們軟體部負責做出軟體可行性方案;

你們市場部負責調查同類軟體和市場份額;

你們營銷部趕快出爐軟體推廣方案,等等。

然後部門總監給各專案經理髮小人物,然後專案經理再分解任務成需求給你。

最後做完了,流程就是:你 -》經理-》總監-》總經理。

在我們這份程式碼中,也是這樣子的:

第一步:你要知道任務來了:

watch: {
 commit (val) {
 if (val) {
  this.handleClickCommit() // 接到任務
 } else {
  this.commitData = {} // 這裡也標記一下
 }
 }
},

第一步:找到最底層的“你”,也就是找到這個樹元件的末梢節點,

它的標誌是:

if (!this.configJsonChildren.length) { ...... } // 他沒有子節點了

d收集它的“工作成果”:

let obj = {}
this.configJsonProperty.forEach(v => {
 if (v[0] && v[1]) {
 obj[v[0]] = v[1]
 } else {
 this.$emit('error') // 如果有項不完整,可以報錯
 }
})

你覺得上面程式碼有沒有小問題?給你五秒想一想。

1

2

3

4

5

有沒有這樣一種情況?我們一不注意寫了兩個同樣鍵名的項,不管是寫到了錯的模組裡面還是怎樣。

那麼在上面的程式碼中,就會使得新值覆蓋舊值,就有可能導致嚴重的事故!!!

所以我們改成:

handleClickCommit () {
 if (!this.configJsonChildren.length) {
 if (!this.moduleName && !this.root) {
  this.$emit('error')
  return
 }
 let obj = {}
 for (let v of this.configJsonProperty) {
  if (v[0] && v[1]) {
  if (obj.hasOwnProperty(v[0])) {
   this.$emit('error') // 啊,一不小心走神了
   return
  }
  obj[v[0]] = v[1]
  } else {
  this.$emit('error')
  // 這裡不需要return是因為不會造成嚴重後果,當然也可以加上
  // 主要是我用這個功能時會一口氣新增好多項,也不一定全填滿,省得一個個刪。
  }
 }
 this.$emit('commit',{ // 把資料給經理!!!這個殺千刀的,天天催!
  key: this.moduleName,// 身份狗牌
  value: obj
 })
 }
}

啊,工作終於提交了,再也不擔心了,接下來的事就交給經理去做吧!

經理:我手下管著這麼多人,不可能來一個我上交一個吧?那就等他們全部上交了,我再整個打包上交吧。

首先第一步,我需要一個箱子來存他們的成果:

data () {
 return {
 moduleName: '',configJsonProperty: [],configJsonChildren: [],commitData: {} // 存放成果的箱子
 }
}

接下來就等他們上交了:

handlerCommit (data) {
 if (!this.moduleName && !this.root) { // 領導也要有名字,但總經理只有一個
 this.$emit('error')
 return
 }
 this.commitData[data.key] = data.value // 先按人頭收下成果
 for (let item of this.configJsonChildren) {
 if (!this.commitData.hasOwnProperty(item[0])) return // 如果沒收齊,繼續等待
 }
 // 歐耶,收齊了
 let obj = {}
 for (let v of this.configJsonProperty) { // 這個for迴圈可以封成一個函式的,畢竟寫了兩次
 if (v[0] && v[1]) {
  if (obj.hasOwnProperty(v[0])) {
  this.$emit('error')
  return
  }
  obj[v[0]] = v[1]
 } else {
  this.$emit('error')
 }
 }
 this.$emit('commit',{
 key: this.moduleName,value: Object.assign(obj,this.commitData) // 領導自己的成果加上員工的成果
 })
}

還記得上面我讓你記下的地方嗎?

handlerCommit (data) {
 console.log(data) // 彙總資料,在這裡可以傳送給後臺了
 this.commit = false // 任務完成標誌
}
watch: {
 commit (val) {
 if (val) {
  this.handleClickCommit()
 } else {
  this.commitData = {} // 初始化子元件
 }
 }
},

到這裡,巢狀元件也大致完工了,貼全程式碼:

<template>
 <div class="config-module-warp">
 <div class="config-head">
  <span class="config-btn" v-if="root">配置:</span>
  <div class="config-btn" v-else>
  <el-input class="config-module-name" type="text" size="mini" placeholder="模組名"
   clearable v-model="moduleName" @change="changeModuleName"></el-input>
  </div>
  <el-button class="config-btn" type="primary" size="mini"
  @click="handleClickAddProperty">新增項</el-button>
  <el-button class="config-btn" type="primary" size="mini"
  @click="handleClickAddModule">新增模組</el-button>
  <el-button v-if="root" class="config-btn" type="danger" size="mini"
  @click="handleClickClear">清空配置</el-button>
  <el-button v-else class="config-btn" type="danger" size="mini"
  @click="handleClickDeleteModule">刪除模組</el-button>
 </div>
 <div class="config-property" v-for="(value,index) in configJsonProperty" :key="index + 'property'">
  <el-input class="config-property-value" type="text" size="mini"
  placeholder="key" clearable v-model="value[0]"></el-input> :
  <el-input class="config-property-value" type="text" size="mini"
  placeholder="value" clearable v-model="value[1]"></el-input>
  <el-button class="config-btn" type="danger" size="mini"
  @click="handleClickDeleteProperty(index)">刪除該項</el-button>
 </div>
 <div class="config-children" v-for="(value,index) in configJsonChildren" :key="index + 'child'">
  <config-module :index="index" @change="changeChildName" @delete="deleteModule"
  @commit="handlerCommit" :commit="commit" :data="{key: value[0],child: value[1]}"></config-module>
 </div>
 </div>
</template>
<script>
export default {
 name: 'configModule',props: ['data','root','commit','index'],data () {
 return {
  moduleName: '',commitData: {},error: false
 }
 },watch: {
 commit (val) {
  if (val) {
  this.handleClickCommit()
  } else {
  this.commitData = {}
  this.error = false
  }
 }
 },computed: {
 },mounted () {
 if (this.data) {
  this.classify(this.data)
 }
 },methods: {
 classify (prop) {
  let data = prop.child
  this.moduleName = prop.key
  for (let key in data) {
  if (typeof data[key] === 'object') {
   this.configJsonChildren.push([
   key,data[key]
   ])
  } else {
   this.configJsonProperty.push([
   key,data[key]
   ])
  }
  }
 },handleClickAddProperty () {
  this.configJsonProperty.push([
  '',''
  ])
 },handleClickAddModule () {
  this.configJsonChildren.push([
  '',{}
  ])
 },handleClickClear () {
  if (!this.configJsonProperty.length && !this.configJsonChildren.length) {
  return
  }
  this.$confirm('確定清空所有配置?',{
  confirmButtonText: '確定',type: 'warning'
  }).then(() => {
  this.configJsonProperty = []
  this.configJsonChildren = []
  })
 },handleClickDeleteProperty (index) {
  this.configJsonProperty.splice(index,1)
 },handleClickDeleteModule () {
  this.$emit('delete',this.index)
 },deleteModule (index) {
  this.configJsonChildren.splice(index,changeModuleName (value) {
  this.$emit('change',value)
 },name) {
  this.$set(this.configJsonChildren[index],name)
 },handleClickCommit () {
  if (!this.configJsonChildren.length) {
  if (!this.moduleName && !this.root) {
   this.$emit('error')
   return
  }
  let obj = {}
  for (let v of this.configJsonProperty) {
   if (v[0] && v[1]) {
   if (obj.hasOwnProperty(v[0])) {
    this.$emit('error')
    return
   }
   obj[v[0]] = v[1]
   } else {
   this.$emit('error')
   }
  }
  this.$emit('commit',{
   key: this.moduleName,value: obj
  })
  }
 },handlerCommit (data) {
  if (!this.moduleName && !this.root) {
  this.$emit('error')
  return
  }
  this.commitData[data.key] = data.value
  for (let item of this.configJsonChildren) {
  if (!this.commitData.hasOwnProperty(item[0])) return
  }
  let obj = {}
  for (let v of this.configJsonProperty) {
  if (v[0] && v[1]) {
   if (obj.hasOwnProperty(v[0])) {
   this.$emit('error')
   return
   }
   obj[v[0]] = v[1]
  } else {
   this.$emit('error')
  }
  }
  this.$emit('commit',{
  key: this.moduleName,this.commitData)
  })
 }
 }
}
</script>

總結

其實聰明的人根本就不需要我總結嘛,程式碼是最好的語言

所以這裡我提出一些我的不足和沒做完的部分,不過都是細枝末節啦:

第一個是錯誤的處理,我這邊沒有加上

第二個是模組應該有摺疊功能,不然配置多看著就眼花繚亂,

不過v-show的使用大家應該也是登峰造極了。

然後,大家有什麼意見和建議都可以在下方反饋。

感謝大家看完這一篇長文,麼麼噠~希望能給大家一個參考,也希望大家多多支援我們