01.重学node js
1.node.js定义
-
Node.js是一个基于V8 JavaScript引擎的JavaScript运行时环境。【node不是语言】
-
也就是说Node.js基于V8引擎来执行JavaScript的代码,但是不仅仅只有V8引擎
- V8可以嵌入到任何C ++应用程序中,无论是Chrome还是Node.js,事实上都是嵌入了V8引擎 来执行JavaScript代码
- 在Chrome浏览器中,还需要解析、渲染HTML、CSS等相关渲染引擎,另外还需要提供支持浏览器操作 的API、浏览器自己的事件循环等;
- 另外,在Node.js中我们也需要进行一些额外的操作,比如文件系统读/写、网络IO、加密、压缩解压文件等 操作
2.node.js的应用场景
- 目前前端开发的库都是以node包的形式进行管理
- npm、yarn工具成为前端开发使用最多的工具
- 越来越多的公司使用Node.js作为web服务器开发
- 大量项目需要借助Node.js完成前后端渲染的同构应用
- 资深前端工程师需要为项目编写脚本工具(前端工程师编写脚本通常会使用JavaScript,而不是Python或者shell)
- 很多企业在使用Electron来开发桌面应用程序;
3.Node程序传递参数
-
node index.js env=development Kim // 代码中拿参数 console.log(process.argv) /** 打印结果 ['C:\\Program Files\\nodejs\\node.exe', 'D:\\learn\\node\\code\\02.path的其他用法.js', 'env=development', 'Kim'] */
-
如果我们这样来使用程序,就意味着我们需要在程序中获取到传递的参数:
- 获取参数其实是在process的内置对象中的;
- 如果我们直接打印这个内置对象,它里面包含特别的信息:
- 其他的一些信息,比如版本、操作系统等大家可以自行查看,后面用到一些其他的我们还会提到;
-
现在,我们先找到其中的argv属性:【argument vector】
- 我们发现它是一个数组,里面包含了我们需要的参数
4.特殊的全局对象
包括:__dirname、__filename、exports、module、require()
- __dirname:获取当前文件所在的路径【注意:不包括后面的文件名】
- __filename:获取当前文件所在的路径和文件名称:【注意:包括后面的文件名称】
5.常见的全局对象
-
process对象:process提供了Node进程中相关的信息:
- 比如Node的运行环境、参数信息等
- 后面在项目中,我也会讲解,如何将一些环境变量读取到 process 的 env 中;
-
console对象:提供了简单的调试控制台
-
定时器函数:在Node中使用定时器有好几种方式
- setTimeout(callback, delay[, …args]):callback在delay毫秒后执行一次;
- setInterval(callback, delay[, …args]):callback每delay毫秒重复执行一次;
- setImmediate(callback[, …args]):callbackI / O事件后的回调的“立即”执行;
- process.nextTick(callback[, …args]):添加到下一次tick队列中;
6.global和window的区别
- 在浏览器中,全局变量都是在window上的,比如有document、setInterval、setTimeout、alert、console等等
- 在Node中,我们也有一个global属性,并且看起来它里面有很多其他对象
- 但是在浏览器中执行的JavaScript代码,如果我们在顶级范围内通过var定义的一个属性,默认会被添加到window 对象上
- 在node中,我们通过var定义一个变量,它只是在当前模块中有一个变量,不会放到全局中【注意】
02.JavaScript模块化
1.什么是模块化开发
- 模块化开发最终的目的是将程序划分成一个个小的结构;
- 这个结构中编写属于自己的逻辑代码,有自己的作用域,不会影响到其他的结构;
- 这个结构可以将自己希望暴露的变量、函数、对象等导出给其结构使用;
- 也可以通过某种方式,导入另外结构中的变量、函数、对象等;
2.CommonJS和Node
- Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:
- 在Node中每一个js文件都是一个单独的模块;
- 这个模块中包括CommonJS规范的核心变量:exports、module.exports、require;
- 我们可以使用这些变量来方便的进行模块化开发;
- exports和module.exports可以负责对模块中的内容进行导出
- require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;
3.exports导出
-
exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出;
-
以下代码意味着foo变量等于exports对象
-
// 导出 exports.name = name exports.age = age exports.sayHi = sayHi // 导入 const foo = require('./foo')
4.module.exports
- Node中我们经常导出东西的时候,又是通过module.exports导出的:
- CommonJS中是没有module.exports的概念的
- 但是为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是 module
- 所以在Node中真正用于导出的其实根本不是exports,而是module.exports;
- 因为module才是导出的真正实现者;
- module对象的exports属性是exports对象的一个引用
- 也就是说 module.exports = exports = main中的foo
5.CommonJS规范缺点
- CommonJS加载模块是同步的:
- 同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行;
- 这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快;
- 浏览器加载js文件需要先从服务器将文件下载下来,之后在加载运行;
- 那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作;
- 所以在浏览器中,我们通常不使用CommonJS规范
- 当然在webpack中使用CommonJS是另外一回事;因为它会将我们的代码转成浏览器可以直接执行的代码
- 在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD:
- 但是目前一方面现代的浏览器已经支持ES Modules,另一方面借助于webpack等工具可以实现对CommonJS或者 ES Module代码的转换;
6.ES Module
- ES Module和CommonJS的模块化有一些不同之处:
- 一方面它使用了import和export关键字
- 另一方面它采用编译期的静态分析,并且也加入了动态引用的方式
- ES Module模块采用export和import关键字来实现模块化
- export负责将模块内的内容导出
- import负责从其他模块导入内容;
- 采用ES Module将自动采用严格模式:use strict
7.export关键字
- export关键字将一个模块中的变量、函数、类等导出
- 我们希望将其他中内容全部导出,它可以有如下的方式:
- 方式一:在语句声明的前面直接加上export关键字
- 方式二:将所有需要导出的标识符,放到export后面的 {}中
- 注意:这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的
- 所以: export {name: name},是错误的写法;
- 方式三:导出时给标识符起一个别名
8.import关键字
- import关键字负责从另外一个模块中导入内容
- 导入内容的方式也有多种:
- 方式一:import {标识符列表} from ‘模块’;
- 注意:这里的{}也不是一个对象,里面只是存放导入的标识符列表内容;
- 方式二:导入时给标识符起别名
- 方式三:通过 * 将模块功能放到一个模块功能对象(a module object)上
- 方式一:import {标识符列表} from ‘模块’;
9.Export和import结合使用
export { nickName as name} from './user.js'
在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中;这样方便指定统一的接口规范,也方便阅读;这个时候,我们就可以使用export和import结合使用;
10.default用法
- 默认导出(default export)
- 默认导出export时可以不需要指定名字;
- 在导入时不需要使用 {},并且可以自己来指定名字;
- 它也方便我们和现有的CommonJS等规范相互操作;
- 注意:在一个模块中,只能有一个默认导出(default export)
11.import函数
-
通过import加载一个模块,是不可以在其放到逻辑代码中的,如:
-
if(true){ import sub form './modules/foo.js' }
-
-
这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系
-
由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况
-
甚至下面的这种写法也是错误的:因为我们必须到运行时能确定path的值;
这个时候我们需要使用 import() 函数来动态加载
if(true){
import('./modules/foo.js').then(res=>{
console.log(res)
})
}
12.CommonJS的加载过程
- CommonJS模块加载js文件的过程是运行时加载的,并且是同步的:
- 运行时加载意味着是js引擎在执行js代码的过程中加载 模块;
- 同步的就意味着一个文件没有加载结束之前,后面的代码都不会执行;
- CommonJS通过module.exports导出的是一个对象
- 导出的是一个对象意味着可以将这个对象的引用在其他模块中赋值给其他变量
- 但是最终他们指向的都是同一个对象,那么一个变量修改了对象的属性,所有的地方都会被修改
13.ES Module加载过程
- ES Module加载js文件的过程是编译(解析)时加载的,并且是异步的
- 编译时(解析)时加载,意味着import不能和运行时相关的内容放在一起使用:
- 比如from后面的路径需要动态获取;
- 比如不能将import放到if等语句的代码块中
- 所以我们有时候也称ES Module是静态解析的,而不是动态或者运行时解析的;
- 异步的意味着:JS引擎在遇到import时会去获取这个js文件,但是这个获取的过程是异步的,并不会阻塞主线程继 续执行;
- 也就是说设置了 type=module 的代码,相当于在script标签上也加上了 async 属性;
- 如果我们后面有普通的script标签以及对应的代码,那么ES Module对应的js文件和代码不会阻塞它们的执行
- ES Module通过export导出的是变量本身的引用:
- export在导出一个变量时,js引擎会解析这个语法,并且创建模块环境记录(module environment record)
- 模块环境记录会和变量进行 绑定(binding),并且这个绑定是实时的;
- 而在导入的地方,我们是可以实时的获取到绑定的最新值的
- 所以,如果在导出的模块中修改了变化,那么导入的地方可以实时获取最新的变量
- 注意:在导入的地方不可以修改变量,因为它只是被绑定到了这个变量上(其实是一个常量)
14.CommonJS和ES Module交互
-
结论一:通常情况下,CommonJS不能加载ES Module
- 因为CommonJS是同步加载的,但是ES Module必须经过静态分析等,无法在这个时候执行JavaScript代码
- 但是这个并非绝对的,某些平台在实现的时候可以对代码进行针对性的解析,也可能会支持
- Node当中是不支持的
-
结论二:多数情况下,ES Module可以加载CommonJS
- ES Module在加载CommonJS时,会将其module.exports导出的内容作为default导出方式来使用
- 这个依然需要看具体的实现,比如webpack中是支持的、Node最新的Current版本也是支持的;
- 但是在最新的LTS版本中就不支持;
03.node常用的内置模块
1.内置模块path
- path模块用于对路径和文件进行处理,提供了很多好用的方法。
- 并且我们知道在Mac OS、Linux和window上的路径时不一样的
- window上会使用 \或者 \ 来作为文件路径的分隔符,当然目前也支持 /;
- 在Mac OS、Linux的Unix操作系统上使用 / 来作为文件路径的分隔符
- 那么如果我们在window上使用 \ 来作为分隔符开发了一个应用程序,要部署到Linux上面应该怎么办呢
- 显示路径会出现一些问题;
- 所以为了屏蔽他们之间的差异,在开发中对于路径的操作我们可以使用 path 模块
- 可移植操作系统接口(英语:Portable Operating System Interface,缩写为POSIX)
- Linux和Mac OS都实现了POSIX接口;
- Window部分电脑实现了POSIX接口;
2.path常见的API
- 从路径中获取信息
- dirname:获取文件的父文件夹;
- basename:获取文件名;
- extname:获取文件扩展名;
- 路径的拼接
- 如果我们希望将多个路径进行拼接,但是不同的操作系统可能使用的是不同的分隔符
- 这个时候我们可以使用path.join函数;
- 将文件和某个文件夹拼接
- 如果我们希望将某个文件和文件夹拼接,可以使用 path.resolve;
- resolve函数会判断我们拼接的路径前面是否有 /或…/或./;
- 如果有表示是一个绝对路径,会返回对应的拼接路径
- 如果没有,那么会和当前执行文件所在的文件夹进行路径的拼接
3.内置模块fs
- fs是File System的缩写,表示文件系统
- 对于任何一个为服务器端服务的语言或者框架通常都会有自己的文件系统:
- 因为服务器需要将各种数据、文件等放置到不同的地方;
- 比如用户数据可能大多数是放到数据库中
- 比如某些配置文件或者用户资源(图片、音视频)都是以文件的形式存在于操作系统上的
- Node也有自己的文件系统操作模块,就是fs:
- 借助于Node帮我们封装的文件系统,我们可以在任何的操作系统(window、Mac OS、Linux)上面直接去 操作文件
- 这也是Node可以开发服务器的一大原因,也是它可以成为前端自动化脚本等热门工具的原因
4.fs 常见的API
- 这些API大多数都提供三种操作方式:
- 方式一:同步操作文件:代码会被阻塞,不会继续执行;
- 方式二:异步回调函数操作文件:代码不会被阻塞,需要传入回调函数,当获取到结果时,回调函数被执行
- 方式三:异步Promise操作文件:代码不会被阻塞,通过 fs.promises 调用方法操作,会返回一个Promise, 可以通过then、catch进行处理;
- fs.stat fs.statSync fs.promise. stat
const fileState = fs.statSync('./file.txt')
fs.stat('./file.txt',(err,fileState)=>{})
fs.promise. stat('./file.txt').then(fileState=>{})
5.文件描述符
- 文件描述符(File descriptors)是什么?
- 在 POSIX 系统上,对于每个进程,内核都维护着一张当前打开着的文件和资源的表格。
- 每个打开的文件都分配了一个称为文件描述符的简单的数字标识符。
- 在系统层,所有文件系统操作都使用这些文件描述符来标识和跟踪每个特定的文件
- Windows 系统使用了一个虽然不同但概念上类似的机制来跟踪资源
- 为了简化用户的工作,Node.js 抽象出操作系统之间的特定差异,并为所有打开的文件分配一个数字型的文件描述 符
- fs.open() 方法用于分配新的文件描述符。
- 一旦被分配,则文件描述符可用于从文件读取数 据、向文件写入数据、或请求关于文件的信息。
6.文件的读写
- 如果我们希望对文件的内容进行操作,这个时候可以使用文件的读写:
- fs.readFile(path[, options], callback):读取文件的内容;
- fs.writeFile(file, data[, options], callback):在文件中写入内容;
- 在上面的方法中,你会发现有一个大括号没有填写任何的内容,这个是写入时填写的option参数
- flag:写入的方式。
- w 打开文件写入,默认值;
- w+打开文件进行读写,如果不存在则创建文件;
- r+ 打开文件进行读写,如果不存在那么抛出异常
- r打开文件读取,读取时的默认值;
- a打开要写入的文件,将流放在文件末尾。如果不存在则创建文件
- a+打开文件以进行读写,将流放在文件末尾。如果不存在则创建文件
- encoding:字符的编码;
- 如果不填写encoding,返回的结果是Buffer;
- flag:写入的方式。
7.文件夹操作
- 新建一个文件夹
- 使用fs.mkdir()或fs.mkdirSync()创建一个新文件夹
- 获取文件夹的内容
- fs.readdir
function getFiles (dirName) {
fs.readdir(dirName, { withFileTypes: 'true' }, (err, files) => {
if (err) return console.log(err)
for (file of files) {
// console.log(file.isDirectory());
if (file.isDirectory()) {
const filePath = path.resolve(dirName, file.name)
// console.log(filePath);
getFiles(filePath)
} else {
console.log(file.name);
}
}
})
}
- 文件重命名
- fs.rename(’…/file, ‘…/name’,err=>{})
8.events模块
- Node中的核心API都是基于异步事件驱动的:
- 在这个体系中,某些对象(发射器(Emitters))发出某一个 事件;
- 我们可以监听这个事件(监听器 Listeners),并且传入的回 调函数,这个回调函数会在监听到事件时调用;
- 发出事件和监听事件都是通过EventEmitter类来完成的,它们都属 于events对象
- emitter.on(eventName, listener):监听事件,也可以使用 addListener;
- emitter.off(eventName, listener):移除事件监听,也可以使 用removeListener;
- emitter.emit(eventName[, …args]):发出事件,可以携带一 些参数;
- EventEmitter的实例有一些属性,可以记录一些信息:
- emitter.eventNames():返回当前 EventEmitter对象注册的事件字符串数组;
- emitter.getMaxListeners():返回当前 EventEmitter对象的最大监听器数量,可以通过setMaxListeners() 来修改,默认是10;
- emitter.listenerCount(事件名称):返回当前 EventEmitter对象某一个事件名称,监听器的个数;
- emitter.listeners(事件名称):返回当前 EventEmitter对象某个事件监听器上所有的监听器数组;
- emitter.once(eventName, listener):事件监听一次
- emitter.prependListener():将监听事件添加到最前面
- emitter.prependOnceListener():将监听事件添加到最前面,但是只监听一次
- emitter.removeAllListeners([eventName]):移除所有的监听器