R
R
Roman2018-11-01 04:35:21
JavaScript
Roman, 2018-11-01 04:35:21

How to connect modules in the browser using require(...) in the same way as it is done in nodejs?

I’ll make a reservation right away, I’m aware of RequireJS, CommonJS, AMD and ES6 modules, as well as systems for building all dependencies into one heap on the server side. But this is not quite the same.
- Build systems are good in release, but in my opinion they are not very convenient when working on code (constant watcher freezes and other inconveniences)
- RequireJS, CommonJS, AMD and ES6 do not allow writing for the browser the way I do it for the node. Some require additional manipulations in the node itself, others in the browser, others have a non-authentic node require, in general, as I said, everything is not right (IMHO)
The result was googling and searching for a ready-made solution, without finding which I sat down to write my own. And here is the long-awaited result of home bike building:

require.js of our own production
/**
 * модуль призван имитировать в браузере поведение функции 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);


Usage example:
[/index.html] - application connection (/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] - main application file
console.log("module: js/main.js");
const User = require('../libs/user.js');
const user = new User({
  race: "ogre",
  name: "Плешивый Людоед"
});
user.displayInfo();

[/libs/user.js] - plugin
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] - loaded json
{
  "human": "человек",
  "ogre": "людоед",
  "dwarf": "гном",
  "elf": "эльф",
  "hobbit": "хоббит"
}

Demo (console output)
Well, actually the questions:
1. Are there ready-made (s) solution (s) in full authentic require from Node.js?
2. Is there a replacement/analogue for document.currentScript in IE?
3. If the answer to the first question is (no), then I will be glad to hear suggestions and suggestions for improving this module in the comments.

Answer the question

In order to leave comments, you need to log in

1 answer(s)
P
profesor08, 2018-11-01
@profesor08

In the browser - no way. Humble yourself. Either write a crutch, which you can probably handle, or build a normal environment for development. webpack devserver or something like that.
It's one thing if the project is large and it needs a lot of rules and libraries, then delays are possible, but even here you can optimize in different ways. Another thing is if everything is easy for you, but the problems are the same.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question