chrome插件入门及如何利用react进行开发

avatar
前端工程师 @字节跳动

豆皮粉儿们,又见面了,今天这一期,由字节跳动数据平台的“皮蛋菌”较大家使用React开发一个自己的Chrome插件。

作者:

作为前端程序员,chrome 插件已经成为了我们工作中必不可少的工具,但是大部分同学都没有去真正了解过 chrome 插件是如何开发的。本文就是带大家一起了解学习如何开发 chrome 插件以及如何利用 react 进行 chrome 插件的开发。

什么是 chrome 插件?

从 Google 的介绍中直译过来,我们通常说的“chrome 插件”其实是指“chrome 扩展”,不过由于大家都已经习惯于叫它作为插件,所以我们也继续以“插件”的名称来称呼。

chrome 插件是一个使用 web 技术进行开发的,用于增强浏览器功能的软件,主要由 html、css、js以及图片组成。

chrome 插件能干什么?

chrome 插件可以为 chrome 浏览器增加个性化的功能,chrome 提供了开放的 api 可供插件调用,以满足用户个性化的需求。

我们常用的插件大概有以下几大类:

  • 书签管理;
  • 窗口控制;
  • 网络请求管理:如Proxy、header修改、拦截请求等;
  • 页面能力增强:去广告、翻译等;
  • 辅助开发调试:Redux 插件等;
  • 其他功能;

chrome 插件的组成元素

1. manifest.json

chrome 插件的核心之一是一个 manifest.json 文件,这个文件是 chrome 插件的配置清单,chrome 浏览器通过读取插件的配置清单来获取插件的详细信息,同时为插件开放相应的 api 能力。

下面就是一个最简单的配置:

{
  "name": "Hello Chrome Extensions",
  "version": "1.0",
  "manifest_version": 3
}

上面的几个配置项是配置清单中的必须项,分别代表扩展名称扩展版本号chrome api 版本。只需要这一个简单的清单文件,我们就完成了最简单的 chrome 插件开发,可以直接添加到 chrome 浏览器中。

Tip: 在 chrome 扩展程序控制面板的右上角打开 开发者模式,通过 “加载已解压的扩展程序” 可以直接读取本地开发的扩展程序。

2. popup 页面

被添加到 chrome 浏览器的插件都可以被固定在浏览器的右上角,有一个小图标,在点击图标的时候,会呼出一个页面,这个页面就是 popup 页面。它是一个临时存在的小窗口,用户可以通过页面内提供的配置项对插件进行简单的配置。

popup 页面和我们普通开发的页面一样,开发者可以开发任意的样式和功能。需要注意的是,这个页面的生命周期很短,只要被关闭了就会被销毁,所以需要长期运行的代码不能在 popup 页面相关的 js 中编写。

manifest.json 中,我们可以通过如下配置来指定 popup

{
    "action": {
        "default_popup": "popup.html"
    },
}

3. background

background 是一个会常驻运行在 chrome 后台的脚本,它不会因为页面的关闭而被销毁,只要浏览器没有被关闭且插件没有被禁用,那么这个插件的 background 就会一直在后台运行。

backgroung 可以在后台监听浏览器事件,根据开发者和用户的配置来对不同的事件作出不同的响应。

popup 一样,我们也需要在 manifest.json 中注册 background

{
    "background": {
        "service_worker": "background.js"
    },
}

4. content_scripts

content_scripts 是可以被注入到我们访问的页面中的脚本,可以是 jscss 文件,它们可以读取浏览器访问的网页的详细信息,对其进行更改,并将信息通过事件的形式与 background 通信。

我们常见的一些插件,如:广告屏蔽、页面翻译等插件,就是利用了 content_scripts 的能力。

manifest.json 中,可以通过如下配置来注册 content_scripts

{
    "content_scripts": [{
        "matches": ["<all_urls>"],
        "js": ["inject.js"],
        "run_at": "document_start"
      }],
}

5. permissions

chrome 插件可以调用 chrome 浏览器开放的 api 对浏览器的能力进行增强,一些基础的 api 无需指定权限即可使用,但是一些比较高级的 api 是需要在 permissions 中进行指定,才能使用。

一些常用的 permissions 如:

  • storage:可以使用 chrome 的文件系统来保存数据的 api;
  • proxy: 可以使用对请求进行代理的 api;
  • declarativeNetRequest: 可以使用通过指定声明性规则来阻止或修改网络请求的 api;

manifest.json 中,可以这样配置:

{
    "permissions": [
        "proxy",
        "storage",
        "declarativeNetRequest",
    ],
}

6. 其他

除了上述的几个比较重要的组成元素,还有一些比较常用的元素:

  • options_ui:和 popup 类似,不过可以在tab页中打开,可以承载更多的内容,更方便配置;
  • devtools_page:开发者工具中的页面,比较常见的有Redux插件等;
  • chrome_url_overrides: tab页的内容,可以自定义;

清单配置如下:

{
    "options_ui": {
        "page": "./options.html",
        "open_in_tab": true
    },
    "devtools_page": "devtools.html",
    "chrome_url_overrides": {
    "newtab": "newtab.html"
  },
}

7. 目录结构

|-- demo
    |-- background.js
    |-- devtools.html
    |-- devtools.js
    |-- inject.js
    |-- manifest.json
    |-- newtab.html
    |-- options.html
    |-- panel.html
    |-- popup.html

如何更高效地开发 chrome 插件

chrome 插件与用户交互主要通过 popupoptions_ui 页面来进行,但是通过写纯粹的 htmlcssjs来进行开发效率会非常低,如何利用现有的框架技术和组件库来快速搭建交互页面是我们需要关注的问题。

1. 框架技术选择

React 、Vue 、Angular 是目前最流行的三大前端开发框架,我们可以根据自己的偏好选择相应的技术,本文是以 React 作为框架技术选择。

2. 脚手架的选择

在开源社区内有非常成熟的前端开发脚手架可供原则,比如 create-react-app。但是考虑到插件开发的实际场景,我们并不需要这些脚手架内置的很多功能,我们只需要一个简单的可以开发多页应用的脚手架,所以我们选择自己手动地去搭建一个简单的 chrome 插件开发工程。

3. 编译打包

由于 chrome 插件支持的语法是 es5,所以我们在依赖 React 等技术开发完成之后,需要借助 babel 以及 webpack 的能力,把我们的代码编译打包成符合 chrome 插件要求的文件及格式。

4. 工程搭建

  1. 首先使用 npm 来初始化一个项目:

npm init -y

  1. 我们需要安装我们开发所需的依赖:

npm install -D @babel/core @babel/plugin-proposal-class-properties @babel/preset-env @babel/preset-react babel-loader copy-webpack-plugin clean-webpack-plugin html-loader html-webpack-plugin webpack webpack-cli webpack-dev-server

npm i -S react react-dom

  1. package.json 中配置开发和打包的命令:
{
    "scripts": {
        "start": "webpack-dev-server",
        "build": "webpack -p"
    }
}
  1. 根据开发插件需求,创建不同的文件和目录:

    |-- react-based-extension |-- package.json |-- webpack.config.js |-- src |-- background.js |-- devtools.html |-- icon.png |-- index-devtools.js |-- index-options.js |-- index-panel.js |-- index-popup.js |-- inject_script.js |-- manifest.json |-- options.html |-- panel.html |-- popup.html |-- components |-- DevTools.js |-- Options.js |-- Panel.js |-- Popup.js

  2. 配置manifest.json

{
  "name": "React Based Extension",
  "description": "Using ReactJS to build a Chrome Extension",
  "version": "1.0.0",
  "manifest_version": 3,
  
  "icons": {
    "128": "icon.png"
  },
  "action": {
    "default_popup": "popup.html",
    "default_icon": "icon.png"
  },
  "background": { "service_worker": "./background.js"},
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["./inject_script.js"],
    "run_at": "document_start"
  }],
  "options_ui": {
    "page": "./options.html",
    "open_in_tab": true
  },
  "devtools_page": "devtools.html",
  "permissions": [
    "proxy",
    "contentSettings"
  ],
  "host_permissions": [
    "*://*/*"
  ]
}
  1. 配置 webpack,一个多页应用的简单配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  devServer: {
    contentBase: path.resolve(__dirname, './src'),
    historyApiFallback: true
  },
  entry: {
    popup: path.resolve(__dirname, "./src/index-popup.js"),
    options: path.resolve(__dirname, "./src/index-options.js"),
    panel: path.resolve(__dirname, "./src/index-panel.js"),
    devtools: path.resolve(__dirname, "./src/index-devtools.js")
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                '@babel/preset-env',
                '@babel/preset-react',
                {
                  'plugins': ['@babel/plugin-proposal-class-properties']
                }
              ]
            }
          }
        ]
      },
      {
        test: /\.html$/,
        use: ['html-loader']
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'popup.html',
      template: 'src/popup.html',
      chunks: ['popup']
    }),
    new HtmlWebpackPlugin({
      filename: 'options.html',
      template: 'src/options.html',
      chunks: ['options']
    }),
    new HtmlWebpackPlugin({
      filename: 'panel.html',
      template: 'src/panel.html',
      chunks: ['panel']
    }),
    new HtmlWebpackPlugin({
      filename: 'devtools.html',
      template: 'src/devtools.html',
      chunks: ['devtools']
    }),
    new CopyWebpackPlugin({
      patterns: [
        { from: 'src/*.json', to: '[name].[ext]' },
        { from: 'src/background.js', to: '[name].[ext]' },
        { from: 'src/inject_script.js', to: '[name].[ext]' },
        { from: 'src/*.png', to: '[name].[ext]' }
      ]
    }),
    new CleanWebpackPlugin()
  ]
}
  1. 通过以上配置,就完成了一个简单的基于 react 的 chrome 插件开发工程搭建,我们可以使用 Antd 等开源组件库来快速的进行交互页面的开发。

如何快速的调试

细心的同学可能会注意到,我们的工程项目其实是针对用户交互页面的开发,并没有涉及到真正的 chrome api 的调用。如果想验证我们的 api 使用是否正确,功能是否符合预期,可能我们需要频繁的进行编译打包,再把编译后的文件重新加载到 chrome 中来进行验证,这是一个非常繁琐和耗时的过程,对开发效率有很大的影响。

那如何才能快速地调试自己在调用 chrome api 及处理的过程中问题呢?

上面我们提到了 background,它是一个常驻在后台运行的脚本,我们的交互页可以和它进行事件通信,突破口就在这里。

  1. 在开发交互页的过程中,我们需要重点关心的是页面本身的逻辑,保证在交互过程中页面本身逻辑质量,如果有涉及到 chrome api 调用相关的逻辑,我们可以先放在一边。
  2. 在交互页面开发完成之后,我们需要考虑调用 chrome api 的逻辑,这个时候我们可以利用“事件机制”,即页面本身不调用具体的 api,而是通过和 background 之间约定事件,通过事件的形式来通知它执行实际的 api 调用。我们只需要事先约定好事件的类型及需要传递的参数即可。
  3. 然后,我们可以先进行代码编译打包,并将打包后的代码添加到 chrome 中,现在已经可以正常的访问插件的交互界面并进行操作,通过事先约定的行为可以向 background 发送事件。
  4. 重点来了,在 chrome 的扩展程序管理界面,我们可以看到每个插件都有一个“查看视图”的按钮,点击以后就会出现一个类似于控制台的窗口,这个窗口里运行的就是background的逻辑代码,我们可以通过在这里动态的修改事件响应的处理函数,来达到快速调试 api 的目的,待调试通过后,即可将正确的逻辑写回到我们的工程文件中,最后进行打包发布。

一些补充

  • chrome 扩展在20年10月份升级到了3.0版本,本文的示例是按照最新的文档来实现的,目前应用市场上大部分的扩展仍然是2.0版本开发的,有一些配置会稍有不同。
  • 习惯了使用typescript开发的同学,可以对上面的工程示例进行改造,添加tsconfig.json并对webpack配置稍作修改即可,还可以引入@types/chrome提示 chrome 相关的 api(目前还没有支持到3.0)。
  • 如果想要发布自己开发的扩展,可以到 chrome 应用商店注册成为开发者即可(需要$5);