谷歌浏览器插件开发中的一些经验

10/24/2024

# 前言

由于目前从事电商行业,为了减小电商部门的小伙伴们的重复劳动,提升工作效率,需要开发一些谷歌插件来辅助运营伙伴们日常管理店铺以及参加各大平台的活动

# 一、什么是谷歌插件

谷歌浏览器插件(Google Chrome Extension)是一种可以在谷歌浏览器(Google Chrome)上安装和运行的软件程序,旨在扩展浏览器的功能,提供额外的功能和定制化选项,以满足用户的特定需求。例如广告拦截、免登录复制文本、登录密码管理、截图、甚至录屏等等,官方文档地址: https://developer.chrome.google.cn/docs/extensions?hl=zh-cn

# 二、插件项目结构介绍

大致项目架构如下(不使用Vue等框架):

chromePlugin/  
    ├── assets/   
    ├── pages/     
    |   ├── tmall
    |   |     ├── getBookInfi.js 
    |   ├── jd 
    |   
    ├── utils/      
    ├── background.js  
    ├── manifest.json  
    ├── popup.html 
    ├── popup.js  
    ├── .gitignore  
    ├── README.md
  • assets/:这个文件夹通常用于存放插件的静态资源,如图片、图标、CSS样式表等。
  • pages/:这个文件夹可以用于存放插件生效的页面对应的js文件
  • utils/:这个文件夹通常用于存放插件的实用工具函数或模块
  • background.js:这个文件是插件的背景脚本
  • manifest.json:这是插件必要的核心配置文件,它包含了插件的所有配置信息,如插件的名称、版本、描述、图标、权限、内容脚本、背景脚本、弹出页面等。它告诉Chrome浏览器如何加载和运行插件。其中 manifest_version字段 推荐都使用3版本 ; content_scripts字段为一个数组, 用于指定脚本生效的地址,可以指定多个地址。
  • popup.html:这个文件定义了用户点击插件图标时弹出页面的结构。当用户点击插件图标时,这个HTML文件定义的内容将会显示为一个弹出窗口。
  • popup.js:这个文件通常用于处理 popup.html 页面中的交互逻辑,例如处理插件登录、存储用户信息(如果插件需要用户登录使用的话)等。

# 三、开发中常见问题解决

1. 捕捉异步载入的页面元素
场景一:在网页加载时,页面中有些元素或片段是异步载入的, 即使你的代码都是在window.onload里执行的,也可能会拿不到对应的元素
场景二:点击某个按钮后会调接口查询数据,并且在一个弹窗中展示,我们又需要等待弹窗出现后再继续进行下一步自动化操作,但是接口响应和弹窗的渲染的时间是不可控的

对于以上的情况,不能简单的使用一个固定时间的定时器延时去执行我们的代码,因为元素展示的时间可能因为不同用户的网络而有所不同,在这里可以使用 Promise.race() 方法来解决这个问题

    //封装一个等待元素出现的方法
    function waitAsyncElementShow(tagName,selector,waitTime,searchTime,parentNode=document){
        let errorTimer = null
        let timer = null
        //失败promise
        const errPromise =  new Promise((resolve,reject)=>{
            errorTimer = setTimeout(function(){
                clearInterval(timer)
                reject('未找到元素')
            },waitTime)
        })

        //成功promise
        const successPromise =  new Promise((resolve,reject)=>{
            timer = setInterval(function(){
                // 1.像天猫这类大型平台的页面,元素的类名的后缀通常是动态生成的随机字符串,需要用includes或startsWith方法来匹配;
                // 2.如果页面中有多个相同固定类名的元素,parentNode可以进一步指定查找范围,以防止匹配到错误的元素;
                // 3.如果等待的元素具有唯一的固定id或者固定类名的话 ,这个方法无需传入tagName,直接通过selector即可匹配; 
                // 4.如果等待的元素的类名或者id的后缀都是动态生成的,且有两个及以上的相同标签名称的元素都使用了这个固定前缀类名,这时下面获取ele时,find()方法里就不能只判断类名,可能还需要根据子节点的信息来对它们进行区分  
                //注:需要根据页面情况来灵活的修改下面这行语句来获取ele等待元素
                const ele = Array.from( parentNode.querySelectorAll(tagName) ).find(item=>item.className.includes(selector))
                if(ele){
                    clearTimeout(errorTimer)
                    clearInterval(timer)
                    resolve('查找元素成功')
                }
            },searchTime)
        })

       return Promise.race([errPromise,successPromise])
    }



    //方法使用示例,以下是我使用tampermonkey自动设置文心一言输入框高度的例子
    window.onload = async function(){
        //文心一言可能使用了一些异步加载机制,window.onload里不一定可以获取到元素,需要等待
        await waitAsyncElementShow('div','editor-container',5000,5)
        const inputBox = Array.from( document.querySelectorAll('div') ).find(item=>item.className.includes('editor-container'))
        console.log(inputBox)
        if(inputBox && inputBox.parentNode){
            inputBox.parentNode.style.minHeight = '160px';
            inputBox.parentNode .style.maxHeight = '500px';
        }
    }