Skip to content

NodeJS 基础

统计信息:字数 21349 阅读43分钟

尚硅谷视频是2018年的,基于 nodeJS 6 版本(两天课程)

2021-10-31——2021-11-05学习,nodeJS 主要是12-16版本,可能部分功能不一样,最后以官网为准。

01.Window 命令行

cmd 介绍(终端 shell)

Windows 目录系统介绍(注意是反斜杠)

C:\Users\Michael

常用指令,和 linux 相同的指令(cd ),不一样的如下

dir === ls 
md === mkdir
rd === rm
打开其他盘,直接 D:
直接输入文件名 hello.txt 就用默认程序可以打开文件

环境变量:path:我们打开一个文件或者程序,需要先进入指定的盘和文件夹内部,很麻烦。我们希望在全局的环境中可以直接打开这个文件或者程序。那么就设置环境变量,相当于把这个文件的链接,设置在全局环境中。例如:

path: c:\Program\cmd.exe;d:\Program/java.exe;c:\User\Desktop\test.txt

操作:计算机属性-高级系统设置-环境变量-用户环境变量-更改 path 的属性(格式是分号隔开的路径)-保存

改完环境变量,需要重启 CMD 窗口。当我们在 cmd 输入命令时,首先在当前目录下找这个文件。如果找不到,在环境变量中找文件。如果还找不到就报错。

我们可以将经常访问的程序,添加到 path,就可以在任意位置访问了

02.进程和线程

进程:计算机中独立的运行环境;不同进程使用不同的内存空间;进程为程序的运行提供环境(可能进程内部没有程序运行,cpu 是0,进程挂起);相当于车间

线程:计算机中最小的计算单位;负责执行进程中的程序;相当于车间中的工人

单线程:一个任务由一个人完成(一个CPU)JS

多线程:一个任务由多个人完成(多个CPU)Java 等语言

为什么浏览器 JS 是单线程?因为浏览器渲染引擎和 JS V8 引擎可能同时修改页面(DOM或者CSS,多线程会混乱)所以是单线程执行

03.node简介

JS 操作浏览器;NodeJS 后端服务器和系统交互

NodeJS 是能够在服务器端运行JS的运行环境。使用 V8 引擎开发,事件驱动(异步操作);非阻塞,异步 I/O模式提高性能。

NodeJS 发展史(了解):创建者 Ryan Dahl 是纽约罗彻斯特大学数学系博士,2006年退学后到智力去旅行,然后做码农赚钱;此时 Ruby on Rails 很火,但是有性能问题(所以很多和 Ruby Rails 类似)

node 版本(了解):从 0.x 直接跳到 4.0 版本;大版本中,偶数版本是稳定版,奇数版本是开发版(所以现在从 16 升级到18版本,17版本是测试开发的)

网络请求步骤:用户发出请求,服务器接受请求,服务器查询数据库并获取数据,服务器发出响应。后端可以提高服务端的代码,可以提高服务器的带宽,但是和数据库 i/o 的速度是有限的,高并发会阻塞。

传统服务器是多线程的,每一个请求发出,会开辟一个线程,然后请求数据库(磁盘),同时这个线程保留;磁盘I/O后,这个线程会发出响应,客户端收到。当高并发时,服务器开启多个线程,但是 i/o 时间较慢,会造成多线程阻塞,服务器性能下降。

nodeJS 是单线程,但是在后台拥有一个 I/O 线程池,没有上述多线程阻塞的问题,最初为了解决服务器端多线程i/o阻塞问题(webJS);最后功能越来越多,改名为 nodeJS(node节点可以不断扩展)。

因为是单线程,性能还也有局限性;现在高并发处理还是 Java 等语言,除非 NodeJS 多服务器分布式(nodeJS服务器成本比较低,所以可以做多个服务器)。现在在服务器端的应用(例如淘宝),主要做中间层渲染HTML页面 SSR(JAVA后端实现数据库交互)。nodeJS 作者也提到 Go 语言服务器的性能更高。

主要用途:web Rest-API;多人游戏;多客户端及时通信(socket-io)跨域;服务端请求等

04.使用node执行js文件

可以在 cmd 中输入 node 打开交互式窗口,然后输入代码(注:测试可以输入 es6 的代码)

可以在本地编写好 test.js 然后运行 node test.js 执行对应的代码(类似 Python 执行)通常采用后一种执行方法

05.node整合IDE

在 VSCode 中,编写好 JS 代码,直接点击左侧的运行按钮,选择 nodeJS 即可运行。其他 IDE 基本类似,设置不同的运行程序即可。

06.模块化简介

模块化有助于后期维护,代码复用等;模块化对于后期的维护比较好;

模块化的问题:模块化有顺序(可能引入模块的先后和模块有关;某些插件必须后面引入)

ECMAScript 的缺陷:没有模块系统;没有官方的规范(缺乏官方管理系统);标准库较少(jquery react 都不是官方的库);没有标准接口。

commonJS:为了让代码在各个平台上都能正常运行,所以制定了 commonjs 规范(cjs)。主要包括:模块引用(require('./math.js'));模块定义(单独的JS文件,exports 导出内部变量,exports.x = 100);模块标识。

每一个模块有一个自己的作用域(类似函数作用域);在模块内部定义的变量,不会在全局环境中访问到;所以希望暴露的函数必须 exports 出去。

07.模块化详解

nodeJS 中通过 require 导入外部的模块;可以传递一个文件的路径(相对路径必须以 . 或者 .. 开头)作为参数,node 会自动导入外部模块。require 引入模块后,函数返回一个对象(就是引入的模块)。

模块标识:就是如何引入模块。可以分为 NodeJS 引擎原生的核心模块(fs, os)或者文件中自己定义的模块。原生的模块直接引入即可;文件模块需要加上相对路径。

全局对象:浏览器中是 window,NodeJS 是 global。全局创建的变量和函数都挂载在 global 上。nodejs 最好是模块化,避免全局中挂载。

执行模块时,实际上就是把模块包裹成一个函数,那么就有了函数的特性。

console.log(arguments); // 当前模块包裹的函数的参数伪数组
console.log(arguments.callee) // 当前执行的函数对象
console.log(arguments.callee + '') // 当前执行的函数对象(转换成字符串)

实际上的函数

function (exports, require, module, __filename, __dirname) {
  // 文件内部代码
}
// 五个实参含义
exports // 导出的参数 exports.x = 10;
require // 导入的模块
module // 当前的模块,exports 是 module 的属性 exports === module.exports
__filename // 文件的绝对路径
__dirname // 文件所在目录的绝对路径

08.exports 和 module.exports

通过上面的模块化,Exports 是 module 的一个属性,exports === module.exports

因为模块是对象,属于引用类型,那么直接更改某一个值是相同的,下面等价

exports.x = 10;
module.exports.x = 10;

但是不能整体替换,下面的不相等,第一个导出无效

exports = { x: 10 }
module.exports = { x: 10 }

exports 只能使用点语法暴露内部变量

module.exports 可以通过点语法,或者直接输出对象,所以用这个就行。

module.exports = {
  name: 10,
  age: 20,
  fn: function() {
    console.log('hi');
  }
};

09.包简介

包 commonJS 规定我们可以将一组模块放在一起,形成一组工具(实际就是一个文件夹)

组成:包结构(各种文件)+包描述文件(说明书,package.json)

常见文件:bin lib src doc test

Package.json 中常见属性(不赘述)name describe version dependencies

10.npm简介

node package manager 包管理工具

npm search lodash
npm remove lodash
npm install lodash --save
npm install less-loader --save-dev
npm install vue-cli -G

11.配置cnpm

https://npmmirror.com/

国内访问 npm 比较慢,所以可以同时安装 cnpm,和 npm 不冲突,不同项目使用不同的安装方式(使用时用 cnpm)更新发布版本时使用 npm

12.node搜索包的流程

默认会在当前目录下面找 node_modules 寻找需要的包;如果没有找到,那么会一直递归到根目录寻找这个包。


前面的是准备工作,后面正式 nodeJS 介绍(一节课很少的内容)

13.Buffer缓冲区

JS 原生数组性能较差(实际是对象索引存储,不是连续的内存空间);传统数组不能存储多媒体文件(二进制文件)所以引入了 buffer

Buffer 类似数组,每一个项是16进制的两位数,表示内存中一个字节。Buffer 通过底层 C++ 申请连续内存空间。

var buf = Buffer.from('string');
// 把字符串转换成二进制(unicode)
// 查看占用的内存空间(如果是英文字符,一个字符占用一个字节)
// 如果是中文汉字,一个字符占用三个字节
// 每一个字节的两位数是十六进制的 00 - ff 对应二进制的 00000000 11111111 (0-255)

buf.length; 表示占用内存的大小

str.length 表示字符串的长度

其他的属性和方法在官网上都有

Buffer.alloc 创建一个指定长度的二进制流(并初始化为00)

Buffer.allocUnsafe 创建一个指定长度的不安全的二进制流(不初始化00)使用已有内存——可能有以前的敏感数据。不初始化的过程性能更好。推荐使用 Buffer.alloc 方法。

可以通过下标改变 buffer 的数据(不能超过256,超过后存储的不正确)

buf[0] = 10;

buf[1] = 0xaa;

超过下标长度改变二进制,不会报错,但是不会写入

console.log(buf[1]) 在控制台输出的数值都是10进制数 170,或者转换成16进制的字符串输出。console.log(buf[1].toString(16))

Buffer 创建后,是连续的内存空间,长度不能变化(JS 中的 array 长度可以变化)如果长度可以变化,那么对应的内存就是不连续的,性能不好(可能需要指针指向新的内存地址)

buf.toString() 即可转换成字符串

其他的 API 见官网文档。实际上我们很少直接使用 buffer,主要了解创建的原理。

14.同步文件写入

fs——file system

服务器的本质就是把服务器的代码发送给浏览器,所以文件操作是必须的

文件的操作都分成两种方式:同步或者异步。

同步操作会阻塞 JS 的执行,需要当前操作执行后,再执行下面的代码。

异步操作需要传入一个 cb 函数,执行完成后,调用回调函数。

同步方法带一个 sync,异步没有带。

下面是简单的同步的例子(实际上就是三步:打开文件、写入文件、关闭文件(释放内存))

var fs = require('fs');
var fd = fs.openSync('test.md', "w");
fs.writeSync(fd, "hello world");
fs.closeSync(fd);

API

fs.openSync(path, flags, mode)

path 是打开文件的路径(相对或者绝对路径)
Flags 表示打开的模式(r, w)字符串
mode 设置文件的操作权限(通常不传参)
返回值是一个数字表示(fd)类似于定时器的返回值,便于操作打开的文件
fs.writeSync(fd, content, [start-position], [encoding-type])

fd 是操作的文件(数字)
content 是写入的字符串
start-position 是写入的起始位置(这个位置前面的写入0)
encoding 是编码(默认是 utf-8)

15.异步的文件写入

同步代码会阻塞后面的程序执行。如果出错,不好排查处理(阻止后面程序执行),可读性较好

异步代码不会阻塞后面程序执行,出错都在回调函数中处理,nodejs 基本是异步的。

var fs = require('fs');

fs.open("hello.md", "w", function(err, fd) {
  if (err) {
    console.err(err);
  } else {
    fs.write(fd, 'this is content', function(err) {
      if (err) {
        console.log(err);
      } else {
        fs.close(fd, function(err) {
          if (err) console.log(err);
        })
      }
    })
  }
})

16.简单文件写入

上面写入和读取文件的操作需要三步:打开,写入,关闭,操作复杂。我们可以一步写入:

fs.writeFile(file, data, [options], cb);
fs.writeFileSync(file, data, [options]);
  • file 表示写入文件的路径 string/buffer,可以是绝对路径或者相对路径

  • data 是写入的数据(字符串或者流) string/buffer/uint8Array

  • Options 是写入的设置 object: encoding: 'utf-8', mode: '0o666', flag: 'w'

  • encoding: 如果data是 buffer,那么格式只能是 utf-8 忽略选项
  • flag 表示打开文件的模式:r 表示只读,w 表示写入,a 表示追加(不存在会创建文件)其他模式使用较少。fs.writeFile 如果是 'w' 就会覆盖原始的文件!!!
  • cb 是回调函数,只有一个参数 err
var fs = require('fs');
fs.writeFile('test.md', '这是写入的内容', { flag: 'w' }, function(err) {
  if (err) console.log(err);
});

17.流式文件写入

如果是大的多媒体文件(流式文件)一次性读取写入,可能速度比较慢,内存溢出。

同步写入,异步写入,简单写入都不适合。

所以有专门的方法处理流式文件(创建一个管道)

fs.createReadStream(path, [options]);
fs.createWriteStream(path, [options]);

创建写入流后,可以多次写入文件

var fs = require('fs');

var ws = fs.createWriteStream('test.md');
ws.write('test');
ws.write('a');

// 流对象可以添加 open close 事件,监听流的打开和关闭
// on 表示事件触发多次回调函数
// once 表示事件触发一次回调函数(触发后销毁,性能更高)

ws.once('open', function() {
  console.log('stream is open');
});

ws.once('close', function() {
  console.log('stream is close');
});

// 关闭流(不能使用 ws.close 关闭)
ws.end();
// end 是发送端全部执行后才关闭,close 是接收端直接关闭,可能还有没有发送的文件

on 是绑定长期的事件

once 是绑定一次执行的事件(打开文件)

18.简单文件读取

读取文件四种方法:同步文件读取,异步文件读取,简单文件读取,流式文件读取

fs.readFile(path, options, cb)

fs.readFile('./test.md', {flag: 'r'}, (err, data) => {
  if (err) console.log(err);
  console.log(data);
  // 这里默认读取的结果都是 Buffer(如果需要转换成 string,手动转换)
  // 因为很多文件都是流模式,所以需要使用流
});

19.流式文件读取

var fs = require('fs');

var rs = fs.createReadStream('test.png');
// 我们还需要一个可写流来接受读取到的文件内容
var ws = fs.createWriteStream('new.png');

rs.once('open', function() {
  console.log('open read stream');
});

rs.once('close', function() {
  console.log('close read stream');
  // 数据读取完毕,关闭可写流
  ws.end();
});

ws.once('open', () => {
  console.log('write stream open');
});

ws.once('close', () => {
  console.log('write stream close');
});

// 如果需要读取可读流中的数据,需要给可读流绑定一个 data 事件,就开始自动读取数据
// 每一次最大读到 65532 个字节的长度,然后开始下一个读取
rs.on('data', function (data) {
  console.log(data.length);
});

上面这样写太麻烦,需要监听不同的事件,其实可以使用简单的方法

var fs = require('fs');

var rs = fs.createReadStream();
var ws = fs.createWriteStream();
rs.pipe(ws);

直接将可读流和可写流连接起来,即可自动读写

问题:如果读取多个文件,然后写入一个文件,怎么处理呢?

var fs = require('fs');

var rs1 = fs.createReadStream('./1.md');
var rs2 = fs.createReadStream('./2.md');
var ws = fs.createWriteStream('./3.md');

// 创建多个可读流,然后分别写入目标文件(如果是小文件写入正常,多媒体文件没有测试)
// 因为多媒体文件的头信息不一样,理论上不能直接拼接(可以试试拼接不同的ts流文件)
rs1.pipe(ws);
rs2.pipe(ws);

可以尝试写成循环实现拼接

var fs = require('fs');
var ws = fs.createWriteStream('./result.mp4');
// 创建多个可读流,然后分别写入目标文件(如果是小文件写入正常,多媒体文件没有测试)
// 因为多媒体文件的头信息不一样,理论上不能直接拼接(可以试试拼接不同的ts流文件)
var count = 10;
for (let i = 0; i < count; i++) {
  var tmp = fs.createReadStream(`${i}.ts`);
  tmp.pipe(ws);
}

20.fs模块的其他方法

对性能有要求的时候,使用异步;如果没有要求,使用同步即可;具体看文件大小。

existsSync 验证路径

验证路径是否存在(异步方法废弃了)

fs.existsSync(path);

var isExists = fs.existsSYnc('test.mp3');

Stat 获取文件信息

fs.stat(path, cb)
fs.statSync(path)

fs.stat("test.ts", function(err, stat) {
  if (err) console.log(err);
  console.log(stat);
});

其中返回值 stat 是一个对象,拥有下面的属性和方法

stats.isFile()
stats.isDirectory()
stats.isBlockDevice()
stats.isSocket()

stats.mode
stats.ino

(取消文件和磁盘的连接)

fs.unlink(path, cb)
fs.unlinkSync(path)

readdir 列出文件

fs.readdir(path, [options], cb)
fs.readdirSync(path, [options])

fs.readdir(path, { encoding: 'utf-8' }, (err, files) => {
  console.log(files);
  // files 是字符串数组,每一项是文件或者文件夹的名字
});

truncate 截断文件

将文件修改成指定字节的大小(从第一个到 len 个)

fs.truncate(path, len, cb);
fs.truncateSync(path, len);

// 注意:len 是字节,截取前 len 个字节;如果截断中文,最后一个汉字可能截取到一半

mkdir 创建目录

fs.mkdir(path, [mode], cb);
fs.mkdirSync(path, [mode]);

fs.mkdirSync('test');

rmdir 删除目录

fs.rmdir(path, cb)
fs.rmdirSync(path)

Rename 重命名文件或者目录

fs.rename(oldPath, newPath, cb)
fs.renameSync(oldPath, newPath)

Watch 监控文件的更改写入

(会轮询,默认5S,消耗性能)

fs.watchFile(filename, [options], listener)

options: {
  persistent: true, // 当文件正在被监视时,进程是否继续运行
  interval: 5007 // 每隔多少毫秒轮询一次
}
listener: 当文件发生变化时回调函数会执行

fs.watchFile('./test.md', { interval: 1000 }, (curr, prev) => {
  // curr 和 prev 也是 stat 对象
  console.log(curr.mtime - prev.mtime);
  console.log(curr.size - prev.size);
});

其他方法

fs.chmod(path, mode, callback)
fa.chmodSync(path, mode)
fs.appendFile(file, data, [options], callback)
fs.appendFileSync(file, data, [options])

其他资料

黑马程序员 nodeJS 第八天

2015年9月10日的课程(6年了!)注意时效性

nodeJS 是6年前的版本,差距太大了;最好学习最新的版本

01 nodeJS 简介

创建者和创建原因已经学过了

  • 语言:nodeJS 不是独立的语言,使用 JS 进行编程

  • 架构:传统的后台架构是 LAMP:linux+Apache+mysql+php, nodeJS 不需要使用服务器软件上,与经典的架构不同。nodeJS 没有 web 容器。

02 nodeJS 特点

nodeJS uses an event-driven, no-blocking I/O model that makes it lightweight and efficient

单线程+非阻塞 I/O+事件驱动

传统服务器端语言中,多线程连接用户,每一个用户消耗 2MB 内存。8GB的服务器可以连接 4000个用户。当用户增加时,硬件成本上升。

nodeJS 只使用一个线程,多用户访问时,通过内部事件调度,在宏观上也是并行的。8GB的服务器可以用时处理超过4万的用户的连接。好处:操作系统不需要创建线程或者销毁线程的时间;缺点,一旦一个用户造成这个线程奔溃,整个服务就奔溃了,那么全部用户无法链接。

nodeJS 执行 JS 中,默认不会热更新程序。更改 JS 后,需要把服务器暂停,然后再次运行,才能显示更改后 的内容

03 nodeJS 原理

底层代码中,半数的代码是事件队列,回调函数队列的构建,用事件驱动完成任务调度。

单线程:减小内存开销;操作系统的内存换页。

非阻塞IO:不会等待IO执行结束,会执行后面的JS语句。

事件循环(event-loop)事件方式加入事件队列,等待执行。

nodeJS 不适合开发运算密集的服务,主要适合处理大量并发的IO。NodeJS 适合于 web-socket 配合,开发长连接的实时交互APP(用户表单收集;聊天室;直播室等)。

当时 NodeJS 还是一个小玩具,性能计算和传统的 Java 后端语言等没办法比。

国内:小公司使用Node做核心业务,大公司使用Node做一部分功能。

现在项目中,使用 Node 处理用户操作的同步(server),用 Python 处理表单、用户管理、大部分的 HTTP 请求等;用 Go 和 C 进行底层计算,文件存储。


Last update: November 9, 2024