阅读视图

发现新文章,点击刷新页面。
🔲 ☆

墨梅博客 1.7.0 发布与 AI 开发实践 | 2026 年第 9 周草梅周报

本文在 草梅友仁的博客 发布和更新,并在多个平台同步发布。如有更新,以博客上的版本为准。您也可以通过文末的 原文链接 查看最新版本。

前言

欢迎来到草梅周报!这是一个由草梅友仁基于 AI 整理的周报,旨在为您提供最新的博客更新、GitHub 动态、个人动态和其他周刊文章推荐等内容。


开源动态

本周依旧在开发 墨梅 (Momei) 中。

您可以前往 Demo 站试用:https://demo.momei.app/

  • 您可以通过邮箱 admin@example.com,密码momei123456登录演示用管理员账号。

或前往官网注册:https://momei.app/

也可以前往文档站来了解项目整体规划和未来开发路线图:https://docs.momei.app/

当前墨梅博客已经正式发布了 1.7.0 版本,以下是页面和功能的一些截图。

在文章编辑页面,新增了 AI 语言输入功能,并支持 AI 润色文本。

image-20260301215655911

新增了 AI 封面生成,可以自动基于文章内容生成对应的封面提示词,并生成对应的图片封面。

image-20260301215904772

新增定时文章发布,同步到 Memos,和通过 Wechatsync 的一键分发功能

image-20260301220229378

新增通过文章生成音频功能,可一键生成播客音频。

image-20260301220334081

新增看板娘(Live2D)和背景粒子动画功能。

请注意 Live2D 资源的版权,使用时请遵守相关协议。

image-20260301220518089

更多功能和页面可以前往官网体验,也可前往之前的博客查看截图。

欢迎各位用户体验。并提出意见和建议。

接下来的话还会继续按照路线图和待办进行开发功能,敬请期待。

当然,目前墨梅博客还有很多需要打磨的细节,功能上也还不完善,如有任何意见和建议,都可以在项目的 GitHub issues 中提出。

如果你也对墨梅博客感兴趣,欢迎参与开发和测试。

AI 项目开发与思考

相信最近这段时间大伙已经被网上的 AI 新闻刷屏了。不管是超一流的视频生成 AI Seedance2.0,还是备受关注的 AI 个人助手 OpenClaw 等,都在互联网上引起不小的风波。今天就跟大家来随便聊聊这段时间,我高强度 AI 开发、AI 使用的一些感受吧。

首先就是这段时间高强度 AI 开发的结果——墨梅博客

从立项到第一次 release,大概用了 1 个月,如果刨去因外出而耽误的时间,则大概不到 3 个星期,满打满算也就开发了 20 天。

可以说,对一个个人开发者而言,能有这样的开发速度已经是一件相当惊人的事情了。

可见,AI 加持下,写代码已经不是一件难事了,一个想法转换为实际成果也更加容易了。

如果要我自己来从 0 开发到第一次 release,则预计要耗时 2-3 个月以上。

而在后面的开发中,随着自定义 Agents 和 Skills 的完善,开发速度进一步提升,每次 release 都能有上百条 commit,也包括很多让我自己去实现有难度的内容,比如说项目的全面国际化、全面的 AI 集成(适配 GPT、Gemini、Claude 等不同渠道)等。

这次项目的感悟之一就是能用最好的 AI 模型就还是要用最好的,可以说真的是一分钱一分货。

比如说,个人在开发中为了节约 GitHub Copilot 的使用额度,一直使用 Gemini 3 Flash 来完成主要的开发任务,而 Gemini 3 Flash 也确实完成的很好,至少九成开发任务都能胜任。

但 Gemini 3 Flash(包括 Gemini 3 Pro)没有那么听从 Agents 和 Skills 的指令,经常会不看文档,所以有时候效果不行,还得手动纠正。

直到有一次出现了用 Gemini3-flash 几个小时无法解决的 bug,本人最后才决定换到 GPT-5.3-Codex 试试,结果半小时解决问题。

我估计 Gemini 3.1 Pro 或 Claude 4.6 Opus 也能解决,所以我觉得还是得直接上最强的模型,不然反复 debug 实在痛苦。

当然了,AI 的调用成本终究是个不可忽略的问题,全部用最强的模型纯属氪佬专属,我们贫民玩家还是得做好成本优化。毕竟付费上班也得有个度,每月投入上百到 AI 中还算可以接受,但投入上千就有点过分了。

接下来要谈的就是 AI 编程经常会导致的一个问题,那就是代码质量的低下。

在人工开发时代,我们总以工作量大、赶时间为借口,忽略代码规范、省略测试。但 AI 时代,这个借口彻底不成立了。AI 的效率极高,完全可以严格遵守代码规范,所以代码质量管控变得比任何时候都重要

用 Eslint 来统一代码格式,用 TypeScript 校验类型,用 Vitest 添加单元测试、集成测试、端对端测试,测试用例更是多多益善。而如果以不会写测试为借口,则可以让直接让 AI 生成。

AI 不怕报错,怕的是没有反馈,没有报错信息,AI 就不知道问题在哪,只会觉得自己的代码是对的;有了测试反馈,AI 修复代码的效率和准确率会超乎想象。

这一点其实和人也是差不多的,代码说到底要跑起来才能知道是否有问题

除此之外,一个很多人没想到的问题就是,AI 时代下,项目开发中,文档比代码更加重要

还是一样,以前总说“写代码没空写文档”,现在这些活全都可以交给 AI。

项目推进中,但凡有方案、思路、功能逻辑,都让 AI 生成详细文档记录下来。

一来避免对话中断、内容丢失,二来,每个项目搭配专属的 AI 智能体,定义好技能、完善好文档,就算换任何人接手,都能无缝衔接开发。现在越来越多的项目加入 AI 智能体配置,这就是大势所趋。

当然了,这也得保证文档和代码同步,在一个阶段的开发告一段落之后就得及时的更新相关文档。

个人现在开发新项目的流程就是先和 AI 聊完项目的框架和设计要点,整理成设计文档和待办文档,然后然 AI 根据待办文档,一条一条完成,然后跟设计文档核对,是否有缺漏或者要改进的部分。

对于开发一些小型项目,实测的结果是可以非常完美的跑完全流程。

例如 auto-backup-database,是先敲定了 todo.md 和 design.md 后全权交给 AI 开发的。

接下来就提一下最近很火的 Seedance2.0 好了,我也试了一下,效果非常炸裂,人物一致性非常强,可以说跟 nano banana pro 包揽了视频生成和图片生成的两大王冠。

虽然说随着用的人太多了,免费版本已经开始降智了,加上越来越严格的审核导致出视频难度高,但不可否认的是 Seedance2.0 确实已经在视频生成领域超越了之前的 Sora2,来到了一个新的高度。

回想起当初 ChatGPT 的上下文只有 4k;图片生成还得在本地部署 Stable-diffusion,还要研究 AI 提示词;视频生成更是得走麻烦的工作流才能得到相对较好的结果。现在,都随着 AI 大模型的发展而得到解决,上下文不够?现在的 AI 上下文百万 token 起步;图片生成?nano banana pro 人物一致性拉满;视频生成?Seedance2.0 效果拔群。

所以说,在 AI 时代,相比去学习各种技巧,等着 AI 模型进步还来的更快一些。

当然了,AI 也不是没有反面例子,之前提到的 OpenClaw 就是另一个情况。

OpenClaw 本身是一个开源的 AI 助手,但和之前的 AI 项目不一样的是,OpenClaw 的权限非常大,可以直接操作电脑上的所有东西,也因此,用的好的话,OpenClaw 可以极大的自动化工作流,完成很多重复工作,但用不好的话,那就是删邮件、删项目、删硬盘了。

所以说,天底下没有免费的午餐,在享受 AI 带来的自动化的同时,也必定要承担误删除带来的风险(当然还有隐私泄露)。

絮絮叨叨说了这么多,核心就是一句话:AI 已经彻底重构了个人开发、内容创作、职场工作等的逻辑,也必将进一步改变世界

AI 的浪潮还在滚滚向前,下一期再跟大家聊更多细节,我们下期见。

最新 GitHub 仓库

  • auto-backup-database - 2026-02-24 23:41:10
    服务器数据库自动备份方案,支持本地备份和异地备份
  • rss-image-download - 2026-02-11 18:19:27
    自动从 RSS 下载图片,自动打包,自动备份

GitHub Release

momei

v1.7.0 - 2026-02-28 20:13:31

摘要:
版本 1.7.0 摘要 (2026-02-28)

新功能:

  • 新增国际化支持,优化错误处理和响应格式
  • 添加火山引擎 TTS/ASR 协议支持,简化配置流程
  • 新增音频元数据处理功能,支持从元数据导出音频信息
  • 添加播客模式支持,优化文稿生成逻辑
  • 新增看板娘系统和 CanvasNest 粒子特效支持
  • 添加文章元数据处理功能,优化发布意图解析
  • 新增 Memos 同步配置支持
  • 添加 AudioWorklet 支持以优化音频处理
  • 新增 MCP 服务器 Cursor 规则和性能测试脚本

Bug 修复:

  • 修复 ajv ReDoS 安全漏洞
  • 解决 TTS 服务流处理中的超时问题
  • 修复 Playwright 配置命令顺序问题
  • 更新多个依赖版本确保安全性
  • 优化云端流处理的错误管理
  • 修复语言切换器类型捕获问题
  • 增强移动端 Live2D 显示支持

代码重构:

  • 优化 HTML 标签移除逻辑
  • 重构数据库表结构
  • 使用绝对路径提高代码可读性
  • 添加 WebSocket 权限校验
  • 优化图像生成和任务轮询逻辑
  • 替换 Markdown 编辑器组件
  • 重构初始化设置逻辑

v1.6.0 - 2026-02-21 20:14:13

摘要:
版本 1.6.0 摘要 (2026-02-21)

新功能:

  • 优化 TTS 配置界面,增强用户体验
  • 合并 TTS 与 AI 任务,重构数据库设计
  • 任务详情新增音频时长、大小等详细信息
  • 接入 AI 音频生成和语音识别功能
  • 重构 AI 服务为 TextService 和 ImageService
  • 新增多个 TTS 提供商支持
  • 添加 Gemini 和 Stable Diffusion 图像生成支持
  • 新增文章音频化系统
  • 添加火山引擎 TTS 支持
  • 新增音频文稿优化功能
  • 重构 AI 基础设施,统一 API 路径

Bug 修复:

  • 修复 TTS 文档冲突问题
  • 更新 AI 任务错误处理逻辑
  • 修复自动填充演示账号类型检查
  • 更新 TTS 服务超时处理
  • 优化火山引擎 TTS 错误处理
  • 新增 TTS 估算 API

代码重构:

  • 统一 AI 模块结构
  • 优化代码格式和错误处理
  • 整合 ASR 使用记录至 AITask

主要更新集中在 TTS 功能增强、AI 服务重构和新增多个云服务提供商支持。

v1.5.0 - 2026-02-14 20:09:34

摘要:
版本 1.5.0 摘要 (2026-02-14)

新功能:

  • 编辑器优化:支持侧边栏精简模式、语音转录、自动保存和草稿恢复功能
  • 阅读体验提升:实现沉浸式阅读模式,支持自定义字号、页宽、行高和主题
  • 发布功能增强:添加定时发布、多平台同步(Memos/WechatSync)和文章版本管理
  • AI 功能扩展:新增图像生成模块(支持封面图生成)、语音创作增强和任务管理
  • 通知系统:基于 SSE 实现实时通知,支持降级轮询机制
  • 导出功能:支持全量文章导出为 Markdown 和 ZIP 格式
  • 移动端优化:改进文章详情页排版和响应式设计

Bug 修复:

  • 数据库:修复 Postgres ID 字段溢出问题
  • CLI:清理未使用的导入变量
  • UI:修复响应式设计问题,优化文章详情页布局
  • 营销推送:补全记录操作接口,修复 404 错误
  • 定时任务:更新调度器以支持无服务器环境
  • 图像处理:添加封面图点击放大预览功能

代码重构:

  • 重构 AI 页面组件,简化代码结构
  • 优化 MomeiApi 模拟实现方式
  • 更新系统配置文档,增强安全性说明

其他改进:

  • 添加 Discord 平台支持
  • 优化按钮交互和状态管理
  • 调整降级轮询机制时间间隔为 120 秒

eslint-config-cmyr

v2.1.4 - 2026-02-10 18:39:32

摘要:
版本 2.1.4 (2026-02-10) 摘要:

主要更新:

  • 修复了 TypeScript ESLint 规则中的 bug,启用了不必要的类型断言检查功能

变更详情:

  1. 规则调整:更新了 TypeScript ESLint 配置,新增了对不必要类型断言的检查功能
  2. 影响范围:此变更会影响使用该配置的所有 TypeScript 项目中的类型断言写法
  3. 技术实现:通过提交 b99a33f 完成该修复

cmyr-template-cli

v1.44.1 - 2026-02-24 23:10:16

摘要:
版本 1.44.1 摘要 (2026-02-24)

主要更新内容:

Bug 修复:

  • 更新了 GitHub Actions 配置,增加了调度时间和时区的设置

v1.44.0 - 2026-02-24 21:43:56

摘要:
版本 1.44.0 摘要 (2026-02-2 4 发布)

主要更新内容:

新增功能:

  • 添加了对 TypeScript 项目的 TypeCheck 初始化功能支持

本次更新主要增加了对 TypeScript 项目的类型检查初始化支持,使项目能够更好地适应 TypeScript 开发环境。

v1.43.3 - 2026-02-24 21:13:30

摘要:
版本 1.43.3 (2026-02-24)

主要更新内容:

Bug 修复:

  • 临时注释掉了 lint 命令,以解决 eslint-config-cmyr 版本更新引发的路径错误问题

本次更新主要针对 eslint 配置更新导致的构建问题进行了临时修复。

v1.43.2 - 2026-02-14 22:37:55

摘要:
版本 1.43.2 更新摘要 (2026-02-14)

Bug 修复:

  1. 在 git 提交中添加了–no-verify 选项,用于跳过钩子检查
  2. 将 libsodium-wrappers 依赖版本从^0.7.15 调整为固定版本 0.7.15,确保版本一致性

auto-backup-database

v1.2.2 - 2026-02-27 09:22:37

摘要:
GitHub Release 摘要生成:

版本 1.2.2 (2026-02-27)

Bug 修复:

  • 注释掉了强制使用 path-style 访问的配置项(提交号:125c9f3)

(注:此版本仅包含一项 bug 修复,总字数符合 500 字以内要求)

v1.2.1 - 2026-02-26 11:00:53

摘要:
版本 1.2.1 (2026-02-26) 摘要:

Bug 修复:

  • 在备份任务结果中新增了压缩前后的文件大小信息显示

代码重构:

  • 改进了通知服务的错误处理机制
  • 优化了压缩结果的输出方式

v1.2.0 - 2026-02-25 09:32:42

摘要:
版本 1.2.0 更新摘要:

主要新功能:

  • 新增 BackupTaskResult 类型,优化了通知服务对备份结果的处理逻辑

v1.1.1 - 2026-02-25 01:23:21

摘要:
版本 1.1.1 更新摘要 (2026-02-24)

Bug 修复:

  • 新增支持通过环境变量配置备份输出路径和配置文件路径

v1.1.0 - 2026-02-25 00:51:10

摘要:
版本 1.1.0 更新摘要:

新功能:

  1. 备份服务加密逻辑优化,增强了错误处理和日志记录功能
  2. 新增文件工具支持获取 MIME 类型,用于 OSS 存储操作

代码重构:

  1. 备份服务优化,移除了不必要的动态导入
  2. 配置加载器和压缩功能改进,简化了文件路径处理

最新 GitHub 加星仓库

  • CaoMeiYouRen starred awesome-openclaw-usecases - 2026-03-01 02:35:37
    OpenClaw 社区收集的使用案例合集,旨在简化生活。该项目拥有 14172 个星标。
  • CaoMeiYouRen starred CoPaw - 2026-03-01 01:18:52
    Personal AI Assistant written in Python that can be installed and deployed locally or on cloud. Supports integration with multiple chat applications and offers extensible functionality. Currently has 2,968 stars on GitHub.
  • CaoMeiYouRen starred daily_stock_analysis - 2026-02-28 10:59:49
    LLM 驱动的跨市场智能分析工具,支持 A 股、港股和美股市场。集成多数据源行情、实时新闻分析和 Gemini 决策系统,提供可视化仪表盘功能。具备多渠道推送能力,完全免费使用,支持定时自动运行。采用 Python 语言开发,已在 GitHub 获得近 1.5 万星标。
  • CaoMeiYouRen starred folio-2025 - 2026-02-28 10:58:09
    该项目是一个 JavaScript 开源项目,在 GitHub 上获得了 926 个星标。
  • CaoMeiYouRen starred cloud-mail - 2026-02-28 10:58:04
    基于 Cloudflare 的电子邮件服务项目,主要使用 JavaScript 语言开发,目前在 GitHub 上获得 4642 个星标。该项目提供邮箱服务功能,技术实现依托于 Cloudflare 平台。

其他博客或周刊推荐

阮一峰的网络日志

HelloGitHub 月刊

阿猫的博客

潮流周刊

二丫讲梵的学习周刊

总结

本周的更新和动态如上所示。感谢您的阅读!
您可以通过以下方式订阅草梅周报的更新:

往期回顾

本文作者:草梅友仁
本文地址: https://blog.cmyr.ltd/archives/2026-09-caomei-weekly-momei-1-7-0-release-ai-development.html
版权声明:本文采用 CC BY-NC-SA 4.0 协议 进行分发,转载请注明出处!

🔲 ☆

墨梅博客 MVP 发布与草梅 Auth 更新 | 2025 年第 51 周草梅周报

本文在 草梅友仁的博客 发布和更新,并在多个平台同步发布。如有更新,以博客上的版本为准。您也可以通过文末的 原文链接 查看最新版本。

前言

欢迎来到草梅周报!这是一个由草梅友仁基于 AI 整理的周报,旨在为您提供最新的博客更新、GitHub 动态、个人动态和其他周刊文章推荐等内容。


本周在开发 墨梅 (Momei) 中。

您可以前往官网试用:https://momei.app/

也可以前往文档站来了解项目整体规划和未来开发路线图:https://docs.momei.app/

经过一段时间的高强度开发,在把 GitHub Copilot 用到上限,甚至还额外支出了几美元之后,我终于可以宣布 墨梅博客 已经到了可以初步使用的地步。

QQ截图20251220232130

当然了,距离真正上线肯定还有点距离,目前还存在不少问题要修。

不过作为一个开源项目,最重要的还是先把 MVP(最小可行性产品)版本端上来,先让用户看下大概的样子。

在当前进度中,已经完成了主页、文章页、登录注册页、后台管理页等多个页面的内容,并且支持国际化和暗色模式。

以下是部分页面的截图展示。

主页:

QQ截图20251221215342

文章列表页:

QQ截图20251221215352

正文页:

QQ截图20251221221235

登录注册页:

QQ截图20251221215644

QQ截图20251221215658

国际化演示:

QQ截图20251221215127

暗色模式演示:

QQ截图20251221215117

当然,目前墨梅博客还有很多需要打磨的细节,页面和功能上也还不完善,如有任何意见和建议,都可以在项目的 GitHub issues 中提出。

墨梅博客的 demo 站也会在后续部署。

如果你也对墨梅博客感兴趣,欢迎参与开发和测试。


本周依旧在开发 草梅 Auth 中。

你也可以直接访问官网地址:https://auth.cmyr.dev/
Demo 站:https://auth-demo.cmyr.dev/
文档地址:https://auth-docs.cmyr.dev/

本周 草梅 Auth 发布了 1.12.1 版本。

本周主要是进行了 BUG 修复,以及替换 sqlite3 的数据库驱动为 better-sqlite3,以支持新版版本的 Node.js

如果你对草梅 Auth 感兴趣,欢迎参与开发和测试。

GitHub Release

caomei-auth

v1.12.1 - 2025-12-20 20:14:35

摘要:
版本 1.12.1 (2025-12-20) 摘要:

Bug 修复:

  1. 修正了 revokeConsentSchema 中 clientId 的错误提示信息格式问题
  2. 新增 secureRandom 函数以提高随机数生成的安全性
  3. 将数据库驱动更新为 better-sqlite3,并相应调整了配置

cmyr-template-cli

v1.43.0 - 2025-12-18 21:15:33

摘要:
版本 1.43.0 更新摘要:

新功能:

  • 新增支持创建 GitHub 仓库分支保护规则的功能

Bug 修复:

  • 移除了 catch 块中的错误参数,简化了错误处理流程

最新 GitHub 加星仓库

  • CaoMeiYouRen starred mavonEditor - 2025-12-20 23:26:42
    基于 Vue 的 Markdown 编辑器,支持多种个性化功能。主要开发语言为 Vue,获得 6581 个星标。
  • CaoMeiYouRen starred Ayaya_Miliastra_Editor - 2025-12-19 21:49:47
    支持使用 Python 代码描述节点图,系统内置引擎可解析验证并自动排版,结合自动化脚本将步骤精准映射到实际编辑器。主要开发语言为 Python,项目获得 149 个星标。
  • CaoMeiYouRen starred gitea - 2025-12-18 21:19:11
    基于 Go 语言开发的一体化软件开发服务平台,提供 Git 托管、代码审查、团队协作、包注册表和 CI/CD 功能。该平台采用自托管方式,设计理念强调简单易用,目前已在 GitHub 获得超过 52,000 颗星标。
  • CaoMeiYouRen starred bili-cured-my-neck-pain - 2025-12-15 17:34:52
    B 站 PC 网页版新增视频旋转和缩放功能,使用 TypeScript 开发。该项目已获得 52 个星标。

其他博客或周刊推荐

阮一峰的网络日志

阿猫的博客

潮流周刊

总结

本周的更新和动态如上所示。感谢您的阅读!
您可以通过以下方式订阅草梅周报的更新:

往期回顾

本文作者:草梅友仁
本文地址: https://blog.cmyr.ltd/archives/2025-51-caomei-weekly-momei-blog-mvp-caomei-auth-update.html
版权声明:本文采用 CC BY-NC-SA 4.0 协议 进行分发,转载请注明出处!

🔲 ☆

【译】React 服务器组件中的关键安全漏洞

重要提醒:React 服务器组件曝光了一枚未认证的远程代码执行(RCE)漏洞,只要项目启用了 RSC 支持,就可能被远程掌控。建议所有依赖 React 19 的项目立即排查并升级。

本文根据 React 团队于 2025 年 12 月 3 日发布的「Critical Security Vulnerability in React Server Components」翻译整理。


🚨 漏洞概述

  • 12 月 3 日,React 官方团队发布最高级别安全通告:React 服务器组件(RSC)存在未认证的远程代码执行漏洞。
  • 漏洞已被登记为 CVE-2025-55182,CVSS 得分 10.0(危害最高级别)。
  • 攻击者无需通过身份验证,只要能向 Server Function 端点发送恶意 HTTP 请求,就可能在服务器上执行任意代码,直接接管你的后端环境。
  • 即便你尚未实现任何 React Server Function,只要你的框架或构建工具启用了 RSC 能力,就可能处于风险之中。

⚠️ 你是否受到影响?

受影响的 React 版本

  • 19.0
  • 19.1.0
  • 19.1.1
  • 19.2.0

受影响的框架与工具

  • Next.js
  • React Router
  • Waku
  • @parcel/rsc
  • @vitejs/plugin-rsc
  • rwsdk

以下情况暂不受影响

✅ 完全运行在客户端、没有任何服务端代码的 React 应用
✅ 没有使用支持 React 服务器组件的框架或打包工具

特别注意:只要支持 React 服务器组件,即便没有配置任何 Server Function 端点,也可能遭到攻击!

🛡️ 紧急修复方案

  • React 团队已在 19.0.119.1.219.2.1 中修复,请立即升级到对应分支的安全版本。
  • 若你使用的是 canary 版本(如 Next.js 14.3.0-canary.77+),请降级到最新稳定版,再等待后续补丁。
  • 一些托管服务商已在 React 团队指导下部署临时缓解,但请勿依赖临时方案,必须升级依赖。

基础升级命令示例

1
2
3
npm install react@19.2.1 react-dom@19.2.1
# 或
yarn add react@19.2.1 react-dom@19.2.1

📅 漏洞时间线

  • 11 月 29 日:安全研究员 Lachlan Davidson 通过 Meta Bug Bounty 报告漏洞。
  • 11 月 30 日:Meta 安全团队确认漏洞,并与 React 团队展开修复。
  • 12 月 1 日:修复方案完成,同时与托管服务商和生态项目联动部署缓解措施。
  • 12 月 3 日:补丁发布到 npm,漏洞以 CVE-2025-55182 正式披露。

💡 技术背景

React Server Functions 允许客户端通过 HTTP 请求调用运行在服务器上的函数,React 会负责序列化与反序列化过程。漏洞恰恰出在服务端解码载荷的环节:

  1. 客户端发起的函数调用被转换为 HTTP 请求。
  2. 服务端解析载荷并执行对应的函数。
  3. 攻击者可以伪造恶意载荷,诱使 React 在反序列化过程中执行任意代码。

在官方确认补丁完全部署之前,更多技术细节将保持保密,以免漏洞被大规模利用。

🎯 行动建议

  1. 立即确认项目所用的 React 与框架版本。
  2. 马上升级 React 核心依赖以及框架/打包器提供的 RSC 支持包。
  3. 同步通知团队、合作伙伴及客户,确保所有部署都得到修复。
  4. 监控服务器日志与入侵检测,关注是否存在可疑请求。
  5. 持续关注官方公告(React、Next.js、Expo、Redwood 等),获取最新补丁状态。

升级指南(框架 & 构建工具)

Next.js

1
2
3
4
5
6
7
npm install next@15.0.5   # 15.0.x
npm install next@15.1.9 # 15.1.x
npm install next@15.2.6 # 15.2.x
npm install next@15.3.6 # 15.3.x
npm install next@15.4.8 # 15.4.x
npm install next@15.5.7 # 15.5.x
npm install next@16.0.7 # 16.0.x

若使用 14.3.0-canary.77 或更高的 canary,请降级至最新 14.x 稳定版:

1
npm install next@14

详见 Next.js 安全公告

React Router(不稳定 RSC API)

1
2
3
4
5
npm install react@latest
npm install react-dom@latest
npm install react-server-dom-parcel@latest
npm install react-server-dom-webpack@latest
npm install @vitejs/plugin-rsc@latest

Expo

1
npm install react@latest react-dom@latest react-server-dom-webpack@latest

Redwood SDK

1
2
npm install rwsdk@latest
npm install react@latest react-dom@latest react-server-dom-webpack@latest

更多迁移说明见 Redwood 文档

Waku

1
npm install react@latest react-dom@latest react-server-dom-webpack@latest waku@latest

详情参考 Waku 官方讨论

@vitejs/plugin-rsc

1
npm install react@latest react-dom@latest @vitejs/plugin-rsc@latest

react-server-dom-parcel

1
npm install react@latest react-dom@latest react-server-dom-parcel@latest

react-server-dom-turbopack

1
npm install react@latest react-dom@latest react-server-dom-turbopack@latest

react-server-dom-webpack

1
npm install react@latest react-dom@latest react-server-dom-webpack@latest

🙏 致谢

感谢安全研究员 Lachlan Davidson 发现并报告漏洞,也向在补丁发布期间提供临时缓解与验证的托管服务商、框架团队和社区贡献者致以谢意。

📢 重要提醒

安全无小事! 该漏洞的严重性不容忽视,请务必在今天完成升级,保障用户数据与业务连续性。

转发给你的技术团队,让更多开发者看到这条重要信息!

🔲 ☆

豆包 AI 编程 | 2025 年第 26 周草梅周报

本文在 草梅友仁的博客 发布和更新,并在多个平台同步发布。如有更新,以博客上的版本为准。您也可以通过文末的 原文链接 查看最新版本。

前言

欢迎来到草梅周报!这是一个由草梅友仁基于 AI 整理的周报,旨在为您提供最新的博客更新、GitHub 动态、个人动态和其他周刊文章推荐等内容。


最近在研究 豆包 AI 编程,效果之好超乎我的预料。

虽然目前只能生成简单的前端 HTML 单页面应用,但作为网页的原型设计却已经是绰绰有余。

本人的 RSS Zero草梅 Auth 两个项目都使用了豆包 AI 编程来生成网页原型,效果相当不错。

RSS Zero 和 草梅 Auth 都还在快速开发中,当前页面上的只是设计原型,并未实现交互。

对于这两个页面有任何看法、意见和建议的都可以在评论区留言。

RSS Zero 正在进行问卷调查,点击参与项目问卷调查

image-20250629191035600

image-20250629191116509

此外,甚至还能实现网页小游戏,比如说五子棋。

这个五子棋小游戏甚至带人机对战。当然,逻辑比较简陋。

image-20250629191254828

我把我以前设想过的一些项目,都交给豆包 AI 编程来生成一遍网页原型,其中有一些效果不错,看着就比较有前途,不过也有一些效果不佳,还需调整。

在如今的时代开发一个网页不再是一件难事,也因此,传统的先进行页面设计,再分别进行前后端开发的模式也需要改变。

我认为目前开发一个网页项目最简单的方式还是先使用 AI 生成页面原型,然后直接交付给用户看。如果用户喜欢,再接着实现功能,如果用户不喜欢,就听取意见改进,或者直接重写页面原型。

接下来说一下使用细节上的的问题。

豆包 AI 编程默认情况下生成的页面中,基本上都是用 tailwindcss 来实现样式的。

对于 tailwindcss 本人倒是持中立态度,不过由于学习 tailwindcss 的时间成本有些高,可能也有些人不喜欢使用 tailwindcss。

因此,可以在生成页面的时候,指定样式的风格为纯CSS,不使用tailwindcss,这样就能避免生成带 tailwindcss 的页面。

然后是指定 UI 框架,例如使用 Vue.js 设计,那就指定风格为:使用Vue,CDN地址:https://unpkg.com/vue@3/dist/vue.global.js

之所以还带了个 CDN 地址,是因为这是单页面应用,第三方包都需要从外链引入。

如果希望兼容桌面端和手机端,还可以要求使用响应式设计

身为前端程序员,使用过豆包 AI 编程后,也算是彻底意识到危险了。

在如今的时代,前端基本上是最先被淘汰的一个职位,因此,只有尽快的学习使用 AI 工具,向更多方向发展,才能在 AI 的浪潮下留有自己一分地。

最新 GitHub 加星仓库

  • CaoMeiYouRen starred Font-Awesome - 2025-06-29 12:49:20
    Font Awesome 是一个流行的图标工具包,提供 SVG、字体和 CSS 解决方案。主要开发语言为 JavaScript,在 GitHub 上获得 75,392 颗星标。

  • CaoMeiYouRen starred obs-backgroundremoval - 2025-06-27 17:45:51
    一款基于 C++开发的 OBS 插件,用于去除人像视频背景,便于在录制或直播时更换背景。该项目在 GitHub 上获得 3557 个星标。

  • CaoMeiYouRen starred workout-cool - 2025-06-27 17:44:52
    现代开源健身教练平台,提供训练计划制定、进度追踪和全面的运动数据库功能。主要使用 TypeScript 开发,获得 3582 个星标。

  • CaoMeiYouRen starred OpenList - 2025-06-27 17:41:40
    AList 是一个 Go 语言编写的开源项目,已获得 9975 个星标。该项目新推出一个分支版本,旨在应对信任危机问题。

  • CaoMeiYouRen starred defendnot - 2025-06-27 17:41:21
    通过 WSC API 禁用 Windows Defender 的巧妙方法。该方法使用 C++语言实现,已在 GitHub 上获得 2072 个星标。

其他博客或周刊推荐

阮一峰的网络日志

HelloGitHub 月刊

潮流周刊

二丫讲梵的学习周刊

总结

本周的更新和动态如上所示。感谢您的阅读!
您可以通过以下方式订阅草梅周报的更新:

往期回顾

本文作者:草梅友仁
本文地址: https://blog.cmyr.ltd/archives/2025-26-caomei-weekly-doubao-ai-coding.html
版权声明:本文采用 CC BY-NC-SA 4.0 协议 进行分发,转载请注明出处!

☑️ ☆

Element Plus 组件库架构与主题定制实战

Element Plus 组件库架构与主题定制实战

Hey,小伙伴们!今天来聊聊 Element Plus,一个基于 Vue 3 的强大组件库,不仅能快速搭建后台管理页面,还支持深度定制。接下来,我将带大家快速上手,并深入探索如何自定义主题。

初步配置:快速上手

安装依赖

确保项目基于 Vue 3,然后安装 Element Plus 和 Element Plus Icons:

1
npm install element-plus @element-plus/icons-vue

或者

1
yarn add element-plus @element-plus/icons-vue

全局引入

在项目入口文件 main.jsmain.ts 中全局引入 Element Plus:

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

const app = createApp(App)

app.use(ElementPlus)
app.mount('#app')

这种方式适合快速搭建项目,但可能会导致打包体积较大。接下来,我们来看看如何优化。

进阶操作:按需引入

按需引入可以显著减少最终打包体积,提升加载速度。

安装插件

安装 unplugin-vue-componentsunplugin-auto-import 插件:

1
npm install unplugin-vue-components unplugin-auto-import --save-dev

或者

1
yarn add unplugin-vue-components unplugin-auto-import --dev

配置插件

在构建配置文件中(如 vite.config.jswebpack.config.js)配置插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import AutoImport from 'unplugin-auto-import/vite'

export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
})
]
})

这样,组件会自动按需加载,无需手动导入。

高级定制:自定义主题

Element Plus 提供了强大的 SCSS 变量覆盖功能,可以轻松定制主题。

创建 SCSS 文件

在项目资源文件夹下的 scss 子目录中,创建 custom-theme.scss 文件。路径通常是 src/assets/scss/

覆盖默认主题变量

custom-theme.scss 文件中,使用 @forward 指令覆盖默认主题变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@forward "element-plus/theme-chalk/src/common/var.scss" with (  
$colors: (
"primary": (
"base": #2c82ff,
),
"success": (
"base": #00d68f,
),
"warning": (
"base": #ffaa00,
),
"danger": (
"base": #ff4961,
),
"info": (
"base": #9915c2,
),
)
);

@import "element-plus/theme-chalk/src/index.scss";

引入自定义主题

在项目入口文件中引入自定义主题文件:

1
import './assets/scss/custom-theme.scss';

这样,项目中的 Element Plus 组件就会应用你自定义的主题。

实战应用:打造后台管理页面

有了前面的配置基础,接下来我们来实战应用一下,构建一个简单的后台管理页面。

页面布局

使用 Element Plus 的布局组件搭建页面:

1
2
3
4
5
6
7
8
9
<template>
<el-container>
<el-header>Header</el-header>
<el-container>
<el-aside width="200px">Aside</el-aside>
<el-main>Main</el-main>
</el-container>
</el-container>
</template>

菜单导航

el-aside 中添加 el-menu 组件:

1
2
3
4
5
6
7
8
<el-menu default-active="2" class="el-menu-vertical-demo">
<el-menu-item index="1">处理中心</el-menu-item>
<el-sub-menu index="2">
<template #title>我的工作台</template>
<el-menu-item index="2-1">选项1</el-menu-item>
<el-menu-item index="2-2">选项2</el-menu-item>
</el-sub-menu>
</el-menu>

数据展示

el-main 中使用 el-tableel-pagination 展示数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="date" label="日期" width="150"></el-table-column>
<el-table-column prop="name" label="姓名" width="200"></el-table-column>
<el-table-column prop="address" label="地址"></el-table-column>
</el-table>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 30, 40]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="400"
></el-pagination>

通过这些组件的组合,可以快速构建出一个功能完善的后台管理页面。

☑️ ☆

Vaadin框架教程:Java工程师的前端开发秘籍

后端工程师开发前端的痛点,通常来说莫过于太过繁琐,经常要为了一些很小的事查半天。Vaadin很好的解决了这个痛点,为后端工程师提供易上手、方便使用的前端代码编写解决方案,今天我们就来了解一下。

大家好,今天跟大家介绍一个对后端工程师特别有价值的工具——Vaadin。

说起来,上手前端基本的html, css开发,确实并不难,但是如果只会这些基本的东西,开发起来会很繁琐。如果想要使用前端生态中的各种轮子,虽说便利度提升了,但学习成本也会同步上升。所以,如果不是职业的全栈工程师,只是作为一个后端,想临时写点前端代码,比如自己想做点小项目,通常来说都会有个很痛苦的过程。

Vaadin很好的解决了这个痛点。通过vaadin包装好的常用前端组件,我们几乎可以零学习成本的编写出功能完备、不太难看的页面。对于后端背景的程序员来说,无疑会大幅度降低自己做些小项目的成本。

Vaadin提供的功能,就是可以直接用java代码来写页面。Vaadin提供了多种输入框、表单等等封装好的前端样式,而且与springboot做了深度的融合,使用起来非常方便。

Vaadin的实际原理并不复杂,主要是基于服务端渲染,即在后端生成最终的html代码,交给浏览器。服务端渲染,这个并不罕见,与客户端渲染的优势和劣势,我们在这里不多讲。当然,对于vaadin来说,使用服务端渲染,似乎也没什么好说的,毕竟是写的后端代码,直接在后端做渲染,是个再正常不过的实现路径。Vaadin的引擎对前后端之间的交互做了封装,所以对使用者来说,前后端之间的交互是无感的,在页面层,我们也可以正常的调用后端service.

下面是我写的一段代码示例,可以更直观的感受Vaadin的作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Route(value = "path", layout = MainView.class)
@PageTitle("路径规划")
public class PathView extends VerticalLayout {

@Autowired
private PathService pathService;

public PathView() {
TextField start = new TextField();
TextField end = new TextField();
HorizontalLayout path = new HorizontalLayout();
path.add(start, end);
Button pathCalculate = new Button("calculate path");

VerticalLayout result = new VerticalLayout();
TextField transferNum = new TextField();
TextField distance = new TextField();
Text stations = new Text("");
result.add(
new H3("换乘数: "),
transferNum,
new H3("总距离 :"),
distance,
new H3("途径站点详情:"),
stations
);

pathCalculate.addClickListener(click -> {
PathInfoVO pathResult = pathService.getPath(start.getValue(), end.getValue());
System.out.println(pathResult);
stations.setText(StringUtils.join(pathResult.getDetail(), ","));
transferNum.setValue(String.valueOf(pathResult.getTransferNum()));
distance.setValue(String.valueOf(pathResult.getDistance()));
});

add(
new Text("hello world"),
path,
pathCalculate,
result
);
}
}

这段代码中,我们完全使用java代码对页面中的各个组件进行了编排,包括button的click函数,也是使用java开发者习惯的方式定义,并且能够直接调用其他后端service, 可以说是几乎零学习成本了。

详细代码和运行效果,可以到 项目地址中查看。

如果你对Vaadin感兴趣,或者有任何问题或想法,欢迎在评论区交流。一起探索如何更好地利用Vaadin,提升我们的开发效率吧!

原文地址: https://lichuanyang.top/posts/43947/

☑️ ☆

【前端工程化】Nextjs项目工程化最佳实践总结(献给2023-1024的礼物)

时间已经是2023年了,马上也2024年了,自己去Web3世界闯荡已经一年多了。记得两年前在写文章的时候,发现自己对新领域的狂热在减弱,但是经过在新领域的锤炼,仿佛换起来自己的新狂热程度。时过境迁,React十周年了,Nextjs v13发布也一年了,App Dir(App router)模式的出现,对于自己的吸引力非常大,刚出没多久就在研究和完善相关的工程化,而今经过在企业化项目的实践里,已经锤炼出了一套自认为可以使用的Starter和经验积累。正值1024节,特为大家献上一片总结!

可以配合我总结并且近期也更新的一个starter项目进行理解哟:nextjs-web3-starter

Nextjs介绍和它自v13版本的技术的改进

Nextjs是一个React的元框架

Nextjs在我的印象里是SSR框架,最初使用v12时,了解其SSG概念。之后深入了解,是一个很不错的全干元框架。2022年v13版本发布,beta版本出来的App Dir,当时是这样叫它的,后来定义为APP Router。这种结合React Server Components范式和React Suspense,让人着迷。当时beta版就开始研究并且引入到正式项目里。后来Turbopack的出现,更让人兴奋,但初时由于Turbopack不够完善,许多包都没法兼容,后来提Issue,改包引用或者自己手撸代码造轮子替代,最后剩下的老大难aws的sdk无法兼容也被手撸代码替代了,最后Nextjsv13+Turbopack,开发体验非常爽!如果遇见不兼容Turbopack的包,建议沉下心手撸替代或者是提交PR。

React Server Components

React Server Components(简称RSC)是一种全新范例的名称,我们可以创建专门在服务器上运行的组件。这使我们能够在 React 组件中执行诸如编写数据库查询之类的操作。传统的react组件被叫做client组件,在RSC上,默认情况下所有组件都被假定为服务器组件。当然服务器组件更多需要结合编译工具才能发挥它的特性,所以Nextjs天生适合做这一块东西。可以结合这两篇文章去了解RSC和Nextjs是如何做的:Making Sense of React Server Components / Nextjs Server Components

在处理RSC的时候,最让人困惑的应该就是它的边界状态问题了,应该不少小伙伴第一次来看这个RSC的时候,都会产生疑惑,Server和Client组件嵌套后的是否会重新渲染问题,状态变量如何保存和变更等等。当然请注意,React有这些规则:客户端组件无法渲染服务器组件服务器组件永远不会重新渲染。所以有状态的hooks是在client组件里进使用的。在server组件里,无法使用useEffect和useStae等。Server组件传递的props也是不变的。

但是当涉及到这种边界时,父子组件的关系就不那么重要了,可以通过父子组件来进行拆分和处理状态量。我们来看看Nextjs是怎么做的:Nextjs结合Suspense,服务器组件呈现为一种特殊的数据格式,叫做React 服务器组件有效负载(简称RSC Payload),然后结合Client JavaScript指令在服务器组合成Html。

1
2
3
4
5
self.__next['$1024'] = {
type: 'p',
props: null,
children: "Hello world!",
};

然后在客户端:HTML用于初始页面加载,然后RSC Payload协调客户端和服务器组件树,并更新 DOM,JavaScript指令用于水合客户端组件并使应用程序具有交互性。结合Streaming流式传输,性能优化明显,服务器组件不包含在我们的JS包中,这减少了需要下载的JavaScript数量以及需要水合的组件数量,大大提升了性能和加载速度!

Nextjs工程化技术栈和架构

了解了Nextjs v13+的新特性,我们来聊聊Nextjs工程化的一些事吧。

Nextjs工程化技术栈

大致可以根据传统的工程化领域划分为如下:

  • 编程语言:TypeScript 5.x
  • 构建工具:Nextjs Turbpack + Webpack
  • 前端框架:Nextjs
  • 路由工具:Nextjs文件路由
  • 状态管理:React Context
  • CSS:Tailwindcss+ Postcss
  • HTTP 工具:Fetch
  • 国际化:Nextjs Middleware + intl-localematcher + negotiator
  • 多环境:ENV Config
  • 数据库ORM:Prisma
  • Git Hook 工具:Husky + Lint-staged
  • 代码规范:EditorConfig + Prettier + ESLint
  • 提交规范:Commitlint

Nextjs工程化目录结构

Nextjs 工程化的一些其他细节

关于API请求

这里使用了fetch进行请求,服务端请求和客户端请求保持了一致性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// server端
import { cookies } from 'next/headers';

export const nextFetchGet = async (api: string) => {
const nextCookies = cookies();
const token = nextCookies.get('token') || '';
const role = nextCookies.get('role');
const roleId = nextCookies.get('roleId');
const url =
`${process.env.BASE_FETCH_URL}/api/be${api}`;
const res = await fetch(url, {
headers: token ? { Authorization: 'Bearer ' + token } : {}
});
if (!res.ok) {
throw new Error('Failed to fetch data');
}
return res.json();
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// client端参考
import { getCookie } from './cookie';
import { getApiUrl } from './helpers';

interface RequestOptions extends RequestInit {
responseType?:
| 'TEXT'
| 'JSON'
| 'BLOB'
| 'ARRAYBUFFER'
| 'text'
| 'json'
| 'blob'
| 'arraybuffer';
body?: any;
}

// 发送数据请求
const request = async (url: string, config?: RequestOptions) => {
const finalUrl: string = getApiUrl(url);

const inital: RequestOptions = {
method: 'GET',
body: null,
headers: {
'Content-Type': 'application/json',
Authorization: getCookie('token') ? 'Bearer ' + getCookie('token') : ''
},
credentials: 'include',
cache: 'no-cache',
mode: 'cors',
responseType: 'JSON'
};

const configs: RequestOptions = {
...inital,
...config
};
if (config && config.headers)
configs.headers = {
...inital.headers,
Authorization: getCookie('token') ? 'Bearer ' + getCookie('token') : '',
...config.headers
};

// 基于fetch请求数据
const finalConfig: RequestInit = {
method: configs.method?.toUpperCase(),
credentials: configs.credentials,
mode: configs.mode,
cache: configs.cache,
headers: configs.headers,
body: configs.body
};

return fetch(`${finalUrl}`, finalConfig)
.then((response: Response) => {
// 走到这边不一定是成功的:
// Fetch的特点的是,只要服务器有返回结果,不论状态码是多少,它都认为是成功
const { status } = response;

if (status >= 200 && status < 400) {
// 真正成功获取数据
let result: any;
switch (configs.responseType && configs.responseType.toUpperCase()) {
case 'TEXT':
result = response.text();
break;
case 'JSON':
result = response.json();
break;
case 'BLOB':
result = response.blob();
break;
case 'ARRAYBUFFER':
result = response.arrayBuffer();
break;
default:
result = response.json();
}
return result;
}
// 失败的处理
return Promise.reject(response);
})
.catch((reason: any) => {
// @2:断网
if (typeof window !== 'undefined' && navigator && !navigator.onLine) {
console.log('Your network is break!');
}
// @1:状态码失败
if (reason && reason.status) {
switch (reason.status) {
case 400:
console.log('Please verify your info!');
break;
case 401:
console.log('Please Login!');
break;
case 403:
console.log('You have no access to this');
break;
case 500:
console.log("Oops, there's something wrong!");
break;
case 504:
console.log("Oops, there's something wrong!");
break;
default:
}
} else {
// @3:处理返回数据格式失败
console.log("Oops, there's something wrong!");
}

return Promise.reject(reason);
});
};

export default request;

关于代理API问题

这里看自己需要,如果Nextjs的资源不及后端接口,还是不建议直接代理所有接口,只需要编写服务器组件所需要的接口代理就行了。如果需要全局代理后端接口,可以使用rewrites的方式,不建议使用以前proxy包的方式了:

1
2
3
4
5
6
7
8
9
10
11
12
const nextConfig = {
rewrites: () => {
return [
{
// 注意,加了一个be/,为什么?为了区分我们项目写的接口,避免被代理进去了。我们只需要代理外部的接口
// Note that a be/ is added, why? In order to distinguish the interface written by our project, avoid being proxied. We only need to proxy the external interface
source: '/api/be/:slug*',
destination: `${process.env.BACKEND_URL}/api/:slug*`
}
];
}
};

Nextjs 国际化

使用了Nextjs中间件模式和intl-localematcher + negotiator进行国际化处理的,由于nextjs版本的更新导致输出的结构变化,所以中间件随着版本更新需要进行维护,如果在生产上版本更新需要注意,最新版中间件如下:

1
pnpm add @formatjs/intl-localematcher negotiator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

import { i18n } from '@/i18n/config';

import { match as matchLocale } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';

function getLocale(request: NextRequest): string | undefined {
// Negotiator expects plain object so we need to transform headers
const negotiatorHeaders: Record<string, string> = {};
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));

// Use negotiator and intl-localematcher to get best locale
const languages = new Negotiator({ headers: negotiatorHeaders }).languages();
const locales: string[] = i18n.locales as unknown as string[];
return matchLocale(languages, locales, i18n.defaultLocale);
}

export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;

// Check if there is any supported locale in the pathname
const pathnameIsMissingLocale = i18n.locales.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
);

// Redirect if there is no locale
if (pathnameIsMissingLocale) {
const locale = getLocale(request);

// e.g. incoming request is /products
// The new URL is now /en-US/products
return NextResponse.redirect(
new URL(`/${locale}${pathname === '' ? '/' : pathname}`, request.url)
);
}
}

export const config = {
// 2023-8-28 update
matcher: '/((?!api|static|.*\\..*|_next).*)'
};

我们配置获取语言和数据,目前由于Turbopack的一些bug,导致需要使用如下方式引用json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// getDictionary.ts
import 'server-only';
import type { Locale } from './config';
import enJson from '@/i18n/locales/en.json' assert { type: 'json' };
import zhCNJson from '@/i18n/locales/zh-CN.json' assert { type: 'json' };

// We enumerate all dictionaries here for better linting and typescript support
// We also get the default import for cleaner types
const dictionaries = {
en: () => enJson,
'zh-CN': () => zhCNJson
// https://github.com/vercel/next.js/issues/47595
// en: () =>
// import('@/i18n/locales/en.json', { assert: { type: 'json' } }).then(
// (module) => module.default
// ),
// 'zh-CN': () =>
// import('@/i18n/locales/zh-CN.json', { assert: { type: 'json' } }).then(
// (module) => module.default
// )
};

export const getDictionary = async (locale: Locale) => dictionaries[locale]();

1
2
3
4
5
6
7
8
// config.ts
export const i18n = {
defaultLocale: 'en',
locales: ['en', 'zh-CN']
} as const;

export type Locale = (typeof i18n)['locales'][number];

Nextjs PWA

参考目前市面上的Nextjs PWA插件,目前采取了@ducanh2912/next-pwa,需要修改next.config.js,参考如下,具体细节和定义可以看包文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// next.config.js
// eslint-disable-next-line @typescript-eslint/no-var-requires
const withPWA = require('@ducanh2912/next-pwa').default({
dest: 'public',
cacheOnFrontEndNav: true,
aggressiveFrontEndNavCaching: true
});

/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};

module.exports = withPWA(nextConfig);

请注意,需要在public目录配置mainfest.json

同时layout.tsx记得引入mainfest.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Metadata } from 'next';

export async function generateMetadata(): Promise<Metadata> {
return {
...DefaultMetadata,
title: 'Create Next App',
description: 'Generated by create next app',
applicationName: 'vadxq',
manifest: '/mainfest.json',
themeColor: '#FFFFFF',
appleWebApp: {
capable: true,
statusBarStyle: 'default',
title: 'vadxq'
},
formatDetection: {
telephone: false
},
icons: {
shortcut: '/favicon.ico',
apple: [{ url: '/favicon.ico', sizes: '180x180' }]
}
};
}

对了,会自动生成一堆文件在public目录下,本地开发的话建议将这些添加到gitignore下:

1
2
3
public/sw.js
public/swe-worker-*
public/workbox-*

关于Nextjs环境区分变量

这里建立使用env环境变量来控制,同时增加本地.env.local配置,进行gitignore忽略,这样大家可以愉快的修改配置文件调试。

关于Nextjs包引入优化

近期Nextjs v13.5+版本更新了,更新了一部分Nextjs配置,在experimental配置项中,增加了optimizePackageImports配置,可以进行包优化!同时更新优化了性能,可以查看此文阅读最新信息Next.js 13.5

关于Nextjs的全局变量

可以使用React的createContext提供,然后通过父子组件嵌套,使其在Client端水合产生作用。这里提供一份代码,用于触发路由进行loading加载进度条的demo,供参考:

1
2
3
4
5
6
7
8
// state.ts
'use client';

import { createContext } from 'react';

const StartRouterChangeContext = createContext<() => void>(() => {});

export default StartRouterChangeContext;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// link.tsx
'use client';

import Link from 'next/link';
import { useContext } from 'react';
import StartRouterChange from './state';

export default function LayoutLink({
href,
style,
children,
className
}: React.ComponentProps<'a'>) {
const startChange = useContext(StartRouterChange);
const useLink = href && href.startsWith('/');
if (useLink)
return (
<Link
href={href}
className={className}
onClick={() => {
const { pathname, search, hash } = window.location;
if (href !== pathname + search + hash) startChange();
}}
style={style}
>
{children}
</Link>
);
return (
<a href={href} style={style} className={className}>
{children}
</a>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// index.tsx
'use client';

import { useCallback, useEffect, useState } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import NProgress from 'nprogress';
import StartRouterChangeContext from './state';

function RouterEventWrapper({
onStart = () => null,
onComplete = () => null,
children
}: React.PropsWithChildren<{ onStart?: () => void; onComplete?: () => void }>) {
const [isChanging, setIsChanging] = useState(false);
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => setIsChanging(false), [pathname, searchParams]);

useEffect(() => {
if (isChanging) onStart();
else onComplete();
}, [isChanging]);

return (
<StartRouterChangeContext.Provider value={() => setIsChanging(true)}>
{children}
</StartRouterChangeContext.Provider>
);
}

export default function RootLayout({ children }: React.PropsWithChildren) {
const onStart = useCallback(() => NProgress.start(), []);
const onComplete = useCallback(() => NProgress.done(), []);
return (
<RouterEventWrapper onStart={onStart} onComplete={onComplete}>
{children}
</RouterEventWrapper>
);
}

关于日志收集

目前有vercel的日志收集和第三方的Sentry。

Sentry直接使用Sentry提供的插件即可。

关于Nextjs操作数据库

我的starter提供了Prisma的调用案例,当然注释了,需要进行以下操作:

  • 安装依赖
1
2
pnpm add @prisma/client
pnpm add -D prisma
1
2
3
4
"script": {
"prisma:push": "npx prisma db push",
"prisma:generate": "npx prisma generate",
}
  • next.config.js
1
2
3
experimental: {
serverComponentsExternalPackages: ['@prisma/client'] // prisma support
},
  • prisma文件目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// prisma/index.ts
import { PrismaClient } from '@prisma/client';

// PrismaClient is attached to the `global` object in development to prevent
// exhausting your database connection limit.
//
// Learn more:
// https://pris.ly/d/help/next-js-best-practices

const globalForPrisma = global as unknown as { prisma: PrismaClient };

export const prisma = globalForPrisma.prisma || new PrismaClient();

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

export default prisma;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}

model Post {
id String @id @default(cuid())
title String
content String? @db.LongText
published Boolean @default(false)
comments Comment[]
@@map(name: "posts")
}

model Comment {
id String @id @default(cuid())
content String?
post Post? @relation(fields: [postId], references: [id])
postId String?
published Boolean @default(false)
}

然后就可以愉快的使用了Prisma orm进行操作数据库啦!其他细节可以查看Prisma文档!

关于 Nextjs持续集成和部署

Nextjs部署最佳方式也许就是Vercel了。但是各大云服务平台也可以,包括AWS在内对于Nextjs进行了优化,访问速度非常不错。

如果是个人项目或者是付费团队项目可以直接使用vercel进行部署。如果是想使用自己的Action或者是自己的部署平台进行部署上传到vercel,以下提供一个action yaml仅供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# VERCEL_ORG_ID就是vercel的组织ID,VERCEL_PROJECT_ID就是vercel的这个项目的ID,VERCEL_TOKEN就是Vercel的api token
name: Production Deployment

env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

on:
push:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Install Vercel CLI
run: npm install --global vercel@canary
- uses: pnpm/action-setup@v2
with:
version: latest
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Pull Vercel Environment Information
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}

限制pnpm工具

1
2
3
4
// pacakge.json
"scripts": {
"preinstall": "npx only-allow pnpm",
}

关于Husky/lint-staged

配置在提交代码前进行代码检测是非常重要的!

1
2
# 安装husky和lint-staged
pnpm add -D husky lint-staged pretty-quick

配置检测,在根目录下的.husky下需要配置命令,可以参考demo

1
2
3
4
5
6
7
// pacakge.json
"scripts": {
"prepare": "husky install",
"lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
"lint:pretty": "pretty-quick --staged",
"lint": "pnpm lint:lint-staged && pnpm lint:pretty",
}

.husky目录下文件

  • lintstagedrc.js
1
2
3
4
5
6
7
8
module.exports = {
'*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
'{!(package)*.json}': ['prettier --write--parser json'],
'package.json': ['prettier --write'],
'*.vue': ['eslint --fix', 'prettier --write', 'stylelint --fix'],
'*.{vue,css,scss,postcss,less}': ['stylelint --fix', 'prettier --write'],
'*.md': ['prettier --write']
};
  • pre-commit
1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
. "$(dirname "$0")/common.sh"

[ -n "$CI" ] && exit 0

# Format and submit code according to lintstagedrc.js configuration
npm run lint:lint-staged

npm run lint:pretty

  • common.sh
1
2
3
4
5
6
7
8
9
#!/bin/sh
command_exists () {
command -v "$1" >/dev/null 2>&1
}

# Workaround for Windows 10, Git Bash and Pnpm
if command_exists winpty && test -t 1; then
exec < /dev/tty
fi

关于配置commitlint检测

使用了@commitlint/cli和@commitlint/config-conventional,配置文件为:commitlint.config.js,

1
2
# 添加包以来
pnpm add -D @commitlint/cli @commitlint/config-conventional
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
module.exports = {
ignores: [(commit) => commit.includes('init')],
extends: ['@commitlint/config-conventional'],
rules: {
'body-leading-blank': [2, 'always'],
'footer-leading-blank': [1, 'always'],
'header-max-length': [2, 'always', 108],
'subject-empty': [2, 'never'],
'type-empty': [2, 'never'],
'type-enum': [
2,
'always',
[
'feat',
'fix',
'perf',
'style',
'docs',
'test',
'refactor',
'build',
'ci',
'chore',
'revert',
'wip',
'workflow',
'types',
'release'
]
]
}
};

然后结合husky配置进行检测,配置文件为commit-msg:

1
2
3
4
5
6
#!/bin/sh

# shellcheck source=./_/husky.sh
. "$(dirname "$0")/_/husky.sh"

npx --no-install commitlint --edit "$1"

生成commit log文件

使用了conventional-changelog和@commitlint/cli然后使用生成log文件

1
2
3
4
// pacakge.json
"scripts": {
"log": "npx conventional-changelog --config ./node_modules/@commitlint/cli -i CHANGELOG.md -s -r 0"
}

总结

好了,这次的总结就到此结束了。Nextjs在正式项目上,几乎毫无压力,工程化的沉淀也差不多就这样啦,具体的业务表现就自由发挥啦。

今天是2023年的1024~在此祝所有程序员节日快乐~我们的狂欢🎉!献给2023-1024的礼物!

by: vadxq

2023.10.24 shanghai

参考文章:
[1] https://www.joshwcomeau.com/react/server-components
[2] https://nextjs.org/docs/app/building-your-application/rendering/server-components

☑️ ☆

【前端工程化】Vite关于Vue3/React项目工程化总结(献给2021-1024的礼物)

时间已经是2021年了,马上也就2022年了,现如今自己的关注点对于新知识的狂热程度也比当年稍减,更多的关注于该新玩意对于现有项目是否有提升的可能性。更多地关注于项目的工程化和稳定性。关于Vite的项目工程化实践也有快一年了,从Vue3刚发布的踩坑实践到现在几乎可以覆盖99.5的机型兼容.是时候在1024节,给大家献上一篇总结啦~

当然也可以查看我之前在公司内部分享的ppt:vadxq的分享ppt小栈

技术栈

Vue3 工程化项目实践demo

  • 编程语言:TypeScript 4.x
  • 构建工具:Vite 2.x
  • 前端框架:Vue 3.x
  • 路由工具:Vue Router 4.x
  • 状态管理:Vuex 4.x
  • CSS:Sass + Postcss
  • HTTP 工具:Axios
  • MOCK: mockjs + vite plugins
  • 移动端调试插件: vite-plugin-vconsole
  • Git Hook 工具:Husky + Lint-staged
  • 代码规范:EditorConfig + Prettier + ESLint
  • 提交规范:Commitlint + changelog

项目细节介绍

  • .husky: 关于Git Hook工具的一些命令配置
  • .vscode: 关于vscode的一些配置,强制开启一些检测插件
  • build: 关于持续集成,构建和部署相关的node脚本
  • mocks: mock数据
  • public: 静态文件夹
  • src: 主要代码相关
  • .cz-config.js: 关于commit规范的配置
  • .env: 环境变量配置
  • .eslintrc/.eslintignore: eslint配置
  • .gitignore: 懂得都懂
  • .prettierignore/.prettierrc: 格式配置
  • .stylelintrc.json: 样式格式化配置
  • commitlint.config.js: commitlint配置
  • index.html: index.html文件
  • package.json
  • postcss.config.js: postcss配置
  • tsconfig.json
  • vite.config.ts: vite配置

先谈谈Vue和React项目内部的实践

Vue3

跑Vue3,其实很简单,在src目录配置main.tsApp.vue入口文件即可。可以参考Vue3官方文档,本文不做过多描述。

而在Vite的配置也很简单,只需要引入@vitejs/plugin-vue即可。配置就一句Vue()。是不是很方便?当然,今天讲的更多是工程化方面,这里简单提一句,可以参考demo项目vite-vue-prod-template

React

跑React,也很简单,在src目录配置main.tsApp.tsx入口文件即可。而在Vite的配置也很简单,只需要引入@vitejs/plugin-react-refresh即可。配置就一句reactRefresh()。非常的方便。可以参考demo项目vite-react-starter

谈谈两者的工程化

关于结构约定

在前端,其实是很自由化的,模块化的,无处不在。但是对于公司项目来说,这种自由化,会导致项目在人才更替的时候,出现难以维护的情况,所以每一个项目,都需要相互约定一个特定的共同认可的项目结构。通用的几乎不需要修改的配置等提取出来,放入初始化项目里。根据业务类型区分不同的文件夹,约定特定的业务在特定的文件结构里进行编码。比如apis,编写与服务端交互的调用api接口代码。routes则为前端路由相关,styles为公共样式文件,views为页面代码,components为组件等等,这些约定每个团队可能都不一样,这里可以大家自己参考demo项目,与团队成员约定结构,这一步也是很重要的哟~

关于环境与构建相关

其实前端的部署很简单,将代码build出来即可。对于公司的项目来说,不可能只有两个环境:development和production。往往至少会有测试环境,甚至有多个测试环境和预生产环境。不同环境的打包也是不一样的,打包通过Vite的配置即可处理大部分情况,当然生产环境可能需要上传静态文件到OSS搭配CDN。这里需要根据自己公司的业务去做针对性处理。demo项目是做了在build下对项目上传到七牛CDN下的处理方式。实现细节可以参考build/build.ts

关于环境的处理

环境的处理其实只要设定mode即可

1
2
3
4
5
6
7
8
9
10
11
// 在package.json
"scripts": {
"dev": "vite --mode mock",
"dev:alpha": "vite --mode alpha",
"dev:test": "vite --mode test",
"dev:test1": "vite --mode test1",
"dev:grey": "vite --mode grey",
"build:test": "vue-tsc --noEmit && vite build --mode test",
"build:test1": "vue-tsc --noEmit && vite build --mode test1",
"build": "vue-tsc --noEmit && vite build --mode prod"
},

关于Vite base的处理

这个是配置base,也就是index.html引入的.js/.css/静态文件等的前缀处理。比如index.html打包后引入app.bc8837848788.js,如果正式环境配置的是cdn情况下,或者是将静态文件放置在不同url赏,则可以在这里对不同情况进行处理。

1
2
3
4
5
6
7
8
9
10
base:
mode === 'prod'
? `${cdnConfig.host}${projectBasePath}`
: mode === 'test'
? baseConfig.publicPath + '/'
: mode === 'test1'
? baseConfig.publicPath + '/'
: mode === 'grey'
? baseConfig.publicPath + '/'
: './',

关于部署与持续集成

这一块根据自家团队的情况自己处理啦。其实也就是几条命令,clone代码,安装node_modules,然后就是build,有上传oss/CDN的可以加一行上传命令,建议写在build下,最后就是将代码copy到服务器或者是serverless。

关于团队规范和git flow

团队规范是我很在意的一点。这一块针对性的做了很多的处理,包括常规js/ts代码规范的检测,还有css代码,以及文件格式的检测和commit规范的检测

关于Husky/lint-staged和commitlint

配置在提交代码前进行代码检测是非常重要的!

1
2
# 安装husky和lint-staged
yarn add -D husky lint-staged
1
2
# 安装commitlint
yarn add -D @commitlint/cli @commitlint/config-conventional commitizen conventional-changelog-cli cz-customizable

配置检测,在根目录下的.husky下需要配置命令,可以参考demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// pacakge.json
"scripts": {
"prepare": "husky install",
"lint": "npx lint-staged"
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.md": "prettier . -w",
"*.{ts,tsx,js,vue}": "eslint . --fix",
"*.{vue,css,scss,less,sass}": "stylelint --fix",
"*": "prettier . -w -u"
},

关于配置提交commit格式的检测

1
2
3
4
5
6
7
8
9
// pacakge.json
"config": {
"commitizen": {
"path": "./node_modules/cz-customizable"
},
"cz-customizable": {
"config": ".cz-config.js"
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// .cz-config.js
// 配置可以参考demo的配置,有注释
module.exports = {
// type 类型
types: [
{ value: 'feat', name: 'feat: 新增产品功能' },
{ value: 'fix', name: 'fix: 修复 bug' },
{ value: 'upd', name: 'upd: update,更新某功能(不是 feat, 不是 fix' },
{ value: 'docs', name: 'docs: 文档的变更' },
{ value: 'release', name: 'release: 版本,打tag' },
{
value: 'style',
name: 'style: 不改变代码功能的变动(如删除空格、格式化、去掉末尾分号等)'
},
{
value: 'refactor',
name: 'refactor: 重构代码。不包括 bug 修复、功能新增'
},
{
value: 'perf',
name: 'perf: 性能优化'
},
{ value: 'test', name: 'test: 添加、修改测试用例' },
{
value: 'build',
name: 'build: 构建流程、外部依赖变更,比如升级 npm 包、修改 webpack 配置'
},
{ value: 'ci', name: 'ci: 修改了 CI 配置、脚本' },
{
value: 'chore',
name: 'chore: 对构建过程或辅助工具和库的更改,不影响源文件、测试用例的其他操作'
},
{ value: 'revert', name: 'revert: 回滚 commit' }
],

// scope 类型,针对 React 项目
scopes: [
// 如果选择 custom ,后面会让你再输入一个自定义的 scope , 也可以不设置此项, 把后面的 allowCustomScopes 设置为 true
['custom', '以下都不是?我要自定义,或者回车跳过此项'],
['components', '组件相关'],
['views', '页面相关'],
['utils', '工具函数相关'],
['apis', '接口对接相关'],
['layout', '调整layout和页面布局相关'],
['styles', '样式相关'],
['deps', '项目依赖'],
['other', '其他修改']
].map(([value, description]) => {
return {
value,
name: `${value.padEnd(30)} (${description})`
};
}),

// allowTicketNumber: false,
// isTicketNumberRequired: false,
// ticketNumberPrefix: 'TICKET-',
// ticketNumberRegExp: '\\d{1,5}',

// 可以设置 scope 的类型跟 type 的类型匹配项,例如: 'fix'
/*
scopeOverrides: {
fix: [
{ name: 'merge' },
{ name: 'style' },
{ name: 'e2eTest' },
{ name: 'unitTest' }
]
},
*/
// 覆写提示的信息
messages: {
type: '请确保你的提交遵循了原子提交规范!\n选择你要提交的类型:',
scope: '\n选择一个 scope (可选):',
// 选择 scope: custom 时会出下面的提示
customScope: '请输入自定义的 scope:',
subject: '填写一个简短精炼的描述语句:\n',
body: '添加一个更加详细的描述,可以附上新增功能的描述或 bug 链接、截图链接 (可选)。使用 "|" 换行:\n',
breaking: '列举非兼容性重大的变更 (可选):\n',
footer: '列举出所有变更的 ISSUES CLOSED (可选)。 例如.: #31, #34:\n',
confirmCommit: '确认提交?'
},

// 是否允许自定义填写 scope ,设置为 true ,会自动添加两个 scope 类型 [{ name: 'empty', value: false },{ name: 'custom', value: 'custom' }]
// allowCustomScopes: true,
allowBreakingChanges: ['feat', 'fix'],
// skip any questions you want
// skipQuestions: [],

// subject 限制长度
subjectLimit: 100
// breaklineChar: '|', // 支持 body 和 footer
// footerPrefix : 'ISSUES CLOSED:'
// askForBreakingChangeFirst : true,
};

关于移动端调试Vconsole

yarn add -D vconsole vite-plugin-vconsole

1
2
3
4
5
6
7
8
9
10
11
12
// vite配置,可以参考项目vite-plugin-vconsole的文档填写,兼容多环境情况
viteVConsole({
entry: resolve(__dirname, './src/main.ts'),
localEnabled: true,
enabled:
command !== 'serve' &&
(mode === 'test' || mode === 'test1' || mode === 'alpha'),
config: {
maxLogNumber: 1000,
theme: 'light'
}
}),

关于兼容性的处理

最难处理的其实就是proxy。

引入@vitejs/plugin-legacy即可,vite的配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
legacy({
// 不考虑ie的兼容性和老的vivo/锤子/荣耀等国产机型,则可以使用下面
// targets: ['defaults', 'not IE 11', '> 0.25%, not dead'],
// 如果考虑上面说的兼容性问题,使用下面配置
targets: ['ie >= 11'],
additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
// 下面可以根据情况选用
renderLegacyChunks: true,
polyfills: [
'es.symbol',
'es.array.filter',
'es.promise',
'es.promise.finally',
'es/map',
'es/set',
'es.array.for-each',
'es.object.define-properties',
'es.object.define-property',
'es.object.get-own-property-descriptor',
'es.object.get-own-property-descriptors',
'es.object.keys',
'es.object.to-string',
'web.dom-collections.for-each',
'esnext.global-this',
'esnext.string.match-all'
],
modernPolyfills: ['es.promise.finally']
})

关于mock的处理

引入vite-plugin-mock即可

1
2
3
4
5
6
7
8
9
10
11
viteMockServe({
mockPath: 'mocks',
supportTs: true,
localEnabled: command === 'serve' && mode === 'mock',
prodEnabled: false
// 这样可以控制关闭mock的时候不让mock打包到最终代码内
// injectCode: `
// import { setupProdMockServer } from './mockProdServer';
// setupProdMockServer();
// `
}),

关于日志收集

目前有阿里云的arms和自己搭建的sentry。

arms可以使用这个插件,根据不同环境去添加不同的arms。vite-plugin-arms.

sentry直接使用sentry提供的插件即可。

总结

好了,这次的总结就到此结束了。Vite在正式项目上,几乎毫无压力,工程化的沉淀也差不多就这样啦,具体的业务表现就自由发挥啦。

今天是2021年的1024~在此祝所有程序员节日快乐~我们的狂欢🎉!献给2020-1024的礼物!

☑️ ☆

【前端工具】前端工具函数合集

收集一些前端工具函数,整理成集,以便查询使用。随时更新:2021-03-03

正则匹配相关

手机号正则验证

将手机号中间四位隐藏为****

1
2
3
4
const phone = 17600000000;
const data = phone.toString().replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
console.log(data);
// 176****0000

cookie 操作相关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 设置cookie
* @param {String} name cookie name
* @param {String} val cookie value
*/
export const setCookie = (name, val) => {
const date = new Date();
const value = val;

// Set it expire in 7 days
date.setTime(date.getTime() + 7 * 24 * 60 * 60 * 1000);

// Set it
document.cookie =
name + "=" + value + "; expires=" + date.toUTCString() + "; path=/";
};

/**
* 获取cookie
* @param {String} name cookie name
*/
export const getCookie = (name) => {
const value = "; " + document.cookie;
const parts = value.split("; " + name + "=");

if (parts.length === 2) {
const ppop = parts.pop();
if (ppop) {
return ppop.split(";").shift();
}
}
};

/**
* 删除cookie
* @param {String} name cookie name
*/
export const deleteCookie = (name) => {
const date = new Date();

// Set it expire in -1 days
date.setTime(date.getTime() + -1 * 24 * 60 * 60 * 1000);

// Set it
document.cookie = name + "=; expires=" + date.toUTCString() + "; path=/";
};

格式化时间

将时间戳变为个位数带 0 前缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 格式化时间,间隔符为-
* @param {Number} date 大小,单位时间戳13位
* @return {String} 格式化后的大小 2021-03-12 20:21:02
*/
export const formatDate = (date) => {
const da = new Date(date);
const year = da.getFullYear();
const month = da.getMonth() + 1;
const day = da.getDate();
const hours = da.getHours();
const mins = da.getMinutes();
const secs = da.getSeconds();
return `${year}-${month > 9 ? month : "0" + month}-${
day > 9 ? day : "0" + day
} ${hours > 9 ? hours : "0" + hours}:${mins > 9 ? mins : "0" + mins}:${
secs > 9 ? secs : "0" + secs
}`;
};

生成 UUID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 生成uuid
* @param {Number} len 生成的uuid长度
* @param {Number} radix 随机字符的长度选择
* @return {String} uuid
*/
export const UUID = (len, radix) => {
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split(
""
);
const uuid = [];
let i;
radix = radix || chars.length;

if (len) {
// Compact form
for (i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)];
} else {
// rfc4122, version 4 form
let r;

// rfc4122 requires these characters
uuid[8] = uuid[13] = uuid[18] = uuid[23] = "-";
uuid[14] = "4";

// Fill in random data. At i==19 set the high bits of clock sequence as
// per rfc4122, sec. 4.1.5
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | (Math.random() * 16);
uuid[i] = chars[i === 19 ? (r & 0x3) | 0x8 : r];
}
}
}

return uuid.join("");
};

type 类型判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
export const isString = (e) => { //是否字符串
return Object.prototype.toString.call(e).slice(8, -1) === 'String'
}

export const isNumber = (e) => { //是否数字
return Object.prototype.toString.call(e).slice(8, -1) === 'Number'
}

export const isBoolean = (e) => { //是否boolean
return Object.prototype.toString.call(e).slice(8, -1) === 'Boolean'
}

export const isFunction = (e) => { //是否函数
return Object.prototype.toString.call(e).slice(8, -1) === 'Function'
}

export const isNull = (e) => { //是否为null
return Object.prototype.toString.call(e).slice(8, -1) === 'Null'
}

export const isUndefined = (e) => { //是否undefined
return Object.prototype.toString.call(e).slice(8, -1) === 'Undefined'
}

export const isObj = (e) => { //是否对象
return Object.prototype.toString.call(e).slice(8, -1) === 'Object'
}

export const isArray = (e) => { //是否数组
return Object.prototype.toString.call(e).slice(8, -1) === 'Array'
}

export const isDate = (e) => { //是否时间
return Object.prototype.toString.call(e).slice(8, -1) === 'Date'
}

export const isRegExp = (e) => { //是否正则
return Object.prototype.toString.call(e).slice(8, -1) === 'RegExp'
}

export const isError = (e) => { //是否错误对象
return Object.prototype.toString.call(e).slice(8, -1) === 'Error'
}

export const isSymbol = (e) => { //是否Symbol函数
return Object.prototype.toString.call(e).slice(8, -1) === 'Symbol'
}

export const isPromise = (e) => { //是否Promise对象
return Object.prototype.toString.call(e).slice(8, -1) === 'Promise'
}

export const isSet = (e) => { //是否Set对象
return Object.prototype.toString.call(e).slice(8, -1) === 'Set'
}

random随机数

返回一定范围的整数随机数

1
2
// min, 最小的值,max最大的数,包含两者
export const randoms = (min, max) => Math.floor(min + Math.random() * ((max + 1) - min))

去除空格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 去除空格
* @param {String} srt: 传入字符串
* @param {Number} type: 1-所有空格 2-前后空格 3-前空格 4-后空格
* @return {String}
*/
export const trim = (str, type = 1) => {
switch (type) {
case 1:
return str.replace(/\s+/g, '')
case 2:
return str.replace(/(^\s*)|(\s*$)/g, '')
case 3:
return str.replace(/(^\s*)/g, '')
case 4:
return str.replace(/(\s*$)/g, '')
default:
return str
}
}

大小写处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* @param {String} str:
* @param {type} type: 1:首字母大写 2:首页母小写 3:大小写转换 4:全部大写 5:全部小写
* @return {String}
*/
export const changeCase = (str, type = 4) => {
switch (type) {
case 1:
return str.replace(/\b\w+\b/g, (word) => return word.substring(0, 1).toUpperCase() + word.substring(1).toLowerCase())
case 2:
return str.replace(/\b\w+\b/g, (word) => word.substring(0, 1).toLowerCase() + word.substring(1).toUpperCase())
case 3:
return str.split('').map((word) => {
if (/[a-z]/.test(word)) {
return word.toUpperCase()
} else {
return word.toLowerCase()
}
}).join('')
case 4:
return str.toUpperCase()
case 5:
return str.toLowerCase()
default:
return str
}
}
☑️ ⭐

【包管理器】一个由第三方包管理器与npm冲突导致的bug

这是一个神奇的bug,也是一个花费时间的bug哈哈哈。当遇到这种类型的bug确实会出现挺难下手的尴尬之处。

下面便是我对这个bug的折磨历程,对,我对它的折磨!

项目背景与问题分析

这是一个老项目,可能由于以前跳版本出现问题,前辈固定了版本,使用的是npm shrinkwrap来固定版本,但由于前辈使用的是cnpm,所以当是形成的依赖包名字是下划线开头的。当后人使用npm安装的时候,便出现了这样的问题:

npm ERR! Invalid package name "_@types_babel-types@7.0.4@@types": name cannot start with an underscore; name can only contain URL-friendly characters

报错原因如字面意思,包名不够友好。所以问题在于包的问题

尝试使用固定依赖包解决问题

当发现这一步的时候,便开始了npm更多可能性的探索历程。npm-shrinkwrap.json文件,我也是第一次见,现在一般用lock用的多些。下面是他们的区别。

  • npm-shrinkwrap.json与npm v2/3/4或者是后面的兼容,而package-lock.json仅可在npm v5+版本可用
  • package-lock.json不在文件的根目录将会被忽略,但须依赖shrinkwrap文件
  • npm-shrinkwrap.json其他人安装依赖必须是一样的,而package-lock.json准确记录您安装的依赖项的版本,其他人则可以安装规定的范围内兼容的依赖项的任何版本

处理方法是使用很笨的方法,将包全部提取出来固定的版本,然后将npm-shrinkwrap.jsonpackage-lock.json文件删除,手动npm i,再将新生成的package-lock.json重新生成一份npm-shrinkwrap.json

结果:解决的大部分安装包问题,但是有一个包出现错误。那就是vue-echarts。报错信息如下:Uncaught TypeError: Cannot use 'in' operator to search for 'default' in undefined

尝试使用webpack解决问题

出现了如上结果,TypeError,问题错误显示错在vue-echarts上,于是乎,google搜索了以下问题,也去github issues搜索了遍,包的issue确实出现过问题,但是并没有什么解决方案。后来查找到了此条TypeError: Cannot use ‘in’ operator to search for ‘default’ in undefined #1407,问题提在了rollup上,看了下,好像是说是升级版本可用,同是打包工具,盲猜是不是因为webpack引入问题。于是乎便重新看了下webpack配置,看看引入vue-echarts的webpack有没有黑魔法。

1
2
3
4
5
6
7
8
9
10
      {
test: /\.js$/,
loader: 'babel-loader',
- include: [resolve('src'), resolve('test')]
+ include: [
+ resolve('src'),
+ resolve('test'),
+ resolve('node_modules/vue-echarts'),
+ ]
}

配置如同文档所写,也没啥大变化,也没啥特别之处,此方法pass。

尝试使用更改vue-echarts版本

我们项目目前使用的vue-echarts版本是v2.6.0版本,由于往后版本是跳大版本3.0.0,引入的Echarts也是大版本更新到
v4+,所以更新往上只尝试了一个版本,往下尝试了好几个版本,皆无功而返,没有任何用处。所以也就不再去比对新版本带来的特性会对现有的功能造成影响。

由于周五下午了,我们公司搬新大楼,收拾东西,也就没有再细究下去,心思也浮了。

尝试比对源码

当到了这一步的时候,已经周一了,收拾了下新公司楼层座位,便开始了上周的问题,开始静心分析源码。比对了vue-echarts的部分初始化问题,自己的推测大概问题出现在了初始化配置项的问题。于是乎在配置项分析了一会,修改配置,更改项目代码的配置项,发现也没甚问题。

后来觉得是不是vue的生命周期导致echarts初始化问题。修改后发现无效果。

后来干脆把npm下载的vue-echarts源码和cnpm下载可运行的vue-echarts包源码对比,不看不知道,一看哭笑不得。

请看如下图:

other-package-manage-error-main-01.jpg.

当时的心情就是想吐血!!!

所以问题出现在了cnpm居然跳版本安装了带有npm-shrinkwrap.json的项目的依赖!

知道问题所在,解决起来就简单了,将包版本更新至v3.1.3后,确实不会报那个错了。

但是会出现其他问题。原先引用的实例是直接挂在在根上,而今挂在VueECharts上,所以这里只要赋值多加个字段就好了。

分析第三方包管理原因

我对此的猜测是因为两种工具的模块机制不太一样。各位看官可以看看cnpm与npm的模块机制不一样的问题。目前网上还没有相关的讨论,只发现了一篇文章:为什么我从 Npm 到 Yarn 再到 Npm?,后面我会继续更新这篇文章具体探讨一下这一块的问题。个人觉得目前如果项目用的npm都是v6.0+版本的话,建议不要使用第三方包管理器了。

附带个人在项目中的解决方案,使用了黑魔法兼容两者的模块机制不同导致会出现的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import * as Echarts from 'vue-echarts'
// 由于旧版使用cnpm安装锁定包版本,但是cnpm将vue-echarts版本从2.6.0跳3.1.3版本,所以会出现用npm安装报错问题,现将版本切换至npm的3.1.3,兼容npm可用。还有个深层原因是因为引入模块的机制导致cnpm与npm差异。
if (Echarts) {
window.echarts = Echarts
if (Echarts.VueECharts) {
window.echarts = Echarts.VueECharts
}
} else {
window.echarts = null
}
// Echarts && Echarts.VueECharts ? window.echarts = Echarts.VueECharts : window.echarts = null

// window.echarts = Echarts.VueECharts
console.log(window.echarts)
🔲 ☆

【前端重温系列】this的不可描述与变化原理(献给2020-1024的礼物)

时间已经是2020年了,马上也就2021年了,现如今的发展,ES6的实现几乎现代化浏览器都实现了。红宝书也出第四版了,删除了过旧的知识,引入了ES6,涵盖至ECMAScript 2019新标准。新的基础工具书的推出,自己也该好好重温一遍基础,重新梳理自己脑海的知识点,过时的删除,腾出空间给新知识。当然,内容主要还是从核心知识开始,扩展性涵盖其他知识点。

今天是本次前端重温系列的第二篇,有关于this的指向原则和call/apply/bind等原理。欢迎各位看官收看。

js的内存管理和堆栈

谈到this,就不得不说一说javascript的内存管理机制。

有许多语言会暴露内存管理的api给开发者,也有的语言会自己默默的独自完成内存的管理操作。

内存生命周期

不管什么程序语言,内存生命周期基本是一致的:

  • 分配你所需要的内存
    • 值的初始化:js在定义变量时就完成了内存分配
    • 通过函数调用分配内存
  • 使用分配到的内存(读、写)
  • 不需要时将其释放\归还

栈内存与堆内存

js有两大数据类型:基本类型和引用类型

基本类型往往在栈内存里存储,而引用类型往往在堆内存里存储。

堆和栈分别是不同的数据结构。栈是线性表的一种,而堆则是树形结构。

垃圾回收

垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)。

  • 引用计数垃圾收集

    • 这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
  • 标记-清除算法

    • 这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象。从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。

内存泄漏

内存泄漏的概念

该释放的变量(内存垃圾)没有被释放,仍然霸占着原有的内存不松手,导致内存占用不断攀高,带来性能恶化、系统崩溃等一系列问题,这种现象就叫内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) // 'originalThing'的引用
console.log("嘿嘿嘿");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log("哈哈哈");
}
};
};
setInterval(replaceThing, 1000);

这段代码有什么问题吗?

在 V8 中,一旦不同的作用域位于同一个父级作用域下,那么它们会共享这个父级作用域。unused 是一个不会被使用的闭包,但和它共享同一个父级作用域的 someMethod,则是一个 “可抵达”(也就意味着可以被使用)的闭包。unused 引用了 originalThing,这导致和它共享作用域的 someMethod 也间接地引用了 originalThing。结果就是 someMethod “被迫” 产生了对 originalThing 的持续引用,originalThing 虽然没有任何意义和作用,却永远不会被回收。不仅如此,originalThing 每次 setInterval 都会改变一次指向(指向最近一次的 theThing 赋值结果),这导致无法被回收的无用 originalThing 越堆积越多,最终导致严重的内存泄漏。

可能导致内存泄漏的写法

  • 无意义的全局变量
1
2
3
function a() {
b = 0
}
  • 未清除的setInterval和链式调用的setTimeout
1
2
setInterval(function() {
}, 1000);
1
2
3
setTimeout(function() {
setTimeout(arguments.callee, 1000);
}, 1000);
  • 清除不当的变量
1
2
3
4
5
6
7
8
9
10
11
var myDiv = document.getElementById('myDiv')

function handleMyDiv() {
// 一些与myDiv相关的逻辑
}

// 使用myDiv
handleMyDiv()

// 尝试删除,但是由于前面函数引用了,在内存上的表现还是可访问的地址,也就是没删除掉。
document.body.removeChild(document.getElementById('myDiv'));

this的指向原则

this指向:指向执行时所在的上下文,即被调用函数所在的对象

  • this的指向由函数执行时确定,而不是定义时决定的。这点和闭包恰恰相反。当调用方法没有明确对象时,则是指向window

  • 如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象

1
2
3
4
5
6
7
8
9
10
var o = {
a:10,
b:{
a:12,
fn: function(){
console.log(this.a); // 12
}
}
}
o.b.fn();
  • this永远指向的是最后调用它的对象,也就是看它执行的时候是谁调用的

  • 如果 new 关键词出现在被调用函数的前面,那么JavaScript引擎会创建一个新的对象,被调用函数中的this指向的就是这个新创建的函数。

  • 如果通过apply、call或者bind的方式触发函数,那么函数中的this指向传入函数的第一个参数

  • 如果一个函数是某个对象的方法,并且对象使用句点符号触发函数,那么this指向的就是该函数作为那个对象的属性的对象,也就是,this指向句点左边的对象。

this特殊情形

  • this必然指向window的情况

    • 立即执行函数(IIFE)
    • setTimeout 中传入的函数
    • setInterval 中传入的函数
  • 严格模式的情形

    • 严格模式下,this 将保持它被指定的那个对象的值,所以,如果没有指定对象,this 就是 undefined
  • 箭头函数

    • 箭头函数中的 this,和你如何调用它无关,由你书写它的位置决定
  • 如果返回值是一个Object,那么this指向的就是那个返回的对象,否则指向函数的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// null
function fn()
{
this.user = 'vadxq';
return null;
}
var a = new fn;
console.log(a.user); // vadxq

// return fn
function fn()
{
this.user = 'vadxq';
return function() {};
}
var a = new fn;
console.log(a.user); // undefined


// return Object
function fn()
{
this.user = 'vadxq';
return {};
}
var a = new fn;
console.log(a.user); // undefined

// return other
function fn()
{
this.user = 'vadxq';
return undefined;
}
var a = new fn;
console.log(a.user); // vadxq

改变this

改变this的方法途径

  • 书写定义时改变,比如箭头函数
  • 调用时改变,显式地调用一些方法,比如call/apply/bind

箭头函数

箭头函数是在定义的时候就决定了指向

1
2
3
4
5
6
7
8
9
10
11
12
var a = 1

var obj = {
a: 2,
// 声明位置
showA: () => {
console.log(this.a)
}
}

// 调用位置
obj.showA() // 1

构造函数:构造函数里面的 this 会绑定到我们 new 出来的这个对象上

显式调用

call/apply/bind的特点

  • call
    • 改变后直接调用
    • fn.call(ctx, arg1, arg2)
  • apply
    • 改变后直接调用
    • fn.apply(ctx, [arg1, arg2])
  • bind
    • 改变后不进行调用操作
    • fn.bind(ctx, arg1, arg2)

实现call/apply/bind方法

可以看此文章,写的很详细:

手写call、apply、bind实现及详解

后记

这一篇文章由于内容涵盖了的知识比较的偏底层和js语法的特性,在准备花费的时间较长,由于这个阶段自己正好在寻找工作,断断续续的在填坑,最后的内容是在去入职的火车上完成的哈哈哈。算是比较有意义的一个纪念!特写了个后记记录一下。

同时今天又是1024!我们的狂欢🎉!献给2020-1024的礼物!

🔲 ☆

【前端重温系列】闭包及其涉及知识点的理解

时间已经是2020年了,马上也就2021年了,现如今的发展,ES6的实现几乎现代化浏览器都实现了。红宝书也出第四版了,删除了过旧的知识,引入了ES6,涵盖至ECMAScript 2019新标准。新的基础工具书的推出,自己也该好好重温一遍基础,重新梳理自己脑海的知识点,过时的删除,腾出空间给新知识。当然,内容主要还是从核心知识开始,扩展性涵盖其他知识点。

今天开始从闭包及其涉及知识点开始说起。

闭包的定义:闭包是什么

不同资料对闭包的解释

MDN

函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。

阮一峰

其理解:闭包就是能够读取其他函数内部变量的函数。

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成”定义在一个函数内部的函数”。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

维基百科

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如C++)。

在支持头等函数的语言中,如果函数f内定义了函数g,那么如果g存在自由变量,且这些自由变量没有在编译过程中被优化掉,那么将产生闭包。

闭包和匿名函数经常被用作同义词。但严格来说,匿名函数就是字面意义上没有被赋予名称的函数,而闭包则实际上是一个函数的实例,也就是说它是存在于内存里的某个结构体。如果从实现上来看的话,匿名函数如果没有捕捉自由变量,那么它其实可以被实现为一个函数指针,或者直接内联到调用点,如果它捕捉了自由变量那么它将是一个闭包;而闭包则意味着同时包括函数指针和环境两个关键元素。在编译优化当中,没有捕捉自由变量的闭包可以被优化成普通函数,这样就无需分配闭包结构体,这种编译技巧被称为函数跃升。

自我理解总结

包含了既不是函数参数、也不是函数的局部变量,而是一个不属于当前作用域的变量,相对于当前作用域来说,是一个自由变量的函数,就叫闭包。

闭包包含了自由变量和函数环境

为什么需要闭包,闭包优缺点

闭包作用

  • 可以引用外部函数的变量或者参数
  • 使该变量或者参数常驻内存,避免被垃圾回收机制所回收

在 js 中变量的作用域属于函数作用域, 在函数执行完后,作用域就会被清理,内存也会随之被回收,但是由于闭包可访问上级作用域,即使上级函数执行完, 作用域也不会随之销毁

总结:某个函数在定义时的词法作用域之外的地方被调用,闭包可以使该函数访问定义时的词法作用域

注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

闭包涉及知识:作用域及作用域链

作用域分为词法作用域和动态作用域,Javascript的作用域遵循的就是词法作用域模型

关于词法作用域和动态作用域区别

词法作用域

  • 也称为静态作用域。这是最普遍的一种作用域模型
  • 在代码书写的时候完成划分,作用域链沿着它定义的位置往外延伸

动态作用域

  • 相对“冷门”,但确实有一些语言采纳的是动态作用域,如:Bash 脚本、Perl 等
  • 在代码运行时完成划分,作用域链沿着它的调用栈往外延伸

词法作用域及其作用域链

词法(lexical)一词指的是,词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。嵌套函数可访问声明于它们外部作用域的变量。

任何变量(不管包含的是原始值还是引用值)都存在于某个执行上下文中(也称为作用域)。这个上下文(作用域)决定了变量的生命周期,以及它们可以访问代码的哪些部分。执行上下文可以总结如下。

  • 执行上下文(作用域)分全局上下文(全局作用域)、函数上下文(局部作用域)和块级上下文(块级作用域)。

  • 代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜索变量和函数。

  • 函数或块的局部上下文不仅可以访问自己作用域内的变量,而且也可以访问任何包含上下文乃至全局上下文中的变量。

  • 全局上下文只能访问全局上下文中的变量和函数,不能直接访问局部上下文中的任何数据。

  • 变量的执行上下文用于确定什么时候释放内存。

  • 整个代码结构中只有函数可以限定作用域(待考证)

  • 作用域规则优先使用变量提升规则分析

  • 如果当前作用规则中有名字了, 就不考虑外面的名字

闭包涉及知识点:js内存管理

JavaScript是使用垃圾回收的编程语言,开发者不需要操心内存分配和回收。JavaScript的垃圾回收程序可以总结如下。

  • 离开作用域的值会被自动标记为可回收,然后在垃圾回收期间被删除。
  • 主流的垃圾回收算法是标记清理,即先给当前不使用的值加上标记,再回来回收它们的内存。
  • 引用计数是另一种垃圾回收策略,需要记录值被引用了多少次。JavaScript引擎不再使用这种算法,但某些旧版本的IE仍然会受这种算法的影响,原因是JavaScript会访问非原生JavaScript对象(如DOM元素)。
  • 引用计数在代码中存在循环引用时会出现问题。
  • 解除变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助。为促进内存回收,全局对象、全局对象的属性和循环引用都应该在不需要时解除引用。

闭包的实现

若函数作为参数被传递

1
2
3
4
5
6
7
8
9
10
11
// 函数作为参数被传递
function print(fn) {
const a = 200
fn()
}

const a = 100
function fn() {
console.log(a)
}
print(fn) // 100

函数作为返回值被返回

1
2
3
4
5
6
7
8
9
10
11
// 函数作为返回值
function create() {
const a = 100
return function () {
console.log(a)
}
}

const fn = create()
const a = 200
fn() // 100

闭包的应用

实现数据(变量和方法)私有化

函数柯里化(函数式编程

闭包相关例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
for (var i = 1; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, 1000 * i)
}
console.log(i)

// 5 5 5 5 5 5
// 创建的5个setTimeout闭包共享一个词法作用域,优先打印外层5
// 闭包只能取得包含函数中任何变量赋值最后一个值 // 5


for (var i = 0; i < 5; i++) {
((j) => {
setTimeout(() => {
console.log(j)
}, 1000 * j)
})(i)
}
// 0 1 2 3 4
// 5个setTimeout闭包有自己独立的词法环境
// 闭包读取到不同的i值

var output = function (i) {
setTimeout(function() {
console.log(i);
}, 1000);
};

for (var i = 0; i < 5; i++) {
output(i);
}

// 0 1 2 3 4
// 这里的 i 被赋值给了 output 作用域内的变量 i

// 变种
function test (){
var num = []
var i

for (i = 0; i < 10; i++) {
num[i] = function () {
console.log(i)
}
}

return num[9]
}

test()()
// 10

var test = (function() {
var num = 0
return () => {
return num++
}
}())

for (var i = 0; i < 10; i++) {
test()
}

console.log(test())
// 10


var a = 1;
function test(){
a = 2;
return function(){
console.log(a);
}
var a = 3;
}
test()();
// 2
// 变量提升,test a提升了。后来又赋值2
// 我们作用域的划分,是在书写的过程中,根据你把它写在哪个位置来决定的。像这样划分出来的作用域,遵循的就是词法作用域模型。这里我们匿名函数被定义的时候 a = 3 的赋值动作还没有发生(只有声明会被提前!),因此它拿到的 a 就是 2!


function foo(a,b){
console.log(b);
return {
foo:function(c){
return foo(c,a);
}
}
}

var func1=foo(0);
func1.foo(1);
func1.foo(2);
func1.foo(3);
var func2=foo(0).foo(1).foo(2).foo(3);
var func3=foo(0).foo(1);
func3.foo(2);
func3.foo(3);
// undefined
// 0
// 0
// 0
// undefined
// 0
// 1
// 2
// undefined
// 0
// 1
// 1
// {foo: ƒ}
🔲 ⭐

非技术,个人总结和展望吧

很久没有更新博客了,前端时间无意中发现我使用的图床不知道什么时候挂了,看来图片还是自己管理比较放心,后面打算直接扔GitHub了。

差不多一年过去了,一年左右想来也没做什么,无非就是学习、面试、工作等等。

☑️ ☆

函数节流和函数防抖

这篇文章来说一下两个相似又不同的的概念,这是一个新手几乎不会注意到,但是却很重要的问题,函数的节流和防抖。下面先来看一看它们究竟是什么。

首先要知道,函数节流和防抖的最终目的都是为了限制函数调用次数。当函数执行频率达到一定限度时候,再增加频率是没有意义的,此时我们就需要限制函数的执行频率,不能白白的浪费性能,甚至还会因此造成卡顿等等超出预期的情况。

函数节流和函数防抖利用的是两种不同的思想,它们使用的业务场景不尽相同,下面来具体看一下。

函数节流(throttle)

函数节流指的是让函数在一定时间内只执行一次,这样就极大限制了函数的执行次数。比如我们要监听滚屏或窗口大小调整事件,其实是没有必要每滚动一点点就执行函数的,我们完全可以减少监听次数,此时就需要函数节流。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 函数节流
var canRun = true;
document.getElementById("throttle").onscroll = function(){
if(!canRun){
// 判断是否已空闲,如果在执行中,则直接return
return;
}
canRun = false;
setTimeout(function(){
console.log("函数节流");
canRun = true;
}, 300);
};

函数节流的实现方式非常类似于加锁的效果,我们采用一个变量作为标记,当一个函数进入执行时,把标记置为不可用,接下来的函数就无法进入执行了,等到函数执行完毕再放开信号,下一次才可以进入。通过借助setTimeout可以实现超时执行的效果,从而限制了函数的执行频率。

函数防抖(debounce)

函数防抖和函数节流的目的不同,函数防抖一个典型的应用场景就是用户输入校验,用户在一定要等到用户输入完成校验才会有意义,而不是用户每输入一个字符就校验一次。

1
2
3
4
5
6
7
8
// 函数防抖
var timer = false;
document.getElementById("debounce").onscroll = function(){
clearTimeout(timer); // 清除未执行的代码,重置回初始化状态
timer = setTimeout(function(){
console.log("函数防抖");
}, 300);
};

函数防抖利用了setTimeout的缓存效果,每次创建一个定时器,把函数放在定时器里面执行,在这个过程中,一旦函数再次被调用,就会首先清除已有定时器,这样未执行的函数就无效了,最多只能有最后一个函数处于待执行状态,于是就实现了只执行最后一次的效果。

函数节流和函数防抖的概念并不复杂,但是在实现性能优化上有很重要的作用,常用的工具库如lodash等也都有函数节流和函数防抖的相关实现。

☑️ ⭐

vue.js原理初探

vue.js是一个非常优秀的前端开发框架,不是我说的,大家都知道。本人也使用过vue.js开发过移动端SPA应用,还是学习阶段,经验尚浅,能力有限。不过我也懂得只会使用轮子不知所以然是远远不够的,凭自己浅薄的见识,斗胆写一篇略微深入的一点文章。

首先我现在的能力,独立阅读源码还是有很大压力的,所幸vue写的很规范,通过方法名基本可以略知一二,里面的原理不懂的地方多方面查找资料,本文中不规范不正确的地方欢迎指正,学生非常愿意接受各位前辈提出宝贵的建议和指导。

写这篇文章时GitHub上vue最新版是v2.5.13,采用了flow作为类型管理工具,关于flow相关内容选择性忽略了,不考虑类型系统,只考虑实现原理,写下这篇文章。

本文大概涉及到vue几个核心的地方:vue实例化,虚拟DOM,模板编译过程,数据绑定。

下图为最新版本vue的生命周期

vue实例化

首先从创建vue实例开始,vue的构造函数在src/core/instance/index.js文件中,不过在src/core/index.js中对其进行了一系列处理,其中关于服务器环境渲染等相关内容在此不做讨论。这里有initGlobalAPI方法在src/core/global-api/index.js中,此方法初始化了一些vue提供的的全局方法,set,delete,nextTick等等,并初始化了和处理mixins,extends等相关功能的方法。现在回过来从全局来看src/core/instance/index.js,在其中还包括几个方法,它们初始化了vue原型上面提供的一些方法,而vue的构造函数中调用的就是原型上面的_init方法。

研究vue的实例化就要研究_init方法,此方法定义在src/core/instance/init.js下的initMixin中,里面是对vue实例即vm的处理。其中包括开发环境下的代理配置等一些列处理,并处理了传递给构造函数的参数等,重点在一系列方法

1
2
3
4
5
6
7
8
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

其实从名字就能看出这些方法都是做什么的:初始化生命周期,初始化事件,初始化渲染,触发执行beforeCreate生命周期方法,初始化data/props数据监听,触发执行created生命周期方法。

此时,对应到生命周期示例图,created方法执行结束,接下来判断是否传入挂载的el节点,如果传入的话此时就会通过$mount函数把组件挂载到DOM上面,整个vue构造函数就执行完成了。以上是vue对象创建的基本流程,其中有几个重要的关键点也是vue的核心所在,下面来重点探讨一下。

模板编译

上面提到了挂载的$mount函数,此函数的实现与运行环境有关,在此只看web中的实现。该方法在src/platforms/web/runtime/index.js中定义,挂载在vue的原型上。实现只有简单的两行,判断运行环境为浏览器,调用工具方法查找到el对应的DOM节点,再调用位于src/core/instance/lifecycle.js下的mountComponent方法来实现挂载,这里就涉及到了挂载之前的处理问题。对于拥有render(JSX)函数的情况,组件可以直接挂载,如果使用的是template,需要从中提取AST渲染方法(注意如果使用构建工具,最终会为我们编译成render(JSX)形式,所以无需担心性能问题),AST即抽象语法树,它是对真实DOM结构的映射,可执行,可编译,能够把每个节点部分都编译成vnode,组成一个有对应层次结构的vnode对象。有了渲染方法,下一步就是更新DOM,注意并不是直接更新,而是通过vnode,于是涉及到了一个非常重要的概念。

虚拟DOM

虚拟DOM技术是一个很流行的东西,现代前端开发框架vue和react都是基于虚拟DOM来实现的。虚拟DOM技术是为了解决一个很重要的问题:浏览器进行DOM操作会带来较大的开销。

操作DOM是不可避免的,常规的操作也不会有任何问题,但是经验不足的开发者往往很容易写出大量的多余或重复的DOM操作,成为前端性能优化中重要的问题。想提升效率,我们就要尽可能减少DOM操作,只修改需要修改的地方。要知道js本身运行速度是很快的,而js对象又可以很准确地描述出类似DOM的树形结构,基于这一前提,人们研究出一种方式,通过使用js描述出一个假的DOM结构,每次数据变化时候,在假的DOM上分析数据变化前后结构差别,找出这个最小差别并且在真实DOM上只更新这个最小的变化内容,这样就极大程度上降低了对DOM的操作带来的性能开销。

上面的假的DOM结构就是虚拟DOM,比对的算法成为diff算法,这是实现虚拟DOM技术的关键,在vue初始化时,首先用JS对象描述出DOM树的结构,用这个描述树去构建真实DOM,并实际展现到页面中,一旦有数据状态变更,需要重新构建一个新的JS的DOM树,对比两棵树差别,找出最小更新内容,并将最小差异内容更新到真实DOM上。

有了虚拟DOM,下面一个问题就是,什么时候会触发更新,接下来要介绍的,就是vue中最具特色的功能–数据响应系统及实现。

数据绑定

记得vue.js的作者尤雨溪老师在知乎上一个回答中提到过自己创作vue的过程,最初就是尝试实现一个类似angular1的东西,发现里面对于数据处理非常不优雅,于是创造性的尝试利用ES5中的Object.defineProperty来实现数据绑定,于是就有了最初的vue。vue中响应式的数据处理方式是一项很有价值的东西。

关于响应式的实现原理,vue官网上面其实有具体介绍,下面是一张官方图片:

vue会遍历此data中对象所有的属性,并使用Object.defineProperty把这些属性全部转为getter/setter,而每个组件实例都有watcher对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。这就是响应实现的基本原理,Object.defineProperty无法shim,所以vue不支持IE8及以下不支持ES5的浏览器。

一个简单的demo:

1
2
3
<input type="text" id="inputName">
<br>
<span id="showName"></span>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 传统方式处理数据
// document.getElementById('inputName').addEventListener('keyup', function (e) {
// document.getElementById('showName').innerText = e.target.value;
// });

// 利用Object.defineProperty自动响应数据
var obj = {};
Object.defineProperty(obj, 'name', {
get: function () {

},
set: function (val) {
document.getElementById('showName').innerText = val;
}
});
document.getElementById('inputName').addEventListener('keyup', function (e) {
obj.name = e.target.value;
});

这个例子并不是什么复杂的实现,但是却体现了vue最核心的东西,我们可以发现,Object.defineProperty下的get和set是可以自动相应的,基于此vue实现了一套基于数据驱动视图的自动响应系统,使得开发模型得到了极大的简化。


至此,本文就暂时结束了,水平一般能力有限,后面随着理解的加深会更深入去学习。更多文章欢迎访问个人网站

❌