RainZhai / rainzhai.github.com

宅鱼
http://rainzhai.github.io
Apache License 2.0
2 stars 0 forks source link

javascript模块化 #4

Open RainZhai opened 7 years ago

RainZhai commented 7 years ago

http://www.ruanyifeng.com/blog/2015/05/commonjs-in-browser.html 模块,又称构件,是能够单独命名并独立地完成一定功能的程序语句的集合(即程序代码和数据结构的集合体)。它具有两个基本的特征:外部特征和内部特征。外部特征是指模块跟外部环境联系的接口(即其他模块或程序调用该模块的方式,包括有输入输出参数、引用的全局变量)和模块的功能;内部特征是指模块的内部环境具有的特点(即该模块的局部数据和程序代码)。 我最喜欢的ES6 模块功能的特性是,导入是实时只读的。(CommonJS 只是相当于把导出的代码复制过来)。

什么是模块 一般来讲,模块是一个独立的JavaScript文件。模块文件可以包含一个类定义、一组相关的类、一个实用函数库的代码。 为什么要使用模块 模块化可以使你的代码低耦合,功能模块直接不相互影响。我个人认为模块化主要有以下几点好处:

1.可维护性:根据定义,每个模块都是独立的。良好设计的模块会尽量与外部的代码撇清关系,以便于独立对其进行改进和维护。

2.命名空间:在JavaScript中,最高级别的函数外定义的变量都是全局变量(这意味着所有人都可以访问到它们)。也正因如此,当一些无关的代码碰巧使用到同名变量的时候,我们就会遇到“命名空间污染”的问题。

3.可复用性

如何引入模块 模块模式 模块模式一般用来模拟类的概念(因为原生JavaScript并不支持类,虽然最新的ES6里引入了Class不过还不普及)这样我们就能把公有和私有方法还有变量存储在一个对象中。这样我们就能在公开调用API的同时,仍然在一个闭包范围内封装私有变量和方法。

实现模块模式的方法有很多种,下面的例子是通过匿名闭包函数的方法。(在JavaScript中,函数是创建作用域的唯一方式。)

例1:匿名闭包函数

(function () {
  // 在函数的作用域中下面的变量是私有的
 var temp= 1223;
 function(){
   temp2 = 45+temp;
 }
}());

通过这种构造,我们的匿名函数有了自己的作用域或“闭包”。 这允许我们从父(全局)命名空间隐藏变量。 这种方法的好处在于,你可以在函数内部使用局部变量,而不会意外覆盖同名全局变量,但仍然能够访问到全局变量。

例2:全局引入

另一种比较受欢迎的方法是一些诸如jQuery的库使用的全局引入。和我们刚才举例的匿名闭包函数很相似,只是传入全局变量的方法不同:

(function (globalVariable) {
  globalVariable.filter = function(collection, test) {
     ...
  }; 
 }(globalVariable));

在这个例子中,globalVariable 是唯一的全局变量。这种方法的好处是可以预先声明好全局变量,让你的代码更加清晰可读。

例3:对象接口

像下面这样,还有一种创建模块的方法是使用独立的对象接口:

var myObj= (function () {
  // 在函数的作用域中下面的变量是私有的
  var myList= [93, 95];

  // 通过接口在外部访问下列方法
  // 与此同时这些方法又都在函数内部
  return {
    average: function() {
      var total = myList.reduce(function(accumulator, item) {
        return accumulator + item;
        }, 0);
      return total / myGrades.length;
    }
  }
})();
myGradesCalculate.average();

例4:揭示模块模式 Revealing module pattern 这和我们之前的实现方法非常相近,除了它会确保,在所有的变量和方法暴露之前都会保持私有.

CommonJS

CommonJS 扩展了JavaScript声明模块的API.

CommonJS模块可以很方便得将某个对象导出,让他们能够被其他模块通过 require 语句来引入。要是你写过 Node.js 应该很熟悉这些语法。

通过CommonJS,每个JS文件独立地存储它模块的内容(就像一个被括起来的闭包一样)。在这种作用域中,我们通过 module.exports 语句来导出对象为模块,再通过 require 语句来引入。

直观的例子:

function myModule() {
  this.hello = function() {
    return 'hello!';
  } 
}
module.exports = myModule;

通过指定导出的对象名称,CommonJS模块系统可以识别在其他文件引入这个模块时应该如何解释。

然后在某个人想要调用 myMoudle 的时候,只需要 require 一下:

var myModule = require('myModule');
var myModuleInstance = new myModule();
myModuleInstance.hello(); // 'hello!' 

这种实现比起模块模式有两点好处:

需要注意的一点是,CommonJS以服务器优先的方式来同步载入模块,假使我们引入三个模块的话,他们会一个个地被载入。

它在服务器端用起来很爽,可是在浏览器里就不会那么高效了。毕竟读取网络的文件要比本地耗费更多时间。只要它还在读取模块,浏览器载入的页面就会一直卡着不动。

AMD CommonJS已经挺不错了,但假使我们想要实现异步加载模块该怎么办?答案就是Asynchronous Module Definition(异步模块定义规范),简称AMD.

通过AMD载入模块的代码一般这么写:

define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
  console.log(myModule.hello());
});

这里我们使用 define 方法,第一个参数是依赖的模块,这些模块都会在后台无阻塞地加载,第二个参数则作为加载完毕的回调函数。

回调函数将会使用载入的模块作为参数。在这个例子里就是 myMoudle 和 myOtherModule.最后,这些模块本身也需要通过 define 关键词来定义。

拿 myModule 来举个例子:

define([], function() {
  return {
    hello: function() {
      console.log('hello');
    }
  };
});

重申一下,不像CommonJS,AMD是优先浏览器的一种异步载入模块的解决方案。

除了异步加载以外,AMD的另一个优点是你可以在模块里使用对象、函数、构造函数、字符串、JSON或者别的数据类型,而CommonJS只支持对象。

再补充一点,AMD不支持Node里的一些诸如 IO,文件系统等其他服务器端的功能。另外语法上写起来也比CommonJS麻烦一些。

UMD

RainZhai commented 7 years ago

打包 CommonJS 在这个示例中,我们只有一个名为 myDependency 的模块依赖。通过下面的命令,Browserify会依次把main.js里引入的所有模块一同打包到一个名为 bundle.js 的文件里: browserify main.js -o bundle.js Browserify 首先会通过抽象语法树(AST)来解析代码中的每一个 require 语句,在分析完所有模块的依赖和结构之后,就会把所有的代码合并到一个文件中。然后你在HTML文件里引入一个bundle.js就够啦。 多个文件和多个依赖也只需要再稍微配置一下就能正常工作了。 之后你也可以使用工具来压缩代码。 打包 AMD 假若你使用的是AMD,你会需要一些例如RequireJS 或 Curl的AMD加载器。模块化加载工具可以在你的应用中按需加载模块代码。

模块加载不影响后续语句执行,逐步加载的的模块也不会导致页面阻塞无法响应。

不过在实际应用中,为了避免用户过多的请求对服务器造成压力。用RequireJS optimizer, r.js一类的构建工具来合并和压缩AMD的模块。

Webpack Webpack 是新推出的构建工具里最受欢迎的。它兼容CommonJS, AMD, ES6各类规范。 Webpack 提供许多例如 code splitting(代码分割) 的有用功能,它可以把你的代码分割成一个个的 chunk 然后按需加载优化性能。

举个例子,要是你的Web应用中的一些代码只在很少的情况下才会被用到,把它们全都打包到一个文件里是很低效的做法。所以我们就需要 code splitting 这样的功能来实现按需加载。而不是把那些很少人才会用到的代码一股脑儿全都下载到客户端去。

code splitting 只是 Webpack 提供的众多强大功能之一。

ES6 模块 ES6模块和CommonJS, AMD一类规范最主要的区别是,当你载入一个模块时,载入的操作实际实在编译时执行的——也就是在代码执行之前。所以去掉那些不必要的exports导出语句可以优化我们应用的性能。

有一个经常会被问到的问题:去除exports和冗余代码消除(UglifyJS一类工具执行后的效果)之间有什么区别? 让ES6模块与冗余代码消除(Dead code elimination)不同的是一种叫做tree shaking的技术。Tree shaking其实恰好是冗余代码消除的反向操作。它只加载你需要调用的代码,而不是删掉不会被执行的代码。我们还是用一个具体的例子说明吧: 假设我们有如下一个使用ES6语法,名为 utils.js 的函数:

export function each(collection, iterator) {
  ...
 }

export function filter(collection, test) {
  ...
}

export function map(collection, iterator) {
  ...
}

export function reduce(collection, iterator, accumulator) {
    ...
}

现在我们也不清楚到底需要这个函数的哪些功能,所以先全部引入到 main.js 中:

//main.js
import * as Utils from './utils.js';

之后我们再调用一下 each 函数:

//main.js
import * as Utils from './utils.js';
Utils.each([1, 2, 3], function(x) { console.log(x) });

通过 "tree shaken" 之后的 main.js 看起来就像下面这样:

//treeshake.js 
function each(collection, iterator) {
...
 };
each([1, 2, 3], function(x) { console.log(x) });

注意到这里只导出了我们调用过的 each 方法。

构建ES6模块 因为浏览器对ES6模块的原生支持还不够完善,所以现阶段还需要我们做一些补充工作。 让ES6模块在浏览器中顺利运行的常用方法有以下几种:

1.使用语法编译器(Babel或Traceur)来把ES6语法的代码编译成ES5或者CommonJS, AMD, UMD等其他形式。然后再通过Browserify 或 Webpack 一类的构建工具来进行构建。

2.使用Rollup.js,这其实和上面差不多,只是Rollup还会捎带的利用“tree shaking”技术来优化你的代码。在构建ES6模块时Rollup优于Browserify或Webpack的也正是这一点,它打包出来的文件体积会更小。Rollup也可以把你的代码转换成包括ES6, CommonJS, AMD, UMD, IIFE在内的各种格式。其中IIFE和UMD可以直接在浏览器里运行,AMD, CommonJS, ES6等还需要你通过Browserify, Webpack, RequireJS一类的工具才能在浏览器中使用。 es6-module-loader