文件操作open

const fs = require('fs')
const path = require('path')

//open
fs.open(path.resolve('data.txt'),'r',(err,fd)=>{
    console.log(fd);//3 文件操作符
    fs.close(fd,err=>{
        console.log('关闭成功')
    })
})

image

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


//write 将缓冲区里的内容写入磁盘文件中
buf = Buffer.from('123456789')
fs.open('b.txt','w',(err,wfd)=>{
    fs.write(wfd,buf,1,3,0,(err,writen,buffer)=>{
//1 从buffer中的哪个位置开始读取
//3 表示当前写入字节长度
//0 从文件的哪个字节开始写操作

        console.log(written);//3 表示当前写入字节长度
        console.log(buffer);
        console.log(buffer.toString());
        fs.close(wfd)
    })
})


文件拷贝自定义实现


let buf = Buffer.alloc(10)
//01 打开指定的文件
fs.open('a.txt','r',(err,rfd)=>{
    //02 打开的文件读取数据
    fs.read(rfd,buf,0,10,0,(err,readBytes)=>{
        //03 打开b文件,用于执行数据写入操作
        fs.open('b.txt','w',(err,wfd)=>{
            //04 将buffer中的数据写入到b.txt当中
            fs.write(wfd,buf,0,10,0,(err,written)=>{
                console.log('写如成功')
            })
        })
    })
})

//01 打开指定的文件
fs.open('a.txt','r',(err,rfd)=>{
    //03 打开b文件, 用于执行数据写入操作
    fs.open('b.txt','w',(err,wfd)=>{
        //02 从打开的文件中读取数据
        fs.read(rfd,buf,0,10,0,(err,readBytes)=>{
            //04 将buffer中的数据写入到b.txt当中
            fs.write(wfd,buf,0.10.0,(err,written)=>{
                console.log('写入成功')
            })
        })
    })
})


数据完全拷贝

fs.open('a.txt','r',(err,rfd)=>{
    fs.open('b.txt','a+',(err,wfd)=>{
        fs.read(rfd,buf,0,10,0,(err,readBytes)=>{
            fs.write(wfd,buf,0,10,0,(err,written)=>{
                fs.read(rfd,buf,0,5,10,(err,readBytes)=>{
                    fs.write(wfd,buf,0,5,10,(err,written)=>{
                        console.log('写入成功')
                    })
                })
            })
        })
    })
})


文件拷贝


const fs = require('fs')
let buf = Buffer.alloc(20)
const BUFFER_SIZE = 10
let readOffset = 0

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

常见目录操作api

  • access 判断文件或目录是否具有操作权限
  • stat 获取目录及文件信息
  • mkdir 创建目录
  • rmdir 删除目录
  • readdir 读取目录中内容
  • unlink 删除指定文件
const fs = require('fs')
fs.access('a.txt',(err)=>{
    if(err){//不存在或没有权限
        console.log(err)
    }else{
        console.log('有权限操作')
    }
})


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

})

fs.mkdir('a/b/c',(err)=>{
    if(!err){
        console.log('创建成功')
    }else{
        console.log(err)
    }
})
//递归创建
fs.mkdir('a/b/c',{recursive:true},(err)=>{
    if(!err){
        console.log('创建成功')
    }else{
        console.log(err)
    }
})



fs.rmdir('a/b/c',(err)=>{
    if(!err){
        console.log('删除成功')
    }
})
//递归操作
fs.rmdir('a',{recursive:true},(err)=>{
    if(!err){
        console.log('删除成功')
    }
})


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

//unlink
fs.unlink('a/a.txt',(err)=>{
    if(!err){
        console.log('删除成功')
    }
})

同步创建目录

01 将来调用需要接收蕾仕于 a/b/c ,这种路径 他们质检采用/取进行连接
02 利用/ 分隔符将路径进行拆分,将每一项放入一个数组中进行管理['a','b','c']
03 对尚需的数组进行遍历,我们需要拿到每一项,然后与前一项拼接 /
04 判断一个当前对拼接之后的路径是否具有可操作的权限,如果有则证明存在,否则的话就需要执行创建

const path = require('path')
const fs = require('fs')
function makeDirSync(dirPath){
    let items = dirPath.split(path.sep);
    console.log(path.sep)// \
    console.log(items) // [ 'a', 'b', 'c' ]
    for(let i = 1;i<=items.length;i++){
        let dir = items.slice(0,i).join(path.sep)
        try{
            fs.accessSync(dir)
        }catch(err){
            fs.mkdirSync(dir)
        }
    }
}
makeDirSync('a\\b\\c')

异步创建目录

const fs = require('fs')
const path = require('path')
function mkDir(dirPath,cb){
    let parts = dirPath.split('/')
    let index = 1
    function next(){
        if(index>parts.length)return cb && cb()
        let current = parts.slice(0,index++).join('/')
        fs.access(current,(err)=>{//是否有操作权限
            if(err){
                fs.mkdir(current,next)
            }else{
                next()
            }
        })
    }
    next();
}

mkDir('a/b/c',()=>{
    console.log('创建成功')
})

util工具包

const fs = require('fs')
const path = require('path')
const {promisify} = require('util')

//promisify 包装处理成async 风格
const access = promisify(fs.access)
const access = promisify(fs.mkdir)

async function myMkdir(dirPath,cb){
    let parts = dirPath.split('/')
    for(let index = 1;index<=parts.length; index++){
        let current =parts.slice(0,index).join('/')
        try{
            await access(current)
        }catch(err){
            await mkdir(current)
        }
    }
    cb && cb()
} 
mkMkdir('a/b/c',()=>{
    console.log('创建成功')
})
function  myRmdir(dirPath,cb){
    fs.stat(dirPath,(err,staObj)=>{
        if(statObj.isDirectory()){
            fs.readdir(dirPath,(err,files)=>{
                let dirs = files.map(item=>{
                    return path.join(dirPath,item)
                })
                let index = 0
                function next(){
                    if(index==dirs.length){
                        return fs.rmdir(dirPath,cb)
                    }
                    let current  = dirs[index++]
                    myRmdir(current,next)
                }
                next()
            }) 
        }else{
            fs.unlink(dirPath,cb)
        }
    })

}

系统开发常见问题

  • 命名冲突和污染
  • 代码冗余,无效请求多
  • 文件间的以来关系复杂

常见模块开发规范

  • Commonjs规范(nodejs 模块加载都是用的同步)
    • 每个模块文件上存在 moduleexportsrequire三个变量
      • module 记录当前模块信息。
      • require 引入模块的方法。
      • exports 当前模块导出的属性
      • 例如:module.exports =function set(){}
  • AMD规范(异步加载模块操作)
define('myModule', ['jquery'], function($) {
    // $ 是 jquery 模块的输出
    $('body').text('hello world');
});
// 使用
require(['myModule'], function(myModule) {});
  • CMD规范(commonjs+amd) 代表cjs
/*
 * cmd 模块
 */
define(function(require, exports, module) {
    exports.alert = function() {
        window.alert("test alert");
    };
});
  • ES modules 规范(2015)
<script type='module'></script>
//导出
export var name = "foo-module"
export function hello () {
	console.log("hello")
}
export class Person {}

//导入
import {引入的成员} from 引入模块地址

模块化规范

  • 模块化是前端走向工程化中的重要一环
  • 早期javascript 语言层面没有模块化规范
  • Commonjs AMD CMD都是模块化规范
  • ES6中将模块化纳入标准规范
  • 当前常用规范是Commonjs(node下) 与 ESM(前端)

Nodejs与CommonJS

  • 任意一个文件就是一个模块,具有独立作用域
  • 使用require导入其他模块
  • 将模块id导入require实现目标模块定位

module 属性

  • 任意js文件就是一个模块,可以直接使用module属性
  • id 返回模块标识符,一般是一个绝对路径
  • filename 返回文件模块的绝对路径
  • loaded 返回布尔值,表示模块是否完成加载
  • parent 返回对象存放调用当前模块的模块
  • children 返回数组,存放当前模块调用的其他模块
  • exports 返回当前模块需要暴露的内容
  • paths 返回数组, 存放不同目录下的node_modules位置

image

image

require 属性

  • 基本功能是读入并执行一个模块文件
  • resolve 返回模块文件绝对路径
  • extensions 依据不同后缀名执行解析操作
  • main 返回主模块对象

CommonJS规范

  • CommonJS 规范起初是为了弥补js语言模块化缺陷
  • CommonJS是语言层面的规范,当前主要用于nodejs
  • CommonJS规定模块化分为引入,定义,标识符三个部分
  • Moudle在任意模块中可直接使用包含模块信息
  • Require接收标识符,加载目标模块
  • Exports与module.exports 都能导出模块数据
  • CommonJS规范定义模块的加载是同步完成

Nodejs 与CommonJS

  • 使用module.exports与require实现模块导入与导出
  • module 属性及其常见信息获取
  • exports 导出数据及其与module.exports 区别
  • CommonJS规范下的模块同步加载

模块导入与导出

./m.js
const age = 18
const addFn = (x,y)=>{
    return x+y
}
//导出
module.exports={
    age:age,
    addFn:addFn,
}

./main.js
//导入
let obj = require('./m')
console.log(obj);//{age:18,addFn:[Function: addFn]}



//module
//nodejs中每一个js文件都有一个module
./m.js
console.log(module)

//module
let obj = require('./m')

exports.name = 'zce'
let obj = require('./m')
console.log(obj)//{name:"zce"}


exports = {//无法导出
    name:'syy',
    age:18,
}



//同步加载


//./main1.js
console.log(require.main===module)//true 主入口文件
./m.js
console.log(require.main===module)//false

模块加载速度

  • 核心模块 Node源码编译时写入到二进制文件中
  • 文件模块 代码运行时, 动态加载

加载流程

  • 路径分析 依据标识符确定模块位置
  • 文件定位 确定目标模块中具体的文件及文件类型
  • 编译执行 采用对应的方式完成文件的编译执行
console.log(module.paths)

文件定位

  • 项目存在m1.js模块,导入时使用require('m1')语法
  • m1.js->m1.json->m1.node 没有扩展名就按照这个扩展名查找文件
  • 查找package.json文件,使用JSON.parse()解析后查找main.js
  • main.js->main.json->main.node 查找main文件
  • 将index做为目标模块中的具体文件名称

js文件的编译执行

  • 使用fs模块同步读入目标文件内容
  • 对内容进行语法包装,生成可执行js函数
  • 调用函数时传入exports module require等属性值

缓存优化原则

  • 提高模块加载速度
  • 当前模块不存在,则经历一次完整加载流程
  • 模块加载完成后,使用路径做为索引进行缓存

加载流程小结

  • 路径分析 确定目标模块位置
  • 文件定位 确定目标模块中的具体文件
  • 编译执行 对模块内容进行编译 返回可用exports对象

模块分类

  • 内置模块
  • 文件模块

模块加载速度

  • 核心模块 node 源码编译时写入到二进制文件中
  • 文件模块 代码运行时 动态加载

image

vm一个独立的沙箱环境

const fs = require('fs')
const vm = require('vm')
let content = fs.readFileSync('test.txt','utf-8')

console.log(content)
eval(content)
console.log(age)//18
const fs = require('fs')
const vm = require('vm')
let content = fs.readFileSync('test.txt','utf-8')

let age = 23 
console.log(age)
let fn = new Function('age','return age+1')
console.log(fn(age))

function Module(id){
    this.id = id
    this.exports = {}
}

Module._resolveFilename = function(filename){
    //利用Path 将filename转为绝对路径
    let absPath = path.resolve(__dirname,filename)
    console.log(absPath);
}
function myRequire(filename){
    //绝对路径
    let mPath = Module._resolveFilename(filename)
}

通过 EventEmitter 类实现事件统一管理

events与EventEmitter
  • node.js 是基于事件驱动的异步操作架构,内置events模块
  • events模块提供了EventEmitter类
  • node.js中很多内置核心模块继承EventEmitter

EventEmitter常见API

  • on 添加当事件被触发时调用的回调函数
  • emit 触发事件 按照注册的序同步调用每个事件监听器
  • once 添加当事件在注册之后首次被触发时调用的回调函数
  • off 移除特定的监听器
const EventEmitter = require('events')
const ev = new EventEmitter()
//on
ev.on('事件1',()=>{
    console.log('事件1执行了')
})
//emit
ev.emit('事件1')
const EventEmitter = require('events')
const ev = new EventEmitter()
//on
ev.on('事件1',()=>{
    console.log('事件1执行了--2')
})
ev.on('事件1',()=>{
    console.log('事件1执行了')
})
//emit
ev.emit('事件1')
//事件1执行了--2
//事件1执行了
//once 
ev.once('事件1',()=>{
    console.log('事件1执行了')
})
ev.once('事件1',()=>{
    console.log('事件1执行了--2')
})
ev.emit('事件1')
ev.emit('事件1')
//事件1执行了
//事件1执行了--2
let cbFn = ()=>{
    console.log('事件1执行')
}
ev.on('事件1',cbFn)
ev.emit('事件1')
ev.off('事件1',cbFn)
ev.emit('事件1')
//事件1执行了
let cbFn = (a,b)=>{
    console.log(a)
    console.log(b)
}
ev.on('事件1',cbFn)
ev.emit('事件1',1,2)
//1
//2
let cbFn = (...args)=>{
    console.log(args)
}
ev.on('事件1',cbFn)
ev.emit('事件1',1,2,3)
//[1,2,3]
ev.on('事件1',function(){
    console.log(this)
})
ev.emit('事件1')

image

发布订阅要素

  • 缓存队列 存放订阅者信息
  • 具有增加 删除订阅的能力
  • 状态改变时通知所有订阅者执行监听
class PubSub{
    constructor(){
        this._events={}
    }
    //注册
    subscribe(event,callback){
        if(this._events[event]){
            //如果当前 event 存在 所以我们值需要往后添加当前次监听操作
            this._events[event].push(callback)
        }else{
            //之前没有订阅过次事件
            this._events[event] = [callback]
        }
    }
    //发布
    publish(event,...args){
        const items = this._events[event]
        if(items && items.length){
            items.forEach(function(){
                callback.call(this,...args)
            })
        }
    }
}


let ps = new PubSub()
ps.Subscribe('事件1',()=>{
    console.log('事件1执行了');
})
ps.publish('事件1')

image

完整事件环执行顺序(浏览器)

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

image

image

队列说明

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

Nodejs 完整事件环

  • 执行同步代码 将不同的任务添加至相应队列
  • 所有同步代码执行后会去执行满足条件微任务
  • 所有微任务代码执行后会执行timer队列中满足的宏任务
  • timer中的所有宏任务执行完成后就会依次切换队列
  • 注意:在完成队列切换之前会先清空微任务代码
setTimeout(()=>{
    console.log('s1') 
})
Promise.resolve().then(()=>{
    console.log('p1')
})
console.log('start')
process.nexTick(()=>{
    console.log('tick')
})
setImmediate(()=>{
    console.log('setimmediate')
})
console.log('end');

image

image

image

Node与浏览器事件环不同

  • 任务队列数不同
  • Nodejs微任务执行时机不同
  • 微任务优先级不同

任务队列数

  • 浏览器中只有两个任务队列
  • Nodejs中有六个事件队列

微任务执行时机

  • 二者都会在同步代码执行完毕后执行微任务
  • 浏览器平台下每当一个宏任务执行完毕后就清空微任务
  • Nodejs平台在事件队列切换时回去清空微队列

微任务优先级

  • 浏览器事件环中,微任务放于事件队列,先进先出
  • Nodejs中process.nextTick先于promise.then
ls|grep *.js

Nodejs 中的流就是处理流式数据的抽象接口

image

常见问题

  • 同步读取资源文件,用户需要等待数据读取完成
  • 资源文件最终一次性加载至内存,开销较大

image

image

v8一般提供的内存大小是1个G

流处理数据的优势

  • 时间效率:流的分段处理可以同时操作多个数据chunk
  • 空间效率 同一时间流无需占用大内存空间
  • 使用方便 流配合管理,扩展程序变得简单

流的分类

  • Readable 可读流 能实现数据的读取
  • Writeable 可写流 能够实现数据的写操作
  • Duplex 双工流 能写又能读
  • Tranform 转换流 可读可写 还能实现数据转换

Node.js流特点

  • Stream 模块实现了四个具体的抽象
  • 所有流都继承EventEmitter
const fs = require('fs')
let rs = fs.createReadStream('./test.txt')
let ws = fs.createWriteStream('./test1.txt')
rs.pipe(ws)


 const fs = require('fs')
 const rs = fs.createReadStream('note.txt')
 rs.pipe(process.stdout)

自定义可读流

  • 继承stream 里的Readable
  • 重写_read方法调用push产出数据

 const {Readable} = require('stream')
 //定义数组存放数据,模拟底层数据
 let source = ['lg','zce','syy']
 //自定义类继承 Readable
 class MyReadable extends Readable{//创建可读流
    constructor(source){
        super()
        this.source = source
    }
    _read(){//重写父类的_read  主要对数据添加
        let data = this.source.shift()||null
        this.push(data);
    }
 }
 //消费数据
 let myReadable = new MyReadable(source)

 myReadable.on('readable',()=>{
    let data = null
    while((data = myReadable.read())!=null){//myReadable.read(2)每次读取两个字符
        console.log(data.toString())
    }
 })

//流动模式
 myReadable.on('data',(data)=>{
    console.log(data.toString())
 })
 

自定义可读流

  • 底层数据读取完成之后如何处理
  • 消费者如何获取刻度流中的数据

image

消费数据

  • readable事件 当流中存在可读数据时触发
  • data事件 当流中数据块传给消费者后触发

可读流总结

  • 明确数据生产与消费流程
  • 利用api实现自定义的可读流
  • 明确数据消费的事件使用

可写流


const fs = require('fs')
//创建一个可读流 生产数据
let rs = fs.createReadStream('note.txt');

//修改字符编码, 便于后续使用
rs.setEncoding('utf-8')
//创建一个可写流 消费数据
let ws = fs.createWriteStream('node2.txt')

//监听事件调用方法完成具体消费
rs.on('data', (chunk) => {
    //执行数据写入
    ws.write(chunk);
})

自定义可写流

  • 继承stream 模块的Writeable
  • 重写_write方法, 调用write执行写入

const { Writable } = require('stream')
class MyWriteable extends Writable {
    constructor() {
        super()
    }
    _write(chunk, en, done) {
        process.stdout.write(chunk.toString() + "-------")
        //写完之后在调用done
        process.nextTick(done)//同步代码执行后回调(异步操作)
    }
}
//创建可写流用于消费
let myWriteable = new MyWriteable()
//执行数据写入
myWriteable.write('XXX', 'utf-8', () => {
    console.log('写入成功');
})
//XXX-------写入成功

可写流事件

  • pipe 事件 可读流调用pipe() 方法时触发
  • unpipe事件 可读流调用unpipe()方法时触发

双工,转换流

Duplex 是双工流 既可以胜场数据也可以消费数据

自定义双工流

  • 继承Duplex类
  • 重写_read方法,调用push 生产数据
  • 重写_write方法,调用write消费数据

let { Readable, Writable, Duplex } = require('stream')
const { isBuffer } = require('util')


//模拟数据
let source = ['a', 'b', 'c']

//自定义双工流
class MyDuplex extends Duplex {
    constructor(options) {
        super(source, options)
        this.source = source
    }
    _read() {

        let data = this.source.shift() || null
        this.push(data);
    }
    _write(chunk, enc, next) {
        if (Buffer, isBuffer) {
            chunk = chunk.toString()
        }
        process.stdout.write(chunk + '-----')
        process.nextTick(next);
    }
}

//实例化
let myDuplex = new MyDuplex(source)
myDuplex.on('data', (chunk) => {
    console.log(chunk.toString())
})

myDuplex.write('测试', 'utf-8', () => {
    console.log('双工流可写操作')
})
测试-----a
b
c
双工流可写操作

Transform转化流(也是双工流)

自定义转化流
  • 继承Transform 类
  • 重写_transform方法,调用push和callback
  • 重写_flush方法,粗粒剩余数据

let { Transform } = require('stream')

class MyTransform extends Transform {
    constructor(options) {
        super()
    }
    _transform(chunk, encoding, callback) {
        this.push(chunk.toString().toUpperCase())
        callback(null)
    }
}
let a = new MyTransform()
a.write('a')
a.write('b')
a.end('c')

a.pipe(process.stdout)
ABC

nodej中的四种流

Readable 可读流

writeable 可写流

Duplex双工流(读写独立)

Transform转换流(读写流是通的可通过管道中间状态做处理)

Last Updated:
Contributors: 刘荣杰