解決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" } } }
四、配置成果物如下:
需求分解
一個簡單的巢狀元件:
<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的使用大家應該也是登峰造極了。
然後,大家有什麼意見和建議都可以在下方反饋。
感謝大家看完這一篇長文,麼麼噠~希望能給大家一個參考,也希望大家多多支援我們