Maison  >  Article  >  interface Web  >  Présentation de la façon d'implémenter un simple chargeur de module js côté navigateur

Présentation de la façon d'implémenter un simple chargeur de module js côté navigateur

高洛峰
高洛峰original
2016-12-28 14:26:391470parcourir

Avant es6, js n'avait pas de fonctions modulaires matures comme les autres langages. La page ne pouvait introduire que ses propres scripts ou ceux de tiers en insérant des balises de script une par une, et il était facile de provoquer des conflits de noms. La communauté js a fait beaucoup d'efforts pour obtenir l'effet « module » dans l'environnement d'exécution à cette époque.

Les normes modulaires générales js incluent CommonJS et AMD. Le premier est utilisé dans l'environnement de nœud, et le second est implémenté dans l'environnement du navigateur par Require.js, etc. En outre, il existe le projet open source national Sea.js, qui suit la spécification CMD. (Actuellement, avec la popularité d'es6, la maintenance a été arrêtée. Qu'il s'agisse d'AMD ou de CMD, ce sera un morceau d'histoire)

Chargeur js côté navigateur

Implémenter un js simple chargeur et Ce n'est pas compliqué et peut être principalement divisé en quatre étapes : analyser le chemin, télécharger le module, analyser les dépendances du module et analyser le module.

Définissez d’abord le module. Dans diverses spécifications, un fichier js représente généralement un module. Ensuite, nous pouvons construire une fermeture dans le fichier du module et passer un objet lors de l'export du module :

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

la fonction de définition reçoit une fonction Factory paramètres Lorsque le navigateur exécute le script, la fonction de définition exécute la fabrique et stocke sa valeur de retour dans les modules objet module du chargeur.

Comment identifier un module ? Vous pouvez utiliser l'URI du fichier, qui est un identifiant unique et un identifiant naturel.

Le chemin du chemin du fichier a plusieurs formes :

Chemin absolu : http://xxx, fichier://xxx

Chemin relatif : ./xxx, ../ xxx, xxx (chemin du fichier par rapport à la page actuelle)

Chemin absolu virtuel : /xxx/ représente le répertoire racine du site Web

Par conséquent, une fonction solvePath est nécessaire pour analyser différentes formes de chemins dans uri, reportez-vous au chemin du fichier de la page actuelle pour résoudre.

Ensuite, supposons que nous devions référencer deux modules, a.js et b.js, et configurer une fonction de rappel f qui nécessite l'exécution de a et b. Nous espérons que le chargeur extraira a et b. Lorsque a et b seront chargés, a et b seront extraits des modules et transmis comme paramètres à f pour effectuer l'étape suivante. Cela peut être implémenté en utilisant le mode observateur (c'est-à-dire le mode abonnement/publication), en créant un eventProxy, en s'abonnant aux événements de chargement a et de chargement b ; la fonction de définition est exécutée jusqu'à la fin, et une fois l'exportation montée dans les modules, émise ; un événement selon lequel le module est chargé, après que eventProxy l'ait reçu, il vérifie si a et b sont tous deux chargés, il transmet les paramètres à f pour exécuter le rappel.

De même, eventProxy peut également implémenter le chargement de dépendances de module

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

Le premier paramètre de la fonction de définition peut être passé dans un tableau de dépendances , indiquant que le module a dépend de c et d. Lorsque définir est exécuté, dites à eventProxy de s'abonner aux événements de chargement de c et d. Après le chargement, exécutez la fonction de rappel f pour stocker l'exportation de a et émettez l'événement indiquant que a a été chargé.

La méthode originale de chargement des scripts côté navigateur consiste à insérer une balise de script. Après avoir spécifié src, le navigateur commence à télécharger le script.

Ensuite, le chargement du module dans le chargeur peut être implémenté à l'aide d'opérations dom. Insérez une balise de script et spécifiez src. À ce stade, le module est en état de téléchargement.

PS : Dans le navigateur, l'insertion dynamique des balises de script est différente de la méthode de chargement du script lorsque la page dom est chargée pour la première fois :

Lorsque la page est chargée pour la première fois À ce moment-là, le navigateur analysera le dom de manière séquentielle de haut en bas. Lorsque vous atteindrez la balise de script, téléchargez le script et bloquez l'analyse du DOM. Attendez que le script soit téléchargé et exécuté avant de continuer à analyser le DOM (les navigateurs modernes ont effectué une optimisation du préchargement). et téléchargera plusieurs scripts à l'avance, mais l'ordre d'exécution est le même que leur ordre d'exécution dans le DOM (l'ordre est cohérent et les autres analyses du DOM sont bloquées pendant l'exécution)

Insertion dynamique du script,

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

Le navigateur l'exécutera une fois le script téléchargé, et le processus est asynchrone.

Une fois le téléchargement terminé, effectuez les opérations ci-dessus, analyser les dépendances->charger les dépendances->analyser ce module->chargement terminé->exécuter le rappel.

Une fois le module téléchargé, comment connaître son uri lors de son analyse ? Il existe deux méthodes, l'une consiste à utiliser srcipt.onload pour obtenir l'attribut src de cet objet ; l'autre consiste à utiliser document.currentScript.src dans la fonction de définition.

Il est relativement simple d'implémenter des fonctions de base, avec moins de 200 lignes de code :

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;
}();
Problème de dépendance circulaire

Le "chargement circulaire" fait référence à la dépendance d'exécution d'un script b script, et l'exécution de b script dépend d'un script. C’est une conception qui devrait être évitée si possible.

Côté navigateur

Utilisez l'outil zmm ci-dessus pour charger le module a :

// 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;
});
tombera dans une impasse en attendant que b soit chargé et b en attente de un état à charger. Sea.js est également dans une impasse lorsqu'il rencontre cette situation. Peut-être que ce comportement ne devrait pas se produire par défaut.

Seajs peut utiliser require.async pour atténuer le problème des dépendances circulaires, mais a.js doit être réécrit :

// 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
});
Mais pour ce faire, a doit d'abord savoir que b dépendra sur lui-même, et La sortie utilisée est la valeur de a avant le chargement de b. Use ne sait pas que la valeur de a changera plus tard.

Côté navigateur, il ne semble pas y avoir de bonne solution. Le problème de dépendance circulaire rencontré par le chargement du module de nœud est beaucoup plus petit.

node/CommonJS

La fonctionnalité importante du module CommonJS est l'exécution lors du chargement, c'est-à-dire que tous les codes de script seront exécutés lorsque cela est nécessaire. L'approche de CommonJS est qu'une fois qu'un module est « chargé en boucle », seule la partie exécutée sera sortie, et la partie non exécutée ne sera pas sortie.

// 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中文网!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn