前言javascript
本系列文章是根据Mosh
大佬的视频教程全方位Node开发 - Mosh整理而成,我的以为视频很是不错,因此计划边学习边整理成文章方便后期回顾。该视频教程是英文的,可是有中文字幕,感谢marking1212提供的中文字幕翻译。前端
经过本篇文章的学习,你将了解什么是模块,为何须要模块,以及它们是怎么工做的。java
一般咱们会用console.log()
在控制台打印一些东西,这个console
就是全局对象,它的做用域是全局,也就是说能够在任何地方任何文件调用它。node
在Node
中也有不少全局对象。编程
在浏览器中,window
对象表明的是全局对象。浏览器
当咱们调用console.log()
的时候,实际上调用的是window.console.log()
,固然咱们能够直接省略前面的window.
,JavaScript引擎会自动前置window
关键字。bash
相似的你看到的其余函数也属于全局对象,好比setTimeout()
、clearTimeout()
等,咱们能够用window.setTimeout()
来调用,也能够直接调用。app
一样的,当咱们声明一个变量ide
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
的模块化系统所致。
在客户端,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播放器的公开接口。
可是在播放器内部有不少复杂到咱们不须要了解的元件,它们彻底能够从一个换成另一个,但无论怎么换它们提供给外部的按钮都是固定的。
在logger
模块中,这个url
是实现细节,其余的模块不须要了解它,它们只要调用log
函数就能够了,因此咱们让log
变为公开的,而url
保持私有。
接下来,咱们要在app
模块中使用它。
咱们使用require
函数来加载模块,这是Node
才有的函数,浏览器里没有,这个函数须要一个参数,也就是咱们想加载的模块名称。
require('./logger.js') // 这边的.js能够省略,由于Node知道这个是一个js文件,会自动添加扩展名
复制代码
因为app.js
和logger.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.js
的logger
就再也不是一个对象,它是一个咱们能够直接调用的函数,咱们命名为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 // 不要这么写
复制代码
由于这个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
复制代码
第一个是文件名,第二个是文件的完整路径。
如今,咱们对Node
模块和它的运做方式有个大概的印象了,咱们知道了如何建立它们和加载它们。
Node
包含了不少有用和经常使用的模块,涉及的知识点较多,下篇文章咱们再一块儿来学习下。
感谢您的阅读,但愿对你有所帮助。因为本人水平有限,若是文中有描述不当的地方,烦请指正,很是感谢。
欢迎你们关注个人公众号前端帮帮忙
。
Node的模块系统(路径模块、操做系统模块、文件模块、事件模块、HTTP模块)