irpas技术客

一篇文章带你玩转前端所有模块化_世态炎凉!!

未知 6254

文章目录 1.前言2.模块化历程3.Js异步加载的几种方式3.1 defer异步加载3.2 async异步加载 4 .CommonJS5 .CMD6.AMD7.UMD8.ES69.总结

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元素的解析,并且会有两种情况:

文件还没加载完成,dom元素已经解析完成了,所以文件在DOMContentLoaded事件之后执行文件已经加载完成,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,看下面用法:

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

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

//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模块将成为最终的主流!


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #一篇文章带你玩转前端所有模块化 #srcquot