lastuniverse
@lastuniverse
Всегда вокруг да около IT тем

Как сделать подключение модулей в браузере с помощью require(...) так же как в это делается в nodejs?

Сразу оговорюсь, я в курсе про RequireJS, CommonJS, AMD и ES6 модули а также про системы сборки всех зависимостей в одну кучу на стороне сервера. Но это все не совсем то.
- Системы сборки хороши в релиз, но на мой взгляд не очень удобны при работе над кодом (постоянные подвисания watcher-а и другие неудобства)
- RequireJS, CommonJS, AMD и ES6 не позволяют писать для браузера так, как я это делаю для ноды. Одни требуют дополнительных манипуляций в самой ноде, другие в браузере, у третьих не аутентичный нодовскому require, в общем как я и сказал, все не то (ИМХО)

Результатом стало гугление и поиск готового решения, не найдя которого я засел писать свое. И вот долгожданный результат домашнего велостроения:
require.js собственного производства
/**
 * модуль призван имитировать в браузере поведение функции require так, как это происходит в node js.
 *   - он позволяет писать свои модули, экспортировать из них объекты, функции и переменные так, как мы это делаем в nodejs
 *   - подключать данные модули необходимо так же как мы это делаем в nodejs
 * для этих целей каждый подключаемый модуль автоматически:
 *   - оборачивается в само вызывающуюся функцию для создания собственного пространства имен
 *   - устанавливает режим "use strict", 
 *   - получает параметр module переданный само вызывающейся функции и содержащий {exports:{}},
 *   - внутри области видимости само вызывающейся функции создается переменная const global=window
 * данная реализация имеет несколько важных ограничений:
 *   - модуль require.js должен быть подключен в секции head в index.html 
 *   - модуль использующий вызовы require(...) сам должен быть подключен с помощью require(...)
 * на практике это реализуется следующим образом:
 *   в секции body в index.html должен быть единственный вызов require, который подключит 
 *   первый модуль, являющийся главным файлом приложения. Все остальные модули, должны подключаться через
 *   вызовы require из модулей также подключенных через require.
 *   !!! это не касается сторонних библиотек, таких как jquery и прочих !!!
 */
(function(module) {
"use strict";

// запоминаем адресное пространство модуля для глобальной ссылки на функцию require
module.require = require.bind(this);

// создаем хранилище с ссылками на загруженные модули
const required = module.required = {};

/**
 * Функция позволяет подключить произвольный скрипт или json так как это делается в nodejs
 * @param  {string} 	path  путь к загружаемому скрипту (может быть полным URL а также относительным путем)
 * @return {string}           возвращает экспортируемое пространство имен подключаемого модуля
 */
function require( path ) {
	// выход если не указан ни один скрипт для загрузки
	if( !path || typeof path !== "string")
		return;

	// получаем полный URL загружаемого скрипта (работает даже если были передан относительный путь )
	const sourceURL = getScriptUrl( path, window.location.href );

	// если модуль уже был загружен ранее возвращаем ссылку
	if( required[sourceURL] )
		return required[sourceURL].exports;

	// иначе загружаем файл
	load( sourceURL );
}

/**
 * Функция загружает скрипт или json по указанному URL
 * @param  {string} sourceURL полный url загружаемого скрипта
 * @return {promise}          возвращает промис, который будет выполнен после того как скрипт 
 *                            и все его зависимости будут загружены
 */
function load( sourceURL ) {

	// готовим место под загружаемый скрипт в массиве required
	required[sourceURL] = {exports:{}};
	
	// создадим промис и ссылку на его функцию resolve
	let onload;
	let promise = new Promise( function( resolve, reject ) {
		onload = resolve;
	} );

	// загружаем скрипт (и все его зависимости)
	getURL(
		sourceURL,
		// если загрузка успешно завершена
		function (code) {
			// создадим массив в который будем складывать промисы
			const waitLoad = [];

			if( sourceURL.search(/\.json$/)+1 ){

				// если запрошен json то просто парсим текст с json-ом
				required[sourceURL].exports = JSON.parse(code);
				onload();

			}else{

				// если скрипт, то производим в code поиск всех require и загружаем их
				findRequires(code)
				.forEach(str=>{
					const targetURL = getScriptUrl(str, sourceURL);
					const re = new RegExp('(["\'`])('+str+')\\1','g');

					// заменяем пути к загружаемым модулям внутри всех вызовов require на полные URL этих модулей
					code = code.replace(re,(a,b,path)=>{
						return b+targetURL+b;
					});

					// если скрипт не был загружен ранее, грузим его и добавляем в промис
					if(!required[targetURL])
						waitLoad.push(load( targetURL ));
				});

				// когда все зависимости загружены добавляем в браузер текущий скрипт
				Promise.all(waitLoad).then(values => { 
					addScript(sourceURL, code);
					onload();
				});

			}
		},

		// если загрузить файл не удалось
		function (error) {
			console.error("ERROR: не удалось загрузить файл по адресу", sourceURL);
			onload();
		}
	);
	
	return promise;
}


/**
 * Функция производит поиск всех вызовов require("...") и формирует
 * список путей запрашиваемых в коде файлов
 * @param  {string} 	code текст исходников в которых производится поиск
 * @return {array}      возвращает список запрашиваемых в коде скриптов
 */
function findRequires(code) {
	const list = [];
		// удаляем комментарии типа // ...
	const text = code.replace(/\/\*[\s\S]*?\*\//g,"\n")
		// удаляем комментарии типа /* ... */
		.replace(/\/\/.*?$/mg,"")
		// если в строках было чтото похожее на require("...") то удаляем
		.replace(/((["'`])[\s\S]*?\2)/g,str=>{
			let ret = str.replace(/require[\s\S]*?\([\s\S]*?\)/g,"o_O");
			return ret;
		})
		// ищем все require и добавляем их в список
		.replace(/require[\s\S]*?\([\s\S]*?(["'`])(.*?)\1/g,(a,b,source)=>{
			if(source)
				list.push(source);
			return a;
		});
	return list;

}


/**
 * функция преобразует переданный ей путь до скрипта в полный URL.
 * Умеет относительные пути.
 * @param  {string} 	path путь к загружаемому скрипту
 * @param  {string} 	curentURL путь к текущему скрипту
 * @return {string}     полный URL загружаемого скрипта
 */
function getScriptUrl( path, curentURL ) {
	if( !path || typeof path !== "string" ) path = "";
	const parser = document.createElement('a');
	if( path.search(/^\.{1,2}/)+1 || !(path.search(/\//)+1) ){
		parser.href = curentURL
			.split(/\//)
			.slice(0,-1)
			.join("/")
			+"/"+path;

	}else{
		parser.href = path;
	}
	return parser.href;
}


/**
 * функция добавляет код скрипта на страницу, 
 * - оборачивет его в самовызывающуюся функцию для создания собственного пространства имен
 * - устанавливает для скрипта режим "use strict", 
 * - передает модулю параметр module = {exports:{}},
 * - внутри области видимости создает переменную const global=window
 * @param {string} 		url  полные URL добавляемого скрипта
 * @param {string} 		code код добавляемого скрипта
 */
function addScript( url, code ) {
	let head = document.getElementsByTagName( 'head' )[ 0 ];
	let script = document.createElement( 'script' );
	script.charset="utf-8";
	script.type = 'text/javascript';
	//script.src = url;
	code = 
		"(function(module){\n\"use strict\";\nconst global=window;\n"
		+code
		+"\n})(required[\""+url+"\"]);";
	script.innerHTML = code;
	head.appendChild( script );
}


/**
 * AJAX - взятый гдето с просторов Интернета
 */
var getURL = function ( url, success, error ) {
	if ( !window.XMLHttpRequest ) return;
	var request = new XMLHttpRequest();
	request.onreadystatechange = function () {
		if ( request.readyState === 4 ) {
			if ( request.status !== 200 ) {
				if ( error && typeof error === 'function' ) {
					error( request.responseText, request );
				}
				return;
			}
			if ( success && typeof success === 'function' ) {
				success( request.responseText, request );
			}
		}

	};
	request.open( 'GET', url );
	request.send();
};

})(this);



Пример использования:
[/index.html] - подключение приложения (/js/main.js)
<!DOCTYPE html>
<html>
<head>
    <title>graph eginere</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <script src="js/require.js" type="text/javascript" charset="utf-8"></script>
</head>
<body class="my_body">
    <script>
        window.onload = function (){
            const test = require("js/main.js");
            // тут более ничего не делаем, все действия производим в main.js
            // и подключаемых в нем модулях
        }
    </script>
</body>
</html>

[/js/main.js] - главный файл приложения
console.log("module: js/main.js");
const User = require('../libs/user.js');
const user = new User({
	race: "ogre",
	name: "Плешивый Людоед"
});
user.displayInfo();

[/libs/user.js] - подключаемый модуль
const races = require("../config/races.json");
module.exports = User;
function User({race, name, level, money, slots, inventory}){
	this.race = races[race]||"человек";
    this.name = name||"Ростендрикс Потендрикс";
    this.level = level||0;
    this.money = money||0;
}
User.prototype.displayInfo = function() {
    console.log(`Расса: ${this.race} \tИмя: ${this.name} \tУровень: ${this.level}\tМонет: ${this.money}`);
};

[/config/races.json] - загружаемый json
{
	"human": "человек",
	"ogre": "людоед",
	"dwarf": "гном",
	"elf": "эльф",
	"hobbit": "хоббит"
}

Демка (вывод в консоль)

Ну и собственно вопросы:
1. Существуют ли готовое(ые) решение(ия) в полном объеме аутентичные require из Node.js?
2. Существует ли замена/аналоги для document.currentScript в IE?
3. Если ответ на первый вопрос (нет) то рад буду услышать в комментариях предложения и пожелания по совершенствованию данного модуля.
  • Вопрос задан
  • 2153 просмотра
Пригласить эксперта
Ответы на вопрос 1
profesor08
@profesor08 Куратор тега JavaScript
В браузере - никак. Смирись. Либо пиши костыль, с чем ты возможно справился, либо построй нормальное окружение для разработки. webpack devserver или что-то в этом духе.

Одно дело, если проект большой и для него нужна куча правил и библиотек, тогда возможны задержки, но и тут можно всяко разно оптимизировать. Другое дело, если у тебя все на легке, а проблемы те-же.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы