timelessover/blog

ES6系列之模块化开发总结(十二)

Opened this issue · 0 comments

前言

模块化规范可分为四种

  • AMD
  • CMD
  • CommonJS
  • ES6 import

下面我们来以下顺序聊聊模块化加载规范

AMD规范

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。

// CommonJS模块
let { start, readFile } = require('fs');

// fs.js
exports.start = fn
exports.readFile = fn

当然这是在 node 中的写法,在浏览器中又是另一种写法。

// fs.js
define(['./start','./readFile'], function() {
    console.log('加载了 start ,readFile 模块')
    return {      
        readFile: function() {
            return fn
        },
        start: function() {
            return fn
        },
    };
});

// 依然按照AMD规范引用
let { start, readFile } = require('fs');

这样浏览器也可以按照AMD规范来实现模块化了

CMD规范

与 AMD 一样,CMD 其实就是 SeaJS 在推广过程中对模块定义的规范化产出。

// fs.js
define(function(require, exports, module) {
    var addModule = require('./start');
    var squareModule = require('./readFile');
});
// start.js
define(function(require, exports, module) {
    console.log('加载了 start 模块')
    module.exports = {      
        start: function() {
            return fn
        }
    };
});

CMD 更像是在 AMD 之上的扩展,使其粒度更细。

AMD 与 CMD 的区别

  1. CMD 推崇依赖就近,AMD 推崇依赖前置
// require.js 例子中的 fs.js
// 依赖必须一开始就写好
require(['./start', './readFile'], function(start, readFile) {
    console.log(startModule)
    console.log(readFileModule)
});
// sea.js 例子中的 fs.js
define(function(require, exports, module) {
    var readFileModule = require('./start');
    // 依赖可以就近书写
    var readFileModule = require('./readFile');
});
  1. AMD 是将需要使用的模块先加载完再执行代码,而 CMD 是在 require 的时候才去加载模块文件,加载完再接着执行。

CommonJS

AMD 和 CMD 都是用于浏览器端的模块规范,而在服务器端比如 node,采用的则是 CommonJS 规范。

导出模块的方式:

var add = function(x, y) { 
    return x + y;
};

module.exports.add = add;

引入模块的方式:

var add = require('./add.js');
console.log(add.add(1, 1));

我们将之前的例子改成 CommonJS 规范:

// main.js
var add = require('./add.js');
console.log(add.add(1, 1))

var square = require('./square.js');
console.log(square.square(3));
// add.js
console.log('加载了 add 模块')

var add = function(x, y) { 
    return x + y;
};

module.exports.add = add;
// multiply.js
console.log('加载了 multiply 模块')

var multiply = function(x, y) { 
    return x * y;
};

module.exports.multiply = multiply;
// square.js
console.log('加载了 square 模块')

var multiply = require('./multiply.js');

var square = function(num) { 
    return multiply.multiply(num, num);
};

module.exports.square = square;

如果我们执行 node main.js,打印的顺序为:

加载了 add 模块
2
加载了 square 模块
加载了 multiply 模块
9

我们发现 CommonJS 是先执行再去加载之后的文件,和 seaJS 一致。

ES6的import

导出模块的方式:

/// profile
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

引入模块的方式:

import {firstName, lastName, year} from './profile';

我们再将上面的例子改成 ES6 规范:

// main.js
import {add} from './add.js';
console.log(add(1, 1))

import {square} from './square.js';
console.log(square(3));
// add.js
console.log('加载了 add 模块')

var add = function(x, y) {
    return x + y;
};

export {add}
// multiply.js
console.log('加载了 multiply 模块')

var multiply = function(x, y) { 
    return x * y;
};

export {multiply}
// square.js
console.log('加载了 square 模块')

import {multiply} from './multiply.js';

var square = function(num) { 
    return multiply(num, num);
};

export {square}

打印的顺序为:

加载了 add 模块
加载了 multiply 模块
加载了 square 模块
2
9

我们可以发现是模块先加载完再执行代码,更加符合 AMD 规范。

ES6 与 CommonJS

引用阮一峰老师的 《ECMAScript 6 入门》

它们有两个重大差异。

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

第二个差异可以从两个项目的打印结果看出,导致这种差别的原因是:

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

重点解释第一个差异。

CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

举个例子:

// 输出模块 counter.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
    counter: counter,
    incCounter: incCounter,
};
// 引入模块 main.js
var mod = require('./counter');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

counter.js 模块加载以后,它的内部变化就影响不到输出的 mod.counter 了。这是因为 mod.counter 是一个原始类型的值,会被缓存。

但是如果修改 counter 为一个引用类型的话:

// 输出模块 counter.js
var counter = {
    value: 3
};

function incCounter() {
    counter.value++;
}
module.exports = {
    counter: counter,
    incCounter: incCounter,
};
// 引入模块 main.js
var mod = require('./counter.js');

console.log(mod.counter.value); // 3
mod.incCounter();
console.log(mod.counter.value); // 4

value 是会发生改变的。不过也可以说这是 "值的拷贝",只是对于引用类型而言,值指的其实是引用。

而如果我们将这个例子改成 ES6:

// counter.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.js
import { counter, incCounter } from './counter';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

这是因为:

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

webpack

webpack 其实是遵循 CommonJS 规范的。

我们以 CommonJS 项目中的 square.js 为例,它依赖了 multiply 模块:

console.log('加载了 square 模块')

var multiply = require('./multiply.js');


var square = function(num) { 
    return multiply.multiply(num, num);
};

module.exports.square = square;

webpack 会将其包裹一层,注入这些变量:

function(module, exports, require) {
    console.log('加载了 square 模块');

    var multiply = require("./multiply");
    module.exports = {
        square: function(num) {
            return multiply.multiply(num, num);
        }
    };
}

那 webpack 又会将 CommonJS 项目的代码打包成什么样呢?我写了一个精简的例子,你可以直接复制到浏览器中查看效果:

// 自执行函数
(function(modules) {

    // 用于储存已经加载过的模块
    var installedModules = {};

    function require(moduleName) {

        if (installedModules[moduleName]) {
            return installedModules[moduleName].exports;
        }

        var module = installedModules[moduleName] = {
            exports: {}
        };

        modules[moduleName](module, module.exports, require);

        return module.exports;
    }

    // 加载主模块
    return require("main");

})({
    "main": function(module, exports, require) {

        var addModule = require("./add");
        console.log(addModule.add(1, 1))

        var squareModule = require("./square");
        console.log(squareModule.square(3));

    },
    "./add": function(module, exports, require) {
        console.log('加载了 add 模块');

        module.exports = {
            add: function(x, y) {
                return x + y;
            }
        };
    },
    "./square": function(module, exports, require) {
        console.log('加载了 square 模块');

        var multiply = require("./multiply");
        module.exports = {
            square: function(num) {
                return multiply.multiply(num, num);
            }
        };
    },

    "./multiply": function(module, exports, require) {
        console.log('加载了 multiply 模块');

        module.exports = {
            multiply: function(x, y) {
                return x * y;
            }
        };
    }
})

最终的执行结果为:

加载了 add 模块
2
加载了 square 模块
加载了 multiply 模块
9

总结

  1. AMD 是先加载完后再执行,CMD是边执行边加载,CMD 结果与 CommonJS 结果一致。
  2. CommonJS 模块输出的是一个值的拷贝,而 ES6 模块输出的是值的引用。
  3. 在 node 中模块加载和 webpack 打包遵循的都是 CommonJS 规范。