<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>小剧客栈_剧中人的个人博客</title>
    <link>https://bh-lay.com</link>
    <description>剧中人的文笔很差，却也喜欢时常写点东西，不管是技术上的分享，生活上的感悟，还是天马行空的乱弹，小剧都会写在这里！</description>
    <language>zh-cn</language>
    <copyright>Copyright 2026 剧中人</copyright>
    <managingEditor>mail@bh-lay.com (剧中人)</managingEditor>
    <webMaster>mail@bh-lay.com (剧中人)</webMaster>
    <lastBuildDate>Tue, 30 Dec 2025 01:00:13 +0000</lastBuildDate>
    <atom:link href="https://bh-lay.com/rss" rel="self" type="application/rss+xml"/>
    <generator>bh-lay.com RSS Generator</generator>
    <docs>https://cyber.harvard.edu/rss/rss.html</docs>
    <item>
      <title>🔥 小剧2025的火焰🔥</title>
      <link>https://bh-lay.com/blog/2ekf60ygegt</link>
      <description><![CDATA[<p>新的一年又要来了，站在 2026 年的窗前回看 2025，如果用一个词来形容，我会选择「火焰」。</p>
<p>小剧的 2025 年不是烟花那样一闪而过的耀眼，也没有烛火飘摇般微弱。而是像柴火一般，在风雨中缓慢且持续的燃烧。</p>
<p>这一年有很多的变化，但它们最终看起来都在慢慢变好。</p>
<p>⬇️ 开局镇场图
<img src="https://static.bh-lay.com/blog/2025/2025-summary/12817532-3c38-47ac-8f03-bd4c9a3ac06f.jpg" alt="开局镇场图" /></p>
<h2 id="">一、火焰不是突然点燃的</h2>
<p>小剧用火焰来定义 2025 年，但这团火并不是从 2025 年才开始燃烧的。</p>
<p>在过往的数年里，小剧在工作、生活、爱好上，反复折腾、试探、犹豫。有过欢腾，也有过冷却。</p>
<p>这些年积累下来的火种，和培育出相对温和的环境，都是 2025 年能够稳定燃烧的前提。</p>
<h2 id="-1">二、生活的火焰落地</h2>
<h3 id="2135">2.1、35 岁了</h3>
<p>在 2025 年末，小剧正式度过了第 35 个生日。这一刻起，小剧无论从哪种方式计算，都已经步入 35 岁了。</p>
<p>也就是到了互联网中的<strong>"退休年龄"</strong>。</p>
<p>很庆幸，公司还愿意让我持续发光发热。</p>
<p>35 岁的小剧还拔了一颗智齿。这并不是一件值得记录的大事，毕竟成年人谁还没拔过一两颗智齿呢。</p>
<p>小剧一直以"完美智齿"称赞这颗智齿。因为自小剧发现它的存在以来，它就是垂直向上生长的，看起来不会影响任何牙齿。</p>
<p>直到把它拔掉才发现，牙根尖尖处的形状说明它最早的方向是很执拗的。粗壮的牙体也说明了它经历很多很多年的缓慢挤压，才变形成小剧发现时的"垂直"状态。</p>
<p>如此看来，2008 年初次牙疼 + 两颗后牙破损，全是拜这颗当时尚未"破土"的智齿所赐。</p>
<p>这颗智齿蛰伏到小剧 35 岁时，才站出来提醒我：身体的负债欠的再久，终究还是要归还的。</p>
<p>⬇️ 胖胖的智齿
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/b0739c47-e553-41cb-869c-9e2312a56914.jpg" alt="胖胖的智齿" /></p>
<h3 id="22">2.2、买了套房子</h3>
<p>2025 年，小剧在房地产市场最低迷的时候，买了套房子。</p>
<p>时隔十年重新成为了一名房奴，但心态却意外地平静。相比"拥有"，小剧更在意的是，小剧生活的这团火，有了一个可以长期安放的地方。</p>
<p>在入手之后如大家预期的一样，房价仍然在持续的下跌。</p>
<p>但就像别人说的那样，<strong>"经济有周期，人无再少年"</strong>。</p>
<p>房子的确在慢慢降，娃也在慢慢长大，父母也在慢慢变老。在自己能扛下房贷的年纪，挑个合适的时候下手，对小剧来说可能算是"相对正确"的选择了。</p>
<p>⬇️ 新房小区
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/5231a293-7d4e-4fff-a154-0210c3e6c31b.jpg" alt="新房小区" /></p>
<p>下半年一直在忙于新房装修。</p>
<p>因为是全拆全换的方式装修，再加上房龄较老，大小问题不断。具体的事宜都是媳妇在对接跟进，这些繁琐的事情也挺让媳妇操心的。</p>
<p>在这个过程中，小剧花了不少时间在一些看起来并不"刚需"的事情上：</p>
<ul>
<li>网络如何规划</li>
<li>智能家居怎么在水电节点预留</li>
<li>如何做才能让后期的智能足够稳定、可维护</li>
<li>灯光的动线怎么规划</li>
</ul>
<p>关于这部分写了两篇文章记录，另外一些零碎的细节记录在了一个小红书小号里。</p>
<p>目前装修仍在硬装阶段。2026 年入住后，小剧会在智能家居搭建、家庭服务器等方面，更多的记录这套房子。</p>
<ul>
<li><a href="https://bh-lay.com/blog/y339na3oqz">灯光动线规划</a></li>
<li><a href="https://bh-lay.com/blog/1vd7ah0fttx">智能家居网络搭建实录</a></li>
</ul>
<p>小红书：<a href="https://www.xiaohongshu.com/user/profile/64aaa7d6000000001f007d38">剧中人在装小院房</a></p>
<h3 id="23">2.3、撕掉的磨砂膜</h3>
<p>这一年，小剧还做了一件很小、却很有象征意义的事 —— 撕掉了餐厅的磨砂膜。</p>
<p>它并不是一次突然的改变，而更像是在确认：光线可以重新回到屋子里，注意力也可以回到真正重要的人身上。</p>
<p>⬇️ 五彩斑斓的窗外
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/742462ed-53f6-4d38-a0a0-1db891252e1b.jpg" alt="五彩斑斓的窗外" /></p>
<h3 id="24">2.4、消费降级，动手能力升级</h3>
<p>让人多才多艺的，从来不是兴趣和爱好，更多的是贫穷。</p>
<p>2025 年小剧动手做了很多动手实践的尝试，这些事情谈不上高级，却让我重新获得了一种踏实感。</p>
<p>正如火焰不是靠一次性添柴而燃烧起来的，而是靠持续、稳定的柴火供给。</p>
<h4 id="241">2.4.1、修小米门锁</h4>
<p>小米的这款门锁好用且便宜，但随着四年来的使用小毛病也渐渐浮现出来。</p>
<p>去年修过一次经常误报门锁被撬的问题。</p>
<p>这一次问题更严重，在经历了几次大风天气，门被重摔过数次，导致开锁时锁舌回收不到位。</p>
<p>每次开锁都得多等一会，等待锁舌缓慢归位。室内应急开锁更严重，离最终复位点始终差一厘米。</p>
<p>因为之前拆过一次，这次拆锁轻车熟路。锁体拆开后把异物清理干净，变形的部位敲击复原，问题也就得到了解决。</p>
<p>⬇️ 拆掉的小米门锁
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/97e0a027-7ef8-40ae-a91e-d9c821eb267d.jpg" alt="拆掉的小米门锁" /></p>
<h4 id="242h3cm1">2.4.2、升级 H3C M1 存储</h4>
<p>H3C M1 是小剧家庭服务的启蒙设备，大概是 2020 年入手的这款存储盒子。在小剧之前的很多帖子里，都有它的身影。 </p>
<p>近些年虽然已经不用它做主力存储了，但是备份的工作一直交给它。 随着个人数据慢慢汇集到本地磁盘，在下半年这个盒子的容量也告急了。</p>
<p>在 B 站、小红书、抖音等各个平台搜寻，都没有找到给它拆机、升级容量的方法。 甚至给 400 打电话，也被告知这款设备已经停止维护，官方也未曾提供过升级容量的方案。</p>
<p>但对于贫穷使我多才多艺的小剧来说，这种小事自然难不到我。</p>
<p>经历了三个小时的拆机方法摸索后，发现升级硬盘这条路是可行的。然后购买了一块更大容量的硬盘，再花八分钟完成了硬盘的更换。</p>
<p>下面的链接是这次升级硬盘的记录视频，如果你或者身边的朋友有这款设备，可以按照视频拆机少走弯路。</p>
<p>Bilibili：<a href="https://www.bilibili.com/video/BV1iKmSBEEW4">H3C M1 硬盘更换</a></p>
<p>⬇️ H3C M1 拆机照
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/7e22d590-a10f-46f4-b82b-6d66225e5642.jpg" alt="H3C M1 拆机照" /></p>
<h4 id="243os">2.4.3、迁移飞牛 OS</h4>
<p>在使用飞牛 OS 之前，小剧一直使用 Mac mini 搭配移动硬盘，作为主力计算 + 存储的设备。</p>
<p>这套方案一直使用的很好，也在小红书骗了很多赞。</p>
<p>但是基于 MacOS 桌面系统的方案，易用性并不够。Docker 的管理也很松散，电源、网络策略等方面都需要精细配置。毕竟 MacOS 不是为 7x24 小时运行的 Nas 而设计的。</p>
<p>随着家里的基础服务逐步稳定下来，今年小剧正式启用了飞牛 OS，作为新的家庭服务系统。</p>
<p>家庭服务这些年一直在变，而这一次，更像是一次初步"定型"。短期内不会再有大的迁移，而是围绕稳定、可维护去搭建。</p>
<p>这个过程记录在下面的文章里，感兴趣的话可以看下小剧的迁移思路。</p>
<p><a href="https://bh-lay.com/blog/2d38u0j7q63">从 Mac mini 到飞牛 OS 的家庭服务器迁移之路</a></p>
<p>⬇️ 家庭服务器全景图
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/082d14b7-398b-4b71-a870-fc9824299a63.jpg" alt="家庭服务器全景图" /></p>
<p>⬇️ 飞牛 OS 肉身
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/b9b825fc-6c31-4910-9357-f144569cd2c1.jpg" alt="飞牛 OS 肉身" /></p>
<h4 id="244">2.4.4、绘制装修概念草图</h4>
<p>房子购买之后，在敲定装修方案前，关于房子未来的样子并没有太多概念。</p>
<p>这段时间和媳妇天马行空的讨论了很多方案，小剧通过绘画的方式，把我们模糊的想法粗略的表达出来。</p>
<p>既便于我们具像化对新房的想象，也方便与设计师讨论时心理预期的构建。</p>
<p>尤其在书房的方案构思上，因为受限于房屋异形的空间，小剧设想了很多个版本都不是很满意。最终在"藏与漏"结合的思路下，借助于 3D 建模把小剧的想法勾勒了出来。</p>
<p>⬇️ 餐客厅柜体概念稿
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/ebfcc944-6bd4-4f31-9c0d-320de93c4cb5.jpg" alt="餐客厅柜体概念稿" /></p>
<p>⬇️ 书房空间设计
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/a95f737e-1f4d-4efd-9768-2139b547919b.jpg" alt="书房空间设计" /></p>
<h4 id="245vr">2.4.5、全景 VR 拍摄</h4>
<p>全景摄影一直是小剧的一大爱好，这些年也积累了很多作品。</p>
<p>这种全景作品都是借助于一张张独立的"全景照片"，基于空间逻辑，再使用链接组织起来的。</p>
<p>浏览的连贯性并不是很好。</p>
<p>相信你肯定用过贝壳 APP 的 VR 看房，它能巧妙的将全景和建模结合起来，整体浏览体验更为丝滑流畅。</p>
<p>一番研究后发现，贝壳 APP 的 VR 是基于如视 VR 拍摄的。更惊喜的是，如视 VR 的全景相机支持列表，竟然包含小剧的全景相机。</p>
<p>此刻小剧意识到自己也能拍摄这类全景 VR 了。经过探索尝试，发现小剧的这款全景相机拍摄的分辨率不高，但好在效果还不错。很适合小剧记录装修的进展。</p>
<p>于是小剧使用全景 VR，记录了新房从收房、拆除、水电完工的 VR 场景。</p>
<p>后面还会补上硬装完毕、软装后入住前的 VR。</p>
<p>如果你也需要拍摄这类 VR，时间合适的话小剧很乐意为你拍摄。</p>
<p>⬇️ 装修过程 VR
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/e3045c84-76de-4d45-b0d3-681d9deb29f3.jpg" alt="装修过程 VR" /></p>
<p>⬇️ VR 拍摄现场
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/8c298a5c-8101-46d7-84f0-67c2562c50b1.jpg" alt="VR 拍摄现场" /></p>
<h2 id="-2">三、创作与技术的持续燃烧</h2>
<h3 id="31">3.1、小剧起始页</h3>
<p>小剧起始页是一个非常个人向的 Web 站点，小剧近些年一直在断断续续的迭代。</p>
<p>今年，小剧重新整理了小剧起始页的 Widgets 路由逻辑，让越来越多的小组件更易于管理。</p>
<p>另外小剧还开发了三款个人使用频率非常高的小组件。这些功能并不追求复杂，而是直指小剧日常的使用场景。</p>
<h4 id="311">3.1.1、文本对比组件</h4>
<p>Code Diff 算是小剧开发中最常用到的功能之一，在很多场景下都能用得到。</p>
<p>比如新旧数据结构对比、代码片段对比、用户数据对比等场景。</p>
<p>基于在线的网页对文本做比对，无论是安装还是使用，相比于本地软件都会更加的轻量高效。</p>
<p>体验链接：<a href="https://e.bh-lay.com/#!=widgets:code-diff">https://e.bh-lay.com/#!=widgets:code-diff</a></p>
<h4 id="312">3.1.2、二维码小组件</h4>
<p>二维码是我们平日里最常用到的工具，常见于付款、扫码登录、收发快递、加好友等场景。</p>
<p>此外，在跨设备传输小文本的时候也非常好用。</p>
<p>但是基于网页版生成、扫描二维码的工具并不多。</p>
<p>于是小剧在起始页中，开发了这一功能。不仅能跨设备传输文本，而且还支持超大文本分片传输。</p>
<p>AI 在这个过程中给了小剧非常大的帮助，明显加快了小组件的开发速度。</p>
<p>体验链接：<a href="https://e.bh-lay.com/#!=widgets:qrcode">https://e.bh-lay.com/#!=widgets:qrcode</a></p>
<h4 id="313">3.1.3、简裁变图</h4>
<p>这是一个非常简单的小组件，就是单纯的图片裁剪。</p>
<p>你可以导入一张照片，用它裁成一寸、五寸等常见照片尺寸，以及各类考试报名用到的照片大小。</p>
<p>体验链接 <a href="https://e.bh-lay.com/#!=widgets:easy-crop-pic">https://e.bh-lay.com/#!=widgets:easy-crop-pic</a></p>
<h3 id="32">3.2、博客</h3>
<p>博客运营到 2025 年已经整整 13 年了。今年对个人博客做的改动很多，终极目标其实只有一个：</p>
<blockquote>
  <p>从「还能跑」，变成「值得长期维护」。</p>
</blockquote>
<h4 id="321">3.2.1、博客后台重构</h4>
<p>今年对博客做的最重要的一次改动，是重构了博客的后台系统。</p>
<p>这套后台系统经历过至少三次大的版本迭代，在 2023 年定型之后几乎就没再动过了。到这次重构前期，很多依赖因为这样那样的原因需要升级，而系统也因为缺少整体维护，导致开发模式已经跑不起来。</p>
<p>整个代码像是一堆燃尽后的冷灰，余温还在但已经没办法继续燃烧了。</p>
<p>今年在博客的迭代中需要对后台做些改动，但受限于前面提的背景无法实施。</p>
<p>于是终于下定决心，把整个后台做了重构。</p>
<p>不得不说 AI 对代码的理解还是很全面的。只花了一个晚上，它就把后台系统阅读理解全面了，并且使用 Vue3 + Tailwind 重新搭了一套，少量调整就可以直接上线运行了。</p>
<p>新的这套后台并不完美，但它重新可控了。</p>
<p>而可控，意味着博客的功能可以继续按照想要的方向燃烧。</p>
<h4 id="322">3.2.2、博文顶图色调修改</h4>
<p>去年小剧把博文改为了氛围感更强的"剧场模式"，这个改动让整个博文界面变得既大气又简洁。</p>
<p>今年在对这个界面看多了之后，发觉依旧有优化的空间。</p>
<p>博客整体偏向亮色，而"剧场模式"在统一明度的操作时，采用的是深色蒙层。</p>
<p>深色过渡到亮色的背景时，对比度过高，并不和谐。而且受限于不同的显示器素质，大范围的明度渐变也会出现丑陋的色彩断层。</p>
<p>所以小剧经过不断调配，最终把顶图的明度改为了亮调的氛围。</p>
<p>⬇️ 暗色亮色对比
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/bb97a801-584c-4052-b468-1664904fc99d.jpg" alt="暗色亮色对比" /></p>
<h4 id="323rss">3.2.3、增加 RSS 支持</h4>
<p>说来惭愧，博客运行 13 年以来，从未支持过 RSS。</p>
<p>最近一直被朋友吐槽、催更，于是一怒之下怒了一下。小剧决定要为博客开发 RSS 功能了。</p>
<p>因为博客的前后台是小剧自己手撸的，并不像成熟的博客系统一样可以一键开启 RSS。也不像基于开源框架的站点，有丰富的插件可以支持 RSS。</p>
<p>但好在小剧把想法交代给 AI，经过几番修改，RSS 功能也就写完了。</p>
<p>并且小剧并未告诉它缓存模块的使用方法，它竟然读懂，并且完美的用上了。</p>
<p>如果你在使用 RSS 阅读器，欢迎订阅小剧客栈。</p>
<p>RSS：<a href="https://bh-lay.com/rss">https://bh-lay.com/rss</a></p>
<h4 id="324toc">3.2.4、手机版支持 TOC 预览</h4>
<p>TOC 全称是 table of content，是一篇文章的大纲。</p>
<p>如果你在使用电脑端阅读这篇文章，右侧就是这篇文章大纲的显示区域。受限于手机端的屏幕尺寸，小剧一直是把 TOC 隐藏了。</p>
<p>但其实对于一篇长文来说，手机端单屏的文章占比略小，是更需要通过 TOC 了解上下文的。</p>
<p>因此参考了 Outline Doc 的编阅读体验，对小剧客栈手机端的文章 TOC 做了动态展示的支持。</p>
<p>⬇️ 手机端 TOC 交互
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/8cb0c860-3332-4953-bcf8-44bc42d0fad5.jpg" alt="手机端 TOC 交互" /></p>
<h4 id="325">3.2.5、增加懒加载功能</h4>
<p>这是另一件很惭愧的事，小剧一直没有对博客的图片做懒加载处理。</p>
<p>那些让你感觉到图片加载很快的错觉，都是以极端压缩图片为手段，再配合过场动画的遮羞布，让你感知不到图片的加载过程。</p>
<p>但随着近些年小剧书写文章的习惯发生变化，喜欢大量的引入图片。在博文包含多张图片时，阅读体验变得更差了。</p>
<p>因此为了优化博文的阅读体验，小剧开始书写图片懒加载逻辑。并且以此为契机，把评论区的头像、摄影作品、全景作品等模块都增加了懒加载支持。</p>
<p>这部分其实很简单，有很多库可以使用。但是这部分也很难，因为博文渲染使用的是类似 v-html 的原生 JS 逻辑，而其他固定模版的懒加载需要使用类似 v-lazy 的 Vue 指令。</p>
<p>想要用一段简单的逻辑同时兼容这两个场景并不容易。当然更容易的做法是分别引入原生 JS 和 Vue 的两个懒加载类库。</p>
<p>但是这种操作显然不是小剧所能接受的，于是小剧就封装了一个原生 JS 的懒加载类。既能给原生逻辑直接调用，又可以被封装成 Vue 的指令，完美兼容。</p>
<p>这部分代码不多，在一些简单的场景里可以直接拿过去用。</p>
<ul>
<li>懒加载类</li>
<li><a href="https://github.com/bh-lay/blog/blob/master/frontEnd/single-page/src/common/js/lazy-load-manager.js">/src/common/js/lazy-load-manager.js</a></li>
<li>v-lazy 指令</li>
<li><a href="https://github.com/bh-lay/blog/blob/master/frontEnd/single-page/src/ui-library/directives/lazy-image.js">/src/ui-library/directives/lazy-image.js</a></li>
<li>博文使用示例</li>
<li><a href="https://github.com/bh-lay/blog/blob/master/frontEnd/single-page/src/view/blog-detail/index.vue#L428">/src/view/blog-detail/index.vue</a></li>
</ul>
<h4 id="326">3.2.6、重构全景作品页面</h4>
<p>去年小剧针对摄影作品的特点，重构了摄影作品分享页面的布局。这次小剧又拿全景作品开刀了。</p>
<p>摄影作品不同的长宽比，隐含的是作品背后的主观视角，全景作品的调性则决定了封面一般都是正方形。</p>
<p>下图的下半部分，是全景作品页面的原始的布局，所有作品都规规矩矩的保持一样的尺寸排列。</p>
<p>小剧的全景作品较杂，有记录日常的拍摄，也有带着创作意图去拍摄的作品。相同尺寸罗列虽然很规整，但却没法表达小剧的推荐倾向。</p>
<p>经过一番思索，小剧决定采用大胆的跳跃尺寸布局。</p>
<p>设计了 1x1、2x2、4x4 三种尺寸展示全景作品，从最大到最小的面积跨度 16 倍。</p>
<p>小剧按照推荐的作品顺序，把封面尺寸重新进行了编排，并且为了保证布局错落有致，又把它们打散重排。</p>
<p>⬇️ 全景分享页面对比
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/3a818071-728f-4dfe-a87e-a82041f7a56d.jpg" alt="全景分享页面对比" /></p>
<h4 id="327">3.2.7、写了四篇博文</h4>
<p>作为一个"年更博主"，2025 年写了四篇博文已经算是非常"高产"了。</p>
<p>博文数量不多，但都是在很认真的记录生活和感悟。</p>
<p>这些文章更像是在确认：小剧依然愿意为了表达留出时间。</p>
<p><strong><a href="https://bh-lay.com/blog/1vd7ah0fttx" title="智能家居网络搭建实录">智能家居网络搭建实录</a></strong></p>
<p>分享了新房全屋智能网络规划经验，涵盖户型分析、Wi-Fi/蓝牙Mesh覆盖、有线布局到施工落地。如果你也想做智能家居，小剧的"一个大脑+两个中心"布线思路以及Wi-Fi仿真工具推荐，可以帮你科学规划网络布局，避免装修遗憾！</p>
<p>2025年11月11日</p>
<p><strong><a href="https://bh-lay.com/blog/y339na3oqz" title="灯光动线规划">灯光动线规划</a></strong></p>
<p>记录为新房设计灯光动线的过程，针对家里双入户门和瘦长户型的特点，以智能开关为核心，规划了如何让人在夜晚走动时"永不摸黑、避免折返"。分享如何优化开关点位、以及配合动线搭建虚拟灯路的具体方法。</p>
<p>2025年9月15日</p>
<p><strong><a href="https://bh-lay.com/blog/2d38u0j7q63" title="从 Mac mini 到飞牛 OS 的家庭服务器迁移之路">从 Mac mini 到飞牛 OS 的家庭服务器迁移之路</a></strong></p>
<p>记录小剧将家庭服务器由 Mac mini 换成了国产飞牛 OS 系统，包括硬件选择、散热优化，以及实际体验。特别是相册更新后加了地图模式，体验很棒。最后 Mac mini 光荣退休送朋友了，飞牛 OS 成了新的家庭服务器主力。</p>
<p>2025年7月7日</p>
<p><strong><a href="https://bh-lay.com/blog/2aj2vwa2qvb" title="局域网公网 DNS 融合，居家外出畅通无阻">局域网公网 DNS 融合，居家外出畅通无阻</a></strong></p>
<p>分享家庭网络局域网外网无缝访问的解决方案，通过DNS融合、反向代理和证书同步，让家人无需切换配置即可流畅使用家庭服务。对比了VPN、IPv6和服务器中转等方案，最终实现「回家自动切内网，外出直连公网」的无感体验，兼顾技术性与实用性。</p>
<p>2025年4月3日</p>
<h2 id="-3">四、火焰之外｜走过的路</h2>
<p>2025 年小剧去的地方不算多，但相对于 2024 年，已经走得更多，也更远了。</p>
<p>旅行对于小剧来说，不是对工作、生活的逃离，更像是给生活的火焰降降温，让它适当冷却不至于失控。</p>
<h3 id="41">4.1、北海的风</h3>
<p>这是 2025 年初，农历新年前夕的一次旅行。</p>
<p>为期九天的超长旅行，陪着宝宝在这座海滨城市慢慢晃悠。</p>
<p>这次旅行在我们心中有不一样的意义，是宝宝成长过程的一座分水岭。</p>
<p>⬇️ 涠洲岛鳄鱼湾
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/030bd776-d1e6-4e21-b991-50d957c44c9a.jpg" alt="涠洲岛鳄鱼湾" /></p>
<p>⬇️ 北海银滩
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/d80e32a8-cfed-4dd2-80c1-0aabf41f444a.jpg" alt="北海银滩" /></p>
<p>⬇️ 北海金滩
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/2e666412-5765-4c16-8ca1-92d89b84a2d3.jpg" alt="北海金滩" /></p>
<p>⬇️ 涠洲岛滴水丹屏
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/b7231c93-2221-4c8d-a737-35c46ff7198e.jpg" alt="涠洲岛滴水丹屏" /></p>
<p>⬇️ 福成机场
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/33c4cdab-c672-4b0c-9e77-135894c5e3d4.jpg" alt="福成机场" /></p>
<p>⬇️ 背着无人机买早餐
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/a1e0de84-a0b9-44a1-9738-eed09491dc3d.jpg" alt="背着无人机买早餐" /></p>
<h3 id="42">4.2、日照的浪</h3>
<p>北海虽然地处赤道线附近，但当时天气太冷无法下水。五月份天还未完全热起来的时候，我们又去了趟同样有海的日照。</p>
<p>面对一眼望不到头的海浪，娃竟然没有一丝的恐惧，刚到海边就扎进水里玩起了浪花。</p>
<p>⬇️ 日照海边
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/cec168bf-78e2-41a7-b22b-992ec5c1dbb8.jpg" alt="日照海边" /></p>
<p>⬇️ 东夷小镇
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/8acc41ee-752e-43ef-b845-fe68141f9cc3.jpg" alt="东夷小镇" /></p>
<p>⬇️ 日照国际博览中心
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/f4ee0907-68c7-4e80-ad86-2f325380615e.jpg" alt="日照国际博览中心" /></p>
<h3 id="43">4.3、扬州的旧城</h3>
<p>这是一次"路过"的旅行，一次完全没有计划的行程。</p>
<p>⬇️ 扬州东关街
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/ab606085-6ef1-4ad3-8f00-d8c552a6b220.jpg" alt="扬州东关街" /></p>
<p>⬇️ 扬州宋夹古城
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/1a2077ae-2c44-4049-8f9e-309237a5ebb2.jpg" alt="扬州宋夹古城" /></p>
<p>⬇️ 火车回程
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/54856910-6640-4d3b-83c3-ae7480c6ff84.jpg" alt="火车回程" /></p>
<h3 id="44">4.4、带娃的日常</h3>
<p>除了前面三次较远的旅途，日常的出行还是围绕着娃在打转。</p>
<p>随着娃的成长，2025 年可以去的地方更多样了。随之而来的是，带娃所消耗的体力也与日俱增。</p>
<p>⬇️ 摘荷叶
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/93d10971-2fcf-40de-92a7-5e92a8a4ab9d.jpg" alt="摘荷叶" /></p>
<p>⬇️ 工地监工
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/a13c8063-6bb1-4c89-aff9-ec876a29c4de.jpg" alt="工地监工" /></p>
<p>⬇️ 喂鸡
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/ce24a1e8-70ee-4501-a404-c1885c47817b.jpg" alt="喂鸡" /></p>
<h3 id="45">4.5、埋了一只猫咪</h3>
<p>这是一段很奇妙的经历，说不清楚对这段经历究竟是什么感觉。也说不清楚这件事对我、对朋友、对这只猫咪意味着什么。</p>
<p>只记得和朋友一起去埋它的时候，蚊虫像暴雨一样飞个不停。</p>
<p>⬇️ 朋友在挖土
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/0d9e85b5-c69a-4ead-a660-46457d63dcca.jpg" alt="朋友在挖土" /></p>
<h2 id="-4">五、感悟｜如何把火烧得更久</h2>
<h3 id="51">5.1、火焰的亮光</h3>
<p>这一年，孩子的状态在慢慢变好。</p>
<p>不是某一次突然的进步，而是一种持续、未被明显感知的变化。</p>
<p>生活里的变化，也开始悄悄体现在一些具体的事情上。</p>
<p>去年为了方便带娃，媳妇买了一辆代步小车。这辆车的所有行程都和娃有关，所以小剧一直叫它"宝宝巴士"。</p>
<p>今年年底，这辆车因为一次意外提前退场。随后我们换了一辆更符合媳妇喜好、也更偏向她自己的车。</p>
<p>这件事本身并不戏剧化，甚至到目前为止"宝宝巴士"的事还未处理完毕。但它让我清楚地意识到：媳妇的生活重心，需要一点点缓慢的回到自己身上。</p>
<p>对孩子的成长来说，稳定的火焰，远比绚烂的烟火重要。</p>
<p>⬇️ 退役的“宝宝巴士”
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/7017b7e1-851a-4d03-9290-e331ebd397de.jpg" alt="退役的“宝宝巴士”" /></p>
<h3 id="52ai">5.2、AI 为火焰助燃</h3>
<p>这一年，小剧对 AI 的态度发生了非常大改变。</p>
<p>从最初的观望，到把 AI 当作玩具把玩，再到试探性的在一些细枝末节上使用 AI 辅助生活和开发。</p>
<p>真正让小剧对 AI 重新审视的，是年初接触 ChatGPT Research 时的惊叹。那是我第一次明确感受到：AI 不再只是"好玩"，而是已经具备了可用性。</p>
<p>从那之后，在下半年，AI 开始全面渗透进小剧的工作和生活。</p>
<p>前面提到的小剧客栈后台重构、二维码小组件开发、博客 RSS 的支持，都是依托于 AI 让小剧在有限的时间里实现尽可能多的想法。</p>
<p>AI 更像是不断添柴的工具，虽然真正决定火焰形态的依然是人，但用好 AI 可以让这团火焰更可控、更容易的朝着自己的想法前进。</p>
<h2 id="-5">尾声｜让火继续烧下去</h2>
<p>2025 年，小剧没有刻意追求更亮的火，而是试着把火烧得更久。</p>
<p>如果说这一年有什么真正确定下来的东西，大概就是：小剧已经开始接受这种缓慢、可控、能够长期燃烧的状态。</p>
<p>希望在未来的日子里，这团火依然能照亮生活，也温暖身边的人。</p>
<p>⬇️ 卡西法工作室｜家庭主页
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/b385004d-4dfd-4f41-9280-0a6e1eb1dffa.jpg" alt="卡西法工作室｜家庭主页" /></p>
<hr />
<hr />
<p>顺带一提，这次装修遇到了一家很年轻的装饰公司，沟通顺畅，适合有自己想法的业主。设计师同时负责施工落地，细节能拍板。如果你在合肥准备装修，这家团队可以考虑一下。</p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Tue, 30 Dec 2025 01:00:13 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/2ekf60ygegt</guid>
      <category>年终总结</category>
      <category>生活</category>
      <category>2025</category>
      <enclosure url="http://static.bh-lay.com/blog/2025/2025-summary/12817532-3c38-47ac-8f03-bd4c9a3ac06f.jpg" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>新的一年又要来了，站在 2026 年的窗前回看 2025，如果用一个词来形容，我会选择「火焰」。</p>
<p>小剧的 2025 年不是烟花那样一闪而过的耀眼，也没有烛火飘摇般微弱。而是像柴火一般，在风雨中缓慢且持续的燃烧。</p>
<p>这一年有很多的变化，但它们最终看起来都在慢慢变好。</p>
<p>⬇️ 开局镇场图
<img src="https://static.bh-lay.com/blog/2025/2025-summary/12817532-3c38-47ac-8f03-bd4c9a3ac06f.jpg" alt="开局镇场图" /></p>
<h2 id="">一、火焰不是突然点燃的</h2>
<p>小剧用火焰来定义 2025 年，但这团火并不是从 2025 年才开始燃烧的。</p>
<p>在过往的数年里，小剧在工作、生活、爱好上，反复折腾、试探、犹豫。有过欢腾，也有过冷却。</p>
<p>这些年积累下来的火种，和培育出相对温和的环境，都是 2025 年能够稳定燃烧的前提。</p>
<h2 id="-1">二、生活的火焰落地</h2>
<h3 id="2135">2.1、35 岁了</h3>
<p>在 2025 年末，小剧正式度过了第 35 个生日。这一刻起，小剧无论从哪种方式计算，都已经步入 35 岁了。</p>
<p>也就是到了互联网中的<strong>"退休年龄"</strong>。</p>
<p>很庆幸，公司还愿意让我持续发光发热。</p>
<p>35 岁的小剧还拔了一颗智齿。这并不是一件值得记录的大事，毕竟成年人谁还没拔过一两颗智齿呢。</p>
<p>小剧一直以"完美智齿"称赞这颗智齿。因为自小剧发现它的存在以来，它就是垂直向上生长的，看起来不会影响任何牙齿。</p>
<p>直到把它拔掉才发现，牙根尖尖处的形状说明它最早的方向是很执拗的。粗壮的牙体也说明了它经历很多很多年的缓慢挤压，才变形成小剧发现时的"垂直"状态。</p>
<p>如此看来，2008 年初次牙疼 + 两颗后牙破损，全是拜这颗当时尚未"破土"的智齿所赐。</p>
<p>这颗智齿蛰伏到小剧 35 岁时，才站出来提醒我：身体的负债欠的再久，终究还是要归还的。</p>
<p>⬇️ 胖胖的智齿
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/b0739c47-e553-41cb-869c-9e2312a56914.jpg" alt="胖胖的智齿" /></p>
<h3 id="22">2.2、买了套房子</h3>
<p>2025 年，小剧在房地产市场最低迷的时候，买了套房子。</p>
<p>时隔十年重新成为了一名房奴，但心态却意外地平静。相比"拥有"，小剧更在意的是，小剧生活的这团火，有了一个可以长期安放的地方。</p>
<p>在入手之后如大家预期的一样，房价仍然在持续的下跌。</p>
<p>但就像别人说的那样，<strong>"经济有周期，人无再少年"</strong>。</p>
<p>房子的确在慢慢降，娃也在慢慢长大，父母也在慢慢变老。在自己能扛下房贷的年纪，挑个合适的时候下手，对小剧来说可能算是"相对正确"的选择了。</p>
<p>⬇️ 新房小区
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/5231a293-7d4e-4fff-a154-0210c3e6c31b.jpg" alt="新房小区" /></p>
<p>下半年一直在忙于新房装修。</p>
<p>因为是全拆全换的方式装修，再加上房龄较老，大小问题不断。具体的事宜都是媳妇在对接跟进，这些繁琐的事情也挺让媳妇操心的。</p>
<p>在这个过程中，小剧花了不少时间在一些看起来并不"刚需"的事情上：</p>
<ul>
<li>网络如何规划</li>
<li>智能家居怎么在水电节点预留</li>
<li>如何做才能让后期的智能足够稳定、可维护</li>
<li>灯光的动线怎么规划</li>
</ul>
<p>关于这部分写了两篇文章记录，另外一些零碎的细节记录在了一个小红书小号里。</p>
<p>目前装修仍在硬装阶段。2026 年入住后，小剧会在智能家居搭建、家庭服务器等方面，更多的记录这套房子。</p>
<ul>
<li><a href="https://bh-lay.com/blog/y339na3oqz">灯光动线规划</a></li>
<li><a href="https://bh-lay.com/blog/1vd7ah0fttx">智能家居网络搭建实录</a></li>
</ul>
<p>小红书：<a href="https://www.xiaohongshu.com/user/profile/64aaa7d6000000001f007d38">剧中人在装小院房</a></p>
<h3 id="23">2.3、撕掉的磨砂膜</h3>
<p>这一年，小剧还做了一件很小、却很有象征意义的事 —— 撕掉了餐厅的磨砂膜。</p>
<p>它并不是一次突然的改变，而更像是在确认：光线可以重新回到屋子里，注意力也可以回到真正重要的人身上。</p>
<p>⬇️ 五彩斑斓的窗外
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/742462ed-53f6-4d38-a0a0-1db891252e1b.jpg" alt="五彩斑斓的窗外" /></p>
<h3 id="24">2.4、消费降级，动手能力升级</h3>
<p>让人多才多艺的，从来不是兴趣和爱好，更多的是贫穷。</p>
<p>2025 年小剧动手做了很多动手实践的尝试，这些事情谈不上高级，却让我重新获得了一种踏实感。</p>
<p>正如火焰不是靠一次性添柴而燃烧起来的，而是靠持续、稳定的柴火供给。</p>
<h4 id="241">2.4.1、修小米门锁</h4>
<p>小米的这款门锁好用且便宜，但随着四年来的使用小毛病也渐渐浮现出来。</p>
<p>去年修过一次经常误报门锁被撬的问题。</p>
<p>这一次问题更严重，在经历了几次大风天气，门被重摔过数次，导致开锁时锁舌回收不到位。</p>
<p>每次开锁都得多等一会，等待锁舌缓慢归位。室内应急开锁更严重，离最终复位点始终差一厘米。</p>
<p>因为之前拆过一次，这次拆锁轻车熟路。锁体拆开后把异物清理干净，变形的部位敲击复原，问题也就得到了解决。</p>
<p>⬇️ 拆掉的小米门锁
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/97e0a027-7ef8-40ae-a91e-d9c821eb267d.jpg" alt="拆掉的小米门锁" /></p>
<h4 id="242h3cm1">2.4.2、升级 H3C M1 存储</h4>
<p>H3C M1 是小剧家庭服务的启蒙设备，大概是 2020 年入手的这款存储盒子。在小剧之前的很多帖子里，都有它的身影。 </p>
<p>近些年虽然已经不用它做主力存储了，但是备份的工作一直交给它。 随着个人数据慢慢汇集到本地磁盘，在下半年这个盒子的容量也告急了。</p>
<p>在 B 站、小红书、抖音等各个平台搜寻，都没有找到给它拆机、升级容量的方法。 甚至给 400 打电话，也被告知这款设备已经停止维护，官方也未曾提供过升级容量的方案。</p>
<p>但对于贫穷使我多才多艺的小剧来说，这种小事自然难不到我。</p>
<p>经历了三个小时的拆机方法摸索后，发现升级硬盘这条路是可行的。然后购买了一块更大容量的硬盘，再花八分钟完成了硬盘的更换。</p>
<p>下面的链接是这次升级硬盘的记录视频，如果你或者身边的朋友有这款设备，可以按照视频拆机少走弯路。</p>
<p>Bilibili：<a href="https://www.bilibili.com/video/BV1iKmSBEEW4">H3C M1 硬盘更换</a></p>
<p>⬇️ H3C M1 拆机照
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/7e22d590-a10f-46f4-b82b-6d66225e5642.jpg" alt="H3C M1 拆机照" /></p>
<h4 id="243os">2.4.3、迁移飞牛 OS</h4>
<p>在使用飞牛 OS 之前，小剧一直使用 Mac mini 搭配移动硬盘，作为主力计算 + 存储的设备。</p>
<p>这套方案一直使用的很好，也在小红书骗了很多赞。</p>
<p>但是基于 MacOS 桌面系统的方案，易用性并不够。Docker 的管理也很松散，电源、网络策略等方面都需要精细配置。毕竟 MacOS 不是为 7x24 小时运行的 Nas 而设计的。</p>
<p>随着家里的基础服务逐步稳定下来，今年小剧正式启用了飞牛 OS，作为新的家庭服务系统。</p>
<p>家庭服务这些年一直在变，而这一次，更像是一次初步"定型"。短期内不会再有大的迁移，而是围绕稳定、可维护去搭建。</p>
<p>这个过程记录在下面的文章里，感兴趣的话可以看下小剧的迁移思路。</p>
<p><a href="https://bh-lay.com/blog/2d38u0j7q63">从 Mac mini 到飞牛 OS 的家庭服务器迁移之路</a></p>
<p>⬇️ 家庭服务器全景图
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/082d14b7-398b-4b71-a870-fc9824299a63.jpg" alt="家庭服务器全景图" /></p>
<p>⬇️ 飞牛 OS 肉身
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/b9b825fc-6c31-4910-9357-f144569cd2c1.jpg" alt="飞牛 OS 肉身" /></p>
<h4 id="244">2.4.4、绘制装修概念草图</h4>
<p>房子购买之后，在敲定装修方案前，关于房子未来的样子并没有太多概念。</p>
<p>这段时间和媳妇天马行空的讨论了很多方案，小剧通过绘画的方式，把我们模糊的想法粗略的表达出来。</p>
<p>既便于我们具像化对新房的想象，也方便与设计师讨论时心理预期的构建。</p>
<p>尤其在书房的方案构思上，因为受限于房屋异形的空间，小剧设想了很多个版本都不是很满意。最终在"藏与漏"结合的思路下，借助于 3D 建模把小剧的想法勾勒了出来。</p>
<p>⬇️ 餐客厅柜体概念稿
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/ebfcc944-6bd4-4f31-9c0d-320de93c4cb5.jpg" alt="餐客厅柜体概念稿" /></p>
<p>⬇️ 书房空间设计
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/a95f737e-1f4d-4efd-9768-2139b547919b.jpg" alt="书房空间设计" /></p>
<h4 id="245vr">2.4.5、全景 VR 拍摄</h4>
<p>全景摄影一直是小剧的一大爱好，这些年也积累了很多作品。</p>
<p>这种全景作品都是借助于一张张独立的"全景照片"，基于空间逻辑，再使用链接组织起来的。</p>
<p>浏览的连贯性并不是很好。</p>
<p>相信你肯定用过贝壳 APP 的 VR 看房，它能巧妙的将全景和建模结合起来，整体浏览体验更为丝滑流畅。</p>
<p>一番研究后发现，贝壳 APP 的 VR 是基于如视 VR 拍摄的。更惊喜的是，如视 VR 的全景相机支持列表，竟然包含小剧的全景相机。</p>
<p>此刻小剧意识到自己也能拍摄这类全景 VR 了。经过探索尝试，发现小剧的这款全景相机拍摄的分辨率不高，但好在效果还不错。很适合小剧记录装修的进展。</p>
<p>于是小剧使用全景 VR，记录了新房从收房、拆除、水电完工的 VR 场景。</p>
<p>后面还会补上硬装完毕、软装后入住前的 VR。</p>
<p>如果你也需要拍摄这类 VR，时间合适的话小剧很乐意为你拍摄。</p>
<p>⬇️ 装修过程 VR
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/e3045c84-76de-4d45-b0d3-681d9deb29f3.jpg" alt="装修过程 VR" /></p>
<p>⬇️ VR 拍摄现场
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/8c298a5c-8101-46d7-84f0-67c2562c50b1.jpg" alt="VR 拍摄现场" /></p>
<h2 id="-2">三、创作与技术的持续燃烧</h2>
<h3 id="31">3.1、小剧起始页</h3>
<p>小剧起始页是一个非常个人向的 Web 站点，小剧近些年一直在断断续续的迭代。</p>
<p>今年，小剧重新整理了小剧起始页的 Widgets 路由逻辑，让越来越多的小组件更易于管理。</p>
<p>另外小剧还开发了三款个人使用频率非常高的小组件。这些功能并不追求复杂，而是直指小剧日常的使用场景。</p>
<h4 id="311">3.1.1、文本对比组件</h4>
<p>Code Diff 算是小剧开发中最常用到的功能之一，在很多场景下都能用得到。</p>
<p>比如新旧数据结构对比、代码片段对比、用户数据对比等场景。</p>
<p>基于在线的网页对文本做比对，无论是安装还是使用，相比于本地软件都会更加的轻量高效。</p>
<p>体验链接：<a href="https://e.bh-lay.com/#!=widgets:code-diff">https://e.bh-lay.com/#!=widgets:code-diff</a></p>
<h4 id="312">3.1.2、二维码小组件</h4>
<p>二维码是我们平日里最常用到的工具，常见于付款、扫码登录、收发快递、加好友等场景。</p>
<p>此外，在跨设备传输小文本的时候也非常好用。</p>
<p>但是基于网页版生成、扫描二维码的工具并不多。</p>
<p>于是小剧在起始页中，开发了这一功能。不仅能跨设备传输文本，而且还支持超大文本分片传输。</p>
<p>AI 在这个过程中给了小剧非常大的帮助，明显加快了小组件的开发速度。</p>
<p>体验链接：<a href="https://e.bh-lay.com/#!=widgets:qrcode">https://e.bh-lay.com/#!=widgets:qrcode</a></p>
<h4 id="313">3.1.3、简裁变图</h4>
<p>这是一个非常简单的小组件，就是单纯的图片裁剪。</p>
<p>你可以导入一张照片，用它裁成一寸、五寸等常见照片尺寸，以及各类考试报名用到的照片大小。</p>
<p>体验链接 <a href="https://e.bh-lay.com/#!=widgets:easy-crop-pic">https://e.bh-lay.com/#!=widgets:easy-crop-pic</a></p>
<h3 id="32">3.2、博客</h3>
<p>博客运营到 2025 年已经整整 13 年了。今年对个人博客做的改动很多，终极目标其实只有一个：</p>
<blockquote>
  <p>从「还能跑」，变成「值得长期维护」。</p>
</blockquote>
<h4 id="321">3.2.1、博客后台重构</h4>
<p>今年对博客做的最重要的一次改动，是重构了博客的后台系统。</p>
<p>这套后台系统经历过至少三次大的版本迭代，在 2023 年定型之后几乎就没再动过了。到这次重构前期，很多依赖因为这样那样的原因需要升级，而系统也因为缺少整体维护，导致开发模式已经跑不起来。</p>
<p>整个代码像是一堆燃尽后的冷灰，余温还在但已经没办法继续燃烧了。</p>
<p>今年在博客的迭代中需要对后台做些改动，但受限于前面提的背景无法实施。</p>
<p>于是终于下定决心，把整个后台做了重构。</p>
<p>不得不说 AI 对代码的理解还是很全面的。只花了一个晚上，它就把后台系统阅读理解全面了，并且使用 Vue3 + Tailwind 重新搭了一套，少量调整就可以直接上线运行了。</p>
<p>新的这套后台并不完美，但它重新可控了。</p>
<p>而可控，意味着博客的功能可以继续按照想要的方向燃烧。</p>
<h4 id="322">3.2.2、博文顶图色调修改</h4>
<p>去年小剧把博文改为了氛围感更强的"剧场模式"，这个改动让整个博文界面变得既大气又简洁。</p>
<p>今年在对这个界面看多了之后，发觉依旧有优化的空间。</p>
<p>博客整体偏向亮色，而"剧场模式"在统一明度的操作时，采用的是深色蒙层。</p>
<p>深色过渡到亮色的背景时，对比度过高，并不和谐。而且受限于不同的显示器素质，大范围的明度渐变也会出现丑陋的色彩断层。</p>
<p>所以小剧经过不断调配，最终把顶图的明度改为了亮调的氛围。</p>
<p>⬇️ 暗色亮色对比
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/bb97a801-584c-4052-b468-1664904fc99d.jpg" alt="暗色亮色对比" /></p>
<h4 id="323rss">3.2.3、增加 RSS 支持</h4>
<p>说来惭愧，博客运行 13 年以来，从未支持过 RSS。</p>
<p>最近一直被朋友吐槽、催更，于是一怒之下怒了一下。小剧决定要为博客开发 RSS 功能了。</p>
<p>因为博客的前后台是小剧自己手撸的，并不像成熟的博客系统一样可以一键开启 RSS。也不像基于开源框架的站点，有丰富的插件可以支持 RSS。</p>
<p>但好在小剧把想法交代给 AI，经过几番修改，RSS 功能也就写完了。</p>
<p>并且小剧并未告诉它缓存模块的使用方法，它竟然读懂，并且完美的用上了。</p>
<p>如果你在使用 RSS 阅读器，欢迎订阅小剧客栈。</p>
<p>RSS：<a href="https://bh-lay.com/rss">https://bh-lay.com/rss</a></p>
<h4 id="324toc">3.2.4、手机版支持 TOC 预览</h4>
<p>TOC 全称是 table of content，是一篇文章的大纲。</p>
<p>如果你在使用电脑端阅读这篇文章，右侧就是这篇文章大纲的显示区域。受限于手机端的屏幕尺寸，小剧一直是把 TOC 隐藏了。</p>
<p>但其实对于一篇长文来说，手机端单屏的文章占比略小，是更需要通过 TOC 了解上下文的。</p>
<p>因此参考了 Outline Doc 的编阅读体验，对小剧客栈手机端的文章 TOC 做了动态展示的支持。</p>
<p>⬇️ 手机端 TOC 交互
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/8cb0c860-3332-4953-bcf8-44bc42d0fad5.jpg" alt="手机端 TOC 交互" /></p>
<h4 id="325">3.2.5、增加懒加载功能</h4>
<p>这是另一件很惭愧的事，小剧一直没有对博客的图片做懒加载处理。</p>
<p>那些让你感觉到图片加载很快的错觉，都是以极端压缩图片为手段，再配合过场动画的遮羞布，让你感知不到图片的加载过程。</p>
<p>但随着近些年小剧书写文章的习惯发生变化，喜欢大量的引入图片。在博文包含多张图片时，阅读体验变得更差了。</p>
<p>因此为了优化博文的阅读体验，小剧开始书写图片懒加载逻辑。并且以此为契机，把评论区的头像、摄影作品、全景作品等模块都增加了懒加载支持。</p>
<p>这部分其实很简单，有很多库可以使用。但是这部分也很难，因为博文渲染使用的是类似 v-html 的原生 JS 逻辑，而其他固定模版的懒加载需要使用类似 v-lazy 的 Vue 指令。</p>
<p>想要用一段简单的逻辑同时兼容这两个场景并不容易。当然更容易的做法是分别引入原生 JS 和 Vue 的两个懒加载类库。</p>
<p>但是这种操作显然不是小剧所能接受的，于是小剧就封装了一个原生 JS 的懒加载类。既能给原生逻辑直接调用，又可以被封装成 Vue 的指令，完美兼容。</p>
<p>这部分代码不多，在一些简单的场景里可以直接拿过去用。</p>
<ul>
<li>懒加载类</li>
<li><a href="https://github.com/bh-lay/blog/blob/master/frontEnd/single-page/src/common/js/lazy-load-manager.js">/src/common/js/lazy-load-manager.js</a></li>
<li>v-lazy 指令</li>
<li><a href="https://github.com/bh-lay/blog/blob/master/frontEnd/single-page/src/ui-library/directives/lazy-image.js">/src/ui-library/directives/lazy-image.js</a></li>
<li>博文使用示例</li>
<li><a href="https://github.com/bh-lay/blog/blob/master/frontEnd/single-page/src/view/blog-detail/index.vue#L428">/src/view/blog-detail/index.vue</a></li>
</ul>
<h4 id="326">3.2.6、重构全景作品页面</h4>
<p>去年小剧针对摄影作品的特点，重构了摄影作品分享页面的布局。这次小剧又拿全景作品开刀了。</p>
<p>摄影作品不同的长宽比，隐含的是作品背后的主观视角，全景作品的调性则决定了封面一般都是正方形。</p>
<p>下图的下半部分，是全景作品页面的原始的布局，所有作品都规规矩矩的保持一样的尺寸排列。</p>
<p>小剧的全景作品较杂，有记录日常的拍摄，也有带着创作意图去拍摄的作品。相同尺寸罗列虽然很规整，但却没法表达小剧的推荐倾向。</p>
<p>经过一番思索，小剧决定采用大胆的跳跃尺寸布局。</p>
<p>设计了 1x1、2x2、4x4 三种尺寸展示全景作品，从最大到最小的面积跨度 16 倍。</p>
<p>小剧按照推荐的作品顺序，把封面尺寸重新进行了编排，并且为了保证布局错落有致，又把它们打散重排。</p>
<p>⬇️ 全景分享页面对比
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/3a818071-728f-4dfe-a87e-a82041f7a56d.jpg" alt="全景分享页面对比" /></p>
<h4 id="327">3.2.7、写了四篇博文</h4>
<p>作为一个"年更博主"，2025 年写了四篇博文已经算是非常"高产"了。</p>
<p>博文数量不多，但都是在很认真的记录生活和感悟。</p>
<p>这些文章更像是在确认：小剧依然愿意为了表达留出时间。</p>
<p><strong><a href="https://bh-lay.com/blog/1vd7ah0fttx" title="智能家居网络搭建实录">智能家居网络搭建实录</a></strong></p>
<p>分享了新房全屋智能网络规划经验，涵盖户型分析、Wi-Fi/蓝牙Mesh覆盖、有线布局到施工落地。如果你也想做智能家居，小剧的"一个大脑+两个中心"布线思路以及Wi-Fi仿真工具推荐，可以帮你科学规划网络布局，避免装修遗憾！</p>
<p>2025年11月11日</p>
<p><strong><a href="https://bh-lay.com/blog/y339na3oqz" title="灯光动线规划">灯光动线规划</a></strong></p>
<p>记录为新房设计灯光动线的过程，针对家里双入户门和瘦长户型的特点，以智能开关为核心，规划了如何让人在夜晚走动时"永不摸黑、避免折返"。分享如何优化开关点位、以及配合动线搭建虚拟灯路的具体方法。</p>
<p>2025年9月15日</p>
<p><strong><a href="https://bh-lay.com/blog/2d38u0j7q63" title="从 Mac mini 到飞牛 OS 的家庭服务器迁移之路">从 Mac mini 到飞牛 OS 的家庭服务器迁移之路</a></strong></p>
<p>记录小剧将家庭服务器由 Mac mini 换成了国产飞牛 OS 系统，包括硬件选择、散热优化，以及实际体验。特别是相册更新后加了地图模式，体验很棒。最后 Mac mini 光荣退休送朋友了，飞牛 OS 成了新的家庭服务器主力。</p>
<p>2025年7月7日</p>
<p><strong><a href="https://bh-lay.com/blog/2aj2vwa2qvb" title="局域网公网 DNS 融合，居家外出畅通无阻">局域网公网 DNS 融合，居家外出畅通无阻</a></strong></p>
<p>分享家庭网络局域网外网无缝访问的解决方案，通过DNS融合、反向代理和证书同步，让家人无需切换配置即可流畅使用家庭服务。对比了VPN、IPv6和服务器中转等方案，最终实现「回家自动切内网，外出直连公网」的无感体验，兼顾技术性与实用性。</p>
<p>2025年4月3日</p>
<h2 id="-3">四、火焰之外｜走过的路</h2>
<p>2025 年小剧去的地方不算多，但相对于 2024 年，已经走得更多，也更远了。</p>
<p>旅行对于小剧来说，不是对工作、生活的逃离，更像是给生活的火焰降降温，让它适当冷却不至于失控。</p>
<h3 id="41">4.1、北海的风</h3>
<p>这是 2025 年初，农历新年前夕的一次旅行。</p>
<p>为期九天的超长旅行，陪着宝宝在这座海滨城市慢慢晃悠。</p>
<p>这次旅行在我们心中有不一样的意义，是宝宝成长过程的一座分水岭。</p>
<p>⬇️ 涠洲岛鳄鱼湾
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/030bd776-d1e6-4e21-b991-50d957c44c9a.jpg" alt="涠洲岛鳄鱼湾" /></p>
<p>⬇️ 北海银滩
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/d80e32a8-cfed-4dd2-80c1-0aabf41f444a.jpg" alt="北海银滩" /></p>
<p>⬇️ 北海金滩
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/2e666412-5765-4c16-8ca1-92d89b84a2d3.jpg" alt="北海金滩" /></p>
<p>⬇️ 涠洲岛滴水丹屏
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/b7231c93-2221-4c8d-a737-35c46ff7198e.jpg" alt="涠洲岛滴水丹屏" /></p>
<p>⬇️ 福成机场
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/33c4cdab-c672-4b0c-9e77-135894c5e3d4.jpg" alt="福成机场" /></p>
<p>⬇️ 背着无人机买早餐
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/a1e0de84-a0b9-44a1-9738-eed09491dc3d.jpg" alt="背着无人机买早餐" /></p>
<h3 id="42">4.2、日照的浪</h3>
<p>北海虽然地处赤道线附近，但当时天气太冷无法下水。五月份天还未完全热起来的时候，我们又去了趟同样有海的日照。</p>
<p>面对一眼望不到头的海浪，娃竟然没有一丝的恐惧，刚到海边就扎进水里玩起了浪花。</p>
<p>⬇️ 日照海边
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/cec168bf-78e2-41a7-b22b-992ec5c1dbb8.jpg" alt="日照海边" /></p>
<p>⬇️ 东夷小镇
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/8acc41ee-752e-43ef-b845-fe68141f9cc3.jpg" alt="东夷小镇" /></p>
<p>⬇️ 日照国际博览中心
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/f4ee0907-68c7-4e80-ad86-2f325380615e.jpg" alt="日照国际博览中心" /></p>
<h3 id="43">4.3、扬州的旧城</h3>
<p>这是一次"路过"的旅行，一次完全没有计划的行程。</p>
<p>⬇️ 扬州东关街
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/ab606085-6ef1-4ad3-8f00-d8c552a6b220.jpg" alt="扬州东关街" /></p>
<p>⬇️ 扬州宋夹古城
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/1a2077ae-2c44-4049-8f9e-309237a5ebb2.jpg" alt="扬州宋夹古城" /></p>
<p>⬇️ 火车回程
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/54856910-6640-4d3b-83c3-ae7480c6ff84.jpg" alt="火车回程" /></p>
<h3 id="44">4.4、带娃的日常</h3>
<p>除了前面三次较远的旅途，日常的出行还是围绕着娃在打转。</p>
<p>随着娃的成长，2025 年可以去的地方更多样了。随之而来的是，带娃所消耗的体力也与日俱增。</p>
<p>⬇️ 摘荷叶
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/93d10971-2fcf-40de-92a7-5e92a8a4ab9d.jpg" alt="摘荷叶" /></p>
<p>⬇️ 工地监工
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/a13c8063-6bb1-4c89-aff9-ec876a29c4de.jpg" alt="工地监工" /></p>
<p>⬇️ 喂鸡
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/ce24a1e8-70ee-4501-a404-c1885c47817b.jpg" alt="喂鸡" /></p>
<h3 id="45">4.5、埋了一只猫咪</h3>
<p>这是一段很奇妙的经历，说不清楚对这段经历究竟是什么感觉。也说不清楚这件事对我、对朋友、对这只猫咪意味着什么。</p>
<p>只记得和朋友一起去埋它的时候，蚊虫像暴雨一样飞个不停。</p>
<p>⬇️ 朋友在挖土
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/0d9e85b5-c69a-4ead-a660-46457d63dcca.jpg" alt="朋友在挖土" /></p>
<h2 id="-4">五、感悟｜如何把火烧得更久</h2>
<h3 id="51">5.1、火焰的亮光</h3>
<p>这一年，孩子的状态在慢慢变好。</p>
<p>不是某一次突然的进步，而是一种持续、未被明显感知的变化。</p>
<p>生活里的变化，也开始悄悄体现在一些具体的事情上。</p>
<p>去年为了方便带娃，媳妇买了一辆代步小车。这辆车的所有行程都和娃有关，所以小剧一直叫它"宝宝巴士"。</p>
<p>今年年底，这辆车因为一次意外提前退场。随后我们换了一辆更符合媳妇喜好、也更偏向她自己的车。</p>
<p>这件事本身并不戏剧化，甚至到目前为止"宝宝巴士"的事还未处理完毕。但它让我清楚地意识到：媳妇的生活重心，需要一点点缓慢的回到自己身上。</p>
<p>对孩子的成长来说，稳定的火焰，远比绚烂的烟火重要。</p>
<p>⬇️ 退役的“宝宝巴士”
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/7017b7e1-851a-4d03-9290-e331ebd397de.jpg" alt="退役的“宝宝巴士”" /></p>
<h3 id="52ai">5.2、AI 为火焰助燃</h3>
<p>这一年，小剧对 AI 的态度发生了非常大改变。</p>
<p>从最初的观望，到把 AI 当作玩具把玩，再到试探性的在一些细枝末节上使用 AI 辅助生活和开发。</p>
<p>真正让小剧对 AI 重新审视的，是年初接触 ChatGPT Research 时的惊叹。那是我第一次明确感受到：AI 不再只是"好玩"，而是已经具备了可用性。</p>
<p>从那之后，在下半年，AI 开始全面渗透进小剧的工作和生活。</p>
<p>前面提到的小剧客栈后台重构、二维码小组件开发、博客 RSS 的支持，都是依托于 AI 让小剧在有限的时间里实现尽可能多的想法。</p>
<p>AI 更像是不断添柴的工具，虽然真正决定火焰形态的依然是人，但用好 AI 可以让这团火焰更可控、更容易的朝着自己的想法前进。</p>
<h2 id="-5">尾声｜让火继续烧下去</h2>
<p>2025 年，小剧没有刻意追求更亮的火，而是试着把火烧得更久。</p>
<p>如果说这一年有什么真正确定下来的东西，大概就是：小剧已经开始接受这种缓慢、可控、能够长期燃烧的状态。</p>
<p>希望在未来的日子里，这团火依然能照亮生活，也温暖身边的人。</p>
<p>⬇️ 卡西法工作室｜家庭主页
 <img src="https://static.bh-lay.com/blog/2025/2025-summary/b385004d-4dfd-4f41-9280-0a6e1eb1dffa.jpg" alt="卡西法工作室｜家庭主页" /></p>
<hr />
<hr />
<p>顺带一提，这次装修遇到了一家很年轻的装饰公司，沟通顺畅，适合有自己想法的业主。设计师同时负责施工落地，细节能拍板。如果你在合肥准备装修，这家团队可以考虑一下。</p>]]></content:encoded>
    </item>
    <item>
      <title>智能家居网络搭建实录</title>
      <link>https://bh-lay.com/blog/1vd7ah0fttx</link>
      <description><![CDATA[<p>上篇文章中，小剧分享了灯光动线规划，并提到将在新房中实现智能灯光系统。</p>
<p>在全屋智能系统中，最关键的部分仍然是网络的整体搭建。</p>
<p>不仅仅因为 Wi-Fi 网络是一些智能家居设备的必选连接方式，更是家人、访客在家里漫游时网络体验的基石。</p>
<p>另外，对速度和稳定性要求高的设备都需要有线网络支持，如米家中枢网关、电视机、电脑、监控、NAS 等。</p>
<p>再回到智能家居的场景，米家基于蓝牙 Mesh2.0 的"蓝牙局域网"，可靠性也非常重要。</p>
<p>因此这一篇笔记会围绕着以下三点展开：</p>
<ul>
<li>Wi-Fi 漫游思路</li>
<li>蓝牙 Mesh2.0 网络搭建 </li>
<li>有线局域网搭建</li>
</ul>
<p>全文会按照户型分析、背景知识补课、网络设计、网络实施等顺序展开。文末会有抽象后的网络远景拓扑图。</p>
<h2 id="">一、户型特点分析</h2>
<p>所有的网络规划，离不开对所处环境的剖析。具体到家庭网络规划，就是针对户型特点选择最适合的方案。</p>
<p><strong>图一、户型图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/smart-home-network-build/494d34b2-14c9-4b67-9716-1ac3514e1cb7.jpg" alt="图一、户型图" /></p>
<p>小剧的这套约 140 平的户型，整体不算特殊，但仍有以下几个特点。</p>
<ul>
<li>户型偏瘦长（加上南北院约25m）</li>
<li>客餐厅空间开阔，但卧室区域墙体林立</li>
<li>南北双院，需要 Wi-Fi 覆盖</li>
<li>双入户门，需要布设监控</li>
</ul>
<p>基于这些特点，一个 Wi-Fi 路由器打天下的方案基本上就被否了，原因有两点。</p>
<ul>
<li>整套房子纵深过长，再加上墙体、混凝土柱子等阻隔，信号衰减严重，按照现行的路由器标准，难以覆盖这么大的尺度。</li>
<li>Wi-Fi 通信分为上下行方向。下行信号由路由器发出，覆盖范围较大；上行信号由手机等终端发出，功率较弱。</li>
</ul>
<p>因此一台路由器覆盖一个较大的面积，很容易出现手机能连接到网络，但使用体验极差的尴尬处境。</p>
<p>这就像路由器和手机在相互喊话，手机能听见路由器的呼声，路由器却很难听清手机在说什么。</p>
<p>这种情况在视频通话时最容易暴露问题。</p>
<p>对方画面很流畅，但自己在对面已经卡成了 PPT。</p>
<h2 id="-1">二、背景知识小课堂</h2>
<h3 id="21">2.1、有线网络</h3>
<p>家庭布线中，超六类网线本身和线长，在万兆以下几乎不构成瓶颈，因此前期优先要考虑的依旧是 Wi-Fi 网络。</p>
<p>另外小剧家采用的是全拆全换的方式去布线，有线网络的布局可以更加自由。因此，有线网络的方案在规划阶段的优先级可以适当靠后。</p>
<p>小剧计划将有线布线放在最后阶段再规划。</p>
<h3 id="2224g5gwifi">2.2、2.4G 和 5G Wi-Fi 频段</h3>
<p>常见的家庭 Wi-Fi 通常分为 2.4G 和 5G 两个频段。</p>
<p>2.4G 是兼容性最好的频段，穿墙能力也非常好，但速度上限不高。</p>
<p>5G 频段更适合高速传输，但信号覆盖面积不大。</p>
<p>因此常规家庭使用 2.4G、5G 双频合一，可以做到一台路由既能支持近距离高速传输、又能照顾到相对远距离干扰较多的场景。</p>
<p>回到前面提到的，小剧准备做全屋智能这个背景。因为一些智能家居设备只支持 2.4G 频段，因此双频合一的模式在小剧家并不适用。</p>
<p>手机还是连接 5G 频段，仅支持 2.4G 的智能家居设备单独走 2.4G 网络。</p>
<h3 id="23">2.3、蓝牙信号覆盖</h3>
<p>蓝牙信号覆盖，主要指的是米家蓝牙 Mesh2.0 的信号覆盖。</p>
<p>在米家的全屋智能背景下，一台米家中枢网关必不可少。</p>
<p>米家中枢网关的信号覆盖可以参照 2.4G Wi-Fi。这是因为蓝牙的工作频段与 2.4G Wi-Fi 几乎一致，覆盖能力也相近。</p>
<p>对了，米家蓝牙 Mesh2.0 的另一个魅力，是它的 Mesh 指的是支持"多跳中继通信"。</p>
<p>具体而言，在一个 20m 的长度范围内，一侧放置米家中枢网关，每隔 3m 放一台支持米家 Mesh2.0 的设备，如墙面开关。另一侧端点则放置温湿度传感器。</p>
<p>即使米家中枢网关无法覆盖到另一端，利用 Mesh2.0 多跳通信的特性，也能顺利的为端点的温湿度传感器建立起连接。</p>
<p><strong>图二、米家 Mesh2.0 示意图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/smart-home-network-build/a020bc90-9e0e-451b-8922-50d3f5995aee.png" alt="图二、米家 Mesh2.0 示意图" /></p>
<h2 id="-2">三、如何规划网络</h2>
<h3 id="31wifi">3.1、Wi-Fi 仿真工具推荐</h3>
<p>Wi-Fi 网络规划在一定程度上可依经验完成，但若能借助可视化工具进行辅助，结果会更直观、可靠。小剧在调研后选出了三个实用的工具。</p>
<ul>
<li>移动爱家 APP</li>
<li>锐捷睿易 APP</li>
<li>UniFi Design Center（文末会附上链接）</li>
</ul>
<p>前两个都是 APP，操作比较简单，但受限于手机屏幕，并不适合精细的规划操作。</p>
<p>经实际测试，小剧更推荐 UniFi Design Center。</p>
<p>UniFi Design Center 虽然是商业品牌工具，但可以免费使用，无需购买其产品。</p>
<p>借助于在线仿真工具，可以导入户型图，配置房间、墙体、门、窗等细节。再添加 Unifi 的路由、AP 等设备进行模拟仿真。</p>
<p>还可以添加监控模拟布控。</p>
<p><strong>图三、5G Wi-Fi 仿真图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/smart-home-network-build/b99b76e2-1e70-4d72-93ae-aaceed6b3540.jpg" alt="图三、5G Wi-Fi 仿真图" /></p>
<p><strong>图四、2.4G Wi-Fi 仿真图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/smart-home-network-build/3aac3ea0-ed8b-41c1-825f-6ef80beeed48.jpg" alt="图四、2.4G Wi-Fi 仿真图" /></p>
<h3 id="32wifi">3.2、Wi-Fi、中枢点位选择</h3>
<p>结合小剧的户型来看，上图中 A、B 两个路由器点位可以较好地覆盖全屋网络。</p>
<p>如果后期覆盖有盲区，C、D 两个监控位也可以视情况增设路由，来补充信号覆盖。只是目前看来没有必要。</p>
<blockquote>
  <p><strong>提示：</strong> </p>
  <p>Unifi Wi-Fi 仿真工具没必要贪图全绿覆盖，橙黄色的区域并不会对日常的使用有任何影响。</p>
  <p>过于密集的绿色区域，反倒可能因为设备在多台路由之间频繁切换影响使用体验。</p>
</blockquote>
<p>因此 A、B 两个点位可以初步定为家庭 Wi-Fi 覆盖的基础点位。</p>
<p>C、D 作为监控位的同时，也是 Wi-Fi 的备用点位，因此也需重点考虑。</p>
<p>米家中枢网关在小剧家里，一台就可以做到基础的信号覆盖，不足的话需要再添一台。</p>
<p>A 点同样作为米家中枢网关的主点位，B 点作为候补点位。</p>
<p>最终 A、B、C、D 四个点位就作为 Wi-Fi、中枢的基准点位确定了下来。</p>
<h3 id="33">3.3、有线点位发掘</h3>
<p>相比 Wi-Fi 的布局，有线点位的规划要简单得多。</p>
<p>前面的 A、B、C、D 虽然是 Wi-Fi 点位，但需要有线做基础支撑，因此这四个是最先确定下来的有线点位。</p>
<p>弱电箱是全屋最重要的"超级点位"，位于书房角落，后期将成为整个家庭网络的大脑。</p>
<p>服役在小剧现在房子里的各类设备，最终都会搬迁到这里。</p>
<p>书房作为设备密集型的房间，又单独预留了两个有线网口。</p>
<p>剩下的就简单了，每个卧室需要预留一个网口，尽管短期内用不到。</p>
<p>B 点处在电视柜中，本身需要有线。IPTV、电视机都需要有线连接。后期可能会增设的米家（副）中枢网关、其他盒子类的终端，为了稳定起见也都需要接入有线网络。</p>
<p>自此全屋的有线点位规划完毕。</p>
<h3 id="34">3.4、网络走线规划</h3>
<p>有线网络点位的梳理，只是小剧对家庭网络的需求，如何把它们组织起来是一件更头疼的事。</p>
<p>为了让网络结构更清晰，小剧采用了<strong>「一个大脑，两个中心」</strong>的思路。</p>
<p>弱电箱是"大脑"，统筹全局；公卫吊顶和电视柜是"两个中心"，分别负责卧室区和公共区的网络覆盖。</p>
<p><strong>图五、网络走线图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/smart-home-network-build/69938da6-247c-44c5-979e-ea83976b443f.jpg" alt="图五、网络走线图" /></p>
<h4 id="341">3.4.1、一个大脑</h4>
<p>弱电箱是家庭网络的大脑。前期布线无需向师傅说明家庭服务器的复杂结构，这部分等入住后小剧会自行搭建，所以上图并未体现。</p>
<p>这里包含了一根入户光纤管、通往"两个中心"以及其他空间的网线，一共九根管线。</p>
<p>小剧也没有留一个常见的铁壳弱电箱，只是简单的把全屋网络汇聚在了这里。</p>
<p>后期小剧会把这里打造成一个 mini 版的家庭机柜，让它成为真正的家庭网络"大脑"。</p>
<h4 id="342">3.4.2、中心一：公卫吊顶</h4>
<p>公卫吊顶作为卧室区域的 Wi-Fi 覆盖节点，同时全屋智能的中枢网关也在此处，所以这里便成了两个中心的其中一个。</p>
<p>由于户型狭长，加上两个卧室的网口仅作为备用，而公卫处的 Mesh 子路由本身有 4 个网口，因此采用了简化布线的方案。</p>
<p>只需从弱电箱引出一根主线，通过 Mesh 子路由分发至北院摄像头和两个次卧。</p>
<h4 id="343">3.4.3、中心二：电视柜</h4>
<p>电视柜节点主要服务于娱乐和会客功能，作为餐客厅的 Wi-Fi 覆盖点，同时承担电视机、IPTV 及其他娱乐设备的网络连接。</p>
<p>这里的路由器还被设计为拨号上网的主路由，因此自然的成为了一个独立的网络中心。</p>
<p>这里小剧尝试使用预埋线路 + 双 50 管来保证电视柜区域的走线隐藏和灵活搭建。</p>
<p>电视机和 IPTV 设备的所有线缆 （电源、网线、HDMI）通过预埋线路 + 上 50 管隐藏在墙体和柜体里。这些固定搭配日常无需变化，可以做到可用但隐形。</p>
<p>未来若新增电视盒子、游戏机等上屏设备，可通过下方 50 管连接至电视柜的开放格。这些小的设计点，可以很大程度保证，电视区域设备增设的灵活性。</p>
<p><strong>图六、电视柜走线</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/smart-home-network-build/f3f3f455-7017-4d4a-83e0-829363d72b74.jpg" alt="图六、电视柜走线" /></p>
<h2 id="-3">四、网络走线实施</h2>
<p>到这个阶段，小剧对家庭网络的需求已经明确了（在3.3部分梳理完成），各个房间的线路走向也划分完毕。</p>
<p>接下来进入水电施工阶段。</p>
<p>由于图纸中包含了许多后期接线逻辑，电工师傅在施工时无需了解这些细节。</p>
<p>因此小剧又把图纸做了进一步的简化，删掉各个节点内的设备信息，以及内部的接线逻辑，仅保留了预埋线路的走向，方便电工师傅进行施工。</p>
<p>连同上篇文章提到的开关点位的图纸，一起交给师傅进行施工作业。</p>
<p><strong>图七、网络走线图（电工版本）</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/smart-home-network-build/99c744ce-8abf-4f1e-905c-0f018355e974.jpg" alt="图七、网络走线图（电工版本）" /></p>
<p><strong>图八、电视柜走线（电工版本）</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/smart-home-network-build/f478391e-27b4-47dd-b0e8-cc1b2cfe97c8.jpg" alt="图八、电视柜走线（电工版本）" /></p>
<h2 id="-4">五、网络拓扑远景图</h2>
<p>以上这些都是家庭网络的基石，作为小剧新房装修的一部分。</p>
<p>目前水电施工已经结束，初步验收网络的走线和点位均符合设计预期。只是水晶头、网络面板尚未安装，网线也未经过测试。</p>
<p><strong>图九、网络拓扑图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/smart-home-network-build/2d992d91-2ea4-4fb9-97c1-dce3c916fbc0.jpg" alt="图九、网络拓扑图" /></p>
<p>值得注意的是，从拓扑图中可以得出一个疑问。</p>
<blockquote>
  <p><strong>Q：为什么同样在书房，需要用到两部交换机，一台不是更合适？</strong> </p>
  <p><strong>A：</strong> 确实是的，一台 16 口交换机在这里最合适不过了，只是小剧目前家里有两台在用的同款交换机，均为八口 2.5G 电口 + 两口万兆 SFP+。</p>
  <p>为了充分利用现有设备，小剧决定继续让手头的交换机发光发热。</p>
</blockquote>
<h2 id="-5">六、写在最后</h2>
<p>从户型分析、工具选择、点位规划到最终施工落地，整个网络规划历时一个月。虽然过程繁琐，但看到水电验收时每一根网线都准确到位，还是很有成就感的。</p>
<p>这套<strong>"一个大脑 + 两个中心"</strong>的架构，本质上是根据狭长户型的特点，用最少的设备实现最优的覆盖。当然，纸面规划再完美，也需要实际使用来检验。</p>
<p>后续等所有设备就位后，小剧会再分享实际使用体验和踩坑记录，包括：</p>
<ul>
<li>Wi-Fi 漫游真实表现</li>
<li>米家蓝牙 Mesh2.0 的稳定性测试</li>
<li>全屋智能场景的联动调试</li>
</ul>
<p>如果你也在规划家庭网络，或者对文中的方案有疑问，欢迎在评论区交流讨论！你的户型是什么样的？遇到了哪些网络覆盖的难题？或者有什么独特的布线经验？都可以分享出来，小剧和你一起探讨！</p>
<hr />
<p><strong>说明：</strong></p>
<p>本文中涉及的 Wi-Fi、米家蓝牙 Mesh 2.0 及有线网络方案，仅适用于小剧当前户型，其他户型请勿直接套用。</p>
<p><strong>相关链接：</strong></p>
<p>UniFi Design Center：<a href="https://design.ui.com/">https://design.ui.com/</a></p>
<p><strong>[小红书] 水电验收-网络篇：</strong> <a href="https://www.xiaohongshu.com/explore/6904a09c000000000703bbef?xsec_token=ABFLhhSzqSboYxKGBLxAnMwuisTE32UCMVlUZdpp1AnKc=&amp;xsec_source=pc_user">https://www.xiaohongshu.com/explore/690…</a></p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Tue, 11 Nov 2025 14:25:13 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/1vd7ah0fttx</guid>
      <category>智能家居</category>
      <category>生活</category>
      <enclosure url="http://static.bh-lay.com/blog/2025/smart-home-network-build/3fc71756-d7d5-4794-aacb-6e4647198fdf.jpg" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>上篇文章中，小剧分享了灯光动线规划，并提到将在新房中实现智能灯光系统。</p>
<p>在全屋智能系统中，最关键的部分仍然是网络的整体搭建。</p>
<p>不仅仅因为 Wi-Fi 网络是一些智能家居设备的必选连接方式，更是家人、访客在家里漫游时网络体验的基石。</p>
<p>另外，对速度和稳定性要求高的设备都需要有线网络支持，如米家中枢网关、电视机、电脑、监控、NAS 等。</p>
<p>再回到智能家居的场景，米家基于蓝牙 Mesh2.0 的"蓝牙局域网"，可靠性也非常重要。</p>
<p>因此这一篇笔记会围绕着以下三点展开：</p>
<ul>
<li>Wi-Fi 漫游思路</li>
<li>蓝牙 Mesh2.0 网络搭建 </li>
<li>有线局域网搭建</li>
</ul>
<p>全文会按照户型分析、背景知识补课、网络设计、网络实施等顺序展开。文末会有抽象后的网络远景拓扑图。</p>
<h2 id="">一、户型特点分析</h2>
<p>所有的网络规划，离不开对所处环境的剖析。具体到家庭网络规划，就是针对户型特点选择最适合的方案。</p>
<p><strong>图一、户型图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/smart-home-network-build/494d34b2-14c9-4b67-9716-1ac3514e1cb7.jpg" alt="图一、户型图" /></p>
<p>小剧的这套约 140 平的户型，整体不算特殊，但仍有以下几个特点。</p>
<ul>
<li>户型偏瘦长（加上南北院约25m）</li>
<li>客餐厅空间开阔，但卧室区域墙体林立</li>
<li>南北双院，需要 Wi-Fi 覆盖</li>
<li>双入户门，需要布设监控</li>
</ul>
<p>基于这些特点，一个 Wi-Fi 路由器打天下的方案基本上就被否了，原因有两点。</p>
<ul>
<li>整套房子纵深过长，再加上墙体、混凝土柱子等阻隔，信号衰减严重，按照现行的路由器标准，难以覆盖这么大的尺度。</li>
<li>Wi-Fi 通信分为上下行方向。下行信号由路由器发出，覆盖范围较大；上行信号由手机等终端发出，功率较弱。</li>
</ul>
<p>因此一台路由器覆盖一个较大的面积，很容易出现手机能连接到网络，但使用体验极差的尴尬处境。</p>
<p>这就像路由器和手机在相互喊话，手机能听见路由器的呼声，路由器却很难听清手机在说什么。</p>
<p>这种情况在视频通话时最容易暴露问题。</p>
<p>对方画面很流畅，但自己在对面已经卡成了 PPT。</p>
<h2 id="-1">二、背景知识小课堂</h2>
<h3 id="21">2.1、有线网络</h3>
<p>家庭布线中，超六类网线本身和线长，在万兆以下几乎不构成瓶颈，因此前期优先要考虑的依旧是 Wi-Fi 网络。</p>
<p>另外小剧家采用的是全拆全换的方式去布线，有线网络的布局可以更加自由。因此，有线网络的方案在规划阶段的优先级可以适当靠后。</p>
<p>小剧计划将有线布线放在最后阶段再规划。</p>
<h3 id="2224g5gwifi">2.2、2.4G 和 5G Wi-Fi 频段</h3>
<p>常见的家庭 Wi-Fi 通常分为 2.4G 和 5G 两个频段。</p>
<p>2.4G 是兼容性最好的频段，穿墙能力也非常好，但速度上限不高。</p>
<p>5G 频段更适合高速传输，但信号覆盖面积不大。</p>
<p>因此常规家庭使用 2.4G、5G 双频合一，可以做到一台路由既能支持近距离高速传输、又能照顾到相对远距离干扰较多的场景。</p>
<p>回到前面提到的，小剧准备做全屋智能这个背景。因为一些智能家居设备只支持 2.4G 频段，因此双频合一的模式在小剧家并不适用。</p>
<p>手机还是连接 5G 频段，仅支持 2.4G 的智能家居设备单独走 2.4G 网络。</p>
<h3 id="23">2.3、蓝牙信号覆盖</h3>
<p>蓝牙信号覆盖，主要指的是米家蓝牙 Mesh2.0 的信号覆盖。</p>
<p>在米家的全屋智能背景下，一台米家中枢网关必不可少。</p>
<p>米家中枢网关的信号覆盖可以参照 2.4G Wi-Fi。这是因为蓝牙的工作频段与 2.4G Wi-Fi 几乎一致，覆盖能力也相近。</p>
<p>对了，米家蓝牙 Mesh2.0 的另一个魅力，是它的 Mesh 指的是支持"多跳中继通信"。</p>
<p>具体而言，在一个 20m 的长度范围内，一侧放置米家中枢网关，每隔 3m 放一台支持米家 Mesh2.0 的设备，如墙面开关。另一侧端点则放置温湿度传感器。</p>
<p>即使米家中枢网关无法覆盖到另一端，利用 Mesh2.0 多跳通信的特性，也能顺利的为端点的温湿度传感器建立起连接。</p>
<p><strong>图二、米家 Mesh2.0 示意图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/smart-home-network-build/a020bc90-9e0e-451b-8922-50d3f5995aee.png" alt="图二、米家 Mesh2.0 示意图" /></p>
<h2 id="-2">三、如何规划网络</h2>
<h3 id="31wifi">3.1、Wi-Fi 仿真工具推荐</h3>
<p>Wi-Fi 网络规划在一定程度上可依经验完成，但若能借助可视化工具进行辅助，结果会更直观、可靠。小剧在调研后选出了三个实用的工具。</p>
<ul>
<li>移动爱家 APP</li>
<li>锐捷睿易 APP</li>
<li>UniFi Design Center（文末会附上链接）</li>
</ul>
<p>前两个都是 APP，操作比较简单，但受限于手机屏幕，并不适合精细的规划操作。</p>
<p>经实际测试，小剧更推荐 UniFi Design Center。</p>
<p>UniFi Design Center 虽然是商业品牌工具，但可以免费使用，无需购买其产品。</p>
<p>借助于在线仿真工具，可以导入户型图，配置房间、墙体、门、窗等细节。再添加 Unifi 的路由、AP 等设备进行模拟仿真。</p>
<p>还可以添加监控模拟布控。</p>
<p><strong>图三、5G Wi-Fi 仿真图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/smart-home-network-build/b99b76e2-1e70-4d72-93ae-aaceed6b3540.jpg" alt="图三、5G Wi-Fi 仿真图" /></p>
<p><strong>图四、2.4G Wi-Fi 仿真图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/smart-home-network-build/3aac3ea0-ed8b-41c1-825f-6ef80beeed48.jpg" alt="图四、2.4G Wi-Fi 仿真图" /></p>
<h3 id="32wifi">3.2、Wi-Fi、中枢点位选择</h3>
<p>结合小剧的户型来看，上图中 A、B 两个路由器点位可以较好地覆盖全屋网络。</p>
<p>如果后期覆盖有盲区，C、D 两个监控位也可以视情况增设路由，来补充信号覆盖。只是目前看来没有必要。</p>
<blockquote>
  <p><strong>提示：</strong> </p>
  <p>Unifi Wi-Fi 仿真工具没必要贪图全绿覆盖，橙黄色的区域并不会对日常的使用有任何影响。</p>
  <p>过于密集的绿色区域，反倒可能因为设备在多台路由之间频繁切换影响使用体验。</p>
</blockquote>
<p>因此 A、B 两个点位可以初步定为家庭 Wi-Fi 覆盖的基础点位。</p>
<p>C、D 作为监控位的同时，也是 Wi-Fi 的备用点位，因此也需重点考虑。</p>
<p>米家中枢网关在小剧家里，一台就可以做到基础的信号覆盖，不足的话需要再添一台。</p>
<p>A 点同样作为米家中枢网关的主点位，B 点作为候补点位。</p>
<p>最终 A、B、C、D 四个点位就作为 Wi-Fi、中枢的基准点位确定了下来。</p>
<h3 id="33">3.3、有线点位发掘</h3>
<p>相比 Wi-Fi 的布局，有线点位的规划要简单得多。</p>
<p>前面的 A、B、C、D 虽然是 Wi-Fi 点位，但需要有线做基础支撑，因此这四个是最先确定下来的有线点位。</p>
<p>弱电箱是全屋最重要的"超级点位"，位于书房角落，后期将成为整个家庭网络的大脑。</p>
<p>服役在小剧现在房子里的各类设备，最终都会搬迁到这里。</p>
<p>书房作为设备密集型的房间，又单独预留了两个有线网口。</p>
<p>剩下的就简单了，每个卧室需要预留一个网口，尽管短期内用不到。</p>
<p>B 点处在电视柜中，本身需要有线。IPTV、电视机都需要有线连接。后期可能会增设的米家（副）中枢网关、其他盒子类的终端，为了稳定起见也都需要接入有线网络。</p>
<p>自此全屋的有线点位规划完毕。</p>
<h3 id="34">3.4、网络走线规划</h3>
<p>有线网络点位的梳理，只是小剧对家庭网络的需求，如何把它们组织起来是一件更头疼的事。</p>
<p>为了让网络结构更清晰，小剧采用了<strong>「一个大脑，两个中心」</strong>的思路。</p>
<p>弱电箱是"大脑"，统筹全局；公卫吊顶和电视柜是"两个中心"，分别负责卧室区和公共区的网络覆盖。</p>
<p><strong>图五、网络走线图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/smart-home-network-build/69938da6-247c-44c5-979e-ea83976b443f.jpg" alt="图五、网络走线图" /></p>
<h4 id="341">3.4.1、一个大脑</h4>
<p>弱电箱是家庭网络的大脑。前期布线无需向师傅说明家庭服务器的复杂结构，这部分等入住后小剧会自行搭建，所以上图并未体现。</p>
<p>这里包含了一根入户光纤管、通往"两个中心"以及其他空间的网线，一共九根管线。</p>
<p>小剧也没有留一个常见的铁壳弱电箱，只是简单的把全屋网络汇聚在了这里。</p>
<p>后期小剧会把这里打造成一个 mini 版的家庭机柜，让它成为真正的家庭网络"大脑"。</p>
<h4 id="342">3.4.2、中心一：公卫吊顶</h4>
<p>公卫吊顶作为卧室区域的 Wi-Fi 覆盖节点，同时全屋智能的中枢网关也在此处，所以这里便成了两个中心的其中一个。</p>
<p>由于户型狭长，加上两个卧室的网口仅作为备用，而公卫处的 Mesh 子路由本身有 4 个网口，因此采用了简化布线的方案。</p>
<p>只需从弱电箱引出一根主线，通过 Mesh 子路由分发至北院摄像头和两个次卧。</p>
<h4 id="343">3.4.3、中心二：电视柜</h4>
<p>电视柜节点主要服务于娱乐和会客功能，作为餐客厅的 Wi-Fi 覆盖点，同时承担电视机、IPTV 及其他娱乐设备的网络连接。</p>
<p>这里的路由器还被设计为拨号上网的主路由，因此自然的成为了一个独立的网络中心。</p>
<p>这里小剧尝试使用预埋线路 + 双 50 管来保证电视柜区域的走线隐藏和灵活搭建。</p>
<p>电视机和 IPTV 设备的所有线缆 （电源、网线、HDMI）通过预埋线路 + 上 50 管隐藏在墙体和柜体里。这些固定搭配日常无需变化，可以做到可用但隐形。</p>
<p>未来若新增电视盒子、游戏机等上屏设备，可通过下方 50 管连接至电视柜的开放格。这些小的设计点，可以很大程度保证，电视区域设备增设的灵活性。</p>
<p><strong>图六、电视柜走线</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/smart-home-network-build/f3f3f455-7017-4d4a-83e0-829363d72b74.jpg" alt="图六、电视柜走线" /></p>
<h2 id="-3">四、网络走线实施</h2>
<p>到这个阶段，小剧对家庭网络的需求已经明确了（在3.3部分梳理完成），各个房间的线路走向也划分完毕。</p>
<p>接下来进入水电施工阶段。</p>
<p>由于图纸中包含了许多后期接线逻辑，电工师傅在施工时无需了解这些细节。</p>
<p>因此小剧又把图纸做了进一步的简化，删掉各个节点内的设备信息，以及内部的接线逻辑，仅保留了预埋线路的走向，方便电工师傅进行施工。</p>
<p>连同上篇文章提到的开关点位的图纸，一起交给师傅进行施工作业。</p>
<p><strong>图七、网络走线图（电工版本）</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/smart-home-network-build/99c744ce-8abf-4f1e-905c-0f018355e974.jpg" alt="图七、网络走线图（电工版本）" /></p>
<p><strong>图八、电视柜走线（电工版本）</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/smart-home-network-build/f478391e-27b4-47dd-b0e8-cc1b2cfe97c8.jpg" alt="图八、电视柜走线（电工版本）" /></p>
<h2 id="-4">五、网络拓扑远景图</h2>
<p>以上这些都是家庭网络的基石，作为小剧新房装修的一部分。</p>
<p>目前水电施工已经结束，初步验收网络的走线和点位均符合设计预期。只是水晶头、网络面板尚未安装，网线也未经过测试。</p>
<p><strong>图九、网络拓扑图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/smart-home-network-build/2d992d91-2ea4-4fb9-97c1-dce3c916fbc0.jpg" alt="图九、网络拓扑图" /></p>
<p>值得注意的是，从拓扑图中可以得出一个疑问。</p>
<blockquote>
  <p><strong>Q：为什么同样在书房，需要用到两部交换机，一台不是更合适？</strong> </p>
  <p><strong>A：</strong> 确实是的，一台 16 口交换机在这里最合适不过了，只是小剧目前家里有两台在用的同款交换机，均为八口 2.5G 电口 + 两口万兆 SFP+。</p>
  <p>为了充分利用现有设备，小剧决定继续让手头的交换机发光发热。</p>
</blockquote>
<h2 id="-5">六、写在最后</h2>
<p>从户型分析、工具选择、点位规划到最终施工落地，整个网络规划历时一个月。虽然过程繁琐，但看到水电验收时每一根网线都准确到位，还是很有成就感的。</p>
<p>这套<strong>"一个大脑 + 两个中心"</strong>的架构，本质上是根据狭长户型的特点，用最少的设备实现最优的覆盖。当然，纸面规划再完美，也需要实际使用来检验。</p>
<p>后续等所有设备就位后，小剧会再分享实际使用体验和踩坑记录，包括：</p>
<ul>
<li>Wi-Fi 漫游真实表现</li>
<li>米家蓝牙 Mesh2.0 的稳定性测试</li>
<li>全屋智能场景的联动调试</li>
</ul>
<p>如果你也在规划家庭网络，或者对文中的方案有疑问，欢迎在评论区交流讨论！你的户型是什么样的？遇到了哪些网络覆盖的难题？或者有什么独特的布线经验？都可以分享出来，小剧和你一起探讨！</p>
<hr />
<p><strong>说明：</strong></p>
<p>本文中涉及的 Wi-Fi、米家蓝牙 Mesh 2.0 及有线网络方案，仅适用于小剧当前户型，其他户型请勿直接套用。</p>
<p><strong>相关链接：</strong></p>
<p>UniFi Design Center：<a href="https://design.ui.com/">https://design.ui.com/</a></p>
<p><strong>[小红书] 水电验收-网络篇：</strong> <a href="https://www.xiaohongshu.com/explore/6904a09c000000000703bbef?xsec_token=ABFLhhSzqSboYxKGBLxAnMwuisTE32UCMVlUZdpp1AnKc=&amp;xsec_source=pc_user">https://www.xiaohongshu.com/explore/690…</a></p>]]></content:encoded>
    </item>
    <item>
      <title>灯光动线规划</title>
      <link>https://bh-lay.com/blog/y339na3oqz</link>
      <description><![CDATA[<p>时隔五年，小剧又重新做回房奴，在 2025 年这个全民看跌的时候买了房。不是基于任何对经济的预测，也不是对楼市还抱有幻想。</p>
<p>小剧开了个<a href="https://www.xiaohongshu.com/user/profile/64aaa7d6000000001f007d38">小红书小号</a>，用来记录装修的过程。但是小红书的特性，天然不适合记录这种枯燥的长文，尤其是小剧常写的这种"悦己"的文章。</p>
<p>所以接下来可能会有数篇，结合新房装修的文章记录博客里。</p>
<h2 id="">一、灯光动线指的是什么？</h2>
<p>室内设计一般提到"动线"，都是指从一个空间到另一个空间，或者在整个大的空间中的行进路线。</p>
<p>对于灯光来说，逻辑是相似的，但它更侧重于夜晚人在房子里走动时，灯光如何配合。</p>
<p>合理的灯光动线规划，可以在夜晚的行走更加顺畅无障碍。</p>
<h2 id="-1">二、为什么要设计灯光动线</h2>
<p>这是个好问题，对于常规户型确实不需要刻意去做灯光的动线，比如以下两种：</p>
<p>一是功能集中的动静分离户型，一侧是卧室区域，另一侧是餐客厨区域，入户门在中间。</p>
<p>或者经典的四叶草，卧室分散在房子的角落，餐客厅被包围在中间，入户门同样也在某条边的中间位置。</p>
<p>对于这两种比较"理想"的户型，做好双控和感应灯，基本上也就万事大吉了。</p>
<p>小剧之所以要去单独做灯光动线规划，主要有两个方面。</p>
<h3 id="21">2.1、房子有两个入户门</h3>
<p>这套房子最大的特点是南北双院双入户门，当然也是一件头疼的事。</p>
<p>一方面双入户门会增加回家方式的灵活性，另一方面家中格局和柜体的设计，都需要为归家动线做避让。</p>
<p>同样在灯光动线上来说，也需要为夜晚回家的情形做更多的考量。</p>
<h3 id="22">2.2、户型偏瘦长</h3>
<p>整体来看，小剧购入的这套房子，格局是接近动静分离的户型。餐客厅在一起，卧室集中在一起。但偏偏小剧使用率较高的书房，在客厅的另一侧。</p>
<p>瘦长的户型加上"遥远"的书房，让夜晚的动线变得狭长又细碎。如果处理得不好，很容易出现跑"数公里"去关一盏灯的情况。 </p>
<h2 id="-2">三、如何设计灯光动线？</h2>
<p>灯光设计本身小剧并不专业，尤其是全屋灯光的设计，主灯、辅灯的搭配。</p>
<p>好在小剧的<strong>设计师出了一份比较详尽的灯光开关点位图，包含了比较细致的灯路细节。</strong></p>
<p>因此小剧可以在设计师的灯光图基础上做动线规划。</p>
<h3 id="31">3.1、智能灯光</h3>
<p>小剧已经使用米家生态好几年，从拿到房子开始，小剧就决定继续做的智能灯光。智能灯光的灵活性，在解决开关灯的便利上有得天独厚的优势。 </p>
<p>小剧的智能灯光实现逻辑很简单，是<strong>以智能开关为基础的智能灯光</strong>。</p>
<p>因为开关本身是智能的，在灯具的选择上自由了很多。可以是智能灯具，也可以传统的灯泡。</p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/c65f5544-4add-4573-899c-fabb58b6ca29.jpg" alt="无需物理双控" /></p>
<p>智能灯具的常在线特性，使得开灯的方式变得非常多样。开关、传感器、智能场景等多种方式都能实现开关灯。不需要传统的线路来实现物理双控。搭配得当的话可以让自己和家人达到"遗忘"开关的效果。 </p>
<h3 id="32">3.2、开关点位的优化</h3>
<p>前面提到了，小剧的设计是<strong>以设计师出具的灯光开关点位图为基础，进而借助智能灯光实现灯光动线的优化。</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/ec5ba852-ec67-4590-ad01-e9f79af0fbc5.jpg" alt="灯具开关点位图标记" /></p>
<p>小剧在拿到点位图之后，就开始用娃的马克笔做起了标注。</p>
<p>仔细审视每个开关，设想夜晚在家里的走动线路。尝试发现一些缺失的开关，或者查找有没有一些开关是永远用不到的。</p>
<p>不得不说设计师出的图很严谨，考虑到了不同环境、不同类型的灯光做搭配。开关位置设计的也很贴心，<strong>这一步没有发现需要改动的地方。</strong></p>
<p>不过在智能灯光优化这个背景下，还是有些开关是可以省掉的。</p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/xiaozoulang.jpg" alt="小走廊动线" /></p>
<p>比如卧室集中区域的"小走廊动线"，原本设计师在走廊首尾各放了一个开关。</p>
<p>这设计很好，但在智能开关的场景下，左侧的双控如果取消， 把它和卧室里的开关放在一起，动线可能会更流畅。</p>
<p>在改造之前，从公共空间进到卧室，只能有以下的两种动线。</p>
<ul>
<li>进入卧室，打开卧室的灯，出卧室门关掉走廊的灯，再进屋。</li>
<li>先关掉走廊的灯，再摸黑进卧室，最后打开卧室灯。</li>
</ul>
<p>如图改造之后，借助于智能灯光灵活的多控逻辑，可以在这一路实现同一灯路的虚拟三控。</p>
<p>取消后的开关分散到卧室内，可以实现人进屋、打开卧室的灯、关闭走廊的灯三个动作，没有折返和摸黑，整个过程一气呵成。</p>
<h3 id="33">3.3、借助于传感器辅助开关灯</h3>
<p>卫生间、厨房、衣帽间，这些空间在动线上很相似。空间相对独立，每天进出频次非常高。</p>
<p>这类场景天然适合引入人体、人在类的传感器，用来辅助实现"人来亮灯、人走关灯"的效果。</p>
<p>在一段时间的适应之后，可以很自然的径直进入再直接走出空间，而不用思考去哪里开灯，什么时候关灯。</p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/zidong.jpg" alt="自动开灯区域" /></p>
<h3 id="34">3.4、归家开灯</h3>
<p>这里的归家开灯并不是小红书、抖音上常见的"归家模式"。在打开门之后，音箱送来一句温馨的欢迎语，同时逐步开启全部的灯、打开（或关闭）窗帘。</p>
<p>这个效果确实很炫，小剧很久之前也尝试过。但后来越做越克制，现在仅保留了开门打开走廊的灯并切成夜光模式。同时还会判断餐客厅是否有别的灯在开着，有的话就什么事都不做。 毕竟家里可能还有其他成员，冷不丁的开灯加音箱呼喊，可能对已经在屋里的家人是一种惊吓。</p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/guijia.jpg" alt="归家开灯" /></p>
<p>对于新房子来说，归家开灯的入户动线变得复杂了起来。因为前面提过，新房子是双入户门，在归家这条动线上有两条完全不一样的路径。</p>
<p>小剧对于归家开灯的设计比较克制，因此只有两种场景需要开灯。一是家里真的没有人，第二个是家里人都已经睡下了。</p>
<p>在这两种场景下，只需要从对应的入户门开始微微照亮通往卧室和通往书房的灯即可。</p>
<p>细心的你可能会发现，归家开灯的两条灯路里，跨越了好几条灯路线，并且是多个灯路里的部分灯，而非全部。</p>
<p>这种开灯方式，在传统灯路设计里是几乎不可能实现的，下面单独介绍下如何实现跨灯路开灯。</p>
<h3 id="35">3.5、公区动线 - 跨灯路开灯</h3>
<p>前面归家开灯的动线里出现的跨灯路开灯，这里展开聊一下。</p>
<p>首先这是一条虚拟的公区动线，当我们人站在左边小走廊，或者右侧书房的时候，也就是这条动线的两端。想要前往餐厅、客厅、出门、进入书房或者卧室。</p>
<p>在餐客厅灯已经全部熄灭的时候，如果能打开一条行动路线上的微弱灯光来引路，行走起来会非常方便，并且不会因为突然之间打开的灯引起视觉的不舒适。</p>
<p>这条虚拟的灯路跨越了四条灯路，共计五盏灯。</p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/gongqudongxian.jpg" alt="公区动线" /></p>
<p>这五盏灯之所以能独立控制，有一个非常必要的前提，就是这四条灯路上的灯全部是常在线的智能灯。</p>
<p>在这个前提下，对应灯路上的开关必然已经被转成了无线开关，并且设置为常通电模式。</p>
<p>此时灯路上所有的灯都已经变成了独立的个体，可以分别控制开启和关闭以及色温亮度。</p>
<h3 id="36">3.6、垃圾回收之关闭公区的灯</h3>
<p>前面提到的归家开灯、公区动线灯，会在操作过程中主动打开部分灯路的部分灯。</p>
<p>另外配合墙壁开关，餐厅、客厅的主灯、多路筒灯，以及独立的筒灯有可能会呈现出非常多的排列组合状态。</p>
<p>在需要出门、睡觉、进书房刷抖音看扭屁股（哦不，是去创作）的时候，如何方便的关掉它们是件麻烦的事情。</p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/gongqu.jpg" alt="关闭公区的灯" /></p>
<p>这里把餐厅、客厅、走廊、入户门、庭院灯做一个组合，叫做"公区"。</p>
<p>在每个卧室、每个入户门边、书房里，通过情景开关的单击或者长按操作，可以一键关闭它们。</p>
<p>这个操作像极了函数运行时的垃圾回收机制。通过一键关闭公区全部的灯，可以避免往返开关灯的繁琐，以及摸黑走路的尴尬。</p>
<h2 id="-3">四、灯光动线几个小原则</h2>
<p>前面介绍具体灯光动线的时候，有几个词是小剧反复提到的，精炼一下就是小剧设计灯光动线的原则。</p>
<h3 id="41">4.1、每条灯路都需要有实体开关</h3>
<p>相信你也知道，如果灯是智能的，对应的开关在设置为常通电的前提下，按键是可以改为其他功能的。</p>
<p>但是小剧极不推荐这么做。一是没必要，实体的按键对于老人、客人来说还是安全感满满的，用起来没有学习成本。</p>
<p>二来如果真的需要无线开关，可以把开关换成双键或者三键版本，多出来的按键就可以随意发挥了。</p>
<h3 id="42">4.2、避免摸黑</h3>
<p>无论是进入还是离开一个空间，都要避免摸黑。可以是在动线的首、尾设置多控开关，也可以是借助于传感器代替手动开关。</p>
<p>摸黑首先体验感非常差，其次可能会无意间发生磕碰，引发危险。</p>
<h3 id="43">4.3、避免折返</h3>
<p>开关灯的场景下，折返一般是因为开关设置不合理，导致开关按键分散且没有逻辑性。</p>
<p>又或者为了避免摸黑，而不得不先进入一个空间开灯，再回到原空间关灯，最终再进入目标空间这种尴尬的情形。</p>
<h3 id="44">4.4、避免打扰和惊吓</h3>
<p>自动化的设置很容易，但是当人处在某个空间中时，因为跨空间的事件，导致当前空间的灯光被执行，这种毫无征兆的发生会带来很不好的体验，甚至是惊吓。</p>
<p>因此自动化的设置要很克制，需要符合空间内的感知逻辑，也就是让人知道真实世界发生了什么，接下来灯光的变化会有心理预期。</p>
<hr />
<p>目前硬装刚刚开始，以上提到的也只是小剧设计阶段的规划。</p>
<p>实际操作配置过程中，细节肯定会有微调，期待三五个月后落地的效果。</p>
<hr />
<hr />
<p><strong>开关灯具布置图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/kaiguan.jpg" alt="开关灯具布置图" /></p>
<p><strong>开关接线（给电工看的）</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/xianlu.jpg" alt="点位图批注版" /></p>
<p><strong>灯具开关统计</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/tongji.jpg" alt="灯具开关统计" /></p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Mon, 15 Sep 2025 14:39:01 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/y339na3oqz</guid>
      <category>智能家居</category>
      <category>米家</category>
      <category>生活</category>
      <enclosure url="http://static.bh-lay.com/blog/2025/lighting-pathways/cover.jpg" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>时隔五年，小剧又重新做回房奴，在 2025 年这个全民看跌的时候买了房。不是基于任何对经济的预测，也不是对楼市还抱有幻想。</p>
<p>小剧开了个<a href="https://www.xiaohongshu.com/user/profile/64aaa7d6000000001f007d38">小红书小号</a>，用来记录装修的过程。但是小红书的特性，天然不适合记录这种枯燥的长文，尤其是小剧常写的这种"悦己"的文章。</p>
<p>所以接下来可能会有数篇，结合新房装修的文章记录博客里。</p>
<h2 id="">一、灯光动线指的是什么？</h2>
<p>室内设计一般提到"动线"，都是指从一个空间到另一个空间，或者在整个大的空间中的行进路线。</p>
<p>对于灯光来说，逻辑是相似的，但它更侧重于夜晚人在房子里走动时，灯光如何配合。</p>
<p>合理的灯光动线规划，可以在夜晚的行走更加顺畅无障碍。</p>
<h2 id="-1">二、为什么要设计灯光动线</h2>
<p>这是个好问题，对于常规户型确实不需要刻意去做灯光的动线，比如以下两种：</p>
<p>一是功能集中的动静分离户型，一侧是卧室区域，另一侧是餐客厨区域，入户门在中间。</p>
<p>或者经典的四叶草，卧室分散在房子的角落，餐客厅被包围在中间，入户门同样也在某条边的中间位置。</p>
<p>对于这两种比较"理想"的户型，做好双控和感应灯，基本上也就万事大吉了。</p>
<p>小剧之所以要去单独做灯光动线规划，主要有两个方面。</p>
<h3 id="21">2.1、房子有两个入户门</h3>
<p>这套房子最大的特点是南北双院双入户门，当然也是一件头疼的事。</p>
<p>一方面双入户门会增加回家方式的灵活性，另一方面家中格局和柜体的设计，都需要为归家动线做避让。</p>
<p>同样在灯光动线上来说，也需要为夜晚回家的情形做更多的考量。</p>
<h3 id="22">2.2、户型偏瘦长</h3>
<p>整体来看，小剧购入的这套房子，格局是接近动静分离的户型。餐客厅在一起，卧室集中在一起。但偏偏小剧使用率较高的书房，在客厅的另一侧。</p>
<p>瘦长的户型加上"遥远"的书房，让夜晚的动线变得狭长又细碎。如果处理得不好，很容易出现跑"数公里"去关一盏灯的情况。 </p>
<h2 id="-2">三、如何设计灯光动线？</h2>
<p>灯光设计本身小剧并不专业，尤其是全屋灯光的设计，主灯、辅灯的搭配。</p>
<p>好在小剧的<strong>设计师出了一份比较详尽的灯光开关点位图，包含了比较细致的灯路细节。</strong></p>
<p>因此小剧可以在设计师的灯光图基础上做动线规划。</p>
<h3 id="31">3.1、智能灯光</h3>
<p>小剧已经使用米家生态好几年，从拿到房子开始，小剧就决定继续做的智能灯光。智能灯光的灵活性，在解决开关灯的便利上有得天独厚的优势。 </p>
<p>小剧的智能灯光实现逻辑很简单，是<strong>以智能开关为基础的智能灯光</strong>。</p>
<p>因为开关本身是智能的，在灯具的选择上自由了很多。可以是智能灯具，也可以传统的灯泡。</p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/c65f5544-4add-4573-899c-fabb58b6ca29.jpg" alt="无需物理双控" /></p>
<p>智能灯具的常在线特性，使得开灯的方式变得非常多样。开关、传感器、智能场景等多种方式都能实现开关灯。不需要传统的线路来实现物理双控。搭配得当的话可以让自己和家人达到"遗忘"开关的效果。 </p>
<h3 id="32">3.2、开关点位的优化</h3>
<p>前面提到了，小剧的设计是<strong>以设计师出具的灯光开关点位图为基础，进而借助智能灯光实现灯光动线的优化。</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/ec5ba852-ec67-4590-ad01-e9f79af0fbc5.jpg" alt="灯具开关点位图标记" /></p>
<p>小剧在拿到点位图之后，就开始用娃的马克笔做起了标注。</p>
<p>仔细审视每个开关，设想夜晚在家里的走动线路。尝试发现一些缺失的开关，或者查找有没有一些开关是永远用不到的。</p>
<p>不得不说设计师出的图很严谨，考虑到了不同环境、不同类型的灯光做搭配。开关位置设计的也很贴心，<strong>这一步没有发现需要改动的地方。</strong></p>
<p>不过在智能灯光优化这个背景下，还是有些开关是可以省掉的。</p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/xiaozoulang.jpg" alt="小走廊动线" /></p>
<p>比如卧室集中区域的"小走廊动线"，原本设计师在走廊首尾各放了一个开关。</p>
<p>这设计很好，但在智能开关的场景下，左侧的双控如果取消， 把它和卧室里的开关放在一起，动线可能会更流畅。</p>
<p>在改造之前，从公共空间进到卧室，只能有以下的两种动线。</p>
<ul>
<li>进入卧室，打开卧室的灯，出卧室门关掉走廊的灯，再进屋。</li>
<li>先关掉走廊的灯，再摸黑进卧室，最后打开卧室灯。</li>
</ul>
<p>如图改造之后，借助于智能灯光灵活的多控逻辑，可以在这一路实现同一灯路的虚拟三控。</p>
<p>取消后的开关分散到卧室内，可以实现人进屋、打开卧室的灯、关闭走廊的灯三个动作，没有折返和摸黑，整个过程一气呵成。</p>
<h3 id="33">3.3、借助于传感器辅助开关灯</h3>
<p>卫生间、厨房、衣帽间，这些空间在动线上很相似。空间相对独立，每天进出频次非常高。</p>
<p>这类场景天然适合引入人体、人在类的传感器，用来辅助实现"人来亮灯、人走关灯"的效果。</p>
<p>在一段时间的适应之后，可以很自然的径直进入再直接走出空间，而不用思考去哪里开灯，什么时候关灯。</p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/zidong.jpg" alt="自动开灯区域" /></p>
<h3 id="34">3.4、归家开灯</h3>
<p>这里的归家开灯并不是小红书、抖音上常见的"归家模式"。在打开门之后，音箱送来一句温馨的欢迎语，同时逐步开启全部的灯、打开（或关闭）窗帘。</p>
<p>这个效果确实很炫，小剧很久之前也尝试过。但后来越做越克制，现在仅保留了开门打开走廊的灯并切成夜光模式。同时还会判断餐客厅是否有别的灯在开着，有的话就什么事都不做。 毕竟家里可能还有其他成员，冷不丁的开灯加音箱呼喊，可能对已经在屋里的家人是一种惊吓。</p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/guijia.jpg" alt="归家开灯" /></p>
<p>对于新房子来说，归家开灯的入户动线变得复杂了起来。因为前面提过，新房子是双入户门，在归家这条动线上有两条完全不一样的路径。</p>
<p>小剧对于归家开灯的设计比较克制，因此只有两种场景需要开灯。一是家里真的没有人，第二个是家里人都已经睡下了。</p>
<p>在这两种场景下，只需要从对应的入户门开始微微照亮通往卧室和通往书房的灯即可。</p>
<p>细心的你可能会发现，归家开灯的两条灯路里，跨越了好几条灯路线，并且是多个灯路里的部分灯，而非全部。</p>
<p>这种开灯方式，在传统灯路设计里是几乎不可能实现的，下面单独介绍下如何实现跨灯路开灯。</p>
<h3 id="35">3.5、公区动线 - 跨灯路开灯</h3>
<p>前面归家开灯的动线里出现的跨灯路开灯，这里展开聊一下。</p>
<p>首先这是一条虚拟的公区动线，当我们人站在左边小走廊，或者右侧书房的时候，也就是这条动线的两端。想要前往餐厅、客厅、出门、进入书房或者卧室。</p>
<p>在餐客厅灯已经全部熄灭的时候，如果能打开一条行动路线上的微弱灯光来引路，行走起来会非常方便，并且不会因为突然之间打开的灯引起视觉的不舒适。</p>
<p>这条虚拟的灯路跨越了四条灯路，共计五盏灯。</p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/gongqudongxian.jpg" alt="公区动线" /></p>
<p>这五盏灯之所以能独立控制，有一个非常必要的前提，就是这四条灯路上的灯全部是常在线的智能灯。</p>
<p>在这个前提下，对应灯路上的开关必然已经被转成了无线开关，并且设置为常通电模式。</p>
<p>此时灯路上所有的灯都已经变成了独立的个体，可以分别控制开启和关闭以及色温亮度。</p>
<h3 id="36">3.6、垃圾回收之关闭公区的灯</h3>
<p>前面提到的归家开灯、公区动线灯，会在操作过程中主动打开部分灯路的部分灯。</p>
<p>另外配合墙壁开关，餐厅、客厅的主灯、多路筒灯，以及独立的筒灯有可能会呈现出非常多的排列组合状态。</p>
<p>在需要出门、睡觉、进书房刷抖音看扭屁股（哦不，是去创作）的时候，如何方便的关掉它们是件麻烦的事情。</p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/gongqu.jpg" alt="关闭公区的灯" /></p>
<p>这里把餐厅、客厅、走廊、入户门、庭院灯做一个组合，叫做"公区"。</p>
<p>在每个卧室、每个入户门边、书房里，通过情景开关的单击或者长按操作，可以一键关闭它们。</p>
<p>这个操作像极了函数运行时的垃圾回收机制。通过一键关闭公区全部的灯，可以避免往返开关灯的繁琐，以及摸黑走路的尴尬。</p>
<h2 id="-3">四、灯光动线几个小原则</h2>
<p>前面介绍具体灯光动线的时候，有几个词是小剧反复提到的，精炼一下就是小剧设计灯光动线的原则。</p>
<h3 id="41">4.1、每条灯路都需要有实体开关</h3>
<p>相信你也知道，如果灯是智能的，对应的开关在设置为常通电的前提下，按键是可以改为其他功能的。</p>
<p>但是小剧极不推荐这么做。一是没必要，实体的按键对于老人、客人来说还是安全感满满的，用起来没有学习成本。</p>
<p>二来如果真的需要无线开关，可以把开关换成双键或者三键版本，多出来的按键就可以随意发挥了。</p>
<h3 id="42">4.2、避免摸黑</h3>
<p>无论是进入还是离开一个空间，都要避免摸黑。可以是在动线的首、尾设置多控开关，也可以是借助于传感器代替手动开关。</p>
<p>摸黑首先体验感非常差，其次可能会无意间发生磕碰，引发危险。</p>
<h3 id="43">4.3、避免折返</h3>
<p>开关灯的场景下，折返一般是因为开关设置不合理，导致开关按键分散且没有逻辑性。</p>
<p>又或者为了避免摸黑，而不得不先进入一个空间开灯，再回到原空间关灯，最终再进入目标空间这种尴尬的情形。</p>
<h3 id="44">4.4、避免打扰和惊吓</h3>
<p>自动化的设置很容易，但是当人处在某个空间中时，因为跨空间的事件，导致当前空间的灯光被执行，这种毫无征兆的发生会带来很不好的体验，甚至是惊吓。</p>
<p>因此自动化的设置要很克制，需要符合空间内的感知逻辑，也就是让人知道真实世界发生了什么，接下来灯光的变化会有心理预期。</p>
<hr />
<p>目前硬装刚刚开始，以上提到的也只是小剧设计阶段的规划。</p>
<p>实际操作配置过程中，细节肯定会有微调，期待三五个月后落地的效果。</p>
<hr />
<hr />
<p><strong>开关灯具布置图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/kaiguan.jpg" alt="开关灯具布置图" /></p>
<p><strong>开关接线（给电工看的）</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/xianlu.jpg" alt="点位图批注版" /></p>
<p><strong>灯具开关统计</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/lighting-pathways/tongji.jpg" alt="灯具开关统计" /></p>]]></content:encoded>
    </item>
    <item>
      <title>从 Mac mini 到飞牛 OS 的家庭服务器迁移之路</title>
      <link>https://bh-lay.com/blog/2d38u0j7q63</link>
      <description><![CDATA[<p>飞牛 OS 自 2024 下半年发布，至今不到一年的时间。作为一个国产的 NAS OS，较高的完成度、免费、高频的更新，这三点让小剧很是敬佩。</p>
<p>小剧使用 Mac mini 做家庭服务器已经两年多了，这台 8 + 256 的丐版小机器在小剧的调教之下也越来越顺手。</p>
<h2 id="">一、为什么要加装飞牛？</h2>
<h3 id="11">1.1、尝鲜</h3>
<p>正如前面提到的，飞牛 OS 作为一款 NAS 操作系统，功能完成度极高，并且体验很好。</p>
<p>在一众 UP 主的欢呼声感染之后，小剧也想实际体验一下究竟有多丝滑。</p>
<h3 id="12macos">1.2、寻找 MacOS 的替代平台</h3>
<p>小剧使用 MacOS 的 Mac mini 做家庭服务器，虽然没有出过大的问题，但是两次事故都让小剧心有余悸。</p>
<p>一次是家里意外断电，导致 Docker 里的容器丢失，最严重的是 Docker 系统 Volumes 丢失。关于这次故障记录在<a href="https://bh-lay.com/blog/1sidzrps0yb">《家庭服务器断电处理记录》</a>。</p>
<p>另一次是 MacOS 升级，导致系统默认 Shell 由 Bash 变为 zsh，Docker Desktop 部分功能需要更改配置才能正常使用。小剧也是发现 Immich 容器无法启动才定位到这个问题。</p>
<p>这两件事看起来不相同，根本原因是一样的。MacOS 的 Docker 是借助于虚拟机实现的，很多我们对 Docker 的理解需要 Docker  Desktop 创建的虚拟机来抹平差异。</p>
<p>飞牛基于 Debian 内核，对 Docker 的支持更直接。</p>
<h3 id="13">1.3、寻找更易用的方案</h3>
<p>经过最近两年多对 Mac mini 的使用，小剧整理出个人最常用的功能有以下几个：</p>
<p><strong>文件共享</strong></p>
<p>小剧使用 MacOS 的 SMB 协议共享文件，在 PC 上体验还不错，但在手机上得借助于第三方 APP 实现访问，易用性较差。</p>
<p><strong>照片备份、查看</strong></p>
<p>小剧一直使用 Immich 做相册管理，多端支持且体验很好。</p>
<p><strong>文档管理</strong></p>
<p>小剧使用 outline 做家庭文档管理，包括你在阅读的这篇博文。以及最近半年的文章都是借助于 outline 书写的。<a href="https://bh-lay.com/blog/2oevmzytwwc">《Outline Docs 初体验》</a>介绍了小剧初次接触 outline 的兴奋劲儿。</p>
<p><strong>影视观看</strong></p>
<p>小剧对于影视并没有太多执念，也没有屯片儿的爱好。创建 Jellyfin 也只是为了<strong>让娃看动画片时，少一些不合时宜，甚至少儿不宜的广告</strong>。另外也能给给媳妇在冷门平台追剧时少付点会员费。</p>
<p><strong>文件备份</strong></p>
<p>数年前不知道从哪个渠道，斥巨资购买了 CCC 备份软件，用于实现 Mac mini 中的文件备份。</p>
<h2 id="-1">二、为飞牛塑造肉身</h2>
<p>如果你有仔细审视过个人的数据，抛开影音、软件、游戏之外，纯个人的数据其实是不多的。</p>
<p>个人文件最大比例，是常见的照片视频。其余各类文档性质的文件，数量庞多且细碎，但总体积并不会很大。</p>
<p>从小剧最近两年的经验来看，1T 是目前个人（家庭）数据的总量上限。预留 2T 的空间足够家庭接下来三到五年的使用了。</p>
<h3 id="21">2.1、硬件选择</h3>
<p>相信你在<strong>"B 站职业技术学院"</strong>进修过很多次，数不清的大佬教你用一百种方法组装 NAS。有追求大容量的超级多盘位，有追求极致性价比的捡垃圾，还有恰饭博主的各种成品推荐。</p>
<p>对于小剧来说，对硬件没有太多的追求，只要能满足：体积小巧、容量够用、低噪音、不夸张的功耗、合理的局域网使用速度，也就足够了。</p>
<p>经过一番挑选，小剧选择了极摩客 G3 Plus 作为飞牛 NAS 的硬件基础。</p>
<p><strong>极摩客 G3 Plus 照片</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/mac-mini-feiniu-os/7558a1de-2d2f-4f25-961d-7d18ccb25f8c.jpg" alt="极摩客 G3 Plus 照片" /></p>
<p>这是小剧购买的第二台极摩客的设备，做工和颜值还是挺讨喜的。</p>
<p>不横向对比其他的选项了，单纯介绍下选择这一款的主要原因：</p>
<ul>
<li>体积小巧，结构紧凑，易部署</li>
<li>N150 的处理器，带核显，功耗低、性能尚可</li>
<li>最大支持 16G 内存</li>
<li>支持两根 SSD 硬盘</li>
<li>内置 2.5G 网卡</li>
<li>价格便宜（这条是刚需）</li>
</ul>
<h3 id="22">2.2、主机配件成本</h3>
<p>极摩客 G3 Plus 准系统版本是没办法开箱即用的，需要加装一根内存条、至少一根 SSD 才可以使用。</p>
<p>以下是这次购入的全部零配件。</p>
<p>极摩客 G3 plus，566 元。</p>
<p>16G 内存，136 元。</p>
<p>购入了一根 512G 2242 规格的 SSD 做系统盘，205.6 元。</p>
<p>一根 2T 2280 规格的 SSD 做数据存储盘，760 元。</p>
<p>另外为了利用 2.5G 网口的性能，升级了即将成为瓶颈的千兆交换机，296 元。</p>
<p><strong>主机 + 配件清单截图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/mac-mini-feiniu-os/259d47d7-95dd-4163-8a6a-1f409bebc914.jpg" alt="主机 + 配件清单截图" /></p>
<h3 id="23">2.3、散热处理</h3>
<p>这里又是小剧日常矫情的环节了。</p>
<p>设备调试期间，小剧发现整个主机最热的部分是 2280 的那根 SSD。待机约 55℃，轻度使用就会超过 65℃。</p>
<p>按照官方的介绍，SN580 这款硬盘在 0-85℃ 之间都是能正常工作的。</p>
<p>所以这就是小剧矫情的地方了，非要想办法把 SSD 的待机温度控制在 50℃ 以下。</p>
<p>小剧尝试了硬盘赠送的散热马甲、加装带风扇的散热马甲、加装侧吹风扇。这些方案都能显著降低温度1-5℃，但长时间使用后都会慢慢"囤积"温度消散不去。</p>
<p>最终小剧还是买了水冷散热的配件，把极摩客小主机的天灵盖开了两个孔。</p>
<p>最终温度控制在待机 40-45℃，重度使用 55℃。</p>
<p>极摩客天灵盖开颅照片</p>
<p><img src="https://static.bh-lay.com/blog/2025/mac-mini-feiniu-os/ee3611d6-4f4b-4755-8f3d-69e99a32bd81.jpg" alt="极摩客天灵盖开颅照片" /></p>
<h2 id="-2">三、实际上手体验</h2>
<p>飞牛 OS 的安装、初始化还是很丝滑的，支持可视化安装。这一步没有太多可吹的地方，毕竟一台设备基本上只会安装一次。即使步骤繁琐也不会每天困扰到你。</p>
<p>不过小剧还是在这步遇到了灵异事件，安装完成后启动系统，始终会报 GRUB 错误，重装三遍都是如此。</p>
<p>最后小剧重新从官网下载镜像，重新安装再启动，竟然奇迹般的好了。</p>
<h3 id="31">3.1、内网速度</h3>
<p>因为小剧家里只有飞牛 OS、交换机支持 2.5G 网速，因此任何一台设备使用飞牛的时候都跑不满 2.5G 的带宽。</p>
<p>但这并不能说明 2.5G 没有意义，在多台设备同时访问飞牛时，飞牛提供高于单台设备的吞吐上限可以缓解并行的压力。</p>
<p>在从 Mac mini 迁移数据到飞牛 OS 的时候，跑满千兆带宽的同时还能给其他设备提供服务。</p>
<p>在每日定时执行跨设备备份任务的时候，体验也不会打折扣。</p>
<p><strong>千兆网速截图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/mac-mini-feiniu-os/9e7c21e4-c655-4a28-94c9-7d1cec4935fd.jpg" alt="千兆网速截图" /></p>
<h3 id="32">3.2、外网速度</h3>
<p>很多飞牛的小伙伴苦于中继服务较低的带宽，在家庭以外很难顺畅的使用 NAS。飞牛 OS 提供免费的中继服务，我们除了感恩戴德并不能埋怨什么。</p>
<p>根本原因还是我们的家庭网络环境较为简陋，导致免费且高速的方案没办法使用。</p>
<p>好在小剧很多年前申请到过公网 IPV4，至今可用。安徽电信的公网 IPV6 也能以很低的成本获得。</p>
<p>因此在外网使用时，是很容易达到直连效果的。给娃看 NAS 里的动画片还是不在话下的。</p>
<h3 id="33">3.3、文件管理逻辑</h3>
<p>之前使用 Mac mini 管理文件，是没有多用户的概念的，或者想实现多用户是的很麻烦的。</p>
<p>飞牛的多用户天然支持就很好，而且在系统级别屏蔽了较深的文件路径前缀。</p>
<p>也可以借助于共享功能在用户之间（其实也就是我媳妇和我妈妈）共享部分数据。</p>
<p>相互隔离也能相互融合。</p>
<p>当然，作为管理和维护飞牛的小剧来说，其实是开了上帝视角的。毕竟我有整台机器的全部权限。</p>
<h3 id="34">3.4、飞牛相册</h3>
<p>小剧使用 Immich 做家庭相册已经一年多了。伴随着 Immich 一次次的升级，个人的很多相册相关的数据都沉淀在这里。</p>
<p><del>小剧尝试拷贝了 2018 全年的照片做测试，发现飞牛 OS 的相册目前还不足以迁移。</del></p>
<p><del>原因有很多，主要原因是不支持地图模式。其次是相册管理的交互不够细腻，例如管理人脸识别数据时很多界面不支持搜索等交互细节。</del></p>
<p>上面删掉的部分是<strong>五月份</strong>写好的内容。</p>
<p>小剧拖沓的惰性导致文章被延期到七月才发布。</p>
<p>在这两个月的时间内，飞牛 OS 推出了数个更新包。不仅包含了相册地图模式，还为宝爸宝妈开发了宝宝相册。</p>
<p>六月初小剧开始逐渐把相册往 immich 和飞牛两个平台分别备份。经过了一个月的体验，飞牛 OS 在相册体验上的更新还是很多的。在六月底小剧正式下线了 immich 服务。</p>
<p>并不是 Immich 不够好，它的体验和功能完整性依然是绝对优于飞牛相册的。但是作为家庭服务的管理者，小剧的抉择需要在体验和运维成本中找平衡。飞牛相册目前的表现足以吸引小剧进行迁移。</p>
<p>为什么地图模式这么重要？</p>
<p><strong>地图模式截图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/mac-mini-feiniu-os/510a0b0b-875b-4784-9379-0c2ac52fcf65.jpg" alt="地图模式截图" /></p>
<p>回想一下你是如何找照片的，除非你之前已经将它标星或者放进指定的相册里。一般的思路都是回忆大致的拍摄时间，或者当天发生了哪些事。</p>
<p>但其实，<strong>这张照片在哪里拍的，这一条线索会比什么时候拍的更清晰。</strong></p>
<p>因此这一条是小剧挑选相册服务的硬性指标。</p>
<h3 id="35">3.5、关于备份</h3>
<p>Mac mini 上小剧是使用 Carbon Copy Cloner 这款付费软件实现备份的。</p>
<p>但它的备份逻辑是基于文件系统的，也就是本机硬盘，或者局域网内的 SMB 之类的文件共享。</p>
<p>基于 321 备份原则，小剧在家中的 H3C m1实现了第一套备份，还缺一份远程备份。</p>
<p>尝试过很多种把网盘、对象存储桶挂在本地的方案。DEMO 能跑通，但时不时的会掉链子，稳定性不行，运维成本很高。</p>
<p>飞牛的备份逻辑相对简单得多，本地文件系统、局域网共享、网盘挂载、FTP 都支持，并且很稳定。</p>
<p>借助于飞牛 OS，小剧的 321 备份原则的最后一块拼图"1"，在这一刻算是稳定的凑齐了。</p>
<h2 id="macmini">四、Mac mini 的归宿？</h2>
<p>在 2024 年，小剧曾经写过一篇《家庭服务器改造记录》的文章。记录了 Mac mini 进入小剧家并承担主服务器的经历。</p>
<p>文末的一句话放在这里仍然适用。</p>
<blockquote>
  <p>不给自己生造需求，也不假设自己几乎用不到的峰值性能。随着自己的使用循序渐进，进行设备的更替，让属于自己的方案陪着自己去成长。</p>
</blockquote>
<p>因为飞牛 OS 的到来，Mac mini 的任务越来越轻了。自从六月开始，Mac mini 就一直处于关机状态了。</p>
<p>外置硬盘盒 + SSD 以原价的一半，在公司二手群出手了。Mac mini 在几天前，也转送给好友了。</p>
<p><strong>Mac mini 被拆掉的痕迹。</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/mac-mini-feiniu-os/40E7-9257-B5-0.jpg" alt="Mac mini 被拆掉的痕迹。" /></p>
<p>这一年 Mac mini 替小剧完成了很多基础工作和新的尝试。</p>
<p>希望它在新的主人手里继续发光发热。（好像用发热这个词并不合适🤩）</p>
<hr />
<p>对了，Mac mini 的新主人是一位自媒体博主。专注于探索人工智能和 3D 打印在普通人身上的可能性。</p>
<p>分享的内容干货与趣味共存，可以去这里给他来个大大的关注：</p>
<p>小红书：<strong><a href="https://www.xiaohongshu.com/user/profile/5d08d0ab000000001003a65c">手搓工程部</a></strong></p>
<p>抖音：<strong><a href="https://www.douyin.com/user/MS4wLjABAAAAIym77h9eoJvkkyi3HFhsmeZ9k3KIbIrV-BHTYA-Rm5w">手搓工程部</a></strong></p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Mon, 07 Jul 2025 13:34:47 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/2d38u0j7q63</guid>
      <category>家庭服务器</category>
      <category>飞牛</category>
      <category>NAS</category>
      <enclosure url="http://static.bh-lay.com/blog/2025/mac-mini-feiniu-os/2eccf1ca-5942-4448-9906-560f522a9391.jpg" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>飞牛 OS 自 2024 下半年发布，至今不到一年的时间。作为一个国产的 NAS OS，较高的完成度、免费、高频的更新，这三点让小剧很是敬佩。</p>
<p>小剧使用 Mac mini 做家庭服务器已经两年多了，这台 8 + 256 的丐版小机器在小剧的调教之下也越来越顺手。</p>
<h2 id="">一、为什么要加装飞牛？</h2>
<h3 id="11">1.1、尝鲜</h3>
<p>正如前面提到的，飞牛 OS 作为一款 NAS 操作系统，功能完成度极高，并且体验很好。</p>
<p>在一众 UP 主的欢呼声感染之后，小剧也想实际体验一下究竟有多丝滑。</p>
<h3 id="12macos">1.2、寻找 MacOS 的替代平台</h3>
<p>小剧使用 MacOS 的 Mac mini 做家庭服务器，虽然没有出过大的问题，但是两次事故都让小剧心有余悸。</p>
<p>一次是家里意外断电，导致 Docker 里的容器丢失，最严重的是 Docker 系统 Volumes 丢失。关于这次故障记录在<a href="https://bh-lay.com/blog/1sidzrps0yb">《家庭服务器断电处理记录》</a>。</p>
<p>另一次是 MacOS 升级，导致系统默认 Shell 由 Bash 变为 zsh，Docker Desktop 部分功能需要更改配置才能正常使用。小剧也是发现 Immich 容器无法启动才定位到这个问题。</p>
<p>这两件事看起来不相同，根本原因是一样的。MacOS 的 Docker 是借助于虚拟机实现的，很多我们对 Docker 的理解需要 Docker  Desktop 创建的虚拟机来抹平差异。</p>
<p>飞牛基于 Debian 内核，对 Docker 的支持更直接。</p>
<h3 id="13">1.3、寻找更易用的方案</h3>
<p>经过最近两年多对 Mac mini 的使用，小剧整理出个人最常用的功能有以下几个：</p>
<p><strong>文件共享</strong></p>
<p>小剧使用 MacOS 的 SMB 协议共享文件，在 PC 上体验还不错，但在手机上得借助于第三方 APP 实现访问，易用性较差。</p>
<p><strong>照片备份、查看</strong></p>
<p>小剧一直使用 Immich 做相册管理，多端支持且体验很好。</p>
<p><strong>文档管理</strong></p>
<p>小剧使用 outline 做家庭文档管理，包括你在阅读的这篇博文。以及最近半年的文章都是借助于 outline 书写的。<a href="https://bh-lay.com/blog/2oevmzytwwc">《Outline Docs 初体验》</a>介绍了小剧初次接触 outline 的兴奋劲儿。</p>
<p><strong>影视观看</strong></p>
<p>小剧对于影视并没有太多执念，也没有屯片儿的爱好。创建 Jellyfin 也只是为了<strong>让娃看动画片时，少一些不合时宜，甚至少儿不宜的广告</strong>。另外也能给给媳妇在冷门平台追剧时少付点会员费。</p>
<p><strong>文件备份</strong></p>
<p>数年前不知道从哪个渠道，斥巨资购买了 CCC 备份软件，用于实现 Mac mini 中的文件备份。</p>
<h2 id="-1">二、为飞牛塑造肉身</h2>
<p>如果你有仔细审视过个人的数据，抛开影音、软件、游戏之外，纯个人的数据其实是不多的。</p>
<p>个人文件最大比例，是常见的照片视频。其余各类文档性质的文件，数量庞多且细碎，但总体积并不会很大。</p>
<p>从小剧最近两年的经验来看，1T 是目前个人（家庭）数据的总量上限。预留 2T 的空间足够家庭接下来三到五年的使用了。</p>
<h3 id="21">2.1、硬件选择</h3>
<p>相信你在<strong>"B 站职业技术学院"</strong>进修过很多次，数不清的大佬教你用一百种方法组装 NAS。有追求大容量的超级多盘位，有追求极致性价比的捡垃圾，还有恰饭博主的各种成品推荐。</p>
<p>对于小剧来说，对硬件没有太多的追求，只要能满足：体积小巧、容量够用、低噪音、不夸张的功耗、合理的局域网使用速度，也就足够了。</p>
<p>经过一番挑选，小剧选择了极摩客 G3 Plus 作为飞牛 NAS 的硬件基础。</p>
<p><strong>极摩客 G3 Plus 照片</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/mac-mini-feiniu-os/7558a1de-2d2f-4f25-961d-7d18ccb25f8c.jpg" alt="极摩客 G3 Plus 照片" /></p>
<p>这是小剧购买的第二台极摩客的设备，做工和颜值还是挺讨喜的。</p>
<p>不横向对比其他的选项了，单纯介绍下选择这一款的主要原因：</p>
<ul>
<li>体积小巧，结构紧凑，易部署</li>
<li>N150 的处理器，带核显，功耗低、性能尚可</li>
<li>最大支持 16G 内存</li>
<li>支持两根 SSD 硬盘</li>
<li>内置 2.5G 网卡</li>
<li>价格便宜（这条是刚需）</li>
</ul>
<h3 id="22">2.2、主机配件成本</h3>
<p>极摩客 G3 Plus 准系统版本是没办法开箱即用的，需要加装一根内存条、至少一根 SSD 才可以使用。</p>
<p>以下是这次购入的全部零配件。</p>
<p>极摩客 G3 plus，566 元。</p>
<p>16G 内存，136 元。</p>
<p>购入了一根 512G 2242 规格的 SSD 做系统盘，205.6 元。</p>
<p>一根 2T 2280 规格的 SSD 做数据存储盘，760 元。</p>
<p>另外为了利用 2.5G 网口的性能，升级了即将成为瓶颈的千兆交换机，296 元。</p>
<p><strong>主机 + 配件清单截图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/mac-mini-feiniu-os/259d47d7-95dd-4163-8a6a-1f409bebc914.jpg" alt="主机 + 配件清单截图" /></p>
<h3 id="23">2.3、散热处理</h3>
<p>这里又是小剧日常矫情的环节了。</p>
<p>设备调试期间，小剧发现整个主机最热的部分是 2280 的那根 SSD。待机约 55℃，轻度使用就会超过 65℃。</p>
<p>按照官方的介绍，SN580 这款硬盘在 0-85℃ 之间都是能正常工作的。</p>
<p>所以这就是小剧矫情的地方了，非要想办法把 SSD 的待机温度控制在 50℃ 以下。</p>
<p>小剧尝试了硬盘赠送的散热马甲、加装带风扇的散热马甲、加装侧吹风扇。这些方案都能显著降低温度1-5℃，但长时间使用后都会慢慢"囤积"温度消散不去。</p>
<p>最终小剧还是买了水冷散热的配件，把极摩客小主机的天灵盖开了两个孔。</p>
<p>最终温度控制在待机 40-45℃，重度使用 55℃。</p>
<p>极摩客天灵盖开颅照片</p>
<p><img src="https://static.bh-lay.com/blog/2025/mac-mini-feiniu-os/ee3611d6-4f4b-4755-8f3d-69e99a32bd81.jpg" alt="极摩客天灵盖开颅照片" /></p>
<h2 id="-2">三、实际上手体验</h2>
<p>飞牛 OS 的安装、初始化还是很丝滑的，支持可视化安装。这一步没有太多可吹的地方，毕竟一台设备基本上只会安装一次。即使步骤繁琐也不会每天困扰到你。</p>
<p>不过小剧还是在这步遇到了灵异事件，安装完成后启动系统，始终会报 GRUB 错误，重装三遍都是如此。</p>
<p>最后小剧重新从官网下载镜像，重新安装再启动，竟然奇迹般的好了。</p>
<h3 id="31">3.1、内网速度</h3>
<p>因为小剧家里只有飞牛 OS、交换机支持 2.5G 网速，因此任何一台设备使用飞牛的时候都跑不满 2.5G 的带宽。</p>
<p>但这并不能说明 2.5G 没有意义，在多台设备同时访问飞牛时，飞牛提供高于单台设备的吞吐上限可以缓解并行的压力。</p>
<p>在从 Mac mini 迁移数据到飞牛 OS 的时候，跑满千兆带宽的同时还能给其他设备提供服务。</p>
<p>在每日定时执行跨设备备份任务的时候，体验也不会打折扣。</p>
<p><strong>千兆网速截图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/mac-mini-feiniu-os/9e7c21e4-c655-4a28-94c9-7d1cec4935fd.jpg" alt="千兆网速截图" /></p>
<h3 id="32">3.2、外网速度</h3>
<p>很多飞牛的小伙伴苦于中继服务较低的带宽，在家庭以外很难顺畅的使用 NAS。飞牛 OS 提供免费的中继服务，我们除了感恩戴德并不能埋怨什么。</p>
<p>根本原因还是我们的家庭网络环境较为简陋，导致免费且高速的方案没办法使用。</p>
<p>好在小剧很多年前申请到过公网 IPV4，至今可用。安徽电信的公网 IPV6 也能以很低的成本获得。</p>
<p>因此在外网使用时，是很容易达到直连效果的。给娃看 NAS 里的动画片还是不在话下的。</p>
<h3 id="33">3.3、文件管理逻辑</h3>
<p>之前使用 Mac mini 管理文件，是没有多用户的概念的，或者想实现多用户是的很麻烦的。</p>
<p>飞牛的多用户天然支持就很好，而且在系统级别屏蔽了较深的文件路径前缀。</p>
<p>也可以借助于共享功能在用户之间（其实也就是我媳妇和我妈妈）共享部分数据。</p>
<p>相互隔离也能相互融合。</p>
<p>当然，作为管理和维护飞牛的小剧来说，其实是开了上帝视角的。毕竟我有整台机器的全部权限。</p>
<h3 id="34">3.4、飞牛相册</h3>
<p>小剧使用 Immich 做家庭相册已经一年多了。伴随着 Immich 一次次的升级，个人的很多相册相关的数据都沉淀在这里。</p>
<p><del>小剧尝试拷贝了 2018 全年的照片做测试，发现飞牛 OS 的相册目前还不足以迁移。</del></p>
<p><del>原因有很多，主要原因是不支持地图模式。其次是相册管理的交互不够细腻，例如管理人脸识别数据时很多界面不支持搜索等交互细节。</del></p>
<p>上面删掉的部分是<strong>五月份</strong>写好的内容。</p>
<p>小剧拖沓的惰性导致文章被延期到七月才发布。</p>
<p>在这两个月的时间内，飞牛 OS 推出了数个更新包。不仅包含了相册地图模式，还为宝爸宝妈开发了宝宝相册。</p>
<p>六月初小剧开始逐渐把相册往 immich 和飞牛两个平台分别备份。经过了一个月的体验，飞牛 OS 在相册体验上的更新还是很多的。在六月底小剧正式下线了 immich 服务。</p>
<p>并不是 Immich 不够好，它的体验和功能完整性依然是绝对优于飞牛相册的。但是作为家庭服务的管理者，小剧的抉择需要在体验和运维成本中找平衡。飞牛相册目前的表现足以吸引小剧进行迁移。</p>
<p>为什么地图模式这么重要？</p>
<p><strong>地图模式截图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/mac-mini-feiniu-os/510a0b0b-875b-4784-9379-0c2ac52fcf65.jpg" alt="地图模式截图" /></p>
<p>回想一下你是如何找照片的，除非你之前已经将它标星或者放进指定的相册里。一般的思路都是回忆大致的拍摄时间，或者当天发生了哪些事。</p>
<p>但其实，<strong>这张照片在哪里拍的，这一条线索会比什么时候拍的更清晰。</strong></p>
<p>因此这一条是小剧挑选相册服务的硬性指标。</p>
<h3 id="35">3.5、关于备份</h3>
<p>Mac mini 上小剧是使用 Carbon Copy Cloner 这款付费软件实现备份的。</p>
<p>但它的备份逻辑是基于文件系统的，也就是本机硬盘，或者局域网内的 SMB 之类的文件共享。</p>
<p>基于 321 备份原则，小剧在家中的 H3C m1实现了第一套备份，还缺一份远程备份。</p>
<p>尝试过很多种把网盘、对象存储桶挂在本地的方案。DEMO 能跑通，但时不时的会掉链子，稳定性不行，运维成本很高。</p>
<p>飞牛的备份逻辑相对简单得多，本地文件系统、局域网共享、网盘挂载、FTP 都支持，并且很稳定。</p>
<p>借助于飞牛 OS，小剧的 321 备份原则的最后一块拼图"1"，在这一刻算是稳定的凑齐了。</p>
<h2 id="macmini">四、Mac mini 的归宿？</h2>
<p>在 2024 年，小剧曾经写过一篇《家庭服务器改造记录》的文章。记录了 Mac mini 进入小剧家并承担主服务器的经历。</p>
<p>文末的一句话放在这里仍然适用。</p>
<blockquote>
  <p>不给自己生造需求，也不假设自己几乎用不到的峰值性能。随着自己的使用循序渐进，进行设备的更替，让属于自己的方案陪着自己去成长。</p>
</blockquote>
<p>因为飞牛 OS 的到来，Mac mini 的任务越来越轻了。自从六月开始，Mac mini 就一直处于关机状态了。</p>
<p>外置硬盘盒 + SSD 以原价的一半，在公司二手群出手了。Mac mini 在几天前，也转送给好友了。</p>
<p><strong>Mac mini 被拆掉的痕迹。</strong></p>
<p><img src="https://static.bh-lay.com/blog/2025/mac-mini-feiniu-os/40E7-9257-B5-0.jpg" alt="Mac mini 被拆掉的痕迹。" /></p>
<p>这一年 Mac mini 替小剧完成了很多基础工作和新的尝试。</p>
<p>希望它在新的主人手里继续发光发热。（好像用发热这个词并不合适🤩）</p>
<hr />
<p>对了，Mac mini 的新主人是一位自媒体博主。专注于探索人工智能和 3D 打印在普通人身上的可能性。</p>
<p>分享的内容干货与趣味共存，可以去这里给他来个大大的关注：</p>
<p>小红书：<strong><a href="https://www.xiaohongshu.com/user/profile/5d08d0ab000000001003a65c">手搓工程部</a></strong></p>
<p>抖音：<strong><a href="https://www.douyin.com/user/MS4wLjABAAAAIym77h9eoJvkkyi3HFhsmeZ9k3KIbIrV-BHTYA-Rm5w">手搓工程部</a></strong></p>]]></content:encoded>
    </item>
    <item>
      <title>局域网公网 DNS 融合，居家外出畅通无阻</title>
      <link>https://bh-lay.com/blog/2aj2vwa2qvb</link>
      <description><![CDATA[<p><img src="https://static.bh-lay.com/blog/2025/home-dns/cover.jpg" alt="局域网公网 DNS 融合，居家外出畅通无阻" /></p>
<p>可能你看到这个标题有些懵逼，小剧这是又在整个什么花活？</p>
<h2 id="">一 、背景</h2>
<p>当你在部署服务时，一般都是部署在内网环境中。比如在公司、学校、家庭环境中。部署一些文件管理、OA系统、各种登记报名请假之类的 Web 服务。</p>
<p>这种场景下的服务，用户群体集中且数量有限，也很容易管理，所以易用性并不是优先级非常高的需求。</p>
<p>在 Web 规模较小时，服务部署之后直接将 IP + 端口告诉大家，也就完成了内网服务的搭建。</p>
<p>考虑到在外网访问需求，出于安全和部署的复杂度的考虑。一般的套路是强制大家使用 VPN 连接内网，接下来就和内网保持相同的访问路径就完事了。</p>
<h3 id="11">1.1、直接公网可访问不行么？</h3>
<p>当然可以，比如请假系统，当在外遭遇突发情况时，需要借助于公共电脑访问内网，发起请假流程。</p>
<p>这时安全性较高的 VPN 很难或无法顺利配置成功。</p>
<p>又或者在时间上或人员上，有很大的比例需要在局域网以外访问内网服务。比如学生的寒暑假，公司的居家办公。</p>
<p>每次的 VPN 认证就显得格外繁琐。</p>
<p>除非保密等级极高，现在主流的服务部署都是尽力让使用者不区分内外网，用同一套网址保持云端使用体验的方案。</p>
<h3 id="12">1.2、小剧为什么讨论这些？</h3>
<p>前面说的有些发散了，只是为了方便大家理解而写的引子。</p>
<p>小剧遇到的和要解决的问题，其实仅限于家庭内网服务，正如标题提到的<strong>"局域网公网DNS 融合，居家外出畅通无阻"。</strong></p>
<p>相信你也了解，小剧近些年在捣鼓家庭服务搭建。没有搞出什么大的名堂，但是也有一些心得体会想和大家分享。</p>
<p>今天分享下，关于家庭服务网络搭建的经验。</p>
<p>家庭服务硬件照片</p>
<p><img src="https://static.bh-lay.com/blog/2025/home-dns/ce43dfc1-d86c-47b4-a4e8-0d8ec73c9b71.jpg" alt="家庭服务硬件照片" /></p>
<h2 id="-1">二、小剧的家庭网络结构有过哪些阶段？</h2>
<h3 id="21v10">2.1、家庭服务萌芽 v1.0</h3>
<p>最早小剧在家中部署了 SMB 文件共享服务、Immich 照片管理服务。</p>
<p>这两个服务几乎涵盖了绝大多数个人文件管理的范畴。使用了一段时间后便给媳妇手机也配置上了。</p>
<p>内网速度贼快，但服务也只有在内网才能访问，使用起来不方便，媳妇并不怎么用它。</p>
<h3 id="22v20">2.2、支持外网隧道回家 v2.0</h3>
<p>正如前文引子里提到的，内网 VPN 的方案被提上了日程。</p>
<p>小剧斥巨资（闲鱼79元）购入了蒲公英设备，用于实现身处公网环境时，一键回到家庭内网中。</p>
<p>蒲公英小盒子</p>
<p><img src="https://static.bh-lay.com/blog/2025/home-dns/2dff3e0a-e0b3-4413-9dca-a75df99d014e.jpg" alt="蒲公英小盒子" /></p>
<p>蒲公英免费版有设备限制，当 P2P 不成功的时候提供的带宽也不高，但访问速度足以应付一般情况下的使用了。</p>
<p>看起来一切问题都解决了，简单给媳妇做了"培训"后，家庭网络 2.0 版本就算完工了。</p>
<p>经过一个月的实际使用，小剧经常开启 Immich 的时候忘记登录蒲公英，转了半天圈加载失败，还得重新登录蒲公英，再回到 Immich 中访问。</p>
<p>又或者登陆了蒲公英后忘记关掉，导致回到家中手机显示网络异常。</p>
<p>蒲公英 APP 截图</p>
<p><img src="https://static.bh-lay.com/blog/2025/home-dns/beaebc05-405b-4572-ba73-f0eca24ff2bf.jpg" alt="蒲公英 APP 截图" /></p>
<p>看起来使用前登录蒲公英，使用后退出蒲公英这么一个小小的动作，实际使用中也会带来不小的困扰。</p>
<p>小剧都是如此，想必媳妇更不习惯如此"繁琐"的使用方式。</p>
<p>还没等小剧去做"用户使用调研"，就发现媳妇早就已经把蒲公英卸载了。</p>
<h3 id="23v30">2.3、支持公网直接访问 v3.0</h3>
<p>相信这一步你比小剧更熟，想让一个内网服务在公网可访问，方案简直不要太多。</p>
<p>有前面提到的蒲公英设备可以搭建，也有 zerotier 之类的免费服务可用，但这些都不算"直接访问"。</p>
<p>能称得上"直接访问"的，一般有以下三个方案。</p>
<ul>
<li>家庭公网 IPV4 + DDNS + 端口转发方案</li>
<li>家庭公网 IPV6 + DDNS 方案</li>
<li>服务器中转方案</li>
</ul>
<p>对于很多小伙伴来说，家庭公网 IPV4 是遥不可及的梦。幸运的是小剧很多年前因为机缘巧合申请到过，至今可用。</p>
<p>但出于<strong>"安全与合规"</strong>的考虑，小剧并没有使用这个方案。</p>
<p>安徽电信还是很给力的，公网 IPV6 无需申请就可以得到，不过使用它还得放弃一点点的安全性。需要额外关闭路由器的 IPV6 防火墙才可以顺利使用。</p>
<p>至于服务器中转这条路，对于个人站博主的小剧来说，简直是零成本。阿里云 99 包年的服务器可太适合干这件事了。</p>
<p>公网直接访问的方案好么？</p>
<p>确实，不管用上面哪种方式，身处于外网时"直接访问"家庭内网服务的需求是达到了。</p>
<p>但仔细想想依然不够。</p>
<p>因为在家时使用的是形如 <code>192.168.xxx.xxx</code> 的内网地址，在外网时使用的是公网 IPV4 或 IPV6 解析后的域名。</p>
<p>绝大多数应用或者 Web 都需要手动切换网址，还是非常麻烦。</p>
<p>虽然有少数 APP 支持配置内外网地址，APP 根据连接的 Wi-Fi 自行切换网络。但前提是你得允许 APP 始终获取设备位置的权限。好用但不优雅，还费电。</p>
<p>Immich 网络配置截图</p>
<p><img src="https://static.bh-lay.com/blog/2025/home-dns/a0146e5d-d891-4d12-bdad-cf8e008c9210.jpg" alt="Immich 网络配置截图" /></p>
<h3 id="24dnsv40">2.4、基于 DNS 的无缝访问方案 v4.0</h3>
<p>v3.0 的方案虽然没有达到给媳妇用的标准，但在网络配置层面已经有了质的飞跃。</p>
<p>再往前进一步就是我们今天的主题了。</p>
<p>这部分涉及到的内容较多，具体的实现下面展开聊。</p>
<h2 id="-2">三、如何实现内外网无缝访问？</h2>
<p>正如标题提到的 <strong>"局域网公网 DNS 融合，居家外出畅通无阻"。</strong></p>
<p>总体思路就是：同一个域名在局域网和公网，借助于 DNS 分别配置不同的解析。让使用者从外面回到家里，或者相反的时候，都能够无感切换服务地址。</p>
<p>我知道你现在肯定有顾虑：DNS 会有缓存的，内外网切换的过程，有问题么？</p>
<p>确实，这也是小剧早期的担忧，测试后发现完全是多虑了。</p>
<p>当然，要实现这一套切换流程，以下几个关键环节必不可少。</p>
<p>局域网 &amp; 公网访问拓扑图</p>
<p><img src="https://static.bh-lay.com/blog/2025/home-dns/38f03357-a8d3-49b5-af5a-28a4ed53719e.png" alt="局域网 &amp; 公网访问拓扑图" /></p>
<h3 id="31">3.1、配置公网解析</h3>
<p>前面已经介绍了三个支持公网"直接访问"的方案。这里只提下思路和小剧的使用情况，具体教程可以去 B 站搜索，有很多手把手教你实现的视频。</p>
<p><strong>公网 IPV4 + DDNS + 端口转发方案</strong></p>
<ul>
<li>在家庭使用这个场景下，出于<strong>"安全与合规"</strong>的考虑，小剧并未使用，不展开聊了。</li>
</ul>
<p><strong>公网 IPV6 + DDNS 方案</strong></p>
<ul>
<li>这套方案成本最低，只有域名租赁成本，只要家中能开启 IPV6 即可使用。</li>
<li>几乎可以理解为点对点通信，带宽瓶颈只在访问设备网速和家庭带宽之间，当然还要受限于链路畅通情况。</li>
<li>因为 DDNS 是动态更新解析，IP 切换过程中会受到各级 DNS 服务缓存的影响，会短暂不可用。好在切换一般发生在夜间，影响不大。</li>
<li>一些餐馆、酒店、机场等公共 Wi-Fi 环境中，是不支持 IPV6 访问的。</li>
<li>好在国内所有运营商的 4G、5G 流量模式，天然支持 IPV6。</li>
</ul>
<p><strong>服务器中转方案</strong></p>
<ul>
<li>这套方案是兼容性最好的，不用管宽带是否支持公网 IPV4、IPV6。</li>
<li>成本比前者略高，除了域名成本，还多了服务器租赁成本，如果服务器是按流量付费，可能花费还会更高。</li>
<li>因为所有流量要经过服务器中转，因而带宽瓶颈除了访问设备和家庭宽带，还多了个服务器带宽上限。</li>
<li>几乎所有云服务提供的都是 IPV4 公网 IP，所以这个方案不受环境限制，任何场所只要能访问互联网，都可以与搭建的服务通联。</li>
</ul>
<h3 id="32">3.2、配置家庭内网反向代理</h3>
<p>无论使用 IPV6 + DDNS 方案，还是服务器中转方案，在公网中都是使用域名或域名 + 端口的访问方式。</p>
<p>而在家中一般是借助于内网 IP + 端口的访问方式。</p>
<p>想要借助 DNS 统一内外网的访问体验，势必需要在内网中也支持域名或域名 + 端口的访问方式。</p>
<p>熟悉服务部署的小伙伴应该知道，这一步需要有 Nginx 之类的反向代理服务，用来根据域名做流量的代理分发。</p>
<p>极摩客小主机</p>
<p><img src="https://static.bh-lay.com/blog/2025/home-dns/41d97f13-5b85-447e-a80f-ddd030c077da.jpg" alt="极摩客小主机" /></p>
<p>反向代理服务可以部署在主服务本机，也可以部署在独立的机器上。</p>
<p>小剧出于设备分离的角度出发，购入了极摩客G5，作为专职的反向代理服务器。</p>
<p>使用 Caddy 作为反向代理服务。</p>
<h3 id="33dns">3.3、配置家庭内网DNS</h3>
<p>有了反向代理服务还不够，局域网内还得支持把特定域名的请求指向到反向代理服务器上，才能完成整个网络的闭环。</p>
<p>我们以访问 immich.sample.com 为例，反向代理服务器 IP 为 192.168.2.10，immich 服务 IP 为 192.168.2.11 端口 8080。</p>
<p>手机电脑之类的设备 → 访问 https://immich.sample.com → 局域网 DNS 解析域名至 192.168.2.10 反向代理服务器 → 反向代理服务根据域名反向代理至 192.168.2.11:8080</p>
<p>这一步就是全文的关键，<strong>如何在内网通过 DNS，将具体的域名指向到特定的服务？</strong></p>
<p>这一步其实方案有很多，如果你们家在使用软路由做主路由，有很多插件可以完成。</p>
<p>还可以搭建了一个局域网 DNS 服务，用更灵活的自定义局域网 DNS 的方法来实现。</p>
<p>最开始小剧用的就是这个方法，但在跑了一段时间才发现，家里在用的小米路由器竟然支持自定义 Hosts，来实现域名的自定义解析。</p>
<p>小米路由器自定义 Hosts 截图</p>
<p><img src="https://static.bh-lay.com/blog/2025/home-dns/cb9de817-5a2f-408a-a01e-753fdea0ccd0.jpg" alt="小米路由器自定义 Hosts 截图" /></p>
<p>因为小剧需要自定义的域名并不多，而且没有动态逻辑，小米路由器自带的自定义 Hosts 足够小剧用了。</p>
<p>借助于小米路由器的自定义 Hosts 功能，Hack 局域网 DNS 的功能就算完成了。</p>
<h3 id="34">3.4、同步服务器证书</h3>
<p>如果你有过服务部署的经验，应该知道在互联网上发布服务，HTTPS 是必须的。</p>
<p>近些年能够免费申请到的 SSL 证书，有效期也越来越短了，并且申请过程也需要各种验证，很是麻烦。</p>
<p>有一些自动脚本可以辅助我们自动签发证书。但前提是 SSL 签名的服务商，需要能够通过公网中域名解析的通道，借助于 80 之类的端口，验证你对这个域名是否拥有控制权限。</p>
<p>局域网 &amp; 公网访问拓扑图</p>
<p><img src="https://static.bh-lay.com/blog/2025/home-dns/38f03357-a8d3-49b5-af5a-28a4ed53719e.png" alt="局域网 &amp; 公网访问拓扑图" /></p>
<p>我们再看一遍拓扑图，从图中可以看出来，在公网和局域网中各有一个反向代理服务器。</p>
<p>按照前面的描述，公网中的反向代理服务器是很容易申请到 SSL 证书签名的。身处于局域网中的反向代理服务器则无法顺利的完成证书签名的鉴权流程。</p>
<p>好在本方案要实现的是局域网公网 DNS 融合，服务器中转这条路中，局域网中配置的域名公网中都存在。因而只需要用任意方案将公网的 SSL 证书的自动签发完成，局域网再定时去更新就好了。</p>
<p>小剧使用的是 <a href="https://caddyserver.com/">Caddy</a> 作为反向代理服务器，它将自动化做到了极致。在公网中几乎可以做到零配置签发 SSL 证书，而且是自动续期的。</p>
<p>局域网则需要放弃 Caddy 的证书自动签发逻辑，手动指明证书位置。</p>
<p>公网 Caddyfile 配置</p>
<pre><code class="none language-none">immich.sample.com {
    reverse_proxy 127.0.0.1:22233
}
</code></pre>
<p>局域网 Caddyfile 配置</p>
<pre><code class="none language-none">immich.sample.com {
    tls ./immich.sample.com/immich.sample.com.crt ./immich.sample.com/immich.sample.com.key
    reverse_proxy 192.168.2.6:8096
}
</code></pre>
<p>同步服务端证书并不难，因为 Caddy 存放证书的目录是确定的，且没有经过二次加密。简单的使用scp 命令就能实现跨服务器拉取证书，再辅助使用 crontab 之类的定时脚本会更加方便。</p>
<p>下载服务端证书脚本</p>
<p><code>scp -r user@11.11.11.11:/user/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/* ./</code></p>
<h3 id="35ipv6ssl">3.5、IPV6 如何签发 SSL 证书？</h3>
<p>如果你细心看上面的网络拓扑图，会发现 DDNS + IPV6 方案的流量不经过公网服务器，因此也不会生成对应域名的证书。</p>
<p>家庭局域网也无法签发证书，HTTPS 是个难题。</p>
<p>正当小剧一筹莫展时，一个精妙的思路浮现在小剧的脑海中。</p>
<p>公网的反向代理同样配置一条 IPV6 服务对应域名的解析，使用静态文本做响应。这一步完全是为了自动化获取 SSL 证书，没有其他意义。</p>
<pre><code class="none language-none">ipv6.sample.com {
    respond "Hello This is bh-lay.com"
}
</code></pre>
<p>局域网的反向代理服务用相同的方法获取对应域名的证书。</p>
<p>当身处家庭局域网中倒没什么问题，因为域名会被 Hack 成仅内网 IP 的解析。</p>
<p>而当你处在公网中就变得复杂了，<code>ipv6.sample.com</code> 域名对应的解析，既有公网 IPV4 的地址，同时也有 IPV6 的地址，设备将会如何选择链路？</p>
<p>这部分小剧并没有找到权威的解释，仅从实际测试结果来同步下结论：</p>
<p>IOS、MacOS 在某个域名同时能解析到 IPV4 和 IPV6 的地址时，会优先使用 IPV6 的链路。如果当前网络环境仅支持 IPV4，将会使用 IPV4 的地址。</p>
<p>如此一来刚好满足小剧的需要，IPV6 同样可以用这套方案兼容。</p>
<p>这里还有一个小贴士：家庭 IPV6 用不了 80、443 端口，需要设置形如 8080、8443 的高位端口。</p>
<p><strong>SSL 证书只关心域名，不同端口可以使用同一套证书。</strong></p>
<p>这一点也是最初小剧的顾虑，万幸验证后并不存在。</p>
<h2 id="-3">四、方案好使么？</h2>
<p>目前来说，家庭局域网、公网 DNS 融合的方案已经成为小剧家中的服务部署范式。在最近半年的使用中也没有掉过链子。</p>
<h3 id="41ipv6">4.1、服务器中转 、IPV6 互补</h3>
<p>有了前面的背景知识，应该能看出来服务器中转和 ipv6 有各自的特点。</p>
<p>服务器中转的方案，速度稍慢但非常稳定，天然适合一些常用的、对网络要求较轻的服务。比如笔记、相册服务。</p>
<p>IPV6 速度理论上的上限较高，但使用场景比较局限，某些时候还得借助于手机流量才能访问。</p>
<p>比较适合一些大流量的服务，比如 Jellyfin 观影类的 APP，在外给娃看动画片最合适不过了。</p>
<p>不同类型的服务可以按照特性，去选择合适的方案，实际使用下来相当舒适。</p>
<h3 id="42">4.2、蒲公英的身份蜕变</h3>
<p>在前面 2.2 部分，提到小剧"斥巨资"购入了蒲公英小盒子。看起来后面的服务部署方案和它没有一丁点关系了。</p>
<p>其实不是的，内网中零零碎碎的服务很多，协议也多种多样，绝大多数服务都不太适合，也没有必要部署到互联网中。</p>
<p>蒲公英作为安全性较高的内网穿透方案，很适合给小剧做远程运维的通道。</p>
<p>在外时借助于它可以连接 Mac mini 的远程桌面，使用 SSH 管理 Docker 容器。</p>
<p>偶尔遇到服务故障时也能掏出手机，从容的进行故障恢复。</p>
<p>因此蒲公英成了小剧专属的回家通道。</p>
<h3 id="43">4.3、来自媳妇的无声评价</h3>
<p>DNS 融合的模式，对小剧媳妇来说不需要任何前置操作。需要用某个 APP 的时候直接点开，用完了放到后台或者关掉即可。</p>
<p>目前 Immich、Jellyfin 已经在媳妇手机里成为常客，Outline 文档 APP 虽然不怎么用，但是也一直没有删掉。</p>
<p>也算是对小剧"瞎折腾"的一种认可吧。</p>
<hr />
<p>最后的提醒：</p>
<p>任何将服务暴露在公网中的行为都是带有风险的，要注意数据安全哦～</p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Thu, 03 Apr 2025 15:49:20 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/2aj2vwa2qvb</guid>
      <category>家庭服务器</category>
      <category>DNS</category>
      <enclosure url="http://static.bh-lay.com/blog/2025/home-dns/cover.jpg" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p><img src="https://static.bh-lay.com/blog/2025/home-dns/cover.jpg" alt="局域网公网 DNS 融合，居家外出畅通无阻" /></p>
<p>可能你看到这个标题有些懵逼，小剧这是又在整个什么花活？</p>
<h2 id="">一 、背景</h2>
<p>当你在部署服务时，一般都是部署在内网环境中。比如在公司、学校、家庭环境中。部署一些文件管理、OA系统、各种登记报名请假之类的 Web 服务。</p>
<p>这种场景下的服务，用户群体集中且数量有限，也很容易管理，所以易用性并不是优先级非常高的需求。</p>
<p>在 Web 规模较小时，服务部署之后直接将 IP + 端口告诉大家，也就完成了内网服务的搭建。</p>
<p>考虑到在外网访问需求，出于安全和部署的复杂度的考虑。一般的套路是强制大家使用 VPN 连接内网，接下来就和内网保持相同的访问路径就完事了。</p>
<h3 id="11">1.1、直接公网可访问不行么？</h3>
<p>当然可以，比如请假系统，当在外遭遇突发情况时，需要借助于公共电脑访问内网，发起请假流程。</p>
<p>这时安全性较高的 VPN 很难或无法顺利配置成功。</p>
<p>又或者在时间上或人员上，有很大的比例需要在局域网以外访问内网服务。比如学生的寒暑假，公司的居家办公。</p>
<p>每次的 VPN 认证就显得格外繁琐。</p>
<p>除非保密等级极高，现在主流的服务部署都是尽力让使用者不区分内外网，用同一套网址保持云端使用体验的方案。</p>
<h3 id="12">1.2、小剧为什么讨论这些？</h3>
<p>前面说的有些发散了，只是为了方便大家理解而写的引子。</p>
<p>小剧遇到的和要解决的问题，其实仅限于家庭内网服务，正如标题提到的<strong>"局域网公网DNS 融合，居家外出畅通无阻"。</strong></p>
<p>相信你也了解，小剧近些年在捣鼓家庭服务搭建。没有搞出什么大的名堂，但是也有一些心得体会想和大家分享。</p>
<p>今天分享下，关于家庭服务网络搭建的经验。</p>
<p>家庭服务硬件照片</p>
<p><img src="https://static.bh-lay.com/blog/2025/home-dns/ce43dfc1-d86c-47b4-a4e8-0d8ec73c9b71.jpg" alt="家庭服务硬件照片" /></p>
<h2 id="-1">二、小剧的家庭网络结构有过哪些阶段？</h2>
<h3 id="21v10">2.1、家庭服务萌芽 v1.0</h3>
<p>最早小剧在家中部署了 SMB 文件共享服务、Immich 照片管理服务。</p>
<p>这两个服务几乎涵盖了绝大多数个人文件管理的范畴。使用了一段时间后便给媳妇手机也配置上了。</p>
<p>内网速度贼快，但服务也只有在内网才能访问，使用起来不方便，媳妇并不怎么用它。</p>
<h3 id="22v20">2.2、支持外网隧道回家 v2.0</h3>
<p>正如前文引子里提到的，内网 VPN 的方案被提上了日程。</p>
<p>小剧斥巨资（闲鱼79元）购入了蒲公英设备，用于实现身处公网环境时，一键回到家庭内网中。</p>
<p>蒲公英小盒子</p>
<p><img src="https://static.bh-lay.com/blog/2025/home-dns/2dff3e0a-e0b3-4413-9dca-a75df99d014e.jpg" alt="蒲公英小盒子" /></p>
<p>蒲公英免费版有设备限制，当 P2P 不成功的时候提供的带宽也不高，但访问速度足以应付一般情况下的使用了。</p>
<p>看起来一切问题都解决了，简单给媳妇做了"培训"后，家庭网络 2.0 版本就算完工了。</p>
<p>经过一个月的实际使用，小剧经常开启 Immich 的时候忘记登录蒲公英，转了半天圈加载失败，还得重新登录蒲公英，再回到 Immich 中访问。</p>
<p>又或者登陆了蒲公英后忘记关掉，导致回到家中手机显示网络异常。</p>
<p>蒲公英 APP 截图</p>
<p><img src="https://static.bh-lay.com/blog/2025/home-dns/beaebc05-405b-4572-ba73-f0eca24ff2bf.jpg" alt="蒲公英 APP 截图" /></p>
<p>看起来使用前登录蒲公英，使用后退出蒲公英这么一个小小的动作，实际使用中也会带来不小的困扰。</p>
<p>小剧都是如此，想必媳妇更不习惯如此"繁琐"的使用方式。</p>
<p>还没等小剧去做"用户使用调研"，就发现媳妇早就已经把蒲公英卸载了。</p>
<h3 id="23v30">2.3、支持公网直接访问 v3.0</h3>
<p>相信这一步你比小剧更熟，想让一个内网服务在公网可访问，方案简直不要太多。</p>
<p>有前面提到的蒲公英设备可以搭建，也有 zerotier 之类的免费服务可用，但这些都不算"直接访问"。</p>
<p>能称得上"直接访问"的，一般有以下三个方案。</p>
<ul>
<li>家庭公网 IPV4 + DDNS + 端口转发方案</li>
<li>家庭公网 IPV6 + DDNS 方案</li>
<li>服务器中转方案</li>
</ul>
<p>对于很多小伙伴来说，家庭公网 IPV4 是遥不可及的梦。幸运的是小剧很多年前因为机缘巧合申请到过，至今可用。</p>
<p>但出于<strong>"安全与合规"</strong>的考虑，小剧并没有使用这个方案。</p>
<p>安徽电信还是很给力的，公网 IPV6 无需申请就可以得到，不过使用它还得放弃一点点的安全性。需要额外关闭路由器的 IPV6 防火墙才可以顺利使用。</p>
<p>至于服务器中转这条路，对于个人站博主的小剧来说，简直是零成本。阿里云 99 包年的服务器可太适合干这件事了。</p>
<p>公网直接访问的方案好么？</p>
<p>确实，不管用上面哪种方式，身处于外网时"直接访问"家庭内网服务的需求是达到了。</p>
<p>但仔细想想依然不够。</p>
<p>因为在家时使用的是形如 <code>192.168.xxx.xxx</code> 的内网地址，在外网时使用的是公网 IPV4 或 IPV6 解析后的域名。</p>
<p>绝大多数应用或者 Web 都需要手动切换网址，还是非常麻烦。</p>
<p>虽然有少数 APP 支持配置内外网地址，APP 根据连接的 Wi-Fi 自行切换网络。但前提是你得允许 APP 始终获取设备位置的权限。好用但不优雅，还费电。</p>
<p>Immich 网络配置截图</p>
<p><img src="https://static.bh-lay.com/blog/2025/home-dns/a0146e5d-d891-4d12-bdad-cf8e008c9210.jpg" alt="Immich 网络配置截图" /></p>
<h3 id="24dnsv40">2.4、基于 DNS 的无缝访问方案 v4.0</h3>
<p>v3.0 的方案虽然没有达到给媳妇用的标准，但在网络配置层面已经有了质的飞跃。</p>
<p>再往前进一步就是我们今天的主题了。</p>
<p>这部分涉及到的内容较多，具体的实现下面展开聊。</p>
<h2 id="-2">三、如何实现内外网无缝访问？</h2>
<p>正如标题提到的 <strong>"局域网公网 DNS 融合，居家外出畅通无阻"。</strong></p>
<p>总体思路就是：同一个域名在局域网和公网，借助于 DNS 分别配置不同的解析。让使用者从外面回到家里，或者相反的时候，都能够无感切换服务地址。</p>
<p>我知道你现在肯定有顾虑：DNS 会有缓存的，内外网切换的过程，有问题么？</p>
<p>确实，这也是小剧早期的担忧，测试后发现完全是多虑了。</p>
<p>当然，要实现这一套切换流程，以下几个关键环节必不可少。</p>
<p>局域网 &amp; 公网访问拓扑图</p>
<p><img src="https://static.bh-lay.com/blog/2025/home-dns/38f03357-a8d3-49b5-af5a-28a4ed53719e.png" alt="局域网 &amp; 公网访问拓扑图" /></p>
<h3 id="31">3.1、配置公网解析</h3>
<p>前面已经介绍了三个支持公网"直接访问"的方案。这里只提下思路和小剧的使用情况，具体教程可以去 B 站搜索，有很多手把手教你实现的视频。</p>
<p><strong>公网 IPV4 + DDNS + 端口转发方案</strong></p>
<ul>
<li>在家庭使用这个场景下，出于<strong>"安全与合规"</strong>的考虑，小剧并未使用，不展开聊了。</li>
</ul>
<p><strong>公网 IPV6 + DDNS 方案</strong></p>
<ul>
<li>这套方案成本最低，只有域名租赁成本，只要家中能开启 IPV6 即可使用。</li>
<li>几乎可以理解为点对点通信，带宽瓶颈只在访问设备网速和家庭带宽之间，当然还要受限于链路畅通情况。</li>
<li>因为 DDNS 是动态更新解析，IP 切换过程中会受到各级 DNS 服务缓存的影响，会短暂不可用。好在切换一般发生在夜间，影响不大。</li>
<li>一些餐馆、酒店、机场等公共 Wi-Fi 环境中，是不支持 IPV6 访问的。</li>
<li>好在国内所有运营商的 4G、5G 流量模式，天然支持 IPV6。</li>
</ul>
<p><strong>服务器中转方案</strong></p>
<ul>
<li>这套方案是兼容性最好的，不用管宽带是否支持公网 IPV4、IPV6。</li>
<li>成本比前者略高，除了域名成本，还多了服务器租赁成本，如果服务器是按流量付费，可能花费还会更高。</li>
<li>因为所有流量要经过服务器中转，因而带宽瓶颈除了访问设备和家庭宽带，还多了个服务器带宽上限。</li>
<li>几乎所有云服务提供的都是 IPV4 公网 IP，所以这个方案不受环境限制，任何场所只要能访问互联网，都可以与搭建的服务通联。</li>
</ul>
<h3 id="32">3.2、配置家庭内网反向代理</h3>
<p>无论使用 IPV6 + DDNS 方案，还是服务器中转方案，在公网中都是使用域名或域名 + 端口的访问方式。</p>
<p>而在家中一般是借助于内网 IP + 端口的访问方式。</p>
<p>想要借助 DNS 统一内外网的访问体验，势必需要在内网中也支持域名或域名 + 端口的访问方式。</p>
<p>熟悉服务部署的小伙伴应该知道，这一步需要有 Nginx 之类的反向代理服务，用来根据域名做流量的代理分发。</p>
<p>极摩客小主机</p>
<p><img src="https://static.bh-lay.com/blog/2025/home-dns/41d97f13-5b85-447e-a80f-ddd030c077da.jpg" alt="极摩客小主机" /></p>
<p>反向代理服务可以部署在主服务本机，也可以部署在独立的机器上。</p>
<p>小剧出于设备分离的角度出发，购入了极摩客G5，作为专职的反向代理服务器。</p>
<p>使用 Caddy 作为反向代理服务。</p>
<h3 id="33dns">3.3、配置家庭内网DNS</h3>
<p>有了反向代理服务还不够，局域网内还得支持把特定域名的请求指向到反向代理服务器上，才能完成整个网络的闭环。</p>
<p>我们以访问 immich.sample.com 为例，反向代理服务器 IP 为 192.168.2.10，immich 服务 IP 为 192.168.2.11 端口 8080。</p>
<p>手机电脑之类的设备 → 访问 https://immich.sample.com → 局域网 DNS 解析域名至 192.168.2.10 反向代理服务器 → 反向代理服务根据域名反向代理至 192.168.2.11:8080</p>
<p>这一步就是全文的关键，<strong>如何在内网通过 DNS，将具体的域名指向到特定的服务？</strong></p>
<p>这一步其实方案有很多，如果你们家在使用软路由做主路由，有很多插件可以完成。</p>
<p>还可以搭建了一个局域网 DNS 服务，用更灵活的自定义局域网 DNS 的方法来实现。</p>
<p>最开始小剧用的就是这个方法，但在跑了一段时间才发现，家里在用的小米路由器竟然支持自定义 Hosts，来实现域名的自定义解析。</p>
<p>小米路由器自定义 Hosts 截图</p>
<p><img src="https://static.bh-lay.com/blog/2025/home-dns/cb9de817-5a2f-408a-a01e-753fdea0ccd0.jpg" alt="小米路由器自定义 Hosts 截图" /></p>
<p>因为小剧需要自定义的域名并不多，而且没有动态逻辑，小米路由器自带的自定义 Hosts 足够小剧用了。</p>
<p>借助于小米路由器的自定义 Hosts 功能，Hack 局域网 DNS 的功能就算完成了。</p>
<h3 id="34">3.4、同步服务器证书</h3>
<p>如果你有过服务部署的经验，应该知道在互联网上发布服务，HTTPS 是必须的。</p>
<p>近些年能够免费申请到的 SSL 证书，有效期也越来越短了，并且申请过程也需要各种验证，很是麻烦。</p>
<p>有一些自动脚本可以辅助我们自动签发证书。但前提是 SSL 签名的服务商，需要能够通过公网中域名解析的通道，借助于 80 之类的端口，验证你对这个域名是否拥有控制权限。</p>
<p>局域网 &amp; 公网访问拓扑图</p>
<p><img src="https://static.bh-lay.com/blog/2025/home-dns/38f03357-a8d3-49b5-af5a-28a4ed53719e.png" alt="局域网 &amp; 公网访问拓扑图" /></p>
<p>我们再看一遍拓扑图，从图中可以看出来，在公网和局域网中各有一个反向代理服务器。</p>
<p>按照前面的描述，公网中的反向代理服务器是很容易申请到 SSL 证书签名的。身处于局域网中的反向代理服务器则无法顺利的完成证书签名的鉴权流程。</p>
<p>好在本方案要实现的是局域网公网 DNS 融合，服务器中转这条路中，局域网中配置的域名公网中都存在。因而只需要用任意方案将公网的 SSL 证书的自动签发完成，局域网再定时去更新就好了。</p>
<p>小剧使用的是 <a href="https://caddyserver.com/">Caddy</a> 作为反向代理服务器，它将自动化做到了极致。在公网中几乎可以做到零配置签发 SSL 证书，而且是自动续期的。</p>
<p>局域网则需要放弃 Caddy 的证书自动签发逻辑，手动指明证书位置。</p>
<p>公网 Caddyfile 配置</p>
<pre><code class="none language-none">immich.sample.com {
    reverse_proxy 127.0.0.1:22233
}
</code></pre>
<p>局域网 Caddyfile 配置</p>
<pre><code class="none language-none">immich.sample.com {
    tls ./immich.sample.com/immich.sample.com.crt ./immich.sample.com/immich.sample.com.key
    reverse_proxy 192.168.2.6:8096
}
</code></pre>
<p>同步服务端证书并不难，因为 Caddy 存放证书的目录是确定的，且没有经过二次加密。简单的使用scp 命令就能实现跨服务器拉取证书，再辅助使用 crontab 之类的定时脚本会更加方便。</p>
<p>下载服务端证书脚本</p>
<p><code>scp -r user@11.11.11.11:/user/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/* ./</code></p>
<h3 id="35ipv6ssl">3.5、IPV6 如何签发 SSL 证书？</h3>
<p>如果你细心看上面的网络拓扑图，会发现 DDNS + IPV6 方案的流量不经过公网服务器，因此也不会生成对应域名的证书。</p>
<p>家庭局域网也无法签发证书，HTTPS 是个难题。</p>
<p>正当小剧一筹莫展时，一个精妙的思路浮现在小剧的脑海中。</p>
<p>公网的反向代理同样配置一条 IPV6 服务对应域名的解析，使用静态文本做响应。这一步完全是为了自动化获取 SSL 证书，没有其他意义。</p>
<pre><code class="none language-none">ipv6.sample.com {
    respond "Hello This is bh-lay.com"
}
</code></pre>
<p>局域网的反向代理服务用相同的方法获取对应域名的证书。</p>
<p>当身处家庭局域网中倒没什么问题，因为域名会被 Hack 成仅内网 IP 的解析。</p>
<p>而当你处在公网中就变得复杂了，<code>ipv6.sample.com</code> 域名对应的解析，既有公网 IPV4 的地址，同时也有 IPV6 的地址，设备将会如何选择链路？</p>
<p>这部分小剧并没有找到权威的解释，仅从实际测试结果来同步下结论：</p>
<p>IOS、MacOS 在某个域名同时能解析到 IPV4 和 IPV6 的地址时，会优先使用 IPV6 的链路。如果当前网络环境仅支持 IPV4，将会使用 IPV4 的地址。</p>
<p>如此一来刚好满足小剧的需要，IPV6 同样可以用这套方案兼容。</p>
<p>这里还有一个小贴士：家庭 IPV6 用不了 80、443 端口，需要设置形如 8080、8443 的高位端口。</p>
<p><strong>SSL 证书只关心域名，不同端口可以使用同一套证书。</strong></p>
<p>这一点也是最初小剧的顾虑，万幸验证后并不存在。</p>
<h2 id="-3">四、方案好使么？</h2>
<p>目前来说，家庭局域网、公网 DNS 融合的方案已经成为小剧家中的服务部署范式。在最近半年的使用中也没有掉过链子。</p>
<h3 id="41ipv6">4.1、服务器中转 、IPV6 互补</h3>
<p>有了前面的背景知识，应该能看出来服务器中转和 ipv6 有各自的特点。</p>
<p>服务器中转的方案，速度稍慢但非常稳定，天然适合一些常用的、对网络要求较轻的服务。比如笔记、相册服务。</p>
<p>IPV6 速度理论上的上限较高，但使用场景比较局限，某些时候还得借助于手机流量才能访问。</p>
<p>比较适合一些大流量的服务，比如 Jellyfin 观影类的 APP，在外给娃看动画片最合适不过了。</p>
<p>不同类型的服务可以按照特性，去选择合适的方案，实际使用下来相当舒适。</p>
<h3 id="42">4.2、蒲公英的身份蜕变</h3>
<p>在前面 2.2 部分，提到小剧"斥巨资"购入了蒲公英小盒子。看起来后面的服务部署方案和它没有一丁点关系了。</p>
<p>其实不是的，内网中零零碎碎的服务很多，协议也多种多样，绝大多数服务都不太适合，也没有必要部署到互联网中。</p>
<p>蒲公英作为安全性较高的内网穿透方案，很适合给小剧做远程运维的通道。</p>
<p>在外时借助于它可以连接 Mac mini 的远程桌面，使用 SSH 管理 Docker 容器。</p>
<p>偶尔遇到服务故障时也能掏出手机，从容的进行故障恢复。</p>
<p>因此蒲公英成了小剧专属的回家通道。</p>
<h3 id="43">4.3、来自媳妇的无声评价</h3>
<p>DNS 融合的模式，对小剧媳妇来说不需要任何前置操作。需要用某个 APP 的时候直接点开，用完了放到后台或者关掉即可。</p>
<p>目前 Immich、Jellyfin 已经在媳妇手机里成为常客，Outline 文档 APP 虽然不怎么用，但是也一直没有删掉。</p>
<p>也算是对小剧"瞎折腾"的一种认可吧。</p>
<hr />
<p>最后的提醒：</p>
<p>任何将服务暴露在公网中的行为都是带有风险的，要注意数据安全哦～</p>]]></content:encoded>
    </item>
    <item>
      <title>小剧2024的烟火</title>
      <link>https://bh-lay.com/blog/qi52bheh6o</link>
      <description><![CDATA[<p>2024 年如往常的年份一样固执，已经不可逆转的融为我们记忆的一部分。</p>
<p>小剧的 2024 年度记忆很充实，但又乏味无比。可能这就是小剧即将步入 35 岁"高龄"的预告吧。</p>
<h2 id="">一、渐入佳境的奶爸生活</h2>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/b4818c9c-c921-4da4-bde1-9a63b85f43ac.jpg" alt="一人站出千军万马的气势" /></p>
<p>2024 年是宝宝彰显破坏力的一年，两岁的娃娃正是行动力和思考能力都开始加速成长的阶段。</p>
<p>不过每个宝宝的成长曲线可能稍有不同吧。年初宝宝查出疑似患有轻度自闭症，随后便开始了漫长的干预治疗生活。</p>
<p>小剧 2024 年工作之余的全部几乎都和这个小家伙一起。但相比媳妇 7 * 24 全年无休的陪伴还是不值一提。</p>
<p>小剧之所以能用"渐入佳境"来形容奶爸的生活，离不开老婆 2024 年的辛苦付出。</p>
<p>希望即将到来的 2025 年宝宝能有更好的进步，老婆可以少一些奔波和焦虑。</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/18d7c328-57e1-4667-affb-9af65ff0b94b.jpg" alt="三口小家" /></p>
<h2 id="-1">二、学到了新知识没？</h2>
<p>2024 年工作上依旧延续着前几年的惯性，持续在 Web 开发上耕作。</p>
<p>对 Vue3 在项目上的实践有了更真实的经历。对 AI 在业务上的落地也有了更具体的认识。</p>
<p>工作之余依旧持续着低频但不间断的学习。</p>
<h3 id="21css">2.1、CSS 的更多可能</h3>
<p>自 2012 年初建立小剧客栈至今，小剧个人的成长持续记录在这里，同样技术上的学习成果也体现在这里。</p>
<p>2024 年小剧客栈没有在 NodeJS 后端服务上做大的改动，更没有对 JS 的更多方向做探索，但对于 CSS 表现层更多的可能性倒是做了不少的尝试。</p>
<h4 id="211cssjs">2.1.1、使用 CSS 替代部分 JS 效果</h4>
<p>上半年小剧客栈的 UI 做了几处小的局部改版，尝试给博客正文的布局更改为"剧场模式"。功能并不复杂，但是在修改代码的过程中却发现，小剧客栈曾经很多让小剧"引以为傲"的实现，正慢慢变为陈旧的"顽疾"。</p>
<p>这次尝试使用 CSS filter 替代 JS 实现了底图模糊的效果，性能上完全上了一个台阶，并且在响应式上代码组织起来更为灵活。</p>
<p>用 CSS sticky 替代了原本 JS 监听的才能实现的灵动布局效果，又删掉一大坨 JS 代码。</p>
<p>另外还使用了纯 CSS 的 Scroll-driven Animations 替代了原本导航粘滞的效果。</p>
<p>Scroll-driven Animations 是一个非常强大的 API，不过小剧这里并没有用它做特别大的改动，仅以导航效果试试水。</p>
<p>关于这部分写了篇<a href="https://bh-lay.com/blog/zk8wczsj2v" title="More CSS, less JS">《More CSS, less JS》</a>做记录，感兴趣的话可以点击查看更多细节。</p>
<h4 id="212viewtransitionapi">2.1.2、View Transition API 学习</h4>
<p>下半年由于机缘巧合，发现了 View Transition API 这个神奇的新特性。</p>
<p>对于 Web 中的过场动画，小剧也算是较早时候就做了尝试，并且在近十年来在小剧客栈上迭代过三个不同的版本。</p>
<p>之前的版本虽然动画也较为细腻，但都建立在新、老视图作为一个独立的整体来实现的，无法或很难对视图更近一步的动画拆分。</p>
<p>这次将视图切换动画由 Vue 的 transition 组件更改为了 View Transition API 来实现，并且针对【博文列表 → 博文详情】的视图切换，尝试了更为大胆的"神奇移动"。</p>
<p><strong>这部分经历记录在<a href="https://bh-lay.com/blog/nqvzmmovu8" title="View Transition API 尝试过场动画">《View Transition API 尝试过场动画》</a>这篇博文中。</strong></p>
<p>这篇文章算是上半年<a href="https://bh-lay.com/blog/zk8wczsj2v" title="More CSS, less JS">《More CSS, less JS》</a>的下篇。介绍小剧利用 View Transition API 实现试图转场动画的过程。作为渐进增强的动画方案，View Transition API 对业务的耦合度极低，很容易与现有业务相结合。</p>
<h4 id="213">2.1.3、使用更适合照片分享的布局方案</h4>
<p>小剧客栈中<strong>摄影照片分享</strong>一直使用的是固定比例容器排列显示，没有大的问题但效果中规中矩。</p>
<p>某天在浏览<a href="https://bh-lay.tuchong.com/">图虫个人首页</a>的时候注意到，它在排列照片的时候，很好的还原了照片的原始比例。</p>
<p>摄影是带有主观视角倾向的作品，如果能够保留原始比例，对于展现照片叙事逻辑尤为重要。</p>
<p>因此在研究了图虫的实现逻辑再结合自己对 Flex 布局的理解，对摄影作品分享页面做了重新布局。</p>
<p>新老布局对比图片</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/e6e8b6e2-f161-4f3e-abfe-b7228a430a0a.jpg" alt="摄影作品新老布局对比图片" /></p>
<h3 id="22svg">2.2、SVG 实现异形动画效果</h3>
<p>这是一个小的尝试，对于 SVG 小剧一直将它视为静态矢量图标或图形的展示方案。除了几年前在做 D3 项目的时候，并未设想过可以将 JS 的灵活性与 SVG 图形的多样性组合起来。</p>
<p>这部分效果的基础代码拷贝自 Copen 的 Luis Castrillo，不清楚是否为他原创。在他的基础上小剧做了代码结构的调整和效果的重新设计。</p>
<p>整体效果还算出彩，代码自由度也算可控，并且作为 SVG 辅助动画效果，这次经验算是抛砖引玉。后续有类似 CSS 无法实现的异形动画的需求，也不至于束手无策。</p>
<p>动画视频
<video id="video" controls="" preload="auto">
      <source id="mp4" src="https://static.bh-lay.com/blog/2024/2024-year-end/280038c7-c182-47c3-b367-899c3cc39548.mp4" type="video/mp4">
</p>
<h3 id="23threejs">2.3、"重学" ThreeJS</h3>
<p>近七年没碰过 threeJS 的小剧，这次破天荒的重新捡起了这个老行当。这次小剧将博客全景作品分享页面的静态背景图下线了，更换成了更贴近场景主题的《妹纸们》全景。</p>
<p>这幅照片是七年前小剧从 SNH48 微博图文中下载并二次加工得来，是唯一一个非小剧拍摄但流传度最广的作品（果然妹纸才是生产力）。</p>
<p>因为单纯作为背景展示，并不需要任何交互逻辑，这次的实现逻辑并不复杂。</p>
<p>简单的创建了一个片状的圆环并贴图，摄像机于圆心处，不停旋转圆环即可实现效果。</p>
<p>全景作品分享顶图截图</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/4ceed937-225d-4a2b-87ca-09e4fb2528f2.jpg" alt="全景作品分享顶图截图" /></p>
<h2 id="-2">三、生活中的小调剂</h2>
<h3 id="31">3.1、摄影</h3>
<p><strong>无</strong></p>
<p>2024年是真的没有进行任何摄影行动。倒不是因为这方面热情消退了，而是奶爸的生活和悠闲且行踪多变的摄影本身是冲突的。</p>
<p>希望 N 年后可以骑着车子载着娃，一起拍属于我们父女俩各自的人生照片。</p>
<h3 id="32">3.2、身边的远方</h3>
<p>2024 年摄影行动虽然没有进行，但是照片依旧咔咔咔拍个不停。只是从有计划的拍摄，变成了记录生活的有啥拍啥。</p>
<p>2024 年小剧陪娃和媳妇一起跑遍了合肥的各大公园、场馆。偶尔出一出城，在不是很远的地方换一种心情。</p>
<p>小岭南稻田
<img src="https://static.bh-lay.com/blog/2024/2024-year-end/24d10ce6-17e1-43f9-9a63-8e8955130244.jpg" alt="小岭南稻田" /></p>
<p>合肥新粮仓</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/6e04334f-ac77-4173-bcce-2506a01c7785.jpg" alt="合肥新粮仓" /></p>
<p>巢湖姥山岛</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/21449da6-1044-4d82-af1f-397b52e1b573.webp" alt="巢湖姥山岛" /></p>
<p>寿县城墙</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/68712216-fada-47c1-8131-8c1b7c7fb1e4.jpg" alt="寿县城墙" /></p>
<p>小区边的油菜地</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/dsc06243.jpg" alt="小区边的油菜地" /></p>
<p>浮槎山</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/1fdc14ac-b498-4b79-a929-ea973e2fcd1f.jpg" alt="浮槎山" /></p>
<p>水世界</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/b0eef1ec-c268-49be-bd49-0060051b2778.jpg" alt="水世界" /></p>
<p>葡萄采摘园</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/1fc56d55-1174-4e78-b02d-15acd192438b.jpg" alt="葡萄采摘园" /></p>
<p>合肥岸上草原</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/c828b506-d259-46bd-9f30-f413f3d61e8a.jpg" alt="合肥岸上草原" /></p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/32b7a435-6fc8-4ecc-9093-6c639272395a.jpg" alt="合肥岸上草原" /></p>
<p>溧阳南山竹海</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/5043f4b5-dcd8-43f3-b948-e96e58dd3d47.jpg" alt="溧阳南山竹海" /></p>
<p>合肥湿地公园</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/ee1adc6b-6c96-457a-85e8-7ccab059ef68.jpg" alt="合肥湿地公园" /></p>
<h3 id="33">3.3、养了只狗狗</h3>
<p>为了给宝宝一个玩伴，年初在老家领养了一只小奶狗。</p>
<p>用了宝宝最喜欢吃的"大米饭"给它做名字。</p>
<p>这是小剧第一次养小狗狗，没有小剧想的那么难，也并不可怕。</p>
<p>然而随着后来生活重心的变化，我们并没有足够的精力花在大米饭身上。此时大米饭也正处在身体快速发育的时候，需要更好的陪伴。</p>
<p>于是在领养了两个多月后，找到了一位妈妈非常支持的小女儿收养。结束了小剧第一次养狗狗的经历。</p>
<p>大米饭</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/839aa3ac-40bb-4cf4-adfe-50389798f2fb.jpg" alt="大米饭" /></p>
<h3 id="34">3.4、家庭服务器</h3>
<p>自从 2021 年小剧购买了 H3C m1 家庭存储后，小剧对于个人照片备份的方案就多了一种选择。</p>
<p>作为文件存储，H3C m1 是够用的，尤其是配合之前那台老款的 Mac mini，可玩性还是有那么一点点的。早期 H3C m1 做主力存储，Mac mini 做备份存储，稳定性上没有较大问题，但易用性严重不足。</p>
<p>首先 H3C m1 作为被新华三抛弃的产品，近三年没有任何功能迭代和 Bug 修复。虽然 App 支持公网访问内网，但可远程查看的文件格式极少。</p>
<p>Mac mini 虽然可以安装服务，但是十年前的设备性能并不强劲，功耗还贼高。</p>
<p>三年后的 2024 年初，小剧按照自己的思路拼凑一份独属于自己的家庭服务器方案。</p>
<p>家庭服务器合影</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/966492ec-0f0b-490c-a9d0-47c692ddd99e.jpg" alt="家庭服务器合影" /></p>
<h4 id="341">3.4.1、服务硬件方案</h4>
<p>这是目前小剧比较满意的一套方案，相较于成品 Nas 在一个单体设备上完成全套家庭服务的方式，小剧更喜欢这种设备灵活增添的自由感。</p>
<p>新购的 m1 版本的 Mac mini 承担的是家庭主服务器的功能，数据的存储、服务的运行都依托于它。H3C m1 降级为备份存储，每四小时备份 Mac mini 中的数据到这里。</p>
<p>下方的小方盒子是个 Ubuntu 服务器，所有需要暴露在公网中的服务都依托它做中转。支持 IPV6 直接暴露与 FRP 服务器中转。</p>
<p>上方的蒲公英盒子仅给我自己使用，连接它即可拥有内网身份，方便突发时候调试。</p>
<p>相比于成品 Nas 的单体设备，小剧这套主服务器、备份机器、内网机器（蒲公英）、公网机器（就这么叫它吧，也没有更好的名字）的组合自由度更高。物理隔离的另外一个好处就是，当不幸遭遇攻击时，仅需要关闭 Ubuntu 公网机器即可。即使公网服务器被攻破，带来的损失也是最小的。</p>
<p><strong>关于这部分经历，小剧写了<a href="https://bh-lay.com/blog/btz0m0kbl3" title="家庭服务器改造记录">《家庭服务器改造记录》</a></strong>这篇文章。</p>
<p>这套方案还没有足够长的时间去验证，仅以小剧的兴趣为出发点，对你的选择不具备指导意义。</p>
<h4 id="342immich">3.4.2、Immich 相册服务</h4>
<p>在<a href="https://bh-lay.com/blog/22jy29grksw">《剧中人平庸的2023》</a>「4.2 尝试部署私有化」部分，曾经介绍过 Photoview 和 Wiki.js 两款服务。</p>
<p>其中 Photoview 近一年的使用并没给到小剧有很大的惊喜。虽然也有人脸识别和时间线浏览，但给小剧的感觉他就是文件夹管理照片的增强版。没有在"照片"这个维度有更多功能。</p>
<p>随着此次物理设备的迁移，小剧也发现了 Immich 这样一款自托管服务，于是小剧在 Mac mini 中也安装了 Immich。</p>
<p>经过近一年的使用，小剧将 Immich 从一个"玩具"正式"册封"为家庭相册管理服务。</p>
<p>首先它的照片备份逻辑非常顺滑，完全没有以前重复备份或者漏备份的问题。</p>
<p>另外它支持多账号，家人可以各自使用自己的数据，也可以相互共享相册，使用起来更合乎逻辑。</p>
<p>最重要的是，它支持移动端、Web 端访问，支持人脸识别、按地理位置聚合等相册该有的功能。并且还在高密度的持续迭代。</p>
<p>Immich 截图</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/c6770ee0-f19e-435a-92c5-fffa02bcf161.jpg" alt="Immich 截图" /></p>
<h4 id="343outline">3.4.3、Outline 文档服务</h4>
<p>前面提到的 Wiki.js 服务，小剧其实使用很久了，因为是基于 NodeJS 开发的，天然能引起小剧的好感。</p>
<p>但实际使用下来体验并不好，倒不是因为 Wiki.js 自身不好，而是不适合小剧自托管这个场景。</p>
<p>Wiki.js 定位为团队 Wiki 发布平台，书写体验并不友好，更适合作为团队的知识沉淀或者对外文档发布。</p>
<p>很久之前小剧就接触过 Outline 文档服务，因为它依赖于第三方的 OAuth 服务，并且需要支持 S3 的服务提供文件存储，并且这一切要以域名的方式进行配置，基于这些种种导致小剧一直没有敢于尝试。</p>
<p>在十月的最后一个夜晚，小剧决定啃一啃这块硬骨头。</p>
<p>实际折腾下来发现 Outline 的部署并不可怕，甚至 S3 服务也不是必须的。</p>
<p>为了提高使用的稳定性，小剧之后还自建了 Keycloak OAuth 服务，用来替代国内使用不是那么友好的 Slack。</p>
<p>安装完成后，就是以用户的身份使用了。</p>
<p>Outline 的书写逻辑和小剧之前参与开发的讯飞文档发非常相似，都是基于增量数据的实时保存逻辑，整个编辑过程几乎可以做到随写随关。</p>
<p>另外它的 UI 非常克制且简洁，完全长在小剧审美的点上。</p>
<p>对了，Outline 是没有移动端 App 的，但是它的 Web 端完美兼容移动端使用。在 2024 年使用 PWA 构建移动应用的产品已经越来越少见了。</p>
<p>这部分经历记录在<a href="https://bh-lay.com/blog/2oevmzytwwc" title="Outline Docs 初体验">《Outline Docs 初体验》</a>中，算是小剧为数不多的怀着兴奋的心情写下的小水文。</p>
<p>对了这份 2024 年终总结的草稿阶段，全部书写在 Outline 中。</p>
<p>Outline 截图</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/decb0dd8-0f69-4a3a-80f8-fa50d0ce4434.jpg" alt="Outline 截图" /></p>
<h2 id="-3">四、写过的文字</h2>
<p>今年小剧一边忙着家里的事儿，一边捣鼓各种代码，所幸记录了一些值得分享的内容。从怎么让网页的动画效果更酷炫，到给自己家搭个实用的服务器，还尝试了不少新的软件和工具。</p>
<p>这里简单罗列一下，前面零散的的提到的文章。</p>
<h3 id="httpsbhlaycomblog1sidzrps0yb"><a href="https://bh-lay.com/blog/1sidzrps0yb" title="家庭服务器断电处理记录">家庭服务器断电处理记录</a></h3>
<p>最近家里停电，Mac mini 的 Docker 里所有的 Containers、Images、系统 Volumes 丢失，记录下此次服务恢复的过程。</p>
<p>2024-04-23</p>
<h3 id="httpsbhlaycomblogbtz0m0kbl3"><a href="https://bh-lay.com/blog/btz0m0kbl3" title="家庭服务器改造记录">家庭服务器改造记录</a></h3>
<p>这是一篇单纯记录小剧此次家庭服务器改造的文章。对 NAS、家庭服务器的很多理解个人色彩很浓，方案还没有足够长的时间去验证，因此对你的选择不具备指导意义。希望你能在小剧这里找到一丝丝可以借鉴的思路，或者可以当做谈资的笑话。</p>
<p>2024-05-15</p>
<h3 id="morecsslessjshttpsbhlaycomblogzk8wczsj2vmorecsslessjs"><a href="https://bh-lay.com/blog/zk8wczsj2v" title="More CSS, less JS">More CSS, less JS</a></h3>
<p>近来给博客 UI 做了小的局部改版，给博客正文的布局更改为"剧场模式"。在改 UI 的时候，却发现小剧客栈曾经很多让小剧"引以为傲"的实现，正慢慢变为陈旧的"顽疾"。</p>
<p>2024-06-11</p>
<h3 id="outlinedocshttpsbhlaycomblog2oevmzytwwcoutlinedocs"><a href="https://bh-lay.com/blog/2oevmzytwwc" title="Outline Docs 初体验">Outline Docs 初体验</a></h3>
<p>分享小剧 Outline Docs 的安装使用体验，此前曾因登录方式特殊未尝试使用。后因闲置设备才尝试安装，虽有波折但成功。使用体验包括移动端 PWA 版、桌面端佳，文档组织独特。</p>
<p>2024-11-04</p>
<h3 id="viewtransitionapihttpsbhlaycomblognqvzmmovu8viewtransitionapi"><a href="https://bh-lay.com/blog/nqvzmmovu8" title="View Transition API 尝试过场动画">View Transition API 尝试过场动画</a></h3>
<p>这篇文章算是上半年《More CSS，Less JS》的下篇。介绍小剧利用 View Transition API 实现试图转场动画的过程。作为渐进增强的动画方案，View Transition API 对业务的耦合度极低，很容易与现有业务相结合。</p>
<p>2024-12-20</p>
<hr />
<h2 id="-4">五、寄语</h2>
<p>2024 的烟火已经消散无踪，小剧在这一年的琐碎日常里，有奶爸生活的酸甜、技术学习的苦乐、生活琐事的起伏，这些都是过去一年闪烁着的烟火。</p>
<p>过往成忆，未来已来，愿新岁里，继续在烟火人间，怀壮志前行，拥温暖而歌，让小剧生活的每一刻，都绽放出独属于自己的烟火之光。</p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Tue, 31 Dec 2024 16:20:25 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/qi52bheh6o</guid>
      <category>年终总结</category>
      <category>生活</category>
      <category>2024</category>
      <enclosure url="http://static.bh-lay.com/blog/2024/2024-year-end/2024-smokes.webp" length="0" type="image/webp"/>
      <content:encoded><![CDATA[<p>2024 年如往常的年份一样固执，已经不可逆转的融为我们记忆的一部分。</p>
<p>小剧的 2024 年度记忆很充实，但又乏味无比。可能这就是小剧即将步入 35 岁"高龄"的预告吧。</p>
<h2 id="">一、渐入佳境的奶爸生活</h2>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/b4818c9c-c921-4da4-bde1-9a63b85f43ac.jpg" alt="一人站出千军万马的气势" /></p>
<p>2024 年是宝宝彰显破坏力的一年，两岁的娃娃正是行动力和思考能力都开始加速成长的阶段。</p>
<p>不过每个宝宝的成长曲线可能稍有不同吧。年初宝宝查出疑似患有轻度自闭症，随后便开始了漫长的干预治疗生活。</p>
<p>小剧 2024 年工作之余的全部几乎都和这个小家伙一起。但相比媳妇 7 * 24 全年无休的陪伴还是不值一提。</p>
<p>小剧之所以能用"渐入佳境"来形容奶爸的生活，离不开老婆 2024 年的辛苦付出。</p>
<p>希望即将到来的 2025 年宝宝能有更好的进步，老婆可以少一些奔波和焦虑。</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/18d7c328-57e1-4667-affb-9af65ff0b94b.jpg" alt="三口小家" /></p>
<h2 id="-1">二、学到了新知识没？</h2>
<p>2024 年工作上依旧延续着前几年的惯性，持续在 Web 开发上耕作。</p>
<p>对 Vue3 在项目上的实践有了更真实的经历。对 AI 在业务上的落地也有了更具体的认识。</p>
<p>工作之余依旧持续着低频但不间断的学习。</p>
<h3 id="21css">2.1、CSS 的更多可能</h3>
<p>自 2012 年初建立小剧客栈至今，小剧个人的成长持续记录在这里，同样技术上的学习成果也体现在这里。</p>
<p>2024 年小剧客栈没有在 NodeJS 后端服务上做大的改动，更没有对 JS 的更多方向做探索，但对于 CSS 表现层更多的可能性倒是做了不少的尝试。</p>
<h4 id="211cssjs">2.1.1、使用 CSS 替代部分 JS 效果</h4>
<p>上半年小剧客栈的 UI 做了几处小的局部改版，尝试给博客正文的布局更改为"剧场模式"。功能并不复杂，但是在修改代码的过程中却发现，小剧客栈曾经很多让小剧"引以为傲"的实现，正慢慢变为陈旧的"顽疾"。</p>
<p>这次尝试使用 CSS filter 替代 JS 实现了底图模糊的效果，性能上完全上了一个台阶，并且在响应式上代码组织起来更为灵活。</p>
<p>用 CSS sticky 替代了原本 JS 监听的才能实现的灵动布局效果，又删掉一大坨 JS 代码。</p>
<p>另外还使用了纯 CSS 的 Scroll-driven Animations 替代了原本导航粘滞的效果。</p>
<p>Scroll-driven Animations 是一个非常强大的 API，不过小剧这里并没有用它做特别大的改动，仅以导航效果试试水。</p>
<p>关于这部分写了篇<a href="https://bh-lay.com/blog/zk8wczsj2v" title="More CSS, less JS">《More CSS, less JS》</a>做记录，感兴趣的话可以点击查看更多细节。</p>
<h4 id="212viewtransitionapi">2.1.2、View Transition API 学习</h4>
<p>下半年由于机缘巧合，发现了 View Transition API 这个神奇的新特性。</p>
<p>对于 Web 中的过场动画，小剧也算是较早时候就做了尝试，并且在近十年来在小剧客栈上迭代过三个不同的版本。</p>
<p>之前的版本虽然动画也较为细腻，但都建立在新、老视图作为一个独立的整体来实现的，无法或很难对视图更近一步的动画拆分。</p>
<p>这次将视图切换动画由 Vue 的 transition 组件更改为了 View Transition API 来实现，并且针对【博文列表 → 博文详情】的视图切换，尝试了更为大胆的"神奇移动"。</p>
<p><strong>这部分经历记录在<a href="https://bh-lay.com/blog/nqvzmmovu8" title="View Transition API 尝试过场动画">《View Transition API 尝试过场动画》</a>这篇博文中。</strong></p>
<p>这篇文章算是上半年<a href="https://bh-lay.com/blog/zk8wczsj2v" title="More CSS, less JS">《More CSS, less JS》</a>的下篇。介绍小剧利用 View Transition API 实现试图转场动画的过程。作为渐进增强的动画方案，View Transition API 对业务的耦合度极低，很容易与现有业务相结合。</p>
<h4 id="213">2.1.3、使用更适合照片分享的布局方案</h4>
<p>小剧客栈中<strong>摄影照片分享</strong>一直使用的是固定比例容器排列显示，没有大的问题但效果中规中矩。</p>
<p>某天在浏览<a href="https://bh-lay.tuchong.com/">图虫个人首页</a>的时候注意到，它在排列照片的时候，很好的还原了照片的原始比例。</p>
<p>摄影是带有主观视角倾向的作品，如果能够保留原始比例，对于展现照片叙事逻辑尤为重要。</p>
<p>因此在研究了图虫的实现逻辑再结合自己对 Flex 布局的理解，对摄影作品分享页面做了重新布局。</p>
<p>新老布局对比图片</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/e6e8b6e2-f161-4f3e-abfe-b7228a430a0a.jpg" alt="摄影作品新老布局对比图片" /></p>
<h3 id="22svg">2.2、SVG 实现异形动画效果</h3>
<p>这是一个小的尝试，对于 SVG 小剧一直将它视为静态矢量图标或图形的展示方案。除了几年前在做 D3 项目的时候，并未设想过可以将 JS 的灵活性与 SVG 图形的多样性组合起来。</p>
<p>这部分效果的基础代码拷贝自 Copen 的 Luis Castrillo，不清楚是否为他原创。在他的基础上小剧做了代码结构的调整和效果的重新设计。</p>
<p>整体效果还算出彩，代码自由度也算可控，并且作为 SVG 辅助动画效果，这次经验算是抛砖引玉。后续有类似 CSS 无法实现的异形动画的需求，也不至于束手无策。</p>
<p>动画视频
<video id="video" controls="" preload="auto">
      <source id="mp4" src="https://static.bh-lay.com/blog/2024/2024-year-end/280038c7-c182-47c3-b367-899c3cc39548.mp4" type="video/mp4">
</p>
<h3 id="23threejs">2.3、"重学" ThreeJS</h3>
<p>近七年没碰过 threeJS 的小剧，这次破天荒的重新捡起了这个老行当。这次小剧将博客全景作品分享页面的静态背景图下线了，更换成了更贴近场景主题的《妹纸们》全景。</p>
<p>这幅照片是七年前小剧从 SNH48 微博图文中下载并二次加工得来，是唯一一个非小剧拍摄但流传度最广的作品（果然妹纸才是生产力）。</p>
<p>因为单纯作为背景展示，并不需要任何交互逻辑，这次的实现逻辑并不复杂。</p>
<p>简单的创建了一个片状的圆环并贴图，摄像机于圆心处，不停旋转圆环即可实现效果。</p>
<p>全景作品分享顶图截图</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/4ceed937-225d-4a2b-87ca-09e4fb2528f2.jpg" alt="全景作品分享顶图截图" /></p>
<h2 id="-2">三、生活中的小调剂</h2>
<h3 id="31">3.1、摄影</h3>
<p><strong>无</strong></p>
<p>2024年是真的没有进行任何摄影行动。倒不是因为这方面热情消退了，而是奶爸的生活和悠闲且行踪多变的摄影本身是冲突的。</p>
<p>希望 N 年后可以骑着车子载着娃，一起拍属于我们父女俩各自的人生照片。</p>
<h3 id="32">3.2、身边的远方</h3>
<p>2024 年摄影行动虽然没有进行，但是照片依旧咔咔咔拍个不停。只是从有计划的拍摄，变成了记录生活的有啥拍啥。</p>
<p>2024 年小剧陪娃和媳妇一起跑遍了合肥的各大公园、场馆。偶尔出一出城，在不是很远的地方换一种心情。</p>
<p>小岭南稻田
<img src="https://static.bh-lay.com/blog/2024/2024-year-end/24d10ce6-17e1-43f9-9a63-8e8955130244.jpg" alt="小岭南稻田" /></p>
<p>合肥新粮仓</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/6e04334f-ac77-4173-bcce-2506a01c7785.jpg" alt="合肥新粮仓" /></p>
<p>巢湖姥山岛</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/21449da6-1044-4d82-af1f-397b52e1b573.webp" alt="巢湖姥山岛" /></p>
<p>寿县城墙</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/68712216-fada-47c1-8131-8c1b7c7fb1e4.jpg" alt="寿县城墙" /></p>
<p>小区边的油菜地</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/dsc06243.jpg" alt="小区边的油菜地" /></p>
<p>浮槎山</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/1fdc14ac-b498-4b79-a929-ea973e2fcd1f.jpg" alt="浮槎山" /></p>
<p>水世界</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/b0eef1ec-c268-49be-bd49-0060051b2778.jpg" alt="水世界" /></p>
<p>葡萄采摘园</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/1fc56d55-1174-4e78-b02d-15acd192438b.jpg" alt="葡萄采摘园" /></p>
<p>合肥岸上草原</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/c828b506-d259-46bd-9f30-f413f3d61e8a.jpg" alt="合肥岸上草原" /></p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/32b7a435-6fc8-4ecc-9093-6c639272395a.jpg" alt="合肥岸上草原" /></p>
<p>溧阳南山竹海</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/5043f4b5-dcd8-43f3-b948-e96e58dd3d47.jpg" alt="溧阳南山竹海" /></p>
<p>合肥湿地公园</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/ee1adc6b-6c96-457a-85e8-7ccab059ef68.jpg" alt="合肥湿地公园" /></p>
<h3 id="33">3.3、养了只狗狗</h3>
<p>为了给宝宝一个玩伴，年初在老家领养了一只小奶狗。</p>
<p>用了宝宝最喜欢吃的"大米饭"给它做名字。</p>
<p>这是小剧第一次养小狗狗，没有小剧想的那么难，也并不可怕。</p>
<p>然而随着后来生活重心的变化，我们并没有足够的精力花在大米饭身上。此时大米饭也正处在身体快速发育的时候，需要更好的陪伴。</p>
<p>于是在领养了两个多月后，找到了一位妈妈非常支持的小女儿收养。结束了小剧第一次养狗狗的经历。</p>
<p>大米饭</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/839aa3ac-40bb-4cf4-adfe-50389798f2fb.jpg" alt="大米饭" /></p>
<h3 id="34">3.4、家庭服务器</h3>
<p>自从 2021 年小剧购买了 H3C m1 家庭存储后，小剧对于个人照片备份的方案就多了一种选择。</p>
<p>作为文件存储，H3C m1 是够用的，尤其是配合之前那台老款的 Mac mini，可玩性还是有那么一点点的。早期 H3C m1 做主力存储，Mac mini 做备份存储，稳定性上没有较大问题，但易用性严重不足。</p>
<p>首先 H3C m1 作为被新华三抛弃的产品，近三年没有任何功能迭代和 Bug 修复。虽然 App 支持公网访问内网，但可远程查看的文件格式极少。</p>
<p>Mac mini 虽然可以安装服务，但是十年前的设备性能并不强劲，功耗还贼高。</p>
<p>三年后的 2024 年初，小剧按照自己的思路拼凑一份独属于自己的家庭服务器方案。</p>
<p>家庭服务器合影</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/966492ec-0f0b-490c-a9d0-47c692ddd99e.jpg" alt="家庭服务器合影" /></p>
<h4 id="341">3.4.1、服务硬件方案</h4>
<p>这是目前小剧比较满意的一套方案，相较于成品 Nas 在一个单体设备上完成全套家庭服务的方式，小剧更喜欢这种设备灵活增添的自由感。</p>
<p>新购的 m1 版本的 Mac mini 承担的是家庭主服务器的功能，数据的存储、服务的运行都依托于它。H3C m1 降级为备份存储，每四小时备份 Mac mini 中的数据到这里。</p>
<p>下方的小方盒子是个 Ubuntu 服务器，所有需要暴露在公网中的服务都依托它做中转。支持 IPV6 直接暴露与 FRP 服务器中转。</p>
<p>上方的蒲公英盒子仅给我自己使用，连接它即可拥有内网身份，方便突发时候调试。</p>
<p>相比于成品 Nas 的单体设备，小剧这套主服务器、备份机器、内网机器（蒲公英）、公网机器（就这么叫它吧，也没有更好的名字）的组合自由度更高。物理隔离的另外一个好处就是，当不幸遭遇攻击时，仅需要关闭 Ubuntu 公网机器即可。即使公网服务器被攻破，带来的损失也是最小的。</p>
<p><strong>关于这部分经历，小剧写了<a href="https://bh-lay.com/blog/btz0m0kbl3" title="家庭服务器改造记录">《家庭服务器改造记录》</a></strong>这篇文章。</p>
<p>这套方案还没有足够长的时间去验证，仅以小剧的兴趣为出发点，对你的选择不具备指导意义。</p>
<h4 id="342immich">3.4.2、Immich 相册服务</h4>
<p>在<a href="https://bh-lay.com/blog/22jy29grksw">《剧中人平庸的2023》</a>「4.2 尝试部署私有化」部分，曾经介绍过 Photoview 和 Wiki.js 两款服务。</p>
<p>其中 Photoview 近一年的使用并没给到小剧有很大的惊喜。虽然也有人脸识别和时间线浏览，但给小剧的感觉他就是文件夹管理照片的增强版。没有在"照片"这个维度有更多功能。</p>
<p>随着此次物理设备的迁移，小剧也发现了 Immich 这样一款自托管服务，于是小剧在 Mac mini 中也安装了 Immich。</p>
<p>经过近一年的使用，小剧将 Immich 从一个"玩具"正式"册封"为家庭相册管理服务。</p>
<p>首先它的照片备份逻辑非常顺滑，完全没有以前重复备份或者漏备份的问题。</p>
<p>另外它支持多账号，家人可以各自使用自己的数据，也可以相互共享相册，使用起来更合乎逻辑。</p>
<p>最重要的是，它支持移动端、Web 端访问，支持人脸识别、按地理位置聚合等相册该有的功能。并且还在高密度的持续迭代。</p>
<p>Immich 截图</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/c6770ee0-f19e-435a-92c5-fffa02bcf161.jpg" alt="Immich 截图" /></p>
<h4 id="343outline">3.4.3、Outline 文档服务</h4>
<p>前面提到的 Wiki.js 服务，小剧其实使用很久了，因为是基于 NodeJS 开发的，天然能引起小剧的好感。</p>
<p>但实际使用下来体验并不好，倒不是因为 Wiki.js 自身不好，而是不适合小剧自托管这个场景。</p>
<p>Wiki.js 定位为团队 Wiki 发布平台，书写体验并不友好，更适合作为团队的知识沉淀或者对外文档发布。</p>
<p>很久之前小剧就接触过 Outline 文档服务，因为它依赖于第三方的 OAuth 服务，并且需要支持 S3 的服务提供文件存储，并且这一切要以域名的方式进行配置，基于这些种种导致小剧一直没有敢于尝试。</p>
<p>在十月的最后一个夜晚，小剧决定啃一啃这块硬骨头。</p>
<p>实际折腾下来发现 Outline 的部署并不可怕，甚至 S3 服务也不是必须的。</p>
<p>为了提高使用的稳定性，小剧之后还自建了 Keycloak OAuth 服务，用来替代国内使用不是那么友好的 Slack。</p>
<p>安装完成后，就是以用户的身份使用了。</p>
<p>Outline 的书写逻辑和小剧之前参与开发的讯飞文档发非常相似，都是基于增量数据的实时保存逻辑，整个编辑过程几乎可以做到随写随关。</p>
<p>另外它的 UI 非常克制且简洁，完全长在小剧审美的点上。</p>
<p>对了，Outline 是没有移动端 App 的，但是它的 Web 端完美兼容移动端使用。在 2024 年使用 PWA 构建移动应用的产品已经越来越少见了。</p>
<p>这部分经历记录在<a href="https://bh-lay.com/blog/2oevmzytwwc" title="Outline Docs 初体验">《Outline Docs 初体验》</a>中，算是小剧为数不多的怀着兴奋的心情写下的小水文。</p>
<p>对了这份 2024 年终总结的草稿阶段，全部书写在 Outline 中。</p>
<p>Outline 截图</p>
<p><img src="https://static.bh-lay.com/blog/2024/2024-year-end/decb0dd8-0f69-4a3a-80f8-fa50d0ce4434.jpg" alt="Outline 截图" /></p>
<h2 id="-3">四、写过的文字</h2>
<p>今年小剧一边忙着家里的事儿，一边捣鼓各种代码，所幸记录了一些值得分享的内容。从怎么让网页的动画效果更酷炫，到给自己家搭个实用的服务器，还尝试了不少新的软件和工具。</p>
<p>这里简单罗列一下，前面零散的的提到的文章。</p>
<h3 id="httpsbhlaycomblog1sidzrps0yb"><a href="https://bh-lay.com/blog/1sidzrps0yb" title="家庭服务器断电处理记录">家庭服务器断电处理记录</a></h3>
<p>最近家里停电，Mac mini 的 Docker 里所有的 Containers、Images、系统 Volumes 丢失，记录下此次服务恢复的过程。</p>
<p>2024-04-23</p>
<h3 id="httpsbhlaycomblogbtz0m0kbl3"><a href="https://bh-lay.com/blog/btz0m0kbl3" title="家庭服务器改造记录">家庭服务器改造记录</a></h3>
<p>这是一篇单纯记录小剧此次家庭服务器改造的文章。对 NAS、家庭服务器的很多理解个人色彩很浓，方案还没有足够长的时间去验证，因此对你的选择不具备指导意义。希望你能在小剧这里找到一丝丝可以借鉴的思路，或者可以当做谈资的笑话。</p>
<p>2024-05-15</p>
<h3 id="morecsslessjshttpsbhlaycomblogzk8wczsj2vmorecsslessjs"><a href="https://bh-lay.com/blog/zk8wczsj2v" title="More CSS, less JS">More CSS, less JS</a></h3>
<p>近来给博客 UI 做了小的局部改版，给博客正文的布局更改为"剧场模式"。在改 UI 的时候，却发现小剧客栈曾经很多让小剧"引以为傲"的实现，正慢慢变为陈旧的"顽疾"。</p>
<p>2024-06-11</p>
<h3 id="outlinedocshttpsbhlaycomblog2oevmzytwwcoutlinedocs"><a href="https://bh-lay.com/blog/2oevmzytwwc" title="Outline Docs 初体验">Outline Docs 初体验</a></h3>
<p>分享小剧 Outline Docs 的安装使用体验，此前曾因登录方式特殊未尝试使用。后因闲置设备才尝试安装，虽有波折但成功。使用体验包括移动端 PWA 版、桌面端佳，文档组织独特。</p>
<p>2024-11-04</p>
<h3 id="viewtransitionapihttpsbhlaycomblognqvzmmovu8viewtransitionapi"><a href="https://bh-lay.com/blog/nqvzmmovu8" title="View Transition API 尝试过场动画">View Transition API 尝试过场动画</a></h3>
<p>这篇文章算是上半年《More CSS，Less JS》的下篇。介绍小剧利用 View Transition API 实现试图转场动画的过程。作为渐进增强的动画方案，View Transition API 对业务的耦合度极低，很容易与现有业务相结合。</p>
<p>2024-12-20</p>
<hr />
<h2 id="-4">五、寄语</h2>
<p>2024 的烟火已经消散无踪，小剧在这一年的琐碎日常里，有奶爸生活的酸甜、技术学习的苦乐、生活琐事的起伏，这些都是过去一年闪烁着的烟火。</p>
<p>过往成忆，未来已来，愿新岁里，继续在烟火人间，怀壮志前行，拥温暖而歌，让小剧生活的每一刻，都绽放出独属于自己的烟火之光。</p>]]></content:encoded>
    </item>
    <item>
      <title>View Transition API 尝试过场动画</title>
      <link>https://bh-lay.com/blog/nqvzmmovu8</link>
      <description><![CDATA[<p>这篇文章算是上半年<a href="https://bh-lay.com/blog/zk8wczsj2v">《More CSS，Less JS》</a>这篇文章的下篇。</p>
<p>在写<a href="https://bh-lay.com/blog/zk8wczsj2v">《More CSS，Less JS》</a>时，介绍了在博客上做的一些改动。主要体现在用 CSS 的新特性替代之前用 JS 实现的一些功能。</p>
<p>如 CSS 模糊替代 JS + canvas 实现模糊，position: sticky 替代 JS Sticky 效果。利用 Scroll-driven Animation 实现导航粘滞效果。</p>
<p>时隔半年，小剧又尝试了 View Transition API 这个大杀器。</p>
<p>点击下面视频，可以预览小剧使用 View Transition API 做的一些效果。</p>
<p>【转场切换视频】</p>
<video id="video" controls="" preload="auto">
      <source id="mp4" src="https://static.bh-lay.com/blog/2024/view-transition-api/3d04e46e-1e06-4ab1-b42f-97bda3f87c73.mp4" type="video/mp4">
</video>
<p>当然，如果你在使用的是 PC 最新版的 Chrome，也可以在小剧客栈界面上点来点去，更直观的感受下。</p>
<h1 id="viewtransitionapi">为什么要学 View Transition API ？</h1>
<p>小剧入行前端开发，其实是从设计师这个身份转过来的。虽然在毕业后就从设计师角色"逃离"到了更为苦逼的研发岗位，但这些年对视觉、交互上的东西还是兴趣满满。</p>
<p>最初入行前端时还是 jQuery 的时代，界面通常是多页面硬切。完全没有"过场动画"这个概念。</p>
<p>之后 History API 普及后，小剧曾写过<a href="https://bh-lay.com/blog/14808da304a">《web 设计中的过场动画》</a>，畅想过在 web 中实现视图切换的可能性。并且在小剧客栈中做了尝试，用了非常简单的淡入淡出的效果。</p>
<p>一个月前第一次接触 View Transition API 时，一下子就惊艳到了我。</p>
<p>很多之前想都不敢想，或者实现难度很大，再或者<strong>为了点动效会让代码的可维护性变得极低的动画，现在可以轻松地实现了</strong>。</p>
<p>面对如此"魅惑"，小剧自然要花点时间学上一学。</p>
<h1 id="viewtransitionapi-1">简述 View Transition API</h1>
<p>在开始介绍小剧客栈中的转场动画实现之前，先简单下 View Transition API 的执行步骤。</p>
<ul>
<li>第一步，执行 <code>docuement.startViewTransition()</code>  方法，浏览器会开始准备动画，并对需要做转场动画的 dom 截图备用，并将界面"冻结"，开始处于不可点击操作的状态。</li>
<li>第二步，浏览器会执行 <code>startViewTransition</code> 方法传入的回调函数，执行视图切换逻辑，函数执行后浏览器同样会执行截图逻辑。</li>
<li>到了第三步，浏览器已经有了旧的视图和新的视图两张截图。如果你在 CSS 中对具体 <code>view-tansition-name</code> 的动画做了定义，渲染会遵循定制后的动画逻辑。默认则会使用淡入淡出叠加位置尺寸的变化，很像 KeyNote 里的"神奇移动"。</li>
<li>最后浏览器会删除转场动画的伪元素，解除"冻结"，恢复页面的可交互性。</li>
</ul>
<p>小剧向来不喜欢讲解 CSS、JS 特性细节，这是 MDN、caniuse 之类的平台擅长且权威的。因此上面的介绍省略了 <code>startViewTransition</code> 返回值的描述、新旧视图中缺失了对应 <code>view-transition-name</code>的处理情况、CSS 伪元素树结构等部分。</p>
<p>如果你对 View Transition API 的更多细节感兴趣，可以点击 <a href="https://developer.mozilla.org/de/docs/Web/API/ViewTransition">MDN - View Transition API</a> 获取更详细的介绍。</p>
<h1 id="">小剧客栈里的实现</h1>
<p>这次小剧客栈的改动其实只有一处，就是 router-view 切换动画。</p>
<p>因为 View Transition API 对 CSS、JS 代码的侵入性极低，可以很方便的对不同的视图切换动画做分别实现。</p>
<p>小剧客栈并没有复杂的页面层级，核心逻辑都在博文部分。因而这次视图切换动画主要的精力都花在了<strong>"博文列表 → 博文详情"</strong>部分。</p>
<p>其余的视图切换保持了以前的旧视图缩小退出，新视图向上渐显的交互。区别是新的交互借助于 View Transition API 实现，原有逻辑则依赖 Vue Router 切换时的 transition。</p>
<p><strong>博文列表 → 博文详情切换动画，参考前文的视频</strong></p>
<p>这里以【<strong>博文列表 → 博文详情</strong>】视图切换的动画实现方式来做示例。</p>
<h2 id="js">JS 部分</h2>
<p>View Transition API 的逻辑部分其实比较简单，就是找到合适的 <code>docuement.startViewTransition()</code>  调用时机。这次实践是在 Vue2 的版本上，因此 router 拦截器就是最好的地方。</p>
<p>唯一麻烦的是要区分当前需要使用哪种视图切换模式。</p>
<pre><code class="javascript language-javascript">const isSupportViewTransition = !!document.startViewTransition;
const baseRouterTransitionClass = "base-router-transition"
const articleRouterTransitionClass = "article-router-transition"

let hasClickArticleBefore = false;
let lastClickedArticleData = null;
export function markArticleClick(clickedNode, articleData) {
    if (!clickedNode) {
        return
    }
    hasClickArticleBefore = true;
    lastClickedArticleData = articleData
    clickedNode.classList.add("router-transition-article-item")
}
export function getLastClickedArticle() {
    const lastData = lastClickedArticleData
    lastClickedArticleData = null
    return lastData
}

function getScrollTop () {
    return Math.max(document.documentElement.scrollTop, document.body.scrollTop)
}
function setBodyScrollToRouteView () {
    const node = document.querySelector(".view-page")
    let scrollTop = getScrollTop()
    node.style.height = "100vh"
    node.style.overflow = "hidden"
    node.scrollTop = scrollTop
}
export function beforeRouterChange (to, from, next) {
    const isFirstPage = !from.name;
    const isSameView = to.name === from.name;
    if (isFirstPage || isSameView || !isSupportViewTransition) {
        return next()
    }

    const addClassForTransition = hasClickArticleBefore ? articleRouterTransitionClass : baseRouterTransitionClass;
    setBodyScrollToRouteView()
    document.documentElement.classList.add(addClassForTransition)
    const viewTransition = document.startViewTransition(() =&gt; {
        window.scrollTo(0, 0)
        next()
    })
    viewTransition.finished.finally(() =&gt; {
        document.documentElement.classList.remove(addClassForTransition)
        hasClickArticleBefore = false
    })
}
</code></pre>
<pre><code class="javascript language-javascript">import { beforeRouterChange } from "@/common/view-transition/"

router.beforeEach(beforeRouterChange)
</code></pre>
<h2 id="css">CSS 部分</h2>
<pre><code class="scss language-scss">// Navigation
.navigation .navigation-body
    view-transition-name navigation-view;

// Root View
.base-router-transition .view-page
    view-transition-name root-view;

@keyframes root-view-in
    0%
        opacity 0
        transform translate(0, 15vh)
    100%
        opacity 1
        transform translate(0, 0)
@keyframes root-view-out
    0%
        transform scale(1)
    100%
        transform scale(0.9)
::view-transition-old(root-view)
    transform-origin 50vw 100vh
    animation root-view-out 0.5s ease-in-out forwards
::view-transition-new(root-view)
    opacity 0
    animation root-view-in 0.8s 0.4s ease-in-out

// Article View
.article-router-transition
    .router-transition-article-item img
        view-transition-name article-head-view;
    .blog-detail header
        view-transition-name article-head-view;
    .section-article
        view-transition-name article-body-view;
::view-transition-group(article-head-view)
    animation-duration 0.4s
    animation-timing-function ease-out
::view-transition-old(article-head-view)
    height 100%
    overflow clip
    object-fit cover
::view-transition-new(article-head-view)
    height 100%
    overflow clip
::view-transition-new(article-body-view)
    opacity 0
    animation article-body-transition-in 0.4s 0.3s ease-out forwards

@keyframes article-body-transition-in
    0%
        opacity 0
        transform translate(0, 8vh)
    100%
        opacity 1
        transform translate(0, 0)
</code></pre>
<h2 id="-1">优化细节</h2>
<p>合理利用缓存数据提高体验。</p>
<p>因为动画的切换在点击前后两个 Tick 内完成，新视图中的数据必然没有加载完成。</p>
<p>因此合理的利用缓存可以极大提升动画的流畅度。例如前面 JS 部分的 <code>markArticleClick</code> 方法，完成了标记点击节点的同时，也缓存了文章的缩略信息。</p>
<p>减少大面积视图重绘，增强视图的简洁稳定。</p>
<p>起初博文详情的 loading 状态为全视图蒙层，画面动画较为割裂，不同阶段的动画闪烁严重。</p>
<p>因此借助于 View Transition API 实现动画的前提下，缩小 loading 区域也能是动画更简洁稳定。</p>
<h1 id="vuetransition">为什么不用 Vue 的 transition 组件 ？</h1>
<p>如果你了解 Vue 的 transition 组件的实现逻辑，会发现 View Transition API 的步骤和 Vue 的实现极其相似。</p>
<h2 id="vuetransitionapi">Vue transition API 组件的优势</h2>
<p>Vue transition 组件使用旧视图 Dom 延迟销毁的方案，因此它拥有更好的兼容性，几乎可以兼容所有支持 Vue 的浏览器。</p>
<p>在动画过程中界面始终处于可交互的状态。</p>
<p>通过这样对比，是不是 Vue transition 组件更胜一筹？</p>
<p>确实是这样的，对于转场动画中，把视图当作一个整体来处理时，Vue transition 组件是最方便的，并且兼容性最好的，甚至借助于这个思路使用原生 JS 实现都不是难事。</p>
<p>前文提到小剧客栈十年前第一次尝试转场动画，当时就是借助于原生 JS 实现的。在动画开始前保留老的 DOM，动画结束后销毁老的 DOM，中间过程组织动画。</p>
<p>后来迁移到 Vue 版本后，一直使用 Vue transition 组件实现转场动画，维持了近五年时间。</p>
<h2 id="viewtransitionapi-2">View transition API 的方便之处。</h2>
<p>前面介绍 Vue transition 组件的优势，提到了把视图当作一个整体处理时极其方便，并且兼容性好。</p>
<p>但如果转场动画较为细腻，包含多个子动画，并且转场前后 DOM 结构千差万别，想实现类似于 KeyNote 的"神奇移动"之类的效果还是很难的，甚至不可实现。</p>
<p>并且因为 View transition API 实现转场动画的逻辑在 JS、CSS 中，不需要动 HTML 或者 template 结构，逻辑组织起来非常灵活，可以更方便地根据数据、状态应用不同的效果。</p>
<p>最后 View Transition API 的视图处理是浏览器原生实现，相比于 Vue transition 组件拥有很好的性能。</p>
<h2 id="viewtransitionapi-3">既然不分伯仲，为什么选择 View Transition API？</h2>
<p>很多年前小剧曾写过一篇<a href="https://bh-lay.com/blog/13f1f4cb058">《谈谈小剧对渐进增强与平稳退化的理解【上】》</a>，可惜十二年后的今天依旧没有水出【下】篇。</p>
<p><strong>渐进增强与平稳退化</strong>是前端处理兼容性问题的两种思维逻辑。</p>
<p>对于转场动画这种锦上添花的功能，有或无对主体功能没有丝毫影响。因此使用渐进增强的思路去处理最合适了。</p>
<p>上面是理由一，非常冠冕堂皇且能唬住人。</p>
<p>理由二就很简单了：对于小剧来说 View Transition API 是个完全没了解过的新特性，看起来炫，不学手痒。</p>
<hr />
<h1 id="viewtransitionapi-4">View Transition API 的更多可能</h1>
<p>对于讨好用户的微交互来说，Apple 是最无所不用其极的。</p>
<p>自从 IOS12 开始就有很多划时代的酷炫界面切换动效。比如桌面 App 文件夹的打开关闭特效、桌面最右屏的 App 资源库交互。 这些动效看起来是很连贯，实际分析初始、终止界面又有很大差异。</p>
<p>小剧很久之前小就想在【小剧起始页】中模拟这类交互效果。但因为传统动画实现方案需要要对结构做很多僵化的改动，后期新功能实现的灵活度和多样性都会大打折扣，所以一直都是放弃的。</p>
<p>通过小剧客栈这次的尝试，小剧对 View Transition API 可以实现场景有了更大的想象空间。</p>
<p>但愿后面可以在更多的地方尝试 Web 动画的可能性。</p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Fri, 20 Dec 2024 14:41:15 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/nqvzmmovu8</guid>
      <category>CSS</category>
      <category>View Transition API</category>
      <category>动画</category>
      <enclosure url="http://static.bh-lay.com/blog/2024/view-transition-api/view-teansition-api.jpg" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>这篇文章算是上半年<a href="https://bh-lay.com/blog/zk8wczsj2v">《More CSS，Less JS》</a>这篇文章的下篇。</p>
<p>在写<a href="https://bh-lay.com/blog/zk8wczsj2v">《More CSS，Less JS》</a>时，介绍了在博客上做的一些改动。主要体现在用 CSS 的新特性替代之前用 JS 实现的一些功能。</p>
<p>如 CSS 模糊替代 JS + canvas 实现模糊，position: sticky 替代 JS Sticky 效果。利用 Scroll-driven Animation 实现导航粘滞效果。</p>
<p>时隔半年，小剧又尝试了 View Transition API 这个大杀器。</p>
<p>点击下面视频，可以预览小剧使用 View Transition API 做的一些效果。</p>
<p>【转场切换视频】</p>
<video id="video" controls="" preload="auto">
      <source id="mp4" src="https://static.bh-lay.com/blog/2024/view-transition-api/3d04e46e-1e06-4ab1-b42f-97bda3f87c73.mp4" type="video/mp4">
</video>
<p>当然，如果你在使用的是 PC 最新版的 Chrome，也可以在小剧客栈界面上点来点去，更直观的感受下。</p>
<h1 id="viewtransitionapi">为什么要学 View Transition API ？</h1>
<p>小剧入行前端开发，其实是从设计师这个身份转过来的。虽然在毕业后就从设计师角色"逃离"到了更为苦逼的研发岗位，但这些年对视觉、交互上的东西还是兴趣满满。</p>
<p>最初入行前端时还是 jQuery 的时代，界面通常是多页面硬切。完全没有"过场动画"这个概念。</p>
<p>之后 History API 普及后，小剧曾写过<a href="https://bh-lay.com/blog/14808da304a">《web 设计中的过场动画》</a>，畅想过在 web 中实现视图切换的可能性。并且在小剧客栈中做了尝试，用了非常简单的淡入淡出的效果。</p>
<p>一个月前第一次接触 View Transition API 时，一下子就惊艳到了我。</p>
<p>很多之前想都不敢想，或者实现难度很大，再或者<strong>为了点动效会让代码的可维护性变得极低的动画，现在可以轻松地实现了</strong>。</p>
<p>面对如此"魅惑"，小剧自然要花点时间学上一学。</p>
<h1 id="viewtransitionapi-1">简述 View Transition API</h1>
<p>在开始介绍小剧客栈中的转场动画实现之前，先简单下 View Transition API 的执行步骤。</p>
<ul>
<li>第一步，执行 <code>docuement.startViewTransition()</code>  方法，浏览器会开始准备动画，并对需要做转场动画的 dom 截图备用，并将界面"冻结"，开始处于不可点击操作的状态。</li>
<li>第二步，浏览器会执行 <code>startViewTransition</code> 方法传入的回调函数，执行视图切换逻辑，函数执行后浏览器同样会执行截图逻辑。</li>
<li>到了第三步，浏览器已经有了旧的视图和新的视图两张截图。如果你在 CSS 中对具体 <code>view-tansition-name</code> 的动画做了定义，渲染会遵循定制后的动画逻辑。默认则会使用淡入淡出叠加位置尺寸的变化，很像 KeyNote 里的"神奇移动"。</li>
<li>最后浏览器会删除转场动画的伪元素，解除"冻结"，恢复页面的可交互性。</li>
</ul>
<p>小剧向来不喜欢讲解 CSS、JS 特性细节，这是 MDN、caniuse 之类的平台擅长且权威的。因此上面的介绍省略了 <code>startViewTransition</code> 返回值的描述、新旧视图中缺失了对应 <code>view-transition-name</code>的处理情况、CSS 伪元素树结构等部分。</p>
<p>如果你对 View Transition API 的更多细节感兴趣，可以点击 <a href="https://developer.mozilla.org/de/docs/Web/API/ViewTransition">MDN - View Transition API</a> 获取更详细的介绍。</p>
<h1 id="">小剧客栈里的实现</h1>
<p>这次小剧客栈的改动其实只有一处，就是 router-view 切换动画。</p>
<p>因为 View Transition API 对 CSS、JS 代码的侵入性极低，可以很方便的对不同的视图切换动画做分别实现。</p>
<p>小剧客栈并没有复杂的页面层级，核心逻辑都在博文部分。因而这次视图切换动画主要的精力都花在了<strong>"博文列表 → 博文详情"</strong>部分。</p>
<p>其余的视图切换保持了以前的旧视图缩小退出，新视图向上渐显的交互。区别是新的交互借助于 View Transition API 实现，原有逻辑则依赖 Vue Router 切换时的 transition。</p>
<p><strong>博文列表 → 博文详情切换动画，参考前文的视频</strong></p>
<p>这里以【<strong>博文列表 → 博文详情</strong>】视图切换的动画实现方式来做示例。</p>
<h2 id="js">JS 部分</h2>
<p>View Transition API 的逻辑部分其实比较简单，就是找到合适的 <code>docuement.startViewTransition()</code>  调用时机。这次实践是在 Vue2 的版本上，因此 router 拦截器就是最好的地方。</p>
<p>唯一麻烦的是要区分当前需要使用哪种视图切换模式。</p>
<pre><code class="javascript language-javascript">const isSupportViewTransition = !!document.startViewTransition;
const baseRouterTransitionClass = "base-router-transition"
const articleRouterTransitionClass = "article-router-transition"

let hasClickArticleBefore = false;
let lastClickedArticleData = null;
export function markArticleClick(clickedNode, articleData) {
    if (!clickedNode) {
        return
    }
    hasClickArticleBefore = true;
    lastClickedArticleData = articleData
    clickedNode.classList.add("router-transition-article-item")
}
export function getLastClickedArticle() {
    const lastData = lastClickedArticleData
    lastClickedArticleData = null
    return lastData
}

function getScrollTop () {
    return Math.max(document.documentElement.scrollTop, document.body.scrollTop)
}
function setBodyScrollToRouteView () {
    const node = document.querySelector(".view-page")
    let scrollTop = getScrollTop()
    node.style.height = "100vh"
    node.style.overflow = "hidden"
    node.scrollTop = scrollTop
}
export function beforeRouterChange (to, from, next) {
    const isFirstPage = !from.name;
    const isSameView = to.name === from.name;
    if (isFirstPage || isSameView || !isSupportViewTransition) {
        return next()
    }

    const addClassForTransition = hasClickArticleBefore ? articleRouterTransitionClass : baseRouterTransitionClass;
    setBodyScrollToRouteView()
    document.documentElement.classList.add(addClassForTransition)
    const viewTransition = document.startViewTransition(() =&gt; {
        window.scrollTo(0, 0)
        next()
    })
    viewTransition.finished.finally(() =&gt; {
        document.documentElement.classList.remove(addClassForTransition)
        hasClickArticleBefore = false
    })
}
</code></pre>
<pre><code class="javascript language-javascript">import { beforeRouterChange } from "@/common/view-transition/"

router.beforeEach(beforeRouterChange)
</code></pre>
<h2 id="css">CSS 部分</h2>
<pre><code class="scss language-scss">// Navigation
.navigation .navigation-body
    view-transition-name navigation-view;

// Root View
.base-router-transition .view-page
    view-transition-name root-view;

@keyframes root-view-in
    0%
        opacity 0
        transform translate(0, 15vh)
    100%
        opacity 1
        transform translate(0, 0)
@keyframes root-view-out
    0%
        transform scale(1)
    100%
        transform scale(0.9)
::view-transition-old(root-view)
    transform-origin 50vw 100vh
    animation root-view-out 0.5s ease-in-out forwards
::view-transition-new(root-view)
    opacity 0
    animation root-view-in 0.8s 0.4s ease-in-out

// Article View
.article-router-transition
    .router-transition-article-item img
        view-transition-name article-head-view;
    .blog-detail header
        view-transition-name article-head-view;
    .section-article
        view-transition-name article-body-view;
::view-transition-group(article-head-view)
    animation-duration 0.4s
    animation-timing-function ease-out
::view-transition-old(article-head-view)
    height 100%
    overflow clip
    object-fit cover
::view-transition-new(article-head-view)
    height 100%
    overflow clip
::view-transition-new(article-body-view)
    opacity 0
    animation article-body-transition-in 0.4s 0.3s ease-out forwards

@keyframes article-body-transition-in
    0%
        opacity 0
        transform translate(0, 8vh)
    100%
        opacity 1
        transform translate(0, 0)
</code></pre>
<h2 id="-1">优化细节</h2>
<p>合理利用缓存数据提高体验。</p>
<p>因为动画的切换在点击前后两个 Tick 内完成，新视图中的数据必然没有加载完成。</p>
<p>因此合理的利用缓存可以极大提升动画的流畅度。例如前面 JS 部分的 <code>markArticleClick</code> 方法，完成了标记点击节点的同时，也缓存了文章的缩略信息。</p>
<p>减少大面积视图重绘，增强视图的简洁稳定。</p>
<p>起初博文详情的 loading 状态为全视图蒙层，画面动画较为割裂，不同阶段的动画闪烁严重。</p>
<p>因此借助于 View Transition API 实现动画的前提下，缩小 loading 区域也能是动画更简洁稳定。</p>
<h1 id="vuetransition">为什么不用 Vue 的 transition 组件 ？</h1>
<p>如果你了解 Vue 的 transition 组件的实现逻辑，会发现 View Transition API 的步骤和 Vue 的实现极其相似。</p>
<h2 id="vuetransitionapi">Vue transition API 组件的优势</h2>
<p>Vue transition 组件使用旧视图 Dom 延迟销毁的方案，因此它拥有更好的兼容性，几乎可以兼容所有支持 Vue 的浏览器。</p>
<p>在动画过程中界面始终处于可交互的状态。</p>
<p>通过这样对比，是不是 Vue transition 组件更胜一筹？</p>
<p>确实是这样的，对于转场动画中，把视图当作一个整体来处理时，Vue transition 组件是最方便的，并且兼容性最好的，甚至借助于这个思路使用原生 JS 实现都不是难事。</p>
<p>前文提到小剧客栈十年前第一次尝试转场动画，当时就是借助于原生 JS 实现的。在动画开始前保留老的 DOM，动画结束后销毁老的 DOM，中间过程组织动画。</p>
<p>后来迁移到 Vue 版本后，一直使用 Vue transition 组件实现转场动画，维持了近五年时间。</p>
<h2 id="viewtransitionapi-2">View transition API 的方便之处。</h2>
<p>前面介绍 Vue transition 组件的优势，提到了把视图当作一个整体处理时极其方便，并且兼容性好。</p>
<p>但如果转场动画较为细腻，包含多个子动画，并且转场前后 DOM 结构千差万别，想实现类似于 KeyNote 的"神奇移动"之类的效果还是很难的，甚至不可实现。</p>
<p>并且因为 View transition API 实现转场动画的逻辑在 JS、CSS 中，不需要动 HTML 或者 template 结构，逻辑组织起来非常灵活，可以更方便地根据数据、状态应用不同的效果。</p>
<p>最后 View Transition API 的视图处理是浏览器原生实现，相比于 Vue transition 组件拥有很好的性能。</p>
<h2 id="viewtransitionapi-3">既然不分伯仲，为什么选择 View Transition API？</h2>
<p>很多年前小剧曾写过一篇<a href="https://bh-lay.com/blog/13f1f4cb058">《谈谈小剧对渐进增强与平稳退化的理解【上】》</a>，可惜十二年后的今天依旧没有水出【下】篇。</p>
<p><strong>渐进增强与平稳退化</strong>是前端处理兼容性问题的两种思维逻辑。</p>
<p>对于转场动画这种锦上添花的功能，有或无对主体功能没有丝毫影响。因此使用渐进增强的思路去处理最合适了。</p>
<p>上面是理由一，非常冠冕堂皇且能唬住人。</p>
<p>理由二就很简单了：对于小剧来说 View Transition API 是个完全没了解过的新特性，看起来炫，不学手痒。</p>
<hr />
<h1 id="viewtransitionapi-4">View Transition API 的更多可能</h1>
<p>对于讨好用户的微交互来说，Apple 是最无所不用其极的。</p>
<p>自从 IOS12 开始就有很多划时代的酷炫界面切换动效。比如桌面 App 文件夹的打开关闭特效、桌面最右屏的 App 资源库交互。 这些动效看起来是很连贯，实际分析初始、终止界面又有很大差异。</p>
<p>小剧很久之前小就想在【小剧起始页】中模拟这类交互效果。但因为传统动画实现方案需要要对结构做很多僵化的改动，后期新功能实现的灵活度和多样性都会大打折扣，所以一直都是放弃的。</p>
<p>通过小剧客栈这次的尝试，小剧对 View Transition API 可以实现场景有了更大的想象空间。</p>
<p>但愿后面可以在更多的地方尝试 Web 动画的可能性。</p>]]></content:encoded>
    </item>
    <item>
      <title>Outline Docs 初体验</title>
      <link>https://bh-lay.com/blog/2oevmzytwwc</link>
      <description><![CDATA[<h2 id="outline">Outline 是什么？</h2>
<p>初识 Outline 大约在今年（2024）的二月份，当时在探索自托管文档服务。最早使用玩客云小盒子安装了基于 NodeJS 的 Wiki.js。</p>
<p>在和好友 Twoer 交流中了解到 Outline 这样一款更"现代"的文档服务。</p>
<p>官网：<a href="https://getoutline.com">getoutline.com</a></p>
<p>上面这个链接是 Outline 的官网，里面有 Outline 精美的"卖家秀"。让小剧用一句话来介绍就是：<strong>「支持自托管的团队协作文档服务」</strong>。</p>
<h2 id="outline-1">为什么一直没有使用 Outline？</h2>
<p>小剧自从开始尝试自托管服务，大大小小的服务也折腾过不少。尤其是借助于 Docker 这样一个大杀器，安装维护服务也几乎是手到擒来。</p>
<p>&nbsp;然而 Outline 文档服务一直让我望而却步。</p>
<p>和其他我装过的服务比，Outline 最大的不同是登录方式很特别。它不支持传统的邮箱密码登录，只能用 Oauth 单点登录。例如 Slack、谷歌之类的，另外还支持其他符合标准 Oauth2.0 的认证服务。</p>
<p>直接可用的这几个认证服务要么需要公司注册，要么对网络要求很高，基本上个人裸的网络环境没办法直接用。而且最易操作的 Slack 也得配置一堆东西。</p>
<p>加上今年小剧的个人时间一直很紧张，能投入到折腾自托管服务的精力上更是少之又少。</p>
<p>因而小剧对 Outline 虽然"倾慕"已久，但始终没有动手尝试安装。</p>
<h2 id="">最近有时间折腾了？</h2>
<p>其实也没时间折腾这些东西，完全是有了想法却不去实现，头脑会发痒，痒又挠不到的那种痒。</p>
<p>前段时间小剧入手了极摩客 G5 小主机，它出现在我们家，是因为小剧希望有一台独立的外部 Web 服务中转设备。</p>
<p><strong>图一：极摩客 G5 小主机</strong></p>
<p><img src="https://static.bh-lay.com/blog/2024/outline-docs/7eeb4eda-a0fd-4b09-bec4-e3174fe06ceb.jpg" alt="极摩客 G5 小主机" /></p>
<p>虽然它的"招聘"流程很短，小剧对比了一天，思考半小时就下单了，然而漫长的"实习期"却让它闲置了很久。</p>
<p>半个月前交给它的第一个任务就是把 Immich 服务代理出去，处理 Immich 这种图片服务它的表现相当不错。</p>
<p>但一台 N97 四核 CPU、12G 内存的 Web 中转服务器，只转发一个 Immich 服务显然是大材小用了。</p>
<p>为了给它找点事干，就把 Outline 的服务安装提上了日程。</p>
<h2 id="-1">安装成功了么？</h2>
<p>周五（2024/10/31）娃睡的比较早，哄完娃就开始研究 Outline 的安装了。</p>
<p>具体过程就不展开了，就是传统的 docker-compse 文件编写，漫长的 docker pull，以及配置 DNS。</p>
<p>折腾到凌晨，临时借助 Slack 完成了登陆流程，终于在 11 月的第一个小时把 Outline 安装好了。草草体验了十几分钟就被媳妇叫回去睡觉了。</p>
<p>三天后今天（2024/11/04）把 Keycloak 的私有化单点登录也搞好了。</p>
<p>不借助于外部服务的本地版 Outline 算是正式安装成功了。</p>
<p>目前还缺少关键数据的验证流程，数据库、文件的定时备份支持。</p>
<h2 id="outline-2">Outline 好用么？</h2>
<p>前面提的这些都是作为运维身份的体验，自托管应用始终是为自己服务的，因此用户侧的体验才是我们所追求的。</p>
<h3 id="-2">客户端体验</h3>
<p>短暂使用了半小时手机端，体验确实很好，比早些时间使用的 Wiki.js 强太多了。</p>
<p><strong>Outline 是没有移动端 App 的</strong>，它用的是 PWA 版本简化 App。作为文档、笔记类的应用，Outline 的 PWA 版本体验能做到媲美原生 App 体验也是挺牛的。</p>
<p>当然实测下来 BUG 同样有一些，例如 Icon 选择器无法滚动、编辑模式光标聚焦到最底部工具条会被遮挡等小问题。</p>
<p>Outline 把最好的体验留给了桌面端，PC 浏览器和 PC 客户端几乎拥有一致的体验，因而小剧甚至更偏向于使用 Web 端而非客户端。</p>
<p><strong>图二：分不清是 Web 端还是客户端的截图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2024/outline-docs/screenshot.jpg" alt="分不清是 Web 端还是客户端的截图" /></p>
<h3 id="-3">文档组织逻辑</h3>
<p>需要注意的是 Outline 的文档管理和我们常见的应用有轻微差异。</p>
<p>笔记类应用一般是单层目录结构，也就是【文件夹 &gt; 笔记】的结构。</p>
<p>国内多数的文档产品是多级目录结构，每级目录下都可以存放文档。</p>
<p>我们常用的 Confluence Wiki 为文档嵌套结构，也就是每级目录都是一篇文档，每篇文档都可以再嵌套子文档。</p>
<p><strong>图三：Outline 目录结构（红圈内为文档集，其余均是文档）</strong></p>
<p><img src="https://static.bh-lay.com/blog/2024/outline-docs/039e5837-3eb0-4533-a716-3ac6f0fc3e97.jpg" alt="Outline 目录结构" /></p>
<p>Outline 对文档的组织结构比较特殊，它是【Collection &gt; 文档 &gt; 文档】结构。和传统笔记类的产品一样，它只有只有一级文件夹。但是每一篇文档又可以再添加子文档，最多 6 层，嵌套逻辑又和 Wiki 很相似。</p>
<p>这种文档组织方式还没有深入使用，尚不好评价是优是劣，但给人耳目一新的感觉。</p>
<h3 id="-4">总体评价如何</h3>
<p>Outline 给我整体的使用体验是：<strong>界面简洁、功能丰富、体验丝滑</strong>。</p>
<h2 id="-5">打算持续使用不？</h2>
<p>早期部署的 Wiki.js 因为定位为 Wiki 站点发布而非团队知识库建设的平台，体验上较为繁琐。</p>
<p>相比之下 Outline 更为丝滑。</p>
<p>接下来几天，打算把原来 Wiki.js 里的内容全迁到这里后，就可以把玩客云小机器彻底下线了。</p>
<p>最后再把讯飞文档里的个人文档全部迁过来。</p>
<p>讯飞文档：<a href="https://iflydocs.com">iflydocs.com</a></p>
<p>对了，讯飞文档是小剧从头参与开发款一款文档类产品，如果你有多人协作的需求，又不想折腾私有化，更不想自行承担数据维护的成本以及内容丢失的风险，讯飞文档是众多可选项中还算不错的选择。</p>
<p><strong>「跑题了，老东家记得打钱。」</strong></p>
<p>小剧个人的文档经历了早早早早期的 Evernote，之后的有道云笔记、石墨文档、老东家的讯飞笔记、讯飞文档、硬盘存储 Markdown 等一堆零散的管理方式。</p>
<p>每个都挺好，都是小剧不同阶段个人知识库的缩影。但小剧的个人内容散落在个各个角落，终归需要有个集中管理的地方，Outline 目前看起来就是那个地方。</p>
<p>打算持续使用 Outline 不？</p>
<p>目前看起来还不错哦，先用用看吧～</p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Mon, 04 Nov 2024 14:49:21 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/2oevmzytwwc</guid>
      <category>家庭服务器</category>
      <category>Outline</category>
      <category>Docker</category>
      <enclosure url="http://static.bh-lay.com/blog/2024/outline-docs/outline-cover.jpg" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<h2 id="outline">Outline 是什么？</h2>
<p>初识 Outline 大约在今年（2024）的二月份，当时在探索自托管文档服务。最早使用玩客云小盒子安装了基于 NodeJS 的 Wiki.js。</p>
<p>在和好友 Twoer 交流中了解到 Outline 这样一款更"现代"的文档服务。</p>
<p>官网：<a href="https://getoutline.com">getoutline.com</a></p>
<p>上面这个链接是 Outline 的官网，里面有 Outline 精美的"卖家秀"。让小剧用一句话来介绍就是：<strong>「支持自托管的团队协作文档服务」</strong>。</p>
<h2 id="outline-1">为什么一直没有使用 Outline？</h2>
<p>小剧自从开始尝试自托管服务，大大小小的服务也折腾过不少。尤其是借助于 Docker 这样一个大杀器，安装维护服务也几乎是手到擒来。</p>
<p>&nbsp;然而 Outline 文档服务一直让我望而却步。</p>
<p>和其他我装过的服务比，Outline 最大的不同是登录方式很特别。它不支持传统的邮箱密码登录，只能用 Oauth 单点登录。例如 Slack、谷歌之类的，另外还支持其他符合标准 Oauth2.0 的认证服务。</p>
<p>直接可用的这几个认证服务要么需要公司注册，要么对网络要求很高，基本上个人裸的网络环境没办法直接用。而且最易操作的 Slack 也得配置一堆东西。</p>
<p>加上今年小剧的个人时间一直很紧张，能投入到折腾自托管服务的精力上更是少之又少。</p>
<p>因而小剧对 Outline 虽然"倾慕"已久，但始终没有动手尝试安装。</p>
<h2 id="">最近有时间折腾了？</h2>
<p>其实也没时间折腾这些东西，完全是有了想法却不去实现，头脑会发痒，痒又挠不到的那种痒。</p>
<p>前段时间小剧入手了极摩客 G5 小主机，它出现在我们家，是因为小剧希望有一台独立的外部 Web 服务中转设备。</p>
<p><strong>图一：极摩客 G5 小主机</strong></p>
<p><img src="https://static.bh-lay.com/blog/2024/outline-docs/7eeb4eda-a0fd-4b09-bec4-e3174fe06ceb.jpg" alt="极摩客 G5 小主机" /></p>
<p>虽然它的"招聘"流程很短，小剧对比了一天，思考半小时就下单了，然而漫长的"实习期"却让它闲置了很久。</p>
<p>半个月前交给它的第一个任务就是把 Immich 服务代理出去，处理 Immich 这种图片服务它的表现相当不错。</p>
<p>但一台 N97 四核 CPU、12G 内存的 Web 中转服务器，只转发一个 Immich 服务显然是大材小用了。</p>
<p>为了给它找点事干，就把 Outline 的服务安装提上了日程。</p>
<h2 id="-1">安装成功了么？</h2>
<p>周五（2024/10/31）娃睡的比较早，哄完娃就开始研究 Outline 的安装了。</p>
<p>具体过程就不展开了，就是传统的 docker-compse 文件编写，漫长的 docker pull，以及配置 DNS。</p>
<p>折腾到凌晨，临时借助 Slack 完成了登陆流程，终于在 11 月的第一个小时把 Outline 安装好了。草草体验了十几分钟就被媳妇叫回去睡觉了。</p>
<p>三天后今天（2024/11/04）把 Keycloak 的私有化单点登录也搞好了。</p>
<p>不借助于外部服务的本地版 Outline 算是正式安装成功了。</p>
<p>目前还缺少关键数据的验证流程，数据库、文件的定时备份支持。</p>
<h2 id="outline-2">Outline 好用么？</h2>
<p>前面提的这些都是作为运维身份的体验，自托管应用始终是为自己服务的，因此用户侧的体验才是我们所追求的。</p>
<h3 id="-2">客户端体验</h3>
<p>短暂使用了半小时手机端，体验确实很好，比早些时间使用的 Wiki.js 强太多了。</p>
<p><strong>Outline 是没有移动端 App 的</strong>，它用的是 PWA 版本简化 App。作为文档、笔记类的应用，Outline 的 PWA 版本体验能做到媲美原生 App 体验也是挺牛的。</p>
<p>当然实测下来 BUG 同样有一些，例如 Icon 选择器无法滚动、编辑模式光标聚焦到最底部工具条会被遮挡等小问题。</p>
<p>Outline 把最好的体验留给了桌面端，PC 浏览器和 PC 客户端几乎拥有一致的体验，因而小剧甚至更偏向于使用 Web 端而非客户端。</p>
<p><strong>图二：分不清是 Web 端还是客户端的截图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2024/outline-docs/screenshot.jpg" alt="分不清是 Web 端还是客户端的截图" /></p>
<h3 id="-3">文档组织逻辑</h3>
<p>需要注意的是 Outline 的文档管理和我们常见的应用有轻微差异。</p>
<p>笔记类应用一般是单层目录结构，也就是【文件夹 &gt; 笔记】的结构。</p>
<p>国内多数的文档产品是多级目录结构，每级目录下都可以存放文档。</p>
<p>我们常用的 Confluence Wiki 为文档嵌套结构，也就是每级目录都是一篇文档，每篇文档都可以再嵌套子文档。</p>
<p><strong>图三：Outline 目录结构（红圈内为文档集，其余均是文档）</strong></p>
<p><img src="https://static.bh-lay.com/blog/2024/outline-docs/039e5837-3eb0-4533-a716-3ac6f0fc3e97.jpg" alt="Outline 目录结构" /></p>
<p>Outline 对文档的组织结构比较特殊，它是【Collection &gt; 文档 &gt; 文档】结构。和传统笔记类的产品一样，它只有只有一级文件夹。但是每一篇文档又可以再添加子文档，最多 6 层，嵌套逻辑又和 Wiki 很相似。</p>
<p>这种文档组织方式还没有深入使用，尚不好评价是优是劣，但给人耳目一新的感觉。</p>
<h3 id="-4">总体评价如何</h3>
<p>Outline 给我整体的使用体验是：<strong>界面简洁、功能丰富、体验丝滑</strong>。</p>
<h2 id="-5">打算持续使用不？</h2>
<p>早期部署的 Wiki.js 因为定位为 Wiki 站点发布而非团队知识库建设的平台，体验上较为繁琐。</p>
<p>相比之下 Outline 更为丝滑。</p>
<p>接下来几天，打算把原来 Wiki.js 里的内容全迁到这里后，就可以把玩客云小机器彻底下线了。</p>
<p>最后再把讯飞文档里的个人文档全部迁过来。</p>
<p>讯飞文档：<a href="https://iflydocs.com">iflydocs.com</a></p>
<p>对了，讯飞文档是小剧从头参与开发款一款文档类产品，如果你有多人协作的需求，又不想折腾私有化，更不想自行承担数据维护的成本以及内容丢失的风险，讯飞文档是众多可选项中还算不错的选择。</p>
<p><strong>「跑题了，老东家记得打钱。」</strong></p>
<p>小剧个人的文档经历了早早早早期的 Evernote，之后的有道云笔记、石墨文档、老东家的讯飞笔记、讯飞文档、硬盘存储 Markdown 等一堆零散的管理方式。</p>
<p>每个都挺好，都是小剧不同阶段个人知识库的缩影。但小剧的个人内容散落在个各个角落，终归需要有个集中管理的地方，Outline 目前看起来就是那个地方。</p>
<p>打算持续使用 Outline 不？</p>
<p>目前看起来还不错哦，先用用看吧～</p>]]></content:encoded>
    </item>
    <item>
      <title>More CSS, less JS</title>
      <link>https://bh-lay.com/blog/zk8wczsj2v</link>
      <description><![CDATA[<p>近来给博客 UI 做了小的局部改版，给博客正文的布局更改为“剧场模式”。其实和原来的布局并没有太大的区别，只是将封面图做了更大面积的展示。</p>
<p>样式的调整不复杂，并没有什么值得分享的。但在改 UI 的时候，却发现小剧客栈曾经很多让小剧“引以为傲”的实现，正慢慢变为陈旧的“顽疾”。</p>
<p>简单来说，就是站在 2024 年，博客里很多借助于 JS 实现的效果，都可以由 CSS 来实现了。</p>
<p><strong>新的“剧场模式”博文截图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2024//css-js/blog-after.jpg" alt=" 新的“剧场模式”博文截图" /></p>
<p><strong>之前的博文界面截图</strong>
<img src="https://static.bh-lay.com/blog/2024//css-js/blog-before.jpg" alt="之前的博文界面截图" /></p>
<h2 id="css">一、为什么要改为 CSS 实现？</h2>
<p>单纯实现上图的效果并不难，但是在实际动手改代码时，却意识到之前的实现比较僵硬。</p>
<p>为了更好的兼容性，早期版本的底图模糊效果是借助于 JS 来实现的。</p>
<p>原理很简单，在模糊逻辑入口，会检测显示区域的尺寸。等待原始图片加载后，构建 canvas，并且设置为和可视区域同等大小，然后将图片居中绘制在 Canvas 上。</p>
<p>覆盖一层 0.4 不透明度的黑色矩形，实现亮度降低的效果。</p>
<p>接下来逐个像素遍历，以像素点周围八像素为半径平均计算执行模糊逻辑。</p>
<p>最后再将 canvas 内容以 base64 的形式读出，设置到 dom 的背景上。</p>
<h3 id="11">1.1、 逻辑有什么问题？</h3>
<p>单纯分析这段逻辑，其实并没有太大问题，甚至像前文提的一样“兼容性很好”。</p>
<p>目前主流的浏览器对 Canvas 支持都挺好，并且模糊计算为常规 JS 逻辑，不涉及到特殊的浏览器 API 调用，可以在所有浏览器中正确运行。</p>
<p>问题在于这里的计算是同步的，需要 JS 硬扛，计算量过大或者系统可用资源不足时，页面会变卡。</p>
<p>以 100px * 100px 的显示区域举例，模糊半径为 5 像素。总的像素数为一万个，遍历的步长就是一万。加上 5 像素半径的模糊，以及 Ｒ、G、B、A 四通道的叠加，计算量还需要再上两个量级。</p>
<p>别忘了这里的例子只有 100px * 100px，模糊半径只有 5 像素哦～</p>
<h3 id="12">1.2、有好的方案么？</h3>
<p>最近两年一直在写<a href="http://e.bh-lay.com">小剧起始页</a>这个个人项目。</p>
<p>在小剧起始页里，大量用到了背景模糊的效果，包括底图模糊、界面模糊。</p>
<p>在现代浏览器下，filter 和 backdrop-filter 可以实现很多诸如：模糊、亮度、对比度之类的图像效果。</p>
<p>虽然借助于 GPU 硬件加速比一般样式更耗费性能，但终究比 JS 死扛要好得多。</p>
<h2 id="css-1">二、用 CSS 替代了哪些 ？</h2>
<p>上面的例子只是在开发博文“剧场模式”时，遇到的最直观的问题。</p>
<p>可改可不改，但不改心里总是痒痒的。</p>
<p>以小剧钻牛角尖的个性，自然第一件事拿它开刀。在写“剧场模式”之前就急不可耐地删了 JS 模糊逻辑。</p>
<p>顺着博文底图模糊效果的由头，小剧又对 TOC sticky 效果、导航粘滞效果做了实现上的替换。</p>
<h3 id="21">2.1、博文底图模糊</h3>
<p>前面一直提到的例子就是博文背景图片模糊效果了。</p>
<p>CSS 的模糊其实可以对任意 Dom 生效，而非仅仅针对图片。</p>
<p>要实现图片模糊一般有两种方案。</p>
<p>一是使用 filter 属性，直接在图片上应用滤镜。</p>
<pre><code class="css language-css">img {
  // 模糊 8px，亮度70%
  filter: blur(8px) brightness(0.7);
}
</code></pre>
<p>第二种方案是在图片上层设置蒙层，使用 backdrop-filter 对覆盖区域设置模糊。</p>
<p>这种方法更为灵活，并且可以实现部分区域模糊的毛玻璃效果。</p>
<pre><code class="css language-css">.overlay {
  // 模糊 8px，亮度70%
  backdrop-filter: blur(8px) brightness(0.7);
}
</code></pre>
<p>此次博文“剧场模式”的底图模糊效果，使用了 backdrop-filter 来实现。</p>
<p>另外在使用 CSS 替换 JS 实现效果的同时，发现这么做还有另一个优势，就是针对多端的响应式实现特别容易。</p>
<p>之前手机版不模糊效果就很难实现。因为手机在横竖屏间切换时，底图也需要在模糊与不模糊之间切换，使用 JS 实现需要频繁的进行模糊的计算与 Dom 修改。</p>
<p>而换为 CSS 实现模糊，仅仅一句 @media 就可以实现响应式的区分。</p>
<h3 id="22tocsticky">2.2、TOC sticky 效果</h3>
<p>sticky 定位布局是一种很灵动的效果。既拥有 static 的稳重，又兼顾 fixed 定位的跳脱，同时又有不脱离父级区域的乖巧。</p>
<p>sticky 布局常应用于侧边栏需要常驻的模块，但是很多无良站长会用它放广告。</p>
<p>如果你在使用电脑端阅读这篇文章，并且是小剧客栈中的原文而非其他平台转载。右侧的 TOC 大纲部分就是 sticky 效果。</p>
<p>或者你可以点击下面链接，预览区域右侧黑色框框，在页面滚动中的效果就是 sticky 效果。</p>
<p><a href="https://codepen.io/bh-lay/pen/QWZaKYe">https://codepen.io/…</a></p>
<p>这种效果早在 <code>position: fixed</code> 支持都不是很完全的十几年前，就被大范围使用了。</p>
<p>因为效果很特殊，需要考虑 Dom 在 static 模式下与浏览器视口的关系。同时还要兼顾滚动到可视区域外时，父级与浏览器视口的关系。</p>
<p>一直以来 sticky 效果都是基于 JS 实现的，各类 UI 框架几乎也都会有自己的实现。</p>
<p>小剧在十年前写过一个 tie.js，用来实现 sticky 效果，一直沿用至今。</p>
<p><a href="https://github.com/bh-lay/tie.js/blob/master/src/tie.js">https://github.com/bh-lay/tie.js</a></p>
<p>前端有一个经典的面试题，是介绍 position 的属性。一般答到 static、relative、absolute、fixed 以及对应的特性就足够了。如果能再追加后面这句，可能会更完整。</p>
<p><strong>火狐浏览器还支持 sticky 布局。</strong></p>
<p>可能是最近几年没有关注 sticky 属性的发展，直到好友 Mofei 去年分享 <a href="https://www.zhuwenlong.com/zh/blog/article/fdeef27ce80711eda19000163e1c4b8d">《CSS position: sticky》</a> 时才发现，CSS sticky 布局的兼容性已经达到了可以在生产环境使用的条件。</p>
<p><strong>position sticky caniuse 截图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2024/css-js/caniuse-sticky.png" alt="position sticky caniuse 截图" /></p>
<p>于是在开发博文“剧场模式”的时候，对博文右侧的 TOC 模块做了重构。</p>
<p>删除了 tie.js 的引用，更改为 <code>position: sticky</code> 布局。</p>
<p>当然了，sticky 布局也并不是一行 CSS 就能搞得定，需要每一层父级放行对 overflow 的定义。</p>
<p>具体细节可以参考 Mofei 分享的 <a href="https://www.zhuwenlong.com/zh/blog/article/fdeef27ce80711eda19000163e1c4b8d">《CSS position: sticky》</a> 。</p>
<h3 id="23">2.3、导航的粘滞效果</h3>
<p>这是小剧客栈使用过很多年的一个效果，如果你是在其他网站看到的转载博文，请回到<a href="http://bh-lay.com">小剧客栈</a>使用电脑或 Pad 体验效果。</p>
<p>效果很简单，就是当页面在初始位置时，导航条以最小形态展示，并且会和顶部保持一点点距离。这样子设计，在配合页面底图显示的时候会更协调。</p>
<p>当页面开始滚动时，导航条会慢慢的吸附到顶部位置，并且会默默恢复成通栏的宽度。</p>
<p>想要这种效果，最常规的方法一定是监听窗口的滚动事件，再配合滚动位置来实现。似乎也没有更好的方案了，小剧也正是这么实现的。</p>
<p>近期接触到 Scroll-driven Animations，发现 CSS 已经愈发变成小剧不认识的样子了。虽然目前滚动驱动动画的兼容性，还没有广到可以随意使用的程度，但对于这种锦上添花的交互还是值得一试的。</p>
<p><strong>滚动驱动动画兼容性截图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2024/css-js/caniuse-scroll-animation.png" alt="滚动驱动动画兼容性截图" /></p>
<p>滚动驱动动画（Scroll-driven Animations）的字面含义非常直白，就是使用 CSS 实现页面滚动时的动画效果。</p>
<p>利用这个特性可以实现很多不可思议的效果。感兴趣的话可以查看 <a href="https://scroll-driven-animations.style/">https://scroll-driven-animations.style/</a> 这个站点，收录了很多有趣的滚动动画。</p>
<p>小剧这里只是做了很小的应用，删掉了原本页面滚动的 JS 监听，使用 CSS 实现这个小魔法。</p>
<h2 id="">三、省流版，到底改了啥 ？</h2>
<p>就三点：</p>
<ol>
<li>CSS 模糊替代 JS 模糊</li>
<li>CSS sticky 布局，替代 JS 计算</li>
<li>CSS Scroll-driven Animation 替代 JS 滚动监听</li>
</ol>
<h3 id="31">3.1、代码提交记录</h3>
<ul>
<li><p>feat: develop theater mode for blog article
<a href="https://github.com/bh-lay/blog/pull/168/commits/6df960761f4312a07b4b68d4a5a958d4bcf58060">https://github.com/bh-lay/blog/pull/168</a></p></li>
<li><p>feat: remove tie module
<a href="https://github.com/bh-lay/blog/pull/170/commits/de21226c97f92967b3839590211446c626a97618">https://github.com/bh-lay/blog/pull/170</a></p></li>
<li><p>feat: navigation use scroll driven animation
<a href="https://github.com/bh-lay/blog/pull/172/commits/b5787ccea48a8cf5a2459775cab8abd1e34c80bb">https://github.com/bh-lay/blog/pull/172</a></p></li>
</ul>
<h2 id="css-2">四、CSS 还有哪些新鲜玩意儿？</h2>
<p>2024 是小剧工作的的第十二年，前端也由 2012 年 IE6 时代，在经历过浏览器大混战之后慢慢趋于统一。</p>
<p>在 CSS3 一统江湖之后，小剧对 CSS 的关注也越来越弱。</p>
<p>甚至一度觉得 CSS 的发展已经到了“尽头”。近些年一直埋头在做业务，偶尔关注下浏览器的新特性、框架的新功能。直到最近重新关注起 CSS，才发现它也没有停滞不前。</p>
<p>比如前文提到的 CSS 滤镜、sticky 定位、Scroll-driven Animation，还有个在个人项目中用到的：CSS 变量、Grid 布局。</p>
<p>甚至小剧在 N 年前就幻想过的容器查询，已经更方便更丰富地支持了。</p>
<p>这些特性都是一份份草案背后的无数提交者，在经历过无数个日夜努力之后，才形成标准的一部分。</p>
<p>而小剧作为 Web 应用开发者，可以在简单的阅读文档后“坐享其成”。</p>
<p><strong>这次大面积的删掉 JS，可惜么？</strong></p>
<p>能够用更简单的方法，用 CSS 来书写本该属于样式的代码，对小剧来说是极其符合心智的，也是能够刺激到爽点的事情。</p>
<p>虽说被删掉的 JS 代码，在当下来看略显“陈旧”。但在五年前、十年前，甚至更早些时候，却是实现效果必不可少的手段。</p>
<p>CSS 依旧在发展，现如今依旧有些样式需要借助 JS 来实现。将来可能用 JS 辅助样式的代码会越来越少，但至少当下，这部分 JS 仍有意义。</p>
<p>所以这次大面积的删掉 JS，可惜么？</p>
<p>挺可惜的，可惜这些好用的 CSS 特性没能来得更早一些。</p>
<hr />
<p><strong>写在最后：</strong></p>
<p>这篇记录的小水文发布前夕，刚好大漠叔叔发布了<a href="https://juejin.cn/post/7377355452890726409">《2024，该放弃框架来实现 Web 布局了》</a>。</p>
<p>文章内容和小剧要表达的主题高度重合，只是小剧这篇小水文，仅限于记录小剧此次的小改动，而大漠这篇文章更系统地介绍了，在 2024 年的现在，CSS 能干的更多的事情，推荐阅读。</p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Mon, 10 Jun 2024 16:38:26 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/zk8wczsj2v</guid>
      <category>CSS</category>
      <enclosure url="http://static.bh-lay.com/blog/2024/css-js/cover.jpg" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>近来给博客 UI 做了小的局部改版，给博客正文的布局更改为“剧场模式”。其实和原来的布局并没有太大的区别，只是将封面图做了更大面积的展示。</p>
<p>样式的调整不复杂，并没有什么值得分享的。但在改 UI 的时候，却发现小剧客栈曾经很多让小剧“引以为傲”的实现，正慢慢变为陈旧的“顽疾”。</p>
<p>简单来说，就是站在 2024 年，博客里很多借助于 JS 实现的效果，都可以由 CSS 来实现了。</p>
<p><strong>新的“剧场模式”博文截图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2024//css-js/blog-after.jpg" alt=" 新的“剧场模式”博文截图" /></p>
<p><strong>之前的博文界面截图</strong>
<img src="https://static.bh-lay.com/blog/2024//css-js/blog-before.jpg" alt="之前的博文界面截图" /></p>
<h2 id="css">一、为什么要改为 CSS 实现？</h2>
<p>单纯实现上图的效果并不难，但是在实际动手改代码时，却意识到之前的实现比较僵硬。</p>
<p>为了更好的兼容性，早期版本的底图模糊效果是借助于 JS 来实现的。</p>
<p>原理很简单，在模糊逻辑入口，会检测显示区域的尺寸。等待原始图片加载后，构建 canvas，并且设置为和可视区域同等大小，然后将图片居中绘制在 Canvas 上。</p>
<p>覆盖一层 0.4 不透明度的黑色矩形，实现亮度降低的效果。</p>
<p>接下来逐个像素遍历，以像素点周围八像素为半径平均计算执行模糊逻辑。</p>
<p>最后再将 canvas 内容以 base64 的形式读出，设置到 dom 的背景上。</p>
<h3 id="11">1.1、 逻辑有什么问题？</h3>
<p>单纯分析这段逻辑，其实并没有太大问题，甚至像前文提的一样“兼容性很好”。</p>
<p>目前主流的浏览器对 Canvas 支持都挺好，并且模糊计算为常规 JS 逻辑，不涉及到特殊的浏览器 API 调用，可以在所有浏览器中正确运行。</p>
<p>问题在于这里的计算是同步的，需要 JS 硬扛，计算量过大或者系统可用资源不足时，页面会变卡。</p>
<p>以 100px * 100px 的显示区域举例，模糊半径为 5 像素。总的像素数为一万个，遍历的步长就是一万。加上 5 像素半径的模糊，以及 Ｒ、G、B、A 四通道的叠加，计算量还需要再上两个量级。</p>
<p>别忘了这里的例子只有 100px * 100px，模糊半径只有 5 像素哦～</p>
<h3 id="12">1.2、有好的方案么？</h3>
<p>最近两年一直在写<a href="http://e.bh-lay.com">小剧起始页</a>这个个人项目。</p>
<p>在小剧起始页里，大量用到了背景模糊的效果，包括底图模糊、界面模糊。</p>
<p>在现代浏览器下，filter 和 backdrop-filter 可以实现很多诸如：模糊、亮度、对比度之类的图像效果。</p>
<p>虽然借助于 GPU 硬件加速比一般样式更耗费性能，但终究比 JS 死扛要好得多。</p>
<h2 id="css-1">二、用 CSS 替代了哪些 ？</h2>
<p>上面的例子只是在开发博文“剧场模式”时，遇到的最直观的问题。</p>
<p>可改可不改，但不改心里总是痒痒的。</p>
<p>以小剧钻牛角尖的个性，自然第一件事拿它开刀。在写“剧场模式”之前就急不可耐地删了 JS 模糊逻辑。</p>
<p>顺着博文底图模糊效果的由头，小剧又对 TOC sticky 效果、导航粘滞效果做了实现上的替换。</p>
<h3 id="21">2.1、博文底图模糊</h3>
<p>前面一直提到的例子就是博文背景图片模糊效果了。</p>
<p>CSS 的模糊其实可以对任意 Dom 生效，而非仅仅针对图片。</p>
<p>要实现图片模糊一般有两种方案。</p>
<p>一是使用 filter 属性，直接在图片上应用滤镜。</p>
<pre><code class="css language-css">img {
  // 模糊 8px，亮度70%
  filter: blur(8px) brightness(0.7);
}
</code></pre>
<p>第二种方案是在图片上层设置蒙层，使用 backdrop-filter 对覆盖区域设置模糊。</p>
<p>这种方法更为灵活，并且可以实现部分区域模糊的毛玻璃效果。</p>
<pre><code class="css language-css">.overlay {
  // 模糊 8px，亮度70%
  backdrop-filter: blur(8px) brightness(0.7);
}
</code></pre>
<p>此次博文“剧场模式”的底图模糊效果，使用了 backdrop-filter 来实现。</p>
<p>另外在使用 CSS 替换 JS 实现效果的同时，发现这么做还有另一个优势，就是针对多端的响应式实现特别容易。</p>
<p>之前手机版不模糊效果就很难实现。因为手机在横竖屏间切换时，底图也需要在模糊与不模糊之间切换，使用 JS 实现需要频繁的进行模糊的计算与 Dom 修改。</p>
<p>而换为 CSS 实现模糊，仅仅一句 @media 就可以实现响应式的区分。</p>
<h3 id="22tocsticky">2.2、TOC sticky 效果</h3>
<p>sticky 定位布局是一种很灵动的效果。既拥有 static 的稳重，又兼顾 fixed 定位的跳脱，同时又有不脱离父级区域的乖巧。</p>
<p>sticky 布局常应用于侧边栏需要常驻的模块，但是很多无良站长会用它放广告。</p>
<p>如果你在使用电脑端阅读这篇文章，并且是小剧客栈中的原文而非其他平台转载。右侧的 TOC 大纲部分就是 sticky 效果。</p>
<p>或者你可以点击下面链接，预览区域右侧黑色框框，在页面滚动中的效果就是 sticky 效果。</p>
<p><a href="https://codepen.io/bh-lay/pen/QWZaKYe">https://codepen.io/…</a></p>
<p>这种效果早在 <code>position: fixed</code> 支持都不是很完全的十几年前，就被大范围使用了。</p>
<p>因为效果很特殊，需要考虑 Dom 在 static 模式下与浏览器视口的关系。同时还要兼顾滚动到可视区域外时，父级与浏览器视口的关系。</p>
<p>一直以来 sticky 效果都是基于 JS 实现的，各类 UI 框架几乎也都会有自己的实现。</p>
<p>小剧在十年前写过一个 tie.js，用来实现 sticky 效果，一直沿用至今。</p>
<p><a href="https://github.com/bh-lay/tie.js/blob/master/src/tie.js">https://github.com/bh-lay/tie.js</a></p>
<p>前端有一个经典的面试题，是介绍 position 的属性。一般答到 static、relative、absolute、fixed 以及对应的特性就足够了。如果能再追加后面这句，可能会更完整。</p>
<p><strong>火狐浏览器还支持 sticky 布局。</strong></p>
<p>可能是最近几年没有关注 sticky 属性的发展，直到好友 Mofei 去年分享 <a href="https://www.zhuwenlong.com/zh/blog/article/fdeef27ce80711eda19000163e1c4b8d">《CSS position: sticky》</a> 时才发现，CSS sticky 布局的兼容性已经达到了可以在生产环境使用的条件。</p>
<p><strong>position sticky caniuse 截图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2024/css-js/caniuse-sticky.png" alt="position sticky caniuse 截图" /></p>
<p>于是在开发博文“剧场模式”的时候，对博文右侧的 TOC 模块做了重构。</p>
<p>删除了 tie.js 的引用，更改为 <code>position: sticky</code> 布局。</p>
<p>当然了，sticky 布局也并不是一行 CSS 就能搞得定，需要每一层父级放行对 overflow 的定义。</p>
<p>具体细节可以参考 Mofei 分享的 <a href="https://www.zhuwenlong.com/zh/blog/article/fdeef27ce80711eda19000163e1c4b8d">《CSS position: sticky》</a> 。</p>
<h3 id="23">2.3、导航的粘滞效果</h3>
<p>这是小剧客栈使用过很多年的一个效果，如果你是在其他网站看到的转载博文，请回到<a href="http://bh-lay.com">小剧客栈</a>使用电脑或 Pad 体验效果。</p>
<p>效果很简单，就是当页面在初始位置时，导航条以最小形态展示，并且会和顶部保持一点点距离。这样子设计，在配合页面底图显示的时候会更协调。</p>
<p>当页面开始滚动时，导航条会慢慢的吸附到顶部位置，并且会默默恢复成通栏的宽度。</p>
<p>想要这种效果，最常规的方法一定是监听窗口的滚动事件，再配合滚动位置来实现。似乎也没有更好的方案了，小剧也正是这么实现的。</p>
<p>近期接触到 Scroll-driven Animations，发现 CSS 已经愈发变成小剧不认识的样子了。虽然目前滚动驱动动画的兼容性，还没有广到可以随意使用的程度，但对于这种锦上添花的交互还是值得一试的。</p>
<p><strong>滚动驱动动画兼容性截图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2024/css-js/caniuse-scroll-animation.png" alt="滚动驱动动画兼容性截图" /></p>
<p>滚动驱动动画（Scroll-driven Animations）的字面含义非常直白，就是使用 CSS 实现页面滚动时的动画效果。</p>
<p>利用这个特性可以实现很多不可思议的效果。感兴趣的话可以查看 <a href="https://scroll-driven-animations.style/">https://scroll-driven-animations.style/</a> 这个站点，收录了很多有趣的滚动动画。</p>
<p>小剧这里只是做了很小的应用，删掉了原本页面滚动的 JS 监听，使用 CSS 实现这个小魔法。</p>
<h2 id="">三、省流版，到底改了啥 ？</h2>
<p>就三点：</p>
<ol>
<li>CSS 模糊替代 JS 模糊</li>
<li>CSS sticky 布局，替代 JS 计算</li>
<li>CSS Scroll-driven Animation 替代 JS 滚动监听</li>
</ol>
<h3 id="31">3.1、代码提交记录</h3>
<ul>
<li><p>feat: develop theater mode for blog article
<a href="https://github.com/bh-lay/blog/pull/168/commits/6df960761f4312a07b4b68d4a5a958d4bcf58060">https://github.com/bh-lay/blog/pull/168</a></p></li>
<li><p>feat: remove tie module
<a href="https://github.com/bh-lay/blog/pull/170/commits/de21226c97f92967b3839590211446c626a97618">https://github.com/bh-lay/blog/pull/170</a></p></li>
<li><p>feat: navigation use scroll driven animation
<a href="https://github.com/bh-lay/blog/pull/172/commits/b5787ccea48a8cf5a2459775cab8abd1e34c80bb">https://github.com/bh-lay/blog/pull/172</a></p></li>
</ul>
<h2 id="css-2">四、CSS 还有哪些新鲜玩意儿？</h2>
<p>2024 是小剧工作的的第十二年，前端也由 2012 年 IE6 时代，在经历过浏览器大混战之后慢慢趋于统一。</p>
<p>在 CSS3 一统江湖之后，小剧对 CSS 的关注也越来越弱。</p>
<p>甚至一度觉得 CSS 的发展已经到了“尽头”。近些年一直埋头在做业务，偶尔关注下浏览器的新特性、框架的新功能。直到最近重新关注起 CSS，才发现它也没有停滞不前。</p>
<p>比如前文提到的 CSS 滤镜、sticky 定位、Scroll-driven Animation，还有个在个人项目中用到的：CSS 变量、Grid 布局。</p>
<p>甚至小剧在 N 年前就幻想过的容器查询，已经更方便更丰富地支持了。</p>
<p>这些特性都是一份份草案背后的无数提交者，在经历过无数个日夜努力之后，才形成标准的一部分。</p>
<p>而小剧作为 Web 应用开发者，可以在简单的阅读文档后“坐享其成”。</p>
<p><strong>这次大面积的删掉 JS，可惜么？</strong></p>
<p>能够用更简单的方法，用 CSS 来书写本该属于样式的代码，对小剧来说是极其符合心智的，也是能够刺激到爽点的事情。</p>
<p>虽说被删掉的 JS 代码，在当下来看略显“陈旧”。但在五年前、十年前，甚至更早些时候，却是实现效果必不可少的手段。</p>
<p>CSS 依旧在发展，现如今依旧有些样式需要借助 JS 来实现。将来可能用 JS 辅助样式的代码会越来越少，但至少当下，这部分 JS 仍有意义。</p>
<p>所以这次大面积的删掉 JS，可惜么？</p>
<p>挺可惜的，可惜这些好用的 CSS 特性没能来得更早一些。</p>
<hr />
<p><strong>写在最后：</strong></p>
<p>这篇记录的小水文发布前夕，刚好大漠叔叔发布了<a href="https://juejin.cn/post/7377355452890726409">《2024，该放弃框架来实现 Web 布局了》</a>。</p>
<p>文章内容和小剧要表达的主题高度重合，只是小剧这篇小水文，仅限于记录小剧此次的小改动，而大漠这篇文章更系统地介绍了，在 2024 年的现在，CSS 能干的更多的事情，推荐阅读。</p>]]></content:encoded>
    </item>
    <item>
      <title>家庭服务器改造记录</title>
      <link>https://bh-lay.com/blog/btz0m0kbl3</link>
      <description><![CDATA[<blockquote>
  <p><strong>写在最前面</strong></p>
  <p>这是一篇单纯记录小剧此次家庭服务器改造的文章。</p>
  <p>对 NAS、家庭服务器的很多理解个人色彩很浓，方案还没有足够长的时间去验证，因此对你的选择不具备指导意义。</p>
  <p>希望你能在小剧这里找到一丝丝可以借鉴的思路，或者可以当做谈资的笑话。</p>
</blockquote>
<p>自从春节前夕，小剧对家庭服务器做较大的改造后，近几个月又慢慢新增、更新了部分小玩意儿，简单回顾下这次改造的过程。</p>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/home-server.jpg" alt="家庭服务器全景" /></p>
<h2 id="">一、什么是家庭服务器？</h2>
<p>可能有些小伙伴会比较懵逼，服务器很容易理解，家庭服务器是什么鬼？</p>
<p>再或者玩过 NAS 的小伙伴会觉得，这不就是 NAS 么，有什么值得分享的？</p>
<p>确实，小剧搭建的家庭服务器和中年老男人口中的 NAS 看起来很像，甚至掰碎了来看零散的碎片都很相似，但在小剧眼中他们却是完全不同的概念。</p>
<h3 id="11nas">1.1、什么是 NAS ？</h3>
<p>NAS 的全称是 Network Attached Storage，中文对应的翻译是：网络附属存储。</p>
<p>字面理解就是：部署在网络上的存储设备。</p>
<p>这里的网络并不强调是广域网还是局域网。既然是存储设备，势必要用到各种存储介质，比如机械硬盘、固态硬盘等，以及可能还需要由这些介质组成各种阵列。</p>
<p>当然了，作为一台部署在网络上的存储设备，单纯依靠一根网线并不能实现文件的存与取，还需要借助于一些基于 SMB、WebDav、NFS 等协议实现的服务，来完成各设备间文件的共享。</p>
<p>以上就是我理解的 NAS 最核心、最原始的部分。</p>
<p>然而当你打开 B 站，或者在各个电商平台搜索 NAS。扑面而来的都是 4K 解码、远程下载、AI 相册、智能家居等一堆花里胡哨的东西。</p>
<p>其实这种情况可以理解，毕竟作为一款存储设备，在进入家用消费端的时候。势必要提升上手的易用性、增强单体设备的可扩展性。</p>
<p>在完成最基础的存储功能之上，支持 Docker 或者内置一些服务可以很好的提升产品的竞争力。</p>
<h3 id="12">1.2、小剧认为的家庭服务器是什么？</h3>
<p>Web 开发出身的小剧，最乐于见到的就是可以自由拆分、组合的服务模型了。
作为定位在家庭场景下使用的服务器，最常见的需求就是照片备份、影音存储、文件备份。可能还会有些小伙伴喜欢折腾智能家居、私有化文档、小说阅读等小众服务。</p>
<p>所以家庭服务器可以是一台机器，也可以是多台机器。基于这些机器搭建起一套适合自己家用的各种服务。</p>
<p>“你看看，你看看，这不是跟上面的 NAS 一模一样么？”</p>
<blockquote>
  <p>小剧搭建的家庭服务器和中年老男人口中的 NAS 看起来很像，甚至掰碎了来看零散的碎片都很相似。</p>
</blockquote>
<p>上面也提到了，这两者放在一起比较，确实很像。</p>
<p>小剧真正想表达的是，市面上对于 NAS 理解，已经远远脱离了 NAS 本身。当你在使用 NAS 设备提供的 Docker 搭建各种服务的时候，你在做的操作其实已经是服务的部署与维护，而非单纯的使用 NAS。</p>
<h3 id="13">1.3、较这个劲区分它干啥 ？</h3>
<p>确实是这样，对于绝大多数家庭来说，成品 NAS 绝对是最好的选择。</p>
<p>借助于一个单体设备，满足家庭存储的绝大多数需求。简单、方便、不用折腾，出了问题还会有客服在线解答。</p>
<p>但是如果能把存储和服务分开理解，或者把存储作为服务的其中一部分来理解。对于选购什么样的设备有非常大的帮助。</p>
<p>为什么这么说呢 ？</p>
<p>因为 NAS 的核心功能是存储。硬盘位数、支持的最大容量、有没有高速缓存，这些配置能直接影响存储性能、存储安全性和存储容量上限。</p>
<p>作为锦上添花的各类服务，需要更大的内存，更高效的 CPU，甚至独立的 GPU 芯片、额外的网卡接口来做支撑。</p>
<p>存储方面各家计价方式几乎没有差别，而非核心的后者才是最让人苦恼的地方。比如多一块 GPU 芯片可能整体价格就会翻倍，而你一旦前期没有选配，后期想要实现一些如 AI 能力或者编解码就会效果大打折扣，甚至无法实现。</p>
<p>上面的分析就是从“存算分离”的架构思路去理解的。</p>
<p>【存】指的是大量消耗硬盘的数据存储服务。</p>
<p>【算】指的是以消耗 CPU、内存等其它计算资源为主的服务，如 AI 相册、智能家居之类。</p>
<p>存和算都是服务的一部分。借助于这个思路，结合市面上的成品，再审视下自己的需求，大概率就能选择到一款适合自己的方案。</p>
<h2 id="-1">二、小剧的需求是什么？</h2>
<p>好的方案不仅要考虑实际需要，还要结合个人财力。</p>
<p>小剧财力这一块没问题，只是实际需求中的预算不高而已（狗头🐶）。</p>
<h3 id="21">2.1、关于容量</h3>
<p>小剧没有收藏癖，不是很热衷于保存大量的影视资源，基本上都是需要看什么就临时下载。</p>
<p>目前硬盘里保存的绝大多数都是和个人、家庭相关的文件。以相册、文档为主，总量也只有 720G 左右。</p>
<p><strong>“因此存储虽然是小剧的核心诉求，但对容量要求并不高。”</strong></p>
<h3 id="22web">2.2、关于 Web 服务</h3>
<p>在 2023 年，小剧曾经写过 <a href="https://bh-lay.com/blog/27dmws2c3hx">《Mongo DB 服务挂起问题排查》</a>，介绍了 Mongo DB 借助于 Docker 部署在了 2013 款 Mac mini 上。使用 frp 与公网服务器上的 NodeJS 服务通信。</p>
<p>因此希望新的服务器同样能够支持 Docker，这样十几年前的古董 Mac mini 就可以成功退役了。</p>
<p>你可能知道，小剧是个爱到处拍拍拍的人。小剧习惯于用【年 &gt; 年月日-事件名 &gt; 照片】这样三层目录结构来备份照片。这是小剧用了很多年的照片备份方案。从早些年的 U 盘备份，到后来的移动硬盘（机械）备份，再到网盘、NAS 阶段皆如此。</p>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/original-photo-store.jpg" alt="原始照片备份模式" /></p>
<p>照片虽然总容量不大，但碎片化的文件特别难以管理。尤其是命名文件夹是件费脑子的事，在绝大多数没有明确主题的日子里产生的照片，总是被丢进类似于【二月】、【五月】这样的毫无特征的文件夹里。</p>
<p>更烦人的是很难分得清手机里，哪些照片已经备份了，需要一次次一张张的去核对，或者无脑上传一遍遍。</p>
<p>在古董 Mac mini 里浅尝过 PhotoView，对照片管理并没有太大帮助。研究过 PhotoPrism，功能看起来很强，但 UI 给我的感觉糙糙的。最终还是 Immich 惊艳到了我，功能强大且多端支持，UI 很精细，支持手机相册备份、重复照片检测、人脸识别、照片搜索等功能，有机会小剧单独介绍它。</p>
<p>但是 Immich 在我那古董 Mac mini上始终无法正常使用，希望新的服务器可以顺利运行 Immich。</p>
<h3 id="23">2.3、关于数据安全</h3>
<p>很多小伙伴可能会被 NAS 商家宣传的各种 RIAD 模式所迷惑。花费了大量时间研究不同的阵列模式，以及对应的安全性。</p>
<p>不可否认，Raid 的设计很精妙，在兼顾数据安全和容量上可以找到很多不同的平衡点。</p>
<p>但即使用安全等级最高的 Raid1 模式，也只是在尽可能的增强单体设备的安全性。而通常情况下，多设备同步模式的系统安全更为可靠。</p>
<p>例如火灾、水淹、盗抢、意外跌落等情况发生时，单体设备的安全系数再高，也无法保证数据的安全性。</p>
<p>因此小剧希望新的方案依旧可以支持多设备同步，甚至云端同步。</p>
<h3 id="24">2.4、关于其他指标</h3>
<h4 id="241">2.4.1、体积、噪音、功耗</h4>
<p>这几个指标应该是家用和商用最大的区别。因为小剧家的户型并不大，并没有足够的空间容纳形如机架服务器、机箱服务器之类的服务器硬件。</p>
<p>另外当我们身处在家里时，有一半是夜深人静的睡眠时间。小剧并不希望嗡嗡嗡的散热风扇声，或者时不时的“炒豆子”声打扰家人的清梦。</p>
<p>可能很多小伙伴并没有意识到，一味的追求各种指标、配置，除了一次性的硬件购买成本外，7 * 24 小时运行所产生的功耗，也是一个非常大的成本。以 40W 功耗差、0.5 元每度电的成本来算，一年下来就要多花将近两百块（好像也不多）。</p>
<h4 id="242">2.4.2、使用速度</h4>
<p>这也是一条让大家拼命掏空口袋的指标。各家厂商会在缓存、CPU、阵列模式、网卡上做速度提升，代价就是你得支付远超对应硬件的价格。</p>
<p>并且千兆、2.5G、万兆等网速的支持，还得依赖家庭网络环境，以及使用设备的网速上限。</p>
<p>小剧对此并没有太高要求，能跑满家里的千兆网就足够了。</p>
<h2 id="-2">三、改造前家庭服务器长什么样？</h2>
<h3 id="31">3.1、之前的主力存储</h3>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/home-server-origin.jpg" alt="家庭服务器改造前的样子" /></p>
<p>这是小剧家庭服务器改造前的一部分，设备被安放在 Macbook 的纸盒子里，很简陋也很随意。</p>
<p>小剧使用 H3C M1 的小盒子做家庭主力存储三年了，就是上图中那个白色的小盒子。  </p>
<p>在 <a href="https://bh-lay.com/blog/8hfd4n48pd">《小剧的2021》</a> 中【2.3、照片存储方案探索】部分曾经介绍过这个设备。容量只有 1T、易用性不太够，但贵在稳定，并且可靠性很高。</p>
<p>在小剧看来它是一款被新华三抛弃的家庭存储产品。APP 自小剧使用以来没有一次更新，发布至今也只有五个版本。</p>
<p>新华三虽然在产品线中抛弃了它，但好在 Web 服务一直在运行着。设备的初始化、远程访问等功能依旧能用。</p>
<p>它是一台单硬盘模式的 NAS，不支持安装第三方 APP，内置功能也很有限，甚至部分功能完全不可用。</p>
<h3 id="32">3.2、之前的主力服务器</h3>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/home-server-origin-2.jpg" alt="家庭服务器改造前的 Mac mini" /></p>
<p>这是前文提到的那个 2013 款 Mac mini，原谅小剧没有找到拍摄清晰的照片，并且原安装位置已被拆除，只能找出来这张主要拍倍思电源的照片凑个数。</p>
<p>Mac mini 被安置在书桌侧面，机器很老，配置很低。</p>
<p>但了解的朋友应该知道，这个版本的 Mac mini 内置了一块机械硬盘，同时主板上预留的接口支持加装一块 m2 的固态硬盘，这也是最后一代支持加装内置硬盘的 Mac mini。</p>
<p>可玩性还是有一点点的，吧～</p>
<p>前文提到的 Mongo DB 服务就是运行在这台 Mac mini 上的，早期使用的 PhotoView 同样也是运行在这里。</p>
<p>另外它内置的机械硬盘容量也是 1T，刚好用来备份 H3C m1 里的数据。</p>
<h2 id="-3">四、改造后的方案长什么样？</h2>
<p>这次改造并没有添置太多的设备，只是根据前面提到的小剧对家庭服务器的理解，以及使用过一些设备，对设备做了增减。</p>
<h3 id="41">4.1、购置清单</h3>
<p>这次改造持续了将近三个月，目前算是稳定下来了。细数一下小剧的购置清单，再加上已有的设备成本，其实并不比成品 NAS 花费低，</p>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/procurement-list.jpg" alt="购置清单" /></p>
<ol>
<li>Mac mini m1</li>
<li>Mac mini 支架</li>
<li>USB4 移动硬盘盒</li>
<li>固态硬盘</li>
<li>液冷散热</li>
<li>UPS</li>
</ol>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/home-server-tag.jpg" alt="家庭服务器标记" /></p>
<h3 id="42">4.2、新的主力服务器</h3>
<p>基于小体积、静音、低功耗的要求，小剧这次主力服务依旧采用的是 Mac mini。</p>
<p>只是淘汰了 2013 款的古董级 Mac mini，转而购入了一款 m1 版本的 Mac mini。</p>
<p>8 + 256 丐中丐的配置，日常办公使用可能略微有点性能不足，但作为一台 7 * 24 开机的家庭服务器足够了。</p>
<p>Mac mini 体积足够小，外形足够简洁漂亮，运行无噪音，这几点完全打在了小剧的爽点上。</p>
<p>根据苹果官方的 <a href="https://support.apple.com/zh-cn/103253">Mac mini 功耗和热输出 (BTU) 信息</a> 显示，m1 版本的Mac mini 闲置功率为 6.8W，最大负载时为 39W，妥妥的省电小能手。</p>
<p>在主力服务器 Mac mini 上安装了 Docker Desktop，用来承载前文提到的 Mongo DB 数据库、Immich 等服务。</p>
<p>各种备份任务也是跑在这台主力服务器上。</p>
<h3 id="43">4.3、新的主力存储</h3>
<p>还记得之前的主力存储不，是一台 H3C m1 的存储盒子。</p>
<p>这一次小剧把 H3C m1 降级为备份存储，每隔四小时备份一次主力存储的数据，并且开启了夜间休眠模式。</p>
<p>主力存储为购置的 USB4 移动硬盘盒 + 2T 的 m2 固态硬盘，连接到 Mac mini 上。</p>
<p><strong>咦，你是不是发现一个神奇的爽点 ？</strong></p>
<p>没错，改造后的主力存储和主力服务器合二为一了。</p>
<p>主力服务器之所以选择 m1 版本的 Mac mini，还有另一个原因，是它的雷雳接口支持 USB4 的移动硬盘盒。虽说是移动硬盘，但是直通 PCIE 接口。根据网上大佬的测试，读写速度是 Mac mini 内置硬盘的两倍（小剧未求证）。</p>
<p>局域网内的 SMB 协议文件共享，使用 MacOS 自带的文件共享功能实现； WebDav 的功能借助于 Alist 来完成。</p>
<p>可能你会反驳说：<strong>固态硬盘不适合做重要数据的主力存储。</strong></p>
<p>这个观点我赞同，但它是相对的。</p>
<p>做好冷备份的前提下，哪怕固态硬盘彻底坏掉了，重新再买一个，恢复数据之后一切又能正常跑起来了。</p>
<p>在不关心寿命的前提下，固态硬盘的高速、小体积、无噪音、不惧跌落等特点全都是优点。</p>
<h3 id="44ups">4.4、UPS 不间断电源</h3>
<p>此前两年多经历过很多次断电，都没有遇到过问题。</p>
<p>这次很不幸，断电后 Mac mini 中的 Docker 数据全部丢失。</p>
<p>为了保障服务的稳定性，“斥巨资”购入了 UPS 来为主力服务器 Mac mini 保驾护航。</p>
<p>对小剧这次悲惨的经历感兴趣的话，可以阅读这篇记录。</p>
<p><a href="https://bh-lay.com/blog/1sidzrps0yb">《家庭服务器断电处理记录》</a></p>
<h3 id="45">4.5、加装液冷散热模块</h3>
<p>这次家庭服务器版本改造开始时，是 2024 年春节前夕。</p>
<p>举国欢庆春节时，也是合肥最冷的时候，因此服务器改造初期并没有发现什么不妥。</p>
<p>合肥这座城市有个典型的特点，就是春秋季特别短。三月中旬一过，已然有了夏天的感觉。</p>
<p>这时原本温润的移动硬盘盒悄悄变成了发热的小火山。</p>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/ssd.jpg" alt="固态硬盘盒" /></p>
<p>其实并没有这么夸张，四五十度对于移动硬盘、m2 固态硬盘来说很清凉，可以不用关心它。一想到它在小剧家 7*24h 工作任劳任怨， 适当的降降温也算是对它辛苦劳作的补偿。</p>
<p>调研后有三个方案：</p>
<ol>
<li>加装风扇散热</li>
<li>半导体散热</li>
<li>液冷散热</li>
</ol>
<p>方案一最常见，也立竿见影，但整体效果一般，而且书房窗户朝西，在接下来的夏天应该很难扛下去。</p>
<p>方案二是我最近刚发现的，效果很惊人，甚至能达到滴水成冰的效果。代价是过高的耗电量，以及冷凝水的隐患。对于小剧这种低功耗家庭服务器的环境来说，显然不合适。</p>
<p>方案三效果比风扇好不了太多，但因为冷却液可以挪到相对温度更低的另一面，在接下来的夏天应该能扛的更久一点。因此就选择了液冷作为降温方案。</p>
<p>找了一圈，市面上并没有直接可用的液冷配件。经过筛选，购入了一款手机液冷套装，改造后加装到移动硬盘上。</p>
<p>小剧玩手机液冷散热</p>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/water-cooling.jpg" alt="手机液冷散热器" /></p>
<h3 id="46">4.6、物理空间设计</h3>
<p>看了前面的介绍，相信你也能发现小剧家庭服务器非常零碎。各种设备以一种奇奇怪怪的姿势连接在一起。</p>
<p>想要保持好用、整洁，甚至还能让人说一句“好看”是很困难的。</p>
<p>空间建模</p>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/3d-model.jpg" alt="空间建模" /></p>
<p>为此小剧测量了书柜开放格的整体尺寸，以及要部署的各个设备的尺寸，借助于 iPad 的建模软件做了方案预演。</p>
<p>最终在比较满意的四套方案中，选择了空间利用率最高的一个。</p>
<p>近期虽然对设备进行了增减，但整体布局并没有发生大的改变。</p>
<h2 id="-4">五、写在最后</h2>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/home-server-tag.jpg" alt="家庭服务器标记" /></p>
<h3 id="51">5.1、前文未提到的设备</h3>
<p>前面重点介绍了 Mac mini、移动硬盘盒，附带提到了 H3C m1、UPS、液冷散热这些设备。这里小剧简单介绍下另外几个小黑盒子。</p>
<p><strong>交换机</strong></p>
<p>这个没什么好介绍的， 单纯是为了拓展网口 + 设备互联而已。唯一值得说道的，应该就是插在上面的一根根，由小剧纯手工打造的网线了吧。</p>
<p>统一的颜色、定制的长度，让这个局促的小空间不至于那么杂乱。</p>
<p><strong>蒲公英</strong></p>
<p>这是一个成品的内网穿透盒子，只有一个功能。就是让小剧在外的时候，可以借助于它构建的虚拟专用网络（V*N）访问家庭内网。</p>
<p>免费账号只能设置三台设备使用，这个盒子自己需要占掉一个。也就是说真正可以给小剧自己和家人添加的设备只有两台。</p>
<p><strong>玩客云盒子</strong></p>
<p>图中在移动硬盘盒和 H3C m1 中间的一个小设备。可能有部分小伙伴知道，它可以挂机跑分挣钱。但小剧只是单纯把它当成一个 Armbain 的服务器，跑一些简单的内网 Web 服务。</p>
<p>另外背后的 USB 口用来给蒲公英供电。</p>
<p><strong>神秘小盒子</strong></p>
<p>这是一台 R2S 的小盒子，当作旁路由来用。绝大多数时候都是关机的状态，偶尔需要给 Docker 等服务加速的时候才会开一下。</p>
<p>轻度使用，可有可无。</p>
<h3 id="52">5.2、小剧的抠搜小贴士</h3>
<p>回顾整篇记录，小剧反复提到过很多遍“单体设备”这个词。</p>
<p>翻阅全文后再看市面上对于 NAS 的理解，和小剧口中所谓的“家庭服务器”。最大的差别就是市面上的 NAS 热衷于用一个单体设备来实现完整的功能。</p>
<p>而小剧的家庭服务器方案更倾向于，在下单前先审视自己的需求，再逆向去找满足需求的设备。可以是单体设备，也可以是各种设备的组合。</p>
<p>不给自己生造需求，也不假设自己几乎用不到的峰值性能。随着自己的使用循序渐进，进行设备的更替，让属于自己的方案陪着自己去成长。</p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Wed, 15 May 2024 15:38:09 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/btz0m0kbl3</guid>
      <category>家庭服务器</category>
      <category>Docker</category>
      <category>UPS</category>
      <category>NAS</category>
      <enclosure url="http://static.bh-lay.com/blog/2024/home-server/home-server.jpg" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<blockquote>
  <p><strong>写在最前面</strong></p>
  <p>这是一篇单纯记录小剧此次家庭服务器改造的文章。</p>
  <p>对 NAS、家庭服务器的很多理解个人色彩很浓，方案还没有足够长的时间去验证，因此对你的选择不具备指导意义。</p>
  <p>希望你能在小剧这里找到一丝丝可以借鉴的思路，或者可以当做谈资的笑话。</p>
</blockquote>
<p>自从春节前夕，小剧对家庭服务器做较大的改造后，近几个月又慢慢新增、更新了部分小玩意儿，简单回顾下这次改造的过程。</p>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/home-server.jpg" alt="家庭服务器全景" /></p>
<h2 id="">一、什么是家庭服务器？</h2>
<p>可能有些小伙伴会比较懵逼，服务器很容易理解，家庭服务器是什么鬼？</p>
<p>再或者玩过 NAS 的小伙伴会觉得，这不就是 NAS 么，有什么值得分享的？</p>
<p>确实，小剧搭建的家庭服务器和中年老男人口中的 NAS 看起来很像，甚至掰碎了来看零散的碎片都很相似，但在小剧眼中他们却是完全不同的概念。</p>
<h3 id="11nas">1.1、什么是 NAS ？</h3>
<p>NAS 的全称是 Network Attached Storage，中文对应的翻译是：网络附属存储。</p>
<p>字面理解就是：部署在网络上的存储设备。</p>
<p>这里的网络并不强调是广域网还是局域网。既然是存储设备，势必要用到各种存储介质，比如机械硬盘、固态硬盘等，以及可能还需要由这些介质组成各种阵列。</p>
<p>当然了，作为一台部署在网络上的存储设备，单纯依靠一根网线并不能实现文件的存与取，还需要借助于一些基于 SMB、WebDav、NFS 等协议实现的服务，来完成各设备间文件的共享。</p>
<p>以上就是我理解的 NAS 最核心、最原始的部分。</p>
<p>然而当你打开 B 站，或者在各个电商平台搜索 NAS。扑面而来的都是 4K 解码、远程下载、AI 相册、智能家居等一堆花里胡哨的东西。</p>
<p>其实这种情况可以理解，毕竟作为一款存储设备，在进入家用消费端的时候。势必要提升上手的易用性、增强单体设备的可扩展性。</p>
<p>在完成最基础的存储功能之上，支持 Docker 或者内置一些服务可以很好的提升产品的竞争力。</p>
<h3 id="12">1.2、小剧认为的家庭服务器是什么？</h3>
<p>Web 开发出身的小剧，最乐于见到的就是可以自由拆分、组合的服务模型了。
作为定位在家庭场景下使用的服务器，最常见的需求就是照片备份、影音存储、文件备份。可能还会有些小伙伴喜欢折腾智能家居、私有化文档、小说阅读等小众服务。</p>
<p>所以家庭服务器可以是一台机器，也可以是多台机器。基于这些机器搭建起一套适合自己家用的各种服务。</p>
<p>“你看看，你看看，这不是跟上面的 NAS 一模一样么？”</p>
<blockquote>
  <p>小剧搭建的家庭服务器和中年老男人口中的 NAS 看起来很像，甚至掰碎了来看零散的碎片都很相似。</p>
</blockquote>
<p>上面也提到了，这两者放在一起比较，确实很像。</p>
<p>小剧真正想表达的是，市面上对于 NAS 理解，已经远远脱离了 NAS 本身。当你在使用 NAS 设备提供的 Docker 搭建各种服务的时候，你在做的操作其实已经是服务的部署与维护，而非单纯的使用 NAS。</p>
<h3 id="13">1.3、较这个劲区分它干啥 ？</h3>
<p>确实是这样，对于绝大多数家庭来说，成品 NAS 绝对是最好的选择。</p>
<p>借助于一个单体设备，满足家庭存储的绝大多数需求。简单、方便、不用折腾，出了问题还会有客服在线解答。</p>
<p>但是如果能把存储和服务分开理解，或者把存储作为服务的其中一部分来理解。对于选购什么样的设备有非常大的帮助。</p>
<p>为什么这么说呢 ？</p>
<p>因为 NAS 的核心功能是存储。硬盘位数、支持的最大容量、有没有高速缓存，这些配置能直接影响存储性能、存储安全性和存储容量上限。</p>
<p>作为锦上添花的各类服务，需要更大的内存，更高效的 CPU，甚至独立的 GPU 芯片、额外的网卡接口来做支撑。</p>
<p>存储方面各家计价方式几乎没有差别，而非核心的后者才是最让人苦恼的地方。比如多一块 GPU 芯片可能整体价格就会翻倍，而你一旦前期没有选配，后期想要实现一些如 AI 能力或者编解码就会效果大打折扣，甚至无法实现。</p>
<p>上面的分析就是从“存算分离”的架构思路去理解的。</p>
<p>【存】指的是大量消耗硬盘的数据存储服务。</p>
<p>【算】指的是以消耗 CPU、内存等其它计算资源为主的服务，如 AI 相册、智能家居之类。</p>
<p>存和算都是服务的一部分。借助于这个思路，结合市面上的成品，再审视下自己的需求，大概率就能选择到一款适合自己的方案。</p>
<h2 id="-1">二、小剧的需求是什么？</h2>
<p>好的方案不仅要考虑实际需要，还要结合个人财力。</p>
<p>小剧财力这一块没问题，只是实际需求中的预算不高而已（狗头🐶）。</p>
<h3 id="21">2.1、关于容量</h3>
<p>小剧没有收藏癖，不是很热衷于保存大量的影视资源，基本上都是需要看什么就临时下载。</p>
<p>目前硬盘里保存的绝大多数都是和个人、家庭相关的文件。以相册、文档为主，总量也只有 720G 左右。</p>
<p><strong>“因此存储虽然是小剧的核心诉求，但对容量要求并不高。”</strong></p>
<h3 id="22web">2.2、关于 Web 服务</h3>
<p>在 2023 年，小剧曾经写过 <a href="https://bh-lay.com/blog/27dmws2c3hx">《Mongo DB 服务挂起问题排查》</a>，介绍了 Mongo DB 借助于 Docker 部署在了 2013 款 Mac mini 上。使用 frp 与公网服务器上的 NodeJS 服务通信。</p>
<p>因此希望新的服务器同样能够支持 Docker，这样十几年前的古董 Mac mini 就可以成功退役了。</p>
<p>你可能知道，小剧是个爱到处拍拍拍的人。小剧习惯于用【年 &gt; 年月日-事件名 &gt; 照片】这样三层目录结构来备份照片。这是小剧用了很多年的照片备份方案。从早些年的 U 盘备份，到后来的移动硬盘（机械）备份，再到网盘、NAS 阶段皆如此。</p>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/original-photo-store.jpg" alt="原始照片备份模式" /></p>
<p>照片虽然总容量不大，但碎片化的文件特别难以管理。尤其是命名文件夹是件费脑子的事，在绝大多数没有明确主题的日子里产生的照片，总是被丢进类似于【二月】、【五月】这样的毫无特征的文件夹里。</p>
<p>更烦人的是很难分得清手机里，哪些照片已经备份了，需要一次次一张张的去核对，或者无脑上传一遍遍。</p>
<p>在古董 Mac mini 里浅尝过 PhotoView，对照片管理并没有太大帮助。研究过 PhotoPrism，功能看起来很强，但 UI 给我的感觉糙糙的。最终还是 Immich 惊艳到了我，功能强大且多端支持，UI 很精细，支持手机相册备份、重复照片检测、人脸识别、照片搜索等功能，有机会小剧单独介绍它。</p>
<p>但是 Immich 在我那古董 Mac mini上始终无法正常使用，希望新的服务器可以顺利运行 Immich。</p>
<h3 id="23">2.3、关于数据安全</h3>
<p>很多小伙伴可能会被 NAS 商家宣传的各种 RIAD 模式所迷惑。花费了大量时间研究不同的阵列模式，以及对应的安全性。</p>
<p>不可否认，Raid 的设计很精妙，在兼顾数据安全和容量上可以找到很多不同的平衡点。</p>
<p>但即使用安全等级最高的 Raid1 模式，也只是在尽可能的增强单体设备的安全性。而通常情况下，多设备同步模式的系统安全更为可靠。</p>
<p>例如火灾、水淹、盗抢、意外跌落等情况发生时，单体设备的安全系数再高，也无法保证数据的安全性。</p>
<p>因此小剧希望新的方案依旧可以支持多设备同步，甚至云端同步。</p>
<h3 id="24">2.4、关于其他指标</h3>
<h4 id="241">2.4.1、体积、噪音、功耗</h4>
<p>这几个指标应该是家用和商用最大的区别。因为小剧家的户型并不大，并没有足够的空间容纳形如机架服务器、机箱服务器之类的服务器硬件。</p>
<p>另外当我们身处在家里时，有一半是夜深人静的睡眠时间。小剧并不希望嗡嗡嗡的散热风扇声，或者时不时的“炒豆子”声打扰家人的清梦。</p>
<p>可能很多小伙伴并没有意识到，一味的追求各种指标、配置，除了一次性的硬件购买成本外，7 * 24 小时运行所产生的功耗，也是一个非常大的成本。以 40W 功耗差、0.5 元每度电的成本来算，一年下来就要多花将近两百块（好像也不多）。</p>
<h4 id="242">2.4.2、使用速度</h4>
<p>这也是一条让大家拼命掏空口袋的指标。各家厂商会在缓存、CPU、阵列模式、网卡上做速度提升，代价就是你得支付远超对应硬件的价格。</p>
<p>并且千兆、2.5G、万兆等网速的支持，还得依赖家庭网络环境，以及使用设备的网速上限。</p>
<p>小剧对此并没有太高要求，能跑满家里的千兆网就足够了。</p>
<h2 id="-2">三、改造前家庭服务器长什么样？</h2>
<h3 id="31">3.1、之前的主力存储</h3>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/home-server-origin.jpg" alt="家庭服务器改造前的样子" /></p>
<p>这是小剧家庭服务器改造前的一部分，设备被安放在 Macbook 的纸盒子里，很简陋也很随意。</p>
<p>小剧使用 H3C M1 的小盒子做家庭主力存储三年了，就是上图中那个白色的小盒子。  </p>
<p>在 <a href="https://bh-lay.com/blog/8hfd4n48pd">《小剧的2021》</a> 中【2.3、照片存储方案探索】部分曾经介绍过这个设备。容量只有 1T、易用性不太够，但贵在稳定，并且可靠性很高。</p>
<p>在小剧看来它是一款被新华三抛弃的家庭存储产品。APP 自小剧使用以来没有一次更新，发布至今也只有五个版本。</p>
<p>新华三虽然在产品线中抛弃了它，但好在 Web 服务一直在运行着。设备的初始化、远程访问等功能依旧能用。</p>
<p>它是一台单硬盘模式的 NAS，不支持安装第三方 APP，内置功能也很有限，甚至部分功能完全不可用。</p>
<h3 id="32">3.2、之前的主力服务器</h3>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/home-server-origin-2.jpg" alt="家庭服务器改造前的 Mac mini" /></p>
<p>这是前文提到的那个 2013 款 Mac mini，原谅小剧没有找到拍摄清晰的照片，并且原安装位置已被拆除，只能找出来这张主要拍倍思电源的照片凑个数。</p>
<p>Mac mini 被安置在书桌侧面，机器很老，配置很低。</p>
<p>但了解的朋友应该知道，这个版本的 Mac mini 内置了一块机械硬盘，同时主板上预留的接口支持加装一块 m2 的固态硬盘，这也是最后一代支持加装内置硬盘的 Mac mini。</p>
<p>可玩性还是有一点点的，吧～</p>
<p>前文提到的 Mongo DB 服务就是运行在这台 Mac mini 上的，早期使用的 PhotoView 同样也是运行在这里。</p>
<p>另外它内置的机械硬盘容量也是 1T，刚好用来备份 H3C m1 里的数据。</p>
<h2 id="-3">四、改造后的方案长什么样？</h2>
<p>这次改造并没有添置太多的设备，只是根据前面提到的小剧对家庭服务器的理解，以及使用过一些设备，对设备做了增减。</p>
<h3 id="41">4.1、购置清单</h3>
<p>这次改造持续了将近三个月，目前算是稳定下来了。细数一下小剧的购置清单，再加上已有的设备成本，其实并不比成品 NAS 花费低，</p>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/procurement-list.jpg" alt="购置清单" /></p>
<ol>
<li>Mac mini m1</li>
<li>Mac mini 支架</li>
<li>USB4 移动硬盘盒</li>
<li>固态硬盘</li>
<li>液冷散热</li>
<li>UPS</li>
</ol>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/home-server-tag.jpg" alt="家庭服务器标记" /></p>
<h3 id="42">4.2、新的主力服务器</h3>
<p>基于小体积、静音、低功耗的要求，小剧这次主力服务依旧采用的是 Mac mini。</p>
<p>只是淘汰了 2013 款的古董级 Mac mini，转而购入了一款 m1 版本的 Mac mini。</p>
<p>8 + 256 丐中丐的配置，日常办公使用可能略微有点性能不足，但作为一台 7 * 24 开机的家庭服务器足够了。</p>
<p>Mac mini 体积足够小，外形足够简洁漂亮，运行无噪音，这几点完全打在了小剧的爽点上。</p>
<p>根据苹果官方的 <a href="https://support.apple.com/zh-cn/103253">Mac mini 功耗和热输出 (BTU) 信息</a> 显示，m1 版本的Mac mini 闲置功率为 6.8W，最大负载时为 39W，妥妥的省电小能手。</p>
<p>在主力服务器 Mac mini 上安装了 Docker Desktop，用来承载前文提到的 Mongo DB 数据库、Immich 等服务。</p>
<p>各种备份任务也是跑在这台主力服务器上。</p>
<h3 id="43">4.3、新的主力存储</h3>
<p>还记得之前的主力存储不，是一台 H3C m1 的存储盒子。</p>
<p>这一次小剧把 H3C m1 降级为备份存储，每隔四小时备份一次主力存储的数据，并且开启了夜间休眠模式。</p>
<p>主力存储为购置的 USB4 移动硬盘盒 + 2T 的 m2 固态硬盘，连接到 Mac mini 上。</p>
<p><strong>咦，你是不是发现一个神奇的爽点 ？</strong></p>
<p>没错，改造后的主力存储和主力服务器合二为一了。</p>
<p>主力服务器之所以选择 m1 版本的 Mac mini，还有另一个原因，是它的雷雳接口支持 USB4 的移动硬盘盒。虽说是移动硬盘，但是直通 PCIE 接口。根据网上大佬的测试，读写速度是 Mac mini 内置硬盘的两倍（小剧未求证）。</p>
<p>局域网内的 SMB 协议文件共享，使用 MacOS 自带的文件共享功能实现； WebDav 的功能借助于 Alist 来完成。</p>
<p>可能你会反驳说：<strong>固态硬盘不适合做重要数据的主力存储。</strong></p>
<p>这个观点我赞同，但它是相对的。</p>
<p>做好冷备份的前提下，哪怕固态硬盘彻底坏掉了，重新再买一个，恢复数据之后一切又能正常跑起来了。</p>
<p>在不关心寿命的前提下，固态硬盘的高速、小体积、无噪音、不惧跌落等特点全都是优点。</p>
<h3 id="44ups">4.4、UPS 不间断电源</h3>
<p>此前两年多经历过很多次断电，都没有遇到过问题。</p>
<p>这次很不幸，断电后 Mac mini 中的 Docker 数据全部丢失。</p>
<p>为了保障服务的稳定性，“斥巨资”购入了 UPS 来为主力服务器 Mac mini 保驾护航。</p>
<p>对小剧这次悲惨的经历感兴趣的话，可以阅读这篇记录。</p>
<p><a href="https://bh-lay.com/blog/1sidzrps0yb">《家庭服务器断电处理记录》</a></p>
<h3 id="45">4.5、加装液冷散热模块</h3>
<p>这次家庭服务器版本改造开始时，是 2024 年春节前夕。</p>
<p>举国欢庆春节时，也是合肥最冷的时候，因此服务器改造初期并没有发现什么不妥。</p>
<p>合肥这座城市有个典型的特点，就是春秋季特别短。三月中旬一过，已然有了夏天的感觉。</p>
<p>这时原本温润的移动硬盘盒悄悄变成了发热的小火山。</p>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/ssd.jpg" alt="固态硬盘盒" /></p>
<p>其实并没有这么夸张，四五十度对于移动硬盘、m2 固态硬盘来说很清凉，可以不用关心它。一想到它在小剧家 7*24h 工作任劳任怨， 适当的降降温也算是对它辛苦劳作的补偿。</p>
<p>调研后有三个方案：</p>
<ol>
<li>加装风扇散热</li>
<li>半导体散热</li>
<li>液冷散热</li>
</ol>
<p>方案一最常见，也立竿见影，但整体效果一般，而且书房窗户朝西，在接下来的夏天应该很难扛下去。</p>
<p>方案二是我最近刚发现的，效果很惊人，甚至能达到滴水成冰的效果。代价是过高的耗电量，以及冷凝水的隐患。对于小剧这种低功耗家庭服务器的环境来说，显然不合适。</p>
<p>方案三效果比风扇好不了太多，但因为冷却液可以挪到相对温度更低的另一面，在接下来的夏天应该能扛的更久一点。因此就选择了液冷作为降温方案。</p>
<p>找了一圈，市面上并没有直接可用的液冷配件。经过筛选，购入了一款手机液冷套装，改造后加装到移动硬盘上。</p>
<p>小剧玩手机液冷散热</p>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/water-cooling.jpg" alt="手机液冷散热器" /></p>
<h3 id="46">4.6、物理空间设计</h3>
<p>看了前面的介绍，相信你也能发现小剧家庭服务器非常零碎。各种设备以一种奇奇怪怪的姿势连接在一起。</p>
<p>想要保持好用、整洁，甚至还能让人说一句“好看”是很困难的。</p>
<p>空间建模</p>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/3d-model.jpg" alt="空间建模" /></p>
<p>为此小剧测量了书柜开放格的整体尺寸，以及要部署的各个设备的尺寸，借助于 iPad 的建模软件做了方案预演。</p>
<p>最终在比较满意的四套方案中，选择了空间利用率最高的一个。</p>
<p>近期虽然对设备进行了增减，但整体布局并没有发生大的改变。</p>
<h2 id="-4">五、写在最后</h2>
<p><img src="https://static.bh-lay.com/blog/2024/home-server/home-server-tag.jpg" alt="家庭服务器标记" /></p>
<h3 id="51">5.1、前文未提到的设备</h3>
<p>前面重点介绍了 Mac mini、移动硬盘盒，附带提到了 H3C m1、UPS、液冷散热这些设备。这里小剧简单介绍下另外几个小黑盒子。</p>
<p><strong>交换机</strong></p>
<p>这个没什么好介绍的， 单纯是为了拓展网口 + 设备互联而已。唯一值得说道的，应该就是插在上面的一根根，由小剧纯手工打造的网线了吧。</p>
<p>统一的颜色、定制的长度，让这个局促的小空间不至于那么杂乱。</p>
<p><strong>蒲公英</strong></p>
<p>这是一个成品的内网穿透盒子，只有一个功能。就是让小剧在外的时候，可以借助于它构建的虚拟专用网络（V*N）访问家庭内网。</p>
<p>免费账号只能设置三台设备使用，这个盒子自己需要占掉一个。也就是说真正可以给小剧自己和家人添加的设备只有两台。</p>
<p><strong>玩客云盒子</strong></p>
<p>图中在移动硬盘盒和 H3C m1 中间的一个小设备。可能有部分小伙伴知道，它可以挂机跑分挣钱。但小剧只是单纯把它当成一个 Armbain 的服务器，跑一些简单的内网 Web 服务。</p>
<p>另外背后的 USB 口用来给蒲公英供电。</p>
<p><strong>神秘小盒子</strong></p>
<p>这是一台 R2S 的小盒子，当作旁路由来用。绝大多数时候都是关机的状态，偶尔需要给 Docker 等服务加速的时候才会开一下。</p>
<p>轻度使用，可有可无。</p>
<h3 id="52">5.2、小剧的抠搜小贴士</h3>
<p>回顾整篇记录，小剧反复提到过很多遍“单体设备”这个词。</p>
<p>翻阅全文后再看市面上对于 NAS 的理解，和小剧口中所谓的“家庭服务器”。最大的差别就是市面上的 NAS 热衷于用一个单体设备来实现完整的功能。</p>
<p>而小剧的家庭服务器方案更倾向于，在下单前先审视自己的需求，再逆向去找满足需求的设备。可以是单体设备，也可以是各种设备的组合。</p>
<p>不给自己生造需求，也不假设自己几乎用不到的峰值性能。随着自己的使用循序渐进，进行设备的更替，让属于自己的方案陪着自己去成长。</p>]]></content:encoded>
    </item>
    <item>
      <title>家庭服务器断电处理记录</title>
      <link>https://bh-lay.com/blog/1sidzrps0yb</link>
      <description><![CDATA[<p>熟悉小剧的同学可能知道，小剧在家里部署了一些 Web 服务，并且借助于 Mac mini 的文件共享实现了一些简单的 NAS 功能。</p>
<p>关于这部分奇奇怪怪的骚操作，后面会单独出篇文章介绍。</p>
<p><img src="https://static.bh-lay.com/blog/2024/home-lab-poweroff/home-lab.jpg" alt="家庭服务器" /></p>
<h2 id="">一、遇到什么事了</h2>
<p>某天早上，小剧骑着小电驴上班的路上，手机微信群突然响了两声，物业说接供电公司通知，即将停电半小时。</p>
<p>本来也没什么大不了的，毕竟家里主动、被动断电很多次了，也没出过问题。</p>
<p>到了公司后检查，发现家里已经恢复供电了，蒲公英和 H3C M1 也已经正常启动了。</p>
<p>只是 Immich 相册服务一直无法正常访问。</p>
<p>于是远程桌面连接家里的 Mac mini 查看，因为着急排查问题，顺手关掉了 Docker Desktop 弹出的错误提示。</p>
<p>接下来的画面就很懵逼了，Docker Desktop 里空空如也，系统 Volumes、Images、Containers 全部是空的。</p>
<h2 id="docker">二、Docker 怎么了？</h2>
<p>按照我的理解，Docker 的 Containers 因为要实时运行，意外断电可能会导致容器损坏。而系统 Volumes 和 Images 因为存储在文件系统中，这两个不应该丢失才对。</p>
<p>换句话来说，即使系统 Volumes 和 Images 的索引丢失了，也应该在文件系统的某个位置找得到。</p>
<p>因此小剧并没有着急恢复服务，想着晚上回家花点时间把 Docker 恢复就完事了。</p>
<p><strong>然而事实比我想的更糟糕！</strong></p>
<p>经过一番研究了之后才发现，MacOS 上的 Docker 是运行在虚机里的，实体文件是 Docker.raw。</p>
<p>因为虚机是单文件，意外断电时如果导致虚机文件损坏，重启后是完全无法恢复的。</p>
<blockquote>
  <p>顺手关掉了 Docker Desktop 弹出的错误提示。</p>
</blockquote>
<p>还记得前文提到被关掉的错误提示么，现在回想起来大概率是 Docker 重启后，检测到了虚机文件损坏，需要重新初始化 Docker 的提示。</p>
<p>在这个提示之前，Docker.raw 虚机文件很可能还是之前的损坏版本，抢救一下说不定还有戏。</p>
<p>但是点击 Confirm 关掉提示后，按照正常逻辑来想，接下来的操作是删掉旧的 Docker.raw 文件，并且使用新的虚机文件重新初始化。</p>
<p>但是嘞，第一我回不到点击 Confirm 前那个时间了。第二即使我回得去，面对 Docker.raw 这种深度封装的文件，我也不一定解得开。第三即使我解开了虚机文件，能不能顺利恢复数据也难说。</p>
<p>所以小剧遇到的情况是：Docker.raw 虚机损坏且被删除，系统 Volumes、Images、Containers 全部丢失。</p>
<p>包括 Immich 在内的六个服务全部损坏。</p>
<h2 id="-1">三、如何恢复服务？</h2>
<p>前面通过确认，恢复 Docker 已经不太可能。听起来很可怕，但其实问题并没有那么糟糕。</p>
<p>熟悉 Docker 的同学可能知道，Image 只是镜像文件，只要重新 Pull 就可以再次拉取得到。Image 在实例化成 Container 前，需要将一些副作用目录挂载到 Volume 上。</p>
<p>因此只要各个 Docker 服务的启动方式还在，Volume 目录还在，重新拉取镜像运行即可。</p>
<h3 id="31docker">3.1、重建 Docker 服务</h3>
<p>小剧前期运行的六个服务均是借助于 Docker Compose 配置的，恢复起来还算简单。</p>
<p>另外五个服务花点时间拉取镜像即可恢复成功。</p>
<p>最为麻烦的还是前文提到的 Immich 相册服务。因为 Immich 官方提供的 Compose 文件将数据库的 Volume 指向为系统 Volume，小剧也未修改过此处的配置。</p>
<p>前文提到 MacOS 的 Docker 运行在虚机内，伴随着此次断电引起的虚机文件损坏，Immich 的数据库 Volume 丢失，数据库服务无法恢复。</p>
<h3 id="32immich">3.2、Immich 如何处理的？</h3>
<p>小剧使用的 Immich 相册服务，副作用目录有两部分，分别是文件和数据库。</p>
<p>文件包含备份上传的原始照片、视频，压缩后的各个尺寸的缩略图，以及统一编码后视频文件。</p>
<p>数据库则是所有已上传图片的索引，EXIF 信息，相册引用数据，照片 TAG，还有就是借助于 AI 解析出的人脸识别数据、图片内容搜索数据等。</p>
<p>因为数据库的丢失，压缩后的缩略图和统一编码后的视频文件没有了意义，这部分文件小剧直接删除了。</p>
<p>备份了原始照片、视频后，并且将 Docker Compose 中的数据库挂载点由系统目录更改为本地目录，重新初始化 Immich，自此 Immich 服务成了“新造的人”。</p>
<p>我希望你能 get 到 “新造的人”这个梗。</p>
<p>因为是全新的服务，Immich 的用户系统和照片全部是空空的。</p>
<p>为了让媳妇无感度过此次宕机过程，小剧给媳妇用原来的邮箱、密码重新创建了账号。</p>
<h3 id="33">3.3、如何导入照片？</h3>
<p>正如前文提到的，此时的 Immich 数据是空空的，小剧自己和媳妇的账号虽然重建了，但账号下没有一张照片，需要将此前的照片重新导入进 Immich 中。</p>
<p>如果你有用过 Immich 可能会知道，Immich 默认存储原始照片的目录结构为【年 &gt; 月 &gt; 日】三层结构。</p>
<p>小剧此前大概有四五万张照片，从已备份的目录中一层层找到照片备份不太现实，并且极容易遗漏。</p>
<p>借助于前东家的星火大模型，找了两段神奇的 Shell 脚本，分别是提取深层目录的文件至新目录，以及递归删除空目录脚本。</p>
<pre><code class="shell language-shell"># 创建一个新的文件夹用来存放提取出来的文件
mkdir /path/to/new_directory

# 使用 find 命令查找所有的文件，并使用 -exec 选项对每个找到的文件执行 mv 命令
find /path/to/old_directory -type f -exec mv {} /path/to/new_directory \;
</code></pre>
<pre><code class="shell language-shell">find . -type d -empty -exec rmdir {} \;
</code></pre>
<p>有了 Shell 脚本的辅助，文件整理变得极为容易。在提取完文件后，为了方便操作和记录，小剧按照照片年份分批导入。</p>
<p>第一次导入照片时，小剧使用的是 Macbook 操作的。</p>
<p>因为已备份文件存放在 Mac mini 电脑上，Macbook 是连接的家庭 WI-FI，借助于文件共享读取 Mac mini 上已备份的文件，再通过 WEB 访问位于 Mac mini 上的 Immich 服务，最后执行导入。</p>
<p>备份操作完整的数据流向大概是个环：Mac mini -&gt; 交换机 -&gt; 路由器 -&gt; Macbook -&gt; 路由器 -&gt; 交换机 -&gt; Mac mini。</p>
<p>虽然局域网操作很快，但毕竟上百 G 的零碎照片，还是能明显的感知到速度被网络禁锢住了。</p>
<p>第二次导入时，小剧直接在 Mac mini 上安装了 Chrome 浏览器，通过 Localhost 的方式导入照片。</p>
<p>跳过了家庭网络各个硬件“中间商”和软件共享的挂载后，整个导入速度极快，后面其他年份的导入工作也在不到十分钟完成。</p>
<p>接下来的人脸识别和照片内容搜索慢慢等 Immich Job 执行就好。</p>
<p><strong>自此小剧断电后挂掉的的家庭服务全部恢复。</strong></p>
<p>对了，新识别的人脸对应的姓名是空的。经常被 AI 认错的外甥和外甥女现在肯定又是乱糟糟的，这些都得等有空了慢慢标记。</p>
<p>此前整理的虚拟相册也得自己按照记忆慢慢整理，或者大概率放弃了。</p>
<h2 id="ups">四、家庭服务器引入 UPS</h2>
<p>前面的操作有一个很重要的点，就是放弃 Docker 的系统 Volume。这样在下次 Docker.raw 文件损坏时，不至于出现数据丢失。</p>
<p>但这个操作也仅仅是最大限度地在断电发生后，提高数据和服务恢复的可能性。</p>
<p>在一个 7*24 小时运行的系统内，断电的危害不仅仅是小剧遇到的 Docker.raw 虚机文件损坏这一种。</p>
<p>为了保障服务的稳定性，小剧“斥巨资”购入了 APC BK650 M2 这款后备式 UPS。</p>
<p><img src="https://static.bh-lay.com/blog/2024/home-lab-poweroff/ups.jpg" alt="UPS" /></p>
<p>UPS 相信大家都知道，作为不间断电源主要有两个作用。</p>
<p>一个是在断电后提供一定时间的续航，保证服务短暂的可用。
另一个作用则是在稳妥的时间或者电量内，通知设备关机。减少意外断电对设备、服务和数据的损坏。</p>
<h3 id="41ups">4.1、UPS 断电检测逻辑</h3>
<p>因为这款 UPS 并未明确说明支持 MacOS，只是介绍了兼容多个 Nas 品牌。下单之前问了淘宝旗舰店客服，被告知这款 UPS 不支持 Mac 系统，但是京东的客服又告诉我支持。</p>
<p>无所谓了，就当它不支持 MacOS 好了，小剧准备手撸断电检测的逻辑，有两个方案可选。</p>
<h4 id="411usb">4.1.1、方案一： 解析 USB 接口数据</h4>
<p>因为 UPS 和设备通信借助于 USB 接口，并且支持多种品牌 NAS，在 USB 连接的场景下，大概率是明文数据。</p>
<p>因此小剧准备借助于 NodeJS 的 USB 包，尝试写一写读取 USB 内 UPS 状态的逻辑，来辅助系统进行关机。</p>
<h4 id="412">4.1.2、方案二： 根据环境推测断电状态</h4>
<p>因为 Mac mini 隐藏在 UPS 后面，断电后一段时间内仍可以正常使用。</p>
<p>家里除了 Mac mini 作为服务器，能够提供服务的设备还有很多，比如主路由、小米中枢网关、玩客云盒子等等。</p>
<p>基于这样的环境，小剧可以整理一个 UPS 外的服务列表，在 Mac mini 上每隔几秒执行一次服务状态检测。</p>
<p>检测内容包括上述提到的设备，以及互联网状态。全部连接失败则可以判定为断电状态，但凡有一个服务能连接成功，都可以判定为市电正常状态。</p>
<h3 id="42ups">4.2、UPS 小惊喜</h3>
<p>京东发货速度还挺快，中午下单第二天上午就收到货了。在给柜子打孔穿线后 UPS 正式和 Mac mini 连接了起来。</p>
<p>开机后在系统设置的电源部分，竟然惊喜的发现是我这款 UPS 支持 MacOS 的。</p>
<p>又省了掉头发的断电检测逻辑的编写。</p>
<p><img src="https://static.bh-lay.com/blog/2024/home-lab-poweroff/macos-power.png" alt="macOS UPS" /></p>
<h2 id="-2">五、问题得到解决了么？</h2>
<p>经过断电后的排查，和比较痛心的服务恢复之后，绝大多数的服务都已经顺利恢复。</p>
<p>这次重点提到的 Immich，丢失了灵魂的数据库部分，但好在照片原始文件均在，经历了照片导入，和漫长的 AI 重建人脸数据以及照片搜索后，服务也算再次跑了起来。</p>
<p>并且将数据库的 Volume 转移到了本地，出错的概率进一步降低。</p>
<p>最后再结合 UPS 的物理保障，让小剧对这套 DIY 服务又重新拾起了信心。</p>
<p>这次家庭服务器断电遇到的问题，以及后续面临断电的风险得到了解决。</p>
<hr />
<p>最后，计划两个月之内补上定时备份 Immich 数据库的脚本。</p>
<p>参考资料：</p>
<ul>
<li>https://docs.docker.com/desktop/faqs/macfaqs/</li>
<li>https://stackoverflow.com/questions/38532483/where-is-var-lib-docker-on-mac-os-x</li>
<li>https://www.freecodecamp.org/news/where-are-docker-images-stored-docker-container-paths-explained/</li>
</ul>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Mon, 22 Apr 2024 16:13:45 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/1sidzrps0yb</guid>
      <category>Docker</category>
      <category>Immich</category>
      <category>UPS</category>
      <category>NAS</category>
      <category>家庭服务器</category>
      <enclosure url="http://static.bh-lay.com/blog/2024/home-lab-poweroff/home-lab.jpg" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>熟悉小剧的同学可能知道，小剧在家里部署了一些 Web 服务，并且借助于 Mac mini 的文件共享实现了一些简单的 NAS 功能。</p>
<p>关于这部分奇奇怪怪的骚操作，后面会单独出篇文章介绍。</p>
<p><img src="https://static.bh-lay.com/blog/2024/home-lab-poweroff/home-lab.jpg" alt="家庭服务器" /></p>
<h2 id="">一、遇到什么事了</h2>
<p>某天早上，小剧骑着小电驴上班的路上，手机微信群突然响了两声，物业说接供电公司通知，即将停电半小时。</p>
<p>本来也没什么大不了的，毕竟家里主动、被动断电很多次了，也没出过问题。</p>
<p>到了公司后检查，发现家里已经恢复供电了，蒲公英和 H3C M1 也已经正常启动了。</p>
<p>只是 Immich 相册服务一直无法正常访问。</p>
<p>于是远程桌面连接家里的 Mac mini 查看，因为着急排查问题，顺手关掉了 Docker Desktop 弹出的错误提示。</p>
<p>接下来的画面就很懵逼了，Docker Desktop 里空空如也，系统 Volumes、Images、Containers 全部是空的。</p>
<h2 id="docker">二、Docker 怎么了？</h2>
<p>按照我的理解，Docker 的 Containers 因为要实时运行，意外断电可能会导致容器损坏。而系统 Volumes 和 Images 因为存储在文件系统中，这两个不应该丢失才对。</p>
<p>换句话来说，即使系统 Volumes 和 Images 的索引丢失了，也应该在文件系统的某个位置找得到。</p>
<p>因此小剧并没有着急恢复服务，想着晚上回家花点时间把 Docker 恢复就完事了。</p>
<p><strong>然而事实比我想的更糟糕！</strong></p>
<p>经过一番研究了之后才发现，MacOS 上的 Docker 是运行在虚机里的，实体文件是 Docker.raw。</p>
<p>因为虚机是单文件，意外断电时如果导致虚机文件损坏，重启后是完全无法恢复的。</p>
<blockquote>
  <p>顺手关掉了 Docker Desktop 弹出的错误提示。</p>
</blockquote>
<p>还记得前文提到被关掉的错误提示么，现在回想起来大概率是 Docker 重启后，检测到了虚机文件损坏，需要重新初始化 Docker 的提示。</p>
<p>在这个提示之前，Docker.raw 虚机文件很可能还是之前的损坏版本，抢救一下说不定还有戏。</p>
<p>但是点击 Confirm 关掉提示后，按照正常逻辑来想，接下来的操作是删掉旧的 Docker.raw 文件，并且使用新的虚机文件重新初始化。</p>
<p>但是嘞，第一我回不到点击 Confirm 前那个时间了。第二即使我回得去，面对 Docker.raw 这种深度封装的文件，我也不一定解得开。第三即使我解开了虚机文件，能不能顺利恢复数据也难说。</p>
<p>所以小剧遇到的情况是：Docker.raw 虚机损坏且被删除，系统 Volumes、Images、Containers 全部丢失。</p>
<p>包括 Immich 在内的六个服务全部损坏。</p>
<h2 id="-1">三、如何恢复服务？</h2>
<p>前面通过确认，恢复 Docker 已经不太可能。听起来很可怕，但其实问题并没有那么糟糕。</p>
<p>熟悉 Docker 的同学可能知道，Image 只是镜像文件，只要重新 Pull 就可以再次拉取得到。Image 在实例化成 Container 前，需要将一些副作用目录挂载到 Volume 上。</p>
<p>因此只要各个 Docker 服务的启动方式还在，Volume 目录还在，重新拉取镜像运行即可。</p>
<h3 id="31docker">3.1、重建 Docker 服务</h3>
<p>小剧前期运行的六个服务均是借助于 Docker Compose 配置的，恢复起来还算简单。</p>
<p>另外五个服务花点时间拉取镜像即可恢复成功。</p>
<p>最为麻烦的还是前文提到的 Immich 相册服务。因为 Immich 官方提供的 Compose 文件将数据库的 Volume 指向为系统 Volume，小剧也未修改过此处的配置。</p>
<p>前文提到 MacOS 的 Docker 运行在虚机内，伴随着此次断电引起的虚机文件损坏，Immich 的数据库 Volume 丢失，数据库服务无法恢复。</p>
<h3 id="32immich">3.2、Immich 如何处理的？</h3>
<p>小剧使用的 Immich 相册服务，副作用目录有两部分，分别是文件和数据库。</p>
<p>文件包含备份上传的原始照片、视频，压缩后的各个尺寸的缩略图，以及统一编码后视频文件。</p>
<p>数据库则是所有已上传图片的索引，EXIF 信息，相册引用数据，照片 TAG，还有就是借助于 AI 解析出的人脸识别数据、图片内容搜索数据等。</p>
<p>因为数据库的丢失，压缩后的缩略图和统一编码后的视频文件没有了意义，这部分文件小剧直接删除了。</p>
<p>备份了原始照片、视频后，并且将 Docker Compose 中的数据库挂载点由系统目录更改为本地目录，重新初始化 Immich，自此 Immich 服务成了“新造的人”。</p>
<p>我希望你能 get 到 “新造的人”这个梗。</p>
<p>因为是全新的服务，Immich 的用户系统和照片全部是空空的。</p>
<p>为了让媳妇无感度过此次宕机过程，小剧给媳妇用原来的邮箱、密码重新创建了账号。</p>
<h3 id="33">3.3、如何导入照片？</h3>
<p>正如前文提到的，此时的 Immich 数据是空空的，小剧自己和媳妇的账号虽然重建了，但账号下没有一张照片，需要将此前的照片重新导入进 Immich 中。</p>
<p>如果你有用过 Immich 可能会知道，Immich 默认存储原始照片的目录结构为【年 &gt; 月 &gt; 日】三层结构。</p>
<p>小剧此前大概有四五万张照片，从已备份的目录中一层层找到照片备份不太现实，并且极容易遗漏。</p>
<p>借助于前东家的星火大模型，找了两段神奇的 Shell 脚本，分别是提取深层目录的文件至新目录，以及递归删除空目录脚本。</p>
<pre><code class="shell language-shell"># 创建一个新的文件夹用来存放提取出来的文件
mkdir /path/to/new_directory

# 使用 find 命令查找所有的文件，并使用 -exec 选项对每个找到的文件执行 mv 命令
find /path/to/old_directory -type f -exec mv {} /path/to/new_directory \;
</code></pre>
<pre><code class="shell language-shell">find . -type d -empty -exec rmdir {} \;
</code></pre>
<p>有了 Shell 脚本的辅助，文件整理变得极为容易。在提取完文件后，为了方便操作和记录，小剧按照照片年份分批导入。</p>
<p>第一次导入照片时，小剧使用的是 Macbook 操作的。</p>
<p>因为已备份文件存放在 Mac mini 电脑上，Macbook 是连接的家庭 WI-FI，借助于文件共享读取 Mac mini 上已备份的文件，再通过 WEB 访问位于 Mac mini 上的 Immich 服务，最后执行导入。</p>
<p>备份操作完整的数据流向大概是个环：Mac mini -&gt; 交换机 -&gt; 路由器 -&gt; Macbook -&gt; 路由器 -&gt; 交换机 -&gt; Mac mini。</p>
<p>虽然局域网操作很快，但毕竟上百 G 的零碎照片，还是能明显的感知到速度被网络禁锢住了。</p>
<p>第二次导入时，小剧直接在 Mac mini 上安装了 Chrome 浏览器，通过 Localhost 的方式导入照片。</p>
<p>跳过了家庭网络各个硬件“中间商”和软件共享的挂载后，整个导入速度极快，后面其他年份的导入工作也在不到十分钟完成。</p>
<p>接下来的人脸识别和照片内容搜索慢慢等 Immich Job 执行就好。</p>
<p><strong>自此小剧断电后挂掉的的家庭服务全部恢复。</strong></p>
<p>对了，新识别的人脸对应的姓名是空的。经常被 AI 认错的外甥和外甥女现在肯定又是乱糟糟的，这些都得等有空了慢慢标记。</p>
<p>此前整理的虚拟相册也得自己按照记忆慢慢整理，或者大概率放弃了。</p>
<h2 id="ups">四、家庭服务器引入 UPS</h2>
<p>前面的操作有一个很重要的点，就是放弃 Docker 的系统 Volume。这样在下次 Docker.raw 文件损坏时，不至于出现数据丢失。</p>
<p>但这个操作也仅仅是最大限度地在断电发生后，提高数据和服务恢复的可能性。</p>
<p>在一个 7*24 小时运行的系统内，断电的危害不仅仅是小剧遇到的 Docker.raw 虚机文件损坏这一种。</p>
<p>为了保障服务的稳定性，小剧“斥巨资”购入了 APC BK650 M2 这款后备式 UPS。</p>
<p><img src="https://static.bh-lay.com/blog/2024/home-lab-poweroff/ups.jpg" alt="UPS" /></p>
<p>UPS 相信大家都知道，作为不间断电源主要有两个作用。</p>
<p>一个是在断电后提供一定时间的续航，保证服务短暂的可用。
另一个作用则是在稳妥的时间或者电量内，通知设备关机。减少意外断电对设备、服务和数据的损坏。</p>
<h3 id="41ups">4.1、UPS 断电检测逻辑</h3>
<p>因为这款 UPS 并未明确说明支持 MacOS，只是介绍了兼容多个 Nas 品牌。下单之前问了淘宝旗舰店客服，被告知这款 UPS 不支持 Mac 系统，但是京东的客服又告诉我支持。</p>
<p>无所谓了，就当它不支持 MacOS 好了，小剧准备手撸断电检测的逻辑，有两个方案可选。</p>
<h4 id="411usb">4.1.1、方案一： 解析 USB 接口数据</h4>
<p>因为 UPS 和设备通信借助于 USB 接口，并且支持多种品牌 NAS，在 USB 连接的场景下，大概率是明文数据。</p>
<p>因此小剧准备借助于 NodeJS 的 USB 包，尝试写一写读取 USB 内 UPS 状态的逻辑，来辅助系统进行关机。</p>
<h4 id="412">4.1.2、方案二： 根据环境推测断电状态</h4>
<p>因为 Mac mini 隐藏在 UPS 后面，断电后一段时间内仍可以正常使用。</p>
<p>家里除了 Mac mini 作为服务器，能够提供服务的设备还有很多，比如主路由、小米中枢网关、玩客云盒子等等。</p>
<p>基于这样的环境，小剧可以整理一个 UPS 外的服务列表，在 Mac mini 上每隔几秒执行一次服务状态检测。</p>
<p>检测内容包括上述提到的设备，以及互联网状态。全部连接失败则可以判定为断电状态，但凡有一个服务能连接成功，都可以判定为市电正常状态。</p>
<h3 id="42ups">4.2、UPS 小惊喜</h3>
<p>京东发货速度还挺快，中午下单第二天上午就收到货了。在给柜子打孔穿线后 UPS 正式和 Mac mini 连接了起来。</p>
<p>开机后在系统设置的电源部分，竟然惊喜的发现是我这款 UPS 支持 MacOS 的。</p>
<p>又省了掉头发的断电检测逻辑的编写。</p>
<p><img src="https://static.bh-lay.com/blog/2024/home-lab-poweroff/macos-power.png" alt="macOS UPS" /></p>
<h2 id="-2">五、问题得到解决了么？</h2>
<p>经过断电后的排查，和比较痛心的服务恢复之后，绝大多数的服务都已经顺利恢复。</p>
<p>这次重点提到的 Immich，丢失了灵魂的数据库部分，但好在照片原始文件均在，经历了照片导入，和漫长的 AI 重建人脸数据以及照片搜索后，服务也算再次跑了起来。</p>
<p>并且将数据库的 Volume 转移到了本地，出错的概率进一步降低。</p>
<p>最后再结合 UPS 的物理保障，让小剧对这套 DIY 服务又重新拾起了信心。</p>
<p>这次家庭服务器断电遇到的问题，以及后续面临断电的风险得到了解决。</p>
<hr />
<p>最后，计划两个月之内补上定时备份 Immich 数据库的脚本。</p>
<p>参考资料：</p>
<ul>
<li>https://docs.docker.com/desktop/faqs/macfaqs/</li>
<li>https://stackoverflow.com/questions/38532483/where-is-var-lib-docker-on-mac-os-x</li>
<li>https://www.freecodecamp.org/news/where-are-docker-images-stored-docker-container-paths-explained/</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>剧中人平庸的2023</title>
      <link>https://bh-lay.com/blog/22jy29grksw</link>
      <description><![CDATA[<p>又到一年岁末时，小剧似乎已经慢慢接受了 2023 年的平庸，在这一年里平平静静地经历着人生的特殊阶段。</p>
<p>相比于 2022 年小剧在工作和生活上较大的变化，2023 年一直在重复着工作、带娃、偶尔搞一搞自己小东西的状态，平淡却不枯燥。</p>
<p>前面提到的<strong>平庸、平静、平淡</strong>，大概就是小剧 2023 年的关键词吧。</p>
<h2 id="">一、日常带娃的生活</h2>
<p>2023 年是宝宝从接近一周岁开始慢慢成长的一年。这一年小剧见证了娃从憨憨傻傻到反骨萌芽，从匍匐攀爬到逛街溜达。每一天好像没有任何变化，不经意间又发现这个小小的人也在慢慢长大。</p>
<p>随着娃的四肢日渐发达，破坏力也在与日俱增。这一年里对家里进行了一次次无死角的“扫荡”，辛苦媳妇要日复一日的收拾整理。</p>
<p><img src="https://static.bh-lay.com/blog/2023-year-end/chaotic-corner.jpg" alt="一地玩具" /></p>
<p><img src="https://static.bh-lay.com/blog/2023-year-end/qiaosi-on-table.jpg" alt="霸占书桌" /></p>
<p>偶尔可爱起来，似乎也没有那么讨人厌。</p>
<p><img src="https://static.bh-lay.com/blog/2023-year-end/qiaosi.jpg" alt="光头娃娃" /></p>
<p>这一年陪着娃逛遍了周边的商场、公园、超市、游乐馆。探索了很多平时很常见，却又很容易被忽略的地方。</p>
<p>之所以把娃放在最前面来介绍，一是因为所谓的父爱吧，最重要的原因是，小剧 2023 年几乎绝大多数业余时光都和她一起度过。</p>
<h2 id="-1">二、今年偷偷学了啥？</h2>
<p>2023 年虽然拿不出整块的时间学一些新玩意儿，可利用的碎片时间依旧有很多。</p>
<p>这一年在业余生活中，除了学习前端老本行的技能点，还学了些 docker 容器化操作、FRP 内网穿透配置等杂七杂八的小东西。</p>
<h3 id="21nodejs">2.1、NodeJS 服务端重构</h3>
<p>年初对小剧客栈服务端做了一次彻底的重构。将 NodeJS 服务端逻辑全部使用 TypeScript 进行了重写，并且使用 Promise  重新组织各服务端逻辑。</p>
<p>严格来说，这部分其实并没有什么学习的成分可言，单纯是服务端代码过于老旧需要进行一次重构而已。并且 TypeScript 和 Promise 的逻辑也已经在工作中用过很多遍了。</p>
<p>之所以将这部分算在 2023 年的学习目录，是因为这一次系统的分析了常见的已有项目向 TypeScript 迁移的方案，以及业务代码、第三方包等不同的逻辑代码迁移的手段。</p>
<p>并且补上了服务端 Debugger 模式的支持，会话级别的错误捕捉等逻辑。</p>
<p>针对这次迁移，写了 <a href="http://bh-lay.com/blog/njs1l1pod0">《Node 迁移 TypeScript 记录》</a>，如果你感兴趣的话可以查看这篇文章。</p>
<h3 id="22indexeddb">2.2、IndexedDB 数据库升级</h3>
<p>相信你也知道，小剧在 2021-2022 年结合自己想学习的技能点，写了 <a href="https://e.bh-lay.com">小剧起始页</a> 这样一个小玩意儿。</p>
<p>关于小剧起始页的想法来源、早期想学习的技能点，以及做出来的初代成品，都可以参考当时写的 <a href="http://bh-lay.com/blog/2o7r91ml5hg">《【开发回顾】小剧起始页》</a>。</p>
<p>小剧起始页正式上线后，小剧并没有对它进行实质的推广，一来因为这是一个很具有个人色彩的工具，灵感来源及后期维护也仅仅是以小剧自己为出发点。二来市面上有很多类似的工具作为替代，小剧也不想把精力消耗在这个纯投入，且无法带来任何收益的项目上。</p>
<p>2023 年在个人的工作、学习、生活很多方面，涉及到链接收集与归档的工作，都放在了小剧起始页上。随着使用深度的加强，愈发觉得在不同场景下的数据管理过于冗杂，很难把数据组织的很具有条理性。</p>
<p>经过了一段时间的构思与设计，决定为小剧起始页增加<strong>多桌面</strong>的特性。</p>
<p>如果你有看过小剧起始页的实现，你应该知道它是一个支持纯离线使用的 PWA 项目。数据的存取均在浏览器本地，不涉及任何服务端 API。</p>
<p>多桌面的特性需要涉及到历史数据的转换，因此也就有了 IndexedDB 数据库升级的学习。</p>
<p>小剧对 IndexedDB 的使用为裸调 API，没有使用任何封装后的库，因此数据库升级的逻辑需要自己去尝试封装。</p>
<p>这部分经历可以参考 <a href="http://bh-lay.com/blog/1g079snv7oo">《小剧起始页 IndexedDB 数据库升级记录》</a>。</p>
<h3 id="23npm">2.3、发布了一个 NPM 包</h3>
<p>2023 年上半年，有一个对于小剧来说非常重要的日子，几乎每天小剧都在数着离这一天还有多久。</p>
<p>前面提到小剧每天都会使用小剧起始页，在这里做一个可配置的倒计时组件岂不是一件美事。</p>
<p>双方一拍即合（小剧自己跟自己拍），决定把可配置倒计时组件的开发提上日程。</p>
<p>很快组件就设计、开发好了，一个人没有沟通成本就是快哈。</p>
<p>然而平平的一块板板，上面堆着一排数字，单调且不精致。很难过小剧审美这道关。</p>
<p>后来机缘巧合刷到了 Youtube 大神分享的《Segmented Display》视频，介绍了工业时代显示数字的各种手段。详细介绍了后来被广泛使用的七段线晶体管，以及各种字体变种。</p>
<p>后来小剧查找了历史上各种经典的七段线版本，最终选择了一款厚重又俏皮的非对称版本，作为倒计时的字体基础。</p>
<p><strong>新老设计对比图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2023-year-end/seven-segment.jpg" alt="新老设计对比图" /></p>
<p>这部分经历原本写了份博文，但拖到现在还没有完稿，有机会重新编辑下再发布。</p>
<p>倒计时小组件完成的同时，小剧封装了一份 NPM 包，用来显示七段线风格的数字，并且发布到 NPM。</p>
<p>基于 Vue3 开发，以组件传递 prop 的方式使用，支持文本复制，支持类文本的字号、颜色的定义。</p>
<p>NPM: <a href="http://npmjs.com/package/vue3-seven-segment-display">vue3-seven-segment-display</a></p>
<h3 id="24frp">2.4、FRP 内网穿透</h3>
<p>这个其实没什么好学的，全部部署完成也不过三两行配置脚本而已。</p>
<p>小剧作为一个前端，视野总是局限在浏览器和单一服务上。只有在遇到具体的问题并且难以找到解决方案时，才会试图在其他方向上找思路。</p>
<p><a href="http://bh-lay.com/blog/27dmws2c3hx">《Mongo DB 服务挂起问题排查》</a></p>
<p>上面文章记录了小剧 2023 年下半年，被 Mongo DB 服务挂起问题折磨的记录，以及后来寻找解决方案并尝试实施的经过。</p>
<p>比较辛酸，就不展开说了，感兴趣的点击上面的链接。</p>
<h3 id="25docker">2.5、Docker 容器化部署</h3>
<p>若干年前玩过 Docker，很惊艳，但作为前端对 Docker 的使用并不深。</p>
<p>直到最近开始尝试各种私有化服务，各类 APP 眼花缭乱的部署步骤以及不知道哪一步就会遇到的系统冲突，让小剧又重新捡起了 Docker 这艘巨轮。</p>
<p>就不展开这里的细节了，后面会在 <strong>别墅大改造</strong> 部分再次提到。</p>
<h2 id="-2">三、到不了的远方</h2>
<p>可能是因为娃太小，不太便于长途奔波；也可能是小剧并没有周密的去做过计划。2023 年小剧的出行也只在合肥周边一百多公里范围内。</p>
<p>⬇️ 紫蓬山庙会
<img src="https://static.bh-lay.com/blog/2023-year-end/zipengshan.jpg" alt="紫蓬山庙会" /></p>
<p>⬇️ 南京钟山
<img src="https://static.bh-lay.com/blog/2023-year-end/nanjing.jpg" alt="南京钟山" /></p>
<p>⬇️ 祥源花世界
<img src="https://static.bh-lay.com/blog/2023-year-end/huashijie.jpg" alt="祥源花世界" /></p>
<p>⬇️ 三河古镇
<img src="https://static.bh-lay.com/blog/2023-year-end/sanhe.jpg" alt="三河古镇" /></p>
<p>⬇️ 在公司楼下露营
<img src="https://static.bh-lay.com/blog/2023-year-end/camping.jpg" alt="公司露营" /></p>
<p>⬇️ 翡翠湖公园
<img src="https://static.bh-lay.com/blog/2023-year-end/feicuihu.jpg" alt="翡翠湖公园" /></p>
<p>⬇️ 长江不夜城
<img src="https://static.bh-lay.com/blog/2023-year-end/changjiang.jpg" alt="长江不夜城" /></p>
<p>⬇️ 肥东县洪疃村
<img src="https://static.bh-lay.com/blog/2023-year-end/feidong-hongtuancun.jpg" alt="肥东县洪疃村" /></p>
<h2 id="-3">四、别墅大改造</h2>
<h3 id="41">4.1、布置书房</h3>
<p>书房是小剧每日必呆的地方，虽然只有小小的 2m²，但独立的小空间足够小剧办公与折腾。公司每周一天的居家办公，这里便是小剧的临时办公室。</p>
<p>2023 年陆陆续续对这个空间作了很多改造。</p>
<p>斥巨资换掉了闷热笨重的椅子，换了把更舒适的网面椅，购入了名字很讨巧的腹灵 F12 键盘，以及巨贵巨好用的罗技 MX Master 3S 鼠标。</p>
<p>利用废旧板材动手制作了显示器增高架，根据 3D 模型在宜家现场建模，购入了最大尺寸的洞洞板，更换了全尺寸的照片墙。</p>
<p>可能若干年后小剧会在一个更大的书房里，怀念这个 2m² 空间的点点滴滴吧。</p>
<p>⬇️ 书房现状
<img src="https://static.bh-lay.com/blog/2023-year-end/study-room.jpg" alt="书房" /></p>
<p>⬇️ 书房3D模型
<img src="https://static.bh-lay.com/blog/2023-year-end/study-structure.jpg" alt="书房3D模型" /></p>
<p>⬇️ 徒手DIY显示器增高架
<img src="https://static.bh-lay.com/blog/2023-year-end/diy-bracket.jpg" alt="DIY显示器增高架" /></p>
<p>⬇️ DIY草太椅
<img src="https://static.bh-lay.com/blog/2023-year-end/diy-chair.jpg" alt="DIY草太椅" /></p>
<h3 id="42">4.2、尝试私有化服务</h3>
<p>在 <a href="https://bh-lay.com/blog/8hfd4n48pd">《小剧的2021》</a> 中曾经提到照片存储方案探索，这种简陋的方案到目前也已经用了两年多。易用性不太够，但贵在稳定，并且可靠性很高。</p>
<p>为了提高本地存储使用的易用性，来来回回换了很多辅助的服务和 APP，这部分一直在折腾，还没有稳定下来，不值得分享，简单给大家看看其中两个常用的服务吧。</p>
<p>⬇️ Photoview，本地相册服务
<img src="https://static.bh-lay.com/blog/2023-year-end/photoview.jpg" alt="wiki 系统" /></p>
<p>⬇️ Wiki.js，本地文档库
<img src="https://static.bh-lay.com/blog/2023-year-end/wiki-js.jpg" alt="wiki 系统" /></p>
<p>家里有台 2013 款古董级的 Macmini，就是托管这些服务的物理机。机器很老，配置也极差，但装上 Docker 跑些 Web 服务却不在话下。</p>
<p>目前遇到的瓶颈是 Immich 服务始终无法运行 AI 相关能力，导致智能识图、人脸识别任务失败。</p>
<h2 id="-4">五、工作上的经历</h2>
<p>2023 年是小剧在 ZOOM 的第二年，很感谢在合肥这座互联网底蕴并不深厚的城市，能有 ZOOM 这样一家走在科技前沿的企业。</p>
<p>和大家一样，ZOOM 的 2023 年也并非一帆风顺。</p>
<h3 id="51">5.1、年初裁员</h3>
<p>对我们公司有了解的同学可能知道，2023 年初，我们公司经历过一轮波及面较广的裁员。大约有 15% 的小伙伴从我们这个集体离开。</p>
<p>很幸运，小剧不在这次名单之中。</p>
<p>对于这次“壮士断腕”的裁员，极快的速度和相对较高的比例，让很多同事非常震惊。</p>
<p>同时超额的补偿和不拖泥带水的效率，也让整个过程显得不那么“残忍”。</p>
<p>作为工作十年的“老油条”，这不是小剧经历的第一次裁员，相信也不会是最后一次。</p>
<p>但却是小剧最信服的一次裁员。</p>
<h3 id="52">5.2、敢于开口了</h3>
<p>因为 ZOOM 是一家外企，时不时的需要和大洋彼岸的同事沟通。</p>
<p>作为英语渣渣的小剧，从 2022 年勉强能听懂对方在说什么，到现在也能简单地说一说自己的观点。</p>
<p>小剧的语法、发音其实并不准确，词汇量也没有明显的增长，but it does not matter。</p>
<p>我们工作中的沟通并不严格要求信达雅，能起到准确的表意最重要。</p>
<p>或 repeat again，或借助翻译工具、再或者手描笔划，实在不行拉一个外援，总归有办法解决单次沟通的问题。</p>
<p>开口、沟通，最重要。</p>
<h2 id="-5">六、摄影还拍么？</h2>
<p>翻了翻 2023 年的相册，一张张几乎都是娃的大脸盘子，能拿得出来的，勉强跟「摄影」沾边的照片几乎没有。</p>
<p>八月份在合肥即将拆迁的照山+高桥+ 龙王片区，拍摄了以<strong>楼梯</strong>为主题的组图，但它们至今仍躺在 NAS 里，还没有被进一步的加工。</p>
<p>零散的放一些今年拍过的照片吧，也算是证明这个爱好还在。</p>
<p>⬇️ 书房外的夕阳
<img src="https://static.bh-lay.com/blog/2023-year-end/sunset.jpg" alt="书房外的夕阳" /></p>
<p>⬇️ 婺源 - 这张是媳妇拍的，凑个数
<img src="https://static.bh-lay.com/blog/2023-year-end/wuyuan.jpg" alt=" 婺源" /></p>
<p>⬇️ 雪地留痕
<img src="https://static.bh-lay.com/blog/2023-year-end/traces-in-snow.jpg" alt="雪地留痕" /></p>
<p>⬇️ 大数据中心
<img src="https://static.bh-lay.com/blog/2023-year-end/big-data-center.jpg" alt="大数据中心" /></p>
<h2 id="2023">七、2023 真的很平庸么？</h2>
<p>疫情放开后的第一年，经济全面复苏的预期被打破，楼市股市都朝着大家期待的反方向一路狂奔。</p>
<p>在大盘下行的背景下，个体的平庸可能也是一种“暴涨”。</p>
<p>小剧的 2023 年更多的时间花在了陪伴宝宝的成长上，也算是另一种不一样的收获吧。</p>
<p>仍然维持着工作之外的学习，避免技能上的一叶障目，虽不会立即带来收益，长久来看大有益处。</p>
<p>工作上没有相当耀眼的表现，高质量的产出和负责的态度，还是博得了身边同事和领导的认可。</p>
<p>所以，平庸的 2023，看起来也很不错哦！</p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Mon, 05 Feb 2024 15:58:07 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/22jy29grksw</guid>
      <category>年终总结</category>
      <category>生活</category>
      <category>2023</category>
      <enclosure url="http://static.bh-lay.com/blog/2023-year-end/xiaomotuo.jpeg" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>又到一年岁末时，小剧似乎已经慢慢接受了 2023 年的平庸，在这一年里平平静静地经历着人生的特殊阶段。</p>
<p>相比于 2022 年小剧在工作和生活上较大的变化，2023 年一直在重复着工作、带娃、偶尔搞一搞自己小东西的状态，平淡却不枯燥。</p>
<p>前面提到的<strong>平庸、平静、平淡</strong>，大概就是小剧 2023 年的关键词吧。</p>
<h2 id="">一、日常带娃的生活</h2>
<p>2023 年是宝宝从接近一周岁开始慢慢成长的一年。这一年小剧见证了娃从憨憨傻傻到反骨萌芽，从匍匐攀爬到逛街溜达。每一天好像没有任何变化，不经意间又发现这个小小的人也在慢慢长大。</p>
<p>随着娃的四肢日渐发达，破坏力也在与日俱增。这一年里对家里进行了一次次无死角的“扫荡”，辛苦媳妇要日复一日的收拾整理。</p>
<p><img src="https://static.bh-lay.com/blog/2023-year-end/chaotic-corner.jpg" alt="一地玩具" /></p>
<p><img src="https://static.bh-lay.com/blog/2023-year-end/qiaosi-on-table.jpg" alt="霸占书桌" /></p>
<p>偶尔可爱起来，似乎也没有那么讨人厌。</p>
<p><img src="https://static.bh-lay.com/blog/2023-year-end/qiaosi.jpg" alt="光头娃娃" /></p>
<p>这一年陪着娃逛遍了周边的商场、公园、超市、游乐馆。探索了很多平时很常见，却又很容易被忽略的地方。</p>
<p>之所以把娃放在最前面来介绍，一是因为所谓的父爱吧，最重要的原因是，小剧 2023 年几乎绝大多数业余时光都和她一起度过。</p>
<h2 id="-1">二、今年偷偷学了啥？</h2>
<p>2023 年虽然拿不出整块的时间学一些新玩意儿，可利用的碎片时间依旧有很多。</p>
<p>这一年在业余生活中，除了学习前端老本行的技能点，还学了些 docker 容器化操作、FRP 内网穿透配置等杂七杂八的小东西。</p>
<h3 id="21nodejs">2.1、NodeJS 服务端重构</h3>
<p>年初对小剧客栈服务端做了一次彻底的重构。将 NodeJS 服务端逻辑全部使用 TypeScript 进行了重写，并且使用 Promise  重新组织各服务端逻辑。</p>
<p>严格来说，这部分其实并没有什么学习的成分可言，单纯是服务端代码过于老旧需要进行一次重构而已。并且 TypeScript 和 Promise 的逻辑也已经在工作中用过很多遍了。</p>
<p>之所以将这部分算在 2023 年的学习目录，是因为这一次系统的分析了常见的已有项目向 TypeScript 迁移的方案，以及业务代码、第三方包等不同的逻辑代码迁移的手段。</p>
<p>并且补上了服务端 Debugger 模式的支持，会话级别的错误捕捉等逻辑。</p>
<p>针对这次迁移，写了 <a href="http://bh-lay.com/blog/njs1l1pod0">《Node 迁移 TypeScript 记录》</a>，如果你感兴趣的话可以查看这篇文章。</p>
<h3 id="22indexeddb">2.2、IndexedDB 数据库升级</h3>
<p>相信你也知道，小剧在 2021-2022 年结合自己想学习的技能点，写了 <a href="https://e.bh-lay.com">小剧起始页</a> 这样一个小玩意儿。</p>
<p>关于小剧起始页的想法来源、早期想学习的技能点，以及做出来的初代成品，都可以参考当时写的 <a href="http://bh-lay.com/blog/2o7r91ml5hg">《【开发回顾】小剧起始页》</a>。</p>
<p>小剧起始页正式上线后，小剧并没有对它进行实质的推广，一来因为这是一个很具有个人色彩的工具，灵感来源及后期维护也仅仅是以小剧自己为出发点。二来市面上有很多类似的工具作为替代，小剧也不想把精力消耗在这个纯投入，且无法带来任何收益的项目上。</p>
<p>2023 年在个人的工作、学习、生活很多方面，涉及到链接收集与归档的工作，都放在了小剧起始页上。随着使用深度的加强，愈发觉得在不同场景下的数据管理过于冗杂，很难把数据组织的很具有条理性。</p>
<p>经过了一段时间的构思与设计，决定为小剧起始页增加<strong>多桌面</strong>的特性。</p>
<p>如果你有看过小剧起始页的实现，你应该知道它是一个支持纯离线使用的 PWA 项目。数据的存取均在浏览器本地，不涉及任何服务端 API。</p>
<p>多桌面的特性需要涉及到历史数据的转换，因此也就有了 IndexedDB 数据库升级的学习。</p>
<p>小剧对 IndexedDB 的使用为裸调 API，没有使用任何封装后的库，因此数据库升级的逻辑需要自己去尝试封装。</p>
<p>这部分经历可以参考 <a href="http://bh-lay.com/blog/1g079snv7oo">《小剧起始页 IndexedDB 数据库升级记录》</a>。</p>
<h3 id="23npm">2.3、发布了一个 NPM 包</h3>
<p>2023 年上半年，有一个对于小剧来说非常重要的日子，几乎每天小剧都在数着离这一天还有多久。</p>
<p>前面提到小剧每天都会使用小剧起始页，在这里做一个可配置的倒计时组件岂不是一件美事。</p>
<p>双方一拍即合（小剧自己跟自己拍），决定把可配置倒计时组件的开发提上日程。</p>
<p>很快组件就设计、开发好了，一个人没有沟通成本就是快哈。</p>
<p>然而平平的一块板板，上面堆着一排数字，单调且不精致。很难过小剧审美这道关。</p>
<p>后来机缘巧合刷到了 Youtube 大神分享的《Segmented Display》视频，介绍了工业时代显示数字的各种手段。详细介绍了后来被广泛使用的七段线晶体管，以及各种字体变种。</p>
<p>后来小剧查找了历史上各种经典的七段线版本，最终选择了一款厚重又俏皮的非对称版本，作为倒计时的字体基础。</p>
<p><strong>新老设计对比图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2023-year-end/seven-segment.jpg" alt="新老设计对比图" /></p>
<p>这部分经历原本写了份博文，但拖到现在还没有完稿，有机会重新编辑下再发布。</p>
<p>倒计时小组件完成的同时，小剧封装了一份 NPM 包，用来显示七段线风格的数字，并且发布到 NPM。</p>
<p>基于 Vue3 开发，以组件传递 prop 的方式使用，支持文本复制，支持类文本的字号、颜色的定义。</p>
<p>NPM: <a href="http://npmjs.com/package/vue3-seven-segment-display">vue3-seven-segment-display</a></p>
<h3 id="24frp">2.4、FRP 内网穿透</h3>
<p>这个其实没什么好学的，全部部署完成也不过三两行配置脚本而已。</p>
<p>小剧作为一个前端，视野总是局限在浏览器和单一服务上。只有在遇到具体的问题并且难以找到解决方案时，才会试图在其他方向上找思路。</p>
<p><a href="http://bh-lay.com/blog/27dmws2c3hx">《Mongo DB 服务挂起问题排查》</a></p>
<p>上面文章记录了小剧 2023 年下半年，被 Mongo DB 服务挂起问题折磨的记录，以及后来寻找解决方案并尝试实施的经过。</p>
<p>比较辛酸，就不展开说了，感兴趣的点击上面的链接。</p>
<h3 id="25docker">2.5、Docker 容器化部署</h3>
<p>若干年前玩过 Docker，很惊艳，但作为前端对 Docker 的使用并不深。</p>
<p>直到最近开始尝试各种私有化服务，各类 APP 眼花缭乱的部署步骤以及不知道哪一步就会遇到的系统冲突，让小剧又重新捡起了 Docker 这艘巨轮。</p>
<p>就不展开这里的细节了，后面会在 <strong>别墅大改造</strong> 部分再次提到。</p>
<h2 id="-2">三、到不了的远方</h2>
<p>可能是因为娃太小，不太便于长途奔波；也可能是小剧并没有周密的去做过计划。2023 年小剧的出行也只在合肥周边一百多公里范围内。</p>
<p>⬇️ 紫蓬山庙会
<img src="https://static.bh-lay.com/blog/2023-year-end/zipengshan.jpg" alt="紫蓬山庙会" /></p>
<p>⬇️ 南京钟山
<img src="https://static.bh-lay.com/blog/2023-year-end/nanjing.jpg" alt="南京钟山" /></p>
<p>⬇️ 祥源花世界
<img src="https://static.bh-lay.com/blog/2023-year-end/huashijie.jpg" alt="祥源花世界" /></p>
<p>⬇️ 三河古镇
<img src="https://static.bh-lay.com/blog/2023-year-end/sanhe.jpg" alt="三河古镇" /></p>
<p>⬇️ 在公司楼下露营
<img src="https://static.bh-lay.com/blog/2023-year-end/camping.jpg" alt="公司露营" /></p>
<p>⬇️ 翡翠湖公园
<img src="https://static.bh-lay.com/blog/2023-year-end/feicuihu.jpg" alt="翡翠湖公园" /></p>
<p>⬇️ 长江不夜城
<img src="https://static.bh-lay.com/blog/2023-year-end/changjiang.jpg" alt="长江不夜城" /></p>
<p>⬇️ 肥东县洪疃村
<img src="https://static.bh-lay.com/blog/2023-year-end/feidong-hongtuancun.jpg" alt="肥东县洪疃村" /></p>
<h2 id="-3">四、别墅大改造</h2>
<h3 id="41">4.1、布置书房</h3>
<p>书房是小剧每日必呆的地方，虽然只有小小的 2m²，但独立的小空间足够小剧办公与折腾。公司每周一天的居家办公，这里便是小剧的临时办公室。</p>
<p>2023 年陆陆续续对这个空间作了很多改造。</p>
<p>斥巨资换掉了闷热笨重的椅子，换了把更舒适的网面椅，购入了名字很讨巧的腹灵 F12 键盘，以及巨贵巨好用的罗技 MX Master 3S 鼠标。</p>
<p>利用废旧板材动手制作了显示器增高架，根据 3D 模型在宜家现场建模，购入了最大尺寸的洞洞板，更换了全尺寸的照片墙。</p>
<p>可能若干年后小剧会在一个更大的书房里，怀念这个 2m² 空间的点点滴滴吧。</p>
<p>⬇️ 书房现状
<img src="https://static.bh-lay.com/blog/2023-year-end/study-room.jpg" alt="书房" /></p>
<p>⬇️ 书房3D模型
<img src="https://static.bh-lay.com/blog/2023-year-end/study-structure.jpg" alt="书房3D模型" /></p>
<p>⬇️ 徒手DIY显示器增高架
<img src="https://static.bh-lay.com/blog/2023-year-end/diy-bracket.jpg" alt="DIY显示器增高架" /></p>
<p>⬇️ DIY草太椅
<img src="https://static.bh-lay.com/blog/2023-year-end/diy-chair.jpg" alt="DIY草太椅" /></p>
<h3 id="42">4.2、尝试私有化服务</h3>
<p>在 <a href="https://bh-lay.com/blog/8hfd4n48pd">《小剧的2021》</a> 中曾经提到照片存储方案探索，这种简陋的方案到目前也已经用了两年多。易用性不太够，但贵在稳定，并且可靠性很高。</p>
<p>为了提高本地存储使用的易用性，来来回回换了很多辅助的服务和 APP，这部分一直在折腾，还没有稳定下来，不值得分享，简单给大家看看其中两个常用的服务吧。</p>
<p>⬇️ Photoview，本地相册服务
<img src="https://static.bh-lay.com/blog/2023-year-end/photoview.jpg" alt="wiki 系统" /></p>
<p>⬇️ Wiki.js，本地文档库
<img src="https://static.bh-lay.com/blog/2023-year-end/wiki-js.jpg" alt="wiki 系统" /></p>
<p>家里有台 2013 款古董级的 Macmini，就是托管这些服务的物理机。机器很老，配置也极差，但装上 Docker 跑些 Web 服务却不在话下。</p>
<p>目前遇到的瓶颈是 Immich 服务始终无法运行 AI 相关能力，导致智能识图、人脸识别任务失败。</p>
<h2 id="-4">五、工作上的经历</h2>
<p>2023 年是小剧在 ZOOM 的第二年，很感谢在合肥这座互联网底蕴并不深厚的城市，能有 ZOOM 这样一家走在科技前沿的企业。</p>
<p>和大家一样，ZOOM 的 2023 年也并非一帆风顺。</p>
<h3 id="51">5.1、年初裁员</h3>
<p>对我们公司有了解的同学可能知道，2023 年初，我们公司经历过一轮波及面较广的裁员。大约有 15% 的小伙伴从我们这个集体离开。</p>
<p>很幸运，小剧不在这次名单之中。</p>
<p>对于这次“壮士断腕”的裁员，极快的速度和相对较高的比例，让很多同事非常震惊。</p>
<p>同时超额的补偿和不拖泥带水的效率，也让整个过程显得不那么“残忍”。</p>
<p>作为工作十年的“老油条”，这不是小剧经历的第一次裁员，相信也不会是最后一次。</p>
<p>但却是小剧最信服的一次裁员。</p>
<h3 id="52">5.2、敢于开口了</h3>
<p>因为 ZOOM 是一家外企，时不时的需要和大洋彼岸的同事沟通。</p>
<p>作为英语渣渣的小剧，从 2022 年勉强能听懂对方在说什么，到现在也能简单地说一说自己的观点。</p>
<p>小剧的语法、发音其实并不准确，词汇量也没有明显的增长，but it does not matter。</p>
<p>我们工作中的沟通并不严格要求信达雅，能起到准确的表意最重要。</p>
<p>或 repeat again，或借助翻译工具、再或者手描笔划，实在不行拉一个外援，总归有办法解决单次沟通的问题。</p>
<p>开口、沟通，最重要。</p>
<h2 id="-5">六、摄影还拍么？</h2>
<p>翻了翻 2023 年的相册，一张张几乎都是娃的大脸盘子，能拿得出来的，勉强跟「摄影」沾边的照片几乎没有。</p>
<p>八月份在合肥即将拆迁的照山+高桥+ 龙王片区，拍摄了以<strong>楼梯</strong>为主题的组图，但它们至今仍躺在 NAS 里，还没有被进一步的加工。</p>
<p>零散的放一些今年拍过的照片吧，也算是证明这个爱好还在。</p>
<p>⬇️ 书房外的夕阳
<img src="https://static.bh-lay.com/blog/2023-year-end/sunset.jpg" alt="书房外的夕阳" /></p>
<p>⬇️ 婺源 - 这张是媳妇拍的，凑个数
<img src="https://static.bh-lay.com/blog/2023-year-end/wuyuan.jpg" alt=" 婺源" /></p>
<p>⬇️ 雪地留痕
<img src="https://static.bh-lay.com/blog/2023-year-end/traces-in-snow.jpg" alt="雪地留痕" /></p>
<p>⬇️ 大数据中心
<img src="https://static.bh-lay.com/blog/2023-year-end/big-data-center.jpg" alt="大数据中心" /></p>
<h2 id="2023">七、2023 真的很平庸么？</h2>
<p>疫情放开后的第一年，经济全面复苏的预期被打破，楼市股市都朝着大家期待的反方向一路狂奔。</p>
<p>在大盘下行的背景下，个体的平庸可能也是一种“暴涨”。</p>
<p>小剧的 2023 年更多的时间花在了陪伴宝宝的成长上，也算是另一种不一样的收获吧。</p>
<p>仍然维持着工作之外的学习，避免技能上的一叶障目，虽不会立即带来收益，长久来看大有益处。</p>
<p>工作上没有相当耀眼的表现，高质量的产出和负责的态度，还是博得了身边同事和领导的认可。</p>
<p>所以，平庸的 2023，看起来也很不错哦！</p>]]></content:encoded>
    </item>
    <item>
      <title>Mongo DB 服务挂起问题排查</title>
      <link>https://bh-lay.com/blog/27dmws2c3hx</link>
      <description><![CDATA[<p>2023 年初，小剧将稳定运行近十年的服务端代码，进行了一次彻底的重构。完成后写了<a href="https://bh-lay.com/blog/njs1l1pod0">《Node 迁移 TypeScript 记录》</a> 作为回顾记录。这篇文章的侧重点在 NodeJS 的 TypeScript 改造，以及 Promise 异步逻辑链重构上。对于 mongo DB 的改动并未过多提及。</p>
<p>在这次改造期间，为了使用最新版本的、支持 Promise 模式的 Mongo DB 连接库。一狠心将使用了近十年 v1.8.1 版本的数据库，升级到了 v4.0.27 版本。</p>
<p>正是这一决定，在接下来的大半年里时不时的就来折腾一下小剧。</p>
<h2 id="">一、遇到了什么问题？</h2>
<p>起初并没有任何异常，小博客也能平稳的运行。直到 2023 年 5 月的某一天，小剧觉得博客的重构彻底告一段落了，于是对服务做了些配置调整后，就不打算理会它了。</p>
<p>大概一个月之后吧，博客竟然毫无征兆的挂了，表现出来就是 API 无法正常返回数据。</p>
<p>经过一番排查，发现是数据库无响应导致的。</p>
<p>这倒很简单，<strong>运维问题一半可以通过重启解决，另一半可以通过多次重启来解决。</strong></p>
<p>强制重启了 Mongo DB 数据库，一切又恢复了正常。</p>
<p>这一次的服务挂起只是开端，接下来多则半个月，短则两三天，服务都会冷不丁的挂掉。</p>
<p><strong>因为要陪孩子和家人，博客也只是业余时间的消遣而已，所以没有花时间研究问题的根源的打算。</strong></p>
<p>又编写了一个重启 Mongo DB 的 Shell 脚本丢在服务器上，再遇到服务挂起的时候直接运行脚本就行。</p>
<h2 id="-1">二、第一次尝试解决</h2>
<p>很快就到了秋末，某一天闲下来无事可做，就想着翻翻 Mongo DB 服务的日志。发现每次服务挂起，都是因为系统资源耗尽导致的。</p>
<h3 id="21">2.1、初步分析原因</h3>
<p>有了日志的支撑，接下来就有了排查的方向。因为 Mongo DB 服务挂起只发生在服务端代码重构之后。因此重点关注重构前后的改动，就可以更接近问题的本源。</p>
<p>此次重构和数据库强相关的改动有三个，分别为：</p>
<ol>
<li>Mongo DB 版本 由 v1.8.1 升级到 v4.0.27</li>
<li>Mongo DB 连接库由 v3.1.13 升级到 v4.17.0</li>
<li>删除了很多 API 的缓存逻辑，并且新增了凌晨三点清除全部缓存的逻辑</li>
</ol>
<p>关于第 1 点，Mongo DB 官方没有给出直观的数据对比，但是通过本地不严谨的对比，新版本对内存、CPU 的占用确实有明显的上升。</p>
<p>第 2 点并未发现明显的问题，因为作为连接库，最有可能的问题就是内存泄漏，实际排查并无此问题。</p>
<p>第 3 点比较明显，因为博客作为一个纯个人的服务，内容更新实时性很低很低。除了少量接口需要动态获取数据，绝大多数的都可以使用缓存。</p>
<p>因此开启缓存，可以使绝大多数 API 直接返回数据，而跳过数据库连接。【<a href="https://github.com/bh-lay/blog/blob/master/backEnd/src/core/cache.ts">查看缓存的实现</a>】</p>
<p>结合前面 1、2、3 点的整理来看，初步排查 1、3 是最有可能导致 Mongo DB 服务挂起的原因。</p>
<p>总结下来就是：升级后的服务对资源占用更高，并且因为缓存利用率不高，导致百度之类的爬虫抓取数据时，短时间内系统无法提供足够的资源，导致进程挂起。</p>
<p>忘了补充一件事，小剧的服务器是一核心、1G 内存、1M 带宽的丐中丐配置。所以<strong>服务挂起的问题是穷病，治不了</strong>，谁叫我没有钱升级服务器配置呢。</p>
<h3 id="22">2.2、怎么解决呢？</h3>
<p>数据库降级这条路不太可行，因为服务已经按照新的 API 重构了代码，调整比较耗时。</p>
<p>那就只有把缓存这块<strong>遮羞布</strong>再盖上，再禁用掉主动清除缓存，来减少数据库的读写压力，进而降低对系统资源的占用，大幅降低 Mongo DB 服务挂起的概率。</p>
<h3 id="23">2.3、效果怎么样？</h3>
<p>因为大幅降低了对数据库的读取操作，Mongo DB 服务挂起的的问题得到了大幅改善。平均两周左右才会挂起一次。</p>
<h2 id="-2">三、再次解决服务挂起的问题</h2>
<p>转眼就要 2024 年了，又到了写年终总结的时候。小剧可不希望发出去一条年终链接后，打开却是不确定的服务挂起状态。为了避免这种尴尬的场景，小剧决定花点时间彻底解决 Mongo DB 服务挂起的问题。</p>
<p>正如上面说的一样，前期的解决方案只是块<strong>遮羞布</strong>，只能大幅<strong>降低</strong> Mongo DB 服务挂起的概率。</p>
<p>其实核心问题很简单，<strong>配置较弱的机器，无法稳定提供 Mongo DB 对资源的需求</strong>。</p>
<p>解决这个问题有三个方案，分别为：</p>
<ol>
<li>降级 Mongo DB 至原始版本</li>
<li>提升服务器配置</li>
<li>将 Mongo DB 服务转移到其他机器</li>
</ol>
<p>前文提到了，数据库降级需要大量重构逻辑代码。小剧现在并不像没娃的时候一样，有大量的时间做这些重复性的改动。并且开历史的倒车，也感觉不是很光彩，不到万不得已不会这么做，因此方案一直接就被否了。</p>
<p>提升服务器配置这一条倒挺简单。只要提前备份好服务数据，在腾讯云（小剧在各种云之间来回换，最近在用腾讯云）后台选择升级到的配置，提交等待重启就完事了。</p>
<p>但是云计算的资源就是这样，内存或者 CPU 提升一点点，价格就会成倍的上升。小剧客栈作为一个纯装X，没有任何收入来平摊成本的个人项目。成本的上升是我所不愿接受的。</p>
<h3 id="31mongodb">3.1、思考 Mongo DB 转移方案</h3>
<p>至此只剩下第三个方案了，直观地来看，这个方案和方案二「提升服务器配置」很像。甚至在花费上不一定比方案二便宜。</p>
<p>这个方案如果能实施落地，至少需要以下几个必要条件：</p>
<ul>
<li>需要充分利用手上的资源，避免不必要的花费</li>
<li>需要一台独立的机器，能够 7 * 24 小时运行，并且配置不低于我的主服务器</li>
<li>这台机器需要能够为主服务器提供服务（相互连接）</li>
</ul>
<p>结合小剧手上的资源来看，还真有一台机器满足要求，后面叫这台服务器为「从服务器」。</p>
<p>文章里不方便介绍从服务器具体的操作系统、部署方式，以及物理位置等信息。如果你有小剧的联系方式，我们可以私下聊聊这台机器。</p>
<p>这里简单的根据网络结构来做下介绍。</p>
<p>它是一个内部网络之中的机器，没有公网 IP，并且和主服务器不在同一个内网，上下行带宽不低于 30M。</p>
<p>从服务器的配置并不算高，但是 8G 内存、双核 CPU 足够 Mongo DB 去驰骋了。</p>
<p>这台机器目前就是 7 * 24 小时运行，运行了一些轻量服务，无需为了这台机器额外付费。</p>
<p>看起来万事俱备，只欠东风了。</p>
<p>具体到实施，有两个很具体的问题需要解决。</p>
<ul>
<li>原本 NodeJS 与 Mongo DB 在同一台机器，数据读取几乎不会因为连接而增加耗时。如何尽量降低网络时延对体验的影响？</li>
<li>机器处在内网中，并且需要对外提供服务，如何组网实现？</li>
</ul>
<p>第一个问题可以先放一放，因为博客更新实时性不高的特性，缓存这块遮羞布还能继续用。并且实际延迟平均是多少还不确定，不需要在尚未开工前将时间精力耗费在这里。</p>
<p>第二个问题比较棘手，毕竟大家都知道，没有公网 IP 是没办法<strong>直接</strong>在互联网上提供服务的。</p>
<h3 id="32">3.2、内网服务组网方案</h3>
<p>内网虽然无法直接提供服务，间接提供的方案倒有很多，我们可以按照机器上层网络的不同情况来看。</p>
<h4 id="321ip">3.2.1、上层网络具备公网 IP，并且有操作权限。</h4>
<p>3.2.1.1、如果上层网络的<strong>公网 IP 是固定的</strong>，在上层网络<strong>设置端口转发</strong>，就能以上层网络的身份提供服务。</p>
<p>有点像小剧住在固定的小区，并且告诉保安大爷，只要有人找小剧，就让 TA 去 X 栋 XXX 找我。</p>
<p>3.2.1.2、如果上层的<strong>公网 IP 是可变的</strong>，也不麻烦，同样是需要<strong>设置端口转发</strong>，额外再借助 <strong>DDNS</strong> 以域名的形式提供服务，或者在主服务器提供一个 API，用来动态接收变动后的 IP，使用服务的时候以<strong>动态 IP</strong> 来执行。</p>
<p>这种模式类似于小剧每隔一段时间就要搬一次家，小剧每搬完家就会把小区地址告诉小剧的朋友 Jim，Jim 有固定的住所。并且小剧会告诉保安大爷，只要有人找小剧，就让 TA 去 X 栋 XXX 找我。这样别人就可以通过小剧的朋友 Jim 找到小区，再通过保安大爷找到我。</p>
<h4 id="322ip">3.2.2、上层网络不具备公网 IP，或者没有操作权限。</h4>
<p>3.2.2.1、<strong>借助于 VPN，实现虚拟组网。</strong></p>
<p>这种网路基础建设起来比较复杂，一般针对多个网络系统融合或者端到系统的接入。</p>
<p>类似于你来找小剧，必须要先到保安大爷那里审核登记，完事了给你颁发一张小区的登记卡，你得时刻拿着这张登记卡才能在小区内有限制的通行，在保安大爷的指引下最后找到小剧。</p>
<p>3.2.2.2、借助于 FRP、NGROK 之类的工具，<strong>提供内网穿透服务</strong>。</p>
<p>这是一种反向代理技术，借助于主服务器提供内网应用转发。</p>
<p>类似于小剧没有固定住所，但小剧的朋友 Jim 有，小剧在 Jim 家里安装了一台电话机。只要小剧搬家就会给 Jim 打电话，电话一直占线不挂，如果挂了就重新拨过去。别人要来找小剧，就去找 Jim，拿起那部电话机进行通话。</p>
<h3 id="33mongodb">3.3、实施 Mongo DB 服务转移</h3>
<p>前端面分析的四种情况，除了 3.2.1.1 提到的固定公网 IP 不满足，剩余三种都可以使用。</p>
<p>其中 3.2.1.2 看起来是最理想的选择。但是 DDNS 本质上是用 DNS 来获取动态 IP，在域名服务商、网络、应用等各个环节都会有缓存，遇到 IP 变动时很难及时更新。动态 IP 方案看起来也不错，但是需要写客户端上报脚本、服务端接收、更新脚本、博客动态读取 IP 配置等代码，并且还得保证这些新的逻辑稳定运行。</p>
<p>而自建 VPN 会使得系统设计过于庞大，杀鸡用牛刀。</p>
<p>相比之下 3.2.2.2 内网穿透组网方式比较轻便灵活，特别适合服务间接入。</p>
<p>咨询了有 FRP 使用经验的小马（此人很懒，没有博客地址）后，决定使用 FRP 来实现服务间的组网。</p>
<p>至此调研阶段结束，正式开始 Mongo DB 服务转移工作。</p>
<h4 id="331mongodb">3.3.1、Mongo DB 服务安装部署</h4>
<p>因为从服务器内有 Docker 环境，安装异常简单，配置好数据库、账号、密码也就十几分钟完成。</p>
<p>在这期间 mongodump 了主服务器中的数据，从服务器 Mongo DB 准备完成后导入数据，数据库相关的操作就算完成了。</p>
<h4 id="332frp">3.3.2、FRP 安装部署</h4>
<p>关于 FRP 的具体使用可以参考 <a href="https://github.com/fatedier/frp">frp 官方仓库</a> ，安装部署很简单，并且支持 Docker 容器，就不介绍部署细节了。</p>
<p>小剧这里的从服务器使用 Docker 安装 FRPC，主服务器通过 Release 包安装配置 FRPS。</p>
<p>测试下 tcp 连通没问题，FRP 配置也算完成了。</p>
<h4 id="333nodejs">3.3.3、NodeJS 博客服务更新数据库连接配置</h4>
<p>经过前两步的操作，主服务器有两个 Mongo DB 服务可用，修改 NodeJS 的 .env 配置文件指向新的数据库，重启 NodeJS 服务，一切就大功告成了。</p>
<p>对了这里小剧多做了一个操作，就是在服务器的入站规则加了 Mongo DB 端口的禁用。</p>
<p>效果就是主服务器内可以访问 Mongo DB，外网无法借助于主服务器 IP + 端口访问数据库，安全性提高了一点点。</p>
<h4 id="334">3.3.4、图解网络流转逻辑</h4>
<p>这里简单画一下终端用户、NodeJS、FRPS、FRPC、Mongo DB 间的数据流转逻辑。</p>
<p><img src="https://static.bh-lay.com/blog/2024/mongo-repair/network-structure.jpg" alt="网络拓扑图" /></p>
<p>您作为在阅读这篇文章的 User，所看到的页面、数据都是主服务器中 NodeJS 的博客服务提供的。</p>
<p>在这篇文章渲染之前需要从 NodeJS 本机的 2222 端口访问数据库，读取文章详情。</p>
<p>当然，这个数据库服务是虚拟的。借助于主服务器的 FRPS 和从服务器的 FRPC 建立起来的连接，将从服务器中 1111 端口映射到了主服务器中的 2222 端口上。</p>
<blockquote>
  <p>类似于小剧没有固定住所，但小剧的朋友 Jim 有，小剧在 Jim 家里安装了一台电话机。只要小剧搬家就会给 Jim 打电话，电话一直占线不挂，如果挂了就重新拨过去。别人要来找小剧，就去找 Jim，拿起那部电话机进行通话。</p>
</blockquote>
<p>这是前文用到过的比喻。主服务器就是这个比喻里的 Jim 家，FRPS 就是 Jim 家那部电话机，FRPC 就是小剧家里那部电话机，所以核心的关键在于 FRP 客户端和服务端之间建立稳定的连接。</p>
<h2 id="mongodb">四、Mongo DB 转移后的问题</h2>
<h3 id="41api">4.1、API 延迟多久？</h3>
<blockquote>
  <p>原本 NodeJS 与 Mongo DB 在同一台机器，数据读取几乎不会因为连接而增加耗时。如何尽量降低网络时延对体验的影响？</p>
</blockquote>
<p>文章写到这里，服务挂起的问题就已经解决了。但前文还遗留了一个问题待解决，就是 MongoDB 转移到从服务器后，API 的响应有没有明显变慢？</p>
<p>以剧中人的朋友圈获取某位好友信息为例，仅对比 <strong>Waiting for server response</strong> 这一指标。</p>
<p><img src="https://static.bh-lay.com/blog/2024/mongo-repair/request-time.jpg" alt="请求耗时" /></p>
<p>Mongo DB 转移前大约为 134ms，转移后大约 339ms，开启缓存后大约 85ms。</p>
<p>不精确地估算，耗时大约增长了 205ms。</p>
<p>205ms 略慢，但对于小剧客栈这样的小博客够用了，并且开启了缓存这块遮羞布之后，耗时更会大幅降低。</p>
<p>因此用局部、低频的耗时增加，换取更为稳定的服务，以及学习到的新的技能，还是值得的。</p>
<h3 id="42">4.2、发现遗留的隐患</h3>
<p>FRP 服务端自带了一个 Web 管理页面，用于查看连接客户端列表，数据吞吐量、以及代理的基本状况。</p>
<p><img src="https://static.bh-lay.com/blog/2024/mongo-repair/frp-dashboard.jpg" alt="frp-dashboard.png" /></p>
<p>小剧客栈 NodeJS 服务端设计的数据库使用模式为：用后即销毁，没有连接复用的设计，理论上在没有并发请求的情况下，Current Connections 应该为零才对。</p>
<p>箭头处的数字不为零，只有两种解释：一是小剧出息了，同一时刻内确实有这么大的连接数；二是某个请求结束后，一直未执行断开连接操作，而 NodeJS 单进程的模式又会导致连接始终不被释放。</p>
<p>很显然小剧并没有这么出息，问题肯定出在断开连接操作上，但检查代码后发现所有数据库连接操作均有断开连接行为。</p>
<p>那只有一种可能，就是数据库连立连接后，在极端情况下遇到业务逻辑异常，比如 FS 读写冲突，局部逻辑健壮性不强导致的报错等问题，进而导致代码未正常直行到数据库断开操作。</p>
<p>此问题虽然不会立即导致 NodeJS 服务和 Mongo DB 崩溃，但累积的无用连接会拖慢服务的响应速度。</p>
<p>随后补充了一段通用逻辑：在建立数据库连接时，默认一秒内会处理完数据的读写操作。若一秒内仍未执行断连操作，则主动断开数据库连接。</p>
<pre><code class="javascript language-javascript">export async function getDbConnect (): Promise&lt;{
client: mongodb.MongoClient,
db: mongodb.Db
}&gt; {
  // 省略逻辑 ……
  // close connect when timout
  const originCloseMethod = client.close
  function closeConnect () {
    return originCloseMethod.apply(client) as unknown as Promise&lt;void&gt;
  }
  client.close = function (): Promise&lt;void&gt; {
    clearTimeout(closeTimoutTimer)
    return closeConnect()
  }
  const closeTimoutTimer = setTimeout(() =&gt; {
    console.trace(’MongoDB missing close connect.‘)
    closeConnect()
  }, 1000)
  // 省略逻辑 ……
}
</code></pre>
<h2 id="-3">五、问题解决了吗？</h2>
<p>在经历了第一次发现 Mongo DB 挂起后，及时启用缓存功能这块遮羞布；第二次分析到问题根源后，决定进行 Mongo DB 服务转移；以及最后补充的数据库超时断连逻辑。</p>
<p>三次操作让 Mongo DB 在起到决定性作用的硬件环境上，得到质了的改善。并且借助于缓存策略，极大降低了请求频次对 Mongo DB 服务的压力。以及极端异常的超时处理，避免了垃圾连接对 NodeJS 和 MongoDB 服务的负担。</p>
<p>经过一个多月的观察，Mongo DB 运行稳定，无任何挂起、无响应的情况。从服务器在每次 IP 变更时都能及时重连上 FRPS。并且随着对逻辑的加强，NodeJS 也极少打印超时断连的日志。</p>
<p><strong>至此 Mongo DB 服务异常挂起的问题得到了的解决。</strong></p>
<p>还额外获得了一个新的思考：小剧一直都是在单节点部署全部服务，这次接触了多节点服务转移，异地备份和负载均衡要不要尝试一下？</p>
<p>从小剧客栈的规模和访问量来看，完全没必要，若是业余时间充足，玩一玩倒也不是不行。</p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Sat, 27 Jan 2024 15:43:51 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/27dmws2c3hx</guid>
      <category>MongoDB</category>
      <category>小剧客栈</category>
      <category>FRP</category>
      <enclosure url="http://static.bh-lay.com/blog/2024/mongo-repair/mongo-bug.jpg" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>2023 年初，小剧将稳定运行近十年的服务端代码，进行了一次彻底的重构。完成后写了<a href="https://bh-lay.com/blog/njs1l1pod0">《Node 迁移 TypeScript 记录》</a> 作为回顾记录。这篇文章的侧重点在 NodeJS 的 TypeScript 改造，以及 Promise 异步逻辑链重构上。对于 mongo DB 的改动并未过多提及。</p>
<p>在这次改造期间，为了使用最新版本的、支持 Promise 模式的 Mongo DB 连接库。一狠心将使用了近十年 v1.8.1 版本的数据库，升级到了 v4.0.27 版本。</p>
<p>正是这一决定，在接下来的大半年里时不时的就来折腾一下小剧。</p>
<h2 id="">一、遇到了什么问题？</h2>
<p>起初并没有任何异常，小博客也能平稳的运行。直到 2023 年 5 月的某一天，小剧觉得博客的重构彻底告一段落了，于是对服务做了些配置调整后，就不打算理会它了。</p>
<p>大概一个月之后吧，博客竟然毫无征兆的挂了，表现出来就是 API 无法正常返回数据。</p>
<p>经过一番排查，发现是数据库无响应导致的。</p>
<p>这倒很简单，<strong>运维问题一半可以通过重启解决，另一半可以通过多次重启来解决。</strong></p>
<p>强制重启了 Mongo DB 数据库，一切又恢复了正常。</p>
<p>这一次的服务挂起只是开端，接下来多则半个月，短则两三天，服务都会冷不丁的挂掉。</p>
<p><strong>因为要陪孩子和家人，博客也只是业余时间的消遣而已，所以没有花时间研究问题的根源的打算。</strong></p>
<p>又编写了一个重启 Mongo DB 的 Shell 脚本丢在服务器上，再遇到服务挂起的时候直接运行脚本就行。</p>
<h2 id="-1">二、第一次尝试解决</h2>
<p>很快就到了秋末，某一天闲下来无事可做，就想着翻翻 Mongo DB 服务的日志。发现每次服务挂起，都是因为系统资源耗尽导致的。</p>
<h3 id="21">2.1、初步分析原因</h3>
<p>有了日志的支撑，接下来就有了排查的方向。因为 Mongo DB 服务挂起只发生在服务端代码重构之后。因此重点关注重构前后的改动，就可以更接近问题的本源。</p>
<p>此次重构和数据库强相关的改动有三个，分别为：</p>
<ol>
<li>Mongo DB 版本 由 v1.8.1 升级到 v4.0.27</li>
<li>Mongo DB 连接库由 v3.1.13 升级到 v4.17.0</li>
<li>删除了很多 API 的缓存逻辑，并且新增了凌晨三点清除全部缓存的逻辑</li>
</ol>
<p>关于第 1 点，Mongo DB 官方没有给出直观的数据对比，但是通过本地不严谨的对比，新版本对内存、CPU 的占用确实有明显的上升。</p>
<p>第 2 点并未发现明显的问题，因为作为连接库，最有可能的问题就是内存泄漏，实际排查并无此问题。</p>
<p>第 3 点比较明显，因为博客作为一个纯个人的服务，内容更新实时性很低很低。除了少量接口需要动态获取数据，绝大多数的都可以使用缓存。</p>
<p>因此开启缓存，可以使绝大多数 API 直接返回数据，而跳过数据库连接。【<a href="https://github.com/bh-lay/blog/blob/master/backEnd/src/core/cache.ts">查看缓存的实现</a>】</p>
<p>结合前面 1、2、3 点的整理来看，初步排查 1、3 是最有可能导致 Mongo DB 服务挂起的原因。</p>
<p>总结下来就是：升级后的服务对资源占用更高，并且因为缓存利用率不高，导致百度之类的爬虫抓取数据时，短时间内系统无法提供足够的资源，导致进程挂起。</p>
<p>忘了补充一件事，小剧的服务器是一核心、1G 内存、1M 带宽的丐中丐配置。所以<strong>服务挂起的问题是穷病，治不了</strong>，谁叫我没有钱升级服务器配置呢。</p>
<h3 id="22">2.2、怎么解决呢？</h3>
<p>数据库降级这条路不太可行，因为服务已经按照新的 API 重构了代码，调整比较耗时。</p>
<p>那就只有把缓存这块<strong>遮羞布</strong>再盖上，再禁用掉主动清除缓存，来减少数据库的读写压力，进而降低对系统资源的占用，大幅降低 Mongo DB 服务挂起的概率。</p>
<h3 id="23">2.3、效果怎么样？</h3>
<p>因为大幅降低了对数据库的读取操作，Mongo DB 服务挂起的的问题得到了大幅改善。平均两周左右才会挂起一次。</p>
<h2 id="-2">三、再次解决服务挂起的问题</h2>
<p>转眼就要 2024 年了，又到了写年终总结的时候。小剧可不希望发出去一条年终链接后，打开却是不确定的服务挂起状态。为了避免这种尴尬的场景，小剧决定花点时间彻底解决 Mongo DB 服务挂起的问题。</p>
<p>正如上面说的一样，前期的解决方案只是块<strong>遮羞布</strong>，只能大幅<strong>降低</strong> Mongo DB 服务挂起的概率。</p>
<p>其实核心问题很简单，<strong>配置较弱的机器，无法稳定提供 Mongo DB 对资源的需求</strong>。</p>
<p>解决这个问题有三个方案，分别为：</p>
<ol>
<li>降级 Mongo DB 至原始版本</li>
<li>提升服务器配置</li>
<li>将 Mongo DB 服务转移到其他机器</li>
</ol>
<p>前文提到了，数据库降级需要大量重构逻辑代码。小剧现在并不像没娃的时候一样，有大量的时间做这些重复性的改动。并且开历史的倒车，也感觉不是很光彩，不到万不得已不会这么做，因此方案一直接就被否了。</p>
<p>提升服务器配置这一条倒挺简单。只要提前备份好服务数据，在腾讯云（小剧在各种云之间来回换，最近在用腾讯云）后台选择升级到的配置，提交等待重启就完事了。</p>
<p>但是云计算的资源就是这样，内存或者 CPU 提升一点点，价格就会成倍的上升。小剧客栈作为一个纯装X，没有任何收入来平摊成本的个人项目。成本的上升是我所不愿接受的。</p>
<h3 id="31mongodb">3.1、思考 Mongo DB 转移方案</h3>
<p>至此只剩下第三个方案了，直观地来看，这个方案和方案二「提升服务器配置」很像。甚至在花费上不一定比方案二便宜。</p>
<p>这个方案如果能实施落地，至少需要以下几个必要条件：</p>
<ul>
<li>需要充分利用手上的资源，避免不必要的花费</li>
<li>需要一台独立的机器，能够 7 * 24 小时运行，并且配置不低于我的主服务器</li>
<li>这台机器需要能够为主服务器提供服务（相互连接）</li>
</ul>
<p>结合小剧手上的资源来看，还真有一台机器满足要求，后面叫这台服务器为「从服务器」。</p>
<p>文章里不方便介绍从服务器具体的操作系统、部署方式，以及物理位置等信息。如果你有小剧的联系方式，我们可以私下聊聊这台机器。</p>
<p>这里简单的根据网络结构来做下介绍。</p>
<p>它是一个内部网络之中的机器，没有公网 IP，并且和主服务器不在同一个内网，上下行带宽不低于 30M。</p>
<p>从服务器的配置并不算高，但是 8G 内存、双核 CPU 足够 Mongo DB 去驰骋了。</p>
<p>这台机器目前就是 7 * 24 小时运行，运行了一些轻量服务，无需为了这台机器额外付费。</p>
<p>看起来万事俱备，只欠东风了。</p>
<p>具体到实施，有两个很具体的问题需要解决。</p>
<ul>
<li>原本 NodeJS 与 Mongo DB 在同一台机器，数据读取几乎不会因为连接而增加耗时。如何尽量降低网络时延对体验的影响？</li>
<li>机器处在内网中，并且需要对外提供服务，如何组网实现？</li>
</ul>
<p>第一个问题可以先放一放，因为博客更新实时性不高的特性，缓存这块遮羞布还能继续用。并且实际延迟平均是多少还不确定，不需要在尚未开工前将时间精力耗费在这里。</p>
<p>第二个问题比较棘手，毕竟大家都知道，没有公网 IP 是没办法<strong>直接</strong>在互联网上提供服务的。</p>
<h3 id="32">3.2、内网服务组网方案</h3>
<p>内网虽然无法直接提供服务，间接提供的方案倒有很多，我们可以按照机器上层网络的不同情况来看。</p>
<h4 id="321ip">3.2.1、上层网络具备公网 IP，并且有操作权限。</h4>
<p>3.2.1.1、如果上层网络的<strong>公网 IP 是固定的</strong>，在上层网络<strong>设置端口转发</strong>，就能以上层网络的身份提供服务。</p>
<p>有点像小剧住在固定的小区，并且告诉保安大爷，只要有人找小剧，就让 TA 去 X 栋 XXX 找我。</p>
<p>3.2.1.2、如果上层的<strong>公网 IP 是可变的</strong>，也不麻烦，同样是需要<strong>设置端口转发</strong>，额外再借助 <strong>DDNS</strong> 以域名的形式提供服务，或者在主服务器提供一个 API，用来动态接收变动后的 IP，使用服务的时候以<strong>动态 IP</strong> 来执行。</p>
<p>这种模式类似于小剧每隔一段时间就要搬一次家，小剧每搬完家就会把小区地址告诉小剧的朋友 Jim，Jim 有固定的住所。并且小剧会告诉保安大爷，只要有人找小剧，就让 TA 去 X 栋 XXX 找我。这样别人就可以通过小剧的朋友 Jim 找到小区，再通过保安大爷找到我。</p>
<h4 id="322ip">3.2.2、上层网络不具备公网 IP，或者没有操作权限。</h4>
<p>3.2.2.1、<strong>借助于 VPN，实现虚拟组网。</strong></p>
<p>这种网路基础建设起来比较复杂，一般针对多个网络系统融合或者端到系统的接入。</p>
<p>类似于你来找小剧，必须要先到保安大爷那里审核登记，完事了给你颁发一张小区的登记卡，你得时刻拿着这张登记卡才能在小区内有限制的通行，在保安大爷的指引下最后找到小剧。</p>
<p>3.2.2.2、借助于 FRP、NGROK 之类的工具，<strong>提供内网穿透服务</strong>。</p>
<p>这是一种反向代理技术，借助于主服务器提供内网应用转发。</p>
<p>类似于小剧没有固定住所，但小剧的朋友 Jim 有，小剧在 Jim 家里安装了一台电话机。只要小剧搬家就会给 Jim 打电话，电话一直占线不挂，如果挂了就重新拨过去。别人要来找小剧，就去找 Jim，拿起那部电话机进行通话。</p>
<h3 id="33mongodb">3.3、实施 Mongo DB 服务转移</h3>
<p>前端面分析的四种情况，除了 3.2.1.1 提到的固定公网 IP 不满足，剩余三种都可以使用。</p>
<p>其中 3.2.1.2 看起来是最理想的选择。但是 DDNS 本质上是用 DNS 来获取动态 IP，在域名服务商、网络、应用等各个环节都会有缓存，遇到 IP 变动时很难及时更新。动态 IP 方案看起来也不错，但是需要写客户端上报脚本、服务端接收、更新脚本、博客动态读取 IP 配置等代码，并且还得保证这些新的逻辑稳定运行。</p>
<p>而自建 VPN 会使得系统设计过于庞大，杀鸡用牛刀。</p>
<p>相比之下 3.2.2.2 内网穿透组网方式比较轻便灵活，特别适合服务间接入。</p>
<p>咨询了有 FRP 使用经验的小马（此人很懒，没有博客地址）后，决定使用 FRP 来实现服务间的组网。</p>
<p>至此调研阶段结束，正式开始 Mongo DB 服务转移工作。</p>
<h4 id="331mongodb">3.3.1、Mongo DB 服务安装部署</h4>
<p>因为从服务器内有 Docker 环境，安装异常简单，配置好数据库、账号、密码也就十几分钟完成。</p>
<p>在这期间 mongodump 了主服务器中的数据，从服务器 Mongo DB 准备完成后导入数据，数据库相关的操作就算完成了。</p>
<h4 id="332frp">3.3.2、FRP 安装部署</h4>
<p>关于 FRP 的具体使用可以参考 <a href="https://github.com/fatedier/frp">frp 官方仓库</a> ，安装部署很简单，并且支持 Docker 容器，就不介绍部署细节了。</p>
<p>小剧这里的从服务器使用 Docker 安装 FRPC，主服务器通过 Release 包安装配置 FRPS。</p>
<p>测试下 tcp 连通没问题，FRP 配置也算完成了。</p>
<h4 id="333nodejs">3.3.3、NodeJS 博客服务更新数据库连接配置</h4>
<p>经过前两步的操作，主服务器有两个 Mongo DB 服务可用，修改 NodeJS 的 .env 配置文件指向新的数据库，重启 NodeJS 服务，一切就大功告成了。</p>
<p>对了这里小剧多做了一个操作，就是在服务器的入站规则加了 Mongo DB 端口的禁用。</p>
<p>效果就是主服务器内可以访问 Mongo DB，外网无法借助于主服务器 IP + 端口访问数据库，安全性提高了一点点。</p>
<h4 id="334">3.3.4、图解网络流转逻辑</h4>
<p>这里简单画一下终端用户、NodeJS、FRPS、FRPC、Mongo DB 间的数据流转逻辑。</p>
<p><img src="https://static.bh-lay.com/blog/2024/mongo-repair/network-structure.jpg" alt="网络拓扑图" /></p>
<p>您作为在阅读这篇文章的 User，所看到的页面、数据都是主服务器中 NodeJS 的博客服务提供的。</p>
<p>在这篇文章渲染之前需要从 NodeJS 本机的 2222 端口访问数据库，读取文章详情。</p>
<p>当然，这个数据库服务是虚拟的。借助于主服务器的 FRPS 和从服务器的 FRPC 建立起来的连接，将从服务器中 1111 端口映射到了主服务器中的 2222 端口上。</p>
<blockquote>
  <p>类似于小剧没有固定住所，但小剧的朋友 Jim 有，小剧在 Jim 家里安装了一台电话机。只要小剧搬家就会给 Jim 打电话，电话一直占线不挂，如果挂了就重新拨过去。别人要来找小剧，就去找 Jim，拿起那部电话机进行通话。</p>
</blockquote>
<p>这是前文用到过的比喻。主服务器就是这个比喻里的 Jim 家，FRPS 就是 Jim 家那部电话机，FRPC 就是小剧家里那部电话机，所以核心的关键在于 FRP 客户端和服务端之间建立稳定的连接。</p>
<h2 id="mongodb">四、Mongo DB 转移后的问题</h2>
<h3 id="41api">4.1、API 延迟多久？</h3>
<blockquote>
  <p>原本 NodeJS 与 Mongo DB 在同一台机器，数据读取几乎不会因为连接而增加耗时。如何尽量降低网络时延对体验的影响？</p>
</blockquote>
<p>文章写到这里，服务挂起的问题就已经解决了。但前文还遗留了一个问题待解决，就是 MongoDB 转移到从服务器后，API 的响应有没有明显变慢？</p>
<p>以剧中人的朋友圈获取某位好友信息为例，仅对比 <strong>Waiting for server response</strong> 这一指标。</p>
<p><img src="https://static.bh-lay.com/blog/2024/mongo-repair/request-time.jpg" alt="请求耗时" /></p>
<p>Mongo DB 转移前大约为 134ms，转移后大约 339ms，开启缓存后大约 85ms。</p>
<p>不精确地估算，耗时大约增长了 205ms。</p>
<p>205ms 略慢，但对于小剧客栈这样的小博客够用了，并且开启了缓存这块遮羞布之后，耗时更会大幅降低。</p>
<p>因此用局部、低频的耗时增加，换取更为稳定的服务，以及学习到的新的技能，还是值得的。</p>
<h3 id="42">4.2、发现遗留的隐患</h3>
<p>FRP 服务端自带了一个 Web 管理页面，用于查看连接客户端列表，数据吞吐量、以及代理的基本状况。</p>
<p><img src="https://static.bh-lay.com/blog/2024/mongo-repair/frp-dashboard.jpg" alt="frp-dashboard.png" /></p>
<p>小剧客栈 NodeJS 服务端设计的数据库使用模式为：用后即销毁，没有连接复用的设计，理论上在没有并发请求的情况下，Current Connections 应该为零才对。</p>
<p>箭头处的数字不为零，只有两种解释：一是小剧出息了，同一时刻内确实有这么大的连接数；二是某个请求结束后，一直未执行断开连接操作，而 NodeJS 单进程的模式又会导致连接始终不被释放。</p>
<p>很显然小剧并没有这么出息，问题肯定出在断开连接操作上，但检查代码后发现所有数据库连接操作均有断开连接行为。</p>
<p>那只有一种可能，就是数据库连立连接后，在极端情况下遇到业务逻辑异常，比如 FS 读写冲突，局部逻辑健壮性不强导致的报错等问题，进而导致代码未正常直行到数据库断开操作。</p>
<p>此问题虽然不会立即导致 NodeJS 服务和 Mongo DB 崩溃，但累积的无用连接会拖慢服务的响应速度。</p>
<p>随后补充了一段通用逻辑：在建立数据库连接时，默认一秒内会处理完数据的读写操作。若一秒内仍未执行断连操作，则主动断开数据库连接。</p>
<pre><code class="javascript language-javascript">export async function getDbConnect (): Promise&lt;{
client: mongodb.MongoClient,
db: mongodb.Db
}&gt; {
  // 省略逻辑 ……
  // close connect when timout
  const originCloseMethod = client.close
  function closeConnect () {
    return originCloseMethod.apply(client) as unknown as Promise&lt;void&gt;
  }
  client.close = function (): Promise&lt;void&gt; {
    clearTimeout(closeTimoutTimer)
    return closeConnect()
  }
  const closeTimoutTimer = setTimeout(() =&gt; {
    console.trace(’MongoDB missing close connect.‘)
    closeConnect()
  }, 1000)
  // 省略逻辑 ……
}
</code></pre>
<h2 id="-3">五、问题解决了吗？</h2>
<p>在经历了第一次发现 Mongo DB 挂起后，及时启用缓存功能这块遮羞布；第二次分析到问题根源后，决定进行 Mongo DB 服务转移；以及最后补充的数据库超时断连逻辑。</p>
<p>三次操作让 Mongo DB 在起到决定性作用的硬件环境上，得到质了的改善。并且借助于缓存策略，极大降低了请求频次对 Mongo DB 服务的压力。以及极端异常的超时处理，避免了垃圾连接对 NodeJS 和 MongoDB 服务的负担。</p>
<p>经过一个多月的观察，Mongo DB 运行稳定，无任何挂起、无响应的情况。从服务器在每次 IP 变更时都能及时重连上 FRPS。并且随着对逻辑的加强，NodeJS 也极少打印超时断连的日志。</p>
<p><strong>至此 Mongo DB 服务异常挂起的问题得到了的解决。</strong></p>
<p>还额外获得了一个新的思考：小剧一直都是在单节点部署全部服务，这次接触了多节点服务转移，异地备份和负载均衡要不要尝试一下？</p>
<p>从小剧客栈的规模和访问量来看，完全没必要，若是业余时间充足，玩一玩倒也不是不行。</p>]]></content:encoded>
    </item>
    <item>
      <title>小剧起始页 IndexedDB 数据库升级记录</title>
      <link>https://bh-lay.com/blog/1g079snv7oo</link>
      <description><![CDATA[<p>一年前，小剧曾总结过在小剧起始页中关于 IndexedDB 的使用经验。提到了前端本地持久化常见的类型和方案，以及为什么小剧起始页需要用 IndexedDB。用了些简单的代码片段来介绍 IndexedDB 的使用方式。</p>
<p>感兴趣的话可以查看这篇文章<a href="http://bh-lay.com/blog/ragwqzkita">《小剧起始页，离线数据篇》</a>。</p>
<p>如果你还没有使用过小剧起始页，可以点击这个链接 <a href="https://e.bh-lay.com/">https://e.bh-lay.com/</a>。</p>
<blockquote>
  <p>而小剧起始页虽然迭代了很多版本，数据结构仍然稳定在最初的版本，所以关于数据库升降级操作小剧仅仅知道很重要、需要做，但是并没有实际操作经验。</p>
  <p>希望后面随着版本的迭代有机会处理这部分的逻辑。</p>
</blockquote>
<p>这段话是上面文章里的原文，解释了在当时版本里对 IndexedDB 的版本升级并没有处理的原因。因此你在阅读的这篇文章可以视作离线数据篇的补充。</p>
<h2 id="">一、什么是数据库版本升级？</h2>
<p>相信很多同学会有这个疑问。</p>
<p>作为传统前端开发，代码向来是 always online 模式的最新版本，根本不用关心数据库的版本与升级。</p>
<p>如果你有服务端开发经验，数据库的相关改动也只是在一次次的上线操作中被执行，最多写一些回滚数据库的脚本。</p>
<p>可能只有作为客户端开发的同学，对数据库升级操作不陌生。</p>
<p>举个例子：</p>
<p><strong>你正在操盘两个产品，一个是 WEB APP，一个是 IOS APP。在历次迭代中对数据结构都有过三次大的改动。</strong></p>
<p>WEB 端每次调整好代码，适配完新的版本，连同服务端一起上线就完事了。此类操作重复三次，不会有什么问题。</p>
<p>客户端就不一样，因为需要在弱网、离线情况下提供良好的使用体验，绝大多数的 APP 需要缓存部分数据以供离线时使用。参考微信在飞行模式下依然可以查看聊天记录。</p>
<p>或者有一部分 APP 的数据完全就是离线的，例如本地版本的小游戏。</p>
<p>问题来了，APP 启动时，有可能是设备第一次安装该应用，也有可能是从最早，或者中间的某个版本，缓存了很多离线数据之后，升级而来的。</p>
<p>因为新版本 APP 的处理逻辑和旧版本的数据库已经有了很大的差异，例如将某个 time 字段更新成了 createTime。新版本的 APP 并不能兼容老版本的数据库。</p>
<p>所以需要在 APP 启动时，检查当前 APP 支持的数据库版本与当前数据库版本，如果不一致，就需要进行数据库升级操作。</p>
<p>Web 端在支持离线数据库之后，和客户端要解决的数据库版本问题几乎一模一样。因此在开发 IndexedDB 时，参考客户端的开发模式，也需要做数据库版本升级。</p>
<h2 id="-1">二、为什么小剧起始页需要做数据库升级？</h2>
<h3 id="21">2.1、之前的数据结构是什么样的？</h3>
<p>在写这篇文章之前，如果你有用过小剧起始页的话，可能你会知道小剧起始页的书签结构和现在是有很大不同的。</p>
<p>在此之前你可以在桌面创建书签、小工具（小工具本质上也是书签），也可以创建书签分组。</p>
<p>还可以在 <strong>小书房</strong> 这个特殊的书签下，以树形结构管理书签。</p>
<p>这种模式可以很好的组织书签结构，做为起始页能很方便的通过排列常用链接，来定制日常使用的界面。</p>
<p>实际上线一年多以来，小剧起始页已经越来越多的被身边的小伙伴所使用。这套桌面书签管理模式也被很多小伙伴夸了又夸。</p>
<h3 id="22">2.2、这个结构有什么限制？</h3>
<p>随着书签越来越多，最近在使用小剧起始页的时候，一直暗暗觉得用起来不是特别爽，但又说不上来是哪里不够好。直到上个月（2023/05）才突然意识到这个点是什么。</p>
<p><strong>这个点就是就是场景化。</strong></p>
<p>以小剧自己举例，目前小剧起始页上堆满了各种图标。其中绝大多数是工作中需要用到个各个平台链接、很多开发环境的地址，零散项目需要用到的资料。</p>
<p>还有一部分是学习所用的一些网址，和各工具的官网链接，以及少部分消遣平台。</p>
<p><strong>根本原因就是小剧起始页只有一个桌面，所有图标都要在这一个地方去陈列。</strong></p>
<p>如果能分场景管理，将工作的时候需要用到的书签放置在一个桌面，摸鱼的时候用到的书签集中在一个桌面，学习时用到的网址集中在一起。既能保证任何时间内桌面的干净整洁，又能在不同场景下更方便去查找和管理书签。</p>
<h3 id="23">2.3、如何去实现新的结构？</h3>
<p>小剧起始页的树形结构的实现还算简单，所有节点通过 <code>parent</code> 字段描述上级节点。</p>
<p>通过给定节点 ID，递归查询下级节点就可以生成出一棵树形结构。</p>
<p>在此之前小剧起始页的所有 <code>parent</code> 字段为空的节点，均被视作桌面上的书签。<code>parent</code> 为 <code>root</code> 的节点被视作小书房下的书签。</p>
<p>羸弱的 <code>parent</code> 定义很难承载多桌面的数据结构。</p>
<p>通过对数据结构的整理，决定对书签树形结构重新规划，具体如下：</p>
<ul>
<li>更改小书房节点 ID，由 <code>root</code> 更名为 <code>bookmark-collection</code></li>
<li>重新定义 <code>root</code> 虚拟节点，作为所有节点的共同根结点</li>
<li>新增桌面 <code>desktop</code> 虚拟节点，用来管理多桌面结构</li>
<li>新增 <code>desktop-default</code>、<code>desktop-fish</code> 节点，作为两个初始桌面</li>
<li>将旧数据中所有 <code>parent</code> 字段为空的节点，指向 <code>desktop-default</code> 桌面</li>
</ul>
<p><img src="https://static.bh-lay.com/blog/2023-indexeddb-upgrade/data-structure.png" alt="数据结构优化图" /></p>
<p>这种破坏性的改动需要修改库表中的数据，因此需要先行对数据库进行升级后，才能在上层，对交互模式做优化。</p>
<h2 id="-2">三、此次数据库升级，做了哪些工作？</h2>
<p>如上面 2.3 部分描述，此次小剧起始页的数据库升级，其实并不需要对库表结构作调整。仅需要对现存数据的部分字段做调整，再增加两个桌面节点，即可完成。</p>
<p>但是前期封装的 IndexedDB 代码并未对数据库升级逻辑做任何适配，因此需要做好数据库升级的基础建设后，才能将此次的数据库升级逻辑进行配置。</p>
<p>此部分的代码均在下面的目录下，可以参考代码一起阅读。</p>
<ul>
<li><a href="https://github.com/bh-lay/lays-workbench/blob/master/src/database/db.ts">/src/database/db.ts</a></li>
<li><a href="https://github.com/bh-lay/lays-workbench/tree/master/src/database/upgrades">/src/database/upgrades</a></li>
</ul>
<h3 id="31indexeddb">3.1、IndexedDB 数据库升级基础支持</h3>
<p>这里是简化后的打开数据库连接的逻辑，其中 <code>onblocked</code> 和 <code>onupgradeneeded</code> 是和数据库升级相关的两个事件。</p>
<ul>
<li><code>onblocked</code> 指有其他页面正在使用数据库，无法进行数据库升级。</li>
<li><code>onupgradeneeded</code> 是当数据库需要进行升级或者初始化时，会进入此回调。</li>
</ul>
<pre><code class="javascript language-javascript">export const dbVersioon = 4

const request = window.indexedDB.open('data-store', dbVersioon)
// request.onerror = function() {}
// request.onerror = function() {}
// request.onsuccess = function() {}
request.onblocked = function(event) {
  alert('本地数据升级，请关闭全部页面重新打开！');
}
request.onupgradeneeded = async function(event) {
  const target = event.target
  const db = target.result

  if (!target || !db) {
    return reject(new Error('could not find target'))
  }

  const newVersion = event.newVersion || 0
  const oldVersion = event.oldVersion || 0

  if (oldVersion === 0) {
    // 数据库初始化逻辑
    // bookmarkEntityInit(db)
  } else {
    // 数据库升级逻辑
    // await bookmarkUpgrade(newVersion, oldVersion, db, target.transaction)
  }
}
</code></pre>
<p>在 <code>onupgradeneeded</code> 回调内，真实情况会比较复杂。</p>
<p>当前 <code>newVersion</code> 为 4，浏览器内的 <code>oldVersion</code> 有可能是 0、1、2、3 这四种情况。</p>
<p><code>oldVersion</code> 是 0 的情况比较简单，说明当前浏览器没有旧的数据库，直接初始化即可。</p>
<p>而当 <code>oldVersion</code> 是 1、2、3 时，我们有两种方案去实现数据库的升级。</p>
<ul>
<li>一种是<strong>一步登天</strong>的模式，分别写 1-&gt;4、 2-&gt;4、3-&gt;4 三个函数，用来应对不同旧版本的情况。</li>
<li>另一种是<strong>步步高升</strong>的模式，分别为：1-&gt;2、2-&gt;3、3-&gt;4 三个函数，组合起来应对不同的旧版本。
如旧版本为 1，则先升级到 2，再升级到 3，最后升级到 4。</li>
</ul>
<p>看起来方案一最高效，然而真实项目中往往是方案二最实用。</p>
<p>因为在实际项目开发中，一次版本发布中最多跨一个数据库版本。因此我们最清楚上一个版本和当前版本的差异，在<strong>步步高升</strong>的走台阶过程中，开发人员的学习成本是最低的，无需考虑历史上所有的版本情况。</p>
<p>另一个原因是，方案一实际运行效率虽高，但开发效率极低且出错率很高。</p>
<p>例如下一次数据库版本升级到 5 时，之前写的所有升级函数都无法再使用，需要重新去写 1、2、3、4 升级到 5 的脚本。</p>
<p>基于上面提到的原因，方案二就成了首选，我们可以用一段非常简单的逻辑去维护版本升级的步骤。</p>
<pre><code class="javascript language-javascript">import one from './1'
import two from './2'
import three from './3'

const upgradeVersionFnMap = {
  // 1-&gt; 2
  1: one,
  // 2 -&gt; 3
  2: two,
  // 2 -&gt; 4
  3: three,
}

export async function bookmarkUpgrade (newVersion, oldVersion, db, transaction) {
  async function upgradeNext(currentVersion) {
    const currentUpgradeFn = upgradeVersionFnMap[currentVersion]
    if (currentUpgradeFn) {
      await currentUpgradeFn(db, transaction)
    }
    const nextVersion = currentVersion + 1
    if (nextVersion &lt; newVersion) {
      await upgradeNext(nextVersion)
    }
  }

  await upgradeNext(oldVersion)
}
</code></pre>
<h3 id="32">3.2、版本升级逻辑开发</h3>
<p>前面的基础建设准备好了，就可以正式编写当前版本的升级逻辑了。</p>
<p>此次版本升级因为不涉及到对库、表结构的改动，因此代码对常规的数据库升级不具备参考价值，可以选择性跳过。</p>
<pre><code class="javascript language-javascript">// upgrade: 1 -&gt; 2

// 将第一版本中 bookmark 的 parentId 转换为标准结构的 parentId
function upgradeParentId(currentBookmark, objectStore) {
  return new Promise((resolve) =&gt; {
    let newParentId = ''
    if (!currentBookmark.parent) {
      // 将为空的 parentId，强制指定为默认桌面
      newParentId = 'desktop-default'
    } else if (currentBookmark.parent === 'root') {
      // 将为 root 的 parentId，强制指定为书签收藏
      newParentId = 'bookmark-collection'
    }
    if (newParentId) {
      currentBookmark.parent = newParentId
      const putRequest = objectStore.put(currentBookmark)
      putRequest.onsuccess = function () {
        resolve(true)
      }
      putRequest.onerror = function () {
        resolve(false)
      }
    } else {
      resolve(true)
    }
  })
}

function fixAllParentId(objectStore) {
  return new Promise((resolve) =&gt; {
    const request = objectStore.openCursor()
    request.onsuccess = function (event) {
      if (!event.target) {
        resolve(false)
        return
      }
      const cursor = event.target.result
      if (cursor) {
        upgradeParentId(cursor.value, objectStore).finally(() =&gt; {
          cursor.continue()
        })
      } else {
        resolve(true)
      }
    }
    request.onerror = function () {
      resolve(false)
    }
  })
}

function addBookmark(objectStore, data) {
  return new Promise((resolve) =&gt; {
    const request = objectStore.add(data)
    request.onsuccess = function () {
      resolve(true)
    }
    request.onerror = function () {
      resolve(false)
    }
  })
}

async function addDesktopDefaultBookmark(objectStore) {
  await addBookmark(objectStore, {
    id: 'desktop-default',
    parent: 'desktop',
    name: '在搬砖',
    undercoat: '#177cb0',
    type: 4,
    size: 1,
    icon: 'mdi:worker',
    sort: 0,
    value: '',
    desc: ''
  })
  await addBookmark(objectStore, {
    id: 'desktop-fish',
    parent: 'desktop',
    name: '在摸鱼',
    undercoat: '#4caf50',
    type: 4,
    size: 1,
    icon: 'mdi:fish',
    sort: 0,
    value: '',
    desc: ''
  })
}
export default async function bookmarkUpgrade (db, transaction) {
  if (!db.objectStoreNames.contains('bookmark')) {
    return false
  }
  const objectStore = transaction.objectStore('bookmark')

  await fixAllParentId(objectStore)
  await addDesktopDefaultBookmark(objectStore)
}
</code></pre>
<h3 id="33">3.3、周边逻辑处理</h3>
<p>自此数据库升级的逻辑就全部写完了，但是一些查询、修改等操作针对的还是老版本的数据库。</p>
<p>例如查询桌面图标的方法依旧还是使用的 <code>parent</code> 为空，而非 <code>desktop-default</code>。</p>
<p>将这些逻辑处理完毕，再完整走一遍小剧起始页的流程，确认对业务没有其他影响，数据库升级逻辑就算完成了。</p>
<h2 id="-3">四、其他的小问题</h2>
<h3 id="41">4.1、为什么没有数据库降级 ？</h3>
<p>这个场景可以被构造出来，例如线上使用版本 5，用户访问 5 这个版本后，数据库就被存储到浏览器中。然后再用比 5 更小的版本发布前端代码，比如 4。</p>
<p>用户再次访问时，就会出现 WEB APP 支持版本为 4，而浏览器中的旧版本为 5 的情况。</p>
<p>可能在客户端开发时会处理此问题，因为在 CS 模式下，用户可以用不同版本的 APP 反复覆盖安装。而作为 BS 模式下的 WEB 开发，我们可以控制 WEB APP 的版本，保证版本永远向上累加而非降低。</p>
<p>事实上这这个场景 IndexedDB 会直接报错，不会进入到 <code>onupgradeneeded</code> 逻辑中。实际业务在回滚代码的时候，如果有涉及到跨数据库版本，需要格外注意这一点。</p>
<h3 id="42">4.2、有没有更省事一点的做法 ？</h3>
<p>数据库升级是为了保证本地数据安全性，避免丢失数据而进行的操作。</p>
<p>如果你的业务中，存储在本地的数据是不重要的 LOG 日志，或者已经同步到服务端的缓存数据，在遇到数据库版本不一致的时候，可以无脑丢弃，没必要折腾自己也折腾浏览器。</p>
<p>但如果涉及到用户数据需要严格同步到服务端，但又可能因各种原因尚未同步至服务端，那还是得老老实实进行数据库升级。</p>
<h2 id="-4">五、小剧起始页升级后会增加哪些功能？</h2>
<p>如前文 <strong>2.2、这个结构有什么限制？</strong> 提到的，此次升级数据库是为了为多桌面开发作准备的。</p>
<p>因此小剧起始页 IndexedDB 数据库升级完成后，第一个功能就是增加多桌面支持。事实上截止本文发布时，多桌面已经上线，分别为：在工作、在摸鱼。</p>
<p>你可以在这两个桌面上进行组织图标，更换壁纸，不同桌面的壁纸是相互独立的。</p>
<p>接下来的时间，小剧会开发自定义桌面功能，提供桌面的创建、删除、更名、调整顺序等功能。</p>
<p>也许下周，也许下个月或更久，这个不做保证，除非你打钱催我，期待更自由的桌面管理功能早日上线。</p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Sat, 24 Jun 2023 15:06:23 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/1g079snv7oo</guid>
      <category>IndexedDB</category>
      <category>数据库升级</category>
      <category>小剧起始页</category>
      <enclosure url="http://static.bh-lay.com/blog/2023-indexeddb-upgrade/cover.png" length="0" type="image/png"/>
      <content:encoded><![CDATA[<p>一年前，小剧曾总结过在小剧起始页中关于 IndexedDB 的使用经验。提到了前端本地持久化常见的类型和方案，以及为什么小剧起始页需要用 IndexedDB。用了些简单的代码片段来介绍 IndexedDB 的使用方式。</p>
<p>感兴趣的话可以查看这篇文章<a href="http://bh-lay.com/blog/ragwqzkita">《小剧起始页，离线数据篇》</a>。</p>
<p>如果你还没有使用过小剧起始页，可以点击这个链接 <a href="https://e.bh-lay.com/">https://e.bh-lay.com/</a>。</p>
<blockquote>
  <p>而小剧起始页虽然迭代了很多版本，数据结构仍然稳定在最初的版本，所以关于数据库升降级操作小剧仅仅知道很重要、需要做，但是并没有实际操作经验。</p>
  <p>希望后面随着版本的迭代有机会处理这部分的逻辑。</p>
</blockquote>
<p>这段话是上面文章里的原文，解释了在当时版本里对 IndexedDB 的版本升级并没有处理的原因。因此你在阅读的这篇文章可以视作离线数据篇的补充。</p>
<h2 id="">一、什么是数据库版本升级？</h2>
<p>相信很多同学会有这个疑问。</p>
<p>作为传统前端开发，代码向来是 always online 模式的最新版本，根本不用关心数据库的版本与升级。</p>
<p>如果你有服务端开发经验，数据库的相关改动也只是在一次次的上线操作中被执行，最多写一些回滚数据库的脚本。</p>
<p>可能只有作为客户端开发的同学，对数据库升级操作不陌生。</p>
<p>举个例子：</p>
<p><strong>你正在操盘两个产品，一个是 WEB APP，一个是 IOS APP。在历次迭代中对数据结构都有过三次大的改动。</strong></p>
<p>WEB 端每次调整好代码，适配完新的版本，连同服务端一起上线就完事了。此类操作重复三次，不会有什么问题。</p>
<p>客户端就不一样，因为需要在弱网、离线情况下提供良好的使用体验，绝大多数的 APP 需要缓存部分数据以供离线时使用。参考微信在飞行模式下依然可以查看聊天记录。</p>
<p>或者有一部分 APP 的数据完全就是离线的，例如本地版本的小游戏。</p>
<p>问题来了，APP 启动时，有可能是设备第一次安装该应用，也有可能是从最早，或者中间的某个版本，缓存了很多离线数据之后，升级而来的。</p>
<p>因为新版本 APP 的处理逻辑和旧版本的数据库已经有了很大的差异，例如将某个 time 字段更新成了 createTime。新版本的 APP 并不能兼容老版本的数据库。</p>
<p>所以需要在 APP 启动时，检查当前 APP 支持的数据库版本与当前数据库版本，如果不一致，就需要进行数据库升级操作。</p>
<p>Web 端在支持离线数据库之后，和客户端要解决的数据库版本问题几乎一模一样。因此在开发 IndexedDB 时，参考客户端的开发模式，也需要做数据库版本升级。</p>
<h2 id="-1">二、为什么小剧起始页需要做数据库升级？</h2>
<h3 id="21">2.1、之前的数据结构是什么样的？</h3>
<p>在写这篇文章之前，如果你有用过小剧起始页的话，可能你会知道小剧起始页的书签结构和现在是有很大不同的。</p>
<p>在此之前你可以在桌面创建书签、小工具（小工具本质上也是书签），也可以创建书签分组。</p>
<p>还可以在 <strong>小书房</strong> 这个特殊的书签下，以树形结构管理书签。</p>
<p>这种模式可以很好的组织书签结构，做为起始页能很方便的通过排列常用链接，来定制日常使用的界面。</p>
<p>实际上线一年多以来，小剧起始页已经越来越多的被身边的小伙伴所使用。这套桌面书签管理模式也被很多小伙伴夸了又夸。</p>
<h3 id="22">2.2、这个结构有什么限制？</h3>
<p>随着书签越来越多，最近在使用小剧起始页的时候，一直暗暗觉得用起来不是特别爽，但又说不上来是哪里不够好。直到上个月（2023/05）才突然意识到这个点是什么。</p>
<p><strong>这个点就是就是场景化。</strong></p>
<p>以小剧自己举例，目前小剧起始页上堆满了各种图标。其中绝大多数是工作中需要用到个各个平台链接、很多开发环境的地址，零散项目需要用到的资料。</p>
<p>还有一部分是学习所用的一些网址，和各工具的官网链接，以及少部分消遣平台。</p>
<p><strong>根本原因就是小剧起始页只有一个桌面，所有图标都要在这一个地方去陈列。</strong></p>
<p>如果能分场景管理，将工作的时候需要用到的书签放置在一个桌面，摸鱼的时候用到的书签集中在一个桌面，学习时用到的网址集中在一起。既能保证任何时间内桌面的干净整洁，又能在不同场景下更方便去查找和管理书签。</p>
<h3 id="23">2.3、如何去实现新的结构？</h3>
<p>小剧起始页的树形结构的实现还算简单，所有节点通过 <code>parent</code> 字段描述上级节点。</p>
<p>通过给定节点 ID，递归查询下级节点就可以生成出一棵树形结构。</p>
<p>在此之前小剧起始页的所有 <code>parent</code> 字段为空的节点，均被视作桌面上的书签。<code>parent</code> 为 <code>root</code> 的节点被视作小书房下的书签。</p>
<p>羸弱的 <code>parent</code> 定义很难承载多桌面的数据结构。</p>
<p>通过对数据结构的整理，决定对书签树形结构重新规划，具体如下：</p>
<ul>
<li>更改小书房节点 ID，由 <code>root</code> 更名为 <code>bookmark-collection</code></li>
<li>重新定义 <code>root</code> 虚拟节点，作为所有节点的共同根结点</li>
<li>新增桌面 <code>desktop</code> 虚拟节点，用来管理多桌面结构</li>
<li>新增 <code>desktop-default</code>、<code>desktop-fish</code> 节点，作为两个初始桌面</li>
<li>将旧数据中所有 <code>parent</code> 字段为空的节点，指向 <code>desktop-default</code> 桌面</li>
</ul>
<p><img src="https://static.bh-lay.com/blog/2023-indexeddb-upgrade/data-structure.png" alt="数据结构优化图" /></p>
<p>这种破坏性的改动需要修改库表中的数据，因此需要先行对数据库进行升级后，才能在上层，对交互模式做优化。</p>
<h2 id="-2">三、此次数据库升级，做了哪些工作？</h2>
<p>如上面 2.3 部分描述，此次小剧起始页的数据库升级，其实并不需要对库表结构作调整。仅需要对现存数据的部分字段做调整，再增加两个桌面节点，即可完成。</p>
<p>但是前期封装的 IndexedDB 代码并未对数据库升级逻辑做任何适配，因此需要做好数据库升级的基础建设后，才能将此次的数据库升级逻辑进行配置。</p>
<p>此部分的代码均在下面的目录下，可以参考代码一起阅读。</p>
<ul>
<li><a href="https://github.com/bh-lay/lays-workbench/blob/master/src/database/db.ts">/src/database/db.ts</a></li>
<li><a href="https://github.com/bh-lay/lays-workbench/tree/master/src/database/upgrades">/src/database/upgrades</a></li>
</ul>
<h3 id="31indexeddb">3.1、IndexedDB 数据库升级基础支持</h3>
<p>这里是简化后的打开数据库连接的逻辑，其中 <code>onblocked</code> 和 <code>onupgradeneeded</code> 是和数据库升级相关的两个事件。</p>
<ul>
<li><code>onblocked</code> 指有其他页面正在使用数据库，无法进行数据库升级。</li>
<li><code>onupgradeneeded</code> 是当数据库需要进行升级或者初始化时，会进入此回调。</li>
</ul>
<pre><code class="javascript language-javascript">export const dbVersioon = 4

const request = window.indexedDB.open('data-store', dbVersioon)
// request.onerror = function() {}
// request.onerror = function() {}
// request.onsuccess = function() {}
request.onblocked = function(event) {
  alert('本地数据升级，请关闭全部页面重新打开！');
}
request.onupgradeneeded = async function(event) {
  const target = event.target
  const db = target.result

  if (!target || !db) {
    return reject(new Error('could not find target'))
  }

  const newVersion = event.newVersion || 0
  const oldVersion = event.oldVersion || 0

  if (oldVersion === 0) {
    // 数据库初始化逻辑
    // bookmarkEntityInit(db)
  } else {
    // 数据库升级逻辑
    // await bookmarkUpgrade(newVersion, oldVersion, db, target.transaction)
  }
}
</code></pre>
<p>在 <code>onupgradeneeded</code> 回调内，真实情况会比较复杂。</p>
<p>当前 <code>newVersion</code> 为 4，浏览器内的 <code>oldVersion</code> 有可能是 0、1、2、3 这四种情况。</p>
<p><code>oldVersion</code> 是 0 的情况比较简单，说明当前浏览器没有旧的数据库，直接初始化即可。</p>
<p>而当 <code>oldVersion</code> 是 1、2、3 时，我们有两种方案去实现数据库的升级。</p>
<ul>
<li>一种是<strong>一步登天</strong>的模式，分别写 1-&gt;4、 2-&gt;4、3-&gt;4 三个函数，用来应对不同旧版本的情况。</li>
<li>另一种是<strong>步步高升</strong>的模式，分别为：1-&gt;2、2-&gt;3、3-&gt;4 三个函数，组合起来应对不同的旧版本。
如旧版本为 1，则先升级到 2，再升级到 3，最后升级到 4。</li>
</ul>
<p>看起来方案一最高效，然而真实项目中往往是方案二最实用。</p>
<p>因为在实际项目开发中，一次版本发布中最多跨一个数据库版本。因此我们最清楚上一个版本和当前版本的差异，在<strong>步步高升</strong>的走台阶过程中，开发人员的学习成本是最低的，无需考虑历史上所有的版本情况。</p>
<p>另一个原因是，方案一实际运行效率虽高，但开发效率极低且出错率很高。</p>
<p>例如下一次数据库版本升级到 5 时，之前写的所有升级函数都无法再使用，需要重新去写 1、2、3、4 升级到 5 的脚本。</p>
<p>基于上面提到的原因，方案二就成了首选，我们可以用一段非常简单的逻辑去维护版本升级的步骤。</p>
<pre><code class="javascript language-javascript">import one from './1'
import two from './2'
import three from './3'

const upgradeVersionFnMap = {
  // 1-&gt; 2
  1: one,
  // 2 -&gt; 3
  2: two,
  // 2 -&gt; 4
  3: three,
}

export async function bookmarkUpgrade (newVersion, oldVersion, db, transaction) {
  async function upgradeNext(currentVersion) {
    const currentUpgradeFn = upgradeVersionFnMap[currentVersion]
    if (currentUpgradeFn) {
      await currentUpgradeFn(db, transaction)
    }
    const nextVersion = currentVersion + 1
    if (nextVersion &lt; newVersion) {
      await upgradeNext(nextVersion)
    }
  }

  await upgradeNext(oldVersion)
}
</code></pre>
<h3 id="32">3.2、版本升级逻辑开发</h3>
<p>前面的基础建设准备好了，就可以正式编写当前版本的升级逻辑了。</p>
<p>此次版本升级因为不涉及到对库、表结构的改动，因此代码对常规的数据库升级不具备参考价值，可以选择性跳过。</p>
<pre><code class="javascript language-javascript">// upgrade: 1 -&gt; 2

// 将第一版本中 bookmark 的 parentId 转换为标准结构的 parentId
function upgradeParentId(currentBookmark, objectStore) {
  return new Promise((resolve) =&gt; {
    let newParentId = ''
    if (!currentBookmark.parent) {
      // 将为空的 parentId，强制指定为默认桌面
      newParentId = 'desktop-default'
    } else if (currentBookmark.parent === 'root') {
      // 将为 root 的 parentId，强制指定为书签收藏
      newParentId = 'bookmark-collection'
    }
    if (newParentId) {
      currentBookmark.parent = newParentId
      const putRequest = objectStore.put(currentBookmark)
      putRequest.onsuccess = function () {
        resolve(true)
      }
      putRequest.onerror = function () {
        resolve(false)
      }
    } else {
      resolve(true)
    }
  })
}

function fixAllParentId(objectStore) {
  return new Promise((resolve) =&gt; {
    const request = objectStore.openCursor()
    request.onsuccess = function (event) {
      if (!event.target) {
        resolve(false)
        return
      }
      const cursor = event.target.result
      if (cursor) {
        upgradeParentId(cursor.value, objectStore).finally(() =&gt; {
          cursor.continue()
        })
      } else {
        resolve(true)
      }
    }
    request.onerror = function () {
      resolve(false)
    }
  })
}

function addBookmark(objectStore, data) {
  return new Promise((resolve) =&gt; {
    const request = objectStore.add(data)
    request.onsuccess = function () {
      resolve(true)
    }
    request.onerror = function () {
      resolve(false)
    }
  })
}

async function addDesktopDefaultBookmark(objectStore) {
  await addBookmark(objectStore, {
    id: 'desktop-default',
    parent: 'desktop',
    name: '在搬砖',
    undercoat: '#177cb0',
    type: 4,
    size: 1,
    icon: 'mdi:worker',
    sort: 0,
    value: '',
    desc: ''
  })
  await addBookmark(objectStore, {
    id: 'desktop-fish',
    parent: 'desktop',
    name: '在摸鱼',
    undercoat: '#4caf50',
    type: 4,
    size: 1,
    icon: 'mdi:fish',
    sort: 0,
    value: '',
    desc: ''
  })
}
export default async function bookmarkUpgrade (db, transaction) {
  if (!db.objectStoreNames.contains('bookmark')) {
    return false
  }
  const objectStore = transaction.objectStore('bookmark')

  await fixAllParentId(objectStore)
  await addDesktopDefaultBookmark(objectStore)
}
</code></pre>
<h3 id="33">3.3、周边逻辑处理</h3>
<p>自此数据库升级的逻辑就全部写完了，但是一些查询、修改等操作针对的还是老版本的数据库。</p>
<p>例如查询桌面图标的方法依旧还是使用的 <code>parent</code> 为空，而非 <code>desktop-default</code>。</p>
<p>将这些逻辑处理完毕，再完整走一遍小剧起始页的流程，确认对业务没有其他影响，数据库升级逻辑就算完成了。</p>
<h2 id="-3">四、其他的小问题</h2>
<h3 id="41">4.1、为什么没有数据库降级 ？</h3>
<p>这个场景可以被构造出来，例如线上使用版本 5，用户访问 5 这个版本后，数据库就被存储到浏览器中。然后再用比 5 更小的版本发布前端代码，比如 4。</p>
<p>用户再次访问时，就会出现 WEB APP 支持版本为 4，而浏览器中的旧版本为 5 的情况。</p>
<p>可能在客户端开发时会处理此问题，因为在 CS 模式下，用户可以用不同版本的 APP 反复覆盖安装。而作为 BS 模式下的 WEB 开发，我们可以控制 WEB APP 的版本，保证版本永远向上累加而非降低。</p>
<p>事实上这这个场景 IndexedDB 会直接报错，不会进入到 <code>onupgradeneeded</code> 逻辑中。实际业务在回滚代码的时候，如果有涉及到跨数据库版本，需要格外注意这一点。</p>
<h3 id="42">4.2、有没有更省事一点的做法 ？</h3>
<p>数据库升级是为了保证本地数据安全性，避免丢失数据而进行的操作。</p>
<p>如果你的业务中，存储在本地的数据是不重要的 LOG 日志，或者已经同步到服务端的缓存数据，在遇到数据库版本不一致的时候，可以无脑丢弃，没必要折腾自己也折腾浏览器。</p>
<p>但如果涉及到用户数据需要严格同步到服务端，但又可能因各种原因尚未同步至服务端，那还是得老老实实进行数据库升级。</p>
<h2 id="-4">五、小剧起始页升级后会增加哪些功能？</h2>
<p>如前文 <strong>2.2、这个结构有什么限制？</strong> 提到的，此次升级数据库是为了为多桌面开发作准备的。</p>
<p>因此小剧起始页 IndexedDB 数据库升级完成后，第一个功能就是增加多桌面支持。事实上截止本文发布时，多桌面已经上线，分别为：在工作、在摸鱼。</p>
<p>你可以在这两个桌面上进行组织图标，更换壁纸，不同桌面的壁纸是相互独立的。</p>
<p>接下来的时间，小剧会开发自定义桌面功能，提供桌面的创建、删除、更名、调整顺序等功能。</p>
<p>也许下周，也许下个月或更久，这个不做保证，除非你打钱催我，期待更自由的桌面管理功能早日上线。</p>]]></content:encoded>
    </item>
    <item>
      <title>Node 迁移 TypeScript 记录</title>
      <link>https://bh-lay.com/blog/njs1l1pod0</link>
      <description><![CDATA[<p>这篇文章是记录 2023 年小剧客栈服务端代码迁移的笔记。</p>
<p><strong>Github：</strong> <a href="https://github.com/bh-lay/blog/tree/master/backEnd">https://github.com/bh-lay/blog/tree/master/backEnd</a></p>
<h2 id="">一、回顾下最早期的版本</h2>
<p>熟悉小剧客栈的同学可能知道，小剧客栈开设于 2012 年 3 月，至今已经是第十一个年头了。</p>
<p>但可能很少有人了解，小剧客栈最早版本的发布异常简陋。</p>
<p><img src="https://static.bh-lay.com/blog/blog-to-ts/domain-info.png" alt="域名信息" /></p>
<p>因为刚入行不久，对前端略知一点儿，但是对服务器、Services 以及 Linux 完全一窍不通。所以最早版本的博客用了最 Low 的方式实现了博客的发布上线。</p>
<p>租用了一台 Windows 服务器，远程桌面的方式连接服务器。用了一款已经记不得叫什么的可视化软件搭建 NGINX、PHP、MySQL 运行环境。采用了当时很流行的 PHP 内容框架：帝国CMS。</p>
<p>小剧客栈以这样一种很粗糙的方式，陪伴了小剧工作的第一个年头。</p>
<p>同样这一年也是小剧各方面技能飞速增长的一年，这种粗糙也在以各种方式限制着小剧想象力的发挥。经历过这个阶段的同学应该有印象，NodeJS 在这段时间飞速发展，前端由此衍生出来了无限可能。</p>
<p>基于这种粗糙的限制，以及 NodeJS 的魅惑，小剧客栈在 2013 年 6 月迎来了全新的改版。NodeJS + MongoDB 的服务端架构让小剧在前端个人博客这个小圈子里小小的火了一把。</p>
<p><a href="http://bh-lay.com/blog/13f4b2a16e8">新博客 新心情</a> 这篇文章很简短地记录了小剧改版后的心情。</p>
<h2 id="-1">二、为什么这次要做迁移</h2>
<h3 id="21">2.1、小剧客栈服务端现状</h3>
<p>可能很多小伙伴会比较奇怪，为什么小剧客栈没有采用 Express、Koa 等流行的 NodeJS 服务端框架进行开发。而是使用一堆零散的工具、方法拼凑出一个极其简陋的服务端实现。</p>
<p>其实把时间倒退回 2013 年 6 月，一切就会变的合理起来。同期 Express 虽然已经发布了 3.0.0 版本，但在 NodeJS 社区中的认可度并没有那么高。Koa 在小剧客栈 NodeJS 版本正式发布的一个月后，才从 Express 中剥离出来并发布。</p>
<p>学习、实践、记录是小剧客栈建立至今的主旋律。如果能够由自己编码实现更多的细节，就可以更深入的了解服务端开发的底层原理，这是小剧所期望的。因此 2013- 2015 小剧大量的业余时间都投入在了个人博客的开发上。</p>
<p>其中最重要的就是服务端开发。实现了服务端 Route、Controller、View、Component、Session、Cache、Mongo 数据库管理以及静态资源管理等模块。</p>
<p>回看那段时间的代码，服务端在经历了完整的功能堆叠，和核心逻辑与业务代码分离后。迭代到了 2014 年之后几乎就没有大的功能性改动了。</p>
<p>目前小剧客栈服务端依赖较少，主要逻辑在仓库代码中，简单列一下代码结构。</p>
<p><strong>package.json 部分</strong></p>
<pre><code class="javascript language-javascript">"dotenv": "^6.2.0",
"formidable": "~1.2.1",
"juicer": "~0.6.5-stable",
"path-to-regexp": "^3.0.0",
"jssha": "^2.0.1",
"mongodb": "^3.1.13",
"node-isbot": "0.0.8",
"nodemailer": "~6.6.1",
"showdown": "^1.9.1"
"cron": "1.0.9",
"request": "^2.81.0"
</code></pre>
<p><strong>/core/ 目录下的服务端核心逻辑</strong></p>
<ul>
<li><strong>utils:</strong>  一些工具方法<ul>
<li>index.js</li>
<li>pagination.js</li>
<li>parse.js</li></ul></li>
<li><strong>DB.js：</strong> 数据库连接方法</li>
<li><strong>cache.js：</strong> 基于 FS 实现的缓存方法</li>
<li><strong>component.js：</strong> 与 views 对应，视图的模版片段方法</li>
<li><strong>connect.js：</strong> 连接类，用来处理 http 请求的获取和响应</li>
<li><strong>index.js：</strong> App 类，包含了 Router 的定义、匹配及 Controller 的分配逻辑</li>
<li><strong>session.js：</strong> 基于 FS 的 Session 管理类</li>
<li><strong>staticFile.js：</strong> 静态文件处理方法</li>
<li><strong>views.js：</strong> 视图处理逻辑</li>
</ul>
<h3 id="22">2.2、现阶段存在的问题</h3>
<p>如果本着能跑就行的原则，小剧客栈的服务端已经满足了最基本的要求，毕竟已经稳定运行了近十年。</p>
<p>若考虑继续维护迭代，有以下痛点亟待解决：</p>
<ol>
<li>逻辑组织基于 NodeJS 经典的回调实现，代码维护较为混乱。</li>
<li>Controller、Views、Cache 等模块之间任务处理较为松散，无法统一进行错误、超时处理。</li>
<li>各种服务端模块由自己实现，没有写以后也不打算写正式的文档，导致方法调用需要大量翻阅代码参考，且容易出错。</li>
<li>Github dependabot 安全检查，局部升级 npm 包，导致大量依赖不匹配，无法稳定运行</li>
</ol>
<h3 id="23">2.3、打算如何处理</h3>
<p>毕竟是七八年前的代码，这些问题在日常工作中积累了很多方法去处理。个人项目要求不多，逻辑简单、代码稳健、好维护就行。因此决定将原代码做以下处理：</p>
<h4 id="231promise">2.3.1、基于 Promise 重新组织调用逻辑</h4>
<p>这几乎是近些年来 JS 处理异步逻辑的标准操作了，在消除回调地狱方面有着天然的优势。</p>
<p>除此之外将复杂逻辑基于 Promise 链串联起来，在对 Controller 进行错误捕捉（HTTP Code 500）、超时处理等方面实现更直观</p>
<h4 id="232typescript">2.3.2、使用 TypeScript 重构代码</h4>
<p>TypeScript 并非灵丹妙药，很多前端项目使用 TypeScript 后对稳定性的提升和投入不一定匹配。然而在 NodeJS 后端逻辑上，TypeScript 则可以大展身手。</p>
<p>其一可以通过 type 定义，约定很多相似的数据类型，减少学习成本，如 userConfigRoute、configRoute、matchedRoute。</p>
<p>其二是借助于 VSCode 的语法提醒和错误提示，在代码编写阶段就可以轻松获取参数数量及类型，真正实现代码即文档。</p>
<h2 id="-2">三、代码迁移过程</h2>
<p>代码迁移 80% 都是重复的体力活，这里挑一些重点的环节进行介绍。</p>
<h3 id="31typescript">3.1、TypeScript 本地开发准备</h3>
<p>这一步出乎意料的简单，记得很久以前想尝试 nodeJS TS 开发，需要配置大量周边环境，而且还需要预编译。现在只需要三个工具即可完成开发。</p>
<ul>
<li><strong>typescript</strong>：TS 支持的核心依赖</li>
<li><strong>ts-node</strong>：TS 开发的基础，有他几乎就够了。</li>
<li><strong>nodemon：</strong>本地 Watch 模式开发，开发调试更丝滑。</li>
</ul>
<h3 id="32typescript">3.2、项目代码 TypeScript 改造</h3>
<p>这里是个体力活，需要大量修改代码才能完成，并且由于 TypeScript 的类型检查，导致很容易因为迁移过程导致项目运行不起来。</p>
<p>任何 JS 项目迁移 TS 都会经历这个过程，并且是整个迁移任务耗时最久工作量最大的过程。</p>
<p>这个过程有很多种实践方案，比如兼容 JS 代码并逐步迁移至 TS。或者一次性更改全部文件为 TS 版本，关闭类型检查并逐步放开类型检查，等等。</p>
<p>这里说下我的处理方式，因为迁移 TS 和 Promise 改造同步进行，涉及大量的代码逻辑调整，因此之前积累的迁移方案都不适用，或者说耗时较久不愿意接受。</p>
<p>小剧最终采用的是由入口文件开始迁移，一开始注释掉全部的调用逻辑。顺着依赖调用逻辑迁移至 TS。</p>
<p>这样的好处有两个，一是 TS 检查从始至终是严格的，不兼容 JS 的，迁移完成后无需回溯代码去补全健壮性。二是项目迁移的任意阶段代码均可以稳定运行，只是过程中存在功能缺失。</p>
<p>之所以说这个工作是体力活，是因为整个过程像是把一棵树的所有主干、树枝、树杈、树叶全部打散，再从根部一节节嫁接回来。</p>
<h3 id="33npmpackgetypescript">3.3、NPM Packge TypeScript 支持</h3>
<p>这一步耗时不久，但是却很困扰新手朋友。从经验上来说有三种处理方案</p>
<h4 id="331">3.3.1、升级依赖包</h4>
<p>大多数模块的贡献者在经历过数个版本迭代后，都会不可避免的面对 TypeScript 兼容的问题。因此可以尝试查看新版本 NPM 包是否已经提供了 TS 支持。</p>
<p>此次迁移 path-to-regexp、showdown 等类库都是通过升级依赖包完成的 TS 支持。</p>
<h4 id="332typespackagename">3.3.2、安装 @types/[packageName]</h4>
<p>一些流行的类库可能已经稳定运行了数年之久，无需新的功能迭代，或者内部有大量的奇技淫巧不适合用 TypeScript 重构，因此作者或者其他贡献者会编写 @types/[packageName] 描述文件，安装对应的依赖包也可以完成 TS 的兼容。</p>
<p>迁移中 node、request、formidable、cron 等依赖都使用了这个方法。</p>
<h4 id="333dts">3.3.3、编写 .d.ts 描述文件</h4>
<p>一般情况下前两个方法足以应对项目里的绝大多数 NPM 依赖包。如果不幸前两种方式均无效，那大概率是这个依赖已经处于无人维护的状态，你需要换一个包。</p>
<p>开个玩笑，事实上大量面向小众领域，或者功能稳定但面世较早的的包都不支持 TS。因此你需要自行编写 .d.ts 描述文件。</p>
<p>其实不需要你把依赖内部实现全部编写一遍，你只需要关注你使用的属性、方法、返回值进行定义即可。</p>
<p>当然，如果你能把细节描述的足够完整，可以参考 <strong>3.3.2</strong> 发布到 npm 或者公司的私服上。</p>
<p>如果你是 TypeScript 新手，对编写描述文件没有信心，还有个更方便的方法。</p>
<p>小剧找到了一个牛逼的工具，可以替我们生成 .d.ts 文件。少量包会生成失败，但绝大多数依赖包都能顺利生成描述文件，只不过细节很粗糙，需要在生成的基础上修改。</p>
<p>dts-gen：<a href="https://github.com/Microsoft/dts-gen">https://github.com/Microsoft/dts-gen</a></p>
<p>此次迁移 juicer 因为历史比较久远没有对应的描述文件支持，node-isbot 因为比较冷门，也不支持 TS，都是借助于编写.d.ts 描述文件解决。</p>
<h3 id="34typescriptdebugger">3.4、TypeScript Debugger 支持</h3>
<p>因为早期开发博客后端代码的时候，NodeJS debugger 并不容易实现。为了提升开发便利性，此次迁移刚好把 Debugger 也做了支持。</p>
<p>借助于 VSCode 对 Nodejs 语言 和 TypeScript 的支持，相较于其他 IDE Debugger 更容易实现。</p>
<p>这里接不介绍实现方式了，你可以参考下面的链接配置。</p>
<p>Debugging TypeScript：</p>
<p><a href="https://code.visualstudio.com/docs/typescript/typescript-debugging">https://code.visualstudio.com/docs/typescript/typescript-debugging</a></p>
<h3 id="35promise">3.5、异步逻辑 Promise 化处理</h3>
<p>核心逻辑 Promise 化其实和 TypeScript 迁移很像，只是 Promise 迁或不迁并不影响迁移进度。</p>
<p>针对 NPM 依赖包，绝大多数升级版本即可以解决。或者手动借助 <code>new Promise</code> 方法也可以封装。比如 Mongo 连接库 mongodb 升级到最新版本后已支持 Promise。</p>
<p>常用的 NodeJS 内置的 FS 文件操作库，早期有第三方封装的 Promisify 版本，这次迁移发现 FS 已经默认支持了 Promise 返回值，为了迁移方便小剧把 FS 的引入全部改为了下面的方式。</p>
<pre><code class="javascript language-javascript">import { promises as fs } from 'fs'
</code></pre>
<p>业务逻辑的 Promise 化改造同样是体力活，尤其是它还和 TypeScript 改造同步进行。</p>
<p>这里就不展开介绍具体的过程了，后面会有改造前后的代码对比。</p>
<h3 id="36typescript">3.6、TypeScript 别名支持</h3>
<p>随着项目目录越来越深，结构分化越来越明显，模块路径别名可以很大程度上简化代码复用时的依赖调整过程。</p>
<p>例如高频使用的核心逻辑类型定义文件，如若使用相对路径则会非常麻烦。</p>
<pre><code class="javascript language-javascript">import { routeItemMatched, Connect, App } from '@/core/types'
</code></pre>
<p>TypeScript 的别名支持其实还算简单，只需要在 tsconfig 文件中的 paths 字段做配置即可完成。</p>
<p>只是在 nodeJS 项目中，借助于 ts-node 运行时会忽略别名设置。需要借助 tsconfig-paths/register 来进行参数补全。</p>
<h3 id="37nodejs">3.7、NodeJS 错误处理</h3>
<p>大家都知道 NodeJS 服务器是单进程，一旦服务内部发生错误会影响服务的稳定性，甚至整个服务挂掉。</p>
<p>早期在实现服务端代码时，尽可能的依靠代码的严谨性来保证基础的稳定性。线上运行则借助 pm2 工具进行兜底的重启工作。</p>
<p>虽然保障了服务的稳定可用，但却有几个问题没有解决。</p>
<p>1、线上发生故障不清楚，无法通过日志发现、解决问题。</p>
<p>2、Controller 逻辑错误，无法给用户及时反馈。</p>
<p>针对这两种场景，结合核心逻辑 Promise 化改造的完成，实施起来就很容易了。</p>
<p>因为这两类错误都在预期之外，因此错误位置的定位很重要。这里使用了统一的错误处理方法，打印出错误本身的基础上，借助于 trace 打印出调用栈，更有利于排查问题。</p>
<p>其次是 Controller 逻辑 Promise 化之后，可以很容易的做错误扑捉和超时处理。（这里仅做了错误捕捉和响应的延续，超时逻辑尚未处理。）</p>
<p>另外增加了进程级别的错误扑捉逻辑，保障服务可用性的最后一道防线。</p>
<pre><code class="javascript language-javascript">//错误处理
function errorHandler(error: Error){
    console.log(error)
    console.trace('Crazy Error')
}

// controller 错误捕捉
const usedRoute = matchedRoutes\[matchedRoutes.length - 1\]
newConnect.route = usedRoute
usedRoute.controller(usedRoute, newConnect, this).catch(async (error) =&gt; {
  console.error('\\ncontroller failed\\n|--------------&gt;\\n$', usedRoute, '\\n', error, '&lt;--------------|')
  const html = await newConnect.views('system/mongoFail',{error})
  newConnect.writeHTML(500, html)
  errorHandler(error)
})

// 全局错误捕捉
process.on('uncaughtException', (error: Error) =&gt; {
  errorHandler(error)
})
process.on('unhandledRejection', (error: Error) =&gt; {
  errorHandler(error)
})
</code></pre>
<h2 id="-3">四、迁移后的优势</h2>
<h3 id="41">4.1、语法提醒，代码即文档</h3>
<p>在之前的版本，仅仅 Javascript 原生属性方法、NodeJS 内置等常用的语法才有提示功能，偶尔修改代码的时候学习成本较高。</p>
<p>经过严格的 TypeScript 迁移后，代码拼写简单了很多，无需反复翻阅前期的代码做参考。</p>
<p><img src="https://static.bh-lay.com/blog/blog-to-ts/grammar-reminder.png" alt="语法提醒" /></p>
<h3 id="42promise">4.2、Promise 简化业务逻辑</h3>
<p>因为之前版本的异步逻辑依靠 callback 层层传递实现，无法保证 callback 一定被调用或者不会被多次重复调用。</p>
<p>而且在异步逻辑中，数据本身也需要被反复传递，很容易发生丢失或者类型错误。</p>
<p>这里以首页的 Controller 为例，看下改造前后的对比。</p>
<h4 id="421controller">4.2.1、首页 Controller 改造前</h4>
<p><img src="https://static.bh-lay.com/blog/blog-to-ts/before-ts.png" alt="首页 Controller 改造前" /></p>
<p>首页的 Controller 很简单，借助于 <code>app.cache</code> 工具检查是否有缓存的 html。</p>
<ul>
<li>若有，则触发第六行的回调函数，将缓存内容返回给用户。</li>
<li>若无，则执行底八行的回调函数，使用 <code>app.views</code> 生成首页的<code>html</code> ，并且在第十七行 <code>app.views</code> 的回调中调用参数中的保存缓存回调函数。<code>app.cache</code> 接收到保存缓存的回调执行后，执行保存缓存的同时，执行第六行的回调函数，将缓存内容返回给用户。</li>
</ul>
<p>整个过程在 Controller 这一侧看来，有四个回调函数。首页因为没有复杂的逻辑，看起来还好，换做博文列表或者其他 API Controller 回调简直是灾难。</p>
<p>这里有两点比较容易让人迷惑：</p>
<p>1、<code>app.cache</code> 第二个回调执行完，再调用回调内参数的回调，会执行 <code>app.cache</code> 的第一个回调。直观上看不出来，理解、学习成本较高。</p>
<p>2、回调众多，任何一个环节出错都会导致外侧不清楚 <code>Controller</code> 任务是否完成。如第十五行捕捉到了错误，也给用户做了响应，但外部并不知晓。</p>
<h4 id="422controller">4.2.2、首页 Controller 改造后</h4>
<p><img src="https://static.bh-lay.com/blog/blog-to-ts/after-ts.png" alt="首页 Controller 改造后" /></p>
<p>改造后的 <code>Controller</code> 其实只有两个方法，</p>
<p>1、通过 <code>app.cache</code> 获取首页 <code>html</code> 。</p>
<p>2、将<code>html</code> 返回给用户。</p>
<p>只是【1】中若没有找到缓存，会执行回调中定义的生成缓存方法。可能你会发现，保存缓存方法去哪儿了？</p>
<p>这其实也是 <code>Promise</code> 异步管理的优势之一，在省掉回调的同时也接管了数据的传递。在执行生成 html 的下一个环节自然能拿到对应的内容，因此 <code>app.cache</code> 可以直接保存缓存，业务无需执行存储缓存逻辑。</p>
<p>另外一个优势，整个<code>Controller</code> 任务是以 Promise 链存在的，在【3.7、NodeJS 错误处理】中进行错误捕捉才有了可能。这一点在之前的架构上实现，几乎难于登天。﻿</p>
<hr />
<p>因为只能在带娃的空闲时间处理，断断续续经历了近一个月的迁移才勉强完成。</p>
<p>期间一度忘了 mongo 数据库如何安装、启动、配置用户、备份恢复数据。好在经过这次迁移，小剧又重新“学会了”小剧客栈个人博客的安装部署，算是为十月份服务器迁移做准备工作。</p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Wed, 01 Mar 2023 12:11:25 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/njs1l1pod0</guid>
      <category>blog</category>
      <category>TypeScript</category>
      <category>Promise</category>
      <enclosure url="http://static.bh-lay.com/blog/blog-to-ts/cover.jpg" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>这篇文章是记录 2023 年小剧客栈服务端代码迁移的笔记。</p>
<p><strong>Github：</strong> <a href="https://github.com/bh-lay/blog/tree/master/backEnd">https://github.com/bh-lay/blog/tree/master/backEnd</a></p>
<h2 id="">一、回顾下最早期的版本</h2>
<p>熟悉小剧客栈的同学可能知道，小剧客栈开设于 2012 年 3 月，至今已经是第十一个年头了。</p>
<p>但可能很少有人了解，小剧客栈最早版本的发布异常简陋。</p>
<p><img src="https://static.bh-lay.com/blog/blog-to-ts/domain-info.png" alt="域名信息" /></p>
<p>因为刚入行不久，对前端略知一点儿，但是对服务器、Services 以及 Linux 完全一窍不通。所以最早版本的博客用了最 Low 的方式实现了博客的发布上线。</p>
<p>租用了一台 Windows 服务器，远程桌面的方式连接服务器。用了一款已经记不得叫什么的可视化软件搭建 NGINX、PHP、MySQL 运行环境。采用了当时很流行的 PHP 内容框架：帝国CMS。</p>
<p>小剧客栈以这样一种很粗糙的方式，陪伴了小剧工作的第一个年头。</p>
<p>同样这一年也是小剧各方面技能飞速增长的一年，这种粗糙也在以各种方式限制着小剧想象力的发挥。经历过这个阶段的同学应该有印象，NodeJS 在这段时间飞速发展，前端由此衍生出来了无限可能。</p>
<p>基于这种粗糙的限制，以及 NodeJS 的魅惑，小剧客栈在 2013 年 6 月迎来了全新的改版。NodeJS + MongoDB 的服务端架构让小剧在前端个人博客这个小圈子里小小的火了一把。</p>
<p><a href="http://bh-lay.com/blog/13f4b2a16e8">新博客 新心情</a> 这篇文章很简短地记录了小剧改版后的心情。</p>
<h2 id="-1">二、为什么这次要做迁移</h2>
<h3 id="21">2.1、小剧客栈服务端现状</h3>
<p>可能很多小伙伴会比较奇怪，为什么小剧客栈没有采用 Express、Koa 等流行的 NodeJS 服务端框架进行开发。而是使用一堆零散的工具、方法拼凑出一个极其简陋的服务端实现。</p>
<p>其实把时间倒退回 2013 年 6 月，一切就会变的合理起来。同期 Express 虽然已经发布了 3.0.0 版本，但在 NodeJS 社区中的认可度并没有那么高。Koa 在小剧客栈 NodeJS 版本正式发布的一个月后，才从 Express 中剥离出来并发布。</p>
<p>学习、实践、记录是小剧客栈建立至今的主旋律。如果能够由自己编码实现更多的细节，就可以更深入的了解服务端开发的底层原理，这是小剧所期望的。因此 2013- 2015 小剧大量的业余时间都投入在了个人博客的开发上。</p>
<p>其中最重要的就是服务端开发。实现了服务端 Route、Controller、View、Component、Session、Cache、Mongo 数据库管理以及静态资源管理等模块。</p>
<p>回看那段时间的代码，服务端在经历了完整的功能堆叠，和核心逻辑与业务代码分离后。迭代到了 2014 年之后几乎就没有大的功能性改动了。</p>
<p>目前小剧客栈服务端依赖较少，主要逻辑在仓库代码中，简单列一下代码结构。</p>
<p><strong>package.json 部分</strong></p>
<pre><code class="javascript language-javascript">"dotenv": "^6.2.0",
"formidable": "~1.2.1",
"juicer": "~0.6.5-stable",
"path-to-regexp": "^3.0.0",
"jssha": "^2.0.1",
"mongodb": "^3.1.13",
"node-isbot": "0.0.8",
"nodemailer": "~6.6.1",
"showdown": "^1.9.1"
"cron": "1.0.9",
"request": "^2.81.0"
</code></pre>
<p><strong>/core/ 目录下的服务端核心逻辑</strong></p>
<ul>
<li><strong>utils:</strong>  一些工具方法<ul>
<li>index.js</li>
<li>pagination.js</li>
<li>parse.js</li></ul></li>
<li><strong>DB.js：</strong> 数据库连接方法</li>
<li><strong>cache.js：</strong> 基于 FS 实现的缓存方法</li>
<li><strong>component.js：</strong> 与 views 对应，视图的模版片段方法</li>
<li><strong>connect.js：</strong> 连接类，用来处理 http 请求的获取和响应</li>
<li><strong>index.js：</strong> App 类，包含了 Router 的定义、匹配及 Controller 的分配逻辑</li>
<li><strong>session.js：</strong> 基于 FS 的 Session 管理类</li>
<li><strong>staticFile.js：</strong> 静态文件处理方法</li>
<li><strong>views.js：</strong> 视图处理逻辑</li>
</ul>
<h3 id="22">2.2、现阶段存在的问题</h3>
<p>如果本着能跑就行的原则，小剧客栈的服务端已经满足了最基本的要求，毕竟已经稳定运行了近十年。</p>
<p>若考虑继续维护迭代，有以下痛点亟待解决：</p>
<ol>
<li>逻辑组织基于 NodeJS 经典的回调实现，代码维护较为混乱。</li>
<li>Controller、Views、Cache 等模块之间任务处理较为松散，无法统一进行错误、超时处理。</li>
<li>各种服务端模块由自己实现，没有写以后也不打算写正式的文档，导致方法调用需要大量翻阅代码参考，且容易出错。</li>
<li>Github dependabot 安全检查，局部升级 npm 包，导致大量依赖不匹配，无法稳定运行</li>
</ol>
<h3 id="23">2.3、打算如何处理</h3>
<p>毕竟是七八年前的代码，这些问题在日常工作中积累了很多方法去处理。个人项目要求不多，逻辑简单、代码稳健、好维护就行。因此决定将原代码做以下处理：</p>
<h4 id="231promise">2.3.1、基于 Promise 重新组织调用逻辑</h4>
<p>这几乎是近些年来 JS 处理异步逻辑的标准操作了，在消除回调地狱方面有着天然的优势。</p>
<p>除此之外将复杂逻辑基于 Promise 链串联起来，在对 Controller 进行错误捕捉（HTTP Code 500）、超时处理等方面实现更直观</p>
<h4 id="232typescript">2.3.2、使用 TypeScript 重构代码</h4>
<p>TypeScript 并非灵丹妙药，很多前端项目使用 TypeScript 后对稳定性的提升和投入不一定匹配。然而在 NodeJS 后端逻辑上，TypeScript 则可以大展身手。</p>
<p>其一可以通过 type 定义，约定很多相似的数据类型，减少学习成本，如 userConfigRoute、configRoute、matchedRoute。</p>
<p>其二是借助于 VSCode 的语法提醒和错误提示，在代码编写阶段就可以轻松获取参数数量及类型，真正实现代码即文档。</p>
<h2 id="-2">三、代码迁移过程</h2>
<p>代码迁移 80% 都是重复的体力活，这里挑一些重点的环节进行介绍。</p>
<h3 id="31typescript">3.1、TypeScript 本地开发准备</h3>
<p>这一步出乎意料的简单，记得很久以前想尝试 nodeJS TS 开发，需要配置大量周边环境，而且还需要预编译。现在只需要三个工具即可完成开发。</p>
<ul>
<li><strong>typescript</strong>：TS 支持的核心依赖</li>
<li><strong>ts-node</strong>：TS 开发的基础，有他几乎就够了。</li>
<li><strong>nodemon：</strong>本地 Watch 模式开发，开发调试更丝滑。</li>
</ul>
<h3 id="32typescript">3.2、项目代码 TypeScript 改造</h3>
<p>这里是个体力活，需要大量修改代码才能完成，并且由于 TypeScript 的类型检查，导致很容易因为迁移过程导致项目运行不起来。</p>
<p>任何 JS 项目迁移 TS 都会经历这个过程，并且是整个迁移任务耗时最久工作量最大的过程。</p>
<p>这个过程有很多种实践方案，比如兼容 JS 代码并逐步迁移至 TS。或者一次性更改全部文件为 TS 版本，关闭类型检查并逐步放开类型检查，等等。</p>
<p>这里说下我的处理方式，因为迁移 TS 和 Promise 改造同步进行，涉及大量的代码逻辑调整，因此之前积累的迁移方案都不适用，或者说耗时较久不愿意接受。</p>
<p>小剧最终采用的是由入口文件开始迁移，一开始注释掉全部的调用逻辑。顺着依赖调用逻辑迁移至 TS。</p>
<p>这样的好处有两个，一是 TS 检查从始至终是严格的，不兼容 JS 的，迁移完成后无需回溯代码去补全健壮性。二是项目迁移的任意阶段代码均可以稳定运行，只是过程中存在功能缺失。</p>
<p>之所以说这个工作是体力活，是因为整个过程像是把一棵树的所有主干、树枝、树杈、树叶全部打散，再从根部一节节嫁接回来。</p>
<h3 id="33npmpackgetypescript">3.3、NPM Packge TypeScript 支持</h3>
<p>这一步耗时不久，但是却很困扰新手朋友。从经验上来说有三种处理方案</p>
<h4 id="331">3.3.1、升级依赖包</h4>
<p>大多数模块的贡献者在经历过数个版本迭代后，都会不可避免的面对 TypeScript 兼容的问题。因此可以尝试查看新版本 NPM 包是否已经提供了 TS 支持。</p>
<p>此次迁移 path-to-regexp、showdown 等类库都是通过升级依赖包完成的 TS 支持。</p>
<h4 id="332typespackagename">3.3.2、安装 @types/[packageName]</h4>
<p>一些流行的类库可能已经稳定运行了数年之久，无需新的功能迭代，或者内部有大量的奇技淫巧不适合用 TypeScript 重构，因此作者或者其他贡献者会编写 @types/[packageName] 描述文件，安装对应的依赖包也可以完成 TS 的兼容。</p>
<p>迁移中 node、request、formidable、cron 等依赖都使用了这个方法。</p>
<h4 id="333dts">3.3.3、编写 .d.ts 描述文件</h4>
<p>一般情况下前两个方法足以应对项目里的绝大多数 NPM 依赖包。如果不幸前两种方式均无效，那大概率是这个依赖已经处于无人维护的状态，你需要换一个包。</p>
<p>开个玩笑，事实上大量面向小众领域，或者功能稳定但面世较早的的包都不支持 TS。因此你需要自行编写 .d.ts 描述文件。</p>
<p>其实不需要你把依赖内部实现全部编写一遍，你只需要关注你使用的属性、方法、返回值进行定义即可。</p>
<p>当然，如果你能把细节描述的足够完整，可以参考 <strong>3.3.2</strong> 发布到 npm 或者公司的私服上。</p>
<p>如果你是 TypeScript 新手，对编写描述文件没有信心，还有个更方便的方法。</p>
<p>小剧找到了一个牛逼的工具，可以替我们生成 .d.ts 文件。少量包会生成失败，但绝大多数依赖包都能顺利生成描述文件，只不过细节很粗糙，需要在生成的基础上修改。</p>
<p>dts-gen：<a href="https://github.com/Microsoft/dts-gen">https://github.com/Microsoft/dts-gen</a></p>
<p>此次迁移 juicer 因为历史比较久远没有对应的描述文件支持，node-isbot 因为比较冷门，也不支持 TS，都是借助于编写.d.ts 描述文件解决。</p>
<h3 id="34typescriptdebugger">3.4、TypeScript Debugger 支持</h3>
<p>因为早期开发博客后端代码的时候，NodeJS debugger 并不容易实现。为了提升开发便利性，此次迁移刚好把 Debugger 也做了支持。</p>
<p>借助于 VSCode 对 Nodejs 语言 和 TypeScript 的支持，相较于其他 IDE Debugger 更容易实现。</p>
<p>这里接不介绍实现方式了，你可以参考下面的链接配置。</p>
<p>Debugging TypeScript：</p>
<p><a href="https://code.visualstudio.com/docs/typescript/typescript-debugging">https://code.visualstudio.com/docs/typescript/typescript-debugging</a></p>
<h3 id="35promise">3.5、异步逻辑 Promise 化处理</h3>
<p>核心逻辑 Promise 化其实和 TypeScript 迁移很像，只是 Promise 迁或不迁并不影响迁移进度。</p>
<p>针对 NPM 依赖包，绝大多数升级版本即可以解决。或者手动借助 <code>new Promise</code> 方法也可以封装。比如 Mongo 连接库 mongodb 升级到最新版本后已支持 Promise。</p>
<p>常用的 NodeJS 内置的 FS 文件操作库，早期有第三方封装的 Promisify 版本，这次迁移发现 FS 已经默认支持了 Promise 返回值，为了迁移方便小剧把 FS 的引入全部改为了下面的方式。</p>
<pre><code class="javascript language-javascript">import { promises as fs } from 'fs'
</code></pre>
<p>业务逻辑的 Promise 化改造同样是体力活，尤其是它还和 TypeScript 改造同步进行。</p>
<p>这里就不展开介绍具体的过程了，后面会有改造前后的代码对比。</p>
<h3 id="36typescript">3.6、TypeScript 别名支持</h3>
<p>随着项目目录越来越深，结构分化越来越明显，模块路径别名可以很大程度上简化代码复用时的依赖调整过程。</p>
<p>例如高频使用的核心逻辑类型定义文件，如若使用相对路径则会非常麻烦。</p>
<pre><code class="javascript language-javascript">import { routeItemMatched, Connect, App } from '@/core/types'
</code></pre>
<p>TypeScript 的别名支持其实还算简单，只需要在 tsconfig 文件中的 paths 字段做配置即可完成。</p>
<p>只是在 nodeJS 项目中，借助于 ts-node 运行时会忽略别名设置。需要借助 tsconfig-paths/register 来进行参数补全。</p>
<h3 id="37nodejs">3.7、NodeJS 错误处理</h3>
<p>大家都知道 NodeJS 服务器是单进程，一旦服务内部发生错误会影响服务的稳定性，甚至整个服务挂掉。</p>
<p>早期在实现服务端代码时，尽可能的依靠代码的严谨性来保证基础的稳定性。线上运行则借助 pm2 工具进行兜底的重启工作。</p>
<p>虽然保障了服务的稳定可用，但却有几个问题没有解决。</p>
<p>1、线上发生故障不清楚，无法通过日志发现、解决问题。</p>
<p>2、Controller 逻辑错误，无法给用户及时反馈。</p>
<p>针对这两种场景，结合核心逻辑 Promise 化改造的完成，实施起来就很容易了。</p>
<p>因为这两类错误都在预期之外，因此错误位置的定位很重要。这里使用了统一的错误处理方法，打印出错误本身的基础上，借助于 trace 打印出调用栈，更有利于排查问题。</p>
<p>其次是 Controller 逻辑 Promise 化之后，可以很容易的做错误扑捉和超时处理。（这里仅做了错误捕捉和响应的延续，超时逻辑尚未处理。）</p>
<p>另外增加了进程级别的错误扑捉逻辑，保障服务可用性的最后一道防线。</p>
<pre><code class="javascript language-javascript">//错误处理
function errorHandler(error: Error){
    console.log(error)
    console.trace('Crazy Error')
}

// controller 错误捕捉
const usedRoute = matchedRoutes\[matchedRoutes.length - 1\]
newConnect.route = usedRoute
usedRoute.controller(usedRoute, newConnect, this).catch(async (error) =&gt; {
  console.error('\\ncontroller failed\\n|--------------&gt;\\n$', usedRoute, '\\n', error, '&lt;--------------|')
  const html = await newConnect.views('system/mongoFail',{error})
  newConnect.writeHTML(500, html)
  errorHandler(error)
})

// 全局错误捕捉
process.on('uncaughtException', (error: Error) =&gt; {
  errorHandler(error)
})
process.on('unhandledRejection', (error: Error) =&gt; {
  errorHandler(error)
})
</code></pre>
<h2 id="-3">四、迁移后的优势</h2>
<h3 id="41">4.1、语法提醒，代码即文档</h3>
<p>在之前的版本，仅仅 Javascript 原生属性方法、NodeJS 内置等常用的语法才有提示功能，偶尔修改代码的时候学习成本较高。</p>
<p>经过严格的 TypeScript 迁移后，代码拼写简单了很多，无需反复翻阅前期的代码做参考。</p>
<p><img src="https://static.bh-lay.com/blog/blog-to-ts/grammar-reminder.png" alt="语法提醒" /></p>
<h3 id="42promise">4.2、Promise 简化业务逻辑</h3>
<p>因为之前版本的异步逻辑依靠 callback 层层传递实现，无法保证 callback 一定被调用或者不会被多次重复调用。</p>
<p>而且在异步逻辑中，数据本身也需要被反复传递，很容易发生丢失或者类型错误。</p>
<p>这里以首页的 Controller 为例，看下改造前后的对比。</p>
<h4 id="421controller">4.2.1、首页 Controller 改造前</h4>
<p><img src="https://static.bh-lay.com/blog/blog-to-ts/before-ts.png" alt="首页 Controller 改造前" /></p>
<p>首页的 Controller 很简单，借助于 <code>app.cache</code> 工具检查是否有缓存的 html。</p>
<ul>
<li>若有，则触发第六行的回调函数，将缓存内容返回给用户。</li>
<li>若无，则执行底八行的回调函数，使用 <code>app.views</code> 生成首页的<code>html</code> ，并且在第十七行 <code>app.views</code> 的回调中调用参数中的保存缓存回调函数。<code>app.cache</code> 接收到保存缓存的回调执行后，执行保存缓存的同时，执行第六行的回调函数，将缓存内容返回给用户。</li>
</ul>
<p>整个过程在 Controller 这一侧看来，有四个回调函数。首页因为没有复杂的逻辑，看起来还好，换做博文列表或者其他 API Controller 回调简直是灾难。</p>
<p>这里有两点比较容易让人迷惑：</p>
<p>1、<code>app.cache</code> 第二个回调执行完，再调用回调内参数的回调，会执行 <code>app.cache</code> 的第一个回调。直观上看不出来，理解、学习成本较高。</p>
<p>2、回调众多，任何一个环节出错都会导致外侧不清楚 <code>Controller</code> 任务是否完成。如第十五行捕捉到了错误，也给用户做了响应，但外部并不知晓。</p>
<h4 id="422controller">4.2.2、首页 Controller 改造后</h4>
<p><img src="https://static.bh-lay.com/blog/blog-to-ts/after-ts.png" alt="首页 Controller 改造后" /></p>
<p>改造后的 <code>Controller</code> 其实只有两个方法，</p>
<p>1、通过 <code>app.cache</code> 获取首页 <code>html</code> 。</p>
<p>2、将<code>html</code> 返回给用户。</p>
<p>只是【1】中若没有找到缓存，会执行回调中定义的生成缓存方法。可能你会发现，保存缓存方法去哪儿了？</p>
<p>这其实也是 <code>Promise</code> 异步管理的优势之一，在省掉回调的同时也接管了数据的传递。在执行生成 html 的下一个环节自然能拿到对应的内容，因此 <code>app.cache</code> 可以直接保存缓存，业务无需执行存储缓存逻辑。</p>
<p>另外一个优势，整个<code>Controller</code> 任务是以 Promise 链存在的，在【3.7、NodeJS 错误处理】中进行错误捕捉才有了可能。这一点在之前的架构上实现，几乎难于登天。﻿</p>
<hr />
<p>因为只能在带娃的空闲时间处理，断断续续经历了近一个月的迁移才勉强完成。</p>
<p>期间一度忘了 mongo 数据库如何安装、启动、配置用户、备份恢复数据。好在经过这次迁移，小剧又重新“学会了”小剧客栈个人博客的安装部署，算是为十月份服务器迁移做准备工作。</p>]]></content:encoded>
    </item>
    <item>
      <title>剧中人变化中的 2022</title>
      <link>https://bh-lay.com/blog/h5de1isyhn</link>
      <description><![CDATA[<p><img src="https://static.bh-lay.com/blog/2022-year-end/cover.jpg" alt="剧中人变化中的 2022" /></p>
<p>2022 是拥抱变化的一年，国际局势、国内市场、疫情政策等方面都充满不确定性。</p>
<p>在激荡的外部环境下，作为一个微小的个体，求稳本应该是全年的关键词。然而在 2022 年，小剧随着外部环境也一起拥抱了个人很大的变化。</p>
<h2 id="">一、工作变动</h2>
<p>2022 年初，小剧离开了工作近五年的科大讯飞。非常感谢<a href="https://github.com/ustbhuangyi">黄轶</a>的引荐，小剧顺利加入 ZOOM。</p>
<h3 id="11">1.1、讯飞离职</h3>
<p>自 2017 年从上海回到安徽，来到了小剧的第二故乡合肥，科大讯飞就成了小剧身上的一个特殊标签。</p>
<p>不仅仅是因为工作在这里，更是因为在这里认识了很多新朋，也奔现了一众旧友。</p>
<p>讯飞的业务线相较于一般企业要宽很多，面对的市场和涉及到的技术栈也更为丰富。因此这里很容易聚集各类奇奇怪怪的人才。</p>
<p>更是因为讯飞庞大的员工体量，在工作外的兴趣团体也更容易形成。</p>
<p>讯飞的四年半里，小剧经历了入职初期的大数据平台建设，后来的在线视频渲染引擎搭建。以及离职前的<a href="https://iflydocs.com/">讯飞文档</a>开发。</p>
<p>同样在这四年半里，小剧也因为个人的兴趣爱好加上机缘巧合，和一帮小伙伴申请成立了讯飞摄影协会，并且被赶鸭子上架担任了第一任会长。</p>
<p>以上这些，在小剧的 2018-2021 四份<a href="http://bh-lay.com/blog?tag=年终总结">年终总结</a>里都有或多或少的提过。</p>
<p>回看这四年半，每一次变化其实都有更好的选择，但对于变化感知不那么敏锐的小剧来说，每一次的选择好像又很适合我。</p>
<p>比如 2017 年的入职，和 2022 年的离职。</p>
<p><strong>[图1]：讯飞“毕业照”</strong></p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/iflytek.jpg" alt="图1：讯飞“毕业照" /></p>
<h3 id="12zoom">1.2、入职 ZOOM</h3>
<p>ZOOM 是一家很神奇的公司，几乎满足了小剧来合肥之前对互联网公司的所有幻想。</p>
<p>在合肥这片科技快速发展中的土地上，ZOOM 的 Deliver Happiness 价值观是不同于很多企业的存在。</p>
<p>和你了解的一样，ZOOM 是一家依托在线视频会议快速壮大的公司，目前也在扩展办公领域上的更多可能。</p>
<p>相比于很多巨无霸企业，ZOOM 的业务更单纯，也更聚焦。</p>
<p>因为 ZOOM 的业务需要面向全球，在国际化和可访问性方面的要求，相较于小剧之前的项目要高很多。</p>
<p>尤其是对视障、行为障碍等人群操作界面的易用性上需要更加关注。</p>
<p>前端在可访问性上有很多优秀的案例可以借鉴，但具体业务的实践依旧有很大的探索空间。这是小剧 2022 年在工作中接触的全新领域。</p>
<p><strong>[图2]：ZOOM 合肥办公楼</strong>﻿</p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/zoom.jpg" alt="图2：ZOOM 合肥办公楼" />﻿</p>
<h2 id="-1">二、生活上的不同</h2>
<h3 id="21">2.1、宝宝出生</h3>
<p>与媳妇一起经历了全部产检后，顺利度过了整个孕期，在四月迎来了小宝宝的诞生。</p>
<p>三十年以来，小剧一直以一个逍遥人的角色在游荡。虽然在九个月的等待中已经做足了心理准备，在小家伙诞生的那一天还是各种恍惚。</p>
<p>有了宝宝之后生活有了两个特别大的变化。</p>
<p>其一是作息变得特别规律，虽然也熬夜，但十一点绝对已经在床上等睡觉了。</p>
<p>其二是时间被莫名其妙的压缩了。仔细算下来小宝宝占用的时间并不多，甚至不及以前刷手机的时间的一半。可就是拿不出整块的时间撸代码、写文章。</p>
<p>不知道几年后，等她开始写作业的时候，我还有没有精力坐在她边上敲自己的代码，维护自己可能支撑不下去的小剧客栈。</p>
<p><strong>[图3]：宝宝陪小剧的第一个生日</strong></p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/qiaosi.jpg" alt="图3：宝宝陪小剧的第一个生日" />﻿</p>
<h3 id="22ipad">2.2、买了一个 iPad</h3>
<p>这其实不是件值得拿出来说的事，毕竟谁家没有个既能看爱奇艺又能压泡面的板板。</p>
<p>在入手这台 iPad 之前，小剧常用的设备有一台很古老的 Mac mini，一部 Macbook，一部 iPhone。</p>
<p>Mac mini 很正式，因为它被固定在书房里。个人项目的代码编写，文章草稿及深加工，邮件查收，以及定时 SMB 数据备份，这些工作都在这台机器上。虽然机器很老、性能不佳，但是这些任务完成的很出色。</p>
<p>Macbook 的身份更像是灵活版的 Mac mini，性能更好、便携性高。Mac min 上的绝大多数任务都能在这里完成。一般在外出，或者处理一些图像后期、全景作品编辑的时候会把它拿出来。</p>
<p>iPhone 就不用说了，社交、娱乐、发呆、轻型办公都在这里。</p>
<p>绘画一直是小剧很喜欢的一项活动。早些年没有积累任何绘画功底，之后也不期望能有比较出色表现。不过能将绘画融入生活，增加生活中的情趣也是一件乐事。</p>
<p>对于小剧而言，购买 iPad 可能更多的是为了替代吃灰很久的手绘板。</p>
<p>曾经期待手绘板能辅助小剧做些绘画的创作，实际使用几年后，只有偶尔全景编辑的时候才会把它拿出来。</p>
<p>经过大半年的体验，iPad 配合 Apple Pencil 对于小剧这种没有绘画功底的人很友好。并且借助于 Apple 的随航功能，在副屏模式下也能对 Mac 上的画面进行编辑。至此小剧的手绘板彻底沦为吃灰工具了。</p>
<p><strong>[图4]：宝宝拉弓射箭</strong></p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/lagong.jpg" alt="图4：宝宝拉弓射箭" />﻿</p>
<p><strong>[图5]：临摹媳妇照片</strong>﻿</p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/draw-picture.jpg" alt="图5：临摹媳妇照片" />﻿</p>
<p><strong>[图6]：巨型宝宝</strong>﻿</p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/large-baby.jpg" alt="图6：巨型宝宝" />﻿</p>
<p><strong>[图7]：设计书桌</strong></p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/design-desktop.jpg" alt="图7：设计书桌" />﻿</p>
<h2 id="-2">三、爱好上的变化</h2>
<p>2022 年因为生活上的重心偏向迎接、照顾宝宝，所以小剧今年在个人爱好上可以聊的很少。</p>
<p>作为生活的润滑剂，个人爱好方面的让步也是预料之中。</p>
<h3 id="31">3.1、几乎没有开展的摄影</h3>
<p>2022 年是小剧近些年来，在摄影这个爱好中投入精力最小的一年。</p>
<p>这一年没有进行任何有计划性的拍摄任务，手机里塞满的几乎全是宝宝的各种照片。</p>
<blockquote>
  <p>个人兴趣嘛，能让自己高兴，能给生活带来情趣，这才是最重要的。</p>
  <p>不能被一点点小兴趣绑架，那样得不偿失。</p>
</blockquote>
<p>这是小剧曾经在某次年终总结时为作品过少写的“狡辩”，放在今年同样适用。</p>
<p>这里就简单放几张今年比较有意思的照片，算是证明自己对摄影的热爱。</p>
<p><strong>[图8]：书房里的夕阳</strong></p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/inside-sun.jpg" alt="图8：书房里的夕阳" /></p>
<p><strong>[图9、10]：书房外的夕阳</strong>﻿</p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/outside-sun.jpg" alt="图9：书房外的夕阳1" /></p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/outside-sun-2.jpg" alt="图10：书房外的夕阳2" />﻿</p>
<p><strong>[图11]：ZOOM 合肥办公楼</strong>﻿
<img src="https://static.bh-lay.com/blog/2022-year-end/zoom.jpg" alt="图11：ZOOM 合肥办公楼" />﻿</p>
<p><strong>[图12]：俯瞰高新</strong></p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/hefei-gaoxin.jpg" alt="图12：俯瞰高新" />﻿</p>
<p><strong>[图13]：再游小朱岗城中村</strong></p>
<p>2019 年小剧在拍摄城中村项目时，曾经拍过拆迁工程刚开始的<a href="https://www.720yun.com/t/3cejrz4wtw2?scene_id=28155725">小朱岗城中村</a>。这次因为一些意外，在这里逗留了小半日，用随身带的手机拍了些斑驳的画面。</p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/xiaozhugang.jpg" alt="图13：再游小朱岗城中村" />﻿</p>
<p><strong>[图14]：经济日报《科创合肥》P5</strong></p>
<p>这张照片已经不知道被多少媒体转载过，但几乎很少会有文章会标明图片来源。比较难得在 2022-09-13 经济日报的《科创合肥》文章中，注明了图片作者。</p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/jjrb.jpg" alt="图14：经济日报《科创合肥》P5" />﻿</p>
<h3 id="32">3.2、勉强写完的小剧起始页</h3>
<p><a href="http://e.bh-lay.com">http://e.bh-lay.com</a> （墙裂推荐您点击一下）</p>
<p>在<a href="http://bh-lay.com/blog/8hfd4n48pd">《小剧的2021》</a>中，曾经提到过小剧起始页。这个项目起始于 2021 年末，在 2021 年还只是个雏形。</p>
<p>如果你感兴趣，可以进入<a href="http://bh-lay.com/blog?tag=小剧起始页">#小剧起始页</a>标签，这里详细介绍了开发小剧起始页的起因、设计、开发，以及各个技术细节。</p>
<p>2022 上半年所剩不多的业余时间，几乎都投入到了这个项目里。</p>
<p>忘了介绍了，小剧起始页是一款更适合前端的上网首页，基于浏览器 IndexedDB 持久化，数据存储更安全。</p>
<p><strong>[图15]：小剧起始页</strong></p>
<p><img src="https://static.bh-lay.com/blog/2022-icon-editor/start-page-screenshot.jpg" alt="图15：小剧起始页" /></p>
<h2 id="2022">四、2022 满意吗？</h2>
<p>时代是一场洪流，历史也终将湮灭为尘埃。作为动荡中的一粒砂石，能在写完这篇年终总结的时候问一句“满意吗？”，其实还是很奢侈的。</p>
<p>2022 年平稳切换到新的工作环境；宝宝平安降生，媳妇的孕、产期顺利度过；群体羊羊时家人几近无碍。</p>
<p>以上这些“幸运”已经将今年的下限抬得足够高。</p>
<p>工作上有了新的斩获，也获得了更为稳定的业余时间；照顾宝宝的辛苦之余也体会到了初为人父的快乐；专业技能方面在工作内外都得到了不小的提升；除此之外个人的兴趣也没有完全丢弃。</p>
<p>这些拔高上限的收获也充实了整个 2022。</p>
<p>2022 满意吗？</p>
<p>当然！</p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Wed, 18 Jan 2023 16:37:16 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/h5de1isyhn</guid>
      <category>年终总结</category>
      <category>生活</category>
      <category>2022</category>
      <enclosure url="http://static.bh-lay.com/blog/2022-year-end/cover.jpg" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p><img src="https://static.bh-lay.com/blog/2022-year-end/cover.jpg" alt="剧中人变化中的 2022" /></p>
<p>2022 是拥抱变化的一年，国际局势、国内市场、疫情政策等方面都充满不确定性。</p>
<p>在激荡的外部环境下，作为一个微小的个体，求稳本应该是全年的关键词。然而在 2022 年，小剧随着外部环境也一起拥抱了个人很大的变化。</p>
<h2 id="">一、工作变动</h2>
<p>2022 年初，小剧离开了工作近五年的科大讯飞。非常感谢<a href="https://github.com/ustbhuangyi">黄轶</a>的引荐，小剧顺利加入 ZOOM。</p>
<h3 id="11">1.1、讯飞离职</h3>
<p>自 2017 年从上海回到安徽，来到了小剧的第二故乡合肥，科大讯飞就成了小剧身上的一个特殊标签。</p>
<p>不仅仅是因为工作在这里，更是因为在这里认识了很多新朋，也奔现了一众旧友。</p>
<p>讯飞的业务线相较于一般企业要宽很多，面对的市场和涉及到的技术栈也更为丰富。因此这里很容易聚集各类奇奇怪怪的人才。</p>
<p>更是因为讯飞庞大的员工体量，在工作外的兴趣团体也更容易形成。</p>
<p>讯飞的四年半里，小剧经历了入职初期的大数据平台建设，后来的在线视频渲染引擎搭建。以及离职前的<a href="https://iflydocs.com/">讯飞文档</a>开发。</p>
<p>同样在这四年半里，小剧也因为个人的兴趣爱好加上机缘巧合，和一帮小伙伴申请成立了讯飞摄影协会，并且被赶鸭子上架担任了第一任会长。</p>
<p>以上这些，在小剧的 2018-2021 四份<a href="http://bh-lay.com/blog?tag=年终总结">年终总结</a>里都有或多或少的提过。</p>
<p>回看这四年半，每一次变化其实都有更好的选择，但对于变化感知不那么敏锐的小剧来说，每一次的选择好像又很适合我。</p>
<p>比如 2017 年的入职，和 2022 年的离职。</p>
<p><strong>[图1]：讯飞“毕业照”</strong></p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/iflytek.jpg" alt="图1：讯飞“毕业照" /></p>
<h3 id="12zoom">1.2、入职 ZOOM</h3>
<p>ZOOM 是一家很神奇的公司，几乎满足了小剧来合肥之前对互联网公司的所有幻想。</p>
<p>在合肥这片科技快速发展中的土地上，ZOOM 的 Deliver Happiness 价值观是不同于很多企业的存在。</p>
<p>和你了解的一样，ZOOM 是一家依托在线视频会议快速壮大的公司，目前也在扩展办公领域上的更多可能。</p>
<p>相比于很多巨无霸企业，ZOOM 的业务更单纯，也更聚焦。</p>
<p>因为 ZOOM 的业务需要面向全球，在国际化和可访问性方面的要求，相较于小剧之前的项目要高很多。</p>
<p>尤其是对视障、行为障碍等人群操作界面的易用性上需要更加关注。</p>
<p>前端在可访问性上有很多优秀的案例可以借鉴，但具体业务的实践依旧有很大的探索空间。这是小剧 2022 年在工作中接触的全新领域。</p>
<p><strong>[图2]：ZOOM 合肥办公楼</strong>﻿</p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/zoom.jpg" alt="图2：ZOOM 合肥办公楼" />﻿</p>
<h2 id="-1">二、生活上的不同</h2>
<h3 id="21">2.1、宝宝出生</h3>
<p>与媳妇一起经历了全部产检后，顺利度过了整个孕期，在四月迎来了小宝宝的诞生。</p>
<p>三十年以来，小剧一直以一个逍遥人的角色在游荡。虽然在九个月的等待中已经做足了心理准备，在小家伙诞生的那一天还是各种恍惚。</p>
<p>有了宝宝之后生活有了两个特别大的变化。</p>
<p>其一是作息变得特别规律，虽然也熬夜，但十一点绝对已经在床上等睡觉了。</p>
<p>其二是时间被莫名其妙的压缩了。仔细算下来小宝宝占用的时间并不多，甚至不及以前刷手机的时间的一半。可就是拿不出整块的时间撸代码、写文章。</p>
<p>不知道几年后，等她开始写作业的时候，我还有没有精力坐在她边上敲自己的代码，维护自己可能支撑不下去的小剧客栈。</p>
<p><strong>[图3]：宝宝陪小剧的第一个生日</strong></p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/qiaosi.jpg" alt="图3：宝宝陪小剧的第一个生日" />﻿</p>
<h3 id="22ipad">2.2、买了一个 iPad</h3>
<p>这其实不是件值得拿出来说的事，毕竟谁家没有个既能看爱奇艺又能压泡面的板板。</p>
<p>在入手这台 iPad 之前，小剧常用的设备有一台很古老的 Mac mini，一部 Macbook，一部 iPhone。</p>
<p>Mac mini 很正式，因为它被固定在书房里。个人项目的代码编写，文章草稿及深加工，邮件查收，以及定时 SMB 数据备份，这些工作都在这台机器上。虽然机器很老、性能不佳，但是这些任务完成的很出色。</p>
<p>Macbook 的身份更像是灵活版的 Mac mini，性能更好、便携性高。Mac min 上的绝大多数任务都能在这里完成。一般在外出，或者处理一些图像后期、全景作品编辑的时候会把它拿出来。</p>
<p>iPhone 就不用说了，社交、娱乐、发呆、轻型办公都在这里。</p>
<p>绘画一直是小剧很喜欢的一项活动。早些年没有积累任何绘画功底，之后也不期望能有比较出色表现。不过能将绘画融入生活，增加生活中的情趣也是一件乐事。</p>
<p>对于小剧而言，购买 iPad 可能更多的是为了替代吃灰很久的手绘板。</p>
<p>曾经期待手绘板能辅助小剧做些绘画的创作，实际使用几年后，只有偶尔全景编辑的时候才会把它拿出来。</p>
<p>经过大半年的体验，iPad 配合 Apple Pencil 对于小剧这种没有绘画功底的人很友好。并且借助于 Apple 的随航功能，在副屏模式下也能对 Mac 上的画面进行编辑。至此小剧的手绘板彻底沦为吃灰工具了。</p>
<p><strong>[图4]：宝宝拉弓射箭</strong></p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/lagong.jpg" alt="图4：宝宝拉弓射箭" />﻿</p>
<p><strong>[图5]：临摹媳妇照片</strong>﻿</p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/draw-picture.jpg" alt="图5：临摹媳妇照片" />﻿</p>
<p><strong>[图6]：巨型宝宝</strong>﻿</p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/large-baby.jpg" alt="图6：巨型宝宝" />﻿</p>
<p><strong>[图7]：设计书桌</strong></p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/design-desktop.jpg" alt="图7：设计书桌" />﻿</p>
<h2 id="-2">三、爱好上的变化</h2>
<p>2022 年因为生活上的重心偏向迎接、照顾宝宝，所以小剧今年在个人爱好上可以聊的很少。</p>
<p>作为生活的润滑剂，个人爱好方面的让步也是预料之中。</p>
<h3 id="31">3.1、几乎没有开展的摄影</h3>
<p>2022 年是小剧近些年来，在摄影这个爱好中投入精力最小的一年。</p>
<p>这一年没有进行任何有计划性的拍摄任务，手机里塞满的几乎全是宝宝的各种照片。</p>
<blockquote>
  <p>个人兴趣嘛，能让自己高兴，能给生活带来情趣，这才是最重要的。</p>
  <p>不能被一点点小兴趣绑架，那样得不偿失。</p>
</blockquote>
<p>这是小剧曾经在某次年终总结时为作品过少写的“狡辩”，放在今年同样适用。</p>
<p>这里就简单放几张今年比较有意思的照片，算是证明自己对摄影的热爱。</p>
<p><strong>[图8]：书房里的夕阳</strong></p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/inside-sun.jpg" alt="图8：书房里的夕阳" /></p>
<p><strong>[图9、10]：书房外的夕阳</strong>﻿</p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/outside-sun.jpg" alt="图9：书房外的夕阳1" /></p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/outside-sun-2.jpg" alt="图10：书房外的夕阳2" />﻿</p>
<p><strong>[图11]：ZOOM 合肥办公楼</strong>﻿
<img src="https://static.bh-lay.com/blog/2022-year-end/zoom.jpg" alt="图11：ZOOM 合肥办公楼" />﻿</p>
<p><strong>[图12]：俯瞰高新</strong></p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/hefei-gaoxin.jpg" alt="图12：俯瞰高新" />﻿</p>
<p><strong>[图13]：再游小朱岗城中村</strong></p>
<p>2019 年小剧在拍摄城中村项目时，曾经拍过拆迁工程刚开始的<a href="https://www.720yun.com/t/3cejrz4wtw2?scene_id=28155725">小朱岗城中村</a>。这次因为一些意外，在这里逗留了小半日，用随身带的手机拍了些斑驳的画面。</p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/xiaozhugang.jpg" alt="图13：再游小朱岗城中村" />﻿</p>
<p><strong>[图14]：经济日报《科创合肥》P5</strong></p>
<p>这张照片已经不知道被多少媒体转载过，但几乎很少会有文章会标明图片来源。比较难得在 2022-09-13 经济日报的《科创合肥》文章中，注明了图片作者。</p>
<p><img src="https://static.bh-lay.com/blog/2022-year-end/jjrb.jpg" alt="图14：经济日报《科创合肥》P5" />﻿</p>
<h3 id="32">3.2、勉强写完的小剧起始页</h3>
<p><a href="http://e.bh-lay.com">http://e.bh-lay.com</a> （墙裂推荐您点击一下）</p>
<p>在<a href="http://bh-lay.com/blog/8hfd4n48pd">《小剧的2021》</a>中，曾经提到过小剧起始页。这个项目起始于 2021 年末，在 2021 年还只是个雏形。</p>
<p>如果你感兴趣，可以进入<a href="http://bh-lay.com/blog?tag=小剧起始页">#小剧起始页</a>标签，这里详细介绍了开发小剧起始页的起因、设计、开发，以及各个技术细节。</p>
<p>2022 上半年所剩不多的业余时间，几乎都投入到了这个项目里。</p>
<p>忘了介绍了，小剧起始页是一款更适合前端的上网首页，基于浏览器 IndexedDB 持久化，数据存储更安全。</p>
<p><strong>[图15]：小剧起始页</strong></p>
<p><img src="https://static.bh-lay.com/blog/2022-icon-editor/start-page-screenshot.jpg" alt="图15：小剧起始页" /></p>
<h2 id="2022">四、2022 满意吗？</h2>
<p>时代是一场洪流，历史也终将湮灭为尘埃。作为动荡中的一粒砂石，能在写完这篇年终总结的时候问一句“满意吗？”，其实还是很奢侈的。</p>
<p>2022 年平稳切换到新的工作环境；宝宝平安降生，媳妇的孕、产期顺利度过；群体羊羊时家人几近无碍。</p>
<p>以上这些“幸运”已经将今年的下限抬得足够高。</p>
<p>工作上有了新的斩获，也获得了更为稳定的业余时间；照顾宝宝的辛苦之余也体会到了初为人父的快乐；专业技能方面在工作内外都得到了不小的提升；除此之外个人的兴趣也没有完全丢弃。</p>
<p>这些拔高上限的收获也充实了整个 2022。</p>
<p>2022 满意吗？</p>
<p>当然！</p>]]></content:encoded>
    </item>
    <item>
      <title>小剧起始页，离线数据篇</title>
      <link>https://bh-lay.com/blog/ragwqzkita</link>
      <description><![CDATA[<p>可能你不太清楚，<a href="https://e.bh-lay.com/">小剧起始页</a>之所以被开发出来，是因为小剧希望在业余时间学习一些新的技能。关于这部分的背景介绍，感兴趣的话可以看 <a href="http://bh-lay.com/blog/2o7r91ml5hg">《【开发回顾】小剧起始页》</a>，这里有详细的介绍。</p>
<p>其中有一个很重要的点，就是学习浏览器离线数据库 IndexedDB 相关的知识。</p>
<h2 id="indexeddb">一、IndexedDB 是什么 ？</h2>
<blockquote>
  <p>IndexedDB 是一种底层 API，用于在客户端存储大量的结构化数据（也包括文件/二进制大型对象（blobs））。该 API 使用索引实现对数据的高性能搜索。</p>
</blockquote>
<p>这段话摘抄自 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API">MDN Web Docs IndexedDb 介绍</a>，介绍了 IndexedDB 是什么，有什么样的特点。</p>
<p>可能干瘪瘪地看这段描述会比较晦涩，下面我们从前端数据存储的发展历程，来帮助我们理解 IndexedDB。</p>
<h2 id="">二、前端数据存储的发展</h2>
<p>相信各位或多或少都有过前端存取数据的经验，在用户身份认证、编辑器离线存储等场景都能发挥很大的作用。</p>
<h3 id="21cookie">2.1、Cookie</h3>
<p>互联网发展的早期，我们都习惯于使用兼容性最好的 Cookie 来存取数据。虽然容量很小，但是提供了前端保存数据的可能性，对于一些关键数据的存取还是够用了。</p>
<p>然而随着业务的发展，需要浏览器本地存储的数据类型愈发丰富，数据量也越来越多。并且因为 cookie 的特性，也暴露出对 HTTP 请求的影响以及数据安全的风险。</p>
<p>基于上面提到的数据容量、安全方面等问题，Cookie 一直没能成为前端数据存储的正规军。</p>
<h3 id="22webstorage">2.2、Web Storage</h3>
<p>直到以 LocalStorage 为代表的 Web Storage 问世，前端存储数据才开始大规模的应用起来。它拥有至少 2MB 的存储空间，丰富的存、取、删除、清空、迭代等 API，这些都让前端在个性化离线数据存储中大放异彩。</p>
<p>看起来前端已经可以为所欲为了，那要 IndexedDB 做什么呢 ？  </p>
<p>前面也提到了，Web Storage 有至少 2MB 的存储空间，在它刚面世的时候这是一个不可思议的容量。然而在贪得无厌的前端们，既要也要的情况下，很快就被蚕食殆尽。</p>
<p>并且一旦不幸需要操作较大的数据量，数据的存、取、序列化都会对 UI 有明显的阻塞。</p>
<h3 id="23websql">2.3、WebSQL</h3>
<p>在今天的主角 IndexedDB 登场的同期，还有另一个叫 WebSQL 的很火。因为目前已经在标准中废弃，并且 Safair 已经率先从浏览器中移除了它，所以这部分就不展开聊了。</p>
<h3 id="24indexeddb">2.4、IndexedDB</h3>
<blockquote>
  <p>虽然 Web Storage 在存储较少量的数据很有用，但对于存储更大量的结构化数据来说力不从心。而 IndexedDB 提供了这种场景的解决方案。</p>
</blockquote>
<p>这段话是前面引用 MDN 介绍的那段文本的后半句。表明了 IndexedDB 的容量更大，并且结构化的数据更有利于前端离线存储。</p>
<p>还有另一个优势这里没有提到，IndexebDB 的 API 是异步的，在涉及到大量数据存取操作的时候不会对 UI 有阻塞。</p>
<p>在 IndexedDB 中，破天荒的引入了 Blob 文件存储特性，也使得前端离线存储文件成为可能。</p>
<p>虽然 IndexedDB 有如此多的优势，但并不是说它一定会替代 Web Storage，就目前来说它们是两种完全不同的存储方式。</p>
<ul>
<li>Web Storage 更适合少量的、单一数据的存储，API 也更简单。</li>
<li>IndexedDB 结构化特点，更适合有大量离线数据存储的场景，API 相对复杂很多。</li>
</ul>
<h3 id="25cookiewebstorageindexeddb">2.5、Cookie、Web Storage、IndexedDB 对比</h3>
<p><strong>Cookie:</strong></p>
<p>4K以下存储空间，会在绝大多数请求携带 cookie 数据，读写操作会阻塞UI。</p>
<p><strong>Web Storage：</strong></p>
<p>5M左右（最小2M）的存储空间，不会在请求中携带数据，读写操作会阻塞UI。</p>
<p><strong>IndexedDB：</strong></p>
<p>磁盘允许的前提下，至少2G存储空间，不会在请求中携带数据，读写操作<strong>不会阻塞UI</strong>，支持事务保证数据一致性。</p>
<h2 id="indexeddb-1">二、为什么小剧起始页要用 IndexedDB</h2>
<p>小剧起始页是一款个性化很强的小网站，设计的目标就是要千人千面，每个人都可以按照自己的习惯排版自己的起始页。根据自己的工作、学习、娱乐等喜好编排自己的桌面和书签库。</p>
<p>如果你有过服务端的开发经验就会发现，要实现这个特点必须要有完善的用户系统，并且配合多张服务端数据表来承载用户和书签数据。</p>
<p>但这种模式并不是小剧想要的。</p>
<p>一来每个人的书签数据相对敏感，可能不会信任我这个独立的小程序员，担心我会时不时的偷窥你存了些什么。</p>
<p>再者用户很可能借助于小剧起始页存储黄暴恐的影音链接，这种架构下小剧势必要承担数据的存储工作，也是个不小的法律风险。</p>
<p>最重要的原因就和小剧起始页无关了。在开发讯飞文档 Web 版的时候小剧就想尝试离线数据存储了，可是鉴于实现的复杂度以及投入产出比，Web 版的离线存储一直没有机会尝试。</p>
<p>因此并不是小剧起始页选择了用 IndexedDB，而是因为小剧想要学习 IndexedDB，结合其他想尝试、学习的技能，才有了现如今的小剧起始页。</p>
<h2 id="indexeddb-2">三、IndexedDB 在小剧起始页的应用</h2>
<p>前面介绍了小剧起始页中使用离线数据存储的背景，离线数据存储的逻辑在项目中占了比较大的比重。下面就从数据库的基础结构组织、数据库的设计、存取逻辑实现来介绍 IndexedDB 在小剧起始页中的应用。</p>
<p>关于离线数据相关的具体代码实现，在 <a href="https://github.com/bh-lay/lays-workbench/tree/master/src/database">/src/database/</a> 目录内，可以打开代码参照着介绍阅读。</p>
<h3 id="31">3.1、基础结构组织</h3>
<pre><code>┣┳ database
  ┣┳ entity
  ┃┗━ bookmark.ts
  ┣┳ manager
  ┃┗━ bookmark-manager.ts
  ┣┳ services
  ┃┗━ bookmark-service.ts
  ┣┳ utils
  ┃┣━ bookmark-query-matches.ts
  ┃┗━ init-bookmark-to-db.ts
  ┗━ db.ts
</code></pre>
<p>数据管理目录划分的经验来自于前东家，在开发客户端时的学到的，这是比较经典的数据管理封装模型。</p>
<p><strong>entity</strong> 用来定义数据实体，对应到小剧起始页中，目前只有书签数据，具体定义了图标的基础数据类型和在 IndexedDB 中的实现。</p>
<p><strong>manager</strong> 用来提供对实体的访问，小剧起始页里主要体现在书签数据的增删改查、排序、导入、清空等操作。</p>
<p><strong>services</strong> 和 mannager 很像，所有的方法几乎一一对应。不同的是 manager 仅提供给 services 调用，而 services 则是暴露给业务使用的代码。</p>
<p><strong>utils</strong> 定义了一些工具方法，db.ts &nbsp;定义了数据库连接公共方法。</p>
<p>这样的分层设计使得逻辑更清晰，后期更换、扩展数据存储逻辑时也更容易。</p>
<p>例如增加在线同步数据逻辑时，仅需要增加 <code>bookmark-online-manger.ts</code> 文件，在 <code>services</code> 中分别调用不同的 manager 即可实现兼容。</p>
<h3 id="32">3.2、数据库表设计</h3>
<p>目前小剧起始页中仅用到一张表，用于存储用户书签数据，具体的列为：</p>
<ul>
<li>id：用于标记书签的唯一 ID。 </li>
<li>name：书签的名称。</li>
<li>type： 书签类型，目前有书签、目录、widgets、dialog 四种类型，其中 dialog 类型并未被使用。</li>
<li>parent：用来标识书签、目录的从属关系，构建树形结构的基础。</li>
<li>size：书签显示在桌面时，视觉上的大小尺寸。</li>
<li>undercoat：图标的底色。</li>
<li>value：链接地址或 widgets 的具体数据值。</li>
<li>icon：定义图标的视觉形象，在 <a href="http://bh-lay.com/blog/2niy1kx588y">小剧起始页，图标编辑组件的实现</a> 中有详细的设计实现说明。</li>
<li>desc：目前尚未用到的字段，计划存储书签说明信息。</li>
</ul>
<h3 id="33">3.3、存取逻辑的实现</h3>
<p>今天不展开介绍 IndexedDB 的具体使用方法，仅仅以获取一条书签数据为例，粗略描述数据库存取的逻辑。</p>
<p><strong>bookmark-service.ts</strong></p>
<pre><code class="typescript language-typescript">export function bookmarkGetService(bookmarkId: string) {
  return getIDBRequest().then((db: IDBDatabase) =&gt; {
    return bookmarkGetManager(db, bookmarkId)
  })
}
</code></pre>
<p><strong>bookmark-manager.ts</strong></p>
<pre><code class="typescript language-typescript">export function bookmarkGetManager(db: IDBDatabase, bookmarkId: string): Promise&lt;Bookmark&gt; {
  return new Promise((resolve, reject) =&gt; {
    const transaction = db.transaction(['bookmark'])
    const objectStore = transaction.objectStore('bookmark')
    const request = objectStore.get(bookmarkId)

    request.onsuccess = function () {
      if (request.result) {
        const bookmark = new Bookmark(request.result)
        // 数据读取成功
        resolve(bookmark)
      } else {
        const error = new Error('数据读取失败')
        reject(error)
      }
    }
    request.onerror = function () {
      // 数据写入失败
      const error = new Error('数据写入失败')
      // error.__detail = event
      reject(error)
    }
  })
}
</code></pre>
<p><strong>db.ts</strong></p>
<pre><code class="typescript language-typescript">import { bookmarkEntityInit } from './entity/bookmark'

function getIDBObject() {
  return window.indexedDB ||
    window.mozIndexedDB ||
    window.webkitIndexedDB ||
    window.msIndexedDB
}
export function getIDBRequest(): Promise&lt;IDBDatabase&gt; {
  return new Promise((resolve, reject) =&gt; {
    const indexedDB = getIDBObject()
    const request = indexedDB.open('data-store', 1)
    request.onerror = function() {
      const error = new Error('建立数据库连接失败！')
      // error.__detail = event
      reject(error)
    }
    request.onerror = function() {
      const error = new Error('建立数据库连接失败！')
      // error.__detail = event
      reject(error)
    }
    request.onsuccess = function() {
      resolve(request.result)
    }
    request.onupgradeneeded = function(event) {
      const db = request.result
      const target = event.target as CustomIDBTransactionEventTarget
      // db = event.target.result;
      if (!target) {
        reject(new Error('could not find target'))
      } else {
        const transaction = target.transaction
        transaction.oncomplete = function() {
          resolve(db)
        }
        // 此处处理数据库初始化、升级逻辑
        bookmarkEntityInit(db)
      }
    }
  })
}
</code></pre>
<h3 id="34">3.4、数据库升降级</h3>
<p>如果你有做过服务端或者客户端开发，一定对数据库升降级不陌生。</p>
<p>随着服务、应用的升级迭代，数据库在必要的时候需要增删字段或者做大面积结构调整。在遇到这类改动时，势必要处理数据库的升降级。</p>
<p>对于服务端来说，数据库的升降级相对简单一点，因为数据库就在自己手中，随着服务的升级数据库做对应的调整即可完成。而且非极端情况不需要处理数据库的降级，更不存在一次操作数据库跨越好几个版本的升降级。</p>
<p>客户端相对麻烦一点，因为应用的数据一般存放在设备的用户目录下。当应用打开时，你不确定他是当前设备的新用户，还是刚跳过很多个版本升级过来的老用户。再极端一点，或者是从很新的版本降级到的一个旧版本。</p>
<p>如果版本差之间存在一次或多次数据库升级操作，不处理数据库的迁移操作，很容易造成应用的崩溃或者数据存取的异常。</p>
<p>在使用 IndexedDB 时遇到的问题，和客户端面对本地数据库时几乎是一致的，唯一的差异是，正常情况下 IndexedDB 仅需要处理数据库的升级即可，很少需要处理数据库的降级。因为作为 Web 应用，用户很难将版本长时间固定下来。</p>
<p>正因为这个原因，在 IndexedDB 项目的第一个版本里，是不需要处理数据库升降级相关的操作的。</p>
<p>而小剧起始页虽然迭代了很多版本，数据结构仍然稳定在最初的版本，所以关于数据库升降级操作小剧仅仅知道很重要、需要做，但是并没有实际操作经验。</p>
<p>希望后面随着版本的迭代有机会处理这部分的逻辑。</p>
<h2 id="-1">四、看起来有点复杂 ？</h2>
<p>经过前面对 IndexedDB 开发迭代的介绍，以及对执行逻辑的梳理，整个实现过程已经简化很多了。然而你若是实际上手尝试，会发现还是挺麻烦的。</p>
<p>因为此次小剧使用 IndexedDB的目的，是以探索学习为主，所以更多的实现是裸调 IndexedDB API。如果你在业务中对 IndexedDB 有使用需求，大可不必一行行代码硬撸。</p>
<p><strong>更容易上手的工具</strong></p>
<p>其实在早期对 IndexedDB 调研的时候，就发现有一款叫做 Dexie 的 IndexedDB 连接库，可以很方便的进行数据结构的定义，数据记录的存取，以及相关事件的拦截。</p>
<p>如果你是以学习尝试为主，建议你研究下 IndexedDB。如果你想深入开发离线存储，了解 IndexedDB 的更多细节可能对你更有帮助。但如果你仅仅是项目中需要用到数据库的离线存储，可能 Dexie 会在开发效率上给你更多的保障。</p>
<hr />
<p>Update: 2023-06-29</p>
<p>关于数据库升级部分，这里有一份补充文章： <a href="http://bh-lay.com/blog/1g079snv7oo">《小剧起始页 IndexedDB 数据库升级记录》</a>。</p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Sun, 12 Jun 2022 14:34:19 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/ragwqzkita</guid>
      <category>小剧起始页</category>
      <category>IndexedDB</category>
      <category>离线存储</category>
      <enclosure url="http://static.bh-lay.com/blog/2022-indexeddb/Indexeddb.jpg" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>可能你不太清楚，<a href="https://e.bh-lay.com/">小剧起始页</a>之所以被开发出来，是因为小剧希望在业余时间学习一些新的技能。关于这部分的背景介绍，感兴趣的话可以看 <a href="http://bh-lay.com/blog/2o7r91ml5hg">《【开发回顾】小剧起始页》</a>，这里有详细的介绍。</p>
<p>其中有一个很重要的点，就是学习浏览器离线数据库 IndexedDB 相关的知识。</p>
<h2 id="indexeddb">一、IndexedDB 是什么 ？</h2>
<blockquote>
  <p>IndexedDB 是一种底层 API，用于在客户端存储大量的结构化数据（也包括文件/二进制大型对象（blobs））。该 API 使用索引实现对数据的高性能搜索。</p>
</blockquote>
<p>这段话摘抄自 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API">MDN Web Docs IndexedDb 介绍</a>，介绍了 IndexedDB 是什么，有什么样的特点。</p>
<p>可能干瘪瘪地看这段描述会比较晦涩，下面我们从前端数据存储的发展历程，来帮助我们理解 IndexedDB。</p>
<h2 id="">二、前端数据存储的发展</h2>
<p>相信各位或多或少都有过前端存取数据的经验，在用户身份认证、编辑器离线存储等场景都能发挥很大的作用。</p>
<h3 id="21cookie">2.1、Cookie</h3>
<p>互联网发展的早期，我们都习惯于使用兼容性最好的 Cookie 来存取数据。虽然容量很小，但是提供了前端保存数据的可能性，对于一些关键数据的存取还是够用了。</p>
<p>然而随着业务的发展，需要浏览器本地存储的数据类型愈发丰富，数据量也越来越多。并且因为 cookie 的特性，也暴露出对 HTTP 请求的影响以及数据安全的风险。</p>
<p>基于上面提到的数据容量、安全方面等问题，Cookie 一直没能成为前端数据存储的正规军。</p>
<h3 id="22webstorage">2.2、Web Storage</h3>
<p>直到以 LocalStorage 为代表的 Web Storage 问世，前端存储数据才开始大规模的应用起来。它拥有至少 2MB 的存储空间，丰富的存、取、删除、清空、迭代等 API，这些都让前端在个性化离线数据存储中大放异彩。</p>
<p>看起来前端已经可以为所欲为了，那要 IndexedDB 做什么呢 ？  </p>
<p>前面也提到了，Web Storage 有至少 2MB 的存储空间，在它刚面世的时候这是一个不可思议的容量。然而在贪得无厌的前端们，既要也要的情况下，很快就被蚕食殆尽。</p>
<p>并且一旦不幸需要操作较大的数据量，数据的存、取、序列化都会对 UI 有明显的阻塞。</p>
<h3 id="23websql">2.3、WebSQL</h3>
<p>在今天的主角 IndexedDB 登场的同期，还有另一个叫 WebSQL 的很火。因为目前已经在标准中废弃，并且 Safair 已经率先从浏览器中移除了它，所以这部分就不展开聊了。</p>
<h3 id="24indexeddb">2.4、IndexedDB</h3>
<blockquote>
  <p>虽然 Web Storage 在存储较少量的数据很有用，但对于存储更大量的结构化数据来说力不从心。而 IndexedDB 提供了这种场景的解决方案。</p>
</blockquote>
<p>这段话是前面引用 MDN 介绍的那段文本的后半句。表明了 IndexedDB 的容量更大，并且结构化的数据更有利于前端离线存储。</p>
<p>还有另一个优势这里没有提到，IndexebDB 的 API 是异步的，在涉及到大量数据存取操作的时候不会对 UI 有阻塞。</p>
<p>在 IndexedDB 中，破天荒的引入了 Blob 文件存储特性，也使得前端离线存储文件成为可能。</p>
<p>虽然 IndexedDB 有如此多的优势，但并不是说它一定会替代 Web Storage，就目前来说它们是两种完全不同的存储方式。</p>
<ul>
<li>Web Storage 更适合少量的、单一数据的存储，API 也更简单。</li>
<li>IndexedDB 结构化特点，更适合有大量离线数据存储的场景，API 相对复杂很多。</li>
</ul>
<h3 id="25cookiewebstorageindexeddb">2.5、Cookie、Web Storage、IndexedDB 对比</h3>
<p><strong>Cookie:</strong></p>
<p>4K以下存储空间，会在绝大多数请求携带 cookie 数据，读写操作会阻塞UI。</p>
<p><strong>Web Storage：</strong></p>
<p>5M左右（最小2M）的存储空间，不会在请求中携带数据，读写操作会阻塞UI。</p>
<p><strong>IndexedDB：</strong></p>
<p>磁盘允许的前提下，至少2G存储空间，不会在请求中携带数据，读写操作<strong>不会阻塞UI</strong>，支持事务保证数据一致性。</p>
<h2 id="indexeddb-1">二、为什么小剧起始页要用 IndexedDB</h2>
<p>小剧起始页是一款个性化很强的小网站，设计的目标就是要千人千面，每个人都可以按照自己的习惯排版自己的起始页。根据自己的工作、学习、娱乐等喜好编排自己的桌面和书签库。</p>
<p>如果你有过服务端的开发经验就会发现，要实现这个特点必须要有完善的用户系统，并且配合多张服务端数据表来承载用户和书签数据。</p>
<p>但这种模式并不是小剧想要的。</p>
<p>一来每个人的书签数据相对敏感，可能不会信任我这个独立的小程序员，担心我会时不时的偷窥你存了些什么。</p>
<p>再者用户很可能借助于小剧起始页存储黄暴恐的影音链接，这种架构下小剧势必要承担数据的存储工作，也是个不小的法律风险。</p>
<p>最重要的原因就和小剧起始页无关了。在开发讯飞文档 Web 版的时候小剧就想尝试离线数据存储了，可是鉴于实现的复杂度以及投入产出比，Web 版的离线存储一直没有机会尝试。</p>
<p>因此并不是小剧起始页选择了用 IndexedDB，而是因为小剧想要学习 IndexedDB，结合其他想尝试、学习的技能，才有了现如今的小剧起始页。</p>
<h2 id="indexeddb-2">三、IndexedDB 在小剧起始页的应用</h2>
<p>前面介绍了小剧起始页中使用离线数据存储的背景，离线数据存储的逻辑在项目中占了比较大的比重。下面就从数据库的基础结构组织、数据库的设计、存取逻辑实现来介绍 IndexedDB 在小剧起始页中的应用。</p>
<p>关于离线数据相关的具体代码实现，在 <a href="https://github.com/bh-lay/lays-workbench/tree/master/src/database">/src/database/</a> 目录内，可以打开代码参照着介绍阅读。</p>
<h3 id="31">3.1、基础结构组织</h3>
<pre><code>┣┳ database
  ┣┳ entity
  ┃┗━ bookmark.ts
  ┣┳ manager
  ┃┗━ bookmark-manager.ts
  ┣┳ services
  ┃┗━ bookmark-service.ts
  ┣┳ utils
  ┃┣━ bookmark-query-matches.ts
  ┃┗━ init-bookmark-to-db.ts
  ┗━ db.ts
</code></pre>
<p>数据管理目录划分的经验来自于前东家，在开发客户端时的学到的，这是比较经典的数据管理封装模型。</p>
<p><strong>entity</strong> 用来定义数据实体，对应到小剧起始页中，目前只有书签数据，具体定义了图标的基础数据类型和在 IndexedDB 中的实现。</p>
<p><strong>manager</strong> 用来提供对实体的访问，小剧起始页里主要体现在书签数据的增删改查、排序、导入、清空等操作。</p>
<p><strong>services</strong> 和 mannager 很像，所有的方法几乎一一对应。不同的是 manager 仅提供给 services 调用，而 services 则是暴露给业务使用的代码。</p>
<p><strong>utils</strong> 定义了一些工具方法，db.ts &nbsp;定义了数据库连接公共方法。</p>
<p>这样的分层设计使得逻辑更清晰，后期更换、扩展数据存储逻辑时也更容易。</p>
<p>例如增加在线同步数据逻辑时，仅需要增加 <code>bookmark-online-manger.ts</code> 文件，在 <code>services</code> 中分别调用不同的 manager 即可实现兼容。</p>
<h3 id="32">3.2、数据库表设计</h3>
<p>目前小剧起始页中仅用到一张表，用于存储用户书签数据，具体的列为：</p>
<ul>
<li>id：用于标记书签的唯一 ID。 </li>
<li>name：书签的名称。</li>
<li>type： 书签类型，目前有书签、目录、widgets、dialog 四种类型，其中 dialog 类型并未被使用。</li>
<li>parent：用来标识书签、目录的从属关系，构建树形结构的基础。</li>
<li>size：书签显示在桌面时，视觉上的大小尺寸。</li>
<li>undercoat：图标的底色。</li>
<li>value：链接地址或 widgets 的具体数据值。</li>
<li>icon：定义图标的视觉形象，在 <a href="http://bh-lay.com/blog/2niy1kx588y">小剧起始页，图标编辑组件的实现</a> 中有详细的设计实现说明。</li>
<li>desc：目前尚未用到的字段，计划存储书签说明信息。</li>
</ul>
<h3 id="33">3.3、存取逻辑的实现</h3>
<p>今天不展开介绍 IndexedDB 的具体使用方法，仅仅以获取一条书签数据为例，粗略描述数据库存取的逻辑。</p>
<p><strong>bookmark-service.ts</strong></p>
<pre><code class="typescript language-typescript">export function bookmarkGetService(bookmarkId: string) {
  return getIDBRequest().then((db: IDBDatabase) =&gt; {
    return bookmarkGetManager(db, bookmarkId)
  })
}
</code></pre>
<p><strong>bookmark-manager.ts</strong></p>
<pre><code class="typescript language-typescript">export function bookmarkGetManager(db: IDBDatabase, bookmarkId: string): Promise&lt;Bookmark&gt; {
  return new Promise((resolve, reject) =&gt; {
    const transaction = db.transaction(['bookmark'])
    const objectStore = transaction.objectStore('bookmark')
    const request = objectStore.get(bookmarkId)

    request.onsuccess = function () {
      if (request.result) {
        const bookmark = new Bookmark(request.result)
        // 数据读取成功
        resolve(bookmark)
      } else {
        const error = new Error('数据读取失败')
        reject(error)
      }
    }
    request.onerror = function () {
      // 数据写入失败
      const error = new Error('数据写入失败')
      // error.__detail = event
      reject(error)
    }
  })
}
</code></pre>
<p><strong>db.ts</strong></p>
<pre><code class="typescript language-typescript">import { bookmarkEntityInit } from './entity/bookmark'

function getIDBObject() {
  return window.indexedDB ||
    window.mozIndexedDB ||
    window.webkitIndexedDB ||
    window.msIndexedDB
}
export function getIDBRequest(): Promise&lt;IDBDatabase&gt; {
  return new Promise((resolve, reject) =&gt; {
    const indexedDB = getIDBObject()
    const request = indexedDB.open('data-store', 1)
    request.onerror = function() {
      const error = new Error('建立数据库连接失败！')
      // error.__detail = event
      reject(error)
    }
    request.onerror = function() {
      const error = new Error('建立数据库连接失败！')
      // error.__detail = event
      reject(error)
    }
    request.onsuccess = function() {
      resolve(request.result)
    }
    request.onupgradeneeded = function(event) {
      const db = request.result
      const target = event.target as CustomIDBTransactionEventTarget
      // db = event.target.result;
      if (!target) {
        reject(new Error('could not find target'))
      } else {
        const transaction = target.transaction
        transaction.oncomplete = function() {
          resolve(db)
        }
        // 此处处理数据库初始化、升级逻辑
        bookmarkEntityInit(db)
      }
    }
  })
}
</code></pre>
<h3 id="34">3.4、数据库升降级</h3>
<p>如果你有做过服务端或者客户端开发，一定对数据库升降级不陌生。</p>
<p>随着服务、应用的升级迭代，数据库在必要的时候需要增删字段或者做大面积结构调整。在遇到这类改动时，势必要处理数据库的升降级。</p>
<p>对于服务端来说，数据库的升降级相对简单一点，因为数据库就在自己手中，随着服务的升级数据库做对应的调整即可完成。而且非极端情况不需要处理数据库的降级，更不存在一次操作数据库跨越好几个版本的升降级。</p>
<p>客户端相对麻烦一点，因为应用的数据一般存放在设备的用户目录下。当应用打开时，你不确定他是当前设备的新用户，还是刚跳过很多个版本升级过来的老用户。再极端一点，或者是从很新的版本降级到的一个旧版本。</p>
<p>如果版本差之间存在一次或多次数据库升级操作，不处理数据库的迁移操作，很容易造成应用的崩溃或者数据存取的异常。</p>
<p>在使用 IndexedDB 时遇到的问题，和客户端面对本地数据库时几乎是一致的，唯一的差异是，正常情况下 IndexedDB 仅需要处理数据库的升级即可，很少需要处理数据库的降级。因为作为 Web 应用，用户很难将版本长时间固定下来。</p>
<p>正因为这个原因，在 IndexedDB 项目的第一个版本里，是不需要处理数据库升降级相关的操作的。</p>
<p>而小剧起始页虽然迭代了很多版本，数据结构仍然稳定在最初的版本，所以关于数据库升降级操作小剧仅仅知道很重要、需要做，但是并没有实际操作经验。</p>
<p>希望后面随着版本的迭代有机会处理这部分的逻辑。</p>
<h2 id="-1">四、看起来有点复杂 ？</h2>
<p>经过前面对 IndexedDB 开发迭代的介绍，以及对执行逻辑的梳理，整个实现过程已经简化很多了。然而你若是实际上手尝试，会发现还是挺麻烦的。</p>
<p>因为此次小剧使用 IndexedDB的目的，是以探索学习为主，所以更多的实现是裸调 IndexedDB API。如果你在业务中对 IndexedDB 有使用需求，大可不必一行行代码硬撸。</p>
<p><strong>更容易上手的工具</strong></p>
<p>其实在早期对 IndexedDB 调研的时候，就发现有一款叫做 Dexie 的 IndexedDB 连接库，可以很方便的进行数据结构的定义，数据记录的存取，以及相关事件的拦截。</p>
<p>如果你是以学习尝试为主，建议你研究下 IndexedDB。如果你想深入开发离线存储，了解 IndexedDB 的更多细节可能对你更有帮助。但如果你仅仅是项目中需要用到数据库的离线存储，可能 Dexie 会在开发效率上给你更多的保障。</p>
<hr />
<p>Update: 2023-06-29</p>
<p>关于数据库升级部分，这里有一份补充文章： <a href="http://bh-lay.com/blog/1g079snv7oo">《小剧起始页 IndexedDB 数据库升级记录》</a>。</p>]]></content:encoded>
    </item>
    <item>
      <title>小剧起始页，拖拽篇</title>
      <link>https://bh-lay.com/blog/a5osdl50g1</link>
      <description><![CDATA[<blockquote>
  <p>如果你还没有用过小剧起始页，建议你先体验一番之后，再回来阅读这篇文章。</p>
  <p><a href="https://e.bh-lay.com/">https://e.bh-lay.com/</a></p>
</blockquote>
<p>小剧起始页上线几个月来，受到很多小伙伴的喜欢。在达成自己学习目标的前提下，还能得到你们的认可，并且激励小剧持续迭代下去，这是最开始完全没有想到的。</p>
<p>收到部分小伙伴的反馈后了解到，小剧起始页让他们眼前一亮的地方集中在两个地方。</p>
<ul>
<li>仿手机桌面的布局，配合精致的图标设计，表现丰富又充满秩序</li>
<li>各类拖拽的交互的使用，简单易用，操作体验上升</li>
</ul>
<p>今天来聊一聊小剧起始页中，拖拽相关的设计及开发思路。</p>
<h2 id="">一、拖拽交互的特点</h2>
<p>在 PC 的操作上，主要借助于鼠标、键盘实现行为输入。键盘一般可以设计快捷键来辅助操作，而鼠标可以设计出更为丰富复杂的交互。</p>
<h3 id="11">1.1、鼠标的基本操作</h3>
<ul>
<li>基本的左右键，在执行按下、释放时，可以触发特定行为，</li>
<li>还有鼠标最独特的功能，可以移动鼠标对应的指针，体现在 WEB 中常见的有 <code>mousemove</code>、 <code>mouseenter</code>、<code>mouseleave</code>。</li>
</ul>
<p>将按下、释放组合起来，是我们交互中使用最广泛的点击、右键菜单行为。多事件延迟触发，还可以模拟出不太常用的双击、长按事件。</p>
<p>将鼠标的基本操作全部结合在一起，拖拽行为就可以被设计出来，鼠标按下是拖拽触发的时机，移动是执行拖拽的预操作，释放则是拖拽行为的最终确定。</p>
<p>拖拽可以用在移动位置、调整大小等常见的交互中，也可用于实现模拟手势，结合业务特定，还能设计出更多好玩的花样。</p>
<h3 id="12">1.2、拖拽交互有哪些优势呢？</h3>
<h4 id="121">1.2.1、界面简洁</h4>
<p>点击操作多数情况下需要占用 UI 界面，体现在 UI 组件上可以是各种链接、按钮、下拉框、右键菜单之类的基础组件。</p>
<p>而拖拽因为其交互特性，可以省略掉额外的 UI 设计，常见的 Slider 滑动、textarea 的缩放大小、侧边栏宽度调节、弹窗位置移动等交互，都不需要或者仅需极小的尺寸就可以辅助完成操作的执行。</p>
<h4 id="122">1.2.2、反馈更加直观</h4>
<p>排序、缩放等操作若是使用点击交互，你得预先知道排序的规则、调整后的目标值。当你了解这一切之后，你才能顺利完成一次排序操作。</p>
<p>拖拽交互则不一样，配合实时反馈的过程展示，操作更加容易，所见即所得的表现可以屏蔽很多内部逻辑设计。</p>
<h4 id="123">1.2.3、操作效率高</h4>
<p>常见的操作，在点击的交互下需要分解为 1、2、3 或者更多步才能完成，例如将 A 分类下的文章移动到 B 分类，常规操作路径如下：</p>
<ul>
<li>点击操作按钮，或右键点击目标</li>
<li>在弹出的菜单列表中选择移动分类</li>
<li>在新的弹窗中寻找 B 分类，并点击选中</li>
<li>最后点击确定，完成移动分类操作</li>
</ul>
<p>而在拖拽交互中，只需要拖住文章应对的 UI 组件，移动到 B 分类所在的位置，释放鼠标即可完成拖拽，非常简单高效。</p>
<h3 id="13">1.3、拖拽交互的弊端是什么？</h3>
<p>拖拽这么好用，但事实是在 WEB 设计中的使用率非常低，你可以打开淘宝、微博等网站，很少能够看到拖拽交互的影子。
这就不得不提一下拖拽交互的弊端了。</p>
<h4 id="131">1.3.1、交互较为隐晦</h4>
<p>前面提到拖拽交互不过分占用 UI 界面，这是其优势，也是弊端。相比点击交互，初次使用产品时可能不会使用拖拽交互，甚至不太容易发现还有拖拽可用。</p>
<h4 id="132">1.3.2、对部分用户不友好</h4>
<p>拖拽比较强依赖鼠标操作，在一些不太灵敏的触摸板上表现很差。对于一些手指活动不是很灵活的老年人、上肢行动不便的残障人士也是使用上的一大障碍。</p>
<p>大型的 WEB 应用的用户群体非常广泛，有对电子设备操作非常流畅的青少年，也有轻度使用电脑的耄耋老人，还有遭遇不幸的残障人士。</p>
<p>为了降低对用户的教育成本，减轻界面学习负担，提高用户覆盖面。均衡考量下，大型 WEB 应用更愿意在交互设计中，使用最为稳妥的点击操作方案。</p>
<h2 id="-1">二、小剧起始页有哪些拖拽交互</h2>
<p>前面提到了拖拽交互的特点，其中的弊端导致市面上很少看到大面积使用拖拽完成的交互。</p>
<p>那为什么小剧起始页要使用拖拽完成大部分功能交互呢？</p>
<p>首先小剧起始页是非常个人化的网站，绝大多数需求的出发点源于小剧自己，个人的喜好在这其中起到了非常决定性的作用。</p>
<p>其次愿意使用小剧起始页的小伙伴大多和我一样，是深谙 WEB 各类交互，对简洁的视觉有一定追求年轻人。</p>
<p>基于以上两点原因，小剧起始页将很多操作都设计为拖拽交互，比如下面几例：</p>
<h3 id="21">2.1、桌面图标操作</h3>
<p>桌面图标的排序调整顺序，两个图标合并成组，图标移入图标组，图标删除，调整图标尺寸等操作，均可以借助拖拽完成。</p>
<p><img src="https://static.bh-lay.com/blog/lays-workbench-drag/desktop.jpg" alt="desktop" /></p>
<h3 id="22">2.2、打开后的图标组</h3>
<p>这里的交互和桌面非常像，差异在于缺少了图标合并成组操作，多了放回桌面。</p>
<p><img src="https://static.bh-lay.com/blog/lays-workbench-drag/bookmark-group.jpg" alt="bookmark-group" /></p>
<h3 id="23">2.3、小书房</h3>
<p>小书房是一个类似于浏览器书签管理器的地方，这里的排序调整、合并成组、移动目录、删除等操作都是借助于拖拽交互完成。</p>
<p><img src="https://static.bh-lay.com/blog/lays-workbench-drag/private-boommark.jpg" alt="private-boommark" /></p>
<h4 id="24slider">2.4、各类 Slider</h4>
<p>Slider 是数值调整中非常常见的交互组件。小剧起始页中，桌面布局的各个尺寸调节，以及三角形生成器工具中，三角形边长微调的工具都是借助于 Slider 组件完成。</p>
<p>小剧在实现 Slider 组件的交互逻辑时，核心交互也是基于拖拽来实现。  </p>
<p><img src="https://static.bh-lay.com/blog/lays-workbench-drag/slider.jpg" alt="private-boommark" /></p>
<h2 id="-2">三、如何实现拖拽交互？</h2>
<blockquote>
  <p>鼠标按下是拖拽触发的时机，移动是执行拖拽的预操作，释放则是拖拽行为的最终确定。</p>
</blockquote>
<p>这句话在【鼠标的基本操作】部分有提到过，我们将鼠标的按下、移动、释放结合在一起，拖拽行为就可以被设计出来。</p>
<p>听起来是不是很简单，然而实际开发中仍然有几点需要考虑：</p>
<h3 id="31">3.1、拖拽的卡顿如何处理？</h3>
<p>很多小伙伴在处理拖拽交互的时候，经常性地发现整个体验非常拉垮。要么是拖拽过快容易导致拖拽行为失效，要么是鼠标移动过程非常生涩不流畅。</p>
<p>这是拖拽交互中最常见的两类问题，具体的原因及解决方法如下：</p>
<h4 id="311">3.1.1、拖拽过快导致拖拽行为失效</h4>
<p>我们以拖拽弹窗移动位置举例，分析一下问题出在哪儿？</p>
<p>通常情况下，我们会把 <code>mousedown</code> 和 <code>mousemove</code> 事件都绑定在弹窗组件上，并且在 <code>mousemove</code> 过程中实时修改弹窗坐标位置，这样即完成了弹窗拖拽移动位置的交互。</p>
<p>是不是听起来很合理？</p>
<p>问题其实就发生在 <code>mousemove</code> 事件的绑定上。因为 <code>mousemove</code> 事件的触发精度并不高，并且事件监听本身是异步的，所以从鼠标发生移动，到弹窗实际发生位移是有一定时间差的。如果弹窗再定义了 <code>transition</code> 缓动，问题会更加明显。</p>
<p>正是因为这个时间差，导致鼠标移动过快时很可能离开弹窗所在区域，自然也不会触发后续的 <code>mousemove</code> 事件，因此拖拽行为到此便被中断。</p>
<p>解决办法很简单，在 <code>mousedown</code> 触发拖拽开始行为后，<code>mousemove</code> 事件绑定在 body 上，而非弹窗本身。这样即使 UI 反馈延迟也不影响整个拖拽逻辑的响应。</p>
<p>当然你需要注意处理 body 的 <code>mousemove</code> 事件的解除，避免交互异常及内存泄漏。</p>
<h4 id="312">3.1.2、鼠标移动不流畅</h4>
<p>同样再拿拖拽弹窗移动位置举例，分析一下问题出在哪儿 ？</p>
<p>我们在鼠标移动过程中，计算当前鼠标与上一次触发 <code>mousemove</code> 的坐标差，再获取弹窗的位置，拿坐标差与当前弹窗位置做计算，即可算出弹窗新的位置。</p>
<p>是不是同样听起来很合理？</p>
<p>这里的问题在于弹窗位置的获取时机。因为这个行为涉及到 DOM 属性的读取，在 <code>mousemove</code> 相对高频触发的条件下会引起不必要的性能开销。</p>
<p>如果切换到拖拽排序的场景下，<code>mousemove</code> 过程中频繁获取列表尺寸、位置数据，性能损耗会更加明显。</p>
<p>想要优化这里的体验，提升操作流畅度，就要想办法减少不必要的 DOM 属性读取。</p>
<p>我们可以充分利用 <code>mousedown</code> 这个时机，将 dom 的尺寸、位置在这个时机获取，并缓存下来，同时缓存下来当前鼠标所在位置。在 <code>mousemove</code> 过程中仅仅根据新的坐标点和缓存的数据相计算，最后再应用到 dom 上，即可大幅提升拖拽的流畅度。</p>
<p>这一优化的效果在列表排序中分尤为明显。</p>
<h3 id="32">3.2、代码如何组织？</h3>
<p>拖拽因为有通用的交互共性，在具体的业务中又有着不同的交互特性，因此这里的代码组织分为两部分，分别是通用代码逻辑和业务代码逻辑。</p>
<h4 id="321">3.2.1、通用代码逻辑</h4>
<p>这是小剧使用了很多年的一段拖拽基础代码，绝大多数拖拽场景下小剧都是基于这段代码来实现交互。</p>
<p><a href="https://github.com/bh-lay/lays-workbench/blob/master/src/assets/ts/drag-handle.ts">drag-handle.ts</a></p>
<pre><code class="typescript language-typescript">const removeSelecteion = window.getSelection
  ? function () {
    const selections = window.getSelection()
    selections &amp;&amp; selections.removeAllRanges()
  }
  : function () {
    // nothing
  }

type dragOptions = {
  stableStart: (startX: number, startY: number) =&gt; void;
  move: (a: dragParams) =&gt; void;
  end: (a: dragParams) =&gt; void;
  cancel?: () =&gt; void;
  stableDistance: number;
};
type dragParams = {
  clientX: number;
  clientY: number;
  xOffset: number;
  yOffset: number;
};
function getParam(e: MouseEvent, startX: number, startY: number) {
  const clientX = e.clientX
  const clientY = e.clientY
  const xOffset = clientX - startX
  const yOffset = clientY - startY
  const returns: dragParams = {
    clientX,
    clientY,
    xOffset,
    yOffset,
  }
  return returns
}
export default function dragHandler(event: MouseEvent, options?: dragOptions) {
  const { stableStart, move, end, cancel, stableDistance } = options || {}
  const startX = event.clientX
  const startY = event.clientY
  let isTriggeredEvent = false
  const _stableDistance = stableDistance || 0
  if (!_stableDistance) {
    event.preventDefault &amp;&amp; event.preventDefault()
    event.stopPropagation &amp;&amp; event.stopPropagation()
  }
  const listenerConfig: AddEventListenerOptions = {
    passive: true,
    capture: true,
  }
  function mousemove(e: MouseEvent) {
    e.stopPropagation &amp;&amp; e.stopPropagation()
    removeSelecteion()
    const param = getParam(e, startX, startY)
    if (isTriggeredEvent) {
      move &amp;&amp; move(param)
    } else if (
      Math.sqrt(param.xOffset * param.xOffset + param.yOffset * param.yOffset) &gt;
      _stableDistance
    ) {
      isTriggeredEvent = true
      stableStart &amp;&amp; stableStart(startX, startY)
      move &amp;&amp; move(param)
    }
  }
  function up(event: MouseEvent) {
    event.stopPropagation()

    document.removeEventListener('mousemove', mousemove, listenerConfig)
    document.removeEventListener('mouseup', up, listenerConfig)
    if (isTriggeredEvent) {
      end &amp;&amp; end(getParam(event, startX, startY))
    } else {
      cancel &amp;&amp; cancel()
    }
  }
  document.addEventListener('mousemove', mousemove, listenerConfig)
  document.addEventListener('mouseup', up, listenerConfig)
}
</code></pre>
<p>代码看起来有七十多行，但除去 type 类型定义及不必要的空行，也只有六十行左右。</p>
<p>具体代码感兴趣的话可以自行研究，这里我只介绍它的使用方法。</p>
<p>还是以拖拽弹窗移动位置举例，在弹窗特定区域触发 <code>mousedown</code> 事件时，调用 <code>dragHandler</code> 方法，并且将当前的 <code>mouseevent</code> 传递进去，在第二个参数 <code>options</code> 中，<code>stableStart</code>、<code>move</code>、<code>end</code>、<code>cancel</code> 均为拖拽过程中的事件回调。</p>
<p>其中 <code>stableStart</code> 回调需要 配合 <code>stableDistance</code> 参数使用，作用是拖拽行为的保护。例如同一个元素既要响应点击事件，又要响应拖拽事件，不能因为鼠标按下抬起过程中的轻微位移就判定为是拖拽。</p>
<p>原生的 <code>mousemove</code> 事件只会单纯的将鼠标位置传递给回调函数，这里的 <code>move</code>、<code>end</code> 回调已经将拖拽过程中的水平、垂直方向上的位移差计算好，在绝大多数的拖拽行为中位移差比坐标更有用。</p>
<p>在 <code>stableStart</code> 中，将弹窗的初始位置获取好，并缓存下来。 在 <code>move</code> 中只需要将计算好的位移差与缓存下来的初始位置做计算，即可得到新的坐标。</p>
<p>是不是很简单？</p>
<p>我们再换一个更复杂的例子。</p>
<h4 id="322">3.2.2、业务代码逻辑</h4>
<p>前面提到的桌面图标的拖拽移动、合并、删除、调整大小要如何在一个拖拽行为中完成？</p>
<p><img src="https://static.bh-lay.com/blog/lays-workbench-drag/desktop.jpg" alt="desktop" /></p>
<p>用前面提到过的 <code>stableStart</code> 、<code>move</code>、<code>end</code>、<code>cancel</code> 四个回调来完成拖拽行为。</p>
<p><code>stableStart</code> 时机，获取被拖拽的元素，同时收集目标元素的尺寸、位置，并以 <code>mapList</code> 的形式记录下来。这里的目标元素包括桌面图标、删除区域、调整尺寸区域。</p>
<p><code>move</code> 阶段，将鼠标所在坐标与 <code>mapList</code> 相计算，判定命中的行为及目标。</p>
<p><code>end</code> 阶段同样需要计算鼠标所在坐标与 <code>mapList</code> 的关系，判定命中的行为及目标，并最终触发对应的操作。</p>
<p><code>cancel</code> 回调想必我不需要解释，表示各种原因导致的拖拽行为结束的回调。 </p>
<h3 id="33">3.3、交互应该如何设计？</h3>
<p>这里可以发挥自己的想象力，在不违反操作直觉的前提下有很大的发挥空间，这里就不展开聊了。</p>
<h3 id="34">3.4、视觉反馈应该如何实现？</h3>
<p>前面的【3.2、代码如何组织？】详细介绍了拖拽行为的实现过程，按照思路描述，完全可以完成拖拽的相关交互。</p>
<p>不知道你有没有疑问，就是视觉反馈有没有必要实现，要如何实现？</p>
<h4 id="341">3.4.1、有没有必要实现视觉反馈 ？</h4>
<p>还是回到桌面图标的拖拽交互中，如果没有视觉反馈，你将无法确定的知道，鼠标释放后将会是移动、还是合并。或者拖拽到了窗口中上方后，无法确定的知道将会执行删除，还是调整尺寸。</p>
<p>因此合理的视觉反馈可以让操作更加稳定可靠，减少心理恐慌。</p>
<h4 id="342">3.4.2、如何实现视觉反馈 ？</h4>
<p>在【3.2.2、业务代码逻辑】部分，其实已经能够将视觉反馈很好的嫁接在其中了。</p>
<p><code>stableStart</code> 获取到的 <code>mapList</code> 可以作为反馈的基础，再结合 <code>move</code> 阶段判定出命中的行为及目标，借助于特定的形状绘制，即可起到视觉反馈的作用。</p>
<p>当然视觉反馈的表现可以是多种多样的，这里仅仅是提供一种思路。</p>
<h2 id="-3">四、为啥不用现成的工具？</h2>
<p>拖拽交互在 WEB 设计中很重要，因此各个框架下都会有对应的组件来实现拖拽功能，其中最常见的功能要数拖拽排序了。</p>
<p>那小剧为什么不用呢 ？</p>
<p>常规的业务开发中，这些工具可以很好地帮助我们完成工作，并且使用合适的话能起到事半功倍的效果。</p>
<p>然而小剧也经常会听到这些的问题：</p>
<ul>
<li>为什么这个排序工具要限制我的布局方式？</li>
<li>为什么我的列表使用了可视区域渲染优化后，就没法再用拖拽排序了 ？</li>
<li>为什么拖拽的 UI 我没办法做定制 ？</li>
</ul>
<p>拖拽相关的组件因为特殊性，或多或少会对代码实施、DOM 布局、CSS 定义等方面增加限制。当你的交互足够复杂时，或者当你们的已有特性和限制冲突时，是否还要使用对应的组件就需要重新考量了。</p>
<p>了解拖拽交互的原理，不仅可以帮助我们更好的使用工具，更可以帮助我们在必要的时候摆脱工具的束缚。</p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Fri, 18 Mar 2022 18:18:21 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/a5osdl50g1</guid>
      <category>小剧起始页</category>
      <category>拖拽</category>
      <category>交互设计</category>
      <enclosure url="http://static.bh-lay.com/blog/lays-workbench-drag/cover.jpg" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<blockquote>
  <p>如果你还没有用过小剧起始页，建议你先体验一番之后，再回来阅读这篇文章。</p>
  <p><a href="https://e.bh-lay.com/">https://e.bh-lay.com/</a></p>
</blockquote>
<p>小剧起始页上线几个月来，受到很多小伙伴的喜欢。在达成自己学习目标的前提下，还能得到你们的认可，并且激励小剧持续迭代下去，这是最开始完全没有想到的。</p>
<p>收到部分小伙伴的反馈后了解到，小剧起始页让他们眼前一亮的地方集中在两个地方。</p>
<ul>
<li>仿手机桌面的布局，配合精致的图标设计，表现丰富又充满秩序</li>
<li>各类拖拽的交互的使用，简单易用，操作体验上升</li>
</ul>
<p>今天来聊一聊小剧起始页中，拖拽相关的设计及开发思路。</p>
<h2 id="">一、拖拽交互的特点</h2>
<p>在 PC 的操作上，主要借助于鼠标、键盘实现行为输入。键盘一般可以设计快捷键来辅助操作，而鼠标可以设计出更为丰富复杂的交互。</p>
<h3 id="11">1.1、鼠标的基本操作</h3>
<ul>
<li>基本的左右键，在执行按下、释放时，可以触发特定行为，</li>
<li>还有鼠标最独特的功能，可以移动鼠标对应的指针，体现在 WEB 中常见的有 <code>mousemove</code>、 <code>mouseenter</code>、<code>mouseleave</code>。</li>
</ul>
<p>将按下、释放组合起来，是我们交互中使用最广泛的点击、右键菜单行为。多事件延迟触发，还可以模拟出不太常用的双击、长按事件。</p>
<p>将鼠标的基本操作全部结合在一起，拖拽行为就可以被设计出来，鼠标按下是拖拽触发的时机，移动是执行拖拽的预操作，释放则是拖拽行为的最终确定。</p>
<p>拖拽可以用在移动位置、调整大小等常见的交互中，也可用于实现模拟手势，结合业务特定，还能设计出更多好玩的花样。</p>
<h3 id="12">1.2、拖拽交互有哪些优势呢？</h3>
<h4 id="121">1.2.1、界面简洁</h4>
<p>点击操作多数情况下需要占用 UI 界面，体现在 UI 组件上可以是各种链接、按钮、下拉框、右键菜单之类的基础组件。</p>
<p>而拖拽因为其交互特性，可以省略掉额外的 UI 设计，常见的 Slider 滑动、textarea 的缩放大小、侧边栏宽度调节、弹窗位置移动等交互，都不需要或者仅需极小的尺寸就可以辅助完成操作的执行。</p>
<h4 id="122">1.2.2、反馈更加直观</h4>
<p>排序、缩放等操作若是使用点击交互，你得预先知道排序的规则、调整后的目标值。当你了解这一切之后，你才能顺利完成一次排序操作。</p>
<p>拖拽交互则不一样，配合实时反馈的过程展示，操作更加容易，所见即所得的表现可以屏蔽很多内部逻辑设计。</p>
<h4 id="123">1.2.3、操作效率高</h4>
<p>常见的操作，在点击的交互下需要分解为 1、2、3 或者更多步才能完成，例如将 A 分类下的文章移动到 B 分类，常规操作路径如下：</p>
<ul>
<li>点击操作按钮，或右键点击目标</li>
<li>在弹出的菜单列表中选择移动分类</li>
<li>在新的弹窗中寻找 B 分类，并点击选中</li>
<li>最后点击确定，完成移动分类操作</li>
</ul>
<p>而在拖拽交互中，只需要拖住文章应对的 UI 组件，移动到 B 分类所在的位置，释放鼠标即可完成拖拽，非常简单高效。</p>
<h3 id="13">1.3、拖拽交互的弊端是什么？</h3>
<p>拖拽这么好用，但事实是在 WEB 设计中的使用率非常低，你可以打开淘宝、微博等网站，很少能够看到拖拽交互的影子。
这就不得不提一下拖拽交互的弊端了。</p>
<h4 id="131">1.3.1、交互较为隐晦</h4>
<p>前面提到拖拽交互不过分占用 UI 界面，这是其优势，也是弊端。相比点击交互，初次使用产品时可能不会使用拖拽交互，甚至不太容易发现还有拖拽可用。</p>
<h4 id="132">1.3.2、对部分用户不友好</h4>
<p>拖拽比较强依赖鼠标操作，在一些不太灵敏的触摸板上表现很差。对于一些手指活动不是很灵活的老年人、上肢行动不便的残障人士也是使用上的一大障碍。</p>
<p>大型的 WEB 应用的用户群体非常广泛，有对电子设备操作非常流畅的青少年，也有轻度使用电脑的耄耋老人，还有遭遇不幸的残障人士。</p>
<p>为了降低对用户的教育成本，减轻界面学习负担，提高用户覆盖面。均衡考量下，大型 WEB 应用更愿意在交互设计中，使用最为稳妥的点击操作方案。</p>
<h2 id="-1">二、小剧起始页有哪些拖拽交互</h2>
<p>前面提到了拖拽交互的特点，其中的弊端导致市面上很少看到大面积使用拖拽完成的交互。</p>
<p>那为什么小剧起始页要使用拖拽完成大部分功能交互呢？</p>
<p>首先小剧起始页是非常个人化的网站，绝大多数需求的出发点源于小剧自己，个人的喜好在这其中起到了非常决定性的作用。</p>
<p>其次愿意使用小剧起始页的小伙伴大多和我一样，是深谙 WEB 各类交互，对简洁的视觉有一定追求年轻人。</p>
<p>基于以上两点原因，小剧起始页将很多操作都设计为拖拽交互，比如下面几例：</p>
<h3 id="21">2.1、桌面图标操作</h3>
<p>桌面图标的排序调整顺序，两个图标合并成组，图标移入图标组，图标删除，调整图标尺寸等操作，均可以借助拖拽完成。</p>
<p><img src="https://static.bh-lay.com/blog/lays-workbench-drag/desktop.jpg" alt="desktop" /></p>
<h3 id="22">2.2、打开后的图标组</h3>
<p>这里的交互和桌面非常像，差异在于缺少了图标合并成组操作，多了放回桌面。</p>
<p><img src="https://static.bh-lay.com/blog/lays-workbench-drag/bookmark-group.jpg" alt="bookmark-group" /></p>
<h3 id="23">2.3、小书房</h3>
<p>小书房是一个类似于浏览器书签管理器的地方，这里的排序调整、合并成组、移动目录、删除等操作都是借助于拖拽交互完成。</p>
<p><img src="https://static.bh-lay.com/blog/lays-workbench-drag/private-boommark.jpg" alt="private-boommark" /></p>
<h4 id="24slider">2.4、各类 Slider</h4>
<p>Slider 是数值调整中非常常见的交互组件。小剧起始页中，桌面布局的各个尺寸调节，以及三角形生成器工具中，三角形边长微调的工具都是借助于 Slider 组件完成。</p>
<p>小剧在实现 Slider 组件的交互逻辑时，核心交互也是基于拖拽来实现。  </p>
<p><img src="https://static.bh-lay.com/blog/lays-workbench-drag/slider.jpg" alt="private-boommark" /></p>
<h2 id="-2">三、如何实现拖拽交互？</h2>
<blockquote>
  <p>鼠标按下是拖拽触发的时机，移动是执行拖拽的预操作，释放则是拖拽行为的最终确定。</p>
</blockquote>
<p>这句话在【鼠标的基本操作】部分有提到过，我们将鼠标的按下、移动、释放结合在一起，拖拽行为就可以被设计出来。</p>
<p>听起来是不是很简单，然而实际开发中仍然有几点需要考虑：</p>
<h3 id="31">3.1、拖拽的卡顿如何处理？</h3>
<p>很多小伙伴在处理拖拽交互的时候，经常性地发现整个体验非常拉垮。要么是拖拽过快容易导致拖拽行为失效，要么是鼠标移动过程非常生涩不流畅。</p>
<p>这是拖拽交互中最常见的两类问题，具体的原因及解决方法如下：</p>
<h4 id="311">3.1.1、拖拽过快导致拖拽行为失效</h4>
<p>我们以拖拽弹窗移动位置举例，分析一下问题出在哪儿？</p>
<p>通常情况下，我们会把 <code>mousedown</code> 和 <code>mousemove</code> 事件都绑定在弹窗组件上，并且在 <code>mousemove</code> 过程中实时修改弹窗坐标位置，这样即完成了弹窗拖拽移动位置的交互。</p>
<p>是不是听起来很合理？</p>
<p>问题其实就发生在 <code>mousemove</code> 事件的绑定上。因为 <code>mousemove</code> 事件的触发精度并不高，并且事件监听本身是异步的，所以从鼠标发生移动，到弹窗实际发生位移是有一定时间差的。如果弹窗再定义了 <code>transition</code> 缓动，问题会更加明显。</p>
<p>正是因为这个时间差，导致鼠标移动过快时很可能离开弹窗所在区域，自然也不会触发后续的 <code>mousemove</code> 事件，因此拖拽行为到此便被中断。</p>
<p>解决办法很简单，在 <code>mousedown</code> 触发拖拽开始行为后，<code>mousemove</code> 事件绑定在 body 上，而非弹窗本身。这样即使 UI 反馈延迟也不影响整个拖拽逻辑的响应。</p>
<p>当然你需要注意处理 body 的 <code>mousemove</code> 事件的解除，避免交互异常及内存泄漏。</p>
<h4 id="312">3.1.2、鼠标移动不流畅</h4>
<p>同样再拿拖拽弹窗移动位置举例，分析一下问题出在哪儿 ？</p>
<p>我们在鼠标移动过程中，计算当前鼠标与上一次触发 <code>mousemove</code> 的坐标差，再获取弹窗的位置，拿坐标差与当前弹窗位置做计算，即可算出弹窗新的位置。</p>
<p>是不是同样听起来很合理？</p>
<p>这里的问题在于弹窗位置的获取时机。因为这个行为涉及到 DOM 属性的读取，在 <code>mousemove</code> 相对高频触发的条件下会引起不必要的性能开销。</p>
<p>如果切换到拖拽排序的场景下，<code>mousemove</code> 过程中频繁获取列表尺寸、位置数据，性能损耗会更加明显。</p>
<p>想要优化这里的体验，提升操作流畅度，就要想办法减少不必要的 DOM 属性读取。</p>
<p>我们可以充分利用 <code>mousedown</code> 这个时机，将 dom 的尺寸、位置在这个时机获取，并缓存下来，同时缓存下来当前鼠标所在位置。在 <code>mousemove</code> 过程中仅仅根据新的坐标点和缓存的数据相计算，最后再应用到 dom 上，即可大幅提升拖拽的流畅度。</p>
<p>这一优化的效果在列表排序中分尤为明显。</p>
<h3 id="32">3.2、代码如何组织？</h3>
<p>拖拽因为有通用的交互共性，在具体的业务中又有着不同的交互特性，因此这里的代码组织分为两部分，分别是通用代码逻辑和业务代码逻辑。</p>
<h4 id="321">3.2.1、通用代码逻辑</h4>
<p>这是小剧使用了很多年的一段拖拽基础代码，绝大多数拖拽场景下小剧都是基于这段代码来实现交互。</p>
<p><a href="https://github.com/bh-lay/lays-workbench/blob/master/src/assets/ts/drag-handle.ts">drag-handle.ts</a></p>
<pre><code class="typescript language-typescript">const removeSelecteion = window.getSelection
  ? function () {
    const selections = window.getSelection()
    selections &amp;&amp; selections.removeAllRanges()
  }
  : function () {
    // nothing
  }

type dragOptions = {
  stableStart: (startX: number, startY: number) =&gt; void;
  move: (a: dragParams) =&gt; void;
  end: (a: dragParams) =&gt; void;
  cancel?: () =&gt; void;
  stableDistance: number;
};
type dragParams = {
  clientX: number;
  clientY: number;
  xOffset: number;
  yOffset: number;
};
function getParam(e: MouseEvent, startX: number, startY: number) {
  const clientX = e.clientX
  const clientY = e.clientY
  const xOffset = clientX - startX
  const yOffset = clientY - startY
  const returns: dragParams = {
    clientX,
    clientY,
    xOffset,
    yOffset,
  }
  return returns
}
export default function dragHandler(event: MouseEvent, options?: dragOptions) {
  const { stableStart, move, end, cancel, stableDistance } = options || {}
  const startX = event.clientX
  const startY = event.clientY
  let isTriggeredEvent = false
  const _stableDistance = stableDistance || 0
  if (!_stableDistance) {
    event.preventDefault &amp;&amp; event.preventDefault()
    event.stopPropagation &amp;&amp; event.stopPropagation()
  }
  const listenerConfig: AddEventListenerOptions = {
    passive: true,
    capture: true,
  }
  function mousemove(e: MouseEvent) {
    e.stopPropagation &amp;&amp; e.stopPropagation()
    removeSelecteion()
    const param = getParam(e, startX, startY)
    if (isTriggeredEvent) {
      move &amp;&amp; move(param)
    } else if (
      Math.sqrt(param.xOffset * param.xOffset + param.yOffset * param.yOffset) &gt;
      _stableDistance
    ) {
      isTriggeredEvent = true
      stableStart &amp;&amp; stableStart(startX, startY)
      move &amp;&amp; move(param)
    }
  }
  function up(event: MouseEvent) {
    event.stopPropagation()

    document.removeEventListener('mousemove', mousemove, listenerConfig)
    document.removeEventListener('mouseup', up, listenerConfig)
    if (isTriggeredEvent) {
      end &amp;&amp; end(getParam(event, startX, startY))
    } else {
      cancel &amp;&amp; cancel()
    }
  }
  document.addEventListener('mousemove', mousemove, listenerConfig)
  document.addEventListener('mouseup', up, listenerConfig)
}
</code></pre>
<p>代码看起来有七十多行，但除去 type 类型定义及不必要的空行，也只有六十行左右。</p>
<p>具体代码感兴趣的话可以自行研究，这里我只介绍它的使用方法。</p>
<p>还是以拖拽弹窗移动位置举例，在弹窗特定区域触发 <code>mousedown</code> 事件时，调用 <code>dragHandler</code> 方法，并且将当前的 <code>mouseevent</code> 传递进去，在第二个参数 <code>options</code> 中，<code>stableStart</code>、<code>move</code>、<code>end</code>、<code>cancel</code> 均为拖拽过程中的事件回调。</p>
<p>其中 <code>stableStart</code> 回调需要 配合 <code>stableDistance</code> 参数使用，作用是拖拽行为的保护。例如同一个元素既要响应点击事件，又要响应拖拽事件，不能因为鼠标按下抬起过程中的轻微位移就判定为是拖拽。</p>
<p>原生的 <code>mousemove</code> 事件只会单纯的将鼠标位置传递给回调函数，这里的 <code>move</code>、<code>end</code> 回调已经将拖拽过程中的水平、垂直方向上的位移差计算好，在绝大多数的拖拽行为中位移差比坐标更有用。</p>
<p>在 <code>stableStart</code> 中，将弹窗的初始位置获取好，并缓存下来。 在 <code>move</code> 中只需要将计算好的位移差与缓存下来的初始位置做计算，即可得到新的坐标。</p>
<p>是不是很简单？</p>
<p>我们再换一个更复杂的例子。</p>
<h4 id="322">3.2.2、业务代码逻辑</h4>
<p>前面提到的桌面图标的拖拽移动、合并、删除、调整大小要如何在一个拖拽行为中完成？</p>
<p><img src="https://static.bh-lay.com/blog/lays-workbench-drag/desktop.jpg" alt="desktop" /></p>
<p>用前面提到过的 <code>stableStart</code> 、<code>move</code>、<code>end</code>、<code>cancel</code> 四个回调来完成拖拽行为。</p>
<p><code>stableStart</code> 时机，获取被拖拽的元素，同时收集目标元素的尺寸、位置，并以 <code>mapList</code> 的形式记录下来。这里的目标元素包括桌面图标、删除区域、调整尺寸区域。</p>
<p><code>move</code> 阶段，将鼠标所在坐标与 <code>mapList</code> 相计算，判定命中的行为及目标。</p>
<p><code>end</code> 阶段同样需要计算鼠标所在坐标与 <code>mapList</code> 的关系，判定命中的行为及目标，并最终触发对应的操作。</p>
<p><code>cancel</code> 回调想必我不需要解释，表示各种原因导致的拖拽行为结束的回调。 </p>
<h3 id="33">3.3、交互应该如何设计？</h3>
<p>这里可以发挥自己的想象力，在不违反操作直觉的前提下有很大的发挥空间，这里就不展开聊了。</p>
<h3 id="34">3.4、视觉反馈应该如何实现？</h3>
<p>前面的【3.2、代码如何组织？】详细介绍了拖拽行为的实现过程，按照思路描述，完全可以完成拖拽的相关交互。</p>
<p>不知道你有没有疑问，就是视觉反馈有没有必要实现，要如何实现？</p>
<h4 id="341">3.4.1、有没有必要实现视觉反馈 ？</h4>
<p>还是回到桌面图标的拖拽交互中，如果没有视觉反馈，你将无法确定的知道，鼠标释放后将会是移动、还是合并。或者拖拽到了窗口中上方后，无法确定的知道将会执行删除，还是调整尺寸。</p>
<p>因此合理的视觉反馈可以让操作更加稳定可靠，减少心理恐慌。</p>
<h4 id="342">3.4.2、如何实现视觉反馈 ？</h4>
<p>在【3.2.2、业务代码逻辑】部分，其实已经能够将视觉反馈很好的嫁接在其中了。</p>
<p><code>stableStart</code> 获取到的 <code>mapList</code> 可以作为反馈的基础，再结合 <code>move</code> 阶段判定出命中的行为及目标，借助于特定的形状绘制，即可起到视觉反馈的作用。</p>
<p>当然视觉反馈的表现可以是多种多样的，这里仅仅是提供一种思路。</p>
<h2 id="-3">四、为啥不用现成的工具？</h2>
<p>拖拽交互在 WEB 设计中很重要，因此各个框架下都会有对应的组件来实现拖拽功能，其中最常见的功能要数拖拽排序了。</p>
<p>那小剧为什么不用呢 ？</p>
<p>常规的业务开发中，这些工具可以很好地帮助我们完成工作，并且使用合适的话能起到事半功倍的效果。</p>
<p>然而小剧也经常会听到这些的问题：</p>
<ul>
<li>为什么这个排序工具要限制我的布局方式？</li>
<li>为什么我的列表使用了可视区域渲染优化后，就没法再用拖拽排序了 ？</li>
<li>为什么拖拽的 UI 我没办法做定制 ？</li>
</ul>
<p>拖拽相关的组件因为特殊性，或多或少会对代码实施、DOM 布局、CSS 定义等方面增加限制。当你的交互足够复杂时，或者当你们的已有特性和限制冲突时，是否还要使用对应的组件就需要重新考量了。</p>
<p>了解拖拽交互的原理，不仅可以帮助我们更好的使用工具，更可以帮助我们在必要的时候摆脱工具的束缚。</p>]]></content:encoded>
    </item>
    <item>
      <title>小剧起始页，图标编辑组件的实现</title>
      <link>https://bh-lay.com/blog/2niy1kx588y</link>
      <description><![CDATA[<p>近来小剧在开发起始页，尝试了很多新的知识，学习到了不少新的技能点。</p>
<p>如果你正在使用电脑端查看，建议你打开 <a href="http://e.bh-lay.com/">小剧起始页</a>，体验一番再回来看图标编辑组件的实现。</p>
<p><img src="https://static.bh-lay.com/blog/2022-icon-editor/start-page-screenshot.jpg" alt="小剧起始页截图" /></p>
<p>今天就围绕图标编辑组件的实现，来聊一聊 <code>v-model</code> 的妙用、 <code>TypeScript</code> 定义特殊字符类型、 <code>watch</code> 动态创建与解除等方面的经验。</p>
<h2 id="">一、要解决什么问题？</h2>
<p><img src="https://static.bh-lay.com/blog/2022-icon-editor/screenshot-part.jpg" alt="图标类型示例" /></p>
<p>这是 <a href="http://e.bh-lay.com/">小剧起始页</a> 可自定义配置图标的桌面截图。如截图所示，图标有三种类型：</p>
<ul>
<li>圆圈所示是抓取对应网站的 favicon</li>
<li>方框标示的是自定义文本作为图标</li>
<li>丑丑的三角形框出的是借助于 <a href="https://material.iconhelper.cn/">material design icon</a> 实现的图标</li>
</ul>
<p>为了呈现上面的效果，需要在开发图标编辑组件功能之前，把数据结构设计和交互设计预先完成。</p>
<h3 id="11">1.1、怎么设计数据结构？</h3>
<p>在初步设计数据结构时，很自然的想到用两个字段来描述图标。例如 iconType、iconValue，分别是图标类型以及图标值。</p>
<p>然而把这两个数据和桌面数据结构放在一起时，为了见名知意被拉长的字段名甚是恶心，总觉得同一配置被拆分成两个字段相当别扭。</p>
<p>图标数据计划存储在浏览器缓存内，多出来的 Key、Value 所承载的数据量过少，而浏览器缓存空间又少的可怜，性价比过低。</p>
<p>增多的字段在图标的配置上是简单了，但是在跨组件参数传递、桌面图标数据存储、数据变更监听上需要增加更多冗余的设计。</p>
<p>一番思考无果后，恰好受到 HomeAssistant 图标配置的启发，发现可以借助同一个字段实现不同类型图标的内容配置。</p>
<p>为了满足前面提到的三种图标类型，小剧决定使用一个 icon 字段，替代 iconType、iconValue。并设计了以下图标字符规则：</p>
<ul>
<li><strong>'crab'</strong>：意为图标是自动抓取对应链接的 favicon</li>
<li><strong>'mdi:xxx'</strong>：意为使用通用的 <a href="https://material.iconhelper.cn/">material design icon</a> </li>
<li><strong>'text:xxx'</strong>：这个很简单，也是最常用的图标类型，显示定义的文本作为图标</li>
</ul>
<h3 id="12">1.2、交互怎么设计 ？</h3>
<p>因为图标有三种类型，很自然的会想到使用下拉框实现类型切换。其中 mdi 图标、文本图标需要用户补充输入内容，因此还需要一个输入框。</p>
<p>虽然 mdi 图标已经很流行了，还是可能有人不知道它是什么，或者临时想找图标不知道去哪里找图标代码。因此需要提供一个在线的链接，方便随时查找自己需要的图标。</p>
<p>初步统计，图标编辑组件至少需要下拉框、输入框、链接，这三个元素才能满足需求。</p>
<p><strong>第一版交互稿</strong></p>
<p><img src="https://static.bh-lay.com/blog/2022-icon-editor/design-1.jpg" alt="第一版交互稿" /></p>
<p>通过第一版交互稿，已经可以满足图标配置的基本需求了，然而三个元素平铺的交互不够简洁，视觉上也较为零散。</p>
<p>而且在选择抓取图标的类型时，输入框需要隐藏或禁用，UI 界面抖动较为明显。</p>
<p>再加上 mdi 图标的说明链接，如果常驻的话有歧义且影响观感，切换时动态显示隐藏又会加剧 UI 的抖动。</p>
<p>基于以上考量，小剧又设计了第二版图标编辑组件。</p>
<p>为了解决视觉上的抖动问题，同时强化编辑组件整体性，小剧把三个元素拉平到了同一条水平线上。</p>
<p>最左侧是与编辑组件融为一体的下拉框，经过统计常用网站提升配置效率，将三类图标的顺序重新调整。</p>
<p>选择抓取图标类型时，因为不需要用户输入，替代显示“尝试自动抓取图标”提示文字。在保证视觉稳定的前提下给用户提示说明。</p>
<p>于是有了下面的交互设计稿。</p>
<p><img src="https://static.bh-lay.com/blog/2022-icon-editor/design-2.jpg" alt="第二版交互稿" /></p>
<h2 id="-1">二、怎样开发图标编辑组件 ？</h2>
<p>因为是编辑组件，整个模块需要能够接收传递进来的图标值，而且在编辑过程中，需要实时将变更后的图标值传回父组件。</p>
<p>所以编辑组件需要能够在父级和自身之间，相互传递图标配置数据。</p>
<h3 id="21vue">2.1、问题：Vue 是单向数据流</h3>
<p>经常使用 Vue 开发的小伙伴应该知道，Vue 的数据模型是单向数据流。简而言之就是父级可以通过 props 改变子组件内的数据，反之则不行。</p>
<p>Vue 官方文档中是这样解释的：</p>
<blockquote>
  <p>所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定：父级 prop 的更新会向下流动到子组件中，但是反过来则不行。这样会防止从子组件意外改变父级组件的状态，从而导致你的应用的数据流向难以理解。</p>
</blockquote>
<p>单纯为了实现图标编辑组件其实不难。编辑组件内部定义一个 <code>prop</code>，用来接收数据，在编辑后使用 <code>$emit</code> 通知父级触发更改。</p>
<p>相对应的，父级在引用编辑组件的时候传递参数，再定义事件监听用于接收数据变动，实现数据的双向流动。</p>
<p>需要这么麻烦么？</p>
<h3 id="22vmodel">2.2、v-model 的妙用</h3>
<p>可能部分眼尖的小伙伴已经发现，在一些自定义组件中，支持使用 <code>.sync</code> 的修饰符来方式实现双向绑定。</p>
<p>在 <code>input</code>、 <code>textarea</code> 之类的原生组件，借助于 <code>v-model</code> 也可以实现双向绑定。</p>
<p>如果你看过 Vue 的源码，或者写过复杂的 <code>render</code> 渲染函数可能会明白，<code>v-model</code> 和 <code>.sync</code> 修饰符都只是语法糖。本质上还是前面提到的 <code>prop</code> 加 <code>$emit</code> 组合实现的。</p>
<p>如果有学习过 Vue3 的话，会发现 <code>v-model</code> 和 <code>.sync</code> 修饰符已经被统一成了同一个 API。</p>
<p>本质上虽然相同，使用上的差异还是很明显的， <code>v-model</code> 是那个相对不麻烦的数据传递方式。</p>
<p>最终促使小剧使用 <code>v-model</code> 实现图标编辑组件的数据通信，有以下三点原因：</p>
<p>1、因为是编辑组件，<code>v-model</code> 的设计可以与 UI 组件定义区分开</p>
<p>2、在语法糖的加持下，作为图标编辑组件的使用方，使用起来比 <code>prop</code>、<code>$emit</code> 更简洁</p>
<p>3、图标编辑组件需要内嵌到表单中，<code>v-model</code>  更容易与传统表单组件、校验组件相结合</p>
<h3 id="23">2.3、模块输入输出设计</h3>
<p>为了将复杂的逻辑抽象到模块内部，便于使用方整合，达到<strong>高内聚低耦合</strong>的效果。需要将模块数据层设计梳理好。</p>
<p>在图标编辑组件字符结构设计部分，小剧将图标的类型和值合并为同一个字符串，因此整个模块的输入输出仅为单一参数。</p>
<p>为了使用方能够达到的易用效果，借助于 v-model 即可实现这一点。</p>
<p>至于交互设计里提到的下拉框、输入框之类的 UI 和交互，以及图标配置字符的拆分与合并，则全部需要吸纳到图标编辑组件内部。</p>
<p>下面的图标编辑组件源码分析部分，会详细分的解释这一点。</p>
<h2 id="-2">三、图标编辑组件源码分析</h2>
<p>图标编辑组件的完整版，可以参考托管在 Github 上的源代码，下面就从几个关键环节描述一下实现过程。</p>
<p>图标编辑组件源码：<a href="https://github.com/bh-lay/lays-workbench/blob/master/src/components/add-bookmark/icon-editor.vue">https://github.com/bh-lay/…</a></p>
<p>接下来展示的代码全部基于 Vue3 的版本开发，使用 ts 编写。碎片化的代码并不能直接运行，感兴趣的话可以将整个项目 clone 下来。</p>
<h3 id="31">3.1、图标类型定义</h3>
<p>为了保证编码中的严谨，需要借助 TypeScript 校验图标内容的合法性。因为这种细粒度的类型定义是第一次尝试，研究之后发现竟然是可行的。</p>
<p>通过使用以下方式，即可定义图标类型。</p>
<pre><code class="javascript language-javascript">// 书签图标类型
type BookmarkIconCrab = 'crab'
type BookmarkIconMdi = `mdi:${string}`
type BookmarkIconText = `text:${string}`

export type BookmarkIcon = BookmarkIconCrab | BookmarkIconMdi | BookmarkIconText
</code></pre>
<h3 id="32">3.2、图标编辑组件数据转换</h3>
<p>为了便于模块使用者操作数据，需要提供干净的裸字符串给父级。</p>
<p>同时又为了编辑组件内部不同的状态支持，便于用户编辑操作，需要将字符拆分为图标类型、用户编辑值。</p>
<p>基于上述的原因，第一步工作就是需要开发数据转换逻辑。</p>
<p><img src="https://static.bh-lay.com/blog/2022-icon-editor/data-transfer.jpg" alt="图标编辑组件数据转换逻辑" /></p>
<pre><code class="javascript language-javascript">function decodeModelValue(iconConfig: BookmarkIcon) {
  const iconSplit = (iconConfig || '').split(':')
  if (iconSplit[0] === 'mdi') {
    return ['mdi', 'mdi-' + iconSplit[1]]
  } else if (iconConfig === 'crab') {
    return ['crab', '']
  }
  return ['text', iconSplit[1]]
}
function encodeModelValue(newIconType: string, inputValue: string): BookmarkIcon {
  if (newIconType === 'mdi') {
    const iconName = (inputValue || '').replace(/^mdi-/, '')
    // 经历过 replace 前后，值若不相等，则认为是合法的 mdi 配置
    if (inputValue !== iconName) {
      return `mdi:${iconName}`
    } else {
      // 否则，更正数据
      return 'mdi:'
    }
  } else if (newIconType === 'crab') {
    return 'crab'
  }
  return `text:${inputValue || ''}`
}
</code></pre>
<h3 id="33">3.3、应用图标数据到视图</h3>
<p>前面的图标编辑组件数据转换只是内存过程中的格式互换，用户看到的则是更直观的 UI 视图。</p>
<p>所以在数据流转过程中，视图的驱动更为重要。</p>
<pre><code class="javascript language-javascript">// 从应用图标数据，更新编辑器视图
function applyBookmarkIcon(bookmarkIcon: BookmarkIcon) {
  // 从图标数据，获取图标类型，用户输入值
  const [ newIconType, newInputValue ] = decodeModelValue(bookmarkIcon)
  // 当图标类型和用户输入值相同时，不处理
  if (newIconType === iconType.value &amp;&amp; newInputValue === inputValue.value) {
    return
  }
  // 更新数据
  iconType.value = newIconType
  inputValue.value = newInputValue
  iconTypeLabel.value = getIconTypeConfig(newIconType).label
}
</code></pre>
<h3 id="34">3.4、父级数据监听</h3>
<p>由于图标编辑组件特性所致，父级元素随时可能因为初始化、数据重置、条件关联等原因修改数据，故而子组件需要实时监听父级元素的参数变化。</p>
<p>这里为了避免父组件数据变化引起 UI 变动，导致被误判为用户操作，在应用图标数据前解除用户操作的监听，完成后再重新绑定。</p>
<pre><code class="javascript language-javascript">// 监听父组件传入的数据变动
watch(
  () =&gt; props.modelValue,
  (newValue: BookmarkIcon) =&gt; {
    // 解除用户操作监听
    unWatchUserIntractive()
    // 更新编辑器数据
    applyBookmarkIcon(newValue)
    // 创建用户操作监听
    watchUserIntractive()
  },
  {
    immediate: true,
  }
)
</code></pre>
<h3 id="35">3.5、用户交互监听</h3>
<p>用户交互监听其实只有两个，一个是图标类型切换事件，另一个是用户输入行为。</p>
<p>当用户交互发生后，需要适当的做默认值填充、常见错误修正工作，然后应用到视图上，最后通知父级元素触发数据修改。</p>
<p>可能你会有疑问：这里的数据应用、变动监听，与父级传入参数的监听之间，不会形成死循环么？</p>
<p>其实最开始的确有，由于 Vue 在设置前后同值时，会静默此行为，不触发数据变动事件。因而死循环在执行数次后会自然停止。</p>
<p>彻底解决循环回路问题是通过以下两个手段。一是父级数据变动后，用户交互的监听增加了先解除再重设的操作。二是增加 <code>applyBookmarkIcon</code>  内的同值检测逻辑。</p>
<pre><code class="javascript language-javascript">// 监听用户交互操作行为
function watchUserIntractive() {
  // 监听图标类型变化
  const unWatchIconType = watch(iconType, newIconType =&gt; {
    // 获取默认填充值
    let defaultInputValue = getIconTypeConfig(newIconType).default
    let newModelValue = encodeModelValue(newIconType, defaultInputValue)
    applyBookmarkIcon(newModelValue)
    context.emit('update:modelValue', newModelValue)
  })
  // 监听用户输入
  const unWatchInput = watch(inputValue, newInputValue =&gt; {
    // 避免粘贴时出现重复的 mdi-，尝试去删除
    if (newInputValue.match(/^mdi-mdi-/)) {
      newInputValue = newInputValue.replace(/^mdi-/, '')
    }
    const newModelValue = encodeModelValue(iconType.value, newInputValue)
    applyBookmarkIcon(newModelValue)
    context.emit('update:modelValue', newModelValue)
  })
  // 标记解除监听回调
  unWatchUserIntractive = () =&gt; {
    unWatchIconType()
    unWatchInput()
  }
}
// 解除用户交互操作行为监听
let unWatchUserIntractive = () =&gt; {
  // nothind
}
</code></pre>
<p>以上就是图标编辑组件核心逻辑。</p>
<p>可能你会发现 Vue3 的代码相较于 Vue2 中的 Options 形式的组织起来更为灵活。其实Vue2 里也有很多类似于 <code>this.$watch</code>、<code>this.$on('hook:beforeDestroy'</code> API，也能够实现灵活的逻辑组织。</p>
<p>只是它们依旧脱离不了 ViewModel 的上下文，并且没有像 Vue3 中一样，抽象出 <code>setup</code> 完成模块的组织。</p>
<h2 id="-3">四、为什么会写这篇文章</h2>
<p>经过对一段时间面试情况的总结，发现近四成的同学对 <code>v-model</code> 的原理并不清楚，或者是理解其中的原理，但是没有实践经历。</p>
<p>这可能是对方基础知识不均衡的原因，也可能是我们的吸引力不够导致。</p>
<p>基于这个情况，原本计划写一篇关于 <code>v-model</code>、 <code>.sync</code> 实现双向通信的原理，Vue3 升级后的异同点，以及项目中常见的实例。</p>
<p>整理素材时发现，关于前两点，Vue2、Vue3 的文档里已经说的足够清楚，无需赘述。具体可以参考以下链接。</p>
<p>[<a href="https://cn.vuejs.org/v2/guide/components-custom-events.html#自定义组件的-v-model">Vue2] 自定义组件的 v-model</a></p>
<p>[<a href="https://cn.vuejs.org/v2/guide/components-custom-events.html#sync-修饰符">Vue2] .sync 修饰符</a></p>
<p>[<a href="https://v3.cn.vuejs.org/guide/migration/v-model.html">Vue3] v-model</a></p>
<p>至于项目中的实例，文档里的代码也已经足够与项目经历产生联想，并没有多大必要去做项目罗列。</p>
<p>倒不如拿自己项目中的一个小例子，从设计到开发理一遍，可能更有用。</p>
<p>最后，再次建议你打开 <a href="http://e.bh-lay.com/">小剧起始页</a>，体验一番。</p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Mon, 31 Jan 2022 22:20:22 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/2niy1kx588y</guid>
      <category>v-model</category>
      <category>Vue3</category>
      <category>Vue2</category>
      <category>小剧起始页</category>
      <category>Vue</category>
      <enclosure url="http://static.bh-lay.com/blog/2022-icon-editor/data-transfer.jpg" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>近来小剧在开发起始页，尝试了很多新的知识，学习到了不少新的技能点。</p>
<p>如果你正在使用电脑端查看，建议你打开 <a href="http://e.bh-lay.com/">小剧起始页</a>，体验一番再回来看图标编辑组件的实现。</p>
<p><img src="https://static.bh-lay.com/blog/2022-icon-editor/start-page-screenshot.jpg" alt="小剧起始页截图" /></p>
<p>今天就围绕图标编辑组件的实现，来聊一聊 <code>v-model</code> 的妙用、 <code>TypeScript</code> 定义特殊字符类型、 <code>watch</code> 动态创建与解除等方面的经验。</p>
<h2 id="">一、要解决什么问题？</h2>
<p><img src="https://static.bh-lay.com/blog/2022-icon-editor/screenshot-part.jpg" alt="图标类型示例" /></p>
<p>这是 <a href="http://e.bh-lay.com/">小剧起始页</a> 可自定义配置图标的桌面截图。如截图所示，图标有三种类型：</p>
<ul>
<li>圆圈所示是抓取对应网站的 favicon</li>
<li>方框标示的是自定义文本作为图标</li>
<li>丑丑的三角形框出的是借助于 <a href="https://material.iconhelper.cn/">material design icon</a> 实现的图标</li>
</ul>
<p>为了呈现上面的效果，需要在开发图标编辑组件功能之前，把数据结构设计和交互设计预先完成。</p>
<h3 id="11">1.1、怎么设计数据结构？</h3>
<p>在初步设计数据结构时，很自然的想到用两个字段来描述图标。例如 iconType、iconValue，分别是图标类型以及图标值。</p>
<p>然而把这两个数据和桌面数据结构放在一起时，为了见名知意被拉长的字段名甚是恶心，总觉得同一配置被拆分成两个字段相当别扭。</p>
<p>图标数据计划存储在浏览器缓存内，多出来的 Key、Value 所承载的数据量过少，而浏览器缓存空间又少的可怜，性价比过低。</p>
<p>增多的字段在图标的配置上是简单了，但是在跨组件参数传递、桌面图标数据存储、数据变更监听上需要增加更多冗余的设计。</p>
<p>一番思考无果后，恰好受到 HomeAssistant 图标配置的启发，发现可以借助同一个字段实现不同类型图标的内容配置。</p>
<p>为了满足前面提到的三种图标类型，小剧决定使用一个 icon 字段，替代 iconType、iconValue。并设计了以下图标字符规则：</p>
<ul>
<li><strong>'crab'</strong>：意为图标是自动抓取对应链接的 favicon</li>
<li><strong>'mdi:xxx'</strong>：意为使用通用的 <a href="https://material.iconhelper.cn/">material design icon</a> </li>
<li><strong>'text:xxx'</strong>：这个很简单，也是最常用的图标类型，显示定义的文本作为图标</li>
</ul>
<h3 id="12">1.2、交互怎么设计 ？</h3>
<p>因为图标有三种类型，很自然的会想到使用下拉框实现类型切换。其中 mdi 图标、文本图标需要用户补充输入内容，因此还需要一个输入框。</p>
<p>虽然 mdi 图标已经很流行了，还是可能有人不知道它是什么，或者临时想找图标不知道去哪里找图标代码。因此需要提供一个在线的链接，方便随时查找自己需要的图标。</p>
<p>初步统计，图标编辑组件至少需要下拉框、输入框、链接，这三个元素才能满足需求。</p>
<p><strong>第一版交互稿</strong></p>
<p><img src="https://static.bh-lay.com/blog/2022-icon-editor/design-1.jpg" alt="第一版交互稿" /></p>
<p>通过第一版交互稿，已经可以满足图标配置的基本需求了，然而三个元素平铺的交互不够简洁，视觉上也较为零散。</p>
<p>而且在选择抓取图标的类型时，输入框需要隐藏或禁用，UI 界面抖动较为明显。</p>
<p>再加上 mdi 图标的说明链接，如果常驻的话有歧义且影响观感，切换时动态显示隐藏又会加剧 UI 的抖动。</p>
<p>基于以上考量，小剧又设计了第二版图标编辑组件。</p>
<p>为了解决视觉上的抖动问题，同时强化编辑组件整体性，小剧把三个元素拉平到了同一条水平线上。</p>
<p>最左侧是与编辑组件融为一体的下拉框，经过统计常用网站提升配置效率，将三类图标的顺序重新调整。</p>
<p>选择抓取图标类型时，因为不需要用户输入，替代显示“尝试自动抓取图标”提示文字。在保证视觉稳定的前提下给用户提示说明。</p>
<p>于是有了下面的交互设计稿。</p>
<p><img src="https://static.bh-lay.com/blog/2022-icon-editor/design-2.jpg" alt="第二版交互稿" /></p>
<h2 id="-1">二、怎样开发图标编辑组件 ？</h2>
<p>因为是编辑组件，整个模块需要能够接收传递进来的图标值，而且在编辑过程中，需要实时将变更后的图标值传回父组件。</p>
<p>所以编辑组件需要能够在父级和自身之间，相互传递图标配置数据。</p>
<h3 id="21vue">2.1、问题：Vue 是单向数据流</h3>
<p>经常使用 Vue 开发的小伙伴应该知道，Vue 的数据模型是单向数据流。简而言之就是父级可以通过 props 改变子组件内的数据，反之则不行。</p>
<p>Vue 官方文档中是这样解释的：</p>
<blockquote>
  <p>所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定：父级 prop 的更新会向下流动到子组件中，但是反过来则不行。这样会防止从子组件意外改变父级组件的状态，从而导致你的应用的数据流向难以理解。</p>
</blockquote>
<p>单纯为了实现图标编辑组件其实不难。编辑组件内部定义一个 <code>prop</code>，用来接收数据，在编辑后使用 <code>$emit</code> 通知父级触发更改。</p>
<p>相对应的，父级在引用编辑组件的时候传递参数，再定义事件监听用于接收数据变动，实现数据的双向流动。</p>
<p>需要这么麻烦么？</p>
<h3 id="22vmodel">2.2、v-model 的妙用</h3>
<p>可能部分眼尖的小伙伴已经发现，在一些自定义组件中，支持使用 <code>.sync</code> 的修饰符来方式实现双向绑定。</p>
<p>在 <code>input</code>、 <code>textarea</code> 之类的原生组件，借助于 <code>v-model</code> 也可以实现双向绑定。</p>
<p>如果你看过 Vue 的源码，或者写过复杂的 <code>render</code> 渲染函数可能会明白，<code>v-model</code> 和 <code>.sync</code> 修饰符都只是语法糖。本质上还是前面提到的 <code>prop</code> 加 <code>$emit</code> 组合实现的。</p>
<p>如果有学习过 Vue3 的话，会发现 <code>v-model</code> 和 <code>.sync</code> 修饰符已经被统一成了同一个 API。</p>
<p>本质上虽然相同，使用上的差异还是很明显的， <code>v-model</code> 是那个相对不麻烦的数据传递方式。</p>
<p>最终促使小剧使用 <code>v-model</code> 实现图标编辑组件的数据通信，有以下三点原因：</p>
<p>1、因为是编辑组件，<code>v-model</code> 的设计可以与 UI 组件定义区分开</p>
<p>2、在语法糖的加持下，作为图标编辑组件的使用方，使用起来比 <code>prop</code>、<code>$emit</code> 更简洁</p>
<p>3、图标编辑组件需要内嵌到表单中，<code>v-model</code>  更容易与传统表单组件、校验组件相结合</p>
<h3 id="23">2.3、模块输入输出设计</h3>
<p>为了将复杂的逻辑抽象到模块内部，便于使用方整合，达到<strong>高内聚低耦合</strong>的效果。需要将模块数据层设计梳理好。</p>
<p>在图标编辑组件字符结构设计部分，小剧将图标的类型和值合并为同一个字符串，因此整个模块的输入输出仅为单一参数。</p>
<p>为了使用方能够达到的易用效果，借助于 v-model 即可实现这一点。</p>
<p>至于交互设计里提到的下拉框、输入框之类的 UI 和交互，以及图标配置字符的拆分与合并，则全部需要吸纳到图标编辑组件内部。</p>
<p>下面的图标编辑组件源码分析部分，会详细分的解释这一点。</p>
<h2 id="-2">三、图标编辑组件源码分析</h2>
<p>图标编辑组件的完整版，可以参考托管在 Github 上的源代码，下面就从几个关键环节描述一下实现过程。</p>
<p>图标编辑组件源码：<a href="https://github.com/bh-lay/lays-workbench/blob/master/src/components/add-bookmark/icon-editor.vue">https://github.com/bh-lay/…</a></p>
<p>接下来展示的代码全部基于 Vue3 的版本开发，使用 ts 编写。碎片化的代码并不能直接运行，感兴趣的话可以将整个项目 clone 下来。</p>
<h3 id="31">3.1、图标类型定义</h3>
<p>为了保证编码中的严谨，需要借助 TypeScript 校验图标内容的合法性。因为这种细粒度的类型定义是第一次尝试，研究之后发现竟然是可行的。</p>
<p>通过使用以下方式，即可定义图标类型。</p>
<pre><code class="javascript language-javascript">// 书签图标类型
type BookmarkIconCrab = 'crab'
type BookmarkIconMdi = `mdi:${string}`
type BookmarkIconText = `text:${string}`

export type BookmarkIcon = BookmarkIconCrab | BookmarkIconMdi | BookmarkIconText
</code></pre>
<h3 id="32">3.2、图标编辑组件数据转换</h3>
<p>为了便于模块使用者操作数据，需要提供干净的裸字符串给父级。</p>
<p>同时又为了编辑组件内部不同的状态支持，便于用户编辑操作，需要将字符拆分为图标类型、用户编辑值。</p>
<p>基于上述的原因，第一步工作就是需要开发数据转换逻辑。</p>
<p><img src="https://static.bh-lay.com/blog/2022-icon-editor/data-transfer.jpg" alt="图标编辑组件数据转换逻辑" /></p>
<pre><code class="javascript language-javascript">function decodeModelValue(iconConfig: BookmarkIcon) {
  const iconSplit = (iconConfig || '').split(':')
  if (iconSplit[0] === 'mdi') {
    return ['mdi', 'mdi-' + iconSplit[1]]
  } else if (iconConfig === 'crab') {
    return ['crab', '']
  }
  return ['text', iconSplit[1]]
}
function encodeModelValue(newIconType: string, inputValue: string): BookmarkIcon {
  if (newIconType === 'mdi') {
    const iconName = (inputValue || '').replace(/^mdi-/, '')
    // 经历过 replace 前后，值若不相等，则认为是合法的 mdi 配置
    if (inputValue !== iconName) {
      return `mdi:${iconName}`
    } else {
      // 否则，更正数据
      return 'mdi:'
    }
  } else if (newIconType === 'crab') {
    return 'crab'
  }
  return `text:${inputValue || ''}`
}
</code></pre>
<h3 id="33">3.3、应用图标数据到视图</h3>
<p>前面的图标编辑组件数据转换只是内存过程中的格式互换，用户看到的则是更直观的 UI 视图。</p>
<p>所以在数据流转过程中，视图的驱动更为重要。</p>
<pre><code class="javascript language-javascript">// 从应用图标数据，更新编辑器视图
function applyBookmarkIcon(bookmarkIcon: BookmarkIcon) {
  // 从图标数据，获取图标类型，用户输入值
  const [ newIconType, newInputValue ] = decodeModelValue(bookmarkIcon)
  // 当图标类型和用户输入值相同时，不处理
  if (newIconType === iconType.value &amp;&amp; newInputValue === inputValue.value) {
    return
  }
  // 更新数据
  iconType.value = newIconType
  inputValue.value = newInputValue
  iconTypeLabel.value = getIconTypeConfig(newIconType).label
}
</code></pre>
<h3 id="34">3.4、父级数据监听</h3>
<p>由于图标编辑组件特性所致，父级元素随时可能因为初始化、数据重置、条件关联等原因修改数据，故而子组件需要实时监听父级元素的参数变化。</p>
<p>这里为了避免父组件数据变化引起 UI 变动，导致被误判为用户操作，在应用图标数据前解除用户操作的监听，完成后再重新绑定。</p>
<pre><code class="javascript language-javascript">// 监听父组件传入的数据变动
watch(
  () =&gt; props.modelValue,
  (newValue: BookmarkIcon) =&gt; {
    // 解除用户操作监听
    unWatchUserIntractive()
    // 更新编辑器数据
    applyBookmarkIcon(newValue)
    // 创建用户操作监听
    watchUserIntractive()
  },
  {
    immediate: true,
  }
)
</code></pre>
<h3 id="35">3.5、用户交互监听</h3>
<p>用户交互监听其实只有两个，一个是图标类型切换事件，另一个是用户输入行为。</p>
<p>当用户交互发生后，需要适当的做默认值填充、常见错误修正工作，然后应用到视图上，最后通知父级元素触发数据修改。</p>
<p>可能你会有疑问：这里的数据应用、变动监听，与父级传入参数的监听之间，不会形成死循环么？</p>
<p>其实最开始的确有，由于 Vue 在设置前后同值时，会静默此行为，不触发数据变动事件。因而死循环在执行数次后会自然停止。</p>
<p>彻底解决循环回路问题是通过以下两个手段。一是父级数据变动后，用户交互的监听增加了先解除再重设的操作。二是增加 <code>applyBookmarkIcon</code>  内的同值检测逻辑。</p>
<pre><code class="javascript language-javascript">// 监听用户交互操作行为
function watchUserIntractive() {
  // 监听图标类型变化
  const unWatchIconType = watch(iconType, newIconType =&gt; {
    // 获取默认填充值
    let defaultInputValue = getIconTypeConfig(newIconType).default
    let newModelValue = encodeModelValue(newIconType, defaultInputValue)
    applyBookmarkIcon(newModelValue)
    context.emit('update:modelValue', newModelValue)
  })
  // 监听用户输入
  const unWatchInput = watch(inputValue, newInputValue =&gt; {
    // 避免粘贴时出现重复的 mdi-，尝试去删除
    if (newInputValue.match(/^mdi-mdi-/)) {
      newInputValue = newInputValue.replace(/^mdi-/, '')
    }
    const newModelValue = encodeModelValue(iconType.value, newInputValue)
    applyBookmarkIcon(newModelValue)
    context.emit('update:modelValue', newModelValue)
  })
  // 标记解除监听回调
  unWatchUserIntractive = () =&gt; {
    unWatchIconType()
    unWatchInput()
  }
}
// 解除用户交互操作行为监听
let unWatchUserIntractive = () =&gt; {
  // nothind
}
</code></pre>
<p>以上就是图标编辑组件核心逻辑。</p>
<p>可能你会发现 Vue3 的代码相较于 Vue2 中的 Options 形式的组织起来更为灵活。其实Vue2 里也有很多类似于 <code>this.$watch</code>、<code>this.$on('hook:beforeDestroy'</code> API，也能够实现灵活的逻辑组织。</p>
<p>只是它们依旧脱离不了 ViewModel 的上下文，并且没有像 Vue3 中一样，抽象出 <code>setup</code> 完成模块的组织。</p>
<h2 id="-3">四、为什么会写这篇文章</h2>
<p>经过对一段时间面试情况的总结，发现近四成的同学对 <code>v-model</code> 的原理并不清楚，或者是理解其中的原理，但是没有实践经历。</p>
<p>这可能是对方基础知识不均衡的原因，也可能是我们的吸引力不够导致。</p>
<p>基于这个情况，原本计划写一篇关于 <code>v-model</code>、 <code>.sync</code> 实现双向通信的原理，Vue3 升级后的异同点，以及项目中常见的实例。</p>
<p>整理素材时发现，关于前两点，Vue2、Vue3 的文档里已经说的足够清楚，无需赘述。具体可以参考以下链接。</p>
<p>[<a href="https://cn.vuejs.org/v2/guide/components-custom-events.html#自定义组件的-v-model">Vue2] 自定义组件的 v-model</a></p>
<p>[<a href="https://cn.vuejs.org/v2/guide/components-custom-events.html#sync-修饰符">Vue2] .sync 修饰符</a></p>
<p>[<a href="https://v3.cn.vuejs.org/guide/migration/v-model.html">Vue3] v-model</a></p>
<p>至于项目中的实例，文档里的代码也已经足够与项目经历产生联想，并没有多大必要去做项目罗列。</p>
<p>倒不如拿自己项目中的一个小例子，从设计到开发理一遍，可能更有用。</p>
<p>最后，再次建议你打开 <a href="http://e.bh-lay.com/">小剧起始页</a>，体验一番。</p>]]></content:encoded>
    </item>
    <item>
      <title>小剧的2021</title>
      <link>https://bh-lay.com/blog/8hfd4n48pd</link>
      <description><![CDATA[<p><img src="https://static.bh-lay.com/blog/2021-summary/code-and-design.jpg" alt="奋笔疾书" /></p>
<p>在新冠肆虐全球的第二个年头，在疫情防控成效卓著的中国，在相对风平浪静的安徽省。小剧作为身处时代洪流中的一粒砂石，能够平安顺利的度过这一年实属幸运。</p>
<p>站在 2022 年的开端回看小剧的 2021，混乱的底色随着时间的推进，逐渐显露出清晰的轮廓。</p>
<p>下面小剧就从<strong>工作、生活、兴趣</strong>三个方面，聊聊小剧这一年来的收获。</p>
<h2 id="">一、关于工作</h2>
<p>2021 年是小剧加入讯飞消费者 BG 商业产品部的第二年。经过 2020 年对 PC 客户端混合开发的探索，2021 年的工作更加得心应手，对产品与客户端的特性也有了更为深入的理解。</p>
<p>这里简单介绍一下小剧负责的两款产品：讯飞文档、讯飞语记。</p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/yuji-wendang.jpg" alt="语记、文档PC客户端截图" /></p>
<h3 id="11">1.1、讯飞文档</h3>
<p><a href="https://iflydocs.com/i/#/folder/102A1">https://iflydocs.com/</a></p>
<blockquote>
  <p>讯飞文档是一款支持多人多端同时编辑的在线文档 App。</p>
</blockquote>
<p>虽说在 5G 时代下网络基础设施已经非常稳定，但是协作模式下离线、弱网的协同处理，依旧是我们需要研究的方向。</p>
<p>这一年我和团队其他小伙伴一起，在协作稳定性上花了很多功夫，也取得了很显著的成效。</p>
<p>为了扩展文档的应用场景，2021 年我和团队小伙伴为会议场景开发了速记文档，为会议记录提供了比较好的支持。</p>
<p>这里额外提一句，讯飞文档在 2021 年有另一个大型功能上线。在团队的努力下，表格文档顺利发布。为周报汇总、问卷搜集、数据统计等场景提供了较好的协作工具。</p>
<h3 id="12">1.2、讯飞语记</h3>
<p><a href="https://iflynote.com/i/">https://iflynote.com/</a></p>
<p>相较于讯飞文档，讯飞语记更偏个人一点。在弱化的协作概念、偏薄的目录结构等方面，都能体现出较强的个人属性。</p>
<p>因为语记较重的历史包袱，2021 年小剧很大一部分工作花在了讯飞语记 PC、WEB 的重构上。</p>
<p>经过内部一次较长的专项行动，我们成功地将讯飞语记 PC、WEB 端做了重构。最早我们尝试将底层结构打散，模块拆装重组。这个阶段，我们感同身受地体验到了前人，在前端底层设计不太合理框架上迭代的痛苦。</p>
<p>经过对开发时的调试环境搭建，运行时的事件系统梳理。重构后的项目不仅开发效率大幅提升，交互体验也得到了质的飞跃。</p>
<p>终结了语记 WEB 端较长时间的停滞迭代局面，也让 PC 端更多的需求落地提供了可能。</p>
<h2 id="-1">二、关于生活</h2>
<p>为了响应疫情防控要求，同时也受限于不太富裕的钱包。2021 少了跨省出游，周末假期基本上就是围着合肥转悠。</p>
<p>多出来的时间用来陪伴家人和尝试新鲜玩意儿，也算有趣。</p>
<h3 id="21">2.1、迎接新身份</h3>
<p>2021 年对小剧来说是意义非凡的一年，在下半年的波折中，成功解锁人生的新身份。</p>
<p>希望接下来的这趟旅程，家人们都能平安顺遂。</p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/hui-and-ju.jpg" alt="汉服合照" /></p>
<h3 id="22">2.2、家庭网络改造</h3>
<p>因为想尝试智能家居，提前做了很多功课，了解到网络的可靠性相当重要，它是智能家居稳定性的关键保障。</p>
<p>复习了网络基础知识之后，再回过头来看家里的网络，发现了很多设计不合理的地方。</p>
<h4 id="221">2.2.1、不合理一：光猫和路由器有回路</h4>
<p>虽然现在的路由器和光猫很智能，能处理数据包在网络中循环震荡的问题。但是数倍的内网消耗成了家里网络不稳定的最大因素。</p>
<p>因此必须解除网络回路。</p>
<h4 id="222">2.2.2、不合理二：原拨号任务由光猫承担</h4>
<p>熟悉家庭网络的小伙伴应该知道，运营商提供的光猫性能一般。如果在其最核心的光电转换任务之外，再承担拨号、NAT 转换等路由任务，势必会加重其负担。</p>
<p>一般情况下光猫能够负担得起这些任务，但是放着更加专业、性能更强的路由器不用实在是浪费。</p>
<p>本着均衡各设备任务，减少网络层级的考虑，最终决定将拨号任务转移到路由器，光猫只单纯的处理光电转换。</p>
<h4 id="223ip">2.2.3、不合理三：缺少公网 IP 的支持</h4>
<p>学习过网络基础知识的同学应该知道，每一层路由结构都会经历一次 NAT 转换，较深的网络会造成一定的网络延迟。</p>
<p>虽然经过前面的拨号转移已经减少了一级网络层级，但经过检查发现，家里的入网 IP 依旧是电信的内网 IP。</p>
<p>经过和运营商的交涉，最终得到了入户公网 IP 的支持。</p>
<blockquote>
  <p>提示：这里公网 IP 是动态的，每隔一段时间，每次重启设备可能都会更新，想用它搭设稳定的服务是不靠谱的，也有法律风险。</p>
</blockquote>
<p><strong>修正后的家庭网络拓扑图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/home-network.jpg" alt="家庭网络拓扑图" /></p>
<h3 id="23">2.3、照片存储方案探索</h3>
<p>仔细看过上面的拓扑图你可能会有疑问，那个 H3C M1 存储盒子是什么？</p>
<p>先听我说段故事吧 ！</p>
<p>随着智能手机的发展，不管你喜不喜欢拍照，各种记录生活的照片、视频都会把手机存储塞的满满的。</p>
<p>于是我们养成了手机快满的时候清理照片的习惯。但是日积月累总会有很多不愿意删的照片装满手机。</p>
<p>iCloud 和各种照片存储服务的免费容量有限，按月付费又比较肉疼。网盘容量大，可沙漏般的网速终究不是好的体验。</p>
<p>尝试了这么多，再进一步研究可能会发现，有一种东西叫 Nas，看起来它是家庭存储的终极解决方案。详细了解之后你会了解到，品牌 Nas 的价格会让你却步。廉价 Nas 的稳定性、耗电量、噪音都会让你头皮发麻。</p>
<p>当然品牌 Nas 也有耗电量、噪音等问题，如果你的房子足够大，这倒不是问题。</p>
<p>经过综合考虑（主要是钱），最终在某鱼购入了 H3C M1 这款家庭存储盒子。不是专业 Nas 设备，但是安静的体验、低功耗、手机相册自动备份功能、以及 SMB 文件服务的支持，都满足了我对照片存储最基本的要求。</p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/h3c-m1.jpg" alt="H3C M1" /></p>
<p>可能你会问，内网速度怎么样 ？怎么保障数据安全？</p>
<p>因为是机械单硬盘，内网上限为 70MB/s，表现一般，但够用。</p>
<p>同样是因为单硬盘，再加上 7 * 24 小时开机，硬盘挂掉的风险还是相当高的。关于我是如何实现数据备份的，如果你感兴趣后面单独再说。</p>
<h3 id="24">2.4、智能家居尝试</h3>
<p>提到智能家居，想到的画面就是喊一声小爱或者天猫精灵，让它开灯关灯。看起来并不能显著改善居住体验。</p>
<p>单看某一项的实施，都只是锦上添花的小玩具。只有微小的改动累积起来之后，引起的量变才会让生活变得更加舒适。</p>
<p>举几个小剧已经在家里实施过的例子吧。</p>
<p>出门扔个垃圾忘带钥匙，被反锁在门外的尴尬可以借助指纹密码锁消除。有朋友临时到访，或者家里没人又恰巧大件快递送上门，一次性密码可以安全的解决这些应急问题。</p>
<p>加班很晚回到家，打开大门的同时打开走廊灯，避免一身疲惫还要摸黑开灯。</p>
<p>像厨房、卫生间这类功能性很强的房间。经常因为拿取物品而懒得开灯，或者离开太久忘记关灯。借助于人体传感器，人来自动开灯，人走一段时间后自动关灯。可以大大减少是否需要开灯的纠结，和离开后是否需要关灯的焦虑。</p>
<p>尤其是现在冬天，临睡前关灯是件痛苦的事。喊一声小爱或者拿出手机就能开关灯和空调，可以大幅促进家庭和谐。</p>
<p>这些都是小剧在 2021 年的一些小尝试，半年体验下来还是挺方便的。</p>
<p><strong>米家APP &amp; HomeAssistant</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/smart-home.jpg" alt="智能家居改造" /></p>
<h3 id="252021">2.5、2021 年都去哪儿嗨了 ？</h3>
<p>2021 年国内的疫情得到了较为平稳的控制，然而出行依旧不能随心所欲。</p>
<p>除了四月跑去上海浪了一圈，其他周末的消遣全都在合肥附近开展。</p>
<p>探索了合肥周边一些【照骗】景点，也发现了很多不一样的角落。</p>
<p><strong>上海野生动物园，开屏的孔雀</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/peacock.jpg" alt="上海野生动物园，开屏的孔雀" /></p>
<p><strong>上海野生动物园，像极了《JavaScript权威指南》封面的犀牛</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/rhinoceros.jpg" alt="上海野生动物园，前端犀牛书封面" /></p>
<p><strong>迪士尼乐园海盗船</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/disney-park.jpg" alt="迪士尼乐园海盗船﻿" />﻿</p>
<p><strong>合肥照骗榜一：浮槎山</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/fuchashan.jpg" alt="合肥照骗榜一：浮槎山" /></p>
<p><strong>合肥照骗榜二：红石咀</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/hongshizui.jpg" alt="合肥照骗榜二：红石咀" /></p>
<p><strong>老年人的噩梦 - 融创茂过山车</strong>  <a href="https://m.okjike.com/originalPosts/609797723a749b0018bc52b3?s=ewoidSI6ICI1ZjBlOWE1ODhlZDE2YzAwMTczMGU1MGEiCn0=">恐怖视频，慎点 </a></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/roller-coaster.jpg" alt="剧中人坐过山车" /></p>
<h2 id="-2">三、关于个人兴趣</h2>
<h3 id="31">3.1、开发小剧起始页</h3>
<p><a href="http://e.bh-lay.com/">http://e.bh-lay.com</a> （墙裂推荐您点击一下）</p>
<p>在 2020 年终总结的时候，发现个人兴趣被工作挤压严重。2021 年紧张的工作使得兴趣爱好施展的空间被进一步压缩。</p>
<p>终于在下半年，决定作出适当的改变。</p>
<blockquote>
  <p>最近一年多一直忙于公司的业务开发。大量的业余精力也都投入在了讯飞语记、讯飞文档的功能迭代上。</p>
  <p><strong>摘自</strong>：<a href="http://bh-lay.com/blog/2o7r91ml5hg">【开发回顾】小剧起始页</a></p>
</blockquote>
<p>因为时间是最宝贵的资源，虽然决定适当做出改变，但是在做选择的时候还是会考虑性价比的。</p>
<p>最终在摄影和学习之间，选择了计划性的实施学习，而不刻意对摄影进行投入。</p>
<p>为了尽可能的用最少的时间尝试更多的技能点，小剧通过开发<a href="http://e.bh-lay.com/"> 小剧起始页 </a>项目，来学习 <strong>Vue3、Vite、IndexedDB、Grid 布局、TypeScript</strong> 等前端知识。</p>
<p>关于这段经历就不展开聊了，感兴趣的话可以阅读上面提到的 <a href="http://bh-lay.com/blog/2o7r91ml5hg">开发回顾</a> 。</p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/lays-start-page.jpg" alt="小剧起始页截图" /></p>
<h3 id="32">3.2、有关摄影</h3>
<p>2021 年小剧几乎没有计划过任何个人拍摄活动，这在近几年里是非常少见的。</p>
<p>虽然很离谱，不过作为一个已经年过 30 的中年程序员来说，也不难理解。</p>
<p>在已经到来的 2022 年，可能会多出去拍拍远方的山水或身边的景色，也可能彻底让相机、无人机吃灰。这一点不做任何保证，也不做任何年度计划。</p>
<p>个人兴趣嘛，能让自己高兴，能给生活带来情趣，这才是最重要的。</p>
<p>不能被一点点小兴趣绑架，那样得不偿失。</p>
<h4 id="321">3.2.1、捡垃圾的长焦大炮</h4>
<p>出于好奇，年初在某鱼上低价淘了个好玩的东西。</p>
<p>佳能 420-800mm 长焦镜头，成像锐度非常差，而且是手动对焦，只能说是个看起来很唬人的大玩具。</p>
<p>转接到我的索尼 A6000 上，能够得到 1200mm 夸张的焦距。</p>
<p>虽然能拍月亮，但较差的锐度，以及拍摄当天比较随意，能见度较差。出来的成片只能勉强看看环形山。</p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/shoot-the-moon.jpg" alt="拍月亮" /></p>
<h4 id="322">3.2.2、和自己的照片合影</h4>
<p>如果你看过小剧 2019、2020 的年终总结，可能你对小剧拍摄的讯飞为主题的照片会有印象。</p>
<p>在此之前，小剧只会在一些线上媒体平台、讯飞内部见到自己的作品。2021 年则不太一样，小剧逐渐在生活的各个场景中，也会发现自己的照片。</p>
<p>于是今年最有意思的事情就是，在一些公共场合，和自己拍摄的照片合影留念。</p>
<p>这份照片在线上的使用也更加广泛，央视新闻、人民网、合肥晚报、合肥高新发布、网易新闻等媒体都曾在 2021 年引用过。</p>
<p><strong>蜀山某公交站牌</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/my-pic-bus-stop.jpg" alt="蜀山某公交站牌" /></p>
<p><strong>某互联网展会</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/my-pic-in-exhibition.jpg" alt="某互联网展会" /></p>
<p><strong>高新区某地铁站</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/my-pic-in-metro.jpg" alt="高新区某地铁站" /></p>
<h4 id="323">3.2.3、紫蓬山全景拍摄</h4>
<p>没记错的话，这应该是小剧 2021 年拍摄的唯一一份全景作品。五月份去紫蓬山玩的时候随便拍的，并没有做任何提前规划。</p>
<p>紫蓬山的景色不算出众、人文历史也不算浓厚，但 ta 对合肥这座城市有着不一样的意义。作为山脉的起点，与城区平原相比多了份别样的情趣，也算是为数不多的近郊踏青好去处。</p>
<p>这里拍摄了两处佛教气息比较浓厚的场景，如果你还没有来过合肥，可以点开下面的链接感受一下。</p>
<p><a href="https://720yun.com/t/78vkt9ibdfb">【查看全景】</a></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/hefei-zipengshan.jpg" alt="合肥紫蓬山" /></p>
<hr />
<hr />
<h2 id="2021">四、2021 年最大的收获</h2>
<p>2021 年已经过去了，各种经历都是隐性的。其中有一条收获最为明显，那就是：</p>
<p><strong>体重</strong></p>
<p>2021 年，小剧以肉眼可见的速度从 145 涨到了 160 斤，创造了个人体重的新纪录。</p>
<p>为了庆祝这一成就，小剧还得到了一份蛋糕作为奖励。</p>
<p>希望在 2022 的年末，迎来我减肥成功的好消息。</p>
<p>2021 年就这样啦，再见！</p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/cake-for-my-weight.jpg" alt="庆祝体重的蛋糕" /></p>]]></description>
      <author>mail@bh-lay.com (剧中人)</author>
      <pubDate>Tue, 18 Jan 2022 13:27:35 +0000</pubDate>
      <guid isPermaLink="true">https://bh-lay.com/blog/8hfd4n48pd</guid>
      <category>年终总结</category>
      <category>2021</category>
      <category>生活</category>
      <enclosure url="http://static.bh-lay.com/blog/2021-summary/code-and-design.jpg" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p><img src="https://static.bh-lay.com/blog/2021-summary/code-and-design.jpg" alt="奋笔疾书" /></p>
<p>在新冠肆虐全球的第二个年头，在疫情防控成效卓著的中国，在相对风平浪静的安徽省。小剧作为身处时代洪流中的一粒砂石，能够平安顺利的度过这一年实属幸运。</p>
<p>站在 2022 年的开端回看小剧的 2021，混乱的底色随着时间的推进，逐渐显露出清晰的轮廓。</p>
<p>下面小剧就从<strong>工作、生活、兴趣</strong>三个方面，聊聊小剧这一年来的收获。</p>
<h2 id="">一、关于工作</h2>
<p>2021 年是小剧加入讯飞消费者 BG 商业产品部的第二年。经过 2020 年对 PC 客户端混合开发的探索，2021 年的工作更加得心应手，对产品与客户端的特性也有了更为深入的理解。</p>
<p>这里简单介绍一下小剧负责的两款产品：讯飞文档、讯飞语记。</p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/yuji-wendang.jpg" alt="语记、文档PC客户端截图" /></p>
<h3 id="11">1.1、讯飞文档</h3>
<p><a href="https://iflydocs.com/i/#/folder/102A1">https://iflydocs.com/</a></p>
<blockquote>
  <p>讯飞文档是一款支持多人多端同时编辑的在线文档 App。</p>
</blockquote>
<p>虽说在 5G 时代下网络基础设施已经非常稳定，但是协作模式下离线、弱网的协同处理，依旧是我们需要研究的方向。</p>
<p>这一年我和团队其他小伙伴一起，在协作稳定性上花了很多功夫，也取得了很显著的成效。</p>
<p>为了扩展文档的应用场景，2021 年我和团队小伙伴为会议场景开发了速记文档，为会议记录提供了比较好的支持。</p>
<p>这里额外提一句，讯飞文档在 2021 年有另一个大型功能上线。在团队的努力下，表格文档顺利发布。为周报汇总、问卷搜集、数据统计等场景提供了较好的协作工具。</p>
<h3 id="12">1.2、讯飞语记</h3>
<p><a href="https://iflynote.com/i/">https://iflynote.com/</a></p>
<p>相较于讯飞文档，讯飞语记更偏个人一点。在弱化的协作概念、偏薄的目录结构等方面，都能体现出较强的个人属性。</p>
<p>因为语记较重的历史包袱，2021 年小剧很大一部分工作花在了讯飞语记 PC、WEB 的重构上。</p>
<p>经过内部一次较长的专项行动，我们成功地将讯飞语记 PC、WEB 端做了重构。最早我们尝试将底层结构打散，模块拆装重组。这个阶段，我们感同身受地体验到了前人，在前端底层设计不太合理框架上迭代的痛苦。</p>
<p>经过对开发时的调试环境搭建，运行时的事件系统梳理。重构后的项目不仅开发效率大幅提升，交互体验也得到了质的飞跃。</p>
<p>终结了语记 WEB 端较长时间的停滞迭代局面，也让 PC 端更多的需求落地提供了可能。</p>
<h2 id="-1">二、关于生活</h2>
<p>为了响应疫情防控要求，同时也受限于不太富裕的钱包。2021 少了跨省出游，周末假期基本上就是围着合肥转悠。</p>
<p>多出来的时间用来陪伴家人和尝试新鲜玩意儿，也算有趣。</p>
<h3 id="21">2.1、迎接新身份</h3>
<p>2021 年对小剧来说是意义非凡的一年，在下半年的波折中，成功解锁人生的新身份。</p>
<p>希望接下来的这趟旅程，家人们都能平安顺遂。</p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/hui-and-ju.jpg" alt="汉服合照" /></p>
<h3 id="22">2.2、家庭网络改造</h3>
<p>因为想尝试智能家居，提前做了很多功课，了解到网络的可靠性相当重要，它是智能家居稳定性的关键保障。</p>
<p>复习了网络基础知识之后，再回过头来看家里的网络，发现了很多设计不合理的地方。</p>
<h4 id="221">2.2.1、不合理一：光猫和路由器有回路</h4>
<p>虽然现在的路由器和光猫很智能，能处理数据包在网络中循环震荡的问题。但是数倍的内网消耗成了家里网络不稳定的最大因素。</p>
<p>因此必须解除网络回路。</p>
<h4 id="222">2.2.2、不合理二：原拨号任务由光猫承担</h4>
<p>熟悉家庭网络的小伙伴应该知道，运营商提供的光猫性能一般。如果在其最核心的光电转换任务之外，再承担拨号、NAT 转换等路由任务，势必会加重其负担。</p>
<p>一般情况下光猫能够负担得起这些任务，但是放着更加专业、性能更强的路由器不用实在是浪费。</p>
<p>本着均衡各设备任务，减少网络层级的考虑，最终决定将拨号任务转移到路由器，光猫只单纯的处理光电转换。</p>
<h4 id="223ip">2.2.3、不合理三：缺少公网 IP 的支持</h4>
<p>学习过网络基础知识的同学应该知道，每一层路由结构都会经历一次 NAT 转换，较深的网络会造成一定的网络延迟。</p>
<p>虽然经过前面的拨号转移已经减少了一级网络层级，但经过检查发现，家里的入网 IP 依旧是电信的内网 IP。</p>
<p>经过和运营商的交涉，最终得到了入户公网 IP 的支持。</p>
<blockquote>
  <p>提示：这里公网 IP 是动态的，每隔一段时间，每次重启设备可能都会更新，想用它搭设稳定的服务是不靠谱的，也有法律风险。</p>
</blockquote>
<p><strong>修正后的家庭网络拓扑图</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/home-network.jpg" alt="家庭网络拓扑图" /></p>
<h3 id="23">2.3、照片存储方案探索</h3>
<p>仔细看过上面的拓扑图你可能会有疑问，那个 H3C M1 存储盒子是什么？</p>
<p>先听我说段故事吧 ！</p>
<p>随着智能手机的发展，不管你喜不喜欢拍照，各种记录生活的照片、视频都会把手机存储塞的满满的。</p>
<p>于是我们养成了手机快满的时候清理照片的习惯。但是日积月累总会有很多不愿意删的照片装满手机。</p>
<p>iCloud 和各种照片存储服务的免费容量有限，按月付费又比较肉疼。网盘容量大，可沙漏般的网速终究不是好的体验。</p>
<p>尝试了这么多，再进一步研究可能会发现，有一种东西叫 Nas，看起来它是家庭存储的终极解决方案。详细了解之后你会了解到，品牌 Nas 的价格会让你却步。廉价 Nas 的稳定性、耗电量、噪音都会让你头皮发麻。</p>
<p>当然品牌 Nas 也有耗电量、噪音等问题，如果你的房子足够大，这倒不是问题。</p>
<p>经过综合考虑（主要是钱），最终在某鱼购入了 H3C M1 这款家庭存储盒子。不是专业 Nas 设备，但是安静的体验、低功耗、手机相册自动备份功能、以及 SMB 文件服务的支持，都满足了我对照片存储最基本的要求。</p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/h3c-m1.jpg" alt="H3C M1" /></p>
<p>可能你会问，内网速度怎么样 ？怎么保障数据安全？</p>
<p>因为是机械单硬盘，内网上限为 70MB/s，表现一般，但够用。</p>
<p>同样是因为单硬盘，再加上 7 * 24 小时开机，硬盘挂掉的风险还是相当高的。关于我是如何实现数据备份的，如果你感兴趣后面单独再说。</p>
<h3 id="24">2.4、智能家居尝试</h3>
<p>提到智能家居，想到的画面就是喊一声小爱或者天猫精灵，让它开灯关灯。看起来并不能显著改善居住体验。</p>
<p>单看某一项的实施，都只是锦上添花的小玩具。只有微小的改动累积起来之后，引起的量变才会让生活变得更加舒适。</p>
<p>举几个小剧已经在家里实施过的例子吧。</p>
<p>出门扔个垃圾忘带钥匙，被反锁在门外的尴尬可以借助指纹密码锁消除。有朋友临时到访，或者家里没人又恰巧大件快递送上门，一次性密码可以安全的解决这些应急问题。</p>
<p>加班很晚回到家，打开大门的同时打开走廊灯，避免一身疲惫还要摸黑开灯。</p>
<p>像厨房、卫生间这类功能性很强的房间。经常因为拿取物品而懒得开灯，或者离开太久忘记关灯。借助于人体传感器，人来自动开灯，人走一段时间后自动关灯。可以大大减少是否需要开灯的纠结，和离开后是否需要关灯的焦虑。</p>
<p>尤其是现在冬天，临睡前关灯是件痛苦的事。喊一声小爱或者拿出手机就能开关灯和空调，可以大幅促进家庭和谐。</p>
<p>这些都是小剧在 2021 年的一些小尝试，半年体验下来还是挺方便的。</p>
<p><strong>米家APP &amp; HomeAssistant</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/smart-home.jpg" alt="智能家居改造" /></p>
<h3 id="252021">2.5、2021 年都去哪儿嗨了 ？</h3>
<p>2021 年国内的疫情得到了较为平稳的控制，然而出行依旧不能随心所欲。</p>
<p>除了四月跑去上海浪了一圈，其他周末的消遣全都在合肥附近开展。</p>
<p>探索了合肥周边一些【照骗】景点，也发现了很多不一样的角落。</p>
<p><strong>上海野生动物园，开屏的孔雀</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/peacock.jpg" alt="上海野生动物园，开屏的孔雀" /></p>
<p><strong>上海野生动物园，像极了《JavaScript权威指南》封面的犀牛</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/rhinoceros.jpg" alt="上海野生动物园，前端犀牛书封面" /></p>
<p><strong>迪士尼乐园海盗船</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/disney-park.jpg" alt="迪士尼乐园海盗船﻿" />﻿</p>
<p><strong>合肥照骗榜一：浮槎山</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/fuchashan.jpg" alt="合肥照骗榜一：浮槎山" /></p>
<p><strong>合肥照骗榜二：红石咀</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/hongshizui.jpg" alt="合肥照骗榜二：红石咀" /></p>
<p><strong>老年人的噩梦 - 融创茂过山车</strong>  <a href="https://m.okjike.com/originalPosts/609797723a749b0018bc52b3?s=ewoidSI6ICI1ZjBlOWE1ODhlZDE2YzAwMTczMGU1MGEiCn0=">恐怖视频，慎点 </a></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/roller-coaster.jpg" alt="剧中人坐过山车" /></p>
<h2 id="-2">三、关于个人兴趣</h2>
<h3 id="31">3.1、开发小剧起始页</h3>
<p><a href="http://e.bh-lay.com/">http://e.bh-lay.com</a> （墙裂推荐您点击一下）</p>
<p>在 2020 年终总结的时候，发现个人兴趣被工作挤压严重。2021 年紧张的工作使得兴趣爱好施展的空间被进一步压缩。</p>
<p>终于在下半年，决定作出适当的改变。</p>
<blockquote>
  <p>最近一年多一直忙于公司的业务开发。大量的业余精力也都投入在了讯飞语记、讯飞文档的功能迭代上。</p>
  <p><strong>摘自</strong>：<a href="http://bh-lay.com/blog/2o7r91ml5hg">【开发回顾】小剧起始页</a></p>
</blockquote>
<p>因为时间是最宝贵的资源，虽然决定适当做出改变，但是在做选择的时候还是会考虑性价比的。</p>
<p>最终在摄影和学习之间，选择了计划性的实施学习，而不刻意对摄影进行投入。</p>
<p>为了尽可能的用最少的时间尝试更多的技能点，小剧通过开发<a href="http://e.bh-lay.com/"> 小剧起始页 </a>项目，来学习 <strong>Vue3、Vite、IndexedDB、Grid 布局、TypeScript</strong> 等前端知识。</p>
<p>关于这段经历就不展开聊了，感兴趣的话可以阅读上面提到的 <a href="http://bh-lay.com/blog/2o7r91ml5hg">开发回顾</a> 。</p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/lays-start-page.jpg" alt="小剧起始页截图" /></p>
<h3 id="32">3.2、有关摄影</h3>
<p>2021 年小剧几乎没有计划过任何个人拍摄活动，这在近几年里是非常少见的。</p>
<p>虽然很离谱，不过作为一个已经年过 30 的中年程序员来说，也不难理解。</p>
<p>在已经到来的 2022 年，可能会多出去拍拍远方的山水或身边的景色，也可能彻底让相机、无人机吃灰。这一点不做任何保证，也不做任何年度计划。</p>
<p>个人兴趣嘛，能让自己高兴，能给生活带来情趣，这才是最重要的。</p>
<p>不能被一点点小兴趣绑架，那样得不偿失。</p>
<h4 id="321">3.2.1、捡垃圾的长焦大炮</h4>
<p>出于好奇，年初在某鱼上低价淘了个好玩的东西。</p>
<p>佳能 420-800mm 长焦镜头，成像锐度非常差，而且是手动对焦，只能说是个看起来很唬人的大玩具。</p>
<p>转接到我的索尼 A6000 上，能够得到 1200mm 夸张的焦距。</p>
<p>虽然能拍月亮，但较差的锐度，以及拍摄当天比较随意，能见度较差。出来的成片只能勉强看看环形山。</p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/shoot-the-moon.jpg" alt="拍月亮" /></p>
<h4 id="322">3.2.2、和自己的照片合影</h4>
<p>如果你看过小剧 2019、2020 的年终总结，可能你对小剧拍摄的讯飞为主题的照片会有印象。</p>
<p>在此之前，小剧只会在一些线上媒体平台、讯飞内部见到自己的作品。2021 年则不太一样，小剧逐渐在生活的各个场景中，也会发现自己的照片。</p>
<p>于是今年最有意思的事情就是，在一些公共场合，和自己拍摄的照片合影留念。</p>
<p>这份照片在线上的使用也更加广泛，央视新闻、人民网、合肥晚报、合肥高新发布、网易新闻等媒体都曾在 2021 年引用过。</p>
<p><strong>蜀山某公交站牌</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/my-pic-bus-stop.jpg" alt="蜀山某公交站牌" /></p>
<p><strong>某互联网展会</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/my-pic-in-exhibition.jpg" alt="某互联网展会" /></p>
<p><strong>高新区某地铁站</strong></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/my-pic-in-metro.jpg" alt="高新区某地铁站" /></p>
<h4 id="323">3.2.3、紫蓬山全景拍摄</h4>
<p>没记错的话，这应该是小剧 2021 年拍摄的唯一一份全景作品。五月份去紫蓬山玩的时候随便拍的，并没有做任何提前规划。</p>
<p>紫蓬山的景色不算出众、人文历史也不算浓厚，但 ta 对合肥这座城市有着不一样的意义。作为山脉的起点，与城区平原相比多了份别样的情趣，也算是为数不多的近郊踏青好去处。</p>
<p>这里拍摄了两处佛教气息比较浓厚的场景，如果你还没有来过合肥，可以点开下面的链接感受一下。</p>
<p><a href="https://720yun.com/t/78vkt9ibdfb">【查看全景】</a></p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/hefei-zipengshan.jpg" alt="合肥紫蓬山" /></p>
<hr />
<hr />
<h2 id="2021">四、2021 年最大的收获</h2>
<p>2021 年已经过去了，各种经历都是隐性的。其中有一条收获最为明显，那就是：</p>
<p><strong>体重</strong></p>
<p>2021 年，小剧以肉眼可见的速度从 145 涨到了 160 斤，创造了个人体重的新纪录。</p>
<p>为了庆祝这一成就，小剧还得到了一份蛋糕作为奖励。</p>
<p>希望在 2022 的年末，迎来我减肥成功的好消息。</p>
<p>2021 年就这样啦，再见！</p>
<p><img src="https://static.bh-lay.com/blog/2021-summary/cake-for-my-weight.jpg" alt="庆祝体重的蛋糕" /></p>]]></content:encoded>
    </item>
  </channel>
</rss>