乐趣区

关于前端:都-2021-年了做个-Chrome-插件给自己吧

随着入职工夫变长,工作一直的深刻,在须要同时解决多个工作的同时,关上几十上百个浏览器 Tab 页就必不可少了,而我的工作简直都是在各种浏览器 Tab 页之间来回切换,如写文档、学习新常识、解决 Bug 单流转、上线等流程,所以我须要对浏览器的 Tab 页进行精细化治理,以达到精细化管理工作流程的目标,于是乎,我对于浏览器的应用变成了上面几个阶段:

Chrome – 横七竖八阶段

Chrome – 进行适当整顿

Edge – 竖向侧边栏

然而无论浏览器层面提供多少这样或那样的辅助,但毕竟浏览器的职责次要是负责帮忙你更好、更快、更高效的浏览网页,并非是帮你治理常识和工作流程,所以如果须要个性化定制的需要,就得本人上手开发啦!毕竟作为程序员,本人入手,饥寒交迫嘛 😋。

需要剖析

我心愿可能开发一个 Chrome 浏览器插件,以后其余浏览器如 Edge、Firefox、Brave,以及其余所有应用 Chromimum 开发的浏览器都是反对 Chrome 插件格局的,而这几大浏览器简直占据了近 83% 左右的桌面端浏览器市场,所以这个 Chrome 插件能够在我喜爱的浏览器上运行。

以下是 2020.3 到 2021.3 的桌面端浏览器占比数据

这个浏览器反对传统的插件点击弹出栏,以及每次关上一个新 Tab 都能展现我的利用,这样可能帮忙我随时理解我以后正在进行的工作,大抵模式如下:

弹出栏:

新 Tab:

针对下面需要的模式不晓得大家是否比拟相熟了?没错,这个插件的框架模式和 掘金 的插件相似,咱们看下掘金的 Chrome 插件:

弹出框:

新 Tab:

也就是说,在看完本次文章,你基本上领有了开发一个掘金插件的能力,心动了🐴?

轻易一提,咱们本次开发插件的技术栈如下:

  • React + TypeScript,基于 Create-React-App 脚手架搭建

通过先进的技术栈来编写 Chrome 插件🚀。

前置常识

Chrome 插件实际上蕴含几个局部:

  • manifest.json 文件,相当于整个我的项目的入口,外面记录着此插件的 icon 图标展现、弹出框款式文件、新建 Tab 逻辑、选项逻辑、内容脚本逻辑等

<!—->

  • background.js,此脚本是在整个浏览器启动或者插件加载之后就会运行的一个脚本文件,它运行在 ServiceWorker 外面,通常用于进行一些前置的数据 storage 存储操作,能够操作所有的 Chrome API

<!—->

  • popup.html,插件的弹出框展现的模板内容,能够通过 CSS 管制款式,JavaScript 管制逻辑

<!—->

  • options.html,右键插件 icon 时弹出菜单页,点击菜单页外面的选项关上的页面

<!—->

  • content.js,此脚本是在你关上一个新的网页的时候,Chrome 浏览器为这个网页注入的一个脚本文件,用于辅助此网页和你的插件进行一个通信,因为插件的运行环境是通过沙盒隔离的,无奈间接操作到 DOM,所以须要通过 content 脚本操作 DOM,而后发送给到插件的解决逻辑

上述 5 大文件组成了一个 Chrome 插件所须要的必须元素,逻辑关系如下:

能够看到,其实开发一个 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"
   }
}

上述的字段阐明如下:

  • name:插件的名字,展现在 Chrome 插件 icon 外面,以及插件市场等

<!—->

  • description:简介插件时干嘛的

<!—->

  • version:插件以后的版本

<!—->

  • manifest_version:以后应用的 manifest 文件的版本,Chrome 插件最小的 manifest 版本是 V3

<!—->

  • action:管制点击插件 icon 时的须要反馈的动作(action),这里咱们设置 hover 时展现的文字为 default_title,点击关上展现的内容为 index.html

<!—->

  • icons:为展现在 Chrome 插件外面的图标

实际上 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 逻辑

咱们之前的逻辑是,只有新开一个 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 页面,同样是对标掘金,咱们晓得掘金的 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 页面。

应用 Content 脚本

咱们曾经开发了新 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 脚本,以及整体插件内容还不够欠缺,心愿有趣味的读者能够持续摸索,将其欠缺。

❤️/ 感激反对 /

以上便是本次分享的全部内容,心愿对你有所帮忙 ^_^

喜爱的话别忘了 分享、点赞、珍藏 三连哦~

欢送关注公众号 程序员巴士 ,来自字节、虾皮、招银的三端兄弟,分享编程教训、技术干货与职业规划,助你少走弯路进大厂。

退出移动版