Nodejs基本知识点汇总

nodejs入门教程

1.前言

首先说明,本篇文章是对nodejs官网入门教程的一份总结。同学们学习时强烈建议先前往nodejs官网入门教程学习,那里几乎有你们需要的一切。本篇文章只对相对重要的知识点进行记录和编码测试,便于未来对nodejs项目进行系统性的学习。

2.入门教程

2.1 命令行

运行node.js脚本

如果主Node.js应用程序文件是 app.js,则可以通过键入以下命令调用它:

node app.js

在这里插入图片描述

2.2 退出node.js程序

1、命令行使用ctrl+c可以停止node.js程序
2、非正常退出(不推荐):process 核心模块提供了process.exit()方法,以编程的方式强制停止程序。这意味着任何待处理的回调、仍在发送中的任何网络请求、任何文件系统访问、或正在写入 stdout 或 stderr 的进程,所有这些都会被立即非正常地终止。可以传入一个整数,向操作系统发送退出码:

process.exitCode = 1


也可以设置 process.exitCode 属性:

process.exitCode = 1

当程序结束时,Node.js 会返回该退出码。当进程完成所有处理后,程序会正常地退出。
3、使用SIGTERM 信号退出

const express = require('express')

const app = express()

app.get('/', (req, res) => {
    
    
  res.send('你好')
})

const server = app.listen(3000, () => console.log('服务器已就绪'))

process.on('SIGTERM', () => {
    
    
  server.close(() => {
    
    
    console.log('进程已终止')
  })
})

process.kill(process.pid, 'SIGTERM')

2.3 如何使用 Node.js REPL

注意:REPL 也被称为运行评估打印循环,是一种编程语言环境(主要是控制台窗口),它使用单个表达式作为用户输入,并在执行后将结果返回到控制台。

2.3.1 使用方式

简单点说,就是你可以简单地把控制台当成

node

 console.log('测试')

在这里插入图片描述
REPL是交互式的,这意味着,你可以使用tab键自动补全预定义的变量。

尝试输入 JavaScript 类或者全局对象的名称,例如 Number,添加一个点号并按下 tab。REPL 会打印可以在该类上访问的所有属性和方法:
在这里插入图片描述

2.3.2 点命令

.help

.help :显示点命令的帮助文档
在这里插入图片描述

.editor

.editor:启用编辑器模式,可以轻松地编写多行 JavaScript 代码。当处于此模式时,按下 Ctrl+D 可以运行编写的代码。
在这里插入图片描述
并且通过实验,我们可以得知,REPL对之前声明的变量是有记忆的,并且不允许重复声明。
在这里插入图片描述
如果需要一个全新的环境,可以使用Ctrl+D退出REPL后再node重新进入:
在这里插入图片描述

.break

.break:当输入多行的表达式时,输入 .break 命令可以中止进一步的输入。相当于按下 Ctrl+C。
当输入函数,比如:

[1, 2, 3].forEach(num => {
    
    
1

或者

function test(){
    
    
1

REPL会了解当前函数未完成,自动进入多行编辑模式,…表示仍在编辑中,也可以当成tab缩进。

在这里插入图片描述

.save

.save:将在 REPL 会话中输入的所有内容保存到文件(需指定文件名)。

在这里插入图片描述

.load

.load: 加载 JavaScript 文件(需要指定文件名)。
在这里插入图片描述

.exit

.exit:退出 REPL(相当于按下两次 ctrl-C)。
在这里插入图片描述

.clear

.clear:将 REPL 上下文重置为空对象,并清除当前正在输入的任何多行的表达式。
在Node.js的模块文件中,start方法返回被开启的REPL运行环境,可以为REPL运行环境指定一个上下文对象(context),可以将该上下文对象保存的变量作为REPL运行环境中的全局变量来进行使用。可以看到,使用.clear命令后,上下文对象保存的变量都变成了not defined(未声明)。
在这里插入图片描述
不过,值得注意的是,当未指定特定的js文件repl作为上下文,而是直接使用命令node进入REPL的话,.clear命令并不会生效
在这里插入图片描述

2.4 从命令行接收参数(参数可以是独立的,也可以具有键和值。)

获取参数值的方法是使用 Node.js 中内置的 process 对象,它公开了 argv 属性,该属性是一个包含所有命令行调用参数的数组。
第一个参数是 node 命令的完整路径。
第二个参数是正被执行的文件的完整路径。
所有其他的参数从第三个位置开始。
在这里插入图片描述
demo04.js测试代码:

process.argv.forEach((val, index) => {
    
    
    console.log(`${
      
      index}: ${
      
      val}`)
})

1、当不携带参数时:

node demo04.js

在这里插入图片描述

只返回node 命令的完整路径和正被执行的文件的完整路径。
2、当携带独立参数时:

node demo04.js joe

在这里插入图片描述
在这里插入图片描述
3、当携带具有键和值的参数时:

node demo04.js name=joe
1

在这里插入图片描述
此时需要对参数进行解析,最好的方法是使用 minimist 库,该库有助于处理参数,但是需要在每个参数名称之前使用双破折号:

console.log(process.argv.slice(2))
const args = require('minimist')(process.argv.slice(2))
console.log(args)
console.log('name:'+args['name'],'  sex:'+args['sex'])
1234

在这里插入图片描述
话说,为什么要在在命令行接收参数呢?我的猜测是,可能程序运行的时候可以根据参数进行以何种模式运行,当然不带参数就是默认模式。

2.5 使用node.js输出到命令行

2.5.1 基础输出

Node.js 提供了 console 模块,该模块提供了大量非常有用的与命令行交互的方法。
它基本上与浏览器中的 console 对象相同。

%s 会格式化变量为字符串
%d 会格式化变量为数字
%i 会格式化变量为其整数部分
%o 会格式化变量为对象

const x = 10;
const y = 20;
console.log(x+y)

const cat = {
    
    
    species:'猫',
    name:'Tom',
    age: 3.1,
    time:0.05
}
console.log('我的%s名字叫%s,已经%i岁了,反应时间是%d秒',cat.species,cat.name,cat.age,cat.time)

console.log(Number)
console.log('%o', Number)

在这里插入图片描述

2.5.2 清空控制台

console.clear() 会清除控制台(其行为可能取决于所使用的控制台)。
在这里插入图片描述
可以看到console.clear()之前的输出语句的结果都被清空了。

2.5.3 元素计数

console.count() 是一个便利的方法。
在这里插入图片描述
可以看到,console.count()是针对整个输出语句进行计数的,只要有变化就是另一个语句。

2.5.4 打印堆栈踪迹

在某些情况下,打印函数的调用堆栈踪迹很有用,可以回答以下问题:如何到达代码的那一部分?

可以使用 console.trace() 实现:

const function2 = () => console.trace()
const function1 = () => function2()
function1()
123

这会打印堆栈踪迹。 如果在 Node.js REPL 中尝试此操作,则会打印以下内容:
在这里插入图片描述
打印堆栈踪迹大多数情况下都是为了方便最先抛出异常的地方,比如现在main()方法中调用了a()和b()方法,而b()中又调用了c()方法,此时如果c()发生了异常,就会打印出类似的信息:
c():产生异常的行号
b():c()方法调用的行号
main():b()方法调用的行号
有了这样的线索,那追踪异常就显得非常简单。

2.5.5 计算耗时

可以使用 time() 和 timeEnd() 轻松地计算函数运行所需的时间:

const doSomething = () => console.log('测试')
const measureDoingSomething = () => {
    
    
  console.time('doSomething()')
  //做点事,并测量所需的时间。
  doSomething()
  console.timeEnd('doSomething()')
}
measureDoingSomething()
12345678

在这里插入图片描述
也可以放在循环中,监测每次循环需要的时间

for(let i = 0;i < 10;i++){
    
    
    console.time('time')
    console.timeEnd('time')
}
1234

在这里插入图片描述

2.5.6 stdout 和 stderr

console.log 非常适合在控制台中打印消息。 这就是所谓的标准输出(或称为 stdout)。

console.error 会打印到 stderr 流。

它不会出现在控制台中,但是会出现在错误日志中。

目前没有遇到相关的内容,暂不做例子。

2.5.7 为输出着色

可以使用转义序列在控制台中为文本的输出着色。 转义序列是一组标识颜色的字符。比如:

console.log('\x1b[33m%s\x1b[0m', '你好')
1

当然,这是执行此操作的底层方法。 为控制台输出着色的最简单方法是使用库。 Chalk 是一个这样的库,除了为其着色外,它还有助于其他样式的设置(例如使文本变为粗体、斜体或带下划线)。
可以使用 npm install chalk 进行安装,然后就可以使用它:

const chalk = require('chalk')
console.log(chalk.yellow('你好'))
12

在这里插入图片描述

2.5.8 创建进度条

使用 npm install progress 进行安装Progress插件,以下代码段会创建一个 10 步的进度条,每 100 毫秒完成一步。 当进度条结束时,则清除定时器:

const ProgressBar = require('progress')

const bar = new ProgressBar(':bar', {
    
     total: 10 })
const timer = setInterval(() => {
    
    
  bar.tick()
  if (bar.complete) {
    
    
    clearInterval(timer)
  }
}, 100)
123456789

在这里插入图片描述

2.6 在 Node.js 中从命令行接收输入(交互性)

2.6.1 readline模块

CLI:命令行界面(英语:command-line interface,缩写:CLI)是在图形用户界面得到普及之前使用最为广泛的用户界面,它通常不支持鼠标,用户通过键盘输入指令,计算机接收到指令后,予以执行。也有人称之为字符用户界面(CUI)。

我们之前使用的终端(控制台)就是一种CLI,为了使Node.js CLI 程序具有交互性,从版本 7 开始,Node.js 提供了 readline 模块来执行以下操作:每次一行地从可读流(例如 process.stdin 流,在 Node.js 程序执行期间该流就是终端输入)获取输入。

const readline = require('readline').createInterface({
    
    
    input: process.stdin,
    output: process.stdout
  })
  
  readline.question(`please enter your name: `, name => {
    
    
    console.log(`hello, ${
      
      name}!`)
    readline.close()
  })
123456789

在这里插入图片描述
这段代码会询问用户名,当输入了文本并且用户按下回车键时,则会发送问候语。

question() 方法会显示第一个参数(即问题),并等待用户的输入。 当按下回车键时,则它会调用回调函数。

在此回调函数中,关闭了 readline 接口。

2.6.2 输入密码使用readline-sync

如果需要密码,则最好不要回显密码,而是显示 * 符号。

最简单的方式是使用 readline-sync 软件包,使用 npm i readline-sync 安装插件。

var readlineSync = require('readline-sync');
 
// Wait for user's response.
var userName = readlineSync.question('May I have your name? ');
console.log('Hi ' + userName + '!');
 
// Handle the secret text (e.g. password).
var favFood = readlineSync.question('What is your favorite food? ', {
    
    
  hideEchoBack: true // The typed text on screen is hidden by `*` (default).
});
console.log('Oh, ' + userName + ' loves ' + favFood + '!');
1234567891011

在这里插入图片描述
可以看到其在 API 方面与readline模块非常相似,question中的第二个参数是一个json对象,当参数hideEchoBack为true时,则隐藏输入。

2.6.3 Inquirer.js 软件包提供了更完整、更抽象的解决方案(官方强烈推荐使用)

使用 npm install inquirer 进行安装,使用方式如下:

const inquirer = require('inquirer')

var questions = [
  {
    
    
    type: 'input',
    name: 'name',
    message: "Please enter your name: "
  },
  {
    
    
    type: 'password',
    name: 'password',
    message: "Please enter your password: "
  }
]

inquirer.prompt(questions).then(answers => {
    
    
  console.log(`hello ${
      
      answers['name']}! You just created a new password:${
      
      answers['password']}`)
})
123456789101112131415161718

在这里插入图片描述
可以看到, Inquirer.js确实更加强大,直接将多个问题以对象数组的形式组织到一起,并且针对密码类型的输入也做了隐藏处理。

Inquirer.js官网对使用方式说明得十分详细,我做一下简单概括,questions是一个对象数组,你可以把它当成一个表单,每个对象代表一个问题的所有相关内容。

其中,type、name、message是最常用的属性

type表示回答问题的方式,包括input(输入框), number(数字), confirm(确认框), list(列表), rawlist(原始列表), expand(展开列表), checkbox(多选框), password(密码), editor(编辑器)9种。name表示存储在答案中的字段名称,message表示提问的问题。

除了以上三个属性,还有11个不是常用的属性,分别是:
default:答案默认值。
choice:列表的选项,对象数组。其中list、rawlist、checkbox的选项对象都是value和name组合,类似:

{
    
    
    value: 'tennis',
    name: 'tennis'
}
1234

expand比较特殊,使用key和value的组合,类似:

{
    
    
    key: 'R',
    value: 'red'
}
1234

validate:用于校验输入内容,是一个函数,只有返回ture时,回车才会生效。
filter:用于将答案进行二次处理,是一个函数,返回值是答案的最终值。
transformer:在输入的信息后添加的提示信息,可以把它当成input的placeholder,对最终答案没有影响,是一个函数。
when:是一个函数,参数是answer,当返回值是true时,该问题才会生效并且进行提问。
pageSize:分页器,给list、rawList,、expand、checkbox列表使用,输入数字(代表每页多少个选项),当数字小于列表总选项时,会进行分页。
prefix:问题前缀。
suffix:问题后缀。
askAnswered:Boolean值,默认情况下为false,同样的答案字段如果已经存在则不会发起提问,但是如果为true,则会强制提问,并且新的答案会覆盖之前的。
loop:Boolean值,默认值为true。通常和pageSize使用,为true时,列表到最后一页继续前进的话会返回第一页。为false时,则不会。

下面这个例子则对以上所有属性进行了使用,可参考,基本上使用一遍就能够有大概的了解:

const inquirer = require('inquirer')

var questions = [
    {
    
    
        type: 'input',  // 输入类型
        name: 'name',   // 字段名称,在then里可以打印出来
        message: "enter your name",    // 问题
        validate: function(v){
    
      // 校验:当输入的值不为空时,回车才有效果
            return v !== '' 
        },
        transformer: function (v) {
    
     // 提示信息(输入的信息后缀添加(input your name))
            return v + '(input your name)'
        },
        filter: function (v) {
    
      // 最终结果,比如在输入字段前加上博士
            return 'Dr ' + v
        },
        prefix:'Please',    // 问题前添加文字
        suffix:':', // 问题之后添加文字
    },
    {
    
    
        type:'input',
        name:'name',
        message:'repeat your name:',
        askAnswered:true    // 默认情况下为false,同样的答案如果已经存在则不会发起提问,但是如果为true,则会强制提问,并且新的答案会覆盖之前的
    },
    {
    
    
        type:'number',
        name:'num',
        message:'Please enter yout favorite number: ',
        validate: function(v){
    
      // 校验:当输入的值为正整数时,回车才有效果
            return (/(^[1-9]\d*$)/.test(v))
        }
    },
    {
    
    
        type:'confirm',
        name:'like',
        message:'Do you like nodejs? '
    },
    {
    
    
        type:'input',
        name:'why',
        message:'Why do you like nodejs? ',
        when: function(answers){
    
        //  接受之前的answer,根据return的值判断当前问题是否该问
            return answers.like
        }
    },
    {
    
    
        type: 'list',   // 选择框
        name: 'fruits',
        message: "Please take your fruit menu of today: ",
        default:'A',  
        choices: [
            {
    
    
                value: 'A',
                name: 'apples'
            },
            {
    
    
                value: 'B',
                name: 'bananas'
            },
            {
    
    
                value: 'O',
                name: 'oranges'
            }
        ],
        pageSize:2, //  当使用list, rawList, expand or checkbox时,用于分页
        loop:false  // Boolean值,默认值为true。通常和pageSize使用,为true时,列表到最后一页继续前进的话会返回第一页。为false时,则不会。
    },
    {
    
    
        type: 'rawlist',   // 单选框1
        name: 'sports',
        message: "Please chooce your favorite sport: ",
        default: 0,
        choices: [
            {
    
    
                value: 'basketball',
                name: 'basketball'
            },
            {
    
    
                value: 'football',
                name: 'football'
            },
            {
    
    
                value: 'tennis',
                name: 'tennis'
            }
        ],
        pageSize:2
    },
    {
    
    
        type: 'expand',   // 单选框2
        name: 'color',
        message: "Which color do you like best? ",
        default: 'red',
        choices: [
            {
    
    
                key: 'R',
                value: 'red'
            },
            {
    
    
                key: 'G',
                value: 'green'
            },
            {
    
    
                key: 'B',
                value: 'blue'
            }
        ]
    },
    {
    
    
        type: 'checkbox',
        name: 'direction',
        message: "Please choose your direction: ",
        choices: [
            {
    
    
                value: 'top',
                name: 'top'
            },
            {
    
    
                value: 'right',
                name: 'right'
            },
            {
    
    
                value: 'bottom',
                name: 'bottom'
            },
            {
    
    
                value: 'left',
                name: 'left'
            }
        ]
    },
    {
    
    
        type: 'password',
        name: 'pwd',
        message: "Please enter your password: "
    },
    {
    
    
        type:'editor',
        name:'editorInput',
        message:'Please input:'
    }

]

inquirer.prompt(questions).then(answers => {
    
    
    console.log(`hello, ${
      
      answers['name']}! You just tell me something. Let me repeat it again to confirm.`)
    console.log(`Your number is ${
      
      answers['num']}`)
    console.log(`You ${
      
      answers['like']?'':'don\'t'} like nodejs`)
    console.log(`You are gonna eat ${
      
      answers['fruits']} today`)
    console.log(`You often play ${
      
      answers['sports']}`)
    console.log(`${
      
      answers['color']} is your color`)
    console.log(`Your direction is ${
      
      answers['direction']}`)
    console.log(`And the new pwd is ${
      
      answers['pwd']}`)
    console.log(`editorInout: ${
      
      answers['editorInput']}`)
}).catch(error =>{
    
    
    console.log(error)
})
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158

在这里插入图片描述

2.7 使用 exports 从 Node.js 文件中公开功能

在通常情况下,文件中定义的对象或者变量都是私有的,不会公开给外界。但是module系统的module.export 的API提供了这样的功能。

当将对象或函数赋值为新的 exports 属性时,这就是要被公开的内容,因此,可以将其导入应用程序的其他部分或其他应用程序中。

第一种方式,将对象赋值给 module.exports(这是模块系统提供的对象),这会使文件只导出该对象:

const car = {
    
    
    brand:'brand',
    speed:'70',
    color:'red'
}

module.exports = car
1234567

在另一个文件中:

const car = require('./car')
1

在这里插入图片描述

第二种方法,将要导出的对象添加为 exports 的属性。这种方式可以导出多个对象、函数或数据:

const obj = {
    
    
    a:1,
    b:2,
    c:3
}

function add(a,b){
    
    
    return a + b;
}

const username = 'Sheldon'

exports.obj = obj
exports.add = add
exports.username = username


1234567891011121314151617

在另一个文件中:

const utils = require('./utils')

console.log(utils.obj)
console.log(utils.add(4,5))
console.log('my name is ' + utils.username)
12345

在这里插入图片描述

2.8 包管理器

2.8.1 npm 是 Node.js 标准的软件包管理器

在 2017 年 1 月时,npm 仓库中就已有超过 350000 个软件包,这使其成为世界上最大的单一语言代码仓库,并且可以确定几乎有可用于一切的软件包。

它起初是作为下载和管理 Node.js 包依赖的方式,但其现在也已成为前端 JavaScript 中使用的工具。

Yarn 是 npm 的一个替代选择。

2.8.2 下载

1)安装所有依赖,如果项目具有 package.json 文件,则通过运行:

npm install
1

它会在 node_modules 文件夹(如果尚不存在则会创建)中安装项目所需的所有东西。

2)安装单个软件包

npm install <package-name>
1

通常会在此命令中看到更多标志:

–save 安装并添加条目到 package.json 文件的 dependencies。
–save-dev 安装并添加条目到 package.json 文件的 devDependencies。

区别主要是,devDependencies 通常是开发的工具(例如测试的库),而 dependencies 则是与生产环境中的应用程序相关。

2.8.3 更新

npm update
1

npm 会检查所有软件包是否有满足版本限制的更新版本。

也可以指定单个软件包进行更新:

npm update <package-name>

12

2.8.4 版本控制

除了简单的下载外,npm 还可以管理版本控制,因此可以指定软件包的任何特定版本,或者要求版本高于或低于所需版本。

很多时候,一个库仅与另一个库的主版本兼容。或者,一个库的最新版本中有一个缺陷(仍未修复)引起了问题。

指定库的显式版本还有助于使每个人都使用相同的软件包版本,以便整个团队运行相同的版本,直至 package.json 文件被更新。

2.8.5 运行任务

package.json 文件支持一种用于指定命令行任务(可通过使用以下方式运行)的格式:

npm run <task-name>
1

例如:

{
    
    
  "scripts": {
    
    
    "start-dev": "node lib/server-development",
    "start": "node lib/server-production"
  },
}
123456

因此可以运行如下,而不是输入那些容易忘记或输入错误的长命令:

npm run start-dev
npm run start
12

2.8.6 软件安装到了哪里

npm安装软件包有两种方式,一种是本地安装,一种是全局安装。

(1)本地安装。
默认情况下,输入npm install命令,比如:

npm install lodash
1

软件包会被安装到当前文件树中的 node_modules 子文件夹下。

在这里插入图片描述

(2)全局安装。
使用 -g 标志可以执行全局安装:

npm install -g lodash
1

在这种情况下,npm 不会将软件包安装到本地文件夹下,而是使用全局的位置。npm root -g 命令会告知其在计算机上的确切位置。

在这里插入图片描述
在这里插入图片描述

2.8.7 如何使用或者执行安装的软件包

(1)使用软件包
如已经安装软件包,只需要在代码中用require引入即可使用,比如:

const _ = require('lodash')
let arr = [0, 1, false, 2, '', 3];
console.log(_.compact(arr))
123

在这里插入图片描述
(2)执行软件包。
如果软件包是可执行文件,它会把可执行文件放到 node_modules/.bin/ 文件夹下,但是.bin文件夹相对编辑器来说是隐藏的,可以直接打开文件资源目录查看。
当使用 npm install cowsay 安装软件包时,它会在 node_modules 文件夹中安装自身以及一些依赖包:
在这里插入图片描述
可以输入 ./node_modules/.bin/cowsay 来运行它:
在这里插入图片描述

最新版本的 npm(自 5.2 起)中包含的 npx提供了更好的运行方式:

npx cowsay
1

或者

npx cowsay take me out of here
1

在这里插入图片描述

2.8.8 package.json 指南

package.json 指南这部分内容官网写得非常详细,建议直接前往官网查看,在这我就不做补充说明了。

2.8.9 package-lock.json 文件

package-lock.json 文件

2.8.10 查看 npm 包安装的版本

查看 npm 包安装的版本
(1)查看已经安装的 npm 软件包列表:

npm list
1

(2)查看已经安装的全局 npm 软件包列表:

npm list -g
1

(3)获取顶层的软件包:

npm list --depth=0
1

(4)指定名称来获取特定软件包的版本:

npm list [package_name]
1

(5)查看软件包在 npm 仓库上最新的可用版本:

npm view [package_name] version
1

2.8.11 安装 npm 包的旧版本

安装 npm 包的旧版本

使用 @ 语法来安装 npm 软件包的旧版本:

npm install <package>@<version>
1

安装全局的软件包:

npm install -g <package>@<version>
1

查看软件包所有的以前的版本:

npm view <package> versions
1

2.8.12 将所有 Node.js 依赖包更新到最新版本

将所有 Node.js 依赖包更新到最新版本

2.8.13 使用 npm 的语义版本控制

使用 npm 的语义版本控制

2.8.14 卸载 npm 软件包

卸载 npm 软件包

主要命令:

npm uninstall <package-name>
1

2.8.15 npm 全局或本地的软件包

npm 全局或本地的软件包
通常,所有的软件包都应本地安装。

2.8.16 npm 依赖与开发依赖

npm 依赖与开发依赖

安装软件包:

npm install <package-name>
1

安装开发依赖软件包:

npm install <package-name> -D
1

在这里插入图片描述

当部署生产环境时,如果执行 npm install 会默认时开发部署,此时应该使用 npm install --production 避免安装这些开发依赖项。

2.8.17 Node.js 包运行器 npx

Node.js 包运行器 npx

无需安装的命令执行:
在这里插入图片描述

2.9 Node.js 事件循环

Node.js 事件循环

事件循环是了解 Node.js 最重要的方面之一。因为它阐明了 Node.js 如何做到异步且具有非阻塞的 I/O,所以它基本上阐明了 Node.js 的“杀手级应用”,正是这一点使它成功了。

2.10 了解 process.nextTick()

了解 process.nextTick()

其实简单来说,process.nextTick()总结一下就是一句话,在事件循环进行一次完整的行程时,会优先执行process.nextTick()中的函数,而不是和setTimeout(()=>{},0)一样将其加入到队列中。换句话说,就是可以把process.nextTick()当成同步执行的最后一步来看。

我们来看下面一个示例:

let bar;

function  asyncApiCall(callback){
    
    
  callback()
}

asyncApiCall(()=>{
    
    
  console.log('bar',bar)
})

bar = 1;
1234567891011

在这里插入图片描述
我们命名一个函数为异步Api回调函数,但是实际上它是同步的,此时bar还未赋值,所以返回undefined。

此时process.nextTick()就派上了用场,我们将上面示例做些改动:

let bar;

function  asyncApiCall(callback){
    
    
  process.nextTick(callback)
}

asyncApiCall(()=>{
    
    
  console.log('bar',bar)
})

bar = 1;
1234567891011

在这里插入图片描述
可以发现此时异步Api回调函数生效了,此时的代码等价于:

let bar;
bar = 1;
console.log('bar',bar);
123

定时器也可以做到类似的效果,那么 process.nextTick() 和 定时器有什么区别吗?这个留给大家做个思考,在接下来的学习中,我们也会更加了解他们的异同。

2.10 了解 setImmediate()

了解 setImmediate()
当要异步地(但要尽可能快)执行某些代码时,其中一个选择是使用 Node.js 提供的 setImmediate() 函数:

console.log(1)
setImmediate(() => {
    
    
    console.log('setImmediate')
})
console.log(2)
12345

在这里插入图片描述
可以看到setImmediate()和setTimeout()、process.nextTick()的作用非常类似,都是为了异步执行某些操作。但是他们的执行顺序又是如何的呢?

官网上是这样解释的,传给 process.nextTick() 的函数会在事件循环的当前迭代中(当前操作结束之后)被执行,作为 setImmediate() 参数传入的任何函数都是在事件循环的下一个迭代中执行的回调,延迟 0 毫秒的 setTimeout() 回调与 setImmediate() 非常相似。

这就意味着 process.nextTick()的回调无论如何都是比setImmediate()和setTimeout()的回调更加提前,但是setImmediate()和setTimeout()的回调执行顺序取决于具体情况。让我们通过下面一个示例直观地比较下:

setTimeout(()=>{
    
    
    console.log('setTimeout 100 s')
},100)
setTimeout(()=>{
    
    
    console.log('setTimeout 0 s')
},0)
setImmediate(() => {
    
    
    console.log('setImmediate')
})

process.nextTick(()=>{
    
    
    console.log('process.nextTick()')
})
console.log('1')
123456789101112131415

在这里插入图片描述
可以看到三者的执行顺序为:
1)process.nextTick()
2)setTimeout(()=>{},0)
3)setImmediate(() => {})
4)setTimeout(()=>{},100)

但是setTimeout 和 setImmediate 的执行顺序其实是有点玄学的,nodejs官方对setImmediate的定义为异步地(但要尽可能快)执行某些代码时使用它,尽可能快这几个字就有点暧昧不清了,说明它的执行时间其实是不一定的。

于是我做了个实验。

只保留 setTimeout 和 setImmediate:

setTimeout(()=>{
    
    
    console.log('setTimeout 0 s')
},0)
setImmediate(() => {
    
    
    console.log('setImmediate')
})
123456

在这里插入图片描述
发现即使在setTimeout的回调时间为0ms的情况下,两者的回调顺序也是不一定的。
在这里插入图片描述
在这里插入图片描述
经过对 setTimeout 回调时间的不断调整,发现在 10s 时,setImmediate的回调都是优先于setTimeout 执行的。但是,这只是我的这台主机在只有这两个函数执行情况下的结果,在面对不同的复杂情况时,结果仍然可能会有所区别。

如果要彻底了解setTimeout和setImmediate到底谁先执行,理解Event Loop,推荐看看《setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop》这篇文章,讲得挺透彻的。主要分成了以下3点进行阐述说明:
(1)同步和异步。
同步和异步简单理解就是,同步是按照书写顺序进行执行的。异步的代码可能和书写顺序不同,可能写在后面的代码先执行。
(2)JS异步是如何实现的。
我们都知道JS是单线程的,那单线程是怎么实现异步的呢?事实上所谓的"JS是单线程的"只是指JS的主运行线程只有一个,而不是整个运行环境都是单线程。JS的运行环境主要是浏览器,以大家都很熟悉的Chrome的内核为例,他不仅是多线程的,而且是多进程的:
在这里插入图片描述
上图只是一个概括分类,意思是Chrome有这几类的进程和线程,并不是每种只有一个,比如渲染进程就有多个,每个选项卡都有自己的渲染进程。有时候我们使用Chrome会遇到某个选项卡崩溃或者没有响应的情况,这个选项卡对应的渲染进程可能就崩溃了,但是其他选项卡并没有用这个渲染进程,他们有自己的渲染进程,所以其他选项卡并不会受影响。这也是Chrome单个页面崩溃并不会导致浏览器崩溃的原因,而不是像老IE那样,一个页面卡了导致整个浏览器都卡。
(3)Event Loop 事件循环是什么
所谓Event Loop,就是事件循环,其实就是JS管理事件执行的一个流程,具体的管理办法由他具体的运行环境确定。目前JS的主要运行环境有两个,浏览器和Node.js。这两个环境的Event Loop还有点区别。

2.11 探索 JavaScript 定时器

探索 JavaScript 定时器

2.11.1 setTimeout()

有时候写JavaScript的时候,我们希望延迟某些函数的执行,这个时候setTimeout()函数就派上了用场,它可以指定该函数延迟执行的时间(单位是毫秒):

setTimeout(()=>{
    
    
    console.timeEnd('定时器2')
},2000)
setTimeout(()=>{
    
    
    console.timeEnd('定时器1')
},50)

console.time('定时器1') 
console.time('定时器2') 
12345678910

在这里插入图片描述
可以看到,定时器其实也并不是百分百精准的,会有一些误差

定时器还可以传入携带函数的参数:

function myFunction(firstParam,secondParam){
    
    
    console.log('firstParam',firstParam)
    console.log('secondParam',secondParam)
}

setTimeout(myFunction,2000,'hello','how do you do')
123456

在这里插入图片描述

2.11.2 setInterval()

setInterval 是一个类似于 setTimeout 的函数,不同之处在于:它会在指定的特定时间间隔(以毫秒为单位)一直地运行回调函数,而不是只运行一次。通常在 setInterval 回调函数中调用 clearInterval,以使其自行判断是否应该再次运行或停止。示例:

let times = 0;
let setIntervalId = setInterval(()=>{
    
    
    times++
    console.log(`这是第${
      
      times}次执行setIntrval`)
    if(times === 10){
    
    
        clearInterval(setIntervalId)
        setIntervalId = null
        return
    }
},1000)
12345678910

在这里插入图片描述

2.11.3 递归的 setTimeout

setInterval 每 n 毫秒启动一个函数,而无需考虑函数何时完成执行。如果一个函数总是花费相同的时间,那就没问题了。但是在很多时候函数可能花费的时间取决于网络条件,此时就有可能导致两个函数的执行时间重叠。为了避免这一情况,可以在回调函数完成时安排要被调用的递归的 setTimeout:

let times = 0
const myFunction = () => {
    
    
    times++
    console.log(`这是第${
      
      times}次执行setTimeout`)
    if(times < 10){
    
    
        setTimeout(myFunction, 1000)
    }
    
}
  
setTimeout(myFunction, 1000)
1234567891011

在这里插入图片描述
Node.js 还提供 setImmediate()(相当于使用 setTimeout(() => {}, 0)),通常用于与 Node.js 事件循环配合使用。

2.12 JavaScript 异步编程与回调

JavaScript 异步编程与回调

2.13 了解 JavaScript Promise

了解 JavaScript Promise

2.13.1 Promise 简介

Promise 通常被定义为最终会变为可用值的代理。

我们知道,当出现回调函数嵌套回调函数,并且层级很深时,就会使代码变得非常复杂,难以阅读和理解。因此,就出现了Promise,它是一种处理异步代码(而不会陷入回调地狱)的方式。

多年来,promise 已成为语言的一部分(在 ES2015 中进行了标准化和引入),并且最近变得更加集成,在 ES2017 中具有了 async 和 await。它们的底层就是使用promise,因此了解promise的工作方式成为了解 async 和 await的基础。

标准的现代 Web API 也使用了 promise,例如:Battery API、Fetch API和Service Worker。

2.13.2 Promise 如何运作

我看了下nodejs官方的解释,总结一句话就是运行异步函数,然后将结果返回给then或者catch。
但是感觉还是对Promise一知半解,于是就上网百度了下Promise,发现一篇名为《大白话讲解Promise》的文章对Promise讲得比较透彻。我在这再做下总结。

(1)Promise到底是啥?
直接在浏览器控制台输出看看:
在这里插入图片描述
可以看到,Promise是一个构造函数,自己身上有all、reject、resolve这几个眼熟的方法,原型上有then、catch等同样很眼熟的方法。这么说用Promise new出来的对象肯定就有then、catch方法咯。

(2)Promise怎么用?

上面说了Promise是一个构造函数,那我们new一个看看看:

var p = new Promise(function(resolve, reject){
    
    
    //做一些异步操作
    setTimeout(function(){
    
    
        console.log('执行完成');
        resolve('测试数据');
    }, 1000);
});
1234567

在这里插入图片描述
可以看到,Promise接收一个函数作为参数,函数又包含两个参数——函数resolve和函数reject,可以先简单理解为异步操作成功的回调函数和异步操作失败的回调函数。

在上面的代码中,我们new一个Promise对象,并没有调用它,但是传进去的函数就已经执行了,所以在实际使用时,Promise通常包裹在一个函数中,在需要的时候再进行调用。

function runAsync(){
    
    
    var p = new Promise(function(resolve, reject){
    
    
        //做一些异步操作
        setTimeout(function(){
    
    
            console.log('执行完成');
            resolve('测试数据');
        }, 1000);
    });
    return p;            
}
runAsync();
1234567891011

在这里插入图片描述
那接下来我们又怎么使用resolve中的数据呢?

这时候就会用到我们开头提到的then方法了,它会接收一个函数,并且该函数的参数为Promise中resolve中的数据。

function runAsync(){
    
    
    var p = new Promise(function(resolve, reject){
    
    
        //做一些异步操作
        setTimeout(function(){
    
    
            console.log('执行完成');
            resolve('测试数据');
        }, 1000);
    });
    return p;            
}
runAsync().then(function(data){
    
    
    console.log(data)
});
12345678910111213

在这里插入图片描述
有人会问:这种写法和回调嵌套有什么区别呢?

2.13.3 链式操作

当我们把回调嵌套变成多层,就能知道Promise的厉害之处了。Promise的正确用法如下:

function runAsync1(){
    
    
    var p = new Promise(function(resolve, reject){
    
    
        //做一些异步操作
        setTimeout(function(){
    
    
            console.log('异步操作1执行完成');
            resolve('测试数据1');
        }, 1000);
    });
    return p;            
}
function runAsync2(){
    
    
    var p = new Promise(function(resolve, reject){
    
    
        //做一些异步操作
        setTimeout(function(){
    
    
            console.log('异步操作2执行完成');
            resolve('测试数据2');
        }, 1000);
    });
    return p;            
}
function runAsync3(){
    
    
    var p = new Promise(function(resolve, reject){
    
    
        //做一些异步操作
        setTimeout(function(){
    
    
            console.log('异步操作3执行完成');
            resolve('测试数据3');
        }, 1000);
    });
    return p;            
}

runAsync1().then(function(data){
    
    
    console.log(data);
    return runAsync2();
}).then(function(data){
    
    
    console.log(data);
    return runAsync3();
}).then(function(data){
    
    
    console.log(data);
})

1234567891011121314151617181920212223242526272829303132333435363738394041

在这里插入图片描述
从表面上看,Promise将原来复杂难懂的地狱回调变成了使用链式调用的方式(当然你也可以简单地这么理解),但是它的精髓在于状态的维护和传递(resolve和reject)使得回调函数能够得到及时的调用,比callback更加简单高效。

另外,在then方法中我们可以还直接return数据而非Promise对象:

runAsync1().then(function(data){
    
    
    console.log(data)
    return runAsync2()
}).then(function(data){
    
    
    console.log(data)
    return '测试的return数据'
}).then(function(data){
    
    
    console.log(data)
    return {
    
    
        a:'1',
        b:2
    }
}).then(function(data){
    
    
    console.log(data)
})
123456789101112131415

在这里插入图片描述

2.13.4 reject用法

目前位置我们已经学习了Promise的创建方式和resolve、then的使用方法,那么reject又如何使用呢?之前我们说的resolve是将Promise的状态设置为fullfiled,然后then就能捕捉到,然后进行成功的回调。那么成功必然就有失败的情况,reject就是将Promise的状态设置为rejected,然后被then捕捉到,进而进行失败的回调。示例:

function getNumber(){
    
    
    var p = new Promise(function(resolve, reject){
    
    
        //做一些异步操作
        setTimeout(function(){
    
    
            var num = Math.ceil(Math.random()*10); //生成1-10的随机数
            if(num<=5){
    
    
                resolve(num);
            }
            else{
    
    
                reject('数字太大了');
            }
        }, 1000);
    });
    return p;            
}
getNumber()
.then(
    function(data){
    
    
        console.log('resolved');
        console.log(data);
    }, 
    function(reason, data){
    
    
        console.log('rejected');
        console.log(reason);
    }
);
1234567891011121314151617181920212223242526

getNumber函数用来异步获取一个1-10的随机数,如果小于等于5,我们就认为成功了,使用resolve修改Promise的状态,并且传递一个参数给then的第一个参数(是一个函数,成功回调)。否则,我们就认为失败了,使用reject修改Promise的状态,并且传递一个参数给then的第二个参数(也是一个函数,失败回调)。多次运行

getNumber()
.then(
    function(data){
    
    
        console.log('resolved');
        console.log(data);
    }, 
    function(reason, data){
    
    
        console.log('rejected');
        console.log(reason);
    }
);
1234567891011

我们将会随机得到
在这里插入图片描述
或者类似于
在这里插入图片描述
这样的两种结果。

2.13.5 catch用法

Promise除了then方法还有一个catch方法,让我们直接通过一个示例来了解下:

getNumber()
.then(function(data){
    
    
    console.log('resolved');
    console.log(data);
})
.catch(function(reason){
    
    
    console.log('rejected');
    console.log(reason);
});
123456789

在这里插入图片描述
可以看到,catch的作用在这和then的第二个参数一样,都能够接收reject的回调。如果是这样,那么catch还有存在的必要吗?我们来看看下面这个示例:

getNumber()
.then(function(data){
    
    
    console.log('resolved');
    console.log(data);
    console.log(somedata); //此处的somedata未定义
})
.catch(function(reason){
    
    
    console.log('rejected');
    console.log(reason);
});
12345678910

在这里插入图片描述
我们知道,在通常情况下,如果有变量未定义,那么程序就会终止,并且报错,在浏览器控制台中通常能够看到类似的红色报错。
在这里插入图片描述
但是Promise中的catch则有所不同,发生错误,catch会捕捉原因,而不是终止程序,以保证程序的正常运转,这个功能和try/catch极为相似。

2.13.6 all用法

之前我们讲了Promise的链式用法,通常情况下,在下一个Promise需要使用到上一个Promise回调参数的情况下大多使用这种方式。

如果多个Promise毫不相关,那么就可以使用all方法,使他们并行执行异步操作,然后在then中一起返回他们的结果,比如我们之前的runAsync1、runAsync2、runAsync3返回的3个Promise对象没有任何联系,就可以写成:

Promise
.all([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
    
    
    console.log(results);
});
12345

在这里插入图片描述
在很多网页表格中,我们经常会遇到多个不同维度的下拉框作为表格的查询条件,并且渲染的网页需要使用这些下拉框的初始值进行渲染表格,这种场景下使用Promise.all方法就显得极为合适。

2.13.7 race用法

话不多说,先看个race的示例:

// 先对三个Promise进行定义,其中runAsync3的时间设置为最短,500ms
function runAsync1(){
    
    
    var p = new Promise(function(resolve, reject){
    
    
        //做一些异步操作
        setTimeout(function(){
    
    
            console.log('异步操作1执行完成');
            resolve('测试数据1');
        }, 1000);
    });
    return p;            
}
function runAsync2(){
    
    
    var p = new Promise(function(resolve, reject){
    
    
        //做一些异步操作
        setTimeout(function(){
    
    
            console.log('异步操作2执行完成');
            resolve('测试数据2');
        }, 1000);
    });
    return p;            
}
function runAsync3(){
    
    
    var p = new Promise(function(resolve, reject){
    
    
        //做一些异步操作
        setTimeout(function(){
    
    
            console.log('异步操作3执行完成');
            resolve('测试数据3');
        }, 500);
    });
    return p;            
}
// 使用race
Promise
.race([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
    
    
    console.log('results',results);
});
12345678910111213141516171819202122232425262728293031323334353637

在这里插入图片描述
可以看到,即使将runAsync3()放到race的最后,但是因为它的反应时间最短,所以then返回的就是runAsync3()的回调。

顾名思义,race在英文中是竞赛的意思,谁先到达终点,那么就返回谁。使用场景多为资源超时请求下,就使用另一个setTimeout进行操作。

2.14 具有 Async 和 Await 的现代异步 JavaScrip

2.14.1 为什么引入Async / Await

之前说的Promise很好地解决了地狱回调的问题,但是同时也引入了它自身的复杂性和语法复杂性。于是,异步函数async/await应运而生,它们使代码看起来像是同步的,但它是异步的并且在后台无阻塞。

2.14.2 工作原理

下面我们通过一个 async/await 的简单示例,了解下他们的工作原理:

1、定义一个函数,返回promise:

const doSomethingAsync = () => {
    
    
  return new Promise(resolve => {
    
    
    setTimeout(() => resolve('做些事情'), 3000)
  })
}
12345

2、调用此函数时加上 await,然后调用的代码就会停止直到 promise 被解决或被拒绝。 注意:客户端函数必须被定义为 async。

const doSomething = async () => {
    
    
  const res = await doSomethingAsync()
  console.log('res',res)
}
1234

3、整合一下:

const doSomethingAsync = () => {
    
    
  return new Promise(resolve => {
    
    
    setTimeout(() => resolve('做些事情'), 3000)
  })
}

const doSomething = async () => {
    
    
  const res = await doSomethingAsync()
  console.log('res',res)
}

console.log('之前')
doSomething()
console.log('之后')
1234567891011121314

在这里插入图片描述
从返回的结果可以看到确实是Promise确实异步的,但是在async和await的加持下,更加像是同步的代码。

2.14.3 async使函数返回promise

在任何函数之前加上 async 关键字意味着该函数会返回 promise。

即使没有显式地这样做,它也会在内部使它返回 promise。

const aFunction = async () => {
    
    
  return '测试'
}

aFunction().then(function(res){
    
    
  console.log(res)
})
1234567

在这里插入图片描述
等价于

const aFunction = () => {
    
    
  return Promise.resolve('测试')
}

aFunction().then(function(res){
    
    
  console.log(res)
})
1234567

在这里插入图片描述

2.14.4 代码更容易阅读

const runAsync1 = () => {
    
    
  return new Promise(resolve => {
    
    
    setTimeout(() => resolve('测试数据1'), 3000)
  })
}
const runAsync2 = () => {
    
    
  return new Promise(resolve => {
    
    
    setTimeout(() => resolve('测试数据2'), 2000)
  })
}
const runAsync3 = () => {
    
    
  return new Promise(resolve => {
    
    
    setTimeout(() => resolve('测试数据3'), 1000)
  })
}

const runAllAsync = async() =>{
    
    
  console.time('runAllAsync')
  let res = []
  let temp = await runAsync1()
  res.push(temp)
  temp = await runAsync2()
  res.push(temp)
  temp = await runAsync3()
  res.push(temp)
  // res[0] = await runAsync1()
  // res[1] = await runAsync2()
  // res[2] = await runAsync3()
  console.timeEnd('runAllAsync')
  console.log('res',res)
}

runAllAsync()

12345678910111213141516171819202122232425262728293031323334

在这里插入图片描述
可以看到,我们定义了3个异步函数,虽然runAsync1 的setTimeout时间最长,但是在async和await中是最早运行完成的,并且从测试的总时间6.037s可以看出,runAllAsync的运行时间为runAsync1、runAsync2、runAsync3的总和,这就意味着他们的执行是有顺序的。

换句话说,你可以理解为async/await有着将异步函数顺序书写和执行的能力,这意味着异步代码的书写将变得更加优雅且易于阅读。一句话,用它!

2.15 Node.js 事件触发器

Node.js 事件触发器

事件触发器,主要有定义事件,触发事件,移除事件等功能,详细的API可以前往官网API文档查看。

2.16 http服务

学了这么多,终于到了http模块

2.16.1 搭建http服务器

话不多说,我们来看一个简单的 HTTP web 服务器示例:

const http = require('http')

// 在server.listen中输入hostman时,会提示回车会自动生成这一行引入
const {
    
     hostname } = require('node:os')

const port = 3000

// 创建服务器
const server = http.createServer((req, res) => {
    
    
  // 设置200,表示响应成功
  res.statusCode = 200
  // 设置charset=utf-8,否则会出现乱码
  res.setHeader('Content-Type', 'text/plain;charset=utf-8')
  // 结束并关闭响应,将网页内容作为参数,添加到end()
  res.end('hello world!')
})

// 设置服务器在3000端口上并进行监听,当服务器就绪时,输出服务器地址
server.listen(port, () => {
    
    
  console.log(`服务器运行在 http://${
      
      hostname}:${
      
      port}/`)
})

简要说明下步骤:
(1)引入http模块:

const http = require('http')
1

(2)创建http服务器:

const server = http.createServer((req, res) => {
    
    
  // 设置200,表示响应成功
  res.statusCode = 200
  // 设置charset=utf-8,否则会出现乱码
  res.setHeader('Content-Type', 'text/plain;charset=utf-8')
  // 结束并关闭响应,将网页内容作为参数,添加到end()
  res.end('hello world!')
})
12345678

(3)设置服务器端口并进行监听,通常回调函数是输出服务器地址:

server.listen(port, () => {
    
    
  console.log(`服务器运行在 http://${
      
      hostname}:${
      
      port}/`)
})
123

在这里插入图片描述
在这里插入图片描述

2.17 文件操作

文件操作这一节就不按照官网的顺序进行说明了,感觉有些乱。我们分别从文件的读取、写入、文件追加写入和文件高级操作这四个点进行说明。

2.17.1 文件的读取

同步读取方法 readFileSync

readFileSync有两个参数:

第一个参数是文件路径或者文件描述符

第二个参数是对象options,有encoding(编码,默认值为null)和flag(标识位,默认值为 r )。也可以直接传入encoding,通常为求简便,都是直接传入encoding。

桌面或者任意位置创建测试文件1.txt,输入任意字符保存。
在这里插入图片描述

代码测试:

const fs = require('fs')

let buf1 = fs.readFileSync('C:/Users/Pactera/Desktop/1.txt')
// 等价写法
let buf2 = fs.readFileSync('C:/Users/Pactera/Desktop/1.txt',{
    
    
    encoding:null,
    flag:'r'
})

let data1 = fs.readFileSync('C:/Users/Pactera/Desktop/1.txt','utf8')
// 等价写法
let data2 = fs.readFileSync('C:/Users/Pactera/Desktop/1.txt',{
    
    
    encoding:'utf8',
    flag:'r'
})

console.log('buf1:',buf1)
console.log('buf2:',buf2)
console.log('data1:',data1)
console.log('data2:',data2)
1234567891011121314151617181920

在这里插入图片描述

异步读取方法 readFile

readFile 有3个参数,其中前两个参数和readFileSync相同,第三个参数为回调函数,函数有两个参数errdata

异步读取之前的1.txt文件:

const fs = require('fs')

fs.readFile('C:/Users/Pactera/Desktop/1.txt','utf8',(err,data)=>{
    
    
    if(err){
    
    
        console.log('err:',err)
    }
    else{
    
    
       console.log('data:',data) 
    }
    
})
1234567891011

在这里插入图片描述
异步读取不存在的4.txt文件:

const fs = require('fs')

fs.readFile('C:/Users/Pactera/Desktop/4.txt','utf8',(err,data)=>{
    
    
    if(err){
    
    
        console.log('err:',err)
    }
    else{
    
    
       console.log('data:',data) 
    }
    
})
1234567891011

在这里插入图片描述

2.17.2 文件的写入

同步写入方法 writeFileSync

writeFileSync 有3个参数:

第一个参数是文件路径或者文件描述符

第二个参数是写入的数据,类型可以是String或者Buffer

第三个参数是对象options,其中有 encoding(编码,默认值为utf8)、flag(标识位,默认值为w) 和 mode(权限位,默认值为0o666)。也可以直接传入enconding,为了方便,通常只传encoding即可。

这次我们使用buffer进行写入:

const fs = require('fs')

const str = 'Cool'
// 将字符串转为Buffer
const buf = Buffer.from(str)
console.log('buf:',buf)

fs.writeFileSync('C:/Users/Pactera/Desktop/1.txt',buf)

let data = fs.readFileSync('C:/Users/Pactera/Desktop/1.txt','utf8')

console.log('data:',data)
123456789101112

在这里插入图片描述
在这里插入图片描述

异步写入方法

writeFile 前两个参数和 writeFileSync相同,第三个参数是回调函数,参数是 err 错误:

const fs = require('fs')

fs.writeFile('C:/Users/Pactera/Desktop/1.txt','Good good study',err =>{
    
    
    if(err){
    
    
        console.log('err:',err)
    }
    else{
    
    
        fs.readFile('C:/Users/Pactera/Desktop/1.txt','utf8',(err,data)=>{
    
    
            console.log('data:',data)
        })
    }
})
123456789101112

在这里插入图片描述
在这里插入图片描述

2.17.3 文件的追加写入

writeFileSyncwriteFile 实现了文件的同步写入和异步写入,但是会直接覆盖原来的文件内容,那么如果要在原来的内容后进行追加写入该如何进行操作呢?

同步追加写入方法 appendFileSync

appendFileSync 有3个参数,和同步同步写入方法writeFileSync参数一致,不做赘述。

const fs = require('fs')

fs.appendFileSync('C:/Users/Pactera/Desktop/1.txt',', day day up!')

let data = fs.readFileSync('C:/Users/Pactera/Desktop/1.txt','utf8')
console.log('data:',data)
123456

在这里插入图片描述
在这里插入图片描述

异步追加写入方法 appendFile

appendFile 有3个参数,和异步写入方法 writeFile 参数一致,不做赘述。

const fs = require('fs')

fs.appendFile('C:/Users/Pactera/Desktop/1.txt',' 好好学习,天天向上',err =>{
    
    
    if(err){
    
    
        console.log('err:',err)
    }
    else{
    
    
        fs.readFile('C:/Users/Pactera/Desktop/1.txt','utf8',(err,data)=>{
    
    
            console.log('data:',data)
        })
    }
})
123456789101112

在这里插入图片描述
在这里插入图片描述

2.17.4 文件拷贝写入

同步拷贝写入方法 copyFileSync

copyFileSync 有2个参数,第一个是被拷贝文件路径,第二个是拷贝文件路径,如果拷贝文件路径不存在,则会创建该文件后进行拷贝。

const fs = require('fs')

fs.copyFileSync('C:/Users/Pactera/Desktop/1.txt','C:/Users/Pactera/Desktop/2.txt')

let data = fs.readFileSync('C:/Users/Pactera/Desktop/2.txt','utf8')
console.log('data:',data)
123456

在这里插入图片描述
在这里插入图片描述

异步拷贝写入方法 copyFile

copyFile 有3个参数,前两个参数分别是被拷贝文件路径和拷贝文件路径,第三个是回调函数。

const fs = require('fs')

fs.copyFile('C:/Users/Pactera/Desktop/1.txt','C:/Users/Pactera/Desktop/3.txt',()=>{
    
    
    fs.readFile('C:/Users/Pactera/Desktop/3.txt','utf8',(err,data)=>{
    
    
        if(err){
    
    
            console.log('err:',err)
        }
        else{
    
    
            console.log('data:',data)
        }
    })
})
123456789101112

在这里插入图片描述
在这里插入图片描述

2.17.5 文件操作的高级方法

打开文件 open

open方法有3个参数,第一个是打开文件路径,第二个权限位,第三个是回调函数:

const fs = require('fs')
fs.open('C:/Users/Pactera/Desktop/1.txt','r',(err,fd)=>{
    
    
    console.log('fd:',fd)
})
1234

在这里插入图片描述

其实对于open这个方法我认为名字取得并不是非常合理,这个方法实际上并不会执行打开我们的文件(类似执行双击文件打开)操作,而是在回调函数中获取文件描述符fd相当于给文件取了个名字,后面就可以通过文件描述符对文件进行操作。比如将同步读取方法中的路径参数修改为文件操作符效果是相同的:

const fs = require('fs')
fs.open('C:/Users/Pactera/Desktop/1.txt','r',(err,fd)=>{
    
    
    console.log('fd:',fd)
    let data = fs.readFileSync(fd,'utf8')
    console.log('data:',data)
})
123456

在这里插入图片描述

关闭文件 close

close方法有2个参数,第一个是文件描述符,第二个是回调函数,参数是err。我们通过以下两个示例来观察close对文件描述符的影响。
在这里插入图片描述

示例一:

const fs = require('fs')
fs.open('C:/Users/Pactera/Desktop/1.txt','r',(err,fd)=>{
    
    
    console.log('fd:',fd)
    fs.open('C:/Users/Pactera/Desktop/2.txt','r',(err,fd)=>{
    
    
        console.log('fd:',fd)
    })
})
1234567

在这里插入图片描述
示例二:

const fs = require('fs')
fs.open('C:/Users/Pactera/Desktop/1.txt','r',(err,fd)=>{
    
    
    console.log('fd',fd)
    let data1 = fs.readFileSync(fd,'utf8')
    console.log('data1:',data1)
    fs.close(fd,err=>{
    
    
        console.log('关闭成功!')
        fs.open('C:/Users/Pactera/Desktop/2.txt','r',(err,fd)=>{
    
    
            console.log('new fd:',fd)
            let data2 = fs.readFileSync(fd,'utf8')
            console.log('data2:',data2)
        })
    })
})
1234567891011121314

在这里插入图片描述
可以看到,当使用close方法后,文件描述符会进行重置。

那么问题来了,如果我们在close之前,将文件描述符保存在一个变量中,那么后面还能够使用吗?

const fs = require('fs')
fs.open('C:/Users/Pactera/Desktop/1.txt','r',(err,fd)=>{
    
    
    let fd1 = fd
    console.log('fd1',fd1)
    fs.close(fd,err=>{
    
    
        console.log('关闭成功!')
        fs.open('C:/Users/Pactera/Desktop/2.txt','r',(err,fd)=>{
    
    
            console.log('fd1:',fd1)
            let data1 = fs.readFileSync(fd1,'utf8')
            console.log('data1:',data1)
            let fd2 = fd
            console.log('fd2:',fd2)
            let data2 = fs.readFileSync(fd2,'utf8')
            console.log('data2:',data2)
        })
    })
})
1234567891011121314151617

在这里插入图片描述
可以看到,原来1.txt的文件描述符的内容变成了2.txt,说明了文件描述符是以指针的方式存在的,当我们使用 close ,再使用 open 打开新的文件时,并不会创建一个新的文件描述符,而是复用原来的,只不过将存储的路径做了替换。就像以下这段操作:
在这里插入图片描述
但是我们注意到,既然覆盖后两个指向的是同一条路径,那为什么会出现data2为空的情况呢?其实这个和 close 方法无关,问题出在readFileSync方法,它无法打开同一个文件两次,一次后的相同操作会失效,做个实验:

const fs = require('fs')
fs.open('C:/Users/Pactera/Desktop/1.txt','r',(err,fd)=>{
    
    
    let fd1 = fd
    console.log('fd1',fd1)
    fs.close(fd,err=>{
    
    
        console.log('关闭成功!')
        fs.open('C:/Users/Pactera/Desktop/2.txt','r',(err,fd)=>{
    
    
            let fd2 = fd
            console.log('fd2:',fd2)
            let data2 = fs.readFileSync(fd2,'utf8')
            let data3 = fs.readFileSync(fd2,'utf8')
            console.log('data2:',data2)
            console.log('data3:',data3)
        })
    })
})
12345678910111213141516

在这里插入图片描述
所以,在使用readFileSync方法后,我们只需要复用之前得到的数据即可。如果数据有刷新,那就重新open获取。

其实还有一个问题,为什么要使用close?直接一直open不久可以了吗?这个问题留给大家思考。

读取大文件 read 方法

之前的 fs.readFile()fs.readFileSync()方法是将文件内容一次性读取到内存中,但是如果读取的文件相当大,那么就会消耗极大的内存,并且对程序执行的速度产生重大影响。因此,在这种情况下,最好的选择是使用流来读取文件。

read 方法有6个参数:
fd:文件描述符,需要使用open方法先获取。
buffer:要将内容读取到的 Buffer。
offset:整数,往buffer写入的初始位置。
length:整数,读取文件的长度。
position:整数,读取文件的初始位置。
callback:回调函数。三个参数err,bytesRead(实际读取的字节数)、buffer(被写入的缓存区对象)

下面将2.txt分成2个部分进行写入读取:

const fs = require('fs')

let buf = Buffer.alloc(20)

fs.open('C:/Users/Pactera/Desktop/2.txt','r',(err,fd)=>{
    
    
    // 以buffer的下标为0的字符为初始位置,共写入9个字符,读取2.txt文件从下标为0的字符开始读取写入
    fs.read(fd,buf,0,4,0,(err,bytesRead,buffer)=>{
    
    
        console.log('bytesRead:',bytesRead)
        console.log('buffer:',buffer)
        console.log('buffer:',buffer.toString())
        // 以buffer的下标为4的字符为初始位置,共写入9个字符,读取2.txt文件从下标为4的字符开始读取写入
        fs.read(fd,buf,4,9,4,(err,bytesRead,buffer)=>{
    
    
            console.log('bytesRead:',bytesRead)
            console.log('buffer:',buffer)
            console.log('buffer:',buffer.toString())
        })
    })
})
123456789101112131415161718

在这里插入图片描述

写入大文件 write

fs.write()fs.writeFile()fs.writeFileSync() 都有所不同,是将Buffer中的文件写入,也是因为当文件过大时使用,多配合fs.read()使用:

write 方法有6个参数:
fd:文件描述符,需要使用open方法先获取。
buffer:要将内容写入文件数据的 Buffer。
offset:整数,往Buffer写入的初始位置。
length:整数,读取Buffer的字节长度。
position:整数,写入文件的初始位置。
callback:回调函数。三个参数err,bytesWritten(实际写入的字节数)、buffer(被读取的缓存区对象。

写入前:
在这里插入图片描述
写入:

const fs = require('fs')
let buf = Buffer.from('night')

fs.open('C:/Users/Pactera/Desktop/2.txt','r+',(err,fd)=>{
    
    
    // 读取buf向文件写入
    fs.write(fd,buf,0,5,5,(err,byteWritten,buffer)=>{
    
    
        fs.fsync(fd,err=>{
    
    
            fs.close(fd,err=>{
    
    
                console.log('关闭文件!')
            })
        })
    })
})

1234567891011121314

写入后:
在这里插入图片描述
上面一段代码将字符串 night的5个字符全都写入到 2.txt 文件的第5个下标位置上,可以看到原来的数据仍然保留,write方法是对原来的数据进行了覆盖操作。

同步磁盘缓存 fsync

fsync 方法有两个参数,第一个参数为文件描述符 fd,第二个参数为回调函数,回调函数中有一个参数 err(错误),在同步磁盘缓存后执行。

在使用 write 方法向文件写入数据时,由于不是一次性写入,所以最后一次写入在关闭文件之前应先同步磁盘缓存,fsync 方法将在后面配合 write 一起使用。

2.18 文件目录操作

2.18.1 创建文件目录

同步创建目录方法 mkdirSync

mkdirSync 方法参数为一个目录的路径,没有返回值,在创建目录的过程中,必须保证传入的路径前面的文件目录都存在,否则会抛出异常。
在这里插入图片描述

const fs = require('fs')
fs.mkdirSync('D:/Study/dirDemo')
12

在这里插入图片描述

异步创建目录方法 mkdir

const fs = require('fs')
fs.mkdir('D:/Study/dirDemo2',err=>{
    
    
    if(!err){
    
    
        console.log('文件夹创建成功!')
    }
})
123456

在这里插入图片描述
在这里插入图片描述

2.18.2 删除文件目录

无论同步还是异步,删除文件目录时必须保证文件目录的路径存在,且被删除的文件目录为空,即不存在任何文件夹和文件。

同步删除目录方法 rmdirSync

const fs = require('fs')
fs.rmdirSync('D:/Study/dirDemo2')
12

在这里插入图片描述

异步删除目录方法 rmdir

const fs = require('fs')
fs.rmdir('D:/Study/dirDemo',err=>{
    
    
    if(!err){
    
    
        console.log('删除文件成功!')
    }
})
123456

在这里插入图片描述
在这里插入图片描述

2.18.3 查看文件目录操作权限

猜你喜欢

转载自blog.csdn.net/qq_36228377/article/details/123629178