問題描述
瀏覽器對於type="password"
的輸入框會自動填充密碼,但有時出於安全或者其他原因,我們不希望瀏覽器記住並自動填充密碼。透過網上查到的一些解決方案,可以總結出以下幾種解決方案(主要用edge瀏覽器進行測試):
- 透過
autocomplete="off"
/autocomplete="new-password"
來關閉瀏覽器自動填充密碼的功能, 但某些對於瀏覽器像edge,firfox等,這種方法並不起作用 - 透過
type="text"
來解決,當focus時,透過js將type="text"
改為type="password"
。
<input type="text" onfocus="this.type='password'">
但同樣對某些瀏覽器不起作用,如edge,在點選輸入框時,仍會自動彈出填充密碼的提示框。
- 某些瀏覽器可能只會識別第一個
type="password"
的輸入框,所以可以在前面新增一些隱藏的type="password"
的輸入框,來解決這個問題。
<form style="display:none">
<input type="password">
</form>
<input type="password" style="display:none">
<input type="password">
但同樣並不是總是有效,拿edge測試時即使前幾個密碼輸入框沒有隱藏,最後一個輸入框也會自動填充密碼,如圖:
- 透過
readonly
屬性來解決,初始化時將readonly
設定為true
,透過setTimeout
來延時設定readonly
為false
。
<input id="passwordInput" type="password" readonly>
setTimeout(() => {
document.getElementById('passwordInput').removeAttribute('readonly')
}, 100)
但同樣並非總是有效,拿edge測試時,雖然點選輸入框時並沒有彈出填充密碼的提示框,但是在輸入框中輸入密碼然後退格到輸入框為空時,又會重新彈出填充密碼的提示框。
上述幾種方法除了會彈出填充密碼的提示框外,在頁面跳轉或重新整理時(如edge瀏覽器),都會彈出儲存密碼的提示框,如圖:
當然,應該還會有其他解決方案我暫時還沒找到,如果有的話,歡迎留言。
自定義密碼輸入框元件解決方案
在嘗試了上述幾種解決方案後,發現效果都不是很好,所以我感覺只有讓input
的type
屬性始終為password
,才能更有效的解決這個問題。可以考慮自定義一個密碼輸入框元件,透過某些方法去改變input
的值的顯示方式,來達到隱藏密碼的效果。
目前想出了兩種方法:一個是不改變input
的值,僅僅隱藏input
的內容,用另一個容器去顯示密碼或者顯示*
;另一個是將實際密碼存在另一個變數中,將input
的value
值改成*
來顯示。
方案一
可以用兩個input
來實現,父容器是relative
定位,兩個input
都是absolute
,一個實際的輸入框位於上層,設定為透明,另一個用於顯示星號的輸入框位於下層。
<div class="container">
<input v-model="passwordDisplay">
<input
v-model="password"
class="password"
@input="passwordDisplay = password.replace(/./g, '*')">
</div>
<style scoped>
.container {
position: relative;
}
.container input {
position: absolute;
left: 0;
top: 0;
font-size: 12px;
}
.password {
opacity: 0;
}
</style>
效果如下圖所示:
確實沒有彈出密碼填充的對話方塊,但樣式上並不是很滿意。因為實際的輸入框被設定成了透明,且在密碼顯示框之上,所以游標無法顯示出來,且無法進行選中一部分內容。
方案二
跟方案一差不多的方式,用input
來接收使用者輸入的密碼,但僅改變輸入內容的透明度, 由於在opacity
為0的情況下設定游標顏色無效,所以要將方案一中的opacity: 0
改為:
.password {
color: transparent;
background-color: transparent;
caret-color: #000; /* 游標顏色 */
}
但是這會有個問題,選中一部分內容時,會導致透明的內容選中後顯現出來,如圖所示:
這種情況下可以考慮監聽選中事件,當選中一部分內容時,將後面的星號也選中,同時透過::selection
偽類來設定選中的內容的背景色,讓兩個選中的內容顏色一致。要實現這種效果,input
顯然做不到修改部分內容的背景色,所以可以考慮用span
代替input
,向其innerHTML
中插入帶背景色的span
:
<div class="container">
<span
ref="passwordInputDisplay"
class="password password-input__behind"
/>
<input
v-model="password"
class="password password-input__front"
@focus="isActive = true"
@blur="isActive = false"
@input="passwordDisplay = password.replace(/./g, '*')">
</div>
<style scoped>
::selection {
background-color: #409eff;
}
.container {
position: relative;
}
.password {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
font-size: 12px;
font-family: monospace; /* 必須用等寬字型 */
}
.password-input__behind {
text-align: left;
z-index: 1;
}
.password-input__front {
color: transparent;
background-color: transparent;
caret-color: #000;
z-index: 2;
}
export default {
props: {
value: {
type: String,
default: ''
}
},
methods: {
handleInput (e) {
// 刪除非法字元(只保留code>=32且code<=126的字元)
const value = e.target.value
const newValue = value.replace(/[^\x20-\x7E]/g, '')
if (newValue !== value) {
this.password = newValue
}
// 釋出input事件,從而修改props中的value值
this.$emit('input', this.password)
}
},
created() {
this.selectionEvent = () => {
const display = this.$refs.passwordInputDisplay
display.style.zIndex = 1
display.innerHTML = this.passwordDisplay
if (!this.isActive) { return }
const selection = window.getSelection()
// 如果選中的內容不為空, 則由passwordInputDisplay顯示
if (!selection.toString()) { return }
const input = this.$refs.passwordInput
const start = input.selectionStart
const end = input.selectionEnd
const highlightString = '<span style="background-color: #409eff; color: #fff;">' + this.passwordDisplay.slice(start, end) + '</span>'
display.innerHTML = this.passwordDisplay.slice(0, start) + highlightString + this.passwordDisplay.slice(end)
display.style.zIndex = 4
}
document.addEventListener('selectionchange', this.selectionEvent)
},
beforeDestory() {
document.removeEventListener('selectionchange', this.selectionEvent)
}
}
需要注意以下幾點:
- 監聽
select
事件不能用input
自帶的onselect
或@select
,因為這隻會在滑鼠鬆開時觸發,並不能實時相應選取區域的變化。所以要監聽selectionchange
事件。注意selectionchange
事件在沒選中內容時也會觸發。 - 由於相比方案一顯示了游標,游標的位置會受到實際字元寬度的影響,所以要使星號與其他字元寬度相等,必須使用如
monospace
之類的等寬字型,且必須阻止中文字元的輸入。 - 修改
innerHtml
後需要改變密碼顯示框的z-index
,否則仍然會被input
中選中的內容覆蓋。
效果如下圖所示:
這裡還有個問題,當輸入內容超過了input
的長度,顯示上就會出現錯誤,可以考慮根據字型寬度計算出最大容納的字元個數,阻止過多字元的輸入。也可以在游標移動時同時移動後面的span
,不過邏輯太過複雜沒必要。
const width = this.$refs.passwordInput.clientWidth - 20 // 20為padding
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.font = '16px monospace'
const fontWidth = ctx.measureText('A').width
this.maxLength = Math.floor(width / fontWidth)
這裡用的是canvas
進行計算字型寬度。
雖然最終實現了目標效果,不過邏輯上還是稍微複雜了點。
完整程式碼在: https://github.com/lxmghct/my-vue-components
src/components/PasswordInput/PasswordInput1.vue
方案三
只使用一個input
,另外設定一個變數去儲存真實密碼。這種方法比上述方法邏輯上要稍微簡單一些,唯一需要注意的就是當輸入框中顯示為星號時,如何區分哪些是新輸入的內容,因為會有滑鼠選中一段內容再刪除或輸入、貼上的操作,而新輸入的內容中也可能包含星號,所以不能處理的過於簡單。最後採用的是監聽selectionchange
事件來隨時更新游標所在位置,從而區分新輸入的內容。
<input
ref="passwordInput"
v-model="passwordDisplay"
autocomplete="off"
@focus="isActive = true"
@blur="isActive = false"
@input="handleInput"
>
export default {
methods: {
handleInput () {
// 獲取新輸入的字元
const tempEnd = this.passwordDisplaylength - (this.password.length - thisselection.end)
const newStr = this.passwordDisplay.slic(this.selection.start, tempEnd)
// 更新輸入框的值
const currentPosition = this.$refspasswordInput.selectionStart
this.password = this.password.slice(0,Math.min(this.selection.start,currentPosition)) + newStr + this.passwordslice(this.selection.end)
this.selection.start = currentPosition
this.selection.end = currentPosition
this.$emit('input', this.password)
}
},
created () {
this.selectionEvent = () => {
if (!this.isActive) { return }
const input = this.$refs.passwordInput
this.selection = {
start: input.selectionStart,
end: input.selectionEnd
}
}
this.copyEvent = (e) => {
if (!this.isActive) { return }
const clipboardData = e.clipboardData || window.clipboardData
clipboardData.setData('text', this.password.slice(this.selection.start, this.selection.end))
e.preventDefault()
}
document.addEventListener('selectionchange', this.selectionEvent)
document.addEventListener('copy', this.copyEvent)
},
beforeDestroy () {
document.removeEventListener('selectionchange', this.selectionEvent)
document.removeEventListener('copy', this.copyEvent)
}
}
有幾點需要注意:
- 輸入框中選定的內容的起始和結束位置無法透過
window.getSelection().anchorOffset
等引數獲取(window.getSelection()
的幾個offset都是0), 只能透過input
的selectionStart
和selectionEnd
可以拿到當前選中區域的起始和結束位置。 - 由於輸入框內實際顯示的是星號,所以複製時若不處理則複製的也是星號,所以需要監聽複製事件,將實際密碼寫入剪貼簿。剪貼簿透過
e.clipboardData || window.clipboardData
獲取。
相比於方案二,這種方法無需要求一定要等寬字型,也無需另外去處理選中內容的事件,唯一多出的地方就是對輸入框實際值的處理,包括輸入和複製,而這裡的邏輯顯然比方案二中修改樣式容易的多。
效果上跟方案二基本差不多,而且沒有長度限制,這裡用this.passwordDisplay = '\u2022'.repeat(this.value.length)
把星號改成了圓點,如下:
完整程式碼在: https://github.com/lxmghct/my-vue-components
src/components/PasswordInput/PasswordInput2.vue
密碼顯示與隱藏
點選眼睛圖示,切換密碼的顯示與隱藏狀態。
export default {
watch: {
value () {
this.updatePasswordDisplay()
},
showPassword () {
this.updatePasswordDisplay()
}
},
methods: {
updatePasswordDisplay () {
if (this.showPassword) {
this.passwordDisplay = this.value
} else {
// this.passwordDisplay = '*'.repeat(this.value.length)
this.passwordDisplay = '\u2022'.repeat(this.value.length) // 圓點
}
}
}
}
眼睛圖示可以用圖示庫或者匯入圖片,我這裡用的是svg
,眼睛圖示的svg
可以透過一些轉換工具來實現,這裡推薦一個網站: https://picsvg.com/
<div class="password-input__eye-wrap">
<div
class="password-input__eye"
@click="showPassword = !showPassword"
>
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="58.000000pt" height="50.000000pt" viewBox="0 0 58.000000 50.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,50.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M228 390 c-61 -19 -148 -96 -148 -130 0 -21 61 -87 103 -110 50 -29
127 -32 173 -8 39 21 114 98 114 118 0 19 -74 97 -111 115 -36 19 -98 26 -131
15z m121 -40 c37 -18 91 -72 91 -90 0 -18 -54 -72 -91 -90 -70 -36 -138 -22
-206 43 -18 17 -33 38 -33 47 0 19 53 71 95 93 41 22 98 21 144 -3z"/>
<path d="M235 338 c-31 -18 -44 -40 -45 -75 0 -45 9 -62 42 -79 84 -43 168 60
106 130 -27 30 -74 41 -103 24z m79 -34 c20 -20 20 -68 0 -88 -35 -35 -104 -6
-104 44 0 50 69 79 104 44z"/>
</g>
</svg>
</div>
</div>
<style scoped>
.password-input__eye-wrap {
display: flex;
align-items: center;
justify-content: center;
}
.password-input__eye {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
</style>
效果如下:
完整程式碼在: https://github.com/lxmghct/my-vue-components
src/components/PasswordInput/PasswordInput2.vue
覺得有幫助的可以在github上點個star~
總結
透過將密碼輸入框的type
設定為text
,修改樣式上的顯示,來實現既可以讓瀏覽器不自動填充密碼,又可以隱藏密碼的效果。