6

数栈技术干货:从0到1实现谷歌插件开发探索及应用

 3 years ago
source link: https://my.oschina.net/u/3869098/blog/5019912
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

一、前言

笔者之前一直想了解一些关于谷歌插件的相关知识,希望通过谷歌插件可以更好的认识到谷歌的调试工具,同时也想着可以使用谷歌插件去写一些小工具,既学习了新的东西,又有一定的趣味性。恰逢近段时间需要分享,因此花了两周时间学习和了解谷歌插件相关知识,本篇文章就将笔者在学习过程中的一些思考分享给大家。当然,因为时间的原因,如果笔者对于这一块的认识有不对的地方,欢迎批评指正~

二、什么是谷歌插件

下面先介绍一下谷歌插件的主要组成部分,因为目前谷歌插件使用比较普遍的版本为 2.0 版本,因此本文都是基于 2.0 版本进行使用说明,3.0 版本相较于 2.0 版本更为简便,感兴趣的同学可以点击文章末尾处的链接了解更多相关知识。

(一)配置文件

谷歌插件的核心文件就是配置文件--manifest.json(清单)文件。
其中,manifest.json 文件最基本的 Api 如下:

{
    "name": "chrome extension",
    "version": "1.0.0",
    "manifest_version": 2,
    "description": "A litlle chrome extension demo"
}

主要是包含所写谷歌插件的名称、版本以及相关描述,其中 manifest_version 表示清单文件版本。
manifest.json 作为谷歌插件的核心部分,笔者认为该文件对插件来说就相当于一个入口配置文件,开发人员只需要在这个文件通过配置相应的 js,调用谷歌浏览器提供的 Api,就能实现达到完善这个插件的目的。

1、基本使用Api

在清单文件中有 许多的Api,笔者就不一一列举了,下面为大家介绍几个笔者认为比较重要的 Api,通过以下几个 Api 的介绍,希望可以使读者对于谷歌插件的开发过程有一个大概的认识。

1)browser_action

{
    ...

    "browser_action": {
        "default_icon": {
            "16": "images/get_started16.png",
            "32": "images/get_started32.png"
        },
        "default_title": "谷歌划词翻译",
        "default_popup": "popup.html"
    },

    ...
}

browser_action可设置浏览器右上角的图标及名称。
default_popup 可配置点击图标后会出现的一个小窗口,这里可以做一些临时性的操作

2)permissions

{
    ...

    "permissions": [ "activeTab", "storage", "tabs", "contextMenus" ],
    
    ...
}

permissions 可配置谷歌插件权限申请,如 contextMenus(右键菜单)、 tabs(标签)和storage(插件本地存储)。

3)content_scripts

{
    ...

    "content_scripts": {
        "matches": ["<all_urls>"],
        "css": ["content/content_script.css"],
        "js": ["content/content_script.js"]
    },
    
    ...
}

content-scripts其实就是谷歌插件中向页面注入脚本的一种形式(虽然名为 script,其实还可以包括 CSS 的),借助 content-scripts 可以实现通过配置的方式轻松向指定页面注入 JS 和 CSS。

4)background

{
    ···

    "background": {
        "scripts": ["background.js"],
        "persistent": false
    },

    ···
}

background 是一个常驻的页面,它的生命周期是插件中所有类型页面中最长的,它随着浏览器的打开而打开,随着浏览器的关闭而关闭,所以通常把需要一直运行的、启动就运行的及全局的代码放在 background 里面。

笔者也画了一个上面涉及到的脚本在浏览器中的分布,如下图:

以上就是笔者认为比较重要的一些Api,在介绍完之后,感兴趣的同学就可以开始着手写几个简单的工具,用来实现自己远大的抱负以及理想,升华自己高贵的灵魂。

一些同学可能不太知道开发谷歌插件的前置条件,在这里为大家简单介绍下。

首先需要打开管理扩展程序,打开开发者模式。

点击加载已解压的程序按钮即可加载本地谷歌插件,开发的时候代码如果有更新的话,需要刷新已加载插件,点击关闭后再开启,不必刷新开发页面。

在了解完前置条件后,笔者将在下文中为大家分享谷歌划词翻译插件从0-1的实现过程,通过开发这个工具也可以加深对于大家谷歌插件的认识。

三、谷歌划词翻译插件

谷歌翻译算是笔者使用比较频繁的插件,对于在网页上看到的不懂的英文单词或者句子,直接使用鼠标选中,轻松快捷的翻译出相应的中文。因此在学习的过程中,笔者就在想谷歌浏览器插件的翻译工具是如何实现的呢?

(一)思考

如何去做一个划词翻译插件,首先要考虑的有以下几点:

  • 如何实现翻译效果
  • 如何选中我们需要的元素
  • 选中元素之后如何展示划词翻译面板
  • 所有的浏览器 Tab 都需要支持翻译效果

思考完上面的这些点后,带着这几个疑惑,笔者将在下文一一解答,同时也列举一下遇到的一些点。

(二)划词翻译面板

首先不去考虑该插件的功能,先写下划词翻译的面板的样式,所达到的效果如下:

HTML 代码如下:

<div class="translate-panel show">
    <header>谷歌划词翻译插件<span class="close">X</span></header>
    <main>
        <div class="source">
            <div class="title">英文</div>
            <div class="content">test</div>
        </div>
        <div class="result">
            <div class="title">简体中文</div>
            <div class="content">...</div>
        </div>
    </main>
</div>

将上面的样式简单写好之后,开始考虑如何将划词翻译的面板展示在浏览器当前页面。对于谷歌浏览器来说,在网页上进行的交互是属于 content_scripts 的,需要引入划词翻译面板所需要的 JS 或者 CSS来生成当前面板。

其次,在配置文件中配置 content_scripts引入 JS 文件,动态的生成 DOM 元素。大致的思路就是通过监听到鼠标松开后,去生成翻译面板,在生成的元素上面添加 opacity 样式控制显隐,使用谷歌免费翻译 Api 进行翻译。

其中代码如下所示:

// manifest.json
{
    ...

    "content_scripts": {
        "matches": ["<all_urls>"],
        "css": ["content_script.css"],
        "js": ["content_script.js"]
    },
    "permissions": [
        "activeTab"
    ],

    ...
}

// content_script.js
class TranslatePanel {

    createPanel = () => {
        let wrapper = document.createElement('div')
        wrapper.innerHTML = `
            <header>谷歌划词翻译插件<span class="close">X</span></header>
            <main>
                <div class="source">
                    <div class="title">英文</div>
                    <div class="content">test</div>
                </div>
                <div class="result">
                    <div class="title">简体中文</div>
                    <div class="content">...</div>
                </div>
            </main>
        `
        wrapper.classList.add('translate-panel')
        wrapper.querySelector('.close').onclick = () => {
            this.wrapper.classList.remove('show')
        }
        document.body.appendChild(wrapper)
        this.wrapper = wrapper

    }

    showPanel = () => {
        this.wrapper.classList.add('show')
    }

    translateSelect = (content) => {
        const source = this.wrapper.querySelector('.source .content')
        const result = this.wrapper.querySelector('.result .content')
        source.innerHTML = content
        result.innerHTML = '翻译中...'

        fetch(`https://translate.google.cn/translate_a/single?client=at&sl=en&tl=zh-CN&dt=t&q=${content}`)
            .then(res => res.json())
            .then(res => {
                result.innerHTML = res[0][0][0]
            })
    }

    locationPanel = (target) => {
        this.wrapper.style.top = target.y + 'px'
        this.wrapper.style.left = target.x + 'px'
   }
}

let panel = new TranslatePanel()
panel.createPanel()

window.onmouseup = (target) => {
    // 获取选中内容
    const content = window.getSelection().toString().trim()

    if (!content) return
    panel.locationPanel({ x: target.pageX, y: target.pageY })
    panel.translateSelect(content)
    panel.showPanel()

}

在上面过程的中,笔者使用了谷歌免费的翻译接口,但是这个接口按照目前的设置还是有一点问题,我们暂时不表。现在划词翻译的面板就已经基本写好了。

(三)脚本通信

划词翻译插件开发到这里,细心的同学应该发现了,每次选中单词时都会触发划词翻译功能,此时急需一个控制翻译功能的开关,这个开关就可以放在 popup 脚本上面。

具体的样式的实现就不去介绍了,主要看一下 HTML 结构:

<div class="switch-wrapper">
    <div class="switch-desc">是否启用划词翻译</div>
    <input type="checkbox" class="switch" />
</div>

基本效果如下:

此时面板和划词翻译的面板都已经有了,再考虑一下如何实现 popup 脚本与 content_script 脚本之间的通信。首先,在
popup 脚本,我们在打开窗口的时候需要去查询是否有存储开启划词翻译的状态,同时,
同时当状态发生变更的时候需要将其存储时,再在当前的Tab下面发送请求。

// popup.js
let switchWrapp = document.querySelector('.switch')

chrome.storage.sync.get(['checked'], (target) => {
    if (target) {
        switchWrapp.checked = target.checked
    }
})

switchWrapp.onclick = (e) => {
    chrome.storage.sync.set({ checked: e.target.checked })

    chrome.tabs.query( {active: true, currentWindow: true }, (tabs) => {
        chrome.tabs.sendMessage(tabs[0].id, { checked: e.target.checked })
    })
}

上面代码中的 chrome.storage 可用于存储数据,追踪数据。storage.sync 的作用是让谷歌浏览器的数据同步,这使得在不同 Tab 页上面切换的状态也是可以同步的,同时也不用将数据保存在 background 后台页面中,storage还有很多Api比如监听 storage数据变化的onChanged,这里就不一一介绍了。

将开启或关闭划词翻译的状态发送后,content_script.JS 需要添加监听事件,获取到该状态后,进行关闭或开启操作。

// content_script.js
let checked = false

window.onmouseup = (target) => {
    ···

    if (!content || !checked) return

    ···

}

chrome.storage.sync.get(['checked'], (target) => {
    if (target) checked = target.checked
})

chrome.runtime.onMessage.addListener((target) => {
    if (target) {
        checked = target.checked
    }
})

在开发过程中,发现在当前的 Tab 是可以去完成这个操作的,但是当开启了多个 Tab 的情况下就会出现开启翻译却不能展示翻译面板的情况。

针对以上情况笔者思考了一下,此时应该将checked储存起来,不应该放在 content_script 脚本当中。

// content_script.js
let panel = new TranslatePanel()
panel.createPanel()

window.onmouseup = (target) => {
    // 获取选中内容
    const content = window.getSelection().toString().trim()

    if (!content) return

    window.chrome.storage.sync.get(['checked'], (result) => {
        if (result.checked) {
            panel.locationPanel({ x: target.pageX, y: target.pageY })
            panel.translateSelect(content)
            panel.showPanel()
        }
    })
}

chrome.runtime.onMessage.addListener((target) => {
    if (target.type == 'CHECKED') {
        chrome.storage.sync.set({ checked: target.checked })
    }
})

以上,popup 脚本和 content_script 脚本之间就实现了通信,翻译插件也可以通过 popup 上面的按钮,进行开启或关闭翻译功能。

同理,也可以知道其他模块也是可以通过这种方式去进行通信,不同的是其他脚本向 content_script 通信是需要使用 tabs,先查找到当前的 Tab 在发送请求。

(四)右键直达翻译页面

当关闭划词翻译的时候,直接无法翻译选中内容也不是很友好,这个时候可以设置为点击右键的时候出现翻译菜单项。因为这部分内容需要一直存在就加在 background 中。

// backgrond.js
// 当扩展程序第一次安装、更新至新版本或 Chrome 浏览器更新至新版本时产生
chrome.runtime.onInstalled.addListener(() => {
    chrome.contextMenus.create({
        "id": "SELECT_TRANSLATE",
        "title": "翻译 %s",
        "contexts": ["selection"]
    })
})

chrome.contextMenus.onClicked.addListener((target) => {
    if (target.menuItemId == 'SELECT_TRANSLATE') {
        chrome.tabs.create({url: `https://translate.google.cn/?sl=en&tl=zh-CN&text=${target.selectionText}&op=translate`})
    }
})

(五)跨域问题

开发过程中,有的同学也看出来了一个问题,比如说谷歌的这个翻译的 Api 需要同源的情况下才能正常调用该接口,然后就只能在谷歌翻译的页面中使用划词翻译,场面一度十分尴尬...

那么,正常来说这个划词翻译使用起来也是十分不合理的,接下来就需要解决一下这个跨域的问题。

笔者当时想要尝试的是使用 JSONP,也就是去使用嵌入脚本去进行跨域,发现还是会有一些问题,主要是谷歌的翻译的接口不支持回调函数。

同时也去查阅了一些资料,发现是可以在 content_script 中通知 background,background 后台去调用谷歌翻译的 Api 是来避免这个情况的。

主要因为 background 的权限非常高,几乎可以调用所有的 Chrome 扩展 Api,而且它可以无限制跨域,也就是可以跨域访问任何网站而无需要求对方设置 CORS。

具体添加的代码如下:

// content_script.js
translateSelect = (content) => {
    const source = this.wrapper.querySelector('.source .content')
    const result = this.wrapper.querySelector('.result .content')
    source.innerHTML = content
    result.innerHTML = '翻译中...'

    chrome.runtime.sendMessage({ type: 'QUERY_TRANSLATE', queryContent: content }, (res) => {
        result.innerHTML = res[0][0][0]
    })
}

// background.js
chrome.runtime.onMessage.addListener((request, sender, callBack)  => {
    if (request.type == 'QUERY_TRANSLATE') {
        fetch(`https://translate.google.cn/translate_a/single?client=at&sl=en&tl=zh-CN&dt=t&q=${request.queryContent}`)
            .then(res => res.json())
            .then(res => {
                callBack(res)
            })
        return true
    }
})

background 中的发送消息的监听事件返回 true 是为了与 content_script 的消息通道保持打开,通过异步的方式发送请求。

现在想想,如果使用插件的 background 就可以去跨域去进行请求一些借口,使用不得当的话感觉还是很危险的,可以去获取其他网站的一些信息,由此可见,还是要慎重的进行此操作。

(六)待完善的点

  • 支持其他语言的翻译,谷歌翻译的接口有两个 Api,sl(文本翻译之前的语言) 和 tl(文本需要翻译成的语言) 可通过改变对应的值支持其他语言的翻译;
  • 样式完善,实现先选中图标在进行翻译。多添加一步感觉对交互更友好,不用去进行开关的操作。

介绍完了划词翻译插件,笔者原本是打算再分享一个关于谷歌 devtool 开发工具,开发一个类似于 React Developer Tools 的本地开发工具,但是由于时间也是不太够,涉及到的点比较多,同时查阅了很多的资料对于这一块的介绍也是比较的浅,所以还是决定着重于介绍划词翻译。

相信通过划词翻译的开发能使得读者比较快速的认识到谷歌插件,除了谷歌插件外,如果对于 devtool 工具插件开发有兴趣的同学也可以去了解一下。

另外,有的同学可能会认为目前开发的效率是有一点低的,现在的话谷歌插件的开发也是可以基于 react + antd 去进行开发的,也是可以达到高效快速的去开发一个插件的效果。

四、结尾

在学习谷歌插件的时候,笔者遇到了一些问题,比如说文档比较少,官方文档又是英文的还经常 404,不过呢,好在谷歌浏览器有提供了很多 Api,笔者这边在写插件的时候也感觉到了很多趣味性。本篇文章还是写的比较通俗易懂,如果有什么点写的不对或着不清晰的地方,欢迎大家留言进行探讨。

相关资源及参考:

本文整理自:技术干货丨谷歌插件开发探索及其应用

数栈是云原生—站式数据中台PaaS,我们在github和gitee上有一个有趣的开源项目:FlinkX,记得给我们点个star!star!star!

gitee开源项目:https://gitee.com/dtstack_dev_0/flinkx

github开源项目:https://github.com/DTStack/flinkx

FlinkX是一个基于Flink的批流统一的数据同步工具,既可以采集静态的数据,比如MySQL,HDFS等,也可以采集实时变化的数据,比如MySQL binlog,Kafka等,是全域、异构、批流一体的数据同步引擎,大家如果有兴趣,欢迎来github社区找我们玩~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK