Skip to content

原生 JS 开发 Chrome 插件 ​

从本期开始,我们就进入新的一章,实战篇,本期先从原生 JS 开发开始

一、开发 Chrome 插件 ​

1. 新建项目并创建 manifest.json 文件 ​

1.1. 添加以下代码 ​

json
{
  "manifest_version": 3,
  "name": "My Chrome Extension",
  "version": "0.0.1",
  "description": "My Chrome Extension Description"
}

1.2. 在谷歌浏览器中打开管理插件 ​

第一种方式

alt text

第二种方式

alt text

第三种方式 浏览器输入:chrome://extensions/

1.3. 点击【加载已解压的扩展程序】 ​

alt text

选择刚刚创建的文件夹即可

alt text

1.4. 可以看到插件已经加载完成 ​

alt text

包含名称、版本、描述、默认图标

1.5. 固定插件 ​

  1. 点击固定按钮,即可把插件固定在工具栏中

alt text

  1. 固定之后的样式

alt text

到了这一步,一个最最基础的插件已经完成了

2. 添加 action 中的 popup html 页面 ​

以谷歌翻译模版为例

alt text

2.1. 添加 action icons ​

创建 icons 文件夹,并添加图片

shell
.
├── icons
│   └── icon.png
├── manifest.json

2.2. 新建 popup 文件夹并创建 index.html 页面 ​

2.2.1. 同时创建 index.css 和 index.js 文件 ​

目前的目录结构

shell
.
├── icons
│   └── icon.png
├── manifest.json
└── popup
    ├── index.css
    ├── index.html
    └── index.js
2.2.2. 填充 popup 代码 ​
  1. index.html
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="./index.css">
</head>
<body>
  <div class="plugin_search">
      <div class="plugin_search_bar">
          <input id="plugin_search_inp" />
          <input id="plugin_search_but" type="button" value="翻译" />
      </div>
      <div class="plugin_span">
          <span>翻译此页面</span>
      </div>
  </div>
  
  <div class="plugin_footer">
      <span>© 2015 Google -</span>
      <a target="_blank" href="https://google.com/">扩展程序选项</a>
      <a target="_blank" href="https://translate.google.com/?source=gtx">Google 翻译</a>
  </div>
</body>
<script src="./index.js"></script>
</html>
  1. index.css
css
*{
  padding: 0;
  margin: 0;
  list-style: none;
  text-decoration: none;
}
.plugin_search{
  margin: 15px 30px 15px 30px;
}
.plugin_search_bar{
  margin: 8px 0 16px 0;
}
#plugin_search_inp{
  font-size: 14px;
  margin: 2px;
  height: 23px;
  width: 70%;
}
#plugin_search_but{
  box-shadow: none;
  background-color: #4d90fe;
  background-image: -webkit-linear-gradient(top,#4d90fe,#4787ed);
  border: 1px solid #3079ed;
  color: #fff;
  border-radius: 2px;
  cursor: default;
  font-size: 11px;
  font-weight: bold;
  text-align: center;
  white-space: nowrap;
  margin-right: 16px;
  height: 27px;
  line-height: 27px;
  width: 54px;
  outline: 0px;
  padding: 0 8px;
  box-sizing: content-box;
}
.plugin_span{
  color: #4285f4;
}
.plugin_footer{
  background-color: #f0f0f0;
  color: #636363;
  font-family: 'Roboto',sans-serif;
  font-size: 11px;
  margin-top: 9px;
  padding: 10px;
  text-align: center;
  width: 400px;
}
.plugin_footer a{
  color: #4285f4;
}
  1. index.js
javascript
const plugin_search_but = document.getElementById('plugin_search_but')
const plugin_search_inp = document.getElementById('plugin_search_inp')
plugin_search_but.onclick = function () {
    alert('plugin_search_inp的值为:' + plugin_search_inp.value.trim())
}

2.3. Manifest.json 文件添加 action 字段 ​

2.3.1. Manifest.json 文件内容 ​
json
{
  "manifest_version": 3,
  "name": "My Chrome Extension",
  "version": "0.0.1",
  "description": "My Chrome Extension Description",
  "action": {
    "default_icon": "icons/icon.png",
    "default_title": "Popup Title",
    "default_popup": "popup/index.html"
  }
}
2.3.2. 点击插件刷新按钮 ​

alt text

可以看到小图标已经展示出来了

alt text

2.3.3. Hover 图标展示 default_title 字段 ​

alt text

2.3.4. 点击图标 ​

可以看到我们的 html 和 css 已经生效了

alt text

2.3.5. 输入框输入,并点击翻译按钮 ​

根据我们 index.js 文件中的代码,我们可以在输入框中输入文案,并点击翻译按钮

alt text

原来这就是惊喜

2.4. Action popup 控制台 ​

点击图标,弹出 popup 页面,右键,会有【检查】选项,点击检查按钮

alt text

到了这一步,我们可以开发一个有点击动作的插件了

3. 添加 background ​

3.1. 如果我们在 index.js 中添加一个 setInterval,每隔几秒弹出一个 alert ​

javascript
setInterval(() => {
  alert('哈哈哈哈,我又出来了')
}, 3000)

保存代码之后,刷新插件,点击插件图标弹出 popup 页面,就会定时弹出 alert 框了

alt text

但是,当我们点击页面某处,把插件 popup 页面隐藏的时候,就会发现 alert 弹框不会再弹出了,只有在此展现 popup 页面的时候才会重新执行 那如果我想的是不展现 popup 的时候,也执行 alert 弹框,那就需要 background 了

3.2. 新建 background 文件夹并创建 service_worker.js 文件 ​

  1. 先把 popup index.js 中的 alert 删除
  2. 在 service_worker.js 中写入代码
typescript
setInterval(() => {
  alert('我是 background service worker 中弹出来的')
}, 3000)
  1. 目录结构
shell
.
├── background
│   └── service_worker.js
├── icons
│   └── icon.png
├── manifest.json
└── popup
    ├── index.css
    ├── index.html

3.3. Manifest.json 文件中添加 background ​

3.3.1. 在上面 action 的基础上添加代码 ​
json
"background": {
  "service_worker": "background/service_worker.js"
}
3.3.2. 点击拓展程序刷新插件 ​
  1. 发现会有错误出现

alt text

  1. 我们先点击检查视图后面的 Service Worker 弹出控制台

alt text

  1. 可以看出报错内容

这是因为 Service Workers 在插件中主要用于后台任务、推送通知等,而不是直接与用户交互。因此,alert 这类弹窗通常无法直接在 Service Worker 中使用。

alt text

  1. 我们点击【错误】按钮,你会发现,每隔三秒会刷新当前错误页面,是因为一直在执行 service_worker.js 中的代码

alt text

  1. 但是当 30s 之后,会发现不刷新了,这个时候我们点击返回,可以看到 service worker 已经被终止了

这个时候如果继续点击 service worker 则会继续触发当前程序

alt text

3.4. 既然不能用 alert,那我们就换个 notifications 用一用 ​

借助 chrome.notifications API,可以通过模板创建内容丰富的通知,并在系统任务栏中向用户显示这些通知。

3.4.1. 修改 service worker 中的代码 ​
typescript
setInterval(() => {
  chrome.notifications.create(
    {
      type: "basic",
      title: "Notifications Title",
      message: "Notifications message to display",
      iconUrl: "../icons/icon.png"
    },
    (notificationId) => {
      console.log('notificationId-->', notificationId)
    }
  );
}, 3000)
3.4.2.刷新插件 ​

会发现还是会报错 说是 notifications 未定义

alt text

这个时候就涉及到 manifest.json 中的另一个字段了,权限(permissions)字段,使用 Chrome.XXX 的 API 绝大多数的时候都需要在 permissions 字段中添加,permissions 是一个 string[]

3.4.3. manifest.json 文件中添加 permissions 字段 ​

添加如下代码

json
"permissions": [
  "notifications"
]
3.4.4. 刷新插件,并点开 service worker 控制台 ​

可以看到控制台中已经有通知的回调返回值了

alt text

那通知呢?

3.4.5. 平台差异 ​

平台差异:对于 Mac OS X 用户,通知的显示方式有所不同。用户看到的不是 Chrome 自己的通知,而是原生 Mac OS X 通知。

点开通知中心,即可看到通知

alt text

3.5. 如果有多个 JS 需要引入怎么办? ​

如果还需要引入 module_1.js 和 module_2.js

3.5.1. manifest.json 中 background 字段添加 type 为 module ​
json
"background": {
  "service_worker": "background/service_worker.js",
  "type": "module"
},
3.5.2. 在 service_worker.js 中 import 引入 ​
javascript
import './module_1.js'
import './module_2.js'
3.5.3. 刷新插件,打开 Service worker 控制台 ​

每个 js 里面只是输出了一句话

alt text

到这一步,我们已经可以操作后台了

4. Action(popup) 和 background(service worker) 之间的通信 ​

点击翻译的时候,发一条信息

alt text

4.1. 先打印下 popup.js 中的 Chrome 数据 ​

typescript
plugin_search_but.onclick = function () {
  // alert('plugin_search_inp的值为:' + plugin_search_inp.value.trim())
  console.log('chrome', chrome)
}

alt text

4.2. 先把 popup.js 中的点击事件修改下 ​

改成点击之后向 Service Worker 发送消息

javascript
plugin_search_but.onclick = function () {
  // alert('plugin_search_inp的值为:' + plugin_search_inp.value.trim())
  console.log('chrome', chrome)
  // 向 Service Worker 发送消息
  chrome.runtime.sendMessage({
    action: 'fromPopup',
    message: 'Hello from Popup!'
  });
}

4.3. 只需要在 service_worker.js 中接收消息即可 ​

把之前的代码注释/删除即可

typescript
chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
  if (message.action === 'fromPopup') {
    chrome.notifications.create(
      {
        type: "basic",
        title: "Notifications Title",
        message: "Notifications message to display",
        iconUrl: "../icons/icon.png"
      },
      (notificationId) => {
        console.log('notificationId-->', notificationId)
      }
    );
  }
});

4.4. 刷新插件 ​

点击图标,弹出 popup 页面,点击翻译按钮,点击消息中心

alt text

5. 内容脚本(Content Script)注入 ​

如果需要在某个页面中注入脚本进去,那就用到了 content_scripts 更新 content script 之后需要刷新对应的页面

5.1. 新建 content 文件夹,并创建 index.js 文件 ​

  1. 写入代码
javascript
console.log('this is content js')
console.log('document', document)
console.log('location', location)
console.log('window', window)
  1. 目录结构
shell
.
├── background
│   ├── module_1.js
│   ├── module_2.js
│   └── service_worker.js
├── content
│   └── index.js
├── icons
│   └── icon.png
├── manifest.json
└── popup
    ├── index.css
    ├── index.html
    └── index.js

5.2. 在 manifest.json 文件中添加 content_scripts ​

json
"content_scripts": [
  {
    "matches": ["https://lkcoffee.com/"],
    "js": ["content/index.js"]
  }
]

可以看出我们引入了 js 文件和匹配了域名

5.3. 打开 https://lkcoffee.com/ 并打开控制台 ​

alt text

5.4. 在当前页面中插入面板框,并可拖拽 ​

使用 jquery

  1. 在 content 中创建 index.css 和 jquery.js(可在 CDN 上下载)
  2. 在 index.css 上写入面板样式
  3. 在 index.js 上创建 dom 和监听鼠标事件
  4. 在 manifest.json 中引入

manifest.json 文件

json
"content_scripts": [
  {
    "matches": ["https://lkcoffee.com/"],
    "css": ["content/index.css"],
    "js": ["content/jquery.js", "content/index.js"]
  }
]

JS/CSS 注入多个时,按照数组顺序注入,所以如果 JS/CSS 有多个的话,需要注意引用关系。 index.css 文件

css
#cj_move_page{
  width: 400px; 
  user-select: none; 
  background: white; 
  border: 1px solid; 
  height: 400px; 
  position: fixed; 
  right: 0; 
  bottom: 0; 
  z-index: 1000001;
}
#cj_move_h3{
  text-align: center; 
  line-height: 40px; 
  cursor: move;
}

index.js 文件

css
console.log('this is content js')
console.log('document', document)
console.log('location', location)
console.log('window', window)

//创建页面函数
function createPage () {
  const page = $('<div id="cj_move_page"></div>')
  const h3 = $('<h3 id="cj_move_h3">My Chrome Ext Content Page</h3>')
  page.append(h3)
  $('body').append(page)
  //拖拽
  drag(cj_move_h3)
}
createPage()

//拖拽
function drag(ele) {
  let oldX, oldY, newX, newY
  ele.onmousedown = function (e) {
    if (!cj_move_page.style.right && !cj_move_page.style.bottom) {
      cj_move_page.style.right = 0
      cj_move_page.style.bottom = 0
    }
    oldX = e.clientX
    oldY = e.clientY
    document.onmousemove = function (e) {
      newX = e.clientX
      newY = e.clientY
      cj_move_page.style.right = parseInt(cj_move_page.style.right) - newX + oldX + 'px'
      cj_move_page.style.bottom = parseInt(cj_move_page.style.bottom) - newY + oldY + 'px'
      oldX = newX
      oldY = newY
    }
    document.onmouseup = function () {
      document.onmousemove = null
      document.onmouseup = null
    }
  }
}

5.5. 刷新 Chrome 插件,并刷新匹配的页面 ​

可以看到右下角有个 content 页面

alt text

到这一步我们已经可以注入自己想要的东西了

6. Content 和 background(Service Worker)通信 ​

如果我想在 content 中点击按钮来触发消息通知要怎么做呢?

6.1. 在刚才的面板页面加一个 button,再加一个 click 事件 ​

先打印下 Chrome 数据,看看有啥

typescript
//创建页面函数
function createPage () {
  const page = $('<div id="cj_move_page"></div>')
  const h3 = $('<h3 id="cj_move_h3">My Chrome Ext Content Page</h3>')
  const but1 = $('<button id="cj_but1">消息通知</button>')
  page.append(h3)
  page.append(but1)
  $('body').append(page)
  $('#cj_but1').click(async (e) => {
    console.log('e', e, chrome)
  })
  //拖拽
  drag(cj_move_h3)
}

6.2. 刷新插件,点击 button 触发事件 ​

alt text

alt text

可以看到 chrome 有个 runtime,里面有 sendMessage 可以发送消息,有 onMessage 可以接收消息

6.3. 在 content_scripts 里面进行消息发送 ​

把 click 事件改一下,加一个消息发送

typescript
$('#cj_but1').click(async (e) => {
  console.log('e', e, chrome)
  // 发送消息
  chrome.runtime.sendMessage({action: "fromContent"});
})

6.4. Service_worker.js 里面进行消息接收 ​

和上面 action 和 service-worker 的通信一样,在一个 if 判断就行

typescript
if (message.action === 'fromContent') {
  chrome.notifications.create(
    {
      type: "basic",
      title: "Notifications Title",
      message: "Notifications message to display",
      iconUrl: "../icons/icon.png"
    },
    (notificationId) => {
      console.log('notificationId-->', notificationId)
    }
  );
}

6.5. 刷新插件 ​

刷新插件,点击按钮,查看消息通知

alt text

7. Action(popup)和 content 通信 ​

因为 content 是注入页面的脚本,所以和 content 通信,需要获取当前 tab 就需要权限(permissions)

7.1. 添加 tab 权限(permissions) ​

添加权限的方式有两种

  1. permissions 中添加 tabs
json
"permissions": [
  "notifications",
  "tabs"
],
  1. 另一种是在主机权限(host_permissions)添加匹配的域名
json
"host_permissions": [
  "https://movie.douban.com/"
]

推荐使用第二种,这种符合权限最小化

7.2. 获取 tab 数据 ​

需要在 popup js 中获取 tab 数据

typescript
const [tab] = await chrome.tabs.query({
  url: ["https://movie.douban.com/*"],
  active: true,
  currentWindow: true
});
console.log('tab', tab)

tab 信息

alt text

7.3. popup 向 content 发送消息,content 接收消息 ​

7.3.1. popup 中使用 chrome.tabs.sendMessage 发送消息,content 中使用 chrome.runtime.onMessage.addListener 接收消息 ​
  1. popup 代码
typescript
const plugin_search_but = document.getElementById('plugin_search_but')
const plugin_search_inp = document.getElementById('plugin_search_inp')
plugin_search_but.onclick = async function () {
  // alert
  // alert('plugin_search_inp的值为:' + plugin_search_inp.value.trim())
  // console.log('chrome', chrome)
  // 向 Service Worker 发送消息
  // chrome.runtime.sendMessage({
  //   action: 'fromPopup',
  //   message: 'Hello from Popup!'
  // });

  const [tab] = await chrome.tabs.query({
    url: ["https://movie.douban.com/*"],
    active: true,
    currentWindow: true
  });
  console.log('tab', tab)
  if (tab) {
    // 使用 chrome.tabs.sendMessage 发送消息
    chrome.tabs.sendMessage(tab.id, {
      action: 'fromPopup2Content'
    })
  }
}
  1. content 监听代码
javascript
chrome.runtime.onMessage.addListener((e) => {
  console.log('e', e)
})
  1. 刷新插件,刷新页面,打开控制台,点击图标,弹出 popup,点击翻译按钮

alt text

7.3.2. popup 中使用 chrome.tabs.connect 发送消息,content 使用 chrome.runtime.onConnect.addListener 来接收消息 ​
  1. popup 代码
typescript
if (tab) {
  // 使用 chrome.tabs.sendMessage 发送消息
  // chrome.tabs.sendMessage(tab.id, {
  //   action: 'fromPopup2Content'
  // })

  const connect = chrome.tabs.connect(tab.id, {name: 'fromPopup2Content'});
  console.log('connect', connect)
  connect.postMessage('这里是弹出框页面,你是谁?')
  connect.onMessage.addListener((mess) => {
    console.log(mess)
  })
}
  1. content 代码
typescript
// chrome.runtime.onMessage.addListener((e) => {
//   console.log('e', e)
// })


chrome.runtime.onConnect.addListener((res) => {
  console.log('contentjs中的 chrome.runtime.onConnect:',res)
  if (res.name === 'fromPopup2Content') {
    res.onMessage.addListener(mess => {
      console.log('contentjs中的 res.onMessage.addListener:', mess)
      res.postMessage('哈哈哈,我是contentjs')
    })
  }
})
  1. 刷新插件,刷新页面,打开控制台,点击图标,弹出 popup,点击翻译按钮

content 页面的日志

alt text

popup 页面的日志

alt text

8. Fetch 请求 ​

V3 版本已经禁止使用 XMLHttpRequest,所有的请求走的都是 fetch

8.1. 我们现在 content_scripts 中的 matches 中加入豆瓣域名(www.douban.com) ​

用于获取数据

***** 通配符匹配

json
"content_scripts": [
  {
    "matches": ["https://lkcoffee.com/", "https://movie.douban.com/*"],
    "css": ["content/index.css"],
    "js": ["content/jquery.js", "content/index.js"]
  }
]

8.2. 在刚才的 index.js 文件中加入按钮,点击的时候触发 fetch 请求 ​

只更改了 createPage 函数,增加了 but2、but3 按钮和 click 事件

javascript
//创建页面函数
function createPage () {
  const page = $('<div id="cj_move_page"></div>')
  const h3 = $('<h3 id="cj_move_h3">My Chrome Ext Content Page</h3>')
  const but1 = $('<button id="cj_but1">消息通知</button>')
  const but2 = $('<button id="cj_but2">content 加载更多</button>')
  const but3 = $('<button id="cj_but3">service worker 加载</button>')
  page.append(h3)
  page.append(but1)
  page.append(but2)
  page.append(but3)
  $('body').append(page)
  // 消息通知按钮事件
  $('#cj_but1').click(async (e) => {
    console.log('e', e, chrome)
    chrome.runtime.sendMessage({action: "fromContent"});
  })
  // content 加载更多按钮事件
  $('#cj_but2').click(async (e) => {
    const response = await fetch("https://movie.douban.com/j/tv/recommend_groups")
    if (!response.ok) {
      throw new Error('Network response was not ok')
    }
    const allData = await response.json()
    console.log('content index allData', allData)
  })
  // service worker 加载按钮事件
  $('#cj_but3').click(async (e) => {
    console.log('e', e, chrome)
    chrome.runtime.sendMessage({action: "fromContentFetch"});
  })
  //拖拽
  drag(cj_move_h3)
}

8.3. service_worker.js 里面进行消息监听 ​

typescript
if (message.action === 'fromContentFetch') {
  const response = await fetch("https://movie.douban.com/j/tv/recommend_groups")
  if (!response.ok) {
    throw new Error('Network response was not ok')
  }
  const allData = await response.json()
  console.log('service worker allData', allData)
}

8.4. 刷新插件并刷新匹配的豆瓣域名页 ​

alt text

8.5. 点击按钮 ​

  1. 打开页面控制台,点击【content 加载更多】

alt text

  1. 打开 service worker 控制台,点击【service worker 加载】

alt text

到这一步,我们已经可以进行接口请求了,整体的链路已经通了

web 开发少不了获取 cookie

使用 chrome.cookies API 查询和修改 Cookie,并在这些 Cookie 发生更改时收到通知。

这就需要在权限(permissions)里面添加 cookies 字段以及在主机权限(host_permissions)里面添加 host

9.1. Manifest.json 文件 ​

json
"host_permissions": [
  "https://*.douban.com/*",
  "https://*.lkcoffee.com/*"
],
"permissions": [
  "notifications",
  "cookies"
]

9.2. Popup 页面获取 cookies ​

typescript
const cookies = await chrome.cookies.getAll({ domain:  '.lkcoffee.com'})
console.log('popup cookies--->', cookies)
const urlCookies = await chrome.cookies.getAll({ url: 'https://leaptest03.lkcoffee.com/' })
console.log("popup urlCookies", urlCookies);

9.3. Service Worker 获取 cookies ​

typescript
const cookies = await chrome.cookies.getAll({ domain:  '.lkcoffee.com'})
console.log('service worker cookies--->', cookies)

alt text

9.4. Content 获取 cookies ​

这个直接用 document.cookie 获取即可

二、代码目录结构 ​

shell
├── background
│   ├── module_1.js
│   ├── module_2.js
│   └── service_worker.js
├── content
│   ├── index.css
│   ├── index.js
│   └── jquery.js
├── icons
│   └── icon.png
├── manifest.json
└── popup
    ├── index.css
    ├── index.html
    └── index.js