🌕手把手从0撸一个 Chrome 插件,《特别关注-掘金》 功能

10,943 阅读10分钟

特别关注-掘金

引言

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

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

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

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

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

好了,接下来

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

功能展示

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

添加特别关注

动画.gif

取消特别关注

动画2.gif

点击跳转

动画3.gif

你以为就这样结束了?

image.png

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

手动录入

动画5.gif

功能演示完毕后

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

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" }
}

目录结构 和 图标

image.png

follow.png

导入

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

  • 打开: chrome 浏览器
  • 打开: 扩展程序
  • 打开: 开发者模式
  • 选择: 加载已解压的扩展程序
  • 加载: 我们的项目文件夹项目
  • 固定: 扩展程序

image.png

image.png

image.png

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

image.png

展示 - popup

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

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

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

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

并且添加一下 index.html 这个文件内容 - 我放到 码上掘金

code.juejin.cn/pen/7124183…

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

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

image.png

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

image.png

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

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

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

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"
    }
  ],

目录结构

image.png

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

下面讲解下

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

背景 - 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 控制台

image.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"]

添加完毕后,刷新插件

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

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)

刷新插件刷新个人主页

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

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 需要自己找哟,找到后填进去即可,很简单,你们肯定可以的

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👌了。

image.png

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

image.png

手动录入

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

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

分析

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

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
    })
})

刷新插件刷新个人主页

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

image.png

至此

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

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

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

image.png

总结

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

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

加油!

我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!

补充新功能

暗黑模式

掘金在 2023-05-23 新增 暗黑模式

这里仅需要修改一个文件即可,处理 content-script.js 这个文件

  • 将里面的 color 属性,全部替换为
    color: var(--juejin-font-1);
    
  • 将里面的 background-color: #fff; 属性,换成
    background: var(--juejin-layer-1);
    

换完后,由于我是本地,所以直接在扩展工具中,点击更新 即可

image.png

再次刷新掘金页面,就可以得到跟随 系统 设置的 暗黑模式 还是 浅色模式 了🎉🎉

image.png