前端模块化

136 阅读4分钟

ES6模块化之前,模块加载方案主要有CommonJS、AMD、CMD 三种。CommonJS用于服务器,AMD、CMD用于浏览器。

ES6 模块的设计思想是尽量的__静态化__,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。

CommonJS模块

CommonJS 模块就是对象,输入时必须查找对象属性。

let { stat, exists, readfile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
module.exports

module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量

// lib.js
module.exports.func = function(){
	console.log('This is module');
} 
//main.js
let a = require('./lib');
a.func();//打印'This is module'
exports

Node为每个模块提供一个exports变量,指向module.exports。相当在每个模块头部,有一行这样的命令。

let exports = module.exports;
// lib.js
const func = function(){
	console.log('This is module');
}
exports.func = func
//等于 module.exports.func = func

AMD

AMD规范是非同步加载模块,允许指定回调函数。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

 require([module], callback);
  • 第一个参数[module],是一个数组,里面的成员就是要加载的模块
  • 第二个参数callback,则是加载成功之后的回调函数
//a.js
define(function(){
  function foo(){
    console.log('hello world!');
  }
  return {
    foo
  }
})

//main.js
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
	a.foo()
    b.foo()
}) 

CMD

  • AMD是依赖关系前置,在定义模块的时候就要声明其依赖的模块
  • CMD是按需加载依赖就近,只有在用到某个模块的时候再去require
define(function(require, exports, module) {
  let a = require('./a')
  a.foo()
  // ...
  let b = require('./b') //依赖可以就近书写
  b.foo()
  // ... 
})

ES6 模块

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象

ES6 模块与 CommonJS 模块的差异

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

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

// lib.js
let 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

lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。

// lib.js
let counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};

上面代码中,输出的counter属性实际上是一个取值器函数。现在再执行main.js,就可以正确读取内部变量counter的变动了

ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。

// 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
// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);

m1.js的变量foo,在刚加载时等于bar,过了 500 毫秒,又变为等于baz。表明,ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。

export通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。

// mod.js
function C() {
  this.sum = 0;
  this.add = function () {
    this.sum += 1;
  };
  this.show = function () {
    console.log(this.sum);
  };
}

export let c = new C();
// x.js
import {c} from './mod';
c.add();

// y.js
import {c} from './mod';
c.show();

// main.js
import './x';
import './y';
//输出1

参考:es6.ruanyifeng.com/#docs/modul…