koa,node基础

Posted cuter、

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了koa,node基础相关的知识,希望对你有一定的参考价值。

Node.js基础

https://nodejs.org/dist/latest/docs/api/

Node.js是一个基于Chrome V8引擎的JS运行环境,它采用了单线程,事件驱动、非阻塞式的I/O模型,尤其适合构建I/O密集型的高性能服务端,如今整个前端生态的工具链都构建在Node.js的基础之上,是往全栈工程师进阶的必备技能,掌握Node.js之后你可以:

  • 编写高性能服务端程序
  • 开发命令行工具
  • 编写爬虫程序
  • 通过Electron之类的框架编写PC客户端程序

这节课我们主要学习使用Node.js进行文件操作和HTTP网络编程,这些内容需要了解一些计算机原理的底层知识。

需要注意,虽然都是JS代码,但是Node.js提供的API并不能用于浏览器环境,反过来也是一样,初学者很容易搞混他们的差别。

使用TypeScript编写Node.js程序

使用TS来编写Node.js程序的关键是安装对应的类型提示模块,我们首先通过 npm init 初始化一个空的npm工程,然后通过下面的命令来安装类型依赖

npm i @types/node --save-dev

@types/node 就是Node.js的类型提示模块,安装之后TS就可以识别Node.js中的API和数据类型了。然后我们需要创建一个 tsconfig.json 文件,用来告诉TS如何编译我们的代码,最关键的一个选项是将模块类型转换为 commonjs,如下是一个非常基本的配置选项。


  "compilerOptions": 
    "module": "commonjs",
    "target": "esnext",
    "skipLibCheck": true,
    "sourceMap": true,
    "outDir": "dist"
  ,
  "include": [
    "src/**/*"
  ]

因为历史原因,Node.js最早是支持 CommonJS 模块规范的,后来随着 ES Modules 的发展Node.js也提供了支持,但是目前两者的兼容还存在很多问题,为了更好的兼容性,我们建议目前还是转换成 CommonJS 模块来执行,当然,TS会帮我们自动完成这个工作,我们还是直接使用 ES Modules 即可。

完成上面的配置后,我们可以把TS源代码放在 src 目录中,通过执行 tsc 命令即可编译输出JS文件到 dist 目录。

Buffer

计算机中数据的存储和传输都是通过二进制数据的形式进行的,最小存储单位是字节 byte,而每个 byte 又由8个二进制位 bit 表示,也就是说每个字节 byte2^8=256 种可能的值(0~255)。Node.js中的 Buffer 表示的就是一段连续的二进制数据字节序列,Node.js中很多API都支持Buffer,我们最常用的就是使用Buffer进行编码的转换,比如我们读取的文本文件内容默认是Buffer类型,也就是一段二进制数据,然后我们可以使用Buffer的 toString 方法将其转换成 utf-8 编码,这样就可以拿到可读的文本内容了。

Buffer类位于全局作用域中,不需要通过 import 引入就可直接使用。

let str = '十'
let buf_1 = Buffer.from(str, 'utf-8')  // 将utf-8字符串转换为Buffer,汉字需要用多个字节表示
console.log(buf_1)  // <Buffer e5 8d 81>,utf-8编码的汉字“十”对应的三个字节,此处为十六进制表示
console.log(buf_1.byteLength, str.length) // buf_1的字节长度为3,但是字符串长度为1,如果使用utf-8编码保存“十”将会占用3个字节
let buf_2 = Buffer.from('a', 'utf-8') // 英文字母只占用一个字节
console.log(buf_2)  // <Buffer 61>,字母a对应的ASCII码为97,用十六进制表示就是61
let buf = Buffer.from([0xe5, 0x8d, 0x81]) // 直接使用字节数组创建Buffer
console.log(buf.toString('utf-8'))  // 十,将Buffer转换为utf-8编码字符串
console.log(buf.toString('base64')) // 5Y2B,将Buffer转换为Base64编码字符串
console.log(buf.toString('hex'))  // e58d81,将Buffer转换为十六进制编码字符串
let str = Buffer.from('5Y2B', 'base64').toString('utf-8') // 将Base64字符串转换为utf-8编码
console.log(str)  // 十
let buf_1 = Buffer.from([0xe5, 0x8d, 0x81]) // 对应utf-8编码的“十”
let buf_2 = Buffer.from([0xe4, 0xb8, 0x80]) // 对应utf-8编码的“一”
let buf = Buffer.concat([buf_1, buf_2]) // 合并多个Buffer片段
console.log(buf.toString('utf-8'))  // 十一

path

path模块提供了用来处理文件路径的实用工具,因为不同操作系统的路径规则不同,比如Windows上面路径采用 \\\\ 分隔,通常路径是这种格式:

D:\\\\www\\\\task\\\\index.html

但Linux采用 / 分隔,如:

/root/www/task/index.html

path 提供了一致的API,可以帮我们屏蔽这些差异,要使用 path 的API我们只需要这样引入即可:

import * as path from 'path'

basename

可以返回路径的最后一部分,用来获取文件名

import * as path from 'path'
path.basename('D:\\\\www\\\\task\\\\index.html')  // index.html

dirname

用来返回路径的目录名

import * as path from 'path'
path.dirname('D:\\\\www\\\\task\\\\index.html') // D:\\\\www\\\\task

extname

用来返回路径的扩展名

import * as path from 'path'
path.extname('D:\\\\www\\\\task\\\\index.html') // .html

resolve

用来将给定的路径片段转换为绝对路径

import * as path from 'path'
path.resolve('/index.html') // 如果当前工作目录位于D盘,则返回 D:\\\\index.html
path.resolve('index.html')  // 如果当前工作目录位于 D:\\\\www\\\\task,则返回 D:\\\\www\\\\task\\\\index.html

join

该方法可以合并多个路径片段,在Node.js的CommonJS模块代码中,有两个特殊的路径变量

  • __dirname:当前代码文件所在目录的绝对路径
  • __filename:当前代码文件的绝对路径
import * as path from 'path'
path.join('D:\\\\www', 'task', 'index.html')  // D:\\\\www\\\\task\\\\index.html
path.join(__dirname, 'test.js') // 假如当前文件为 D:\\\\www\\\\task\\\\index.js,则返回 D:\\\\www\\\\task\\\\test.js

fs

fs模块提供了操作本地文件的能力,是我们最常用的功能之一,fs中的绝大多数API都有三种版本,分别为

异步非阻塞版本

这种版本的API异步非阻塞,在执行的时候不会阻塞线程,但是需要通过回调函数才能拿到返回结果,例如

import * as fs from 'fs'

fs.readFile('data.txt', 'utf-8', (err, data) => 
  if (err) 
    console.error('文件读取失败')
   else 
    console.log(data)
  
)

同步阻塞版本

这种版本的API在执行的时候会阻塞线程,需要等它执行完才能处理其他任务,所以一般在服务端开发中我们都会禁止使用这种同步IO的API,同步版本的API名字和异步版本的类似,会在末尾加上 Sync,如

import * as fs from 'fs'

try 
  let data = fs.readFileSync('data.txt', 'utf-8')
  console.log(data)
 catch (err) 
  console.error('文件读取失败')

Promise版本

Promise版本的API是Node.js新加入的特性,它相当于是对异步非阻塞版本API的Promise化,配合async/await,既可以让我们以串行的方式写代码,又可以避免IO阻塞线程,通过 fs.promises 可以访问到这些API

async function run() 
  try 
    let data = await fs.promises.readFile('data.txt', 'uttf-8')
    console.log(data)
   catch (err) 
    console.error('文件读取失败')
  

需要注意,在实际项目中文件操作时最好传入文件的绝对路径,如果传入的是相对路径,那么得到的是相对于当前工作目录的路径,而不是相对于代码文件的路径,这个和模块加载的相对路径规则不同。

假如有代码 D:\\\\www\\\\node\\\\src\\\\fs.js,内容如下

import * as fs from 'fs'

let data = fs.readFileSync('../data/news.json', 'utf-8')
console.log(data)

我们在读取文件的时候传入了一个相对路径,本来的期望是读取 D:\\\\www\\\\node\\\\data\\\\news.json 这个文件,但是这个相对路径是相对于我们的工作目录的,而不是代码文件自身,所以如果我们在 D:\\\\www\\\\node\\\\src\\\\ 目录下面执行 node fs.js 那么得到的将会是正确的结果,如果我们在 D:\\\\www\\\\node\\\\ 目录下面执行,则文件路径变成了 D:\\\\www\\\\data\\\\news.json,读取出错,所以一定要使用绝对路径来读取,这样就不会受工作目录的影响,如

import * as fs from 'fs'
import * as path from 'path'

let data = fs.readFileSync(path.join(__dirname, '../data/news.json'), 'utf-8')
console.log(data)

通过 path.join__dirname 和目标文件的相对路径连接起来就可以得到目标文件的绝对路径了,同样的道理,其他的文件操作API也需要用这种方式

readFile

该方法可以读取整个文件的内容到内存中,通常用来直接处理一些比较小的文本文件,具体用法参考上面的例子

writeFile

该方法可以将数据写入到磁盘文件,写入的内容可以是字符串文本,也可以是Buffer二进制序列

import * as fs from 'fs'

fs.writeFile('./data.txt', 'hello', err => 
  if (err) 
    console.error('文件写入失败')
   else 
    console.log('文件写入成功')
  
)

stat

该方法可以用来获取文件的一些属性

import * as fs from 'fs'

let stats = fs.statSync(file)
console.log(stats.size) // 文件的字节大小
console.log(stats.mtimeMs)  // 文件的修改时间戳
console.log(stats.isDirectory())  // 是否为目录

readdir

该方法可以读取指定目录下的所有文件和目录名

import * as fs from 'fs'

let dirs = fs.readdirSync('./')
console.log(dirs) // 当前工作目录下面所有文件和目录名,注意不是绝对路径

events

Node.js的一大特点就是事件驱动,事件的发布订阅也是经典的设计模式,通过events模块中的 EventEmitter 我们可以轻松实现这个功能,Node.js中的很多对象也都是 EventEmitter 的示例

import  EventEmitter  from 'events'

// 继承自EventEmitter
class EventBus extends EventEmitter 

let eventBus = new EventBus()

// 监听custom事件
eventBus.on('custom', args => 
  console.log(args)
)

// 触发custom事件
eventBus.emit('custom', 'custom event data')

stream

stream(流)是Node.js中处理流式数据的抽象接口,Node.js中有很多流对象,最常见的比如文件读写流,HTTP请求、响应流等等,stream是Node.js中IO的精髓,通常用来处理大规模的流式数据。

让我们来看这样一个场景,假如我们通过Node.js创建了一个HTTP服务,用户可以通过这个服务下载服务器上的文件,你可能会使用前面讲到的 fs.readFile 来读取整个文件,然后将数据写入到HTTP返回流中。对于一些小文件这样做可能没有太大的问题,但是如果用户要下载的是一个超大的文件,这就会带来一些严重问题:

  • 直接读取整个文件会占用很多内存
  • 如果用户的带宽很小,被写入到HTTP返回流中的数据需要很长时间才能被读取完,没有被读取的数据会一直缓冲在服务器内存中
  • 如果很多用户同时来下载,服务器内存很快就会被耗尽

如果使用stream则会是这样的流程:

  • 创建一个文件读取流,开辟一块固定的内存缓冲区(比如64KB),读取文件内容填充缓冲区,等待数据被消费
  • 将缓冲区的数据写入HTTP返回流,缓冲区的数据被读取完毕之后,继续读取文件数据填充缓冲区
  • 重复前面的流程直到文件传输完毕

这样一来我们就不需要担心内存耗尽的问题了,因为stream只会占用缓冲区的内存大小。

stream也有多种类型,我们最常见的主要是

  • readableStream:可读取数据的流,比如文件读取流、HTTP请求流
  • writableStream:可写数据的流,比如文件写入流、HTTP返回流

stream还提供了管道(pipe)API,可以将一个读取流和一个写入流连接起来,这样它就会自动完成前面stream读取的控制过程,非常方便,示例如下,通过stream接口来实现大文件的复制

let readable = fs.createReadStream('./data.bin')
let writable = fs.createWriteStream('./target.bin')
readable.pipe(writable)
// 监听写入流的finish事件
writable.on('finish', () => 
  console.log('文件写入完毕')
)

http

http模块提供了创建http服务端和客户端的能力,http提供的API是比较底层的,它只进行流处理和消息解析。

server

通过http模块创建一个服务端是非常简单的

import * as http from 'http'

const server = http.createServer((req, res) => 
  res.end('hello')
)

server.listen(3000)

通过上面的代码,Node.js会创建一个http服务,并且监听3000端口,当我们在浏览器访问 http://localhost:3000/ 时,可以看到服务端返回文字 hello

createServer 后面是一个回调函数,每次有新的请求到达就会触发该函数的调用,其中 req 是Node.js解析的请求对象,它是一个可读流,里面包含了请求头信息、请求体数据流等,res 是返回对象,它是一个可写流,可以用来设定返回状态码、响应头,也可以往返回流里面写入数据。

createServer 创建的服务端功能比较基础,我们需要自己去实现路由、body的解析等功能,所以在实际开发中我们通常会使用一些框架,它们封装了更强大的功能,开发起来更方便,后面我们会介绍Koa框架。

client

同服务端一样,http提供的客户端功能也比较底层,这里我们推荐一个官方新推出的库 undicihttps://undici.nodejs.org/

它的用法也非常的简单,参考我们的示例代码。

MongoDB

MongoDB是一种文档数据库,他和传统的SQL数据库不同,MongoDB没有表结构,每个集合里面保存的是一条一条的BSON数据,这是一种类似于JSON的数据结构,但是类型比JSON更丰富。MongoDB非常灵活,BSON的数据结构又和Node.js天然接近,两者结合开发效率非常的高。

MongoDB 包含了多个工具,其中最核心的是

  • mongod:MongoDB的服务端程序,通过它可以启动MongoDB的的服务实例
  • mongo:MongoDB的客户端程序,通过它可以连接MongoDB服务,执行命令进行查询或者数据的修改

MongoDB的存储结构可以分为库 -> 集合 -> 文档,我们来看一些基本操作

列出所有数据库

show databases;

切换数据库

use testdb;

查看当前数据库所有集合

show collections;

插入数据

db.users.insertOne(name: 'Tom', age: 24)

当我们往数据库中插入数据时,如果目标数据库或者集合不存在的话,MongoDB会自动创建。默认情况下MongoDB会给插入的每一条记录创建一个类型为 ObjectId 的属性 _id,它是全局唯一的,所以上面插入的数据在MongoBD中会是这个样子:


  "_id": ObjectId("603cee6abd814d23c09912f5"),
  "name": "Tom",
  "age": 24

查询数据

db.users.find()
db.users.findOne(_id: ObjectId("603cee6abd814d23c09912f5"))

更新数据

db.users.update(_id: ObjectId("603cee6abd814d23c09917f5"), $set:  age: NumberInt(18) )

这里需要特别注意,update默认会替换掉整个文档,如果只想修改部分属性,需要使用 $set

删除数据

db.users.remove(_id: ObjectId("603cee6abd814d23c09917f5"))

Koa

以上是关于koa,node基础的主要内容,如果未能解决你的问题,请参考以下文章

node.js使用Koa搭建基础项目

Node.js 蚕食计划—— Koa 基础项目搭建

koa,node基础

koa,node基础

koa,node基础

koa2 从入门到进阶之路