Node js

419 阅读11分钟

Node js 可以做什么

  • 轻量级、高性能的 web 服务
  • 前后端 javascript 同构开发
  • 便捷高效的前端工程化

Nodejs 架构

image.png

Natives modules

  • 当前层内容由 JS 实现
  • 提供应用程序可直接调用库,例如 fs、path、http 等
  • JS 语言无法直接操作底层硬件设备

Builtin modules “胶水层”: 与硬件设备通信

底层

  • V8: 执行 JS 代码,提供桥梁接口(nodejs 是由 node c/c++ 写的,这里的桥梁就是 js 与 c/c++ 之间的转换)
  • Libuv: 事件循环、事件队列、异步IO
  • 第三方模块: zlib、http、c-ares 等

image.png

全局变量 process

  • 无需 require 可直接使用
  • 可以获取进程信息。例如对 CPU 以及内存的消耗,本地环境
  • 可以执行进程操作。监听内置事件,创建始进程
// 1 资源: CPU 内存
console.log(process.memoryUsage())
console.log(process.cpuUsage())

// 2 运行环境:运行目录、node版本、cpu架构、用户环境、系统平台
console.log(process.cwd())      // 当前工作目录
console.log(process.version)    // node 版本
console.log(process.versions)   // 得到的信息多一些
console.log(process.arch)       // cpu 架构  x64
console.log(process.env)        // 用户环境
console.log(process.env.NODE_ENV)
console.log(process.env.PATH)   // 环境变量
console.log(process.env.HOME)   // 管理员目录, window 系统用 USERPROFILE, mac 用 HOME
console.log(process.platform)   // 操作系统

// 3 运行状态:启动参数、PID、运行时间
console.log(process.argv)       // 返回数组,前两项为 node 路径和文件目录,第三项开始是参数
console.log(process.execArgv)
console.log(process.pid)
console.log(process.uptime())

核心模块 - path

const path = require('path')

// console.log(__filename)

// 1 获取路径中的基础名称`
/**
 * 01 返回的就是接收路径当中的最后一部分
 * 02 第二个参数表示扩展名,如果说没有设置则返回完整的文件名称带后缀
 * 03 第二个参数作为后缀时,如果没有在当前路径中被匹配到,那么就会忽略
 * 04 处理目录路径的时候如果说,结尾处有路径分隔符,则也会被忽略掉
 */
console.log(path.basename(__filename))              // a.js
console.log(path.basename(__filename, '.js'))       // a
console.log(path.basename(__filename, '.css'))      // a.js
console.log(path.basename('a/b/c'))                 // c
console.log(path.basename('a/b/c/'))                // c

// 2 获取路径目录名(路径)
/**
 * 01 返回路径中最后一个部分的上一层目录所在路径
 */
console.log(path.dirname(__filename))               // /Users/lwang15/Desktop/project
console.log(path.dirname('/a/b/c'))                 // /a/b
console.log(path.dirname('/a/b/c/'))                // /a/b

// 3 获取路径的扩展名
/**
 * 01 返回 path 路径中相应文件的后缀名
 * 02 如果 path 路径当中存在多个点,它匹配的是最后一个点到结尾的内容
 */
console.log(path.extname(__filename))               // .js
console.log(path.extname('/a/b'))                   // (返回空)
console.log(path.extname('/a/b/index.html.js.css')) // .css
console.log(path.extname('/a/b/index.html.js.'))    // .

// 4 解析路径
/**
 * 01 接收一个路径,返回一个对象,包含不同的信息
 * 02 root  dir  base  ext  name
 */
// const obj = path.parse('/a/b/c/index.html')
// {
//     root: '/',
//     dir: '/a/b/c',
//     base: 'index.html',
//     ext: '.html',
//     name: 'index'
// }
// const obj = path.parse('/a/b/c')
// const obj = path.parse('/a/b/c/')
// { root: '/', dir: '/a/b', base: 'c', ext: '', name: 'c' }
// const obj = path.parse('./a/b/c/')
// { root: '', dir: './a/b', base: 'c', ext: '', name: 'c' }
// console.log(obj)


// 5 序列化路径
const obj = path.parse('./a/b/c/')
console.log(path.format(obj))

// 6 判断当前路径是否为绝对路径
console.log(path.isAbsolute('foo'))     // false
console.log(path.isAbsolute('/foo'))    // true
console.log(path.isAbsolute('///foo'))  // true
console.log(path.isAbsolute(''))        // false
console.log(path.isAbsolute('.'))       // false
console.log(path.isAbsolute('../bar'))  // false

// 7 拼接路径
console.log(path.join('a/b', 'c', 'index.html'))        // a/b/c/index.html
console.log(path.join('/a/b', 'c', 'index.html'))       // /a/b/c/index.html
console.log(path.join('/a/b','c','../','index.html'))   // /a/b/index.html
console.log(path.join('/a/b','c','./','index.html'))    // /a/b/c/index.html
console.log(path.join('/a/b','c','','index.html'))      // /a/b/c/index.html
console.log(path.join(''))                              // .

// 8 规范化路径
console.log(path.normalize(''))             // .
console.log(path.normalize('a/b/c/d'))      // a/b/c/d
console.log(path.normalize('a///b/c../d'))  // a/b/c../d
console.log(path.normalize('a//\\b/c\\/d')) // a/\b/c\/d
console.log(path.normalize('a//\b/c\\/d'))  // a/c\/d

// 9 绝对路径
// console.log(path.resolve())
/**
 * resolve([from], to)
 */
console.log(path.resolve('a', 'b'))     // /Users/lwang15/Desktop/project/a/b
console.log(path.resolve('a', '/b'))    // /b
console.log(path.resolve('/a', '/b'))   // /b
console.log(path.resolve('/a', 'b'))    // /a/b
console.log(path.resolve('index.html')) // /Users/lwang15/Desktop/project/index.html

全局变量 Buffer

  • 无需 require 的一个全局变量
  • 实现 nodejs 平台下的二进制数据操作
  • 不占据 V8 堆内存大小的内存空间
  • 内存的使用由 Node 来控制,由 V8 的 GC 回收
  • 一般配合 Stream 流使用,充当数据缓冲区

创建 Buffer 实例

  • alloc: 创建指定字节大小的 buffer
  • allocUnsafe: 创建指定大小的 buffer(不安全)
  • from: 接收数据,创建 buffer
const b1 = Buffer.alloc(10)
const b2 = Buffer.allocUnsafe(10)

const b3 = Buffer.from('1')
const b4 = Buffer.from([1,2,3])

Buffer 实例方法

  • fill: 使用数据填充 buffer
  • write: 向 buffer 中写入数据
  • toString: 从 buffer 中提取数据
  • slice: 截取 buffer
  • indexOf: 在 buffer 中查找数据
  • copy: 拷贝 buffer 中的数据
let buf = Buffer.alloc(6)

// fill
/**
 * 指使用指定字符填充这6个字节,长度不够就重复填充,长度太长就截取
 * 可以接受第二个和第三个参数,分别代表开始位置和结束位置
 *  */
// buf.fill('123')
// console.log(buf)
// console.log(buf.toString())  // 123123
// buf.fill('123456789')
// console.log(buf)
// console.log(buf.toString())  // 123456
// buf.fill('123', 1)
// console.log(buf)
// console.log(buf.toString())  // 12312
buf.fill('123', 1, 3)
console.log(buf)
console.log(buf.toString())  // 12
let buf = Buffer.alloc(6)

// write
/**
 * write 直接将字符写入
 * 可以传第二个参数和第三个参数,分别代表开始位置和填充长度
 */
// buf.write('123')
// console.log(buf)
// console.log(buf.toString())    // 123
// buf.write('12345678')
// console.log(buf)
// console.log(buf.toString())    // 123456
buf.write('123456', 1, 4)
console.log(buf)
console.log(buf.toString())    // 1234
// toString
/**
 * toString 的第二个参数表示从哪个字节开始(一个汉字占3个字节)
 * 第三个参数为结束位置
 */
let buf = Buffer.from('学习笔记')
console.log(buf)
console.log(buf.toString()) // 学习笔记
console.log(buf.toString('utf-8')) // 学习笔记
console.log(buf.toString('utf-8', 3)) // 习笔记
console.log(buf.toString('utf-8', 3, 9)) // 习笔
// slice
/**
 * slice 为截取,参数分别为开始位置和结束位置
 * 如果参数为负数,则从后往前数位置
 */
let buf = Buffer.from('学习笔记')
let b1 = buf.slice()
console.log(b1)
console.log(b1.toString())  // 学习笔记
let b2 = buf.slice(3)
console.log(b2)
console.log(b2.toString())  // 习笔记
let b3 = buf.slice(3, 9)
console.log(b3)
console.log(b3.toString())  // 习笔
let b4 = buf.slice(-3)
console.log(b4)
console.log(b4.toString())  // 记
// indexOf
/**
 * indexOf 用来找字节所在位置,第二个参数表示从第几个开始找
 */
let buf = Buffer.from('我爱学习,爱劳动')
console.log(buf)
console.log(buf.indexOf('爱')) // 3
console.log(buf.indexOf('爱', 4)) // 15
console.log(buf.indexOf('爱q', 4)) // -1
// copy
let b1 = Buffer.alloc(6)
let b2 = Buffer.from('学习')
// 将 b2 拷贝到 b1 里边
// b2.copy(b1)
// console.log(b1.toString()) // 学习
// console.log(b2.toString()) // 学习
// b2.copy(b1, 3)
// console.log(b1.toString()) //  学
// console.log(b2.toString()) // 学习
/**
 * 第二个参数代表从 b1 的第几个位置开始写入
 * 第三个参数和第四个参数代表从 b2 的第几个位置开始和结束
 */
// b2.copy(b1, 3, 3)
// console.log(b1.toString()) //  习
// console.log(b2.toString()) // 学习
b2.copy(b1, 3, 3, 6)
console.log(b1.toString()) //  习
console.log(b2.toString()) // 学习

Buffer 静态方法

  • concat: 将多个 buffer 拼接成一个新的 buffer
  • isBuffer: 判断当前数据是否为 buffer
let b1 = Buffer.from('好好')
let b2 = Buffer.from('学习')

// let b = Buffer.concat([b1, b2])
// console.log(b)
// console.log(b.toString())  // 好好学习
let b = Buffer.concat([b1, b2], 9)
console.log(b)
console.log(b.toString())  // 好好学
// isBuffer
let b1 = Buffer.alloc(3)
console.log(Buffer.isBuffer(b1))  // true

FS

基本概念

FS 是内置核心模块,提供文件系统操作的 API。

image.png

FS 模块结构:

  • FS 基本操作类。可以实现文件信息的获取,例如判断当前是目录还是文件,文件的可读流以及可写流的操作,文件的监听行为等
  • FS 常用 API。文件的打开关闭,增删改查等操作。

系统及文件前置知识:文件的权限位、标识符、文件描述符fd。

权限位:不同的用户角色对于当前文件可以执行的不同权限操作。 文件的权限操作分为三种:r(读权限),w(写权限),x(执行权限),如果采用八进制的数字表示,r 就是 4, w 就是 2, x 就是 1,如果不具备该权限,就是 0. 操作系统将用户也分为三类:文件所有者(当前用户),文件所属组(当前用户家人),其他用户(访客用户)。

image.png

image.png

第一位 d 代表文件夹,- 代表文件, 后边 rwx,如果没有某一个权限就用 - 。

标识符(flag):表示对文件的操作方法。可读,可写,或即可读有可写等。

  • r:可读
  • w:可写
  • s:同步
  • +:执行相反操作
  • x:排它操作
  • a:追加操作 可以进行组合。例如 r+,w+ 都表示可读可写,不过有区别, r+ 在读的时候如果没有这个文件会抛异常,而 w+ 如果没有这个文件会直接生成一个文件写入;如果目标文件存在,使用 r+ 不会清空当前文件,而 w+ 会清空再写入。

文件描述符fd:当一个文件被打开的时候,操作系统就会给他分配一个数字标识,也就是文件标识符。操作系统每打开一个文件,文件标识符就会递增1, 文件标识符是从3开始的,因为 0 1 2 已经被标准输入,标准输出,标准错误占用了。利用 fs.open 打开一个文件就可以得到 fd。

总结:

  • fs 是 Nodejs 中内置核心模块
  • 代码层面上 fs 分为基本操作类和常用 API
  • 文件和系统的权限位,标识符,操作符

文件操作 API

适合小文件的操作

  • readFile:从指定文件中读取数据
  • writeFile:向指定文件中写入数据
  • appendFile:追加的方式向指定文件中写入数据
  • copyFile:将某个文件中的数据拷贝至另一文件
  • watchFile:对指定文件进行监控
const fs = require('fs')
const path = require('path')

// readFile
fs.readFile(path.resolve('data.txt'), 'utf-8', (err, data) => {
    console.log(err)
    console.log(data)
})

// writeFile
// 第三个参数可不传
fs.writeFile(path.resolve('data.txt'), 'hello node.js', {
    mode: 438,
    flag: 'w+',
    encoding: 'utf-8'
}, (err) => {
    if (!err) {
        fs.readFile(path.resolve('data.txt'), 'utf-8', (err, data) => {
            console.log(data)
        })
    }
})

// appendFile
// 第三个参数同上
fs.appendFile(path.resolve('data.txt'), '\n好好学习', (err) => {
    console.log('写入成功')
})

// copyFile
fs.copyFile(path.resolve('data.txt'), 'test.txt', () => {
    console.log('拷贝成功')
})

// watchFile  unwatchFile
fs.watchFile(path.resolve('data.txt'), { interval: 20 }, (current, prev) => {
    // mtime 为修改时间
    if (current.mtime !== prev.mtime) {
        console.log('文件被修改了')
        fs.unwatchFile(path.resolve('data.txt'))
    }
})

文件打开与关闭

  • open
  • close
const fs = require('fs')
const path = require('path')

// open close
fs.open(path.resolve('data.txt'), 'r', (err, fd) => {
    console.log(fd)
    fs.close(fd, err => {
        console.log('关闭成功')
    })
})

适合大体积文件的操作

  • read
  • write
const fs = require('fs')

/**
 * read: 所谓的读操作就是将数据从磁盘文件中读取,写入到 buffer 中
 * fs.read 方法的参数:
 * fd 定位当前被打开的文件
 * buf 用于表示当前缓冲区
 * offset 表示当前从 buf 的哪个位置开始执行写入(写入到缓冲区)
 * length 表示当前次写入的长度
 * position 表示当前从文件的哪个位置开始读取操作
 */
let buf = Buffer.alloc(10)
fs.open('data.txt', 'r', (err, rfd) => {
    console.log(rfd)
    fs.read(rfd, buf, 0, 4, 0, (err, readBytes, data) => {
        console.log(readBytes)
        console.log(data.toString())
    })
})

// write 将缓冲区里的内容写入到磁盘文件中
buf = Buffer.from('1234567890')
fs.open('b.txt', 'w', (err, wfd) => {
    fs.write(wfd, buf, 2, 4, 0, (err, written, buffer) => {
        console.log(written, '-----')
        fs.close(wfd)
    })
})

利用 read 和 write 实现大文件拷贝

const BUFFER_SIZE = buf.length
let readOffset = 0
fs.open(path.resolve('a.txt'), 'r', (err, rfd) => {
    fs.open(path.resolve('b.txt'), 'w', (err, wfd) => {
        function next() {
            fs.read(rfd, buf, 0, BUFFER_SIZE, readOffset, (err, readBytes, buffer) => {
                if(!readBytes){
                    // 如果条件成立,说明内容已经读取完毕
                    fs.close(rfd, () => {})
                    fs.close(wfd, () => {})
                    console.log('拷贝完成')
                    return
                }
                readOffset += readBytes
                fs.write(wfd, buf, 0, readBytes, (err, written, buffer) => {
                    next()
                })
            })
        }
        next()
    })
})

目录操作 API

  • acces:判断文件或目录是否具有操作权限
  • stat:获取目录及文件信息
  • mkdir:创建目录
  • rmdir:删除目录
  • readdir:读取目录中内容
  • unlink:删除指定文件
const fs = require('fs')

//  access
fs.access('a.txt', (err) => {
    if (err) {
        console.log(err)
    } else {
        console.log('有操作权限')
    }
})

// stat
fs.stat('a.txt', (err, statObj) => {
    console.log(statObj)
    console.log(statObj.isFile())
    console.log(statObj.isDirectory())
})

// mkdir
// 要保证父级目录 a 存在。
// fs.mkdir('a/b', (err) => {
//     if (!err) {
//         console.log('创建成功')
//     } else {
//         console.log(err)
//     }
// })
// 若 设置 {recursive: true} 则可以递归创建目录,无需保证 a 存在。
fs.mkdir('a/b/c', {recursive: true}, (err) => {
    if (!err) {
        console.log('创建成功')
    } else {
        console.log(err)
    }
})

// rmdir
// 若 a 目录下不为空,需加上 {recursive: true} 才能删除,否则会报错文件不为空
fs.rmdir('a', {recursive: true}, (err) => {
    if (!err) {
        console.log('删除成功')
    } else {
        console.log(err)
    }
})

// readdir
fs.readdir('a', (err, files) => {
    console.log(files)
})

CommonJS 规范

image.png

image.png

image.png

Eventloop

首先回顾一下浏览器中的事件循环过程:

  • 从上至下执行所有的同步代码
  • 执行过程中将遇到的宏任务与微任务添加至相应的队列
  • 同步代码执行完毕后,执行满足条件的微任务回调
  • 微任务队列执行完毕后执行所有满足需求的宏任务回调
  • 循环事件环操作
  • 注意: 每执行一个宏任务之后就会立刻检查微任务队列

Nodejs 事件循环机制

image.png

队列说明:

  • timers:执行 setTimeout 与 setInterval 回调
  • pending callbacks: 执行系统操作的回调,例如 tcp udp
  • idle, prepare:只在系统内部使用
  • poll:执行与 I/O 相关的回调
  • check:执行 setImmediate 中的回调
  • close callbacks: 执行 close 事件的回调

Nodejs 完整事件环:

  • 执行同步代码,将不同的任务添加至相应的队列
  • 所有同步代码执行后会去执行满足条件的微任务(注意: nextTick 的微任务优先级高于 promise 的微任务,所以会先执行)
  • 所有微任务代码执行后会执行宏任务,宏任务的执行顺序为 timer 队列 -> poll 队列 -> check 队列
  • 每个宏任务执行完都会检查微任务队列,清空微任务后继续执行宏任务
  • 注:在新版本中每一个宏任务执行完都会去清空微任务队列(和浏览器一样),在旧版本中每个队列的宏任务执行完切换队列之前才会清空微任务队列

Node 与浏览器事件环不同:

  • 任务队列数不同。浏览器中只有微任务和宏任务两个任务队列;Nodejs 中除了微任务有6个事件队列
  • Nodejs 微任务执行时机不同(旧版本)
  • 微任务优先级不同。浏览器中微任务存放于事件队列,先进先出;Nodejs 中process.nextTick 优先级高于 promise.then