如何绕过RequireJS以使用全局方式加载模块?

8

我想从书签加载一个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);
}

这可以工作,但我感觉当其他应用程序部分可能依赖它时更改全局变量可能会有问题。有更好的方法吗?

使用XHR(AJAX)加载脚本对你来说是一个选项吗(需要启用CORS)?如果是这样,你应该能够将define本地重新分配给该函数。 - Dheeraj Vepakomma
@DheerajV.S. 那是一个选项。我之前没有想到过。我可以通过 AJAX 加载 JS 并用 (function(define){ ... })(); 包装它。如果我找不到更简单的方法,你可以发布答案,我会接受它。 - Web_Designer
@DheerajV.S。接下来,我会将它放入一个带有text属性的新的<script>元素中。 - Web_Designer
4个回答

5
您可以使用 XMLHttpRequestjQuery.ajax 或新的 Fetch API 来获取脚本。
这样可以让您在执行脚本之前操作并重新分配 define。两个选项:
  1. Have the module export a global by wrapping the script with:

    (function(define){ ... })(null);
    
  2. Handle the module exports yourself by wrapping the script with:

    (function(define, module){ ... })((function() {
        function define(factory) {
            var exports = factory();
        }
        define.amd = true;
        return define;
    })());
    
你可以使用新的<script>标签或eval加载它。
注意,当使用XHR时,你可能需要解决跨域资源共享(CORS)问题。

2
如果您能使用上述AJAX方法,那将是最好的。但正如所述,您需要处理CORS问题,这并不总是易于解决——如果您不能控制原始服务器甚至可能不可能解决。

这是一种使用iframe在隔离的上下文中加载脚本的技术,允许脚本导出其全局对象。然后我们获取全局对象并将其复制到父级。这种技术不会受到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个不同的iframes和100个不同的jQuery实例加载。

enter image description here

父窗口的jQuery变量会被不断覆盖,这意味着只有最后一个变量生效,之前的所有引用都被清除了。这并不完全科学,您需要进行自己的测试,但这应该足够安全让您开始使用。
更新:上述代码要求您知道导出对象的名称,这并不总是已知的。有些模块也可能会导出多个变量。例如,jQuery同时导出$和jQuery。以下fiddle演示了一种通过复制在脚本加载之前不存在的任何全局对象来解决此问题的技术:

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


1

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

暂时取消定义define

我提到这个是因为你尝试过。

不要这样做!

在加载脚本之前取消定义define并在之后恢复它的方法不安全。一般情况下,页面上的其他代码可能会执行require调用,在你取消定义define之后解析,然后在你重新定义它之前解析。在执行document.body.appendChild(s);之后,你将控制权交回JavaScript引擎,它可以立即执行先前所需的脚本。如果这些脚本是AMD模块,则它们将失败或安装不正确。

包装脚本

正如Dheeraj V.S.建议的那样,你可以包装脚本来使define在本地未定义:

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

这种方法可以处理像你在问题中展示的那样的琐碎情况。然而,当你尝试加载的脚本实际上依赖于其他库时可能会导致处理依赖关系时出现问题。以下是一些例子:

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

  2. 脚本需要一个没有被其他任何东西加载的库。因此,现在您需要产生将加载该库的机制。

使用RequireJS上下文

通过定义新的context,RequireJS能够将多个配置隔离开来。您的书签应该定义一个新的上下文,该上下文为您尝试加载的脚本和其依赖项配置足够的路径以进行加载:

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的返回值,因为它是一个require调用,使用您的上下文。

使用Webpack创建捆绑包

(或者您可以使用Browserify或其他捆绑器。我更熟悉Webpack。)
您可以使用Webpack来消耗所有必要的AMD模块,以便加载您正在尝试加载的脚本并生成将其“模块”导出为全局变量的捆绑包。最少,您需要在配置中添加类似于以下内容:
// 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元素,就不必再担心任何包装或依赖关系的问题了。

-2
如果是我,我会让URL提供提示如何加载模块。而不是只有一个“scripts/”目录->我会制作“scripts/amd/”,“scripts/require/”等。然后在loadScript方法中查询“amd”,“require”等的URL...例如使用:
if (url.includes('amd')) {
    // do something
} else if (url.includes('require')) {
    // do something different
}

这样做可以完全避免全局变量。它还可能为您的应用程序提供更好的结构。

您还可以返回一个带有 script 属性和 loadType 属性的对象,指定 amd、require 等等... 但在我看来,第一种选项是最快的,可以节省一些额外的输入。

干杯!


1
因为我是通过书签加载脚本,所以我正在尝试将一个已知的脚本加载到未知的环境中(我从中请求脚本的网页可能会有或没有RequireJS或其他模块加载器,当它加载时拦截脚本)。这个答案似乎假设我正在尝试将一个未知的脚本加载到我的网页应用程序中。 - Web_Designer

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