走进Chrome拓展开发,定制自己的图床扩展

前言 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的形式(点击右上角弹出扩展)来说明拓展的开发,这也是我们最常用的拓展方式。
走进Chrome拓展开发,定制自己的图床扩展
文章图片

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的能力,来实现一个翻译插件。
走进Chrome拓展开发,定制自己的图床扩展
文章图片

页面UI非常的简单(简陋),输入你要翻译的英文,点击GO帮你翻译成中文,点击复制把翻译结果复制到粘贴板中。
趁着开发之前,我们先来讲一下扩展的目录结构,一个popup拓展的目录结构大概是下面这个样子的
project -css -js manifest.json popup.html popup.js

在这个扩展的开发过程中,我们主要关注popup.htmlpopup.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 = https://www.it610.com/article/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 = https://www.it610.com/article/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 = https://www.it610.com/article/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一毛一样。
走进Chrome拓展开发,定制自己的图床扩展
文章图片

复制
在完成了最基本的翻译功能对接之后,接下来就是如何更方便地使用翻译后的结果。一键点击复制是令人幸福指数增加的操作,所以接下来要做的事情就是点击按钮,将翻译结果复制到粘贴板中。浏览器提供了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 = https://www.it610.com/article/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 = https://www.it610.com/article/value fakeTextarea.select() try { document.execCommand('copy') alert('复制成功') } catch (error) { console.log(error) } })

看到这里你肯定已经知道如何通用地复制一个标签的内容,只要将其内容放到隐藏的输入框中执行select即可。
Token存储
细心的同学已经发现,我们每一次调用翻译接口之前都会去拿一次access_token,实际上它在一段时间内都是有效的,在这段时间内我们都不需要去取新的access_token
走进Chrome拓展开发,定制自己的图床扩展
文章图片

由图可以看到它的过期时间是30天,也就是说绝大多数的令牌请求都是可以去掉的,那我们就一定要想办法把它存起来了。如果是在常规的前端开发中我们很容易就想到把它存在localStorage或者indexDB等地方。
在扩展开发的场景下,Chrome也提供了本地存储的APIchrome.storage。它与localStorage具体区别有以下几点:
  • 数据可以与Chrome同步
  • 即使使用隐身模式也可以正常存储
  • 用户数据可以存储为对象
  • 无需后台页面,内容脚本也可以直接使用
  • 提供批量的异步读写API,性能更好
值得一提的是,要使用storage功能,别忘了在manifest.json加上权限配置。
{ "permissions":[ "storage" ] }

在了解了API提供的存储能力后,就可以改造一下我们的代码,把access_token存起来了,对我们的代码也会做一些如下改造,思路参见如下流程图。
走进Chrome拓展开发,定制自己的图床扩展
文章图片

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 = https://www.it610.com/article/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 = https://www.it610.com/article/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.jsonpopup.js,对你入门Chrome拓展开发有所帮助。
右键拓展 有的同学可能会说,我还是觉得这个拓展比较鸡肋或者使用起来步骤还是不够简洁。在阅读英文文档的场景下,我希望直接右键选中某一段英文直接就翻译出来。这个需求是比较常见的,而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.jscontent-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扩展的APIpopup.js就是弹出类型插件的脚本,它与background.js十分类似,运用于弹出类型插件的逻辑开发,content-scripts是扩展注入当前tab页面的脚本,它可以访问的Chrome扩展只有我们上面提到的chrome.runtimechrome.storage还有chrome.18n等,这些API已经够用了,如果还有别的需求的话,可以通过脚本通信让background.js来帮忙调用。
background.js的调试需要你进入Chrome的扩展页面,点击如下按钮打开其控制台
走进Chrome拓展开发,定制自己的图床扩展
文章图片

popup.js的调试就是在popup页面直接右键打开开发者工具即可
content-scripts的调试最简单,直接在当前的tab页打开控制台调试就行
图床扩展 在了解上面的知识后,我们就要做一开始所说的事情了,就是开发一款自己的图床拓展。我们当然是用popup类型的扩展来开发,上传图片的方式点击、拖拽、复制这三种我们都会盘一下,至于后台服务的话则需要一台云服务器或者其他的云存储服务,我没购买云存储服务,只能自己写一个简单的接口了,实际上使用云存储服务应该是会更方便一些。最后实现的效果大概是这样,话不多说接下来进入开发。
走进Chrome拓展开发,定制自己的图床扩展
文章图片

图床实现 我们开发的是一个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 = https://www.it610.com/article/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 = https://www.it610.com/article/(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拓展开发,定制自己的图床扩展】以上就是本文分享的关于Chrome扩展开发的所有内容,现在你已经了解了一些基本概念以及如何调试,发挥你的主观能动性,开发一些扩展小工具来让你的生活更加方便吧~

    推荐阅读