CommonJS规范概述

本文内容是自己的一个笔记,内容和原文相差不大,在原文的基础上,修改了一部分结构,添加了自己的一部分内容
原文看的是阮一峰老师的文章
可以通过链接去看

一.CommonJS

1.概述

CommonJS是node采用的模块化规范

1.模块
在node中,每一个文件就是一个独立的模块,有着自己的作用域。在一个模块中定义的变量,函数,类等,都是该模块私有的,其他模块不可见。

// a.js
let age=21
function idd(){
    console.log(age);
}

上面代码中,这些属性和方法都是独属于模块a.js的,其他文件读取不到,比如另一个模块main.js试图输出age,则会报错

// main.js
console.log(age);
ReferenceError: age is not defined

要想让其他模块能够读取到该模块的变量,可以将变量定义为global对象的属性,其他组件require后就可以通过global对象使用该属性。

// a.js
// let age=21
global.age=21
function idd(){
    console.log(age);
}
// main.js
require('./a')
console.log(global.age);
[nodemon] starting `node main.js`
21

上面的方法切实可行,但并不推荐,因为它不符合CommonJS规范

2.CommonJS规范
CommonJS规范规定,在每个模块中,存在一个module对象,代表当前模块自身,他有一个属性exports,是对外的接口,也就是说,当加载某个模块时,就是在加载这个模块的module.exports属性

下面就是通过把变量核函数挂载到module.exports上实现输出,其他模块通过require加载该模块后就可以使用。

// a.js
let age=21
function idd(){
    console.log(age);
}
module.exports.age=age
module.exports.idd=idd
// main.js
let a=require('./a')
console.log(a);
{ age: 21, idd: [Function: idd] }

3.CommonJS的特点
①所有代码都运行在模块作用域,不会污染全局作用域。
②缓存机制,模块可以多次加载,但是只会在第一次加载时运行一次,然后就会将其缓存,此后再加载就直接读取缓存结果,若想重新加载运行,可以清除缓存
③模块加载顺序就是在代码中出现的顺序
④CommonJS模块加载是同步执行
⑤模块加载机制为,输入的是被输出值得拷贝

2.module对象

1.module对象的属性
node提供一个构造函数Module,每一个模块都是Module的实例
每个模块内部都有一个module对象,代表当前模块本身,它有着诸多属性,如下所示

module.id:模块识别符,通常是带有绝对路径的模块文件名
module.filename:模块文件名,带有绝对路径。
module.loaded:返回一个布尔值。表示模块是否已经完成加载
module.parent:返回一个对象,表示调用该模块的模块
module.children:返回一个数组,表示模块要用到的其他模块
module.exports:表示模块对外输出的值
module.paths:模块的目录名称
module.paths:模块的搜索路径

Module {
  id: '.',
  path: 'F:\\desk\\commontest',
  exports: {},
  filename: 'F:\\desk\\commontest\\main.js',
  loaded: false,
  children: [
    Module {
      id: 'F:\\desk\\commontest\\a.js',
      path: 'F:\\desk\\commontest',
      exports: [Object],
      filename: 'F:\\desk\\commontest\\a.js',
      loaded: true,
      children: [],
      paths: [Array]
    }
  ],
  paths: [
    'F:\\desk\\commontest\\node_modules',
    'F:\\desk\\node_modules',
    'F:\\node_modules'
  ]
}

tips:可以通过module.parent判断当前模块是否为入口脚本
首先说一下,如果两个模块,a.js调用了b.js,直接node b.js,输出的module.parent就是null,但是如果在脚本a.js中调用b,输出的module.parent就是调用它的模块。
这样就可以通过调用程序时,在每个模块内进行parent值判断,获悉哪个是入口脚本

2.module.exports属性
module.exports属性就是模块对外输出的接口,其他文件加载该模块,本质上就是读取module.exports变量。
events模块
该模块只提供了一个对象,events.EventEmitter,它的核心功能就是事件触发与事件监听器功能的封装。
模块引入

let EventEmitter=require('events').EventEmitter

实例创建

let event =new EventEmitter()

使用

let EventEmitter=require('events').EventEmitter
var event =new EventEmitter()
// 触发事件
setTimeout(function(){
    event.emit('ready')
},3000)
// 绑定事件
event.on('ready',function(){
    console.log('事件触发了');
})

module.exports使用

// event.js
let EventEmitter=require('events').EventEmitter
// 将实例赋给module.exports导出
module.exports =new EventEmitter()
// 延时发布事件
setTimeout(function(){
    module.exports.emit('ready')
},3000)

上面的event模块会在3s后发布ready事件
下面的文件因为调用了模块,可以监听到该事件

// main.js
let event=require('./event')
// 加载模块并监听事件
event.on('ready',function(){
    console.log('监听到');
})

3.exports变量
exports变量是node为了提供方便,为每个模块提供的一个变量,指向module.exports,相当于在每个模块的头部,执行了如下命令。

var exports =module.exports

这样的话,可以使用exports挂载属性方法进行输出,

module.exports.ahh=123
console.log(exports);//{ ahh: 123 }
exports.whh=233
console.log(module.exports);//{ ahh: 123, whh: 233 }

注意:
①不能将exports指向某个值,这样会切断与module.exports的联系

exports='new'
console.log(module.exports);//{}

②也不能在最后将module.exports指向某个值,因为这样module.exports被重新赋值,无法输出exports挂载的属性方法。

exports.whh=233
console.log(module.exports);//{ whh: 233 }
module.exports='new'
console.log(module.exports);//new

③如果觉得二者难以分清,就直接抛弃exports,直接用module.exports,省心

3.require命令

node使用CommonJS规范,内置的require命令用于加载模块
1.加载模块
require命令会读入执行一个js文件,然后返回该模块的exports对象
(第一次加载模块会执行文件,所以模块内的一些诸如console.log的命令会执行一次。之后就会缓存模块,直接从缓存中读取,便不会再执行)

模块通过module.exports导出指定的方法和属性

// example.js
console.log('模块执行了');
module.exports.age=500
module.exports.act=function(){
    console.log('复活卡莲');
}

主文件使用require加载模块

// main.js
let example=require('./example')
console.log(example);
example=require('./example')
console.log(example);

如下为输出的结果,可见只有第一次加载模块会运行文件,之后因为是从缓存中读取,不再执行文件。

模块执行了
{ age: 500, act: [Function (anonymous)] }
{ age: 500, act: [Function (anonymous)] }

2.加载规则
require首先会从缓存中读取模块,如果缓存中没有,便会根据路径寻找文件
路径分析
根据不同的格式,require命令会去不同路径寻找模块文件
①以‘/’开头,则加载的是一个文娱绝对路径下的模块文件
②以‘./‘或者’…/’开头,则加载的是一个位于相对路径的模块文件
③不以‘/’或者‘./‘或者’…/’开头,则代表加载的是核心模块,或者位于各级node_modules目录的已安装模块
首先会去node的安装文件夹搜索文件,如若没有,回去搜索各级node_modules文件夹(从同级逐步到上级)
④如果无开头符号,且是一个文件路径,就会先找到开头文件所在位置,然后找到后续路径,
(比如require('example-module/path/to/file'),则将先找到example-module的位置,然后再以它为参数,找到后续路径)

文件扩展名
如果文件不包含扩展名,node会按照.js,.json,.node顺序依次补充到文件后,再去搜索。

require.resolve()方法
用于解析模块的具体位置
①相对路径,会查找同路径下的模块位置
require.resolve(‘./index’)
②如果只有名字,且为核心模块,则只会输出名字,非核心模块,会在node_modules中寻找,然后输出路径
require.resolve(‘path’)

3.目录加载规则
一般我们会把相关文件放在一个目录文件夹中,然后设置一个入口文件
我们需要在目录中放置一个package.json文件,并写入main字段,
require发现参数字符串指向一个目录后,会自动查看该目录下的package.json文件,然后加载main字段指定的入口文件,如果没有main或者package文件,就会默认加载目录下的index.js或者index.node文件

4.模块缓存
第一次加载某个模块,会读取运行该模块,并进行缓存,之后再加载模块,就直接从缓存中取出模块的exports属性
缓存例子
如果想要多次执行某个模块,可以让模块输出一个函数,这样每次require模块时,就会重新执行输出函数
所有的缓存都保存在了require.cache中,如果要清除缓存,可以执行下面的命令。

清除缓存
// 删除指定模块的缓存
delete require.cache[modulePath];

// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key) {
  delete require.cache[key];
})

注意:
modulepath为绝对路径,所以我们可以使用resolve获取绝对路径

delete require.cache[require.resolve('相对路径')]

5.循环加载
在模块调用中,可能会出现a加载b,b又加载a的循环加载,CommonJS的应对方法是,一旦出现此类情况,被循环加载的模块就只输出已经执行的部分。
这么说可能还是不懂,下面看个例子

// a.js
exports.x='a1'
console.log('a.js',require('./b').x);
exports.x='a2'
// b.js
exports.x='b1'
console.log('b.js',require('./a').x);
// console.log('b.js',exports.x);
exports.x='b2'
// main.js
console.log('main.js',require('./a').x);
console.log('main.js',require('./b').x);

输出结果

b.js a1
a.js b2
main.js a2
main.js b2

还是看不懂?正常,我刚看到的时候也是一头懵,自己敲了一遍才搞懂。
我们来一步一步地看程序的执行,如下所示

// main.js
console.log('main.js',require('./a').x);//首先执行main的第一句,遇到了require,那么就会先去加载a模块
    // a.js
    exports.x='a1'
    console.log('a.js',require('./b').x);//第一次加载要先运行一次,运行到此处,又遇到一个require,接着去加载b模块
        // b.js
        exports.x = 'b1';
        console.log('b.js ', require('./a.js').x); //运行到此处,遇到了require要加载a,这里a就是被循环加载的模块,规范提到只输出已经执行的部分 ,在这里就是指这上面a模块中已经执行的两行,这里获取到的x值就是a1 
        // 输出b.js a1
        exports.x = 'b2';
    // b模块全部执行完后,a这里就可以输出了,输出a.js b2
    exports.x = 'a2';
// 这里也是,a执行完,main第一行可以输出,输出main.js a2
console.log('main.js ', require('./b.js').x);//b模块已经执行过一次,进入缓存,这里加载直接从缓存中获取数据,不会再运行模块了
// 输出main.js b2

6.require.main
该方法可以判断模块时直接执行还是被调用执行,直接执行时指向模块本身

// a.js
let b=require('./b')
exports.x='a1'
console.log('a',require.main===module);
// b.js
exports.x='b1'
console.log('b',require.main===module);
// main.js
let a=require('./a')
console.log('main',require.main===module);
b false
a false
main true

这样可以来判断谁是入口文件。

7.require常用命令

require(): 加载外部模块
require.resolve():将模块名解析到一个绝对路径
require.main:指向主模块
require.cache:指向所有缓存的模块
require.extensions:根据文件的后缀名,调用不同的执行函数

8.require加载流程
这部分先简单了解一下就好
require并不是一个全局命令,他只想当前模块的module.require命令,后者又调用node内部命令

Module._load = function(request, parent, isMain) {
  // 1. 检查 Module._cache,是否缓存之中有指定模块
  // 2. 如果缓存之中没有,就创建一个新的Module实例
  // 3. 将它保存到缓存
  // 4. 使用 module.load() 加载指定的模块文件,
  //    读取文件内容之后,使用 module.compile() 执行文件代码
  // 5. 如果加载/解析过程报错,就从缓存删除该模块
  // 6. 返回该模块的 module.exports
};

上面的第4步,采用module.compile()执行指定模块的脚本,逻辑如下。

Module.prototype._compile = function(content, filename) {
  // 1. 生成一个require函数,指向module.require
  // 2. 加载其他辅助方法到require
  // 3. 将文件内容放到一个函数之中,该函数可调用 require
  // 4. 执行该函数
};

4.模块加载机制

CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值

// a.js
let x='a1'
let change=function(){
    x+='233'
    console.log(x);
    
}
module.exports={
    x,change
}
// main.js
let a=require('./a')
console.log(a.x);
a.change()
console.log(a.x);
a1
a1233
a1

可见模块内x值发生了变化,但并不影响main内x的值

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值