前端模块化(commonJs、ES6模块)


参考文献
前端模块化:CommonJS,AMD,CMD,ES6
ES6 模块与 CommonJS 模块的差异
前端模块化,AMD与CMD的区别,包括了模块的发展历程

一、引言

模块化的开发方式可以提高代码复用率,方便进行代码的管理。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。目前流行的js模块化规范有CommonJS、AMD、CMD以及ES6的模块系统。

首先站在框架设计者的角度,思考一个模块加载器要做什么事:

  • 分析模块代码依赖的文件
  • 下载文件
  • JS加载文件

二、CommonJs


所以虽然JavaScript在web端发展这么多年,第一个流行的模块化规范却由服务器端的JavaScript应用带来,CommonJS规范是由NodeJS发扬光大,这标志着JavaScript模块化编程正式登上舞台。 Node.js是commonJS规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块。

// 定义模块math.js
var basicNum = 0;
function add(a, b) {
  return a + b;
}
module.exports = { //在这里写上需要向外暴露的函数、变量
  add: add,
  basicNum: basicNum
}

// 引用自定义的模块时,参数包含路径,可省略.js
var math = require('./math');
math.add(2, 5);

// 引用核心模块时,不需要带路径
var http = require('http');
http.createService(...).listen(3000);

内部原理:
导出的是运行时对象——module.exports

  • 在 node 中,每个模块内部都有一个自己的module对象
  • 该 module 对象中,有一个成员叫:exports对象,默认是一个空对象
  • 也就是说如果你需要对外导出成员,只需要把导出的成员挂载到 module.exports 中
  • 我们发现,每次导出接口成员的时候都通过 module.exports.XXX = XXX 的方式很麻烦
  • 所以 node 为了简化操作,专门提供了一个变量:exports = module.exports
var module = {
    exports: {
        foo: 'bar',
        add: function (x, y) {
            return x + y
        }
    }
}

// 也就是说在模块中还有这么一句代码
// var exports = module.exports
module.exports.foo = bar

module.exports.add = function (x, y) {
    return x + y
}

console.log(exports === module.exports)  // true

// 当一个模块需要导出单个成员的时候,直接给 exports 赋值是不管用的
exports = 'hello'

 谁来 require 我,谁就得到 module.exports
 默认在代码的最后有一句:
 return module.exports
 一定要记住,最后return 的是module.exports,不是exports
 所以你给 exports 重新赋值不管用(引用类型赋值原理)


// 如果你实在分不清 exports 和module.exports,你可以选择忘记 exports而只使用 module.exports 也没问题,因为 exports 只是一个快捷方式
// module.exports.xxx = xxx
// module.exports = {}

module的属性如下:

{
  id: '<repl>',                   // 模块标识符
  exports: {},                        // 模块导出的内容
  parent: undefined,              // 最先引用该模块的模块
  filename: null,                 // 模块文件名
  loaded: false,                  // 该模块是否已经加载完毕
  children: [],                       // 该模块引用的模块
  paths:                          // 该模块的搜索路径
   [
     '/Users/ces/repl/node_modules',
     '/Users/ces/node_modules',
     '/Users/node_modules',
     '/node_modules',
     '/Users/ces/.node_modules',
     '/Users/ces/.node_libraries',
     '/usr/local/lib/node' 
   ]
}

尴尬的浏览器

CommonJs中,由于require同步的。模块系统需要同步读取模块文件内容,并编译执行以得到模块接口。这在服务器端实现很简单,也很自然,然而, 想在浏览器端实现问题却很多,但脚本标签天生异步加载(不是解析,解析默认是同步的),因此传统CommonJS模块在浏览器环境中无法正常加载。
script 标签的加载是异步的吗?

三、AMD和require.js


AMD 即Asynchronous Module Definition,即异步模块定义。它是一个在浏览器端模块化开发的规范,由于不是JavaScript原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是大名鼎鼎RequireJS.实际上AMD 是 RequireJS 在推广过程中对模块定义的规范化的产出

requireJS主要解决两个问题:

  1. 多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器
  2. js加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长

语法:
require.config()指定引用路径等,用define()定义模块,用require()加载模块。

// 定义math.js模块
define(function () {
    var basicNum = 0;
    var add = function (x, y) {
        return x + y;
    };
    return {
        add: add,
        basicNum :basicNum
    };
});
// 定义一个依赖underscore.js的模块
define(['underscore'],function(_){
  var classify = function(list){
    _.countBy(list,function(num){
      return num > 30 ? 'old' : 'young';
    })
  };
  return {
    classify :classify
  };
})

// 引用模块,将模块放在[]内
require(['jquery', 'math'],function($, math){
  var sum = math.add(10,20);
  $("#sum").html(sum);
});

四、CMD和sea.js


CMD 即Common Module Definition通用模块定义,此规范其实是在sea.js推广过程中产生的。CMD有个浏览器的实现SeaJS,SeaJS要解决的问题和requireJS一样,CMD它与AMD很类似,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同

CMD和AMD区别:

  1. AMD推崇依赖前置,在定义模块的时候就要声明并初始化其依赖的所有模块 , 即便没用到某个模块 ,但还是提前执行了
  2. CMD推崇就近依赖,只有在用到某个模块的时候再去require 声明并执行
/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
     // 等于在最前面声明并初始化了要用到的所有模块
    a.doSomething();
    if (false) {
        // 即便没用到某个模块 b,但 b 还是提前执行了
        b.doSomething()
    } 
});

/** CMD写法 **/
define(function(require, exports, module) {
    var a = require('./a'); //在需要时申明
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});

五、ES6 Module


其模块功能主要由两个命令构成:exportimport。ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

特点:

  • ES6 Module是静态的,也就是说它是在编译阶段运行,和var以及function一样具有提升效果(这个特点使得它支持tree shaking)
  • 自动采用严格模式(顶层的this返回undefined)
  • ES6 Module支持使用export {<变量>}导出具名的接口,或者export default导出匿名的接口
  • 目前浏览器对 ES6 Module 兼容还不太好,我们平时在 Webpack 中使用的 export 和 import,会经过 Babel 转换为 CommonJS 规范
/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
    return a + b;
};
export { basicNum, add };

/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
    ele.textContent = add(99 + basicNum);
}

如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法。

/** export default **/
//定义输出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
    ele.textContent = math.add(99 + math.basicNum);
}

六、 ES6 模块与 CommonJS 模块的差异


1、 CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

  • CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
  • 通过export {<变量>}导出的,它导出的是一个变量的引用。属性可以成功改写,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。 但是通过export default导出的,在导入的时候的值相当于只是导入一个值。

2、 CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

  • CommonJs运行在服务器上,被设计为运行时加载,即代码执行到那一行才回去加载模,CommonJS的模块是对象,输入时必须查找对象属性,只有运行时才能得到这个对象,不能在编译时做到静态化
  • ES6 ModuleES6模块不是对象,而是通过export命令显示指定输出代码,是静态的输出一个接口,在编译的阶段就加载模块,且和var以及function一样具有提升效果(这个特点使得它支持tree shaking)

3、因此CommonJs require 可以做动态加载(比如可以写在判断里),ES6 Module 静态语法import 语句必须位于顶层作用域中

  • 由于import命令没有办法代替require的动态加载功能,因此引入了import()函数。完成动态加载。import()返回一个Promise对象,可以.then()
  • import()函数适用场合:按需加载、条件加载(if...else

4、CommonJS 加载的是整个模块,即将所有的接口全部加载进来,ES6 可以单独加载其中的某个接口(方法)

CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。

5、ES6 模块中默认采用严格模式,因此顶层的 this 指向 undefined,而CommonJS 模块的顶层 this 指向当前模块

6、CommonJs加载具有缓存,ES6模块不会缓存运行结果

区别:(另外一种)

  1. ES6模块在编译时,就能确定模块的依赖关系,以及输入和输出的变量。
    CommonJS 模块,运行时加载。
  2. 因此require 可以做动态加载,import 语句做不到,import 语句必须位于顶层作用域中。
  3. ES6 模块自动采用严格模式,无论模块头部是否写了 “use strict”;
  4. ES6 模块中顶层的 this 指向 undefined,CommonJS 模块的顶层 this 指向当前模块。
  5. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

下面看两个经典的例子对比一下:
CommonJs模块化

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
// main.js
var mod = require('./lib');
 
console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

ES6模块化

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}
 
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

从上面我们看出,CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。而ES6 模块是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。

另外CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

目前阶段,通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面,但是最好不要这样做。因为import在静态解析阶段执行,所以它是一个模块之中最早执行的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值