likes
comments
collection
share

Node.js 基础笔记

作者站长头像
站长
· 阅读数 31

Node.js 的应用场景

  • 前端工程化

    • 打包工具(bundle):webpack、vite、esbuild、parcel
    • 代码压缩(uglify):uglifyjs
    • 语法转换(transpile):bablejs、typescript
    • 其他语言加入竞争:esbuild(golang)、parcel(rust)、prisma
    • Node.js 现状:在前端工程化领域仍然难以替代
  • Web 服务端应用

    • 学习曲线平缓,开发效率较高
    • 运行效率接近常见的编译语言
    • 社区生态丰富以及工具链成熟(npm、V8 inspector)
    • 与前端结合的场景会有优势(服务端渲染ssr)
    • 现状:竞争激烈,Node.js 有自己独特的优势
  • Electron 跨端桌面应用

    • 商业应用:vscode、slack、discord、zoom
    • 大型公司内的效率工具
    • 现状:大部分场景在选型时,都值得考虑
  • 其他应用场景

    • BFF(Backends For Frontends)应用:作为中间件自由处理后端接口,使得前后端开发更加分离
    • SSR(Server Side Render)应用:将组件或页面通过服务器生成 HTML 字符串,再发送到浏览器

Node.js 运行时结构

  • Node.js 组成Node.js 基础笔记

    • V8:JavaScript Runtime,诊断调试工具(inspector)
    • libuv:eventLoop(事件循环),syscall(系统调用)
    • Node.js 运行示例(以 node-fetch 为例):Node.js 基础笔记
  • Node.js 运行时结构特点

    • 异步 I/O

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

      • JavaScript 是单线程的(只有一个主线程),但实际在 Node.js 环境中是:JavaScript 单线程 + libuv 线程池 + V8 任务线程池 + V8 inspector 线程
      • 比如读取文件(常见的I/O操作)时,将该任务交给 libuv 线程池去操作,JS 主线程便可以进行其他任务
      • V8 inspector 单独作为一个线程的作用:比如写了一个死循环,常规情况无法再去调试,便可以利用V8 inspector 线程调试该死循环
      • 优点:不用考虑多线程状态同步问题,也就不需要锁,同时还能比较高效地利用系统资源
      • 缺点:阻塞会产生更多负面影响,解决办法:多进程或多线程
    • 跨平台

      • Node.js 跨平台 + JS 无需编译环境 + Web 跨平台 + 诊断工具跨平台
      • 优点:开发成本低,整体学习成本低

编写 HTTP Server

  • 编写一个简单的 server 服务,使得浏览器打开主机的3000端口看到 hello!nodejs!

    const http = require('http');
    const port = 3000;
    const server = http.createServer((req, res) => {
        res.end('hello!nodejs!');
    })
    
    server.listen(port, () => {
        console.log('success!', port);
    });
  • 把用户请求中的数据转换为 JSON 格式,并在响应中返回给浏览器

    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 buf = Buffer.concat(bufs).toString('utf-8');
            let msg = 'hello!'
            try {
                const ret = JSON.parse(buf);
                msg = ret.msg;
            }
            catch (err) {
                //这里请求数据不合法暂时不做处理(因为无法发送合法数据),msg的值仍是hello
            }
            // 处理响应 
            const responseJson = {
                msg: `receive : ${msg}`,
            }
            // 响应头设置Content-Type
            res.setHeader('Content-Type', 'application/json');
            res.end(JSON.stringify(responseJson));
        });
    });
    
    server.listen(port, () => {
        console.log('success!', port);
    });
  • 搭建一个客户端,能给server发送post请求

    const http = require('http');
    const port = 3000;
    
    const body = JSON.stringify({
        msg: 'Hello from my client',
    })
    
    const req = http.request('http://127.0.0.1:3000', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        }
    }, (res) => {
        // 拿到响应以后,转换为JSON格式,输出在控制台
        const bufs = [];
        res.on('data', (buf) => {
            bufs.push(buf);
        })
        res.on('end', () => {
            const buf = Buffer.concat(bufs);
            const res = JSON.parse(buf);
            console.log('json.msg is : ', res);
        })
    });
    // 发送请求
    req.end(body);
  • 用 promise + async await 重写这两个例子

    const http = require('http');
    const port = 3000;
    
    const server = http.createServer(async (req, res) => {
        // recieve 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('utf-8');
                let msg = 'hello!nodejs!'
                try {
                    const ret = JSON.parse(buf);
                    msg = ret.msg;
    
                }
                catch (err) {
                    //
                }
                resolve(msg);
            });
        })
    
        // response
        const responseJson = {
            msg: `receive : ${msg}`,
        }
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify(responseJson));
    });
    
    server.listen(port, () => {
        console.log('success!', port);
    });
  • 搭建一个可以读取服务器上静态资源的 server

    const http = require('http');
    const fs = require('fs');
    const url = require('url');
    const path = require('path');
    
    // 文件路径
    const folderPath = path.resolve(__dirname, './static_server');
    
    const server = http.createServer((req, res) => {
        // expected http://127.0.0.1:3000/index.html?abc=10
        const info = url.parse(req.url);
    
        // static_server.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);
    })
    • 与高性能、可靠的静态文件服务器相比,还需要缓存、加速、分布式缓存的能力
  • SSR(server side render)有什么特点

    • 相比传统 HTML 模板引擎避免了重复编写代码
    • 相比SPA(single page application)首屏渲染更快,SEO(搜索引擎友好 Search Engine Optimization)友好
    • 缺点:通常 qps(每秒查询率 Queries-per-second) 较低,前端代码编写时需要考虑服务端渲染情况
  • 编写 React-SSR HTTP Server

    const React = require('react');
    const ReactDOMServer = require('react-dom/server');
    const http = require('http');
    
    const App = (props) => {
        // 避免编译问题不使用 jsx
        return React.createElement('div', {}, props.children || 'Hello!')
    }
    
    const port = 3000;
    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>
                  init......
                </script>
            </body >
    
            </html >
        `);
    })
    
    server.listen(port, () => {
        console.log('success!', port);
    });
  • SSR 难点:

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

    • 运行js文件时,在文件名之前加上 --inspector 指令,在浏览器输入对应的调试地址,查看 json 信息可以跳转到前端调试界面。以static_file_server为例,输入127.0.0.1:9229/json即可
    • 实际开发环境中打断点调试会比较危险,可以打 logpoint
    • Memory 面板:可以打一个 snapshot 查看 JavaScript 主线程中的内存占用信息
    • Profile 面板:可以录制一个 profile 查看 CPU 一段时间内的运行情况,可以看到调用了哪些函数。比如应用发生了死循环,可以通过 profile 查看死循环时在运行什么代码
    • 也可以通过上述数据观察代码性能,判断函数运行时间是否符合预期
  • HTTP Server -部署

    • 部署要解决的问题

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

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