回头再看JS模块化编程

回头再看JS模块化编程

什么是模块?我们可能要从需求上出发进行理解,当web应用的规模变得越来越大,业务变得越来越复杂时,我们需要将一些函数分门别类,在分类的基础上对函数进行封装,这就形成了模块。下面看一下js模块的一些形式。

对象模块

假如我们有多个函数,想作为一个模块使用,最原始的做法就是把这几个函数全部放在一个js文件,通过文件的形式来对js进行划分模块。

1
2
3
4
5
6
7
8
// my_module.js

function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}

然而这样的做法会污染全局环境,引用这个js后,window对象就会多了两个方法。那么如何减少这种对全局环境的污染呢?想到的最简单的一个办法就是,把这几个函数都放在一个对象中,只暴露一个对象。

1
2
3
4
5
6
7
8
9
10
// my_module.js

var MyModule = {
add: function (a, b) {
return a + b;
},
multiply: function (a, b) {
return a * b;
}
}

IIFE

IIFE(immediately invoked function expression),也就是立即执行函数表达式。假设有这样一个场景,你的模块需要定义默认参数,而你又希望这个默认参数不被外界所改变,那么使用对象模块的方式就没有办法做到了,因为这个对象已经暴露在全局环境中。那么如何能隔离作用域呢?聪明的你已经想到了函数,对的,函数可以做到。我们通过IIFE为window挂载了setColor方法。

1
2
3
4
5
6
7
8
(function(global) {
var default_option = {
color: 'blue'
}
global.setColor = function(id, colorValue) {
document.getElementById(id).style.color = colorValue || default_option.color
}
})(this)

这里的default_option就不会暴露在全局环境中,你可以尝试一下在控制台console.log(window.default_option),得到的就是undefined。

CommonJS

引用百度给出的定义

CommonJS API定义很多普通应用程序(主要指非浏览器的应用)使用的API,从而填补了这个空白。它的终极目标是提供一个类似Python,Ruby和Java标准库。

CommonJS提供的模块方案认为,一个js就是一个模块,我们经常用到的变量和函数有global,module,exports,require。global是nodejs环境的全局对象,类似与浏览器环境的window,也是根对象,任何在全局环境下定义的变量或函数都是global的属性或方法,global涉及很多东西,这里不再赘述。而module是模块对象,exports包含该模块要导出的变量或函数,require是导入模块的方法。我们首先写两个简单的js来认识它们。

1
2
3
4
5
6
7
8
9
10
// a.js
module.exports.add = function (a, b) {
return a + b;
}

// b.js
var a = require('./a.js');

var result = a.add(1, 2);
console.log(result); // 输出3

这是最简单的模块写法,a.js通过exports导出add函数,而b.js通过require导入a模块,便可以调用a模块的add函数。

module

那么我们先来看看module这个对象。在b.js中我们console.log(module),则会打印出模块b的信息。

moduleB

可以看到,模块b的children里有模块a,说明模块b引用了模块a。我们再观察一下模块a,修改a.js的代码如下,再运行b.js

1
2
3
4
5
console.log(module)

module.exports.add = function (a, b) {
return a + b;
}

moduleA

可以看到,模块a的parent指向模块b,是因为执行的是b.js,而b.js引用了模块a。注意此时模块a的loaded属性值仍是false,因为此时模块还没加载完成。如果我们在add方法中打日志,而b.js调用a.js的add函数,则会发现此时模块a的loaded已经变成了true

从上面可以了解到,module对象下面有以下属性

  • id:模块id,一般默认是模块的路径
  • exports:模块对外导出对象,包含了对外导出的函数和属性
  • parent:指向首次加载本模块的模块(为什么说是首次呢?假设b.js引用了模块a,c.js引用了模块a和模块b,此时运行c.js,模块a的parent指向的是模块b)
  • filename:模块的绝对路径
  • loaded:模块是否已经加载完成
  • children:当前模块引用的其他模块
  • paths:对于加载模块时没给出./ ../ /…/时,加载模块的搜索路径。依次从第一个路径搜索到最后一个路径。

exports

接下来我们说说exports,在这里要了解module.exports与exports的区别。Node.js 在初始化时执行了 exports = module.exports , 所以 exports 与 module.exports 指向了相同的内存。当不改变两者的指向时,两者还是全等的。因此,我前面的写法 exports.add 只是给 exports 指向的对象上添加了add方法,并未改变其指向。这之后exports与module.exports仍是一致的。到这里大家应该明白了什么情况两者会不相等了。

1
2
3
4
5
6
// 如果采用这种改变指向的写法,那么之后exports与module.exports就不一样了。
module.exports = {
add: function (a, b) {
return a + b
}
}

通过exports导出的函数和属性可以被其他模块调用,这一点想必大家都清楚了。

require

这里先说一下模块的分类,NodeJS中模块分为核心模块和文件模块。核心模块是被编译成二进制代码,引用的时候只需require表示符即可,如require(‘fs’),不需要加路径的。而引用文件模块时需要加上路径,表示对文件的引用。假如你加载一个自定义的test.js模块时,没有指定路径,那么它会首先从当前目录的node_modules子目录下寻找test.js,如果没有,则查找上一级目录的node_modules子目录,一直查到盘符的根目录为止。也就是前面提到的module.paths的查找顺序。

说到这里,我们再来回顾一下我们在开发时,npm install 安装的一些依赖包。它们的package.json一般都包含了main字段,用来标识入口js文件。

package.json的main字段

如果没有指定main字段,那么nodejs会默认去加载index.js或者index.node文件。例如:

index.js作为入口js文件

看到这里,是不是突然有点懂了node_modules哪些依赖包的写法了。好的,接着往下看。

我们在c.js中打出日志,观察require方法的结构。

1
console.log(require)

require方法的结构

可以知道,require函数包含了以下属性和方法。

  • require.resolve():将模块名解析,得到该模块的绝对路径
  • require.main:指向当前执行的主模块
  • require.cache:指向所有缓存的模块
  • require.extensions:根据文件的后缀名,调用不同的执行函数

我们再细致看一下,主要看看resolve和extensions

1
2
3
4
5
6
7
8
9
10
11
12
var a = require('./a.js');
var b = require('./b.js');

console.log('resolve测试')
console.log(require.resolve('./a.js'))

console.log('extensions测试')
console.log(require.extensions['.js'].toString())

console.log(require.extensions['.json'].toString())

console.log(require.extensions['.node'].toString())

得到的结果如下图所示:

module的resolve和extensions

缓存

使用require()加载模块是有缓存的,如果要清理缓存,则需要调用delete require.cache,示例如下:

1
delete require.cache[require.resolve('./json/damei_admin.json')];

写到这里,算是对模块有一点初步的认识。接下来我们还需要了解AMD,CMD,UMD的概念。由于篇幅太长,接下来我将分篇叙述这些概念,请阅读后续系列文章!以上观点源于自己的一些理解,如有描述不对的地方,请您指正!


扫一扫下方小程序码或搜索Tusi博客,即刻阅读最新文章!

Tusi博客

You forgot to set the qrcode for Alipay. Please set it in _config.yml.
You forgot to set the qrcode for Wechat. Please set it in _config.yml.
You forgot to set the business and currency_code for Paypal. Please set it in _config.yml.
You forgot to set the url Patreon. Please set it in _config.yml.
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×