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

75 阅读5分钟

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

一、本堂课重点内容:

  • Node.js的应用场景(why)
  • Node.js运行时结构(what)
  • 编写Http Server(how)
  • 延伸话题

二、详细知识点介绍:

1. Node.js的应用场景
  • 前端工程化

Nodejs的出现加速了这些前端工程化的工具的出现。

用于打包的工具:Bundle:webpack、vite、ebuild、parcel

代码压缩工具:Uglify:uglifyjs

语法转换工具:Transpile:bablejs,typescript

现状:尽管有其他语言的加入,Nodejs学习成本较低,地位仍处于难以替代的地位。

  • Web服务端应用

Nodejs可以做到其他后端语言能做到的事情,且学习曲线平缓,开发效率高,运行效率接近常见的编译语言,社区生态丰富且工具链成熟(npm,VB inspector),对于与前端结合的场景会有优势(SSR)。

现状:多种语言竞争激烈,但Nodejs有自己独特的优势。

  • Electron跨端桌面应用

有非常多应用都是基于Electron进行开发的,因为它基于跨端方面有较大的优势。

例如vscode、slack、discord、zoom等商业应用,还有许多大型公司内部的效率工具。

现状:大部分场景在选型时,都值得考虑。

2. Node.js运行时结构
  • Nodejs的组成部分:

    acron

    node-inspect:调试

    npm

    用户代码:业务代码和外部导入的包

    Node.js Core(JavaScript):Node.js模块,JavaScript代码有时会调用C++代码进行一些功能实现。

    N-API:通过JavaScript无法满足需求时,例如性能需求,就会用更Native的语言与JavaScript通信,这种时候就会用到N-API。

    Node.js Core(C++):Node.js模块,作为底层模块存在。

    V8:JavaScript Runtime,诊断调试工具(inspector)。

    libuv:封装操作系统api,提供跨平台I/O操作,包括eventloop(事件循环),syscall(系统调用)。

    nghttp2:http2相关模块

    zlib:常见压缩和解压缩算法

    c-ares:DNS查询库

    llhttp:http协议解析

    OpenSSL:网络层面的加密和解密

  • 特点:

    异步I/O

    当Node.js执行I/O操作时,会在响应返回后恢复操作,而不是阻塞线程并占用额外内存等待。

    例:example.ts

    setTimeout({} => {
        console.log('B')
    })
    console.log('A')
    

    单线程

    JS单线程实际:JS线程 + uv线程池 + v8任务线程池 + v8 Inspector线程

    优点:不用考虑多线程状态同步问题,也就不需要锁;同时还能比较高效地利用系统资源。

    缺点:阻塞会产生更多负面影响,所以解决办法为多进程或多线程。

    例:example.ts

    function fibonacci(num: number): number {
        if(num === 1 || num === 2){
            return 1;
        }
        return fibonacci(num - 1) + fibonacci(num - 2);
    }
    
    fibonacci(42)
    fibonacci(43)
    

    跨平台

    Node.js跨平台 + JS无需编译环境(+ Web跨平台 + 诊断工具跨平台) = 开发成本低(大部分场景无需考虑跨平台问题),整体学习成本低。

    const net = require('net')
    
    const socket = new net.Socket('/tmp/socket.sock')
    
3. 编写Http Server
  1. 安装Node.js

Mac、Linux推荐使用nvm进行多版本管理。

Windows推荐使用nvm4w或官方安装包。

安装慢或失败的情况可以设置安装源

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

    server:每次接收到http请求,触发回调函数。

    res:客户端传入数据。

    req:服务端返回数据。

    port:监听的端口号。用server.listen进行监听,监听到后就调用回调函数,打印listening on,server返回hello显示在客户端。

Server:

Hello world:

```
const http = require('http')

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

const port = 3000

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

json:

```
const http = require('http')

const server = http.createServer((req,res) => {
    const bufs = []
    req.on('data',(buf) => {
        bufs.push(buf)
    })
    req.on('end',() => {
        const buf = Buffer.concat(bufs).toString('utf8')
        let msg = 'hello'
        try{
            const ret = JSON.parse(buf)
            msg = ret.msg 
        } catch (err) {
            //
        }
        const responseJson = {
            msg:'recetive: ${msg}'
        }
        res.setHeader('Content-type, 'application/Json')
        res.end(Json.stringfy(responeJson))
    })
})

const port = 3000

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

Client:

```
const http = require('http')

const body = JSON.stringfy({
    msg: 'Hello from my own  client',
})

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 buf = Buffer.concat(bufs)
        const json = JSON.parse(buf)
        
        console.log('json.msg is:', json.msg)
    })
})

req.end(body)
    
```

可以用Promise和async await 重写这两个例子,把callback转换成promise。但只适用于只会被调用一次的回调函数。

json:

```
const http = require('http')

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.push(buf)
        })
        req.on('error',(err) => {
            reject(err)
        })
        req.on('end',() => {
            const buf = Buffer.concat(bufs).toString('utf8')
            let msg = 'hello'
            try{
                const ret = JSON.parse(buf)
                msg = ret.msg 
            } catch (err) {
                //
            }
            
            resolve(msg)
        })
    })
    
    
    //respone
    const responseJson = {
        msg:'recetive: ${msg}'
    }
    res.setHeader('Content-type, 'application/Json')
    res.end(Json.stringfy(responeJson))
      
})

const port = 3000

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

2. 编写静态文件服务器

编写一个服务,接收用户发送的http请求,处理http请求参数,也就是获取url,读取文件内容,返回给这个请求。

除了使用http模块,还要使用fs模块、path模块和url模块,path模块可以进行绝对和相对路径的转化。

例:使用stream风格api进行编写

```
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) => {
    // expected 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
    const filestream= fs.createReadStream(filepath)
    filestream.pipe(res)
    
})

const port = 3000

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

访问localhost:3000/index.html即可获得index.html页面内容。

  • 与高性能、可靠的服务相比,还需要:

CDN:缓存+加速

分布式储存,容灾

例如一些优秀的外部服务:cloudflare,七牛云,阿里云,火山云。

  1. 编写React SSR服务
  • SSR特点:

相比传统HTML模板引擎:避免重复编写代码。

相比SPA:首屏渲染更快,SEO友好

缺点:通常qps较低,前端代码编写时需要考虑服务端渲染情况。

  • HTML例子:
const React = require('react')
const ReactDOMServer = require('react-dom/server')
const http = require('http')

//require <div>Hello<div>
function App(props) {
    return React.createElement('div',{}, 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,{}, 'my_content'))}
                <script>
                    alert('yes')
                </script>
            </body>
        </html>
        ')
    })
    
    const port = 3000

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

需要打包代码。

需要思考前端代码在服务器运行时的逻辑。

一处对服务端无意义的副作用,或重置环境。

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

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

场景:查看console.log内容、breakpoint、高CPU或死循环的场景(如cpuprofile)、高内存占用(如heapsnapshot)、性能分析等。

node --inspector 文件名 启动调试,打开http://localhost:9299/json 进行调试。

  1. 部署简介
  • 部署要解决的问题:

守护进程:当进程退出时,重新拉起。

多线程:cluster便捷地利用多进程。

记录进程状态,用于诊断

  • 容器环境:

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

4. 延伸话题
  • 了解Node.js贡献代码

好处:

从使用者的角色逐步理解底层逻辑,可以解决更复杂的问题;

自我证明,有助于职业发展;

解决社区问题,促进社区发展

难点:

花时间

  • WASM、NAPI

Node.js是执行WASM代码的天然容器,和浏览器WASM是同一个运行时,同时Node.js支持WASI。

NAPI执行C接口的代码(C/C++/Rust),同时能保留原生代码的性能。

不同编程语言间通信的一种方案

三、课后个人总结:

我学习到了关于Node.js的应用场景和运行时结构,并且通过实际编写Http Server,懂得了Node.js的实际运用方式,以及调试方法。