多年来,浏览器扩展使我们能够定制我们的网络体验。起初,扩展程序由小部件和通知徽章组成,但随着技术的发展,扩展程序开始与网站深度整合。现在甚至有一些扩展是完整的应用程序。
随着扩展变得越来越复杂,开发者创造了一些解决方案,使已有的工具能够扩展和适应。例如,像React这样的框架改善了网络开发,甚至被用于--而不是普通的JavaScript--构建网络扩展。
什么是Chrome浏览器扩展?
为了了解更多关于浏览器扩展的信息,我们来看看谷歌浏览器。Chrome浏览器扩展是一个由不同模块(或组件)组成的系统,每个模块提供与浏览器和用户的不同互动类型。模块的例子包括背景脚本、内容脚本、选项页和用户界面元素。
在本教程中,我们将使用Chrome和React构建一个浏览器扩展。这篇博文将涵盖。
- 如何使用React来构建UI元素
- 如何创建内容脚本以与网站互动
- 如何在完整的解决方案中使用TypeScript
构建一个Chrome浏览器扩展
在进入实施阶段之前,让我们介绍一下我们的Chrome扩展:SEO验证器扩展。这个扩展对网站进行分析,以检测SEO元数据的实施和网站结构中常见的技术问题。

我们的扩展将在当前页面DOM上运行一套预定义的检查,并揭示任何检测到的问题。
建立我们的扩展的第一步是创建一个React应用程序。你可以在这个GitHubrepo中查看代码。
用Create React App (CRA)创建一个React应用程序
使用CRA创建一个支持TypeScript的React应用程序很容易。
npx create-react-app chrome-react-seo-extension --template typescript
现在,我们的骨架应用程序已经开始运行,我们可以将其转化为一个扩展。
将React应用转换为Chrome扩展程序
因为Chrome扩展程序是一个网络应用,所以我们不需要调整应用代码。不过,我们确实需要确保Chrome能够加载我们的应用。
扩展程序配置
扩展程序的所有配置都属于manifest.js 文件,该文件目前存在于我们的public 文件夹中。
这个文件是由CRA自动生成的。然而,为了对一个扩展有效,它必须遵循扩展指南。目前有两个版本的清单文件被Chrome v2和v3支持,但在本指南中,我们将使用v3。
让我们首先用以下代码更新文件public/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": "index.html",
"default_title": "Open the popup"
},
"icons": {
"16": "logo192.png",
"48": "logo192.png",
"128": "logo192.png"
}
}
接下来,让我们对每个字段进行细分。
name:这是扩展程序的名称description:扩展的描述version:扩展的当前版本manifest_version:我们想在项目中使用的清单格式的版本action:行动允许您自定义出现在Chrome工具条上的按钮,这些按钮通常会触发一个弹出的扩展用户界面。在我们的案例中,我们定义我们的按钮要启动一个弹出窗口,显示我们的index.html,其中承载我们的应用程序的内容。icons:一组扩展图标
构建你的应用程序
要构建一个React应用程序,只需运行。
npm run build
这个命令调用react-scripts 来构建我们的应用程序,在build 文件夹中产生输出。但那里到底发生了什么?
当React构建我们的应用程序时,它为我们生成了一系列的文件。让我们来看看一个例子。

正如我们所看到的,CRA将应用的代码压缩成几个JavaScript文件,用于chunks、main和runtime。此外,它还生成了一个文件,包含我们所有的样式、我们的index.html ,以及我们公共文件夹中的所有资产,包括manifest.json 。
这看起来很好,但如果我们在Chrome中尝试,我们会开始收到内容安全策略(CSP)错误。这是因为当CRA构建应用程序时,它试图提高效率,为了避免添加更多的JavaScript文件,它直接在HTML页面上放置一些内联JavaScript代码。在一个网站上,这不是一个问题--但它不会在一个扩展中运行。
因此,我们需要通过设置一个名为INLINE_RUNTIME_CHUNK 的环境变量,告诉CRA为我们把额外的代码放到一个单独的文件中。
因为这个环境变量是特殊的,只适用于构建,我们不会把它添加到.env 文件中。相反,我们将在package.json 文件中更新我们的build 命令。
你的package.json 脚本部分目前是这样的。

编辑构建命令如下。
“build”: “INLINE_RUNTIME_CHUNK=false react-scripts build”,
如果你重建你的项目,生成的index.html 将不包含对内联JavaScript代码的引用。
将扩展加载到你的浏览器
我们现在准备将扩展程序加载到Chrome浏览器中。这个过程相对简单。首先,在您的Chrome浏览器上访问chrome://extensions/ ,并启用开发者模式的切换。

然后,点击加载解压,选择你的build 文件夹。你的扩展现在已经加载,并列在扩展页面上。它看起来应该是这样的。

此外,在你的扩展工具条上应该出现一个新的按钮。如果你点击它,你会看到React演示程序的弹出窗口。

构建弹出式窗口
动作图标是我们扩展的主要入口。当按下时,它启动一个弹出式窗口,其中包含index.html 。
在我们目前的实现中,我看到两个主要问题:弹出式窗口太小,而且它正在渲染React演示页面。
第一步是调整弹出窗口的大小,使其能够包含我们想要呈现给用户的信息。我们所要做的就是调整body 元素的宽度和高度。
打开React生成的文件index.css ,修改body 元素,使其包含宽度和高度。
body {
width: 600px;
height: 400px;
...
}
现在,返回到Chrome浏览器。在这里你不会看到任何区别,因为我们的扩展只在编译后的代码中工作,这意味着要看到扩展本身的任何变化,我们必须重新构建代码。这是一个相当大的弊端。为了尽量减少工作,我一般把扩展作为网络应用程序运行,只把它们作为测试用的扩展运行。
重建后,Chrome会自动注意到这些变化,并为你刷新扩展。现在它应该是这样的。

如果你有任何问题,而且你的更改没有应用,请查看Chrome扩展页面,看看你的扩展是否有任何错误,或者手动强制重新加载。
设计用户界面
设计用户界面完全发生在React应用程序上,使用你熟悉和喜爱的组件、函数和样式,我们不会专注于创建屏幕本身。
让我们直接跳到我们的App.tsx ,用我们更新的代码。
import React from 'react';
import './App.css';
function App() {
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 Error">
90 Characters
</span>
</div>
<div className="SEOVAlidationFieldValue">
The title of the page
</div>
</li>
<li className="SEOValidation">
<div className="SEOValidationField">
<span className="SEOValidationFieldTitle">Main Heading</span>
<span className="SEOValidationFieldStatus Ok">
1
</span>
</div>
<div className="SEOVAlidationFieldValue">
The main headline of the page (H1)
</div>
</li>
</ul>
</div>
);
}
export default App;
添加一些样式,使它看起来像这样。

它看起来好多了,但还没有完全达到目的。在组件代码中,我们硬编码了页面的标题、主标题和验证。
就目前而言,我们的扩展作为一个纯粹的React应用,在隔离状态下运行良好。但如果我们想与用户访问的页面进行交互,会发生什么?我们现在需要让扩展与浏览器进行交互。
访问网站内容
我们的React代码在弹出式窗口内孤立地运行,不了解用户正在访问的浏览器信息、标签和网站。React应用程序不能直接改变浏览器内容、标签或网站。然而,它可以通过注入的全局对象(chrome )访问浏览器API。
Chrome的API允许我们的扩展与浏览器中的几乎任何东西进行交互,包括访问和改变标签和它们所承载的网站,尽管此类任务需要额外的权限。
然而,如果你探索API,你不会发现任何从网站的DOM中提取信息的方法,那么,我们如何才能访问网站的标题或头条数量等属性?答案就在内容脚本中。
内容脚本是特殊的JavaScript文件,在网页的上下文中运行,可以完全访问DOM元素、对象和方法。这使得它们非常适合我们的用例。
但剩下的问题是,我们的React应用如何与这些内容脚本互动?
使用消息传递
消息传递是一种技术,它允许在不同上下文中运行的不同脚本相互通信。Chrome浏览器中的消息并不局限于内容脚本和弹出式脚本,消息传递还可以实现跨扩展的消息传递、常规的网站到扩展的消息传递,以及本地应用程序的消息传递。
正如你所期望的,消息在两个部分之间建立连接,其中一个部分发送请求,另一个部分可以发送响应,也被称为一次性请求。

让我们通过在扩展中建立我们的消息传递系统来实践消息传递的API。
设置项目
针对我们的要求与消息传递API进行互动需要三点。
- 访问Chrome的API
- 权限
- 内容脚本
通过我们的React应用中全局可用的chrome 对象,可以访问Chrome API。例如,我们可以直接使用它,通过API调用chrome.tabs.query ,查询有关浏览器标签的信息。
试图这样做会在我们的项目中引发类型错误,因为我们的项目对这个chrome 对象一无所知。因此,我们需要做的第一件事是安装适当的类型。
npm install @types/chrome --save-dev
接下来,我们需要告知Chrome关于扩展所需的权限。我们在manifest 文件中通过permissions 属性做到这一点。
因为我们的扩展只需要访问当前标签,所以我们只需要一个权限:activeTab 。
请更新你的清单,包括一个新的permissions 密钥。
"permissions": [
"activeTab"
],
最后,我们需要构建内容脚本来收集我们需要的所有网站信息。
在独立的 JavaScript 文件中构建内容脚本
我们已经了解到,内容脚本是在网页上下文中运行的特殊JavaScript文件,这些脚本与React应用不同,是隔离的。
然而,当我们解释CRA如何构建我们的代码时,我们了解到React将只生成一个带有应用程序代码的文件。那么,我们如何才能生成两个文件,一个用于React应用程序,另一个用于内容脚本?
我知道有两种方法。第一种是在public 文件夹中直接创建一个JavaScript文件,这样它就被排除在构建过程之外,并按原样复制到输出中。然而,我们不能在这里使用TypeScript,这是非常不幸的。
值得庆幸的是,还有第二种方法:我们可以从CRA更新构建设置,要求它为我们生成两个文件。这可以在一个叫做Craco的额外库的帮助下完成。
CRA执行所有运行和构建React应用程序所需的魔法,但它将所有配置、构建设置和其他文件都封装在他们的库中。Craco允许我们覆盖其中的一些配置文件,而不必弹出项目。
要安装Craco,只需运行。
npm install @craco/craco --save
接下来,在你项目的根目录下创建一个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,
}
}
},
}
}
CRA利用webpack 来构建应用程序。在这个文件中,我们用一个新条目覆盖现有的设置。这个条目将从./src/chromeServices/DOMEvaluator.ts ,并将其与其他内容分开构建到输出文件static/js/[name].js ,其中名称为content ,即我们提供源文件的键。
在这一点上,Craco已经安装和配置好了,但并没有被使用。在你的package.json ,有必要再一次编辑你的build 脚本,以这。
"build": "INLINE_RUNTIME_CHUNK=false craco build",
我们所做的唯一改变是用craco 替换了react-scripts 。我们现在差不多完成了。我们要求craco ,为我们建立一个新的文件,但我们从未创建过它。我们以后再来讨论这个问题。现在,要知道缺少一个关键文件,在这期间,构建是不可能的。
告诉Chrome在哪里可以找到内容脚本
我们做了这么多工作,生成了一个名为content.js 的新文件,作为我们构建项目的一部分,但 Chrome 却不知道该如何处理这个文件,甚至不知道它的存在。
我们需要配置我们的扩展,让浏览器知道这个文件,并将其作为一个内容脚本注入。当然,我们要在manifest 文件上这样做。
在manifest 规范中,有一个关于content_scripts的部分。它是一个脚本数组,每个脚本必须包含文件的位置和应该被注入到哪些网站。
让我们在manifest.json 文件中添加一个新的部分。
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["./static/js/content.js"]
}
],
通过这些设置,Chrome 将把content.js 文件注入到任何使用 HTTP 或 HTTPS 协议的网站。
开发DOMEvaluator内容脚本
在配置、库和设置方面,我们已经准备就绪。唯一缺少的是创建我们的DOMEvaluator内容脚本,并利用信息传递API来接收请求和传递信息给React组件。
下面是我们项目的样子。

首先,让我们创建缺少的文件。在文件夹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);
有三行关键的代码。
- 注册一个消息监听器
- 听众函数声明 (
messagesFromReactAppListener) sendResponse(定义为听众函数的一个参数
现在我们的函数可以接收消息并派发响应,接下来让我们转向React方面的事情。
React应用程序组件
我们的应用程序现在已经准备好与Chrome API互动,并向我们的内容脚本发送消息。
因为这里的代码比较复杂,让我们把它分解成几个部分,最后再把它们放在一起。
向内容脚本发送消息需要我们确定哪个网站会收到消息。如果你还记得上一节的内容,我们只授予了扩展程序对当前标签的访问权,所以让我们获得对该标签的引用。
获取当前标签是很容易的,而且有很好的记录。我们简单地用某些参数查询标签集合,然后我们得到一个带有所有找到的引用的回调。
chrome.tabs && chrome.tabs.query({
active: true,
currentWindow: true
}, (tabs) => {
// Callback function
});
有了对标签的引用,我们就可以发送一个消息,这个消息可以被运行在该网站上的内容脚本自动选中。
chrome.tabs.sendMessage(
// Current tab ID
tabs[0].id || 0,
// Message type
{ type: 'GET_DOM' } as DOMMessage,
// Callback executed when the content script sends a response
(response: DOMMessageResponse) => {
...
});
这里发生了一些重要的事情:当我们发送一条消息时,我们提供了消息对象,而且,在这个消息对象中,我设置了一个名为type 的属性。该属性可用于分离不同的消息,这些消息将在另一侧执行不同的代码和响应。在处理状态时,可以把它看作是调度和减少。
总结
今天,我们介绍了许多新的概念和想法,从扩展如何工作,它们的不同模块,以及它们如何沟通。我们还建立了一个奇妙的扩展,充分利用了React和TypeScript的全部力量。谢谢你的阅读!