isdream
  • 搜索

    想要找点什么呢?

    • 数据结构-算法
    • 笔记
    • 总结
    • 实用
    • 代码段
    • typeScript
    • 后端
    • 设计模式
    • vue3
    • vite
    • node
    • react
    • 转载
  • 首页
  • 归档
  • 友链
  • 关于
  • 统计
ISDREAM-BLOG
  • 搜索

    想要找点什么呢?

    • 数据结构-算法
    • 笔记
    • 总结
    • 实用
    • 代码段
    • typeScript
    • 后端
    • 设计模式
    • vue3
    • vite
    • node
    • react
    • 转载
  • 首页
  • 归档
  • 友链
  • 关于
  • 统计
置顶发布于 2020-05-09

Git常用指令

310浏览量0条评论总结实用

## [git commit emoji](https://gitmoji.dev/) `git commit -m ":tada: init"` ### git ```bash 查看版本信息 git version 区分大小写 git config core.ignorecase false 查看配置信息 git config --list 在当前目录新建一个Git代码库(生成隐藏.git文件) git init 查看当前历史记录、查看所有的操作记录 git log、git reflog 版本回退 git reset --hard 版本id 查看xx文件修改了哪些内容 git diff xx 清除本地和远端的资源连接 git remote rm origin 关联一个远程库 git remote add origin [远程仓库git地址] xxx.git ``` ## 代码 ```bash 查看文件状态 git status 拉取远程代码 git clone xxxxxxxxx.git 工作区到暂存区 git add 文件名字 多个文件操作 git add . 暂存区到版本区 git commit -m "注释信息" 上传代码 git push git push origin 分支名 创建新分支,并将代码推送 git push -u origin 分支名 同步远程仓库 git pull origin master 强制覆盖本地 git fetch --all && git reset --hard origin/master ``` ### 分支 ```bash 查看分支 git branch 切换分支 git checkout 分支名 合并分支 git merge 分支名 删除本地分支 git branch -d 分支名 创建新的分支并转到该分支 git checkout -b 分支名 查看已合并的分支 git branch --merge 查看未合并的分支 git branch --no-merge 查看远程分支 git branch -r 删除未合并的分支 git branch -d 分支名 删除已合并的分支 git branch -- ``` ### 配置 ```bash 开启git对文件名的大小写敏感 git config core.ignorecase false ``` ### 代理 ```bash # 当前项目设置代理 git config --local http.proxy http://127.0.0.1:7890 git config --local https.proxy https://127.0.0.1:7890 # 当前项目取消代理 git config --local --unset http.proxy git config --local --unset https.proxy # 全局设置代理 git config --global http.proxy http://127.0.0.1:7890 git config --global https.proxy https://127.0.0.1:7890 # 全局取消代理 git config --global --unset http.proxy git config --global --unset https.proxy ``` ### 文件 ```bash 删除文件名 git rm 文件名 恢复一个文件 git checkout ``` ### git commit --amend 修改git提交记录 ```bash 提交过的git历史 git log 接下来,在bash里输入wq退出log状态 git commit --amend second commit 是你上次提交的描述,下面是一下说明信息,有告诉你上次提交的文件信息等等,可忽略。接下来你要是想修改描述信息的话。直接键入:i,此时进入了输入模式 可用键盘上下键转到描述所在的那一行,然后进行修改 修改完成后,按下 Esc键退出编辑模式,在键入 :wq 回车退出并保存修改,完成提交 ``` ### 代码回滚 ```bash 首先通过 git log 查找要回退到的提交标记(commit id),该命令显示从最近到最远的提交日志; git log 或者只显示提交的 commit id 和对应的注释的选项 git log --pretty=oneline 通过 git reset 回滚到指定 commit id git reset --hard 将本地回滚的代码推送到远程仓库,这里需要加强制的选项 `-f` 或 `--force`; git push -f origin ``` ### ssh或http ```bash 切换成http方式: git remote set-url origin xxxxxxxxx.git 切换成ssh方式: git remote set-url origin xxxxxxxxx.git ``` ### 发布npm ```bash 生成一个可供发布的压缩包 git archive 登录npm npm login 发布npm npm publish npm 换源 // npm源 npm config set registry http://registry.npmjs.org // 淘宝源 npm config set registry http://registry.npm.taobao.org ``` ### 查看已安装依赖(只显示一级) `npm ls -g --depth 0 0`

发布于 2026-02-23

AI Agent 协作模式

3浏览量0条评论总结笔记

# AI Agent 协作模式 ## 协作流程 ``` 问题 → [研究专家] → 研究报告 ↓ [设计专家] → 实现计划 ↓ [执行者] → 代码 ↓ [验收者] → 验收报告 ``` ## Agent详解 ### 1. 代码库研究专家 **定位**:理解代码,产出研究报告 **核心职责**: - 分析代码逻辑和数据流向 - 追踪调用链和依赖关系 - 识别潜在风险和设计模式 **输入 → 输出**:问题/代码 → `docs/research/*.md` **关键约束**:只读不写,所有结论必须有代码证据 **使用场景**:需要理解一段陌生代码的工作原理时 ````markdown # Codebase Research Agent Prompt (Optimized for Trae) ## 🔴 核心指令 (System Role) 你是一个资深的代码库研究专家(Codebase Researcher)。你的唯一目标是**深入理解代码逻辑并产出高质量的研究报告**。 **绝对禁止**: - 禁止修改任何现有代码。 - 禁止创建除了研究报告以外的任何文件。 - 禁止基于猜测回答,所有结论必须有代码作为证据。 ## 🎯 执行工作流 (Workflow) 请严格按照以下步骤进行思维推导和执行: ### Phase 1: 广度扫描 (Discovery) 1. **关键词搜索**:基于用户问题,使用全局搜索(grep/search)定位相关的类名、函数名、变量名或错误信息。 2. **文件定位**:识别核心文件,记录文件路径。 3. **历史文档检查**:优先检查 `docs/` 目录下的现有文档,以及项目根目录的 README 或 DESIGN 文档,避免重复研究。 ### Phase 2: 深度分析 (Deep Dive) 1. **逐行阅读**:阅读核心文件的关键函数,理解输入、处理逻辑和输出。 2. **依赖分析**:追踪关键函数的调用链(Call Hierarchy),理清上游调用者和下游依赖。 3. **逻辑映射**:在内存中构建数据流向图,识别状态是如何变化的。 ### Phase 3: 洞察与记录 (Synthesis) 1. **提炼模式**:识别代码中的设计模式或反模式。 2. **风险评估**:思考并发、边界条件、空指针等潜在风险。 3. **撰写报告**:将以上所有发现按照指定格式写入文件。 ## 📝 输出规范 (Documentation Standard) ### 1. 文件保存规则 - **目录**: `docs/research/` (如果目录不存在请自动创建) - **命名**: `YYYY-MM-DD_{主题的英文或拼音简写}.md` (例如: `2023-10-27_auth_login_flow.md`) ### 2. 文档内容模板 请严格遵守以下 Markdown 结构: ```markdown # [研究主题] 研究报告 > **生成时间**: YYYY-MM-DD > **原始问题**: [用户的问题] ## 1. 核心发现摘要 (Executive Summary) [用 3-5 句话概括最关键的发现,直接回答用户的问题核心] ## 2. 涉及文件清单 (File Scope) | 文件路径 (Path) | 核心职责 (Responsibility) | 关键行号 (Lines) | | :--- | :--- | :--- | | `src/controllers/UserController.ts` | 处理登录请求验证 | L45-L89 | | `src/services/AuthService.js` | 具体的 Token 生成逻辑 | L102-L150 | ## 3. 实现逻辑分析 (Implementation Analysis) ### 3.1 数据流向图 (Data Flow) \\```mermaid User -> Controller -> Service -> Database \\``` ### 3.2 关键代码详解 **文件**: `[文件路径]` \\```typescript // 在这里引用关键代码片段 // 必须保留原始行号注释或说明 \\``` *分析说明:[解释这段代码具体做了什么,为什么重要]* ## 4. 架构洞察与模式 (Architecture & Insights) - **设计模式**: [识别到的模式,如单例、工厂等] - **复用机会**: [哪些逻辑可以被提取复用] - **历史债务**: [发现的代码异味或混乱逻辑] ## 5. 潜在风险 (Risks & Edge Cases) - [🔴 高风险]: 描述风险点 - [🟡 中风险]: 描述风险点 ## 6. 待确认问题 (Open Questions) - [ ] [问题 1] - [ ] [问题 2] ``` ## ✅ 任务完成标准 (Definition of Done) 1. 文档已成功保存到指定路径。 2. 向用户输出一段简短的回复,包含: * 文档链接(使用 Markdown 链接格式)。 * 3 个要点的总结。 * 需要用户回答的“待确认问题”列表。 ```` ### 2. 技术方案设计专家 **定位**:制定方案,产出实现计划 **核心职责**: - 将研究报告转化为可执行计划 - 拆分大任务为小阶段(Phase) - 确认技术选型和架构变更 **输入 → 输出**:研究报告 → `docs/plans/*.md` **关键约束**:不写具体实现代码,只定义接口和伪代码 **使用场景**:研究完成后,需要规划如何实现时 ````markdown # Implementation Plan Architect Prompt (Optimized for Trae) ## 🔴 核心指令 (System Role) 你是一个**技术方案架构师 (Implementation Architect)**。你的目标是将高层的研究文档转化为可执行、低耦合、高内聚的代码实现计划。 **核心原则**: 1. **依据事实**:严格基于提供的研究文档 (docs/research/*.md`) 进行设计。 2. **原子化 (Atomicity)**:每个阶段 (Phase) 必须是一个完整的、可独立运行或测试的单元。避免“半成品”代码导致项目无法编译。 3. **上下文友好**:确保每个阶段的代码变更量控制在 LLM 的舒适区(约 200-300 行变更以内)。 4. **不写具体实现**:此阶段只定义接口、伪代码和逻辑流,**不编写**完整的实现代码。 ## 🎯 执行工作流 (Workflow) 请按照以下逻辑进行思考和执行: ### Step 1: 语境摄入 (Ingestion) 1. 读取用户指定的研究文档(必须存在)。 2. 读取项目当前的目录结构和核心配置文件(如 `package.json`, `go.mod` 等),理解项目规范。 ### Step 2: 交互式方案构建 (Iterative Design) **不要直接输出最终文档**。你需要通过对话与用户确认方案。 你需要判断任务的复杂度,灵活决定交互轮次: * **对于简单任务**:直接提出草案,并在末尾附上“待确认事项”。 * **对于复杂任务**: 1. 先确认**技术选型**和**架构变更**。 2. 再确认**边缘情况 (Edge Cases)** 和 **错误处理**。 3. 最后确认**测试策略**。 *在每一轮回复中,请使用``标签简述你对当前方案完整性的评估。* ### Step 3: 方案定稿 (Finalization) 当用户对方案表示满意(回复“通过”、“LGTM”或类似确认)后,将最终计划写入文件。 ## 📝 输出文档规范 (Documentation Standard) ### 1. 文件保存规则 - **目录**: docs/plans/` (不存在则创建) - **命名**: `YYYY-MM-DD_{feature_name}.md` ### 2. 文档内容模板 请严格遵守以下 Markdown 结构: ```markdown # [功能名称] 实现计划 > **制定日期**: YYYY-MM-DD > **基于研究**: [研究文档的相对路径] > **状态**: [Draft/Approved] ## 1. 方案摘要 (Executive Summary) [简述功能目标和核心技术路线] ## 2. 阶段性实现计划 (Phased Implementation) ### Phase 1: [阶段名称 - 例如:基础结构搭建] **目标**: [一句话描述该阶段产出] #### 涉及文件 (File Changes) | 动作 | 文件路径 | 说明 | | :--- | :--- | :--- | | `[Create/Mod/Del]` | `src/api/handler.ts` | 新增接口定义 | #### 逻辑变更 (Logical Changes) **1. [模块/函数名]** - **输入/输出**: `Input: UserDTO -> Output: Result` - **伪代码/逻辑**: //```text IF user exists THEN return error ELSE create user AND send email //``` - **依赖**: 需要先完成 Database Schema 变更 #### 验证标准 (Verification) - [ ] **自动化**: 运行 `npm test path/to/test.ts` 通过 - [ ] **手动**: 调用 API 返回 201 状态码 --- ### Phase 2: [阶段名称 - 例如:核心业务逻辑] ... (同上结构) --- ## 3. 风险管理 (Risk Management) - **风险点**: [描述] - **影响**: [高/中/低] - **缓解**: [具体方案] ## 4. 回滚策略 (Rollback Plan) [如果上线失败,如何快速恢复?例如:Revert commit xxx] //``` ## ✅ 交互引导 (Interactive Prompts) 在每次回复用户(除了最后生成文档)时,必须在末尾包含以下引导模块: //```text --- ### 🛡️ 架构师确认点 为了确保计划万无一失,请确认: 1. [具体问题 1 - 关于技术实现的细节] 2. [具体问题 2 - 关于业务边界的确认] 3. 当前的 Phase 拆分是否过大? ``` ```` ### 3. 精确执行的开发者 **定位**:按计划写代码 **核心职责**: - 严格按照计划文档执行 - 一次只完成一个Phase - 自动运行测试验证 **输入 → 输出**:实现计划 → 代码 **关键约束**:不改计划外的代码,测试未通过不标记完成 **使用场景**:计划确认后,需要落地实现时 ````markdown # Code Implementation Agent Prompt (Optimized for Trae) ## 🔴 核心指令 (System Role) 你是一个**严格执行的资深工程师 (Implementation Engineer)**。你的唯一职责是将《实现计划文档》转化为高质量的代码。 **你的信条**: 1. **按图索骥**:绝对遵循计划文档,不在此阶段进行架构设计或需求变更。 2. **单步执行**:一次只关注一个 Phase,绝不跨阶段操作。 3. **测试驱动**:代码变更必须伴随验证,通过所有检查才算完成。 ## 🎯 执行工作流 (Workflow) 请严格按照以下步骤循环执行: ### Step 1: 锚定与确认 (Anchor & Confirm) 1. **读取计划**:读取用户提供的计划文档 (docs/plans/xxx.md`)。 2. **定位阶段**:找到用户指定的 Phase(或下一个未完成的 Phase)。 3. **展示摘要**:向用户输出即将执行的任务摘要。 > 格式示例: > "我即将执行 **Phase 1: 基础架构**。 > 涉及文件:`src/utils.ts`, `src/app.ts`。 > 预期变更:新增日志工具函数。 > **是否开始执行?**" ### Step 2: 代码落地 (Execution) **用户确认后**,开始修改代码。 1. **逐个修改**:按文件依赖顺序进行编辑。 2. **最小改动**:只保留计划中提到的变更,不要顺手重构无关代码。 3. **保持风格**:遵循项目现有的 ESLint/Prettier 规则。 ### Step 3: 自动化验证 (Auto-Verification) 代码修改完成后,**立即**运行验证命令。 1. **静态检查**:运行 `lint` 或 `typecheck` 命令(根据项目配置)。 2. **单元测试**:运行与当前变更相关的测试用例。 * **如果失败**: * 尝试自动修复简单的 Lint/Type 错误。 * 如果是逻辑错误,向用户报告错误日志,并提出修复建议。 * **禁止**在测试未通过的情况下标记阶段完成。 ### Step 4: 状态同步与提交 (Commit & Sync) **验证通过后**: 1. **更新文档**:自动编辑计划文档,将该 Phase 的状态标记为完成(例如将 `[ ]` 改为 `[x]`)。 2. **提交建议**:建议用户进行 Git 提交,以清空思维负担。 > 建议命令:`git add . && git commit -m "feat: complete Phase X - [description]"` ## 📝 交互输出规范 (Output Standard) ### 1. 阶段完成报告 (Phase Report) 当一个 Phase 彻底完成后,输出以下卡片: ```markdown ## ✅ Phase [N] 执行报告 ### 1. 变更摘要 - [x] 修改 `src/api/user.ts`: 实现了 login 接口 - [x] 新建 `src/types/auth.ts`: 定义了 User 类型 ### 2. 验证结果 | 检查项 | 结果 | 备注 | | :--- | :--- | :--- | | Lint Check | ✅ Pass | 无警告 | | Unit Test | ✅ Pass | 覆盖率 85% | | Manual Check | ⏳ Pending | 请用户手动验证 | ### 3. 下一步行动 建议运行以下命令提交代码,释放上下文: //```bash git commit -m "feat: finish Phase [N]" //``` ``` ## 🚨 异常处理协议 (Exception Protocol) 1. **计划有误时**:如果发现代码现状与计划描述严重不符,**立即暂停**。 * *Action*: 停止写代码,告诉用户:“计划与实际代码冲突,建议先更新计划文档。” 2. **超出范围时**:如果用户要求添加计划外功能。 * *Action*: 拒绝执行:“这超出了当前 Phase 的范围。请先更新计划文档,或在后续 Phase 中处理。” 3. **陷入死循环时**:如果连续 3 次修复 bug 失败。 * *Action*: 停止自动修复,请求人类介入,列出所有尝试过的方法。 ```` ### 4. 严格的OA工程师 **定位**:验收把关,产出验收报告 **核心职责**: - 静态审计:检查代码是否符合计划 - 动态验证:运行测试确保功能正确 - 记录偏差:发现遗漏和问题 **输入 → 输出**:计划+代码 → `docs/validation/*.md` **关键约束**:零信任,零偏差,只陈述事实 **使用场景**:代码完成后,需要验收确认时 ````markdown # Plan Verification Agent Prompt (Optimized for Trae) ## 🔴 核心指令 (System Role) 你是一个**严格的代码审计与QA专家 (Code Auditor & QA)**。你的任务是进行“验收测试”:验证代码实现是否完全符合《实现计划文档》的要求。 **你的信条**: 1. **零信任**:不要假设代码是工作的,除非你看到了测试通过的日志。 2. **零偏差**:计划中定义的任何细节(文件名、函数签名、逻辑流),如果与代码不符,必须报告为“偏差”。 3. **客观中立**:只陈述事实(Fact),不进行主观美化。 ## 🎯 执行工作流 (Workflow) 请严格按照以下步骤执行审计: ### Step 1: 基准确立 (Baseline) 1. **读取计划**:读取用户提供的计划文档 (docs/plans/xxx.md`)。 2. **解析清单**:在内存中构建一个“验收清单 (Checklist)”,包含所有涉及的文件、修改点和验证命令。 ### Step 2: 静态代码审计 (Static Audit) **不运行代码,只看代码。** 1. **存在性检查**:检查计划中要求创建/修改的文件是否存在。 2. **一致性检查**:读取关键文件内容,对比计划中的伪代码/逻辑描述。 * *Check*: 核心函数的签名是否一致? * *Check*: 是否引入了计划未提及的依赖? * *Check*: 是否留有 `TODO` 或 `FIXME` 注释? 3. **Git 状态检查**:运行 `git status` 和 `git diff --stat` 查看实际变更量是否符合预期。 ### Step 3: 动态功能验证 (Dynamic Verification) **运行代码进行验证。** 1. **执行自动化测试**:运行计划中指定的 `make lint`, `npm test` 等命令。 2. **捕获输出**:必须完整捕获命令的标准输出和错误输出。 * 如果命令失败(Exit Code != 0),记录详细错误。 * 如果命令成功,记录通过的测试用例数量。 ## 📝 输出文档规范 (Report Standard) ### 1. 文件保存规则 - **目录**: `docs/validation/` (不存在则创建) - **命名**: `YYYY-MM-DD_{feature_name}.md` ### 2. 文档内容模板 请严格遵守以下 Markdown 结构: ```markdown # 🛡️ 实现验收报告 > **计划文档**: `[路径]` > **验收范围**: Phase [X] - Phase [Y] > **审计时间**: YYYY-MM-DD HH:MM ## 1. 执行摘要 (Executive Summary) **结论**: [ ✅ 通过 / ⚠️ 有偏差但可接受 / ❌ 拒绝验收 ] **完成度**: [X]% (按计划条目计算) ## 2. 详细审计表 (Detailed Audit) | 计划项 (Plan) | 实际代码 (Actual) | 状态 | 证据/备注 | | :--- | :--- | :--- | :--- | | `src/auth.ts` 新增 login | 存在 login 函数 | ✅ | L45-L90 | | 错误处理逻辑 | 只有 try-catch,无重试 | ⚠️ | 计划要求有重试机制 | | `src/utils.js` | 文件不存在 | ❌ | **严重缺失** | ## 3. 自动化验证结果 (Automated Tests) - [x] **Lint**: ✅ Passed (0 errors) - [ ] **Unit Tests**: ❌ Failed - *Failure*: `User.test.ts` - expected 200, got 500. ## 4. 发现的问题 (Issues & Deviations) ### 🔴 阻断性问题 (Blockers) 1. 单元测试未通过,无法保证功能稳定性。 2. 缺失了计划中要求的 `src/utils.js` 文件。 ### 🟡 偏差与警告 (Warnings) 1. 额外安装了 `lodash` 库,但这不在计划范围内。 2. 代码中残留了 console.log 调试信息。 ## 5. 修复建议 (Action Plan) 建议执行以下操作以通过验收: 1. 修复 `User.test.ts` 中的逻辑错误。 2. 补充 `src/utils.js` 的实现。 3. 运行 `npm uninstall lodash` 移除多余依赖。 ``` ## 🚨 异常处理 - 如果**计划文件找不到**:停止执行,要求用户提供正确路径。 - 如果**无法执行测试命令**(如环境未配置):标记为“无法验证”,并建议用户检查环境。 ```` ## 总结 这套模式的核心价值:**让AI在正确的时机做正确的事**。 | Agent | 输入 | 输出 | 核心职责 | | -------- | --------- | -------- | -------- | | 研究专家 | 问题/代码 | 研究报告 | 理解现状 | | 设计专家 | 研究报告 | 实现计划 | 制定方案 | | 执行者 | 实现计划 | 代码 | 写代码 | | 验收者 | 计划+代码 | 验收报告 | 检查质量 |

发布于 2025-09-06

开箱即用的 SPA 动态渲染服务,解决 SEO 与首屏加载问题

6浏览量0条评论总结笔记

# spa-prerender 开箱即用的 SPA 动态渲染服务 - [github](https://github.com/isdreamcn/spa-prerender) ## 为什么需要 SPA 预渲染? 单页应用(SPA)在现代前端开发中占据主导地位,Vue、React 等框架让开发体验大幅提升。然而,SPA 有一个致命弱点——**搜索引擎优化(SEO)困难**。 传统 SPA 的工作流程是:服务器返回一个几乎空白的 HTML 页面,然后 JavaScript 在客户端执行,动态渲染页面内容。这对搜索引擎爬虫非常不友好,因为大多数爬虫不会执行 JavaScript,导致它们只能看到空白页面。 **spa-prerender** 就是为了解决这个问题而生的。它作为一个中间层服务,在服务端使用 Puppeteer 预渲染页面,将完整的 HTML 返回给爬虫和用户,同时保持 SPA 的交互体验。 ## 整体架构 用户/爬虫请求 → spa-prerender (Express) → 预渲染/缓存 → 返回完整 HTML ↓ 静态资源代理 → SPA 源服务器 ``` 核心思路: 1. **静态资源代理**:CSS、JS、图片等静态资源直接代理到源 SPA 服务器 2. **页面预渲染**:HTML 页面请求通过 Puppeteer 进行服务端渲染 3. **Redis 缓存**:渲染结果缓存到 Redis,避免重复渲染 4. **并发控制**:限制同时渲染的数量,防止服务器过载 ## 爬虫识别策略:只对爬虫启用预渲染 在实际生产环境中,预渲染服务会消耗较多服务器资源(启动浏览器、执行 JavaScript)。为了优化性能,我们可以**只对搜索引擎爬虫启用预渲染**,普通用户直接访问原始 SPA。 ### Nginx 配置示例 ```nginx # 根据 UA 判断是否为搜索引擎爬虫 map $http_user_agent $is_bot { default 0; # 常见爬虫关键字,可按需补充 ~*(googlebot|bingbot|slurp|baiduspider|yandex|sogou|360spider|msnbot|duckduckbot|twitterbot|facebookexternalhit) 1; } server { listen 80; server_name example.com; location / { # 爬虫请求转发到预渲染服务 if ($is_bot = 1) { proxy_pass http://127.0.0.1:8080; } # 普通用户直接返回 SPA try_files $uri $uri/ /index.html; } } ``` ### 优化后的架构 ``` ┌──────────────────────────────────────┐ │ Nginx │ │ (爬虫识别与分流) │ └──────────────┬───────────────────────┘ │ ┌────────────────────┼────────────────────┐ │ │ │ ▼ ▼ ▼ 爬虫请求 普通用户 静态资源 │ │ │ ▼ │ │ spa-prerender │ │ (预渲染服务) │ │ │ │ │ └────────────────────┼────────────────────┘ │ ▼ SPA 源服务器 ``` ### 常见搜索引擎爬虫 UA | 搜索引擎 | UA 关键字 | | ---------- | --------------------- | | Google | `googlebot` | | Bing | `bingbot`, `msnbot` | | 百度 | `baiduspider` | | 搜狗 | `sogou` | | 360 | `360spider` | | Yandex | `yandex` | | DuckDuckGo | `duckduckbot` | | Twitter | `twitterbot` | | Facebook | `facebookexternalhit` | 这种策略的优势: - **节省资源**:普通用户不走预渲染,大幅降低服务器负载 - **用户体验**:真实用户直接访问 SPA,交互更流畅 - **SEO 友好**:爬虫获取完整渲染后的 HTML,收录效果最佳 ### 测试爬虫识别 使用 curl 模拟 Google 爬虫请求: ```bash curl -A "Googlebot" http://example.com/ ``` ## 核心代码解析 ### 1. 服务入口:请求路由与代理 ```typescript import express from 'express' import { createProxyMiddleware } from 'http-proxy-middleware' import { ssr } from './utils/ssr' const app = express() // 静态资源直接代理,不走预渲染 app.use( createProxyMiddleware({ pathFilter: (pathname) => { return /\.(css|js|json|png|jpe?g|gif|svg|ico|woff2?|ttf|...)$/i.test( pathname ) }, target: SPA_ORIGIN, changeOrigin: true }) ) // 其他所有请求进行动态渲染 app.get(/.*/, async (req, res) => { const url = `${SPA_ORIGIN}${req.originalUrl}` const { html, ttRenderMs, hitCache } = await ssr(url) res.set('Server-Timing', `Prerender;dur=${ttRenderMs}`) res.set('X-Cache', hitCache ? 'HIT' : 'MISS') res.send(html) }) ``` 通过正则匹配静态资源后缀,直接代理到源服务器,避免不必要的预渲染开销。只有 HTML 页面请求才会触发预渲染流程。 ### 2. 核心渲染引擎:Puppeteer 驱动的 SSR 这是整个项目最核心的部分: ```typescript import puppeteer, { Browser } from 'puppeteer' import pLimit from 'p-limit' const limit = pLimit(3) // 最大 3 条并发渲染 let browserPromise: Promise | undefined let pagesCount = 0 const MAX_PAGES_PER_BROWSER = 50 // 每 50 个 page 重启浏览器 async function realSsr(url: string) { // 1. 安全校验:只允许渲染指定源 const u = new URL(url) if (u.origin !== SPA_ORIGIN) { throw new Error(`Forbidden origin: ${u.origin}`) } // 2. 缓存检查 const key = `ssr:${crypto.createHash('sha256').update(url).digest('hex')}` const redis = await getRedis() const cached = await redis.get(key) if (cached) return { html: cached, ttRenderMs: 0, hitCache: true } // 3. 启动浏览器并渲染 const browser = await getBrowser() const page = await browser.newPage() // 4. 定期重启浏览器,防止内存泄漏 if (++pagesCount >= MAX_PAGES_PER_BROWSER) { browserPromise = browser.close().then(() => launchBrowser()) pagesCount = 0 } await page.setViewport({ width: 1280, height: 720 }) await page.setUserAgent('Mozilla/5.0 (compatible; PrerenderBot/1.0)') await page.goto(url, { waitUntil: 'networkidle0', timeout: 30_000 }) // 5. 等待首屏渲染完成 if (WAIT_FOR_FLAG) { await page.waitForSelector('html[data-prerender-ready="true"]', { timeout: 10_000 }) } else { await page.waitForFunction( () => document.querySelector('#app')?.innerHTML || document.querySelector('#root')?.innerHTML || document.querySelector('#__next')?.innerHTML, { timeout: 10_000 } ) } const html = await page.content() await redis.setEx(key, REDIS_CACHE_TTL, html) return { html, ttRenderMs: Date.now() - start, hitCache: false } } export const ssr = (url: string) => limit(() => realSsr(url)) ``` **关键设计点**: | 设计 | 说明 | | -------------- | ------------------------------------------------------- | | **并发控制** | 使用`p-limit` 限制最大 3 个并发渲染,防止服务器资源耗尽 | | **浏览器复用** | 复用同一个浏览器实例,减少启动开销 | | **定期重启** | 每 50 个页面重启浏览器,防止内存泄漏 | | **双模式等待** | 支持手动标记和自动检测两种首屏完成判定方式 | ### 3. 首屏渲染完成的判定 这是预渲染最关键的问题:**如何知道页面已经渲染完成?** **模式一:手动标记(推荐)** 前端代码在首屏渲染完成后设置标记: ```javascript // Vue 示例 createApp(App).mount('#app') requestAnimationFrame(() => { document.documentElement.setAttribute('data-prerender-ready', 'true') }) ``` **模式二:自动检测** 无需修改业务代码,检测常见挂载点是否有内容: ```typescript await page.waitForFunction( () => document.querySelector('#app')?.innerHTML || document.querySelector('#root')?.innerHTML || document.querySelector('#__next')?.innerHTML, { timeout: 10_000 } ) ``` ### 4. Redis 缓存层 ```typescript import { createClient } from 'redis' let _client: ReturnType export async function getRedis() { if (_client) return _client _client = createClient({ url: process.env.REDIS_URL, socket: { reconnectStrategy: (retries) => Math.min(retries * 50, 500) } }) .on('error', (err) => logger.error('redis error', err)) .on('connect', () => logger.info('redis connected')) await _client.connect() return _client } ``` 缓存策略: - **Key 设计**:`ssr:` + URL 的 SHA256 哈希,确保唯一性 - **TTL**:可配置的过期时间(默认 600 秒) - **连接复用**:单例模式,避免重复创建连接 ## 性能优化总结 ``` ┌─────────────────────────────────────────────────────────────┐ │ 性能优化策略 │ ├─────────────────┬───────────────────────────────────────────┤ │ 静态资源代理 │ 避免不必要的预渲染,直接透传 │ ├─────────────────┼───────────────────────────────────────────┤ │ Redis 缓存 │ 命中缓存时响应时间 < 10ms │ ├─────────────────┼───────────────────────────────────────────┤ │ 浏览器复用 │ 减少浏览器启动开销(约 500ms/次) │ ├─────────────────┼───────────────────────────────────────────┤ │ 并发限制 │ 防止高并发场景下服务器资源耗尽 │ ├─────────────────┼───────────────────────────────────────────┤ │ 定期重启浏览器 │ 防止内存泄漏,保持服务稳定 │ └─────────────────┴───────────────────────────────────────────┘ ``` ## 总结 spa-prerender 通过以下核心技术实现了 SPA 的 SEO 优化: 1. **Express 中间层**:智能路由,静态资源代理 + 页面预渲染 2. **Puppeteer 渲染**:无头浏览器执行 JavaScript,获取完整 DOM 3. **Redis 缓存**:大幅降低重复渲染开销 4. **并发控制**:保护服务稳定性 5. **优雅降级**:支持双模式首屏判定,适应不同场景 这个方案的优点是**对原有 SPA 项目零侵入**——你不需要改造现有的 Vue/React 项目,只需要部署 spa-prerender 服务,配置好源站地址即可。

发布于 2025-08-03

使用Rollup打包发布NPM包

5浏览量0条评论总结笔记

# 使用Rollup打包发布NPM包 在现代前端开发中,打造一个高质量的 NPM 包需要考虑诸多因素:多格式输出、TypeScript 支持、开发体验、发布流程等。下面将以一个真实的 `isdream-oauth` 库为例,讲解如何使用Rollup打包发布NPM包。 ## isdream-oauth - [npm](https://www.npmjs.com/package/isdream-oauth) - [github](https://github.com/isdreamcn/npm-oauth) ## 一、为什么选择 Rollup? Rollup 相比 Webpack,在库打包场景下有天然优势: - **Tree-shaking 原生支持**:生成的代码更精简 - **多格式输出**:一套配置,输出 UMD、CJS、ESM 三种格式 - **无运行时开销**:不像 Webpack 有 bootstrap 代码 ## 二、项目结构设计 ``` npm-oauth/ ├── src/ # 源码目录 │ ├── index.ts # 入口文件 │ └── ... ├── scripts/ # 构建脚本 │ ├── rollup.config.base.js # 基础配置 │ ├── rollup.config.dev.js # 开发配置 │ └── rollup.config.prod.js # 生产配置 ├── examples/ # 示例代码 ├── dist/ # 构建产物 ├── publish/ # 发布目录 ├── index.js # 入口引导文件 └── package.json ``` ## 三、NPM Scripts 设计 ```json { "scripts": { "dev": "rollup -w --environment NODE_ENV:development -c scripts/rollup.config.dev.js", "build": "rollup --environment NODE_ENV:production -c scripts/rollup.config.prod.js", "lint": "eslint . --ext .js,.cjs,.mjs,.ts --fix", "release": "npm run build && cd publish && npm publish" } } ``` | 命令 | 用途 | | --------- | -------------------------- | | `dev` | 启动开发服务器,支持热重载 | | `build` | 生产环境构建,生成发布产物 | | `release` | 构建并发布到 NPM | ## 四、Rollup 配置详解 ### 4.1 基础配置 (rollup.config.base.js) ```javascript eimport path from 'path' import fs from 'fs-extra' import { nodeResolve } from '@rollup/plugin-node-resolve' import commonjs from '@rollup/plugin-commonjs' import alias from '@rollup/plugin-alias' import replace from '@rollup/plugin-replace' import eslint from '@rollup/plugin-eslint' import typescript from '@rollup/plugin-typescript' import { babel } from '@rollup/plugin-babel' import terser from '@rollup/plugin-terser' import clear from 'rollup-plugin-clear' const { name, version, author } = fs.readJsonSync('package.json') export const pkgName = 'isdream-oauth' const banner = `/*! * ${name} v${version} * (c) 2025 ${author} * @license MIT. */` export default { input: 'src/index.ts', external: ['axios'], // 排除 peerDependency output: [ // UMD 格式 - 浏览器直接引用 { file: `dist/${pkgName}.js`, format: 'umd', name: 'isdreamOAuth', banner }, { file: `dist/${pkgName}.min.js`, format: 'umd', name: 'isdreamOAuth', banner, plugins: [terser()] }, // CJS 格式 - Node.js 环境 { file: `dist/${pkgName}.cjs.js`, format: 'cjs', banner }, { file: `dist/${pkgName}.cjs.min.js`, format: 'cjs', banner, plugins: [terser()] }, // ESM 格式 - 现代打包工具 { file: `dist/${pkgName}.esm.js`, format: 'es', banner }, { file: `dist/${pkgName}.esm.min.js`, format: 'es', banner, plugins: [terser()] } ], plugins: [ clear({ targets: ['dist', 'publish'] }), // 清理旧产物 alias({ entries: [{ find: '@', replacement: path.resolve('src') }] }), replace({ 'process.env.NODE_ENV': JSON.stringify('production'), preventAssignment: true }), eslint({ throwOnError: true, include: ['src/**'] }), nodeResolve(), commonjs(), typescript({ tsconfig: './tsconfig.json' }), babel({ babelHelpers: 'bundled', extensions: ['.js', '.ts'] }) ] } ``` **关键点解析:** 1. **external 配置**:将 `axios` 声明为外部依赖,避免打包进去 2. **多格式输出**:同时输出 UMD、CJS、ESM 三种格式,满足不同使用场景 3. **Banner 注释**:自动注入版本、作者等信息 ### 4.2 开发配置 (rollup.config.dev.js) ```javascript import baseConfig from './rollup.config.base.js' import serve from 'rollup-plugin-serve' import livereload from 'rollup-plugin-livereload' export default { ...baseConfig, output: { ...baseConfig.output.find(o => o.format === 'umd'), sourcemap: true // 开启 sourcemap 便于调试 }, plugins: [ ...baseConfig.plugins, serve({ port: 8080, contentBase: ['dist', 'examples/browser'], openPage: '/index.html' }), livereload({ watch: ['dist', 'examples/browser'] }) ] } ``` 开发模式下: - 启动本地服务器,自动打开示例页面 - 文件变化自动重新构建并刷新页面 - 生成 sourcemap 便于调试 ### 4.3 生产配置 (rollup.config.prod.js) ```javascript import fs from 'fs-extra' import baseConfig, { pkgName } from './rollup.config.base.js' import filesize from 'rollup-plugin-filesize' import dts from 'rollup-plugin-dts' // 自动生成发布用的 package.json const generatePackageJson = () => { const pkg = fs.readJsonSync('package.json') fs.outputJsonSync('publish/package.json', { name: pkg.name, version: pkg.version, main: 'index.js', module: 'dist/isdream-oauth.esm.js', types: 'dist/isdream-oauth.d.ts', unpkg: 'dist/isdream-oauth.min.js', jsdelivr: 'dist/isdream-oauth.min.js', // ...其他字段 }, { spaces: 2 }) } // 复制文件到发布目录 const generatePublish = () => ({ name: 'generate-publish', writeBundle() { generatePackageJson() fs.copySync('dist', 'publish/dist') fs.copySync('index.js', 'publish/index.js') fs.copySync('LICENSE', 'publish/LICENSE') fs.copySync('README.md', 'publish/README.md') } }) export default [ // 主构建任务 { ...baseConfig, plugins: [...baseConfig.plugins, filesize(), generatePublish()] }, // 类型声明文件生成 { input: baseConfig.input, output: [{ file: `publish/dist/${pkgName}.d.ts`, format: 'es' }], plugins: [dts()] } ] ``` 生产配置的核心功能: 1. **自动生成发布目录**:将构建产物复制到 `publish/` 目录 2. **精简 package.json**:只保留发布需要的字段 3. **类型声明文件**:使用 `rollup-plugin-dts` 生成 `.d.ts` 文件 ## 五、入口引导文件设计 `index.js` 作为包的主入口,根据环境自动选择产物: ```javascript 'use strict' if (process.env.NODE_ENV === 'production') { module.exports = require('./dist/isdream-oauth.cjs.min.js') } else { module.exports = require('./dist/isdream-oauth.cjs.js') } ``` ## 六、package.json 关键字段 ```json { "name": "isdream-oauth", "version": "0.1.3", "main": "index.js", "module": "dist/isdream-oauth.esm.js", "types": "dist/isdream-oauth.d.ts", "unpkg": "dist/isdream-oauth.min.js", "jsdelivr": "dist/isdream-oauth.min.js", "peerDependencies": { "axios": "^1.0.0" } } ``` | 字段 | 用途 | | ---------------- | --------------------------- | | `main` | Node.js 入口(CJS) | | `module` | ESM 入口,支持 Tree-shaking | | `types` | TypeScript 类型声明 | | `unpkg/jsdelivr` | CDN 自动识别 | ## 七、发布流程 ```bash # 1. 开发调试 npm run dev # 2. 代码检查 npm run lint # 3. 构建并发布 npm run release ``` 执行 `npm run release` 后,Rollup 会: 1. 清理 `dist/` 和 `publish/` 目录 2. 编译 TypeScript 源码 3. 生成 UMD、CJS、ESM 三种格式的产物 4. 生成类型声明文件 `.d.ts` 5. 复制必要文件到 `publish/` 目录 6. 进入 `publish/` 目录执行 `npm publish` ## 八、总结 通过这套 Rollup 配置,实现了: | 特性 | 实现方式 | | --------------- | -------------------------------- | | 多格式输出 | output 配置多个目标 | | TypeScript 支持 | @rollup/plugin-typescript | | 开发体验 | rollup-plugin-serve + livereload | | 类型声明 | rollup-plugin-dts | | 发布自动化 | 自定义插件 + fs-extra | | 代码压缩 | @rollup/plugin-terser | 这套配置不仅适用于 `isdream-oauth` 库,也可以作为任何 JavaScript/TypeScript 库的打包模板使用。 --- **参考资源:** - [Rollup 官方文档](https://rollupjs.org/) - [Rollup 插件列表](https://github.com/rollup/plugins)

发布于 2025-07-01

基于 vue-grid-layout 实现的可拖拽网格布局组件

5浏览量0条评论总结代码段

# Vue Grid Layout 组件 基于 `vue-grid-layout` 实现的可拖拽网格布局组件。 ## 文件说明 | 文件 | 说明 | |------|------| | `grid.vue` | 独立演示组件,包含完整的拖拽功能 | | `gridLayout.vue` | 可复用组件,支持禁用模式,需父组件控制拖拽逻辑 | ### grid.vue ``` Displayed as [x, y, w, h]: {{ item.i }}</b >: [{{ item.x }}, {{ item.y }}, {{ item.w }}, {{ item.h }}] <div @drag="drag" @dragend="dragend" class="droppable-element" draggable="true" unselectable="on" > Droppable Element (Drag me!) <grid-layout ref="gridlayout" v-model:layout="layout" :col-num="state.colCount" :row-height="state.rowHeight" :maxRows="state.rowCount" :margin="state.margin" :is-draggable="true" :is-resizable="true" :vertical-compact="true" :use-css-transforms="true" > <grid-item :key="item.i" v-for="item in layout" ref="gridItems" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :i="item.i" > {{ item.i }} x import { ref, onMounted, onUnmounted } from 'vue' interface GridItem { x: number y: number w: number h: number i: string } interface State { colCount: number rowCount: number rowHeight: number margin: number[] } const state = ref({ colCount: 24, rowHeight: 10, rowCount: 24, margin: [10, 10] }) let mouseXY: any = { x: null, y: null } let DragPos: any = { x: null, y: null, w: 1, h: 1, i: null } // refs const content = ref() const gridlayout = ref() const gridItems = ref([]) const layout = ref([ { x: 0, y: 0, w: 2, h: 6, i: '0' }, { x: 2, y: 0, w: 2, h: 6, i: '1' }, { x: 4, y: 0, w: 2, h: 8, i: '2' }, { x: 6, y: 0, w: 2, h: 10, i: '3' }, { x: 8, y: 0, w: 2, h: 12, i: '4' }, { x: 10, y: 0, w: 2, h: 4, i: '5' }, { x: 0, y: 5, w: 2, h: 6, i: '6' }, { x: 2, y: 5, w: 2, h: 8, i: '7' }, { x: 4, y: 5, w: 2, h: 12, i: '8' }, { x: 5, y: 10, w: 4, h: 4, i: '9' } ]) const setRowHeight = () => { const el = content.value if (!el) return state.value.rowHeight = el.clientHeight / state.value.rowCount - state.value.margin[1] } onMounted(() => { setRowHeight() window.addEventListener('resize', setRowHeight) window.addEventListener('orientationchange', setRowHeight) }) onUnmounted(() => { window.removeEventListener('resize', setRowHeight) window.removeEventListener('orientationchange', setRowHeight) }) onMounted(() => { document.addEventListener( 'dragover', function (e) { mouseXY.x = e.clientX mouseXY.y = e.clientY }, false ) }) const removeItem = (val) => { const index = layout.value.map((item) => item.i).indexOf(val) layout.value.splice(index, 1) } // 拖拽开始 const drag = (e) => { let parentRect = document.getElementById('content')!.getBoundingClientRect() let mouseInGrid = false if ( mouseXY.x > parentRect.left && mouseXY.x < parentRect.right && mouseXY.y > parentRect.top && mouseXY.y < parentRect.bottom ) { mouseInGrid = true } if ( mouseInGrid === true && layout.value.findIndex((item) => item.i === 'drop') === -1 ) { layout.value.push({ x: (layout.value.length * 2) % state.value.colCount, y: layout.value.length + state.value.colCount, // puts it at the bottom w: 8, h: 8, i: 'drop' }) } let index = layout.value.findIndex((item) => item.i === 'drop') if (index !== -1) { try { ;( gridItems.value[layout.value.length - 1] as any ).$refs.item.style.display = 'none' } catch {} let el: any = gridItems.value[index] el.dragging = { top: mouseXY.y - parentRect.top, left: mouseXY.x - parentRect.left } let new_pos = el.calcXY( mouseXY.y - parentRect.top, mouseXY.x - parentRect.left ) if (mouseInGrid === true) { gridlayout.value.dragEvent( 'dragstart', 'drop', new_pos.x, new_pos.y, 8, 8 ) DragPos.i = String(index) DragPos.x = layout.value[index].x DragPos.y = layout.value[index].y } if (mouseInGrid === false) { gridlayout.value.dragEvent('dragend', 'drop', new_pos.x, new_pos.y, 1, 1) layout.value = layout.value.filter((obj) => obj.i !== 'drop') } } } // 拖放结束 const dragend = (e) => { let parentRect = document.getElementById('content')!.getBoundingClientRect() let mouseInGrid = false if ( mouseXY.x > parentRect.left && mouseXY.x < parentRect.right && mouseXY.y > parentRect.top && mouseXY.y < parentRect.bottom ) { mouseInGrid = true } if (mouseInGrid === true) { // alert( // `Dropped element props:\n${JSON.stringify( // DragPos, // ['x', 'y', 'w', 'h'], // 2 // )}` // ) gridlayout.value.dragEvent('dragend', 'drop', DragPos.x, DragPos.y, 1, 1) layout.value = layout.value.filter((obj) => obj.i !== 'drop') // UNCOMMENT below if you want to add a grid-item layout.value.push({ x: DragPos.x, y: DragPos.y, w: 8, h: 8, i: DragPos.i }) gridlayout.value.dragEvent('dragend', DragPos.i, DragPos.x, DragPos.y, 1, 1) try { ;( gridItems.value[layout.value.length - 1] as any ).$refs.item.style.display = 'block' } catch {} } } .droppable-element { width: 150px; text-align: center; background: #fdd; border: 1px solid black; margin: 10px 0; padding: 10px; } .layoutJSON { background: #ddd; border: 1px solid black; margin-top: 10px; padding: 10px; } .columns { -moz-columns: 120px; -webkit-columns: 120px; columns: 120px; } /*************************************/ #content { width: 60vw; height: 60vh; background-color: #eee; } .remove { position: absolute; right: 2px; top: 0; cursor: pointer; } .vue-grid-layout { background: #eee; } .vue-grid-item:not(.vue-grid-placeholder) { background: #ccc; border: 1px solid black; } .vue-grid-item .resizing { opacity: 0.9; } .vue-grid-item .static { background: #cce; } .vue-grid-item .text { font-size: 24px; text-align: center; position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; height: 100%; width: 100%; } .vue-grid-item .no-drag { height: 100%; width: 100%; } .vue-grid-item .minMax { font-size: 12px; } .vue-grid-item .add { cursor: pointer; } .vue-draggable-handle { position: absolute; width: 20px; height: 20px; top: 0; left: 0; background: url("data:image/svg+xml;utf8,") no-repeat; background-position: bottom right; padding: 0 8px 8px 0; background-repeat: no-repeat; background-origin: content-box; box-sizing: border-box; cursor: pointer; } :deep(.vue-grid-item.vue-grid-placeholder) { background: red !important; } ``` ### gridLayout.vue ``` <!-- <div @drag="drag" @dragend="dragend" class="droppable-element" draggable="true" unselectable="on" > Droppable Element (Drag me!) --> <grid-layout ref="gridlayout" v-model:layout="layout" :col-num="state.colCount" :row-height="state.rowHeight" :maxRows="state.rowCount" :margin="state.margin" :is-draggable="true" :is-resizable="true" :vertical-compact="true" :use-css-transforms="true" v-bind="disabledAttrs" > <grid-item :key="item.i" v-for="item in layout" ref="gridItems" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :i="item.i" > {{ item.i }} <span v-if="!disabled" class="remove" @click="removeItem(item.i)" >x</span > import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue' interface GridItem { x: number y: number w: number h: number i: string } interface State { colCount: number rowCount: number rowHeight: number margin: number[] } const props = withDefaults( defineProps<{ disabled?: boolean }>(), { disabled: false } ) const disabledAttrs = computed(() => props.disabled ? { isDraggable: false, isResizable: false } : {} ) const state = ref({ colCount: 24, rowHeight: 10, rowCount: 24, margin: [10, 10] }) let mouseXY: any = { x: null, y: null } let DragPos: any = { x: null, y: null, w: 24, h: 8, i: null } // refs const content = ref() const gridlayout = ref() const gridItems = ref([]) const layout = ref([ { x: 0, y: 0, w: 24, h: 4, i: '0' }, { x: 0, y: 0, w: 12, h: 6, i: '1' }, { x: 12, y: 0, w: 12, h: 8, i: '2' } ]) const setRowHeight = () => { const el = content.value if (!el) return state.value.rowHeight = el.clientHeight / state.value.rowCount - state.value.margin[1] } onMounted(() => { setRowHeight() window.addEventListener('resize', setRowHeight) window.addEventListener('orientationchange', setRowHeight) }) onUnmounted(() => { window.removeEventListener('resize', setRowHeight) window.removeEventListener('orientationchange', setRowHeight) }) onMounted(() => { document.addEventListener( 'dragover', function (e) { mouseXY.x = e.clientX mouseXY.y = e.clientY }, false ) }) const removeItem = (val) => { const index = layout.value.map((item) => item.i).indexOf(val) layout.value.splice(index, 1) } // 拖拽开始 const drag = (e) => { let parentRect = content.value!.getBoundingClientRect() let mouseInGrid = false if ( mouseXY.x > parentRect.left && mouseXY.x < parentRect.right && mouseXY.y > parentRect.top && mouseXY.y < parentRect.bottom ) { mouseInGrid = true } if ( mouseInGrid === true && layout.value.findIndex((item) => item.i === 'drop') === -1 ) { layout.value.push({ x: (layout.value.length * 2) % state.value.colCount, y: layout.value.length + state.value.colCount, // puts it at the bottom w: DragPos.w, h: DragPos.h, i: 'drop' }) } let index = layout.value.findIndex((item) => item.i === 'drop') nextTick(() => { if (index !== -1) { try { ;( gridItems.value[layout.value.length - 1] as any ).$refs.item.style.display = 'none' } catch {} let el: any = gridItems.value[index] el.dragging = { top: mouseXY.y - parentRect.top, left: mouseXY.x - parentRect.left } let new_pos = el.calcXY( mouseXY.y - parentRect.top, mouseXY.x - parentRect.left ) if (mouseInGrid === true) { gridlayout.value.dragEvent( 'dragstart', 'drop', new_pos.x, new_pos.y, DragPos.h, DragPos.w ) DragPos.i = String(index) DragPos.x = layout.value[index].x DragPos.y = layout.value[index].y } if (mouseInGrid === false) { gridlayout.value.dragEvent( 'dragend', 'drop', new_pos.x, new_pos.y, DragPos.h, DragPos.w ) layout.value = layout.value.filter((obj) => obj.i !== 'drop') } } }) } // 拖放结束 const dragend = (e) => { let parentRect = content.value!.getBoundingClientRect() let mouseInGrid = false if ( mouseXY.x > parentRect.left && mouseXY.x < parentRect.right && mouseXY.y > parentRect.top && mouseXY.y < parentRect.bottom ) { mouseInGrid = true } if (mouseInGrid === true) { gridlayout.value.dragEvent( 'dragend', 'drop', DragPos.x, DragPos.y, DragPos.h, DragPos.w ) layout.value = layout.value.filter((obj) => obj.i !== 'drop') // UNCOMMENT below if you want to add a grid-item layout.value.push({ x: DragPos.x, y: DragPos.y, w: DragPos.w, h: DragPos.h, i: DragPos.i }) try { ;( gridItems.value[layout.value.length - 1] as any ).$refs.item.style.display = 'block' } catch {} } } defineExpose({ drag, dragend }) .grid-layout { width: 60vw; height: 60vh; background-color: #eee; } .remove { position: absolute; right: 2px; top: 0; cursor: pointer; } .vue-grid-layout { // background: #eee; } .vue-grid-item:not(.vue-grid-placeholder) { background: #ccc; border: 1px solid black; } .vue-grid-item .resizing { opacity: 0.9; } .vue-grid-item .static { background: #cce; } .vue-grid-item .text { font-size: 24px; text-align: center; position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; height: 100%; width: 100%; } .vue-grid-item .no-drag { height: 100%; width: 100%; } .vue-grid-item .minMax { font-size: 12px; } .vue-grid-item .add { cursor: pointer; } .vue-draggable-handle { position: absolute; width: 20px; height: 20px; top: 0; left: 0; background: url("data:image/svg+xml;utf8,") no-repeat; background-position: bottom right; padding: 0 8px 8px 0; background-repeat: no-repeat; background-origin: content-box; box-sizing: border-box; cursor: pointer; } :deep(.vue-grid-item.vue-grid-placeholder) { background: red !important; } ``` ## 功能特性 - 24x24 网格系统 - 支持从外部拖拽元素到网格 - 网格项可拖拽重排 - 网格项可调整大小 - 支持删除网格项 - 响应式行高自适应 - 支持禁用模式(只读) ## 依赖 - vue 3 - vue-grid-layout ## 使用方式 ### grid.vue(独立演示) 直接作为页面组件使用,包含完整的拖拽演示功能。 ### gridLayout.vue(可复用组件) 父组件通过 ref 引用调用暴露的 `drag` 和 `dragend` 方法控制拖拽行为: ```vue <div @drag="handleDrag" @dragend="handleDragEnd" class="droppable-element" draggable="true" > 拖拽我到网格中 import { ref } from 'vue' import GridLayout from './gridLayout.vue' const gridRef = ref() const isReadonly = ref(false) // 拖拽过程中调用 const handleDrag = (e: DragEvent) => { gridRef.value?.drag(e) } // 拖拽结束时调用 const handleDragEnd = (e: DragEvent) => { gridRef.value?.dragend(e) } .droppable-element { width: 150px; text-align: center; background: #fdd; border: 1px solid black; margin: 10px 0; padding: 10px; cursor: grab; } ``` **控制流程:** ``` 父组件拖拽元素 → @drag/@dragend 事件 → gridRef.value.drag(e)/dragend(e) → 子组件处理布局 ``` ## Props | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | disabled | boolean | false | 禁用拖拽和调整大小 | ## 暴露方法 | 方法 | 说明 | |------|------| | drag(e) | 处理拖拽过程 | | dragend(e) | 处理拖拽结束 |

ICP备案号 苏ICP备19073933号-2

备案苏公网安备 32031202000595号

Copyright © isdream.cn