如何绕过 RequireJS 加载全局模块?

How to circumvent RequireJS to load module with global?

我正在尝试从小书签加载 JS 文件。 JS文件有这个包装模块的JS:

(function (root, factory) {
    if (typeof module === 'object' && module.exports) {
        // Node/CommonJS
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(factory);
    } else {
        // Browser globals
        root.moduleGlobal = factory();
    }
}(this, function factory() {
    // module script is in here

    return moduleGlobal;
}));

因此,如果网页使用 RequireJS,则脚本在加载时不会导出全局。为了解决这个问题,我暂时将 define 设置为 null,加载脚本,然后将 define 重置为其原始值:

function loadScript(url, cb) {
    var s = document.createElement('script');
    s.src = url;
    s.defer = true;
    var avoidRequireJS = typeof define === 'function' && define.amd;
    if (avoidRequireJS) {
        var defineTmp = define;
        define = null;
    }
    s.onload = function() {
        if (avoidRequireJS) define = defineTmp;
        cb();
    };
    document.body.appendChild(s);
}

这行得通,但在我看来,当应用程序的其他部分可能依赖于它时更改全局变量可能会有问题。有没有更好的方法来解决这个问题?

如果是我,我会让 url 提供有关如何加载模块的提示。而不是只有一个 "scripts/" 目录 -> 我会创建 "scripts/amd/"、"scripts/require/" 等。然后在 url 中查询 "amd"、"require"等。在您的 loadScript 方法中...使用,例如,

if (url.includes('amd')) {
    // do something
} else if (url.includes('require')) {
    // do something different
}

这应该可以让您完全避免使用全局变量。它也可能为您的应用提供更好的整体结构。

您还可以 return 一个带有 script 属性 和 loadType 属性 的对象,它指定了 amd、require 等...但是恕我直言第一个选项是最快的并且可以节省您一些额外的输入。

干杯

您可以使用 XMLHttpRequestjQuery.ajax 或新的 Fetch API.

获取脚本

这将允许您在执行脚本之前操纵脚本并重新分配 define。两个选项:

  1. 让模块通过包装脚本导出一个全局变量:

    (function(define){ ... })(null);
    
  2. 通过使用以下代码包装脚本自行处理模块导出:

    (function(define, module){ ... })((function() {
        function define(factory) {
            var exports = factory();
        }
        define.amd = true;
        return define;
    })());
    

然后您可以使用新的 <script> 元素或 eval 加载它。

请注意,使用 XHR 时,您可能需要解决 CORS 个问题。

哪种方法最有效取决于项目的具体需求。上下文将决定我使用哪一个。

暂时取消定义define

我提到它是因为你试过了。

不要这样做!

在加载脚本之前取消定义 define 并在 之后恢复它的方法是不安全的。在一般情况下,页面上的其他代码可能会执行一个 require 调用,该调用将解析 你未定义 define 之后和你之前'我们重新定义了它。在您执行 document.body.appendChild(s); 之后,您将控制权交还给 JavaScript 引擎,它可以自由地立即执行之前需要的脚本。如果脚本是 AMD 模块,它们要么会崩溃,要么会错误地安装自己。

封装脚本

作为,您可以包装脚本以使define局部未定义:

(function(define) { /* original module code */ }())

可以处理像您在问题中展示的那种琐碎的案例。但是,如果您尝试加载的脚本实际上依赖于其他库 ,则在处理依赖性时可能会导致问题 。一些例子:

  1. 页面加载 jQuery 2.x 但您尝试加载的脚本取决于 jQuery 3.x 中添加的功能。或者页面加载 Lodash 2 但脚本需要 Lodash 4,反之亦然。 (Lodash 2 和 4 之间存在巨大差异。)

  2. 该脚本需要一个不会被其他东西加载的库。所以现在你负责生产加载库的机器。

使用 RequireJS 上下文

RequireJS 能够通过定义一个新的 context 来将多个配置相互隔离。您的小书签可以定义一个新上下文,为您尝试加载的脚本配置足够的路径以加载自身及其依赖项:

var myRequire = require.config({
  // Within the scope of the page, the context name must be unique to 
  // your bookmarklet.
  context: "Web Designer's Awesome Bookmarklet",
  paths: {
    myScript: "https://...",
    jquery: "https://code.jquery.com/jquery-3.2.1.min.js",
  },
  map: {...},
  // Whatever else you may want.      
});

myRequire(["myScript"]);

当您使用这样的上下文时,您希望保存 require.config 的 return 值,因为它是一个使用您的上下文的 require 调用。

使用 Webpack 创建一个包

(或者您可以使用 Browserify 或其他一些捆绑器。我对 Webpack 更熟悉。)

您可以使用 Webpack 消耗您尝试加载的脚本所需的所有 AMD 模块,以生成将其 "module" 作为全局导出的包。至少,您的配置中需要这样的东西:

// Tell Webpack what module constitutes the entry into the bundle.
entry: "./MyScript.js",
output: {
  // This is the name under which it will be available.
  library: "MyLibrary", 

  // Tell Webpack to make it globally available.
  libraryTarget: "global",

  // The final bundle will be ./some_directory/MyLibrary.js
  path: "./some_directory/",
  filename: "MyLibrary.js",
}

一旦完成,小书签只需要插入一个新的 script 指向生成的包的元素,不再需要担心包装任何东西或处理依赖关系。

如果能用上面的AJAX方法就最好了。但如前所述,您将需要处理 CORS 问题,这并不总是微不足道的——如果您不控制源服务器,甚至是不可能的。

这是一种使用 iframe 在隔离上下文中加载脚本的技术,允许脚本导出其 global 对象。然后我们获取全局对象并将其复制到父对象。此技术不受 CORS 限制。

(fiddle: https://jsfiddle.net/qu0pxesd/)

function loadScript (url, exportName) {
  var iframe = document.createElement('iframe');
  Object.assign(iframe.style, {
    position: 'fixed',
    top: '-9999em',
    width: '0px'
  });
  var script = document.createElement('script');
  script.onload = function () {
    window[exportName] = iframe.contentWindow[exportName];
    document.body.removeChild(iframe);
  }
  script.src = url;
  document.body.appendChild(iframe);
  iframe.contentWindow.document.open();
  iframe.contentWindow.document.appendChild(script);
  iframe.contentWindow.document.close();
}
loadScript('https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js', 'jQuery');

我 运行 进行快速测试,看看删除 iframe 是否会发生内存泄漏,它似乎是内存安全的。这是加载脚本 100 次的快照,导致 100 个不同的 iframe 和 100 个不同的 jQuery 加载实例。

父window的jQuery变量不断被覆盖,这意味着只有最后一个优先,所有以前的引用都被清除。这并不完全科学,您需要自己进行测试,但这应该足够安全,可以让您入门。

更新: 上面的代码要求你知道导出对象的名称,并不总是已知。一些模块也可能导出多个变量。例如,jQuery 导出 $jQuery。以下 fiddle 说明了通过复制加载脚本之前不存在的任何全局对象来解决此问题的技术:

https://jsfiddle.net/qu0pxesd/3/