平时做前端开发,对于页面切换、最小化、长时间不操作这些安全问题,其实接触不多,除非涉及到隐秘问题,可能需要将这些实现考虑进去。这次我接到这个需求时,也是处于懵逼状态
visibilityChange,这个在浏览器标签页被隐藏或显示的时候都会除非该方法,看似能满足页面切换或最小化。但是它有个问题,就是在页面缩放,下图绿色标识,而非最小化时也会执行该方法。
document.addEventListener(visibilityChange, () => { let screenTop = window.localStorage.getItem('screenTop'); // 隐藏时触发了2次 setTimeout(() => { // 采用screenTop,是因为缩放时也会触发该事件,无法区分是缩放还是最小化 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能将缩放这个排除在外,结合document.visibilityState能实现页面切换、页面最小化的时候修改状态保证页面内容安全,譬如通过设置状态为false,不展示设计安全的内容。
而判断长时间是否操作,主要是通过setInterval来倒计时变量count,如果有操作就将count初始化,从新倒计时。
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; })
上面代码,主要通过统计count等于你设置的初始化时间second,进行回调,回调的主要内容是隐藏安全页面;如果在统计的过程中发现有滚动或鼠标移动以及键盘按下等相关操作,则将count初始化为0,酱紫就能完成长时间不操作的问题了
TML 中使用 <input> 元素表示单行输入框和 <textarea> 元素表示多行文本框。
HTML中使用的 <input> 元素在 JavaScript 中对应的是 HTMLInputElement 类型。HTMLInputElement 继承自 HTMLElement 接口:
interface HTMLInputElement extends HTMLElement {
...
}
HTMLInputElement 类型有一些独有的属性和方法:
而在上述介绍 HTMLInputElement 类型中的属性时,type 属性要特别关注一下,因为根据 type 属性的改变,可以改变<input>的属性。
类型 | 描述 |
text | 文本输入 |
password | 密码输入 |
submit | 表单数据提交 |
button | 按钮 |
radio | 单选框 |
checkbox | 复选框 |
file | 文件 |
hidden | 隐藏的字段 |
image | 定义图像作为提交按钮 |
reset | 重置按钮 |
省略 type 属性与 type="text"效果一样, <input> 元素显示为文本框。
当 type 的值为text/password/number/时,会有以下属性对 <input> 元素有效。
属性 | 类型 | 描述 |
autocomplete | string | 字符串on或off,表示<input>元素的输入内容可以被浏览器自动补全。 |
maxLength | long | 指定<input>元素允许的最多字符数。 |
size | unsigned long | 表示<input>元素的宽度,这个宽度是以字符数来计量的。 |
pattern | string | 表示<input>元素的值应该满足的正则表达式 |
placeholder | string | 表示<input>元素的占位符,作为对元素的提示。 |
readOnly | boolean | 表示用户是否可以修改<input>的值。 |
min | string | 表示<input>元素的最小数值或日期。 |
max | string | 表示<input>元素的最大数值或日期。 |
selectionStart | unsigned long | 表示选中文本的起始位置。如果没有选中文本,返回光标在<input>元素内部的位置。 |
selectionEnd | unsigned long | 表示选中文本的结束位置。如果没有选中文本,返回光标在<input>元素内部的位置。 |
selectionDirection | string | 表示选中文本的方向。可能的值包括forward、backward、none。 |
下面创建一个 type="text" ,一次显示 25 个字符,但最多允许显示 50 个字符的文本框:
<input type="text" size="25" maxlength="50" value="initial value">
HTML 使用的 <textarea> 元素在 JavaScript 中对应的是 HTMLTextAreaElement 类型。HTMLTextAreaElement类型继承自 HTMLElement 接口:
interface HTMLTextAreaElement extends HTMLElement {
...
}
HTMLTextAreaElement 类型有一些独有的属性和方法:
下面创建一个高度为 25,宽度为 5 的 <textarea> 多行文本框。它与 <input> 不同的是,初始值显示在 <textarea>...</textarea> 之间:
<textarea rows="25" cols="5">initial value</textarea>
注意:处理文本框值的时候最好不要使用 DOM 方法,而应该使用 value 属性。
<input> 与 <textarea> 都支持 select() 方法,该方法用于选中文本框中的所有内容。该方法的语法为:
select(): void
下面看一个示例:
let textbox = document.forms[0].elements["input-box"];
textbox.select();
也可以在文本框获得焦点时,选中文本框的内容:
textbox.addEventListener("focus", (event) => {
event.target.select();
});
当选中文本框中的文本或使用 select() 方法时,会触发 select 事件。
let textbox = document.forms[0].elements["textbox1"];
textbox.addEventListener("select", (event) => {
console.log(`Text selected: ${textbox.value}`);
});
HTML5 对 select 事件进行了扩展,通过 selectionStart 和 selectionEnd 属性获取文本选区的起点偏移量和终点偏移量。如下所示:
function getSelectedText(textbox){
return textbox.value.substring(textbox.selectionStart,
textbox.selectionEnd);
}
注意:在 IE8 及更早版本不支持这两个属性。
HTML5 提供了 setSelectionRange() 方法用于选中部分文本:
setSelectionRange(start, end, direction): void;
下面看一个例子:
<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>
如果想要看到选中效果,必须让文本框获得焦点。
不同文本框经常需要保证输入特定类型或格式的数据,或许数据需要包含特定字符或必须匹配某个特定模式。而文本框并未提供验证功能,因此要配合 JavaScript 脚本实现输入过滤功能。
有些输入框需要出现或不出现特定字符。如果想要将输入框变成只读的,只需要使用 preventDefault()方法将按键都屏蔽:
input.addEventListener("keypress", (event) => {
event.preventDefault();
});
而要屏蔽特定字符,就需要检查事件的 charCode 属性。如下所示,使用正则表达式实现只允许输入数字的输入框:
input.addEventListener("keypress", (event) => {
if (!/\d/.test(event.key)) {
event.preventDefault();
}
});
还有一个问题需要处理:复制、粘贴及涉及Ctrl 键的其他功能。在除IE 外的所有浏览器中,前面代码会屏蔽快捷键Ctrl+C、Ctrl+V 及其他使用Ctrl 的组合键。因此,最后一项检测是确保没有按下Ctrl键,如下面的例子所示:
textbox.addEventListener("keypress", (event) => {
if (!/\d/.test(String.fromCharCode(event.charCode)) &&
event.charCode > 9 &&
!event.ctrlKey){
event.preventDefault();
}
});
最后这个改动可以确保所有默认的文本框行为不受影响。这个技术可以用来自定义是否允许在文本框中输入某些字符。
IE 是第一个实现了剪切板相关的事件以及通过JavaScript访问剪切板数据的浏览器,其它浏览器在后来也都支持了相同的事件和剪切板的访问,后来 HTML5 将其纳入了规范。以下是与剪切板相关的 6 个事件:
剪切板事件的行为及相关对象会因浏览器而异。在 Safari、Chrome 和 Firefox 中,beforecopy、beforecut 和 beforepaste 事件只会在显示文本框的上下文菜单时触发,但 IE 不仅在这种情况下触发,也会在 copy、cut 和 paste 事件在所有浏览器中都会按预期触发。
在实际的事件发生之前,通过beforecopy、beforecut 和 beforepaste 事件可以在向剪贴板发送或从中检索数据前修改数据。不过,取消这些事件并不会取消剪贴板操作。要阻止实际的剪贴板操作,必须取消 copy、cut和 paste 事件。
剪贴板的数据通过 clipboardData 对象来获取,且clipboardData 对象提供 3 个操作数据的方法:
而 clipboardData 对象在 IE 中使用 window 获取,在 Firefox、Safari 和 Chrome 中使用 event 获取。为防止未经授权访问剪贴板,只能在剪贴板事件期间访问 clipboardData 对象;IE 会在任何时候都暴露 clipboardData 对象。因此,要兼容两者,最好在剪贴板事件期间使用该对象。
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);
}
}
如果文本框只有数字,那剪贴时,就需要使用paste事件检查剪贴板上的文本是否无效。如果无效,可以取消默认行为:
input.addEventListener("paste", (event) => {
let text = getClipboardText(event);
if (!/^\d*$/.test(text)){
event.preventDefault();
}
});
注意:Firefox、Safari和Chrome只允许在onpaste事件中访问getData()方法。
在 JavaScript 中,可以用在当前字段完成时自动切换到下一个字段的方式来增强表单字段的易用性。比如,常用手机号分为国家好加手机号。因此,我们设置 2 个文本框:
<form>
<input type="text" name="phone1" id="phone-id-1" maxlength="4">
<input type="text" name="phone2" id="phone-id-2" maxlength="11">
</form>
当文本框输入到最大允许字符数后,就把焦点移到下一个文本框,这样可以增加表单的易用性并加速数据输入。如下所示:
<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() 函数通过比较用户输入文本的长度与 maxLength 属性的值来检测输入是否达到了最大长度。如果两者相等,就通过循环表中的元素集合找到当前文本框,并把焦点设置到下一个元素。
注意:上面的代码只适用于之前既定的标记,没有考虑可能存在的隐藏字段。
HTML5 新增了一些表单提交前,浏览器会基于指定的规则进行验证,并在出错时显示适当的错误信息。而验证会基于某些条件应用到表单字段中。
表单字段中添加 required 属性,用于标注该字段是必填项,不填则无法提交。该属性适用于<input>、<textarea>和<select>。如下所示:
<input type="text" name="account" required>
也可以通过 JavaScript 检测对应元素的 required 属性来判断表单字段是否为必填项:
let isRequired = document.forms[0].elements["account"].required;
也可以检测浏览器是否支持 required 属性:
let isRequiredSupported = "required" in document.createElement("input");
注意:不同浏览器处理必填字段的机制不同。Firefox、Chrome、IE 和Opera 会阻止表单提交并在相应字段下面显示有帮助信息的弹框,而Safari 什么也不做,也不会阻止提交表单。
HTML5 为 <input> 元素增加了几个新的 type 值。如下所示:
类型 | 描述 |
number | 数字值的输入 |
date | 日期输入 |
color | 颜色输入 |
range | 一定范围内的值的输入 |
month | 允许用户选择月份和年份 |
week | 允许用户选择周和年份 |
time | 允许用户选择时间(无时区) |
datetime | 允许用户选择日期和时间(有时区) |
datetime-local | 允许用户选择日期和时间(无时区) |
电子邮件地址的输入 | |
search | 搜索(表现类似常规文本) |
tel | 电话号码的输入 |
url | URL地址的输入 |
这些输入表名字段应该输入的数据类型,并且提供了默认验证。如下所示:
<input type="email" name="email">
<input type="url" name="homepage">
要检测浏览器是否支持新类型,可以在 JavaScript 中创建 <input> 并设置 type 属性,之后读取它即可。老版本中会将我只类型设置为 text,而支持的会返回正确的值。如下所示:
let input = document.createElement("input");
input.type = "email";
let isEmailSupported = (input.type == "email");
而上面介绍的几个如 number/range/datetime/datetime-local/date/month/week/time 几个填写数字的类型,都可以指定 min/max/step 等几个与数值有关的属性。step 属性用于规定合法数字间隔,如 step="2",则合法数字应该为 0、2、4、6,依次类推。如下所示:
<input type="number" min="0" max="100" step="5" name="count">
上面的例子是<input>中只能输入从 0 到 100 中 5 的倍数。
也可以使用 stepUp() 和 stepDown() 方法对 <input> 元素中的值进行加减,它俩会接收一个可选参数,用于表示加减的数值。如下所示:
input.stepUp(); // 加1
input.stepUp(5); // 加5
input.stepDown(); // 减1
input.stepDown(10); // 减10
HTML5 还为文本添加了 pattern 属性,用于指定一个正则表达式。这样就可以自己设置 <input> 元素的输入模式了。如下所示:
<input type="text" pattern="\d+" name="count">
注意模式的开头和末尾分别假设有^和$。这意味着输入内容必须从头到尾都严格与模式匹配。
与新增的输入类型一样,指定 pattern 属性也不会阻止用户输入无效内容。模式会应用到值,然后浏览器会知道值是否有效。通过访问 pattern 属性可以读取模式:
let pattern = document.forms[0].elements["count"].pattern;
使用如下代码可以检测浏览器是否支持pattern 属性:
let isPatternSupported = "pattern" in document.createElement("input");
HTML5 新增了 checkValidity() 方法,用来检测表单中任意给定字段是否有效。而判断的条件是约束条件,因此必填字段如果没有值会被视为无效,字段值不匹配 pattern 属性也会被视为无效。如下所示:
if (document.forms[0].elements[0].checkValidity()){
// 字段有效,继续
} else {
// 字段无效
}
要检查整个表单是否有效,可以直接在表单上调用checkValidity()方法。这个方法会在所有字段都有效时返回true,有一个字段无效就会返回false:
if(document.forms[0].checkValidity()){
// 表单有效,继续
} else {
// 表单无效
}
validity 属性会返回一个ValidityState 对象,表示 <input> 元素的校验状态。返回的对象包含一些列的布尔值的属性:
因此,通过 validity 属性可以检查表单字段的有效性,从而获取更具体的信息,如下所示:
if (input.validity && !input.validity.valid){
if (input.validity.valueMissing){
console.log("请指定值.")
} else if (input.validity.typeMismatch){
console.log("请指定电子邮件地址.");
} else {
console.log("值无效.");
}
}
通过指定 novalidate 属性可以禁止对表单进行任何验证:
<form method="post" action="/signup" novalidate>
<!-- 表单元素 -->
</form>
也可以在 JavaScript 通过 noValidate 属性设置,为 true 表示属性存在,为 false 表示属性不存在:
document.forms[0].noValidate = true; // 关闭验证
如果一个表单中有多个提交按钮,那么可以给特定的提交按钮添加formnovalidate 属性,指定通过该按钮无需验证即可提交表单:
<form method="post" action="/foo">
<!-- 表单元素 -->
<input type="submit" value="注册提交">
<input type="submit" formnovalidate name="btnNoValidate"
value="没有验证的提交按钮">
</form>
也可以使用 JavaScript 设置 formNoValidate 属性:
// 关闭验证
document.forms[0].elements["btnNoValidate"].formNoValidate = true;
以上总结了 <input> 和 <textarea> 两个元素的一些功能,主要是 <input> 元素可以通过设置 type 属性获取不同类型的输入框,可以通过监听键盘事件并检测要插入的字符来控制文本框的内容。
还有一些与剪贴板相关的事件,并对剪贴的内容进行检测。还介绍了一些 HTML5 新增的属性和方法和新增的更多的 <input> 元素的类型,和一些与验证相关的属性和方法。
着入职时间变长,工作不断的深入,在需要同时处理多个任务的同时,打开几十上百个浏览器 Tab 页就必不可少了,而我的工作几乎都是在各种浏览器 Tab 页之间来回切换,如写文档、学习新知识、处理 Bug 单流转、上线等流程,所以我需要对浏览器的 Tab 页进行精细化管理,以达到精细化管理工作流程的目的,于是乎,我对于浏览器的使用变成了下面几个阶段:
Chrome - 杂乱无章阶段
Chrome - 进行适当整理
Edge - 竖向侧边栏
但是无论浏览器层面提供多少这样或那样的辅助,但毕竟浏览器的职责主要是负责帮助你更好、更快、更高效的浏览网页,并非是帮你管理知识和工作流程,所以如果需要个性化定制的需求,就得自己上手开发啦!毕竟作为程序员,自己动手,丰衣足食嘛 。
我希望能够开发一个 Chrome 浏览器插件,当前其他浏览器如 Edge、Firefox、Brave,以及其他所有使用 Chromimum 开发的浏览器都是支持 Chrome 插件格式的,而这几大浏览器几乎占据了近 83% 左右的桌面端浏览器市场,所以这个 Chrome 插件可以在我喜欢的浏览器上运行。
以下是 2020.3 到 2021.3 的桌面端浏览器占比数据
这个浏览器支持传统的插件点击弹出栏,以及每次打开一个新 Tab 都能展示我的应用,这样能够帮助我随时了解我当前正在进行的工作,大致形式如下:
弹出栏:
新 Tab:
针对上面需求的形式不知道大家是否比较熟悉了?没错,这个插件的框架形式和 掘金 的插件类似,我们看下掘金的 Chrome 插件:
弹出框:
新 Tab:
也就是说,在看完本次文章,你基本上拥有了开发一个掘金插件的能力,心动了?
随便一提,我们本次开发插件的技术栈如下:
通过先进的技术栈来编写 Chrome 插件。
Chrome 插件实际上包含几个部分:
上述 5 大文件组成了一个 Chrome 插件所需要的必须元素,逻辑关系如下:
image.png
可以看到,其实开发一个 Chrome 的插件也是使用 HTML/JavaScript/CSS 这些知识,只不过使用场景,每种 JavaScript 使用的权限与功能、操作的 API 不太一样,那么既然是使用基本的 Web 基础技术,我们就可以借助更为上层的 Web 开发框架如 React 等来将 Chrome 插件的开发上升到一个现代化的程度。
确保你安装了最新版的 Node.js,然后在命令行中运行如下命令:
npx create-react-app chrome-react-extension --template typescript
初始化好项目、安装完依赖之后,我们可以看到 CRA 产生的模板代码,其中就有我们需要的 public/manifest.json 文件:
当然内容并没有我们上图那样丰富我们需要做一些修改,将内容改为如下内容:
{
"name": "Chrome React Extension",
"description": "使用 React TypeScript 构建 Chrome 扩展",
"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"
}
}
上述的字段说明如下:
实际上 Chrome 插件只能理解原生的 JavaScript,CSS,HTML 等, 所以我们使用 React 学完之后,需要进行构建,将构建的产物打包给到浏览器插件去加载使用,在构建时,还有一个需要注意的就是,为了保证最优化性能,CRA 的脚本在构建时会将一些小的 JS 文件等,内联到 HTML 文件中,而不是打包成独立的 JS 文件,在 Chrome 插件的运行环境下,这种形式的 HTML 是不支持的,会触发插件的 CSP(内容安全策略)错误。
所以为了测试我们的插件当前效果,我们修改构建脚本,在 package.json 里面:
"scripts": {
"start": "react-scripts start",
"build": "INLINE_RUNTIME_CHUNK=false react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
通过设置 INLINE_RUNTIME_CHUNK=false 确保所有的 JS 会构建成独立的文件,然后引入到 HTML 中加载使用。
一切准备完毕,是时候构建我们的 React 应用了~ 在命令行中运行如下命令:
npm run build
会发现内容构建输出在 build/xxx 下面,包含 manifest.json、index.html、对应的 JS/CSS 文件还有图片等,其中 manifest 中索引了 index.html 来作为点击插件时的 Popup 的展示页,这个时候我们就可以使用 Chrome 加载我们构建好的文件,来查看插件运行效果了:
我们打开扩展程序面板,设置开发者模式,然后点击加载文件,选择我们的 build 文件地址加载:
Magic !我们可以在浏览器里面看到我们的插件,并使用它了,一个最简化插件完成!
当然这里我们虽然能够使用 React/TypeScript 以及一切现代的 Web 开发技术来写插件,但是目前没有很好的方式能够实时的进行开发-查看效果,就是我们常见的 HMR、Live Reload 这种技术暂时还没有很好的支持到 Chrome 插件的开发,所以每次我们需要查看编写的效果都需要构建之后点击插件查看。
当然如果纯针对 UI 或者和 Chrome API 无关的逻辑,那么你可以放心的直接在 Web 里面开发,等到开发完毕再构建到 Chrome 插件预览即可。
我们之前的逻辑是,只要新开一个 Tab,那么就会访问我们提供的页面,类似掘金的插件,而且我们也主要到,其实针对 Popup 页面只是几个按钮,而重头戏都在新 Tab 页界面展示,也就是我们这里其实需要一个多页应用?因为最终要生成页面,一个用在 Popup 页面展示,一个用在新 Tab 页展示。
但是我们知道 CRA 脚手架生成的模板是主要用于单页应用,如果需要切换到多页应用有一定的成本,但是我们的 Popup 页面实际上就只有几个按钮,所以这里可以做一层简化,即 Popup 页面直接手动写最原始的 HTML/JS/CSS,然后将重头戏、复杂的新 Tab 页的逻辑来用 React TypeScript 等现代 Web 技术来开发。
通过这样设计之后,我们的目录结构变成了如下形式:
其中 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"
}
}
我们可以看到,点击 Chrome 插件弹出的页面 Popup,换成了 ./popup/index.html ,而我们新加了一个 chrome_url_overrides 字段,在 newtab 时,我们打开构建后的 index.html 文件。
通过上面的操作,我们每次打开一个新 Tab,都会展示下面的页面:
完美!我们已经实现了掘金的插件的核心思想:便捷的获取技术知识,就在你每次打开 Tab 时。
接下来我们尝试改造一下我们的 Popup 页面,同样是对标掘金,我们知道掘金的 Popup 页面是一个比较简单的菜单栏,里面主要是一些用于跳转到新 Tab 或者设置页的操作:
我们现在也需要实现类似的点击某个按钮,跳转到我们新 Tab 页,打开我们上一部分定制的 Tab 逻辑。
这一部分我们就需要修改 popup/index.html ,添加相关的 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">打开新标签页</li>
<li class="go_to_github">访问 Github</li>
<li class="go_to_settings">设置</li>
</ul>
<script src="popup.js"></script>
</body>
</html>
这一次需求我们只会操作打开新标签页、访问 Github,设置我们不做操作,留给读者自己去扩展。
可以看到我们导入了 popup.js 文件,在这个 JS 文件里,我们需要完成对应打开新标签页、和访问 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");
});
可以看到,因为 popup.js 是运行在 Chrome 插件的沙箱环境下的,所以它能够使用到 chrome 这个 API,进行页面、浏览器等相关的操作。
当我们写入了上述逻辑之后,我们就可以点击对应的打开新标签页,访问新标签页并展示我们上一节说到的内容,访问 Github,则会跳转到 Github 页面。
我们已经开发了新 Tab 页,开发了 Popup 逻辑,接下来我们可以尝试一下通过 content 脚本,来实现用户页面与插件脚本进行通信,以间接的操作 DOM。
首先我们需要在 manifest.json 里面注册 content 相关的脚本:
{
"name": "Chrome React Extension",
// ...
"permissions": ["activeTab", "tabs"],
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["./static/js/content.js"]
}
]
}
上述脚本通过 content_scripts 指定 content 脚本,matches 指定匹配到那些域名时,才执行这个注入脚本的逻辑,js 代表需要注入的脚本的位置,这里我们填写的为 ./static/js/content.js ,即为通过构建之后产生的 JS 内容地址。
接着我们在 Tab 页的 React 项目里面去建立与 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;
可以看到我们通过 chome API,去查询当前正在激活的 Tab 页,然后给这个 Tab 页的 content 脚本,通过 chrome.tabs.sendMessage 发了一个 { type: "GET_DOM" } 的消息。
然后我们创建对应的 content 的脚本,在 src/chromeServices 下创建 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);
这个脚本在加载的时候,通过 onMessage.addListener 监听,然后回调 messagesFromReactAppListener ,在函数里面,可以直接获取 DOM,查询这个页面中的 标题 和所有的 H1 标签,然后返回。
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;
然后扩展一下 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!我们成功编写了新 Tab 页模板、逻辑与样式,以及创建了 Content 脚本逻辑,最后我们的展示效果如下:
然后我们需要进行代码构建,因为 content 我们使用 TypeScript 语法写,将 content 的逻辑构建为单独的 JS 输出,我们安装 craco 依赖,然后修改对应的脚本:
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 ,添加如下内容:
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,
},
};
},
},
};
准备完毕,开始构建:yarn`` build ,我们会发现构建目录输出如下:
在本篇文章中,我们完整体验了使用 React+TypeScript,开发新 Tab 内容展示页以及 content 通信脚本,然后通过配置 react-scripts 为 craco 进行了分文件构建,以及直接开发原生的 popup 页,通过这种融汇的技术,成功开发出了一个类似掘金框架的 Chrome 插件。
这篇文章没有介绍的有 background 脚本,以及整体插件内容还不够完善,希望有兴趣的读者可以继续探索,将其完善。
以上便是本次分享的全部内容,希望对你有所帮助^_^
喜欢的话别忘了 分享、点赞、收藏 三连哦~
欢迎关注公众号 程序员巴士,来自字节、虾皮、招银的三端兄弟,分享编程经验、技术干货与职业规划,助你少走弯路进大厂。
*请认真填写需求信息,我们会在24小时内与您取得联系。