模仿RequireJs的用法写一个低配版的模块加载器

作者: 化工塑胶  发布:2019-11-26

模仿RequireJs的用法写一个低配版的模块加载器


在es6之前,js不像其他语言自带成熟的模块化功能,页面只能靠插入一个个script标签来引入自己的或第三方的脚本,并且容易带来命名冲突的问题。js社区做了很多努力,在当时的运行环境中,实现"模块"的效果。

Contents

  1. 前言
  2. 回顾RequireJs的基本用法
  3. 实现原理
  4. 使用方法
  5. 总结

通用的js模块化标准有CommonJS与AMD,前者运用于node环境,后者在浏览器环境中由Require.js等实现。此外还有国内的开源项目Sea.js,遵循CMD规范。(目前随着es6的普及已经停止维护,不论是AMD还是CMD,都将是一段历史了)

前言


前段时间一直想用单页开发技术写一个自己的个人网站(使用es2015),写了一部分之后,发现单页应用因为只有一个页面,所以第一次加载index.html时就要下载所有js文件,并且为了好管理各个部分的状态,需要划分页面的各个功能区为各个模块,es2015本身是不支持一些模块规范的(比如AMD、CMD、CommonJs等),所以只能这样模拟实现:

  // global
  var spa = (function(){...})();

  // module blog
  spa.blog = (function(){
    ...
    return {
      do1: do1,
      do2: do2,
    };
  })();

  // module model
  spa.model = (function(){...})();

  // module shell
  spa.model = (function(){...})();

并且各个模块之间又存在一些依赖关系,在index.html里面写script标签来载入模块时需要写很多个,同时也要根据依赖关系来确定书写顺序,页面逻辑混乱,如下:

  <script type="text/javascript" src="/javascripts/spa.utils.js"></script>
  <script type="text/javascript" src="/javascripts/spa.model.js"></script>
  <script type="text/javascript" src="/javascripts/spa.mock.js"></script>
  <script type="text/javascript" src="/javascripts/spa.chat.js"></script>
  <script type="text/javascript" src="/javascripts/spa.blog.js"></script>
  <script type="text/javascript" src="/javascripts/spa.action.js"></script>
  <script type="text/javascript" src="/javascripts/spa.shell.js"></script>

之前用过RequireJs(一个流行的JavaScript模块加载器),它是用同构js的架构来写的,所以node.js环境下也能使用。我想自己可以尝试一下写一个低配版的js模块加载器 requireJs-nojsja 来应付一下我这个单页网站,当然只是大致模仿了主要功能。

浏览器端js加载器

回顾RequireJs的基本用法


  1. 配置模块信息
  requirejs.config({
      baseUrl: '/javascripts',  // 配置根目录
      paths: {
        moduleA: 'a.js',
        moduleB: 'b.js',
        moduleC: 'c.js',
      },
      shim: {  // 配置不遵循amd规范的模块
        moduleC: {
          exports: 'log',
          deps: ['moduleA']
        }
      },
  });
  1. 定义一个模块
  define(name, ['moduleA', 'moduleB'], function(a, b){
    ...
    return {
      do: function() {
        a.doSomething();
        b.doAnother();
      }
    };
  });
  1. 引用一个模块
  // 引用模块
  require(['moduleA', 'moduleB'], function(a, b) {
    a.doSomething();
    b.doAnother();
  });

实现一个简单的js加载器并不复杂,主要可以分为解析路径、下载模块、解析模块依赖、解析模块四个步骤。

实现原理


  1. config方法确定各个模块的依赖关系
  /* 记录模块访问地址和模块的依赖等信息 */
  Require.config({
    baseUrl: '/javascripts/',
    paths: {
      'moduleA': './moduleA.js',  // 相对于当前目录
      'moduleB': '/javascripts/moduleB.js',  // 不使用baseUrl
      'moduleC': 'moduleC.js',

      'moduleD': {
        url: 'moduleD.js',
        deps: ['moduleE', 'moduleF'],
      },
      ...
    },
    shim: {
      'moduleH': {
        url: 'moduleH.js',
        exports: 'log',
      },
    }
  });
  1. 数据请求过程分析

(1) config配置模块信息时并不会触发网络请求
(2) 在index.js主入口文件里使用require方法引用多个模块时,根据config配置文件构造一下所有模块的依赖分析树。按深度优先或是广度优先来遍历这个依赖树,将所有依赖按照依赖顺序放进一个数组,最后进行数组去重处理,因为会出现依赖重复的情况

  var dependsTree = new Tree('dependsTree');
  var dependsArray = [];
  var dependsFlag = {};  // 解决循环依赖

  // 创建树
  setDepends(depends, dependsTree);
  // 得到依赖数组
  sortDepends(dependsArray, dependsTree);
  // 数据去重
  arrayFilter(dependsArray);

  return dependsArray;

(3) 创建XHR对象异步下载数组里面的所有js文件,按照依赖顺序挨个解析js代码,解析完成后触发回调函数,回调函数里传入各个模块的引用

  // ajax下载代码文件
  Utils.request(url, 'get', null, function(responseText){
    // 暂时保存
    _temp[module_name] = responseText;
  });

  // 文件下载完成后eval解析代码
  array.map(function(jsText){
    ...
    eval(jsText);
    ...
  });

  // 调用回调函数
  callback.apply(null, [dep1, dep2, dep3]);

首先定义一下模块。在各种规范中,通常一个js文件即表示一个模块。那么,我们可以在模块文件中,构造一个闭包,并传出一个对象,作为模块的导出:

使用方法


详细说明: github README.md

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

总结


  1. 下载js代码时我用了ajax来实现,所以对于跨域文件和CDN会有点问题,这个可以改成创建script标签,指定标签src,最后将document.head.appendChild(script),这样来解决,其它的诸如使用XMLHttpRequest 2.0,iframe等也可以的,可以实验一下。
  2. 解析代码时我用了eval的方法,这个eval在JavaScript里面是众说纷纭,可以看看这个,如果是用了上面创建script标签的方法的话,就不用自己eval了。
  3. 发现一个bug,存在循环依赖时,代码会报错,还没去解决。RequireJs是这样处理的:模块a依赖b,同时b依赖a,这种情况下b的模块函数被调用时,被传入的a是undefined,所以需要自己在b里面手动require一下a。

define函数接收一个工厂函数参数,浏览器执行该脚本时,define函数执行factory,并把它的return值存储在加载器的模块对象modules里。

如何标识一个模块呢?可以用文件的uri,它是唯一标识,是天然的id。

文件路径path有几种形式:

绝对路径:, file://xxx 相对路径:./xxx , ../xxx , xxx 虚拟绝对路径:/xxx /表示网站根目录

因此,需要一个resolvePath函数来将不同形式的path解析成uri,参照当前页面的文件路径来解析。

接着,假设我们需要引用a.js与b.js两个模块,并设置了需要a与b才能执行的回调函数f。我们希望加载器去拉取a与b,当a与b都加载完成后,从modules里取出a与b作为参数传给f,执行下一步操作。这里可以用观察者模式实现,创建一个eventProxy,订阅加载a与加载b事件;define函数执行到最后,已经把导出挂载modules里之后,emit一个本模块加载完成的事件,eventProxy收到后检查a与b是否都加载完成,如果完成,就传参给f执行回调。

同理,eventProxy也可以实现模块依赖加载

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

define函数的第一个参数可以传入一个依赖数组,表示a模块依赖c与d。define执行时,告诉eventProxy订阅c与d加载事件,加载好了就执行回调函数f存储a的导出,并emit事件a已加载。

浏览器端加载脚本的原始方法是插入一个script标签,指定src之后,浏览器开始下载该脚本。

那么加载器中的模块加载可以用dom操作实现,插入一个script标签并指定src,此时该模块为下载中状态。

PS:浏览器中,动态插入script标签与初次加载页面dom时的script加载方式不同:

初次加载页面,浏览器会从上到下顺序解析dom,碰到script标签时,下载脚本并阻塞dom解析,等到该脚本下载、执行完毕后再继续解析之后的dom(现代浏览器做了preload优化,会预先下载好多个脚本,但执行顺序与它们在dom中顺序一致,执行时阻塞其他dom解析)

动态插入script,

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

浏览器会在该脚本下载完成后执行,过程是异步的。

下载完成后执行上述的操作,解析依赖->加载依赖->解析本模块->加载完成->执行回调。

模块下载完成后,如何在解析它时知道它的uri呢?有两种发发,一种是用srcipt.onload获取this对象的src属性;一种是在define函数中采用document.currentScript.src。

实现基本的功能比较简单,代码不到200行:

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

本文由88必发手机版发布于化工塑胶,转载请注明出处:模仿RequireJs的用法写一个低配版的模块加载器

关键词:

上一篇:没有了
下一篇:没有了