大厂技术 高级前端 Node进阶
点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群
引言
总因白天疯狂撸代码,导致没有太多时间摸鱼逛掘金,很是难受😭!
难得有摸🐟时间,抓紧逛一波掘金,却又要在众多的关注里,翻呀翻,找半天才找到我们最喜爱的 掘金酱[1]、沸点小助手[2] 等优秀的作者们。
而 掘金 又没有 特别关注 这项功能,这就很容易错过 第一时间 去阅读 优秀创作者们 的文章或动态。
所以这里通过 chrome 插件开发,来给 掘金 安排一波 特别关注 功能 ❤️❤️❤️
当然,这也是我对 chrome 插件开发 的一个详细理解(_非常适合入门及进阶_)
好了,接下来

功能展示
先进行一波帅气😎功能展示
添加特别关注

取消特别关注

点击跳转

你以为就这样结束了?

再支持一个点击图标进行 手动录入 模式
手动录入

功能演示完毕后
接下来,下面就开始详细的讲解了,涉及知识点还挺多,坐好,🚈发车!

插件开发
配置 和 目录
若要开始开发 chrome 插件,则需要一个配置文件,就是 manifest.json
文件
manifest.json 配置
我们先将 manifest.json
里面的内容设置如下。
{
// 插件名
"name": "特别关注-掘金",
// 插件描述
"description": "一个 掘金-特别关注用户 的工具",
// 插件版本
"version": "0.0.1",
// 使用的 manifest.json 的版本
"manifest_version": 2,
// 图标
"icons": { "16": "image/follow.png", "48": "image/follow.png", "128": "image/follow.png" }
}
复制代码
目录结构 和 图标


导入
通过上面的简单配置,我们可以先尝试将插件在浏览器中进行展示。
打开: chrome 浏览器
打开: 扩展程序
打开: 开发者模式
选择: 加载已解压的扩展程序
加载: 我们的项目文件夹项目
固定: 扩展程序



此时可以看到我们的图标是一个灰色的状态,并且点击后没有什么效果。

展示 - popup
在展示阶段,就迎来了我们 chrome 插件开发 的第一个知识点。
就是 popup 页面,即 我们点击图标后所展示的页面。
而上面导入后没有效果的原因是,正是我们上面的 manifest.json
配置中,并没有配置 popup 文件,下面添加一下
"browser_action": {
// 我们使用的默认图标
"default_icon": "image/follow.png",
// 鼠标放到图标上,即可展示的 标题 内容
"default_title": "特别关注-掘金",
// 点击按钮后,展示的页面
"default_popup": "index.html"
},
复制代码
并且添加一下 index.html 这个文件内容 -
添加完毕后,_刷新我们的插件_
注:下面所有的操作后,记得都要刷新该插件后,才会出效果

我们的图标就变亮了✨,而且点击后,会出现 popup 页面。

但是呢,相关 js 代码还并没有去填写,所以点击按钮没有什么反应。
这一块的 js 代码 先不着急编写。毕竟先要有页面展示,才能有效果,对吧。
先跟着我的思路 到 掘金 个人主页 的 特别关注列表 开发当中。

插件的前后台
既然要在页面操作,那就离不开 chrome 插件中 两个重要的 知识点
background 和 content_scripts
这两个功能 要想使用,还是需要在 manifest.json
中进行配置,这里进行添加。
"background": {
// 对应的js文件位置
"scripts": ["js/background.js"],
// true 则表示一直开启运行,false 则表示,只通过相关事件驱动运行
// 一般我们设置 false 即可
"persistent": false
},
"content_scripts": [
{
// 指定了,只有在哪几个页面中,才会开启注入操作,是一个数组
"matches": ["https://juejin.cn/user/*", "https://juejin.cn/post/*"],
// 对应的js文件位置
"js": ["js/content-script.js"],
// js文件添加的时机,有三个值可以选择,推荐 document_idle
// document_start :在 dom 页面之前插入 js 代码,dom 未解析完毕。
// document_end : 在 dom 末尾插入js 代码,dom已经解析,但一些图片资源可能未加载完毕。
// document_idle : 浏览器空闲时处理,即 dom 已经解析,资源已经完毕。
"run_at": "document_idle"
}
],
复制代码
目录结构

添加完成后,记得 刷新插件
下面讲解下
它们两个分别 对应的功能 、 相互 通信的方式 、 展示的位置
背景 - background
它是在我们浏览器后台运行的,与我们的前台内容无关
即 不操作我们的相关页面 _DOM_。
那它有什么用呢?
听说熟悉了这个,_Postman_ 都可以不用了,是真的吗?
是的,这个不得不服,确实 👍
不仅仅可以进行请求,而且可以 跨域请求,_B T_ 的是,还是无限制的跨域请求。
还可以进行数据存储
当然大家学会了,可千万别做 不好的事情哟~☀️
content_scripts
它就可以对 DOM 进行操作。
通过 配置指定的 相关 url 的页面,来将 js 代码插入到页面中
我们的相关页面内容展示,都是通过它来进行的。
通信方式
两者通信的前提是,两个配置文件都必须存在,否则通信失败。
通信方式为:
chrome.runtime.onMessage.addListener
chrome.runtime.sendMessage
展示位置
打开 _背景页_,得到 background 控制台
image.png image.png 打开掘金个人主页,_F12_ 得到 content_scripts 控制台

页面开发
上面讲解完 基础 后,终于到 精彩 的地方了🔥🔥🔥!

下面进行主要代码的开发
这里先说明一下后续数据存储的格式
const userlist = [
{
user_name: '掘金酱',
avatar_large: 'https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/mirror-assets/168e0858b6ccfd57fe5~tplv-t2oaga2asx-image.image',
user_id: '1556564194374926'
},
{
user_name: '掘金酱',
avatar_large: 'https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/mirror-assets/168e0858b6ccfd57fe5~tplv-t2oaga2asx-image.image',
user_id: '1556564194374926'
}
]
复制代码
特别关注 - 列表页开发
流程为:
页面加载完毕后执行
获取本地存储的数据
userlist
循环遍历
userlist
的数据,并生成 html 的相关标签内容在指定元素位置处
添加我们定义的元素
content-script.js 代码如下
console.log('这是content script!')
const getStrItem = (obj) => {
const { user_name, avatar_large, user_id } = obj
return `
<a
href="https://juejin.cn/user/${user_id}"
target="_blank"
rel="nofollow noopener noreferrer"
style="display: flex; align-items: center; font-size: 1.25rem; color: #000; margin-bottom: 0.8rem;"
>
<img src="${avatar_large}" alt="" style="width: 45px; height: 45px; border-radius: 50%; margin-right: 1.2rem; object-fit: cover;" />
<span style="margin: 0 0.3em; font-weight: 500">${user_name}</span>
</a>
`
}
const getStrHeader = (userlistLength) => {
return `
<div style="position: absolute; right: -247px; flex: 0 0 auto; margin-left: 1rem; width: 20rem; line-height: 1.2">
<div style="position: fixed; top: 6.766999999999999rem; width: 20rem; transition: all 0.2s">
<div id="wangzaimisu" style="margin-bottom: 1rem; background-color: #fff; border-radius: 2px; max-height: calc(100vh - 90px); overflow-y: auto">
<div style="padding: 1.333rem; font-size: 1.333rem; font-weight: 600; color: #31445b; border-bottom: 1px solid rgba(230, 230, 231, 0.5)">特别关注 - ${userlistLength}</div>
<div style="padding: 1.333rem">
`
}
const getStrFooter = () => {
return `
</div>
</div>
</div>
</div>
`
}
const getStrCenter = (str) => {
if (!str.trim()) {
return `
<div style="text-align:center; font-size: 1.25rem; color: #000; margin-bottom: 1rem;">
<span>暂无特别关注,快去添加吧!</span>
</div>
`
} else {
return str
}
}
const getUserList = () => {
return new Promise((resolve, reject) => {
chrome.storage.sync.get('userlist', (arg) => {
resolve(arg.hasOwnProperty('userlist') ? arg.userlist : [])
})
})
}
const addEle = (str) => {
const stickyWrap = document.querySelector('.main-container')
stickyWrap.insertAdjacentHTML('beforeEnd', str)
}
const main = () => {
getUserList().then((res) => {
let strcenter = ``
res.forEach((item) => {
const stritem = getStrItem(item)
strcenter += stritem
})
const strAll = getStrHeader(res.length) + getStrCenter(strcenter) + getStrFooter()
addEle(strAll)
})
}
window.onload = () => {
setTimeout(() => {
main()
}, 1000)
}
复制代码
可以看到上面的代码中,我们使用到了 _存储_,
那么就引入 另一个知识点 permissions
权限
我们仍需要在 manifest.json
中继续配置,后面还有很多权限,都将会在这里进行添加配置。
"permissions": ["storage"]
复制代码
添加完毕后,_刷新插件_
刷新我们的 _个人主页_,页面展示就有了

特别关注 - 右键菜单
右键菜单,并不需要我们再在 manifest.json
中进行配置
而需要我们在 background.js 中进行定义并创建
const contextMenus = {
id: 'wangzaimisuAdd',
title: '添加为特别关注-掘金',
type: 'radio',
contexts: ['image']
}
const contextMenus2 = {
id: 'wangzaimisuCancel',
title: '取消对该用户特别关注',
type: 'radio',
contexts: ['image']
}
chrome.contextMenus.create(contextMenus)
chrome.contextMenus.create(contextMenus2)
复制代码
刷新插件 、_刷新个人主页_
即可 成功显示 我们的 _右键菜单_。

特别关注 - 右键功能
来到 最最最 重要的 功能模块了 !!!
流程为:
监听右键点击事件
判断是 添加 还是 取消 特别关注
获取本地储存
删除 取消特别关注的 用户
存储数据
发起请求
获得用户数据
获取本地储存
判断是否已经存在,存在则 _更新_,否则 push
存储数据
添加 特别关注
取消 特别关注
更新页面
分割
由于代码太多,我这里将功能拆分为 四大块
代码所在的 文件地址,已经标注,认真看哟!
一、_监听右键点击事件_
// background.js
const baseUrl = 'https://juejin.cn/user/'
const reqQuery = {
aid: '', // 需要自己找哟
uuid: '', // 需要自己找哟
user_id: '',
not_self: '1',
need_badge: '1'
}
/**
* 正则匹配用户 user_id
*/
const regFunc = (str) => {
return str.match(/\d{15,16}/g)[0]
}
chrome.contextMenus.onClicked.addListener((clickData) => {
if (clickData.menuItemId === 'wangzaimisuAdd') {
if (clickData.linkUrl && clickData.linkUrl.includes(baseUrl)) {
// 获取id
const user_id = regFunc(clickData.linkUrl)
reqQuery.user_id = user_id
// 发起请求
requestUserInfo()
} else if (clickData.pageUrl.includes(baseUrl)) {
// 获取id
const user_id = regFunc(clickData.pageUrl)
reqQuery.user_id = user_id
// 发起请求
requestUserInfo()
}
}
if (clickData.menuItemId === 'wangzaimisuCancel') {
if (clickData.linkUrl && clickData.linkUrl.includes(baseUrl)) {
// 获取id
const user_id = regFunc(clickData.linkUrl)
// 从storage中删除
cnacelFollowUserInfo(user_id)
} else if (clickData.pageUrl.includes(baseUrl)) {
// 获取id
const user_id = regFunc(clickData.pageUrl)
// 从storage中删除
cnacelFollowUserInfo(user_id)
}
}
})
复制代码
上面的 reqQuery
中的 aid
和 uuid
需要自己找哟,找到后填进去即可,很简单,你们肯定可以的

二、 请求、存储 的方法
// background.js
/**
* 获取本地存储
*/
const getUserList = () => {
return new Promise((resolve, reject) => {
chrome.storage.sync.get('userlist', (arg) => {
resolve(arg.hasOwnProperty('userlist') ? arg.userlist : [])
})
})
}
/**
* 添加 本地存储
*/
const setStorage = (userListItem, tabId) => {
return new Promise((resolve, reject) => {
getUserList().then((userlist) => {
let flag = true
const { user_id } = userListItem
// 判断是否存在相同的用户,有则更新
for (let i = 0; i < userlist.length; i++) {
if (userlist[i].user_id === user_id) {
userlist[i] = userListItem
flag = false
}
}
// 没有则push
if (flag) userlist.push(userListItem)
// 进行存储
chrome.storage.sync.set({ userlist: userlist })
// 判断入口,是 手动录入 还是 右键添加
if (tabId) {
sendDataPopup(tabId)
} else {
sendData()
}
resolve()
})
})
}
/**
* 进行请求
* @param {*} tabId 页面id
*/
const requestUserInfo = (tabId = 0) => {
const reqUrl = `https://api.juejin.cn/user_api/v1/user/get?aid=${reqQuery.aid}&uuid=${reqQuery.uuid}&user_id=${reqQuery.user_id}¬_self=${reqQuery.not_self}&need_badge=${reqQuery.need_badge}`
return new Promise((resolve, reject) => {
fetch(reqUrl)
.then((response) => response.text())
.then((text) => {
const resObj = JSON.parse(text)
const { user_name, avatar_large, user_id } = resObj.data
const userListItem = {
user_name,
avatar_large,
user_id
}
console.log(userListItem, 'userListItem')
setStorage(userListItem, tabId)
.then(() => {
resolve()
})
.catch((error) => {
reject(2)
})
})
.catch((error) => {
console.log(error)
reject(1)
})
})
}
/**
* 取消 特别关注
*/
const cnacelFollowUserInfo = (user_id) => {
getUserList().then((userlist) => {
const newUserList = userlist.filter((item) => {
return item.user_id !== user_id
})
chrome.storage.sync.set({ userlist: newUserList })
sendData()
})
}
复制代码
三、_数据通信_
// background.js
/**
* 页面右键菜单,只需要 background 向 content-script 发送数据
*/
const sendData = () => {
chrome.tabs.query(
{
active: true,
currentWindow: true
},
(tabs) => {
let message = {
refresh: true
}
chrome.tabs.sendMessage(tabs[0].id, message, (res) => {
console.log('background => content-script')
})
}
)
}
/**
* 点击 popup 里面的确认,发过来的消息
*/
const sendDataPopup = (tabId) => {
let message = {
refresh: true
}
chrome.tabs.sendMessage(tabId, message, (res) => {
console.log('bg=>content, popup')
// console.log('res', res)
})
}
复制代码
四、更新页面
// content-script.js
/**
* 清除页面 DOM
*/
const clearDom = () => {
return new Promise((resolve, reject) => {
const myPluginEle = document.getElementById('wangzaimisu')
if (myPluginEle) {
myPluginEle.parentNode.removeChild(myPluginEle)
}
resolve()
})
}
/**
* 监听 background 传来的 数据
*/
chrome.runtime.onMessage.addListener((data, sender, sendResponse) => {
if (data.refresh) {
clearDom()
.then((res) => {
main()
})
.finally(() => {
return true
})
}
})
复制代码
权限
上面代码写完后,需要我们在 manifest.json
的 permissions
中,配置相关权限,才能解锁功能
"permissions": [
// 支持访问浏览器选项卡
"tabs",
// 获取当前活动选项卡
"activeTab",
// 存储
"storage",
// 右键菜单
"contextMenus",
// 请求地址
"https://api.juejin.cn/user_api/v1/user"
]
复制代码
刷新插件 、_刷新个人主页_
大家就可以去试试了,效果已经ok👌了。

能够看到这里的小伙伴们,给你们比个心 🤞🤞🤞,鼓个掌👏👏👏

手动录入
最后的 手动录入 效果 也是一个很重要的知识点
是我们的 popup 与 background 之间的相关逻辑
分析
由于我们不清楚,是在哪个页面中点击的图标按钮,可能 非掘金 网站,例如:

那我们该如何知道,自己是在掘金的个人主页中点击的呢?
此时需要 permissions
中的一个权限为 activeTab
通过 chrome.tabs.getSelected
可以获取当前页面的 url 地址,我们来进行匹配即可。
流程为:
点击确定按钮
发起请求
存储数据
更新页面
获取 input 输入框里面的内容
校验内容格式是否正确,校验 url 是否是 掘金
与 background 通信
点击取消按钮
清除 input 输入框内容
在上面我们只是将 popup 页面编写好了,并没有写 js 逻辑,现在可以动手了
popup 逻辑
// js/index.js
// 通信需要 activeTab
const tabUrlList = ['https://juejin.cn/user', 'https://juejin.cn/post']
/**
* 匹配我们当前打开的url,是否是这两个中的
*/
const existenceFunc = (tabUrl) => {
return tabUrlList.filter((item) => {
return tabUrl.includes(item)
}).length
}
window.onload = () => {
const wzmsInput = document.getElementById('wzmsInput')
const wzmsBtnCancel = document.getElementById('wzmsBtnCancel')
const wzmsBtnConfirm = document.getElementById('wzmsBtnConfirm')
let tabId
let tabUrl
chrome.tabs.getSelected(null, function (tab) {
// 先获取当前页面的tabID
tabId = tab.id
tabUrl = tab.url
})
wzmsBtnCancel.onclick = function () {
wzmsInput.value = ''
}
wzmsBtnConfirm.onclick = function () {
if (existenceFunc(tabUrl)) {
// 发送消息给 background
chrome.runtime.sendMessage({ user_id: wzmsInput.value, tabId }, function (res) {
wzmsInput.value = ''
})
}
}
}
复制代码
background 逻辑
/**
* 接收到 popup 发来的消息
*/
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const { user_id, tabId } = message
reqQuery.user_id = user_id
let flag = false
requestUserInfo(tabId)
.then(() => {
flag = true
})
.catch((res) => {
flag = false
console.log('fail', fail)
console.log('res', res)
})
.finally(() => {
console.log('ssss', flag)
flag ? sendResponse('success') : sendResponse('fail')
return true
})
})
复制代码
刷新插件 、_刷新个人主页_
最终我们的 _手动录入_,也成功展示🎉🎉🎉

至此
特别关注 - 掘金 所有功能开发完毕。
当然还可以在此基础上开发更多功能,小伙伴们都可以发挥想象。
都到这了,是不是该咳咳 点个赞❤️❤️❤️,收藏一下呢😘😘😘

总结
个人感觉功能挺不错,平时自己用起来也挺舒服的
当然是希望自己的文章能够帮助更多小伙伴快速入门 _chrome 插件开发_,少踩坑!
加油!
关于本文
作者:旺仔米苏
https://juejin.cn/post/7124085369926074399
Node 社群
我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。
如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:
1. 点个「在看」,让更多人也能看到这篇文章
2. 订阅官方博客 www.inode.club 让我们一起成长
点赞和在看就是最大的支持