preface

Chrome should be the software we will open every day, BEFORE I use Chrome, I don’t seem to have any requirements for the browser, 360, Sogou are good, but after using Chrome, then I will never use other browsers (no derogative meaning here). One of the most popular reasons for Chrome is its Extension ecosystem. Many people call it a Chrome Extension, but it is also called Chrome Extension in English.

Recently, WHEN I was writing an article, I suffered from the lack of graph bed software, and the graph bed on the Internet was more or less meaningless, so I decided to make a simple graph bed by myself. The first time I thought of the chart bed display form into Chrome extension, because it is convenient enough light weight, you can also take this opportunity to understand the development of Chrome extension. This paper will not too much Chrome provide apis with respect to the expanding ability, when need to use a certain ability to you can go to a query document, is the main focus of extension development need to understand the key concepts in the process, here I’ll show a few examples to combat these concepts, and I usually meet some want to use tools to solve problems.

Before I start, I will post an official document, extensions-doc, now the official recommended version is V3, so I will post the v3 document directly here.

The basic concept

Well, let’s start with a few basic concepts and dive into Chrome extension development. The concepts mentioned here may be a bit boring, but don’t worry, we have practical examples to help you reinforce these concepts, and you can skip them if you don’t want to.

manifest

This is the first step in extension development. You need to fill the configuration file with information about your extension. Here is a simple example to get a glimpse of the configuration file.

{
    "name": "Translation".// Expand the name
    "description": "Quick translation".// Expand description
    "version": "1.0"."manifest_version": 3.// Set to 3
    "permissions": [
        "storage".// Permission requests, if you want to use an API like Chrome. XXX, are described here first
        "activeTab"."scripting"]."action": { // Extended display page
        "default_popup": "popup.html"
    },
    "host_permissions": [// Trusted domain name
        "<all_urls>"]."content_scripts": [// This will be introduced later
        {
            "matches": [
                "<all_urls>" // only under matching rules, all_urls means to match all web pages]."js": [
                "js/content-script.js"]."css": [
                "css/style.css"]."run_at": "document_start" // This must be entered, or the corresponding script will not be executed}}]Copy the code

Here, we will mainly explain the development of extensions in the form of popup (click on the upper right corner to popup the extension), which is the most commonly used extension method.

background-scripts

The name suggests that this is a script that runs in the background, but it actually does much the same thing. The life cycle starts with the browser opening and ends with the browser closing, and it can use all of the extension apis. The scripts in the Popup page (hereafter referred to as popup.js) are very similar, but most different are their life cycles. The popup.js life cycle ends with the closure of the Popup page. Json to register your background script.

    "background": {
        "service_worker": "background.js"
    }
Copy the code

content-scripts

This property allows developers to inject styles or scripts into the current TAB page, which share DOM elements with the current TAB page.

Practical development

Let’s take a look at some of the basics of Chrome extension development as an example of developing a suggested popup page. I believe this will give you a deeper understanding of the main concepts of extension development.

This extension

When I was reading the extension document, I really had a pain in English. I had to keep switching back and forth between the document and translation website. Here we use some translation API capabilities to implement a translation plug-in.

The UI of the page is very simple (simple), input the English you want to translate, click GO to help you translate into Chinese, click Copy to copy the translation result to the paste board.

Before we get started, let’s look at the directory structure of the extension. A PopUp extension directory structure might look something like this

project
    -css
    -js
    manifest.json
    popup.html
    popup.js
Copy the code

During the development of this extension, we will focus on popup.html and popup.js. The simple content of the template is as follows:

<div>
    <input placeholder="Enter what you want to translate" id="input" />
    <button id="translate">GO</button>
</div>
<div>
    <textarea readonly id="result"></textarea>
    <button id="copy">copy</button>
</div>
<script src="popup.js"></script>
Copy the code

In the logical implementation, I use Baidu’S API for translation service, and the free quota is 5 million characters (it should be enough for me to use for a long time). The specific access method can be click here. So now that you have a back-end interface, it’s just a matter of writing some simple front-end logic. Popup.js or background-scripts are not subject to cross-domain requests after domain permissions have been configured, so developers can do whatever they like, but most of the time as extension users should be on the alert and try to download legitimate extensions from the official market to use them.

Fetch is used here to initiate a network request. The content of the input box can be directly regarded as the parameter request API. Let’s see the code directly.

const input = document.querySelector('#input')
const translateEl = document.querySelector('#translate')
const resultEl = document.querySelector('#result')
const copy = document.querySelector('#copy')
input.focus()
let loading = false
let val = ' '

// The parameters required for the API call, see the documentation for details
const grantType = 'client_credentials' / / a fixed value
const clientId = 'your clientId'
const clientSecret = 'your clientSecret'
const accessTokenUrl = ` https://aip.baidubce.com/oauth/2.0/token?grant_type=${grantType}&client_id=${clientId}&client_secret=${clientSecret}`
const translateUrl = ` https://aip.baidubce.com/rpc/2.0/mt/texttrans/v1 `

// Encapsulate a request function slightly
function request(url, config = {}) {
    const defaultConfig = {
        method: 'GET'.headers: {
            'Content-Type': 'application/json; charset=utf-8'}}const requestConfig = Object.assign({}, defaultConfig, config)
    if (requestConfig.body) {
        requestConfig.body = JSON.stringify(requestConfig.body)
    }
    return new Promise((resolve, reject) = > {
        fetch(url, requestConfig).then(res= > res.json()).then(data= > {
            resolve(data)
        }).catch(err= > {
            reject(err)
        })
    })
}

async function translate(val) {
    const tokenData = await getAccessToken();
    const { access_token: accessToken } = tokenData
    const config = {
        method: "POST".body: {
            from: 'en'.// Here is fixed English ->, in fact can be made more flexible configuration if necessary
            to: 'zh'.q: val
        }
    }
    const url = `${translateUrl}? access_token=${accessToken}`
    const data = await request(url, config)
    const { result } = data
    constdst = result? .trans_result[0]? .dst resultEl.value = dst }function getAccessToken() {
    return new Promise(async (resolve, reject) => {
        try {
            const data = await request(accessTokenUrl)
            resolve(data)
        } catch (error) {
            reject(error)
        }
    })
}

input.addEventListener('input'.e= > {
    const { value } = e.target
    val = value
})

translateEl.addEventListener('click'.async() = > {if(! val.trim()) {return
    }
    translate(val)
})
Copy the code

The above code is simple enough that we have implemented a crude but perhaps convenient translation extension. I forgot to mention that to debug popup.js, just right-click the extended page and open the developer tools, just like we would debug the Web.

copy

After completing the most basic translation function docking, the next step is how to use the translated results more easily. One-click copy is the happiness-boosting action, so the next thing to do is click the button and copy the translation into the stickboard. The browser provides the copy command to copy the selected content. The code is as follows.

copy.addEventListener('click'.() = > {
    resultEl.select()
    try {
        document.execCommand('copy')
        alert('Copy successful')}catch (error) {
        console.log(error)
    }
})
Copy the code

A few lines of code can copy the content to the paste board, but the resultel.select () line, which checks all the text of the input box and activates it, doesn’t look very comfortable. We can actually use a hidden input field and just tell it to perform select.

let fakeTextarea = null
copy.addEventListener('click'.() = > {
    const value = resultEl.value
    if(! fakeTextarea) {const textarea = document.createElement('textarea')
        textarea.style = 'position:absolute; top:-999px; left:-999px'; // Hide it
        document.body.appendChild(textarea)
        fakeTextarea = textarea
    }
    fakeTextarea.value = value
    fakeTextarea.select()
    try {
        document.execCommand('copy')
        alert('Copy successful')}catch (error) {
        console.log(error)
    }
})
Copy the code

You already know how to generically copy the contents of a label by placing its contents in a hidden input box and performing select.

Token storage

Careful students have found that we always get an Access_token before we call the translation interface. In fact, it is valid for a period of time. During this period, we do not need to get a new access_token.

As you can see from the graph, its expiration time is 30 days, which means that most token requests can be removed, so we must find a way to save it. In normal front-end development it would be easy to think of storing it somewhere like localStorage or indexDB.

In the case of extension development, Chrome also provides an API for local storage: Chrome.storage. It has the following specific differences with localStorage:

  • Data can be associated withChromesynchronous
  • Even when using stealth mode, it can be stored properly
  • User data can be stored as objects
  • Content scripts can also be used directly without background pages
  • Provides batch asynchronous reads and writesAPIBetter performance

It is worth mentioning that to use storage, don’t forget to add permissions to manifest.json.

{
    "permissions": ["storage"]}Copy the code

After understanding the storage capabilities provided by the API, we can modify our code to store access_token. We will also make some modifications to our code, as shown in the following flow chart.

const ACCESS_TOKEN_STORAGE_KEY = 'ACCESS_TOKEN_STORAGE_KEY'
let globalAccessToken = null // Cache one in memory
function getResultFromStorage(keys = []) {
    return new Promise(resolve= > {
        / / read the cache
        chrome.storage.sync.get(keys, result= > {
            resolve(result)
        })
    })
}

function setStorage(key, value) {
    return new Promise(resolve= > {
        / / write cache
        chrome.storage.sync.set({ [key]: value }, res= > {
            resolve()
        })
    })
}
async function translate(val) {
    / /......
    const accessToken = globalAccessToken ? globalAccessToken : await getAccessToken()
    const url = `${translateUrl}? access_token=${accessToken}`
    const data = await request(url, config)
    const { result, error_code } = data
    if (error_code === 110) {
        / / token expired
        await refreshToken()
        loading = false
        // Replay the request
        translate(val)
    } else {
        // Write the result normally}}async function refreshToken() {
    // If there is no value in either of these places, a request will be sent to retrieve the token
    globalAccessToken = null
    await setStorage(ACCESS_TOKEN_STORAGE_KEY, globalAccessToken)
}

function getAccessToken() {
    return new Promise(async (resolve, reject) => {
        try {
            // Get the token from the cache first
            const accessToken = await getResultFromStorage([ACCESS_TOKEN_STORAGE_KEY])
            if(! accessToken[ACCESS_TOKEN_STORAGE_KEY]) {// If you can't get it from the cache, write it to the cache and memory
                const data = await request(accessTokenUrl)
                globalAccessToken = data.access_token
                await setStorage(ACCESS_TOKEN_STORAGE_KEY, globalAccessToken)
            } else {
                // Write to memory
                globalAccessToken = accessToken[ACCESS_TOKEN_STORAGE_KEY]
            }
            resolve(globalAccessToken)
        } catch (error) {
            reject(error)
        }
    })
}
Copy the code

Popup. Json, popup. Js, popup.

Right click to expand

Some students may say, I still think this extension is a little weak or the steps are not simple enough to use. In the case of reading an English document, I want to right-click a paragraph and translate it directly. This requirement is fairly common, and Chrome offers an extension of the right-click menu. Specific renderings are as follows:

Without further ado, let’s get started with a quick look at the directory structure:

menu
    background.js
    content-script.js
    manifest.json
Copy the code

The manifest.json configuration is slightly different, so let’s take a look at it:

"permissions": [
    "storage"."activeTab"."contextMenus" // Right-click menu permissions remember to open]."background": {
    "service_worker": "background.js"
},
// Enable the domain name permission, otherwise it will be blocked by the same Origin policy
"host_permissions": [
    "https://aip.baidubce.com/"
]
Copy the code

The content-Scripts configuration was mentioned above and won’t be covered here. For a moment, as a right-click extension, it should be available on every page, so its clickback logic should be registered in background-js, since only its life cycle can satisfy this requirement. Also, from the GIF above, the pop-up content should be related to the current browser TAB, so the pop-up logic should be written in Content-scripts. Further, background.js doesn’t have an intuitive connection to Content-scripts, so you need to use the communication capabilities that Chrome provides. So here we have a very clear idea, mainly divided into the following steps to achieve this extension:

Registration menu

Without further ado, get right to the code.

//background.js
const QUICK_TRANSLATE = 'quick-translate'
chrome.contextMenus.create({
    id: QUICK_TRANSLATE, //id to distinguish the corresponding menu item clicked
    title: '%s'.contexts: ['selection'].// When the text is selected, %s is the selected text
})

// Listen for the menu item and click callback
chrome.contextMenus.onClicked.addListener(menuItemClickCallback)

function menuItemClickCallback(info) {
    const { menuItemId } = info
    if (menuItemId === QUICK_TRANSLATE) {
        translateCallback(info)
    }
}

async function translateCallback({ selectionText }) {
    const res = await translate(selectionText) // The translation function implemented earlier
}
Copy the code

As you can see, the code above is very simple, registering the menu, listening for callbacks, and making a translation request to get the results is very clear. The best way to display the results once you have them, of course, is to present them to the TAB that is currently using the extension, which is where the communication between the two scripts is concerned.

Script communication

Here directly on the code line, the implementation is also very simple.

//background.js
async function translateCallback({ selectionText }) {
    const res = await translate(selectionText)
    sendMessageToContentScript({res})
}

async function sendMessageToContentScript(message) {
    const [tab] = await chrome.tabs.query({ active: true.currentWindow: true })
    chrome.tabs.sendMessage(tab.id, message, function (response) {
        console.log(response);
    });
}

//content-scripts.js
chrome.runtime.onMessage.addListener(
    function (request, sender, sendResponse) {
        sendResponse('success')
        alert(request.res)
    }
);
Copy the code

I just alerted the results above (because I was too lazy), but of course you can make better interactions. After all, Content-scripts shares DOM elements with the current TAB page, so you can do whatever you want.

Practical summary

The manifest.json file is configured to register permissions before using certain apis or developing certain features. At the same time, we introduced two types of plug-in, one is pop-up in the upper right corner, one is right-click menu, in fact, there are other types, I will not expand here, interested students please refer to the documentation; In developing these two plug-ins, we learned about the “three Js”; Popup.js is a popup plugin script, which is similar to background.js, and can be used for logic development of pop-ups. Content-scripts is a script that extends the current TAB page. The only Chrome extensions that it can access are chrome.runtime, Chrome. storage, and Chrome. 18n. You can call background.js via script communication for help.

background.jsDebug requires you to enterChromeClick the button below to open its console

To debug popup.js, right click on the Popup page and open the developer tools

Content-scripts is the easiest to debug by opening console debugging on the current TAB page

Figure bed expansion

After understanding the above knowledge, we are going to do what we said at the beginning, which is to develop a map bed extension of our own. We are, of course, with the expansion of the popup type to develop, the way to upload pictures click and drag, copy the three we can offer, as for the background services would need a cloud server or other cloud storage services, I didn’t buy cloud storage service, only oneself write a simple interface, it will actually use the cloud storage service should be more convenient. And the result is something like this, so without further ado, let’s move on to development.

Figure bed implementation

We are developing a popup type extension, so we won’t go into config files here. In fact, what we want to develop is a function of uploading pictures, and there is no difficulty in the implementation. Remember to use some style to hide the input box, which is generally accepted practice. Note that writing embedded javascript code is generally not supported in popup.html.

<! -- popup.html -->
<div class="content">Click, drag, paste and upload</div>
<input id="upload" type="file" />
Copy the code
//popup.js
const url = 'yourhostname'
const content = document.querySelector('.content')
content.addEventListener('click'.() = > {
    uploadEL.click()
})

const uploadEL = document.querySelector("#upload")
uploadEL.addEventListener('change'.async e => {
    const file = e.target.files[0]
    upload(file)
})

function request(url, config) {
    return new Promise((resolve, reject) = > {
        fetch(`${url}`, config).then(res= > res.json()).then(data= > {
            resolve(data)
        }).catch(err= > {
            reject(err)
        })
    })
}

async function upload(file) {
    var formData = new FormData();
    formData.append('file', file);
    const res = await request(`${url}/upload.php`, {
        method: 'POST'.body: formData
    })
    const { code, path } = res
    if (code === 200) {
        afterUpload(path) // Backfill the upload result into the page}}Copy the code

The click-upload code is also pretty simple, with nothing special to say. You will see that I have reproduced several different results in my screenshots to make it more comfortable for me to use, but you can also customize the interaction logic after the successful upload.

Drag & copy

The drag implementation relies on the DROP event, but remember that the default event needs to be blocked during the Drag phase before the drop, otherwise the drop will not take effect.

content.addEventListener('dragover'.e= > {
    // The default event must be prevented, otherwise the drop will not take effect
    e.preventDefault()
})

content.addEventListener('drop'.e= > {
    e.preventDefault()
    const file = e.dataTransfer.files[0]
    upload(file)
})
Copy the code

The implementation of paste depends on paste events. The paste content is not necessarily an image, so it is necessary to filter the content, which is relatively easy to implement.

document.addEventListener('paste'.async event => {
    if (event.clipboardData || event.originalEvent) {
        const clipboardData = (event.clipboardData || event.originalEvent.clipboardData);
        const { items } = clipboardData
        const { length } = items
        let blob = null
        for (let i = 0; i < length; i++) {
            if (items[i].type.indexOf("image")! = = -1) {
                blob = items[i].getAsFile()
            }
        }
        upload(blob)
    }
})
Copy the code

Interface implementation

As for the interface to upload pictures, I wrote it in PHP. If you are interested, you can have a look. I will write the comments well.

//upload.php

      
$date = date('Y-m-d');
$file = $_FILES['file'];
$file_name = $file['name'];
// Check whether the file is empty
if (empty($file_name)) {
    echo 400;
    die;
}
// The type of files that can be uploaded
$type = array('image/jpg'.'image/gif'.'image/png'.'image/bmp');
// Get the file suffix
$file_type = $file['type'];
$ext = explode('/'.$file_type) [1];
// Upload path
$upload_path = '/img/';
$res = [];
if (in_array($file_type.$type)) {
    //do·· while generates a random name for the file
    do {
        $new_name = get_file_name(10).'. ' . $ext;
        $path = $upload_path . $new_name;
    } while (file_exists($path));
    $temp_file = $_FILES['file'] ['tmp_name'];
    // Move the file to the upload path
    move_uploaded_file($temp_file.$path);
    $res['path'] = $new_name;
    $res['code'] = 200;
} else {
    $res['code'] = 400;
}
// Returns a random file name
function get_file_name($len)
{
    $new_file_name = ' ';
    $chars = "1234567890qwertyuiopasdfghjklzxcvbnm";
    for ($i = 0; $i < $len; $i{+ +)$new_file_name. =$chars[mt_rand(0, strlen($chars) - 1)];
    }
    return $new_file_name;
}
// spit out the result
echo json_encode($res);
Copy the code

I also use interfaces to read images rather than direct static resources, so it’s best not to expose your static resources directly.


      
// Get parameters
$name = $_GET['name'];
if (empty($name)) {
    echo '404';
    die;
}
// Filter unsafe characters
$name = addslashes($name);
$name = str_replace('/'.' '.$name);
// Splice paths
$path = '/img/' . $name;
// Check whether the file exists
if(! file_exists($path)) {
    echo '404';
    die;
}
$ext = explode('. '.$name) [1];
// Set strong cache
header('Cache-Control: public');
header('Pragma: cache');
$offset = 30 * 60 * 60 * 24;/ / 1 month
$exp_str = 'Expires: ' . gmdate('D, d M Y H:i:s', time() + $offset).' GMT';
header($exp_str);
header('Content-type: image/'.$ext);
// Spit out the contents of the file
echo file_get_contents($path);
Copy the code

The last

That’s all you need to know about Chrome extension development. Now you’ve learned some basic concepts and how to debug, use your initiative, and develop some extension widgets to make your life easier