Marco Nie - node.js https://blog.niekun.net/category/node/ zh-CN Sun, 21 Feb 2021 22:02:00 +0800 Sun, 21 Feb 2021 22:02:00 +0800 node.js 入门教程之十四 -- TypeScript https://blog.niekun.net/archives/2169.html https://blog.niekun.net/archives/2169.html Sun, 21 Feb 2021 22:02:00 +0800 admin TypeScript 是微软开发的开源编程语言,在全世界的开发者中流行。

本质上来说 typescriptJavaScript 的超集,为其添加了很多功能。标志性的就是添加了 static type 静态类型定义,它可以声明期待的数据类型,例如:声明 function 中的期待的传入数据类型,期望的返回值的类型,object 的结构及 property 的数据类型等。

typescript 是一个强大的工具,给 JavaScript 项目开启了新的世界。它使我们的代码更加的安全有力,在程序发布前就可以防止很多的 bug 出现。在编写代码期间就能够及时反映出问题点,并且它已经被整合在现代的编译器中,如 VS code。

语法说明

下面是一个 typescript 代码示例:

type User = {
    name: string;
    age: number;
};

function isAdult(user: User): boolean {
    return user.age >= 18;
}

const justine: User = {
    name: 'Justine',
    age: 23,
};

const isJustineAnAdult: boolean = isAdult(justine);
  • 首先我们通过 type 关键词定义一个自定义的 object 类型 User,其中定义了 User 的两个 properties 及其数据类型。
  • 然后在定义的 function isAudlt 中接收一个参数,此参数类型为 User 类型,然后返回值类型为 boolean 类型。
  • 然后我们定义了 justine object 且其类型为 User。在其中定义的两个 properties 也符合 User 中的类型。
  • 最后定义了变量 isJustineAnAdult 且其数据类型为 boolean,将 isAudlt 的返回值赋给它。

如果我们在编程中没有使用指定的数据类型,typescript 将会给出警告提醒我们错误的地方,比如上面的示例中修改 justine 的 age 为一个字符串:
2021-02-21T13:39:09.png

在 vs code 中会提示此处有错误。

另一方面也并不需要将所有地方的数据类型都做出声明,typescript 会自动推断出需要的类型。例如:即使我们不给 isJustineAnAdult 声明 boolean 类型,typescript 也会自动推断它是 boolean 类型的。

那么如何运行 typescript 代码呢?首先需要通过 npm 安装 typescript,它会安装我们需要的一些可执行程序:

npm install typescript

下面我们可以通过 tsc 命令将 typescript 代码转换为标准 JavaScript 代码。

我们将上面示例中的代码保存为 demo.ts 文件,然后通过 npx 调用 tsc 命令(如果以 global 方式安装 typescript 则可以直接运行命令):

npx tsc demo.ts

以上命令会转换 JavaScript 代码并自动创建一个 demo.js 文件,此文件可以在 node.js 运行:

function isAdult(user) {
    return user.age >= 18;
}
var justine = {
    name: 'Justine',
    age: 23
};
var isJustineAnAdult = isAdult(justine);

如果 typescript 代码中包含错误,则使用上面的转换命令会出现报错信息:

% npx tsc demo.ts
demo.ts:12:5 - error TS2322: Type 'string' is not assignable to type 'number'.

12     age: '23',
       ~~

  demo.ts:3:5
    3     age: number;
          ~~
    The expected type comes from property 'age' which is declared here on type 'User'


Found 1 error.

可以看到 typescript 成功的防止将包含潜在错误代码的程序发布出去。

更多介绍

除此之外 typescript 也包含了很多其他很好的工具,如:interfaces, classes, utility types 等。更多使用方法参考 typescript 官方文档:https://www.typescriptlang.org/docs

typescript 在 node.js 生态中已经有了很好的发展,且被用于很多开源项目和框架中。以下是一些使用 typescript 的开源项目:

  • NestJS - robust and fully-featured framework that makes creating scalable and well-architected systems easy and pleasant
  • TypeORM - great ORM influenced by other well-known tools from other languages like Hibernate, Doctrine or Entity Framework
  • Prisma - next-generation ORM featuring a declarative data model, generated migrations and fully type-safe database queries
  • RxJS - widely used library for reactive programming

总结

通过这 14 篇教程,我们介绍了 node.js 的基本语法和相关使用场景。后期可以在具体项目中进行更加详细的体验。

]]>
0 https://blog.niekun.net/archives/2169.html#comments https://blog.niekun.net/feed/category/node/archives/2169.html
node.js 入门教程之十三 -- environment,error handler 和 log object https://blog.niekun.net/archives/2165.html https://blog.niekun.net/archives/2165.html Sat, 20 Feb 2021 17:19:00 +0800 admin environment 运行环境

production environment 产品环境development environment 开发环境可以设置不同的配置。

node.js 默认为 development env 开发环境。通过设置环境变量:NODE_ENV=production 可以告诉 node.js 当前为产品环境。Linux 中可以通过如下指令修改:

export NODE_ENV=production

这样在当前 shell 中就会以 production 模式运行 node 程序,如果要永久修改此环境变量可以将命令写入 ~/.bashrc 文件。

也可以在运行 node 程序时定义 NODE_ENV

NODE_ENV=production node app.js

设置 NODE_ENVproduction 有如下优势:

  • 日志记录保持在最小 level
  • 更多的 caching 缓存级别来提高运行效率

可以通过状态判读符来判断当前运行环境,执行不同指令:

if (process.env.NODE_ENV === 'development') {
    console.log('running on development environment')
}
if (process.env.NODE_ENV === 'production') {
    console.log('running on production environment')
}
if (['production', 'staging'].indexOf(process.env.NODE_ENV) != -1) {
    console.log('running on production environment')
} else {
    console.log('running on development environment')
}

上面第三个指令通过调用 array 的 indexOf 来获取某个字符串在数组中的索引号,如果不存在这个字符串则返回 -1。注意使用类型判断符=== 是为了保证类型和数值都匹配。

error handling 错误处理

node.js 中通过 exceptions 来处理 error 情况。

可以通过 throw 创建一个 exception

const test = 123;
throw test;

当 JavaScript 执行到上面的 throw 时程序会立刻停止,然后控制权会交给最近的 exception handler 例外控制器。

JavaScript 中 test 可以是任意的字符串,数字或者 object。但在 node.js 中只用来 throw Error objects。

一个 error object 可以是 Error 的实例或者是 Error class 的 child class 的实例:

throw new Error('error')

class childError extends Error {
}
throw new childError('error')

下面介绍几种 exception handler 处理器。

第一种是通过 try/catch 声明来处理,在 try 中定义任意 exception 以及对应的 catch 块来响应:

try {
    const a = 1;
    if (a == 0) {
        throw 'abc'
    } else {
        throw 123
    }
} catch (e) {
    console.log(e)
}

catch 的参数 e 就是对应 throw 的数据。

如果在程序执行中,如果某个 throw 的 exception 没有被 catch,程序就会立刻 crash。可以通过监听 processuncaughtException event 事件来解决这个问题:

process.on('uncaughtException', err => {
    console.log('there was a uncaught error: ', err.message)
    process.exit(1)
})

throw new Error('error')

第二种promise 中的 error handler。

使用 promise 可以将多个操作过程组合成链,在链的最后处理 error 情况,下面是一个简单示例:

let a = 0;
let b = 0;

const doSomething1 = new Promise((resolve, reject) => {
    if (a == 0)
        resolve('resolve 1')
    else
        reject('reject 1')
})

const doSomething2 = (data) => {
    return new Promise((resolve, reject) => {
        if (b == 0)
            resolve(data + ' : resolve 2')
        else
            reject('reject 2')

    })
}

doSomething1
    .then(doSomething2)
    .then(res => console.log(res))
    .catch(rej => console.log(rej))

doSomething1 为 resolve 时会执行 doSomething2(),为 reject 时会直接进入 catch 块。同样的 doSomething2 返回的 promise 为 reject 时也会进入 catch 块。此时通过输出的信息可以知道是在哪一个 promise 链响应了 exception。

第三种async/await 的 error handling。

使用 async/await 也是通过 try/catch 来处理 exception 情况的:

let a = 1;
const demo = new Promise((resolve, reject) => {
    if (a == 0) {
        setTimeout(resolve, 1000, 'resolve');
    } else {
        reject('reject')
    }
})

const test = async() => {
    try {
        const data = await demo
        console.log(data)
    } catch (error) {
        console.log(error)
    }
}
test()

//output:
//reject

demo promise 为 reject 时会响应 test() 中的 catch 块。

日志输出 object

当我们在浏览器中使用 console.log() 输出某个 object 时,会得到一个很好的效果:
2021-02-21T07:32:18.png

点击箭头可以展开日志,完整地展示 object 的 properties:
2021-02-21T07:33:15.png

在 node.js 中和浏览器有所不同,它是将 object 的内容以字符串的形式输出到终端或者日志文件中。

对于简单的 object 来说没有什么问题,但是对于包含超过 2 层嵌套以上的 object 来说 node.js 就会放弃输出所有内容而用一个 [Object] 占位符表示剩余内容:

const obj = {
    name: 'joe',
    age: 35,
    person1: {
        name: 'Tony',
        age: 50,
        person2: {
            name: 'Albert',
            age: 21,
            person3: {
                name: 'Peter',
                age: 23
            }
        }
    }
}
console.log(obj)

输出结果为:

{
  name: 'joe',
  age: 35,
  person1: {
    name: 'Tony',
    age: 50,
    person2: { name: 'Albert', age: 21, person3: [Object] }
  }
}

可以看到第三层的内容已经用 [Object] 取代。

强制要求输出 object 所有内容的最好的方法就是使用 JSON.stringify()

console.log(JSON.stringify(obj, undefined, 2))

输出如下:

{
  "name": "joe",
  "age": 35,
  "person1": {
    "name": "Tony",
    "age": 50,
    "person2": {
      "name": "Albert",
      "age": 21,
      "person3": {
        "name": "Peter",
        "age": 23
      }
    }
  }
}

第一个参数定义需要输出的 object,第二个参数是定义一个 function 用来转换字符串内容也可以不定义,第三个是定义缩进量。

]]>
0 https://blog.niekun.net/archives/2165.html#comments https://blog.niekun.net/feed/category/node/archives/2165.html
node.js 入门教程之十二 -- Streams 流 https://blog.niekun.net/archives/2161.html https://blog.niekun.net/archives/2161.html Sat, 20 Feb 2021 14:43:00 +0800 admin Streams 是驱动 node.js 程序的核心概念。它提供了处理对文件的读写,网络传输,或者其他端到端的数据交换的更加高效的方式。

streams 流并不是 node.js 首先引入的概念,unix 操作系统在很久之前就在使用了,一个程序可以通过 pipe 管道操作符 | 来传递 streams 流给其他程序。

下面的示例是在 Linux 中,通过 pipe 管道将 cat 读取的文件数据传递给 grep 进行过滤,test.txt 文件内容如下:

aaa bbb
bbb ccc
aaa ccc
$ cat test.txt | grep aaa
aaa bbb
aaa ccc

stream 优点

在传统方法中,当程序读取一个文件内容时会先将文件内容全部读取到内存中,然后再去使用它。而使用 stream 时会一段段的读取文件内容并进行处理,而不需要整体读取到内存。

node.js 的 stream 模块是构建所有 streaming API 的底层。所有的 streams 流都是 eventEmitter 的实例。每种类型的 stream 都有各自的 event 事件,在数据流变化时触发对应事件。

相比于其他数据处理方法,streams 有两个优势:

  • 内存效率:在使用数据前不需要将大量数据存入内存中。
  • 时间效率:能够更快的进入数据处理阶段,在获取到文件后能够很快的使用它而不需要等待它全部加载完毕。

使用方法

下面我们从一个示例来说明 stream 的使用方法。我们建立一个 http server,当收到请求时读取一个本地文件的内容并作为 response 发送给客户端。

首先传统实现方式如下:

const http = require('http')
const fs = require('fs')

const hostname = '127.0.0.1'
const port = 3000

const server = http.createServer((req, res) => {
    fs.readFile(__dirname + '/test.txt', (err, data) => {
        if (err) {
            console.log('read error')
        }
        res.end(data)
    })
})

server.listen(port, hostname, () => {
    console.log(`Server running at http://${hostname}:${port}/`)
})

以上方式在收到请求后,通过 readFile() 读取文件内容到内存中,当成功后会调用 callback 通过 res.end(data) 发送读取的数据给客户端。如果文件较大时以上过程会花费一些时间。

__dirname 返回当前执行文件的路径。

下面介绍使用 stream 实现上面的过程:

const http = require('http')
const fs = require('fs')

const hostname = '127.0.0.1'
const port = 3000

const server = http.createServer((req, res) => {
    const stream = fs.createReadStream(__dirname + '/test.txt')
    stream.pipe(res)
})

server.listen(port, hostname, () => {
    console.log(`Server running at http://${hostname}:${port}/`)
})

通过 fs.createReadStream() 返回一个文件的 stream。

不同于第一种,以上方式会在 stream 中只要有了 data chunk 块就立刻作为 response 数据传输给客户端。这样就会一边读取文件一边传输数据。

pipe()

以上示例中调用 streampipe() method。它的功能是建立一个 source stream 源流到 destination stream 目标流的 pipe 管道。这样文件的 stream 流通向了 http response。

pipe() 的返回是 destination stream 目标流,这样就可以很方便的链接多个 pipe:

src.pipe(dest1).pipe(dest2)

以下写法和上面示例效果一样:

src.pipe(dest1)
dest1.pipe(dest2)

基于 stream 的 node.js API

由于 stream 的巨大优势,很多 node.js 核心模块提供了 stream 的原生支持,以下是常用的部分:

  • process.stdin 返回一个链接到 stdin 的 stream
  • process.stdout 返回一个链接到 stdout 的 stream
  • process.stderr 返回一个链接到 stderr 的 stream
  • fs.createReadStream() 创建一个文件的可读的 stream
  • fs.createWriteStream() 创建一个文件的可写的 stream
  • net.connect() 初始化一个基于 stream 的连接
  • http.request() 返回一个 http.ClientRequest 实例,它是一个可写的 stream

streams 的类型

有四个 streams 的 calsses:

  • Readable: 一个可以作为管道源头的 stream,不能作为管道的目标也就是不能写入数据
  • Writable: 一个可以作为管道目标的 stream,不能作为管道源头也就是不能从中获取数据
  • Duplex: 一个既可作为管道源头也可以作为目标的 stream
  • Transform: 类似于 Duplex

创建 readable stream

从 stream 模块定义一个可读的 stream 然后通过定义 readable._read() method 内容完成初始化:

const Stream = require('stream')
const readableStream = new Stream.Readable()

readableStream._read = () => {}

初始化也可以这样写:

const readableStream = new Stream.Readable({
    read() {}
})

以上就创建了一个可读的 stream,可以给其传输数据:

readableStream.push('hi')
readableStream.push('hello')

可以将这个 stream 和一个可写的 stream 之间建立管道:

readableStream.pipe(process.stdout)

创建 writable stream

通过实例化一个 Writable object 然后通过定义 _write() method 内容完成初始化:

const Stream = require('stream')
const writeableStream = new Stream.Writable()

writeableStream._write = (chunk, encoding, callback) => {
    console.log(chunk.toString())
    callback()
}

接收到的数据块传入 chunk,encoding 定义数据类型,callback function 是当一个 chunk 数据块传输完成后调用,此 callback 可在 writeableStream.write 内定义,一般可以是 error 处理,下面会介绍。

可以将定义的可写 stream 同一个可读 stream pipe 管道连接:

process.stdin.pipe(writeableStream)

这样就可以在 stdin 和 writeableStream 间建立 pipe,此时在 stdin 输入数据就会立刻将数据显示在终端。

从可读的 stream 读取数据

下面的示例中,我们建立一个可读的 stream 和一个 可写的 stream,并建立 pipe 管道连接:

const Stream = require('stream')

const readableStream = new Stream.Readable()
readableStream._read = () => {}

const writeableStream = new Stream.Writable()
writeableStream._write = (chunk, encoding, callback) => {
    console.log(chunk.toString())
    callback()
}

readableStream.pipe(writeableStream)

readableStream.push('hi\n')
readableStream.push('hello\n')

也通过 readable event 事件来处理可读 stream。当 stream 中有准备好的数据块时会触发 readable event:

readableStream.on('readable', () => {
    console.log(readableStream.read().toString())
})

给可写的 stream 写入数据

使用 write method 来写入数据:

writeableStream.write('hello world\n')

可以定义写入数据的编码格式和 callback function:

writeableStream.write('hello world\n', 'utf-8', err => {console.log(err)})

实际中 callback function 是否会执行要看 stream 初始化中在 writeableStream._write 是否调用了 callback。

如果数据写入已经完成,可以使用 end method 来告诉可写的 stream:

const Stream = require('stream')
const writeableStream = new Stream.Writable()

writeableStream._write = (chunk, encoding, next) => {
    console.log(chunk.toString())
    next()
}

process.stdin.pipe(writeableStream)

writeableStream.write('hello world\n')
writeableStream.end()

以上示例中如果在最后一句不调用 end method 则 writeableStream 会持续保持接收来自 stdin 的数据状态。

创建 transform stream

和可写的 stream 创建方式类似,可读也可写:

const Stream = require('stream')
const transformStream = new Stream.Transform()

transformStream._transform = (chunk, encoding, callback) => {
    console.log('transform' + chunk.toString())
    transformStream.push(chunk)
    callback()
}

const writeableStream = new Stream.Writable()
writeableStream._write = (chunk, encoding, callback) => {
    console.log('write' + chunk.toString())
    callback()
}

process.stdin.pipe(transformStream).pipe(writeableStream)

transformStream._transform 定义了当 transform stream 接收到数据后通过 transformStream.push 将数据发到 readable stream 中,这样其他 stream 就可以读取到它接收到的数据了。

更多 stream 使用方法参考:https://nodejs.org/api/stream.html

]]>
0 https://blog.niekun.net/archives/2161.html#comments https://blog.niekun.net/feed/category/node/archives/2161.html
node.js 入门教程之十一 -- buffers 缓存 https://blog.niekun.net/archives/2157.html https://blog.niekun.net/archives/2157.html Tue, 16 Feb 2021 17:26:00 +0800 admin 一个 buffer 缓存就是一个内存空间。JavaScript 开发者相较于 c++/go 等语言的开发者对内存的理解比较少。它表示在 JavaScript V8 engine 之外的分配的一块 fixed-size 固定大小的内存。可以想象 buffer 就是一个 integer 整型数组,每个元素代表一个 byte 的数据。

node.js 中 buffer 通过 Buffer class 实现。

为何需要 buffer

引入 Buffer 是为了帮助开发者处理 binary 二进制数据。传统的 ecosystem 只可以处理 strings 字符串数据。Buffer 和 streams 是紧密关联的,当 stream processor 流处理器接收数据的速度大于其处理速度时,就会先将数据放在 buffer 中。

可以想象下,在我们看在线视频时,在网络较好的情况下进度条中可以看到视频下载的进度是超过当前播放进度的,此时浏览器就会先 buffer 缓存数据后续按时间顺序播放出来。

创建 buffer

可以通过 Buffer.from(), Buffer.alloc()Buffer.allocUnsafe() methods 来创建一个 buffer,下面依次介绍。

使用 Buffer.from() 可以从 array 数组创建 buffer:

const buf1 = Buffer.from('hey!')

使用 Buffer.from(string[, encoding]) 可以设置 string 的编码类型,Default: utf8

const buf2 = Buffer.from('7468697320697320612074c3a97374', 'hex')

也可以根据另一个 buffer:

const buf3 = Buffer.from(buf1)

通过 Buffer.alloc()Buffer.allocUnsafe() 来初始化一个指定大小的 buffer,单位为 byte 字节:

const buf4 = Buffer.alloc(1024)
const buf5 = Buffer.allocUnsafe(1024)

以上会创建一个 1kb 大小的 buffer。

通过 alloc 创建的 buffer 会初始化其数据为 0,而 allocUnsafe 创建的 buffer 内的数据会是 uninitiated 未初始化的,这就表示使用 allocUnsafealloc 创建速度更快。但是通过 allocUnsafe 创建的 buffer 内存空间会包含有以前的数据可能引起潜在问题。

使用 buffer

我们可以像访问数组一样访问 buffer 数据:

const buf1 = Buffer.from('hey!')

console.log(buf1[0])
console.log(buf1[1])
console.log(buf1[2])

//output:
//104
//101
//121

这些输出的数字就是代表 buffer 中每个字符的 Unicode Code,h => 104, e => 101, y => 121。

使用 toString() method 可以将 buffer 输出为字符:

console.log(buf1)
console.log(buf1.toString())

//output:
//<Buffer 68 65 79 21>
//hey!

buf1.length 可以读取 buffer 长度:

console.log(buf1.length)

//output:
//4

也可以使用循环 iterate buffer 的数据:

for (const iterator of buf1) {
    console.log(iterator)
}

//output:
104
101
121
33

通过 write() method 可以给 buffer 写入数据:

const buf6 = Buffer.alloc(5)
buf6.write('hello')

也可以像数组一样修改某个元素的数据:

const buf6 = Buffer.alloc(5)
buf6.write('hello')
buf6[0] = 111
console.log(buf6.toString())

//output:
//oello

可以看到需要修改字符对应的 Unicode Code。

使用 copy() method 可以复制 buffer 数据到其他 buffer:

const buf1 = Buffer.from('hey!')
const buf7 = Buffer.alloc(4)
buf1.copy(buf7)
console.log(buf7.toString())

//output:
//hey!

注意是源 buffer 掉用 copy method 来复制数据到目标 buffer。

默认情况下会复制 buffer 的整个数据, copy method 可以设置其他三个参数来自定义目标 buffer 起始接收数据位置,源 buffer 起始复制数据位置,以及复制的新 buffer 的长度:

const buf8 = Buffer.alloc(2)
buf1.copy(buf8, 0, 0, 2)
console.log(buf8.toString())

//output:
//he

可以使用 slice method 来选取 buffer 的一个片段,它和 copy 的不同是 slice 获取的 buffer 片段仍然和源 buffer 相关联,修改源 buffer 会改变片段中的 buffer 数据。

slice 的第一个参数定义起始位置,第二个参数是选项,可以定义终止位置:

const buf1 = Buffer.from('hey!')
const slice = buf1.slice(0, 2)
console.log(slice.toString())
buf1[0] = 111
console.log(slice.toString())

//output:
//he
//oe

以上就是 Buffer 的简单使用方法。

]]>
0 https://blog.niekun.net/archives/2157.html#comments https://blog.niekun.net/feed/category/node/archives/2157.html
node.js 入门教程之十 -- http 模块 https://blog.niekun.net/archives/2156.html https://blog.niekun.net/archives/2156.html Mon, 15 Feb 2021 14:01:00 +0800 admin http 模块是 node.js 处理 networking 网络的关键模块。在之前的章节我介绍了使用 http 模块建立 server 以及发起 GET/POST 请求的方法,可以参考:https://blog.niekun.net/archives/2137.html

http 模块集成于 node.js 核心无需单独安装,使用下面命令引入模块:

const http = require('http')

模块提供了很多 propertiesmethodsclasses

Properties 属性

http.METHODS

http.METHODS property 返回 http 所有的可用 method 列表:

> console.log(require('http').METHODS)
[
  'ACL',        'BIND',        'CHECKOUT',
  'CONNECT',    'COPY',        'DELETE',
  'GET',        'HEAD',        'LINK',
  'LOCK',       'M-SEARCH',    'MERGE',
  'MKACTIVITY', 'MKCALENDAR',  'MKCOL',
  'MOVE',       'NOTIFY',      'OPTIONS',
  'PATCH',      'POST',        'PRI',
  'PROPFIND',   'PROPPATCH',   'PURGE',
  'PUT',        'REBIND',      'REPORT',
  'SEARCH',     'SOURCE',      'SUBSCRIBE',
  'TRACE',      'UNBIND',      'UNLINK',
  'UNLOCK',     'UNSUBSCRIBE'
]

http.STATUS_CODES

http.STATUS_CODES 返回所有的 http 状态码及描述的 list 列表:

> console.log(require('http').STATUS_CODES)
{
  '100': 'Continue',
  '101': 'Switching Protocols',
  '102': 'Processing',
  '103': 'Early Hints',
  '200': 'OK',
  '201': 'Created',
  '202': 'Accepted',
  '203': 'Non-Authoritative Information',
  '204': 'No Content',
  '205': 'Reset Content',
  '206': 'Partial Content',
  '207': 'Multi-Status',
  '208': 'Already Reported',
  '226': 'IM Used',
  '300': 'Multiple Choices',
  '301': 'Moved Permanently',
  '302': 'Found',
  '303': 'See Other',
  '304': 'Not Modified',
  '305': 'Use Proxy',
  '307': 'Temporary Redirect',
  '308': 'Permanent Redirect',
  '400': 'Bad Request',
  '401': 'Unauthorized',
  '402': 'Payment Required',
  '403': 'Forbidden',
  '404': 'Not Found',
  '405': 'Method Not Allowed',
  '406': 'Not Acceptable',
  '407': 'Proxy Authentication Required',
  '408': 'Request Timeout',
  '409': 'Conflict',
  '410': 'Gone',
  '411': 'Length Required',
  '412': 'Precondition Failed',
  '413': 'Payload Too Large',
  '414': 'URI Too Long',
  '415': 'Unsupported Media Type',
  '416': 'Range Not Satisfiable',
  '417': 'Expectation Failed',
  '418': "I'm a Teapot",
  '421': 'Misdirected Request',
  '422': 'Unprocessable Entity',
  '423': 'Locked',
  '424': 'Failed Dependency',
  '425': 'Too Early',
  '426': 'Upgrade Required',
  '428': 'Precondition Required',
  '429': 'Too Many Requests',
  '431': 'Request Header Fields Too Large',
  '451': 'Unavailable For Legal Reasons',
  '500': 'Internal Server Error',
  '501': 'Not Implemented',
  '502': 'Bad Gateway',
  '503': 'Service Unavailable',
  '504': 'Gateway Timeout',
  '505': 'HTTP Version Not Supported',
  '506': 'Variant Also Negotiates',
  '507': 'Insufficient Storage',
  '508': 'Loop Detected',
  '509': 'Bandwidth Limit Exceeded',
  '510': 'Not Extended',
  '511': 'Network Authentication Required'
}

http.globalAgent

http.globalAgent 指向 http.Agent class 的一个 instance 实例,也就是一个 Agent object。

这个 Agent object 用来管理 server 同 http 客户端链接的持续连接和链接复用,这也是 node.js 网络服务的关键点。

> console.log(require('http').globalAgent)
Agent {
  _events: [Object: null prototype] {
    free: [Function (anonymous)],
    newListener: [Function: maybeEnableKeylog]
  },
  _eventsCount: 2,
  _maxListeners: undefined,
  defaultPort: 80,
  protocol: 'http:',
  options: { path: null },
  requests: {},
  sockets: {},
  freeSockets: {},
  keepAliveMsecs: 1000,
  keepAlive: false,
  maxSockets: Infinity,
  maxFreeSockets: 256,
  scheduling: 'lifo',
  maxTotalSockets: Infinity,
  totalSocketCount: 0,
  [Symbol(kCapture)]: false
}

methods 功能

http.createServer()

http.createServer() 返回一个 http.Server class 实例。

const server = http.createServer((req, res) => {
  //handle every single request with this callback
})

http.request()

http.request() 对服务器发起一个 http 请求。返回一个 http.ClientRequest class 实例。

const options = {
    hostname: 'localhost',
    port: 3000,
    path: '/',
    method: 'GET'
}

const req = https.request(options, res => {
    console.log(`statusCode is:${res.statusCode}`);
    res.on('data', d => {
        process.stdout.write(d)
    })
})

req.on('error', err => console.log(err))
req.end()

http.get()

http.get()http.request() 类似,但会自动将 http method 设置为 GET,且自动调用 req.end()

const https = require('http')
const options = {
    hostname: 'localhost',
    port: 3000,
    path: '/'
}

const req = https.get(options, res => {
    console.log(`statusCode is:${res.statusCode}`);
    res.on('data', d => {
        process.stdout.write(d)
    })
})

req.on('error', err => console.log(err))

Classes 类

http 模块提供 5 个 calsses:

  • http.Agent
  • http.ClientRequest
  • http.Server
  • http.ServerResponse
  • http.IncomingMessage

http.Agent

node.js 会创建一个全局的 http.Agent instance 实例来管理 server 同 http 客户端链接的持续连接和链接复用,这个 object 可以确保一个客户端对服务端发起的一系列请求是按队列排序的,且使用单独的 socket 接口。同样 agent object 管理着一个 sockets 池,这是影响性能的关键因素。

http.ClientRequest

http.request()http.get() 被调用时会创建一个 http.ClientRequest object。

当收到来自服务器的 response 时,response event 会被触发,http.request() 内定义的 callback 会作为 response 的响应被调用,同时将 http.IncomingMessage 的实例作为传入参数,其中包含了 response 数据。

response 数据有两种方式读取,第一种是通过 response.read() method,第二种是在 response 的 callback function 中通过监听 data event 来获取 stream 中的数据。

http.Server

当通过 http.createServer() 创建一个 server 时会返回 http.Server 实例。 http.Server 实例需要使用它的以下 method:

  • close() 停止 server 接收新的链接
  • listen() 启动 server 并开始监听请求

http.createServer() 的 callback 监听 request event 并响应。

http.ServerResponse

http.Server object 创建,作为 request event 的第二个参数传入,代码中作为 callback 的 res 参数使用:

const server = http.createServer((req, res) => {
  //res is an http.ServerResponse object
})

end()http.ServerResponse object 必须被使用的 method,用来表示 response 信息已经完整,可以结束这个 response,同时将 response 发送出去。

下面的 method 可以用来处理 http headers:

  • getHeaderNames() 获取当前 http headers 名称的列表
  • getHeaders() 获取当前 http headers 副本
  • setHeader('headername', value) 设置一个 http header
  • getHeader('headername') 获取当前的一个 http header 的设置
  • removeHeader('headername') 删除一个 http header
  • hasHeader('headername') 如果某个 http header 有被设置则返回 true
  • headersSent() 如果 http headers 已经发送给了客户端则返回 true

服务端编辑好 headers 条目后可以通过 response.writeHead() method 发送给客户端,第一个参数是 statusCode。

通过 response.write() 在 response body 中发送数据给客户端,它会将缓冲区的数据发送到 http response stream。如果还没有使用 response.writeHead() 发送 headers 则会先发送 headers,包含 statusCode 和 message,它们可以通过下面语法设置:

response.statusCode = 500
response.statusMessage = 'Internal Server Error'

http.IncomingMessage

http.IncomingMessage 可以在以下两个地方创建:

  • http.Server 当监听 request event
  • http.ClientRequest 当监听 response event

它可以用来访问 response 数据:

  • 通过 statusCodestatusMessage 读取状态信息
  • 通过 headers method 或 rawHeaders method 读取 headers 信息
  • 通过 method method 获取可用 http method
  • 通过 httpVersion method 获取 http 版本信息
  • 通过 url method 获取 URL 信息
  • 通过 socket method 获取底层 socket 信息

http.IncomingMessage 生效一个可读的 stream 接口后,数据可在 stream 中读取。

]]>
0 https://blog.niekun.net/archives/2156.html#comments https://blog.niekun.net/feed/category/node/archives/2156.html
node.js 入门教程之九 -- events 模块 https://blog.niekun.net/archives/2151.html https://blog.niekun.net/archives/2151.html Mon, 08 Feb 2021 21:42:00 +0800 admin 在之前介绍 event loop 的章节,我们简单介绍了 events 模块的使用,它可以设置并监听某个信号并在信号触发时响应 callback function。

events 模块提供了 EventEmitter class 它是处理 events 的关键工具。

引入及初始化 events 模块如下:

const EventEmitter = require('events')
const eventEmitter = new EventEmitter()

每个 event listener 事件监听器都是互相独立的,同时它们使用如下 events:

  • newListener 当一个 event 被添加
  • removeListener 当一个 event 被删除

下面介绍常用的一些 method。

eventEmitter.addListener()

eventEmitter.addListener()eventEmitter.on() 功能相同。可以给某个 event 添加一个 listener,语法如下:

eventEmitter.addListener('test', () => {})

eventEmitter.emit()

eventEmitter.emit() 用来触发一个信号,信号触发后,对应信号的 event listener 会响应 callback function:

eventEmitter.addListener('test', () => { console.log('test emited') })
eventEmitter.emit('test')

//output:
//test emited

eventEmitter.eventNames()

eventEmitter.eventNames() 会返回当前 EventEmitter object 内注册已的 events 为一个字符串数组:

eventEmitter.addListener('test1', () => {})
eventEmitter.addListener('test2', () => {})
console.log(eventEmitter.eventNames())

//output:
//[ 'test1', 'test2' ]

eventEmitter.getMaxListeners()

eventEmitter.getMaxListeners() 返回当前 EventEmitter object 能添加的 event listener 最大数量。默认为 10 个,可以通过 setMaxListeners() 修改。

eventEmitter.listenerCount()

eventEmitter.listenerCount() 返回某个 event 定义的 listeners 数量:

eventEmitter.addListener('test4', () => { console.log('a') })
eventEmitter.addListener('test4', () => { console.log('b') })
console.log(eventEmitter.listenerCount('test4'))

//output:
//2

eventEmitter.listeners()

eventEmitter.listeners() 返回某个 event 所有的 listeners 为一个数组:

const f1 = () => {}
const f2 = () => {}
eventEmitter.addListener('test5', f1)
eventEmitter.addListener('test5', f2)
console.log(eventEmitter.listeners('test5'))

//output:
//[ [Function: f1], [Function: f2] ]

eventEmitter.off()

eventEmitter.off()eventEmitter.removeListener() 功能相同。可以删除一个 event 的某个 listener,需要指定 event 名称和 listener callback 名称:

const f3 = () => {}
eventEmitter.addListener('test6', f3)
eventEmitter.off('test6', f3)

eventEmitter.on()

eventEmitter.on()eventEmitter.addListener() 功能相同。可以给某个 event 添加一个 listener:

eventEmitter.on('test6', () => {})

eventEmitter.once()

eventEmitter.once() 监听一个 event 且其 callback function 只能被执行一次:

eventEmitter.once('test7', () => console.log('once show once'))
eventEmitter.emit('test7')
eventEmitter.emit('test7')

//output:
//once show once

eventEmitter.prependListener()

当通过 eventEmitter.on() 添加 listener 时,会按照添加顺序排列在此 event 队列中,当对应 event 触发时,会按照排列顺序以此执行 callback。

使用 eventEmitter.prependListener() 添加的 listener 会添加到队列的最前面,触发时最先被执行:

eventEmitter.on('test8', () => console.log('first listener'))
eventEmitter.prependListener('test8', () => console.log('second listener'))
eventEmitter.emit('test8')

//output:
//second listener
//first listener

eventEmitter.prependOnceListener()

eventEmitter.prependOnceListener()eventEmitter.on() 的区别和上面类似,会将 listener 添加到对应 event listeners 队列的最前面,然后最先被执行且只能响应一次:

eventEmitter.once('test9', () => console.log('first listener'))
eventEmitter.prependOnceListener('test9', () => console.log('second listener'))
eventEmitter.emit('test9')
eventEmitter.emit('test9')

//output:
//second listener
//first listener

eventEmitter.removeAllListeners()

eventEmitter.removeAllListeners() 可以删除指定 event 的所有 listeners:

eventEmitter.removeAllListeners('test9')

eventEmitter.removeListener()

eventEmitter.removeListener() 可以删除 event 的指定 listener。需要将 listener 的 callback function 定义为变量格式然后再调用,这样就可以在此 reference 它:

const f1 = () => {}
const f2 = () => {}
eventEmitter.addListener('test5', f1)
eventEmitter.addListener('test5', f2)

eventEmitter.removeListener('test5', f1)

eventEmitter.setMaxListeners()

eventEmitter.setMaxListeners() 可以定义 EventEmitter object 最大可以添加的 listeners 数量:

eventEmitter.setMaxListeners(20)
console.log(eventEmitter.getMaxListeners())

//output:
//20

以上就是 events 模块最常用功能的介绍。

]]>
0 https://blog.niekun.net/archives/2151.html#comments https://blog.niekun.net/feed/category/node/archives/2151.html
node.js 入门教程之八 -- os 模块 https://blog.niekun.net/archives/2147.html https://blog.niekun.net/archives/2147.html Mon, 08 Feb 2021 14:03:00 +0800 admin os 模块可以用来获取操作系统的底层信息和系统上运行的程序,以及和系统交互。

它集成在 node.js 中,只需要引入即可:

const os = require('os')

这里先介绍一些在处理文件时有用的 properties:

  • os.EOL 会返回 line delimiter 行定界符,\n on Linux and macOS, and \r\n on Windows
  • os.constants.signals 提供处理进程的信号常数,如:SIGHUP, SIGKILL
  • os.constants.errno 提供报告 error 的相关常数,如:EADDRINUSE, EOVERFLOW

完整的 signals 信号列表参考:https://nodejs.org/api/os.html#os_signal_constants

下面介绍 os 模块常用的 method。

os.arch()

os.arch() 会返回底层架构的字符串,如:arm, x64, arm64:

console.log(os.arch())

//output:
x64

os.cpus()

os.cpus() 会返回系统的 cpu 信息:

console.log(os.cpus())

//output:
[
  {
    model: 'Intel(R) Core(TM) i7-6500U CPU @ 2.50GHz',
    speed: 2591,
    times: {
      user: 1871810,
      nice: 17880,
      sys: 1451160,
      idle: 81134010,
      irq: 0
    }
  },
  {
    model: 'Intel(R) Core(TM) i7-6500U CPU @ 2.50GHz',
    speed: 2591,
    times: {
      user: 1838130,
      nice: 12780,
      sys: 1532060,
      idle: 81234660,
      irq: 0
    }
  }
]

os.endianness()

根据 node.js 是通过 Big Endian 或 Little Endian 大尾数或小尾数编译的,os.endianness() 返回 BELE

console.log(os.endianness())

//output:
LE

关于 Big Endian 或 Little Endian 参考:https://en.wikipedia.org/wiki/Endianness
1.jpg

os.freemem()

os.freemem() 返回系统可用内存,以字节为单位:

console.log(os.freemem())

//output:
574537728

os.homedir()

os.homedir() 返回系统当前用户的 home 目录路径:

console.log(os.homedir())

//output:
/home/marco

os.hostname()

os.hostname() 返回 hostname 信息:

console.log(os.hostname())

//output:
marco-virtual-machine

os.loadavg()

os.loadavg() 返回系统计算的平均加载时间,此 method 只在 Linux/macOS 下有意义:

console.log(os.loadavg())

//output:
[ 0.08, 0.17, 0.13 ]

os.networkInterfaces()

os.networkInterfaces() 返回系统的网卡信息:

console.log(os.networkInterfaces())

//output:
{ lo0:
   [ { address: '127.0.0.1',
       netmask: '255.0.0.0',
       family: 'IPv4',
       mac: 'fe:82:00:00:00:00',
       internal: true },
     { address: '::1',
       netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
       family: 'IPv6',
       mac: 'fe:82:00:00:00:00',
       scopeid: 0,
       internal: true },
     { address: 'fe80::1',
       netmask: 'ffff:ffff:ffff:ffff::',
       family: 'IPv6',
       mac: 'fe:82:00:00:00:00',
       scopeid: 1,
       internal: true } ],
  en1:
   [ { address: 'fe82::9b:8282:d7e6:496e',
       netmask: 'ffff:ffff:ffff:ffff::',
       family: 'IPv6',
       mac: '06:00:00:02:0e:00',
       scopeid: 5,
       internal: false },
     { address: '192.168.1.38',
       netmask: '255.255.255.0',
       family: 'IPv4',
       mac: '06:00:00:02:0e:00',
       internal: false } ],
  utun0:
   [ { address: 'fe80::2513:72bc:f405:61d0',
       netmask: 'ffff:ffff:ffff:ffff::',
       family: 'IPv6',
       mac: 'fe:80:00:20:00:00',
       scopeid: 8,
       internal: false } ] }

os.platform()

os.platform() 返回当前安装的 node.js 的编译平台,可能的返回值为:

  • darwin
  • freebsd
  • linux
  • openbsd
  • win32
  • ...more

os.release()

os.release() 返回当前系统的版本号信息:

console.log(os.release())

//output:
5.8.0-41-generic

os.tmpdir()

os.tmpdir() 返回系统的临时文件夹路径:

console.log(os.tmpdir())

//output:
/tmp

os.totalmem()

os.totalmem() 返回系统的总内存数量,单位为 byte 字节:

console.log(os.totalmem()/1024/1024)

//output:
1958.45703125

os.type()

os.type() 返回当前操作系统类型,返回值可以有:

  • Linux
  • Darwin on macOS
  • Windows_NT on Windows

os.uptime()

os.uptime() 返回系统从上次开机来已运行的总时间,单位为秒:

console.log(os.uptime()/3600)

//output:
24.260555555555555

os.userInfo()

os.userInfo() 返回一个 object 包含当前的 username, uid, gid, shell, 和 homedir

console.log(os.userInfo())

//output:
{
  uid: 1000,
  gid: 1000,
  username: 'marco',
  homedir: '/home/marco',
  shell: '/bin/bash'
}

以上就是 os 模块的简单使用方法。

]]>
0 https://blog.niekun.net/archives/2147.html#comments https://blog.niekun.net/feed/category/node/archives/2147.html
node.js 入门教程之七 -- fs 和 path 模块 https://blog.niekun.net/archives/2146.html https://blog.niekun.net/archives/2146.html Mon, 08 Feb 2021 11:21:00 +0800 admin 上一章节我们了解了如果访问文件系统。使用到了 fs 和 path 模块的部分功能,下面我们详细了解这两个模块可以实现的功能。

fs module

通过以上几个使用场景可以看到 fs 模块提供了很多有用的功能来对 file system 进行访问和交互。并且他是集成在 node.js core 中的,并不需要手动安装:

const fs = require('fs')

通过以上方式引入 fs 模块后,我们就可以使用它的各种功能了。包括:

  • fs.access(): 检查文件是否存在以及 node.js 是否有权限访问
  • fs.appendFile(): 给一个文件附加数据,如果文件不存在则会创建它
  • fs.chmod(): 修改文件的权限
  • fs.chown(): 修改文件的 owner 和 group 设置
  • fs.close(): 关闭已经打开的文件的 descriptor
  • fs.copyFile(): 复制一个文件
  • fs.createReadStream(): 创建一个可读的文件 stream
  • fs.createWriteStream(): 创建一个可写的文件 stream
  • fs.link(): 创建一个文件的硬链接
  • fs.mkdir(): 新建文件夹
  • fs.mkdtemp(): 创建一个临时文件夹
  • fs.open(): 打开一个文件的 descriptor,可定义打开编码类型
  • fs.readdir(): 读取一个目录下的内容
  • fs.readFile(): 读取一个文件的内容
  • fs.readlink(): 读取一个软连接的值
  • fs.realpath(): resolve 查询一个相对路径文件的绝对路径
  • fs.rename(): 重命名文件或文件夹
  • fs.rmdir(): 删除一个文件夹
  • fs.stat(): 返回一个文件的状态信息
  • fs.symlink(): 创建一个文件的软链接
  • fs.truncate(): 将一个文件分割成指定长度大小
  • fs.unlink(): 删除一个文件或一个软链接
  • fs.unwatchFile(): 停止监测对一个文件的修改
  • fs.utimes(): 修改一个文件的时间戳
  • fs.watchFile(): 开始监测对一个文件的修改
  • fs.writeFile(): 对一个文件写入数据

fs 模块的一个特性是它的以上所有 method 默认都是 asynchronous 异步的,给 method 后加上 Sync 就可以使用同步模式,例如:

  • fs.rename() -> fs.renameSync()
  • fs.write() -> fs.writeSync()

同步模式下通过使用 try/catch 块来处理 error 情况。同步模式下程序会在处理文件过程中出现进程阻塞直到处理完成,在编程中可以根据需要灵活运用。

path module

path 模块也提供了很多实用的功能来访问文件系统以及交互。同样的它也是集成在 node.js core 中而不需要单独安装。

const path = require('path')

通过 path.sep 可以获取当前系统的 path separator 路径分隔符:\ on Windows, and / on Linux/macOS。
通过 path.delimiter 可以获取当前系统的 path delimiter 路径定界符:; on Windows, and : on Linux / macOS。

例如在 Linux 下:

console.log(process.env.PATH);
// Prints: '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin'

process.env.PATH.split(path.delimiter);
// Returns: ['/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/bin']

在 Windows 下:

console.log(process.env.PATH);
// Prints: 'C:\Windows\system32;C:\Windows;C:\Program Files\node\'

process.env.PATH.split(path.delimiter);
// Returns ['C:\\Windows\\system32', 'C:\\Windows', 'C:\\Program Files\\node\\']

下面是 path 的常用 method 介绍。

path.basename()

返回路径上的最后一部分,通过第二个参数可以过滤掉扩展名:

console.log(path.basename('/home/marco/dir'))
console.log(path.basename('/home/marco/file.txt'))
console.log(path.basename('/home/marco/file.txt', '.txt'))

//output:
dir
file.txt
file

path.dirname()

返回路径上的文件夹部分:

console.log(path.dirname('/home/marco/file'))

//output:
/home/marco

path.extname()

返回文件的扩展名:

console.log(path.extname('/home/marco/file.txt'))

//output:
.txt

path.isAbsolute()

返回路径是否是绝对路径:

console.log(path.isAbsolute('/home/marco'))
console.log(path.isAbsolute('./marco'))

//output:
true
false

path.join()

将多个部分合并为一个路径:

const name = 'marco';
console.log(path.join('/home/' + name + '/file'))

//output:
/home/marco/file

path.normalize()

将一个包含相对标记符的路径转换为常规绝对路径:

console.log(path.normalize('/home/marco/../test'))

//output:
/home/test

path.parse()

将路径拆分为一个 object,包含下面的 properties:

  • root: 根目录
  • dir: 从根目录开始的文件夹部分
  • base: 文件名和扩展名
  • name: 文件名不包含扩展名
  • ext: 扩展名
console.log(path.parse('/home/marco/test/file.txt'))

//output:
{
  root: '/',
  dir: '/home/marco/test',
  base: 'file.txt',
  ext: '.txt',
  name: 'file'
}

path.relative()

需要传入两个路径作为参数,第一个路径作为 base 基础路径,返回第二个路径相对于第一个路径的相对路径:

console.log(path.relative('/home/marco', '/home/marco/test/file.txt'))

//output:
test/file.txt

path.resolve()

计算一个相对路径的绝对路径,基于当前程序执行的路径:

console.log(path.resolve('file.txt'))

//output:
/mnt/hgfs/Development/node.js/filesystem/file.txt

也可以设置一个 base 路径,将会附加到相对路径中:

console.log(path.resolve('tmp/test', 'file.txt'))

//output:
/mnt/hgfs/Development/node.js/filesystem/tmp/test/file.txt

如果第一个 base 路径是绝对路径,则系统会认为这就是个绝对路径:

console.log(path.resolve('/tmp/test', 'file.txt'))

//output:
/tmp/test/file.txt

注意 path 模块对路径的处理不会判断这个路径是否真实存在,它只会按照指令处理路径这个字符串数据。

]]>
0 https://blog.niekun.net/archives/2146.html#comments https://blog.niekun.net/feed/category/node/archives/2146.html
node.js 入门教程之六 -- FileSystem https://blog.niekun.net/archives/2140.html https://blog.niekun.net/archives/2140.html Sun, 07 Feb 2021 17:17:00 +0800 admin File descriptor 文件描述器

当需要访问文件系统里的某个文件时,需要首先得到这个文件的 file descriptor 文件描述器。

一个 file descriptor 就是通过 fs 模块的 open() method 打开对应文件的返回数据。它的结构如下:

const fs = require('fs')

fs.open('./test.txt', 'r', (err, data) => {
    if (err) {
        console.log('open fail')
        return
    }
    console.log('open success')
})

如果文件打开成功,callback 中的 data 数据就是一个 file descriptor。注意并不是文件的内容。

这种方式是异步处理的,文件打开过程中系统会执行其他任务。

在上面的示例中,fs.open() 的第三个参数使用了 'f' 标记,它的意思就是以只读模式打开文件。我们可以通过使用不同的标记来以不同的方式打开文件:

  • r 只读模式,文件不存在会报错
  • a 只写模式,streaming 流定位到文件结尾位置,如果文件不存在则创建文件
  • r+ 读写模式,文件不存在会报错
  • w+ 读写模式,streaming 流定位到文件起始位置,如果文件不存在则创建文件
  • a+ 读写模式,streaming 流定位到文件结尾位置,如果文件不存在则创建文件

更多 flag 的使用参考:https://nodejs.org/api/fs.html#fs_file_system_flags

也可以使用 fs.openSync() method 打开文件,其返回值为 file descriptor 而不是通过 callback 的方式:

try {
    const data = fs.openSync('./test.txt', 'r')
    console.log('open success')
} catch (error) {
    console.log('open fail')
}

这种方式是同步模式的,程序会等待接收到 file descriptor 或者 error 后才执行后面的指令。

File stats 文件信息

每个文件都包含独自的属性信息,可以通过 node.js 查看。通常我们使用 fs 模块的 stat() method。

以下是一个简单示例:

const fs = require('fs')

fs.stat('./test.txt', (err, stats) => {
    if (err) {
        console.log('read fail')
        return
    }
    console.log(stats)
})

同样的,node.js 也提供了同步模式 method,在读取 stats 过程中会阻塞进程知道读取结束:

try {
    const stats = fs.statSync('./test.txt')
    console.log(stats)
} catch (error) {
    console.log(error)
}

文件信息存储在 stats 变量中,可以读取其中的需要的信息,常用的有以下:

  • 是否是文件或文件夹:stats.isFile()stats.isDirectory()
  • 是否是一个链接文件:stats.isSymbolicLink()
  • 文件大小(单位为字节):stats.size

File Path 文件路径

每个文件在系统中都有一个路径。在 Linux 中路径格式如:/users/joe/file.txt,在 Windows 中路径格式如:C:\users\joe\file.txt。在程序中引用路径时需要特别注意路径的格式。

node.js 中可以使用 path 模块来处理文件路径相关数据。

如果给定一个路径,可以提取其相关数据,如:

  • path.dirname: 获取文件的上级文件夹路径
  • path.basename: 获取文件名部分
  • path.extname: 获取文件扩展名
const path = require('path')

const file = '/home/marco/file.txt'
console.log(path.dirname(file))
console.log(path.basename(file))
console.log(path.extname(file))

//output:
/home/marco
file.txt
.txt

可以获取文件名不包含扩展名部分,通过给 path.basename() 设置第二个参数:

console.log(path.basename(file, path.extname(file)))

//output:
file

可以组合多个部分为一个 path:

const dir = 'home/marco/'
p = path.join('/' + dir + 'test.txt')
console.log(p)

//output:
/home/marco/test.txt

使用 path.resolve() 可以从一个相对路径获取到绝对路径:

p = path.resolve('file.txt')
console.log(p)
p = path.resolve('./test/file.txt')
console.log(p)

//OUTPUT:
/mnt/hgfs/Development/node.js/filesystem/file.txt
/mnt/hgfs/Development/node.js/filesystem/test/file.txt

以上示例中,会将程序当前执行路径作为绝对路径添加给后面定义的文件相对路径。

也可以将文件的相对目录单独定义,会自动组合它们:

p = path.resolve('tmp', 'file.txt')
console.log(p)

//output:
/mnt/hgfs/Development/node.js/filesystem/tmp/file.txt

如果在路径前加斜杠/ 表明这就是一个绝对路径:

p = path.resolve('/tmp', 'file.txt')
console.log(p)

//output:
/tmp/file.txt

如果路径中包含相对关系标记符如: .., //等,可以使用 path.normalize() 得到常规形式的路径:

p = path.normalize('/home/../test/file.txt')
console.log(p)

//output:
/test/file.txt

path.resolve 和 path.normalize 都不会检查路径是否真实存在,它们仅仅是根据提供的数据计算路径结果。

reading file 读取文件

最简单的方式读取文件内容就是通过 fs.readFile() method,需要给它传入文件路径,编码格式,callback function:

const fs = require('fs')

fs.readFile('./test.txt', 'utf-8', (err, data) => {
    if (err) {
        console.log('read fail')
        return
    }
    console.log(data)
})

或者也可以使用同步模式的 fs.readFileSync()

try {
    data = fs.readFileSync('./test.txt', 'utf-8')
    console.log(data)
} catch (error) {
    console.log('read error')
}

fs.readFile()fs.readFileSync() 都会先将文件内容读取到内存中,然后返回数据。这就意味着读取大文件会影响系统内存的占用量,所以一个比较好的选择是使用 stream 流读取文件内容。后续章节会介绍 stream 模块。

writing file 文件写入

最简单的写入文件的方法就是使用 fs.writeFile() method。

示例如下:

const fs = require('fs')

const content = 'some new words'
fs.writeFile('./test.txt', content, err => {
    if (err) {
        console.log('write error')
        return
    }
    console.log('write success')
})

或者使用同步模式的版本 fs.writeFileSync()

try {
    fs.writeFileSync('./test.txt', content)
    console.log('write success')
} catch (error) {
    console.log('write error')
}

默认情况下,如果这个文件已经存在,API 会替换掉这个文件中已有的内容。我们可以通过定义 flag 来修改这个设置:

fs.writeFile('./test.txt', content, {flag: 'a+'}, err => {
    if (err) {
        console.log('write error')
        return
    }
    console.log('write success')
})

flag 的定义和上面的 fs.open 中定义的 flag 一样,可以参考设置。

append to a file 给文件添加内容

给已有文件附加内容更加方便的方法是使用 fs.appendFile()fs.appendFileSync(),例如:

const fs = require('fs')

content = '\nnew line\n'
fs.appendFile('./test.txt', content, err => {
    if (err) {
        console.log('write error')
        return
    }
    console.log('write success')
})

上面了方法都会在将数据完全写入文件后在执行 callback,这种情况下使用 stream 是一个更好的方法。

working with folder 文件夹操作

node.js 的 fs 模块提供了很多实用的 method 来对文件夹进行操作。

检查文件夹是否存在

使用 fs.access() 可以检查文件夹是否存在,以及 node.js 是否有访问它的权限:

const fs = require('fs')

fs.access('./tmp', err => {
    console.log(err ? 'not exist' : 'exist')
})

//output:
//not exist

创建新文件夹

使用 fs.mkdir()fs.mkdirSync() 创建新文件夹:

const fs = require('fs')

const fd = 'tmp'
try {
    if (!fs.existsSync(fd)) {
        fs.mkdirSync(fd)
        console.log('create success')
    }
} catch (error) {
    console.log('create fail')
}

上面使用了同步模式 method 创建文件夹,看起来更加直观。

read content of a directory 读取目录内容

使用 fs.readdir()fs.readdirSync() 可以读取某个目录中的内容,包括其中的文件和文件夹。返回值为一个数组:

const fs = require('fs')

const p = '/usr'
const d = fs.readdirSync(p)
console.log(d)

输出为:

[
  'bin',     'games',
  'include', 'lib',
  'lib32',   'lib64',
  'libexec', 'libx32',
  'local',   'sbin',
  'share',   'src'
]

通过 path 模块的 joinresolve method 可以输出每个元素的绝对路径,修改以上示例:

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

const p = '/usr'
const d = fs.readdirSync(p).map(f => {
    return path.resolve(p, f)
})
console.log(d)

输出如下:

[
  '/usr/bin',     '/usr/games',
  '/usr/include', '/usr/lib',
  '/usr/lib32',   '/usr/lib64',
  '/usr/libexec', '/usr/libx32',
  '/usr/local',   '/usr/sbin',
  '/usr/share',   '/usr/src'
]

如果只想获取目录下的文件而不包含文件夹,可以定义一个 filter 过滤元素:

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

const isFile = fileName => {
    return fs.statSync(fileName).isFile()
}
const p = '/usr'
const d = fs.readdirSync(p).map(f => {
    return path.resolve(p, f)
}).filter(isFile)

console.log(d)

rename folder 重命名文件夹

使用 fs.rename()fs.renameSync() 重命名文件夹。第一个参数定义当前路径,第二个参数定义新路径:

const fs = require('fs')

fs.rename('./tmp', './new', err => {
    if (err) {
        console.log('rename error')
        return
    }
    console.log('rename success')
})

remove folder 删除文件夹

使用 fs.rmdir()fs.rmdirSync() 可以删除文件夹。

但删除文件夹相比其他操作稍微复杂,用到的工具可能超出你需要的部分。所以最简单的方法就是使用第三方模块:fs-extra 来处理。它是 fs 的替代品,在 fs 模块的基础上提供了很多新功能。

使用 fs-extraremove() method 可以实现删除文件夹的功能:

const fs = require('fs-extra')

const p = './new'
fs.remove(p, err => {
    if (err) {
        console.log('remove error')
        return
    }
    console.log('remove success')
})

也可以使用 promise 模式:

fs.remove(p)
.then(() => console.log('remove success'))
.catch((err) => console.log(err))

还可以使用 async/await 模式:

const rm = async (p) => {
    if (!fs.existsSync(p)) {
        console.log('folder does not exist')
        return
    }
    try {
        await fs.remove(p)
        console.log('remove success')
    } catch (error) {
        console.log('remove error')
    }
}
rm(p)

下一章我们继续了解 fs 和 path 模块的详细内容。

]]>
0 https://blog.niekun.net/archives/2140.html#comments https://blog.niekun.net/feed/category/node/archives/2140.html
node.js 入门教程之五 -- HTTP request and response https://blog.niekun.net/archives/2137.html https://blog.niekun.net/archives/2137.html Sat, 06 Feb 2021 16:50:00 +0800 admin 搭建 http server

下面是一个 http server 的示例:

const http = require('http')
process.env.PORT = 3000
const port = process.env.PORT

const server = http.createServer((req, res) => {
    res.statusCode = 200
    res.setHeader('Content-Type', 'text/html')
    res.end('<h1>hello world</h1>')
})
server.listen(port, () => console.log(`server running at port ${port}`))

访问 http://localhost:3000 就会看到页面显示 hello world

下面我们简单解释下以上示例的执行过程:

  • 首先引入 http module
  • 设置环境变量 PORT 为 3000,然后赋值给 port 变量
  • 创建一个 server,其中含有一个 callback function,用户访问 server 时会被调用
  • 设置监听端口并启动 server,当 server 启动成功后,会执行 server.listen 内定义的 callback function

当 server 收到用户的请求时,会触发 request event 并提供了两个数据:一个 request(http.IncomingMessage object) 和一个 response(http.ServerResponse object)。

  • request 提供了 request 的详细信息,可以用来获取请求 hreaders 和请求数据。
  • response 用来操作将要发送给客户端的数据。

在上面的示例中,我们修改了 response 相关信息:

  • 设置 statusCode 为 200
  • 设置一个 header
  • 结束 response 并将响应内容作为数据传入 end()

发起 GET 请求

下面示例发起一个 GET 请求:

const https = require('https')
const options = {
    hostname: 'niekun.net',
    port: 443,
    path: '/',
    method: 'GET'
}

const req = https.request(options, res => {
    console.log(`statusCode is:${res.statusCode}`);
    res.on('data', d => {
        process.stdout.write(d)
    })
})

req.on('error', err => console.log(err))
req.end()

以上示例执行过程为:

  • 首先引入 https 模块。
  • 创建一个 options object 存储请求的信息。
  • 创建一个 https request,使用 options 作为请求信息,当请求成功后会执行后面定义的 callback function。
  • 定义一个 event handler 事件管理器来处理当 error 被触发时的响应。
  • 最后结束这个 request

发起 POST 请求

下面是一个 POST 请求示例:

const https = require('https')

const data = JSON.stringify({
    todo: 'go to sleep'
})

const options = {
    hostname: 'niekun.net',
    port: 443,
    path: '/',
    method: 'POST',
    Headers: {
        'Content-Type': 'application/json',
        'Content-Length': data.length
    }
}

const req = https.request(options, res => {
    console.log(`statusCode is:${res.statusCode}`);
    res.on('data', d => {
        process.stdout.write(d)
    })
})
req.on('error', err => console.log(err))

req.write(data)
req.end()

POST 请求相比 GET 请求多了 request.write(data) 部分,因为 POST 是把数据消息放在 body 中的。

PUT 和 DELETE 请求和 POST 格式相同,只需要修改 request.method 即可。

使用 Axios 库发起 POST 请求

上面介绍了是应用 https 模块来发起 POST 请求,我们也可以使用第三方库使代码更加简洁,这里介绍 axios 库实现(需要通过 npm 安装):

const axios = require('axios')

axios
.post('http://localhost:3000', {
    todo: 'go to sleep'
})
.then(res => {
    console.log(`statusCode is: ${res.status}`)
    process.stdout.write(res.data)
})
.catch(err => console.log(err))

获取 http request body 数据

下面介绍 server 端如何提取在 http request body 中的 json 数据。

如果你使用 Express 模块创建 http server,那么只需要使用它的 body-parser 内容拆分模块即可实现提取 json 数据,Express 模块在第一章做过示例,如果客户端使用上面的 axios 示例发起请求,服务端代码如下:

const express = require('express');
const app = express();

process.env.PORT = 3000
const port = process.env.PORT

app.use(
    express.urlencoded({
        extended: true
    })
)
app.use(express.json())

app.get('/', (req, res) => {
    res.statusCode = 200
    res.setHeader('Content-Type', 'text/html')
    res.end('<h1>hello world</h1>')
})

app.post('/', (req, res) => {
  console.log(req.body.todo)
  res.statusCode = 200
  res.setHeader('Content-Type', 'text/html')
  res.end('hello world\n')
});

const server = app.listen(port, () => console.log('Server ready'));

我们使用 express 模块创建了一个 server,并同时监听 GET 和 POST 请求并定义了 callback 响应,通过 app.use 设置了对请求 body 数据的 json 支持。

当启动上面实例中使用 axios 开启的服务端的程序后,server 端收到 POST 请求后就会提取请求 body 中的 todo 对应内容并输出到终端。客户端成功收到 response 后会输出 200 状态码和响应内容。

我们通过浏览器直接访问 localhost:3000 会发起一个 GET 请求,浏览器会显示 hello world。

如果你不想使用 express 模块创建 server,那么 server 端要提取 POST request body 中的 json 数据就稍微麻烦一些。

首先需要理解的是当我们通过 http.createServer() 创建了一个 http 服务后,callback function 会在所有的 request headers 内容都接收到后被调用,而不是 request body。

connection callback 中传递的完整的 request object 数据是在 stream 流中。所以我们必须监听传输的 request body 的内容,它们是在一些 chunks 数据块中的。

首先我们通过监听 data event 获取 body 数据,data event 在传输过程中会被多次触发,当 request 数据传输完毕后 end event 会被触发。

通过下面的代码可以获取到完整的 body 数据:

const server = http.createServer((req, res) => {
    let data = '';
    req.on('data', chunk => {
        data += chunk;
    })
    req.on('end', () => {
        console.log(JSON.parse(data).todo)
    })
})

以上代码中,data 最终存储了 request body 的数据,然后通过 JSON.parse method 可以解析 json 内容。

server 端完整的代码如下:

const http = require('http')
process.env.PORT = 3000
const port = process.env.PORT

const server = http.createServer((req, res) => {
    let data = '';
    req.on('data', chunk => {
        data += chunk;
    })
    req.on('end', () => {
        console.log(JSON.parse(data).todo)
    })

    res.statusCode = 200
    res.setHeader('Content-Type', 'text/html')
    res.end('<h1>hello world</h1>')
})
server.listen(port, () => console.log(`server running at port ${port}`))

以上就是 http request 和 response 部分的简单介绍。

]]>
0 https://blog.niekun.net/archives/2137.html#comments https://blog.niekun.net/feed/category/node/archives/2137.html