Lecture7 Node.js 与前端开发实战 | 青训营笔记

139 阅读4分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 7 天

7.1 Node.js的应用场景

  • 前端工程化(基于Node.js开发的工具大量出现)
  • Web服务端应用
  • Electron跨端桌面应用(VSCode就是基于它实现的)

前端工程化

  • Bundle:打包工具webpack, vite
  • Uglify:代码优化(压缩)
  • Transpile:代码转换babeljs, ts
  • 其它语言加入竞争:esbuild, parcel, prisma
  • 现状:Node.js难以替代

Web服务端应用

  • 学习曲线平缓,开发效率较高
  • 运行效率接近常见的编译语言(不需要编译环境)
  • 社区生态丰富(npm),工具链成熟(V8 inspector就是前端的F12开发者工具)
  • 与前端结合的场景有优势(SSR)
  • 现状:竞争激烈,Node.js有自己的优势

Electron跨端桌面应用

  • 商业应用
  • 大型公司的效率工具
  • 现状:大部分场景选型时值得考虑

Node.js在字节

  • BFF应用:谁需要接口,谁去开发;后端不再需要和前端结合那么紧密
  • SSR应用:服务端渲染
  • 服务端应用:后端直接用Node.js
  • Electron:飞书飞连
  • Node.js在字节的生态:健康发展

7.2 Node.js运行时结构

  • npm外部包代码,acron,node-inspect,用户代码
  • Node.js核心代码(JS),N-API
  • Node.js核心代码(底层C++)
  • V8引擎JS运行时,libuv操作系统api,nghttp2,zlib压缩和解压算法,c-ares查询DNS,llhttp协议解析,OpenSSL网络层加解密……

V8, libuv

  • V8:JS运行时,诊断调试工具
  • libuv:事件循环、系统调用
  • 举例:node-fetch发起请求
    • 先安装到用户代码处
    • 调用时会在V8运行
    • 调用底层JS模块和C++模块
    • 数据通过libuv和llhttp传输

特点

  • 异步IO
    • 执行I/O操作的时候,在响应返回之后恢复操作,而不是阻塞线程等待
    • 当前线程可以调用其它的东西了,效率更高
  • 单线程
    • JS主线程是单线程,实际上还有其它线程(uv线程池,V8任务线程池,V8 inspector线程)
    • 优点:不用考虑多线程状态同步问题(不需要锁),比较高效地利用系统资源
    • 缺点:阻塞会产生更多的负面影响(用多进程/多线程解决)
  • 跨平台
    • 大部分功能和api可以跨平台
    • Node.js跨平台+JS无需编译环境+Web工具跨平台+诊断工具跨平台
    • 开发成本低,学习成本低

7.3 编写Http Server

其实是举个例子

7.3.0 安装Node.js

  • Mac, Linux推荐nvm,多版本管理
  • WIndows推荐nvm4w或官方安装包

设置安装源

NVM_NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node nvm install 16

7.3.1 编写http server

一个简单的server,返回一个helloworld

const http = require('http')
const port = 3000
const server = http.createServer((req, res) => {
  res.end('hello')
})

server.listen(port, () => {
  console.log(`server listening on port ${port}`)
})

运行:node http_server.js

取出request内容的一个server

const http = require('http')
const port = 3000
const server = http.createServer((req, res) => {
  const bufs = [] //缓冲区
  req.on('data', (buf) => {
    bufs.push(buf) //读取数据存到缓冲区
  })
  req.on('end', () => {
    const buff = Buffer.concat(bufs).toString('utf-8') //拼接并转换成字符串
    try {
      const ret = JSON.parse(buff) //JSON解析
      const msg = ret.msg || 'Hello'
      const responseJSON = {
        msg: `reveived: ${msg}`
      }
      res.setHeader('Content-Type', 'application/json') //添加头
      res.end(JSON.stringify(responseJSON)) // 序列化
    } catch (error) {
      res.end('Invalid JSON !')
    }
  })
})

server.listen(port, () => {
  console.log(`server is listening on port ${port}`)
})

这段代码实现了一个监听在3000端口的服务器,它会读取请求,并将msg(如有)传回,如果没有msg则传回hello。回传的数据格式为JSON

7.3.2 编写http client

const http = require('http')
const msg = JSON.stringify({
  msg: 'Test message.',
})

const req = http.request('http://127.0.0.1:3000', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  }
}, (res) => {
  const bufs = []
  res.on('data', (buf) => {
    bufs.push(buf)
  })
  res.on('end', () => {
    const buff = Buffer.concat(bufs)
    const json = JSON.parse(buff)
    console.log(`get message: ${json}`)
  })
}) //创建请求以及对于响应的回调
req.end(msg)

这段代码的客户端向服务端发送了一个请求,body是一个键值对(msg),受到服务器的响应之后会将其打印在控制台上

7.3.3 通过Promise重构

技巧:callback转换为Promise

function wait(t) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve()
    }, t)
  })
}

上面的代码将定时器写成了promise形式,在结束后调用的事件写在.then里面
promise适合调用一次的函数
改写思路:

  • createServer的参数函数写成async
  • 在里面,将对于返回数据的处理拆到后面
  • 对于请求的处理用await Promise包住,resolve发生在成功读取信息,reject发生在处理异常

7.3.4 编写一个静态文件服务器

const http = require('http')
const fs = require('fs')
const path = require('path')
const url = require('url')

const folderPath = path.resolve(__dirname, './static')
const server = http.createServer((req, res) => {
  const info = url.parse(req.url)
  const filepath = path.resolve(folderPath, './' + info.path)
  const filestream = fs.createReadStream(filepath)
  filestream.pipe(res)
})
const port = 3000
server.listen(port, () => {
  console.log(`server listening on ${port}`)
})

这个例子中,index.html存放在static文件夹下,用fs和path寻找到文件,使用文件流将HTML文件输出

7.3.5 React SSR

  • SSR相比传统HTML模板引擎,避免重复写代码
  • 相比SPA(single page application),首屏渲染更快,SEO友好
  • 缺点:通常qps较低,前端代码编写时需要考虑服务端渲染情况

需要使用React, ReactDOM

难点:

  • 需要处理打包代码
  • 需要思考前端代码在服务端运行时的逻辑
  • 移除对服务端无意义的副作用或重置环境

7.3.6 调试Debug

围绕V8 Inspector
启用:node --inspect test.js
场景:

  • 查看console.log
  • breakpoint
  • 高CPU、死循环:cpuprofile
  • 高内存占用:memory面板
  • 性能分析

7.3.7 部署

要解决的问题:

  • 守护进程:退出的时候拉起
  • 多进程
  • 记录状态便于诊断

容器环境:通常有健康检查手段

7.4 延伸话题

Node.js贡献代码

  • 从使用者角色逐步理解底层细节,解决更复杂的问题
  • 助于职业发展
  • 解决社区问题,促进社区发展
  • 比较花时间

编译Node.js

  • 黑盒到白盒

诊断/追踪

  • 低频,重要,有挑战
  • 技术咨询行业的热门角色
  • 需要了解Node.js底层,操作系统及工具
  • 需要经验

WASM, NAPI

  • Node.js支持WASM
  • NAPI执行C接口代码,同时保留原生代码的性能
  • 不同编程语言通信的一种方案