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 相反
- 对象属性:
- root - 根路径
- dir - 路径
- base - 文件名及后缀
- name - 文件名
- 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是文件操作模块
- 跟数据库操作有些类似,大的方向为 - 增、删、改、查
- 区别:
- 文件操作
- 目录操作(文件夹/容器)
文件操作
文件写入
- fs.writeFile
- 参数
- 指定路径/文件名称(包含后缀)
- 内容(content)
- flag(写入方式)/ 4
- a - 追加写入
- w - 写入
- 回调函数
- 如果第三参数不填,默认为写入,如有相同文件,文件将会被覆盖
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
- 参数
- 文件路径/名称(包含后缀)
- 编码格式 / 3
- 我们一般采用utf-8,进行中文编码,需确保内容为字符串
- 如果不写编码格式,会通过 Buffer 的形式表达
- 回调函数(包含 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 修改的是文件名
- 参数
- 需要修改的文件路径/名称
- 指定的路径/名称
- 函数回调
fs.rename('1.txt', '2.txt', err => {
if (err) return console.log(err);
console.log('修改成功');
});
文件删除
- fs.unlink
- 参数
- 需要删除的文件路径/名称
- 函数回调
fs.unlink('1.txt', err => {
if (err) return console.log(err);
console.log('删除成功');
});
文件复制
- fs.copyFile
- 在以前版本的 Node.js 是没有复制方法的,我们需要通过 读取文件 -> 写入文件 进行复制
- 参数
- 需要复制的文件路径/名称
- 指定的文件路径/名称
- 函数回调
fs.copyFile('1.txt', '2.txt', err => {
if (err) return console.log(err);
console.log('复制成功');
});
目录操作
创建目录
- fs.mkdir
- 参数
- 需要创建的目录路径/名称
- 函数回调
fs.mkdir('1', err => {
if (err) return console.log(err);
console.log('创建成功');
});
读取目录
- fs.readdir
- 读取目录内的所有 文件 及 子目录 名称
- 读取到的内容会以数组的形式呈现
- 参数
- 需要读取的目录路径/名称
- 函数回调
fs.readdir('1', (err, data) => {
if (err) return console.log(err);
console.log(data);
});
目录修改(目录名)
- fs.rename
- 使用与 文件修改 一致
目录删除
- 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
- 可用于判断文件是否为文件/目录
- 参数
- 路径/文件||目录
- 函数回调
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 称为域
- 以
http://127.0.0.1:8080/1.html为例- http - 协议
- 127.0.0.1 - 主机
- 8080 - 端口
- 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 去读取,编译成浏览器可读的形式传给浏览器
-
参数:
- 内容:可用 fs.readFile 读取文件写入(html)
- 编码格式(可选)
- 回调函数(可选)
-
-
response.end([data[, encoding]][, callback])
- 参数类似于 response.write
- 此方法向服务器发送信号,指示所有响应头和主体已发送。该服务器应认为此消息已完成。
response.end()必须在每个响应上调用方法,否则页面会一直卡着
-
response.setHeader()
- 设置头部信息
- 参数:
- 设置头名称
- 设置内容
- 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 给定的优先级
- 参数:
- 状态码
- 状态信息
- 头信息
server.listen
- 在当前电脑上监听一个指定的端口
- 接受两个参数:
- 端口号
- 回调函数:在启动服务器后执行
示例
// 引入 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