Mosh的Node.js教程(二)

转载
作者:大志前端
链接:https://juejin.cn/post/6844904056310202376
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


前言

本系列文章是根据Mosh大佬的视频教程全方位Node开发 - Mosh整理而成,个人觉得视频非常不错,所以计划边学习边整理成文章方便后期回顾。该视频教程是英文的,但是有中文字幕,感谢marking1212提供的中文字幕翻译。

本篇文章大纲

  • Node的模块系统
  • 全局对象
  • Modules 模块
  • 创建一个模块
  • 加载一个模块
  • 模块包装函数

Node的模块系统

通过本篇文章的学习,你将了解什么是模块,为什么需要模块,以及它们是怎么工作的。

Node内置核心模块

  • 操作系统 os
  • 文件系统 fs
  • 事件 events
  • http

全局对象 Global Object

通常我们会用console.log()在控制台打印一些东西,这个console就是全局对象,它的作用域是全局,也就是说可以在任何地方任何文件调用它。

Node中也有很多全局对象。

在浏览器中,window对象代表的是全局对象。

当我们调用console.log()的时候,实际上调用的是window.console.log(),当然我们可以直接省略前面的window.,JavaScript引擎会自动前置window关键字。

类似的你看到的其他函数也属于全局对象,比如setTimeout()clearTimeout()等,我们可以用window.setTimeout()来调用,也可以直接调用。

同样的,当我们声明一个变量

var message = ''
复制代码

这个变量同样属于window对象。

这里要注意的是Node中并没有window对象,它有个类似的对象global,我们同样可以通过global调用setTimeout()或者其他函数,也可以直接省略global.调用。

还有一点要注意的是,这里面定义的message变量并不会添加到global对象中,换句话说,比如你想打印console.log(global.message),你会在控制台看到未定义的错误。

我们在app.js文件输入以下代码:

var message = ''
console.log(global.message) // undefined
复制代码

在控制台会打印出undefined

global的作用域只在这个文件,也就是app.js文件中,在文件外它是不可见的,这就是Node的模块化系统所致。

Modules 模块

在客户端,JavaScript是运行在浏览器中。

当我们定义一个函数或变量,它的作用域是全局的,比如我们定义一个sayHello函数

var sayHello = function (name) {

}
复制代码

它的作用域是全局,可以通过window对象访问,但其实这种行为逻辑有个问题,在真实的编程中,我们经常讲不同的代码放到不同的文件中,也许在两个文件中定义了同名的sayHello函数,因为函数被添加到全局变量,当我们在另一个文件中定义这个名字的函数,新的定义将会覆盖旧的定义,这就是全局作用域的问题。

所以为了建立可信和可维护的应用我们应避免定义全局函数和变量,我们应该模块化。

我们需要创建小型拼装块或者叫模块来存放变量和函数,这样不同模块之间的同名函数和变量不会相互覆盖,他们被封装在模块中了。

Node的核心概念就是模块。

每个Node中的文件都被看做模块,每个模块中定义的变量和函数作用域仅在模块内,以面向对象的观点我们叫它们私有成员,它们在容器外,也就是模块外是不可见的。

如果你要在模块外使用一个定义在模块中的变量或函数,我们需要明确的导出它为公开成员。

每个Node工程至少要包含一个文件或者说一个模块。

这里的app.js就是这个项目的主模块,我们把module对象打印出来看一下

console.log(module)
复制代码

这个module对象看起来像全局对象,你可能会想通过全局对象global访问它,但是实际上它不是全局对象。

回到控制台,我们运行node app.js,可以看到打印出了module对象的详细信息,它是一个JSON对象,包含了键值对,比如id,每个模块都有独一无二的id。

Module {
  id: '.',
  exports: {},
  parent: null,
  filename: 'F:\\2020\\study\\Node.js\\node-course\\first-app\\app.js',
  loaded: false,
  children: [],
  paths:
   [ 'F:\\2020\\study\\Node.js\\node-course\\first-app\\node_modules',
     'F:\\2020\\study\\Node.js\\node-course\\node_modules',
     'F:\\2020\\study\\Node.js\\node_modules',
     'F:\\2020\\study\\node_modules',
     'F:\\2020\\node_modules',
     'F:\\node_modules' ] }
复制代码

这里,我们先不用了解每个属性的意思,随着课程的深入我们都会了解。

所以在Node中,每个文件都是模块,模块中定义的成员作用域只在模块中,它们在模块外是不可见的。

创建一个模块

我们给应用添加一个模块,新建一个文件logger.js,假设我们为记录信息创建一个模块,我们要在很多地方复用这个模块,有可能的话也会在别的应用复用。

在模块中,我们假设要使用一个远程日志服务来记录我们的日志,有个其他的网站可以提供日志服务,它提供了一个URL,可以通过给它发送HTTP请求来记录日志。

我们在logger.js写一些代码

var url = 'http://mylogger.io/log' // 这不是一个真实的地址,只做演示用,我们就假设这个例子会向这个地址发送请求

function log(message) {
    console.log(message)
}
复制代码

这个变量和这个函数的作用域都是这个文件,它们是私有的,在外部不可见。

但是,我们想在app.js也就是我们的主模块中想用到日志模块,我们应该要访问log这个函数,我们需要从app模块调用它,我们需要将它变为公共的,可以在外部访问。

还记得module对象吗,里面有个exports属性

Module {
  id: '.',
  exports: {},
  parent: null,
  filename: 'F:\\2020\\study\\Node.js\\node-course\\first-app\\app.js',
  loaded: false,
  children: [],
  paths:
   [ 'F:\\2020\\study\\Node.js\\node-course\\first-app\\node_modules',
     'F:\\2020\\study\\Node.js\\node-course\\node_modules',
     'F:\\2020\\study\\Node.js\\node_modules',
     'F:\\2020\\study\\node_modules',
     'F:\\2020\\node_modules',
     'F:\\node_modules' ] }
复制代码

我们看到这个属性是一个空对象,所有添加到这个对象的属性将可在外部访问。

回到我们的logger.js模块,加入以下代码

module.exports.log = log
复制代码

我们给exports对象添加一个log方法,让它赋值为我们这里的log函数,换句话说,我们这里的导出的对象,有一个同名的log方法,类似的我们想公开这个url变量可以这样做

module.exports.url = url
复制代码

我们也可以在导出的时候改名,不一定要相同的名字,比如在内部我们叫它url,但是我们在外部叫它endPoint

module.exports.endPoint = url
复制代码

这个例子中,我们不需要导出url变量,因为这纯粹是实现的细节,在现实中,每个模块都有很多变量和函数,我们只想公开最少限度的成员,因为我们想保持模块简单易用。

打个比方,想象DVD播放器,DVD播放器有几个按钮是可以给我们操作的,这些按钮就是我们所说的DVD播放器的公开接口。

image

<figcaption></figcaption>

但是在播放器内部有很多复杂到我们不需要了解的元件,它们完全可以从一个换成另外一个,但不管怎么换它们提供给外部的按钮都是固定的。

image

<figcaption></figcaption>

logger模块中,这个url是实现细节,其他的模块不需要了解它,它们只要调用log函数就可以了,所以我们让log变为公开的,而url保持私有。

接下来,我们要在app模块中使用它。

加载模块

我们使用require函数来加载模块,这是Node才有的函数,浏览器里没有,这个函数需要一个参数,也就是我们想加载的模块名称。

require('./logger.js') // 这边的.js可以省略,因为Node知道这个是一个js文件,会自动添加扩展名
复制代码

由于app.jslogger.js在同一个文件夹下,我们使用./来代表当前文件夹。

如果这个模块是在子文件夹中可以添加子文件夹的路径。

require('./subFolder/logger')
复制代码

如果在父文件夹,可以使用../来代表相对路径

require('../logger')
复制代码

这个require函数返回参数模块导出的对象,就是这边的exports对象,我们来代码演示一下

app.js

var logger = require('./logger')
console.log(logger)
复制代码

我们把logger变量打印出来看下得到什么,回到控制台,执行node app.js

{ log: [Function: log] }
复制代码

我们得到一个对象,对象中有一个单一的函数log,我们就可以在app.js中调用这个函数了。

我们来调用一个看看

var logger = require('./logger')

logger.log('message')
复制代码

回到控制台,运行程序,可以看到打印了message信息。

这就是Node中模块的工作方式,定义一个模块,导出一个或多个成员。

作为最佳实践,导入的模块应该保存在常量中,因为我们有可能意外的将logger重新赋值。

var logger = require('./logger')

logger = 1 // 重新赋值

logger.log('message')
复制代码

重新赋值为1后,我们再调用log方法,就会发生异常

TypeError: logger.log is not a function
复制代码

作为对比,我们把它定义为一个常量

const logger = require('./logger') // 使用const定义为常量

logger = 1 // 重新赋值

logger.log('message')
复制代码

再运行程序,得到另一个异常,试图给常量赋值,有些专门检查这类错误的工具,使用它们可以避免在运行时出现问题,比如jshint

TypeError: Assignment to constant variable.
复制代码

如果我们意外的重写了常量的值,我们会在查错时而不是运行时得到报错,这就是定义为常量的好处。

如果我们不想导出一个对象,只想导出一个简单的函数,比如在logger模块中,我们不需要导出一个对象,我们只有一个简单的函数,对象在有多个属性或方法时才需要用得到。

我们可以将exports直接赋值为log函数。

var url = 'http://mylogger.io/log'

function log(message) {
  console.log(message)
}

module.exports = log
复制代码

这样之后,我们在app.jslogger就不再是一个对象,它是一个我们可以直接调用的函数,我们命名为log会更好。

const log = require('./logger')

log('message') // 直接调用
复制代码

所以在你的模块中,你可以导出对象或者单一的函数。

模块包装函数

我们已经知道了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 // 不要这么写
复制代码

因为这个exportsmodule.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
复制代码

第一个是文件名,第二个是文件的完整路径。

现在,我们对Node模块和它的运作方式有个大概的印象了,我们知道了如何创建它们和加载它们。

Node包含了很多有用和常用的模块,涉及的知识点较多,下篇文章我们再一起来学习下。

最后

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


作者:大志前端
链接:https://juejin.cn/post/6844904056310202376
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

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

相关阅读更多精彩内容

友情链接更多精彩内容