什么是模块?我们可能要从需求上出发进行理解,当web应用的规模变得越来越大,业务变得越来越复杂时,我们需要将一些函数分门别类,在分类的基础上对函数进行封装,这就形成了模块。下面看一下js模块的一些形式。
对象模块
假如我们有多个函数,想作为一个模块使用,最原始的做法就是把这几个函数全部放在一个js文件,通过文件的形式来对js进行划分模块。
1 | // my_module.js |
然而这样的做法会污染全局环境,引用这个js后,window对象就会多了两个方法。那么如何减少这种对全局环境的污染呢?想到的最简单的一个办法就是,把这几个函数都放在一个对象中,只暴露一个对象。
1 | // my_module.js |
IIFE
IIFE(immediately invoked function expression),也就是立即执行函数表达式。假设有这样一个场景,你的模块需要定义默认参数,而你又希望这个默认参数不被外界所改变,那么使用对象模块的方式就没有办法做到了,因为这个对象已经暴露在全局环境中。那么如何能隔离作用域呢?聪明的你已经想到了函数,对的,函数可以做到。我们通过IIFE为window挂载了setColor方法。
1 | (function(global) { |
这里的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 | // a.js |
这是最简单的模块写法,a.js通过exports导出add函数,而b.js通过require导入a模块,便可以调用a模块的add函数。
module
那么我们先来看看module这个对象。在b.js中我们console.log(module),则会打印出模块b的信息。
可以看到,模块b的children里有模块a,说明模块b引用了模块a。我们再观察一下模块a,修改a.js的代码如下,再运行b.js
1 | console.log(module) |
可以看到,模块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 | // 如果采用这种改变指向的写法,那么之后exports与module.exports就不一样了。 |
通过exports导出的函数和属性可以被其他模块调用,这一点想必大家都清楚了。
require
这里先说一下模块的分类,NodeJS中模块分为核心模块和文件模块。核心模块是被编译成二进制代码,引用的时候只需require表示符即可,如require(‘fs’),不需要加路径的。而引用文件模块时需要加上路径,表示对文件的引用。假如你加载一个自定义的test.js模块时,没有指定路径,那么它会首先从当前目录的node_modules子目录下寻找test.js,如果没有,则查找上一级目录的node_modules子目录,一直查到盘符的根目录为止。也就是前面提到的module.paths的查找顺序。
说到这里,我们再来回顾一下我们在开发时,npm install 安装的一些依赖包。它们的package.json一般都包含了main字段,用来标识入口js文件。
如果没有指定main字段,那么nodejs会默认去加载index.js或者index.node文件。例如:
看到这里,是不是突然有点懂了node_modules哪些依赖包的写法了。好的,接着往下看。
我们在c.js中打出日志,观察require方法的结构。
1 | console.log(require) |
可以知道,require函数包含了以下属性和方法。
- require.resolve():将模块名解析,得到该模块的绝对路径
- require.main:指向当前执行的主模块
- require.cache:指向所有缓存的模块
- require.extensions:根据文件的后缀名,调用不同的执行函数
我们再细致看一下,主要看看resolve和extensions
1 | var a = require('./a.js'); |
得到的结果如下图所示:
缓存
使用require()
加载模块是有缓存的,如果要清理缓存,则需要调用delete require.cache
,示例如下:
1 | delete require.cache[require.resolve('./json/damei_admin.json')]; |
写到这里,算是对模块有一点初步的认识。接下来我们还需要了解AMD,CMD,UMD的概念。由于篇幅太长,接下来我将分篇叙述这些概念,请阅读后续系列文章!以上观点源于自己的一些理解,如有描述不对的地方,请您指正!
扫一扫下方小程序码或搜索Tusi博客
,即刻阅读最新文章!