编程导航Node话题讨论

Node

12 参与
分享

快来分享你的内容吧~

点击登录,快来和大家讨论吧~
表情
图片
话题
打卡
综合
交流
文章
问答

【Node】HTML 代码生成器

这是一个基于 AI (deepseek v3)的 HTML 代码生成器,可以根据用户的自然语言描述生成相应的 HTML 代码,并提供实时预览功能。 在线地址:[Easy HTML Generator](http://123.60.153.252:8104/) <br/> 参考: [即刻-只需一句话,即可生成完整 UI,无需复杂提示词,让编程与设计变得更直观、更简单。](https://web.okjike.com/u/E156EF37-73A5-4D51-8FE7-10F0A2349B82/post/67c91eddda34320149ebe87a) [一个提示词 claude 生成一个 app 的 ui/ux - 飞书云文档](https://ew6rccvpnmz.feishu.cn/wiki/ILO2waqXLi1EvqkuKHvcceMOnVd?fromScene=spaceOverview)

【Node】直播视界

在线地址:[123.60.153.252:8086](http://123.60.153.252:8086) ## 效果 <img src="https://pic.code-nav.cn/post_picture/1631847729158258689/5BWqlGLLEtAkNhEt.webp" alt="PixPin_2025-03-04_15-40-27.png" width="100%" /> ## 介绍 一个优雅的多平台直播聚合系统,**支持直播状态实时更新**,支持斗鱼、B 站、抖音等多个直播平台。 ## 功能特点 - 🎮 多平台支持:聚合斗鱼、B 站、抖音等多个直播平台 - 🔄 实时更新:自动更新直播状态和观看人数 - 🏷️ 标签分类:支持按标签筛选直播内容 - 🎯 平台筛选:可按不同直播平台进行筛选 - 📊 状态统计:实时显示直播中和未开播主播数量 - 💻 响应式设计:完美支持各种设备尺寸 - ⚡ 实时排序:直播中的主播优先显示 ## 技术栈 - 前端:HTML5 + Tailwind CSS + JavaScript - 后端:Node.js + Express - 数据存储:JSON ## 配置说明 主播配置文件位于 `public/data.json`,可以通过页面上的"设置"按钮进行在线编辑。 配置格式示例: ```json { "platform": { "douyu": "斗鱼", "bilibili": "B站", "douyin": "抖音" }, "platformJump": { "douyu": "https://www.douyu.com/", "bilibili": "https://live.bilibili.com/", "douyin": "https://live.douyin.com/" }, "douyu": [ { "name": "主播名称", "roomId": "房间号", "cate_name": "分类", "tag": "标签1,标签2", "info": "简介" } ] } ``` ## 更新日志 ### v1.0.0 - 🎉 首次发布 - 支持多平台直播聚合,支持直播状态实时更新 - 支持标签和平台筛选 - 支持在线配置编辑 ## 开源协议 MIT License 哔哩哔哩 获取直播间信息 https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/live/info.md#获取直播间信息 [如何获得抖音直播的开播状态 - 吾爱破解 - 52pojie.cn](https://www.52pojie.cn/thread-1808116-1-1.html) > 刚刚加上了抖音平台支持;以前我是斗鱼的忠实观众,但是好多喜欢的主播都停播了,现在打开斗鱼,总觉得少了以前那种兴奋劲儿,看直播也没了当初的激情。

【Node】图片智能展示墙

AI: cursor ## 过程 <img src="https://pic.code-nav.cn/post_picture/1631847729158258689/IsZorldGbcObBQCh.webp" alt="PixPin_2025-02-22_17-19-17.png" width="690px" /> ## 效果 <img src="https://pic.code-nav.cn/post_picture/1631847729158258689/mB3Bk0YjU5AqLZU1.webp" alt="PixPin_2025-02-22_18-04-10.png" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1631847729158258689/xaqzvrD3pV8cqv10.webp" alt="PixPin_2025-02-22_18-04-28.png" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1631847729158258689/qrRTnR8L5j8KK3c9.webp" alt="PixPin_2025-02-22_18-05-04.png" width="100%" /> ## 项目理念 > 以下为 AI 生成 ## 🌟 项目背景 现代人浏览网页时常常批量下载图片到本地文件夹(如壁纸、设计素材、摄影作品等),但原生操作系统缺乏针对图片集的智能展示方案。本项目旨在通过 Node.js + Express 打造一个**高效的本地图片管理系统**,提供直观的可视化界面和便捷的浏览体验。 ## 🎯 核心目标 1. **简单易用** - 快速部署,即刻可用 2. **时间管理** - 按时间维度智能组织图片 3. **性能优化** - 动态加载与智能缓存结合 4. **优雅界面** - 现代化的交互视觉效果 --- ## 🖼️ 核心功能模块 ### 1. 智能缩略图矩阵 - **动态布局引擎**:根据屏幕尺寸自动调整缩略图排列密度 - **延迟加载**:仅加载可视区域的图片,优化性能 - **智能缓存**:缓存常用图片,提升访问速度 ### 2. 时间轴模式 - **时间维度**:按年/月/日组织图片 - **智能分组**:自动对图片进行时间分类 - **快速定位**:支持时间维度的快速跳转 ### 3. 搜索系统 - **文件名搜索**:快速定位目标图片 - **模糊匹配**:支持关键词模糊搜索 - **实时反馈**:即时显示搜索结果 ### 4. 图片管理 - **快速预览**:便捷的图片预览功能 --- ## 💡 技术亮点 ### 前端架构 ```mermaid graph TD A[本地图片文件夹] --> B[File System API] B --> C[Web Workers] C --> D{数据处理} D --> E[缩略图生成] D --> F[时间分析] E --> G[IndexedDB缓存] F --> G G --> H[渲染引擎] H --> I[Canvas 2D] ``` ### 创新交互 - **手势控制**:支持手机端双指缩放、PC 端滚轮惯性滚动 - **键盘导航**:←→ 键切换焦点图片,空格键快速预览 - **智能预加载**:根据用户行为预测并预加载可能查看的图片 ### 性能优化 - **渐进式加载**:优先加载可视区域图片 - **内存管理**:闲置图片自动卸载 DOM 节点 - **后台处理**:使用 Web Worker 处理耗时操作 --- ## 🛠️ 技术栈 - **核心框架**: Node.js + Express - **前端技术**: Vanilla JavaScript + Web Components - **图片处理**: Web Workers - **存储方案**: File System API + IndexedDB --- ## 📁 项目结构 ``` / ├── server.js # Express 服务器 ├── index.html # 主页面 ├── styles/ # 样式文件 ├── scripts/ # 脚本文件 │ ├── core/ # 核心逻辑 │ ├── workers/ # Web Worker │ └── utils/ # 工具函数 └── README.md # 项目文档 ``` --- ## 🚀 未来展望 1. **性能优化**:进一步提升大量图片处理性能 2. **智能分类**:基于机器学习的图片分类 3. **云端同步**:可选的云端备份功能 4. **插件系统**:支持功能扩展 --- **启动方式:** 1. 克隆仓库 2. 安装依赖 3. 启动服务 4. 开始使用! 这个方案通过 Node.js + Express 提供了稳定的服务端支持,同时保持了轻量级的特性,为用户提供流畅的本地图片管理体验。

【Node】B站随机视频

在线地址:[B站随机视频](http://123.60.153.252:8102) 参考: [带我去一个随机的B站视频!](https://jyblog.cn/513) BV 号规则 https://www.zhihu.com/question/381784377/answer/1099438784 ## 代码: ### 结构 <img src="https://pic.code-nav.cn/post_picture/1631847729158258689/hwhKnBKPpZT7Z3Ft.png" alt="PixPin_2025-02-18_16-49-17.png" width="178px" /> </br> **初始化项目** ``` npm init -y npm install express axios ``` server.js ```js const express = require("express"); const axios = require("axios"); const app = express(); const port = 8102; // BV转换所需的常量 const table = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF"; const tr = {}; for (let i = 0; i < 58; i++) { tr[table[i]] = i; } const s = [11, 10, 3, 8, 4, 6]; const xor = 177451812; const add = 8728348608; // AV号转BV号 function enc(x) { x = (x ^ xor) + add; const r = "BV1 4 1 7 ".split(""); for (let i = 0; i < 6; i++) { r[s[i]] = table[Math.floor(x / Math.pow(58, i)) % 58]; } return r.join(""); } // 按年份范围生成随机BV号 function generateRandomBV() { const yearRanges = { 2013: { min: 1, max: 1000000 }, 2014: { min: 1000001, max: 2000000 }, 2015: { min: 2000001, max: 4000000 }, 2016: { min: 4000001, max: 8000000 }, 2017: { min: 8000001, max: 16000000 }, 2018: { min: 16000001, max: 35000000 }, 2019: { min: 35000001, max: 60000000 }, 2020: { min: 60000001, max: 85000000 }, 2021: { min: 85000001, max: 500000000 }, 2022: { min: 500000001, max: 700000000 }, 2023: { min: 700000001, max: 835000000 }, 2024: { min: 835000001, max: 950000000 }, }; const years = Object.keys(yearRanges); const randomYear = years[Math.floor(Math.random() * years.length)]; const range = yearRanges[randomYear]; const avid = Math.floor(Math.random() * (range.max - range.min)) + range.min; return enc(avid); } // 验证BV号是否存在 async function verifyBV(bvid) { try { const response = await axios.get( `https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`, { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", Referer: "https://www.bilibili.com", }, } ); return response.data.code === 0; } catch { return false; } } // 随机视频接口 app.get("/random", async (req, res) => { try { let bvid = generateRandomBV(); while (!(await verifyBV(bvid))) { bvid = generateRandomBV(); } res.json({ bvid }); } catch (error) { console.error("Error:", error.message); res.status(500).json({ error: "Internal Server Error" }); } }); // 前端页面路由 app.get("/", (req, res) => { res.sendFile(__dirname + "/index.html"); }); // 启动服务 app.listen(port, () => { console.log(`服务运行在 http://localhost:${port}`); }); ``` index.html ```html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>B站随机视频</title> <style> body { display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f0f0f0; } .btn { padding: 20px 40px; font-size: 24px; background: #00a1d6; color: white; border: none; border-radius: 50px; cursor: pointer; transition: transform 0.2s; } .btn:hover { transform: scale(1.1); } </style> </head> <body> <button class="btn" onclick="getRandomVideo()">随机看个B站视频</button> <script> async function getRandomVideo() { try { const response = await fetch("/random"); const data = await response.json(); if (data.bvid) { window.open(`https://www.bilibili.com/video/${data.bvid}`, "_blank"); } } catch (error) { console.error("Error:", error); } } </script> </body> </html> ```

【Node】git 统计用户某天的提交次数、提交详情

效果: <img src="https://pic.code-nav.cn/post_picture/1631847729158258689/KvfOZd3A9w7rabYp.webp" alt="PixPin_2025-02-18_10-27-08.png" width="100%" /> 使用方式: node gitCommitReport.js > 注意修改:定义要扫描的根目录 代码: ```js const child_process = require("child_process"); const fs = require("fs"); const readline = require("readline"); // 定义要扫描的根目录 const rootDir = "C:\\99\\code"; // 要忽略的目录 const ignoreDirs = [ "node_modules", "dist", "build", "target", ".idea", ".vscode", ".cache", "logs", "__pycache__", ".venv", ".next", ".nuxt", "vendor/bundle", ]; console.log("开始查找 Git 仓库目录..."); // 查找所有包含 .git 文件夹的目录,并排除忽略的目录 function findGitRepos(dir) { const repos = []; const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = `${dir}\\${entry.name}`; if (entry.isDirectory()) { if (ignoreDirs.some((ignoreDir) => fullPath.includes(ignoreDir))) { continue; } if (fs.existsSync(`${fullPath}\\.git`)) { repos.push(fullPath); } else { repos.push(...findGitRepos(fullPath)); } } } return repos; } const repos = findGitRepos(rootDir); console.log(`共找到 ${repos.length} 个 Git 仓库目录。`); // 创建 readline 接口 const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); // 提示用户输入 Git 账号名 rl.question("Please enter your Git username: ", (gitUsername) => { // 提示用户输入日期(格式:YYYY-MM-DD) rl.question("Please enter the date (format: yyyy-MM-dd): ", (inputDate) => { rl.close(); // 计算时间范围 const startDate = `${inputDate} 00:00:00`; const endDate = `${inputDate} 23:59:59`; // 初始化计数器 let totalCommits = 0; const commitDetails = []; let completedRepos = 0; // 遍历每个仓库 repos.forEach((repo) => { const command = `git -C ${repo} log --after="${startDate}" --before="${endDate}" --pretty="%h|%an|%ad|%s" --date=iso --no-merges`; child_process.exec( command, { encoding: "utf8" }, (error, stdout, stderr) => { if (error) { console.error( `Error processing repository ${repo}:`, error.message ); } else { const commits = stdout .split("\n") .filter((line) => line.trim() !== ""); // 过滤当前用户的提交 const userCommits = commits.filter((line) => line.startsWith(`${line.split("|")[0]}|${gitUsername}|`) ); // 统计提交次数 const count = userCommits.length; totalCommits += count; // 收集提交详情 if (count > 0) { userCommits.forEach((line) => { const parts = line.split("|", 4); commitDetails.push({ Repo: repo.split("\\").pop(), Hash: parts[0], Author: parts[1], Date: parts[2], Message: parts[3], }); }); } } completedRepos++; if (completedRepos === repos.length) { // 按照日期时间递增排序 commitDetails.sort((a, b) => { const dateA = new Date( `${a.Date.split(" ")[0]} ${a.Date.split(" ")[1] .split(":") .slice(0, 2) .join(":")}` ); const dateB = new Date( `${b.Date.split(" ")[0]} ${b.Date.split(" ")[1] .split(":") .slice(0, 2) .join(":")}` ); return dateA - dateB; }); // 输出结果 console.log("Daily Commits Report"); console.log("===================="); console.log(`Date: ${inputDate}`); console.log(`User: ${gitUsername}`); console.log(`Total Commits: ${totalCommits}`); console.log("\nCommit Details:"); if (commitDetails.length === 0) { console.log("No commits found"); } else { console.table( commitDetails.map((item) => ({ Repository: item.Repo, Hash: item.Hash, Date: `${item.Date.split(" ")[0]} ${item.Date.split(" ")[1] .split(":") .slice(0, 2) .join(":")}`, Message: item.Message, })) ); } } } ); }); }); }); ``` ## 使用 uTools 插件 快捷命令 版 > 输出今天和昨天的 git 提交信息 效果: <img src="https://pic.code-nav.cn/post_picture/1631847729158258689/JJPHcpl93Cjz8fVD.webp" alt="PixPin_2025-02-18_11-11-49.png" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1631847729158258689/tp6KRoyWG45pPBOo.webp" alt="PixPin_2025-02-18_11-10-42.png" width="100%" /> 代码: 注意修改 Git 账号 用户名: ```js // 写死 Git 账号 const gitUsername = "ygr99"; ``` ```js const child_process = require("child_process"); const fs = require("fs"); // 定义要扫描的根目录 const rootDir = "C:\\99\\code"; // 要忽略的目录 const ignoreDirs = [ "node_modules", "dist", "build", "target", ".idea", ".vscode", ".cache", "logs", "__pycache__", ".venv", ".next", ".nuxt", "vendor/bundle", ]; console.log("开始查找 Git 仓库目录..."); // 查找所有包含 .git 文件夹的目录,并排除忽略的目录 function findGitRepos(dir) { const repos = []; const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = `${dir}\\${entry.name}`; if (entry.isDirectory()) { if (ignoreDirs.some((ignoreDir) => fullPath.includes(ignoreDir))) { continue; } if (fs.existsSync(`${fullPath}\\.git`)) { repos.push(fullPath); } else { repos.push(...findGitRepos(fullPath)); } } } return repos; } const repos = findGitRepos(rootDir); console.log(`共找到 ${repos.length} 个 Git 仓库目录。`); // 写死 Git 账号 const gitUsername = "ygr99"; // 获取今天和昨天的日期 const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); // 格式化日期为 yyyy-MM-dd 格式 function formatDate(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } const todayDate = formatDate(today); const yesterdayDate = formatDate(yesterday); // 处理日期范围 function processDateRange(date) { const startDate = `${date} 00:00:00`; const endDate = `${date} 23:59:59`; // 初始化计数器 let totalCommits = 0; const commitDetails = []; let completedRepos = 0; // 遍历每个仓库 repos.forEach((repo) => { const command = `git -C ${repo} log --after="${startDate}" --before="${endDate}" --pretty="%h|%an|%ad|%s" --date=iso --no-merges`; child_process.exec( command, { encoding: "utf8" }, (error, stdout, stderr) => { if (error) { console.error( `Error processing repository ${repo}:`, error.message ); } else { const commits = stdout .split("\n") .filter((line) => line.trim() !== ""); // 过滤当前用户的提交 const userCommits = commits.filter((line) => line.startsWith(`${line.split("|")[0]}|${gitUsername}|`) ); // 统计提交次数 const count = userCommits.length; totalCommits += count; // 收集提交详情 if (count > 0) { userCommits.forEach((line) => { const parts = line.split("|", 4); commitDetails.push({ Repo: repo.split("\\").pop(), Hash: parts[0], Date: parts[2], Message: parts[3], }); }); } } completedRepos++; if (completedRepos === repos.length) { // 按照日期时间递增排序 commitDetails.sort((a, b) => { const dateA = new Date( `${a.Date.split(" ")[0]} ${a.Date.split(" ")[1] .split(":") .slice(0, 2) .join(":")}` ); const dateB = new Date( `${b.Date.split(" ")[0]} ${b.Date.split(" ")[1] .split(":") .slice(0, 2) .join(":")}` ); return dateA - dateB; }); // 输出结果 console.log("Daily Commits Report"); console.log("===================="); console.log(`Date: ${date}`); console.log(`User: ${gitUsername}`); console.log(`Total Commits: ${totalCommits}`); console.log("\nCommit Details:"); if (commitDetails.length === 0) { console.log("No commits found"); } else { commitDetails.forEach((item) => { const time = item.Date.split(" ")[1].split(":").slice(0, 2).join(":"); const repoName = `【${item.Repo}】`; console.log(`${time} ${repoName} ${item.Message}`); }); } } } ); }); } // 先处理今天的 processDateRange(todayDate); // 等待一段时间确保今天的处理完成后再处理昨天的(可以根据实际情况调整等待时间) setTimeout(() => { processDateRange(yesterdayDate); }, 3000); ```

【Node】Web 版 日记笔记 博客

> 数据使用 data.json 存储 参考: 1. [Heynote 1.8.0 ](https://heynote.com) 2. [Heynote 介绍 2.2K Star开源专为程序员打造的记事本](https://cloud.tencent.com/developer/article/2377228) > 我现在使用 Heynote 记笔记,但 Heynote 是个本地应用,于是想搞个 Web 版的记东西,并且自动渲染成博客 ## 介绍 一个基于 Web 的实时记事本和博客系统,支持动态添加、编辑和删除文本块,支持 markdown 语法高亮,并可以实时保存数据。 用户可以通过简单的按钮操作插入时间、日记、笔记,并在文本块之间灵活插入新内容,并且自动渲染成博客。 ## 效果 <img src="https://pic.code-nav.cn/post_picture/1631847729158258689/f95Nb95T0pboveih.webp" alt="PixPin_2025-02-14_15-59-25.png" width="723px" /> <img src="https://pic.code-nav.cn/post_picture/1631847729158258689/MIxq6hywKBB0b0ab.webp" alt="PixPin_2025-02-14_15-59-35.png" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1631847729158258689/kNX7ROPQuz1sIRJe.webp" alt="PixPin_2025-02-14_15-59-44.png" width="748px" /> --- ## 功能特性 - **实时编辑**:每个文本块都可以实时编辑,内容自动保存 - **动态插入**: - 在当前块前或后插入新文本块 - 在光标位置插入时间和日期 - **自动保存**:所有更改会自动保存到数据文件中 - **响应式设计**:适配不同屏幕尺寸 - **博客系统**: - 支持文章发布和管理 📆是日记 📘是笔记 - 文章列表展示 - 单文章详情页面 ## 项目结构 ``` real-time-notepad/ ├── public/ # 静态资源文件 │ ├── blog.js # 博客相关的 JavaScript │ └── blog.css # 博客样式文件 ├── views/ # HTML 文件 │ ├── index.html # 主页面 │ ├── blog.html # 博客列表页面 │ └── article.html # 文章详情页面 ├── data.json # 数据存储文件 ├── index.js # 服务器主文件 ├── README.md # 项目说明文件 └── package.json # 项目依赖配置文件 ``` ## 使用方法 1. **记事本功能**: - 编辑文本块,不同块自由移动 - 插入日记和笔记的标题、时间戳 - 添加新文本块 - 快捷键 ctrl+enter 当前块后面插入新块,alt+enter 当前块前面插入新块 - data.json 与页面上的数据顺序保持一致 2. **博客功能**: - 访问 `/blog` 查看博客(热力图+笔记列表)

【Node】自动生成HTML目录脚本分享

> 本文为 AI 生成内容 > 对众多 HTML 文件进行管理 今天,我将为大家分享一个 Node.js 脚本,它能够自动化地生成 HTML 文件目录,极大地提升我们管理多个 HTML 文件的效率。 ## 脚本功能概述 这个脚本能够扫描指定目录(默认为当前目录),获取所有的HTML文件(不包括index.html)和一级子目录中的index.html文件,并按照文件的创建时间倒序排列,最后生成一个包含文件列表和创建时间的index.html文件。 通过这个index.html文件,我们可以方便地查看和访问项目中的各个HTML页面,同时了解它们的创建时间,便于管理和维护。 ## 效果 <img src="https://pic.code-nav.cn/post_picture/1631847729158258689/XVqWjGgTwK5JBaaA.webp" alt="PixPin_2025-02-13_08-47-34.png" width="100%" /> ## 如何使用 1. **确保环境准备**:确保你的系统已经安装了Node.js。如果没有安装,可以从[Node.js官方网站](https://nodejs.org/en/)下载并安装。 2. **获取脚本**:将下面的代码复制到一个新的文件中,例如`generateHtmlIndex.js`。 这代码 ```javascript const fs = require("fs"); const path = require("path"); const directoryPath = path.join(__dirname); // 获取当前目录下的所有文件和子目录 fs.readdir(directoryPath, { withFileTypes: true }, (err, files) => { if (err) { return console.log("Unable to scan directory: " + err); } // 过滤出 HTML 文件和一级子目录 const htmlFiles = files.filter( (file) => file.isFile() && path.extname(file.name).toLowerCase() === ".html" && file.name!== "index.html" ); const subDirectories = files.filter( (file) => file.isDirectory() && file.name!== "node_modules" ); // 获取每个 HTML 文件的创建时间 const fileStats = htmlFiles.map((file) => { const filePath = path.join(directoryPath, file.name); const stats = fs.statSync(filePath); return { name: file.name, birthtime: stats.birthtime, // 使用 birthtime 获取创建时间 isDirectory: false, }; }); // 获取每个子目录中的 index.html 文件的创建时间 subDirectories.forEach((dir) => { const indexPath = path.join(directoryPath, dir.name, "index.html"); if (fs.existsSync(indexPath)) { const stats = fs.statSync(indexPath); fileStats.push({ name: dir.name, birthtime: stats.birthtime, // 使用 birthtime 获取创建时间 isDirectory: true, }); } }); // 按照创建时间倒序排列 fileStats.sort((a, b) => b.birthtime - a.birthtime); // 生成 HTML 内容 const htmlContent = ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>HTML 目录</title> <style> body { display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; margin: 0; font-family: Arial, sans-serif; background-color: #f4f4f4; overflow-y: auto; } .container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); max-width: 80%; width: 100%; margin: 20px 0; } h1 { text-align: center; margin-bottom: 20px; } ul { list-style-type: none; padding-left: 20px; } li { margin: 10px 0; display: flex; align-items: center; } a { text-decoration: none; color: #333; font-size: 18px; flex-grow: 1; } a:hover { color: #007bff; } .file-info { font-size: 14px; color: #777; margin-left: 10px; } .file-number { font-size: 18px; color: blue; margin-right: 10px; } </style> </head> <body> <div class="container"> <h1>HTML 目录</h1> <ul> ${fileStats .map( (file, index) => ` <li> <span class="file-number">${ fileStats.length - index }</span> <a href="${ file.isDirectory ? `${file.name}/index.html` : file.name }" target="_blank">${ file.isDirectory ? file.name : file.name.replace(".html", "") }</a> <span class="file-info">${formatDate( file.birthtime // 使用 birthtime 显示创建时间 )}</span> </li> ` ) .join("")} </ul> </div> </body> </html> `; // 写入 index.html 文件 fs.writeFile(path.join(directoryPath, "index.html"), htmlContent, (err) => { if (err) { return console.log("Error writing file: " + err); } console.log("index.html 文件已生成"); }); }); // 格式化日期 function formatDate(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, "0"); const seconds = String(date.getSeconds()).padStart(2, "0"); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } ``` 3. **运行脚本**:打开命令行工具,进入到保存`generateHtmlIndex.js`文件的目录,执行以下命令: ```bash node generateHtmlIndex.js ``` 4. **查看结果**:运行脚本后,在当前目录下会生成一个`index.html`文件,打开这个文件,你就可以看到按照创建时间倒序排列的HTML文件目录。 ## 注意事项 1. 脚本默认扫描当前目录,如果需要扫描其他目录,需要修改`directoryPath`变量的值。 2. 脚本会忽略`node_modules`目录,如果你有其他需要忽略的目录,可以在`subDirectories`过滤条件中添加。 3. 生成的`index.html`文件中的样式可以根据自己的需求进行修改,以适应不同的项目风格。 希望这个脚本能对你的项目开发和管理有所帮助,让我们的工作更加高效和便捷!如果在使用过程中有任何问题或建议,欢迎留言交流。

Node 和浏览器里实现 Excel 的解析和生成

<html> <head></head> <body> <div class="content ql-editor"> <p>Excel 是常用的办公软件,我们会用它来做数据的整理。</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/0l2jxgeb.jpeg" alt=""></p> <p>后台管理系统一般都会支持从 Excel 导入数据,或者导出数据到 Excel 文件:</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/ny2mkt6g.jpeg" alt=""></p> <p>那这种功能是如何实现的呢?</p> <p>在 Node 和浏览器里,如何解析、生成 Excel 文件呢?</p> <p>一般我们会用 <a href="https://www.npmjs.com/package/exceljs" target="_blank">exceljs</a> 这个包来做。</p> <p>在 npm 官网可以看到,这个包每周有 30w+ 的下载量,用的还是很多的:</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/0hv6tx9n.jpeg" alt=""></p> <p>我们具体写代码试试:</p> <div class="ql-code-block-container"> <div class="ql-code-block"> mkdir exceljs-test cd exceljs-test npm init -y </div> </div> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/91n3ezgm.jpeg" alt=""></p> <p>安装 exceljs:</p> <div class="ql-code-block-container"> <div class="ql-code-block"> npm install --save exceljs </div> </div> <p>把刚才这个 excel 文件复制过来:</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/iqhgmnye.jpeg" alt=""></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/gjg8eyzd.jpeg" alt=""></p> <p>我们在代码里读取出来看看:</p> <div class="ql-code-block-container"> <div class="ql-code-block"> const { Workbook } = require('exceljs'); async function main(){ const workbook = new Workbook(); const workbook2 = await workbook.xlsx.readFile('./data.xlsx'); workbook2.eachSheet((sheet, index1) =&gt; { console.log('工作表' + index1); sheet.eachRow((row, index2) =&gt; { const rowData = []; row.eachCell((cell, index3) =&gt; { rowData.push(cell.value); }); console.log('行' + index2, rowData); }) }) } main(); </div> </div> <p>工作表就是这个东西:</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/fi67lqw9.jpeg" alt=""></p> <p>每个工作表下都是独立的表格。</p> <p>也就是 workbook(工作簿) &gt; worksheet(工作表) &gt; row(行) &gt; cell(列)这样的层级关系。</p> <p>每一层都可以遍历:</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/7msgblkw.jpeg" alt=""></p> <p>所以我们遍历 sheet、row、cell 这几层,就能拿到所有的数据。</p> <p>跑下看看:</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/12r70255.jpeg" alt=""></p> <p>确实都拿到了。</p> <p>这样就是数据导入,我们从 excel 文件里解析出数据,然后存入数据库。</p> <p>exceljs 还提供了简便的方法,可以直接调用 worksheet 的 getSheetValues 来拿到表格数据,不用自己遍历:</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/l3isws5i.jpeg" alt=""></p> <p>解析 excel 文件还是很简单的。</p> <p>导入数据的时候,按照格式从中解析数据然后存入数据库就行。</p> <p>有同学可能会说,那如果 excel 的格式不符合要求呢?</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/mbq8wplg.jpeg" alt=""></p> <p>一般我们都会提供一个 excel 模版,用这个模版来填数据,然后再导入。</p> <p>excel 解析我们会了,再来看下 excel 的生成:</p> <div class="ql-code-block-container"> <div class="ql-code-block"> const { Workbook } = require('exceljs'); async function main(){ const workbook = new Workbook(); const worksheet = workbook.addWorksheet('guang111'); worksheet.columns = [ { header: 'ID', key: 'id', width: 20 }, { header: '姓名', key: 'name', width: 30 }, { header: '出生日期', key: 'birthday', width: 30}, { header: '手机号', key: 'phone', width: 50 } ]; const data = [ { id: 1, name: '光光', birthday: new Date('1994-07-07'), phone: '13255555555' }, { id: 2, name: '东东', birthday: new Date('1994-04-14'), phone: '13222222222' }, { id: 3, name: '小刚', birthday: new Date('1995-08-08'), phone: '13211111111' } ] worksheet.addRows(data); workbook.xlsx.writeFile('./data2.xlsx'); } main(); </div> </div> <p>相当简单,也是按照层次结构,先 addWorkSheet、然后 addRows,之后写入文件。</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/64jzf92i.jpeg" alt=""></p> <p>可以看到 worksheet 的名字,还有每行的数据都是对的。</p> <p>右边手机号那一列最宽,因为我们设置了 width 是 50。</p> <p>excel 是可以设置格式的,比如字体、背景色等,在代码里同样可以。</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/pu6gohbc.jpeg" alt=""></p> <p>就是遍历 row、cell,根据行数设置 style 就好了:</p> <div class="ql-code-block-container"> <div class="ql-code-block"> worksheet.eachRow((row, rowIndex) =&gt; { row.eachCell(cell =&gt; { if(rowIndex === 1) { cell.style = { font: { size: 10, bold: true, color: { argb: 'ffffff' } }, alignment: { vertical: 'middle', horizontal: 'center' }, fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '000000' } }, border: { top: { style: 'dashed', color: { argb: '0000ff' } }, left: { style: 'dashed', color: { argb: '0000ff' } }, bottom: { style: 'dashed', color: { argb: '0000ff' } }, right: { style: 'dashed', color: { argb: '0000ff' } } } } } else { cell.style = { font: { size: 10, bold: true, }, alignment: { vertical: 'middle', horizontal: 'left' }, border: { top: { style: 'dashed', color: { argb: '0000ff' } }, left: { style: 'dashed', color: { argb: '0000ff' } }, bottom: { style: 'dashed', color: { argb: '0000ff' } }, right: { style: 'dashed', color: { argb: '0000ff' } } } } } }) }) </div> </div> <p>style 可以设置 font、fill、border、alignment 这些。</p> <p>跑下看看:</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/59rlnke2.jpeg" alt=""></p> <p>这样,就完成了数据的导出。</p> <p>而且,exceljs 这个库可以直接在浏览器里用。</p> <p>我们试试:</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/c5p92k9x.jpeg" alt=""></p> <p>创建 index.html,从 CDN 引入 exceljs 包。</p> <p>添加一个 file 类型的 input,onchange 的时候解析其中的内容,解析逻辑和之前一样,只是从 readFile 换成 load。</p> <div class="ql-code-block-container"> <div class="ql-code-block"> &lt;!DOCTYPE html&gt; &lt;html lang="en"&gt; &lt;head&gt; &lt;meta charset="UTF-8"&gt; &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt; &lt;title&gt;Document&lt;/title&gt; &lt;script src="https://unpkg.com/exceljs@4.4.0/dist/exceljs.min.js"&gt;&lt;/script&gt; &lt;/head&gt; &lt;body&gt; &lt;input id="fileInput" type="file"/&gt; &lt;script&gt; const fileInput = document.getElementById('fileInput'); fileInput.onchange = async () =&gt; { const file = fileInput.files[0]; const { Workbook } = ExcelJS; const workbook = new Workbook(); const workbook2 = await workbook.xlsx.load(file); workbook2.eachSheet((sheet, index1) =&gt; { console.log('工作表' + index1); const value = sheet.getSheetValues(); console.log(value); }) } &lt;/script&gt; &lt;/body&gt; &lt;/html&gt; </div> </div> <p>起个静态服务:</p> <div class="ql-code-block-container"> <div class="ql-code-block"> npx http-server . </div> </div> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/831gud1l.jpeg" alt=""></p> <p>浏览器访问下:</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/tdmyh6vv.jpeg" alt=""></p> <p>可以看到,同样解析出了 excel 的内容。</p> <p>然后再试试生成 excel:</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/7je56n6c.jpeg" alt="image.png"></p> <p>前面的逻辑一样,只是把 writeFile 换成了 writeBuffer。</p> <p>这里我创建了一个 10M 的 ArrayBuffer 来写入数据,之后再读取。</p> <div class="ql-code-block-container"> <div class="ql-code-block"> &lt;!DOCTYPE html&gt; &lt;html lang="en"&gt; &lt;head&gt; &lt;meta charset="UTF-8"&gt; &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt; &lt;title&gt;Document&lt;/title&gt; &lt;script src="https://unpkg.com/exceljs@4.4.0/dist/exceljs.min.js"&gt;&lt;/script&gt; &lt;/head&gt; &lt;body&gt; &lt;script&gt; const { Workbook } = ExcelJS; async function main(){ const workbook = new Workbook(); const worksheet = workbook.addWorksheet('guang111'); worksheet.columns = [ { header: 'ID', key: 'id', width: 20 }, { header: '姓名', key: 'name', width: 30 }, { header: '出生日期', key: 'birthday', width: 30}, { header: '手机号', key: 'phone', width: 50 } ]; const data = [ { id: 1, name: '光光', birthday: new Date('1994-07-07'), phone: '13255555555' }, { id: 2, name: '东东', birthday: new Date('1994-04-14'), phone: '13222222222' }, { id: 3, name: '小刚', birthday: new Date('1995-08-08'), phone: '13211111111' } ] worksheet.addRows(data); worksheet.eachRow((row, rowIndex) =&gt; { row.eachCell(cell =&gt; { if(rowIndex === 1) { cell.style = { font: { size: 10, bold: true, color: { argb: 'ffffff' } }, alignment: { vertical: 'middle', horizontal: 'center' }, fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '000000' } }, border: { top: { style: 'dashed', color: { argb: '0000ff' } }, left: { style: 'dashed', color: { argb: '0000ff' } }, bottom: { style: 'dashed', color: { argb: '0000ff' } }, right: { style: 'dashed', color: { argb: '0000ff' } } } } } else { cell.style = { font: { size: 10, bold: true, }, alignment: { vertical: 'middle', horizontal: 'left' }, border: { top: { style: 'dashed', color: { argb: '0000ff' } }, left: { style: 'dashed', color: { argb: '0000ff' } }, bottom: { style: 'dashed', color: { argb: '0000ff' } }, right: { style: 'dashed', color: { argb: '0000ff' } } } } } }) }) const arraybuffer = new ArrayBuffer(10 * 1024 * 1024); const res = await workbook.xlsx.writeBuffer(arraybuffer); console.log(res.buffer); } main(); &lt;/script&gt; &lt;/body&gt; &lt;/html&gt; </div> </div> <p>跑下试试:</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/0j9fbqz0.jpeg" alt=""></p> <p>可以看到,确实有数据了。</p> <p>那有了 arraybuffer 的数据,如何触发下载呢?</p> <p>创建一个 a 标签,设置 download 属性,然后触发点击就可以了。</p> <p>也就是这样:</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/g68svo21.jpeg" alt=""></p> <div class="ql-code-block-container"> <div class="ql-code-block"> function download(arrayBuffer) { const link = document.createElement('a'); const blob = new Blob([arrayBuffer]); const url = URL.createObjectURL(blob); link.href = url; link.download = 'guang.xlsx'; document.body.appendChild(link); link.click(); link.addEventListener('click', () =&gt; { link.remove(); }); } </div> </div> <p>跑一下:</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/1eddlnye.jpeg" alt=""></p> <p>可以看到,生成了 excel 并且触发了下载。</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/o9xlsgrj.jpeg" alt=""></p> <p>打开文件,可以看到和 node 里生成的一样。</p> <p>案例代码上传了 github:https://github.com/QuarkGluonPlasma/nestjs-course-code/tree/main/exceljs-test</p> <h2>总结</h2> <p>Excel 的导入导出是后台管理系统的常见功能,我们一般用 exceljs 来实现。</p> <p>excel 文件分为 workbook、worksheet、row、cell 这 4 层,解析和生成都是按照这个层次结构来。</p> <p>解析就是 readFile 之后,遍历 worksheet、row,拿到 cell 中的数据 。</p> <p>生成就是 addWorkSheet、addRow 添加数据,然后 writeFile 来写入文件。</p> <p>如果是在浏览器里,就把 readFile 换成 load,把 writeFile 换成 writeBuffer 就好了。</p> <p>浏览器里生成 excel 之后,可以通过 a 标签触发下载,设置 download 属性之后,触发点击就好了。</p> <p>这样,我们就分别在 node 和浏览器里完成了 excel 的解析和生成。</p> #前端# #后端# </div> </body> </html>

Node 如何调用 Java 微服务?

<html> <head></head> <body> <div class="content ql-editor"> <p>后端系统大多都是微服务的架构,而且还可能同时有多种语言实现的微服务,比如 java、go、python、c++、node 等。</p> <p></p> <p>那么问题来了,多种语言实现的微服务之间如何通信呢?</p> <p></p> <p>有的同学会说 http 不就行?</p> <p></p> <p>但 http 是文本传输,通信效率低。更重要的是这些微服务并不会提供 http 接口,因为它们又不是直接面向客户端的。</p> <p></p> <p>跨语言调用服务一般会用 gRPC,它是 google 出的一种跨语言的远程方法调用的方案。</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/0an9m05w.jpeg" alt=""></p> <p></p> <p>其中,RPC 是 Remote Procedure Call,远程过程调用。</p> <p></p> <p>比如 java 微服务有个方法 aaa,node 微服务想调用它,就可以通过 gRPC 来实现。</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/gobyaz7w.jpeg" alt=""></p> <p></p> <p>今天我们就来用一下 gRPC。</p> <p></p> <p>当然,我们不会真的创建 java 的微服务,而是会用两个 nest 的微服务之间实现 gRPC 通信。</p> <p></p> <p>在 java、phthon、go 等语言的微服务里,接入 gRPC 都是类似的。</p> <p></p> <p>我们创建个 nest 项目:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> npm install -g <span class="ql-token hljs-meta">@nestjs</span>/cli nest <span class="ql-token hljs-keyword">new</span> <span class="ql-token hljs-title">grpc</span>-client </div> </div> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/gra5cnzo.jpeg" alt=""></p> <p></p> <p>我们用 monorepo 的形式来放 client 和 server 的代码。</p> <p></p> <p>执行:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> nest g app grpc-server </div> </div> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/zvqonn3m.jpeg" alt=""></p> <p></p> <p>这样,就有了两个 nest 的 application:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/1nqkm96v.jpeg" alt=""></p> <p></p> <p>改下 grpc-server 的启动端口号为 3001:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/pbhv8ke0.jpeg" alt=""></p> <p></p> <p>分别把两个 nest 应用跑起来:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> npm run start:dev grpc-client npm run start:dev grpc-server </div> </div> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/t1s891wq.jpeg" alt=""></p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/b8ykv24v.jpeg" alt=""></p> <p></p> <p>浏览器访问下:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/5pbiwjoc.jpeg" alt=""></p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/9wu2riv9.jpeg" alt=""></p> <p></p> <p>这就代表两个 nest 应用都跑起来了。</p> <p></p> <p>然后我们把 grpc-server 改造成 grpc 的微服务。</p> <p></p> <p>安装用到的微服务的包:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> npm install --save <span class="ql-token hljs-meta">@nestjs</span>/microservices </div> </div> <p></p> <p>grpc 的包:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> npm install --save <span class="ql-token hljs-meta">@grpc</span>/grpc-js <span class="ql-token hljs-meta">@grpc</span>/proto-loader </div> </div> <p></p> <p>修改下 grpc-server 的 main.ts</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-keyword">import</span> { NestFactory } from <span class="ql-token hljs-string">'@nestjs/core'</span>; <span class="ql-token hljs-keyword">import</span> { GrpcOptions, Transport } from <span class="ql-token hljs-string">'@nestjs/microservices'</span>; <span class="ql-token hljs-keyword">import</span> { GrpcServerModule } from <span class="ql-token hljs-string">'./grpc-server.module'</span>; <span class="ql-token hljs-keyword">import</span> { join } from <span class="ql-token hljs-string">'path'</span>; async function <span class="ql-token hljs-title">bootstrap()</span> { <span class="ql-token hljs-type">const</span> <span class="ql-token hljs-variable">app</span> <span class="ql-token hljs-operator">=</span> await NestFactory.createMicroservice&lt;GrpcOptions&gt;(GrpcServerModule, { transport: Transport.GRPC, options: { url: <span class="ql-token hljs-string">'localhost:8888'</span>, <span class="ql-token hljs-keyword">package</span>: <span class="ql-token hljs-string">'book'</span>, protoPath: join(__dirname, <span class="ql-token hljs-string">'book/book.proto'</span>), }, }); await app.listen(); } bootstrap(); </div> </div> <p></p> <p>微服务不需要暴露 http 端口,只需要提供微服务之间通信的 tcp 接口就行。</p> <p></p> <p>这里使用 createMicroservice 创建微服务,指定传输方式 transport 改为 GRPC,并且指定微服务监听端口为 8888。</p> <p></p> <p>然后在 options 指定 protoPath。</p> <p></p> <p>在 src 下创建这个对应的文件:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/e14bj9ve.jpeg" alt=""></p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> syntax = <span class="ql-token hljs-string">"proto3"</span>; <span class="ql-token hljs-keyword">package</span> book; service BookService { rpc <span class="ql-token hljs-title">FindBook</span> <span class="ql-token hljs-params">(BookById)</span> returns (Book) {} } message BookById { <span class="ql-token hljs-type">int32</span> <span class="ql-token hljs-variable">id</span> <span class="ql-token hljs-operator">=</span> <span class="ql-token hljs-number">1</span>; } message Book { <span class="ql-token hljs-type">int32</span> <span class="ql-token hljs-variable">id</span> <span class="ql-token hljs-operator">=</span> <span class="ql-token hljs-number">1</span>; <span class="ql-token hljs-type">string</span> <span class="ql-token hljs-variable">name</span> <span class="ql-token hljs-operator">=</span> <span class="ql-token hljs-number">2</span>; <span class="ql-token hljs-type">string</span> <span class="ql-token hljs-variable">desc</span> <span class="ql-token hljs-operator">=</span> <span class="ql-token hljs-number">3</span>; } </div> </div> <p></p> <p>这是一种叫做 protocol buffer 的语法。</p> <p></p> <p>我们安装个语法高亮插件</p> <p></p> <p>搜索 ext:proto,也就是处理 .proto 文件的插件:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/4uc8uv5a.jpeg" alt=""></p> <p></p> <p>安装之后就有语法高亮了:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/4adt9qa7.jpeg" alt=""></p> <p></p> <p>为什么要有一种 protocol buffer 的语法呢?</p> <p></p> <p>因为要跨语言通信,不同语言的语法又不一样,那怎么通信呢?</p> <p></p> <p>定义一种用于通信的语言,各种语言都支持这种语法的解析不就行了?</p> <p></p> <p>这就是为什么会有 protocol buffer。</p> <p></p> <p>这些语法也很容易看懂:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> syntax = <span class="ql-token hljs-string">"proto3"</span> </div> </div> <p></p> <p>是使用 proto3 版本的语法。</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-keyword">package</span> book; </div> </div> <p></p> <p>是当前包为 book,也就是一种命名空间。</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> service BookService { rpc <span class="ql-token hljs-title">FindBook</span> <span class="ql-token hljs-params">(BookById)</span> returns (Book) {} } </div> </div> <p></p> <p>这个就是定义当前服务可以远程调用的方法。</p> <p></p> <p>有一个 FindBook 方法,参数是 BookById,返回值是 Book</p> <p></p> <p>然后下面就是参数和返回值的消息格式:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> message BookById { <span class="ql-token hljs-type">int32</span> <span class="ql-token hljs-variable">id</span> <span class="ql-token hljs-operator">=</span> <span class="ql-token hljs-number">1</span>; } message Book { <span class="ql-token hljs-type">int32</span> <span class="ql-token hljs-variable">id</span> <span class="ql-token hljs-operator">=</span> <span class="ql-token hljs-number">1</span>; <span class="ql-token hljs-type">string</span> <span class="ql-token hljs-variable">name</span> <span class="ql-token hljs-operator">=</span> <span class="ql-token hljs-number">2</span>; <span class="ql-token hljs-type">string</span> <span class="ql-token hljs-variable">desc</span> <span class="ql-token hljs-operator">=</span> <span class="ql-token hljs-number">3</span>; } </div> </div> <p></p> <p>book.proto 只是定义了可用的方法和参数返回值的格式,我们还要在 controller 里实现对应的方法:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/81jeytjl.jpeg" alt=""></p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-meta">@GrpcMethod('BookService', 'FindBook')</span> findBook(data: { id: number}) { <span class="ql-token hljs-type">const</span> <span class="ql-token hljs-variable">items</span> <span class="ql-token hljs-operator">=</span> [ { id: <span class="ql-token hljs-number">1</span>, name: <span class="ql-token hljs-string">'前端调试通关秘籍'</span>, desc: <span class="ql-token hljs-string">'网页和 node 调试'</span> }, { id: <span class="ql-token hljs-number">2</span>, name: <span class="ql-token hljs-string">'Nest 通关秘籍'</span>, desc: <span class="ql-token hljs-string">'Nest 和各种后端中间件'</span> }, ]; <span class="ql-token hljs-keyword">return</span> items.find(({ id }) =&gt; id === data.id); } </div> </div> <p></p> <p>实现了 findBook 方法,并通过 @GrpcMethod 把它标识为 grpc 的远程调用的方法。</p> <p></p> <p>在 nest-cli.json 添加 assets 配置,让 nest 在 build 的时候把 proto 也复制到 dist 目录下:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/x7dd0q31.jpeg" alt=""></p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-string">"assets"</span>: [<span class="ql-token hljs-string">"**/*.proto"</span>], <span class="ql-token hljs-string">"watchAssets"</span>: <span class="ql-token hljs-literal">true</span>, </div> </div> <p></p> <p>把它跑起来:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> npm run start:dev grpc-server </div> </div> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/yhxmaxkk.jpeg" alt=""></p> <p></p> <p>这时 dist 下就有 grpc-server 的代码了:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/8h2df4uq.jpeg" alt=""></p> <p></p> <p>然后我们在 grpc-client 里连上它:</p> <p></p> <p>在 AppModule 里添加连接 grpc-server 的微服务的配置:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/k5chp1x4.jpeg" alt=""></p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-keyword">import</span> { Module } from <span class="ql-token hljs-string">'@nestjs/common'</span>; <span class="ql-token hljs-keyword">import</span> { AppController } from <span class="ql-token hljs-string">'./app.controller'</span>; <span class="ql-token hljs-keyword">import</span> { AppService } from <span class="ql-token hljs-string">'./app.service'</span>; <span class="ql-token hljs-keyword">import</span> { ClientsModule, Transport } from <span class="ql-token hljs-string">'@nestjs/microservices'</span>; <span class="ql-token hljs-keyword">import</span> { join } from <span class="ql-token hljs-string">'path'</span>; <span class="ql-token hljs-meta">@Module({ imports: [ ClientsModule.register([ { name: 'BOOK_PACKAGE', transport: Transport.GRPC, options: { url: 'localhost:8888', package: 'book', protoPath: join(__dirname, 'book/book.proto'), }, }, ]), ], controllers: [AppController], providers: [AppService], })</span> export <span class="ql-token hljs-keyword">class</span> <span class="ql-token hljs-title">AppModule</span> {} </div> </div> <p></p> <p>同样,客户端也是需要 proto 文件的,不然不知道怎么解析协议数据。</p> <p></p> <p>把 book/book.proto 文件复制过来:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/me1kc33d.jpeg" alt=""></p> <p></p> <p>然后在 AppController 里实现调用远程方法的逻辑:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/a6bdjeau.jpeg" alt=""></p> <p></p> <p>注入 BOOK_PACKAGE 的 grpc 客户端对象。</p> <p></p> <p>在 onModuleInit 的时候调用 getService 方法,拿到 BookService 的实例。</p> <p></p> <p>然后调用它的 findBook 方法。</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-keyword">import</span> { Controller, Get, Inject, Param, Query } from <span class="ql-token hljs-string">'@nestjs/common'</span>; <span class="ql-token hljs-keyword">import</span> { AppService } from <span class="ql-token hljs-string">'./app.service'</span>; <span class="ql-token hljs-keyword">import</span> { ClientGrpc } from <span class="ql-token hljs-string">'@nestjs/microservices'</span>; <span class="ql-token hljs-keyword">interface</span> <span class="ql-token hljs-title">FindById</span> { id: number; } <span class="ql-token hljs-keyword">interface</span> <span class="ql-token hljs-title">Book</span> { id: number; name: string; desc: string; } <span class="ql-token hljs-keyword">interface</span> <span class="ql-token hljs-title">BookService</span> { findBook(param: FindById): Book } <span class="ql-token hljs-meta">@Controller()</span> export <span class="ql-token hljs-keyword">class</span> <span class="ql-token hljs-title">AppController</span> { constructor(<span class="ql-token hljs-keyword">private</span> readonly appService: AppService) {} <span class="ql-token hljs-meta">@Inject('BOOK_PACKAGE')</span> <span class="ql-token hljs-keyword">private</span> client: ClientGrpc; <span class="ql-token hljs-keyword">private</span> bookService: BookService; onModuleInit() { <span class="ql-token hljs-built_in">this</span>.bookService = <span class="ql-token hljs-built_in">this</span>.client.getService(<span class="ql-token hljs-string">'BookService'</span>); } <span class="ql-token hljs-meta">@Get('book/:id')</span> getHero(<span class="ql-token hljs-meta">@Param('id')</span> id: number) { <span class="ql-token hljs-keyword">return</span> <span class="ql-token hljs-built_in">this</span>.bookService.findBook({ id }); } } </div> </div> <p></p> <p>把它跑起来:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> npm run start:dev grpc-client </div> </div> <p></p> <p>浏览器访问下:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/j0t7hlps.jpeg" alt=""></p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/5cs3eued.jpeg" alt=""></p> <p></p> <p>可以看到,远程方法调用成功了。</p> <p></p> <p>这就是基于 grpc 的远程方法调用,用 java、python、go、c++ 等实现的微服务也是这样来通信。</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/zier389s.jpeg" alt=""></p> <p></p> <p>通过 protocol buffer 的语法定义通信数据的格式,比如 package、service 等。</p> <p></p> <p>然后 server 端实现 service 对应的方法,client 端远程调用这个 service。</p> <p></p> <p>比如在 java 的 srping 里,需要安装这两个依赖:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/fv3f1dym.jpeg" alt=""></p> <p></p> <p>然后也是定义这样的 proto 文件:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/gpoy6qqh.jpeg" alt=""></p> <p></p> <p>之后定义对应的 servie:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1797461877294809089/7wq024yw.jpeg" alt=""></p> <p></p> <p>和 node 里差不多。</p> <p></p> <p>这样就能跨语言调用对方的方法了。</p> <p></p> <p>案例代码上传了 github:https://github.com/QuarkGluonPlasma/nestjs-course-code/tree/main/grpc-client</p> <p></p> <h2>总结</h2> <p></p> <p>今天我们学习了基于 gRPC 的远程方法调用。</p> <p></p> <p>不同语言的微服务之间可以基于 gRPC 来相互调用对方的方法。</p> <p></p> <p>它的实现方式是通过 protocol buffer 的语法来定义通信数据的格式,定义 package、service。</p> <p></p> <p>然后 server 端实现 service 对应的方法,client 端远程调用这些 service。</p> <p></p> <p>这样就可以实现在 java、node、go、python 等多种语言之间实现微服务的远程方法调用。</p> <p></p> <p>如果你写一个 Node 的 BFF 层,调用别的语言的微服务时就会用到 gRPC。</p> <p>#后端# #Node.js#</p> </div> </body> </html>

#学习总结——node.js版本切换 之前的项目使用的是node15版本,用户中心项目使用的是node16版本,为了能自由切换,学习了网上nvm版本切换方法,做下记录。

下载 APP