Node基础

Node.js 应用程序运行于单个进程中,无需为每个请求创建新的线程。 Node.js 在其标准库中提供了一组异步的 I/O 原生功能(用以防止 JavaScript 代码被阻塞),并且 Node.js 中的库通常是使用非阻塞(异步)的范式编写的(从而使阻塞行为成为例外而不是规范)。

当 Node.js 执行 I/O 操作时(例如从网络读取、访问数据库或文件系统),Node.js 会在响应返回时恢复操作(异步),而不是阻塞线程并浪费 CPU 循环等待。

这使 Node.js 可以在一台服务器上处理数千个并发连接,而无需引入管理线程并发的负担

Node.js是什么?

  • Node.js不是一门语言,也不是库,也不是框架
  • Node.js是一个JS运行时环境 (Node.js可以解析和执行JS代码)
  • Node.js 中的 JavaScript
    • 没有 BOM、DOM
    • EcmaScript 基本的 JavaScript 语言部分
    • 在 Node 中为 JavaScript 提供了一些服务器级别的 API
      • 文件操作的能力
      • http 服务的能力
  • 构建于Chrome的V8引擎之上
    • 代码只是具有特定格式的字符串而已,引擎可以将这些字符串拿去解析和执行
    • Node.js的作者吧Google Chrome中的V8引擎移植出来,开发了一个独立的JavaScript运行时环境。

Node.js 能干嘛?

在Node这个JavaScript执行环境中为JavaScript提供了一些服务器级别的操作API

  • 例如文件读写
  • 网络服务的构建
  • 网络通信
  • http服务器
  • 命令行工具
  • 等等……

优势

Node.js 具有独特的优势,因为为浏览器编写 JavaScript 的数百万前端开发者现在除了客户端代码之外还可以编写服务器端代码,而无需学习完全不同的语言。(ECMAScript)

在 Node.js 中,可以毫无问题地使用新的 ECMAScript 标准,因为不必等待所有用户更新其浏览器,你可以通过更改 Node.js 版本来决定要使用的 ECMAScript 版本,并且还可以通过运行带有标志的 Node.js 来启用特定的实验中的特性。

只用到了JS中的ECMAScript,因为Node.js是不操作页面,所以没有DOM和BOM

console.log(window)
console.log(document)

模块化开发

Node.js规定一个JavaScript文件就是一个模块,模块内部定义的变量和函数默认情况下在外部无法得到

模块内部可以使用exports对象进行成员导出, 使用require方法导入其他模块。

exports和module exports的区别

nodejs模块中的exports对象,你可以用它创建你的模块。例如:(假设这是rocker.js文件)

exports.name = function() {
    console.log('My name is Lemmy Kilmister');
};

在另一个文件中你这样引用

var rocker = require('./rocker.js');
rocker.name(); // 'My name is Lemmy Kilmister'

那到底Module.exports是什么呢?它是否合法呢?

其实,**Module.exports才是真正的接口,exports只不过是它的一个辅助工具。 最终返回给调用的是Module.exports而不是exports。**

所有的exports收集到的属性和方法,都赋值给了**Module.exports。当然,这有个前提,就是Module.exports本身不具备任何属性和方法``。如果,Module.exports**`已经具备一些属性和方法,那么exports收集来的信息将被忽略。```

修改rocker.js如下:

module.exports = 'ROCK IT!';
exports.name = function() {
    console.log('My name is Lemmy Kilmister');
};

再次引用执行rocker.js

var rocker = require('./rocker.js');
rocker.name(); // TypeError: Object ROCK IT! has no method 'name'

发现报错:对象“ROCK IT!”没有name方法

rocker模块忽略了exports收集的name方法,返回了一个字符串“ROCK IT!”。由此可知,你的模块并不一定非得返回“实例化对象”。你的模块可以是任何合法的javascript对象–boolean, number, date, JSON, string, function, array等等。

Hello World

  1. 创建编写JS代码脚本文件
  2. 打开终端,定位到文件目录下
  3. 输入node 文件名打开并执行对应文件

注意:文件名不要用node.js来命名

核心模块

Node为JavaScript提供了很多服务器级别的API,这些API绝大多数都被包装到了具名的核心模块中了。例如文件操作的fs核心模块,http服务构建的http模块,path路径模块,os操作系统信息模块等等

在node中,没有全局作用域,只有模块作用域,每一个模块相互独立,互不影响

在node当中,模块有三种:

  • 具名的核心模块
  • 自己编写的文件模块(用相对路径调用必须加./,可以省略后缀名)
  • 第三方模块

fs(文件模块)

fs是file-system的简写,就是文件系统的意思,在这个核心模块中,就提供了所有的文件操作相关的 API

步骤

  • 使用require方法加载fs核心模块

    var fs = require('fs')
    

    require方法有两个作用:

    ​ 1.加载文件模块执行里面的代码

    ​ 2.拿到被加载文件模块导出的接口对象

    ​ 在每个文件模块中都提供了一个对象:exports用于存放需要被外部访问的成员(默认是一个空对象{})

    • 例子:读取文件
      • 第一个参数是要读取的文件路径
      • 第二个参数是回调函数,接收error和data两个参数
        • error
          • 如果读取失败,error为Error对象
          • 如果读取成功,error为null
        • data
          • 如果读取失败,data就是undefined
          • 如果读取成功,data就是读取的数据
    fs.readFile('./hello.txt',(error, data)=>{
        //如果成功
        console.log(data)     //16进制数据,所以应该用toString方法把其转为我们能认识的字符(data.toString)
        console.log(error)    // null
    })
    

注意:要习惯性得用error去做错误判断

http(网络通信模块)

可以用Node非常轻松的构建一个Web服务器,在Node中专门提供了一个核心模块:http,这个模块的职责就是帮你创建编写服务器的

步骤

  • 加载http核心模块

    var http = require('require')
    
  • 使用http.createServer()方法创建一个Web服务器,并返回一个Server实例

    var server = http.createServer()
    
  • 服务器要做些什么事情呢?

    • 提供服务—>对数据的服务

    • 发送请求

    • 接受请求

    • 处理请求

    • 发送响应(处理的反馈)

    • 注册request请求事件

    • 当客户端请求过来,就触发了服务器的request请求事件,然后执行回调函数

      • request请求事件处理函数,需要接受两个参数(request,response)
        • request 请求对象:请求对象可以用来获取客户端的一些请求信息,例如请求的路径
        • response响应对象:响应对象可以用来给客户端发送响应消息
          • response对象有一个write方法,write可以用来给客户端发送响应数据,write方法可以使用多次,但是最后一次必须用end方法来结束响应,否则客户端会一直等待
    server.on('request', function (request,response) {
        //不同的资源对应的Content-Type是不一样的,具体参考:https://tool.oschina.net/commons表格
        response.setHeader('Content-Type', 'text/html; charset=utf-8');
        console.log('收到客户端请求了,路径是:'+ request.url)
        switch(request.url){
          case '/login' : response.write('<p>进入登录界面中...请稍后</p>'); break;
          case '/register' : response.write('<p>正在进入注册界面...请稍后</p>'); break;
          case '/index' : response.write('<p>这就是首页</p>'); break;
          default : response.write('404 Not Found'); break;
        }
        response.end();
    })
    
  • 绑定端口号,启动服务号(计算机有一些默认端口号,最好也不要使用,比如http服务的80端口号)

    server.listen(5555,()=>{
      console.log('服务器启动成功了,可以通过127.0.0.1:5555/ 来进行访问');
    })
    

端口号的范围在0~65535之间,为什么是65535?

TCP报文头部存放端口号为16位,所以端口号为2^16-1=65535个(减去全0情况),所以范围在0~65535之间

但是如果启动端口为0时,会随机生成一个范围内的端口号

启动端口为1或者65535时效果一样,都是项目可以正常启动,浏览器访问时拒绝。说是不安全的端口

端口号为65536或者更大时。直接项目无法启动,提示你换一个其它的端口号

端口号为2,65534或者其它区间内的任意端口一切正常。happy!

child_process、cluster(进程模块)

events(事件模块)

等等……


体系架构

Node.js主要分为四大部分,Node Standard Library,Node Bindings,V8,Libuv,架构图如下:

  • Node Standard Library 是我们每天都在用的核心模块,如Http, Buffer 模块。

  • Node Bindings 是沟通JS 和 C++的桥梁,封装V8和Libuv的细节,向上层提供基础API服务。

    • 这一层是支撑 Node.js 运行的关键,由 C/C++ 实现。
      • V8 是Google开发的JavaScript引擎,提供JavaScript运行环境,可以说它就是 Node.js 的发动机。
      • Libuv 是专门为Node.js开发的一个封装库,提供跨平台的异步I/O能力.
      • C-ares:提供了异步处理 DNS 相关的能力。
      • http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、数据压缩等其他的能力。

libuv 架构

从左往右分为两部分,一部分是与网络I/O相关的请求,而另外一部分是由文件I/O, DNS Ops以及User code组成的请求。

从图中可以看出,对于Network I/O和以File I/O为代表的另一类请求,异步处理的底层支撑机制是完全不一样的。

对于Network I/O相关的请求, 根据OS平台不同,分别使用Linux上的epoll,OSX和BSD类OS上的kqueue,SunOS上的event ports以及Windows上的IOCP机制。

而对于File I/O为代表的请求,则使用thread pool。利用thread pool的方式实现异步请求处理,在各类OS上都能获得很好的支持。


什么是koa中的ctx

遇事不决就打印:

const Koa = require('koa');
const app = new Koa();

app.use(ctx => {
  ctx.body = 'Hello Koa in app-async.js';
  console.log(ctx)
});

app.listen(5555,()=>{console.log('服务器启动成功了,可以通过127.0.0.1:5555/ 来进行访问');});

​ ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

{
  request: {
    method: 'GET',
    url: '/',
    header: {
      host: '127.0.0.1:5555',
      connection: 'keep-alive',
      'upgrade-insecure-requests': '1',
      'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.3
6 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36',
      accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,
image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
      'sec-fetch-site': 'none',
      'sec-fetch-mode': 'navigate',
      'sec-fetch-dest': 'document',
                                                                                      accept: 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8',
      'sec-fetch-site': 'same-origin',
      'sec-fetch-mode': 'no-cors',
      'sec-fetch-dest': 'image',
      referer: 'http://127.0.0.1:5555/',
      'accept-encoding': 'gzip, deflate, br',
      'accept-language': 'zh-CN,zh;q=0.9'
    }
  },
  response: {
    status: 200,
    message: 'OK',
    header: [Object: null prototype] {
      'content-type': 'text/plain; charset=utf-8',
      'content-length': '25'
    }
  },
  app: { subdomainOffset: 2, proxy: false, env: 'development' },
  originalUrl: '/favicon.ico',
  req: '<original node req>',
  res: '<original node res>',
  socket: '<original node socket>'
}

可见它主要包括request和response两部分。

ctx是context的缩写中文一般叫成上下文,这个在所有语言里都有的名词,可以理解为上(request)下(response)沟通的环境,所以koa中把他们两都封装进了ctx对象,koa官方文档里的解释是为了调用方便,ctx.req=ctx.request,ctx.res=ctx.response

body是http协议中的响应体,header是指响应头
ctx.body = ctx.res.body = ctx.response.body

Koa 提供一个 Context 对象,表示一次对话的上下文(包括 HTTP 请求和 HTTP 回复)。通过加工这个对象,就可以控制返回给用户的内容。

Koa的关键代码——Compose

这里分享一个在思否上一个博主写的我觉得比较好的源码分析:https://segmentfault.com/a/1190000013447551

首先理解一个思想是:g()+h()=g(h()),这就是洋葱模型的关键

中间件这儿的重点,是compose函数。compose函数的源代码虽然很简洁,但要理解明白着实要下一番功夫。
以下为源码分析:

'use strict'
/**
 * Expose compositor.
 */
// 暴露compose函数
module.exports = compose
/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */
// compose函数需要传入一个数组队列 [fn,fn,fn,fn]
function compose (middleware) {
  // 如果传入的不是数组,则抛出错误
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  // 数组队列中有一项不为函数,则抛出错误
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

   // compose函数调用后,返回的是以下这个匿名函数
   // 匿名函数接收两个参数,第一个随便传入,根据使用场景决定
   // 第一次调用时候第二个参数next实际上是一个undefined,因为初次调用并不需要传入next参数
   // 这个匿名函数返回一个promise
  return function (context, next) {
    // last called middleware #
    //初始下标为-1
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      // 如果传入i为负数且<=-1 返回一个Promise.reject携带着错误信息
      // 所以执行两次next会报出这个错误。将状态rejected,就是确保在一个中间件中next只调用一次
      

      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      // 执行一遍next之后,这个index值将改变
      index = i
      // 根据下标取出一个中间件函数
      let fn = middleware[i]
      // next在这个内部中是一个局部变量,值为undefined
      // 当i已经是数组的length了,说明中间件函数都执行结束,执行结束后把fn设置为undefined
      // 问题:本来middleware[i]如果i为length的话取到的值已经是undefined了,为什么要重新给fn设置为undefined呢?
      if (i === middleware.length) fn = next

      //如果中间件遍历到最后了。那么。此时return Promise.resolve()返回一个成功状态的promise
      // 方面之后做调用then
      if (!fn) return Promise.resolve()

      // try catch保证错误在Promise的情况下能够正常被捕获。

      // 调用后依然返回一个成功的状态的Promise对象
      // 用Promise包裹中间件,方便await调用
      // 调用中间件函数,传入context(根据场景不同可以传入不同的值,在KOa传入的是ctx)
      // 第二个参数是一个next函数,可在中间件函数中调用这个函数
      // 调用next函数后,递归调用dispatch函数,目的是执行下一个中间件函数
      // next函数在中间件函数调用后返回的是一个promise对象
      // 读到这里不得不佩服作者的高明之处。
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

为了助于理解,我整理了一张图

再来个动图配合理解