>  기사  >  웹 프론트엔드  >  간단한 브라우저 측 js 모듈 로더를 구현하는 방법 개요

간단한 브라우저 측 js 모듈 로더를 구현하는 방법 개요

高洛峰
高洛峰원래의
2016-12-28 14:26:391526검색

es6 이전에는 js에 다른 언어처럼 성숙한 모듈 기능이 없었습니다. 페이지에는 스크립트 태그를 하나씩 삽입하여 자체 스크립트나 타사 스크립트만 도입할 수 있었고 이름 충돌이 발생하기 쉬웠습니다. js 커뮤니티는 당시 실행 환경에서 "모듈" 효과를 얻기 위해 많은 노력을 기울였습니다.

일반적인 js 모듈 표준에는 CommonJS와 AMD가 포함됩니다. 전자는 노드 환경에서 사용되고 후자는 Require.js 등으로 브라우저 환경에서 구현됩니다. 이 밖에도 CMD 스펙을 따르는 국내 오픈소스 프로젝트 Sea.js가 있다. (현재는 es6의 인기로 유지보수가 중단되었습니다. AMD든 CMD든 역사에 남을 겁니다)

브라우저측 js 로더

간단한 js 구현 로더는 복잡하지 않으며 경로 구문 분석, 모듈 다운로드, 모듈 종속성 구문 분석, 모듈 구문 분석의 네 단계로 나눌 수 있습니다.

먼저 모듈을 정의합니다. 다양한 사양에서 js 파일은 일반적으로 모듈을 나타냅니다. 그런 다음 모듈 파일에 클로저를 구성하고 모듈 내보내기로 개체를 전달할 수 있습니다.

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

define 함수가 Factory 함수를 받습니다. 브라우저가 스크립트를 실행할 때 정의 함수는 팩토리를 실행하고 해당 반환 값을 로더의 모듈 객체 모듈에 저장합니다.

모듈을 식별하는 방법은 무엇입니까? 고유 식별자이자 자연 ID인 파일의 URI를 사용할 수 있습니다.

파일 경로 경로에는 여러 형식이 있습니다.

절대 경로: http://xxx, file://xxx

상대 경로: ./xxx, ../ xxx , xxx(현재 페이지에 상대적인 파일 경로)

가상 절대 경로: /xxx/는 웹 사이트 루트 디렉터리를 나타냅니다.

따라서 다양한 형태의 경로를 uri로 구문 분석하려면 해결 경로 함수가 필요합니다. , 현재 페이지의 파일 경로를 참고하여 해결하세요.

다음으로 a.js와 b.js라는 두 모듈을 참조하고 a와 b를 실행해야 하는 콜백 함수 f를 설정해야 한다고 가정해 보겠습니다. 로더가 a와 b를 가져오기를 바랍니다. a와 b가 로드되면 a와 b가 모듈에서 가져와 다음 단계를 수행하기 위해 f에 매개변수로 전달됩니다. 이는 관찰자 모드(예: 구독/게시 모드)를 사용하여 구현할 수 있으며, eventProxy를 생성하고, a 로드 및 b 이벤트 로드를 구독하고, 정의 함수가 끝까지 실행되고, 내보내기가 모듈에 마운트된 후 내보냅니다. 모듈이 로드되는 이벤트입니다. eventProxy는 이를 수신한 후 a와 b가 모두 로드되었는지 확인합니다. 완료되면 매개변수를 f에 전달하여 콜백을 실행합니다.

마찬가지로 eventProxy는 모듈 종속성 로딩을 구현할 수도 있습니다.

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

정의 함수의 첫 번째 매개변수는 종속성 배열에 전달될 수 있습니다. , 이는 모듈 a가 c와 d에 의존함을 나타냅니다. Define이 실행되면 eventProxy에게 c와 d의 로딩 이벤트를 구독하도록 지시합니다. 로딩 후 콜백 함수 f를 실행하여 a의 내보내기를 저장하고 a가 로드된 이벤트를 내보냅니다.

브라우저 측에서 스크립트를 로드하는 원래 방법은 src를 지정한 후 스크립트 다운로드를 시작하는 것입니다.

그런 다음 dom 연산을 사용하여 로더에 로드되는 모듈을 구현할 수 있습니다. 이때, 모듈은 다운로드 중인 상태입니다.

PS: 브라우저에서 스크립트 태그의 동적 삽입은 페이지 DOM이 처음 로드될 때의 스크립트 로딩 방법과 다릅니다.

페이지가 처음 로드될 때 시간이 지나면 브라우저는 위에서 아래로 순차적으로 DOM을 구문 분석합니다. 스크립트 태그에 도달하면 스크립트를 다운로드하고 DOM 구문 분석을 차단합니다. DOM 구문 분석을 계속하기 전에 스크립트가 다운로드되고 실행될 때까지 기다립니다. 최신 브라우저는 사전 로드 최적화를 수행했습니다. 여러 스크립트를 미리 다운로드하지만 실행 순서는 DOM의 실행 순서와 동일합니다. 순서가 일관되어 실행 중 다른 DOM 구문 분석을 차단합니다.)

스크립트의 동적 삽입,

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

스크립트가 다운로드된 후 브라우저가 이를 실행합니다. 프로세스는 비동기식입니다.

다운로드가 완료된 후 위의 작업을 수행하고 종속성 구문 분석->종속성 로드->이 모듈 구문 분석->로딩 완료->콜백 실행을 수행합니다.

모듈을 다운로드한 후 구문 분석할 때 해당 URI를 어떻게 알 수 있나요? 두 가지 방법이 있습니다. 하나는 이 객체의 src 속성을 얻기 위해 srcipt.onload를 사용하는 것이고, 다른 하나는 정의 함수에서 document.currentScript.src를 사용하는 것입니다.

200줄 미만의 코드로 기본 기능을 구현하는 것은 비교적 간단합니다.

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;
}();

순환 종속성 문제

"순환 로딩"은 실행 종속성을 나타냅니다. b 스크립트의 경우 b 스크립트의 실행은 스크립트에 따라 다릅니다. 가능하다면 피해야 할 디자인입니다.

브라우저 측

위의 zmm 도구를 사용하여 모듈 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;
});

는 b가 로드되기를 기다리고 b가 로드되기를 기다리는 교착 상태에 빠집니다. 로드되는 상태입니다. 이 상황이 발생하면 Sea.js도 교착 상태에 빠지게 됩니다. 아마도 이 동작은 기본적으로 발생해서는 안 됩니다.

Seajs는 require.async를 사용하여 순환 종속성 문제를 완화할 수 있지만 a.js를 다시 작성해야 합니다.

// 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
});

그러나 이렇게 하려면 먼저 a가 b가 의존한다는 것을 알아야 합니다. 사용 중인 출력은 b가 로드되기 전의 a 값입니다. use는 a의 값이 나중에 변경될 것이라는 것을 모릅니다.

브라우저 측면에서는 마땅한 해결책이 없는 것 같습니다. 노드 모듈 로딩 시 발생하는 순환 종속성 문제는 훨씬 작습니다.

node/CommonJS

CommonJS 모듈의 중요한 기능은 로딩 시 실행, 즉 스크립트 코드가 필요할 때 모두 실행된다는 것입니다. CommonJS의 접근 방식은 모듈이 "루프 로드"되면 실행된 부분만 출력되고 실행되지 않은 부분은 출력되지 않는다는 것입니다.

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

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.