在已经使用Angular的页面上使用Angular扩展



我正在为我的库编写一个Chrome扩展(此处),它使用angular作为UI。它在不使用角度的页面上效果很好,但它会导致有角度的页面出现问题。例如,在Angular文档页面上:

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,似乎都会发生这种情况。我只需要在manifest.json中包含angular作为content_script,就可以重现这个问题,并抛出这个错误。任何关于我如何在不破坏一个棱角分明的网站的情况下完成这项工作的想法都将不胜感激。

就像我说的,我是否真的使用角度并不重要,但我所做的就是使用它:

makeWishForAnchors(); // This just loads the global genie object. I don't believe it's 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']);

谢谢!

问题

一旦注入Angular,它就会解析DOM,查找具有ng-app指令的元素。如果找到,Angular将自动引导。当页面使用Angular本身时,这就成了一个问题,因为(尽管它们有单独的JS执行上下文)页面和内容脚本共享相同的DOM。

解决方案

您需要防止您的Angular实例(我所说的"您的"是指由您的扩展作为内容脚本注入的实例)自动引导。通常情况下,您只需省略ng-app指令就可以了,但由于您无法控制原始DOM(也不想破坏页面的功能),因此这不是一个选项。

可以执行的操作是将手动引导用于您的Angular应用程序,并结合

延迟引导同时,您需要从页面的Angular实例中"保护"(即隐藏)应用程序的根元素。为了实现这一点,您可以使用ngNonBindable指令将根元素包装在父元素中,这样页面的Angular实例将不使用它。

总结以上文档中的步骤,您需要执行以下操作:

  1. 在注入Angular之前,用NG_DEFER_BOOTSTRAP!预处理window.name
    例如,注入一个只包含一行的小脚本(在angluar.js之前):

    window.name = 'NG_DEFER_BOOTSTRAP!' + window.name;
    
  2. 将应用程序的根元素包装在具有属性ng-non-bindable:的父元素中

    var wrapper = ...    // <div ng-non-bindable></div>
    wrapper.appendChild(lamp);   // or whatever your root element is
    document.body.appendChild(wrapper);
    
  3. 在应用程序的主脚本中,手动引导Angular应用程序:

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

精细打印:我自己还没有测试过,但我保证很快就会测试


更新

下面的代码旨在证明上述方法的概念。基本上,它是一个演示扩展,每当单击浏览器操作按钮时,它都会将Angular支持的内容脚本加载到任何http:/https:页面中。

扩展采取了所有必要的预防措施,以免干扰(或被)页面自己的Angular实例(如果有的话)。

最后,我不得不添加第三个要求(请参阅上面更新的解决方案描述),以保护/隐藏页面的Angular实例中内容脚本的Angular应用程序
(即,我使用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's 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's 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's 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's 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网页),一切似乎都如预期一样。然而,我并没有彻底测试过这种方法


如果有人感兴趣,这就是"Angularite"Genie灯的制作过程

@ExpertSystem的回答非常酷。但还有更多的案例出现同样的错误。如果您在内容脚本部分的manifest.json中定义角度,如下所示:

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

可能面临此错误(就像我一样)

@ExpertSystem的答案很好,但它对我不起作用,我认为它可能不是每个场景的解决方案,所以最后我决定编辑angular.js.的源文件

我发现angular.js会在文件末尾监听一个ready事件(在1.4.6中):

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

删除这些行,一切都清楚了。

如果您使用Bower,您可以在bower installbower 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()[sS]+?});/ ,
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.' );
}

最新更新