Mosh的Node.js教程(三)

作者:大志前端
链接: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对象,我们不能获取操作系统的信息。

但是到了NodeJavaScript可以在浏览器外执行,或者说在服务器上,这样我们就可以访问操作系统,操作文件,操作网络,比如可以监听某个特定端口的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模块的功能,在后面的文章我们会学习到。

好了,本篇文章先到这里。

最后

感谢您的阅读,希望对你有所帮助。由于本人水平有限,如果文中有描述不当的地方,烦请指正,非常感谢。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • Node.js是什么? Node.js是建立在谷歌Chrome的JavaScript引擎(V8引擎)的Web应用程...
    陈威24阅读 3,085评论 1 1
  • 内容来自《Node.js开发指南》 核心模块是 Node.js 的心脏,它由一些精简而高效的库组成,为 Node....
    angelwgh阅读 4,421评论 0 1
  • 本文来自 悟尘纪,获取更新内容可查看原文: https://www.lixl.cn/2020/011231581....
    悟尘80阅读 2,944评论 0 1
  • Module definition patterns 除了作为加载依赖的机制之外,模块系统也是一种用于定义AP...
    宫若石阅读 3,421评论 0 0
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 12,753评论 28 53

友情链接更多精彩内容