1.JS Module Systems
概述
随着JS使用越来越普及,导致namespace
以及depedencies
变得难以维护。因此,为了解决这类问题就开发处理不同的模块系统。
JS Modules的必要性
如果有其他平台开发的经验,那么可能对封装
和依赖
比较容易理解。
在项目中引入一个新的代码块的话,那就要求新加入的代码块不会影响到原有项目的正常运行。
例如,在
C
中,通过添加前缀的方式,进行区分
#ifndef MYLIB_INIT_H
#define MYLIB_INIT_H
enum mylib_init_code {
mylib_init_code_success,
mylib_init_code_error
};
enum mylib_init_code mylib_init(void);
// (...)
#endif //MYLIB_INIT_H
封装,是一种有效的解决冲突的手段。
传统的JS客户端开发中,依赖关系都是隐含的。换句话说,开发者需要手动控制代码的引入顺序(确保正确的依赖关系)。
例如,在
Backbone.js
中,必须手动控制模块的加载顺序。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Backbone.js Todos</title>
<link rel="stylesheet" href="todos.css"/>
</head>
<body>
<script src="../../test/vendor/json2.js"></script>
<script src="../../test/vendor/jquery.js"></script>
<script src="../../test/vendor/underscore.js"></script>
<script src="../../backbone.js"></script>
<script src="../backbone.localStorage.js"></script>
<script src="todos.js"></script>
</body>
<!-- (...) -->
</html>
当前,随着JS开发越来越复杂化,依赖管理也变得难以维护。那么,我们应当如何保证正确的加载顺序呢?
JS Module Systems, 就是为开发者们解决这类问题而开发的。
旧模块处理方式
目前的模块系统都是最新的概念,在这之前,定义模块的方式如下:
let myModule = (function(){
let _uid = '123456'
function _fn() {
console.log('private fn',_uid)
}
function publicFn(uid) {
_uid = uid
}
function getUid () {
_fn()
}
return {
setUid: publicFn,
getUid: getUid
}
})()
myModule.setUid('345')
参考资料:JS设计模式
上面的例子中,返回了一个“Dictionary”,暴露了两个公有方法。而没有暴露出的变量和方法就成为了私有属性,外部禁止访问。
JS变量作用域都是在Function
定义的{}(块级作用域)内。
即:在函数体内声明的任何变量,都无法脱离该函数声明的作用域。
利用上面这个特性,可以揭示模块模式是利用了函数来封装私有属性和方法。
这种特性在模块依赖方面却没起多大作用,但是,功能完善的模块系统,将会解决这一问题。
优点
- 简洁并能在任何地方被引入(无需任何库、不依赖任何语言)
- 在单个文件中定义多个module
缺点
- 无法使用编程方式导入模块(除非使用eval)
- 手动处理模块依赖
- 不能异步加载模块
- 可能导致环形依赖
- 静态编译分析较难
2. CommonJs
概述
CommonJs 项目最初旨在定义一系列规范去帮助开发服务端应用的开发者。CommonJs 团队尝试的其中一个领域就是modules
,对应的API都是同步的。Node.js在最初尝试使用CommonJs规范,后来决定不再遵循。但是,当讲到modules时,Node.js深受影响。
const Square = require('./square.js');
const mySquare = new Square(2);
console.log(`The area of mySquare is ${mySquare.area()}`);
Node.js模块系统抽象为一种库的形式。它关联了Node.js模块和CommonJs的差异。
在Node和CommonJs模块中,两者都有基本的两个元素进行关联:require
&exports
。
require
可以在当前模块中,导入其他模块代码的符号。
- 传递给requrie中的参数,是模块的id
- 在
node_modules
中,它是模块的名称(如果不存在,则为它的路径)
exports
是一个特殊的对象:任何元素(属性或方法)存放入该对象中,将会暴露为一个公共元素,并且保留字段名称。
require vs exports
Node.js和CommonJs一个独特的区别在于module.exports
对象。
在Node中,只能通过module.exports
才会导出预构造对象。
// This won't work.
exports = (params) => {
return {
attr: () => {params + 1}
}
}
// This works as expected.
module.exports = (params) => {
return {
attr: () => {params + 1}
}
}
- 在Node.js中,
module.exports
是导出的真正对象,而exports
只是默认绑定到module.exports
上的变量。 - 在CommonJs中,并没有定义
module.exports
对象。
优点
- 通俗易懂:开发者可以不用看文档就可以理解概念
- 集成的依赖管理:模块间的引用都是按需顺序加载
require
可以在任何地方使用: module可以被编程化加载- 支持循环依赖
缺点
- 同步API不适用某些场景(client-side)
- 每个模块对应单独一个文件
- 浏览器需要加载库或转换
- 模块没有构造函数(尽管Node支持)
- 静态编译较难
3.Asynchronous Module Definition(AMD)
AMD 源于对CommonJs不满的一批开发者。两者最大的区别在于,AMD支持异步加载(asynchronous module loading)。
// 通过依赖数组或者工厂函数调用define
define(['dep1','dep2'], function(dep1,dep2){
// 通过返回值来定义module value
return function() {}
})
// Or
define(function(require) {
let dep1 = require('dep1')
dep2 = require('dep2')
return function () {}
})
在JS中,可以使用闭包来实现异步加载模块:模块请求加载完成时,再调用该函数。
模块定义和模块导入都有同一个函数进行表示:当定义一个模块时,它的依赖关系就是明确的。
AMD加载模块时,可以在运行时获得给定项目的依赖关系图。因此,可以在加载模块的同时,将彼此不依赖的库进行加载。
这对于浏览器来说十分重要,因为启动时间对于用户良好的体验有着至关重要。
优点
- 异步加载(快速启动)
- 支持循环引用
- 支持
require
和exports
- 完全继承依赖管理
- 模块可以被拆分多个文件
- 支持构造
- 支持插件
缺点
- 语法稍微复杂
- 除非已编译,否则需要加载程序库
- 静态编译困难
在客户端开发中,有两种主流的模块加载方式:
Webpack
和Browserify
。
Browserify
希望开发一套解析类似Node.js定义的模块(许多Node包与它一起开箱即用),并且,将原有代码和来自这些模块中的代码捆绑在一个包含所有依赖项的文件中。
Webpack
was developed to handle creating complex pipelines of source transformations before publishing. This includes bundling together CommonJS modules.
4.ES2015 Modules
幸运的是,ES Team决定讨论发布一套关于JS的模块系统。
最终结果就是我们熟知的ES6(ES2015),它的语法很好的兼容了同步和异步的模块操作。
// lib.js
export const sqrt = Math.sqrt
export function square (x) {
return x * x * x
}
///////////////
import { sqrt , square} from 'lib'
console.log('sqrt',sqrt(3)) // 9
console.log('square',square(2)) // 4
import
该指令是将模块引入到namespace。和require
&define
指令定义相反,它无法动态引入。
export
该指令使暴露出的元素公有化。
草案中规范,ES2015并不支持动态加载模块。
In practice, ES2015 implementations are not required to do anything after parsing these directives. Module loaders such as System.js are still required. A draft specification for browser module loading is available.
优点
- 同时支持同步和异步加载
- 语法简单
- 支持静态分析工具
- 支持动态分析工具
- 集成到JS中
- 循环依赖
缺点
- 不支持 run everywhere
遗憾的是,没有一个主要的JavaScript运行时支持ES2015模块。这意味着Firefox,Chrome或Node.js不支持。
幸运的是,许多转发器都支持模块,也可以使用polyfill。目前,Babel的ES2015预设可以处理模块。
6. module.export vs exports
module.exports.method = function () {...}
//vs
export.method = function () {...}
6.1 module.exports
简单示例
// calculator.js
module.exports.add = (a,b) => a+b
// use-calculator.js
const calc = require('./calculator.js')
calc.add(2,3) // 5
- 视为从require()调用,并返回模块对象的引用
- 是由Node.js创建
- 它只是引用一个JS空对象
- 默认是一个空对象,并且可以添加任意值
6.1.1 用法
- 为模块添加公共方法
- 继承对象
如何理解继承?
// 导出一个class实例
// calc.js
module.exports = class Calculator {
add(a,b){return a+b}
}
// 继承该实例,并导出新的class
// calc-advance.js
const Calc = require('./calc.js')
class Advancec extends Calc {
sub(a,b) { return a-b}
}
module.exports = new Advancec()
用法
const calc = require('./calc-advance')
calc.add(2,3) // 5
calc.sub(3,5) // -2
6.1.2 module对象
module对象是指当前的模块。
它的每一个模块都是本地的和私有的。(只允许从module中访问)
// calc.js
module.exports = (a,b) => a + b
console.log(module)
6.2 exports
exports
只是一个中间变量,可以帮助模块开发者写更少的代码- 推荐使用它的属性,是安全的。(eg:exports.add = function …)
exports
不会通过require()
返回任何值
下面有一些正确和错误的范例:
// good
module.exports = {
add(a,b) { return a+b }
}
// good
module.exports.add = (a,b) => a+b
// valid
exports = module.exports
// bad
exports = {
add (a,b) { return a+b }
}
通常, 我们是将module.exports
替换为一个对象或者自定义函数。(good example)
其中,exports = module.exports
是方便我们了解模块中的属性和方法。
结论
exports
变量可能只有部分方法导出,这一点对于Node.js新手来说有一些困惑,即使是Node官方文档也有这种声明。
拓展:
如何理解下面的代码:
module.exports = exports = nano = (a,v) a*v
我们可以设想,在文件的起始处默认进行下面的声明:
// hidden define
const module = new Module()
const exports = module.exports
// ....
exports 可以理解为指向module.exports
的对象,而只有module.exports
才会返回值。