Как обойти RequireJS для загрузки модуля с глобальным?

Я пытаюсь загрузить файл 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);
}

Это работает, но мне кажется, что может быть проблематично изменить глобальную переменную, когда другие части приложения могут зависеть от нее. Есть ли лучший способ сделать это?

4 ответа

Решение

Вы можете получить скрипт, используя XMLHttpRequest, jQuery.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.

Если вы можете использовать метод AJAX выше, это будет лучше. Но, как уже говорилось, вам нужно будет решить проблемы с CORS, что не всегда тривиально - даже невозможно, если вы не контролируете исходный сервер.

Вот методика, которая использует iframe для загрузки сценария в изолированном контексте, позволяя сценарию экспортировать свой глобальный объект. Затем мы берем глобальный объект и копируем его в родительский объект. Этот метод не страдает от ограничений CORS.

(скрипка: 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 разных фреймов и 100 разных случаев загрузки jQuery.

Переменная jQuery родительского окна постоянно перезаписывается, то есть преобладает только последняя, ​​а все предыдущие ссылки очищаются. Это не совсем научно, и вам нужно будет провести собственное тестирование, но это должно быть достаточно безопасно, чтобы начать работу.

Обновление: приведенный выше код требует, чтобы вы знали имя экспортируемого объекта, что не всегда известно. Некоторые модули также могут экспортировать несколько переменных. Например, jQuery экспортирует оба $ а также jQuery, Следующая скрипта иллюстрирует метод решения этой проблемы путем копирования любых глобальных объектов, которые не существовали до загрузки скрипта:

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

Какой подход будет работать лучше всего, зависит от конкретных потребностей проекта. Контекст определит, какой из них я бы использовал.

не определенное define Временно

Я упоминаю об этом, потому что вы попробовали это.

НЕ ДЕЛАЙТЕ ЭТОГО!

Подход неопределенности define перед загрузкой скрипта и его восстановлением после не является безопасным. В общем случае другой код на странице может выполнять require вызов, который разрешится после того, как вы не определились define и прежде чем вы определили это снова. После того как вы document.body.appendChild(s); Вы возвращаете управление движку JavaScript, который может сразу же выполнить сценарии, которые требовались ранее. Если скрипты являются модулем AMD, они либо будут бомбить, либо устанавливать себя неправильно.

Обертывание сценария

Как предлагает Dheeraj VS, вы можете обернуть скрипт, чтобы сделать define локально не определено:

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

может работать для тривиальных случаев, таких как тот, который вы показываете в своем вопросе. Однако случаи, когда скрипт, который вы пытаетесь загрузить, действительно имеют зависимости от других библиотек, могут вызывать проблемы, когда дело доходит до работы с зависимостями. Некоторые примеры:

  1. Страница загружает jQuery 2.x, но сценарий, который вы пытаетесь загрузить, зависит от функции, добавленной в jQuery 3.x. Либо страница загружает Lodash 2, но для сценария требуется Lodash 4, либо наоборот. (Есть огромные различия между Lodash 2 и 4.)

  2. Сценарию нужна библиотека, которая иначе не загружается чем-то другим. Так что теперь вы несете ответственность за производство оборудования, которое будет загружать библиотеку.

Использование контекстов RequireJS

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 элемент, который указывает на созданный пакет и больше не должен беспокоиться о переносе чего-либо или работе с зависимостями.

Если бы это был я, я бы попросил URL дать подсказку о том, как загрузить модуль. Вместо того, чтобы просто иметь каталог "scripts/" -> я бы сделал "scripts/amd/", "scripts/require/" и т. Д. Затем запросите URL для "amd", "require" и т. Д. В вашем методе loadScript... используя, например,

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

Это должно позволить вам полностью избежать глобальной переменной. Это может также обеспечить лучшую структуру для вашего приложения в целом.

Вы также можете вернуть объект с script собственность и loadType свойство, которое определяет amd, require и т.д... но imho первый вариант будет самым быстрым и сэкономит вам некоторую дополнительную печать.

ура

Другие вопросы по тегам