Webpack:如何使Angular自动检测jQuery并将其用作angular.element而不是jqLite?

44
我正在使用Webpack构建一个Angular 1.4项目。该项目使用了多个jQuery插件,这些插件被包装成Angular指令。这些指令内部使用angular.element,可能意味着angular.element是真正的jQuery,而不是jqLite。
我希望Angular能够自动检测到jQuery并使用它,而不是jqLite。我尝试在我的入口模块app.js中本地引用jQuery:require('jquery'),并通过require(expose?$!expose?jQuery!jquery)将jQuery全局暴露。
无论我做什么,angular.element都指向jqLite。

我的研究得出了几个发现:

  1. 即使作为CommonJS模块导入,Angular也会将自己分配给全局变量window.angular,因此我不需要使用Webpack来expose它:当作为CommonJS模块加载时,Angular是否将其自己分配给全局变量`window.angular`?
  2. ProviderPlugin似乎行不通:它不会将jQuery暴露到全局命名空间;相反,对于每个依赖于全局名称jQuery的模块,它会在其中插入require('jquery')。我不是100%确定,但看起来 Angular 不直接从全局命名空间访问jQuery,而是尝试在bindJQuery函数中访问window.jQuery,因此这种方法没有帮助:如何在Webpack中将jQuery暴露给真正的Window对象
  3. 由于和ProviderPlugin一样的原因,imports-loader似乎不适合:Angular想要的是window.jQuery,而不仅仅是jQuery
  4. 使用expose-loader,jQuery会出现在window对象中。我的问题在于Babel将所有导入置于模块的顶部,在结果代码中。因此,虽然require(expose?jquery!jquery)在源文件中在import angular from "angular"之前,但是在捆绑时require("angular")在文件顶部,在jquery之前,因此在导入Angular时,jquery尚不可用。我想知道如何使用带有ECMA6导入语法的Webpack加载程序。
  5. 有建议使用import语法而不是require语法与jquery一起使用:import "jquery"import $ from "jquery",而不是require(jquery):(Petr Averyanov: 如何在ECMAScript 6导入中使用Webpack加载程序语法(imports/exports/expose)?)。jquery源代码包装在一个特殊的包装器中,它识别jquery如何被需要(使用AMD/require,CommonJS还是全局<script>语句)。基于这个,它为jquery工厂设置一个特殊的参数noGlobal,根据noGlobal的值创建或不创建window.jQuery。截至jquery 2.2.4,import "jquery"noGlobal === true ,并且window.jQuery不会创建。我IRC,一些旧版本的jquery没有将import识别为CommonJS导入,并将import的jquery添加到全局命名空间中,这使得angular可以使用它。

细节:这是我的app.js文件:
'use strict';

require("expose?$!expose?jQuery!jquery");
require("metisMenu/dist/metisMenu");
require("expose?_!lodash");
require("expose?angular!angular");

import angular from "angular";
import "angular-animate";
import "angular-messages";
import "angular-resource";
import "angular-sanitize";
import "angular-ui-router";
import "bootstrap/dist/css/bootstrap.css";
import "font-awesome/css/font-awesome.css";
import "angular-bootstrap";

require("../assets/styles/style.scss");
require("../assets/fonts/pe-icon-7-stroke/css/pe-icon-7-stroke.css");

// Import all html files to put them in $templateCache
// If you need to use lazy loading, you will probably need
// to remove these two lines and explicitly require htmls
const templates = require.context(__dirname, true, /\.html$/);
templates.keys().forEach(templates);

import HomeModule from "home/home.module";
import UniverseDirectives from "../components/directives";

angular.module("Universe", [
    "ngAnimate",
    "ngMessages",
    "ngResource",
    "ngSanitize",
    "ui.router",
    "ui.bootstrap",

    HomeModule.name,
    UniverseDirectives.name,
])
.config(function($urlRouterProvider, $locationProvider, $stateProvider){
    // $urlRouterProvider.otherwise('/');

    // $locationProvider.html5Mode(true);

    $stateProvider
      .state('test', {
        url: "/test",
        template: "This is a test"
      });
});

你如何将Angular和jQuery加载到你的应用程序中?它们是捆绑在一起还是从CDN加载? - Hamlet Hakobyan
@HamletHakobyan 它们都在 webpack 捆绑包中。 - Boris Burkov
你使用的 Angular 版本是什么? - maioman
@maioman Angular 1.4。 - Boris Burkov
如果在加载 Angular 之前加载 jQuery,那么 Angular 将聪明地使用 jQuery 而不是 jqlite。 - Ji_in_coding
5个回答

52

这个答案来自john-reilly
Webpack、Angular和jQuery的神秘案例

bob-sponge的答案不完全正确 - Provide插件实际上是对其处理的模块进行文本替换,因此我们需要提供window.jQuery(这是angular正在寻找的内容),而不仅仅是jQuery

  

在您的webpack.config.js中,您需要将以下条目添加到插件中:

new webpack.ProvidePlugin({
    "window.jQuery": "jquery"
}),

这里使用了 webpack 的ProvidePlugin插件,在进行 webpack 化(© 2016 John Reilly)时,所有对 window.jQuery 的引用都会被替换为一个包含 jQuery 的 webpack 模块的引用。因此当您查看捆绑文件时,检查 window 对象中的 jQuery 的代码变成了这样:

jQuery = isUndefined(jqName) ?
  __webpack_provided_window_dot_jQuery : // use jQuery (if present)
    !jqName ? undefined : // use jqLite
    window[jqName]; // use jQuery specified by `ngJq`
没错,webpack在提供jQuery给Angular的同时仍然不会jQuery变量放到window上。很棒,是吧?

1
我爱你,因为这个问题我花了两天时间试图找出这些疯狂的错误。 - tinyCoder

9

!!更新

显然,在ES6示例中,您仍然需要使用commonJs require来使用angular。

import $ from "jquery"

window.$ = $;
window.jQuery = $;

var angular = require("angular");

我希望提出一个更简便的解决方案。只需将jQuery作为窗口全局变量,这样angular就可以识别它:
var $ = require("jquery")

window.$ = $;
window.jQuery = $;

var angular = require("angular");

或者在你的情况下(已过时):

import $ from "jquery"

window.$ = $;
window.jQuery = $;

import angular from "angular";

我希望这可以帮到您 :)

你的解决方案很好。此外,我发现你可以只需import jquery; import angular,然后angular应该能够检测到jquery(请参阅问题的更新,#5)。你能测试一下这个是否有效吗? - Boris Burkov
嗯,我亲自测试过了。使用jquery 2.2.4 noGlobal=true,以及 import $ from "jquery"import "jquery" - Boris Burkov
你的结果是什么? - Lukas Chen
啊,抱歉,结果是负面的 - 它不起作用。 jquery 的包装器识别它作为 CommonJS 的一种风格导入,将 noGlobal 设置为 true,并且不设置 window.$,因此 angular 无法检测到它。 - Boris Burkov
你能否使用我的解决方案,将 noGlobal=false 设为 false。 - Lukas Chen
如果jquery将其noGlobal设置为false,它将分配window.$ = window.jQuery = $,从本质上复制您的解决方案。我认为,更像webpack的解决方案是使用曝光加载器,如方法#4中建议的那样,因为webpack自己也会执行相同的操作。 - Boris Burkov

3

在您的情况下最好使用ProvidePlugin。只需将这些行添加到您的Webpack配置文件的插件部分,jQuery就会在您的应用程序中可用:

    new webpack.ProvidePlugin({
         "$": "jquery",
         "jQuery": "jquery"
    })

嗨,Bob Sponge!这没什么用: angular.element -> function JQLite()。虽然我不明白为什么要将angular导出到全局命名空间 - 我已经注释掉了这行代码:require("expose?angular!angular"); - Boris Burkov
jQuery 2.2.1,是可用的,可以使用 jQuery 和 $ 名称(即使没有 ProviderPlugin)。 - Boris Burkov
我进行了一些额外的研究,并更新了问题及其结果。 - Boris Burkov
4
终于解决了。问题出在Babel上,将import放在requires之前。这会重新排列我的代码,以至于angular始终在jquery之前被导入。该死的。 :) - Boris Burkov
1
@invincibleDudess 我并没有完全解决它,我只是用CommonJS的require替换了所有ECMA6的imports。因为在Webpack中没有办法像ECMA6的import语法一样写出require(“expose?$!expose?jQuery!jquery”);。不过,还有另一个解决方案。原来,当使用ECMA6 import导入jquerylodashangular时,它们会自动添加到window对象中并成为全局可用,因此对于它们,您不需要expose装载程序。所以你可以随处使用ECMA6的import,除非你的一些次要库需要暴露。就我而言,我目前还是坚持使用require - Boris Burkov
显示剩余5条评论

2
有一篇日文文章"在webpack + AngularJS中使用jQuery而不是jQLite"似乎谈到了同样的问题(我还不会日语)。我用谷歌翻译成英文,感谢cither提供这个好答案。
他提供了四种解决方法:
  1. Assign directly to the window (not really cool)

    window.jQuery = require('jquery');
    var angular = require('angular');
    console.log(angular.element('body'));
    //[body, prevObject: jQuery.fn.init[1], context: document, selector: "body"]
    
  2. Use the expose-loader (ok, but not that cool)

    npm install --saveDev expose-loader
    

    webpack.config.js

    module.exports = {
        entry: "./index",
        output: {
            path: __dirname,
            filename: "bundle.js"
        },
        module: {
            loaders: [{
                test: /\/jquery.js$/,
                loader: "expose?jQuery"
            }]
        }
    };
    

    usage:

    require('jquery');
    var angular = require('angular');
    console.log(angular.element('body'));
    //[body, prevObject: jQuery.fn.init[1], context: document, selector: "body"]
    
  3. Use expose-loader (better)

    npm install --saveDev expose-loader
    

    webpack.config.js

        module.exports = {
        entry: "./index",
        output: {
            path: __dirname,
            filename: "bundle.js"
        },
        module: {
            loaders: [{
                test: /\/angular\.js$/,
                loader: "imports?jQuery=jquery"
            }, {
                test: /\/jquery.js$/,
                loader: "expose?jQuery"
            }]
        }
    };
    

    usage:

    var angular = require('angular');
    console.log(angular.element('body'));
    //[body, prevObject: jQuery.fn.init[1], context: document, selector: "body"]
    
  4. Use ProvidePlugin (Best solution)

    This is actually the same as studds's accepted answer here

    module.exports = {
        entry: "./index",
        output: {
            path: __dirname,
            filename: "bundle.js"
        },
        plugins: [
            new webpack.ProvidePlugin({
                "window.jQuery": "jquery"
            })
        ],
    };
    

    usage:

    var angular = require('angular');
    console.log(angular.element('body'));
    //[body, prevObject: jQuery.fn.init[1], context: document, selector: "body"]
    

我想在这里分享一下,因为我们遇到了完全相同的问题。我们在项目中使用expose-loader解决方案,取得了成功。我认为直接将jquery注入windowProvidePlugin也是一个不错的主意。


2
因此,我提供了我的解决方案,实际上是@BobSponge答案和@Bob的提示/评论的混合。没有什么原创性,只是展示了在我这个项目中有效的方法(顺便说一下,该项目不使用Babel / ES6),并试图解释为什么它有效...
(最终)诀窍确实是使用expose loader
正如他们网页中所解释的那样,我们必须在module.loaders中添加:
{ test: require.resolve("jquery"), loader: "expose?$!expose?jQuery" },

此外,在插件列表中,我有:
        new webpack.ProvidePlugin(
        {
            $: 'jquery',
            jQuery: 'jquery',
            _: 'lodash',
            // [...] some other libraries that put themselves in the global scope.
            //angular: 'angular', // No, I prefer to require it everywhere explicitly.
        }),

这个插件实际上会在代码中查找这些变量的全局出现,并将它们require到每个模块的本地变量中。这样可以简化现有项目(从RequireJS迁移到Webpack)的迁移,但如果您更喜欢显式导入,我认为我们可以不使用这个插件。

而且,重要的是(我认为),在应用程序的入口点,我按照想要的顺序require它们。在我的情况下,我制作了一个供应商捆绑包,因此这个顺序就是在这个捆绑包中的顺序。

require('jquery');

require('lodash');
// [...]

var angular = require('angular');
// Use the angular variable to declare the app module, etc.

Webpack会按照它看到的require的顺序将库添加到相关捆绑包中(除非您使用重新排序它们的插件/加载器)。 但是导入是隔离的(没有全局泄漏),因此Angular无法看到jQuery库。这就需要使用expose。(我尝试过window.jQuery = require('jquery');,但它不起作用,也许太晚了。)


你说的“在供应商捆绑包中,因为我制作了一个”,是什么意思?我的意思是......你要把定义顺序的代码放在哪里? - refaelos
2
我不得不重新阅读几次才能理解我的意思... :-) 我希望这些库按供应商捆绑包的顺序排序。正确顺序的require放在应用程序捆绑包中,即当然是在应用程序代码中。 - PhiLho

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接