Загрузите «ванильные» библиотеки Javascript в Node.js

108

Есть некоторые сторонние библиотеки Javascript, которые обладают некоторыми функциями, которые я хотел бы использовать на сервере Node.js. (В частности, я хочу использовать библиотеку javascript QuadTree, которую я нашел.) Но эти библиотеки представляют собой простые .jsфайлы, а не «библиотеки Node.js».

Таким образом, эти библиотеки не следуют exports.var_nameсинтаксису, который Node.js ожидает от своих модулей. Насколько я понимаю, это означает, что когда вы это сделаете, module = require('module_name');или module = require('./path/to/file.js');вы получите модуль без общедоступных функций и т. Д.

Тогда мой вопрос: «Как мне загрузить произвольный файл javascript в Node.js, чтобы я мог использовать его функциональные возможности без необходимости переписывать его, чтобы он работал exports

Я новичок в Node.js, поэтому, пожалуйста, дайте мне знать, если в моем понимании того, как это работает, есть явная дыра.


РЕДАКТИРОВАТЬ : изучая вещи больше, и теперь я вижу, что шаблон загрузки модуля, который использует Node.js, на самом деле является частью недавно разработанного стандарта для загрузки библиотек Javascript под названием CommonJS . Об этом говорится прямо на странице документации модуля для Node.js , но до сих пор я это пропустил.

Может оказаться, что ответ на мой вопрос: «подождите, пока авторы вашей библиотеки не займутся написанием интерфейса CommonJS, или сделайте это сами».

Крис В.
источник
связанный вопрос: stackoverflow.com/questions/22898080/…
Josmar 06

Ответы:

75

Есть гораздо лучший метод, чем использование eval: vmмодуль.

Например, вот мой execfileмодуль, который оценивает сценарий pathв любом contextили глобальном контексте:

var vm = require("vm");
var fs = require("fs");
module.exports = function(path, context) {
  context = context || {};
  var data = fs.readFileSync(path);
  vm.runInNewContext(data, context, path);
  return context;
}

И его можно использовать так:

> var execfile = require("execfile");
> // `someGlobal` will be a global variable while the script runs
> var context = execfile("example.js", { someGlobal: 42 });
> // And `getSomeGlobal` defined in the script is available on `context`:
> context.getSomeGlobal()
42
> context.someGlobal = 16
> context.getSomeGlobal()
16

Где example.jsсодержится:

function getSomeGlobal() {
    return someGlobal;
}

Большим преимуществом этого метода является то, что у вас есть полный контроль над глобальными переменными в исполняемом скрипте: вы можете передавать пользовательские глобальные переменные (через context), и все глобальные переменные, созданные скриптом, будут добавлены в context. Отладка также упрощается, потому что синтаксические ошибки и т.п. будут сообщаться с правильным именем файла.

Дэвид Волевер
источник
runInNewContextИспользует ли глобальный контекст, если context(иначе называемый sandboxв документации) не определен? (этот момент не был прояснен какими-либо документами, которые я нашел)
Стивен Лу
Кажется, что для игры со сторонней библиотекой, не знающей Node или шаблона CommonJS, хорошо работает метод eval Кристофера < stackoverflow.com/a/9823294/1450294 >. Какие преимущества может vmпредложить модуль в этом случае?
Майкл Шепер,
2
Смотрите мои обновления, чтобы узнать, почему этот метод лучше, чем eval.
Дэвид Уолевер
1
это совершенно потрясающе - это позволило мне мгновенно повторно использовать мой веб-немодульный код для реализации на стороне сервера, которая отправляет результаты по электронной почте [по расписанию] вместо того, чтобы отображать их на веб-странице. Весь веб-код использовал шаблон модуля с произвольным расширением и внедрение сценария - так что это прекрасно работает !!
Эл Джослин,
Как мы можем использовать это в Node.js, если example.js зависит от библиотеки example1.js?
sytolk
80

Вот что я считаю наиболее «правильным» ответом на эту ситуацию.

Скажем, у вас есть файл сценария с именем quadtree.js.

Вы должны создать кастом node_moduleс такой структурой каталогов ...

./node_modules/quadtree/quadtree-lib/
./node_modules/quadtree/quadtree-lib/quadtree.js
./node_modules/quadtree/quadtree-lib/README
./node_modules/quadtree/quadtree-lib/some-other-crap.js
./node_modules/quadtree/index.js

Все в вашем ./node_modules/quadtree/quadtree-lib/каталоге - это файлы из вашей сторонней библиотеки.

Затем ваш ./node_modules/quadtree/index.jsфайл просто загрузит эту библиотеку из файловой системы и выполнит работу по правильному экспорту.

var fs = require('fs');

// Read and eval library
filedata = fs.readFileSync('./node_modules/quadtree/quadtree-lib/quadtree.js','utf8');
eval(filedata);

/* The quadtree.js file defines a class 'QuadTree' which is all we want to export */

exports.QuadTree = QuadTree

Теперь вы можете использовать свой quadtreeмодуль как любой другой модуль узла ...

var qt = require('quadtree');
qt.QuadTree();

Мне нравится этот метод, потому что нет необходимости менять какой-либо исходный код вашей сторонней библиотеки, поэтому его проще поддерживать. Все, что вам нужно сделать при обновлении, - это посмотреть их исходный код и убедиться, что вы по-прежнему экспортируете нужные объекты.

Крис В.
источник
3
Только что нашел свой ответ (создание многопользовательской игры и необходимость включения JigLibJS, нашего физического движка как на сервере, так и на клиенте), вы сэкономили мне МНОГО времени и хлопот. Спасибо!
stevendesu
8
Если вы точно следуете этому, имейте в виду, что довольно легко случайно стереть вашу папку node_modules с помощью NPM, особенно если вы не вернете ее в SCM. Обязательно подумайте о том, чтобы поместить вашу библиотеку QuadTree в отдельный репозиторий, а затем npm linkвставить ее в свое приложение. Затем он обрабатывается, как если бы это был собственный пакет Node.js.
btown
@btown, не могли бы вы немного рассказать для новичков вроде меня, что именно SCM и ссылка npm делают для предотвращения упомянутой вами потенциальной проблемы?
Flion
Действительно ли это необходимо, если я просто хочу добавить сценарий?
Quantumpotato 05
1
@flion отвечает на старый комментарий для других ссылок, поскольку я уверен, что вы уже знаете, что ответили. SCM - Управление исходным кодом (например, GIT) и ссылка на быструю, но хорошую демонстрацию ссылки npm
delp
30

Самый простой способ: он eval(require('fs').readFileSync('./path/to/file.js', 'utf8')); отлично подходит для тестирования в интерактивной оболочке.

Кристофер Вайс
источник
1
Ура, приятель! Очень
помогло
Это также самый быстрый способ, а иногда и быстрый и грязный - то, что вам нужно. Между этим и ответом Дэвида эта страница SO - отличный ресурс.
Майкл Шепер
5

AFAIK, именно так и должны загружаться модули. Однако вместо того, чтобы прикреплять все экспортируемые функции к exportsобъекту, вы также можете прикрепить их this(что в противном случае было бы глобальным объектом).

Итак, если вы хотите сохранить совместимость других библиотек, вы можете сделать это:

this.quadTree = function () {
  // the function's code
};

или, когда внешняя библиотека уже имеет собственное пространство имен, например jQuery(не то чтобы вы могли использовать это в серверной среде):

this.jQuery = jQuery;

В среде, отличной от Node, он thisбудет преобразован в глобальный объект, что сделает его глобальной переменной ... что уже было. Так что он ничего не должен сломать.

Изменить : у Джеймса Хердмана есть хорошая статья о node.js для начинающих, в которой также упоминается об этом.

Мартейн
источник
Уловка 'this' звучит как хороший способ сделать вещи более переносимыми, чтобы библиотеки Node.js можно было использовать за пределами Node.js, но это по-прежнему означает, что мне нужно вручную изменить свои библиотеки javascript для поддержки синтаксиса Node.js. .
Крис В.
@ChrisW .: да, вам придется вручную изменить свои библиотеки. Лично мне также хотелось бы иметь второй механизм для включения внешних файлов, который автоматически преобразовывал глобальное пространство имен включенного файла в импортированное пространство имен. Может быть, вы могли бы подать RFE разработчикам Node?
Martijn
3

Я не уверен, что действительно буду использовать это, потому что это довольно хакерское решение, но один из способов обойти это - создать небольшой импортер мини-модулей, подобный этому ...

В файле ./node_modules/vanilla.js:

var fs = require('fs');

exports.require = function(path,names_to_export) {
    filedata = fs.readFileSync(path,'utf8');
    eval(filedata);
    exported_obj = {};
    for (i in names_to_export) {
        to_eval = 'exported_obj[names_to_export[i]] = ' 
            + names_to_export[i] + ';'
        eval(to_eval); 
    }
    return exported_obj;
}

Затем, когда вы захотите использовать функциональные возможности своей библиотеки, вам нужно будет вручную выбрать, какие имена экспортировать.

Итак, для библиотеки, такой как файл ./lib/mylibrary.js...

function Foo() { //Do something... }
biz = "Blah blah";
var bar = {'baz':'filler'};

Если вы хотите использовать его функциональность в своем коде Node.js ...

var vanilla = require('vanilla');
var mylibrary = vanilla.require('./lib/mylibrary.js',['biz','Foo'])
mylibrary.Foo // <-- this is Foo()
mylibrary.biz // <-- this is "Blah blah"
mylibrary.bar // <-- this is undefined (because we didn't export it)

Не знаю, насколько хорошо все это будет работать на практике.

Крис В.
источник
Эй, вау! Проголосованный (не мной) и одобренный ответ одного и того же пользователя на один и тот же вопрос! Для этого должен быть значок! ;-)
Майкл Шепер
2

Я смог заставить его работать, очень легко обновив их сценарий, просто добавив module.exports =там, где это необходимо ...

Например, я взял их файл и скопировал в ./libs/apprise.js. Тогда где это начинается с

function apprise(string, args, callback){

Я назначил функцию module.exports =таким образом:

module.exports = function(string, args, callback){

Таким образом, я могу импортировать библиотеку в свой код следующим образом:

window.apprise = require('./libs/apprise.js');

И мне было хорошо идти. YMMV, это было с веб-пакетом .

Джон Ми
источник
0

Простая include(filename)функция с улучшенным обменом сообщениями об ошибках (стек, имя файла и т. Д.) evalВ случае ошибок:

var fs = require('fs');
// circumvent nodejs/v8 "bug":
// https://github.com/PythonJS/PythonJS/issues/111
// http://perfectionkills.com/global-eval-what-are-the-options/
// e.g. a "function test() {}" will be undefined, but "test = function() {}" will exist
var globalEval = (function() {
    var isIndirectEvalGlobal = (function(original, Object) {
        try {
            // Does `Object` resolve to a local variable, or to a global, built-in `Object`,
            // reference to which we passed as a first argument?
            return (1, eval)('Object') === original;
        } catch (err) {
            // if indirect eval errors out (as allowed per ES3), then just bail out with `false`
            return false;
        }
    })(Object, 123);
    if (isIndirectEvalGlobal) {
        // if indirect eval executes code globally, use it
        return function(expression) {
            return (1, eval)(expression);
        };
    } else if (typeof window.execScript !== 'undefined') {
        // if `window.execScript exists`, use it
        return function(expression) {
            return window.execScript(expression);
        };
    }
    // otherwise, globalEval is `undefined` since nothing is returned
})();

function include(filename) {
    file_contents = fs.readFileSync(filename, "utf8");
    try {
        //console.log(file_contents);
        globalEval(file_contents);
    } catch (e) {
        e.fileName = filename;
        keys = ["columnNumber", "fileName", "lineNumber", "message", "name", "stack"]
        for (key in keys) {
            k = keys[key];
            console.log(k, " = ", e[k])
        }
        fo = e;
        //throw new Error("include failed");
    }
}

Но с nodejs становится еще грязнее: вам нужно указать это:

export NODE_MODULE_CONTEXTS=1
nodejs tmp.js

В противном случае вы не сможете использовать глобальные переменные в файлах, включенных в include(...).

lama12345
источник