Overview of how to implement a simple browser-side JS module Loader _javascript Tips

Source: Internet
Author: User
Tags define function emit script tag hasownproperty

Before Es6, JS does not have the mature modularity function in other languages, the page can only insert a script tag to introduce its own or third-party scripts, and easy to bring the problem of naming conflicts. JS community did a lot of effort, in the then operating environment, the implementation of the "module" effect.

The common JS Modular standard has COMMONJS and AMD, the former is used in the node environment, the latter in the browser environment by Require.js and so on. In addition, there are domestic open source project Sea.js, follow the CMD specification. (at present, with the popularization of ES6 has stopped maintenance, whether it is AMD or CMD, will be a period of history)

Browser-side JS Loader

The realization of a simple JS loader is not complicated, mainly can be divided into analytic path, download module, Analysis module dependencies, parsing module four steps.

First, define the module. In a variety of specifications, usually a JS file that represents a module. So, in the module file, we can construct a closure and an object that is exported as a module:

Define (Factory () {
 var x = {
  a:1
 };
 return x;
});

The Define function receives a factory function parameter, and when the browser executes the script, the Define function executes the factory and stores its return value in the loader's module object modules.

How do you identify a module? You can use the URI of the file, which is the unique identifier and is the natural ID.

File path Path has several forms:

    • Absolute path: http://xxx, file://xxx
    • Relative path:./xxx,. /xxx, XXX (relative to the current page's file path)
    • Virtual absolute path:/xxx/representing Web site root directory

Therefore, a resolvepath function is needed to parse different forms of path into URIs, referring to the file path of the current page.

Next, suppose we need to refer to A.js and b.js two modules, and set a callback function f that requires a and B to execute. We want the loader to pull A and B, and when A and B are loaded, remove a and b from the modules as parameters to F, and perform the next step. This can be implemented in observer mode (i.e. subscription/publishing mode), creating a eventproxy, subscribing to load a and loading B events, and define function execution to the end, after the export has been mounted modules, emit an event that this module loads and completes. When Eventproxy is received, check that both A and B are both loaded and, if it is done, pass the call to F to perform the callback.

Similarly, Eventproxy can also implement module-dependent loading

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

The first parameter of the Define function can pass in a dependent array, which means that a module relies on C and D. When define executes, tell Eventproxy to subscribe to the C and D loading events, load up on the Execute callback function f store A's export, and emit event A is loaded.

The original method of browser-side loading scripts is to insert a script tag, after which the browser starts downloading the script after specifying SRC.

Then the module load in the loader can be implemented with DOM operations, insert a script label and specify SRC, at which time the module is in the download state.

PS: In the browser, the dynamic insertion of the script tag differs from the script loading method when the page DOM is first loaded:

When the page is first loaded, the browser parses the DOM from top to bottom, and when it encounters the script tag, it downloads and blocks Dom parsing, and waits until the script downloads, executes, and then continues parsing the DOM (modern browsers do preload optimizations that will download multiple scripts beforehand, But the order of execution is consistent with their order in the DOM, blocking other DOM parsing at execution time.

Dynamic Insert Script,

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

The browser executes after the script download completes, and the procedure is asynchronous.

After the download is completed, the resolution relies on-> load dependency-> resolves this module-> load completion-> execution callback.

After the module is downloaded, how do you know its URI when parsing it? There are two kinds of hair, one is to get the SRC attribute of this object with Srcipt.onload, and one is to use DOCUMENT.CURRENTSCRIPT.SRC in the Define function.

Simple to achieve basic functionality, less than 200 lines of code:

var zmm = {_modules: {}, _configs: {//For stitching relative path 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),//For stitching relative root path Host:location.protocol + '//' + Location.host + '/'}};
Zmm.hasmodule = function (_uri) {//To determine whether the module is already in load or loaded with a good return This._modules.hasownproperty (_uri); zmm.ismoduleloaded = function (_uri) {//Determine if the module has been loaded well return!!
This._modules[_uri];
}; Zmm.pushmodule = function (_uri) {//new module pit, but not completed at this time, indicates load, prevent duplicate load if (!this._modules.hasownproperty (_uri)) {This._mo
 Dules[_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));
    Start loading var nsc = document.createelement (' script ');
   Nsc.src = URI;Document.body.appendChild (NSC);
}
 }
};
 Zmm.resolvepath = function (path) {//return absolute path var res = ', paths = [], respaths; 
 if (Path.match (/.*:\/\/.*/)) {//absolute path res = Path.match (/.*:\/\/.*?\//) [0];//protocol + domain Name path = path.substr (res.length);
  else if (path.charat (0) = = = '/') {//relative root path/opening res = This._configs.host;
 Path = PATH.SUBSTR (1); else {//relative path./or ...
  /beginning or direct filename res = this._configs.host;
 Respaths = This._configs.basepath.split ('/'); } respaths = Respaths | |
 [];
 paths = Path.split ('/'); for (var i = 0; i < paths.length i++) {if (paths[i] = = ' ... ')
  {Respaths.pop (); else if (paths[i] = = '. ')
  {//Do nothing} else {Respaths.push (paths[i]);
 } res + + respaths.join ('/');
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]; Mount to module groupMedium Zmm.installmodule (_uri, Factory ());
 Tell the proxy that the module is loaded with Zmm.proxy.emit (_uri);
  else {//dependent case factory = arguments[1];
   Load-Completed callback function 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));
 ///Register the event first, then load 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; When a module load completes, traverse the current task stack for (k in taskList) {if (!tasklist.hasownproperty (k)) {continue;
   task = Tasklist[k];
     if (task.uris.indexOf (_uri) >-1) {//See whether the modules in this task have been loaded for (i = 0, sum = 0; i < task.uris.length; i++) {
     if (zmm.ismoduleloaded (Task.uris[i])) {sum + +;
     } if (sum = = task.uris.length) {//All load completes Delete task Delete (tasklist[k]);
    Execute (Task);
 }
   }
  }
 }; Proxy.watch = function (URIs, callback) {//first check to see if all is loaded for (var i = 0, sum = 0; i < uris.length; i++) {if (Z
   Mm.ismoduleloaded (Uris[i]) {sum + +;
  } if (sum = = Uris.length) {Execute ({uris:uris, callback:callback});
   else {//subscribe to the New load task var task = {uris:uris, callback:callback};
   tasklist[' + taskId] = task;
  TaskId + +;
 }
 };
  Proxy.emit = function (_uri) {console.log (_uri + ' is loaded! ');
 Deal_loaded (_uri);
 };
return proxy;

 }();

Circular dependency Issues

"Cyclic loading" means that the execution of a script relies on the B script, while the execution of the B script relies on a script. This is a design that should be avoided as much as possible.

Browser end

Load module A with the ZMM tool above:

main.html
zmm.use ('/a.js ', function () {...});
A.js
define ('/b.js ', function (b) {
 var a = 1;
 A = B + 1;
 return A;
});
B.js
define ('/a.js ', function (a) {
 var b = a + 1;
 return b;
});

Will be caught in a wait for B load complete, b waiting for a load to complete the deadlock state. Sea.js encountered this situation is also a deadlock, perhaps by default this behavior should not appear.

Seajs can be require.async to alleviate the problem of circular dependencies, but the a.js must be rewritten:

A.js
define ('./js/a ', function (require, exports, module) {
 var a = 1;
 Require.async ('./b ', function (b) {
  A = B + 1;
  Module.exports = A; A= 3
 });
 Module.exports = A; A= 1
});
B.js
define ('./js/b ', function (require, exports, module) {
 var a = require ('. a ');
 var B = a + 1;
 Module.exports = b;
});
main.html
seajs.use ('./js/a ', function (a) {
 console.log (a);//1
});

But in doing so a must know that B will depend on itself, and the use of the output is a B has not loaded the value of a, use does not know a value will be changed after.

On the browser side, there seems to be no good solution. Node module loading encounters a much smaller loop dependency problem.

Node/commonjs

An important feature of the COMMONJS module is loading-time execution, in which the script code executes when it is require. Commonjs's approach is that once a module is "cycled", it outputs only the part that has been executed, and the part that has not been executed will not be output.

A.js
var a = 1;
Module.exports = A;
var B = require ('./b ');
A = B + 1;
Module.exports = A;
B.js
var a = require ('. a ');
var B = a + 1;
Module.exports = b;
Main.js
var a = require ('. a ');
Console.log (a); 3

In the Main.js code above, load module a first, execute the Require function, at this time a module A is already hung in memory, its exports is a null object a.exports={}; Then execute the code in A.js; execute var b = require ('./b ') Before, A.exports=1, then executes require (b), B.js is executed when the a.exports=1,b load completes, the execution right returns to A.js, the last a module output is 3.

There is an implementation difference between the COMMONJS and the browser-side loader. Node-loaded modules are local, execute is the synchronous loading process, that is, load by dependency, execute to load statement to load another module, load and then return to function call point to continue execution; browser-side loading scripts due to inherent limitations, can only take asynchronous loading, execution callback to implement.

ES6

The operating mechanism of the ES6 module is not the same as the COMMONJS, when it encounters the import of the module load command, it does not execute the module, but only generates a reference. Wait until it really needs to be used, and then go to the module to fetch the value. Therefore, the ES6 module is a dynamic reference, there is no problem with the cached value, and the variables inside the module bind to the module in which it resides.

This causes ES6 to deal with "cyclic loading" and commonjs are essentially different. ES6 does not care if "cyclic loading" occurs, but simply generates a reference to the loaded module, requiring the developer to ensure that the value is fetched when the value is actually fetched.

Take a look at an example:

Even.js
Import {odd} from './odd ';
Export var counter = 0;
Export function even (n) {counter++; return n = = 0 | | odd (N-1);}
Odd.js
Import {even} from './even ';
Export function odd (n) {return n!= 0 && even (n-1);}
Main.js
Import * as M from './even.js ';
M.even (10); True M.counter = 6

In the code above, even.js inside the function even has a parameter n, as long as does not equal to 0, will subtract 1, passes in the loaded odd (). Odd.js will do the same.

In the code above, when parameter n changes from 10 to 0, Foo () executes 6 times, so the variable counter equals 6. When the second call to even (), the parameter n changes from 20 to 0,foo () is performed 11 times, plus the preceding 6 times, so the variable counter equals 17.

This example, if rewritten into commonjs, can not be implemented at all, will be an error.

Even.js
var odd = require ('./odd ');
var counter = 0;
Exports.counter = counter;
Exports.even = function (n) {
counter++;
return n = = 0 | | Odd (n-1);
}
Odd.js
var even = require ('./even '). even;
Module.exports = function (n) {return
n!= 0 && even (n-1);
}
Main.js
var m = require ('./even ');
M.even (10); Typeerror:even is not a function

In the code above, Even.js loads the odd.js, and odd.js loads the even.js to form a "cyclic load." At this point, the execution engine outputs the part of the even.js that has already been executed (no results), so in odd.js, the variable even equals null, and the error occurs when the subsequent call to even (n-1).

The above is the entire content of this article, I hope the content of this article for everyone's study or work can bring some help, if there are questions you can message exchange, but also hope that a lot of support cloud Habitat community!

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.