在javascript中一开始是没有模块化的概念的,因为javascript最终的设计思想就是运行在浏览器的脚本语言,根本就没有想过使用javascript去开发大型的应用程序或者是后台程序。而随着时代的发展,慢慢的对JS的要求越来越高,使用JS开发大型程序也成为了一种诉求的时候就产生了CommonJS。
模块化
模块化是指将软件中的代码或其他的组成元素进行分割和打包。每一个模块都应该完成一个特定的子功能,所有的模块按照某种方法(视具体的项目要求而定)组装起来,成为一个整体。
这样做的好处就在于我们按照特定的方式进行模块化的打包,将来在本项目中或其他项目实现模块的复用(扩展)并且也不会对现有的其他模块产生不良的影响。
模块化系统基本要求:
- 定义模块
- 暴露模块
- 导入模块
Node.js采用CommonJS规范
在Node.js中采用的模块化规范就是CommonJS的规范。
CommonJS规定:
- 一个文件就是一个单独的模块。
- 模块采用同步的加载模式。
- 模块通过 exports 对象暴露。
- 模块通过 require 函数导入。
- 每一个模块都拥有自己的局部作用域。
一个最基本的模块化实例
想要对一个概念或技术有所了解,说得再多都不如自己走一遍试试来的清楚透彻。
我们创建两个**.js文件,分别为:index.js和util.js**。
util.js
// 在工具模块中定义一些变量和函数
// 将来在别的模块中进行访问和使用
let age = 20
let name = 'tom'
let address = '江苏南京'
// exports 对象是专门暴露模块中属性和方法的对象
// 将所有需要暴露出去的属性和方法都挂载到exports对象上
exports.num1 = num1;
exports.name = name;
exports.address = address;
index.js
// 导入模块
let util = require('./util.js');
console.log(util);// { age: 20, name: 'tom', address: '江苏南京' }
以上就是一个基本的实现Node.js模块管理的方法,exports对象负责将数据暴露出去,require方法负责将模块导入。
Tips
在上面的案例中我们是将所有的数据都绑定在exoprts对象身上的,而代码在书写的时候其实非常麻烦,所以我们也可以采用另外一种写法: module.exports。
// 暴露数据的方式改成对象赋值
module.exports = {
age,
name,
address
};
上面的这种写法是怎么回事呢?
Module
在每一个模块中都拥有一个对象:module,该对象为模块对象。
在module对象中存储的都是关于本模块的信息,包括了文件名、暴露接口、id、parent、路径等等。
具体请看下面的列表:
- id:当前模块的唯一标识,默认为当前模块所处的绝对路径(如果查看的是当前的模块那么就是 ".")。
- filename:当前模块的绝对路径。
- parent:最先引用该模块的模块。如果index.js加载了util.js模块那么index.js就是util.js的父级模块。
- children:当前模块的子级模块,也就是被当前模块加载的模块。
- loaded:模块是否已经加载完成,或正在加载中。
- paths:模块的搜索路径。
- require:已解析的模块的 module.exports。
- 其实我们使用的require( id )完整写法应该是:module.require( id )
- exports:暴露数据的接口属性
- exports === module.exports // true
在module对象下拥有一个exports的属性,它就是我们需要的用于暴露数据的接口。
没错,我们第一次暴露的方式是直接使用exports对象的,而这个对象其实就是module对象下的exports属性。
exports ==== module.exports;// true
上面的代码很清晰的帮助我们理解它们之间的联系,但是我们还是需要注意的是,它们虽然是同一个东西但是它们之间还是有区别的。
- exports是module.exports的快捷方式。
- exports与module.exports的暴露方式有语法上的区别。
// 第一种:错误的写法
exports = {
age,
name,
address
};
// 第二种:正确的写法
module.exports = {
age,
name,
address
};
上面的错误的写法其实无论是从ECMAScript语法上还是从模块化语法上来看都没有错,只是当我们在引入第一种方式的时候在引入模块只会得到一个空对象,这是为什么呢?
地址引用出了问题
首先我们需要知道一个事情,exports 相当于的对 module.exports 属性的直接赋值。
let module = {
exports:{}
};
// 直接赋值操作,相当于复制了module.exports的地址值
let exports = module.exports;
在模块引入的时候其实在引入的是暴露模块的 module.exports 属性,所以如果我们直接对 exports 进行直接赋值的话就会出现地址覆盖的现象。
exports = { name:'tom',age:20 };
上面的操作相当于重新分配了变量exports的地址值,对module.exports不会有任何影响,所以我们导入模块的时候就只能看见一个空对象了。
小练习:在项目根目录下index.js中引入 ./my_modules/user.js模块,并利用user模块的user(class)类创建出一个user对象。
模块分类
在CommonJS规范中对于模块化有几个分类是我们需要了解的。
- File Modules:文件模块,也就是刚刚我们学习的模块化的方式。
- Folders as Modules:文件夹模块
- Node_Modules Folders:从 node_modules 目录加载,第三方模块
- Global Folders:从全局目录加载
- Core Modules:核心模块
文件夹模块
在大型项目中按照CommonJS规范中文件模块的方式来管理我们的模块系统,会出现超级多的模块,这时候是非常不利于我们管理的,所以我们一般这时候都会选择文件夹模块。
下面举一个具体的例子:
// 目录结构
app-|
app.js
util-|
index.js
size.js
app目录为项目的根目录,项目入口为app.js。
我们在app.js中写入
let util = require('./util/index.js');
// 等价于
let util = require('./util');
此时我们可以采用上面第二句代码的简写来导入模块。
之所以可以做到这种简写的格式就是因为Node.js文件模块系统的支持。
在util/index.js中写入
let size = require('./size');
let name = 'array_util';
let version = '1.0.0';
module.exports = {
name,
version,
getSize:size.getSize
};
同时也在util/size.js中写入
function getSize(array){
return array.length;
}
exports.getSize = getSize;
在 app.js 中打印输出util模块的内容。
console.log(util);
/*
{
name: 'array_util',
version: '1.0.0',
getSize: [Function: getSize]
}
*/
到此时我们的基本的文件夹模块的加载就已经实现了,但是有人可能会像如果我并不希望将文件夹模块的入口文件设置成index.js该怎么办呢?
如何修改文件夹模块的入口文件?
对文件夹模块的入口文件的修改其实非常简单,因为Node.js文件夹模块加载机制的原因,在文件夹模块中如果找不到package.json文件就会默认的去找index.js或者index.node,而想修改文件夹模块的入口文件就需要添加一个package.json文件即可。
在util目录下添加package.json文件,并写入
{
"name":"array_util",// 模块的名称
"main":"./main.js" // 指定模块的主入口
}
通过设置上面的 package.json 文件我们就可以指定项目/模块的主入口文件,但是这里需要注意的是,如果 package.json 文件指定的主入口的文件存在的话就会优先去找指定的文件,如果不存在还是会默认去找 index.js 。
加载机制:
- 当我们导入的模块名称是一个文件夹的时候
- 读取过海文件夹下package.json文件。
- 导入package.json文件中main属性指定的文件。
- 如果main属性指定的文件不存在,会默认去找index.js。
Node_Module Folders(第三方模块)
在开发中我们经常会使用第三方的模块。
第三方模块:由其他的软件工程师写好的功能模块,放到网上以便于将来别人在自己的项目使用。
这时候我们就会用到 Node_Module Folders 。
node_modules
在项目的根目录下如果出现了 node_modules 目录,那么它的加载机制就不能再用之前的路径加载机制了。
在 module 对象下有一个属性: paths ,该属性的值为一个数组,数组中保存的就是 node_modules 文件夹下所有的模块的路径查找列表。
我们在app.js中打印出 module.paths 。
console.log( module.paths );
/*
[
'C:\\Users\\admin\\Desktop\\node\\test\\03_folders_modules\\node_modules',
'C:\\Users\\admin\\Desktop\\node\\test\\node_modules',
'C:\\Users\\admin\\Desktop\\node\\node_modules',
'C:\\Users\\admin\\Desktop\\node_modules',
'C:\\Users\\admin\\node_modules',
'C:\\Users\\node_modules',
'C:\\node_modules'
]
*/
通过打印我们看到该属性下拥有很多的路径。
注意:这些路径就是用来查找模块的,并且是从数组的第0位查找到数组的最后一位。
从数组元素中可以看出它的查找规则是从当前项目中开始查找并一直向上一级目录查找,直到查找到当前盘符的根目录为止。这种加载的机制就是第三方包的加载机制。
Global Folders
通过上述的这模块加载机制Node.js可以让node_modules文件夹被当前项目和当前项目的子项目共同使用。既然是这样的话如果我们有一些模块希望被所有的项目都能使用应该怎么办呢?
这就要说到我们的全局模块了,这个模块的文件夹一般都是在 npm 的安装目录中。 找到 npm 的安装目录很轻易的就能够发现里面的 node_modules 目录,而在这里面一般都是通过如下指令安装的模块。
npm module_name -g
npm module_name --global
引入方式
require( module_name );
关于全局模块我们作为了解即可,不做更深的讨论。
Core Modules(核心模块)
核心模块是指 Node.js 自带的功能模块,例如将来我们会常用的http,fs,url,path...等。
核心模块的加载速度最快,因为在加载核心模块时对路径的分析和编译工作这两个步骤是省略的。
我们还需要注意的是,核心模块的加载和判断的优先级都是最高的。核心模块的导入方式
const http = require('http');
const fs = require('fs');
...
关于核心模块后面我们会进行大量的学习,暂时也不做过多讨论。
附加Tips
其实Node.js也支持 ES6 的模块化加载方式,如果你感兴趣的话不妨上网查一查!