8
头图

Key knowledge of Chrome plug-in development

This article first introduces several core knowledge points of the plug-in, and then deeply memorizes it from the perspective of actual project combat.

Introduction

Chrome Extension ) in the upper right corner of Google Chrome is actually a lower-level browser function extension. Chrome plug-in is a software developed with Web technology to enhance browser functions. It is actually a suffix compressed package composed of HTML, CSS, JS, pictures and other resources.

Core introduction

manifest.json

This is the most important and indispensable file for a Chrome plug-in. It is used to configure all plug-in-related configurations and must be placed in the root directory. Among them, manifest_version , name , and version are indispensable, and description and icons are recommended.

The following are some common configuration items, all with Chinese annotations. For complete configuration documents, please click here .

{
    // 清单文件的版本,这个必须写,而且必须是2
    "manifest_version": 2,
    // 插件的名称
    "name": "demo",
    // 插件的版本
    "version": "1.0.0",
    // 插件描述
    "description": "简单的Chrome扩展demo",
    // 图标,一般偷懒全部用一个尺寸的也没问题
    "icons":
    {
        "16": "img/icon.png",
        "48": "img/icon.png",
        "128": "img/icon.png"
    },
    // 会一直常驻的后台JS或后台页面
    "background":
    {
        // 2种指定方式,如果指定JS,那么会自动生成一个背景页
        "page": "background.html"
        //"scripts": ["js/background.js"]
    },
    // 浏览器右上角图标设置,browser_action、page_action、app必须三选一
    "browser_action": 
    {
        "default_icon": "img/icon.png",
        // 图标悬停时的标题,可选
        "default_title": "这是一个示例Chrome插件",
        "default_popup": "popup.html"
    },
    // 当某些特定页面打开才显示的图标
    /*"page_action":
    {
        "default_icon": "img/icon.png",
        "default_title": "我是pageAction",
        "default_popup": "popup.html"
    },*/
    // 需要直接注入页面的JS
    "content_scripts": 
    [
        {
            //"matches": ["http://*/*", "https://*/*"],
            // "<all_urls>" 表示匹配所有地址
            "matches": ["<all_urls>"],
            // 多个JS按顺序注入
            "js": ["js/jquery-1.8.3.js", "js/content-script.js"],
            // JS的注入可以随便一点,但是CSS的注意就要千万小心了,因为一不小心就可能影响全局样式
            "css": ["css/custom.css"],
            // 代码注入的时间,可选值: "document_start", "document_end", or "document_idle",最后一个表示页面空闲时,默认document_idle
            "run_at": "document_start"
        },
        // 这里仅仅是为了演示content-script可以配置多个规则
        {
            "matches": ["*://*/*.png", "*://*/*.jpg", "*://*/*.gif", "*://*/*.bmp"],
            "js": ["js/show-image-content-size.js"]
        }
    ],
    // 权限申请
    "permissions":
    [
        "contextMenus", // 右键菜单
        "tabs", // 标签
        "notifications", // 通知
        "webRequest", // web请求
        "webRequestBlocking",
        "storage", // 插件本地存储
        "http://*/*", // 可以通过executeScript或者insertCSS访问的网站
        "https://*/*" // 可以通过executeScript或者insertCSS访问的网站
    ],
    // 普通页面能够直接访问的插件资源列表,如果不设置是无法直接访问的
    "web_accessible_resources": ["js/inject.js"],
    // 插件主页,这个很重要,不要浪费了这个免费广告位
    "homepage_url": "https://www.baidu.com",
    // 覆盖浏览器默认页面
    "chrome_url_overrides":
    {
        // 覆盖浏览器默认的新标签页
        "newtab": "newtab.html"
    },
    // Chrome40以前的插件配置页写法
    "options_page": "options.html",
    // Chrome40以后的插件配置页写法,如果2个都写,新版Chrome只认后面这一个
    "options_ui":
    {
        "page": "options.html",
        // 添加一些默认的样式,推荐使用
        "chrome_style": true
    },
    // 向地址栏注册一个关键字以提供搜索建议,只能设置一个关键字
    "omnibox": { "keyword" : "go" },
    // 默认语言
    "default_locale": "zh_CN",
    // devtools页面入口,注意只能指向一个HTML文件,不能是JS文件
    "devtools_page": "devtools.html"
}
content-scripts

The so-called content-scripts is actually a form of injecting scripts into the page in the Chrome plug-in (although it is called script, it can also include css). With the help of content-scripts we can easily inject JS and specific pages into the specified page through configuration. CSS (if you need dynamic injection, you can refer to the following), the most common such as: ad blocking, page CSS customization, and so on.

Sample configuration:

{
    // 需要直接注入页面的JS
    "content_scripts": 
    [
        {
            //"matches": ["http://*/*", "https://*/*"],
            // "<all_urls>" 表示匹配所有地址
            "matches": ["<all_urls>"],
            // 多个JS按顺序注入
            "js": ["js/jquery-1.8.3.js", "js/content-script.js"],
            // JS的注入可以随便一点,但是CSS的注意就要千万小心了,因为一不小心就可能影响全局样式
            "css": ["css/custom.css"],
            // 代码注入的时间,可选值: "document_start", "document_end", or "document_idle",最后一个表示页面空闲时,默认document_idle
            "run_at": "document_start"
        }
    ],
}

Special attention, if you do not actively specify run_at as document_start (default is document_idle ), the following code will not take effect:

document.addEventListener('DOMContentLoaded', function()
{
    console.log('我被执行了!');
});

content-scripts shares DOM with the original page, but does not share JS. If you want to access the page JS (for example, a certain JS variable), you can only use injected js . content-scripts cannot access most of chrome.xxx.api , except for the following 4 types:

  • chrome.extension(getURL , inIncognitoContext , lastError , onRequest , sendRequest)
  • chrome.i18n
  • chrome.runtime(connect , getManifest , getURL , id , onConnect , onMessage , sendMessage)
  • chrome.storage

In fact, don't be pessimistic when you see this. These APIs are enough for most of the time. If you have to call other APIs, you can also use communication to make the background call for you (the communication will be described in detail later).

background

The background (let’s translate it this way) is a resident page. Its life cycle is the longest of all types of pages in the plug-in. It opens when the browser is opened, and closes when the browser is closed, so Usually put the global code that needs to be running all the time, running on startup, and putting it in the background.

The background permission is very high, almost all Chrome extension APIs (except devtools) can be called, and it can cross domains without restriction, that is, cross domains can visit any website without requiring the other party to set CORS .

After testing, it is not only the background, but all the chrome-extension://id/xx.html opened directly through 0614a8e30ec448 can cross domains without restriction.

In the configuration, background can specify a webpage page scripts , and Chrome will automatically generate a default webpage for this JS:

{
    // 会一直常驻的后台JS或后台页面
    "background":
    {
        // 2种指定方式,如果指定JS,那么会自动生成一个背景页
        "page": "background.html"
        //"scripts": ["js/background.js"]
    },
}

It should be noted that although you can chrome-extension://xxx/background.html , the background page you open is not the same as the one that has been running in the background. In other words, you can open countless background.html , but they are really in the background. There is only one resident, and you can never see its interface, you can only debug its code.

event-pages

Here is an introduction to event-pages , what is it? In view of the background life cycle is too long, mounting the background for a long time may affect performance, so Google has made another event-pages . In the configuration file, the only difference between it and background is the addition of a persistent parameter:

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

Its life cycle is: it is loaded when it is needed, and it is closed when it is idle. What does it mean when it is needed? For example, the first installation, plug-in update, content-script to send messages to it, and so on.

In addition to the configuration file changes, there are also some minor changes in the code. Just a brief understanding of this is enough. Under normal circumstances, the background will not consume performance.

popup

popup is a small window webpage that opens when you click on the browser_action or page_action . When the focus leaves the webpage, it will be closed immediately. It is generally used for some temporary interaction.

博客园网摘插件popup效果

popup can contain any HTML content you want, and it will adapt to the size. You can default_popup field, or you can call the setPopup() method.

Configuration method:

{
    "browser_action":
    {
        "default_icon": "img/icon.png",
        // 图标悬停时的标题,可选
        "default_title": "这是一个示例Chrome插件",
        "default_popup": "popup.html"
    }
}

img

It is important to note that since you click the icon to open the popup, and the focus is left and then immediately closed, the life cycle of the popup page is generally very short. Do not write code that takes a long time to run in the popup.

In terms of permissions, it is very similar to background. The biggest difference between them is the life cycle. In the popup, you can directly obtain the window object of the background chrome.extension.getBackgroundPage()

injected-script

The injected-script here is what I took for it, which refers to a type of JS injected into the page through DOM manipulation. Why should this JS be discussed separately? Or why do you need to inject JS in this way?

This is because content-script has a big "defect", that is, it cannot access the JS in the page. Although it can manipulate the DOM, the DOM cannot call it, that is, it cannot be called in the DOM by binding events in content-script . Code (including direct writing onclick and addEventListener two ways are not good), but "add a button on the page and call the extension API of the plug-in" is a very common requirement, so what should I do? In fact, this is what this section will talk about.

inject-script code example into the page through DOM in content-script

// 向页面注入JS
function injectCustomJs(jsPath)
{
    jsPath = jsPath || 'js/inject.js';
    var temp = document.createElement('script');
    temp.setAttribute('type', 'text/javascript');
    // 获得的地址类似:chrome-extension://ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject.js
    temp.src = chrome.extension.getURL(jsPath);
    temp.onload = function()
    {
        // 放在页面不好看,执行完后移除掉
        this.parentNode.removeChild(this);
    };
    document.head.appendChild(temp);
}

Do you think that's all right? If you execute it, you will see the following error:

Denying load of chrome-extension://efbllncjkjiijkppagepehoekjojdclc/js/inject.js. Resources must be listed in the web_accessible_resources manifest key in order to be loaded by pages outside the extension.

It means that if you want to directly access the resources in the plug-in on the web, you must display the declaration. The following is added to the configuration file:

{
    // 普通页面能够直接访问的插件资源列表,如果不设置是无法直接访问的
    "web_accessible_resources": ["js/inject.js"],
}

As for how inject-script content-script , I will introduce it in detail in a special message communication chapter later.

homepage_url

img

img

8 display forms of Chrome plug-in

browserAction (upper right corner of the browser)

By configuring browser_action browser_action can be added to the upper right corner of the browser. A 0614a8e30ec999 can have an icon, a tooltip , a badge and a popup .

The sample configuration is as follows:

"browser_action":
{
    "default_icon": "img/icon.png",
    "default_title": "这是一个示例Chrome插件",
    "default_popup": "popup.html"
}
pageAction (right side of the address bar)

The so-called pageAction refers to the icon that is displayed only when certain pages are opened. The browserAction is that one is always displayed, and the other is displayed only in specific situations.

It should be noted that in earlier versions of Chrome, pageAction was placed on the far right of the address bar, left-click to pop up popup, and right-click to pop up related default options menu:

img

The new version of Chrome has changed this strategy. The pageAction is placed in the upper right corner of the browser just like the ordinary browserAction, but it is gray when it is not lit, and it is colored when it is lit. When it is gray, it does not matter whether the left button or the right button is clicked. Clicking are all pop-up options:

img

The specific version was changed from which I did not go carefully. Anyway, I knew that it was the former when v50.0, and it was changed to the latter when v58.0.

We can simply regard the adjusted pageAction browserAction that can be grayed out.

  • chrome.pageAction.show(tabId) display icon;
  • chrome.pageAction.hide(tabId) hide icon;

Example (the icon will only be displayed when Baidu is opened):

// manifest.json
{
    "page_action":
    {
        "default_icon": "img/icon.png",
        "default_title": "我是pageAction",
        "default_popup": "popup.html"
    },
    "permissions": ["declarativeContent"]
}

// background.js
chrome.runtime.onInstalled.addListener(function(){
    chrome.declarativeContent.onPageChanged.removeRules(undefined, function(){
        chrome.declarativeContent.onPageChanged.addRules([
            {
                conditions: [
                    // 只有打开百度才显示pageAction
                    new chrome.declarativeContent.PageStateMatcher({pageUrl: {urlContains: 'baidu.com'}})
                ],
                actions: [new chrome.declarativeContent.ShowPageAction()]
            }
        ]);
    });
});

Effect picture:

img

Right click menu

The right-click menu of the browser can be customized by developing a Chrome plug-in, mainly through the chrome.contextMenus API. The right-click menu can appear in different contexts, such as ordinary pages, selected text, pictures, links, etc., if there is the same plug-in If multiple menus are defined, Chrome will automatically combine them into the secondary menu named after the plug-in, as follows:

img

The simplest right-click menu example:

// manifest.json
{"permissions": ["contextMenus"]}

// background.js
chrome.contextMenus.create({
    title: "测试右键菜单",
    onclick: function(){alert('您点击了右键菜单!');}
});

img

override (override a specific page)

Use the override page to replace some of the Chrome default specific pages and use the pages provided by the extension instead.

Extensions can replace the following pages:

  • History: The page visited when you click History on the Tools menu, or directly enter chrome://history from the address bar
  • New tab page: the page to visit when creating a new tab, or directly enter chrome://newtab from the address bar
  • Bookmarks: browser bookmarks, or directly enter chrome://bookmarks

Notice:

  • An extension can only replace one page;
  • Cannot replace the new tab page of the incognito window;
  • A title must be set on the webpage, otherwise the user may see the URL of the webpage, causing confusion;

Code (note that a plug-in can only replace one default page, the following is just a demonstration):

"chrome_url_overrides":
{
    "newtab": "newtab.html",
    "history": "history.html",
    "bookmarks": "bookmarks.html"
}
devtools (developer tools)

Those who have used Vue should have seen this type of plug-in:

img

Yes, Chrome allows plugins to be used on the developer tools (devtools), mainly in:

  • Customize one or more Sources same level as Elements , Console , 0614a8e30ed2f2;
  • Customize the sidebar (sidebar), currently you can only customize the sidebar of the Elements

Every time a developer tools window is opened, an instance of the devtools page will be created, and the F12 window will be closed, and the page will also be closed, so the life cycle of the devtools page and the devtools window are the same. The devtools page can access a unique set of DevTools API and limited extension APIs. This unique set of DevTools API can only be accessed by the devtools page, and the background is not authorized. These APIs include:

  • chrome.devtools.panels : Panel related;
  • chrome.devtools.inspectedWindow : Obtain the relevant information of the censored window;
  • chrome.devtools.network : Get information about network requests;

Most of the extended API can not be directly DevTools page calls, but it can be as content-script as a direct call chrome.extension and chrome.runtime API, but it also can be like content-script as using the Message interacts with background pages to communicate.

option (option page)

The so-called options page is the plug-in settings page. There are 2 entrances, one is the right-click icon with an "option" menu, and the other is on the plug-in management page:

img

img

Before Chrome40, the options page was no different from other ordinary pages, and there have been some changes after Chrome40.

We first look at the old version of options :

{
    // Chrome40以前的插件配置页写法
    "options_page": "options.html",
}

You can use the content on this page as you like. After configuration, you will see a option button entry on the plug-in management page. Clicking in is to open a web page. There is nothing to talk about.

Effect:

img

Look at the new version of optionsV2 :

{
    "options_ui":
    {
        "page": "options.html",
        // 添加一些默认的样式,推荐使用
        "chrome_style": true
    },
}

img

Does it look tall?

A few notes:

  • For compatibility, it is recommended to write both. If both are written, Chrome 40 will read the new version by default in the future;
  • Alert cannot be used in the new version of options;
  • It is recommended to use chrome.storage for data storage, because it will be automatically synchronized with the user;
omnibox

omnibox is a way to provide users with search suggestions. Let's take a look at the gif picture first to understand what the hell is this thing:

img

Register a certain keyword to trigger the plug-in’s own search suggestion interface, and then you can play it arbitrarily.

First, the configuration file is as follows:

{
    // 向地址栏注册一个关键字以提供搜索建议,只能设置一个关键字
    "omnibox": { "keyword" : "go" },
}

Then register the monitoring event in background.js

// omnibox 演示
chrome.omnibox.onInputChanged.addListener((text, suggest) => {
    console.log('inputChanged: ' + text);
    if(!text) return;
    if(text == '美女') {
        suggest([
            {content: '中国' + text, description: '你要找“中国美女”吗?'},
            {content: '日本' + text, description: '你要找“日本美女”吗?'},
            {content: '泰国' + text, description: '你要找“泰国美女或人妖”吗?'},
            {content: '韩国' + text, description: '你要找“韩国美女”吗?'}
        ]);
    }
    else if(text == '微博') {
        suggest([
            {content: '新浪' + text, description: '新浪' + text},
            {content: '腾讯' + text, description: '腾讯' + text},
            {content: '搜狐' + text, description: '搜索' + text},
        ]);
    }
    else {
        suggest([
            {content: '百度搜索 ' + text, description: '百度搜索 ' + text},
            {content: '谷歌搜索 ' + text, description: '谷歌搜索 ' + text},
        ]);
    }
});

// 当用户接收关键字建议时触发
chrome.omnibox.onInputEntered.addListener((text) => {
    console.log('inputEntered: ' + text);
    if(!text) return;
    var href = '';
    if(text.endsWith('美女')) href = 'http://image.baidu.com/search/index?tn=baiduimage&ie=utf-8&word=' + text;
    else if(text.startsWith('百度搜索')) href = 'https://www.baidu.com/s?ie=UTF-8&wd=' + text.replace('百度搜索 ', '');
    else if(text.startsWith('谷歌搜索')) href = 'https://www.google.com.tw/search?q=' + text.replace('谷歌搜索 ', '');
    else href = 'https://www.baidu.com/s?ie=UTF-8&wd=' + text;
    openUrlCurrentTab(href);
});
// 获取当前选项卡ID
function getCurrentTabId(callback)
{
    chrome.tabs.query({active: true, currentWindow: true}, function(tabs)
    {
        if(callback) callback(tabs.length ? tabs[0].id: null);
    });
}

// 当前标签打开某个链接
function openUrlCurrentTab(url)
{
    getCurrentTabId(tabId => {
        chrome.tabs.update(tabId, {url: url});
    })
}
Desktop notification

Chrome provides a chrome.notifications API for plug-ins to push desktop notifications. The significant differences and advantages between chrome.notifications Notification that comes with HTML5 have not been found yet.

In the background JS, no matter you use chrome.notifications or Notification do not need to apply for permission (the HTML5 method requires permission to apply), and you can use it directly.

The simplest notice:

img

Code:

chrome.notifications.create(null, {
    type: 'basic',
    iconUrl: 'img/icon.png',
    title: '这是标题',
    message: '您刚才点击了自定义右键菜单!'
});

5 types of JS comparison

Chrome plug-in JS can be divided into these 5 categories: injected script , content-script , popup js , background js and devtools js

Permission comparison
JS typeAccessible APIDOM accessJS access situationDirect cross-domain
injected scriptNo difference from ordinary JS, no extension API can be accessedCan visitCan visitCan not
content scriptOnly some APIs such as extension and runtime can be accessedCan visitCan notCan not
popup jsCan access most APIs, except for the devtools seriesNot directly accessibleCan notCan
background jsCan access most APIs, except for the devtools seriesNot directly accessibleCan notCan
devtools jsCan only access some APIs such as devtools, extension, runtime, etc.CanCanCan not
Comparison of debugging methods
JS typeDebugging methodillustrate
injected scriptJust plain F12Too lazy to take a screenshot
content-scriptOpen the Console and switch as shownimg
popup-jsRight-click on popup page to review elementsimg
backgroundClick on the background page on the plug-in management pageimg
devtools-jsNo effective method has been found yet-

Message communication

Communication homepage: https://developer.chrome.com/extensions/messaging

Earlier we introduced the 5 types of JS that exist in the Chrome plug-in, so how do they communicate with each other? Let's first give an overview of the system, and then classify them in detail. What you need to know is that popup and background can almost be regarded as one kind of thing, because they have the same access API, the same communication mechanism, and they can be cross-domain.

Overview of mutual communication

Note: - means that it does not exist or is meaningless, or to be verified.

injected-scriptcontent-scriptpopup-jsbackground-js
injected-script-window.postMessage--
content-scriptwindow.postMessage-chrome.runtime.sendMessage chrome.runtime.connectchrome.runtime.sendMessage chrome.runtime.connect
popup-js-chrome.tabs.sendMessage chrome.tabs.connect-chrome.extension. getBackgroundPage()
background-js-chrome.tabs.sendMessage chrome.tabs.connectchrome.extension.getViews-
devtools-jschrome.devtools. inspectedWindow.eval-chrome.runtime.sendMessagechrome.runtime.sendMessage
Communication details
1. popup and background

The popup can directly call the JS method in the background, or directly access the DOM of the background:

// background.js
function test()
{
    alert('我是background!');
}

// popup.js
var bg = chrome.extension.getBackgroundPage();
bg.test(); // 访问bg的函数
alert(bg.document.body.innerHTML); // 访问bg的DOM
A small episode, I encountered a situation today and found that popup could not obtain any method of background. After looking for a long time, I discovered that it was because the background js reported an error, and if you did not actively check the background js, you would not see the error message. Hereby remind.

As for background access popup as follows (provided that popup has been opened):

var views = chrome.extension.getViews({type:'popup'});
if(views.length > 0) {
    console.log(views[0].location.href);
}
2. Popup or bg actively send messages to content

background.js or popup.js:

function sendMessageToContentScript(message, callback)
{
    chrome.tabs.query({active: true, currentWindow: true}, function(tabs)
    {
        chrome.tabs.sendMessage(tabs[0].id, message, function(response)
        {
            if(callback) callback(response);
        });
    });
}
sendMessageToContentScript({cmd:'test', value:'你好,我是popup!'}, function(response)
{
    console.log('来自content的回复:'+response);
});

content-script.js receives:

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse)
{
    // console.log(sender.tab ?"from a content script:" + sender.tab.url :"from the extension");
    if(request.cmd == 'test') alert(request.value);
    sendResponse('我收到了你的消息!');
});

Both parties directly send JSON objects, not JSON strings, so there is no need to parse them, which is very convenient (of course, you can also send strings directly).

Some old codes on the Internet use chrome.extension.onMessage , and the difference between the two is not fully understood (it looks like an alias), but it is recommended to use chrome.runtime.onMessage .
3. The content-script initiatively sends messages to the background

content-script.js:

chrome.runtime.sendMessage({greeting: '你好,我是content-script呀,我主动发消息给后台!'}, function(response) {
    console.log('收到来自后台的回复:' + response);
});

background.js or popup.js:

// 监听来自content-script的消息
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse)
{
    console.log('收到来自content-script的消息:');
    console.log(request, sender, sendResponse);
    sendResponse('我是后台,我已收到你的消息:' + JSON.stringify(request));
});

Precautions:

  • The premise for content_scripts to popup is that the popup must be opened! Otherwise, you need to use background as a transit;
  • If the background and popup listen at the same time, then they can both receive the message at the same time, but only one can sendResponse, one is sent first, then the other one will be invalid after sending;
4. injected script and content-script

content-script only thing shared between 0614a8e30ee670 and the script in the page ( injected-script also belongs to the script in the page) is the DOM element of the page. There are two ways to achieve communication between the two:

  1. Message communication between the two can be achieved through window.postMessage and window.addEventListener
  2. Realize through custom DOM events;

The first method (recommended):

injected-script :

window.postMessage({"test": '你好!'}, '*');

In the content script:

window.addEventListener("message", function(e)
{
    console.log(e.data);
}, false);

The second method:

injected-script :

var customEvent = document.createEvent('Event');
customEvent.initEvent('myCustomEvent', true, true);
function fireCustomEvent(data) {
    hiddenDiv = document.getElementById('myCustomEventDiv');
    hiddenDiv.innerText = data
    hiddenDiv.dispatchEvent(customEvent);
}
fireCustomEvent('你好,我是普通JS!');

content-script.js :

var hiddenDiv = document.getElementById('myCustomEventDiv');
if(!hiddenDiv) {
    hiddenDiv = document.createElement('div');
    hiddenDiv.style.display = 'none';
    document.body.appendChild(hiddenDiv);
}
hiddenDiv.addEventListener('myCustomEvent', function() {
    var eventData = document.getElementById('myCustomEventDiv').innerText;
    console.log('收到自定义事件消息:' + eventData);
});
Long connection and short connection

In fact, the above has already been involved, and I will explain it separately here. There are two communication methods in the Chrome plug-in, one is a short connection ( chrome.tabs.sendMessage and chrome.runtime.sendMessage ), and the other is a long connection ( chrome.tabs.connect and chrome.runtime.connect ).

A short connection is like squeezing toothpaste. I will send it, and you will reply when you receive it. If the other party does not reply, you can only re-send it, while a long connection like WebSocket will always establish a connection, and both parties can send messages to each other at any time.

There is already a code example above for short connection, here we only talk about long connection.

popup.js:

getCurrentTabId((tabId) => {
    var port = chrome.tabs.connect(tabId, {name: 'test-connect'});
    port.postMessage({question: '你是谁啊?'});
    port.onMessage.addListener(function(msg) {
        alert('收到消息:'+msg.answer);
        if(msg.answer && msg.answer.startsWith('我是'))
        {
            port.postMessage({question: '哦,原来是你啊!'});
        }
    });
});

content-script.js:

// 监听长连接
chrome.runtime.onConnect.addListener(function(port) {
    console.log(port);
    if(port.name == 'test-connect') {
        port.onMessage.addListener(function(msg) {
            console.log('收到长连接消息:', msg);
            if(msg.question == '你是谁啊?') port.postMessage({answer: '我是你爸!'});
        });
    }
});

Other supplements

Dynamically inject or execute JS

Although the page DOM cannot be directly accessed background and popup chrome.tabs.executeScript to access the DOM of the web page (note that this method also cannot directly access the page JS).

Example manifest.json configuration:

{
    "name": "动态JS注入演示",
    ...
    "permissions": [
        "tabs", "http://*/*", "https://*/*"
    ],
    ...
}

JS:

// 动态执行JS代码
chrome.tabs.executeScript(tabId, {code: 'document.body.style.backgroundColor="red"'});
// 动态执行JS文件
chrome.tabs.executeScript(tabId, {file: 'some-script.js'});
Dynamically inject CSS

Example manifest.json configuration:

{
    "name": "动态CSS注入演示",
    ...
    "permissions": [
        "tabs", "http://*/*", "https://*/*"
    ],
    ...
}

JS code:

// 动态执行CSS代码,TODO,这里有待验证
chrome.tabs.insertCSS(tabId, {code: 'xxx'});
// 动态执行CSS文件
chrome.tabs.insertCSS(tabId, {file: 'some-style.css'});
Get the current window ID
chrome.windows.getCurrent(function(currentWindow)
{
    console.log('当前窗口ID:' + currentWindow.id);
});
Get the ID of the current tab

There are generally 2 methods:

// 获取当前选项卡ID
function getCurrentTabId(callback)
{
    chrome.tabs.query({active: true, currentWindow: true}, function(tabs)
    {
        if(callback) callback(tabs.length ? tabs[0].id: null);
    });
}

Another way to get the id of the current tab is similar most of the time, only a few times will be different (for example, when the window is minimized)

// 获取当前选项卡ID
function getCurrentTabId2()
{
    chrome.windows.getCurrent(function(currentWindow)
    {
        chrome.tabs.query({active: true, windowId: currentWindow.id}, function(tabs)
        {
            if(callback) callback(tabs.length ? tabs[0].id: null);
        });
    });
}
Local storage

For local storage, it is recommended to use chrome.storage instead of the ordinary localStorage . There are several differences. I personally think that the most important two differences are:

  • chrome.storage against global plugins, even if you background stored data, in content-script can get to;
  • chrome.storage.sync can be automatically synchronized with the currently logged-in user. The modified settings of this computer will automatically be synchronized to other computers. It is very convenient. If you are not logged in or not connected to the Internet, save it to the local first, and then synchronize to the network after logging in;

Need to declare storage permissions, there are chrome.storage.sync and chrome.storage.local , examples of use are as follows:

// 读取数据,第一个参数是指定要读取的key以及设置默认值
chrome.storage.sync.get({color: 'red', age: 18}, function(items) {
    console.log(items.color, items.age);
});
// 保存数据
chrome.storage.sync.set({color: 'blue'}, function() {
    console.log('保存成功!');
});
webRequest

beforeRequest through the webRequest series of APIs. Here is a brief demonstration of the tip of the iceberg through 0614a8e31002f1:

//manifest.json
{
    // 权限申请
    "permissions":
    [
        "webRequest", // web请求
        "webRequestBlocking", // 阻塞式web请求
        "storage", // 插件本地存储
        "http://*/*", // 可以通过executeScript或者insertCSS访问的网站
        "https://*/*" // 可以通过executeScript或者insertCSS访问的网站
    ],
}


// background.js
// 是否显示图片
var showImage;
chrome.storage.sync.get({showImage: true}, function(items) {
    showImage = items.showImage;
});
// web请求监听,最后一个参数表示阻塞式,需单独声明权限:webRequestBlocking
chrome.webRequest.onBeforeRequest.addListener(details => {
    // cancel 表示取消本次请求
    if(!showImage && details.type == 'image') return {cancel: true};
    // 简单的音视频检测
    // 大部分网站视频的type并不是media,且视频做了防下载处理,所以这里仅仅是为了演示效果,无实际意义
    if(details.type == 'media') {
        chrome.notifications.create(null, {
            type: 'basic',
            iconUrl: 'img/icon.png',
            title: '检测到音视频',
            message: '音视频地址:' + details.url,
        });
    }
}, {urls: ["<all_urls>"]}, ["blocking"]);
API summary

Some commonly used API series:

  • chrome.tabs
  • chrome.runtime
  • chrome.webRequest
  • chrome.window
  • chrome.storage
  • chrome.contextMenus
  • chrome.devtools
  • chrome.extension

Project actual combat

Use vue cli 4 to develop chrome plug-ins

Create vue cli 4 project

vue create google-extension-demo

Check Babel , TypeScript , CSS Pre-processors , Linter / Formatter

Here, the CSS preprocessor is arbitrary, I chose Sass/SCSS (with dart-sass) .

In addition, linter / formatter in TSLint is no longer maintained. It is recommended to use ESLint + Prettier .
Do not use class-style component syntax , see vue-cli-plugin-chrome-ext .

Add chrome-ext plugin

vue add chrome-ext

Remember to check ts here:

? Name of the Chrome Extension? todolist
? Description for the Chrome Extension? chrome extension
? Version for the Chrome Extension? 0.0.1
? javascript or typescript? 
  js 
❯ ts 

An error occurred:

⠋  Running completion hooks...error: Require statement not part of import statement (@typescript-eslint/no-var-requires) at vue.config.js:1:27:
> 1 | const CopyWebpackPlugin = require("copy-webpack-plugin");
    |                           ^
  2 | const path = require("path");
  3 | 
  4 | // Generate pages object

This is because eslint does not allow the syntax of commonjs vue.config.js is a special file, we should put it in the .eslintignore file and let eslint ignore it.

Write in .eslintignore

vue.config.js

vue-cli-plugin-chrome-ext `Creation is not a SPA application. The chrome plug-in has many interfaces, so each interface is compiled separately.

For example, src/popup is an interface, and src/options is also an interface.

In this way, many of the original files are useless, we can delete them, which are src/main.ts and src/App.vue .

Hot reload development

Use npm run build-watch to develop using hot reload.

After running, a dist folder will be generated, and it will be recompiled and loaded every time you change the code.

Import dist into the chrome plugin
Enter chrome://extensions/ , select and load the dist folder to enter.

image-20210921181313389

After loading successfully, remember to go to the management interface in the upper right corner to open the popup interface (new version of chrome).

image-20210921181416179

Now, after typing the code, you can immediately see the rendering result in the chrome popup view.

Use UI components

Although vue is running, it is useless if the component cannot be imported.

Take ant design vue as an example, let's test it.

npm i --save ant-design-vue

Import in the popup interface

Add the following in src/popup/index.ts

import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';
Vue.use(Antd);

Vue.config.productionTip = false;

As we said before, each folder under the src folder represents a page, and each page has a corresponding index.ts (equivalent to main.js), index.html, and App.vue.

In other words, if we want to use the UI library, it is not necessary to configure it in a global main.js, we need to import it in the index.ts under the folder of each page.

Here we only imported it under the popup interface. If you need to use it in the options interface, you need to go to src/options/index.ts and import it again.

Write a simple popup interface

Use ant design of card components to test, whether you can use.

The main code of the interface should be written in the App/App.vue directory.

<template>
  <div class="main_app">
    <a-card title="ghost wang">
      coding
    </a-card>
  </div>
</template>

<script lang="ts">
export default {
  name: "app"
};
</script>

<style>
.main_app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  height: 500px;
  width: 300px;
  padding: 10px;
}
</style>

The effect is like this:

image-20210921181907717

More fun to be discovered...


MangoGoing
780 声望1.2k 粉丝

开源项目:详见个人详情


引用和评论

0 条评论