Home > Article > Web Front-end > Overview of how to implement a simple browser-side js module loader
Before es6, js did not have mature modular functions like other languages. The page could only introduce its own or third-party scripts by inserting script tags one by one, and it was easy to cause naming conflicts. The js community has made a lot of efforts to achieve the "module" effect in the running environment at that time.
Common js modular standards include CommonJS and AMD. The former is used in the node environment, and the latter is implemented by Require.js in the browser environment. In addition, there is the domestic open source project Sea.js, which follows the CMD specification. (Currently, with the popularity of es6, maintenance has been stopped. Whether it is AMD or CMD, it will be a piece of history)
Browser-side js loader
Implement a simple js loader and It is not complicated and can be divided into four steps: parsing the path, downloading the module, parsing module dependencies, and parsing the module.
First define the module. In various specifications, a js file usually represents a module. Then, we can construct a closure in the module file and pass out an object as the export of the module:
define(factory() { var x = { a: 1 }; return x; });
define function receives a factory function Parameters, when the browser executes the script, the define function executes the factory and stores its return value in the module object modules of the loader.
How to identify a module? You can use the uri of the file, which is a unique identifier and a natural id.
File path path has several forms:
Absolute path: http://xxx, file://xxx
Relative path: ./xxx, ../ xxx, xxx (file path relative to the current page)
Virtual absolute path:/xxx/ represents the website root directory
Therefore, a resolvePath function is needed to resolve different forms of paths into uri, Refer to the file path of the current page to resolve.
Next, suppose we need to reference two modules, a.js and b.js, and set up a callback function f that requires a and b to be executed. We hope that the loader will pull a and b. When a and b are loaded, a and b will be taken from the modules and passed as parameters to f to perform the next step. This can be implemented using the observer mode (i.e. subscription/publishing mode), creating an eventProxy, subscribing to the loading a and loading b events; the define function is executed to the end, and after the export has been mounted in modules, emit an event that the module is loaded. , after eventProxy receives it, it checks whether a and b are both loaded. If completed, it passes the parameters to f to execute the callback.
Similarly, eventProxy can also implement module dependency 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 be passed in a dependency array, indicating Module a depends on c and d. When define is executed, tell eventProxy to subscribe to the loading events of c and d. After loading, execute the callback function f to store the export of a, and emit the event that a has been loaded.
The original method of loading scripts on the browser side is to insert a script tag. After specifying src, the browser starts to download the script.
Then the module loading in the loader can be implemented using dom operations. Insert a script tag and specify src. At this time, the module is in the downloading state.
PS: In the browser, the dynamic insertion of script tags is different from the script loading method when the page dom is loaded for the first time:
When the page is loaded for the first time, the browser will parse the dom sequentially from top to bottom. When reaching the script tag, download the script and block the DOM parsing. Wait until the script is downloaded and executed before continuing to parse the DOM (modern browsers have done preload optimization and will download multiple scripts in advance, but the execution order is the same as their execution in the DOM. The order is consistent and other DOM parsing is blocked during execution)
Dynamicly insert script,
var a = document.createElement('script'); a.src='xxx'; document.body. appendChild(a);
The browser will execute the script after the download is completed, and the process is asynchronous.
After the download is completed, perform the above operations, parse dependencies->load dependencies->parse this module->loading completed->execute callback.
After the module is downloaded, how to know its uri when parsing it? There are two methods, one is to use srcipt.onload to obtain the src attribute of this object; the other is to use document.currentScript.src in the define function.
It is relatively simple to implement basic functions, with less than 200 lines of 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('script'); nsc.src = uri; document.body.appendChild(nsc); } } }; zmm.resolvePath = function (path) { // 返回绝对路径 var res = '', paths = [], resPaths; if (path.match(/.*:\/\/.*/)) { // 绝对路径 res = path.match(/.*:\/\/.*?\//)[0]; // 协议+域名 path = path.substr(res.length); } else if (path.charAt(0) === '/') { // 相对根路径 /开头 res = this._configs.host; path = path.substr(1); } else { // 相对路径 ./或../开头或直接文件名 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]; // 挂载到模块组中 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['' + taskId] = task; taskId ++; } }; proxy.emit = function (_uri) { console.log(_uri + ' is loaded!'); deal_loaded(_uri); }; return proxy; }();
Cyclic dependency problem
"Cyclic loading" refers to the fact that the execution of script a depends on script b , and the execution of script b depends on script a. This is a design that should be avoided if possible.
Browser side
Using the above zmm tool to load module a:
// 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 fall into a deadlock state where a is waiting for b to be loaded and b is waiting for a to be loaded. Sea.js is also deadlocked when encountering this situation. Perhaps this behavior should not occur by default.
Seajs can use require.async to alleviate the problem of circular dependencies, but 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 to do this, a must first know that b will depend on itself, and in use The output is the value of a when b has not been loaded. Use does not know that the value of a will change later.
On the browser side, there doesn't seem to be a good solution. The circular dependency problem encountered by node module loading is much smaller.
node/CommonJS
The important feature of the CommonJS module is execution when loading, that is, all script codes will be executed when required. CommonJS's approach is that once a module is "loop loaded", only the executed part will be output, and the unexecuted part 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
上面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 './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
上面代码中,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('./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
上面代码中,even.js加载odd.js,而odd.js又去加载even.js,形成"循环加载"。这时,执行引擎就会输出even.js已经执行的部分(不存在任何结果),所以在odd.js之中,变量even等于null,等到后面调用even(n-1)就会报错。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,同时也希望多多支持PHP中文网!
更多概述如何实现一个简单的浏览器端js模块加载器相关文章请关注PHP中文网!