前言
Chrome
应该是我们每天都会打开的软件了,我在没有用过Chrome
之前,对浏览器似乎没什么要求,360、搜狗都挺好的,但是用过Chrome
之后,之后就再也不会去用别的浏览器了(这里没有贬低其他浏览器的意思)。Chrome
让人青睐的一大原因之一我觉得应该是他的拓展生态吧,也有很多人把它叫做插件,不过它的英文叫做Chrome Extension
,那在这篇文章里面我们就把它叫做扩展了,大家知道是同一个东西就行。
最近在写文章的时候苦于没有图床软件使用,网上的图床多多少少差点意思,所以决定自己弄一个简单的图床。第一时间就想到了把图床的展示形式做成Chrome
拓展,因为它足够的便捷轻量,也可以趁此机会了解一下Chrome
拓展的开发。本文不会过多介绍Chrome
在拓展方面提供的API
能力,在需要用到某一个能力的时候你可以去查询文档,主要专注的是扩展开发过程中需要了解的重要概念,在这里我会展示几个例子来实战这些概念,也是我平时遇到的一些想用工具解决的问题。
开始之前,先来贴一份官方文档,extensions-doc,现在官方推荐的版本是v3
,所以我这里直接贴了v3
的文档。
基本概念
好的,让我们从几个基本概念开始,走进Chrome
扩展开发。这里所说的概念可能会比较枯燥,不过不要着急,下面我们会有实战例子去帮你巩固这些概念,不想看的同学可以直接跳过。
manifest
这是扩展开发的第一步,你需要在这个配置文件里面填入你的扩展的各种信息,下面举一个简单的例子来稍微了解一下这个配置文件,其他更具体的选项建议移步API
文档,这里就不一一赘述。
{
"name": "翻译", //拓展名称
"description": "快速翻译", //拓展描述
"version": "1.0",
"manifest_version": 3, //固定为3
"permissions": [
"storage", //权限申请,如果要使用chrome.xxx这样的api,首先要在这里描述
"activeTab",
"scripting"
],
"action": { //拓展的展示页面
"default_popup": "popup.html"
},
"host_permissions": [//信任的域名
""
],
"content_scripts": [//后面会介绍这个东西
{
"matches": [
"" //只会在匹配规则下执行,all_urls表示匹配所有网页
],
"js": [
"js/content-script.js"
],
"css": [
"css/style.css"
],
"run_at": "document_start" //这里一定要填写,不然对应的脚本不会执行
}
]
}
这里主要会以popup
的形式(点击右上角弹出扩展)来说明拓展的开发,这也是我们最常用的拓展方式。
background-scripts
从名字上看这是一个运行在后台的脚本,实际上它干的事情也差不多是这样。生命周期从浏览器的打开开始,结束于浏览器关闭,它可以使用所有的拓展API
。与popup
页面中的脚本(下文会称为popup.js
)大同小异,最不一样的是他们的生命周期。popup.js
的生命周期随着popup
页面的关闭而结束。在manifest.json
中加入如下代码来注册你的background script
。
"background": {
"service_worker": "background.js"
}
content-scripts
这个属性让开发者可以往当前的tab页面中注入样式或者script
脚本,script
脚本与当前的tab
页面共用dom
元素。
实战开发
讲得再多概念(其实上面讲的不过寥寥数行),不如来实战一把,我们就以开发一个建议的popup
页面为例,学习一下Chrome
扩展的最基本开发,在实际开发的过程中,我相信可以让你更加深刻地去理解扩展开发中的主要概念。
翻译扩展
在看拓展文档的时候,纯英文看的我真是蛋疼,我需要不停的在文档与翻译网站之间来回切换。那我们这里利用一些翻译API
的能力,来实现一个翻译插件。
页面UI
非常的简单(简陋),输入你要翻译的英文,点击GO
帮你翻译成中文,点击复制把翻译结果复制到粘贴板中。
趁着开发之前,我们先来讲一下扩展的目录结构,一个popup
拓展的目录结构大概是下面这个样子的
project
-css
-js
manifest.json
popup.html
popup.js
在这个扩展的开发过程中,我们主要关注popup.html
和popup.js
就行。页面模版内容简陋如下:
在逻辑实现中,翻译服务我用的是百度的API
,免费额度是500万
个字符(应该够我用很久了),具体的接入方式可以点击这里。那么现在已经有了后端接口,只要开始写一些简单的前端逻辑就行。在配置好域名权限后,popup.js
或者background-scripts
发起网络请求是不受跨域限制的,所以开发者可以尽情的挥洒笔墨,但更多时候作为扩展使用者的我们应当提高警惕,尽量去官方市场中下载正版扩展来使用。
这里使用的是fetch
来发起网络请求,直接把输入框的内容当成参数请求API
即可,直接看代码吧。
const input = document.querySelector('#input')
const translateEl = document.querySelector('#translate')
const resultEl = document.querySelector('#result')
const copy = document.querySelector('#copy')
input.focus()
let loading = false
let val = ''
//API调用所需的参数,详情可看文档
const grantType = 'client_credentials' //固定值
const clientId = '你的clientId'
const clientSecret = '你的clientSecret'
const accessTokenUrl = `https://aip.baidubce.com/oauth/2.0/token?grant_type=${grantType}&client_id=${clientId}&client_secret=${clientSecret}`
const translateUrl = `https://aip.baidubce.com/rpc/2.0/mt/texttrans/v1`
//稍微封装一个请求函数
function request(url, config = {}) {
const defaultConfig = {
method: 'GET',
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
}
const requestConfig = Object.assign({}, defaultConfig, config)
if (requestConfig.body) {
requestConfig.body = JSON.stringify(requestConfig.body)
}
return new Promise((resolve, reject) => {
fetch(url, requestConfig).then(res => res.json()).then(data => {
resolve(data)
}).catch(err => {
reject(err)
})
})
}
async function translate(val) {
const tokenData = await getAccessToken();
const { access_token: accessToken } = tokenData
const config = {
method: "POST",
body: {
from: 'en', //这里固定了英->中,实际上如有需要可以做成更灵活的配置
to: 'zh',
q: val
}
}
const url = `${translateUrl}?access_token=${accessToken}`
const data = await request(url, config)
const { result } = data
const dst = result?.trans_result[0]?.dst
resultEl.value = dst
}
function getAccessToken() {
return new Promise(async (resolve, reject) => {
try {
const data = await request(accessTokenUrl)
resolve(data)
} catch (error) {
reject(error)
}
})
}
input.addEventListener('input', e => {
const { value } = e.target
val = value
})
translateEl.addEventListener('click', async () => {
if (!val.trim()) {
return
}
translate(val)
})
上述代码十分简单,这样我们就实现了一个简陋但也许比较方便的翻译扩展。忘了说一句,要调试popup.js
的话直接右键拓展的页面打开开发者工具就行,跟我们平时调试web
一毛一样。
复制
在完成了最基本的翻译功能对接之后,接下来就是如何更方便地使用翻译后的结果。一键点击复制是令人幸福指数增加的操作,所以接下来要做的事情就是点击按钮,将翻译结果复制到粘贴板中。浏览器提供了copy
命令,可以复制选中的内容,具体代码实现如下。
copy.addEventListener('click', () => {
resultEl.select()
try {
document.execCommand('copy')
alert('复制成功')
} catch (error) {
console.log(error)
}
})
这样几行代码就可以将内容复制到粘贴板中了,但是resultEl.select()
这行代码会选中输入框的所有文本并激活输入框,看起来不是那么的舒服。实际上我们可以用一个隐藏起来的输入框,让它执行select
就好了。
let fakeTextarea = null
copy.addEventListener('click', () => {
const value = resultEl.value
if (!fakeTextarea) {
const textarea = document.createElement('textarea')
textarea.style = 'position:absolute;top:-999px;left:-999px'; //隐藏起来
document.body.appendChild(textarea)
fakeTextarea = textarea
}
fakeTextarea.value = value
fakeTextarea.select()
try {
document.execCommand('copy')
alert('复制成功')
} catch (error) {
console.log(error)
}
})
看到这里你肯定已经知道如何通用地复制一个标签的内容,只要将其内容放到隐藏的输入框中执行select
即可。
Token存储
细心的同学已经发现,我们每一次调用翻译接口之前都会去拿一次access_token
,实际上它在一段时间内都是有效的,在这段时间内我们都不需要去取新的access_token
。
由图可以看到它的过期时间是30天,也就是说绝大多数的令牌请求都是可以去掉的,那我们就一定要想办法把它存起来了。如果是在常规的前端开发中我们很容易就想到把它存在localStorage
或者indexDB
等地方。
在扩展开发的场景下,Chrome
也提供了本地存储的API
:chrome.storage
。它与localStorage
具体区别有以下几点:
- 数据可以与
Chrome
同步 - 即使使用隐身模式也可以正常存储
- 用户数据可以存储为对象
- 无需后台页面,内容脚本也可以直接使用
- 提供批量的异步读写
API
,性能更好
值得一提的是,要使用storage
功能,别忘了在manifest.json
加上权限配置。
{
"permissions":[
"storage"
]
}
在了解了API
提供的存储能力后,就可以改造一下我们的代码,把access_token
存起来了,对我们的代码也会做一些如下改造,思路参见如下流程图。
const ACCESS_TOKEN_STORAGE_KEY = 'ACCESS_TOKEN_STORAGE_KEY'
let globalAccessToken = null //内存里也缓存一个
function getResultFromStorage(keys = []) {
return new Promise(resolve => {
//读缓存
chrome.storage.sync.get(keys, result => {
resolve(result)
})
})
}
function setStorage(key, value) {
return new Promise(resolve => {
//写缓存
chrome.storage.sync.set({ [key]: value }, res => {
resolve()
})
})
}
async function translate(val) {
// ······
const accessToken = globalAccessToken ? globalAccessToken : await getAccessToken()
const url = `${translateUrl}?access_token=${accessToken}`
const data = await request(url, config)
const { result, error_code } = data
if (error_code === 110) {
//token过期
await refreshToken()
loading = false
//重放一次请求
translate(val)
} else {
//正常写入结果
}
}
async function refreshToken() {
//刷新token的时候只要把内存的和缓存的清掉即可,这样两处地方都没有值就会发请求拿
globalAccessToken = null
await setStorage(ACCESS_TOKEN_STORAGE_KEY, globalAccessToken)
}
function getAccessToken() {
return new Promise(async (resolve, reject) => {
try {
// 先从缓存拿token
const accessToken = await getResultFromStorage([ACCESS_TOKEN_STORAGE_KEY])
if (!accessToken[ACCESS_TOKEN_STORAGE_KEY]) {
//缓存拿不到就发请求拿,拿到后写入缓存和内存
const data = await request(accessTokenUrl)
globalAccessToken = data.access_token
await setStorage(ACCESS_TOKEN_STORAGE_KEY, globalAccessToken)
} else {
//写入内存
globalAccessToken = accessToken[ACCESS_TOKEN_STORAGE_KEY]
}
resolve(globalAccessToken)
} catch (error) {
reject(error)
}
})
}
以上就是这个popup
拓展的全部内容,希望能够帮助你理解manifest.json
、popup.js
,对你入门Chrome
拓展开发有所帮助。
右键拓展
有的同学可能会说,我还是觉得这个拓展比较鸡肋或者使用起来步骤还是不够简洁。在阅读英文文档的场景下,我希望直接右键选中某一段英文直接就翻译出来。这个需求是比较常见的,而Chrome
也为我们提供了右键菜单的拓展开发。具体的效果图如下:
话不多说,我们马上开始,先简单看下目录结构:
menu
background.js
content-script.js
manifest.json
这里的manifest.json
配置稍有不同,我们一起来看一下:
"permissions": [
"storage",
"activeTab",
"contextMenus" //右键菜单权限记得打开
],
"background": {
"service_worker": "background.js"
},
//域名权限记得打开,不然会被同源策略拦截
"host_permissions": [
"https://aip.baidubce.com/"
]
content-scripts
的配置已经在上面提过了,这里便不再赘述。我们先来思考一下,作为一个右键拓展,应该是每一个页面都有,所以它的点击回调逻辑应该是注册在background.js
里面,因为只有它的生命周期可以满足这个需求。再者,从上面的gif
看来,弹出的内容应该跟现在所处的浏览器tab
是有关系的,所以这个弹出逻辑应该写在content-scripts
中。进而来说,background.js
与content-scripts
没有什么直观的联系,所以这里就需要用到Chrome
提供的通信能力。那么到这里我们的思路已经十分清晰,主要分为以下几步去实现这个扩展即可:
注册菜单
话不多说,直接上代码。
//background.js
const QUICK_TRANSLATE = 'quick-translate'
chrome.contextMenus.create({
id: QUICK_TRANSLATE, //id来区分点击的对应菜单项
title: '快速翻译:%s',
contexts: ['selection'], //当文字被选中时触发,%s是被选中的文字
})
//监听菜单项点击回调
chrome.contextMenus.onClicked.addListener(menuItemClickCallback)
function menuItemClickCallback(info) {
const { menuItemId } = info
if (menuItemId === QUICK_TRANSLATE) {
translateCallback(info)
}
}
async function translateCallback({ selectionText }) {
const res = await translate(selectionText) //之前实现的翻译函数
}
可以看到上面的代码十分简单,注册菜单、监听回调、发起翻译请求拿到结果,流程十分清晰。在拿到结果之后,如何把结果显示出来,最好的方法当然是把结果交给当前使用扩展的tab
来展示,这里就涉及到两个脚本之间的通信。
脚本通信
这里直接上代码就行,实现也十分简单。
//background.js
async function translateCallback({ selectionText }) {
const res = await translate(selectionText)
sendMessageToContentScript({res})
}
async function sendMessageToContentScript(message) {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
chrome.tabs.sendMessage(tab.id, message, function (response) {
console.log(response);
});
}
//content-scripts.js
chrome.runtime.onMessage.addListener(
function (request, sender, sendResponse) {
sendResponse('success')
alert(request.res)
}
);
上面我只是将结果alert
了出来(因为太懒),当然你也可以做成更好的交互。毕竟content-scripts
跟当前的tab
页是共用dom
元素的,所以你可以尽情发挥。
实战小结
在上面,我们介绍了manifest.json
文件的配置,在使用某些API
或者开发某些功能之前记得先来这里注册对应的权限;同时我们介绍了两种插件的类型,一种是右上角弹出,一种是右键菜单,实际上还有别的类型,在这里就不再展开,感兴趣的同学请自行查阅文档;在开发这两种插件的过程中我们了解到了“三种js
”;background.js
是常驻后台的脚本,可以使用任何Chrome
扩展的API
,popup.js
就是弹出类型插件的脚本,它与background.js
十分类似,运用于弹出类型插件的逻辑开发,content-scripts
是扩展注入当前tab
页面的脚本,它可以访问的Chrome
扩展只有我们上面提到的chrome.runtime
、chrome.storage
还有chrome.18n
等,这些API
已经够用了,如果还有别的需求的话,可以通过脚本通信让background.js
来帮忙调用。
background.js
的调试需要你进入Chrome
的扩展页面,点击如下按钮打开其控制台
popup.js
的调试就是在popup页面直接右键打开开发者工具即可
content-scripts
的调试最简单,直接在当前的tab
页打开控制台调试就行
图床扩展
在了解上面的知识后,我们就要做一开始所说的事情了,就是开发一款自己的图床拓展。我们当然是用popup
类型的扩展来开发,上传图片的方式点击、拖拽、复制这三种我们都会盘一下,至于后台服务的话则需要一台云服务器或者其他的云存储服务,我没购买云存储服务,只能自己写一个简单的接口了,实际上使用云存储服务应该是会更方便一些。最后实现的效果大概是这样,话不多说接下来进入开发。
图床实现
我们开发的是一个popup
类型的拓展,配置文件啥的这里就不再说了。其实我们要开发的就是一个上传图片的功能,实现上并没有什么困难的地方。记得利用一些样式把input框隐藏起来,这也是比较公认的做法。要注意的是,一般来说,在popup.html
中是不支持写内嵌的javascript
代码的。
点击、拖拽、粘贴上传
//popup.js
const url = 'yourhostname'
const content = document.querySelector('.content')
content.addEventListener('click', () => {
uploadEL.click()
})
const uploadEL = document.querySelector("#upload")
uploadEL.addEventListener('change', async e => {
const file = e.target.files[0]
upload(file)
})
function request(url, config) {
return new Promise((resolve, reject) => {
fetch(`${url}`, config).then(res => res.json()).then(data => {
resolve(data)
}).catch(err => {
reject(err)
})
})
}
async function upload(file) {
var formData = new FormData();
formData.append('file', file);
const res = await request(`${url}/upload.php`, {
method: 'POST',
body: formData
})
const { code, path } = res
if (code === 200) {
afterUpload(path) //将上传结果回填到页面中
}
}
点击上传的代码也十分简单,没有什么特别值得讲的地方。你会看到我的截图上给出了几种不同结果的复制,这存粹是为了让我自己用起来更舒服而已,你也可以自行定制上传成功后的交互逻辑。
拖拽&复制
拖拽的实现主要依赖drop
事件,不过记得在drop
之前的drag
阶段中需要阻止默认事件,不然的话drop
事件是不会生效的。
content.addEventListener('dragover', e => {
//这里一定要阻止默认事件,要不然drop是不会生效的
e.preventDefault()
})
content.addEventListener('drop', e => {
e.preventDefault()
const file = e.dataTransfer.files[0]
upload(file)
})
复制的实现主要依赖paste
事件,粘贴的内容不一定是图片,所以我们有必要对内容进行一下过滤,实现起来也比较简单。
document.addEventListener('paste', async event => {
if (event.clipboardData || event.originalEvent) {
const clipboardData = (event.clipboardData || event.originalEvent.clipboardData);
const { items } = clipboardData
const { length } = items
let blob = null
for (let i = 0; i < length; i++) {
if (items[i].type.indexOf("image") !== -1) {
blob = items[i].getAsFile()
}
}
upload(blob)
}
})
接口实现
至于上传图片的接口我是用PHP
写的,感兴趣的同学可以看一看,我会把注释写好。
//upload.php
读图片的时候我用的也是接口而不是直接静态资源指向,最好不要直接暴露你的静态资源。
最后
以上就是本文分享的关于Chrome
扩展开发的所有内容,现在你已经了解了一些基本概念以及如何调试,发挥你的主观能动性,开发一些扩展小工具来让你的生活更加方便吧~