平時(shí)做前端開(kāi)發(fā),對(duì)于頁(yè)面切換、最小化、長(zhǎng)時(shí)間不操作這些安全問(wèn)題,其實(shí)接觸不多,除非涉及到隱秘問(wèn)題,可能需要將這些實(shí)現(xiàn)考慮進(jìn)去。這次我接到這個(gè)需求時(shí),也是處于懵逼狀態(tài)
visibilityChange,這個(gè)在瀏覽器標(biāo)簽頁(yè)被隱藏或顯示的時(shí)候都會(huì)除非該方法,看似能滿足頁(yè)面切換或最小化。但是它有個(gè)問(wèn)題,就是在頁(yè)面縮放,下圖綠色標(biāo)識(shí),而非最小化時(shí)也會(huì)執(zhí)行該方法。
document.addEventListener(visibilityChange, () => { let screenTop = window.localStorage.getItem('screenTop'); // 隱藏時(shí)觸發(fā)了2次 setTimeout(() => { // 采用screenTop,是因?yàn)榭s放時(shí)也會(huì)觸發(fā)該事件,無(wú)法區(qū)分是縮放還是最小化 if (screenTop && screenTop == window.screenTop && document.visibilityState === hidden) { this.props.dispatch({ type: 'SET_PAYROLL_STATUS', data: false }) // window.location.href = window.location.href; } else { window.localStorage.setItem('screenTop', window.screenTop) } }, 0) }, false)
上面代碼中的判斷screenTop==window.screenTop能將縮放這個(gè)排除在外,結(jié)合document.visibilityState能實(shí)現(xiàn)頁(yè)面切換、頁(yè)面最小化的時(shí)候修改狀態(tài)保證頁(yè)面內(nèi)容安全,譬如通過(guò)設(shè)置狀態(tài)為false,不展示設(shè)計(jì)安全的內(nèi)容。
而判斷長(zhǎng)時(shí)間是否操作,主要是通過(guò)setInterval來(lái)倒計(jì)時(shí)變量count,如果有操作就將count初始化,從新倒計(jì)時(shí)。
hasOperate = (callback, second) => { let count = 0; const countTime = () => { timer = setInterval(() => { if (document.visibilityState === 'hidden') { count = 0; // clearInterval(timer); } count++; if (count == second) { callback(); clearInterval(timer); count=0 } }, 1000); } let x; let y; document.addEventListener('mousemove', () => { let x1 = event.clientX; let y1 = event.clientY; if (x != x1 || y != y1) { count = 0; } x = x1; y = y1; }) document.addEventListener('keydown', () => { count = 0; }) document.addEventListener('scroll', () => { count = 0; })
上面代碼,主要通過(guò)統(tǒng)計(jì)count等于你設(shè)置的初始化時(shí)間second,進(jìn)行回調(diào),回調(diào)的主要內(nèi)容是隱藏安全頁(yè)面;如果在統(tǒng)計(jì)的過(guò)程中發(fā)現(xiàn)有滾動(dòng)或鼠標(biāo)移動(dòng)以及鍵盤(pán)按下等相關(guān)操作,則將count初始化為0,醬紫就能完成長(zhǎng)時(shí)間不操作的問(wèn)題了
TML 中使用 <input> 元素表示單行輸入框和 <textarea> 元素表示多行文本框。
HTML中使用的 <input> 元素在 JavaScript 中對(duì)應(yīng)的是 HTMLInputElement 類型。HTMLInputElement 繼承自 HTMLElement 接口:
interface HTMLInputElement extends HTMLElement {
...
}
HTMLInputElement 類型有一些獨(dú)有的屬性和方法:
而在上述介紹 HTMLInputElement 類型中的屬性時(shí),type 屬性要特別關(guān)注一下,因?yàn)楦鶕?jù) type 屬性的改變,可以改變<input>的屬性。
類型 | 描述 |
text | 文本輸入 |
password | 密碼輸入 |
submit | 表單數(shù)據(jù)提交 |
button | 按鈕 |
radio | 單選框 |
checkbox | 復(fù)選框 |
file | 文件 |
hidden | 隱藏的字段 |
image | 定義圖像作為提交按鈕 |
reset | 重置按鈕 |
省略 type 屬性與 type="text"效果一樣, <input> 元素顯示為文本框。
當(dāng) type 的值為text/password/number/時(shí),會(huì)有以下屬性對(duì) <input> 元素有效。
屬性 | 類型 | 描述 |
autocomplete | string | 字符串on或off,表示<input>元素的輸入內(nèi)容可以被瀏覽器自動(dòng)補(bǔ)全。 |
maxLength | long | 指定<input>元素允許的最多字符數(shù)。 |
size | unsigned long | 表示<input>元素的寬度,這個(gè)寬度是以字符數(shù)來(lái)計(jì)量的。 |
pattern | string | 表示<input>元素的值應(yīng)該滿足的正則表達(dá)式 |
placeholder | string | 表示<input>元素的占位符,作為對(duì)元素的提示。 |
readOnly | boolean | 表示用戶是否可以修改<input>的值。 |
min | string | 表示<input>元素的最小數(shù)值或日期。 |
max | string | 表示<input>元素的最大數(shù)值或日期。 |
selectionStart | unsigned long | 表示選中文本的起始位置。如果沒(méi)有選中文本,返回光標(biāo)在<input>元素內(nèi)部的位置。 |
selectionEnd | unsigned long | 表示選中文本的結(jié)束位置。如果沒(méi)有選中文本,返回光標(biāo)在<input>元素內(nèi)部的位置。 |
selectionDirection | string | 表示選中文本的方向。可能的值包括forward、backward、none。 |
下面創(chuàng)建一個(gè) type="text" ,一次顯示 25 個(gè)字符,但最多允許顯示 50 個(gè)字符的文本框:
<input type="text" size="25" maxlength="50" value="initial value">
HTML 使用的 <textarea> 元素在 JavaScript 中對(duì)應(yīng)的是 HTMLTextAreaElement 類型。HTMLTextAreaElement類型繼承自 HTMLElement 接口:
interface HTMLTextAreaElement extends HTMLElement {
...
}
HTMLTextAreaElement 類型有一些獨(dú)有的屬性和方法:
下面創(chuàng)建一個(gè)高度為 25,寬度為 5 的 <textarea> 多行文本框。它與 <input> 不同的是,初始值顯示在 <textarea>...</textarea> 之間:
<textarea rows="25" cols="5">initial value</textarea>
注意:處理文本框值的時(shí)候最好不要使用 DOM 方法,而應(yīng)該使用 value 屬性。
<input> 與 <textarea> 都支持 select() 方法,該方法用于選中文本框中的所有內(nèi)容。該方法的語(yǔ)法為:
select(): void
下面看一個(gè)示例:
let textbox = document.forms[0].elements["input-box"];
textbox.select();
也可以在文本框獲得焦點(diǎn)時(shí),選中文本框的內(nèi)容:
textbox.addEventListener("focus", (event) => {
event.target.select();
});
當(dāng)選中文本框中的文本或使用 select() 方法時(shí),會(huì)觸發(fā) select 事件。
let textbox = document.forms[0].elements["textbox1"];
textbox.addEventListener("select", (event) => {
console.log(`Text selected: ${textbox.value}`);
});
HTML5 對(duì) select 事件進(jìn)行了擴(kuò)展,通過(guò) selectionStart 和 selectionEnd 屬性獲取文本選區(qū)的起點(diǎn)偏移量和終點(diǎn)偏移量。如下所示:
function getSelectedText(textbox){
return textbox.value.substring(textbox.selectionStart,
textbox.selectionEnd);
}
注意:在 IE8 及更早版本不支持這兩個(gè)屬性。
HTML5 提供了 setSelectionRange() 方法用于選中部分文本:
setSelectionRange(start, end, direction): void;
下面看一個(gè)例子:
<input type="text" id="text-sample" size="20" value="Hello World!">
<button onclick="selectText()">選中部分文本</button>
<script>
function selectText() {
let input = document.getElementById("text-sample");
input.focus();
input.setSelectionRange(4, 8); // o Wo
}
</script>
如果想要看到選中效果,必須讓文本框獲得焦點(diǎn)。
不同文本框經(jīng)常需要保證輸入特定類型或格式的數(shù)據(jù),或許數(shù)據(jù)需要包含特定字符或必須匹配某個(gè)特定模式。而文本框并未提供驗(yàn)證功能,因此要配合 JavaScript 腳本實(shí)現(xiàn)輸入過(guò)濾功能。
有些輸入框需要出現(xiàn)或不出現(xiàn)特定字符。如果想要將輸入框變成只讀的,只需要使用 preventDefault()方法將按鍵都屏蔽:
input.addEventListener("keypress", (event) => {
event.preventDefault();
});
而要屏蔽特定字符,就需要檢查事件的 charCode 屬性。如下所示,使用正則表達(dá)式實(shí)現(xiàn)只允許輸入數(shù)字的輸入框:
input.addEventListener("keypress", (event) => {
if (!/\d/.test(event.key)) {
event.preventDefault();
}
});
還有一個(gè)問(wèn)題需要處理:復(fù)制、粘貼及涉及Ctrl 鍵的其他功能。在除IE 外的所有瀏覽器中,前面代碼會(huì)屏蔽快捷鍵Ctrl+C、Ctrl+V 及其他使用Ctrl 的組合鍵。因此,最后一項(xiàng)檢測(cè)是確保沒(méi)有按下Ctrl鍵,如下面的例子所示:
textbox.addEventListener("keypress", (event) => {
if (!/\d/.test(String.fromCharCode(event.charCode)) &&
event.charCode > 9 &&
!event.ctrlKey){
event.preventDefault();
}
});
最后這個(gè)改動(dòng)可以確保所有默認(rèn)的文本框行為不受影響。這個(gè)技術(shù)可以用來(lái)自定義是否允許在文本框中輸入某些字符。
IE 是第一個(gè)實(shí)現(xiàn)了剪切板相關(guān)的事件以及通過(guò)JavaScript訪問(wèn)剪切板數(shù)據(jù)的瀏覽器,其它瀏覽器在后來(lái)也都支持了相同的事件和剪切板的訪問(wèn),后來(lái) HTML5 將其納入了規(guī)范。以下是與剪切板相關(guān)的 6 個(gè)事件:
剪切板事件的行為及相關(guān)對(duì)象會(huì)因?yàn)g覽器而異。在 Safari、Chrome 和 Firefox 中,beforecopy、beforecut 和 beforepaste 事件只會(huì)在顯示文本框的上下文菜單時(shí)觸發(fā),但 IE 不僅在這種情況下觸發(fā),也會(huì)在 copy、cut 和 paste 事件在所有瀏覽器中都會(huì)按預(yù)期觸發(fā)。
在實(shí)際的事件發(fā)生之前,通過(guò)beforecopy、beforecut 和 beforepaste 事件可以在向剪貼板發(fā)送或從中檢索數(shù)據(jù)前修改數(shù)據(jù)。不過(guò),取消這些事件并不會(huì)取消剪貼板操作。要阻止實(shí)際的剪貼板操作,必須取消 copy、cut和 paste 事件。
剪貼板的數(shù)據(jù)通過(guò) clipboardData 對(duì)象來(lái)獲取,且clipboardData 對(duì)象提供 3 個(gè)操作數(shù)據(jù)的方法:
而 clipboardData 對(duì)象在 IE 中使用 window 獲取,在 Firefox、Safari 和 Chrome 中使用 event 獲取。為防止未經(jīng)授權(quán)訪問(wèn)剪貼板,只能在剪貼板事件期間訪問(wèn) clipboardData 對(duì)象;IE 會(huì)在任何時(shí)候都暴露 clipboardData 對(duì)象。因此,要兼容兩者,最好在剪貼板事件期間使用該對(duì)象。
function getClipboardText(event){
var clipboardData = (event.clipboardData || window.clipboardData);
return clipboardData.getData("text");
}
function setClipboardText (event, value){
if (event.clipboardData){
return event.clipboardData.setData("text/plain", value);
} else if (window.clipboardData){
return window.clipboardData.setData("text", value);
}
}
如果文本框只有數(shù)字,那剪貼時(shí),就需要使用paste事件檢查剪貼板上的文本是否無(wú)效。如果無(wú)效,可以取消默認(rèn)行為:
input.addEventListener("paste", (event) => {
let text = getClipboardText(event);
if (!/^\d*$/.test(text)){
event.preventDefault();
}
});
注意:Firefox、Safari和Chrome只允許在onpaste事件中訪問(wèn)getData()方法。
在 JavaScript 中,可以用在當(dāng)前字段完成時(shí)自動(dòng)切換到下一個(gè)字段的方式來(lái)增強(qiáng)表單字段的易用性。比如,常用手機(jī)號(hào)分為國(guó)家好加手機(jī)號(hào)。因此,我們?cè)O(shè)置 2 個(gè)文本框:
<form>
<input type="text" name="phone1" id="phone-id-1" maxlength="4">
<input type="text" name="phone2" id="phone-id-2" maxlength="11">
</form>
當(dāng)文本框輸入到最大允許字符數(shù)后,就把焦點(diǎn)移到下一個(gè)文本框,這樣可以增加表單的易用性并加速數(shù)據(jù)輸入。如下所示:
<script>
function tabForward(event){
let target = event.target;
if (target.value.length == target.maxLength){
let form = target.form;
for (let i = 0, len = form.elements.length; i < len; i++) {
if (form.elements[i] == target) {
if (form.elements[i+1]) {
form.elements[i+1].focus();
}
return;
}
}
}
}
let inputIds = ["phone-id-1", "phone-id-2"];
for (let id of inputIds) {
let textbox = document.getElementById(id);
textbox.addEventListener("keyup", tabForward);
}
</script>
這里,tabForward() 函數(shù)通過(guò)比較用戶輸入文本的長(zhǎng)度與 maxLength 屬性的值來(lái)檢測(cè)輸入是否達(dá)到了最大長(zhǎng)度。如果兩者相等,就通過(guò)循環(huán)表中的元素集合找到當(dāng)前文本框,并把焦點(diǎn)設(shè)置到下一個(gè)元素。
注意:上面的代碼只適用于之前既定的標(biāo)記,沒(méi)有考慮可能存在的隱藏字段。
HTML5 新增了一些表單提交前,瀏覽器會(huì)基于指定的規(guī)則進(jìn)行驗(yàn)證,并在出錯(cuò)時(shí)顯示適當(dāng)?shù)腻e(cuò)誤信息。而驗(yàn)證會(huì)基于某些條件應(yīng)用到表單字段中。
表單字段中添加 required 屬性,用于標(biāo)注該字段是必填項(xiàng),不填則無(wú)法提交。該屬性適用于<input>、<textarea>和<select>。如下所示:
<input type="text" name="account" required>
也可以通過(guò) JavaScript 檢測(cè)對(duì)應(yīng)元素的 required 屬性來(lái)判斷表單字段是否為必填項(xiàng):
let isRequired = document.forms[0].elements["account"].required;
也可以檢測(cè)瀏覽器是否支持 required 屬性:
let isRequiredSupported = "required" in document.createElement("input");
注意:不同瀏覽器處理必填字段的機(jī)制不同。Firefox、Chrome、IE 和Opera 會(huì)阻止表單提交并在相應(yīng)字段下面顯示有幫助信息的彈框,而Safari 什么也不做,也不會(huì)阻止提交表單。
HTML5 為 <input> 元素增加了幾個(gè)新的 type 值。如下所示:
類型 | 描述 |
number | 數(shù)字值的輸入 |
date | 日期輸入 |
color | 顏色輸入 |
range | 一定范圍內(nèi)的值的輸入 |
month | 允許用戶選擇月份和年份 |
week | 允許用戶選擇周和年份 |
time | 允許用戶選擇時(shí)間(無(wú)時(shí)區(qū)) |
datetime | 允許用戶選擇日期和時(shí)間(有時(shí)區(qū)) |
datetime-local | 允許用戶選擇日期和時(shí)間(無(wú)時(shí)區(qū)) |
電子郵件地址的輸入 | |
search | 搜索(表現(xiàn)類似常規(guī)文本) |
tel | 電話號(hào)碼的輸入 |
url | URL地址的輸入 |
這些輸入表名字段應(yīng)該輸入的數(shù)據(jù)類型,并且提供了默認(rèn)驗(yàn)證。如下所示:
<input type="email" name="email">
<input type="url" name="homepage">
要檢測(cè)瀏覽器是否支持新類型,可以在 JavaScript 中創(chuàng)建 <input> 并設(shè)置 type 屬性,之后讀取它即可。老版本中會(huì)將我只類型設(shè)置為 text,而支持的會(huì)返回正確的值。如下所示:
let input = document.createElement("input");
input.type = "email";
let isEmailSupported = (input.type == "email");
而上面介紹的幾個(gè)如 number/range/datetime/datetime-local/date/month/week/time 幾個(gè)填寫(xiě)數(shù)字的類型,都可以指定 min/max/step 等幾個(gè)與數(shù)值有關(guān)的屬性。step 屬性用于規(guī)定合法數(shù)字間隔,如 step="2",則合法數(shù)字應(yīng)該為 0、2、4、6,依次類推。如下所示:
<input type="number" min="0" max="100" step="5" name="count">
上面的例子是<input>中只能輸入從 0 到 100 中 5 的倍數(shù)。
也可以使用 stepUp() 和 stepDown() 方法對(duì) <input> 元素中的值進(jìn)行加減,它倆會(huì)接收一個(gè)可選參數(shù),用于表示加減的數(shù)值。如下所示:
input.stepUp(); // 加1
input.stepUp(5); // 加5
input.stepDown(); // 減1
input.stepDown(10); // 減10
HTML5 還為文本添加了 pattern 屬性,用于指定一個(gè)正則表達(dá)式。這樣就可以自己設(shè)置 <input> 元素的輸入模式了。如下所示:
<input type="text" pattern="\d+" name="count">
注意模式的開(kāi)頭和末尾分別假設(shè)有^和$。這意味著輸入內(nèi)容必須從頭到尾都嚴(yán)格與模式匹配。
與新增的輸入類型一樣,指定 pattern 屬性也不會(huì)阻止用戶輸入無(wú)效內(nèi)容。模式會(huì)應(yīng)用到值,然后瀏覽器會(huì)知道值是否有效。通過(guò)訪問(wèn) pattern 屬性可以讀取模式:
let pattern = document.forms[0].elements["count"].pattern;
使用如下代碼可以檢測(cè)瀏覽器是否支持pattern 屬性:
let isPatternSupported = "pattern" in document.createElement("input");
HTML5 新增了 checkValidity() 方法,用來(lái)檢測(cè)表單中任意給定字段是否有效。而判斷的條件是約束條件,因此必填字段如果沒(méi)有值會(huì)被視為無(wú)效,字段值不匹配 pattern 屬性也會(huì)被視為無(wú)效。如下所示:
if (document.forms[0].elements[0].checkValidity()){
// 字段有效,繼續(xù)
} else {
// 字段無(wú)效
}
要檢查整個(gè)表單是否有效,可以直接在表單上調(diào)用checkValidity()方法。這個(gè)方法會(huì)在所有字段都有效時(shí)返回true,有一個(gè)字段無(wú)效就會(huì)返回false:
if(document.forms[0].checkValidity()){
// 表單有效,繼續(xù)
} else {
// 表單無(wú)效
}
validity 屬性會(huì)返回一個(gè)ValidityState 對(duì)象,表示 <input> 元素的校驗(yàn)狀態(tài)。返回的對(duì)象包含一些列的布爾值的屬性:
因此,通過(guò) validity 屬性可以檢查表單字段的有效性,從而獲取更具體的信息,如下所示:
if (input.validity && !input.validity.valid){
if (input.validity.valueMissing){
console.log("請(qǐng)指定值.")
} else if (input.validity.typeMismatch){
console.log("請(qǐng)指定電子郵件地址.");
} else {
console.log("值無(wú)效.");
}
}
通過(guò)指定 novalidate 屬性可以禁止對(duì)表單進(jìn)行任何驗(yàn)證:
<form method="post" action="/signup" novalidate>
<!-- 表單元素 -->
</form>
也可以在 JavaScript 通過(guò) noValidate 屬性設(shè)置,為 true 表示屬性存在,為 false 表示屬性不存在:
document.forms[0].noValidate = true; // 關(guān)閉驗(yàn)證
如果一個(gè)表單中有多個(gè)提交按鈕,那么可以給特定的提交按鈕添加formnovalidate 屬性,指定通過(guò)該按鈕無(wú)需驗(yàn)證即可提交表單:
<form method="post" action="/foo">
<!-- 表單元素 -->
<input type="submit" value="注冊(cè)提交">
<input type="submit" formnovalidate name="btnNoValidate"
value="沒(méi)有驗(yàn)證的提交按鈕">
</form>
也可以使用 JavaScript 設(shè)置 formNoValidate 屬性:
// 關(guān)閉驗(yàn)證
document.forms[0].elements["btnNoValidate"].formNoValidate = true;
以上總結(jié)了 <input> 和 <textarea> 兩個(gè)元素的一些功能,主要是 <input> 元素可以通過(guò)設(shè)置 type 屬性獲取不同類型的輸入框,可以通過(guò)監(jiān)聽(tīng)鍵盤(pán)事件并檢測(cè)要插入的字符來(lái)控制文本框的內(nèi)容。
還有一些與剪貼板相關(guān)的事件,并對(duì)剪貼的內(nèi)容進(jìn)行檢測(cè)。還介紹了一些 HTML5 新增的屬性和方法和新增的更多的 <input> 元素的類型,和一些與驗(yàn)證相關(guān)的屬性和方法。
著入職時(shí)間變長(zhǎng),工作不斷的深入,在需要同時(shí)處理多個(gè)任務(wù)的同時(shí),打開(kāi)幾十上百個(gè)瀏覽器 Tab 頁(yè)就必不可少了,而我的工作幾乎都是在各種瀏覽器 Tab 頁(yè)之間來(lái)回切換,如寫(xiě)文檔、學(xué)習(xí)新知識(shí)、處理 Bug 單流轉(zhuǎn)、上線等流程,所以我需要對(duì)瀏覽器的 Tab 頁(yè)進(jìn)行精細(xì)化管理,以達(dá)到精細(xì)化管理工作流程的目的,于是乎,我對(duì)于瀏覽器的使用變成了下面幾個(gè)階段:
Chrome - 雜亂無(wú)章階段
Chrome - 進(jìn)行適當(dāng)整理
Edge - 豎向側(cè)邊欄
但是無(wú)論瀏覽器層面提供多少這樣或那樣的輔助,但畢竟瀏覽器的職責(zé)主要是負(fù)責(zé)幫助你更好、更快、更高效的瀏覽網(wǎng)頁(yè),并非是幫你管理知識(shí)和工作流程,所以如果需要個(gè)性化定制的需求,就得自己上手開(kāi)發(fā)啦!畢竟作為程序員,自己動(dòng)手,豐衣足食嘛 。
我希望能夠開(kāi)發(fā)一個(gè) Chrome 瀏覽器插件,當(dāng)前其他瀏覽器如 Edge、Firefox、Brave,以及其他所有使用 Chromimum 開(kāi)發(fā)的瀏覽器都是支持 Chrome 插件格式的,而這幾大瀏覽器幾乎占據(jù)了近 83% 左右的桌面端瀏覽器市場(chǎng),所以這個(gè) Chrome 插件可以在我喜歡的瀏覽器上運(yùn)行。
以下是 2020.3 到 2021.3 的桌面端瀏覽器占比數(shù)據(jù)
這個(gè)瀏覽器支持傳統(tǒng)的插件點(diǎn)擊彈出欄,以及每次打開(kāi)一個(gè)新 Tab 都能展示我的應(yīng)用,這樣能夠幫助我隨時(shí)了解我當(dāng)前正在進(jìn)行的工作,大致形式如下:
彈出欄:
新 Tab:
針對(duì)上面需求的形式不知道大家是否比較熟悉了?沒(méi)錯(cuò),這個(gè)插件的框架形式和 掘金 的插件類似,我們看下掘金的 Chrome 插件:
彈出框:
新 Tab:
也就是說(shuō),在看完本次文章,你基本上擁有了開(kāi)發(fā)一個(gè)掘金插件的能力,心動(dòng)了?
隨便一提,我們本次開(kāi)發(fā)插件的技術(shù)棧如下:
通過(guò)先進(jìn)的技術(shù)棧來(lái)編寫(xiě) Chrome 插件。
Chrome 插件實(shí)際上包含幾個(gè)部分:
上述 5 大文件組成了一個(gè) Chrome 插件所需要的必須元素,邏輯關(guān)系如下:
image.png
可以看到,其實(shí)開(kāi)發(fā)一個(gè) Chrome 的插件也是使用 HTML/JavaScript/CSS 這些知識(shí),只不過(guò)使用場(chǎng)景,每種 JavaScript 使用的權(quán)限與功能、操作的 API 不太一樣,那么既然是使用基本的 Web 基礎(chǔ)技術(shù),我們就可以借助更為上層的 Web 開(kāi)發(fā)框架如 React 等來(lái)將 Chrome 插件的開(kāi)發(fā)上升到一個(gè)現(xiàn)代化的程度。
確保你安裝了最新版的 Node.js,然后在命令行中運(yùn)行如下命令:
npx create-react-app chrome-react-extension --template typescript
初始化好項(xiàng)目、安裝完依賴之后,我們可以看到 CRA 產(chǎn)生的模板代碼,其中就有我們需要的 public/manifest.json 文件:
當(dāng)然內(nèi)容并沒(méi)有我們上圖那樣豐富我們需要做一些修改,將內(nèi)容改為如下內(nèi)容:
{
"name": "Chrome React Extension",
"description": "使用 React TypeScript 構(gòu)建 Chrome 擴(kuò)展",
"version": "1.0",
"manifest_version": 3,
"action": {
"default_popup": "index.html",
"default_title": "Open the popup"
},
"icons": {
"16": "logo192.png",
"48": "logo192.png",
"128": "logo192.png"
}
}
上述的字段說(shuō)明如下:
實(shí)際上 Chrome 插件只能理解原生的 JavaScript,CSS,HTML 等, 所以我們使用 React 學(xué)完之后,需要進(jìn)行構(gòu)建,將構(gòu)建的產(chǎn)物打包給到瀏覽器插件去加載使用,在構(gòu)建時(shí),還有一個(gè)需要注意的就是,為了保證最優(yōu)化性能,CRA 的腳本在構(gòu)建時(shí)會(huì)將一些小的 JS 文件等,內(nèi)聯(lián)到 HTML 文件中,而不是打包成獨(dú)立的 JS 文件,在 Chrome 插件的運(yùn)行環(huán)境下,這種形式的 HTML 是不支持的,會(huì)觸發(fā)插件的 CSP(內(nèi)容安全策略)錯(cuò)誤。
所以為了測(cè)試我們的插件當(dāng)前效果,我們修改構(gòu)建腳本,在 package.json 里面:
"scripts": {
"start": "react-scripts start",
"build": "INLINE_RUNTIME_CHUNK=false react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
通過(guò)設(shè)置 INLINE_RUNTIME_CHUNK=false 確保所有的 JS 會(huì)構(gòu)建成獨(dú)立的文件,然后引入到 HTML 中加載使用。
一切準(zhǔn)備完畢,是時(shí)候構(gòu)建我們的 React 應(yīng)用了~ 在命令行中運(yùn)行如下命令:
npm run build
會(huì)發(fā)現(xiàn)內(nèi)容構(gòu)建輸出在 build/xxx 下面,包含 manifest.json、index.html、對(duì)應(yīng)的 JS/CSS 文件還有圖片等,其中 manifest 中索引了 index.html 來(lái)作為點(diǎn)擊插件時(shí)的 Popup 的展示頁(yè),這個(gè)時(shí)候我們就可以使用 Chrome 加載我們構(gòu)建好的文件,來(lái)查看插件運(yùn)行效果了:
我們打開(kāi)擴(kuò)展程序面板,設(shè)置開(kāi)發(fā)者模式,然后點(diǎn)擊加載文件,選擇我們的 build 文件地址加載:
Magic !我們可以在瀏覽器里面看到我們的插件,并使用它了,一個(gè)最簡(jiǎn)化插件完成!
當(dāng)然這里我們雖然能夠使用 React/TypeScript 以及一切現(xiàn)代的 Web 開(kāi)發(fā)技術(shù)來(lái)寫(xiě)插件,但是目前沒(méi)有很好的方式能夠?qū)崟r(shí)的進(jìn)行開(kāi)發(fā)-查看效果,就是我們常見(jiàn)的 HMR、Live Reload 這種技術(shù)暫時(shí)還沒(méi)有很好的支持到 Chrome 插件的開(kāi)發(fā),所以每次我們需要查看編寫(xiě)的效果都需要構(gòu)建之后點(diǎn)擊插件查看。
當(dāng)然如果純針對(duì) UI 或者和 Chrome API 無(wú)關(guān)的邏輯,那么你可以放心的直接在 Web 里面開(kāi)發(fā),等到開(kāi)發(fā)完畢再構(gòu)建到 Chrome 插件預(yù)覽即可。
我們之前的邏輯是,只要新開(kāi)一個(gè) Tab,那么就會(huì)訪問(wèn)我們提供的頁(yè)面,類似掘金的插件,而且我們也主要到,其實(shí)針對(duì) Popup 頁(yè)面只是幾個(gè)按鈕,而重頭戲都在新 Tab 頁(yè)界面展示,也就是我們這里其實(shí)需要一個(gè)多頁(yè)應(yīng)用?因?yàn)樽罱K要生成頁(yè)面,一個(gè)用在 Popup 頁(yè)面展示,一個(gè)用在新 Tab 頁(yè)展示。
但是我們知道 CRA 腳手架生成的模板是主要用于單頁(yè)應(yīng)用,如果需要切換到多頁(yè)應(yīng)用有一定的成本,但是我們的 Popup 頁(yè)面實(shí)際上就只有幾個(gè)按鈕,所以這里可以做一層簡(jiǎn)化,即 Popup 頁(yè)面直接手動(dòng)寫(xiě)最原始的 HTML/JS/CSS,然后將重頭戲、復(fù)雜的新 Tab 頁(yè)的邏輯來(lái)用 React TypeScript 等現(xiàn)代 Web 技術(shù)來(lái)開(kāi)發(fā)。
通過(guò)這樣設(shè)計(jì)之后,我們的目錄結(jié)構(gòu)變成了如下形式:
其中 manifest.json 的邏輯變成了如下:
{
"name": "Chrome React SEO Extension",
"description": "The power of React and TypeScript for building interactive Chrome extensions",
"version": "1.0",
"manifest_version": 3,
"action": {
"default_popup": "./popup/index.html",
"default_title": "Open the popup"
},
"chrome_url_overrides": {
"newtab": "index.html"
},
"icons": {
"16": "logo192.png",
"48": "logo192.png",
"128": "logo192.png"
}
}
我們可以看到,點(diǎn)擊 Chrome 插件彈出的頁(yè)面 Popup,換成了 ./popup/index.html ,而我們新加了一個(gè) chrome_url_overrides 字段,在 newtab 時(shí),我們打開(kāi)構(gòu)建后的 index.html 文件。
通過(guò)上面的操作,我們每次打開(kāi)一個(gè)新 Tab,都會(huì)展示下面的頁(yè)面:
完美!我們已經(jīng)實(shí)現(xiàn)了掘金的插件的核心思想:便捷的獲取技術(shù)知識(shí),就在你每次打開(kāi) Tab 時(shí)。
接下來(lái)我們嘗試改造一下我們的 Popup 頁(yè)面,同樣是對(duì)標(biāo)掘金,我們知道掘金的 Popup 頁(yè)面是一個(gè)比較簡(jiǎn)單的菜單欄,里面主要是一些用于跳轉(zhuǎn)到新 Tab 或者設(shè)置頁(yè)的操作:
我們現(xiàn)在也需要實(shí)現(xiàn)類似的點(diǎn)擊某個(gè)按鈕,跳轉(zhuǎn)到我們新 Tab 頁(yè),打開(kāi)我們上一部分定制的 Tab 邏輯。
這一部分我們就需要修改 popup/index.html ,添加相關(guān)的 JS 邏輯如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fake Juejin Extensions</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<ul>
<li class="open_new_tab">打開(kāi)新標(biāo)簽頁(yè)</li>
<li class="go_to_github">訪問(wèn) Github</li>
<li class="go_to_settings">設(shè)置</li>
</ul>
<script src="popup.js"></script>
</body>
</html>
這一次需求我們只會(huì)操作打開(kāi)新標(biāo)簽頁(yè)、訪問(wèn) Github,設(shè)置我們不做操作,留給讀者自己去擴(kuò)展。
可以看到我們導(dǎo)入了 popup.js 文件,在這個(gè) JS 文件里,我們需要完成對(duì)應(yīng)打開(kāi)新標(biāo)簽頁(yè)、和訪問(wèn) Github 的邏輯配置:
document.querySelector(".open_new_tab").addEventListener("click", (e) => {
chrome.tabs.create({}, () => {});
});
document.querySelector(".go_to_github").addEventListener("click", (e) => {
window.open("https://github.com");
});
可以看到,因?yàn)?popup.js 是運(yùn)行在 Chrome 插件的沙箱環(huán)境下的,所以它能夠使用到 chrome 這個(gè) API,進(jìn)行頁(yè)面、瀏覽器等相關(guān)的操作。
當(dāng)我們寫(xiě)入了上述邏輯之后,我們就可以點(diǎn)擊對(duì)應(yīng)的打開(kāi)新標(biāo)簽頁(yè),訪問(wèn)新標(biāo)簽頁(yè)并展示我們上一節(jié)說(shuō)到的內(nèi)容,訪問(wèn) Github,則會(huì)跳轉(zhuǎn)到 Github 頁(yè)面。
我們已經(jīng)開(kāi)發(fā)了新 Tab 頁(yè),開(kāi)發(fā)了 Popup 邏輯,接下來(lái)我們可以嘗試一下通過(guò) content 腳本,來(lái)實(shí)現(xiàn)用戶頁(yè)面與插件腳本進(jìn)行通信,以間接的操作 DOM。
首先我們需要在 manifest.json 里面注冊(cè) content 相關(guān)的腳本:
{
"name": "Chrome React Extension",
// ...
"permissions": ["activeTab", "tabs"],
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["./static/js/content.js"]
}
]
}
上述腳本通過(guò) content_scripts 指定 content 腳本,matches 指定匹配到那些域名時(shí),才執(zhí)行這個(gè)注入腳本的邏輯,js 代表需要注入的腳本的位置,這里我們填寫(xiě)的為 ./static/js/content.js ,即為通過(guò)構(gòu)建之后產(chǎn)生的 JS 內(nèi)容地址。
接著我們?cè)?Tab 頁(yè)的 React 項(xiàng)目里面去建立與 content 腳本的通信:
import React from "react";
import "./App.css";
import { DOMMessage, DOMMessageResponse } from "./types";
function App() {
// 前置邏輯
React.useEffect(() => {
/**
* We can't use "chrome.runtime.sendMessage" for sending messages from React.
* For sending messages from React we need to specify which tab to send it to. */ chrome.tabs &&
chrome.tabs.query(
{
active: true,
currentWindow: true,
},
(tabs) => {
/**
* Sends a single message to the content script(s) in the specified tab,
* with an optional callback to run when a response is sent back.
*
* The runtime.onMessage event is fired in each content script running
* in the specified tab for the current extension. */ chrome.tabs.sendMessage(
tabs[0].id || 0,
{ type: "GET_DOM" } as DOMMessage,
(response: DOMMessageResponse) => {
setTitle(response.title);
setHeadlines(response.headlines);
}
);
}
);
});
return (
// ... 模板
);
}
export default App;
可以看到我們通過(guò) chome API,去查詢當(dāng)前正在激活的 Tab 頁(yè),然后給這個(gè) Tab 頁(yè)的 content 腳本,通過(guò) chrome.tabs.sendMessage 發(fā)了一個(gè) { type: "GET_DOM" } 的消息。
然后我們創(chuàng)建對(duì)應(yīng)的 content 的腳本,在 src/chromeServices 下創(chuàng)建 DOMEvaluator.ts:
import { DOMMessage, DOMMessageResponse } from "../types";
// Function called when a new message is received const messagesFromReactAppListener = (
msg: DOMMessage,
sender: chrome.runtime.MessageSender,
sendResponse: (response: DOMMessageResponse) => void
) => {
console.log("[content.js]. Message received", msg);
const headlines = Array.from(document.getElementsByTagName<"h1">("h1")).map(
(h1) => h1.innerText
);
// Prepare the response object with information about the site const response: DOMMessageResponse = {
title: document.title,
headlines,
};
sendResponse(response);
};
/**
* Fired when a message is sent from either an extension process or a content script. */ chrome.runtime.onMessage.addListener(messagesFromReactAppListener);
這個(gè)腳本在加載的時(shí)候,通過(guò) onMessage.addListener 監(jiān)聽(tīng),然后回調(diào) messagesFromReactAppListener ,在函數(shù)里面,可以直接獲取 DOM,查詢這個(gè)頁(yè)面中的 標(biāo)題 和所有的 H1 標(biāo)簽,然后返回。
import React from "react";
import "./App.css";
import { DOMMessage, DOMMessageResponse } from "./types";
function App() {
const [title, setTitle] = React.useState("");
const [headlines, setHeadlines] = React.useState<string[]>([]);
// ...消息通信邏輯
return (
// ... 模板
<div className="App">
<h1>SEO Extension built with React!</h1>
<ul className="SEOForm">
<li className="SEOValidation">
<div className="SEOValidationField">
<span className="SEOValidationFieldTitle">Title</span>
<span
className={`SEOValidationFieldStatus ${
title.length < 30 || title.length > 65 ? "Error" : "Ok"
}`}
>
{title.length} Characters
</span>
</div>
<div className="SEOVAlidationFieldValue">{title}</div>
</li>
<li className="SEOValidation">
<div className="SEOValidationField">
<span className="SEOValidationFieldTitle">Main Heading</span>
<span
className={`SEOValidationFieldStatus ${
headlines.length !== 1 ? "Error" : "Ok"
}`}
>
{headlines.length}
</span>
</div>
<div className="SEOVAlidationFieldValue">
<ul>
{headlines.map((headline, index) => (
<li key={index}>{headline}</li>
))}
</ul>
</div>
</li>
</ul>
</div>
);
}
export default App;
然后擴(kuò)展一下 CSS 代碼:
.App {
background: #edf0f6;
padding: 0.5rem;
}
.SEOForm {
list-style: none;
margin: 0;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 10%), 0 1px 2px 0 rgb(0 0 0 / 6%);
background: #fff;
padding: 1rem;
}
.SEOValidation {
margin-bottom: 1.5rem;
}
.SEOValidationField {
width: 100%;
display: flex;
justify-content: space-between;
}
.SEOValidationFieldTitle {
font-size: 1rem;
color: #1a202c;
font-weight: bold;
}
.SEOValidationFieldStatus {
color: #fff;
padding: 0 1rem;
height: 1.5rem;
font-weight: bold;
align-items: center;
display: flex;
border-radius: 9999px;
}
.SEOValidationFieldStatus.Error {
background-color: #f23b3b;
}
.SEOValidationFieldStatus.Ok {
background-color: #48d660;
}
.SEOVAlidationFieldValue {
overflow-wrap: break-word;
width: 100%;
font-size: 1rem;
margin-top: 0.5rem;
color: #4a5568;
}
Nice!我們成功編寫(xiě)了新 Tab 頁(yè)模板、邏輯與樣式,以及創(chuàng)建了 Content 腳本邏輯,最后我們的展示效果如下:
然后我們需要進(jìn)行代碼構(gòu)建,因?yàn)?content 我們使用 TypeScript 語(yǔ)法寫(xiě),將 content 的邏輯構(gòu)建為單獨(dú)的 JS 輸出,我們安裝 craco 依賴,然后修改對(duì)應(yīng)的腳本:
yarn add -D craco
// package.json
"scripts": {
"start": "react-scripts start",
"build": "INLINE_RUNTIME_CHUNK=false craco build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
將 react-scripts 改為 craco 。
然后新建 craco.config.js ,添加如下內(nèi)容:
module.exports = {
webpack: {
configure: (webpackConfig, { env, paths }) => {
return {
...webpackConfig,
entry: {
main: [
env === "development" &&
require.resolve("react-dev-utils/webpackHotDevClient"),
paths.appIndexJs,
].filter(Boolean),
content: "./src/chromeServices/DOMEvaluator.ts",
},
output: {
...webpackConfig.output,
filename: "static/js/[name].js",
},
optimization: {
...webpackConfig.optimization,
runtimeChunk: false,
},
};
},
},
};
準(zhǔn)備完畢,開(kāi)始構(gòu)建:yarn`` build ,我們會(huì)發(fā)現(xiàn)構(gòu)建目錄輸出如下:
在本篇文章中,我們完整體驗(yàn)了使用 React+TypeScript,開(kāi)發(fā)新 Tab 內(nèi)容展示頁(yè)以及 content 通信腳本,然后通過(guò)配置 react-scripts 為 craco 進(jìn)行了分文件構(gòu)建,以及直接開(kāi)發(fā)原生的 popup 頁(yè),通過(guò)這種融匯的技術(shù),成功開(kāi)發(fā)出了一個(gè)類似掘金框架的 Chrome 插件。
這篇文章沒(méi)有介紹的有 background 腳本,以及整體插件內(nèi)容還不夠完善,希望有興趣的讀者可以繼續(xù)探索,將其完善。
以上便是本次分享的全部?jī)?nèi)容,希望對(duì)你有所幫助^_^
喜歡的話別忘了 分享、點(diǎn)贊、收藏 三連哦~
歡迎關(guān)注公眾號(hào) 程序員巴士,來(lái)自字節(jié)、蝦皮、招銀的三端兄弟,分享編程經(jīng)驗(yàn)、技術(shù)干貨與職業(yè)規(guī)劃,助你少走彎路進(jìn)大廠。
*請(qǐng)認(rèn)真填寫(xiě)需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。