Heim  >  Artikel  >  Web-Frontend  >  Überblick über die Implementierung eines einfachen browserseitigen JS-Modulladers

Überblick über die Implementierung eines einfachen browserseitigen JS-Modulladers

高洛峰
高洛峰Original
2016-12-28 14:26:391526Durchsuche

Vor es6 verfügte js nicht über ausgereifte modulare Funktionen wie andere Sprachen. Die Seite konnte nur eigene Skripte oder Skripte von Drittanbietern einführen, indem sie nacheinander Skript-Tags einfügte, und es konnte leicht zu Namenskonflikten kommen. Die js-Community hat damals große Anstrengungen unternommen, um den „Modul“-Effekt in der laufenden Umgebung zu erzielen.

Zu den allgemeinen js-Modularstandards gehören CommonJS und AMD. Ersteres wird in der Knotenumgebung verwendet, und letzteres wird in der Browserumgebung durch Require.js usw. implementiert. Darüber hinaus gibt es das inländische Open-Source-Projekt Sea.js, das der CMD-Spezifikation folgt. (Mit der Beliebtheit von es6 wurde die Wartung derzeit eingestellt. Ob AMD oder CMD, es wird ein Stück Geschichte sein)

Browserseitiger JS-Loader

Implementieren Sie ein einfaches JS Loader und Es ist nicht kompliziert und kann in vier Schritte unterteilt werden: Parsen des Pfads, Herunterladen des Moduls, Parsen der Modulabhängigkeiten und Parsen des Moduls.

Definieren Sie zunächst das Modul. In verschiedenen Spezifikationen stellt eine JS-Datei normalerweise ein Modul dar. Dann können wir einen Abschluss in der Moduldatei erstellen und ein Objekt als Export des Moduls übergeben:

define(factory() {
 var x = {
  a: 1
 };
 return x;
});

define function empfängt eine Factory-Funktion Parameter Wenn der Browser das Skript ausführt, führt die Funktion define die Factory aus und speichert ihren Rückgabewert in den Modulobjektmodulen des Loaders.

Wie identifiziere ich ein Modul? Sie können den URI der Datei verwenden, der eine eindeutige Kennung und eine natürliche ID ist.

Dateipfadpfad hat verschiedene Formen:

Absoluter Pfad: http://xxx, Datei://xxx

Relativer Pfad: ./xxx, ../ xxx , xxx (Dateipfad relativ zur aktuellen Seite)

Virtueller absoluter Pfad: /xxx/ stellt das Stammverzeichnis der Website dar

Daher ist eine „resolvePath“-Funktion erforderlich, um verschiedene Formen von Pfaden in URI zu analysieren , Sehen Sie sich zum Auflösen den Dateipfad der aktuellen Seite an.

Angenommen, wir müssen als Nächstes auf zwei Module, a.js und b.js, verweisen und eine Rückruffunktion f einrichten, die die Ausführung von a und b erfordert. Wir hoffen, dass der Loader a und b zieht. Wenn a und b geladen werden, werden a und b aus den Modulen entnommen und als Parameter an f übergeben, um den nächsten Schritt auszuführen. Dies kann mithilfe des Beobachtermodus (d. h. Abonnement-/Veröffentlichungsmodus) implementiert werden, indem ein EventProxy erstellt, die Lade-a- und Lade-b-Ereignisse abonniert werden, die Definitionsfunktion bis zum Ende ausgeführt wird und nach dem Mounten des Exports in Modulen ausgegeben wird Ein Ereignis, bei dem das Modul geladen wird. Nachdem eventProxy es empfangen hat, prüft es, ob sowohl a als auch b geladen sind. Wenn dies abgeschlossen ist, werden die Parameter an f übergeben, um den Rückruf auszuführen.

In ähnlicher Weise kann eventProxy auch das Laden von Modulabhängigkeiten implementieren

// a.js
define([ 'c.js', 'd.js' ], factory (c, d) {
 var x = c + d;
 return x;
});

Der erste Parameter der Definitionsfunktion kann in einem Abhängigkeitsarray übergeben werden , was darauf hinweist, dass Modul a von c und d abhängt. Wenn define ausgeführt wird, weisen Sie eventProxy an, die Ladeereignisse von c und d zu abonnieren. Führen Sie nach dem Laden die Rückruffunktion f aus, um den Export von a zu speichern, und geben Sie das Ereignis aus, dass a geladen wurde.

Die ursprüngliche Methode zum Laden von Skripten auf der Browserseite besteht darin, ein Skript-Tag einzufügen. Nach der Angabe von src beginnt der Browser mit dem Herunterladen des Skripts.

Dann kann das Laden des Moduls im Loader mithilfe von DOM-Operationen implementiert werden. Fügen Sie ein Skript-Tag ein und geben Sie src an. Zu diesem Zeitpunkt befindet sich das Modul im Download-Status.

PS: Im Browser unterscheidet sich das dynamische Einfügen von Skript-Tags von der Skript-Lademethode, wenn der Seitendom zum ersten Mal geladen wird:

Wenn die Seite zum ersten Mal geladen wird Anschließend analysiert der Browser das DOM nacheinander von oben nach unten, lädt das Skript herunter und blockiert die DOM-Analyse. Warten Sie, bis das Skript heruntergeladen und ausgeführt wurde, bevor Sie mit der Analyse des DOM fortfahren und lädt mehrere Skripte im Voraus herunter, aber die Reihenfolge der Ausführung ist dieselbe wie ihre Ausführungsreihenfolge im DOM. Die Reihenfolge ist konsistent und andere DOM-Analysen werden während der Ausführung blockiert)

Dynamisches Einfügen von Skripten,

var a = document.createElement('script'); a.src='xxx';

Der Browser führt es aus, nachdem das Skript heruntergeladen wurde. und der Prozess ist asynchron.

Nachdem der Download abgeschlossen ist, führen Sie die oben genannten Vorgänge aus, analysieren Sie Abhängigkeiten – > laden Sie Abhängigkeiten – > analysieren Sie dieses Modul – > Laden abgeschlossen – > führen Sie den Rückruf aus.

Woher wissen Sie nach dem Herunterladen des Moduls beim Parsen dessen URL? Es gibt zwei Methoden: Eine besteht darin, srcipt.onload zu verwenden, um das src-Attribut dieses Objekts abzurufen, und die andere darin, document.currentScript.src in der Definitionsfunktion zu verwenden.

Es ist relativ einfach, Grundfunktionen mit weniger als 200 Zeilen Code zu implementieren:

var zmm = {
 _modules: {},
 _configs: {
  // 用于拼接相对路径
  basePath: (function (path) {
   if (path.charAt(path.length - 1) === '/') {
    path = path.substr(0, path.length - 1);
   }
   return path.substr(path.indexOf(location.host) + location.host.length + 1);
  })(location.href),
  // 用于拼接相对根路径
  host: location.protocol + '//' + location.host + '/'
 }
};
zmm.hasModule = function (_uri) {
 // 判断是否已有该模块,不论加载中或已加载好
 return this._modules.hasOwnProperty(_uri);
};
zmm.isModuleLoaded = function (_uri) {
 // 判断该模块是否已加载好
 return !!this._modules[_uri];
};
zmm.pushModule = function (_uri) {
 // 新模块占坑,但此时还未加载完成,表示加载中;防止重复加载
 if (!this._modules.hasOwnProperty(_uri)) {
  this._modules[_uri] = null;
 }
};
zmm.installModule = function (_uri, mod) {
 this._modules[_uri] = mod;
};
zmm.load = function (uris) {
 var i, nsc;
 for (i = 0; i < uris.length; i++) {
  if (!this.hasModule(uris[i])) {
   this.pushModule(uris[i]);
   // 开始加载
   var nsc = document.createElement(&#39;script&#39;);
    nsc.src = uri;
   document.body.appendChild(nsc);
  }
 }
};
zmm.resolvePath = function (path) {
 // 返回绝对路径
 var res = &#39;&#39;, paths = [], resPaths;
 if (path.match(/.*:\/\/.*/)) {
  // 绝对路径
  res = path.match(/.*:\/\/.*?\//)[0]; // 协议+域名
  path = path.substr(res.length);
 } else if (path.charAt(0) === &#39;/&#39;) {
  // 相对根路径 /开头
  res = this._configs.host;
  path = path.substr(1);
 } else {
  // 相对路径 ./或../开头或直接文件名
  res = this._configs.host;
  resPaths = this._configs.basePath.split(&#39;/&#39;);
 }
 resPaths = resPaths || [];
 paths = path.split(&#39;/&#39;);
 for (var i = 0; i < paths.length; i++) {
  if (paths[i] === &#39;..&#39;) {
   resPaths.pop();
  } else if (paths[i] === &#39;.&#39;) {
   // do nothing
  } else {
   resPaths.push(paths[i]);
  }
 }
 res += resPaths.join(&#39;/&#39;);
 return res;
};
var define = zmm.define = function (dependPaths, fac) {
 var _uri = document.currentScript.src;
 if (zmm.isModuleLoaded(_uri)) {
  return;
 }
 var factory, depPaths, uris = [];
 if (arguments.length === 1) {
  factory = arguments[0];
  // 挂载到模块组中
  zmm.installModule(_uri, factory());
  // 告诉proxy该模块已装载好
  zmm.proxy.emit(_uri);
 } else {
  // 有依赖的情况
  factory = arguments[1];
  // 装载完成的回调函数
  zmm.use(arguments[0], function () {
   zmm.installModule(_uri, factory.apply(null, arguments));
   zmm.proxy.emit(_uri);
  });
 }
};
zmm.use = function (paths, callback) {
 if (!Array.isArray(paths)) {
  paths = [paths];
 }
 var uris = [], i;
 for (i = 0; i < paths.length; i++) {
  uris.push(this.resolvePath(paths[i]));
 }
 // 先注册事件,再加载
 this.proxy.watch(uris, callback);
 this.load(uris);
};
zmm.proxy = function () {
 var proxy = {};
 var taskId = 0;
 var taskList = {};
 var execute = function (task) {
  var uris = task.uris,
   callback = task.callback;
  for (var i = 0, arr = []; i < uris.length; i++) {
   arr.push(zmm._modules[uris[i]]);
  }
  callback.apply(null, arr);
 };
 var deal_loaded = function (_uri) {
  var i, k, task, sum;
  // 当一个模块加载完成时,遍历当前任务栈
  for (k in taskList) {
   if (!taskList.hasOwnProperty(k)) {
    continue;
   }
   task = taskList[k];
   if (task.uris.indexOf(_uri) > -1) {
    // 查看这个任务中的模块是否都已加载好
    for (i = 0, sum = 0; i < task.uris.length; i++) {
     if (zmm.isModuleLoaded(task.uris[i])) {
      sum ++;
     }
    }
    if (sum === task.uris.length) {
     // 都加载完成 删除任务
     delete(taskList[k]);
     execute(task);
    }
   }
  }
 };
 proxy.watch = function (uris, callback) {
  // 先检查一遍是否都加载好了
  for (var i = 0, sum = 0; i < uris.length; i++) {
   if (zmm.isModuleLoaded(uris[i])) {
    sum ++;
   }
  }
  if (sum === uris.length) {
   execute({
    uris: uris,
    callback: callback
   });
  } else {
   // 订阅新加载任务
   var task = {
    uris: uris,
    callback: callback
   };
   taskList[&#39;&#39; + taskId] = task;
   taskId ++;
  }
 };
 proxy.emit = function (_uri) {
  console.log(_uri + &#39; is loaded!&#39;);
  deal_loaded(_uri);
 };
 return proxy;
}();

Problem der zirkulären Abhängigkeit

„Zirkuläres Laden“ bezieht sich auf die Ausführungsabhängigkeit eines Skripts B-Skript, und die Ausführung von B-Skript hängt von einem Skript ab. Dies ist ein Design, das vermieden werden sollte.

Browserseite

Verwenden Sie das obige ZMM-Tool, um Modul a zu laden:

// main.html
zmm.use(&#39;/a.js&#39;, function(){...});
// a.js
define(&#39;/b.js&#39;, function(b) {
 var a = 1;
 a = b + 1;
 return a;
});
// b.js
define(&#39;/a.js&#39;, function(a) {
 var b = a + 1;
 return b;
});

gerät in einen Deadlock, bei dem a darauf wartet, dass b geladen wird, und b darauf wartet ein zu ladender Zustand. Wenn diese Situation auftritt, ist Sea.js ebenfalls blockiert. Möglicherweise sollte dieses Verhalten nicht standardmäßig auftreten.

Seajs können require.async verwenden, um das Problem der zirkulären Abhängigkeiten zu lindern, aber a.js muss neu geschrieben werden:

// a.js
define(&#39;./js/a&#39;, function (require, exports, module) {
 var a = 1;
 require.async(&#39;./b&#39;, function (b) {
  a = b + 1;
  module.exports = a; //a= 3
 });
 module.exports = a; // a= 1
});
// b.js
define(&#39;./js/b&#39;, function (require, exports, module) {
 var a = require(&#39;./a&#39;);
 var b = a + 1;
 module.exports = b;
});
// main.html
seajs.use(&#39;./js/a&#39;, function (a) {
 console.log(a); // 1
});

Um dies zu tun, muss a jedoch zunächst wissen, dass b davon abhängt auf sich selbst, und die verwendete Ausgabe ist der Wert von a, bevor b geladen wird. Verwendung weiß nicht, dass sich der Wert von a später ändern wird.

Auf der Browserseite scheint es keine gute Lösung zu geben. Das Problem der zirkulären Abhängigkeit beim Laden von Knotenmodulen ist viel geringer.

node/CommonJS

Die wichtige Funktion des CommonJS-Moduls ist die Ausführung beim Laden, das heißt, wenn der Skriptcode erforderlich ist, wird er vollständig ausgeführt. Der Ansatz von CommonJS besteht darin, dass, sobald ein Modul „in einer Schleife geladen“ ist, nur der ausgeführte Teil ausgegeben wird und der nicht ausgeführte Teil nicht ausgegeben wird.

// a.js
var a = 1;
module.exports = a;
var b = require(&#39;./b&#39;);
a = b + 1;
module.exports = a;
// b.js
var a = require(&#39;./a&#39;);
var b = a + 1;
module.exports = b;
// main.js
var a = require(&#39;./a&#39;);
console.log(a); //3

上面main.js的代码中,先加载模块a,执行require函数,此时内存中已经挂了一个模块a,它的exports为一个空对象a.exports={};接着执行a.js中的代码;执行var b = require('./b');之前,a.exports=1,接着执行require(b);b.js被执行时,拿到的是a.exports=1,b加载完成后,执行权回到a.js;最后a模块的输出为3。

CommonJS与浏览器端的加载器有着实现上的差异。node加载的模块都是在本地,执行的是同步的加载过程,即按依赖关系依次加载,执行到加载语句就去加载另一个模块,加载完了再回到函数调用点继续执行;浏览器端加载scripts由于天生限制,只能采取异步加载,执行回调来实现。

ES6

ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值。因此,ES6模块是动态引用,不存在缓存值的问题,而且模块里面的变量,绑定其所在的模块。

这导致ES6处理"循环加载"与CommonJS有本质的不同。ES6根本不会关心是否发生了"循环加载",只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

来看一个例子:

// even.js
import { odd } from &#39;./odd&#39;;
export var counter = 0;
export function even(n) { counter++; return n == 0 || odd(n - 1);}
// odd.js
import { even } from &#39;./even&#39;;
export function odd(n) { return n != 0 && even(n - 1);}
// main.js
import * as m from &#39;./even.js&#39;;
m.even(10); // true; m.counter = 6

上面代码中,even.js里面的函数even有一个参数n,只要不等于0,就会减去1,传入加载的odd()。odd.js也会做类似作。

上面代码中,参数n从10变为0的过程中,foo()一共会执行6次,所以变量counter等于6。第二次调用even()时,参数n从20变为0,foo()一共会执行11次,加上前面的6次,所以变量counter等于17。

而这个例子要是改写成CommonJS,就根本无法执行,会报错。

// even.js
var odd = require(&#39;./odd&#39;);
var counter = 0;
exports.counter = counter;
exports.even = function(n) {
counter++;
return n == 0 || odd(n - 1);
}
// odd.js
var even = require(&#39;./even&#39;).even;
module.exports = function(n) {
return n != 0 && even(n - 1);
}
// main.js
var m = require(&#39;./even&#39;);
m.even(10); // TypeError: even is not a function

上面代码中,even.js加载odd.js,而odd.js又去加载even.js,形成"循环加载"。这时,执行引擎就会输出even.js已经执行的部分(不存在任何结果),所以在odd.js之中,变量even等于null,等到后面调用even(n-1)就会报错。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,同时也希望多多支持PHP中文网!

更多概述如何实现一个简单的浏览器端js模块加载器相关文章请关注PHP中文网!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn