Использование Angular в расширении на странице, которая уже использует angular

Я пишу расширение Chrome (здесь) для моей библиотеки, в котором используется angular для его пользовательский интерфейс. Он отлично работает на страницах, которые не используют angular, но это вызывает проблемы со страницами, которые имеют angular. Например, на странице angular docs:

Uncaught Error: [$injector:modulerr] Failed to instantiate module docsApp due to:
  Error: [$injector:nomod] Module 'docsApp' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.
  http://errors.angularjs.org/1.2.7/$injector/nomod?p0=docsApp
at chrome-extension://cfgbockhpgdlmcdlcbfmflckllmdiljo/angular.js:78:14
at chrome-extension://cfgbockhpgdlmcdlcbfmflckllmdiljo/angular.js:1528:19
at ensure (chrome-extension://cfgbockhpgdlmcdlcbfmflckllmdiljo/angular.js:1453:40)
at module (chrome-extension://cfgbockhpgdlmcdlcbfmflckllmdiljo/angular.js:1526:16)
at chrome-extension://cfgbockhpgdlmcdlcbfmflckllmdiljo/angular.js:3616:24
at Array.forEach (native)
at forEach (chrome-extension://cfgbockhpgdlmcdlcbfmflckllmdiljo/angular.js:302:13)
at loadModules (chrome-extension://cfgbockhpgdlmcdlcbfmflckllmdiljo/angular.js:3610:7)
at createInjector (chrome-extension://cfgbockhpgdlmcdlcbfmflckllmdiljo/angular.js:3550:13)
at doBootstrap (chrome-extension://cfgbockhpgdlmcdlcbfmflckllmdiljo/angular.js:1298:22)
http://errors.angularjs.org/1.2.7/$injector/modulerr?p0=docsApp&p1=Error%3A…xtension%3A%2F%2Fcfgbockhpgdlmcdlcbfmflckllmdiljo%2Fangular.js%3A1298%3A22) angular.js:78

Странно то, что кажется, что мое расширение действительно использует angular или нет. Все, что мне нужно воспроизвести, это включить angular в мой manifest.json как content_script, и эта ошибка будет выбрана. Любые идеи о том, как я мог бы сделать эту работу, не испортив сайт angular, будут очень благодарны.

Как я уже сказал, неважно, действительно ли я использую angular или нет, но это все, что я делаю, чтобы использовать его:

makeWishForAnchors(); // This just loads the global genie object. I don't believe it related.

var lamp = '<div class="genie-extension"><div ux-lamp lamp-visible="genieVisible" rub-class="visible" local-storage="true"></div></div>';
$('body').append(lamp);

angular.module('genie-extension', ['uxGenie']);
angular.bootstrap($('.genie-extension')[0], ['genie-extension']);

Спасибо!

Ответы

Ответ 1

Проблема

Как только вводится Angular, он анализирует DOM, ища элемент с директивой ng-app. Если вы обнаружите, что Angular автоматически загрузится. Это становится проблемой, когда страница использует Angular сама, потому что (хотя они имеют отдельные контексты JS-исполнения), страница и контент script имеют один и тот же DOM.

Решение

Вам нужно предотвратить ваш экземпляр Angular ( "ваш", я имею в виду тот, который был добавлен вашим расширением в качестве содержимого script), из автоматической начальной загрузки. Обычно вы просто опускаете директиву ng-app, и вам было бы хорошо идти, но поскольку вы не имеете контроля над исходным DOM (и не хотите нарушать функциональность страницы), это не вариант.

Что вы можете использовать ручную загрузку для вашего приложения Angular в сочетании с отложенная загрузкa > (чтобы ваша программа Angular пыталась автоматически загружать страницу Angular).

В то же время вам нужно "защитить" (т.е. скрыть) свой корневой элемент приложения на странице Angular. Для этого вы можете обернуть свой корневой элемент в родительский элемент директивой ngNonBindable, поэтому экземпляр Angular будет оставьте это в покое.

Подводя итоги шагам из вышеуказанных документов, вам необходимо сделать следующее:

  • Подготовьте window.name с помощью NG_DEFER_BOOTSTRAP! перед вводом вашего Angular.
    Например. введите script (до angluar.js), содержащий только одну строку:

    window.name = 'NG_DEFER_BOOTSTRAP!' + window.name;
    
  • Оберните свой корневой элемент приложения в родительском элементе с атрибутом ng-non-bindable:

    var wrapper = ...    // <div ng-non-bindable></div>
    wrapper.appendChild(lamp);   // or whatever your root element is
    document.body.appendChild(wrapper);
    
  • В главном приложении script вручную загрузите приложение Angular:

    var appRoot = document.querySelector('#<yourRootElemID');
    angular.bootstrap(appRoot, ['genie-extension']);
    

<суб > Точная печать: Я сам ее не тестировал, но я обещаю сделать это скоро! Суб > удаp >


UPDATE

Приведенный ниже код предназначен для доказательства концепции описанного выше подхода. В принципе, это демонстрационное расширение, которое загружает Angular -powered content- script на любую страницу http:/https: всякий раз, когда нажимается кнопка действия браузера.

Расширение принимает все необходимые меры предосторожности, чтобы не мешать (или не разбиваться) на странице собственный экземпляр Angular (если есть).

Наконец, мне пришлось добавить третье требование (см. выше описание выше), чтобы защитить/скрыть приложение content-script Angular со страницы Angular.
(I.e. Я обернул корневой элемент в родительском элементе директиву ngNonBindable.)

manifest.json:

{
  "manifest_version": 2,
  "name": "Test Extension",
  "version": "0.0",

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

  "browser_action": {
    "default_title": "Test Extension"
//    "default_icon": {
//      "19": "img/icon19.png",
//      "38": "img/icon38.png"
//    },
  },

  "permissions": ["activeTab"]
}

background.js:

// Inject our Angular app, taking care
// not to interfere with page Angular (if any)
function injectAngular(tabId) {
  // Prevent immediate automatic bootstrapping
  chrome.tabs.executeScript(tabId, {
    code: 'window.name = "NG_DEFER_BOOTSTRAP!" + window.name;'
  }, function () {
    // Inject AngularJS
    chrome.tabs.executeScript(tabId, {
      file: 'angular-1.2.7.min.js'
    }, function () {
      // Inject our app script
      chrome.tabs.executeScript(tabId, {file: 'content.js'});
    });
  });
}

// Call `injectAngular()` when the user clicks the browser-action button
chrome.browserAction.onClicked.addListener(function (tab) {
  injectAngular(tab.id);
});

content.js:

// Create a non-bindable wrapper for the root element
// to keep the page Angular instance away
var div = document.createElement('div');
div.dataset.ngNonBindable = '';
div.style.cssText = [
  'background:  rgb(250, 150, 50);',
  'bottom:      0px;',
  'font-weight: bold;',
  'position:    fixed;',
  'text-align:  center;',
  'width:       100%;',
  ''].join('\n');

// Create the app root element (everything else should go in here)
var appRoot = document.createElement('div');
appRoot.dataset.ngController = 'MyCtrl as ctrl';
appRoot.innerHTML = 'Angular says: {{ctrl.message}}';

// Insert elements into the DOM
document.body.appendChild(div);
div.appendChild(appRoot);

// Angular-specific code goes here (i.e. defining and configuring
// modules, directives, services, filters, etc.)
angular.
  module('myApp', []).
  controller('MyCtrl', function MyCtrl() {
    this.message = 'Hello, isolated world !';
  });

/* Manually bootstrap the Angular app */
window.name = '';   // To allow `bootstrap()` to continue normally
angular.bootstrap(appRoot, ['myApp']);
console.log('Boot and loaded !');

<суб > Точная печать:
Я провел несколько предварительных тестов (с веб-страницами Angular и не Angular), и все работает так, как ожидалось. Тем не менее, я никоим образом не проверял этот подход! Суб >


Если кто-то заинтересован, это то, что нужно, чтобы "Angularize" Genie lamp.

Ответ 2

Отвечать @ExpertSystem довольно круто. Но там в большем случае с той же ошибкой. Если вы определяете angular в manifest.json в контенте script, как показано ниже:

"content_scripts": [
    {
      "css": ["app.css"],
      "js": [
        "libs/jquery/dist/jquery.min.js",
        "libs/angular/angular.min.js",
        "content.js"
      ],
      "matches": ["\u003Call_urls\u003E"]
    }
  ],

Вы можете столкнуться с этой ошибкой (например, я)

Ответ 3

@ExpertSystem ответ хорош, но для меня это не сработает, я думаю, что это не может быть решением для каждого сценария, поэтому в конце я решил отредактировать исходный файл angular.js.

Я нашел angular.js будет прослушивать событие ready в конце файла (в 1.4.6):

jqLite(document).ready(function() {
  angularInit(document, bootstrap);
});

Удалите эти строки, и все ясно.

Если вы используете Bower, вы можете добавить простой крючок, чтобы сделать это, когда bower install или bower update.

В .bowerrc файле:

{
  "scripts" : {
    "postinstall" : "node ./strip"
  }
}

В strip.js:

const fs  = require( 'fs' ) ,
filePath  = './bower_components/angular/angular.js' ,
angularJs = fs.readFileSync( filePath , 'utf8' ) ,
deleteStr = /jqLite\(document\)\.ready\(function\(\)[\s\S]+?\}\);/ ,
isDeleted = angularJs.search( deleteStr ) < 0;

if ( !isDeleted ) {
  fs.writeFileSync( filePath , angularJs.replace( deleteStr , '' ) );
  console.log( 'successful disabled auto-bootstrap' );
} else {
  console.log( 'auto-bootstrap already disabled.' );
}