Node.js 基础笔记

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(` my application ${ReactDOMServer.renderToString( React.createElement(App, {}, 'my_content') )}`); })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 便携地利用多进程
      • 记录进程状态用于诊断
    • 【Node.js 基础笔记】容器环境
      • 通常有健康检查的手段,只需要考虑多核 CPU 利用率即可

    推荐阅读