Node.js入门| 青训营笔记

63 阅读4分钟

Node.js应用场景

  • 前端工程化,难以替代
    • 打包工具:webpack、vite、esbuild、parcel
    • 代码压缩转换:uglyfyjs
    • 语法转译:bablejs、typescript
    • 其他语言加入竞争:esbuild(go)、parcel(rust)、prisma
  • Web服务端应用
    • 运行效率接近常见的编译语言
    • 社区生态丰富、工具链成熟
    • 与前端结合的场景有优势
  • Electron跨端桌面应用

Node.js运行时结构

image.png

  • V8:JavaScript Runtime,诊断调试工具(inspector)
  • libuv: eventloop (事件循环),syscall (系统调用)

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

  1. 使用npm安装的node-fetch属于用户代码部分
  2. node-fetch属于JavaScript代码,会在V8中执行
  3. 然后调用Node.js Core(JavaScript)中的HTTP模块
  4. HTTP模块再调用Node.js Core(C++)的API
  5. 调用llhttp来做HTTP协议的序列化和反序列化
  6. 再将得到的数据通过libuv创建TCP连接,将数据发给远端
  7. 远端得到数据后,在libuv循环中得到消息
  8. 将数据传给llhttp解析
  9. 然后将数据传给Node.js Core(JavaScript)代码,最后将数据传给用户代码

Node.js运行时结构

结构带来的特点:

  • 异步IO

    • 当Node.js执行IO操作时,会在响应返回后恢复操作,而不是阻塞线程并占用额外内存等待
  • 单线程(JS主线程为单线程)

    • 实际:JS线程 + libuv线程池 + V8任务线程池 + V8 Inspector线程
    • 优点:不用考虑多线程状态同步问题,不需要锁机制;可以较高效的利用系统资源
    • 缺点:阻塞会带来负面影响;
      • 解决办法:多进程或多线程
  • 跨平台(大部分功能、api)

    • Node.js + JS无需编译环境 + Web跨平台 + 诊断工具跨平台
    • 开发成本低、学习成本低

编写HTTP Server

  1. 安装Node.js
  2. 编写HTTP Server + Client,收发GET、POST请求
  3. 编写静态文件服务器
  4. 编写React SSR服务
  5. 使用Inspector进行调试、诊断
  6. 部署简介

HTTP Server

使用Node.js开启http server:

const http = require('http')

// 创建http server,req为客户端传来的数据,res为传回客户端的数据
const server = http.createServer((req, res) => {
    res.end('hello')    // end:结束传送
})

const port = 3000

// listen的回调函数会在server监听到端口后调用
server.listen(port, () => {
    console.log('listening on: ', port)
})

JSON server

创建JSON server,获取一段请求,然后从请求中取出数据,再通过response返回给用户:

const http = require('http')

// 创建http server,req为客户端传来的数据,res为传回客户端的数据
const server = http.createServer((req, res) => {
    const bufs = []
    // on('data', () => {}):表示当接收到数据时触发回调函数
    req.on('data', (buf) => { 
        bufs.push(buf)
    })
    // on('end', () => {}):表示当数据接收完时触发回调函数
    req.on('end', () => {
        const buf = Buffer.concat(bufs).toString('utf8')
        let msg = 'hello'   // 当没有接收数据时,返回hello
        try {
            // 当可以解析为JSON时,对内容进行处理
            const ret = JSON.parse(buf)
            msg = ret.msg
        } catch (err) {
            //
        }

        const responseJson = {
            msg: `receive: ${msg}`
        }
        res.setHeader('Content-type', 'application/json')
        res.end(JSON.stringify(responseJson))
    })
})

const port = 3000

// listen的回调函数会在server监听到端口后调用
server.listen(port, () => {
    console.log('listening on: ', port)
})

HTTP Client

编写Client给HTTP Server发送POST请求,传送数据:

const http = require('http')

// 发送数据内容
const body = JSON.stringify({
    msg: 'Hello from my own client',
})

// 使用request函数创建请求
const req = http.request('http://127.0.0.1:3000', {
    method: 'POST',
    headers: {
        // 设置header,提示发送的数据为JSON数据
        'Content-Type': 'application/json',
    }
}, (res) => {   // 当接收到response时触发回调函数
    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)
    })
})

// 发送数据内容
req.end(body)

Promise + async await

上述代码中使用了大量的回调函数,大量的回调函数会变得难以维护,不容易分析回调函数的触发时间,且会导致函数关系较乱。

Promise可以更加清晰的处理异步请求,将异步请求改写为有前后时序的代码,Promise相关信息:JavaScript Promise

所以可以将callback的代码改写成promise的代码:

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

wait(1000).then(() => { console.log('get called') })

使用Promise方法改写json server:

// promise代码只适合只会被调用一次的回调函数
// 需要多次调用的回调函数可以使用async关键字,使用await调用Promise
const server = http.createServer(async (req, res) => {
    // 接收body数据,msg会接收resolve()方法返回的结果
    const msg = await new Promise((resolve, reject) => {
        const bufs = []
        
        req.on('data', (buf) => { 
            bufs.push(buf)
        })
        
        // on('error', () => {}):表示当出现错误时触发回调函数
        req.on('error', (err) => {
            reject(err)    // 使用reject()方法抛出异常
        })
        
        req.on('end', () => {
            const buf = Buffer.concat(bufs).toString('utf8')
            let msg = 'hello'   // 当没有接收数据时,返回hello
            try {
                // 当可以解析为JSON时,对内容进行处理
                const ret = JSON.parse(buf)
                msg = ret.msg
            } catch (err) {
                reject(err)
            }

            resolve(msg)    // 使用resolve返回需要传回client的数据
        })
    })
    

    // 返回response数据
    const responseJson = {
        msg: `receive: ${msg}`
    }
    res.setHeader('Content-Type', 'application/json')
    res.end(JSON.stringify(responseJson))
})

静态文件

编写简单的静态文件服务:

const http = require('http')
const fs = require('fs')        // 文件读写模块
const path = require('path')    // 路径处理模块
const url = require('url')      // url处理模块

const folderPath = path.resolve(__dirname, './static')

// 创建http server,req为客户端传来的数据,res为传回客户端的数据
const server = http.createServer((req, res) => {
    // http://127.0.0.1:3000/index.html?abc=10
    const info = url.parse(req.url)     // 对用户的url进行解析
    console.log(info);

    // static/index.html
    const filepath = path.resolve(folderPath, './' + info.path)
    console.log(filepath);

    // stream api可以优化内存使用率
    // 读取文件
    const filestream = fs.createReadStream(filepath)    
    // 返回文件内容
    filestream.pipe(res)
})

const port = 3000

// listen的回调函数会在server监听到端口后调用
server.listen(port, () => {
    console.log('listening on: ', port)
})

React SSR

SSR(Server Side Rendering)的特点:

  • 避免了重复编写代码
  • 首屏渲染更快,SEO友好
    • 缺点:通常qps较低,前端代码编写时需要考虑服务端渲染情况

编写简单的React SSR:

const React = require('react')
const ReactDOMServer = require('react-dom/server')
const http = require('http')

// 创建React.FunctionComponent
function App(props) { 
    return React.createElement('div', {className: 'je'}, props.children || 'Hello')
}

const server = http.createServer((req, res) => {
    res.end(`
        <!DOCTYPE html>
        <html>
        <head>
            <title>My Application</title>
        </head>
        <body>
            ${ReactDOMServer.renderToString(React.createElement(App, {}, 'props.children'))}
        </body>
        </html>
    `)
})

const port = 3000

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

SSR难点:

  • 需要处理打包代码,处理类似CSS文件的打包方式
  • 需要考虑前端代码在服务端运行时的逻辑,如将前端代码放在服务端渲染之后
  • 要移除对服务端毫无意义的副作用,或重置环境

Debug

V8 Inspector:开箱即用、强大丰富、与前端调试工具一致、跨平台,使用方法:

  • 在运行代码时添加--inspect参数:node --inspect example.js
  • 在浏览器打开链接:http://127.0.0.1:9229/json

V8 Inspector的常见使用场景:

  • 查看Console.log的打印内容
  • 设置断点及logpoint
  • 分析CPU行为、死循环等
  • 查看内存占用
  • 性能分析

部署

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