vlad.golubev vlad.golubev - 1 month ago 8
Javascript Question

Can't connect API to Chrome extension

I am developing chrome extension. I want to connect some API to current tab after click on button in

popup.html
. I use this code in
popup.js
:

$('button').click(function() {
chrome.tabs.executeScript({
file: 'js/ymaps.js'
}, function() {});
});


In
ymaps.js
I use following code to connect API to current tab:

var script = document.createElement('script');
script.src = "http://api-maps.yandex.ru/2.0-stable/?load=package.standard&lang=ru-RU";
document.getElementsByTagName('head')[0].appendChild(script);


This API is needed to use Yandex Maps. So, after that code I create
<div>
where map should be placed:

$('body').append('<div id="ymapsbox"></div>');


And this simple code only loads map to created
<div>
:

ymaps.ready(init);//Waits DOM loaded and run function
var myMap;
function init() {
myMap = new ymaps.Map("ymapsbox", {
center: [55.76, 37.64],
zoom: 7
});
}


I think, everything is clear, and if you are still reading, I'll explain what is the problem.
When I click on button in my
popup.html
I get in Chrome's console
Uncaught ReferenceError: ymaps is not defined
. Seems like api library isn't connected. BUT! When I manually type in console
ymaps
- I get list of available methods, so library is connected. So why when I call
ymaps
-object from executed
.js
-file I get such an error?

UPD: I also tried to wrap
ymaps.ready(init)
in
$(document).ready()
function:

$(document).ready(function() {
ymaps.ready(init);
})


But error is still appearing.
Man below said that api library maybe isn't loaded yet. But this code produces error too.

setTimeout(function() {
ymaps.ready(init);
}, 1500);


I even tried to do such a way...

chrome.tabs.onUpdated.addListener(function(tabId, changeInfo) {
if (changeInfo.status == "complete") {
chrome.tabs.executeScript({
file: 'js/gmm/yandexmaps.js'
});
}
});

Answer

ymaps is not defined because you're trying to use it in the content script, while the library is loaded in the context of the page (via the <script> tag).

Usually, you can solve the problem by loading the library as a content script, e.g.

chrome.tabs.executeScript({
    file: 'library.js'
}, function() {
    chrome.tabs.executeScript({
        file: 'yourscript.js'
    });
});

However, this will not solve your problem, because your library loads more external scripts in <script> tags. Consequently, part of the library is only visible to scripts within the web page (and not to the content script, because of the separate script execution environments).

Solution 1: Intercept <script> tags and run them as a content script.

Get scriptTagContext.js from https://github.com/Rob--W/chrome-api/tree/master/scriptTagContext, and load it before your other content scripts. This module solves your problem by changing the execution environment of <script> (created within the content script) to the content script.

chrome.tabs.executeScript({
    file: 'scriptTagContext.js'
}, function() {
    chrome.tabs.executeScript({
        file: 'js/ymaps.js'
    });
});

See Rob--W/chrome-api/scriptTagContext/README.md for documentation.
See the first revision of this answer for the explanation of the concept behind the solution.

Solution 2: Run in the page's context

If you -somehow- do not want to use the previous solution, then there's another option to get the code to run. I strongly recommend against this method, because it might (and will) cause conflicts in other pages. Nevertheless, for completeness:

Run all code in the context of the page, by inserting the content scripts via <script> tags in the page (or at least, the parts of the extension that use the external library). This will only work if you do not use any of the Chrome extension APIs, because your scripts will effectively run with the limited privileges of the web page.

For example, the code from your question would be restructed as follows:

var script = document.createElement('script');
script.src = "http://api-maps.yandex.ru/2.0-stable/?load=package.standard&lang=ru-RU";
script.onload = function() {
    var script = document.createElement('script');
    script.textContent = '(' + function() {
        // Runs in the context of your page
        ymaps.ready(init);//Waits DOM loaded and run function
        var myMap;
        function init() {
            myMap = new ymaps.Map("ymapsbox", {
                center: [55.76, 37.64],
                zoom: 7
            });
        }
    } + ')()';
    document.head.appendChild(script);
};
document.head.appendChild(script);

This is just one of the many ways to switch the execution context of your script to the page. Read Building a Chrome Extension - Inject code in a page using a Content script to learn more about the other possible options.