44
55当年 Brendan Eich 草创 JavaScript 之际,他应该无法想象 JavaScript 在未来二十年内发挥的巨大作用;同样作为广为诟病的过于随意的语言,缺乏强有力的模块化解决方案一直是 JavaScript 的缺陷之一。早期的 JavaScript 往往作为嵌入到 HTML 页面中的用于控制动画与简单的用户交互的脚本语言,我们习惯于将其直接嵌入到 ` script ` 标签中:
66
7- ```
7+ ``` html
88<!-- html-->
99<script type =" application/javascript" >
10- // module1 code
11- // module2 code
10+ // module1 code
11+ // module2 code
1212 </script >
1313```
1414
1515不过随着单页应用与富客户端的流行,不断增长的代码库也急需合理的代码分割与依赖管理的解决方案,这也就是我们在软件工程领域所熟悉的模块化(Modularity)。所谓模块化主要是解决代码分割、作用域隔离、模块之间的依赖管理以及发布到生产环境时的自动化打包与处理等多个方面。二十年间流行过的 JavaScript 模块化解决方案包括但不限于直接声明依赖(Directly Defined Dependences)、命名空间(Namespace Pattern)、模块模式(Module Pattern)、依赖分离定义(Detached Dependency Definitions)、沙盒(Sandbox)、依赖注入(Dependency Injection)、CommonJS、AMD、UMD、标签化模块(Labeled Modules)、YModules、ES 2015 Modules。在早期的 Web 开发中,所有的嵌入到网页内的 JavaScript 对象都会使用全局的 `window` 对象来存放未使用 `var` 定义的变量。大概在上世纪末,JavaScript 多用于解决简单的任务,这也就意味着我们只需编写少量的 JavaScript 代码;不过随着代码库的线性增长,我们首先会碰到的就是所谓命名冲突(Name Collisions)困境:
1616
17- ``` // file greeting.js
17+ ``` js
18+ // file greeting.js
1819var helloInLang = {
19- en: ' Hello world!' ,
20- es: ' ¡Hola mundo!' ,
21- ru: ' Привет мир!'
20+ en: ' Hello world!' ,
21+ es: ' ¡Hola mundo!' ,
22+ ru: ' Привет мир!'
2223};
2324
2425function writeHello (lang ) {
25- document .write (helloInLang[lang]);
26+ document .write (helloInLang[lang]);
2627}
2728
2829// file hello.js
2930function writeHello () {
30- document .write (' The script is broken' );
31+ document .write (' The script is broken' );
3132}
3233```
3334
3435当我们在页面内同时引入这两个 JavaScript 脚本文件时,显而易见两个文件中定义的 ` writeHello ` 函数起了冲突,最后调用的函数取决于我们引入的先后顺序。此外在大型应用中,我们不可能将所有的代码写入到单个 JavaScript 文件中;我们也不可能手动地在 HTML 文件中引入全部的脚本文件,特别是此时还存在着模块间依赖的问题,相信很多开发者都会遇到 ` jQuery ` 尚未定义这样的问题。不过物极必反,过度碎片化的模块同样会带来性能的损耗与包体尺寸的增大,这包括了模块加载、模块解析、因为 Webpack 等打包工具包裹模块时封装的过多 IIFE 函数导致的 JavaScript 引擎优化失败等。譬如我们的源码如下:
3536
36- ```
37+ ``` js
3738// index.js
38- var total = 0
39- total += require('./module_0')
40- total += require('./module_1')
41- total += require('./module_2')
39+ var total = 0 ;
40+ total += require (' ./module_0' );
41+ total += require (' ./module_1' );
42+ total += require (' ./module_2' );
4243// etc.
43- console.log(total)
44-
44+ console .log (total);
4545
4646// module_0.js
47- module.exports = 0
48-
47+ module .exports = 0 ;
4948
5049// module_1.js
51- module.exports = 1
50+ module .exports = 1 ;
5251```
5352
5453经过 Browserify 打包之后的代码变成了如下式样:
5554
56- ```
55+ ``` js
5756(function e (t ,n ,r ){function s (o ,u ){if (! n[o]){if (! t[o]){var a= typeof require== " function" && require;if (! u&& a)return a (o,! 0 );if (i)return i (o,! 0 );var f= new Error (" Cannot find module '" + o+ " '" );throw f .code = " MODULE_NOT_FOUND" ,f}var l= n[o]= {exports: {}};t[o][0 ].call (l .exports ,function (e ){var n= t[o][1 ][e];return s (n? n: e)},l,l .exports ,e,t,n,r)}return n[o].exports }var i= typeof require== " function" && require;for (var o= 0 ;o< r .length ;o++ )s (r[o]);return s})({1 : [function (require ,module ,exports ){
5857module .exports = 0
5958},{}],2 : [function (require ,module ,exports ){
@@ -72,22 +71,20 @@ module.exports = 100
7271
7372命名空间模式始于 2002 年,顾名思义我们可以使用特殊的约定命名。譬如我们可以为某个模块内的变量统一添加 ` myApp_` 前缀,譬如 ` myApp_address` ,` myApp_validateUser ()` 等等。同样,我们也可以将函数赋值给模块内的变量或者对象的属性,从而可以使得可以像 ` document .write ()` 这样在子命名空间下定义函数而避免冲突。首个采样该设计模式的界面库当属 Bindows,其是 Erik Arvidsson 创建于 2002 年。他没有简单地为自定义函数或者对象添加命名前缀,而是将所有的 Bindows 当中的数据与逻辑代码封装于某个全局对象内,从而避免所谓的全局作用域污染。命名空间模式的设计思想如下所示:
7473
75- ```
74+ ` ` ` js
7675// file app.js
7776var app = {};
7877
79-
8078// file greeting.js
8179app .helloInLang = {
82- en: 'Hello world!',
83- es: '¡Hola mundo!',
84- ru: 'Привет мир!'
80+ en: ' Hello world!' ,
81+ es: ' ¡Hola mundo!' ,
82+ ru: ' Привет мир!'
8583};
8684
87-
8885// file hello.js
89- app.writeHello = function (lang) {
90- document.write(app.helloInLang[lang]);
86+ app .writeHello = function (lang ) {
87+ document .write (app .helloInLang [lang]);
9188};
9289` ` `
9390
@@ -126,12 +123,12 @@ APP.module3 = new module3({ a: 42 });
126123
127124那么在引入的时候我们需要手动地按照模块间依赖顺序引入进来:
128125
129- ```
126+ ` ` ` html
130127<!-- html-->
131- <script type="application/javascript" src="PATH/polyfill-vendor.js" ></script>
132- <script type="application/javascript" src="PATH/module1.js" ></script>
133- <script type="application/javascript" src="PATH/module2.js" ></script>
134- <script type="application/javascript" src="PATH/app.js" ></script>
128+ < script type= " application/javascript" src= " PATH/polyfill-vendor.js" >< / script>
129+ < script type= " application/javascript" src= " PATH/module1.js" >< / script>
130+ < script type= " application/javascript" src= " PATH/module2.js" >< / script>
131+ < script type= " application/javascript" src= " PATH/app.js" >< / script>
135132` ` `
136133
137134不过这种方式对于模块间通信也是个麻烦。命名空间模式算是如今 JavaScript 领域最为著名的模式之一,而在 Bindows 之后 Dojo(2005),YUI(2005) 这些优秀的界面框架也是承袭了这种思想。
@@ -140,28 +137,28 @@ APP.module3 = new module3({ a: 42 });
140137
141138Martin Fowler 于 2004 年提出了依赖注入([Dependency Injection](https://martinfowler.com/articles/injection.html))的概念,其主要用于 Java 中的组件内通信;以 Spring 为代表的一系列支持依赖注入与控制反转的框架将这种设计模式发扬光大,并且成为了 Java 服务端开发的标准模式之一。依赖注入的核心思想在于某个模块不需要手动地初始化某个依赖对象,而只需要声明该依赖并由外部框架自动实例化该对象实现并且传递到模块内。而五年之后的 2009 年 [Misko Hevery](https://github.com/mhevery) 开始设计新的 JavaScript 框架,并且使用了依赖注入作为其组件间通信的核心机制。这个框架就是引领一时风骚,甚至于说是现代 Web 开发先驱之一的 Angular。Angular 允许我们定义模块,并且在显式地声明其依赖模块而由框架完成自动注入。其核心思想如下所示:
142139
143- ```
140+ ` ` ` js
144141// file greeting.js
145- angular.module('greeter', [])
146- .value('greeting', {
147- helloInLang: {
148- en: 'Hello world!',
149- es: '¡Hola mundo!',
150- ru: 'Привет мир!'
151- },
152-
153-
154- sayHello: function(lang) {
155- return this.helloInLang[lang];
156- }
157- });
142+ angular .module (' greeter' , []).value (' greeting' , {
143+ helloInLang: {
144+ en: ' Hello world!' ,
145+ es: ' ¡Hola mundo!' ,
146+ ru: ' Привет мир!'
147+ },
158148
149+ sayHello : function (lang ) {
150+ return this .helloInLang [lang];
151+ }
152+ });
159153
160154// file app.js
161- angular.module('app', ['greeter'])
162- .controller('GreetingController', ['$scope', 'greeting', function($scope, greeting) {
163- $scope.phrase = greeting.sayHello('en');
164- }]);
155+ angular .module (' app' , [' greeter' ]).controller (' GreetingController' , [
156+ ' $scope' ,
157+ ' greeting' ,
158+ function ($scope , greeting ) {
159+ $scope .phrase = greeting .sayHello (' en' );
160+ }
161+ ]);
165162` ` `
166163
167164之后在 [Angular 2](https://github.com/angular/angular) 与 [Slot](https://github.com/2gis/slot) 之中依赖注入仍是核心机制之一,这也是 Angular 一系的更多的被视为大而全的框架而不是小而美的库的原因之一。
@@ -290,27 +287,28 @@ hello.js 作为整个应用的入口模块,我们使用 `define` 关键字声
290287
291288AMD 与 CommonJS 虽然师出同源,但还是分道扬镳,关注于代码异步加载与最小化入口模块的开发者将目光投注于 AMD;而随着 Node.js 以及 Browserify 的流行,越来越多的开发者也接受了 CommonJS 规范。令人扼腕叹息的是,符合 AMD 规范的模块并不能直接运行于实践了 CommonJS 模块规范的环境中,符合 CommonJS 规范的模块也不能由 AMD 进行异步加载,整个 JavaScript 生态圈貌似分崩离析。2011 年中,UMD,也就是 Universal Module Definition 规范正是为了弥合这种不一致性应运而出,其允许在环境中同时使用 AMD 与 CommonJS 规范。[Q](https://github.com/kriskowal/q) 算是 UMD 的首个规范实现,其能同时运行于浏览器环境(以脚本标签形式嵌入)与服务端的 Node.js 或者 Narwhal(CommonJS 模块)环境中;稍后,James 也为 Q 添加了对于 AMD 的支持。我们将上述例子中的 greeting.js 改写为同时支持 CommonJS 与 AMD 规范的模块:
292289
293- ```
290+ ` ` ` js
294291(function (define ) {
295- define(function () {
296- var helloInLang = {
297- en: 'Hello world!',
298- es: '¡Hola mundo!',
299- ru: 'Привет мир!'
300- };
301-
302-
303- return {
304- sayHello: function (lang) {
305- return helloInLang[lang];
306- }
307- };
308- });
309- }(
310- typeof module === 'object' && module.exports && typeof define !== 'function' ?
311- function (factory) { module.exports = factory(); } :
312- define
313- ));
292+ define (function () {
293+ var helloInLang = {
294+ en: ' Hello world!' ,
295+ es: ' ¡Hola mundo!' ,
296+ ru: ' Привет мир!'
297+ };
298+
299+ return {
300+ sayHello : function (lang ) {
301+ return helloInLang[lang];
302+ }
303+ };
304+ });
305+ })(
306+ typeof module === ' object' && module .exports && typeof define !== ' function'
307+ ? function (factory ) {
308+ module .exports = factory ();
309+ }
310+ : define
311+ );
314312` ` `
315313
316314该模式的核心思想在于所谓的 IIFE(Immediately Invoked Function Expression),该函数会根据环境来判断需要的参数类别,譬如在 CommonJS 环境下上述代码会以如下方式执行:
0 commit comments