Fork me on GitHub

深入理解模块化

什么是模块化?

早期的 JavaScript 往往作为嵌入到 HTML 页面中的用于控制动画与简单的用户交互的脚本语言,所有的嵌入到网页内的 JavaScript 对象都会使用全局的 window 对象来存放未使用 var 定义的变量。这就会导致一个问题,那就是,最后调用的函数或变量取决于我们引入的先后顺序。模块化时代。随着单页应用与富客户端的流行,不断增长的代码库也急需合理的代码分割与依赖管理的解决方案,这也就是我们在软件工程领域所熟悉的模块化(Modularity)。

简而言之,模块化就是将一个大的功能拆分为多个块,每一个块都是独立的,你不需要去担心污染全局变量,命名冲突什么的。

模块化的好处:

  • 避免命名冲突和变量污染
  • 依赖管理
  • 增强代码可读性
  • 提高代码复用性

js有模块化吗?

  • JS没有模块系统,不支持封闭的作用域和依赖管理
  • 没有标准库,没有文件系统和IO流API
  • 也没有包管理系统

JS在最初是没有模块化设计的,那时候如何避免命名冲突和变量污染呢?

方法一:函数封装,缺点是污染全局作用域

1
2
3
function fn1 () {}

function fn2 () {}

方法二:使用对象,缺点是没有私有变量,外部可以修改

1
2
3
4
var myModule = {
var1: 1,
fn1: function() {}
}

方法三:使用IIFE(立即执行函数表达式),在老项目中很常见,一个 JS 文件中就是一个立即执行函数。

  • 创建一个立即调用的匿名函数表达式
  • return一个变量,其中这个变量里包含你要暴露的东西
  • 返回的这个变量将赋值给 module
1
2
3
4
5
6
7
8
9
10
11
12
let myMoudule = (function() {
let num = 10;
return {
publicName: 5,
getPrivateNum: function () {
return num;
},
setPrivateNum: function (reNum) {
return num = reNum;
}
}
})()

模块化规范

  • CommonJS
  • AMD 异步模块定义
  • CMD 通用模块定义
  • ES6 模块化

CommonJS 文件即模块

《深入浅出nodejs》一书中提到,每个模块文件的require,exports和module这3个变量并没有在模块中定义,也并非全局函数/对象。而是在编译的时候Node对js文件内容进行了头尾的包装。在头部加了(function (exports, require, module, filename, dirname) {,在尾部加了 \n});。

  • 使用module.exports(exports)暴露对外的接口
  • 模块引用时会找到绝对路径
  • 模块加载时是同步操作
  • 默认会加后缀js,json,…
  • 模块加载过会有缓存,把文件名作为key,module作为value
  • node实现模块化就是增加了一个闭包,并且自执行这个闭包(runInThisContext)
  • 不同模块下的变量不会相互冲突

nodeJS也是使用CommonJS规范

1
2
3
4
5
6
7
8
9
10
// a.js
module.exports = {
a: 1
}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a // 1

module.exports 和 exports什么原理呢?

require流程图:

基本实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
//node原生的模块,用来读写文件(fileSystem)
let fs = require('fs')

//node原生的模块,用来解析文件路径
let path = require('path')

//提供了一系列 API 用于在 V8 虚拟机环境中编译和运行代码。
let vm = require('vm')

//Module类,就相当于我们的模块(因为node环境不支持es6的class,这里用function)
function Module(p){
//当前模块的标识
this.id = p
//没个模块都有一个exports属性
this.exports = {}
//这个模块默认没有加载完
this.loaded = false
//模块加载方法
this.load = function(filepath){
//判断文件是json还是 node还是js
let ext = path.extname(filepath)
// 返回一个exports
return Module._extensions[ext](this)
}
}
// exports 是指向的 module.exports 的引用,所以不要直接对exports赋值
var exports = Module.exports
//js文件加载的包装类
Module._wrapper = ['(function(exports,require,module,__dirname,__filename){','\n})']

//所有的加载策略
Module._extensions = {
// 这里的module参数是就是Module的实例
'.js': function(module){
let fn = Module._wrapper[0] + fs.readFileSync(module.id,'utf8') + Module._wrapper[1]
//执行包装后的方法 把js文件中的导出引入module的exports中
//模块中的this === module.exports === {} exports也只是module.exports的别名
//runInThisContext:虚拟机会产生一个干净的作用域来跑其中的代码,类似于沙箱sandbox
// 第一个参数是改变 this 指向,那第二个参数是 module.exports,所以在每个模块导出的时候,使用 module.exports = xxx,而 req 最后返回的就是我们模块导出的内容。第三个参数之所以传入 req 是因为我们还可能在一个模块中导入其他模块,而 req 会返回其他模块的导出在当前模块使用,这样整个 CommonJS 的规则就这样建立起来了。
vm.runInThisContext(fn).call(module.exports,module.exports,req,module)
return module.exports
},
'.json': function(module){
// //同步读取文件中的内容并把它转为JSON对象
return JSON.parse(fs.readFileSync(module.id,'utf8'))
},
'.node': 'xxx',
}

// 以绝对路径为key存储一个module
Module._catcheModule = {}

// 解析绝对路径的方法,返回一个绝对路径
Module._resolveFileName = function(moduleId){
// 获取moduleId的绝对路径
let p = path.resolve(moduleId)
try {
// 同步地测试 path 指定的文件或目录的用户权限
fs.accessSync(p)
return p
}catch(e){
console.log(e)
}
//对象中所有的key做成一个数组[]
let arr = Object.keys(Module._extensions)
for(let i=0;i<arr.length;i++){
let file = p+arr[i]
//因为整个模块读取是个同步过程,所以得用sync,这里判断有没有这个文件存在
try{
fs.accessSync(file)
return file
}catch(e){
console.log(e)
}
}
}

//require方法
function req(moduleId) {
// 解析绝对路径的方法,返回一个绝对路径
let p = Module._resolveFileName(moduleId)
// 查看是否有缓存
if(Module._catcheModule[p]){
// 有缓存直接返回对应模块的exports
return Module._catcheModule[p].exports
}
//没有缓存就生成一个
let module = new Module(p)
// 放入缓存中
Module._catcheModule[p] = module
//加载模块
module.exports = module.load(p)
// require() 返回的是 module.exports 而不是 exports
return module.exports
}

CommonJS采用同步加载模块的机制,node服务端-文件存在本地硬盘,加载快,可同步加载,而浏览器端可不行,文件通过网络加载耗时,同步加载阻塞页面,因此需要异步加载所需的模块。所以出现了以下几种模块化方式用于浏览器端异步加载模块。

AMD && RequireJS

AMD异步模块定义(Asynchronous Model Definition):
define定义模块,require调用模块

1
define(id,dependencies,factory)
  • id: 模块标识
  • dependencies:依赖的模块数组,默认为[‘require’,’exports’,’module’]
  • factory:模块初始化要执行的函数或者对象

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义模块A
define('moduleA', function (require, exports, module) {
exports.getNum = function (x) {
return 5;
}
});

// 定义模块D,其依赖模块A
define('moduleD', ['moduleA'], function (moduleA) {
//通过模块A的方法初始变量index
var index = moduleA.getNum();
//通过return保留addIndex方法
return {
addIndex: function () {
index += 1;
}
}
});

加载模块:

1
require(modules(数组),callback(加载后的回调))

例如:加载模块A和模块D

1
2
3
require(['moduleA', 'moduleB'], function (moduleA, moduleB) {
//
})

requireJS是AMD规范的模块加载器,也是AMD规范的具体实现,原理实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let factories = {};     // 管理一个关联对象,将模块名和函数关联起来
// 定义模块define 三个参数:1.模块名 2.依赖 3.工厂函数
function define(name, depend, factory) {
factories[name] = factory;
factory.depend = depend; // 将依赖记到factory上
}
// 通过require使用模块
function require(modules, callback) {
let result = modules.map(mod => { // 返回一个结果数组
let factory = factories[mod]; // 拿到模块对应的函数
let exports;
let depend = factory.depend; // 取到函数上的依赖 ['a']

// require(['song','album'], function(song,album) {}) 可能会有很多依赖
require(depend, () => { // 递归require
exports = factory.apply(null, arguments);
});
return exports; // exports得到的是函数返回的值
});
callback.apply(null, result); // result为一个结果数组,所以用apply
}

CMD && SeaJS

CMD通用模块定义(Common Module Definition)

例如:

1
2
3
4
5
 define(function (require, exports, module) {
// exports : 对外的接口
// requires : 依赖的模块
require('./a.js');//如果地址是一个模块的话,那么require的返回值就是模块中的exports
});

通常我们会拿 CMD 规范来和 AMD 规范进行对比,对于依赖的模块,AMD 和 CMD 的处理方式是不一样的。

  • AMD 推崇依赖前置,在定义模块的时候就要声明其依赖的模块。
  • CMD 推崇依赖就近,只有在用到某个模块的时候再去 require
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// CMD 
define(function (require, exports, module) {
var a = require('./a')
a.doSomething()
// 依赖可以就近书写
var b = require('./b')
b.doSomething()
})

// AMD 默认推荐的是 依赖必须一开始就写好
define(['./a', './b'], function (a, b) {
a.doSomething()
b.doSomething()
})

执行依赖模块时机:

  • AMD 提前执行依赖(异步加载:依赖先执行)+ 延迟执行
  • CMD 延迟执行依赖(运行到需加载,根据顺序执行)

加载器:

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

SeaJS 是 CMD 规范的具体实现。使用 SeaJS 和使用 requireJS 十分类似,只是写法上稍微有所不同。

es6模块化

设计思想就是:一个 JS 文件就代表一个 JS 模块。在模块中你可以使用 import 和 export 关键字来导入或导出模块中的东西。

ES6 模块主要具备以下几个基本特点:

  1. 自动开启严格模式,即使你没有写 use strict

⚠️严格模式主要有以下限制:

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • eval和arguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.caller和fn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protected、static和interface)
  1. 每个模块都有自己的上下文,每一个模块内声明的变量都是局部变量,不会污染全局作用域
  2. 模块中可以导入和导出各种类型的变量,如函数,对象,字符串,数字,布尔值,类等
  3. 每一个模块只加载一次,每一个 JS 只执行一次, 如果下次再去加载同目录下同文件,直接从内存中读取。 一个模块就是一个单例,或者说就是一个对象

export:export 命令用于规定模块的对外接口。如果你希望外部能够读取模块内部的变量,函数或类等,就必须使用 export 关键字输出该内容。

import:使用 export 命令定义了模块的对外接口以后,其他 JS 文件就可以通过 import 命令加载这个模块。

1
2
// 引入的语法就这样 import,XXX 这里有很多语法变化
import XXX from './a.js'
1
2
3
4
5
// 输出单个值,使用export default
export default function() {}

// 输出多个值,使用export
export function a() {}

export default与普通的export不要同时使用

参考文章:
https://juejin.im/post/5b67c342e51d45172832123d
https://juejin.im/post/5b966d1ff265da0ae800f8ca

本文标题:深入理解模块化

文章作者:tongtong

发布时间:2019年03月19日 - 19:03

最后更新:2019年04月26日 - 16:04

原始链接:https://ilove-coding.github.io/2019/03/19/深入理解模块化/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!
-------------本文结束-------------