摘要:参考精读模块化发展模块化七日谈前端模块化开发那点历史本文先发布于我的个人博客模块化开发的演进历程,后续如有更新,可以查看原文。
Brendan Eich用了10天就创造了JavaScript,因为当时的需求定位,导致了在设计之初,在语言层就不包含很多高级语言的特性,其中就包括模块这个特性,但是经过了这么多年的发展,如今对JavaScript的需求已经远远超出了Brendan Eich的预期,其中模块化开发更是其中最大的需求之一。
尤其是2009年Node.js出现以后,CommonJS规范的落地极大的推动了整个社区的模块化开发氛围,并且随之出现了AMD、CMD、UMD等等一系列可以在浏览器等终端实现的异步加载的模块化方案。
此前,虽然自己也一直在推进模块化开发,但是没有深入了解过模块化演进的历史,直到最近看到了一篇文章《精读JS模块化发展》,文章总结了History of JavaScript这个开源项目中关于JavaScript模块化演进的部分,细读几次之后,对于一些以前模棱两可的东西,顿时清晰了不少,下面就以时间线总结一下自己的理解:
在1999年的时候,绝大部分工程师做JS开发的时候就直接将变量定义在全局,做的好一些的或许会做一些文件目录规划,将资源归类整理,这种方式被称为直接定义依赖,举个例子:
// greeting.js var helloInLang = { en: "Hello world!", es: "¡Hola mundo!", ru: "Привет мир!" }; function writeHello(lang) { document.write(helloInLang[lang]); } // third_party_script.js function writeHello() { document.write("The script is broken"); } // index.htmlBasic example
但是,即使有规范的目录结构,也不能避免由此而产生的大量全局变量,这就导致了一不小心就会有变量冲突的问题,就好比上面这个例子中的writeHello。
于是在2002年左右,有人提出了命名空间模式的思路,用于解决遍地的全局变量,将需要定义的部分归属到一个对象的属性上,简单修改上面的例子,就能实现这种模式:
// greeting.js var app = {}; app.helloInLang = { en: "Hello world!", es: "¡Hola mundo!", ru: "Привет мир!" }; app.writeHello = function (lang) { document.write(helloInLang[lang]); } // third_party_script.js function writeHello() { document.write("The script is broken"); }
不过这种方式,毫无隐私可言,本质上就是全局对象,谁都可以来访问并且操作,一点都不安全。
所以在2003年左右就有人提出利用IIFE结合Closures特性,以此解决私有变量的问题,这种模式被称为闭包模块化模式:
// greeting.js var greeting = (function() { var module = {}; var helloInLang = { en: "Hello world!", es: "¡Hola mundo!", ru: "Привет мир!", }; module.getHello = function(lang) { return helloInLang[lang]; }; module.writeHello = function(lang) { document.write(module.getHello(lang)); }; return module; })();
IIFE可以形成一个独立的作用域,其中声明的变量,仅在该作用域下,从而达到实现私有变量的目的,就如上面例子中的helloInLang,在该IIFE外是不能直接访问和操作的,可以通过暴露一些方法来访问和操作,比如说上面例子里面的getHello和writeHello2个方法,这就是所谓的Closures。
同时,不同模块之间的引用也可以通过参数的形式来传递:
// x.js // @require greeting.js var x = (function(greeting) { var module = {}; module.writeHello = function(lang) { document.write(greeting.getHello(lang)); }; return module; })(greeting);
此外使用IIFE,还有2个好处:
提高性能:通过IIFE的参数传递常用全局对象window、document,在作用域内引用这些全局对象。JavaScript解释器首先在作用域内查找属性,然后一直沿着链向上查找,直到全局范围,因此将全局对象放在IIFE作用域内可以提升js解释器的查找速度和性能;
压缩空间:通过参数传递全局对象,压缩时可以将这些全局对象匿名为一个更加精简的变量名;
在那个年代,除了这种解决思路以外,还有通过其它语言的协助来完成模块化的解决思路,比如说模版依赖定义、注释依赖定义、外部依赖定义等等,不过不常见,所以就不细说了,究其本源,它们想最终实现的方式都差不多。
不过,这些方案,虽然解决了依赖关系的问题,但是没有解决如何管理这些模块,或者说在使用时清晰描述出依赖关系,这点还是没有被解决,可以说是少了一个管理者。
没有管理者的时候,在实际项目中,得手动管理第三方的库和项目封装的模块,就像下面这样把所有需要的JS文件一个个按照依赖的顺序加载进来:
如果页面中使用的模块数量越来越多,恐怕再有经验的工程师也很难维护好它们之间的依赖关系了。
于是如LABjs之类的加载工具就横空出世了,通过使用它的API,动态创建,从而达到控制JS文件加载以及执行顺序的目的,在一定的程度上解决了依赖关系,例如:
$LAB.script("greeting.js").wait() .script("x.js") .script("y.js").wait() .script("run.js");
不过LABjs之类的加载工具是建立在以文件为单位的基础之上的,但是JS中的模块又不一定必须是文件,同一个文件中可以声明多个模块,YUI作为昔日前端领域的佼佼者,很好的糅合了命名空间模式及沙箱模式,下面来一睹它的风采:
// YUI - 编写模块 YUI.add("dom", function(Y) { Y.DOM = { ... } }) // YUI - 使用模块 YUI().use("dom", function(Y) { Y.DOM.doSomeThing(); // use some methods DOM attach to Y }) // hello.js YUI.add("hello", function(Y){ Y.sayHello = function(msg){ Y.DOM.set(el, "innerHTML", "Hello!"); } },"3.0.0",{ requires:["dom"] }) // main.js YUI().use("hello", function(Y){ Y.sayHello("hey yui loader"); })
此外,YUI团队还提供的一系列用于JS压缩、混淆、请求合并(合并资源需要server端配合)等性能优化的工具,说其是现有JS模块化的鼻祖一点都不过分。
不过,随着Node.js的到来,CommonJS规范的落地以及各种前端工具、解决方案的出现,很快,YUI3就被湮没在了历史的长流里面,这样成为了JS模块化开发的一个分水岭,引用一段描述:
从 1999 年开始,模块化探索都是基于语言层面的优化,真正的革命从 2009 年 CommonJS 的引入开始,前端开始大量使用预编译。
CommonJS是一套同步的方案,它考虑的是在服务端运行的Node.js,主要是通过require来加载依赖项,通过exports或者module.exports来暴露接口或者数据的方式,想了解更多,可以看一下《CommonJS规范》,下面举个简单的例子:
var math = require("math"); esports.result = math.add(2,3); // 5
由于服务器上通过require加载资源是直接读取文件的,因此中间所需的时间可以忽略不计,但是在浏览器这种需要依赖HTTP获取资源的就不行了,资源的获取所需的时间不确定,这就导致必须使用异步机制,代表主要有2个:
基于 AMD 的RequireJS
基于 CMD 的SeaJS
它们分别在浏览器实现了define、require及module的核心功能,虽然两者的目标是一致的,但是实现的方式或者说是思路,还是有些区别的,AMD偏向于依赖前置,CMD偏向于用到时才运行的思路,从而导致了依赖项的加载和运行时间点会不同,关于这2者的比较,网上有很多了,这里推荐几篇仅供参考:
《SeaJS 和 RequireJS 的异同》
《再谈 SeaJS 与 RequireJS 的差异》
本人就先接触了SeaJS后转到RequireJS,虽然感觉AMD的模式写确实没有CMD这么符合一惯的语义逻辑,但是写了几个模块以后就习惯了,而且社区资源比较丰富的AMD阵营更加符合当时的项目需求(扯多了),下面分别写个例子做下直观的对比:
// CMD define(function (require) { var a = require("./a"); // <- 运行到此处才开始加载并运行模块a var b = require("./b"); // <- 运行到此处才开始加载并运行模块b // more code .. })
// AMD define( ["./a", "./b"], // <- 前置声明,也就是在主体运行前就已经加载并运行了模块a和模块b function (a, b) { // more code .. } )
通过例子,你可以看到除了语法上面的区别,这2者主要的差异还是在于:
何时加载和运行依赖项?
这也是CommonJS社区中质疑AMD最主要原因之一,不少人认为它破坏了规范,反观CMD模式,简单的去除define的外包装,这就是标准的CommonJS实现,所以说CMD是最贴近CommonJS的异步模块化方案,不过孰优孰劣,这里就不扯了,需求决定一切。
此外同一时期还出现了一个UMD的方案,其实它就是AMD与CommonJS的集合体,通过IIFE的前置条件判断,使一个模块既可以在浏览器运行,也可以在Node.JS中运行,举个例子:
// UMD (function(define) { define(function () { var helloInLang = { en: "Hello world!", es: "¡Hola mundo!", ru: "Привет мир!" }; return { sayHello: function (lang) { return helloInLang[lang]; } }; }); }( typeof module === "object" && module.exports && typeof define !== "function" ? function (factory) { module.exports = factory(); } : define ));
个人觉得最少用到的就是这个UMD模式了。
2015年6月,ECMAScript2015也就是ES6发布了,JavaScript终于在语言标准的层面上,实现了模块功能,使得在编译时就能确定模块的依赖关系,以及其输入和输出的变量,不像 CommonJS、AMD之类的需要在运行时才能确定(例如FIS这样的工具只能预处理依赖关系,本质上还是运行时解析),成为浏览器和服务器通用的模块解决方案。
// lib/greeting.js const helloInLang = { en: "Hello world!", es: "¡Hola mundo!", ru: "Привет мир!" }; export const getHello = (lang) => ( helloInLang[lang]; ); export const sayHello = (lang) => { console.log(getHello(lang)); }; // hello.js import { sayHello } from "./lib/greeting"; sayHello("ru");
与CommonJS用require()方法加载模块不同,在ES6中,import命令可以具体指定加载模块中用export命令暴露的接口(不指定具体的接口,默认加载export default),没有指定的是不会加载的,因此会在编译时就完成模块的加载,这种加载方式称为编译时加载或者静态加载。
而CommonJS的require()方法是在运行时才加载的:
// lib/greeting.js const helloInLang = { en: "Hello world!", es: "¡Hola mundo!", ru: "Привет мир!" }; const getHello = function (lang) { return helloInLang[lang]; }; exports.getHello = getHello; exports.sayHello = function (lang) { console.log(getHello(lang)) }; // hello.js const sayHello = require("./lib/greeting").sayHello; sayHello("ru");
可以看出,CommonJS中是将整个模块作为一个对象引入,然后再获取这个对象上的某个属性。
因此ES6的编译时加载,在效率上面会提高不少,此外,还会带来一些其它的好处,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
可惜的是,目前浏览器和Node.js的支持程度都并不理想,截止发稿,也就只有 Chrome61+ 与 Safari10.1+ 才做到了部分支持。
不过可以通过Babel这类工具配合相关的plugin(可以参考《Babel笔记》),转换为ES5的语法,这样就可以在Node.js运行起来了,如果想在浏览器上运行,可以添加Babel配置,为模块文件添上AMD的define函数作为外层,再并配合RequireJS之类的加载器即可。
更多关于ES6 Modules的资料,可以看一下《ECMAScript 6 入门 - Module 的语法》。
参考精读 js 模块化发展
History of JavaScript
JavaScript 模块化七日谈
JavaScript Module Pattern: In-Depth
前端模块化开发那点历史
JavaScript Modules: A Beginner’s Guide
本文先发布于我的个人博客《JavaScript模块化开发的演进历程》,后续如有更新,可以查看原文。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/88457.html
摘要:服务提供方对外发布服务,服务需求方调用服务提供方所发布的服务。应用服务器通过统一数据访问模块访问各种数据,减轻应用程序管理诸多数据源的麻烦。 原文地址:https://blog.coding.net/blog/General-architecture-for-Java-applications 当我们架设一个系统的时候通常需要考虑到如何与其他系统交互,所以我们首先需要知道各种系统之间是...
摘要:月号,杭州和联合主办的第八期技术分享会,在公司如期举行。张伟林,宋小菜资深前端开发工程师,年,霹雳迷,已手残的纸牌魔术师,喜欢神奇的东西,技术栈从上向下不断横向纵向贯穿,目前在寻找前后端大一统思想的路上越走越偏。 showImg(https://segmentfault.com/img/bVbkWN4?w=3000&h=1686); 12 月 9 号,杭州 NodeParty 和 Ro...
摘要:月日,杭州站圆满收场。第二位嘉宾阿里巴巴移动安全专家何星宇,业界知名白帽子,本次的分享主题移动开发者所必须关注的安全那些事。杭州站分享已经结束,非常感谢大家的参与。 showImg(https://segmentfault.com/img/bVqWqG); 11 月 14 日,SegmentFault D-Day 杭州站圆满收场。虽然这次『云』议题比较高深,但丝毫没有影响到爱挑战的小伙...
摘要:阿里巴巴的共享服务理念以及企业级互联网架构建设的思路,给这些企业带来了不少新的思路,这也是我最终决定写这本书的最主要原因。尽在双阿里巴巴技术演进与超越是迄今唯一由阿里巴巴集团官方出品全面阐述双八年以来在技术和商业上演进和创新历程的书籍。 showImg(https://segmentfault.com/img/remote/1460000015386860); 1、大型网站技术架构:核...
阅读 967·2023-04-25 14:45
阅读 2715·2021-09-30 09:59
阅读 3079·2021-09-22 15:48
阅读 2389·2019-08-30 15:55
阅读 3381·2019-08-30 15:44
阅读 503·2019-08-29 14:07
阅读 3387·2019-08-26 13:45
阅读 516·2019-08-26 11:31