手把手教你撸一个 Chrome 插件,实现《特别关注》 功能

大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群

引言

总因白天疯狂撸代码,导致没有太多时间摸鱼逛掘金,很是难受😭!

难得有摸🐟时间,抓紧逛一波掘金,却又要在众多的关注里,翻呀翻,找半天才找到我们最喜爱的 掘金酱[1]、沸点小助手[2] 等优秀的作者们。

而 掘金 又没有 特别关注 这项功能,这就很容易错过 第一时间 去阅读 优秀创作者们 的文章或动态。

所以这里通过 chrome 插件开发,来给 掘金 安排一波 特别关注 功能 ❤️❤️❤️

当然,这也是我对 chrome 插件开发 的一个详细理解(_非常适合入门及进阶_)

好了,接下来

outside_default.png
Snipaste_2022-07-23_23-13-17.png

功能展示

先进行一波帅气😎功能展示

添加特别关注
outside_default.png

取消特别关注
outside_default.png

点击跳转
outside_default.png

你以为就这样结束了?

outside_default.png
image.png

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

手动录入
outside_default.png

功能演示完毕后

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

outside_default.png
image.png

插件开发

配置 和 目录

若要开始开发 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" }
}
复制代码
目录结构 和 图标
outside_default.png
image.png
outside_default.png
follow.png
导入

通过上面的简单配置,我们可以先尝试将插件在浏览器中进行展示。

  • 打开: chrome 浏览器

  • 打开: 扩展程序

  • 打开: 开发者模式

  • 选择: 加载已解压的扩展程序

  • 加载: 我们的项目文件夹项目

  • 固定: 扩展程序

outside_default.png
image.png
outside_default.png
image.png
outside_default.png
image.png

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

outside_default.png
image.png
展示 - popup

在展示阶段,就迎来了我们 chrome 插件开发 的第一个知识点。

就是 popup 页面,即 我们点击图标后所展示的页面。

而上面导入后没有效果的原因是,正是我们上面的 manifest.json 配置中,并没有配置 popup 文件,下面添加一下

"browser_action": {
    // 我们使用的默认图标
    "default_icon": "image/follow.png",
    // 鼠标放到图标上,即可展示的 标题 内容
    "default_title": "特别关注-掘金", 
    // 点击按钮后,展示的页面
    "default_popup": "index.html"
  },
复制代码

并且添加一下 index.html 这个文件内容 -

添加完毕后,_刷新我们的插件_

注:下面所有的操作后,记得都要刷新该插件后,才会出效果

outside_default.png
image.png

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

outside_default.png
image.png

但是呢,相关 js 代码还并没有去填写,所以点击按钮没有什么反应。

这一块的 js 代码 先不着急编写。毕竟先要有页面展示,才能有效果,对吧。

先跟着我的思路 到 掘金 个人主页 的 特别关注列表 开发当中。

outside_default.png
3d70ef3502dcd85af79389180098fc9.jpg

插件的前后台

既然要在页面操作,那就离不开 chrome 插件中 两个重要的 知识点

backgroundcontent_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"
    }
  ],
复制代码

目录结构

outside_default.png
image.png

添加完成后,记得 刷新插件

下面讲解下

它们两个分别 对应的功能 、 相互 通信的方式展示的位置

背景 - background

它是在我们浏览器后台运行的,与我们的前台内容无关

即 不操作我们的相关页面 _DOM_。

  • 那它有什么用呢?

  • 听说熟悉了这个,_Postman_ 都可以不用了,是真的吗?

是的,这个不得不服,确实 👍

  • 不仅仅可以进行请求,而且可以 跨域请求,_B T_ 的是,还是无限制的跨域请求。

  • 还可以进行数据存储

当然大家学会了,可千万别做 不好的事情哟~☀️

content_scripts

它就可以对 DOM 进行操作。

通过 配置指定的 相关 url 的页面,来将 js 代码插入到页面中

我们的相关页面内容展示,都是通过它来进行的。

通信方式

两者通信的前提是,两个配置文件都必须存在,否则通信失败。

通信方式为:

  • chrome.runtime.onMessage.addListener

  • chrome.runtime.sendMessage

展示位置
  • 打开 _背景页_,得到 background 控制台

    outside_default.png
    image.png
    outside_default.png
    image.png
  • 打开掘金个人主页,_F12_ 得到 content_scripts 控制台

outside_default.png
image.png

页面开发

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

outside_default.png
a0ea0029cda713075f15f182de27df1.jpg

下面进行主要代码的开发

这里先说明一下后续数据存储的格式

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"]
复制代码

添加完毕后,_刷新插件_

刷新我们的 _个人主页_,页面展示就有了

outside_default.png
image.png
特别关注 - 右键菜单

右键菜单,并不需要我们再在 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)
复制代码

刷新插件 、_刷新个人主页_

即可 成功显示 我们的 _右键菜单_。

outside_default.png
image.png
特别关注 - 右键功能

来到 最最最 重要的 功能模块了 !!!

流程为:

  • 监听右键点击事件

  • 判断是 添加 还是 取消 特别关注

    • 获取本地储存

    • 删除 取消特别关注的 用户

    • 存储数据

    • 发起请求

    • 获得用户数据

    • 获取本地储存

    • 判断是否已经存在,存在则 _更新_,否则 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 中的 aiduuid 需要自己找哟,找到后填进去即可,很简单,你们肯定可以的

outside_default.png
ba2620cdf405b8fd184134ed0578c44.jpg
二、 请求、存储 的方法
// 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}&not_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.jsonpermissions 中,配置相关权限,才能解锁功能

"permissions": [
    // 支持访问浏览器选项卡
    "tabs",
    // 获取当前活动选项卡
    "activeTab",
    // 存储
    "storage", 
    // 右键菜单
    "contextMenus",
    // 请求地址
    "https://api.juejin.cn/user_api/v1/user"
  ]
复制代码

刷新插件 、_刷新个人主页_

大家就可以去试试了,效果已经ok👌了。

outside_default.png
image.png

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

outside_default.png
image.png

手动录入

最后的 手动录入 效果 也是一个很重要的知识点

是我们的 popupbackground 之间的相关逻辑

分析

由于我们不清楚,是在哪个页面中点击的图标按钮,可能 非掘金 网站,例如:

outside_default.png
image.png

那我们该如何知道,自己是在掘金的个人主页中点击的呢?

此时需要 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
    })
})
复制代码

刷新插件 、_刷新个人主页_

最终我们的 _手动录入_,也成功展示🎉🎉🎉

outside_default.png
image.png

至此

特别关注 - 掘金 所有功能开发完毕。

当然还可以在此基础上开发更多功能,小伙伴们都可以发挥想象。

都到这了,是不是该咳咳 点个赞❤️❤️❤️,收藏一下呢😘😘😘

outside_default.png
image.png

总结

个人感觉功能挺不错,平时自己用起来也挺舒服的

当然是希望自己的文章能够帮助更多小伙伴快速入门 _chrome 插件开发_,少踩坑!

加油!

关于本文

作者:旺仔米苏

https://juejin.cn/post/7124085369926074399

Node 社群

我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

8ca960409b6eec0016382c80cc408af3.jpeg

如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:

1. 点个「在看」,让更多人也能看到这篇文章

2. 订阅官方博客 www.inode.club 让我们一起成长

点赞和在看就是最大的支持

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值