1.Node.js基础

185 阅读9分钟

Node是什么

Node.js不是一门语言,是一个js运行环境。Node.js是一个基于Chrome V8引擎的JavaScript运行环境。Node.js使用了一个事件驱动非阻塞式I/O的模型,使其轻量又高效。此外Node自带包管理工具npm(node package manager),下载前后端的包可以使用npm指令。

Node.js与js的区别

js:

  • ECMAScript
  • DOM
  • BOM

Node.js:

  • 没有DOM和BOM
  • ECMAScript + es6 的相关语法在node脚本下都能使用
  • 支持commonJS规范

在node环境下如何运行脚本文件

通过node 文件名来运行脚本文件,每次修改js文件需要重新执行才会生效,使用指令npm i -g nodemon安装nodemon,安装后通过nodemon 文件名运行脚本文件,可以监视文件改动,自动重启。

REPL指令

  • ctrl + c 退出当前终端
  • ctrl + c 连按两次,退出 Node REPL
  • ctrl + d 退出 Node REPL

Node中的异步编程

Node.js异步编程的直接体现在回调函数上。

异步:就是一个不等待的过程。非阻塞。
优点:性能提高,可以处理大量的并发请求

阻塞代码

readFile

fs.readFile('/etc/passwd', 'utf8', callback);
参数:

  1. 文件路径
  2. 编码格式,如果不指定utf-8格式,获取到的data是一个Buffer对象
  3. 回调函数
const fs = require('fs');
//同步方法,会产生阻塞效果
const data = fs.readFileSync('./runcode.js');
//Buffer流
console.log(data);
//调用Buffer的toString方法
console.log(data.toString());
//会等读取文件执行完后再执行
console.log('阻塞式');

非阻塞代码

异步编程,不阻塞,大大提高程序性能。

const fs = require('fs');
//异步编程
fs.readFile('./runcode.js', (err, data) => {
    //第一个参数为 err ,node中的错误优先机制
    if (err) {
    	//error.stack 属性是一个字符串,描述代码中 Error 被实例化的位置。
        console.log(err.stack);
        return;
    }
    console.log(data.toString());
})
//readFile不会阻塞下列代码执行,而readFile需要时间,所以下列代码会先执行
console.log('非阻塞式');

setTimeout(()=>{},time)就是典型的非阻塞。阻塞是按顺序执行的,而非阻塞是不需要按顺序执行的。

util.promisify(original)

const fs = require('fs');
// 使用promise
const {
    promisify
} = require('util');
// promisify 将fs.readFile包装成promise对象
const readFile = promisify(fs.readFile);

async function asyncReadFile() {
    try {
        //让异步代码同步化
        const data = await readFile('./runcode.js');
        console.log(data.toString());
    } catch (error) {
        //打印错误的位置
        console.log(error.stack);
    }
}

asyncReadFile();

Generator

const fs = require('fs');
const {
    promisify
} = require('util');
const readFile = promisify(fs.readFile);

function* read() {
    yield readFile('./runcode.js');
}

//获取Generator对象
let gen = read();

gen.next().value.then((res) => {
    console.log(res.toString());
}).catch((err) => {
    console.log(err);
})

Buffer对象

// 创建一个长度为 10 的 Buffer对象
const buf1 = Buffer.alloc(10);
console.log(buf1);

// 创建一个长度为 10、且用 0x1 填充的 Buffer。 
const buf2 = Buffer.alloc(10, 1);
console.log(buf2);

// 通过数据创建,创建一个包含UTF-8 编码字节
const buf3 = Buffer.from('hello world');
console.log(buf3);

// 创建一个包含字节 [1, 2, 3] 的 Buffer。
const buf4 = Buffer.from([1, 2, 3]);
console.log(buf4);

//写入
buf1.write('hello');
console.log(buf1);

//当填充满后不再填充
buf1.write('hello buffer');
//读取
console.log(buf1.toString()); //hello buff
//转换编码格式,默认 utf-8
console.log(buf1.toString('base64')); //aGVsbG8gYnVmZg==

//合并
const buf5 = Buffer.concat([buf1, buf3]);
console.log(buf5.toString());//hello buffhello world

此外还有Buffer.compare(buf1, buf2)主要用于 Buffer 实例数组的排序等函数。

Stream流

  • Writable - 可写入数据的流(例如 fs.createWriteStream())
  • Readable - 可读取数据的流(例如 fs.createReadStream())

管道流

管道提供了一个输出流到输入流的机制。通常我们用于从一个流中获取数据并将数据传递到另外一个流中。

复制文件

const fs = require('fs');
//创建可读流
const readerStream = fs.createReadStream('./readme.md');
//创建可写的流
const writerStream = fs.createWriteStream('./test.txt');
//通过管道进行传输
readerStream.pipe(writerStream);
console.log('复制完毕');

链式流

链式是通过连接输出流到另外一个流并创建多个流操作链的机制。链式流一般用于管道操作

压缩文件

const fs = require('fs');
const zlib = require('zlib');
const gzip = zlib.createGzip();

fs.createReadStream('./test.txt').pipe(gzip).pipe(fs.createWriteStream('./test.zip'));
console.log('文件压缩成功');

解压文件

const fs = require('fs');
const zlib = require('zlib');
const gunzip = zlib.createGunzip();

fs.createReadStream('./test.zip').pipe(gunzip).pipe(fs.createWriteStream('./1.txt'));
console.log('文件解压成功');

events

  • 事件循环

    • Node.js 是单进程单线程应用程序,但是因为 V8 引擎提供的异步执行回调接口,通过这些接口可以处理大量的并发,所以性能非常高。
    • Node.js 几乎每一个 API 都是支持回调函数的。
    • Node.js 基本上所有的事件机制都是用设计模式中观察者模式实现。
    • Node.js 单线程类似进入一个while(true)的事件循环,直到没有事件观察者退出,每个异步事件都生成一个事件观察者,如果有事件发生就调用该回调函数.
  • 事件驱动程序

    • Node.js 使用事件驱动模型,当web server接收到请求,就把它关闭然后进行处理,然后去服务下一个web请求。
    • 当这个请求完成,它被放回处理队列,当到达队列开头,这个结果被返回给用户。
    • 这个模型非常高效可扩展性非常强,因为 webserver 一直接受请求而不等待任何读写操作。(这也称之为非阻塞式IO或者事件驱动IO)
    • 在事件驱动模型中,会生成一个主循环来监听事件,当检测到事件时触发回调函数。

整个事件驱动的流程就是这么实现的,非常简洁。有点类似于观察者模式,事件相当于一个主题(Subject),而所有注册到这个事件上的处理函数相当于观察者(Observer)。

Node.js 有多个内置的事件,我们可以通过引入 events 模块,并通过实例化 EventEmitter 类来绑定和监听事件。

// 引入 events 模块
const events = require('events');
// 创建 eventEmitter 对象
const eventEmitter = new events.EventEmitter();

// 绑定事件处理程序 定好一个主题
eventEmitter.on('conn', () => {
    console.log('连接成功');
    //触发另外的事件处理
    eventEmitter.emit('data_receive');
});
// 绑定事件
eventEmitter.on('data_receive', () => {
    console.log('数据接收成功');
})
// 观察者 观察对应的主题要做什么事情
eventEmitter.emit('conn');
console.log('程序执行完毕');

模块系统

为了让Node.js的文件可以相互调用,Node.js提供了一个简单的模块系统。 模块是Node.js 应用程序的基本组成部分,文件和模块是一一对应的。换言之,一个 Node.js 文件就是一个模块,这个文件可能是JavaScript 代码、JSON 或者编译过的C/C++ 扩展。

Node.js提供了exportsrequire两个对象,其中exports是模块公开的接口,require用于从外部获取一个模块的接口。

创建模块

exports.name = () => {
    console.log('Holo');
}
//也可抛出多个
exports.age = () => {
    console.log(18);
}
//还可抛出数组、变量、对象等

引入模块

let Obj = require('./hello');
Obj.name()
//
Obj.age();

抛出构造函数

function Dog() {
    let name;
    this.setName = function (myName) {
        name = myName;
    }

    this.getName = function () {
        console.log('hello' + name);
    }
}

module.exports = Dog;

引入

const Dog = require('./module');
const d1 = new Dog();
d1.setName('小黄');
d1.getName();

如果通过module.exports抛出对象,抛出的就是当前的对象,如果是通过exports抛出,会是一个键值对的形式挂载到对象上{ name: [Function], age: [Function], dog: '小黄' }。并且exports在一个文件中可以有多个抛出,而module.exports只能有一个抛出。

模块加载策略

Node.js 的 require方法中的文件查找策略如下:

由于Node.js中存在4类模块(原生模块和3种文件模块)

  • http、fs、path等,原生模块。
  • ./mod或../mod,相对路径的文件模块。
  • /pathtomodule/mod,绝对路径的文件模块。
  • mod,非原生模块的文件模块

尽管require方法极其简单,但是内部的加载却是十分复杂的,其加载优先级也各自不同。如下图所示:

常用内置模块

fs模块

nodejs.jakeyu.top/#%E6%96%87%…

os模块

nodejs.jakeyu.top/#os-%E6%A8%…

url模块

nodejs.cn/api/url.htm…

注意:使用WHATWG标准的新API可以直接const myURL = new URL('https://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash');,不需要再去导入const url = require('url');

path模块

nodejs.jakeyu.top/#path-%E6%A…

  • path.join() 方法会将所有给定的 path 片段连接到一起(使用平台特定的分隔符作为定界符),然后规范化生成的路径。

__dirname: 表示当前执行脚本所在的目录。

  • path.resolve() 方法会将路径或路径片段的序列解析为绝对路径。
const path = require('path');
//二者都能获取绝对路径名
console.log(path.join(__dirname, 'demo1.js'));
console.log(path.resolve('demo1.js'));
  • path.extname() 方法会返回 path 的扩展名

注意:如果在服务器端使用path.resolve()方法返回的是服务器上的一个绝对路径,使用path.join()方法返回的是当前电脑上的绝对路径。建议使用path.join(),以免出错。

http模块

搭建服务器

//引入http模块
const http = require('http');
//创建Server对象
const app = http.createServer((req, res) => {
    //中文会出现乱码,需要进行设置
    res.end('hello node.js');
})
//监听端口号
app.listen(3000);

对不同的url做不同的响应处理

//引入http模块
const http = require('http');
//引入fs模块
const fs = require('fs');
//创建Server对象
const app = http.createServer((req, res) => {
    /*
    url: '/about',
    method: 'GET',
    */
    const {
        url,
        method
    } = req;
    //对路由进行判断
    if (url === '/index' && method === 'GET') {
        //返回一个首页给浏览器
        fs.readFile('./static/index.html', 'utf-8', (err, data) => {
            if (err) {
                //设置状态码
                res.statusCode = 500;
                //返回错误信息
                res.end('500 - Interval Serval Error!');
            }
            //成功
            res.statusCode = 200;
            //设置响应头    表明返回回去的一定是一个html
            //一般情况下不会特意去指定,如果需要指定,一定要根据文件类型去指定
            //mime 文件类型 https://www.w3school.com.cn/media/media_mimeref.asp
            res.setHeader('Content-Type', 'text/html');
            //返回信息
            res.end(data);
        });
    } else if (url === '/about' && method === 'GET') {
        fs.readFile('./static/about.html', 'utf-8', (err, data) => {
            if (err) {
                res.statusCode = 500;
                res.end('500 - Interval Serval Error!');
            }
            res.statusCode = 200;
            res.setHeader('Content-Type', 'text/html');
            res.end(data);
        })
    } else if (req.headers.accept.indexOf('image/*') !== -1 && method === 'GET') {
        //对图片进行处理    注意:这边就是 `image`,匹配的是一个mime类型,而不是你的文件夹名
        console.log(url);
        //流的方式
        fs.createReadStream(__dirname + url).pipe(res);
    } else if (url === '/user' && method === 'GET') {
        //处理json数据
        res.statusCode = 200;
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify([{
            name: 'Alex'
        }]))
    } else {
        //中文会出现乱码,需要进行设置
        res.end('hello node.js');
    }
})
//监听端口号
app.listen(3000);

express框架快速上手

  • 在当前项目目录下 npm init 初始化
package name: (node)				--- 包名
version: (1.0.0) 1.0.1				--- 版本
description:					--- 描述
entry point: (Buffer.js) index.js		--- 入口文件
test command:					--- 测试
git repository:					--- git地址
keywords: Alex					--- 关键字
author: Alex					--- 作者
license: (ISC)					--- 认证

也可以直接npm init --yes直接创建,然后再去修改配置文件。

  • 安装express npm install express --save为了能够安装更快,也可使用淘宝镜像安装 cnpm i express -s

HelloWorld

//引入express
const express = require('express')
//引入fs
const fs = require('fs');
//引入path
const path = require('path');
//创建app实例
const app = express()
const port = 3000
//匹配的路由地址
app.get('/', (req, res) => res.send('Hello World!'))
//index路由
app.get('/index', (req, res) => {
    fs.readFile('./static/index.html', 'utf-8', (err, data) => {
        if (err) {
            //设置状态码
            res.statusCode = 500;
            //返回错误信息
            res.end('500 - Interval Serval Error!');
        }
        //成功
        res.statusCode = 200;
        //设置响应头    表明返回回去的一定是一个html
        res.setHeader('Content-Type', 'text/html');
        //返回信息
        res.end(data);
    });
})
//匹配所有路由
app.get('*', (req, res) => {
    res.setHeader('Content-Type', 'image/*');

    // console.log(path.resolve(req.url)); // D:\static\images\timg.jpeg
    // console.log(path.join(__dirname, req.url)); // D:\code\MicrosoftVSCodeProjects\Study\node\static\images\timg.jpeg

    fs.readFile(path.join(__dirname, req.url), (err, data) => {
        if (err) {
            throw err;
        }
        //end()或send()
        res.send(data);
    })
})

//监听端口
app.listen(port, () => console.log(`Example app listening on port ${port}!`))

实现Mini的Express框架

MExpress.js

const Application =  require('./application')
function MExpress() {
  return new Application();
}
module.exports = MExpress;

application.js

// 需求
// 1.实现http服务器
// 2.实现get路由请求
const http = require('http');
const url = require('url');
class Application {
  constructor() {
    this.router = [];
  }
  get(path, cb) {
    this.router.push({
      path: path,
      method: "GET",
      handle: cb
    })
  }
  listen() {
    // 创建http服务器
    http.createServer((req, res) => {
      // 获取前端给我的url路径 pathname
      const { pathname } = url.parse(req.url);
      for(const route of this.router){
        if(route.path === pathname){
          route.handle(req,res);
          return;
        }
        // 处理其他的路由
        if(route.path === '*'){
          route.handle(req,res);
          return;
        }
      }
    }).listen(...arguments)

  }
}
module.exports = Application;