Node.js基础

956 阅读16分钟

Node.js 介绍

是什么

  • Node.js = ECMAScript + 各种基于 ES 的模块(net、http、fs)
  • Node.js 脱离了浏览器,可以操作网络、文件等

用途

  • 服务端开发(通过网络提供各种数据)
  • 工具开发:vue-cli / create-react-app
  • 桌面端(electorn, nw.js):vsCode

安装Node.js

  • Node.js官网下载稳定版本,左边为稳定版本,右边为非稳定版本
  • 安装完Node.js会自动安装NPM(Node Package Manager) - 包管理工具
  • 通过指令 node -v 来查看是否安装完成和查看node版本号;npm -v 来查看npm版本

使用Node.js实现第一个服务器

  • 初步感受一下用 Node.js 写一个最简单的服务器
// 引入http模块
const http = require('http');
// 创建一个服务器
const server = http.createServer((req, res) => {
    res.end('hello world');
});
// 设置端口号
server.listen(8080)
  • 通过命令行启用服务器,浏览器输入127.0.0.1:8080访问
node app.js
  • 就是这么简单

Node.js模块化 (CommonJS)

  • 在 Node.js 推出的时候 JavaScript 还没有自己的模块的功能,到服务端后,面对文件系统、网络、操作系统等等的业务场景,使得极其简单的代码组织规范不足以驾驭如此庞大规模的代码,CommonJS 模块化规范应运而生
  • Node.js 应用由模块组成,每个文件就是一个模块,有自己的作用域,里面定义的变量、函数、类,都是私有的
  • 实际上熟悉 ES Module 的话,CommonJS 规范很容易上手

自定义模块&导出

  • ./moduleA.js
// 使用 module.exports 导出模块
module.exports = '我是模块A';
// 如有多个导出需求
module.exports = {
    str: '我是模块A',
    hobby() {
        console.log('唱、跳、rap、music');
    }
}
// 或者还能写成
module.exports.music = () => {
    console.log('鸡你太美');
}
  • 自定义模块呢?这一个 JS 文件,就是一个模块

按需导出

  • 引入文件夹形式模块
  • .js 可省略,如果是文件夹 默认寻找 index.js
// 通过require来引入
// 引入 './moduleA/a.js' 模块
const { str, hobby } = require('./moduleA/a');
// 引入 './moduleA/index.js' 模块
const music = require('./moduleA').music;

console.log(str);
hobby();
music();
  • 我们也可以配置默认启动文件夹,在文件夹内新建 package.json 来指定执行文件
{
    "name": "module-a",
    // 版本号
    "version": "1.0.0",
    // 指定执行文件
    "main": "main.js"
}

node_modules

  • node_modules 是 CommonJS 主要用于管理第三方模块的文件,当然你也可以在里面写自定义模块
  • 一般我们会把 第三方模块 放在 node_modules 内,自定义模块由另一个文件夹管理,这样我们就可以更有条理,很清晰知道哪些模块是第三方的,哪些模块是自己写的
  • 在 node_modules 内的模块由主入口 index.js 进入,同样的你也可以通过 package.json 指定入口
  • node_modules 内的模块在导入的时候,不需要写路径,直接写 文件夹名(模块名称),由 入口文件 进入

node_modules/myModule/index.js

module.exports = {
    name: 'my name is myModule'
}

app.js

// node_modules 内的模块,只需写模块名即可
const myModule = require('myModule');
console.log(myModule.name);

内置模块

  • 在 最简单的服务器 的示例中,我们使用了 http 模块,但是我们发现我们并没有定义模块,也没有导入第三方模块,他是从哪里来的呢?
  • 为了实现一些文件操作,网络系统等操作,这些都是原本 JavaScript 不能实现的,由 Node.js 提供,我们也可以称他们为内置模块或者是官方模块,我们不需要进行下载,也不需要自定义,他已经集成在 Node.js 内
  • 我们也可以理解为这些模块是随着我们安装 Node.js 的时候一并安装进来的
  • Node.js 内置模块 - Buffer,C/C++Addons,Child Processes,Cluster,Console,Crypto,Debugger,DNS,Domain,Errors,Events,File System,Globals,HTTP,HTTPS,Modules,Net,OS,Path,Process,P unycode,Query Strings,Readline,REPL,Stream,String De coder,Timers,TLS/SSL,TTY,UDP/Datagram,URL, Utilities,V8,VM,ZLIB等

npm包管理器

  • NPM(Node Package Manager) 官网的地址是 npm官网
  • npm 是随着安装 Node.js 的时候一并安装的,目的就是为了更方便的下载第三方模块插件
  • 虽然我们自己也可以通过内置模块以及自定义模块实现功能,但使用第三方模块可以让我们减少一些麻烦的代码书写,省时间,如果有比较好的模块,我们也可以上传至 npm
  • 建议安装 nrm 工具切换到 taobao 来源
  • npm 常用指令:
    • npm version (npm -v) - 查看npm版本
    • npm help(npm -h) - 查看npm帮助信息
    • npm init - 引导创建一个package.json文件
    • npm install(简写:npm i) - 安装 默认在当前目录,如果没有node_modules 会创建文件夹
      • npm install module_name -S 或者 npm install module_name --save 写入dependencies(需要发布到生产环境)
      • npm install module_name -D 或者 npm install module_name --save-dev 写入devDependencies(只用于开发环境)
      • npm install module_name -g 全局安装
      • 指定版本安装模块 npm i module_name @1.0 通过 "@"符号指定
    • npm remove 或者 npm uninstall - 删除
    • npm update(npm -up) - 更新
    • npm root 查看当前包安装的路径 或者通过 npm root -g 来查看全局安装路径

path 模块

  • __dirname - 指向被执行 js 文件的绝对路径
  • __firename - 指向被执行 js 文件的绝对路径 + js文件名
  • ./ - 你执行 Node.js 命令的路径,即工作路径

规范化路径

  • path.normalize
  • 不同系统的路径有可能不同,例如,Unix系统是 /,Windows系统是 \
  • 该方法可规范化路径

连接路径

  • path.join
  • 该方法path使用特定于平台的分隔符作为分隔符将所有给定的段连接在一起,然后对结果路径进行规范化
path.join('..', 'fs', '1', '1.txt'); // windows下:..\fs\1\1.txt

拼接路径段为绝对路径

  • path.resolve( [from…], to )
  • path.join 不同,前者只是简单的将路径段进行连接,而 path.resolve 会对路径段进行解析
  • 给定的路径的序列是从右往左被处理的,后面每个 path 被依次解析,直到构造完成一个绝对路径
path.resolve('/foo/bar', './baz')   // returns '/foo/bar/baz'
path.resolve('/foo/bar', 'baz')   // returns '/foo/bar/baz'
path.resolve('/foo/bar', '/baz')   // returns '/baz'
path.resolve('/foo/bar', '../baz')   // returns '/foo/baz'
path.resolve('home','/foo/bar', '../baz')   // returns '/foo/baz'
path.resolve('home','./foo/bar', '../baz')   // returns '/home/foo/baz'
path.resolve('home','foo/bar', '../baz')   // returns '/home/foo/baz'
path.resolve('home', 'foo', 'build','aaaa','aadada','../../..', 'asset') //return '/home/foo/asset'

判断参数 path 是否是绝对路径

  • path.isAbsolute
  • 返回布尔值
path.isAbsolute(__dirname); // true

将绝对路径转为相对路径

  • path.relative(from, to)
  • 基于当前工作目录,返回从 from 到 to 的相对路径
path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb'); // ../../impl/bbb

返回路径最后一部分/去除后缀

  • path.basename(path[, ext])
  • 此方法区分大小写,包括扩展名
path.basename('/foo/bar/baz/asdf/quux.html'); // quux.html
path.basename('/foo/bar/baz/asdf/quux.html', '.html'); // quux
path.basename('/foo/bar/baz/asdf/quux.HTML', '.html'); // quux.HTML

返回路径后缀名

  • path.extname
  • 返回路径中最后一个 . 之后的部分
  • 如果一个路径中并不包含 . 或该路径只包含一个 . 且这个 . 为路径的第一个字符,则此命令返回空字符串
path.extname('1.html'); // .html
path.extname('.html'); // ''

路径字符串与对象的转换

  • path.parse
    • 返回路径字符串的对象
  • path.format(pathObject)
    • 从对象中返回路径字符串,和 path.parse 相反
  • 对象属性:
    1. root - 根路径
    2. dir - 路径
    3. base - 文件名及后缀
    4. name - 文件名
    5. ext - 文件后缀
path.parse('/home/user/dir/file.txt'); // { root: '/', dir: '/home/user/dir', base: 'file.txt', ext: '.txt', name: 'file' }
path.format({ root: '/', dir: '/home/user/dir', base: 'file.txt', ext: '.txt', name: 'file' }); // /home/user/dir/file.txt

File system(fs) 模块

  • fs是文件操作模块
  • 跟数据库操作有些类似,大的方向为 - 增、删、改、查
  • 区别:
    1. 文件操作
    2. 目录操作(文件夹/容器)

文件操作

文件写入

  • fs.writeFile
  • 参数
    1. 指定路径/文件名称(包含后缀)
    2. 内容(content)
    3. flag(写入方式)/ 4
      • a - 追加写入
      • w - 写入
    4. 回调函数
  • 如果第三参数不填,默认为写入,如有相同文件,文件将会被覆盖
const fs = require('fs'); // 文件操作

fs.writeFile('1.txt', '我是内容', err => {
    if (err) return console.log(err);
    console.log('写入成功');
});
fs.writeFile('1.txt', '我是内容', { flag: 'w' }, err => { });

文件读取

  • fs.readFile
  • 参数
    1. 文件路径/名称(包含后缀)
    2. 编码格式 / 3
      • 我们一般采用utf-8,进行中文编码,需确保内容为字符串
      • 如果不写编码格式,会通过 Buffer 的形式表达
    3. 回调函数(包含 err, data)
  • 不写编码格式的情况下,我们可以通过 .toString() 的方法转化成中文
    • 接收一个参数 - 编码格式
    • 如不填写参数,则将使用 utf-8 作为默认值,需确保内容为字符串
fs.readFile('1.txt', 'utf-8', (err, data) => {
    if (err) return console.log(err);
    console.log(data);
});
fs.readFile('1.txt', (err, data) => { });

文件操作的同步与异步

  • 所有文件操作都是有同步和异步之分,同步方法会加上 "Sync"
  • 同步操作会造成代码阻塞,视情况采用不同的方法
fs.writeFileSync('1.txt', '我是内容'); // 同步写入 => 返回值为undefined
const data = fs.readFileSync('1.txt', 'utf-8'); // 同步读取 => 返回读取到的内容:'我是内容'

文件修改(文件名)

  • fs.rename
  • 文件内容修改我们一般是先读取文件内容,再按需修改,重新写入,所以 fs 修改的是文件名
  • 参数
    1. 需要修改的文件路径/名称
    2. 指定的路径/名称
    3. 函数回调
fs.rename('1.txt', '2.txt', err => {
    if (err) return console.log(err);
    console.log('修改成功');
});

文件删除

  • fs.unlink
  • 参数
    1. 需要删除的文件路径/名称
    2. 函数回调
fs.unlink('1.txt', err => {
    if (err) return console.log(err);
    console.log('删除成功');
});

文件复制

  • fs.copyFile
  • 在以前版本的 Node.js 是没有复制方法的,我们需要通过 读取文件 -> 写入文件 进行复制
  • 参数
    1. 需要复制的文件路径/名称
    2. 指定的文件路径/名称
    3. 函数回调
fs.copyFile('1.txt', '2.txt', err => {
    if (err) return console.log(err);
    console.log('复制成功');
});

目录操作

创建目录

  • fs.mkdir
  • 参数
    1. 需要创建的目录路径/名称
    2. 函数回调
fs.mkdir('1', err => {
    if (err) return console.log(err);
    console.log('创建成功');
});

读取目录

  • fs.readdir
  • 读取目录内的所有 文件 及 子目录 名称
  • 读取到的内容会以数组的形式呈现
  • 参数
    1. 需要读取的目录路径/名称
    2. 函数回调
fs.readdir('1', (err, data) => {
    if (err) return console.log(err);
    console.log(data);
});

目录修改(目录名)

目录删除

  • fs.rmdir
  • 删除目录只能删除空目录/文件夹,否则会报错
  • 删除非空文件夹需要 - 先把目录内的文件/文件夹删除 -> 删除空目录
function removeDir(_path) {
    const data = fs.readdirSync(_path);
    data.forEach(item => {
        // 判断是否是文件 文件: 直接删除 目录:递归
        // 注意:item是名称,需要组装完整路径: path.join(_path, item)
        const url = path.join(_path, item);
        if (fs.statSync(url).isFile()) {
            // 文件: 直接删除
            fs.unlinkSync(url);
        } else {
            // 目录:递归
            removeDir(url)
        }
    });
    // 删除空目录
    fs.rmdirSync(_path);
}

removeDir('1');

文件与目录操作通用方法

修改名称

判断文件或者目录是否存在

  • fs.existsSync
  • 异步方法 exists 官方已弃用
  • 返回布尔值
const isExists = fs.existsSync('1'); // true || false

获取文件或者目录的详细信息

  • fs.stat
  • 可用于判断文件是否为文件/目录
  • 参数
    1. 路径/文件||目录
    2. 函数回调
fs.stat('1/abc.jpg', (err, stats) => {
    if (err) return console.log(err);
    console.log(stats); // 返回 fs.stats 对象
    // 判断是否为文件
    const fileRes = stats.isFile();
    console.log(fileRes); // 返回布尔值 true || false
    const dirRes = stats.isDirectory();
    console.log(dirRes); // 返回布尔值 true || false
});

服务器基础概念

  • 服务端(浏览器)可通过 Network 查看请求
  • URL 的组成(规则/资源定位):统一资源定位器
    • 分为三部分组成:
      1. 协议
      2. 主机
      3. 端口
      4. 路径
      5. 1+2+3 称为域
    • http://127.0.0.1:8080/1.html 为例
      1. http - 协议
      2. 127.0.0.1 - 主机
      3. 8080 - 端口
      4. 1.html - 路径
  • 每次交换信息(请求,响应),我们成为:报文
  • 每次报文(一条数据)包含:行、头、正文
    • 行(General):包含状态码、请求方法、请求路径等等
    • 头信息:
      • 请求头(Request Headers)、响应头(Response Headers
      • 用于额外信息处理
    • 正文:内容
  • 静态 / 动态
    • 静态 - 当同一个 URL 返回的内容是固定的(相对的,不改变文件的基础上,如:服务器有一个文件,存在硬盘中 - 图片、html等,如果我们不修改他,那么通过某个 URL 返回的内容是固定的)
    • 动态 - 当同一个 URL 返回的内容是非固定的,根据后端的处理会得到不同的内容(例如:同一个 URL,早上访问的内容和晚上访问的内容不一样,在不修改文件的情况下,内容是不固定的)
  • 一个网站通常包含 静态资源 和 动态资源
    • 通常我们会把静态资源按照某种规则进行管理和映射,即:文件进行统一管理,然后提供一种有规则的 URL 去访问
    • 又或者独立一个服务器用于管理静态资源,也是可以的

http 模块

  • http模块被用于服务器开发
  • 简单来说,就是监听网络(端口),当有客户端请求了,那么就返回对应的数据

http.createServer

  • 接收一个回调函数
  • 回调函数有两个传参

request - http.ClientRequest 类

  • 储存当前请求的客户端信息和方法
  • 客户端访问的地址(url)与后端的文件不是一对一的关系,他们只是以中虚拟映射的关系,这个关系是我们后端程序根据实际情况返回的
  • 例如 URL 为 a 时,我可以返回 1.html ,访问 b 时,返回 hhhh.html

response - http.ServerResponse 类

  • 提供了服务器响应相关的信息和方法

  • response.write(chunk[, encoding][, callback])

    • 浏览器会响应并把内容打在页面上

    • 我们可以 html 等各种资源(数据)储存在外部文件中,然后通过 node 去读取,编译成浏览器可读的形式传给浏览器

    • 参数:

      1. 内容:可用 fs.readFile 读取文件写入(html)
      2. 编码格式(可选)
      3. 回调函数(可选)
  • response.end([data[, encoding]][, callback])

    1. 参数类似于 response.write
    2. 此方法向服务器发送信号,指示所有响应头和主体已发送。该服务器应认为此消息已完成。response.end()必须在每个响应上调用方法,否则页面会一直卡着
  • response.setHeader()

    • 设置头部信息
    • 参数:
      1. 设置头名称
      2. 设置内容
    • Content-Type:编译类型
      • 如不写 编译类型告诉浏览器 这是一个什么文件,有可能解析失败
      • text/html;charset=utf-8:html
      • application/zip:下载的 zip 文件
      • 等等等等 MIME(多用途互联网邮件扩展类型)
      • 可通过 JSON 文件 根据后缀名 设置头信息,mime - npm
  • response.writeHeader(statusCode[, statusMessage][, headers])

    • 发送写入头信息(包括状态码)
    • response.writeHeader 必须在 response.setHeader 后面,response.setHeader 将合并传递给 response.writeHeader 给定的优先级
    • 参数:
      1. 状态码
      2. 状态信息
      3. 头信息

server.listen

  • 在当前电脑上监听一个指定的端口
  • 接受两个参数:
    1. 端口号
    2. 回调函数:在启动服务器后执行

示例

// 引入 Node 内置的 http 模块,来进行 http 的服务器开发
const http = require('http');
const fs = require('fs');

// 创建一个 http 的服务器
const server = http.createServer((req, res) => {
    console.log('有人发送了请求');
    // 回调参数有两个 request, response 这里简写为 req 和 res
    // 判断当前请求的地址是什么?(请求信息)
    let url = req.url;
    console.log('请求地址' + url);
    switch (url) {
        case '/':
            // 通过 fs.readFile 读取 html 文件
            res.write(fs.readFileSync('./template/1.html'));
            break;
        case '/css.css':
            // 设置头部信息:告诉浏览器输出的内容为 css
            res.setHeader('Content-Type', 'text/css;charset=utf-8')
            // css.css 请求通过 1.html <link> 标签请求
            res.write(fs.readFileSync('./template/static/css/css.css'));
            break;
        default:
            // 设置头部信息:告诉浏览器输出的内容为 html
            res.setHeader('Content-Type', 'text/html;charset=utf-8')
            res.writeHeader(200, { 'Content-Type': 'text/html;charset=utf-8' })
            // 可直接输出字符串
            res.write('<h1>你好~</h1>');
    }
    // 发送消息已完成
    res.end();
});

// 在当前电脑上监听一个指定的端口
server.listen(8080, () => {
    // 回调函数,当有客户端请求发送,并被当前 server 监听到了,则会执行该函数
    console.log('服务器开启成功,你可以启用通过http://127.0.0.1:8080');
});

Buffer

  • 在上面文件读取的时候我们提到,如果不写编码格式,会通过 Buffer 的形式表达,那 Buffer 是什么东西嘞
  • Buffer 中文解释为缓冲区,他是一个类,也可以理解为是一个数据格式
  • JavaScript 语言自身只有字符串数据类型,没有二进制数据类型,在文件流时,必须用到二进制数据,Buffer 就是专门存放二进制数据的缓存区

创建Buffer

Buffer.alloc

  • 创建空 Buffer
  • 在 Node.js 6.0 之前通过 new Buffer() 进行创建
// 接收参数为 Buffer 大小;10:10字节
Buffer.alloc(10); // <Buffer 00 00 00 00 00 00 00 00 00 00>

Buffer.from

  • 通过字符串/数组方式创建 Buffer
  • 展示形式是 十六进制 的方式展示的
Buffer.from('果冻与布丁'); // <Buffer e6 9e 9c e5 86 bb e4 b8 8e e5 b8 83 e4 b8 81>
Buffer.from([0xe6, 0x9e, 0x9c, 0xe5, 0x86, 0xbb, 0xe4, 0xb8, 0x8e, 0xe5, 0xb8, 0x83, 0xe4, 0xb8, 0x81]); // <Buffer e6 9e 9c e5 86 bb e4 b8 8e e5 b8 83 e4 b8 81>

Buffer合并

Buffer.concat

const buffer1 = Buffer.from([0xe6, 0x9e, 0x9c, 0xe5, 0x86, 0xbb, 0xe4]);
const buffer2 = Buffer.from([0xb8, 0x8e, 0xe5, 0xb8, 0x83, 0xe4, 0xb8, 0x81]);
// 中文每个字由3个十六进制组成,所以buffer1中最后1位和buffer2中前2位会出现乱码
console.log(buffer1.toString(), buffer2.toString()); // 果冻� ��布丁

Buffer.concat([buffer1, buffer2]).toString(); // 果冻于布丁

string_decoder

  • 引用 string_decoder 模块进行合并,该模块会将编码剩余的十六进制储存起来加入下一个中
  • 该方法性能更好
const { StringDecoder } = require('string_decoder');
const decoder = new StringDecoder();

const buffer1 = Buffer.from([0xe6, 0x9e, 0x9c, 0xe5, 0x86, 0xbb, 0xe4]);
const buffer2 = Buffer.from([0xb8, 0x8e, 0xe5, 0xb8, 0x83, 0xe4, 0xb8, 0x81]);

const res1 = decoder.write(buffer1);
const res2 = decoder.write(buffer2);

console.log(res1); // 果冻
console.log(res2); // 与布丁

Stream

  • Stream 是一个抽象接口,Node 中有很多对象实现了这个接口。例如,对 http 服务器发起请求的request 对象就是一个 Stream
  • 假设一个场景 - 我们需要传输一个 2G 的文件,如果直接一次性传输的话,那么服务器需要 2G 的带宽,客户端也需要 2G 的内存,如果极端环境下就会出现 传输失败、内存溢出 的情况
  • Stream 流会把一个大的文件切成多份的小段,像流水一样一点一点的进行传输,这样可能时间会长一点,但性能会更好,避免内存、带宽不足的情况发生
  • Stream 有四种类型
    • Readable - 可读操作
    • Writable - 可写操作
    • Duplex - 可读可写操作
    • Transform - 操作被写入数据,然后读出结果
  • 所有的 Stream 对象都是 EventEmitter 的实例。常用的事件有:
    • data - 当有数据可读时触发
    • end - 没有更多的数据可读时触发
    • error - 在接收和写入过程中发生错误时触发
    • finish - 所有数据已被写入到底层系统时触发

读取流

  • fs.createReadStream
const fs = require('fs');

// 创建一个65kb的文件
const buffer = Buffer.alloc(65 * 1024);
fs.writeFileSync('65kb', buffer);

// 创建可读流
const readrStream = fs.createReadStream('65kb');
// chunk 就是每个小方块
let num = 0;
// Stream 会把文件分割成每 64kb 的大小进行传输
readrStream.on('data', chunk => {
    console.log(++num);
    console.log(chunk);
});
// 处理流事件 完成
readrStream.on('end', () => {
    console.log('数据读完了');
});
// 处理流事件 错误
readrStream.on('error', err => {
    console.log(err.stack);
});

写入流

  • fs.createWriteStream
const fs = require('fs');
const writerStream = fs.createWriteStream('1.txt');

// 使用 utf8 编码写入数据
writerStream.write('果冻与布丁', 'utf-8');
// 标记文件末尾
writerStream.end();

// 处理流事件 完成
writerStream.on('finish', () => {
    console.log("写入完成");
});
// 处理流事件 错误
writerStream.on('error', err => {
    console.log(err.stack);
});

管道流

  • pipe
  • 管道提供了一个输出流到输入流的机制。通常我们用于从一个流中获取数据并将数据传递到另外一个流中
const fs = require('fs');

const readrStream = fs.createReadStream('1.txt');
const writerStream = fs.createWriteStream('2.txt');

// 通过管道写入
readrStream.pipe(writerStream);

PS

  • 当我们使用 Node.js 去执行文件代码的时候,Node.js 会把代码加载进内存,提高运行速度
  • 当代码文件(存在硬盘)发生改变的时候,Node.js 默认是不会去主动重新加载的,我们需要重新运行 Node 去执行该文件
  • 我们可以通过一些工具来完成文件的监听(变化),当文件改变时,就去重新加载文件
    • fs.watch()
    • supervisor
    • nodemon
  • 建议使用 nodemon 工具监听文件变化:npm install nodemon