一篇文章带你玩转前端所有模块化

1.前言

最初js并没有模块化的概念,直到ajax的时代,前端逻辑变的越来越复杂,一个html页面可能需要加载好多个js文件,也就出现了许多问题,比如全局变量污染,函数名冲突,文件的引入顺序,或者文件漏引等等问题,如果js也可以像java一样多好,所有的文件模块化,就可以解决我们上面碰到的种种问题,我们也可以更方便的复用自己和别人的代码,想要什么功能,就加载什么模块!多爽!我相信大家只要认真的读完这篇文章(小白都能看懂),你会受益很多!接下来从js模块化的历史开始说起!

2.模块化历程

最开始的写法,只要把不同的函数简单地放在一起,就是一个模块!

//test.js
function moudleTest1() {

}

function moudleTest2() {

}

这个test.js就是一个模块,里面有moudleTest1,moudleTest2方法,各种都有自己的功能,外面用的时候直接调用即可,那么问题是什么,全是全局变量,你命名moudleTest1,moudleTest2,下次别人写模块的时候也同样命名怎么办?接下来来就引入了对象来作为一个模块

//test.js
var test = {
   moudleTest1: function () {

   },
   moudleTest2: function () {

   }
}

现在moudleTest1,moudleTest2方法被放到了test对象里面,但是这样写暴露了test全部变量,同样也暴露了方法,甚至外面还可以改写你的方法!于是后面有出现了立即执行函数的写法

(function () {

    var a = 10;
    
    function moudleTest1() {

    }

    function moudleTest2() {

    }
    return {
        moudleTest1: moudleTest1,
        moudleTest2: moudleTest2
    }
})()

这样写已经不会有全部变量污染了,并且外面也访问不到函数内部的私有成员a,同时也向外暴露了moudleTest1,moudleTest2的方法,但是还是有问题,当这个模块依赖别的模块,或者比的模块要继承这个模块怎么办?不能传参数呢?那么后面就出现了jquery框架,我的模块需要依赖jq就出现了下面的写法

(function ($) {
  var a = 10;

  function moudleTest1() {
      //    $.....
  }

  function moudleTest2() {
      //    $.....
  }
  return {
      moudleTest1: moudleTest1,
      moudleTest2: moudleTest2
  }
})(jQuery);

现在看起来有点像模块了,解决了一些基本的问题,但是每个js文件都要写成立即执行函数,并且要严格保证文件引入的顺序,依赖的模块必须先执行!考虑到这些问题,后面又出现了 CommomJs,AMD,CMD,UMD等各种规范的模块化,下面我会一一讲述各种模块化之间的差异,不过说这些之前,先要了解下Js的异步加载几种方式!

3.Js异步加载的几种方式

说到js加载,我们就会想到浏览器的渲染机制,html文档从上往下执行的,正常情况下遇见js文件就会阻塞dom树的解析,那么H5就出现了两种异步加载的方式!

3.1 defer异步加载
<script type="text/javascript" src="./test1.js" defer ></script>
<script type="text/javascript" src="./test2.js" defer></script>
<script type="text/javascript" src="./test3.js" defer></script>

什么叫异步加载,上面有3个js文件,它会同时加载,加载不等于执行,要分清楚这两个概念,加载只是去把这个文件请求过来,这里3个文件会同时请求,具体谁先回来就要看文件大小和网络的速度了,script里面都有defer属性代表 3个文件同时加载,加载完成之后不会立刻执行,而是等到dom元素解析完成之前执行!并且是严格按照顺序执行的!

//test1.js
console.log("test1.js");

//test2.js
console.log("test2.js");

//test3.js
console.log("test3.js");



//defer.html
//onload事件 是所有标签 图片 样式 及脚本加载完成是触发
window.onload = function () {
    console.log("onload事件完成");
}

//DOMContentLoaded事件是所有DOM元素解析完成是触发(HTML文档被完全加载和解析)
//这个要IE9以上才兼容
document.addEventListener("DOMContentLoaded", function () {
    var span = document.querySelector("span");
    console.log("DOMConetentLoaded事件完成")
})

在这里插入图片描述
结论:虽然是异步加载,但是不会阻塞dom元素的解析,并且严格按照书写顺序执行!

3.2 async异步加载
<script type="text/javascript" src="./test1.js" async ></script>
<script type="text/javascript" src="./test2.js" async></script>
<script type="text/javascript" src="./test3.js" async></script>

上面三个js文件都有async 属性,async看字面意思都知道是异步了,所以这三个文件肯定是异步加载了,但是它们只要一加载完就会立刻执行,谁加载的快谁先执行,所以它会阻塞dom元素的解析,并且会有两种情况:

  1. 文件还没加载完成,dom元素已经解析完成了,所以文件在DOMContentLoaded事件之后执行
  2. 文件已经加载完成,dom元素还没解析完成,那么文件就会再DOMContentLoaded事件之前执行!

看第一次执行结果:
在这里插入图片描述
看第二次执行结果:
在这里插入图片描述

结论:虽然是异步加载,会阻塞dom元素的解析,并且执行顺序是无序的,谁先加载完谁先执行!有可能在DOMContentLoaded事件之前执行,也可能在DOMContentLoaded事件之后执行,但是一定在onload事件之前执行

4 .CommonJS

首先是基于node.js环境下的CommonJS规范,所以它在服务端得到了良好的支持,并且它是同步的,因为服务器端读取本地文件也是很快的,所以它被广泛应用于服务端,当然浏览器要加载 JS 文件,需要远程从服务器读取,而网络传输的效率远远低于node 环境中读取本地文件的效率。并且浏览器也不支持(因为缺少moudle, exports, require, global这几个变量),所以要用第三方工具webpack进行处理, CommonJS 是同步的,这会极大的降低运行性能。下面先说下CommonJS的用法,require引入,exports或者module.exports导出两者之间的区别!

//a.js
let a = {
   name: "张三",
   age: 18,
   getAge: function () {
       return this.age;
   }
};
moudle.exports = a;


//b.js
let a = require("./a.js");
let age = a.getAge() + 3;
let b = {
   name: "李四",
   age: age,
};
moudle.exports = b;

上面a.js,b.js都是单独模块,b模块里面依赖了a模块!并且当执行b模块 require("./a.js")时候才会去加载a模块,所以它是加载执行是同步的!并且CommonJS输出值是一个值的拷贝(浅拷贝)!

//a.js
let a = {
    name: "张三",
    age: 18,
    getAge: function () {
        return this.age;
    }
};
setTimeout(() => {
    a.age = 28;
}, 1000);
module.exports = a;


//b.js
let a = require('./a.js');
console.log(a.age); //18
setTimeout(() => {
   console.log(a.age); //18
   console.log(a.getAge())//28
}, 1000);

上面模块1秒钟之后改了a模块的age值为28,但是b模块1秒之后输出的还是之前的值18,值会被缓存。除非写成一个函数,才能得到内部变动后的值,因为CommonJS模块输出值是一个值的拷贝!而不是引用,es6输入的模块才是值的引入!之前说过CommonJS之所以在浏览器上不能执行,是因为少了moudle, exports, require, global这几个变量,下面来简单模拟下CommonJS的实现!

(function (modulesParameter) {

    //模块缓存用的
    let modules = {};

    let require = (moudleName) => {
        //先从缓存中读取
        if (modules.moudleName) {
            return modules.moudleName.exports;
        }
        //定义模块
        let singModule = {
            exports: {},
            name: moudleName,
            loaded: false
        }
        // 把moudles.exports引用给exports
        let exports = singModule.exports;
        modulesParameter[moudleName].apply(singModule.exports, [singModule, exports, require]);
        //加载完成
        singModule.loaded = true;
        return singModule.exports;
    }

    return require("./b.js");
})({
    "./a.js": (moudle, exports, require, global) => {
        let a = {
            name: "张三",
            age: 18,
            getAge: function () {
                return this.age;
            }
        };
        console.log(a);
        moudle.exports = a;
    },
    "./b.js": (moudle, exports, require, global) => {
        let a = require("./a.js");
        let age = a.getAge() + 3;
        let b = {
            name: "李四",
            age: age,
        };
        console.log(b);
        moudle.exports = b;
    }
})

结论:CommonJS服务于服务端,是一种同步的加载方式,不能直接用于浏览器端,需要通过babel转换成所有浏览器支持的 es5代码,并且CommonJS 模块输出的是一个值的拷贝,一旦输出一个值,模块内部的变化就影响不到这个值了 如果输出的是对象,改变其属性的话,外部引用的地方是会发生变化的,commonjs 一个文件就是一个模块,require 命令只会第一次执行加载该脚本,无论后面再require 多少次,都是从缓存里面取


5 .CMD

上面已经说了什么是异步加载,以及异步加载的几种方式!现在再来说下CMD规范,其实它很CommonJS写法很相似,但是它是异步加载的,并且它是严格按照顺序执行的,是不是有点像defer加载(虽然异步,但是按顺序执行)!有点类似懒加载,用的时候再去加载,用于浏览器端,基于CMD规范的框架有sea.js,用法和CommonJS类似,看下面代码!

//a.js
define(function (require, exports, module) {
    console.log('a模块加载');
    module.exports = {
        loaded: function () {
            console.log('a模块加载完成');
        }
    };
});
 
//b.js
define(function(require, exports, module) {
    console.log('b模块加载');
    module.exports = {
        loaded: function () {
            console.log('b模块加载完成');
        }
    };
});

//main.js
define(function (require, exports, module) {
    console.log('main模块加载');

    let a = require('./a');
    a.loaded();
    let b = require('./b');
    b.loaded();

    module.exports = {
        loaded: function () {
            console.log('main模块加载完成');
        }
    };
});

//app.js
seajs.use('./main.js', function(main) {
    main.loaded();
});

//index.html
<script data-main="./app.js" src="./require.js"></script>

之前commonJs不能在浏览器上直接使用,是因为少了变量,sea.js定义模块的时候就需要加这几个变量,所以它可以直接用在浏览器上!用法和commonJs相似就不多说了,看下面执行结果!
在这里插入图片描述
结论:CMD是服务于浏览器端,模块异步加载,但是严格按顺序执行,像H5的defer加载,CMD推崇依赖就近,延迟执行,所以一般不在define的参数中写依赖,在factory中写依赖,碰到reqiure才去执行依赖

6.AMD

AMD规范也是异步加载,和CMD有什么区别呢?CMD是按需异步加载,而AMD呢?它类似async无序加载,谁先加载谁先执行(具体看网络速度和文件大小),并且AMD是依赖前置,啥意思呢?被依赖的会被先执行!由于异步加载也常用于浏览器端!基于AMD规范的框架有require.js,看下面用法:

  1. 通过调用define()来注册工厂函数,而不是立即执行它。
  2. 将依赖项作为字符串值数组传递,不要获取全局变量。
  3. 仅在加载并执行所有依赖项后才执行工厂函数。
  4. 将依赖模块作为参数传递给工厂函数。

写法和之前稍有不同(依赖前置),看下面代码!

//a.js
define(function () {
    console.log('a模块加载');

    return {
        loaded: function () {
            console.log('a模块加载完成');
        }
    };
});

//b.js
define(function() {
    console.log('b模块加载');
    return {
        loaded: function () {
            console.log('b模块加载完成');
        }
    };
});

//main.js
define(["a", "b"], function (a, b) {
    console.log('main模块加载');

    a.loaded();
    b.loaded();

    return {
        loaded: function () {
            console.log('main模块加载完成');
        }
    };
});

//app.js
define(['main'], function(main) {
    main.loaded();
});

//index.html
 <script data-main="./app.js" src="./require.js"></script>

看下面第一次执行结果!
在这里插入图片描述
看下面第二次执行结果!
在这里插入图片描述

两次的执行结果不一样,说明模块是异步加载的,谁先加载完谁先执行!

其实require.js也可以用CMD的写法,虽然写法和commonJS类似,但是执行还是无序的,由于使用commonJS写法时,require检测依赖关系的机制是通过调用Function.prototype.toString(),将所需的依赖预先加载然后再能进行调用!

//a.js
define(function (require, exports, module) {
    console.log('a模块加载');

    module.exports = {
        loaded: function () {
            console.log('a模块加载完成');
        }
    };
    // 也可用下面简便的写法
    // return {
    //     loaded: function () {
    //         console.log('a模块加载完成');
    //     }
    // }
});

//b.js
define(function (require, exports, module) {
    console.log('b模块加载');

    module.exports = {
        loaded: function () {
            console.log('b模块加载完成');
        }
    };
    // 也可用下面简便的写法
    // return {
    //     loaded: function () {
    //         console.log('b模块加载完成');
    //     }
    // }
});

//main.js
define(function (require, exports, module) {
    console.log('main模块加载完成');

    let  a = require('a');
    a.loaded();
    let b = require('b');
    b.loaded();

    module.exports = {
        loaded: function () {
            console.log('main模块加载完成');
        }
    };
    // 也可用下面简便的写法
    // return {
    //     loaded: function () {
    //         console.log('main模块加载完成');
    //     }
    // }
});

//app.js
define(function(require, exports, module) {
    let main = require("./main");
    main.loaded();
});

//index.html
 <script data-main="./app.js" src="./require.js"></script>

看下面第一次执行结果!
在这里插入图片描述
看下面第二次执行结果!
在这里插入图片描述

下面我们也用个最简单的方法来实现下AMD

let modules = {};

function define(moduleName, deps, Fn) {
   if (deps && Fn) {
       deps.forEach((dep, i) => {
           deps[i] = modules[dep];
       });
       modules[moduleName] = Fn.apply(Fn, deps)
   } else {
       modules[moduleName] = deps();
   }
};

function require(moduleNames, Fn) {
   moduleNames.forEach((moduleName, i) => {
       moduleNames[i] = modules[moduleName];
   });
   Fn.apply(Fn, moduleNames);
}



define("a", function () {
   let a = {
       name: "张三",
       age: 18,
       getAge: function () {
           return this.age;
       }
   };
   console.log(a);
   return a;
})

define("b", ["a"], function (a) {
   let b = {
       name: "李四",
       age: a.getAge() + 2,
       getName: function () {
           return this.name;
       }
   };
   console.log(b);
   return b;
})



require(["b"], function (b) {
   console.log(b)
});

结论:AMD是服务于浏览器端,模块异步加载,谁先加载完谁就先执行(无序的),像H5的async加载,AMD推崇依赖前置的,所以必须提前在头部依赖参数部分写好,AMD也有兼容commonJS的写法,require('name')"同步"加载模块的形式,require会预先加载模块的所有依赖,因此在调用require('name')时,name模块已经提前加载好了;这种写法就类似commonJS语法,一般都是用require(['name1', 'name2'])这种依赖前置的写法,尽管AMD的设计理念很好,但与同步加载的模块标准相比其语法要更加冗长。另外其异步加载的方式并不如同步显得清晰,并且容易造成回调地狱,在目前的实际应用中已经用得越来越少,大多数开发者还是会选择CommonJS或ES6 Module的形式。


7.UMD

UMD是AMD和CommonJS的组合版本,AMD是异步加载模块,CommonJS 模块同步加载,它的模块无需包装,后来人们又想出另一个更通用的模式UMD (Universal Module Definition),希望解决跨平台的方案。

(function (window, factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
      // Node环境下CommonJS规范
        module.exports = factory;
    } else if (typeof define === 'function' && define.amd) {
       //说明是AMD规范
          define(function(){
              return factory
          });
    } else {
     //非模块化环境
        window.eventUtil = factory;
    }
})(this, function () {
    //module ...
});

UMD先判断是否支持Node.js的模块(exports)是否存在,在判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块

结论:严格来说,UMD并不能说是一种模块标准,不如说它是一组模块形式的集合更准确。UMD的全称是Universal Module Definition,也就是通用模块标准,它的目标是使一个模块能运行在各种环境下,不论是CommonJS、AMD,还是非模块化的环境(当时ES6 Module还未被提出)。

8.ES6

浏览器加载 ES6 模块,也使用< script >标签,但是要加入type="module"属性。

<script type="module" src="./a.js"></script>

浏览器对于带有type="module"的< script >都是异步加载,不会造成堵塞浏览器,等到整个页面渲染完,再执行模块脚本,等同于打开了< script >标签的defer属性。

<script type="module" src="./a.js" defer></script>

ES6模块引入是import命令,输入是export,是和上面所说的模块引入和输入是不一样的

import{a}from "./a.js"
export a

ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段,它是编译时输出接口,并且输出的是值的引用

//a.js
let a = {
    name: "张三",
    age: 18,
    getAge: function () {
        return this.age;
    }
};

setTimeout(() => {
    a.age = 28;
}, 1000);
export { a };

//b.js
import { a } from "./a.js"
console.log(a.age);//18
setTimeout(() => {
    console.log(a.age); //28
}, 1000);

//index.html
<script type="module" src="./b.js"></script>

看下面执行结果:
在这里插入图片描述
JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用(赋值会报错)。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值,由于import语句是在编译时,无法在运行时加载模块,如果import命令要取代 Node 的require方法,因为require是运行时加载模块,import命令无法取代require的动态加载功能,于是ES2020提案 引入import()函数,支持动态加载模块,import命令能够接受什么参数,import()函数就能接受什么参数,两者区别主要是后者为动态加载,并且import()返回一个 Promise 对象

//a.js
let age = 18;
export {age};

//b.js
let test = import("./a.js").then((data) => {
    console.log(data.age);//18
});
console.log(test);//Promise 

看下面执行结果
在这里插入图片描述

结论:ES6 import命令是编译时输出接口`,并且是动态引用,不会缓存值,模块里面的变量绑定其所在的模块,它的import()函数,也可以运行时加载模块!


9.总结

随着前端快速发展,需要使用javascript处理越来越多的事情,不在局限页面的交互,项目的需求越来越多,更多的逻辑需要在前端完成,所以前端也需要模块化思想,由于早期javascrip没有模块化,所以后来出现了 CommonJs.CMD,AMD,UMD,ES6,CommonJs是服务于服务端,是个同步加载方式,CMD,AMD是异步加载方式,UMD是为了兼容CommonJS、AMD,它并不属于一套模块规范,而是一种跨平台解决的方案,ES6也是异步加载的,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成,但是它的import()函数也可以做到和CommonJs require运行时加载,所以随着时间的变迁,我觉得ES6模块将成为最终的主流!

  • 62
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 120
    评论
评论 120
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值