Related
I'm trying to develop a simple chrome extension. There is a pageAction's default icon that should appear on the pages with a specific URL (http://www.example.com/*).
There is a two file
manifest.json
{
"manifest_version": 2,
"name": "name",
"description": "description",
"version": "1.0",
"background": {
"scripts": [
"background.js"
],
"persistent": false
},
"page_action": {
"default_icon" : "images/icons/19.png"
},
"permissions": [
"declarativeContent"
]
}
background.js
chrome.runtime.onInstalled.addListener(function () {
chrome.declarativeContent.onPageChanged.removeRules(undefined, function () {
chrome.declarativeContent.onPageChanged.addRules([
{
// rule1
conditions : [
new chrome.declarativeContent.PageStateMatcher({
pageUrl : {urlPrefix : 'http://www.example.com/'}
})
],
actions : [
new chrome.declarativeContent.ShowPageAction()
]
},
{
// rule2
conditions : [
new chrome.declarativeContent.PageStateMatcher({
pageUrl : {queryContains : 'q1=green'}
})
],
actions : [
new chrome.declarativeContent.SetIcon({
path : {"19" : "images/icons/green.png"}
})
]
}
]);
});
});
rule1 should show pageAction's icon and rule2 should change icon to green version on the pages with URL that looks like http://www.example.com/?q1=green
But during installation of extension things come to:
Error in response to events.removeRules: Error: Invalid value for argument 1. Property '.0': Value does not match any valid type choices.
I dug deeply into this error, and it seems like the documentation does not reflect well the fact that using path parameter is not implemented. This is certainly a bug, tracked here.
For now, to fix this you need to load the image and convert it to ImageData format before calling SetIcon.
// Takes a local path to intended 19x19 icon
// and passes a correct SetIcon action to the callback
function createSetIconAction(path, callback) {
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
var image = new Image();
image.onload = function() {
ctx.drawImage(image,0,0,19,19);
var imageData = ctx.getImageData(0,0,19,19);
var action = new chrome.declarativeContent.SetIcon({imageData: imageData});
callback(action);
}
image.src = chrome.runtime.getURL(path);
}
chrome.declarativeContent.onPageChanged.removeRules(undefined, function () {
createSetIconAction("images/icons/green.png", function(setIconAction) {
chrome.declarativeContent.onPageChanged.addRules([
/* rule1, */
{
conditions : [
new chrome.declarativeContent.PageStateMatcher({
pageUrl : {queryContains : 'q1=green'}
})
],
actions : [ setIconAction ]
}
]);
});
});
If needed, this can be generalized to support high-DPI icon (19 + 38):
function createSetIconAction(path19, path38, callback) {
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
var image19 = new Image();
image19.onload = function() {
ctx.drawImage(image19,0,0,19,19); // fixed
var imageData19 = ctx.getImageData(0,0,19,19);
var image38 = new Image();
image38.onload = function() {
ctx.drawImage(image38,0,0,38,38);
var imageData38 = ctx.getImageData(0,0,38,38);
var action = new chrome.declarativeContent.SetIcon({
imageData: {19: imageData19, 38: imageData38}
});
callback(action);
}
image38.src = chrome.runtime.getURL(path38);
}
image19.src = chrome.runtime.getURL(path19);
}
In fact, you can use new chrome.declarativeContent.SetIcon({ path:'yourPath.png' }),
No need to specify size path: {"19": "images/icons/green.png"}, its default value is: 16
Use declarativeContent.SetIcon need to pay attention to a problem, it is actually a bug.
Actual use of path will eventually be automatically converted to ImageData.
see screenshot:
The root cause of the error of declarativeContent.SetIcon is: it is an asynchronous API, but at the same time it has no asynchronous callback. The only thing you can do is wait.
const action = new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' });
console.log(action.imageData); // => undefined
see screenshot:
// invalid
new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' }, action => console.log(action));
It takes a while to wait:
const action = new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' });
setTimeout(() => {
console.log(action.imageData); // {16: ArrayBuffer(1060)}
}, 5);
see screenshot:
When you understand the reason for the error of SetIcon, the problem will be solved well.
You only need to put the operation of addRules in the event.
onInstalled event
const rule2 = { id: 'hideAction', conditions: [...], actions: [new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' })]};
chrome.runtime.onInstalled.addListener(() => {
chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
chrome.declarativeContent.onPageChanged.addRules([rule2]);
});
});
pageAction.onClicked
const rule2 = { id: 'hideAction', conditions: [...], actions: [new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' })]};
chrome.pageAction.onClicked.addListener(() => {
if (condition) {
chrome.declarativeContent.onPageChanged.removeRules(['hideAction']);
} else {
chrome.declarativeContent.onPageChanged.addRules([rule2]);
}
});
There are some related information:
SetIcon source code
declarativeContent.SetIcon = function (parameters) {
// TODO(devlin): This is very, very wrong. setIcon() is potentially
// asynchronous (in the case of a path being specified), which means this
// becomes an "asynchronous constructor". Errors can be thrown *after* the
// `new declarativeContent.SetIcon(...)` call, and in the async cases,
// this wouldn't work when we immediately add the action via an API call
// (e.g.,
// chrome.declarativeContent.onPageChange.addRules(
// [{conditions: ..., actions: [ new SetIcon(...) ]}]);
// ). Some of this is tracked in http://crbug.com/415315.
setIcon(
parameters,
$Function.bind(function (data) {
// Fake calling the original function as a constructor.
$Object.setPrototypeOf(this, nativeSetIcon.prototype);
$Function.apply(nativeSetIcon, this, [data]);
}, this)
);
};
Discussion of related issues:
http://crbug.com/415315
No solution
As the guys before me mentioned, this is a bug. There are no solutions, only workarounds.
Workarounds
#1: Draw icon using canvas
As Xan described in his answer already.
#2 Wait for icon load (timeout hack)
Thanks to weiya-ou's answer I realized that I can just wait for the async icon data transformation to finish.
// Make your handler `async`
chrome.runtime.onInstalled.addListener(async () => {
const action = await new chrome.declarativeContent.SetIcon({
path: {
19: 'images/19.png',
38: 'images/38.png',
},
})
// THE WAIT STARTS
// Wait max. 10 loops
for (let i = 0; i < 10; i++) {
// Create a promise
const checkAvailability = new Promise((resolve) => {
// Resolve promise after 100ms
setTimeout(() => resolve(!!action.imageData), 100)
})
// Wait for the promise resolution
const isAvailable = await checkAvailability
// When image available, we are done here
if (isAvailable) break
}
// THE WAIT ENDS
const condition = new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostEquals: 'my.page.net' },
})
chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
chrome.declarativeContent.onPageChanged.addRules([
{
conditions: [condition],
actions: [action],
},
]);
});
});
#3 Use chrome.tabs
You would need the tabs permission (as said here).
chrome.tabs.onUpdated.addListener((tabId, { status }, { url }) => {
// Only check when URL is resolved
if (status !== 'complete') return
// What is our target page?
const isOurPage = url?.match(/my\.page\.net/)
if (isOurPage) {
// Show active icon
chrome.pageAction.setIcon({
path: {
19: 'images/19.png',
38: 'images/38.png',
},
tabId,
})
} else {
// Show inactive icon
chrome.pageAction.setIcon({
path: {
19: 'images/19-inactive.png',
38: 'images/38-inactive.png',
},
tabId,
})
}
})
I'm trying to setup communication between my Vue browser app and the electron main process, but it is not working.
Before startBot() is even called, I get an error message that __Dirname is unknown. But this constant is nowhere in to be found in the code.
What am I doing wrong?
https://gist.github.com/Quenos/7d6dbe4f5410739499ea9e3b0b4f961a.js
This is the background.js where I open the browser window with a preload. This has the purpose of making window available to the browser process
function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 1300,
height: 1100,
title: "Hedgehog TRDR Bot",
icon: path.join(__static, "hedgehog.jpg"),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
// __static is set by webpack and will point to the public directory
preload: path.resolve(__static, "preload.js"),
},
});
This is said preload.js
const { contextBridge, ipcRenderer } = require("electron");
const validChannels = ["READ_FILE", "WRITE_FILE"];
contextBridge.exposeInMainWorld("ipc", {
send: (channel, data) => {
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
on: (channel, func) => {
if (validChannels.includes(channel)) {
// Strip event as it includes `sender` and is a security risk
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
});
The main process which contains listeners that then will do file handling
const { ipcMain } = require("electron");
const fs = require("fs");
var file;
ipcMain.on("OPEN_FILE", (event, payload) => {
console.log("daaro");
file = fs.createWriteStream(payload.path);
event.reply("OPEN_FILE", { content: "roger" });
});
ipcMain.on("TEST_FILE", (event, payload) => {
console.log("daaro");
file.write(payload.path);
event.reply("OPEN_FILE", { content: "roger" });
});
And the browser process which send requests to do file handling:
async startBot() {
window.ipc.send("OPEN_FILE", { path: "./HH_trdr_bot.csv" });
}
In the meantime I've found this article that perfectly answers my question
https://medium.com/swlh/how-to-safely-set-up-an-electron-app-with-vue-and-webpack-556fb491b83
Docs of Vue CLI Plugin Electron Builder contain description of how to do it, but it's a little scattered.
First, see how to configure a preload script, i.e.:
vue.config.js
module.exports = {
// ...
pluginOptions: {
electronBuilder: {
preload: 'src/preload.js',
}
}
}
and then, repeat the path in BrowserWindow's webPreferences.preload constructor option.
preload: path.join(__dirname, 'preload.js')
Finally, in src/preload.js expose the IPC Renderer as described in IPC Without Node Integration.
src/preload.js
import { contextBridge, ipcRenderer } from 'electron'
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld('ipcRenderer', {
send: (channel, data) => {
// whitelist channels
let validChannels = ['toMain']
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
},
receive: (channel, func) => {
let validChannels = ['fromMain']
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args))
}
}
})
It might be also a good idea to unsubscribe the event listener when the component that subscribed is about to be destroyed.
I made mine working by putting the preload.js file inside public folder, in the https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/guide.html#preload-files docs, there is a little diagram under Folder Structure section that says ├── public/ # Files placed here will be available through __static or process.env.BASE_URL, so what I did is simply, use the __static variable as described in docs, and append it with \preload.js
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
preload: __static + '/preload.js', // <-- simple solution
}
})
I have a content script in a Chrome Extension that's passing messages. Every so often, when the content script calls
chrome.runtime.sendMessage({
message: 'hello',
});
it throws an error:
Uncaught Error: Extension context invalidated.
What does this error mean? I couldn't find any documentation on it.
It doesn't happen consistently. In fact, it's hard to reproduce. Seems to happen if I just leave the page open for a while in the background.
Another clue: I've written many Chrome Extensions with content scripts that pass messages and I haven't seen this error before. The main difference is that this content script is injected by the background page using
chrome.tabs.executeScript({
file: 'contentScript.js',
});
Does using executeScript instead of the manifest file somehow change the lifecycle of the content script?
This is certainly related to the message listener being lost in the middle of the connection between content and background scripts.
I've been using this approach in my extensions, so that I have a single module that I can use in both background and content scripts.
messenger.js
const context = (typeof browser.runtime.getBackgroundPage !== 'function') ? 'content' : 'background'
chrome.runtime.onConnect.addListener(function (port) {
port.onMessage.addListener(function (request) {
try {
const object = window.myGlobalModule[request.class]
object[request.action].apply(module, request.data)
} catch () {
console.error(error)
}
})
})
export function postMessage (request) {
if (context === 'content') {
const port = chrome.runtime.connect()
port.postMessage(request)
}
if (context === 'background') {
if (request.allTabs) {
chrome.tabs.query({}, (tabs) => {
for (let i = 0; i < tabs.length; ++i) {
const port = chrome.tabs.connect(tabs[i].id)
port.postMessage(request)
}
})
} else if (request.tabId) {
const port = chrome.tabs.connect(request.tabId)
port.postMessage(request)
} else if (request.tabDomain) {
const url = `*://*.${request.tabDomain}/*`
chrome.tabs.query({ url }, (tabs) => {
tabs.forEach((tab) => {
const port = chrome.tabs.connect(tab.id)
port.postMessage(request)
})
})
} else {
query({ active: true, currentWindow: true }, (tabs) => {
const port = chrome.tabs.connect(tabs[0].id)
port.postMessage(request)
})
}
}
}
export default { postMessage }
Now you'll just need to import this module in both content and background script. If you want to send a message, just do:
messenger.postMessage({
class: 'someClassInMyGlobalModuçe',
action: 'someMethodOfThatClass',
data: [] // any data type you want to send
})
You can specify if you want to send to allTabs: true, a specific domain tabDomain: 'google.com' or a single tab tabId: 12.
I have around four components using the same mixin that I created. In order to get the dimensions of this.$el (the CORRECT ones), I had to implement window.onload = function() mixin. My mixin looks like this:
module.exports = {
methods: {
onPageload: function( callback ) {
window.onload = function() {
callback();
}
}
}
};
Very simple. However, more than one component uses it. I'm creating a slideshow. My Slides component uses it, my Slide component, my Thumb component, and my overall Slider component.
HOWEVER only the very last one to compile fires it.
var pageLoad = require('../../mixins/Pageload');
module.exports = {
template: require('./templates/thumbs.html'),
replace: true,
data: function() {
return {
style: {
width: 800
},
count: 2
}
},
computed: {
styles: function() {
return {
width: this.style.width + 'px'
}
}
},
mixins: [pageLoad],
props: ['count'],
attached: function() {
this.onPageload( this.setDimensions );
},
methods: {
// Set dimensions for the first time
setDimensions: function() {
console.log('setting thumb');
this.style.width = this.$parent.slideWidth;
}
}
};
My other "parent" components do the same thing to set their dimensions. This is the MOST inner child of the entire thing.. it's the ONLY one that fires. If I erase it from here, the next child up is the only one that fires. They are overwriting each other in a way. My vue instance is here:
new Vue({
el: '#slideshow',
components: {
'sliderarrows': require('../../components/slider/SliderArrows'),
'sliderthumb': require('../../components/slider/SliderThumb'),
'sliderslide': require('../../components/slider/SliderSlide'),
'slides': require('../../components/slider/SliderSlides'),
'slider': require('../../components/slider/Slider'),
'thumbnails': require('../../components/slider/SliderThumbs')
}
});
So the thumbnails component is the only one that's firing the mixin method. I feel like it's something to do with the whole compilation of require() but I'm not sure since I'm not that familiar with the Node.js module format.
Thanks for any help on how to get this to work!
I had to change my mixin to this:
module.exports = {
methods: {
onPageload: function( callback ) {
window.onload = callback();
}
}
};
It was something with the window.onload opening a new function.
In my app I have layouts and views for those layouts. Layouts only change on login/logout, but the other special case is on pageload I need to load the proper layout. However, in my defaultAction my layout does not actually render after it returns and so when the view tries to render, the el it is supposed to be contained in does not exist.
// Filename: router.js
var app_router;
define( [ 'views/layouts/beta', 'views/beta/requestInvite', 'views/beta/login', 'views/app/dashboard' ],
function(betaLayout, requestInviteView, loginView, dashboardView) {
var AppRouter = Backbone.Router.extend( {
routes : {
// Pages
'login' : 'login',
'dashboard' : 'dashboard',
// Default
'*actions' : 'defaultAction'
},
// Pages
login : function() {
loginView.render();
},
dashboard : function() {
dashboardView.render();
},
// Default
defaultAction : function(actions) {
betaLayout.render();
requestInviteView.render();
}
});
var initialize = function() {
app_router = new AppRouter;
$('a').live('click', function() {
var href = $(this).attr('href');
// only navigate to real links
if(href == undefined)
return;
app_router.navigate(href, {trigger: true});
return false;
});
Backbone.history.start({pushState: true});
};
return {
initialize : initialize
};
});
How can I have my layout render completely before my view?
Define a callback for betaLayout.render() to take as an argument, that gets executed when the render is actually complete.
E.g., betaLayout.render() would look something like:
render: function(callback) {
/* code to render your element,
using the following line when all
asynchronous calls are complete */
if (callback) callback();
}
and your defaultAction would use it like so, passing the second render() as its callback.
betaLayout.render(requestInviteView.render);
The problem was that because my layout didn't render before my view was initialized, the el was empty. What I did was convert all of my objects to return the object instead of the instance, and let them render once they were initialized. This way, when I declare a new MyLayout and then a new MyView, I can be guaranteed that MyView's el is valid.