作者:大志前端
链接:https://juejin.cn/post/6844904057618825229
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
前言
本系列文章是根据Mosh大佬的视频教程全方位Node开发 - Mosh整理而成,个人觉得视频非常不错,所以计划边学习边整理成文章方便后期回顾。该视频教程是英文的,但是有中文字幕,感谢marking1212提供的中文字幕翻译。
本篇文章大纲
- Node常用内置模块
- Path 路径模块
- OS 系统模块
- File System Module 文件系统模块
- Events 事件模块
- HTTP模块
Node常用内置模块
我们打开官网Node.js v12.14.1 Documentation,可以看到Node有很多的内置模块,但是这个列表并不是所有都是模块,比如Console就是个对象,还有Buffer是一个全局对象,这些后面会学习到,这里标注一些经常用到的。
- File System 操作文件系统
- HTTP 使用它可以创建监听HTTP请求的网络服务
- OS 操作系统
- Path 提供很多工具可以操作路径
- Process 给我们现在正在处理的信息
- QueryString 创建HTTP服务的时候非常有用
- Stream 用来操作数据流
Path 路径模块
我们打开官网文档,点击进入Path模块的地址Path,可以看到这个模块的所有函数,也有具体的使用方法,可以自己查看文档。
我们先来看下parse函数
const path = require('path') // 引入内置path模块
var pathObj = path.parse(__filename) // 传入之前在模块包装函数中看到的参数__filename
console.log(pathObj)
复制代码
下面是控制台打印结果
{ root: 'F:\\',
dir: 'F:\\2020\\study\\Node.js\\node-course\\first-app',
base: 'app.js',
ext: '.js',
name: 'app' }
复制代码
返回的对象有很多有用的属性
- root 根路径
- dir 文件夹路径
- base 文件名
- ext 扩展名
- name 去除扩展名的文件名
OS 系统模块
通过OS系统模块,我们可以获取操作系统的信息,官方文档OS。
- freemem() 返回当前可用的内存有多少
- totalmem() 总内存的大小
- userInfo 当前用户的信息
- uptime() 开机时间
- ...
我们来使用几个方法
const os = require('os') // 引入内置的os模块
var freeMem = os.freemem() // 可用内存
var totalMem = os.totalmem() // 总内存
console.log(`freeMem: ${freeMem}`)
console.log(`totalMem:${totalMem}`)
复制代码
控制台打印结果如下
freeMem: 3998625792
totalMem:8468672512
复制代码
在Node之前,用JavaScript是获取不到这些信息的。
JavaScript被设计为只在浏览器里运行,这样就只能操作window或者document对象,我们不能获取操作系统的信息。
但是到了Node,JavaScript可以在浏览器外执行,或者说在服务器上,这样我们就可以访问操作系统,操作文件,操作网络,比如可以监听某个特定端口的HTTP请求。
File System Module 文件系统模块
打开官网文档File System,可以看到文件系统模块有非常复杂的操作文件和路径的各种方法,我们不会每个方法都看,因为重复的有很多,我们来看个例子。
const fs = require('fs') // 引入fs模块
const files = fs.readdirSync('./') // 同步的方法,返回当前文件夹下的所有文件夹和文件
console.log(files) // => [ 'app.js', 'logger.js' ]
复制代码
我们输入fs.,通过编辑器的代码智能提示,我们可以看到几乎所有的方法都分为两类,同步或者阻塞的方法,和异步或者非阻塞的方法。
我们要避免使用同步方法,应该使用异步方法,因为这是非阻塞的。之前我们有学习到Node是单线程的,如果你使用Node来创建你应用的后端,你可能有成千上百的客户端接入后端,如果单线程时刻忙碌,就无法服务众多客户端,所以永远使用异步方法。
我们来看下异步的方法
const fs = require('fs') // 引入fs模块
// 异步方法
fs.readdir('./', function(err, files) {
if (err) {
// 简单处理下错误,不适合用在实际开发中
console.log('Error', error)
} else {
console.log(files)
}
})
复制代码
控制台输出结果是一样的。
我们模拟一个异常,把路径改成一个随便的字符
const fs = require('fs') // 引入fs模块
// 随便输入一个不存在的路径
fs.readdir('test', function(err, files) {
if (err) {
// 简单处理下错误,不适合用在实际开发中
console.log('Error', error)
} else {
console.log(files)
}
})
复制代码
控制台就会输出报错信息。
第一个参数是路径,所有的异步方法都用一个函数作为最后一个参数,Node会在异步操作完成后自动执行函数,我们叫这种函数为回调函数。
通过代码智能提示我们可以看到回调函数的第一个参数是异常,第二个参数是结果,是一个字符串数组。
这里我们要检测是否有err或者files,只有一个会有值,另一个是null。
Events 事件模块
Node中一个核心的概念就是事件。
事实上很多Node的模块都是基于事件的。
事件就是提示程序中发生了什么信号,例如Node中有个模块是HTTP,可以用来创建网络服务,我们监听给定的端口,每次我们在这个端口得到请求,HTTP类就会发起一个事件,我们的工作就是响应这个事件,具体说就是读取请求内容,并给出对应的反馈。
看下Node的文档,你可以看到很多不同的模块发起不同的事件,你的代码关心的是如何反馈这些事件。
我们打开官方文档Events模块,找到里面的一个类EventEmitter,这个Node的核心模块之一,很多类都是基于这个EventEmitter的,我们来看看如何操作这个类。
// 因为这是一个类,根据规范,我们首字母大写,代表这不是一个函数,不是一个简单的值,而是一个类
// 类是包含属性和函数的容器,函数也叫方法
const EventEmitter = require('events') // 导入EventEmitter
// 创建一个实例对象
// 实例和对象的区别:打个比方,类就像是人类,实例是具体的某个人,比如John,Mary等。
// 类定义了人应该具有的属性和行为特征,实例是类的一个具体对象
const emitter = new EventEmitter()
复制代码
这个emitter有很多方法,但是大部分情况我们只用到其中两个,一个是emit,是用来发起一个事件的,这个方法需要传一个参数,即事件的名称。
接下去我们要扩展logger模块,每次记录一个日志都要发起一个事件。
const EventEmitter = require('events')
const emitter = new EventEmitter()
// 发起一个事件
emitter.emit('messageLogged')
复制代码
现在如果我们运行程序,是不会有任何结果的,因为我们的应用中没有任何地方注册了对这个事件感兴趣的监听器,监听器是当事件发生时被调用的函数。
现在我们来注册关注messageLogged事件发生的监听器
const EventEmitter = require('events')
const emitter = new EventEmitter()
// 注册一个监听器
// 用addListener()和on()都可以,on()更常用,接受两个参数:第一个是事件名称,第二个是回调函数,也就是事实上的监听者
emitter.on('messageLogged', function () {
console.log('Listener called')
})
// 发起一个事件
emitter.emit('messageLogged')
复制代码
运行程序,控制台会输出
Listener called
复制代码
要注意这里的顺序很重要,如果你在发起事件之后才注册监听器,什么都不会发生,因为当你发起事件时,emit遍历了所有的监听者。
Event Arguments 事件参数
经常我们在发起事件的时候想带点数据,例如在logger模块中当我们记录日志时,我们的服务可能想创建一个日志的编号之后返回给客户端,或者给它一个URL,可以直接访问日志的信息,所以发起事件的时候,我们可以带一个参数作为事件的参数,比如可以添加一个id值1,然后添加一个url。
// 发起一个事件,带一个对象参数
emitter.emit('messageLogged', { id: 1, url: 'http:// '})
复制代码
我们称这个对象为事件的参数。
当注册一个事件的时候,监听者也可以得到事件的参数,我们添加一个arg参数,这个名字可以随便取,但是约定俗成一般用arg,或者用e或者用eventArg。
emitter.on('messageLogged', function (arg) {
console.log('Listener called', arg)
})
复制代码
运行程序,控制台打印结果如下
Listener called { id: 1, url: 'http://' }
复制代码
我们得到了信息,也看到事件的参数对象。
这边, 我们还可以使用ES6中的箭头函数来简化注册事件的代码
emitter.on('messageLogged', (arg) => {
console.log('Listener called', arg)
})
复制代码
Extending EventEmitter 扩展事件参数
现实编程中,很少直接使用EventEmitter类,相反你会创建一个类拥有所有EventEmitter的功能然后使用它。为什么呢?
我们打开logger模块,写入代码
const EventEmitter = require('events')
const emitter = new EventEmitter()
function log(message) {
console.log(message)
// 发起事件
emitter.emit('messageLogged', { id: 1, url: 'http://' })
}
module.exports = log
复制代码
打开主模块app,写入代码
const EventEmitter = require('events')
const emitter = new EventEmitter()
const log = require('./logger')
// 注册事件
emitter.on('messageLogged', (arg) => {
console.log('Listener called', arg)
})
log('message')
复制代码
回到控制台,运行程序,你会发现控制台只打印了message,我们注册的事件中的回调函数并没有被调用。
这是因为在logger模块中,我们使用了一个emitter对象来发起事件,但是在app模块中使用了另一个EventEmitter对象来处理这个事件,这完全是不同的。当我们在app模块注册一个监听器,这个监听器只在当前模块的EventEmitter对象注册,与别的无关,这就是为什么不经常直接使用EventEmitter的原因。
相反要创建一个继承并扩展了EventEmitter所有能力的类,这个例子中,我们要创建一个Logger类,并且拥有一个扩展的log方法。
我们来把代码改写一下,首先是logger模块
class Logger {
log(message) {
console.log(message)
this.emit('messageLogged', { id: 1, url: 'http://' })
}
}
复制代码
首先需要创建一个类,使用ES6中的class关键字来创建一个Logger类,并扩展一个log方法。
为了让现在的Logger类完全具备EventEmitter的所有功能,我们使用ES6新增加的extends关键字
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)
// 发起事件
this.emit('messageLogged', { id: 1, url: 'http://' })
}
}
module.exports = Logger
复制代码
接下来,我们改写下app主模块,代码如下
// 引入Logger类
const Logger = require('./logger')
// 创建一个实例对象
const logger = new Logger()
// 注册事件
logger.on('messageLogged', (arg) => {
console.log('Listener called', arg)
})
logger.log('message')
复制代码
回到控制台,运行程序,打印结果如下
message
Listener called { id: 1, url: 'http://' }
复制代码
可以看到我们注册事件的回调函数被调用了。
HTTP 模块
Node中有一个非常强大的模块就是用于创建网络应用的HTTP模块。
例如我们可以创建一个服务监听某个给定端口,这样我们就可以为客户端创建一个后端服务,就像React或者Angular创建的应用或者在手机上使用的移动端应用。
打开官方文档HTTP,可以找到HTTP模块的信息。
我们直接来看个例子
const http = require('http')
// 创建一个网络服务
const server = http.createServer()
复制代码
这边有趣的是这个server是一个EventEmitter,它具备你之前看到的所有EventEmitter的功能,我们在官网文档找到http.Server类,这个类继承自net.Server,这是另一个定义在net模块中的类,而net.Server又继承自EventEmitter,这就是为什么之前说的Node中很多功能都是基于EventEmitter。
完整代码如下
const http = require('http')
// 创建一个网络服务
const server = http.createServer()
// 注册事件
server.on('connection', (socket) => {
console.log('New connection...')
})
// 监听端口
server.listen(3000)
console.log('Listening is on port 3000...')
复制代码
回到控制台,运行程序,控制台打印结果
Listening is on port 3000...
复制代码
然后我们打开浏览器,地址栏输入localhost:3000回车,回到控制台,能看到输出
New connection...
复制代码
在真实的编程中,是不会发起connection事件然后处理的,这样太低级了,我们常用的做法是给createServer方法一个回调函数,这个函数需要两个参数,分别是请求和反馈。
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...')
复制代码
回到控制台,运行程序,控制台打印结果
Listening is on port 3000...
复制代码
然后我们打开浏览器,地址栏输入localhost:3000回车,浏览器会显示
Hello world
复制代码
如果我们想要创建一个网络应用的后端服务,我们需要处理很多的路由规则,我们需要另一个if代码块,比如我们想从数据库返回课程的列表
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...')
复制代码
回到控制台,运行程序,打开浏览器,地址栏输入localhost:3000/api/courses,浏览器显示
[1,2,3]
复制代码
创建网络服务是很容易的,但是现实中我们不会使用HTTP模块直接创建后端服务,理由是你看到当这里的规则越来越多的时候,代码会变得很复杂,因为我们都是在回调函数中线性的增加它们的内容,取而代之,我们使用一个叫Express的框架,它可以给应用一个清晰的结构,来处理不同的路由请求,我们使用Express来代替Node原有的HTTP模块的功能,在后面的文章我们会学习到。
好了,本篇文章先到这里。
最后
感谢您的阅读,希望对你有所帮助。由于本人水平有限,如果文中有描述不当的地方,烦请指正,非常感谢。