JavaScript模块化(ES Module/CommonJS/AMD/CMD)

1模块化历史

1.1前言

参照前端模块化开发的价值

1.2无模块化

每次说到JavaScript都会想到Brendan Eich花了十来天就发明了它,那就是JS的鸿蒙时期,混沌初开。
就像当年在校初学前端时写的代码,没有那么多的套路,就是从上往下码代码,没有想着去声明函数神马的,甚至多少代码都写在一个JS里。现在想来真是惨不忍睹。虽然本人入坑前端距那个鸿蒙时代实在久远,但是据各种典籍记载,那时候的代码风格就差不多这样子,从上往下一直堆着就好了。

var a = 0;
if (xxx) {
  // 省略100L
}
document.getElementById('id').onclick = function(event) {
  // 省略若干行
}
......

1.3模块化冒泡

每个行业都有梗,现在和同事聊天有时候还会吐槽十几年前的老网站,真是有幸见过。前辈的聊天更有意思了,当年的登录居然是写死在前端代码里,就像这样子

if (username === 'xxxxx' && password === 'xxxxxx') {
  // 登录成功
}

是不是觉得很无语。当年的前端都是静态页面,没有现在这样子通过ajax和后端交互神马的,内容更是丰富多彩,更新及时。
前端代码愈发庞大,那么自然而然会暴露很多问题。
无非就俩个:

  • 命名冲突
  • 文件依赖

1.3.1命名冲突解决

  1. java风格的namespace,这个很好理解,在此不赘诉,缺点的话,自行想象,不堪
  2. 自执行函数(内部变量不可见不被污染)
// jQuery式的匿名自执行函数
// 缺点是增添了全局变量、依赖需要提前提供
(function(root) {
    root.jQuery = window.$ = jQuery; // 挂载到window之上
})(window)
// 普通自执行函数
// 缺点就是暴露了全局变量,而且随着模块的增加,全局变量会很多
module = function() {
    function module() {

    }
    return module;
}()
  1. YUI3的沙箱机制,这个表示不晓得。

1.3.2文件依赖解决

这个没有解决方法,乖乖自行保证顺序和不缺漏吧



1.4CommonJS

1.4.1前言

随着前端的发展,到node.js被创,js可以用来写server端代码。
做过后端的同学肯定知道没有模块化怎么能忍呢?
我们通过上节所得,我们可以得出以下几点需待解决

  • 模块代码安全,不可被污染也不可污染别人,沙箱呀
  • 把模块接口暴露出去(得优雅呀,不能增添全局变量)
  • 这个依赖顺序管理

1.4.2发展

这个还真没有经历了解过。度娘一番,幸好看到seajs下的一个issues。
大致就是大牛很牛,推出了Modules/1.0规范。
之后为了推广到浏览器端,大牛产生分歧,分为三大流派:

  • Modules/1.x 流派(Modules/Transport
    通过工具转换现有的CommonJS)
  • Modules/Async 流派(自立门派)
  • Modules/2.0 流(Modules/Wrappings
    对1.0的升级)

这里说下为什么不能用在浏览器

  • 服务端代码在硬盘,加载模块时间几乎忽略不计。浏览器端就不成了。
  • 模块引用未被function,所以暴露在了全局之下。

1.4.3番外(AMD、CMD)

AMD是 RequireJS 在推广过程中对模块定义的规范化产出。
CMD是 SeaJS 在推广过程中对模块定义的规范化产出。

1.5ES Module

这个是ECMA搞得一套。和之前的区别在于人家是官方的,根正苗红,上文的是社区搞得,野生。

2 ES Module/CommonJS/AMD/CMD差异

2.1 ES Module与CommonJS的差异

编译时和运行时

首先说下编译时和运行时。JavaScript有俩种声明方法(声明变量和声明方法)。var/const/let和function
编译时,对于声明变量会在内存中开辟一块内存空间并指向变量名,且指向变量名,赋值为undefined。对于函数声明会一样的开启空间。不过赋值为声明的函数体。PS:无论顺序如何,都会先声明变量
运行时,执行变量初始化之类的。

// 源码
var a = 3;
function f() {
    return 'f';
}

// 编译时
var a = undefined;
var f = function() {
    return 'f';
}
// 运行时
a = 3;

CommonJS模块是对象,是运行时加载,运行时才把模块挂载在exports之上(加载整个模块的所有),加载模块其实就是查找对象属性。
ES Module不是对象,是使用export显示指定输出,再通过import输入。此法为编译时加载,编译时遇到import就会生成一个只读引用。等到运行时就会根据此引用去被加载的模块取值。所以不会加载模块所有方法,仅取所需。

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
    详情参见

2.2CommonJS与AMD/CMD的差异

AMD/CMD是CommonJS在浏览器端的解决方案。CommonJS是同步加载(代码在本地,加载时间基本等于硬盘读取时间)。AMD/CMD是异步加载(浏览器必须这么干,代码在服务端)

2.3AMD与CMD的差异

  • AMD是提前执行(RequireJS2.0开始支持延迟执行,不过只是支持写法,实际上还是会提前执行),CMD是延迟执行
  • AMD推荐依赖前置,CMD推荐依赖就近

3 用法

3.1 CommonJS

// 导出使用module.exports,也可以exports。exports指向module.exports;即exports = module.exports
// 就是在此对象上挂属性
// commonjs
module.exports.add = function add(params) {
    return ++params;
}
exports.sub = function sub(params) {
    return --params;
}

// 加载模块使用require('xxx')。相对、绝对路径均可。默认引用js,可以不写.js后缀
// index.js
var common = require('./commonjs');
console.log(common.sub(1));
console.log(common.add(1));

3.2 AMD/RequireJS

  • 定义模块:define(id?, dependencies?, factory)
  • 加载模块:require([module], factory)
// a.js
// 依赖有三个默认的,即"require", "exports", "module"。顺序个数均可视情况
// 如果忽略则factory默认此三个传入参数
// id一般是不传的,默认是文件名
define(["b", "require", "exports"], function(b, require, exports) {
    console.log("a.js执行");
    console.log(b);
// 暴露api可以使用exports、module.exports、return
    exports.a = function() {
        return require("b");
    }
})
// b.js
define(function() {
    console.log('b.js执行');
    console.log(require);
    console.log(exports);
    console.log(module);
    return 'b';
})
// index.js
// 支持Modules/Wrappings写法,注意dependencies得是空的,且factory参数不可空
define(function(require, exports, module) {
    console.log('index.js执行');
    var a = require('a');
    var b = require('b');
})
// index.js
require(['a', 'b'], function(a, b) {
    console.log('index.js执行');
})

3.3 CMD/SeaJS

SeaJS平时没有到,不过了解了下,丰富用法看CMD定义规范。

  • 定义模块:define(factory)
// a.js
// require, exports, module参数顺序不可乱
// 暴露api方法可以使用exports、module.exports、return
// 与requirejs不同的是,若是未暴露,则返回{},requirejs返回undefined
define(function(require, exports, module) {
    console.log('a.js执行');
    console.log(require);
    console.log(exports);
    console.log(module);
})
// b.js
// 
define(function(require, module, exports) {
    console.log('b.js执行');
    console.log(require);
    console.log(exports);
    console.log(module);
})
// index.js
define(function(require) {
    var a = require('a');
    var b = require('b');
    console.log(a);
    console.log(b);
})

定义模块无需列依赖,它会调用factory的toString方法对其进行正则匹配以此分析依赖。预先下载,延迟执行

3.4 ES Module

输出/export

// 报错1
export 1;
// 报错2
const m = 1;
export m;

// 接口名与模块内部变量之间,建立了一一对应的关系
// 写法1
export const m = 1;
// 写法2
const m = 1;
export { m };
// 写法3
const m = 1;
export { m as module };

PS:这个有点不是很明白,大致理解就是不能直接导出变量,但是可以导出声明(函数、变量声明)。这里的接口理解是export之后的变量,它和变量建立了映射关系。总的而言,export之后只能接声明或者语句

输入/import

基本用法

// 类似于对象解构
// module.js
export const m = 1;
// index.js
// 注意,这里的m得和被加载的模块输出的接口名对应
import { m } from './module';
// 若是想为输入的变量取名
import { m as m1 }  './module';
// 值得注意的是,import是编译阶段,所以不能动态加载,比如下面写法是错误的。因为'a' + 'b'在运行阶段才能取到值,运行阶段在编译阶段之后
import { 'a' + 'b' } from './module';
// 若是只是想运行被加载的模块,如下
// 值得注意的是,即使加载两次也只是运行一次
import './module';
// 整体加载
import * as module from './module';

PS:CommonJS和ES Module是可以写一起的,但是最好不要。毕竟一个是编译阶段一个是运行阶段。就在项目中入过坑,自行体会。

赋值

首先输入的模块变量是不可重新赋值的,它只是个可读引用,不过却可以改写属性

// 单例
// module.js
export const a = {};
// module2.js
export { a } from './module';
import { a as a1 } from './module';
import { a } from './module2';
a1.e = 3;
console.log(a1) // { e: 3 }
console.log(a) // { e: 3 }

输出/export default

// module.js
// 其实export default就是export { xxx as default }
const m = 1;
export default m;
===
export { m as default }
// index.js
// 对应的输入也得相应变化
import module from './module';
===
import { default as module } from './module';

还记得之前export小结处的俩报错么?如下写法正确,因为提供了default接口

// 写法1
export default 1;
// 写法2
const m = 1;
export default m;
// 错误写法
export default const m = 1;

PS:export default只能一次

复合写法

可用于模块间继承。比如在a模块写下如下,那么a模块不就有了./module的方法了

export { a } from './module';
export { a as a1 } from './module';
export * from './module';

动态加载/import()

因为编译时加载,所以不能动态加载模块。不过幸好有import()方法。
这家伙返回值是一个Promise对象,所以then、catch你开心就好

// 普通写法
import('./module').then(({ a }) => {})
// async、await
const { a } = await import('./module');

4 番外自实现

var MyModules = (function(){
    var modules = [];
    function define(name, deps, cb) {
        deps.forEach(function(dep, i) {
            deps[i] = modules[dep];
        });
        modules[name] = cb.apply(cb, deps);
    }
    function get(name) {
        return modules[name];
    }
    return {
        define: define,
        get: get
    };
})();
MyModules.define('add', [], function() {
    return function(a, b) {
            return a + b;
        };
})
MyModules.define('foo', ['add'], function(add) {
    var a = 3;
    var b = 4;
    return {
        doSomething: function() {
            return add(a, b) + a;;
        }
    };
})
var add = MyModules.get('add');
var foo = MyModules.get('foo');
console.log(add(1, 2));
console.log(foo.doSomething());

你可能感兴趣的:(JavaScript模块化(ES Module/CommonJS/AMD/CMD))