簡介
先來看最終成果:
操作邏輯為:
- 點選
...
彈出context-menu
; - 點選非
context-menu
區域,隱藏context-menu
; - 點選
context-menu
中的任何一個選項,隱藏context-menu
;
思考
專案是基於 vux
做的,本想著偷懶直接在 vux
庫翻元件用,但看了一圈下來,居然這麼通用的元件在 vuex
中沒有!接著又去翻開源的解決方案,看了幾個庫還算 ok,但此時前端小哥來了,說實現這個選單不需要用到這麼重的東西,直接寫就行了。
當時我的腦海中在思考了把 context-menu
封裝成一個 component
,透過資料配置的方式動態擴充選單選項。但沒想到前端小哥直接給我幹了回來,沒必要進行封裝,這個元件對頁面依賴性太強,就算封裝完了下次也不一定能直接用,PM 的思路十分清奇。
所以,最後的做法就直接硬上了。
實現
調整操作邏輯
該頁面是一個通俗意義上的列表展示頁,使用了 vux
的 swipeout
表單元件,給使用者提供了側滑操作,需要把原先寫好的側滑功能刪除。
調整 UI
在調整 UI 的過程中我感到了 CSS 滿滿的惡意,當然說是這麼說,但實際上還是因為太久沒有用而導致的不夠熟悉。非常費勁的終於調整了好了新 UI,此時已經過去了整整一天了,非常懷念 autoLayout
。
context-mune
在正式開始寫之前,上文已經說了我一直在翻開源庫,主要是不懂得如何下手去,因為距離上一次寫 vue
已經過去了快兩個月了,而且也沒搞清楚如何寫一個元件,所以中間也有一段時間浪費在了這個上。最後的解決思路讓我感到意外:
<div class="more-menu-wrapper">
<ul v-show="item.showOption">
<li>更換分類</li>
<li>向上移動</li>
<li>移至頂部</li>
<li>取消收藏</li>
</ul>
</div>
沒想到使用無序列表就可以完成了~在 iOS 中,我會在 UITableView
和 UIStackView
中糾結。當然只有這樣是不行的,當又調整了 UI 後,發現 ...
和 context-menu
“融合”在一起了,沒有設計圖中的“懸浮”效果,最後的解決方法是:
.more-wrapper {
/* ... */
position: absolute;
.more-menu-wrapper {
position: relative;
/* ... */
}
}
當繼續調整 CSS 時又發現 context-menu
的會被其父元件擋住,context-menu
的顯示範圍會限制與其父元件的顯示高度,最後得知是 overflow
這個屬性在最底層的父元件中設定了 overflow: hidden;
,刪除掉,使其為預設的 visible
即可顯示為 context-menu
高度溢位的效果。
事件繫結
UI 都調整完後開始繫結事件。因為只是改造 UI,並沒有涉及到多少的新邏輯,所以很快的就寫出了以下程式碼:
<ul v-show="item.showOption">
<li @click="moveItem(item)">更換分類</li>
<li @click="moveUp(item)">向上移動</li>
<li @click="setTop(item)">移至頂部</li>
<li @click="deleteItem(item)">取消收藏</li>
</ul>
context-menu
的顯示依賴 v-show
,當頁面首次拉取到網路資料時,data
中對每個 listData
的 item
新增了 context-menu
顯示隱藏的初始化標誌位 item.showOption = false
,且在這四個入口方法中都控制了 item.showOption
的改變:
//...
moveUp(item) {
item.showOption = false;
// ...
}
//...
重新整理頁面,很愉快的看到了 context-menu
的顯示,但在點選選單選項時沒有任何反應!一開始以為是標識位的問題,但看來看去沒有任何問題哇~本來想去找前端小哥看一眼,但一直不在工位上,最後問了下同組的前端實習生,他認為是 item.showOption
欄位在資料更新時沒有加上,導致後續直接讀取時不存在。
但我其實一直納悶如果 item.showOption
欄位資料不存在的話,那第一次的頁面渲染實際上是有錯誤的。我們兩個人看了一會也沒發現具體是哪有問題,最後只能四處尋找前端小哥,沒想到他已經被封閉起來做商業化了,我說怎麼四處找不到人。
前端小哥在檔案中加上了 debugger
進行除錯,發現進入到 moveUp
等一類事件時雖然 item.showOption
被修改成功了,一旦出去事件週期外,又被改回去了。
最後發現,問題出在被冒泡到了父元件中,呼叫了 ...
所繫結的 onMore
事件中,而在 onMore
事件中 item.showOption = true
,所以實際上是執行了 context-menu
和 ...
的兩者所繫結的事件。解決的方法是:
<ul v-show="item.showOption">
<li @click.stop="moveItem(item)">更換分類</li>
<li @click.stop="moveUp(item)">向上移動</li>
<li @click.stop="setTop(item)">移至頂部</li>
<li @click.stop="deleteItem(item)">取消收藏</li>
</ul>
使用 @click.stop
來阻止冒泡事件。解決完問題後,前端小哥還好奇我做 iOS 怎麼會不知道冒泡事件的問題,但實際上在 iOS 中跟前端的思路完全是反過來的。iOS 的事件響應鏈是逐級傳遞到子元件中,也就是向下傳遞,而不是像前端中的向上傳遞。所以在遇到這個問題時也就完全沒有往冒泡的方面去思考。
觸控其它區域消失 context-menu
在 iOS 中,我會直接封裝出一個帶有 UIWindow
的元件。與 context-menu
有關的所有操作與主 window
沒有任何關係,更別說事件穿透了。所以最終我的做法是多加了一個遮罩層,顯示和隱藏的時機與 context-menu
的時機保持一致。
最後在我拿著最終的成果去找前端小哥複查時,他對這個做法不滿意,還是覺得要使用 outside-click
的做法。也就是使用 js 中的事件代理,透過 e.targe
去判斷。最後找到了可以使用 v-outside-click
進行。v-outside-click
有兩種引入的方式,為了簡潔,我選擇了“指令”的方式引入。
在使用 v-outside-click
這個庫的過程中遇到了一個比較大的問題。v-outside-click
此庫給我的感覺是用於單個元件,而不適用於多個元件。列表中的每一個 cell
都需要帶上一個單獨的 context-menu
,如果給每一個 context-menu
都綁上一個單獨的 outside-click
事件,一旦使用者的觸控範圍不在 context-menu
中,則檢視上的所有 context-menu
都會響應這個 outside-click
事件,列表資料一旦多起來,事件響應次數將線性增長。
這個問題跟前端小哥說過後,他說問題不大,那就這樣吧~接下來的問題就到了怎麼在 outside-click
事件中標識出是哪個 context-menu
需要隱藏呢?剛開始就按照了以往的套路,直接使用瞭如下所示的方式:
<div v-click-outside="onClickOutside(item)">
<!-- ... -->
</div>
然後開心看到了報錯 Binding value must be a function or an object
。提示需要傳入一個方法?!翻了原始碼後發現了這麼一段:
function processDirectiveArguments(bindingValue) {
const isFunction = typeof bindingValue === 'function'
if (!isFunction && typeof bindingValue !== 'object') {
throw new Error('v-click-outside: Binding value must be a function or an object')
}
// ...
}
回過頭去看之前寫的程式碼,沒有問題啊!思來想去還是沒弄明白,又去找了前端小哥請求幫忙,經過了一番折騰了,他的結論是這個庫應該是有問題的。最後採取的解決方法是:
<div v-click-outside="onClickOutside">
<p>…</p>
<!-- 重點 -->
<div :id="item.metricId" v-show="item.showOption">
<ul>
<li>更換分類</li>
<!-- ... -->
</ul>
</div>
</div>
onClickOutside (event, el) {
let queryInstance = el.querySelector('.more-menu-wrapper')
if (queryInstance) {
let metricId = el.querySelector('.more-menu-wrapper').id;
if (metricId != "") {
this.listData.some((item) => {
if (item.metricId == metricId) {
item.showOption = false;
return true;
}
});
}
}
}
透過設定 context-menu
的 id
作為標識,然後在 v-outside-click
的指令方法中獲取 id
,透過這個 id
去資料來源中找到對應的 item
,從而設定 item.showOption = false
來隱藏 context-menu
。
總結
這算是轉大前端完成的第一個功能吧,因為不熟悉導致中間出現了一些好玩的事情。客戶端和前端的開發流程說大也不大,但要是說沒有是絕對不可能的。在一些小的問題上,沒有踩過坑或者沒有大佬帶一帶,真的會爬不起來或者就棄坑了,說到底其實還是需要多加學習啊!
原文地址:PJ 的 iOS 開發之路
本作品採用《CC 協議》,轉載必須註明作者和本文連結