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

83 阅读5分钟

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

Node.js 的应用场景

  • 前端工程化
    • Bundle:webpack, vite, esbuild, parcel
    • Uglify:uglifyjs
    • Transpile:bablejs, typescript
    • 其他语言加入竞争:esbuild, parcel, prisma
    • 现状:难以替代
  • Web服务端应用
    • 学习曲线平缓,开发效率较高
    • 运行效率接近常见的编译语言
    • 社区生态丰富及工具链成熟(npm, V8 inspector)
    • 与前端结合的场景会有优势(SSR)
    • 现状:竞争激烈,Node.js 有自己独特的优势
  • Electron跨端桌面应用
    • 商业应用:vscode, slack, discord, zoom
    • 大型公司的内部效率工具
    • 现状:大部分场景在选型时,都值得考虑

字节内部的应用场景

  • BFF(Back-end For Front-end,接口由前端提供)应用、SSR应用(举例:Modern.js)

    BFF——服务于前端的后端

  • 服务端应用(举例:头条搜索,西瓜视频,懂车帝)

  • Electron 应用(飞连,飞书)

  • 每年新增1000+ Node.js应用

Node.js运行时结构

  • N-API提供的JS的API不能提供的Native能力
  • V8是JS运行时,也提供调试工具(Inspector)
  • libuv提供系统调用(syscall)、event loop
  • nghttp2提供HTTP/2相关能力
  • zlib提供压缩能力
  • c-ares提供DNS查询能力
  • llhttp提供HTTP报文解析能力

特点

异步I/O

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

向开发者暴露的是单线程模型

绝大多数情况下,开发者完全可以认为是在开发单线程应用。当然也可以用worker_thread另行启动独立线程。

实际上Node.js内部实现了多线程的互操作:

  • JS Runtime线程
  • libuv线程池(默认4线程)
  • V8任务线程池
  • V8 Inspector线程

优点:

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

缺点:

  • JS Runtime线程中的阻塞会产生更多负面影响(解决办法:多进程或多线程)

跨平台

Node.js的大部分功能、API是跨平台的,大部分场景无需考虑platform-specific细节。

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

编写Http Server

安装Node.js

  • Mac, Linux推荐使用nvm多版本管理工具来安装。

  • Windows推荐nvm4w或是官方安装包。

  • 安装慢/安装失败的情况:设置安装源

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

编写简易 HTTP Server & Client

HTTP Server

const http = require("http");
const port = 3000;
const server = http.createServer((req, res) => {
    res.end("hello");
});
server.listen(port, () => {
    console.log(`Server is listening on port ${port}`);
});

JSON Server

const http = require("http");
const port = 3000;

const server = http.createServer((req, res) => {
    const bufs = [];
    req.on("data", data => {
        bufs.push(data);
    });
    req.on("end", () => {
        let reqData = {};
        try {
            reqData = JSON.parse(Buffer.concat(bufs).toString("utf-8"));
        } catch (err) {
            console.warn("Failed to parse json!");
        }
        res.setHeader("Content-Type", "application/json");
        res.end(JSON.stringify({
            echo: reqData.msg || "Hello",
        }));
    });
});
server.listen(port, () => {
    console.log(`JSON Server is listening on port ${port}`);
});

Promise化

上述代码中充斥着各类回调函数,不容易理清业务逻辑,容易给后续代码维护留下坑,因此尝试使用Promise+async/await重写一下:

适合改写成Promise的callback应当只被调用一次。

const http = require("http");
const port = 3000;

const server = http.createServer(async (req, res) => {
    
    // receive body from client
    const reqData = await new Promise((resolve, reject) => {
        const bufs = [];
        req.on("data", data => {
            bufs.push(data);
        });
        req.on("error", err => {
            reject(err);
        })
        req.on("end", () => {
            let reqData = {};
            try {
                reqData = JSON.parse(Buffer.concat(bufs).toString("utf-8"));
            } catch (err) {
                console.warn("Failed to parse json!");
            }
            resolve(reqData);
        });
    });

    // response
    res.setHeader("Content-Type", "application/json");
    res.end(JSON.stringify({
        echo: reqData.msg || "Hello",
    }));

});

server.listen(port, () => {
    console.log(`JSON Server is listening on port ${port}`);
});

JSON Client

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:${port}`, {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
    },
}, (res) => {
    const bufs = [];
    res.on("data", data => {
        bufs.push(data);
    });
    res.on("end", () => {
        const received = JSON.parse(Buffer.concat(bufs).toString("utf-8"));
        console.log("Received:", received);
    });
});

req.end(body);

编写静态文件服务器

这里采用Steam风格的API。Steam风格的API可以占用尽可能少的内存空间,直接readFile全部读入内存可能导致内存溢出。

const http = require("http");
const fs = require("fs");
const path = require("path");
const url = require("url");

const port = 3000;

// __dirname 表示当前文件所在的目录
const staticRootPath = path.resolve(__dirname, "./static");

const server = http.createServer((req, res) => {
    // expected url example: http://127.0.0.1:3000/index.html?abc=1
    const info = url.parse(req.url); // 为了去除查询参数而parsse
    
    // expected filePath = static/index.html
    const filePath = path.resolve(staticRootPath, “./” + info.path);
    
    const file = fs.createReadStream(filePath);
    file.pipe(res);
    
    // 这里还可以写根据文件后缀名设置Content-Type
})

server.listen(port, () => {
    console.log(`Static File Server is listening on port ${port}`);
})

要达成高性能、可靠服务的目标,还需要实现:

  • CDN:缓存、加速
  • 分布式储存:容灾

也可以使用外部服务:Cloudflare、七牛云、阿里云、火山云……

编写React SSR服务

SSR(Server Side Rendering,服务端渲染,但这里其实是指同构渲染)的特点:

  • 优点

    • 相比传统模版引擎:前后端不分离

    • 相比 SPA(Single Page Application):首屏渲染更快,SEO友好

  • 缺点

    • 通常QPS(每秒查询率)较低,前端代码编写时需要考虑服务端渲染情况
  • 实现难点

    • 需要处理打包代码(部分静态文件的加载代码可能没有意义)
    • 需要思考前端代码在服务端运行时的逻辑(拉取数据的代码需要注意执行时机)
    • 移除对服务端无意义的副作用(例如对window对象的修改),或每次渲染都重置环境(有性能问题)

适用Inspector进行调试、诊断

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

node --inspect

默认可以用 http://127.0.0.1:9229/json ,找到devtoolFrontendURL来在浏览器端连接调试进程。

场景:

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

部署简介

部署要解决的问题:

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

容器环境:通常有健康检查的手段,只需考虑多核cpu利用率即可

延伸话题

Node.js 代码贡献

  • 快速了解 Node.js 代码(Node.js Core 贡献入门
  • 好处
    • 从使用者的角色逐步理解底层细节,可以解决更复杂的问题
    • 自我证明,有助于职业发展
    • 解决社区问题,促进社区发展
  • 难点
    • 花时间

编译Node.js

  • 为什么要学习编译 Node.js
    • 认知:黑盒到白盒,发生问题时能有迹可循
    • 贡献代码的第一步
  • 如何编译
    • 参考:Building Node.js
    • ./configure && make install
    • 演示:给net模块添加自定义属性

诊断/追踪

  • 诊断是一个低频、重要同时也相当有挑战的方向,是企业衡量自己能否依赖一门语言的重要参考。
  • 技术咨询行业中的热门角色。
  • 难点
    • 需要了解 Node.js 底层,需要了解操作系统以及各种工具
    • 需要经验

image.png

WASM、N-API

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