This is a block blue lake cut image and upload chrome plugin

Making the address

Technology stack: Webpack +vue2.x

Note: the interface to upload pictures is the company’s interface, so it will not be put here, you can improve

Method of use

  1. npm run buildPackaging code
  2. Click more tools in Chrome => Extensions => Load the unzipped extension => Select the packaged Dist file and the extension is added.
  3. Then open blue Lake, download the cut image, and it will automatically block the upload and display the image link.

preface

Since there are a lot of cut diagrams in the small program project, and the main package of the small program has a size limit of 2M, pictures cannot always be stuffed into the project file. However, the company has a resource server specially used to store pictures, so we can upload the cut diagrams to the server, and then access them through the HTTP link of pictures in the small program. This has the advantage of reducing the volume of the main package or subpackage, and also the HTTP cache after the image is loaded once

Preliminary knowledge

1. Chrome Extension

manifest.json

Configuration file, must be placed in the root directory, complete configuration view

{
	// The version of the manifest file, this must be written and must be 2
	"manifest_version": 2.// The name of the plug-in
	"name": "autoUploadImg".// Version of the plug-in
	"version": "1.0.0".// Use the same size icon
	"icons": {
    "16": "static/images/upload.png"."48": "static/images/upload.png"."128": "static/images/upload.png"
  },
	// The background JS or background page will always be resident
	"background":
	{
		// If you specify JS, a background page will be generated automatically
		"page": "background.html"
		//"scripts": ["js/background.js"]
	},
	// Set the icon in the upper right corner of the browser
	"browser_action": 
	{
		"default_icon": "static/images/upload.png".// The title of the hovering icon, optional
		"default_title": "autoUploadImg"
	},
	// Need to inject the JS directly into the page
	"content_scripts": [{//"matches": ["http://*/*", "https://*/*"],
			// "
      
       " matches all addresses
      
			"matches": ["<all_urls>"].// Multiple JS are injected in sequence
			"js": ["contentScripts.js"].// JS injection can be arbitrary, but CSS must be careful, because careless can affect the global style
			"css": ["css/custom.css"].Optional values: "document_start", "document_end", or "document_idle". The last one indicates that the page is idle, and the default is document_idle
			"run_at": "document_start"}].// Permission request
	"permissions":
	[
		"contextMenus".// Right-click the menu
		"tabs"./ / label
		"notifications"./ / notice
		"webRequest"./ / web request
		"webRequestBlocking"."storage".// Plugin local storage
		"http://*/*".// A website accessible from executeScript or insertCSS
		"https://*/*" // A website accessible from executeScript or insertCSS].// A list of plug-in resources that can be accessed directly from a normal page
	"web_accessible_resources": ["js/inject.js"].// Plugin home page, this is very important, do not waste this free advertising space
	"homepage_url": "https://www.baidu.com".// Override the browser default page
	"chrome_url_overrides":
	{
		// Overrides the browser's default new TAB page
		"newtab": "newtab.html"
	},
	// plugin configuration page before Chrome40
	"options_page": "options.html".// Chrome40 after the plugin configuration page, if both write, the new version of Chrome only recognize the latter one
	"options_ui":
	{
		"page": "options.html".// Add some default styles, recommended
		"chrome_style": true}}Copy the code

content-scripts

A script that the Chrome plugin injects into a page. It is not possible to share the DOM with the original page across domains, but not the JS, and access to the PAGE JS (e.g. a JS variable) can only be achieved through injected JS. Content-scripts does not have access to most chrome.xxx. API. But you can access the following four

chrome.extension(getURL , inIncognitoContext , lastError , onRequest , sendRequest)

chrome.i18n

chrome.runtime(connect , getManifest , getURL , id , onConnect , onMessage , sendMessage)

chrome.storage
Copy the code

injected-script

Inject scripts Content-script can be classified into two types: declared statically and injected programmatically. The content-script declared in Mainfest.json above is static, and programatically injected here is called injected-script for the purpose of differentiation

Content-script has a major “flaw” in that it can’t access the JS in the page, and while it can manipulate the DOM, the DOM can’t call it, You can’t call content-script code in the DOM by binding events (including onclick and AddeventListenerdirectly)

Example of code to inject inject-script into a page via DOM in content-script:

// Inject JS into the page
function injectCustomJs(jsPath)
{
	jsPath = jsPath || 'js/inject.js';
	var temp = document.createElement('script');
	temp.setAttribute('type'.'text/javascript');
	/ / get the address of the similar: chrome - the extension: / / ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject. Js
	temp.src = chrome.extension.getURL(jsPath);
	temp.onload = function()
	{
		// It will not look good on the page
		this.parentNode.removeChild(this);
	};
	document.head.appendChild(temp);
}
Copy the code

Then add the declaration to the configuration file

{
	// A list of plug-in resources that can be accessed directly from a normal page
	"web_accessible_resources": ["js/inject.js"],}Copy the code

background

Is a resident page that has the longest life cycle of any type of page in the plug-in. It opens with the browser and closes with the browser, so it usually places global code that needs to run all the time, run on startup, in background. It can cross domains. Each plug-in has its own background and does not affect each other.

popup

This is a small window that pops up when you click on the browser plugin icon. Popup can contain any HTML content you want, and it will be sized accordingly. The POPUP page can be specified through the default_popUP field. You can cross domain

{
	"browser_action":
	{
		"default_icon": "img/icon.png".// The title of the hovering icon, optional
		"default_title": "This is a sample Chrome plugin"."default_popup": "popup.html"}}Copy the code

Popup can be directly through the chrome. The extension. GetBackgroundPage () to obtain the background of the window object

Message communication

The official document is the communication between the various JS in the plug-in

content-script popup-js background-js
content-script chrome.runtime.sendMessage chrome.runtime.connect chrome.runtime.sendMessage chrome.runtime.connect
popup-js chrome.tabs.sendMessage chrome.tabs.connect chrome.extension. getBackgroundPage()
background-js chrome.tabs.sendMessage chrome.tabs.connect chrome.extension.getViews

Long connection and short connection

Chrome plug-ins have 2 kinds of communication mode, one is the short connection (Chrome. Tabs. SendMessage and Chrome. Runtime. SendMessage), One is long connect (Chrome.tabs. Connect and Chrome.runtime.connect)

A long connection is similar to a WebSocket. The two parties can send messages to each other at any time

2. Magic Numbers

Use specific hexadecimal data to represent the file type, and the magic number is always the first few bits. Different file types have different magic numbers.

Vscode has a plug-in called hexDump that can be used to view the Magic Numbers of images. We usually distinguish the image type, using the suffix name, which is actually not accurate, because the suffix name can be changed at any time, and does not affect the open view. The control is set to accept file types, but only by the suffix name. Once before, there was a problem that the pictures on wechat mini program real machine were not displayed. Later, the reason for the investigation was that although the final name of the picture was PNG, the magic number was webP type when viewed. However, there would be compatibility problems with WebP on ios, so it was not displayed.

3. The Blob and ArrayBuffer

This article makes the relationship between bloBs and ArrayBuffers very clear

Talk about the binary family of JS: Blob, ArrayBuffer, and Buffer

4. Get the image type

What we need to do now is get the Magic Number of the photo, You’re reading binary data to determine the type of image so you need the readAsArrayBuffer of the FileReader API to read an ArrayBuffer object that represents a generic, fixed-length buffer of raw binary data. You can’t manipulate the contents of an ArrayBuffer directly, so you need TypeArray

var buffer = null
/ * * *@desc After reading the file into memory and typing the array, read the first few bits of hexadecimal data *@param {File} source 
 */
function fileType (source) {
  // The sourcr passed in is of type File, which is inherited from Blob
  // console.log(source instanceof Blob); true
  const reader = new FileReader()
  reader.onload = () = > {
    // Result is an array of binary data
    // console.log('event', reader.result);
    // Read binary data with TypeArray
    buffer = new Uint8Array(reader.result);
    const hexArr = []
    // Take only the first four bits
    for (let i = 0; i < 4; i++) {
      // Convert binary to hexadecimal
      hexArr.push(buffer[i].toString(16))}const hexStr = hexArr.join(' ')
    console.log(hexStr);
  }
  // Read into memory
  reader.readAsArrayBuffer(source)
}
Copy the code

5. Image compression

The front-end image compression is mainly based on two canvas apis

HTMLCanvasElement.toBlob()

HTMLCanvasElement.toDataURL()

Various types of transformation relationships

There is an encoderOptions parameter that specifies the image display quality if the image format is image/ JPEG or image/webp. This parameter is used to compress the image. For JPG images, we compare the image pixels before and after compression, but there is no change, which can be understood as the photo has not been clipped, and the image is processed by the compression algorithm at the bottom of the browser

This parameter does not necessarily work if the converted image is required to be in PNG format, which has been proven to be the case, and sometimes the exported PNG image can be larger. Then we found that the original PNG image on the Blue Lake was compared with the compressed PNG image through the blue Lake, the length and width of the latter is half of the former, so I think the blue Lake is compressed by cropping the PNG image. From the above, we can know that we only input three image types: JPEG, PNG and GIF. So we distinguish between image types:

  1. GIF images do not undergo compression
  2. Convert to PNG => Compress by cropping
  3. Convert to JPEG => Compress with the encoderOptions parameter

Compression process: the File/Blob = > dataURL = > Image Object, using the canvas, = > = > canvas. ToBlob/canvas toDataURL

// The main code
/** * Default configuration */
 const defaultOptions = {
  // Compress the quality
  quality: 0.5.// The type of output image
  mimeType: 'image/jpeg'.// The default is not PNG
  isPNG: false
}

class ImageCompress {
  / * * * *@param {File|Blob} Source The source object to compress */
   constructor () {
    this.source = null
    this.options = defaultOptions
    this.canvas = null
    this.blob = null
    this.isPNG = this.options.isPNG
  }
  async compress (source, options = {}) {
    this.source = source
    // Merge options
    Object.assign(this.options, options)
    // Compress the information before
    console.log('Size before compression:The ${this.source.size/1000}`.'Type before compression:The ${this.source.type}`);
    this._inspectParams()
    // The output box is passed in type File
    try {
      // Convert all methods of callback type to await form
      const dataURL = await this._fileToDataURLAsync(this.source)
      const image = await this._dataURLToImageASync(dataURL)
      const canvas = await this._imageToCanvasAsync(image)
      const blob = await this._canvasToBlobAsync(canvas)
      console.log('Compressed size:${blob.size/1000}`.'Compressed type:${blob.type}`);
      // It is possible that the compression is larger than the original, so we need to determine the size of the front and back
      console.log('blob.size', blob.size, 'this.source.size'.this.source.size);
      return blob.size > this.source.size ? this.source : blob
    } catch (error) {
      console.log('compress error:', error);
      return false}}/ * * *@desc Get dataURL *@param {File} file 
   * @returns {Promise}* /
  _fileToDataURLAsync (file) {
    return new Promise((resolve, reject) = > {
      const reader = new FileReader()
      reader.onload = () = > resolve(reader.result)
      reader.onerror = () = > reject(reader.error)
      reader.readAsDataURL(file)
    })
  }
  _dataURLToImageASync (dataUrl) {
    return new Promise((resolve, reject) = > {
      const image = new Image()
      image.src = dataUrl
      image.onload = () = > resolve(image)
      image.onerror = reject
    })
  }
  _imageToCanvasAsync (image) {
    // When drawing a canvas here, the width of the canvas is the original width and height of the original canvas
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    / / naturalWidth naturalHeight is the attribute of image, cannot be modified
    const { naturalWidth, naturalHeight } = image
    PNG for clipping compression, JPEG for quality compression
    let clipRatio = 1
    if (this.isPNG) {
      // use quality as the clipping factor
      clipRatio = this.options.quality
    }
    // Set the canvas width and height
    canvas.width = naturalWidth*clipRatio
    canvas.height = naturalHeight*clipRatio
    
    // When PNG is converted to JPEG, the canvas background is black by default
    // The canvas background is directly filled with white
    ctx.fillStyle = '#fff'
    ctx.fillRect(0.0, canvas.width, canvas.height)
    // Paint to canvas
    ctx.drawImage(image, 0.0, canvas.width, canvas.height)
    return canvas
  }
  _canvasToBlobAsync (canvas) {
    const { mimeType, quality } = this.options
    return new Promise((resolve, reject) = > {
      canvas.toBlob((blob) = > resolve(blob), mimeType, quality)
    })
    
  }
  // ...
}
Copy the code

Train of thought

First, open Blue Lake, find a slice at random, open the Developer tool’s Network, and click download slice button

Preview shows the image we just downloaded. We copy the link to the browser and download a file directly

Here is a comparison of the two file properties. The image on the left is a PNG image downloaded directly by clicking the download button. With no suffix on the right, let’s use vscode’s hexdump plugin to look at the hexadecimal data of the file

The original is still PNG file, we add a suffix to it, it will open normally.

Another detail is that the size of the left image is smaller than that of the right image, indicating that Blue Lake compressed the image after downloading it, rather than directly downloading the compressed image.

So what we need to do is block the image request. We look in the Chrome Extension documentation and see that there is a related API: Chrome.webrequest

Use the chrome.webRequest API to observe and analyze traffic and to intercept, block, or modify requests in-flight.

What we need to do is to intercept the image request through the API provided by Chrome. webRequest, and then compress the image data obtained with canvas. Here we can make a judgment that if the compressed image is less than 10KB, we should directly download it to the local. If the size is larger than 10KB, upload it to the resource server (10KB can be defined according to the project). A pop-up window will appear on the right side of the page with a preview image and HTTP link

Another thing to consider is that if the UI simply loses a large image and asks to change it, the plugin needs to provide a function for uploading images

Open dry

Project construction

We know that the Chrome plugin is divided into three parts:

  • Content-scripts cannot cross domains
  • Popup can cross domain
  • Background can cross domain

There is also a configuration file and a static resource file. Therefore, there need to be three export files, equivalent to output a multi-page application, we combined with vue2. X technology stack, using Webpack to do customization.

The general catalogue is as follows:

├ ─ ─ package - lock. Json ├ ─ ─ package. The json ├ ─ ─ the readme. Md ├ ─ ─ the SRC | ├ ─ ─ background | ├ ─ ─ contentScripts | ├ ─ ─ the manifest. Json | ├ ─ ─ popup | └ ─ ─ utils ├ ─ ─ the static | └ ─ ─ images └ ─ ─ webpack. Config. JsCopy the code

webpack.config.js

We need to compile the three parts of SRC into a file, so there are three entries

const path = require('path')
// vue-loder compiles vue files
const VueLoaderPlugin = require('vue-loader/lib/plugin')
// Clean up the files in the build directory
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
/ / copy plugin
const CopyWebpackPlugin = require('copy-webpack-plugin');

// webpack-cli command:
// --progress Prints the percentage of compilation progress
// https://webpack.docschina.org/api/cli
// --watch, -w listen for file system changes


module.exports = {
    mode: 'development'.// Default is production
    entry: {
            'background': path.resolve(__dirname, `./src/background/index.js`),
            'popup': path.resolve(__dirname, `./src/popup/index.js`),
            'contentScripts': path.resolve(__dirname, `./src/contentScripts/index.js`)},output: {
            filename: '[name].js'./ / file name
            path: path.resolve(__dirname, `./dist/`), / / path
            // https://webpack.docschina.org/configuration/output/#outputpublicpath
            publicPath: '/' //script import path
    },
    resolve: {
            // Do not write the corresponding suffix when importing the path
            extensions: ['.js'.'.vue'].alias: {
                    // direct to SRC with @
                    The '@': path.resolve(__dirname, './src'),}},// https://webpack.docschina.org/configuration/watch/
    // Listen for file changes and recompile when they are modified
    // Do not run build NPM every time
    watchOptions: {
            aggregateTimeout: 800.poll: 1000.ignored: [
                    '**/node_modules'.'**/docs']},// https://stackoverflow.com/questions/48047150/chrome-extension-compiled-by-webpack-throws-unsafe-eval-error
    // The default is evel. Using eval, Chrome considers it unsafe and will fail to introduce extensions
    // I don't want the map file either, so I just empty it
    devtool: ' '.module: {
            rules: [{test: /\.vue$/.//vue-loader compiles the vue module
                            use: 'vue-loader'
                    },
                    {
                            test: /\.js$/,
                            exclude: /node_modules/,
                            use: {
                                    loader: 'babel-loader'}}, {test: /\.css$/.// Order from right to left
                            use: [
                                    'style-loader'.'css-loader',]}, {test: /\.(woff2? |eot|ttf|otf)(\? . *)? $/,
                            loader: "url-loader".options: {
                                    limit: 10000}}},plugins: [
            new CleanWebpackPlugin(),
            new VueLoaderPlugin(),  // Vue-loader plug-in is enabled
            // Copy some files directly to the fixed location
            new CopyWebpackPlugin([
                    { from: path.resolve(__dirname, './src/popup/popup.html'), to: ' ' },
                    { from: path.resolve(__dirname, './src/manifest.json'), to: ' ' },
                    { from: path.resolve(__dirname, './static/'), to: './static/'}, {copyUnmodified: true}})]Copy the code

package.json

"scripts": {
    "watch": "webpack --progress --w --mode=development"."build": "webpack --progress"
},
Copy the code

Background part

  • Intercepting requests via API, sending intercepted requests to background (the webRequest API can only be accessed from background)
  • Get the image resource according to the request API
  • Gets the file type and compresses it
  • Upload/download

Request request on onBeforeRequest. Only matched urls are captured. The onBeforeRequestCallback callback returns a value and will be disabled only if return {cancel: true}

// Block image download requests from blue Lake web page
chrome.webRequest.onBeforeRequest.addListener(
  onBeforeRequestCallback,
  { 
    // Match rules
    urls: ["https://alipic.lanhuapp.com/*?noCache=true"[]},"blocking"]);Copy the code
function onBeforeRequestCallback (details) {
  // The url is the link to download the image
  // tabId indicates the page id
  const { url, tabId } = details
  // ...
  return { cancel: cancelVariable };
}
Copy the code

The blob object has an attribute size. According to the minimum upload size we set (such as 10KB), we can directly determine whether the current image should be directly downloaded or uploaded.

// Get the image...
await axios({
  url,
  method: 'get'.responseType: 'blob' // Returns the blob type
})
Copy the code
  1. Image larger than preset size, compression upload

The process of compressing pictures is described above. After compression, use the interface provided by the company, directly upload

// The compressed object returns a Blob object
async uploadImg (imageBlob) {
    // Add binary data to the FormData object
    const formData = new FormData()
    formData.append('file', imageBlob)
    try {
      const { data } = await axios({
        method: 'post'.url: this.uploadImgApi,
        headers: {
          "Content-Type": "multipart/form-data"
        },
        data: formData
      })
      const { groupName, filePath } = data.data
      return `The ${this.picUrl}/${groupName}/${filePath}`
    } catch (error) {
      console.log('error', error);
      return false}}Copy the code

Get the image link, and background from Chrome.tabs. SendMessage sends data to Content-script

  1. Then the image is smaller than the preset size

Blob types cannot be passed directly in Chrome.tabs. SendMessage. They need to be converted to a string (base64)

// Sample code
async function fileToDataURLAsync (file) {
  return new Promise((resolve, reject) = > {
    const reader = new FileReader()
    reader.onload = () = > resolve(reader.result)
    reader.onerror = () = > reject(reader.error)
    reader.readAsDataURL(file)
  })
}
if (imgBlob.size < minSizeToUpload) {
  // If it is smaller than that, it is not compressed and uploaded directly to content-script
  // BloB data cannot be passed directly
  // Convert to base64 via the fileReader interface
  const base64Str = await fileToDataURLAsync(imgBlob)
  sendMessage(tabId, {
    status: 'success'.body: base64Str,
    bodyType: 'base64'.message: ' '
  })
  return
}
Copy the code

The content – part scripts

  • A pop-up window on the right shows the preview and access links, with a delete button

Download the pictures

As mentioned earlier, we will pass in a base64 string, which we need to download directly from the web page.

downloadWithLink (str) {
  console.log('str', str);
  const link = document.createElement("a");
  link.href = str
  link.download = 'download.png';
  link.click();
  link.remove();
}
Copy the code

Manually mount

The Content-script part can be thought of as an H5 because it is inserted into the page and shares the DOM with the page. Usually, there is a node with a preset ID equal to app in vUE project development, and then VUE specifies this ID to mount. Here, we choose manual mount because it is inserted into the page

// src/contentScript.js
import Vue from 'vue'
import App from './App.vue'
// Manually mount
const MyComponent = Vue.extend(App)
const component = new MyComponent().$mount()
window.onload = () = > {
  const body = document.getElementsByTagName('body') [0]
  body.appendChild(component.$el)
}
Copy the code

Then there is general page development, see project repository

Popup part todo

  • Property setting page: whether to block, whether to compress, the size of the picture to upload, the number of display, etc
  • Image upload page

To optimize the

1. Obtain the image type

ReadAsArrayBuffer is used to read the file for the image type above. This is fine, but if the image is too large, the API reads all the data into memory and the browser crashes. This is similar to Node’s fs.readfile, in that reading too much data at once can leak memory, so Node provides a Stream for reading. Since browsers provide apis for manipulating in-memory data, it’s important to take a look at memory.

The source read is of type File, which is derived from the Blob type. Blob has a slice method that reads data. Like slice on arrays, it does not operate on the original data, but copies a portion of it. So we just need to intercept the data before readAsArrayBuffer.

/** * The second version reads part of the Buffer to determine magic number *@param {File} source 
 */
function fileType2 (source) {
  const reader = new FileReader()
  reader.onload = () = > {
    buffer = new Uint8Array(reader.result);
    // console.log('buffer', buffer);
    let hexStr = ' '
    for (let i = 0; i < 8; i++) {
      hexStr += buffer[i].toString(16)}console.log(hexStr);
  }
  reader.readAsArrayBuffer(source.slice(0.8))}Copy the code

2. Content-script updates instantly

This part of the code is in the V1 branch

One of the things that’s really annoying about developing the Content-script part is that when you update the code, you have to refresh the page, because in manifest.json we define the timing of the content-Script injection, which is after the document is loaded, New code is injected only when the document is reloaded. We want to update the Content-Script when updating extensions, and when opening and refreshing web pages. The declarative content-script does not meet this requirement. As we mentioned earlier, content-script also contains a kind of programmatically injected injected injector-script. Need to use chrome. Tabs. ExecuteScript inject inject – script this API, this load was controlled.

The first step is to load or update the extension, and background.js is reloaded

// background.js
// This API gets information about all the pages currently open
chrome.tabs.query({}, (tabList) = > {
  if (!Array.isArray(tabList)) return
  // Filter the currently open Blue Lake page
  const openingPages = tabList.filter(tab= > tab.url.includes(blockingDomain))
  openingPages.forEach(page= > injectFile(page))
});

// Declare the files to be injected in advance
const injectScripts = ['contentScripts.js']

function injectFile (page) {
  injectScripts.forEach(script= > {
    chrome.tabs.executeScript(page.id, {
      file: `${script}`.runAt: 'document_idle' // represents an idle moment after the DOM is loaded
    }, () = > {
      console.log('injectFile', chrome.runtime.lastError); })})}Copy the code

Step 2, inject inject-script when the web page is refreshed or opened

How to inject script immediately after Page reload?

Background can make use of the chrome. WebNavigation. OnCommittedapi, at the time of dom already exists, triggered the callback

chrome.webNavigation.onCommitted.addListener((page) = > {
  // only listen on the blue lake domain name
  if(! page.url.includes(blockingDomain))return
  injectFile(page)
});
Copy the code

In the third step, we immediately injected the script into the web page, but the web page may still have the DOM generated by the script before injection, so we must also destroy the DOM

// Find the generated DOM node, remove
const uploadDom = document.getElementById('upload')
document.body.removeChild(uploadDom)
Copy the code

But how do we know when to destroy the DOM, refresh the page or load the page? That’s not the case, just the extension reloading in the first step.

This article provides a very cool way to search. Background and Content-script can establish long connections, which will only be broken when the extension is uninstalled or reloaded, in which case the onDisconnect callback will be triggered, in which case the DOM will be destroyed

const connectObj = chrome.runtime.connect();
connectObj.onDisconnect.addListener(() = > {
	const uploadDom = document.getElementById('upload')
	console.log('__remove contentScripts__');
	document.body.removeChild(uploadDom)
});
Copy the code

3. PNG image compression to be optimized

Transparent background PNG becomes black after canvas compression

reference

  • [1] Chrome plugin development overview

  • [2] Chrome Extension official documentation

  • [3] Chrome extension | how to updating the content script