Skip to content
当前页导航

nodejs

说说你对 Node.js 的理解?优缺点?应用场景?

Node.js 是一个开源且跨平台的 JavaScript 运行时环境,它在浏览器外运行 V8 JavaScript 引擎(Google Chrome 的内核),利用事件驱动、非阻塞和异步输入输出模型等技术来提高性能,使得 JavaScript 可以用于服务器端开发。Node.js 采用非阻塞型 I/O 机制,在处理 I/O 操作时不会阻塞进程,而是通过事件通知的方式处理操作结果。这种机制使得 Node.js 在处理高并发场景时表现优秀。

非阻塞异步

Node.js 采用了非阻塞型 I/O 机制,在执行 I/O 操作时不会造成阻塞,当操作完成后通过回调函数以事件的形式通知执行结果。这样可以提高程序的执行效率,使得在执行高并发任务时表现出色。

事件驱动

Node.js 使用事件驱动的编程模型。当有新的请求进来时,请求会被压入一个事件队列中,然后通过事件循环来检测队列中的事件状态变化,一旦有状态变化,就会执行对应的回调函数来处理事件。这使得 Node.js 能够高效处理 I/O 密集型应用。

Node.js 的优缺点

优点:

  • 处理高并发场景性能更佳:Node.js 适用于处理大量并发请求的场景,由于其非阻塞 I/O 和事件驱动模型,能够高效地处理并发请求。
  • 适合 I/O 密集型应用:Node.js 擅长处理 I/O 密集型应用,其执行效率仍然较高,即使在运行极限时,CPU 占用率仍然较低。
  • 轻量快速:Node.js 采用 V8 引擎作为底层,使得其执行速度快,且具有较小的内存占用。

缺点:

  • 不适合 CPU 密集型应用:由于 Node.js 是单线程的,如果应用程序中有大量计算密集型任务,会导致阻塞,影响整个应用的性能。
  • 单线程:Node.js 采用单线程事件循环,虽然能够高效处理 I/O 密集型应用,但是无法充分利用多核 CPU,不能充分发挥多核处理器的优势。
  • 可靠性低:由于 Node.js 是单线程的,一旦某个环节崩溃,整个系统都会崩溃,可靠性较低。

Node.js 的应用场景

  • Web 应用程序:对于高并发的网络应用程序,如用户表单收集系统、后台管理系统、实时交互系统、考试系统等,Node.js 表现优越。
  • 多人联网游戏:基于 Web、Canvas 等技术的多人联网游戏能够充分利用 Node.js 的高并发特性。
  • 实时聊天和直播:基于 Web 的多人实时聊天客户端、聊天室、图文直播等应用。
  • 单页面应用程序:对于单页面应用程序,Node.js 能够处理前端与后端的数据交互,提供高效的 API 服务。
  • 数据库操作和 API 服务:Node.js 可以用于操作数据库,并为前端和移动端提供基于 JSON 的 API 服务。

说说对中间件概念的理解,如何封装 node 中间件?

中间件(Middleware)是介于应用系统和系统软件之间的一类软件,它使用系统软件所提供的基础服务(功能),衔接网络上应用系统的各个部分或不同的应用,能够达到资源共享、功能共享的目的。

在 Node.js 中,中间件主要是指封装 HTTP 请求细节处理的方法。例如在 Express、Koa 等 Web 框架中,中间件的本质为一个回调函数,参数包含请求对象、响应对象和执行下一个中间件的函数。在这些中间件函数中,我们可以执行业务逻辑代码,修改请求和响应对象,返回响应数据等操作。

Token 校验中间件

js
// token校验中间件
module.exports = (options) => async (ctx, next) {
  try {
    // 获取token
    const token = ctx.header.authorization;
    if (token) {
      try {
          // 使用verify函数验证token,并获取用户相关信息
          await verify(token);
      } catch (err) {
        console.log(err);
      }
    }
    // 进入下一个中间件
    await next();
  } catch (err) {
    console.log(err);
  }
}

日志中间件

js
const fs = require('fs')

// 日志中间件
module.exports = (options) => async (ctx, next) => {
  const startTime = Date.now()
  const requestTime = new Date()
  await next()
  const ms = Date.now() - startTime
  let logEntry = `${ctx.request.ip} -- ${requestTime} -- ${ctx.method} -- ${ctx.url} -- ${ms}ms`
  // 输出日志到文件
  fs.appendFileSync('./log.txt', logEntry + '\n')
}

Koa-static 中间件简单实现

js
const fs = require('fs')
const path = require('path')
const mime = require('mime')
const { promisify } = require('util')

// 将 stat 和 access 转换成 Promise
const stat = promisify(fs.stat)
const access = promisify(fs.access)

module.exports = function (dir) {
  return async (ctx, next) => {
    // 将访问的路由处理成绝对路径,这里要使用 join 因为有可能是 /
    let realPath = path.join(dir, ctx.path)

    try {
      // 获取 stat 对象
      let statObj = await stat(realPath)

      // 如果是文件,则设置文件类型并直接响应内容,否则当作文件夹寻找 index.html
      if (statObj.isFile()) {
        ctx.set('Content-Type', `${mime.getType()};charset=utf8`)
        ctx.body = fs.createReadStream(realPath)
      } else {
        let filename = path.join(realPath, 'index.html')

        // 如果不存在该文件则执行 catch 中的 next 交给其他中间件处理
        await access(filename)

        // 存在设置文件类型并响应内容
        ctx.set('Content-Type', 'text/html;charset=utf8')
        ctx.body = fs.createReadStream(filename)
      }
    } catch (e) {
      await next()
    }
  }
}

Koa-bodyparser 中间件简单实现

js
// 自定义koa-bodyparser中间件
const querystring = require('querystring')

module.exports = function bodyParser() {
  return async (ctx, next) => {
    await new Promise((resolve, reject) => {
      // 存储数据的数组
      let dataArr = []

      // 接收数据
      ctx.req.on('data', (data) => dataArr.push(data))

      // 整合数据并使用 Promise 成功
      ctx.req.on('end', () => {
        // 获取请求数据的类型 json 或表单
        let contentType = ctx.get('Content-Type')

        // 获取数据 Buffer 格式
        let data = Buffer.concat(dataArr).toString()

        if (contentType === 'application/x-www-form-urlencoded') {
          // 如果是表单提交,则将查询字符串转换成对象赋值给 ctx.request.body
          ctx.request.body = querystring.parse(data)
        } else if (contentType === 'applaction/json') {
          // 如果是json,则将字符串格式的对象转换成对象赋值给 ctx.request.body
          ctx.request.body = JSON.parse(data)
        }

        // 执行成功的回调
        resolve()
      })
    })

    // 继续向下执行
    await next()
  }
}

在封装中间件时,每个中间件应足够简单,职责单一。中间件的代码编写应高效,必要时通过缓存重复获取数据。

Node.js 中的 Web 框架如 Koa 本身比较简洁,但通过中间件的机制能实现各种需要的功能,使得 Web 应用具备良好的可拓展性和组合性。

通过将公共逻辑处理编写在中间件中,可以避免在每个接口回调中重复编写相同的代码,减少冗余代码,达到类似装饰者模式的效果。

Node 性能如何进行监控以及优化?

Node 性能监控指标

  • CPU 占用率:CPU 负载和使用率,反映系统 CPU 繁忙程度。
  • 内存占用率:监控 Node 进程的内存使用情况,避免内存泄漏和过度消耗。
  • 磁盘 I/O:监控硬盘的读写操作,避免 I/O 瓶颈影响性能。
  • 网络:监控网络流量,确保网络连接稳定。

如何监控 Node 性能

  1. 使用 Node.js 内置工具
  • util: 提供了一些实用函数,包括 util.promisify 用于将回调函数转换为 Promise 形式,方便异步编程。
  • perf_hooks: 是一个性能度量和监控的工具,提供了 performance 和 performanceObserver 等 API,可以用于测量 Node 应用的性能指标。
  • v8: 提供了许多 V8 引擎的信息和统计数据,可以通过 v8.getHeapStatistics()来获取堆内存使用情况。
js
// 使用util.promisify将回调函数转换为Promise形式
const util = require('util')
const fs = require('fs')
const readFileAsync = util.promisify(fs.readFile)

// 使用perf_hooks测量函数执行时间
const { performance } = require('perf_hooks')

async function readLargeFile() {
  const start = performance.now()
  const data = await readFileAsync(__dirname + '/large-file.txt')
  const end = performance.now()
  console.log(`读取文件耗时: ${end - start} 毫秒`)
  return data
}

readLargeFile()
  1. 使用开源监控工具
  • PM2:是一个成熟的 Node.js 进程管理器,提供了丰富的监控和管理功能,包括 CPU 和内存监控、进程状态查看、错误日志记录等。
  • New Relic:是一款功能强大的应用性能监控工具,支持多种语言和平台,提供实时性能数据和报告,帮助定位性能问题。
  • AppDynamics:是一种全栈的应用性能管理解决方案,可监控分布式应用、云基础架构和网络性能。
  1. 自定义监控指标

可以通过日志记录、指标上报等方式实现。

js
// 使用日志记录自定义监控指标
const fs = require('fs')
const http = require('http')

function logMetrics(metric) {
  fs.appendFile(
    'metrics.log',
    `${new Date().toISOString()} - ${metric}\n`,
    (err) => {
      if (err) {
        console.error('写入日志失败', err)
      }
    }
  )
}

// 读取大文件并返回
http
  .createServer((req, res) => {
    const start = process.hrtime.bigint()
    const buffer = fs.readFileSync(__dirname + '/large-file.txt')
    const end = process.hrtime.bigint()
    const duration = Number(end - start) / 1e6 // 转换为毫秒
    logMetrics(`读取文件耗时: ${duration} 毫秒`)
    res.end(buffer)
  })
  .listen(3000)

如何优化 Node 性能

  1. 使用异步操作

Node.js 的异步非阻塞特性是其高性能的重要原因之一。在编写 Node 应用时,应尽可能使用异步操作,避免阻塞事件循环。

js
// 使用异步操作优化性能
const http = require('http')
const fs = require('fs')

http
  .createServer((req, res) => {
    fs.readFile(__dirname + '/large-file.txt', (err, data) => {
      if (err) {
        console.error('读取文件失败', err)
        res.statusCode = 500
        res.end('读取文件失败')
      } else {
        res.end(data)
      }
    })
  })
  .listen(3000)
  1. 内存管理优化

Node.js 中的内存管理是关键性能优化的一部分。合理使用内存可以减少垃圾回收的频率,提高性能。

  • 使用对象池:通过重复使用对象,避免频繁的对象创建和销毁,减少内存开销。
  • 控制内存使用:限制单个请求或任务的内存使用,防止出现内存泄漏和堆栈溢出。
js
// 使用对象池优化内存管理
const fs = require('fs')
const http = require('http')

const bufferPool = []

function createBuffer() {
  return fs.readFileSync(__dirname + '/large-file.txt')
}

http
  .createServer((req, res) => {
    let buffer = bufferPool.pop()
    if (!buffer) {
      buffer = createBuffer()
    }
    res.end(buffer)
    bufferPool.push(buffer)
  })
  .listen(3000)
  1. 使用缓存

缓存是提高 Node 应用性能的有效方式之一。可以使用内存缓存(如 Redis、Memcached)或本地缓存(如 Node 缓存模块)来存储经常使用的数据,减少重复计算和查询。

js
// 使用内存缓存优化性能
const http = require('http')
const fs = require('fs')
const NodeCache = require('node-cache')

const cache = new NodeCache()

http
  .createServer(
    (
      req,

      res
    ) => {
      const key = 'large-file-data'
      let data = cache.get(key)
      if (!data) {
        data = fs.readFileSync(__dirname + '/large-file.txt')
        cache.set(key, data)
      }
      res.end(data)
    }
  )
  .listen(3000)
  1. 使用 CDN 加速

如果 Node.js 应用涉及到静态资源(如 CSS、JavaScript、图片等),可以考虑使用 CDN(内容分发网络)来加速资源的传输,提高页面加载速度。