Node.js初识 | 青训营笔记

151 阅读5分钟

Node.js初识 | 青训营笔记

这是我参与「第四届青训营 」笔记创作活动的的第10天。

什么是Node.js

  • Node.js 是一个开源和跨平台的 JavaScript 运行时环境
  • Node.js 在浏览器之外运行 V8 JavaScript 引擎(Google Chrome 的内核)。
  • 它使得JavaScript可以编写后端程序,让其实现其他后端语言能实现的绝大部分功能。

Node.js的应用场景

  • 前端工程化

    早期 ajax, jquery 比较流行的时候,我们直接在页面中引入需要的 lib 即可。

    后续随着模块化、transpile 逐渐成熟、需求逐渐增多,对后端能力的需求也逐渐强烈。

    反过来说,也正是 Node.js 赋予了 js 开发者在浏览器外运行代码的能力,加速催生了这些项目的出现。

    • Bundle(构建):webpack,vite,esbuild,parcel
    • Uglify(压缩):uglifyjs
    • Transpile(语法转换):bablejs,typescript
    • 其他语言加入竞争:esbuild,parcel,prisma
    • 现状:难以替代
  • Web服务端应用(即后端服务)

    • 学习曲线平缓,开发效率较高

    • 运行效率接近常见的编译语言

    • 社区生态丰富工具链成熟(npm,V8 inspector)

    • 与前端结合的场景会有优势(SSR)

    • 现状:竞争激烈,Node.js有自己独特的优势

    较为突出的一个特点是 服务端渲染 ,我们希望把前端代码用在模板引擎代码的渲染上面, 让它去生成HTML,而不需要自己重复去写HTML代码,这样的话就是要执行JavaScript代码, 这种场景就是说基本上只有node.js可以做

  • Electron跨端桌面应用

    这里不只是指 electron,还包括 nw.js..

    • 商业应用:vscode,slack,discord,zoom
    • 大型公司内的效率工具
    • 现状:大部分场景在选型的时候都值得考虑
  • Node.js在字节

    • BFF应用、SSR应用,距离:Modern.js
    • 服务端应用:头条搜索,西瓜视频,懂车帝
    • Electron应用:飞连,飞书
    • 每年新增1000+Node.js应用

Node.js运行时结构

image.png

  • V8:JavaScript Runtime,争端调试工具(inspector)

  • libuv:eventloop(事件循环),syscall(系统调用)

    libuv实现事件循环,封装操作系统上的各种API实现跨平台的I/O操作

  • 举例:用 node-fetch 发起请求时

    解释:结合上图阐述 node-fetch 是如何运行的

    用 node-fetch 发起请求时,首先通过npm安装node-fetch模块,之后在用户代码里面调用node-fetch模块,这些代码会在V8中执行,那么node-fetch执行是调用底层http模块(Node.js Core(JavaScript)),再去调用更底层的Node.js Core(c++),之后可能调用llhttp去做http的序列化与反序列化,然后把得到的数据再通过libuv,可能调用TCP连接,再把这个数据发给远端,远端返回数据,在事件循环中得到这个数据,在把拿到的数据给llhttp解析出来,得到的数据返回给Node.js Core(JavaScript),最后送到用户代码从而得到最终的数据。

  • 特点

    • 异步IO

      • 执行IO操作的时候不需要阻塞线程执行,可以去做其他调用,不会占用额外内存等待
      • 当调用异步API(fs.readFile())读取文件时,会把读取文件的操作交给libuv的线程池去做,此时就不用等到文件读取完,就可以做其他调用,之后文件读取的操作从node线程处理完返回给主线程执行回调,然后在做其他事情

      image.png

    • 单线程

      • 不适合做CPU密集的工作
      • 但是在新版本中,woker_thread可以独立线程,但每个线程的模型没有太大变化
      • 其实是说JS单线程:实际Node.js还是有很多线程的 :JS线程+uv线程池+V8任务线程池+V8 Inspector线程
      • 优点:不用开率多线程状态同步问题,也不需要锁,同时还能比较高校的利用系统资源
      • 缺点:阻塞会产生更多负面影响;解决办法:多进程或多线程
    • 跨平台

      • 大部分功能、api跨平台

      • Node.js跨平台+JS无需编译环境(+Web跨平台+诊断工具跨平台)

        • 开发成本低(大部分场景无需担心跨平台问题),整体学习成本低

安装node.js

  • Mac,Linux推荐使用nvm。多版本管理
  • Windows nvm4w 或官方安装包
  • node.js下载
  • 安装慢,安装失败的情况,设置安装源

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

编写HttpServer

  • HelloWorld
 const http = require('http')
 //创建server
 const server = http.createServer((req,res)=>{
     res.end('hello')
 })
 //监听端口(3000)
 const port = 3000
 server.listen(port,()=>{
     //成功监听,触发回调函数
     console.log(`server listens on: ${port}`)
 })
  • 返回Json
 const http = require('http')
 //创建server
 const server = http.createServer((req,res)=>{
     const bufs=[]
     //从request取出body,绑定data事件,当连接返回数据收集起来
     req.on('data',buf=>{
     bufs.push(buf)
     })
     req.on('end',()=>{
         //bufs
         const buf = Buffer.concat(bufs).toString('utf8')
         let msg='hello'
         try{
             const ret =  JSON.parse(buf)
             msg = ret.msg
         
         }catch(err){
             //如果用户没有传给我们json数据,返回该内容
             //res.end('invalid json')
         }
         const responseJson = {
             msg: `receive:${msg}`
         }
         res.setHeader('Content-Type','application/json')
         //序列化返回json
         res.end(JSON.stringify(responseJson))
     })
 })
 //监听端口(3000)
 const port = 3000
 server.listen(port,()=>{
     //成功监听,触发回调函数
     console.log(`server listens on: ${port}`)
 })
  • Client
 const http = require('http')
 const body = JSON.stringify({msg:'hello from my client'})
 const req = http.request('http://localhost:3000',{
     method:'POST',
     headers:{
         'Content-Type':'application/json',
         'Content-Length':body.length,
     },   
 },(res)=>{
     const bufs=[]
     res.on('data',data=>{
         bufs.push(data)
     })
     res.on('end',()=>{
         const receive = JSON.parse(Buffer.concat(bufs).toString())
         console.log('receive:',receive)
     })
 })
 req.end(body)
  • Promisify

    • 用Promise+async await重写
    • 技巧:将callback转成promise
 const http = require('http')
 //创建server
 const server = http.createServer(async(req,res)=>{
     //接收body数据
     const msg = await new Promise((resolve, reject) => {
         const bufs=[]
         req.on('data',buf=>{
             bufs.push(buf)
         })
         req.on('error',(err)=>{
             reject(err)
         })
         req.on('end',()=>{
             //bufs
             const buf = Buffer.concat(bufs).toString('utf8')
             let msg = 'hello'
             try{
                 const ret =  JSON.parse(buf)
                 msg = ret.msg
                 
             }catch(err){
                 //如果用户没有传给我们json数据,返回该内容
                 //res.end('invalid json')
             }
             resolve(msg)
         })
     })  
     //根据client数据,返回服务端数据
     const responseJson = {
         msg: `receive:${msg}`
     }
     res.setHeader('Content-Type','application/json')
     //序列化返回json
     res.end(JSON.stringify(responseJson))
 })
 //监听端口(3000)
 const port = 3000
 server.listen(port,()=>{
     //成功监听,触发回调函数
     console.log(`server listens on: ${port}`)
 })
  • 静态文件服务

    • 与高性能可靠的服务相比还差什么

      • CDN:缓存+加速
      • 分布式存储,容灾
 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)=>{
     //期望收到http://127.0.0.1:3000/index.html?abc=10
     const info =  url.parse(req.url)
     //static/index.html
     const filepath=path.resolve(folderPath,'./'+info.path)
     //stream风格api的好处:内部做了处理,占用尽可能少的空间,按需返回给client
     const filestream = fs.createReadStream(filepath)
     filestream.pipe(res)
 })
 ​
 const port = 3000
 server.listen(port,()=>{
     //成功监听,触发回调函数
     console.log(`server listens on: ${port}`)
 })
  • React SSR

    • SSR(server side rendering)特点

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

      • 需要处理打包代码
      • 需要思考前端代码在服务端运行的逻辑
      • 移除服务端无意义的副作用,或充值环境
 const React = require('react')
 const ReactDOMServer = require('react-dom/server')
 const http = require('http')
 ​
 function App(props){
     return React.createElement('div',{},props.children|| 'hello')
 }
 const server = http.createServer((req,res)=>{
     res.end(
         `
         <!DOCTYPE html>
         <html lang="en">
         <head>
             <title>Document</title>
         </head>
         <body>
         ${ReactDOMServer.renderToString(React.createElement(App,{},'my_content'))}        
             <script>
                 //初始化React应用
             </script>
         </body>
         </html>
         `
     )
 })
 const port = 3000
 server.listen(port,()=>{
     //成功监听,触发回调函数
     console.log(`server listens on: ${port}`)
 })

Debug与部署

DEBUG

  • V8 Inspector:开箱即用、特性丰富强大、与前端开发一致、跨平台

    • node --inspect

    • 查看方式:

      • open http://localhost:9229/json 进入devtoolsFrontendUrl后的url
      • open chrome://inspect然后点击Open dedicated DevTools for Node
  • 场景:

    • 查看console.log内容
    • breakpoint,logpoint
    • 高CPU、死循环: cpuprofile
    • 高内存占用: heapsnapshot
    • 性能分析

部署

  • 部署要解决的问题

    • 守护进程:当进程退出时,重新拉起
    • 多进程:cluster便捷地利用多进程
    • 记录进程状态,用于诊断
  • 容器环境

    • 通常有健康检查的手段,只需考虑多核cpu利用率即可

参考博客: