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

127 阅读6分钟

这是我参加[第五届青训营]伴学笔记创作活动的第7天

本堂课重点知识

  • 了解Node.js的应用场景
  • 了解Node.js运行时结构
  • 了解如何编写Http Server以及延伸话题

应用场景

1.前端工程化(现状:nodejs在这方面是难以替代)

  • 打包工具:webpack、vite、esbuild(由go实现)、parcel(由Rust实现)prisma
  • 压缩工具:uglifyjs
  • 语法转换:bablejs、typescript()

2.web服务端应用(现状:语言竞争激烈,Node.js有自己独特的优势)

  • 学习曲线平缓,开发效率较高
  • 运行效率接近常见的编译语言
  • 社区生态丰富及工具链成熟(npm,V8 inspector)
  • 与前端结合的场景会有优势(SSR)

3.Electron跨端桌面应用(现状:大部分场景在选型时,都值得考虑)

  • 商业应用:vscode,slack,discord,zoom
  • 大型公司内部桌面的效率工具

4.Node.js在字节中的应用

  • BFF应用、SSR应用,举例:Modern.js--接口拼接和裁剪
  • 服务端应用,举例:头条搜索,西瓜视频,懂车帝
  • Electron应用:飞连,飞书
  • 每年新增1000+Node.js应用

Node.js运行时结构

image.png V8:JavaScript Runtime,诊断调试工具(inspector) libuv:eventloop(事件循环),syscall(系统调用)
举例:用node-fetch发起请求时

首先用npm安装node-fetch模块后会到用户代码,然后在代码中调用node-fetch模块,因为这些代码为JavaScript代码,因此会找到V8去执行,因为node-fetch是含http模块,所以调用的是Node.js Core(JavaScript)模块,然后这个模块再调用底层的Node.js Core(C++)的API,那么就可能调用到llhttp协助完成http的序列化和反序列化工作,然后将得到的数据通过libuv创建TCP连接发送给远端,同理,远端传送过来的数据通过libuv的事件循环得到消息获取数据,然后再给llhttp解析,再将数据给Node.js Core(JavaScript)中的js代码,最后传给用户代码就会收到整个数据。

特点

  • 异步I/O
  • 单线程
  • 跨平台 image.png nodejs12开始可以使用worker_thread
异步I/O

当Nodejs执行I/O操作时,会在响应返回后恢复操作,而不是阻塞线程并占用额外内存等待(异步调用,可先处理其他调用,异步处理请求返回数据后执行回调) image.png

单线程
  • JS单线程
    • 实际:JS线程+uv线程池+V8任务线程池+V8 Inspector线程
  • 优点:不用考虑多线程状态同步问题,也就不需要锁;同时还能比较高效地利用系统资源;
  • 缺点:阻塞会产生更多负面影响
    • 解决办法:多进程或多线程
跨平台
  • 跨平台(大部分功能、api需要该平台特有的或底层的来提升性能)
  • Node.js跨平台+JS无需编译环境(+Web跨平台+诊断工具跨平台)=开发成本低(大部分场景无需担心跨平台问题),整体学习成本低

编写Http Server

0.安装Node.js

image.png 考虑多个nodejs版本问题,采用nvm进行管理 mac和linux

1.编写Http Server+Client,收发GET, POST请求

Http Server

1.Hello World image.png 测试 在终端文件对应目录输入node http_server.js,等"listening on: 3000"出现后打开浏览器输入localhost:3000后页面显示“hello”。 2.返回JSON数据

const http = require('http')
const port =3000

//回调函数
const server = http.createServer((req, res) => {
  const bufs = []
  req.on('data',(buf)=>{
    //获取数据后将数据存储在bufs中
    bufs.push(buf)
  })
  //当end事件触发后表明数据传输完成
  req.on('end',()=>{
    //连接每一段bufs
    const buf = Buffer.concat(bufs).toString('utf8')
    console.log("1"+buf)
    let msg = 'Hello'
    console.log("2"+msg)
    try {
        const ret = JSON.parse(buf)
        console.log("3"+ret)
        msg = ret.msg
        console.log("4"+msg)
    } catch (err) {
        // res.end('invalid json')
    }
    const responseJson = {
        msg:`receive: ${msg}`
    }
    res.setHeader('Content-Type','application/json')
    res.end(JSON.stringify(responseJson))
  })
})

server.listen(port, ()=>{
    console.log('listening on:',port)
})

测试 在终端文件对应目录输入node http_server_json.js,"listening on: 3000"出现后打开浏览器输入localhost:3000后页面显示{"msg":"receive: Hello"}。
注意 responseJson中的msg一定要用“`”这个引号包含起来,否则${msg}接收不到。

Http Client

发送一个post请求

const http = require('http')

//创建body
const body = JSON.stringify({
    msg: 'Hello from my own client',

})
//创建请求
const req=http.request('http://127.0.0.1:3000',{
    method: 'POST',
    headers:{
        'Content-Type':'applocation/json'
    }
},(res)=>{
    const bufs=[]
    res.on('data',(buf)=>{
        bufs.push(buf)
    })
    res.on('end',()=>{
        const buf =Buffer.concat(bufs)
        const json=JSON.parse(buf)

        console.log("json.msg is",json.msg)
    })
})
//发送body
req.end(body)

测试 在服务端3000端口还在运行的时候,新建终端将文件对应目录输入node http_client.js,在终端上显示json.msg is receive: Hello from my own client。


Promisify

用Promise+async await重写这个例子 原因:回调函数虽然在很多场景可以直接用,但不太好维护和管理,无法确定触发回调函数的时机,而有些需要在回调函数中实现就会写的深,并且函数之间的关联难以通过代码察觉
技巧 将callback转换成promise
并非所有回调函数都能改成promise,比如http_server_json,但可以改成http.createServer(async(req, res)...)
修改后

const http = require('http')
const port =3000

//回调函数
const server = http.createServer(async(req, res) => {
  //receive body from client
  const msg = await new Promise((resolve,reject)=>{
    const bufs = []
  req.on('data',(buf)=>{
    //获取数据后将数据存储在bufs中
    bufs.push(buf)
  })
  req.on("error",(err)=>{
     reject(err)
  })
  //当end事件触发后表明数据传输完成
  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) {
        // res.end('invalid json')
    }
    resolve(msg)
  })
  })
  
  //response
    const responseJson = {
      msg:`receive: ${msg}`
  }
  res.setHeader('Content-Type','application/json')
  res.end(JSON.stringify(responseJson))
})

server.listen(port, ()=>{
    console.log('listening on:',port)
})

2.编写静态文件服务器

首先在同一目录下创建一个static的文件夹并且添加一个index.html的文件 image.png 测试 用node启动后会发现直接输是无法访问,输入localhost:3000/index.html,才能从static文件中访问到index页面。
与高性能、可靠的服务相比,还差什么?
1.CDN:缓存+加速
2.分布式储存,容灾
外部服务:cloudflare,七牛云,阿里云,火山云...

3.编写ReactSSR服务以及渲染原理

SSR (server side rendering)有什么特点?

  • 相比传统HTML模版引擎:避免重复编写代码
  • 相比SPA(single page application):首屏渲染更快,SEO 友好
  • 缺点:
    • 通常qps较低,前端代码编写时需要考虑服务端渲染情况 image.png
const React =require('react')
const ReactDOMServer = require('react-dom/server')
const http = require('http')

function App(props){
    // return <div>hello</div>
    return React.createElement('div',{},props.children || 'Hello')
}

const server = http.createServer((req, res) => {
    res.end(`
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>My Application</title>
    </head>
    <body>
        ${ReactDOMServer.renderToString(React.createElement(App,{},'my_content'))}
       <script>
          //不会渲染,此处可以初始化react应用
        </script>
    </body>
    </html>
    `)
  })
  server.listen(port, ()=>{
      console.log('listening on:',port)
  })

SSR难点:
1.需要处理打包代码
tmp.js
require('./static/style.css') 2.需要思考前端代码在服务端运行时的逻辑
async componentDidMount(){
const res = await fetch('http://my.server.domain')
3.移除对服务端无意义的副作用,或重置环境

4.适用inspector进行调试、诊断

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

场景:

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

5.部署简介

  • 部署要解决的问题
    • 守护进程:当进程退出时,重新拉起
    • 多进程:cluster便捷地利用多进程
    • 记录进程状态,用于诊断
  • 容器环境
    • 通常有健康检查的手段,只需考虑多核cpu利用率即可

延伸话题

贡献代码

快速了解Node.js代码 Node.js Core贡献入门

  • 好处:
    • 从使用者的角色逐步理解底层细节,可以解决更复杂的问题;
    • 自我证明,有助于职业发展;
    • 解决社区问题,促进社区发展;
  • 难点:
    • 花时间
编译

为什么要学习编译Node.js

  • 认知:黑盒到白盒,发生问题时能有迹可循
  • 贡献代码的第一步

如何编译

  • 参考:Maintaining the build files
  • ./configure && make install
  • 演示:给net模块添加自定义属性
诊断与追踪
  • 诊断是一个低频、重要同时也相当有挑战的方向。是企业衡量自己能否依赖一门语言的重要参考。
  • 技术咨询行业中的热门角色。
  • 难点:
    • 需要了解Node.js底层,需要了解操作系统以及各种工具
    • 需要经验

image.png

WASM,NAPI
  • Node.js (因为V8)是执行WASM代码的天然容器,和浏览器器WASM是同一运行时,同时 Node.js支持 WASI。
  • NAPI执行C接口的代码(C/C++/Rust...),同时能保留原生代码的性能。
  • 不同编程语言间通信的一种方案。

课后