本文歸檔於 易浮的小窩

Chrome 瀏覽器作為目前最受歡迎的瀏覽器之一,實在是又快又安全(又耗電又費記憶體),然而我在開發我們的店鋪系統的時候,無意間發現有一個小 bug(其他瀏覽器均不會出現這種情況)

# 起源

這段時間在開發我們的店鋪系統,這中間一定會有的內容是登入註冊之類模組。在這之前,由於 UI 的要求,我們需要在 PC 端自己實現一個完整的輸入框元件,這個元件具備一個特別的動畫效果:自己實現來一個縮小移動的

placeholder

提示。

你可以檢視 友好速搭的官方示例店鋪 藤煤竹,我們希望的的效果如下:

我發現了 Chrome 的一個 Bug

我發現了 Chrome 的一個 Bug

那麼為什麼說出現來一個 “BUG” 呢?首先來複現一下:

step1: 登入網站,一般 Chrome 就會提示是否記住密碼,選擇記住;

step2: 退出該賬戶,然後重新整理頁面,一般 Chrome 就會自動幫你填充表單,於是有很大機率出現下圖:

我發現了 Chrome 的一個 Bug

我發現了 Chrome 的一個 Bug

這就是問題所在來,我們希望的效果是跟使用者名稱那樣子,而不是現在輸入提示和自動填充的密碼小圓點重疊在一起!這對我造成來很大的困擾。

當然,我也在懷疑自己的程式碼的問題,那麼就先來研究一下這個輸入框的觸發機制到底是怎樣的。

# 糟糕的挖坑歷程

所有類似的輸入框我都封裝了一個叫

YhsdInput

的類,裡面有一些共有的方法和屬性,透過例項化物件得到針對特殊輸入框的行為,拋開這些,我們單就針對密碼框談一談核心邏輯:

$

function

()

{

var

$input

=

$

‘#password’

$input

one

‘input propertychange’

function

()

{

// 為方便展示,重新編排了實現,換成假如加上一個 class 就會觸發特效

$input

val

()

&&

$input

addClass

‘active’

})

})

邏輯可以簡單認為這樣:頁面載入後,給輸入框繫結一次性事件監聽,當輸入框出現了內容,就會給其加上一個

active

的 Class 表示當前輸入框已經啟用/有內容(這裡不考慮復原問題如刪除內容並移除焦點),因為是針對瀏覽器的自動填充的,所以這個邏輯理論上是正常的,因為自動填充表單會觸發

propertychange

才是正常的邏輯。

經過我們的測試,瀏覽器們表現在意料之中,唯獨 Chrome 例外

那麼這說明什麼問題呢?我得到第一次結論是,Chrome 的自動填充並不會觸發輸入框的

propertychange

事件,但密碼又確實被填充進去了,於是就會出現前面圖片中詭異的

placeholder

和密碼圓點共存的現象

## 第一次嘗試

為了解決這個問題,我們只好在程式碼中針對 Chrome 進行 hack,加上了以下的修復程式碼:

if

window

navigator

userAgent

indexOf

‘Chrome’

>

-

1

{

setTimeout

function

()

{

$input

val

()

&&

$input

addClass

‘active’

},

300

}

我認為這樣可行的理由是:既然不會在變動的時候觸發

propertychange

事件,那麼我就手動延遲監聽輸入框,如果被密碼填充了,那總該可以取到值。至於

300ms

,先隨便填一個放上去。

結果……讓人失望,還是沒有預想中的效果,我還特意把時間改為長達 1 秒,然並卵。

## 第二次嘗試

經過第一次嘗試,我發現似乎 Chrome 自動填充的密碼有兩個特性:一是不會觸發輸入框自帶的事件,二是用指令碼取不到密碼的存在。

於是開啟劫持工具,加入斷點,有一些新發現。

Chrome 並不會在一進來就填充表單,甚至可以說是填充表單是已經執行完指令碼後面的時,因此

DOM ready

的時候,指令碼嗅探不到值。但是為什麼我特意延遲指令碼嗅探也還是拿不到值呢?

焦灼地倒騰了半天,使用

debugger

調試出現了奇怪的事情:透過程式碼

$('input').val()

要麼一直拿不到值(顯示為空字串,但明明介面是存在密碼的,檢視 DOM 修改

type=

text

也可以讓密碼明文顯示),要麼是一直可以拿到值……

## 發現秘密

一直到第二天,快要崩潰的時候,我發現如果我與頁面

互動過

,比如隨便點選了一下頁面,反正就是頁面有獲得焦點(在控制檯輸入程式碼並沒有讓頁面獲得焦點),那麼就可以透過程式碼

$('input').val()

拿到密碼!

最最最奇怪的是,當你第一次與頁面互動時,居然觸發了之前死活不觸發的

propertychange

事件(這裡指最開始的

$input.one(handle)

),也就是跟你期望一開始就發生的一樣,只是延遲到了這個點。

## 勉強的解決方式

有了新發現就好辦了,我思考了幾十分鐘,認為這個是 Chrome 的一個安全機制,那麼在使用者開始與頁面互動之前,用指令碼是無法知道密碼到底有沒有被自動填充的,可以放棄前面的思路了。但是我也注意到,只有密碼輸入框會被這樣限制,使用者名稱那一欄是一切如期待進行中的。

這裡我想到了一個曲線救國的方案:

if

window

navigator

userAgent

indexOf

‘Chrome’

>

-

1

{

setTimeout

function

()

{

// 找到使用者名稱

var

usr

=

$input

parents

()。

find

‘。input[type=text]’

usr

val

()

&&

$input

addClass

‘active’

},

300

}

很精妙吧?我們已經沒法一開始就拿到密碼,但是自動錶單填充有一個地方被我注意到了:那就是自動填充總是成對出現的,在這個登入場景中,如果使用者名稱沒有被自動填充,密碼肯定也沒有。因此只需要判斷使用者名稱有沒有被自動填充,就可以判斷密碼框的狀態了!

實際測試,一切如我所料地工作。然而好景不長,同事在虛擬機器中測試時並沒有按預料的觸發,於是我又得出一個結論:在低端機器中,自動錶單填充受限於機器效能,會不止 300ms,而這時,程式碼已經執行完了。

於是最終我上線的版本是這樣的:

if

window

navigator

userAgent

indexOf

‘Chrome’

>

-

1

{

var

times

=

0

// 給定一個計數器,最多重複20次,超過一秒我就認為沒有填充

function

loop

()

{

times

++

var

usr

=

$input

parents

()。

find

‘。input[type=text]’

if

usr

val

())

{

$input

addClass

‘active’

}

else

if

times

<

20

{

setTimeout

function

()

{

loop

()

},

50

}

})()

}

# 追蹤

總算專案中實現了設計師的要求,但這究竟是 Chrome 的一個 bug,還是我的實現有問題呢?抱著這個問題,我們先搜尋一下網路,經過艱難的檢索(事實上貌似很少有人會遇到這個奇怪的需求),我居然直接就在

chromium

的 bug 反饋專區找到了這個 issue:

Issue 378419

嗯,是14年中的問題,基本沒人關注,甚至剛報上去的時候,版主還認為是原生的

placeholder

中的 bug(笑)

issues 中明確了只有 Chrome 會出現自動填充密碼的

input.value === "" // true

,裡面果然出現了關鍵資訊

It seems to me that for some reason the autocompletion GUI state (visible password dots) is inconsistent with the DOM state (input。value == “”) until an interaction with the user occurs。

The problem here is that while password。value reports empty string, the control is not actually empty

然而 Chrome 的開發者認為這確實是正常的邏輯,關閉了這個 issue,這意味著直到現在,Chrome 也還存在著這個特性。

So,我還能說啥呢?因為這既可以認為是一個安全特性,也可以認為是一個體驗的缺陷。不過我想,如果你也遇到了同樣的問題,那麼,我這一趟懟 bug 沒有白走,你應該知道怎麼 hack 了吧?但是我還是不明白為什麼只有 Chrome 有這種實現,為什麼不修?