第二章:Node学习之Node模块系统
模块系统
前言
本系列文章是通过学习Mosh的视频node教程全方位Node开发-Mosh而整理的笔记,该教程是英文的,有中文字幕,感谢marking1212提供中文字幕翻译
一、Node全局对象
上次我们使用打印命令打印了一些东西到控制台
console.log()
这个console
对象就是我们说的全局对象,它的作用域是全局,也就是说可以在任何地方任何文件调用它,在node
中也有很多其他的全局函数,比如setTimeout()
我们用这个函数设置延迟调用函数的时间,如1s,2s等等 这是标准的JavaScript
,你可以在客户端使用,我们还有 clearTimeout()
还有setInterval()
—给延迟后重复调用函数 clearInterval()-
–清空正在反复调用的函数 这些都是全局函数
在浏览器中window
对象代表的是全局对象,所有被定义为全局函数的对象,都可以通过window
访问,我们可以写成window.console.log
或者直接省略window.
JavaScript引擎会自动前置window
关键字
node
没有 window
对象 有个类似的对象叫做global
,函数都可以通过global
对象调用,我们可以写成global.console.log()
,同样也可以简写去掉前面的global
,但是有一点需要知道的是,这里定义的变量没有添加到global
对象中,换句话说,如果你想打印:
var message ="";
console.log(global.message);
你会在控制台看到未定义,这里定义的变量并没有添加到global
对象中,它的作用域只在它所在的文件中,在文件外它是不可见的,这就是node
的模块化系统所导致的。
二、模块
当我们定义一个函数或变量,它的作用域都是全局的
比如我们定义一个sayHello的函数
var sayHello = function() {};
window.sayHello()
它的作用域是全局,可以通过window
对象访问,这种行为逻辑有个问题,在编程过程中,我们经常讲不同的代码放到不同的文件中 ,也许两个文件中定义了同名的sayHello
函数,因为函数被添加到全局变量,当我们在另一个文件中定义了这个名字的函数,新的定义会覆盖旧的定义 ,这是全局作用域的问题。
为了建立可维护可靠的应用,我们应避免定义全局变量和函数 ,因此我们需要模块化,我们需要创建小型拼装块或者叫模块来存放变量和函数,不同模块之间的同名变量和函数不会相互覆盖 --他们被封在了模块中
node
的核心概念就是模块,每个node
中的文件都被看做模块,每个模块定义的变量和函数作用域就在模块内,用面向对象的观点我们叫他们私有成员 ,他们在容器外,也就是模块外是不可见的,如果要在模块外使用一个定义在模块中的变量或函数,我们需要明确导出他为公开成员
每个node
工程至少要包含一个文件或者说一个模块——主模块 例如:
console.log(module);
在node
中,每个文件都是模块,模块中定义的成员作用域只在模块中,他们在模块外是不可见的
1.创建模块
我们给应用添加一个模块,创建一个logger.js
的文件,
假设我们为记录信息创建一个模块,在模块中我们假设要使用一个远程日志服务来记录我们的日志,有个网站可以提供日志服务,它提供了一个url,可以通过给它发送Http
请求来记录日志,这里我们可以定义一个变量url
var url - 'http://mylogger.io/log';
function log(message) {
// Send an HTTP request
console.log(message)
这并不是一个真实的地址,我们就假设这里会向这个地址发送请求,这里的变量和函数的作用域都是这个文件,他们是私有的,在外部不可见。但是我们想在app.js
也就是我们的主模块在想用到logger.js
,我们应该访问log函数,我们需要将它变为公共的,可以在外部访问它,在module
对象里面有个exports
属性,所有添加到这个对象的属性将可在外部访问,所以回到logger.js
我们要设置:
module.exports.log = log;
这样logger
模块就做好了,我们就能在app.js
中使用它了
2.加载模块
加载模块需要用到require
函数,这是node
才有的函数,浏览器里没有,这个函数需要一个参数,也就是我们想要加载的模块名称 require
函数返回参数模块导出的对象
var logger = require('./logger');
console.log(logger);
require
函数返回参数是模块导出的对象,就是exports
对象,这就是require
函数得到的东西 ,所以控制台运行node app.js
我们能得到 { log:[function: log] }
对象,里面有一个函数log , 所以我们可以在app.js
中调用这个函数了
这就是node中模块的工作方式,定义一个模块,导出一个或多个成员,为了使用模块,我们使用require
函数
在当前版本的JavaScript
中,我们可以定义常量了,所以导入的模块应该保存在常量中,以为我们有可能意外的将logger
重新赋值,赋值后,当我们在调用log
方法就会有异常,如果我们定义它为一个常量的话,就会出现 Assignment to constant variable
的问题我们有些专门检查这类错误的工具,使用它们可以避免在运行时出现问题,一个很有名的工具是jshint
,通过jshint
这类工具可以捕捉所有类似的问题,这就是将logger
设置为常量的好处。
有时候我们不需要导出一个对象,我们只有一个简单的函数 ,对象在有多个属性或者方法时才用得到,我们可以只导出一个函数而不是一个对象,我们可以将exports
直接赋值给函数 这样不用设置为对象,我们直接设置为函数 例如:
module.exprots = log;
我们将exports
直接赋值给log
函数,这样logger
就不在是一个对象,它是一个我们可以直接调用的函数
3.模块包装函数
我们已经知道了node
模块中定义的变量和函数的作用域只在当前模块内,node
是如何实现的呢?
实际上,node
并没有直接运行代码,而是包装在一个函数中,在运行时,我们的代码被转换成这样,我们拿logger
模块来举例。
(function (exports, require, module, __filename, __dirname) {
var url = 'http://mylogger.io/log'
function log(message) {
console.log(message)
}
module.exports = log
})
复制代码如果你是一个有经验的JavaScript
开发者,你可能知道这是立即调用函数表达式,也叫做IIFE
。如果你不清楚也不要紧,这并不是node
的内容,这边想表达的是node
不直接执行代码,node
总是将代码包裹在这样的一个函数中。
看看这个函数的参数,看下require
,这个require
看起来像全局的但实际不是,事实上它是每个模块本地的,在每个模块中,require
都是作为参数传给函数,我们称之为模块包装函数。
还有module
参数,还有module.exports
简写为exports,所以当你想将函数公开的时候可以这么写
module.exports.log = log
复制代码也可以这么写
exports.log = log
复制代码但是如果没有module
对象引用就不能重置exports
对象,换句话说,不能给exports
对象赋值。
exports = log
// 不要这么写
复制代码因为这个exports
是module.exports
的一个引用,你不能更改它的引用。
还有__filename
和__dirname
分别代表文件名和目录名,我们打印出来看一下。
console.log(__filename)
console.log(__dirname)
var url = 'http://mylogger.io/log'
function log(message) {
console.log(message)
}
module.exports = log
复制代码回到控制台,运行程序,打印结果如下
F:\2020\study\Node.js\node-course\first-app\logger.js
F:\2020\study\Node.js\node-course\first-app
message
4.路径模块(Path module)
路径模块:path
模块提供了用于处理文件和目的路径的实用工具。 可以使用以下方式访问它:
const path = require('path');
操作系统OS module: 获取当前操作系统信息
os
模块提供了与操作系统相关的实用方法和属性。 可以使用以下方式访问它:
const os = require('os');
os.freemem()
:当前可用内存大小
os.totalmem(
):总内存大小
os.userInfo([options])
:可以得到当前用户信息
os.uptime()
:可以得到开机时间
ES6新特性:模板字符串 Template string 可以避免使用字符串拼接
console.log('Total Memory' + totalMemory); =>
console.log(Total Memory: ${totalMemory});
JavaScript被设计为只在浏览器里运行,这样就只能操作window
或者document
对象,我们就不能获取操作系统的信息
5.文件系统模块(File system module)
const fs = require('fs');
fs
几乎所有的方法都分为两类,同步或阻塞的方式,和异步或者非阻塞的方式,比如access
,就是一个异步方法 ;还有accessSync
就是同步方法 ,就算是有同步方法也要避免使用 ,现实中应该使用异步方法,因为这是非阻塞的。 node
是单线程的 ,如果你使用node来创建你应用的后端,你可能有成百上千的客户接入后端,如果单线程时刻忙碌,就无法服务众多客户端,所以永远使用异步方法
同步方法(Synchronous)和异步方法(Asynchronous)的区别:
同步方法调用在程序继续执行之前需要等待同步方法执行完毕返回结果
异步方法则在被调用之后立即返回以便程序在被调用方法完成其任务的同时执行其它操作
- 异步请求是进行局部刷新,同步请求是进行整体刷新
- 异步请求是由ajax的引擎发起的,同步请求是由浏览器发起的
- 异步请求在请求发起之后还没收到响应之前还可以再次发起其它请求不影响当前页面的操作,而同步请求在发起之后,只有收到了响应后才能进行其它操作。
所有异步方法都用一个函数作为最后的一个参数,node
会在异步操作完成后自动执行函数,我们称这种函数为回调函数
fs.readdir('$', function (err, files) {
if (err) console.log('Error', err);
else console.log('Result', files);
});
为了在node
中操作文件或路径,首先要导入fs模块,然后就可以使用模块中的方法,所有的方法都是成对的,同步或者异步的,记住永远使用异步方法。
6.事件模块(Events Module)
node
中一个核心概念就是事件,事实上很多node
的模块都是基于事件的。事件就是提示程序中发生了什么的信号? 例如node
中有个模块是http
可以用来创建网络服务,我们监听给定的端口,每次我们在这个端口得到请求,http
类就会发起一个事件,我们的工作就是响应这个事件,具体说就是读取请求内容,并给出对应的反馈
不同的模块发起不同的事件,你的代码关心的是如何反馈这些事件
如何操作事件模块(Events Module)?
在我们的node
文档中有Events
(事件触发器) 模块 ,里面有个类叫做EventEmiiter
,这是node
的核心模块之一,很多类是基于这个EventEmitter
类的, 如何操作这个类?
const EventsEmitter = require('events');
我们导入events
模块,这里我们导入的就是EventEmitter
类,注意 EventEmitter
每个单词的第一个字母是大写,这是一种约定俗成,说明EventEmitter
是一个类(class) ,这不是一个函数(function),不是一个简单的值(value),而是一个类(class).
类(class)是包含属性和函数的容器,函数也叫方法
为了使用EvenEmitter
首先我们要创建类的实例:
我们将emitter设置为一个EventEmitter
对象
const emitter = new EventEmitter();
如果你不清楚实例和类的区别可以给你打个比方
类就像是人类,实例是具体的某个人,类定义了人应该具有的属性和行为特征,实例是类的具体某一个对象
这里第一个EventEmitter
是一个类,这是设计一个emitter
能做什么
第二个emitter
是一个对象,是我们在应用中用到的
emitter
中有很多方法,大部分时间我们只用到其中两个:
一个是emit
,是用来发起一个事件的 (emit
意思是制造一个噪音或者产生什么)
emitter.emit('messageLogged');
这里,我们传入一个参数,就是事件的名称,比如我传入messageLogged
,以后扩展logger
模块,每次记录一个日志都要发起一个事件
如果我们这时运行程序什么都不会发生,因为我们发起了事件,我们的应用中没有任何人注册了对这个事件感兴趣的监听器,监听器是当事件发生时被调用的函数,现在我们来注册关于messageLogged
事件发生的监听器
emitter
中有个方法叫addListener
,这里我们有个更常用 那就是on
如果你用过jQuery
,你就见过了on
和addListener
是一样的,但是on
方法更加常用
emitter.on('messageLogged', function () {
console.log('Listenner called');
});`
on
方法需要两个参数第一个是事件的名称,这里就是messageLogged
,第二个是回调函数,也就是实时监听者,我们传入一个方法,这个方法在事件发生时被调用
这里要注意,如果你发起方法之后才定义监听器,什么都不会发生,因为当你发起事件时,emit
遍历了所有的监听者
事件参数(Event Arguments)
经常我们在发起事件的时候想带点数据,例如在logger
模块中当我们记录日志时,我们的服务可能想创建一个日志的编号之后返回给客户端,或者给他一个url
,可以直接访问日志的信息 所以发起事件的时候,我们可以带一个参数作为事件的参数,比如可以添加一个id值为1,然后添加一个url
emitter.emit('messageLogged', 1, 'url');
如果我们需要给事件加上多个数据,更好的方式是将这些数据封装在对象中,多以我们创建一个对象,我们成这个对象为事件的参数
emitter.emit('messageLogged', { id: 1, url: 'http://' });
当注册了一个监听器之后,这个监听者也可以得到事件的参数
emitter.on('messageLogged', function (arg)
我们给监听器添加一个arg参数,你叫他什么都行,这个名字不重要,但是约定俗成一般用arg,或者某些人用e或者eventArg,我们得到了arg,就在控制台打印打印显示出来
Listenner called { id: 1, url: 'http://' }
我们得到了信息,也看到了事件的参数对象,使用这样的技巧就可以传递事件发生时的数据了,还有一个让代码变简洁的技巧,在ES6
中有个语法叫箭头函数,使用箭头函数,可以去掉function
关键字,在arg
参数之后就是函数体,为了分开两部分,中间使用一个箭头,所以叫箭头函数
emitter.on('messageLogged', (arg) => {}
这就是代码简化了,很多人喜欢用这个新语法。
扩展事件参数(Extending EventEmitter)
现实编程中很少直接使用EventEmitter,相反,你会创建一个类似拥有所有EventEmitter的功能然后使用它
打开我们的logger模块,这个模块我们导出了logger函数,这里我们打印了内容,在这之后我想发起一个事件,然后在app模块中监听然后做点事:
const EventEmitter = require('events');
const emitter = new EventEmitter();
var url = 'http://mylogger.io/log';
function log(message) {
// Send an HTTP request
console.log(message);
}
// Raise an event
emitter.emit('messageLogged', { id: 1, url: 'http://' });
module.exports = log;
这里我们要加载logger
模块,并且调用log
函数,设置常量log
导入logger
模块
const EventEmitter = require('events');
const emitter = new EventEmitter();
// Register a listener
emitter.on('logging', (arg) => {
console.log('Listener called', arg);
});
const log = require('./logger');
log('message');
当我们运行代码时,只会看到message
字符串,不会调用监听器,这是因为我们操作着两个不同的EventEmitte
r ,
在app.js中 const emitter = new EventEmitter();
是一个EvenEmitter
对象;而在logger
模块中是另一个EventEmitter
对象实例,类就是蓝图,对象就是类的实例,比如说,有一个类是人,但是人类的实例可以说张三,李四,王五等等,这个例子中有两个不同的对象 ,在logger
模块中,你使用一个emitter
对象来发起事件,但是在app
模块中使用了另一个EventEmitter
对象来处理这个事件,这是完全不同的,当我们注册一个监听器,这个监听器只在当前模块的EventEmitter
对象注册,与别的无关,这就是不直接使用EventEmitter
的原因,相反要创建一个继承并扩展了EventEmitter
所有能力的类 ,这个例子我们要创建一个Logger
类,并且拥有一个扩展的log
方法
首先要创建一个类,在ES6中有个新的关键字class
,这是一个创建构造函数的语法,这样就可以定义一个Logger
类(注意:类名的第一个字母都应该大写,这是用于类命名的帕斯卡命名法),在类体中创建的函数不需要function
关键字,当一个函数在类体中,就说是这个类的方法。
为了让现在的Logger
类完全具备EventEmitter
的所有功能,实现的方法就是使用ES6
新增的extends
关键字
这里Logger
类就具备了EventEmitter
所有的功能和特性,之后发起事件,不需要在使用emitter
对象,我们换为this
关键字.这个类就可以发起事件了。
我们不再需要emitter
对象,我们直接操作logger
对象就可以了
我们来把代码改写一下,首先是logger
模块
class Logger {
log(message) {
console.log(message)
this.emit('messageLogged', { id: 1, url: 'http://' })
}
}
首先需要创建一个类,使用ES6
中的class
关键字来创建一个Logger
类,并扩展一个log
方法。
class Logger extends EventEmitter {
log(message) {
console.log(message)
this.emit('messageLogged', { id: 1, url: 'http://' })
}
}
最后logger
模块完整代码如下
const EventEmitter = require('events')
class Logger extends EventEmitter {
log(message) {
console.log(message)
// Raise an event
this.emit('messageLogged', { id: 1, url: 'http://' })
}
}
module.exports = Logger
接下来,我们改写下app
主模块,代码如下
const Logger = require('./logger')
// Create new logger Object
const logger = new Logger()
// register a listener
logger.on('messageLogged', (arg) => {
console.log('Listener called', arg)
})
logger.log('message')
回到控制台,运行程序,打印结果如下
message
Listener called { id: 1, url: 'http://' }
7.HTTP模块
node
中有一个非常强大的模块就是用于创建网络应用的HTTP
模块。例如我们可以创建一个服务监听某个给定端口,这样我们就可以为客户端创建一个后端服务,就像React
或者Angular
创建的应用或者在手机上使用的移动端应用。
接下来我们导入 HTTP
模块,创建一个网络服务:
const http = require('http')
const server = http.createServer()
有趣的是这个server
是一个EventEmitter
,它具备你之前看到的所有EventEmitter
的功能,输入server
.就可以看到on
方法和add
监听器方法。我们可以在官网文档找到http.Server
类,这个类继承自net.Server
,这是另一个定义在net模块中的类,而net.Server
又继承自EventEmitter
,这就是为什么之前说的node
中很多功能都是基于EventEmitter
。
当有一个请求或者连接是,serve
就会发起事件,我们就可以使用on
方法来处理事件
const http = require('http')
// create new server
const server = http.createServer()
// register an event
server.on('connection', (socket) => {
console.log('New connection...')
})
// listen a port
server.listen(3000)
console.log('Listening is on port 3000...')
回到控制台,运行app.js
,控制台打印结果
Listening is on port 3000...
然后我们打开浏览器,地址栏输入localhost:3000
回车,回到控制台,能看到输出
New connection...
而在真实的编程中,是不会发起connection
事件然后处理的,这样的做法太低级了,我们常用的做法是给createServer
方法一个回调函数,这个函数需要两个参数,分别是请求和反馈,这个函数中,我们可以直接操作真实的request
和response
对象
const http = require('http')
const server = http.createServer((req, res) => {
if (req.url === '/') {
res.write('Hello world')
res.end()
}
})
server.listen(3000)
console.log('Listening is on port 3000...')
如果我们想要创建一个网络应用的后端服务,我们需要处理很多的路由规则,我们需要另一个if代码块,如果url
是 /api/course
这样我们想从数据库返回课程的列表
const http = require('http')
const server = http.createServer((req, res) => {
if (req.url === '/') {
res.write('Hello world')
res.end()
}
// 返回课程列表
if (req.url === '/api/courses') {
res.write(JSON.stringify([1, 2, 3]))
res.end()
}
})
server.listen(3000)
console.log('Listening is on port 3000...')
创建网络服务虽然是很容易的,但是现实中我们不会使用HTTP
模块直接创建后端服务,理由是你看到当这里的规则越来越多的时候,代码会变得很复杂,因为我们都是在回调函数中线性的增加它们的内容,取而代之,我们使用一个叫Express
的框架,它可以给应用一个清晰的结构,来处理不同的路由请求,我们使用Express
来代替node
原有的HTTP
模块的功能.