• [高校训练营] 基于华为云码道(CodeArts)代码智能体的校园二手书交易平台全栈开发实践
    基于华为云码道的校园二手书交易平台全栈开发实践1. 案例概述在高校学习生活中,教材和课外书具有明显的阶段性。课程结束、考试完成或学生毕业后,大量书籍进入闲置状态;与此同时,新生和低年级学生仍然需要以较低成本获取教材。传统微信群、朋友圈和线下摆摊存在信息分散、搜索效率低、书籍状态不透明、交易过程无法追踪等问题。本案例围绕“让校园闲置书低成本、安全、可追踪地流转”这一目标,构建一套前后端分离的校园二手书交易平台。项目提供用户注册登录、书籍发布与审核、关键词检索、购物车、站内余额、管理员充值、订单履约、收藏、评价、站内通知和后台统计等功能,最终形成“发布—审核—选购—支付—发货—收货—评价”的完整业务闭环。项目在开发过程中使用华为云码道辅助完成需求梳理、系统设计、任务拆解、功能开发和代码检查,并在仓库中沉淀了 spec.md、design.md 和 tasks.md 等规格文档。当前交付版本采用 Vue 3、Node.js、Express 和 sql.js,能够在本地快速运行,也便于进一步部署到华为云。2. 案例目标本案例不是只完成静态页面,而是以“可正常运行、可完整演示、可自动验证”为交付目标。主要目标如下:实现普通用户、买家、卖家和管理员之间清晰的权限边界。实现真实书籍封面、分类筛选、关键词搜索和书籍详情。实现管理员为用户充值、用户使用站内余额购买书籍的资金闭环。实现购物车、跨卖家拆单、订单支付、卖家发货、买家收货和评价。为充值、支付和订单状态变化保留资金流水与站内通知。使用自动化测试验证权限、状态、余额和异常场景。保留可部署到华为云 ECS、RDS 和 OBS 的演进空间。3. 开发环境与技术选型3.1 前端Vue 3:构建组件化单页应用。Vue Router:管理用户端和管理端路由。Pinia:保存登录用户、余额和公共业务状态。Element Plus:提供表单、表格、弹窗、分页和消息反馈组件。Axios:统一访问后端 REST API。Vite:提供开发服务器和生产构建。3.2 后端Node.js + Express:提供 REST API。JWT:完成身份认证和接口鉴权。bcryptjs:保护用户密码。Multer:处理书籍封面上传。sql.js:使用 SQLite 数据文件完成轻量持久化。Supertest + Node.js Test Runner:实现接口级集成测试。3.3 工程结构项目当前主要由以下部分组成:used-book-web/ Vue 3 前端 used-book-api/ Express 后端与 SQLite 数据 used-book-server/ 早期 Spring Boot 实现(保留版本) .codeartsdoer/ 需求、设计和任务规格文档 README.md 项目启动、账号和功能说明早期系统设计采用 Spring Boot/MySQL。为了降低实习项目演示和单机部署的复杂度,当前默认运行主线演进为 Express/sql.js。案例以实际运行版本为准,同时保留早期版本作为架构演进记录。4. 使用华为云码道完成项目构建4.1 需求分析首先向华为云码道提供校园二手书的业务背景、目标角色和初步功能设想,请其帮助拆分功能域、明确验收条件和识别边界场景。需求阶段重点确认了以下规则:新发布书籍默认进入待审核状态。普通用户不能访问管理接口。用户不能购买自己发布的书籍。订单详情只能由相关买家、卖家或管理员访问。余额不足时必须阻止支付。同一购物车存在多个卖家时,需要按卖家拆分订单。只有卖家可以发货,只有买家可以确认收货和评价。需求分析结果沉淀在 .codeartsdoer/specs/used_book/spec.md 中,为后续设计和测试提供统一依据。4.2 系统设计在需求规格基础上,继续让华为云码道从数据模型、接口、权限、业务状态和异常处理几个方面给出设计建议。系统最终包含 13 张业务表:数据表作用user用户、角色、余额与账户状态category书籍分类book书籍基础信息、卖家、价格与状态book_image书籍图片price_change_log价格变更记录cart购物车order买家、卖家和订单状态order_item订单中的书籍价格快照review交易完成后的评价wallet_transaction充值、购买支出和卖家收入流水notification支付、发货、收货等站内通知favorite用户收藏book_view_log书籍浏览记录系统设计文档和任务拆解分别记录在 design.md 和 tasks.md 中。设计阶段最大的价值是提前发现“余额支付不是单表状态更新”“多卖家订单不能由一个卖家统一履约”等问题。4.3 任务拆解项目按照依赖关系分阶段实施:初始化前后端项目与数据库。完成用户注册、登录和 JWT 鉴权。完成分类、书籍发布、封面上传和审核。完成首页检索、详情、收藏和浏览记录。完成购物车和订单创建。完成管理员充值、余额支付和资金流水。完成发货、收货、评价和通知。完成管理后台和统计。增加集成测试并执行完整代码审查。通过规格驱动的任务拆解,可以减少前后端并行开发时的接口歧义,也方便在每个阶段运行测试。4.4 关键功能迭代在初版完成后,又使用华为云码道对照 README 检查实际代码和需求,重点完成了两轮增强。第一轮增强解决了交易资金来源问题。系统原本具有买卖功能,但缺少适合演示的充值入口。优化后只有管理员可以为指定用户增加站内余额,同时记录充值金额、备注、操作时间和变动后余额。普通用户不能直接修改余额。第二轮增强提升了书籍展示真实性。项目演示数据补充了真实书籍封面,并为图片加载失败保留默认占位图,避免外部图片失效导致页面结构异常。最后对完整仓库进行功能和代码检查,运行后端集成测试和前端生产构建,修复字段命名、参数校验、权限边界、待审核书籍可见性等问题。5. 系统总体方案5.1 逻辑架构浏览器 │ ▼ Vue 3 单页应用 │ Axios / JSON / JWT ▼ Express REST API ├─ 用户与鉴权 ├─ 书籍与审核 ├─ 购物车与订单 ├─ 钱包与资金流水 ├─ 收藏、评价与通知 └─ 管理后台与统计 │ ├─ SQLite 数据文件 └─ 封面上传目录前端通过 Axios 访问 API,后端使用 JWT 判断用户身份和角色。所有关键权限均在服务端再次校验,不能只依赖前端隐藏按钮。业务数据持久化到 SQLite 文件,封面图片由上传目录提供静态访问。5.2 用户与权限普通用户可以浏览和搜索书籍、发布书籍、加入购物车、购买、发货或确认收货,但具体操作受到资源归属限制。管理员拥有用户管理、书籍审核、充值和数据统计权限。管理接口统一检查 admin 角色,普通用户直接调用接口也会被拒绝。5.3 书籍发布与审核用户发布书籍时需要填写书名、作者、分类、价格、成色、出版信息和描述,并可上传封面。前端提供表单提示,后端再次校验日期、价格和分类合法性。新书默认处于待审核状态。只有审核通过并处于在售状态的书籍才进入公共列表,避免违规或错误内容直接公开。首页支持分类、关键词、价格与排序筛选。搜索同时匹配书名和作者,详情页展示封面、价格、成色、卖家和描述。5.4 购物车与跨卖家拆单用户可将不同卖家的书籍加入同一个购物车。创建订单时,后端重新读取每本书的最新状态和价格,并按 sellerId 分组,为每个卖家生成独立订单。这种设计使每个卖家只能处理自己的订单,发货和结算互不影响,也避免某个卖家延迟发货导致其他订单无法推进。5.5 站内余额与管理员充值系统买卖书籍使用平台站内余额。管理员可在用户管理页面选择用户,输入充值金额和备注。服务端校验管理员身份、金额范围和目标用户,并同时更新用户余额和钱包流水。钱包流水包含:管理员充值;购买支出;卖家收入;交易说明;变动后余额;操作时间。这样既能方便训练营项目演示,也能让每次资金变化可追踪。5.6 订单状态订单的主要状态流转如下:待支付 pending_payment │ 买家余额支付 ▼ 已支付 paid │ 卖家发货 ▼ 已发货 shipped │ 买家确认收货 ▼ 已完成 completed │ ▼ 评价 review创建订单后,买家可以支付或取消。支付时后端重新检查订单状态和余额,余额不足则拒绝。卖家只能对已支付订单发货,买家只能对已发货订单确认收货。关键状态变化会生成站内通知。6. 关键技术难点与解决方案6.1 资金与业务状态一致性支付涉及买家余额、钱包流水、订单状态、书籍状态和通知等多项变化。如果只在前端扣减显示余额,刷新页面后数据会失真,也可能产生越权支付。本项目将后端作为唯一可信来源。支付请求到达后,后端依次验证:当前用户是否为订单买家;订单是否处于待支付状态;订单中的书籍是否仍可交易;买家余额是否足够;所有校验通过后再执行扣款、状态更新和流水记录。前端只负责展示后端返回的新余额和订单状态。6.2 多卖家订单拆分购物车是买家维度的数据,但订单履约是卖家维度的数据。如果不同卖家的书籍进入同一订单,一个卖家可能看到或操作另一个卖家的书籍。系统在创建订单时先按卖家分组,再分别创建订单和订单条目。自动化测试专门覆盖跨卖家拆单,验证订单数量、卖家归属和金额计算。6.3 状态机与越权防护订单详情、评价删除、书籍审核和后台统计都存在越权风险。系统在每个接口中校验用户角色、资源归属和前置状态。例如普通用户不能删除他人评价,未参与订单的用户不能查看订单详情,待审核书籍不会出现在公共列表。6.4 真实封面与降级展示真实封面可以显著改善项目效果,但外部图片可能失效、加载缓慢或出现跨域问题。当前演示数据使用可访问封面,同时保留默认封面作为降级展示。部署到生产环境后,建议将确认合规的图片迁移到 OBS 或项目静态资源。6.5 架构演进的一致性项目早期规格采用 Spring Boot/MySQL,当前交付主线采用 Express/sql.js。为了避免评审时出现文档与实现不一致,本案例明确区分“早期设计版本”和“当前可运行版本”,并建议后续以实际云端部署技术栈更新设计文档。7. 测试与验证项目执行统一检查命令:npm run check检查结果如下:后端集成测试:14/14 通过,0 失败;前端生产构建:成功;Vite 版本:8.1.5;前端转换模块:1799 个;本地 Web 服务:http://localhost:5173;本地 API 服务:http://localhost:8088。14 个集成场景覆盖:公共书籍接口字段采用 camelCase;搜索同时匹配书名和作者;非法出版日期和分类被拒绝;不存在的接口返回统一 JSON 404;订单详情权限隔离;普通用户不能删除评价;购物车返回卖家信息;待审核书籍的可见性;管理后台统计;管理员充值、余额购买和卖家入账;跨卖家购物车拆分订单;支付、发货、收货、评价和通知完整闭环;收藏功能;余额不足时拒绝支付,并允许取消订单。除自动化测试外,还进行了真实页面操作。本次演示由管理员为用户充值 100 元,用户以 20 元购买《三体》,支付后余额为 80 元,同时生成购买支出流水和支付成功通知。8. 运行与演示8.1 启动方式在项目根目录执行:npm run dev该命令同时启动前端和 API。执行 npm run check 可以运行后端测试并构建前端生产包。8.2 推荐演示路径在首页搜索并查看《三体》详情。管理员进入后台,为测试用户充值 100 元。买家将书籍加入购物车并创建订单。使用站内余额支付,查看余额由 100 元变为 80 元。查看资金流水和支付成功通知。卖家发货,买家确认收货并评价。回到管理后台查看订单和书籍状态统计。8.3 运行效果截图以下截图展示首页、书籍详情、管理员充值、购物车、订单支付、钱包流水、站内通知和后台统计等核心效果。9. 华为云实际部署项目已部署到华为云 ECS,在线演示地址为 http://124.70.103.252/,健康检查地址为 http://124.70.103.252/health(受论坛外链白名单限制,复制地址时请将中文全角冒号 : 改为英文半角冒号 :)。完整案例 PDF 下载地址:http://124.70.103.252/used-book-case-report.pdf(复制时同样将 : 改为 :)。使用 Nginx 部署 used-book-web/dist,并反向代理 /api、/uploads 和 /health;使用 systemd 运行 Node.js API,支持开机启动和异常重启;SQLite 与 uploads 位于独立持久化目录;API 仅监听 127.0.0.1:8088,8088 不直接对公网开放;部署时生成随机 JWT 密钥,并使种子账号默认密码失效;演示实例计划于 2026 年 7 月 31 日 02:49(GMT+08:00)自动释放。当前部署适合训练营阶段验收。如果后续需要长期运行,可增加域名和 HTTPS,将 SQLite 迁移到华为云 RDS,将上传图片迁移到 OBS,并增加集中日志、监控和备份。10. 案例总结本项目完成了从需求、设计、编码、测试到交付材料整理的完整过程。与简单的增删改查项目相比,校园二手书平台重点解决了多角色权限、订单状态、跨卖家拆单、资金流水和业务一致性问题。华为云码道在项目中的主要价值体现在:将模糊的项目想法转化为可执行的规格和验收条件;从需求中识别状态、权限和异常场景;形成设计文档和任务拆解;辅助实现管理员充值、真实封面和完整交易闭环;对照 README 和代码进行仓库级检查;通过自动化测试验证功能并发现边界问题;将代码、截图、测试结果和指导书要求整理为可交付案例。
  • 考研备考管家
    一、概述1.1 案例介绍考研备考管家是一款面向考研学子的 AI 智能学习管理网站,覆盖数学一、英语一、政治、408计算机基础四门统考科目,提供知识图谱追踪、刷题记录、艾宾浩斯错题复习、智能排期、AI 管家对话、专注计时等一站式备考功能。前端采用 Vue3 + Element Plus 手绘手账风格,后端采用 Flask + SQLite + DeepSeek 大模型,全栈可一键构建运行。项目地址:https://github.com/LeoMay23/ExamPreparationDEMO展示:https://www.bilibili.com/video/BV1MpKh6nEJr/?spm_id_from=333.1387.upload.video_card.click&vd_source=69e3b625d4662860466dccf85672cf571.2 适用对象高校学生个人开发者1.3 案例时间本案例总时长预计100分钟。1.4 案例流程说明:环境准备:安装 Node.js、Python,配置 DeepSeek 大模型 API Key;后端构建:安装 Python 依赖,初始化 SQLite 数据库与知识点种子数据,启动 Flask 服务;前端构建:安装 npm 依赖,配置 Vite 代理,构建并启动 Vue3 开发服务器;功能体验:注册登录 → 考研档案 → 仪表盘 → 科目中心 → 刷题记录 → 错题本 → 智能计划 → AI 管家 → 专注学习 → 复盘中心。1.5 资源总览本案例预计花费10元。所有依赖均为开源免费软件,大模型 API 可使用 DeepSeek 免费额度。资源名称规格单价(元)Node.jsv22.x免费Python3.10+免费DeepSeek APIdeepseek-v4-pro10元华为云码道(CodeArts)代码智能体专业版代金券兑换二、环境和资源准备2.1 安装 Node.js 与 Python确保本地已安装:Node.js v18+(推荐 v22.x),下载地址:cid:link_1Python 3.10+,下载地址:cid:link_0验证安装:node --version python --version 2.2 配置 DeepSeek 大模型 API Key注册 DeepSeek 开放平台账号在「API Keys」页面创建新的 API Key,复制保存。在项目 backend/ 目录下创建 .env 文件,填入以下内容:DEEPSEEK_BASE_URL=https://api.deepseek.com DEEPSEEK_AUTH_TOKEN=sk-xxxxxxxxxxxxxxxxxxxxxxxx DEEPSEEK_MODEL=deepseek-v4-pro2.3 获取项目代码将项目代码下载到本地工作目录:git clone <项目仓库地址> ExamPreparation cd ExamPreparation三、构建考研备考管家应用3.1 项目结构说明ExamPreparation/ ├── backend/ # Flask 后端 │ ├── app.py # 应用入口,注册蓝图 │ ├── config.py # 配置(DB路径、DeepSeek API) │ ├── database.py # 数据库表定义 + 种子数据 + 迁移 │ ├── requirements.txt # Python 依赖 │ ├── .env # 环境变量(API Key 等) │ ├── agent/ # AI Agent 模块 │ │ ├── llm_client.py # DeepSeek LLM 客户端 │ │ ├── orchestrator.py # Agent 编排器 │ │ ├── prompts.py # Prompt 模板 │ │ ├── tool_registry.py # 工具注册 │ │ └── tool_executor.py # 工具执行器 │ ├── planner/ # 排期引擎 │ │ ├── scheduler.py # 6维权重4阶段排期算法 │ │ └── time_slot.py # 空闲时段计算 │ ├── routes/ # API 路由(11个蓝图) │ │ ├── auth.py # 用户注册/登录/档案 │ │ ├── subjects.py # 科目中心(14个端点) │ │ ├── mistakes.py # 错题本 │ │ ├── plans.py # 智能计划 │ │ ├── chat.py # AI 管家对话 │ │ ├── sessions.py # 专注学习 Session │ │ └── ... │ └── services/ # 业务逻辑层 │ ├── subject_service.py # 科目中心(知识树/刷题/词汇/背诵/算法) │ ├── mistake_service.py # 错题本(艾宾浩斯复习) │ ├── planner_service.py # 计划服务(生成/延后/提前/锁定) │ ├── ai_butler_service.py# AI管家(今日概览/提醒/专注建议) │ ├── user_service.py # 用户系统 │ └── ... ├── web/ # Vue3 前端 │ ├── package.json # npm 依赖 │ ├── vite.config.js # Vite 配置(代理 /api → localhost:5000) │ └── src/ │ ├── main.js # 应用入口 │ ├── assets/main.css # 全局手绘风格 CSS │ ├── router/index.js # 路由(15个页面 + 守卫) │ ├── stores/ # Pinia 状态管理 │ │ ├── user.js # 用户状态(登录/档案/倒计时) │ │ ├── subject.js # 科目状态(知识树/统计/刷题) │ │ └── mistake.js # 错题状态 │ ├── utils/api.js # Axios 封装(拦截器注入 user_id/token) │ ├── components/ │ │ └── layout/MainLayout.vue # 侧边栏 + 主内容区 │ └── views/ # 15个页面 │ ├── Login.vue # 登录注册 │ ├── Onboarding.vue # 考研档案 │ ├── Dashboard.vue # 仪表盘(倒计时+4科卡片+今日计划+错题) │ ├── Subjects.vue # 科目中心入口 │ ├── math/MathIndex.vue # 数学一 │ ├── english/EnglishIndex.vue # 英语一 │ ├── politics/PoliticsIndex.vue # 政治 │ ├── cs408/Cs408Index.vue # 408 │ ├── Plan.vue # 智能计划 │ ├── Chat.vue # AI 管家 │ ├── Focus.vue # 专注学习 │ ├── Mistakes.vue # 错题本 │ ├── Review.vue # 复盘中心 │ ├── Tasks.vue # 计划管理 │ └── Settings.vue # 设置 └── HLD-KaoyanPrep-Website-20260710.md # HLD 设计文档 3.2 后端:安装依赖并启动服务步骤1:进入后端目录,安装 Python 依赖:cd backend pip install -r requirements.txtrequirements.txt 内容如下:Flask==3.0.3 flask-cors==4.0.1 requests==2.32.3 python-dotenv步骤2:启动 Flask 服务:python app.py启动成功后,服务运行在 http://localhost:5000。首次启动时会自动:创建 SQLite 数据库 aiplanpal.db建立全部 25 张表插入知识点种子数据(数学64个、政治32个、408共55个节点)关键代码讲解:数据库初始化与种子数据backend/database.py 中的 init_db() 函数负责数据库的创建与迁移:def init_db(): with closing(sqlite3.connect(DB_PATH)) as conn: conn.row_factory = sqlite3.Row conn.execute("PRAGMA foreign_keys = ON") create_tables(conn) # 创建全部25张表 migrate_existing_tables(conn) # 迁移旧表结构 + 种子数据 conn.commit() 考研专用表包括:表名用途math_knowledge_nodes数学知识点(高数/线代/概率论,64个节点)math_practice_records数学刷题记录english_vocabulary_progress英语词汇复习进度(艾宾浩斯间隔)english_reading_records英语阅读训练记录english_writing_records英语写作记录politics_knowledge_nodes政治知识点(马原/毛中特/史纲/思修,32个节点)politics_choice_records政治选择题记录politics_memorization_progress政治背诵进度cs408_knowledge_nodes408知识点(数据结构/计组/OS/网络,55个节点)cs408_practice_records408刷题记录cs408_algorithm_records408算法训练记录mistake_records错题本(艾宾浩斯复习)种子数据示例——数学知识点在 _seed_knowledge_nodes() 中批量插入:math_nodes = [ ("higher_math", "函数与极限", "函数的概念", 2, "core", "unstarted", 3.0), ("higher_math", "函数与极限", "极限的概念与性质", 3, "core", "unstarted", 4.0), ("higher_math", "一元微分学", "微分中值定理", 5, "core", "unstarted", 6.0), # ... 共64个节点 ] conn.executemany( "INSERT INTO math_knowledge_nodes (subject, chapter, section, difficulty, importance, mastery, estimated_hours) VALUES (?, ?, ?, ?, ?, ?, ?)", math_nodes, ) 关键代码讲解:DeepSeek LLM 客户端backend/agent/llm_client.py 封装了与大模型的交互,支持普通对话和 JSON 结构化输出:def chat_completion(messages, max_tokens=900, temperature=0.2, response_format=None, timeout_seconds=45): if not DEEPSEEK_AUTH_TOKEN: raise LLMUnavailableError("DEEPSEEK_AUTH_TOKEN is not configured") payload = { "model": DEEPSEEK_MODEL, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, } if response_format: payload["response_format"] = response_format response = requests.post( f"{DEEPSEEK_BASE_URL.rstrip('/')}/chat/completions", headers={ "Authorization": f"Bearer {DEEPSEEK_AUTH_TOKEN}", "Content-Type": "application/json", }, json=payload, timeout=timeout_seconds, ) response.raise_for_status() # ... 解析返回 降级策略:当 DEEPSEEK_AUTH_TOKEN 未配置时,所有 AI 功能会自动降级为本地规则引擎,不会报错阻断服务。关键代码讲解:智能排期引擎backend/services/planner_service.py 中的 generate_plans() 实现了6维权重4阶段的动态排期:def generate_plans(user_id=DEFAULT_USER_ID): tasks = fetch_tasks(user_id) if not tasks: # 无任务时自动创建8个默认考研任务 default_tasks = [ ("高数极限与连续刷题", "math", 90, "4", 3), ("英语阅读理解训练", "english", 60, "3", 2), ("政治马原选择题", "politics", 45, "3", 2), ("408数据结构算法练习", "cs408", 60, "4", 3), # ... 共8个 ] for title, task_type, est_min, priority, difficulty in default_tasks: db.execute("INSERT INTO tasks (...) VALUES (...)") # 删除未锁定的旧计划,保留用户锁定的计划 db.execute("DELETE FROM study_plans WHERE user_id = ? AND locked = 0 AND status IN (...)") # 调用排期算法生成新计划 generated = schedule_tasks(tasks, courses, existing_plans=locked_plans, days=7) 排期策略:schedule_tasks() 综合考虑任务优先级、难度、截止时间、课表冲突、用户偏好时段和当前备考阶段(基础/强化/冲刺/模考)6个维度,自动生成7天学习计划。3.3 前端:安装依赖并启动开发服务器步骤1:进入前端目录,安装 npm 依赖:cd web npm install 步骤2:启动 Vite 开发服务器:npm run dev启动成功后,浏览器访问 http://localhost:3000。关键代码讲解:全局手绘风格 CSSweb/src/assets/main.css 定义了整套手绘手账风格的设计令牌::root { --ink: #171717; /* 墨黑:边框、文字 */ --paper: #f7eed7; /* 网格纸背景 */ --cream: #fff7e8; /* 奶油白卡片 */ --yellow: #f8d46b; /* 黄色强调 */ --blue: #86b7d9; /* 蓝色强调 */ --red: #e1452d; /* 红色强调 */ --green: #4c9b54; /* 绿色强调 */ --border-thick: 4px solid var(--ink); /* 粗黑边 */ --shadow: 6px 6px 0 var(--ink); /* 偏移投影 */ --radius: 16px; /* 大圆角 */ --radius-pill: 999px; /* 药丸圆角 */ --color-math: #4A90D9; /* 数学蓝 */ --color-english: #7B68EE; /* 英语紫 */ --color-politics: #e1452d; /* 政治红 */ --color-cs408: #4c9b54; /* 408绿 */ } 网格纸背景通过 CSS 渐变实现:body { background: linear-gradient(90deg, rgba(23,23,23,0.03) 1px, transparent 1px), linear-gradient(0deg, rgba(23,23,23,0.025) 1px, transparent 1px), var(--paper); background-size: 24px 24px; } 关键代码讲解:AI 管家对话与确认卡片web/src/views/Chat.vue 实现了 AI 管家对话界面,支持确认卡片(confirm_card)交互:<div v-if="msg.confirmCard" class="confirm-card"> <div class="confirm-title">{{ msg.confirmCard.title }}</div> <div class="confirm-summary">{{ msg.confirmCard.summary }}</div> <ul v-if="msg.confirmCard.details?.length" class="confirm-details"> <li v-for="(d, di) in msg.confirmCard.details" :key="di">{{ d }}</li> </ul> <div class="confirm-actions"> <el-button v-for="(action, ai) in msg.confirmCard.actions" :key="ai" :type="ai === 0 ? 'primary' : 'default'" size="small" @click="handleConfirm(msg.confirmCard, action, i)" :disabled="msg.confirmed"> {{ action }} </el-button> </div> </div> 交互流程:用户发送消息 → 后端 Agent 分析意图 → 如果需要执行操作(如生成计划),返回 confirm_card → 用户点击确认 → 前端发送 confirmed_tool → 后端执行操作并返回结果。3.4 构建生产版本前端构建生产版本:cd web npm run build构建成功后,静态文件输出到 web/dist/ 目录,可直接部署到任何静态服务器。后端无需额外构建步骤,直接运行 python app.py 即可。3.5 运行效果展示登录注册页面用户首次访问时进入登录页面,支持注册和登录。页面采用居中布局,手绘风格卡片和药丸按钮。考研档案登录后进入考研档案页面,设置目标院校、专业、考研日期(日期选择器)、当前阶段、每日可用时间等。仪表盘仪表盘是用户首页,核心展示:考研倒计时:大号数字显示距考研天数,右侧显示当前阶段和考试日期四科进度卡片:数学/英语/政治/408 各一张卡片,显示掌握进度和正确率,点击进入对应科目今日计划:展示当天学习块列表,支持一键生成待复习错题:展示最近5条待复习错题AI 管家:显示今日概览和建议科目中心科目中心入口展示四门科目的卡片,每张卡片带有科目色带、图标和简介,hover 时有手绘风格的旋转偏移动画。数学一页面数学一页面包含三个 Tab:知识图谱:按高数/线代/概率论分组展示知识点,每个知识点可设置掌握度(未开始/学习中/已练习/已掌握)刷题记录:表单录入题源、题号、知识点、结果、耗时、错误类型统计:展示总刷题数、正确率、知识点掌握分布(进度条)、最近20条刷题记录(药丸标签)英语一页面英语一页面包含四个 Tab:词汇管理:展示今日待复习词汇,支持艾宾浩斯间隔复习阅读训练:录入年份、篇号、正确数、耗时写作工坊:录入作文类型、题目、内容统计:展示阅读篇数、写作篇数、最近阅读记录、最近写作记录政治页面政治页面包含三个 Tab:知识框架:按马原/毛中特/史纲/思修/时政分组,标注核心考点和考试频率选择题训练:录入题源、题号、单选/多选、结果、错误类型背诵管理:今日待背诵内容,支持艾宾浩斯间隔复习统计:展示选择题总数、正确率、最近选择题记录408计算机基础页面408页面包含四个 Tab:知识图谱:按数据结构/计组/OS/网络分组,标注算法类知识点刷题记录:录入子科目、题源、题号、题型、结果、耗时算法训练:录入算法名、语言、代码、时间/空间复杂度统计:展示刷题总数、正确率、算法题数、最近刷题记录、最近算法记录错题本错题本页面功能完整:筛选栏:按科目和掌握状态筛选统计药丸:总错题、已掌握、未掌握、今日待复习错题卡片列表:左侧色带标识科目,展示题目内容、题源、错误类型、复习次数操作按钮:复习(艾宾浩斯)、已掌握详情弹窗:展示题目内容、我的答案、正确答案、解析添加错题弹窗:手动录入完整错题信息自动创建:在数学/政治/408刷题时,答错的题目会自动创建错题记录。智能计划智能计划页面展示7天学习计划,支持:一键生成计划(6维权重4阶段排期算法)开始/完成/延后/锁定计划AI 辅助调整(延后时自动寻找合适空闲时段)AI 管家AI 管家是对话式交互界面:支持自然语言对话确认卡片交互(如「生成今日计划」会弹出确认卡片)4种管家风格(温柔鼓励型/朋友陪伴型/严肃型/极简提醒型)专注学习专注学习页面提供番茄钟计时器:选择科目后自动推荐专注时长(数学50分钟、英语40分钟、政治30分钟、408为45分钟)大号圆形计时器,手绘风格暂停/继续/提前结束AI 专注建议卡片结束时自动记录 Session 到后端复盘中心复盘中心展示学习统计数据,包括各科目专注时长、完成率等。设置设置页面支持修改:考研日期(日期选择器)当前备考阶段管家名称和风格四、技术要点总结4.1 前后端分离架构层级技术栈说明前端Vue3 + Vite + Element Plus + Pinia手绘风格 SPA,15个页面后端Flask + SQLite + DeepSeek11个蓝图,25张表,14个科目端点AIDeepSeek API + Agent4种管家风格,降级为规则引擎4.2 关键设计决策降级策略:所有 AI 功能在无 LLM 时自动降级为本地规则引擎,确保核心功能可用艾宾浩斯复习:错题本和词汇/背诵模块均采用 [1,2,4,7,15,30] 天间隔复习6维排期:综合优先级、难度、截止时间、课表、偏好时段、备考阶段6个维度手绘风格:通过 CSS 变量体系统一管理,粗黑边 + 偏移投影 + 药丸标签 + 网格纸背景自动错题:刷题答错时自动创建错题记录,无需手动添加默认任务:首次生成计划时自动创建8个考研默认任务4.3 已知注意事项Element Plus 的 el-input-number 组件需要 min-width: 140px 和排除全局 wrapper 样式覆盖axios 拦截器中 POST 请求无 body 时 user_id 不会被注入,所有无 body 的 POST/PUT 必须传 {}Python 代码中不能使用中文引号 ""(与 f-string 冲突),必须用 「」 替代esbuild 偶尔需要清理 node_modules 重装才能正常构建五、开发流程5.1 设计开发大纲5.2 基于初始文档构建网站5.3 逐步修改细节
  • [使用说明] Matepad Edge下的Codearts IDE中,文件监视器数量过少
    如题,Matepad Edge下的Codearts IDE虽然可以正常使用,但是需要启动的时候,就会出现文件监视器数量过少的错误提示。Matebook Pro下则没有这个问题。具体错误(Node.js)如下:/storage/Users/currentUser/Develop/Website/Liantu-Game/node_modules/react-scripts/scripts/start.js:19  throw err;  ^Error: ENOSPC: System limit for number of file watchers reached, watch '/storage/Users/currentUser/Develop/Website/Liantu-Game/public'    at FSWatcher.<computed> (node:internal/fs/watchers:254:19)    at Object.watch (node:fs:2551:36)    at createFsWatchInstance (/storage/Users/currentUser/Develop/Website/Liantu-Game/node_modules/chokidar/lib/nodefs-handler.js:119:15)    at setFsWatchListener (/storage/Users/currentUser/Develop/Website/Liantu-Game/node_modules/chokidar/lib/nodefs-handler.js:166:15)    at NodeFsHandler._watchWithNodeFs (/storage/Users/currentUser/Develop/Website/Liantu-Game/node_modules/chokidar/lib/nodefs-handler.js:331:14)    at NodeFsHandler._handleDir (/storage/Users/currentUser/Develop/Website/Liantu-Game/node_modules/chokidar/lib/nodefs-handler.js:567:19)    at process.processTicksAndRejections (node:internal/process/task_queues:103:5)    at async NodeFsHandler._addToNodeFs (/storage/Users/currentUser/Develop/Website/Liantu-Game/node_modules/chokidar/lib/nodefs-handler.js:617:16)    at async /storage/Users/currentUser/Develop/Website/Liantu-Game/node_modules/chokidar/index.js:451:21    at async Promise.all (index 0)Emitted 'error' event on FSWatcher instance at:    at FSWatcher._handleError (/storage/Users/currentUser/Develop/Website/Liantu-Game/node_modules/chokidar/index.js:647:10)    at NodeFsHandler._addToNodeFs (/storage/Users/currentUser/Develop/Website/Liantu-Game/node_modules/chokidar/lib/nodefs-handler.js:645:18)    at process.processTicksAndRejections (node:internal/process/task_queues:103:5)    at async /storage/Users/currentUser/Develop/Website/Liantu-Game/node_modules/chokidar/index.js:451:21    at async Promise.all (index 0) {  errno: -28,  syscall: 'watch',  code: 'ENOSPC',  path: '/storage/Users/currentUser/Develop/Website/Liantu-Game/public',  filename: '/storage/Users/currentUser/Develop/Website/Liantu-Game/public'}Node.js v24.13.0虽然文件监视器数量可以被看到,但是因没有su或sudo权限,其相关文件也是只有读取权限,因此无法修改文件监视器的数量。还请改善一下这个问题! 
  • [问题求助] nvm-windows安装后设置华为源地址问题
    方式三:使用nvm-windows安装首先请参考此处(cid:link_0)安装nvm-windows,然后执行如下命令安装对应的版本:nvm node_mirror https://mirrors.huaweicloud.com/nodejs/nvm npm_mirror https://mirrors.huaweicloud.com/npm-software/nvm install latestnvm use latest按照上面的方法设置后在CMD中执行 nvm install latest报错:Error retrieving "https://mirrors.huaweicloud.com/nodejs/latest/SHASUMS256.txt": HTTP Status 418各位大佬们,这个需要怎么解决呢?
  • [技术干货] Vscode中launch.json与tasks.json文件的详细介绍【转】
    launch.json文件launch.json 文件是 Visual Studio Code (VS Code) 中用于配置调试会话的文件。它定义了调试器如何启动和运行程序。以下是 launch.json 文件的详细配置说明,包括常见的属性及其用途。基本结构launch.json 文件通常位于 .vscode 目录下,具有以下基本结构:主要属性每个配置块代表一个调试配置,包含多个属性。以下是一些常见属性的说明:type: 调试器类型,如 python, cppdbg, node, java, 等。request: 调试请求类型,通常为 launch(启动)或 attach(附加)。name: 配置名称,用户可以在调试配置列表中看到。program: 要调试的程序路径或文件。args: 传递给程序的命令行参数,数组形式。cwd: 当前工作目录。env: 环境变量设置。sourceMaps: 是否启用源映射(通常用于 JavaScript 调试)。preLaunchTask: 调试前要执行的任务(通常用于编译等)。postDebugTask: 调试结束后要执行的任务。stopOnEntry: 是否在程序入口处停止。console: 控制台类型,如 integratedTerminal, externalTerminal, 或 internalConsole。justMyCode: 是否只调试用户代码(用于 Python)。pythonPath: Python 可执行文件的路径(用于 Python)。常见配置项1. Pythonprogram: 要调试的 Python 文件。pythonPath: Python 解释器路径。args: 传递给 Python 脚本的命令行参数。env: 环境变量。console: 控制台类型。2. C++program: 可执行文件路径。args: 命令行参数。stopAtEntry: 是否在程序入口处暂停。cwd: 当前工作目录。environment: 环境变量。externalConsole: 是否使用外部控制台。MIMode: 调试器模式(如 gdb, lldb)。miDebuggerPath: 调试器路径。3. Node.jsprogram: 要调试的 Node.js 文件。args: 命令行参数。runtimeExecutable: Node.js 可执行文件路径。runtimeArgs: 传递给 Node.js 的参数。env: 环境变量。sourceMaps: 是否启用源映射。outFiles: 编译输出文件路径。tasks.jsontasks.json 文件是 Visual Studio Code (VS Code) 中用于配置任务(Tasks)的文件。这些任务可以是编译代码、运行测试、构建项目等自动化任务。以下是 tasks.json 文件的详细配置说明,包括常见的属性及其用途。主要属性每个任务配置块代表一个任务,包含多个属性。以下是一些常见属性的说明:label: 任务的名称或标签,用于在任务列表中标识任务。type: 任务类型,例如 shell 或 process。shell 表示任务将在 shell 中运行,process 表示任务将作为独立的进程运行。command: 要执行的命令,可以是编译器、构建工具、脚本等。args: 传递给命令的参数,数组形式。group: 任务分组,可以设置为 build 或 test,用于标识构建任务或测试任务。presentation: 控制任务输出的呈现方式,例如是否显示在终端中,是否清除之前的输出等。problemMatcher: 配置错误和警告的匹配器,用于从任务输出中解析错误和警告。options: 任务执行的选项,例如环境变量、当前工作目录等。常见配置项label任务的标签名称,用于在 VS Code 任务列表中标识任务。1"label": "build"type任务类型,可以是 shell 或 process。shell 表示任务将在 shell 中运行,process 表示任务将作为独立的进程运行。1"type": "shell"command要执行的命令,例如编译器、脚本或构建工具。1"command": "g++"args传递给命令的参数,数组形式。123456"args": [    "-g",    "${file}",    "-o",    "${fileDirname}/${fileBasenameNoExtension}.out"]group任务分组,用于标识任务的类别,可以是 build 或 test。1234"group": {    "kind": "build",    "isDefault": true}presentation控制任务输出的呈现方式。123456"presentation": {    "echo": true,    "reveal": "always",    "focus": false,    "panel": "shared"}problemMatcher用于解析任务输出中的错误和警告。VS Code 内置了多种匹配器,例如 $gcc, $eslint 等。1"problemMatcher": ["$gcc"]options任务执行的选项,例如环境变量、当前工作目录等。
  • [技术干货] 本地JS文件批量压缩
    最近在维护一个小后台项目,有段JS需要压缩上传到CDN存储服务器。由于之前压缩的JS文件都比较少,都是手动压缩的。这次需要压缩的文件比较多,所以用了批量压缩。特此记录一下,方便大家和自己以后再用到的时候备忘。回到顶部v准备工作安装nodejs首先在本地安装node.js和npm,一般npm集成于nodejs,即安装nodejs,同时也安装了npm。node.js下载地址,下载以后直接不停下一步就行,全部使用默认选项即可。下载完成后打开CMD,node -v检测是否安装成功,安装成功则会显示nodejs版本号。安装uglify插件在cmd命令行执行:npm install uglify-js -g回到顶部v开始压缩压缩的时候将下面的代码拷贝下来,然后生成bat文件,再运行bat文件(有些电脑可能需要windows管理员身份运行),然后依次输入当前的JS文件目录。再输入生成输出压缩后JS的目录即可。@ECHO OFF setlocal enabledelayedexpansion set source_path=%1 set target_dir=%2 IF [%1]==[] ( rem echo please input javascript file or directory set /p source_path=please input javascript file or directory: ) IF [%2]==[] ( rem echo please input output directory set /p target_dir=please input output directory: ) rem source path exists? FOR %%i IN (%source_path%) DO SET FileAttrib=%%~ai if "%FileAttrib%" equ "" ( rem not found file attribute, source path not exist echo source path ^(%source_path%^) doesn't exist exit /b 0 ) ELSE IF "%FileAttrib:~0,1%" equ "d" ( rem source path is directory and not end with \, append \ to source path IF %source_path:~-1% neq \ ( set source_path=%source_path%\ ) ) echo source path is %source_path% rem target path exists? FOR %%i IN (%target_dir%) DO SET fa=%%~ai IF [%fa%]==[] ( rem target path not exist, make it mkdir %target_dir% ) IF %target_dir:~-1% neq \ ( rem append \ to target path set target_dir=%target_dir%\ ) echo target path is %target_dir% IF [%FileAttrib:~0,1%]==[d] ( for /r %source_path% %%I in (*.js) do ( set file_name=%%~nI set parent=%%~dpI set target_parent=%target_dir%!parent:%source_path%=! if not exist !target_parent! mkdir !target_parent! cd !target_parent! if [!file_name:~-4!] neq [.min] ( set w= uglifyjs %%I -m -c -O ascii_only=true -o !target_parent!%%~nI.min.js rem uglify .js file echo uglifyjs from "%%I" to "!target_parent!%%~nI.min.js" start cmd /c "!w!" ) else ( rem copy min.js file echo copy file from "%%~dpnI.js" to "!target_parent!%%~nI.js" start cmd /c "copy %%~dpnI.js !target_parent!%%~nI.js" ) ) ) else ( for %%I in (%source_path%) do ( IF "%%~xI" EQU ".js" ( set file_name=%%~nI if [!file_name:~-4!] neq [.min] ( rem uglify .js file set val=%target_dir%%%~nI.min.js echo uglifyjs from "%%I" to "!val!" start cmd /c "uglifyjs %%I -m -c -O ascii_only=true -o !val!" ) else ( rem copy min.js file echo copy file from "%%I" to "%target_dir%%%~nI.js" start cmd /c "copy %%I %target_dir%%%~nI.js" ) ) ) ) echo done
  • 你敢信?比 setTimeout 还快 80 倍的定时器【转】
    起因很多人都知道,setTimeout是有最小延迟时间的,根据MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间中所说:在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度)。在HTML Standard规范中也有提到更具体的:Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.简单来说,5 层以上的定时器嵌套会导致至少 4ms 的延迟。用如下代码做个测试:12345678910111213141516171819202122232425let a = performance.now();setTimeout(() => {  let b = performance.now();  console.log(b - a);  setTimeout(() => {    let c = performance.now();    console.log(c - b);    setTimeout(() => {      let d = performance.now();      console.log(d - c);      setTimeout(() => {        let e = performance.now();        console.log(e - d);        setTimeout(() => {          let f = performance.now();          console.log(f - e);          setTimeout(() => {            let g = performance.now();            console.log(g - f);          }, 0);        }, 0);      }, 0);    }, 0);  }, 0);}, 0);在浏览器中的打印结果大概是这样的,和规范一致,第五次执行的时候延迟来到了 4ms 以上。探索假设就需要一个「立刻执行」的定时器呢?有什么办法绕过这个 4ms 的延迟吗,在 MDN 文档的角落里有一些线索:如果想在浏览器中实现 0ms 延时的定时器,可以参考这里所说的window.postMessage()。这篇文章里的作者给出了这样一段代码,用postMessage来实现真正 0 延迟的定时器:12345678910111213141516171819202122232425(function () {  var timeouts = [];  var messageName = 'zero-timeout-message';  // 保持 setTimeout 的形态,只接受单个函数的参数,延迟始终为 0。  function setZeroTimeout(fn) {    timeouts.push(fn);    window.postMessage(messageName, '*');  }  function handleMessage(event) {    if (event.source == window && event.data == messageName) {      event.stopPropagation();      if (timeouts.length > 0) {        var fn = timeouts.shift();        fn();      }    }  }  window.addEventListener('message', handleMessage, true);  // 把 API 添加到 window 对象上  window.setZeroTimeout = setZeroTimeout;})();由于postMessage的回调函数的执行时机和setTimeout类似,都属于宏任务,所以可以简单利用postMessage和addEventListener('message')的消息通知组合,来实现模拟定时器的功能。这样,执行时机类似,但是延迟更小的定时器就完成了。再利用上面的嵌套定时器的例子来跑一下测试:全部在 0.1 ~ 0.3 毫秒级别,而且不会随着嵌套层数的增多而增加延迟。测试从理论上来说,由于postMessage的实现没有被浏览器引擎限制速度,一定是比 setTimeout 要快的。设计一个实验方法,就是分别用postMessage版定时器和传统定时器做一个递归执行计数函数的操作,看看同样计数到 100 分别需要花多少时间。实验代码:1234567891011121314151617181920212223242526272829303132333435363738394041424344function runtest() {  var output = document.getElementById('output');  var outputText = document.createTextNode('');  output.appendChild(outputText);  function printOutput(line) {    outputText.data += line + '\n';  }  var i = 0;  var startTime = Date.now();  // 通过递归 setZeroTimeout 达到 100 计数  // 达到 100 后切换成 setTimeout 来实验  function test1() {    if (++i == 100) {      var endTime = Date.now();      printOutput(        '100 iterations of setZeroTimeout took ' +        (endTime - startTime) +        ' milliseconds.'      );      i = 0;      startTime = Date.now();      setTimeout(test2, 0);    } else {      setZeroTimeout(test1);    }  }  setZeroTimeout(test1);  // 通过递归 setTimeout 达到 100 计数  function test2() {    if (++i == 100) {      var endTime = Date.now();      printOutput(        '100 iterations of setTimeout(0) took ' +        (endTime - startTime) +        ' milliseconds.'      );    } else {      setTimeout(test2, 0);    }  }}实验代码很简单,先通过setZeroTimeout也就是postMessage版本来递归计数到 100,然后切换成 setTimeout计数到 100。直接放结论,这个差距不固定,在 mac 上用无痕模式排除插件等因素的干扰后,以计数到 100 为例,大概有 80 ~ 100 倍的时间差距。在硬件更好的台式机上,甚至能到 200 倍以上。Performance 面板只是看冷冰冰的数字还不够过瘾,打开 Performance 面板,看看更直观的可视化界面中,postMessage版的定时器和setTimeout版的定时器是如何分布的。这张分布图非常直观的体现出了上面所说的所有现象,左边的postMessage版本的定时器分布非常密集,大概在 5ms 以内就执行完了所有的计数任务。而右边的setTimeout版本相比较下分布的就很稀疏了,而且通过上方的时间轴可以看出,前四次的执行间隔大概在 1ms 左右,到了第五次就拉开到 4ms 以上。作用也许有同学会问,有什么场景需要无延迟的定时器?其实在 React 的源码中,做时间切片的部分就用到了。12345678910111213141516171819const channel = new MessageChannel();const port = channel.port2;// 每次 port.postMessage() 调用就会添加一个宏任务// 该宏任务为调用 scheduler.scheduleTask 方法channel.port1.onmessage = scheduler.scheduleTask;const scheduler = {  scheduleTask() {    // 挑选一个任务并执行    const task = pickTask();    const continuousTask = task();    // 如果当前任务未完成,则在下个宏任务继续执行    if (continuousTask) {      port.postMessage(null);    }  },};React 把任务切分成很多片段,这样就可以通过把任务交给postMessage的回调函数,来让浏览器主线程拿回控制权,进行一些更优先的渲染任务(比如用户输入)。为什么不用执行时机更靠前的微任务呢?关键的原因在于微任务会在渲染之前执行,这样就算浏览器有紧急的渲染任务,也得等微任务执行完才能渲染。总结可以了解如下几个知识点:setTimeout的 4ms 延迟历史原因,具体表现。如何通过postMessage实现一个真正 0 延迟的定时器。postMessage定时器在 React 时间切片中的运用。为什么时间切片需要用宏任务,而不是微任务。本文转载于:https://juejin.cn/post/7229520942668824633
  • [技术干货] Node.js 未来会在前端领域一家独大吗?
    Node.js 团队近期新发布20.6.0版本,在今年4月的版本上增强了安全性和API引入,同时也增加对新操作系统的支持以及对 TypeScript 兼容性的优化,虽然现在该版本处于测试阶段,你觉得Node.js在未来会呈现怎样的发展态势呢?Node.js新版本发布,你觉得哪一性能提升最实用?你觉得Node.js与JAVA相比,其优势在哪里?未来会超越JAVA吗?
  • [技术干货] 前端使用node.js连接sql.server数据库教程- 前端
     最近项目中要用到node写接口然后连接公司现有的sql.server数据库,再把执行结果返回给前端(还是我),因为之前一直做前端这块,后端这方面不是很懂,花了很长的时间终于研究出来了(还是太菜了,走了很多弯路),所以写个博客,一是复习巩固,二是给其他有需要的小伙伴一个参考,尽量少走弯路,废话不多说,直接上代码  1.首先在自己的数据库新建一张表,用于接下来的测试    2.在自己的项目文件夹中建两个文件,config.js & mssql.js  3.封装数据库信息,config.js  //config.js let app = {     user: 'xxxxxx', //这里写你的数据库的用户名     password: 'xxxxxx',//这里写数据库的密码     server: 'localhost',      database: 'testDB', // 数据库名字     port: 1433, //端口号,默认1433     options: {         encrypt: false,  //加密,设置为true时会连接失败 Failed to connect to localhost:1433 - self signed certificate         enableArithAbort: false     },     pool: {         min: 0,         max: 10,         idleTimeoutMillis: 3000     } }   module.exports = app  4.对sql语句的二次封装 , mssql.js  //mssql.js /**  *sqlserver Model  **/ const mssql = require("mssql"); const conf = require("./config.js");   const pool = new mssql.ConnectionPool(conf) const poolConnect = pool.connect()   pool.on('error', err => {     console.log('error: ', err) }) /**  * 自由查询  * @param sql sql语句,例如: 'select * from news where id = @id'  * @param params 参数,用来解释sql中的@*,例如: { id: id }  * @param callBack 回调函数  */ let querySql = async function (sql, params, callBack) {     try {         let ps = new mssql.PreparedStatement(await poolConnect);         if (params != "") {             for (let index in params) {                 if (typeof params[index] == "number") {                     ps.input(index, mssql.Int);                 } else if (typeof params[index] == "string") {                     ps.input(index, mssql.NVarChar);                 }             }         }         ps.prepare(sql, function (err) {             if (err)                 console.log(err);             ps.execute(params, function (err, recordset) {                 callBack(err, recordset);                 ps.unprepare(function (err) {                     if (err)                         console.log(err);                 });             });         });     } catch (e) {         console.log(e)     } };   /**  * 按条件和需求查询指定表  * @param tableName 数据库表名,例:'news'  * @param topNumber 只查询前几个数据,可为空,为空表示查询所有  * @param whereSql 条件语句,例:'where id = @id'  * @param params 参数,用来解释sql中的@*,例如: { id: id }  * @param orderSql 排序语句,例:'order by created_date'  * @param callBack 回调函数  */ let select = async function (tableName, topNumber, whereSql, params, orderSql, callBack) {     try {         let ps = new mssql.PreparedStatement(await poolConnect);         let sql = "select * from " + tableName + " ";         if (topNumber != "") {             sql = "select top(" + topNumber + ") * from " + tableName + " ";         }         sql += whereSql + " ";         if (params != "") {             for (let index in params) {                 if (typeof params[index] == "number") {                     ps.input(index, mssql.Int);                 } else if (typeof params[index] == "string") {                     ps.input(index, mssql.NVarChar);                 }             }         }         sql += orderSql;         console.log(sql);         ps.prepare(sql, function (err) {             if (err)                 console.log(err);             ps.execute(params, function (err, recordset) {                 callBack(err, recordset);                 ps.unprepare(function (err) {                     if (err)                         console.log(err);                 });             });         });     } catch (e) {         console.log(e)     } };   /**  * 查询指定表的所有数据  * @param tableName 数据库表名  * @param callBack 回调函数  */ let selectAll = async function (tableName, callBack) {     try {         let ps = new mssql.PreparedStatement(await poolConnect);         let sql = "select * from " + tableName + " ";         ps.prepare(sql, function (err) {             if (err)                 console.log(err);             ps.execute("", function (err, recordset) {                 callBack(err, recordset);                 ps.unprepare(function (err) {                     if (err)                         console.log(err);                 });             });         });     } catch (e) {         console.log(e)     } };   /**  * 添加字段到指定表  * @param addObj 需要添加的对象字段,例:{ name: 'name', age: 20 }  * @param tableName 数据库表名  * @param callBack 回调函数  */ let add = async function (addObj, tableName, callBack) {     try {         let ps = new mssql.PreparedStatement(await poolConnect);         let sql = "insert into " + tableName + "(";         if (addObj != "") {             for (let index in addObj) {                 if (typeof addObj[index] == "number") {                     ps.input(index, mssql.Int);                 } else if (typeof addObj[index] == "string") {                     ps.input(index, mssql.NVarChar);                 }                 sql += index + ",";             }             sql = sql.substring(0, sql.length - 1) + ") values(";             for (let index in addObj) {                 if (typeof addObj[index] == "number") {                     sql += addObj[index] + ",";                 } else if (typeof addObj[index] == "string") {                     sql += "'" + addObj[index] + "'" + ",";                 }             }         }         sql = sql.substring(0, sql.length - 1) + ") SELECT @@IDENTITY id"; // 加上SELECT @@IDENTITY id才会返回id         ps.prepare(sql, function (err) {             if (err) console.log(err);             ps.execute(addObj, function (err, recordset) {                 callBack(err, recordset);                 ps.unprepare(function (err) {                     if (err)                         console.log(err);                 });             });         });     } catch (e) {         console.log(e)     } };   /**  * 更新指定表的数据  * @param updateObj 需要更新的对象字段,例:{ name: 'name', age: 20 }  * @param whereObj 需要更新的条件,例: { id: id }  * @param tableName 数据库表名  * @param callBack 回调函数  */ let update = async function (updateObj, whereObj, tableName, callBack) {     try {         let ps = new mssql.PreparedStatement(await poolConnect);         let sql = "update " + tableName + " set ";         if (updateObj != "") {             for (let index in updateObj) {                 if (typeof updateObj[index] == "number") {                     ps.input(index, mssql.Int);                     sql += index + "=" + updateObj[index] + ",";                 } else if (typeof updateObj[index] == "string") {                     ps.input(index, mssql.NVarChar);                     sql += index + "=" + "'" + updateObj[index] + "'" + ",";                 }             }         }         sql = sql.substring(0, sql.length - 1) + " where ";         if (whereObj != "") {             for (let index in whereObj) {                 if (typeof whereObj[index] == "number") {                     ps.input(index, mssql.Int);                     sql += index + "=" + whereObj[index] + " and ";                 } else if (typeof whereObj[index] == "string") {                     ps.input(index, mssql.NVarChar);                     sql += index + "=" + "'" + whereObj[index] + "'" + " and ";                 }             }         }         sql = sql.substring(0, sql.length - 5);         ps.prepare(sql, function (err) {             if (err)                 console.log(err);             ps.execute(updateObj, function (err, recordset) {                 callBack(err, recordset);                 ps.unprepare(function (err) {                     if (err)                         console.log(err);                 });             });         });     } catch (e) {         console.log(e)     } };   /**  * 删除指定表字段  * @param whereSql 要删除字段的条件语句,例:'where id = @id'  * @param params 参数,用来解释sql中的@*,例如: { id: id }  * @param tableName 数据库表名  * @param callBack 回调函数  */ let del = async function (whereSql, params, tableName, callBack) {     try {         let ps = new mssql.PreparedStatement(await poolConnect);         let sql = "delete from " + tableName + " ";         if (params != "") {             for (let index in params) {                 if (typeof params[index] == "number") {                     ps.input(index, mssql.Int);                 } else if (typeof params[index] == "string") {                     ps.input(index, mssql.NVarChar);                 }             }         }         sql += whereSql;         ps.prepare(sql, function (err) {             if (err)                 console.log(err);             ps.execute(params, function (err, recordset) {                 callBack(err, recordset);                 ps.unprepare(function (err) {                     if (err)                         console.log(err);                 });             });         });     } catch (e) {         console.log(e)     } };   exports.config = conf; exports.del = del; exports.select = select; exports.update = update; exports.querySql = querySql; exports.selectAll = selectAll; exports.add = add;  5.接口代码,api/user.js  //user.js const express = require('express'); const db = require('../utils/mssql.js'); const moment = require('moment'); const router = express.Router();   /* GET home page. */ router.get('/info', function (req, res, next) {     db.selectAll('userInfo', function (err, result) {//查询所有userInfo表的数据         res.send(result.recordset)         // res.render('userInfo', { results: result.recordset, moment: moment });     }); }); router.post('/delete', function (req, res, next) {//删除一条id对应的userInfo表的数据     console.log(req.body, 77);     const { UserId } = req.body     const id = UserId     db.del("where id = @id", { id: id }, "userInfo", function (err, result) {         console.log(result, 66);         res.send('ok')     }); }); router.post('/update/:id', function (req, res, next) {//更新一条对应id的userInfo表的数据     var id = req.params.id;     var content = req.body.content;     db.update({ content: content }, { id: id }, "userInfo", function (err, result) {         res.redirect('back');     }); });   module.exports = router;  6.启动文件,server.js  //1.导入模块 const express = require('express')   //2.创建服务器 let server = express() server.use(express.urlencoded()) //中间件要写在启动文件里面   const cors = require('cors') server.use(cors())   const user = require('./api/user.js')   server.use('/', user)   //3.开启服务器 server.listen(8002, () => {     console.log('服务器已启动,在端口号8002') })  7.启动服务器   8.用postman测试接口        数据成功返回~~~ ———————————————— 版权声明:本文为CSDN博主「快乐的小鱼儿12」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/m0_67296095/article/details/126326217 
  • [技术干货] Node.js使用多进程提高任务执行效率【转载】
    在Node.JS中使用多进程非常简单,合理使用多进程,可以解放硬件的能力,让软件的运行效率得到肉眼可见的提升。本文详细讲解了Node.js使用多进程提高任务执行效率的方法,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下最近我有个学员写了个 Node.js 脚本程序,定时从某个服务器下载文件,并向另一个云服务商上传文件。但是每次只能先下载一个文件,再去上传一个文件。效率比较低。于是他向我请教,怎么样才能提高效率?我告诉他应该用 Node 的多进程技术。什么是 Node 多进程?Node 是在单个线程中运行,我们虽然没办法开启额外的线程,但是可以开启进程集群。这样可以让下载任务和上传任务同时进行。使用多进程进行初步代码优化简单看了一下学员的代码,大概是这样:123456789101112const dl = require('./download.js')const ul = require('./upload.js')const source = require('./source.js')async function runTask() {  const { originUrl, targetUrl } = source.getNext()  const { data } = await dl(originUrl)  await ul(targetUrl, data)  runTask()}runTask()这个代码逻辑上是没问题的,但是它只能在 1 个 CPU 核心中运行。我们完全可以使用 Node.js 的多进程来利用 CPU 的多核心来增加这个程序的吞吐量。怎么改造呢?也非常简单。1234567891011121314151617181920212223242526const os = require('os')const cluster = require('cluster')const dl = require('./download.js')const ul = require('./upload.js')const source = require('./source.js')function run() {  if(cluster.isMaster) {    const numCPUs = os.cpus().length;    for(let idx = 0; idx < numCPUs; idx++) {      cluster.fork();    }  } else {    runTask()  }}async function runTask() {    const { originUrl, targetUrl } = source.getNext()    const { data } = await dl(originUrl)    await ul(targetUrl, data)    runTask()  }}run()在上面的代码中,我添加了 os 和 cluster 模块。os 模块可以告诉我们运行环境的 CPU 信息,我们可以通过它来做为创建进程数量的限制条件。然后通过 cluster.isMaster 来判断是否是主进程,因为只有主进程才拥有 fork 的能力。worker和master通信其实上面的代码还可以继续做更深层次的优化,仔细分析一下,下载速度和上传速度其实是不一致的。通常来说,下载速度会很慢,但上传速度会很快。我们可以让其他进程去下载文件,当下载成功之后,让主进程去上传文件。Node 中的多进程之间不会共享内存,所以我们可以通过消息传递的方式,让下载进程通知主进程去上传文件。12345678910111213141516171819202122232425262728const os = require('os')const cluster = require('cluster')const dl = require('./download.js')const ul = require('./upload.js')const source = require('./source.js')function run() {  if(cluster.isMaster) {    const numCPUs = os.cpus().length;    for(let idx = 0; idx < numCPUs; idx++) {      const worker = cluster.fork();      worker.on('message', ({ targetUrl, data }) => {        ul(targetUrl, data)      })    }  } else {    runTask()  }}async function runTask() {  const { originUrl, targetUrl } = source.getNext()  const { data } = await dl(originUrl)  process.send({ targetUrl, data })  runTask()}run()可以在主进程中通过 worker.on('message', (msg)=>{}) 的方式来监听子进程发送的消息。在子进程中通过 process.send 来向主进程发送消息。总结在 NodeJS 中使用多进程非常简单,合理使用多进程,可以解放硬件的能力,让软件的运行效率得到肉眼可见的提升。转载自https://www.jb51.net/article/263647.htm
  • [其他语言] 求助,鲲鹏服务器编译问题
    Node.js前端工程在arm机器安装依赖时出现很多前端组件找不到arm版的问题,很多依赖不兼容arm,请问是否有arm版本的前端组件库?
  • [技术干货] npm install安装报错:gyp info it worked if it ends with ok的解决方法【转载】
    今天新启动一个项目,在 npm install 安装依赖项时出现报错,所以下面这篇文章主要给大家介绍了关于npm install安装报错:gyp info it worked if it ends with ok的解决方法,需要的朋友可以参考下目录• 1.可能原因1:node和node-sass版本冲突, • 2.node.js卸载与安装 • 3.node-sass卸载安装, • 4.安装好后对应版本的node和node-sass后启动 • 总结 前端vue新项目 npm install安装报错 gyp info it worked if it ends with ok1.可能原因1:node和node-sass版本冲突,对应的node和node-sass的版本如下2.node.js卸载与安装之前有安装过弄过node.js的如果node.js版本过高需要重新安装低版本node.js,在控制面板卸载node.js,再在node官网 下载对应的msi包安装到之前的安装过node.js的文件夹下。低版本的node.js 只需直接下载高版本的msi包安装无需卸载之前的低版本node.js,这样不需要再去配置环境变量啥的,3.node-sass卸载安装,node-sass卸载:1npm uninstall node-sassnode-sass安装可以指定版本:npm install node-sass 布置的版本直接安装npm install node-sass@4.14.0 指定4.14版本的,如果安装报错试试换下淘宝镜像,一个好的解决方案在是直接在项目根目录下创建一个 .npmrc文件指定淘宝镜像文件内容:sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ phantomjs_cdnurl=https://npm.taobao.org/mirrors/phantomjs/ electron_mirror=https://npm.taobao.org/mirrors/electron/ registry=https://registry.npm.taobao.org4.安装好后对应版本的node和node-sass后启动再npm install , npm run dev 没有报错啦总结到此这篇关于npm install安装报错:gyp info it worked if it ends with ok解决的文章就介绍到这了转载自https://www.jb51.net/article/255441.htm
  • [技术干货] Node.js 商标转让给 OpenJS 基金会[转载]
    原文链接:https://blog.csdn.net/mengyidan/article/details/122947823在昨日的情人节当天,OpenJS基金会为保证JavaScript开源社区的持续增长,正式宣布收购Node.js徽标和商标的所有权。即日起,为Node.js提供长期赞助与管理的Joyent公司将把Node.js商标的所有权与管理权转让给OpenJS基金会,后者将负责Node.js的商标管理与维护工作。图源:OpenJS基金会官网Node.js是基于Chrome 的V8 JavaScript 引擎构建的JavaScript 运行时,由美国软件工程师瑞安·达尔开发。起初,Node.js的开发和维护工作是由达尔主持,其当时所在的公司Joyen提供赞助,随后达尔离开,Node.js社区也经历了一些分支与合并事件。目前Node.js有OpenJS基金会持有和维护,当下的最新版本为Node.js 17.5.0,其中LTS版本为Node.js 16.14.0。Node.js官网:https://nodejs.org/en/其实在6年前,OpenJS基金会已经获得了Node.js商标的免费永久许可使用权,包括那个六边形图形,商标转让并不会对贡献者产生任何影响。就开源产品而言,商标不仅可以保护开源项目,还能识别代码的具体来源。据OpenJS官博介绍,他们的目标是确保OpenJS商标政策在法律上尽可能的灵活和容易理解,同时保证Node.js以及OpenJS其它产品的服务和质量。Joyent总裁兼首席运营官Sung Whan Moon也表示,Joyent一直相信开源能给开发者和企业创造更多机会,很高兴能看到Node.js为这么多开发者、企业的经济增长带来支撑。随着Node.js进入第2个十年,把它放在一个中立的、并且在需要时能够制定商标限制,充分保证了项目的完整性,OpenJS基金会会是Node.js商标所有权的最佳容身之处。Node.js作为一个健康社区,得到了许多公司的广泛支持,这些公司扩大了这个项目的规模和商业应用,包括彭博社、美国国家航空航天局、Netflix等等。Node.js刚刚交付了Node.js 17,并将Node.js 16移至长期支持(LTS)。Joyent商业集团负责人和OpenJS董事会白金董事Sean Johnson提到,Node.js的采用前景将比以往任何时候都要光明。IBM开放技术和开发者倡导副总裁、OpenJS基金会董事会主席Todd Moore表示,"就推动JavaScript技术的广泛采用以及关键Node.js解决方案和相关技术的持续发展的使命,OpenJS基金会已经做好了准备。"Node.js的维护者们也正在为Node.js未来十年的战略方向进行合作。关于OpenJS基金会OpenJS基金会致力于支持JavaScript生态系统和网络技术的健康发展,提供一个中立的组织来主持和维持项目,并为整个社区的利益合作资助活动。OpenJS基金会由38个开源JavaScript项目组成,包括Appium、Dojo、jQuery、Node.js和webpack,并得到30个企业和最终用户成员的支持,包括GoDaddy、Google、IBM、Intel、Joyent和Microsoft。这些成员认识到JavaScript生态系统的相互关联性,以及为代表重大共享价值的项目提供一个中央家园的重要性。关于Linux基金会Linux基金会成立于2000年,由1000多个成员支持,是世界上领先的开源软件、开放标准和开放硬件的合作之家。Linux基金会的项目,如Linux、Kubernetes、Node.js等,被认为对世界上最重要的基础设施的发展至关重要。其开发方法利用既定的最佳实践,并解决贡献者、用户和解决方案提供者的需求,以创造可持续的开放合作模式。
  • [Node.js] 一文搞懂如何使用Node.js进行TCP网络通信
    >摘要: 网络是通信互联的基础,Node.js提供了net、http、dgram等模块,分别用来实现TCP、HTTP、UDP的通信,本文主要对使用Node.js的TCP通信部份进行实践记录。本文分享自华为云社区[《一文搞懂如何使用Node.js进行TCP网络通信》](https://bbs.huaweicloud.com/blogs/300544?utm_source=zhihu&utm_medium=bbs-ex&utm_campaign=other&utm_content=content),作者:lwq1228 。 # 1、构建TCP服务器 ## 1.1、使用Node.js创建TCP服务器 为了使用Node.js创建TCP服务器,首先要调用require(‘net’)来加载net模块,然后调用net模块的createServer方法就可以轻松地创建一个TCP服务器,语法格式如下: net.createServer([options][, connectionListener]) options是一个对象参数值,有两个布尔类型的属性allowHalfOpen和pauseOnConnect。这两个属性默认都是false; connectionListener是一个当客户端与服务端建立连接时的回调函数,这个回调函数以socket端口对象作为参数。 ## 1.2、监听客户端的连接 使用TCP服务器的listen方法就可以开始监听客户端的连接,语法格式如下: server.listen(port[, host][, backlog][, callback]); port:为需要监听的端口号,参数值为0的时候将随机分配一个端口号; host:服务器地址; backlog:连接等待队列的最大长度; callback:回调函数。 以下代码可以创建一个TCP服务器并监听8001端口: //引入net模块 const net = require('net'); //创建TCP服务器 const server = net.createServer(function (socket) { console.log('有新的客户端接入'); }); //设置监听端口 server.listen(8001, function () { console.log('服务正在监听中。。。') }); 运行这段代码,可以在控制台看到执行了listen方法的回调函数,如图所示: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/03/101037r6ftcnpmytewdq7y.png) 可以使用相应的TCP客户端或者调试工具来连接这个已经创建好的TCP服务器。例如,要使用Windows的Telnet就可以用以下命令来连接: `telnet localhost 8001` 连接成功后可以看到控制台打印了“有新的客户端接入”字样,表明createServer方法的回调函数已经执行,说明已经成功连接到这个创建好的TCP服务器。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/03/101103m2r5z7vxw1udupia.png) server.listen()方法其实触发的是server下的listening事件,所以也可以手动监听listening事件,代码如下: //设置监听端口 server.listen(8001); //设置监听时的回调函数 server.on('listening', function () { console.log("服务正在监听中。。。") }); 除了listening事件外,TCP服务器还支持以下事件: connection:当有新的链接创建时触发,回调函数的参数为socket连接对象。 close:TCP服务器关闭的时候触发,回调函数没有参数。 error:TCP服务器发生错误的时候触发,回调函数的参数为error对象。 下列代码通过net.Server类来创建一个TCP服务器,添加以上事件: //引入net模块 const net = require('net'); //实例化一个服务器对象 const server = new net.Server(); //监听connection事件 server.on('connection', function (socket) { console.log('有新的客户端接入'); }); //设置监听端口 server.listen(8001); //设置监听时的回调函数 server.on('listening', function () { console.log('服务正在监听中。。。'); }); //设置关闭时的回调函数 server.on('close', function () { console.log('服务已关闭'); }); //设置出错时的回调函数 server.on('error', function (err) { console.log('服务运行异常', err); }); ## 1.3、查看服务器监听的地址 当创建了一个TCP服务器后,可以通过server.address()方法来查看这个TCP服务器监听的地址,并返回一个JSON对象,因为这个方法返回的是TCP服务器监听的地址信息,所以应该在调用了server.listen()方法或者绑定了事件listening中的回调函数中调用该方法。这个对象的属性有: port:TCP服务器监听的端口号; family:说明TCP服务器监听的地址是IPv6还是IPv4; address:TCP服务器监听的地址。 代码如下: //引入net模块 const net = require('net'); //创建TCP服务器 const server = net.createServer(function (socket) { console.log('有新的客户端接入'); }); //设置监听端口 server.listen(8001); //设置监听时的回调函数 server.on('listening', function () { //获取地址信息 let address = server.address(); //获取地址详细信息 console.log("服务器监听的端口是:" + address.port); console.log("服务器监听的地址是:" + address.address); console.log("服务器监听的地址类型是:" + address.family); }); 运行结果如图: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/03/101232bvfcuix6qlupwa3o.png) ## 1.4、连接服务器的客户端数量 创建一个TCP服务器后,可以通过server.getConnections()方法获取连接这个TCP服务器的客户端数量。这个方法是一个异步的方法,回调函数有两个参数: 第一个参数为error对象。 第二个参数为连接TCP服务器的客户端数量。 除了获取连接数外,也可以通过设置TCP服务器的maxConnections属性来设置这个TCP服务器的最大连接数。当连接数超过最大连接数的时候,服务器将拒绝新的连接。如下代码设置这个TCP服务器的最大连接数为3。 //引入net模块 const net = require('net'); //创建TCP服务器 const server = net.createServer(function (socket) { console.log('有新的客户端接入'); //设置最大连接数量 server.maxConnections = 3; server.getConnections(function (err, count) { console.log("当前连接的客户端个数为:" + count); }); }); //设置监听端口 server.listen(8001, function () { console.log("服务正在监听中。。。") }); 运行这段代码,并尝试用多个客户端连接。可以发现当客户端连接数超过3的时候,新的客户端就无法连接这个服务器了,如图所示: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/03/101341lkrbbbuscutnlsh1.png) 1.5、获取客户端发送的数据 createServer方法的回调函数参数是一个net.Socket对象(服务器所监听的端口对象),这个对象同样也有一个address()方法,用来获取TCP服务器绑定的地址,同样也是返回一个含有port、family、address属性的对象。通过socket对象可以获取客户端发送的流数据,每次接收到数据的时候触发data事件,通过监听这个事件就可以在回调函数中获取客户端发送的数据,代码如下: //引入net模块 const net = require('net'); //创建TCP服务器 const server = net.createServer(function (socket) { //监听data事件 socket.on("data", function (data) { //打印数据 console.log("接收到数据:" + data.toString()); }); }); //设置监听端口 server.listen(8001, function () { console.log("服务正在监听中。。。") }); 测试结果如下: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/03/101359sey88z16zzwercn5.png) socket对象除了有data事件外,还有connect、end、error、timeout等事件。 1.6、发送数据给客户端 调用socket.write()可以使TCP服务器发送数据,这个方法只有一个必需参数,就是需要发送的数据;第二个参数为编码格式,可选。同时,可以为这个方法设置一个回调函数。当有用户连接TCP服务器的时候,将发送数据给客户端,代码如下: //引入net模块 const net = require('net'); //创建TCP服务器 const server = net.createServer(function (socket) { //设置消息内容 const message = "Hello Client......"; //发送数据 socket.write(message, function () { const writeSize = socket.bytesWritten; console.log("数据发送成功,数据长度为:" + writeSize); }); //监听data事件 socket.on("data", function (data) { const readSize = socket.bytesRead; //打印数据 console.log("接收到数据为:" + data.toString(), ";接收的数据长度为:" + readSize); }); }); //设置监听端口 server.listen(8001, function () { console.log("服务正在监听中。。。") }); 测试结果如下: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/03/1014289szjyty70obklol6.png) 在上面这段代码中还用到了socket对象的bytesWritten和bytesRead属性,这两个属性分别代表着发送数据的字节数和接收数据的字节数。除了上面这两个属性外,socket对象还有以下属性: socket.localPort:本地端口的地址; socket.localAddress:本地IP地址; socket.remotePort:进程端口地址; socket.remoteFamily:进程IP协议族; socket.remoteAddress:进程IP地址。 # 2、构建TCP客户端 Node.js在创建一个TCP客户端的时候同样使用的是net(网络)模块。 ## 2.1、使用Node.js创建TCP客户端 为了使用Node.js创建TCP客户端,首先要调用require(‘net’)来加载net模块。创建一个TCP客户端只需要创建一个连接TCP客户端的socket对象即可: //引入net模块 const net = require('net'); //创建TCP客户端 const client = new net.Socket(); 创建一个socket对象的时候可以传入一个json对象。这个对象有以下属性: fd:指定一个存在的文件描述符,默认值为null; readable:是否允许在这个socket上读,默认值为false; writeable:是否允许在这个socket上写,默认值为false; allowHalfOpen:该属性为false时,TCP服务器接收到客户端发送的一个FIN包后,将会回发一个FIN包;该属性为true时,TCP服务器接收到客户端发送的一个FIN包后不会回发FIN包。 ## 2.2、连接TCP服务器 创建了一个socket对象后,调用socket对象的connect()方法就可以连接一个TCP服务器,代码如下: //引入net模块 const net = require('net'); //创建TCP客户端 const client = new net.Socket(); //设置连接的服务器 client.connect(8001, '127.0.0.1', function () { console.log("连接服务器成功"); }); 连接成功如下图所示: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/03/101536kxduvsr8pn6sjaez.png) ## 2.3、获取从TCP服务器发送的数据 socket对象有data、error、close、end等事件,因可以通过监听data事件来获取从TCP服务器发送的数据,代码如下: //引入net模块 const net = require('net'); //创建TCP客户端 const client = new net.Socket(); //设置连接的服务器 client.connect(8001, '127.0.0.1', function () { console.log("连接服务器成功"); }); //监听data事件 client.on("data", function (data) { //打印数据 console.log("接收到数据为:" + data.toString()); }); 先启动TCP服务端,再运行上面客户端,可以发现命令行中已经输出了来自服务端的数据,说明此时已经实现了服务端和客户端之间的通信: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/03/101604mnpkt4rtvxzjxtdv.png) ## 2.4、向TCP服务器发送数据 因为TCP客户端是一个socket对象,所以可以使用以下代码来向TCP服务器发送数据: //引入net模块 const net = require('net'); //创建TCP客户端 const client = new net.Socket(); //设置连接的服务器 client.connect(8001, '127.0.0.1', function () { console.log("连接服务器成功"); //给服务端发送数据 client.write("Hello Server......"); }); //监听data事件 client.on("data", function (data) { //打印数据 console.log("接收到数据为:" + data.toString()); }); //监听end事件 client.on("end", function () { console.log("客户端发送数据结束") }); 客户端控制台输出: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/03/101633kpjtrqlzbs54yawl.png) 服务端控制台输出: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202112/03/101639opss6jdlnpuorkba.png) 至此使用Node.js进行TCP网络通信完成,如有不对的地方欢迎指正
  • [Node.js] 带你了解Node.js包管理工具:包与NPM
    >摘要:包与NPM Node组织了自身的核心模块,也使得第三方文件模块可以有序的编写和使用。本文分享自华为云社区[《NodeJs深入浅出之旅:包与NPM》](https://bbs.huaweicloud.com/blogs/307034?utm_source=zhihu&utm_medium=bbs-ex&utm_campaign=other&utm_content=content),作者:空城机。 # 包与NPM Node组织了自身的核心模块,也使得第三方文件模块可以有序的编写和使用。 但是在第三方模块中,模块与模块之间仍然是散列在各地的,**相互之间不能直接引用** 所以在模块外,包和NPM是将模块联系起来的机制。 - 包组织模块示意图 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/27/141323rxr3v28syu8prqok.png) CommonJS 的包规范定义其实也很简单,由包结构和包描述文件两部分组成。 # 包结构 用于组织包中的各种文件,是一个存档文件,即一个目录直接打包为.zip或tar.gz格式的文件。 符合规范的包目录: - package.json: 包描述文件 - bin: 用于存放可执行二进制文件的目录 - lib: 用于存放JavaScript代码的目录 - doc: 用于存放文档的目录 - test: 用于存放单元测试用例的代码 # package.json包描述文件 NPM所有行为都与包描述文件的字段息息相关 一些字段: - name: 包名。 规范定义需要用小写的字母和数字组成,不允许出现空格。 包名必须是唯一的,以免对外公布时产生重名冲突 - description: 包简介 - version: 版本号,关于其介绍在《Node.js学习(一)——简介》也有提及 - keywords: 关键字数组, NPM中主要用来作分类搜索。 - maintainers: 包维护者列表。 每个维护者由name、email和web这3个属性组成。 NPM通过这个属性进行权限认证。 格式: ```"maintainers":[{ "name":"kongchengji", "email":"111@.com", "web":"[http:](https://blog.csdn.net/qq_36171287)" }]``` - contributors: 贡献者列表,格式与维护者列表相同 - bugs: 一个可以反馈bug的网页地址或邮件地址 - licenses: 当前包所使用的许可证列表,表示包在哪些许可证下使用 格式: "licenses":[{ "type": "GPLv2", "url":"" }] // 或者 "license": "ISC" - repositories: 托管源代码的位置列表,表明可以通过哪些方式和地址访问包源代码。 - 格式: - "repository": { - "type": "git", - "url": "git+https://github.com/kongchengji/UiSelfMade.git" - }, - dependencies: 使用当前包所需要依赖的包列表。 这个属性非常重要 - homepage: 当前包的网站地址 - os: 操作系统支持列表, 如果列表为空,则不对操作系统做任何假设 - cpi: CPU架构支持列表 - engine:支持的JavaScript引擎列表 - directories:包目录说明 - implements: 实施规范的列表。 标志当前包实现了CommonJS哪些规范 - scripts: 脚本说明对象。 主要用于被包管理器用来安装、编译、测试和卸载包 "scripts": { "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "start": "npm run dev", "lint": "eslint --ext .js,.vue src", "build": "node build/build.js" }, NPM与包规范区别在于多了四个字段: - author: 包作者 :ok_man: - bin: 一些包作者希望包可以作为命令行工具使用。 - main: 模块引入包时,会有限检查这个字段,并将其作为包中其余模块的入口模块。 如果不存在,require会查找包目录下的index.js、index.node、index.json作为默认入口 - devDependencies: 一些模块只在开发时需要依赖。 devDependencies:开发环境使用 dependencies:生产环境使用 # 前后端共用模块 JavaScript在Node出现后,有一项优势 --> 一些模块可以在前后端实现共用。 但是前后端上始终还是有一些差别的 :sweat_drops: # 前后端模块侧重点 前后端JavaScript分别搁置在HTPP的两端,扮演的角色并不同。 浏览器端的JavaScript需要经历从同一个服务器端分发到多个客户端执行,瓶颈是带宽,从网络加载代码 服务器端的JavaScript是相同代码需要多次执行,瓶颈是CPU和内存等资源,从磁盘中加载 在前端JavaScript中,主要还是应用**AMD规范**。 CommonJS并不完全适用于前端JavaScript,比如Node的模块引入基本是同步的,但是前端引入如果使用同步引入,UI在初始化过程中需要花费很多时间等待脚本加载完成。 # AMD规范 AMD规范 是CommonJS规范的一个延伸,全称:Asynchronous Module Definition。 是异步模块定义 模块定义:define(id?, dependencies?, factory); id 是模块的名字,它是可选的参数。 dependencies 指定了所要依赖的模块列表,它是一个数组,也是可选的参数 AMD需要在声明模块时指定所有的依赖,通过形参传递依赖到模块内容中: define(['./a', './b'], function (dep1, dep2) { a.doSomethimg() b.doSomething() }); # CMD规范 与AMD规范相对的还有CMD规范,全称:Common Module Definition。 是公共模块定义 这是由国内的玉伯(也是一位大佬)提出的 模块定义:define(factory) CMD支持动态引入: define(function(require, exports, module) { var a=require('./a') a.doSomethimg() var b=require('./b') b.doSomething() }) 在需要依赖模块时, 随时调用require()引入即可 CMD 推崇依赖就近; AMD 推崇依赖前置 CMD 是延迟执行; AMD 是提前执行 CMD性能好,因为只有用户需要的时候才执行; AMD用户体验好,因为没有延迟,依赖模块提前执行了 AMD和CMD最大的区别是对依赖模块的执行时机处理不同 # 兼容多种模块规范 创建一个hello方法,让hello方法能在不同运行环境中运行,兼容Node、AMD、CMD和常见浏览器 匿名函数前加一个;是个好习惯 name是方法名,definition是方法体 通过typeof检测环境是否为AMD或CMD还是Node环境 可以将模块执行结果挂载在window变量中,这样可以直接调用 // 匿名函数前加一个;是个好习惯 name是方法名,definition是方法体 ;(function (name, definition) { //检查环境是否是AMD或CMD var hasDefine = typeof define === 'function', // 检查环境是否为Node hasExports = typeof module !== 'undefined' && mudule.exports; if(hasDefine) { define(definition); } else if (hasExports) { module.exports = definition(); } else { // 将模块的执行结果挂在window变量中,在浏览器中this指向window对象 this[name] = definition(); } })('hello', function () { var hello = function () { console.log('hello'); } return hello; });