<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/rss/atom-styles.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Ethan Yang</title>
  <subtitle>Ethan Young 的个人博客，记录前端开发、工程实践、产品构建、工具配置、生活观察和持续迭代的日常思考。</subtitle>
  <link href="https://www.ethyoung.me//atom.xml" rel="self" type="application/atom+xml"/>
  <link href="https://www.ethyoung.me/" rel="alternate" type="text/html"/>
  <updated>2026-07-01T09:26:00.412Z</updated>
  <language>zh-CN</language>
  <id>https://www.ethyoung.me//</id>
  <author>
    <name>Ethan</name>
    <uri>https://www.ethyoung.me/</uri>
  </author>
  <generator uri="https://github.com/AZenking/Litos" version="5.0">Astro &lt;EY /&gt; Theme</generator>
  <rights>Copyright © 2026 Ethan</rights>
  
  <entry>
    <title>从 WebStation 到 Cloudflare Tunnel：我的群晖网络架构演进</title>
    <link href="https://www.ethyoung.me//posts/synology-docker-cloudflare-tunnel" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/synology-docker-cloudflare-tunnel</id>
    <updated>2026-05-07T00:00:00.000Z</updated>
    <published>2026-05-07T00:00:00.000Z</published>
    <author>
      <name>Ethan Yang</name>
    </author>
    <summary type="text">记录一次从 WebStation + Alias 到 Cloudflare Tunnel、群晖反向代理、Docker Compose 与 Tailscale 的家庭 Homelab 架构演进。</summary>
    <content type="html"><![CDATA[
<p>做家庭 Homelab，其实最终绕不开几个关键词：网络、反向代理、Docker 与稳定性。</p>
<p>前几年刚开始折腾群晖的时候，我的思路其实很简单：</p>
<pre><code>WebStation
+
Docker
+
Alias
</code></pre>
<p>例如：</p>
<pre><code>/epg
/jellyfin
/ai
</code></pre>
<p>当时感觉非常合理，甚至会觉得：</p>
<blockquote><p>这不就是网站部署吗？</p></blockquote>
<p>但后来服务越来越多：</p>
<ul>
<li>React / Next.js</li>
<li>Jellyfin</li>
<li>AI WebUI</li>
<li>EPG</li>
<li>Grafana</li>
<li>code-server</li>
<li>WebSocket 服务</li>
</ul>
<p>整个系统开始逐渐变得混乱。</p>
<p>最开始只是偶发页面不存在、静态资源 404、WebSocket 失败、反向代理异常。后来这些问题越来越明显，我才意识到：WebStation 更像旧时代 Web Hosting 的产物。</p>
<p>它非常适合：</p>
<ul>
<li>PHP</li>
<li>WordPress</li>
<li>Apache</li>
<li>Typecho</li>
</ul>
<p>但并不真正适合：</p>
<ul>
<li>SPA</li>
<li>WebSocket</li>
<li>Docker App Gateway</li>
<li>Timeline / Streaming</li>
<li>React Runtime</li>
</ul>
<p>尤其是 <code>Alias + 子路径</code> 这个方案。</p>
<p>现代前端其实大量默认运行在：</p>
<pre><code>/
</code></pre>
<p>而不是：</p>
<pre><code>/subpath
</code></pre>
<p>于是：</p>
<pre><code>/epg
/jellyfin
</code></pre>
<p>这种结构会天然开始和现代前端冲突。</p>
<p>我后来慢慢意识到，现代 Homelab 的核心已经不是“网站托管”，而是：</p>
<blockquote><p>服务编排 + 网络系统</p></blockquote>
<p>这其实是一个非常大的认知变化。</p>
<p>于是后来我开始逐渐迁移整个架构：</p>
<pre><code>Cloudflare Tunnel
+
Reverse Proxy
+
Docker Compose
+
Tailscale
</code></pre>
<p>最终变成现在这套方案。</p>
<p>它可能不是最快的。但在家庭网络环境下，足够稳定。而稳定，其实远比“理论最快”重要。</p>
<h2>这篇文章记录什么</h2>
<p>过去很长一段时间，我在群晖上部署服务时，习惯使用：</p>
<pre><code>WebStation
+
Alias
+
Docker
</code></pre>
<p>一开始看起来很方便。</p>
<p>但随着 React / Next.js、WebSocket、AI WebUI、Jellyfin、EPG 系统和 Docker 服务越来越多，问题也越来越明显：</p>
<ul>
<li>页面不存在</li>
<li>静态资源 404</li>
<li>WebSocket 失败</li>
<li>页面白屏</li>
<li>reverse proxy 混乱</li>
<li>国内公网访问越来越不稳定</li>
</ul>
<p>后来我逐渐意识到，WebStation 更像“传统网站托管时代”的产物，而现代 Homelab / Docker / React 服务，其实已经是另一套体系。</p>
<p>于是我开始重新设计整个家庭服务器架构，最终形成了现在这套：</p>
<pre><code>Cloudflare Tunnel
+
群晖 Reverse Proxy
+
Docker Compose
+
Tailscale
</code></pre>
<p>这篇文章主要记录：</p>
<ul>
<li>为什么我放弃 WebStation Alias</li>
<li>为什么 Cloudflare Tunnel 非常适合家庭宽带</li>
<li>群晖 + Docker 的长期最佳实践</li>
<li>家庭网络环境下的优化经验</li>
</ul>
<h2>适用场景</h2>
<p>这套方案适用于：</p>
<ul>
<li>群晖 NAS</li>
<li>Docker / Container Manager</li>
<li>家庭宽带</li>
<li>无公网 IP</li>
<li>80 / 443 被限制</li>
<li>想公网访问家庭服务</li>
<li>React / Next.js / Jellyfin / EPG / AI WebUI 等现代 Web 应用</li>
</ul>
<h2>推荐整体架构</h2>
<p>推荐架构是：</p>
<pre><code>公网
  ↓
Cloudflare Tunnel
  ↓
群晖 Reverse Proxy
  ↓
Docker Services
</code></pre>
<p>架构图：</p>
<pre><code>┌──────────────────┐
│     Browser      │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ Cloudflare Edge  │
└────────┬─────────┘
         │ Tunnel
         ▼
┌──────────────────┐
│ Synology NAS     │
│ Reverse Proxy    │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ Docker Services  │
│ EPG / Jellyfin   │
│ Grafana / AI     │
└──────────────────┘
</code></pre>
<p>核心思路很简单：公网入口交给 Cloudflare Tunnel，NAS 内部再用群晖 Reverse Proxy 转发到不同的 Docker 服务。</p>
<h2>为什么不推荐 WebStation + Alias</h2>
<p>WebStation 更适合传统网站，例如：</p>
<ul>
<li>WordPress</li>
<li>PHP</li>
<li>Apache</li>
<li>Typecho</li>
<li>静态 HTML</li>
</ul>
<p>它不太适合：</p>
<ul>
<li>React SPA</li>
<li>Next.js</li>
<li>WebSocket</li>
<li>Docker Gateway</li>
<li>流媒体</li>
</ul>
<p>Alias 的典型写法是：</p>
<pre><code>/epg
/jellyfin
/ai
</code></pre>
<p>但很多现代应用默认运行在：</p>
<pre><code>/
</code></pre>
<p>而不是：</p>
<pre><code>/subpath
</code></pre>
<p>因此会出现：</p>
<ul>
<li>页面不存在</li>
<li>静态资源 404</li>
<li>WebSocket 失败</li>
<li>redirect loop</li>
<li>js/css 丢失</li>
</ul>
<p>所以更推荐使用子域名。</p>
<p>推荐：</p>
<pre><code>epg.matrixpunk.com
jellyfin.matrixpunk.com
ai.matrixpunk.com
</code></pre>
<p>不推荐：</p>
<pre><code>matrixpunk.com/epg
matrixpunk.com/jellyfin
</code></pre>
<h2>为什么 Cloudflare Tunnel 很适合家庭宽带</h2>
<p>Tunnel 的本质不是：</p>
<pre><code>公网主动访问你家
</code></pre>
<p>而是：</p>
<pre><code>NAS 主动连接 Cloudflare
</code></pre>
<p>因此它不需要：</p>
<ul>
<li>公网 IP</li>
<li>DDNS</li>
<li>端口映射</li>
<li>开放 80 / 443</li>
</ul>
<p>真实链路是：</p>
<pre><code>用户
  ↓
Cloudflare Edge
  ↓
Cloudflare Tunnel
  ↓
群晖 Docker
</code></pre>
<p>这点对家庭宽带非常关键。很多家庭宽带没有公网 IPv4，80 / 443 入站端口也经常不可用。</p>
<h2>为什么访问会慢</h2>
<p>Cloudflare Tunnel 解决的是“能稳定访问”的问题，不是“绝对高速”的问题。</p>
<h3>国际链路</h3>
<p>Cloudflare Tunnel 很多时候仍然走国际线路，例如：</p>
<ul>
<li>香港</li>
<li>日本</li>
<li>新加坡</li>
<li>欧洲</li>
</ul>
<p>不同运营商、不同地区、不同时间段，体验会有明显差异。</p>
<h3>QUIC / UDP 不稳定</h3>
<p>cloudflared 默认可能使用 QUIC。QUIC 基于 UDP，而国内家庭宽带里 UDP 经常会遇到：</p>
<ul>
<li>UDP QoS</li>
<li>丢包</li>
<li>晚高峰波动</li>
<li>运营商限制</li>
</ul>
<p>所以在家庭网络里，QUIC 不一定是最稳的选择。</p>
<h3>家宽上传较弱</h3>
<p>Tunnel 更依赖：</p>
<pre><code>NAS → Cloudflare
</code></pre>
<p>也就是家庭宽带的上传稳定性。上传带宽小、晚高峰抖动、运营商路由差，都会影响最终访问体验。</p>
<h2>推荐强制 HTTP2</h2>
<p>推荐配置：</p>
<pre><code>--protocol http2
</code></pre>
<p>原因很直接：TCP 通常比 QUIC / UDP 更稳。</p>
<h2>推荐 Docker Compose</h2>
<p><code>docker-compose.yml</code>：</p>
<pre><code>services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    network_mode: host
    command: tunnel run --protocol http2 --edge-ip-version 4
    environment:
      - TUNNEL_TOKEN=你的TunnelToken
</code></pre>
<h2>配置说明</h2>
<h3><code>--protocol http2</code></h3>
<p>避免 QUIC 在国内网络下的不稳定。</p>
<h3><code>--edge-ip-version 4</code></h3>
<p>国内很多宽带的 IPv6 质量反而更差，优先 IPv4 通常更稳。</p>
<h3><code>network_mode: host</code></h3>
<p>群晖上推荐使用 host 网络模式，主要是为了避免：</p>
<ul>
<li>bridge 网络问题</li>
<li>localhost 不通</li>
<li>DNS 异常</li>
</ul>
<h3><code>restart: unless-stopped</code></h3>
<p>NAS 重启后自动恢复，不需要手动启动 Tunnel。</p>
<h2>如何获取 Tunnel Token</h2>
<p>进入：</p>
<pre><code>https://one.dash.cloudflare.com
</code></pre>
<p>路径：</p>
<pre><code>Networks
→ Tunnels
→ Create Tunnel
→ Docker
</code></pre>
<p>Cloudflare 会生成一段：</p>
<pre><code>docker run ...
</code></pre>
<p>里面：</p>
<pre><code>--token xxx
</code></pre>
<p><code>xxx</code> 就是 Tunnel Token。</p>
<h2>推荐目录结构</h2>
<pre><code>/docker
  /cloudflared
    docker-compose.yml
</code></pre>
<h2>启动方式</h2>
<pre><code>docker compose up -d
</code></pre>
<p>查看日志：</p>
<pre><code>docker logs -f cloudflared
</code></pre>
<p>成功日志里会出现：</p>
<pre><code>Registered tunnel connection
</code></pre>
<h2>推荐公网结构</h2>
<p>推荐：</p>
<pre><code>Tunnel
  ↓
群晖 Reverse Proxy
  ↓
Docker Services
</code></pre>
<p>例如：</p>

























<table><thead><tr><th>服务</th><th>本地端口</th></tr></thead><tbody><tr><td>EPG</td><td>3000</td></tr><tr><td>Jellyfin</td><td>8096</td></tr><tr><td>Grafana</td><td>3001</td></tr><tr><td>Portainer</td><td>9000</td></tr></tbody></table>
<h2>群晖 Reverse Proxy 推荐</h2>
<p>DSM 路径：</p>
<pre><code>登录门户
→ 反向代理
</code></pre>
<p>示例：</p>

















<table><thead><tr><th>来源</th><th>目标</th></tr></thead><tbody><tr><td>epg.matrixpunk.com</td><td>localhost&lt;3000&gt;</td></tr><tr><td>grafana.matrixpunk.com</td><td>localhost&lt;3001&gt;</td></tr></tbody></table>
<p>Cloudflare Tunnel 只负责把请求送进 NAS，具体服务分发交给群晖 Reverse Proxy。这样每个服务都使用独立子域名，应用本身仍然运行在根路径 <code>/</code>。</p>
<h2>哪些服务适合公网</h2>
<p>推荐公网暴露：</p>
<ul>
<li>Blog</li>
<li>EPG</li>
<li>AI WebUI</li>
<li>Grafana</li>
<li>轻量后台</li>
</ul>
<p>不推荐公网暴露：</p>
<ul>
<li>DSM</li>
<li>SSH</li>
<li>Portainer</li>
<li>数据库</li>
<li>SMB / NFS</li>
</ul>
<p>这些更推荐使用 Tailscale 访问。</p>
<h2>推荐长期架构</h2>
<p>长期架构可以按职责拆分：</p>





























<table><thead><tr><th>职责</th><th>推荐方案</th></tr></thead><tbody><tr><td>公网入口</td><td>Cloudflare Tunnel</td></tr><tr><td>内网管理</td><td>Tailscale</td></tr><tr><td>服务部署</td><td>Docker Compose</td></tr><tr><td>HTTPS</td><td>Cloudflare</td></tr><tr><td>反向代理</td><td>群晖 Reverse Proxy</td></tr></tbody></table>
<p>不推荐：</p>
<pre><code>WebStation + Alias + Docker
</code></pre>
<p>推荐：</p>
<pre><code>Cloudflare Tunnel
+
Reverse Proxy
+
子域名
+
Docker Compose
</code></pre>
<p>这是目前对群晖、HomeLab、家庭网络来说，长期更稳的方案。</p>
<h2>适合我的最终实践</h2>
<p>推荐公网暴露：</p>
<pre><code>epg.matrixpunk.com
blog.matrixpunk.com
ai.matrixpunk.com
</code></pre>
<p>推荐仅内网访问：</p>
<pre><code>DSM
SSH
Portainer
数据库
</code></pre>
<p>这些通过 Tailscale 访问即可。</p>
<h2>最终架构图</h2>
<pre><code>                ┌─────────────────────┐
                │   Cloudflare DNS    │
                └─────────┬───────────┘
                          │
                          ▼
                ┌─────────────────────┐
                │ Cloudflare Tunnel   │
                └─────────┬───────────┘
                          │
                          ▼
                ┌─────────────────────┐
                │ Synology NAS        │
                │ Reverse Proxy       │
                └─────────┬───────────┘
                          │
        ┌─────────────────┼─────────────────┐
        ▼                 ▼                 ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ EPG Service  │ │ Jellyfin     │ │ Grafana      │
│ Port 3000    │ │ Port 8096    │ │ Port 3001    │
└──────────────┘ └──────────────┘ └──────────────┘
</code></pre>
<h2>经验总结</h2>
<p>Tunnel 的核心价值不是高速，而是稳定暴露家庭服务。</p>
<p>在家庭网络环境里，推荐：</p>
<ul>
<li>HTTP2</li>
<li>IPv4</li>
<li>子域名</li>
<li>Reverse Proxy</li>
</ul>
<p>尽量避免：</p>
<ul>
<li>QUIC</li>
<li>Alias 子路径</li>
<li>复杂多层反代</li>
</ul>
<p>简单、可恢复、职责清晰，才是家庭服务长期稳定运行的关键。</p>]]></content>
    <category term="NAS" />
    <category term="Docker" />
    <category term="Cloudflare" />
    <category term="Homelab" />
  </entry>
  <entry>
    <title>我用 AI 重构了自己的博客 UI</title>
    <link href="https://www.ethyoung.me//posts/ai-rebuild-blog-ui" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/ai-rebuild-blog-ui</id>
    <updated>2026-04-27T00:00:00.000Z</updated>
    <published>2026-04-27T00:00:00.000Z</published>
    <author>
      <name>Ethan Yang</name>
    </author>
    <summary type="text">记录一次从上一版个人博客 UI 到当前暗色编辑部风格首页的重构过程：为什么要改、AI 如何参与、以及这次设计真正解决了什么问题。</summary>
    <content type="html"><![CDATA[
<p>这次博客改版，准确来说不是简单换皮，而是一次重新思考：我的博客到底应该先展示什么？</p>
<p>上一版 UI 更像一个轻量的个人主页。它基于 Next.js、Tailwind CSS 和 Shadcn UI，整体偏现代、克制，也有主题切换、View Transitions、圆形友链、MDX 内容这些功能。它并不差，甚至作为一个个人站已经够用了。</p>
<p>但问题也在这里：它更像一个“能用的博客系统”，而不是一个有明确识别度的个人内容入口。</p>
<p>这次我使用 AI 参与重构，并不是想让 AI 随便生成一个好看的页面，而是把它当成一个设计和工程协作对象：我给方向、约束和审美偏好，AI 帮我拆解问题、提出方案、修改代码、检查移动端和构建结果。最后留下来的不是某个一次性 prompt 的产物，而是一轮轮取舍之后的结果。</p>
<h2>上一版 UI 的问题</h2>
<p>上一版 UI 的核心问题不是丑，而是信息优先级不够清晰。</p>
<p>首页更像一页个人简介，重点在头像、导航、简介和文章入口。读者当然可以继续点进去看文章，但首屏没有马上告诉读者：这里最近写了什么、有哪些笔记、有什么项目、这个站点的内容重心是什么。</p>
<p>对于一个个人博客来说，这种方式很常见，也很稳。但我后来发现，博客如果只停留在“自我介绍”，它会让内容被藏起来。每次访问首页，读者都要再多点一次，才能真正进入内容。</p>
<p>另外，上一版的视觉气质也偏通用。白底、卡片、柔和圆角、主题切换、常规导航，这些都很安全，但也容易让站点变成一个“还不错的模板”。我希望它更像自己的东西，而不是又一个技术博客主题。</p>
<h2>这次重构的目标</h2>
<p>这次我给首页定了一个方向：它首先要有强品牌识别，其次要立刻承接内容。</p>
<p>所以现在的首页保留了很大的 <code>&lt;EY /&gt;</code> 字标。它不是一个普通 logo，而是首屏最重要的记忆点。进入页面时，先让人知道这是 Ethan Young 的站点。</p>
<p>但它不能只是一张品牌封面。首屏高度被控制下来，让下方的 Dispatch 内容区在第一屏就露出一点。这样读者会知道：往下滚马上就是最近文章、短笔记和项目，而不是又一个单屏落地页。</p>
<p>新的首页更像一个内容调度台：</p>
<ul>
<li>左侧是最新文章，采用类似 StoryStream 的时间线结构。</li>
<li>右侧是短笔记 rail，密度更高，适合快速扫读。</li>
<li>下方是精选项目，用更大的 feature block 承接长期实践。</li>
<li>Footer 里保留漫画人物，让它变成收尾的品牌装饰，而不是继续挤压首屏。</li>
</ul>
<p>这套结构对我来说更合理。因为我的博客不是纯作品集，也不是纯简历页，而是文章、笔记、项目和生活记录混在一起的个人归档。</p>
<h2>为什么选择暗色编辑部风格</h2>
<p>这次视觉方向参考的是 The Verge 式的编辑部气质：近黑画布、巨大字标、酸薄荷和紫色硬色块、1px 边框、mono 标签、扁平但很强烈的层级。</p>
<p>我喜欢它的一点是：它不靠阴影、渐变和玻璃拟态制造“高级感”，而是靠排版、边框、色块和信息节奏撑起来。</p>
<p>这和个人博客其实很搭。博客最重要的是内容，但内容也需要一个有态度的容器。暗色画布让页面更像一个编辑台，mint 和紫色则负责制造记忆点。卡片和边框不只是装饰，而是用来划分信息：哪些是文章，哪些是笔记，哪些是项目。</p>
<p>我也去掉了主题切换。之前我一直觉得博客应该有 light / dark mode，但这次改完发现，主题切换反而会稀释设计。当前首页的暗色不是一个模式，而是整个视觉系统的基础。强行做亮色版，意味着每个色块、边框、Logo、插画和 hover 状态都要重新推一遍，否则就会变成“能切换，但不好看”。</p>
<p>与其维护一个不完整的双主题，不如把单主题做好。</p>
<h2>AI 在这次重构里做了什么</h2>
<p>这次 AI 最大的价值不是写代码快，而是帮我持续做决策拆解。</p>
<p>如果只是说“帮我把首页改好看”，AI 很容易给出一堆通用建议：更大的 hero、更漂亮的卡片、更丰富的动效。但这些建议通常没有站点上下文，也不一定适合个人博客。</p>
<p>我这次的做法是不断给它更具体的约束：</p>
<ul>
<li>当前是个人博客，不是营销落地页。</li>
<li>首页需要第一屏看到品牌，但下面要露出内容。</li>
<li>不新增新主题，不恢复主题切换。</li>
<li>漫画人物要保留，但不能压迫首屏。</li>
<li>视觉方向要延续暗色编辑部风格。</li>
<li>移动端不能横向溢出，按钮和文字不能裁切。</li>
</ul>
<p>有了这些约束之后，AI 更像一个可以一起推敲方案的前端搭档。它会先分析信息架构，再给出修改计划，然后落到 Astro 组件、Tailwind class、全局 CSS 和构建检查里。</p>
<p>当然，AI 也不是一次就对。比如主题切换按钮曾经显示不出来，点击也没有效果；Logo 颜色一开始也像照片负片；漫画人物放在首屏底部时，移动端存在压迫感。这些问题都不是靠一句 prompt 解决的，而是不断发现、判断、调整。</p>
<h2>这次我学到的</h2>
<p>第一，AI 更适合在有明确方向时放大执行力，而不是替我决定审美。</p>
<p>如果我不给它设计系统、布局目标和判断标准，它很容易走向“通用 SaaS 首页”。但当我明确说这是个人博客、需要暗色编辑部风格、不要渐变光晕、不要恢复主题切换，它就能更稳定地沿着这个方向推进。</p>
<p>第二，重构 UI 不能只看首屏截图。</p>
<p>一个首页好不好，不只取决于 hero 漂不漂亮，还取决于首屏之后有没有内容接力，移动端会不会挤，Footer 会不会突兀，链接 hover 是否统一，文章列表、笔记列表、项目区是不是同一种语言。</p>
<p>第三，很多功能不是越多越好。</p>
<p>主题切换、动画、装饰图、复杂交互都可以让页面看起来更“完整”，但如果它们没有服务于内容，就会变成噪音。这次反而是删掉一些东西之后，首页更清楚了。</p>
<h2>现在的博客更像我想要的样子</h2>
<p>重构后的博客不是完美的，但它更接近我想要的状态。</p>
<p>它仍然是一个个人博客，但不再只是一个简单的个人主页。它有一个明确的视觉入口，也能马上看到最近内容；它保留了一些个性化装饰，但不会让装饰抢走信息；它有强烈的暗色风格，但没有为了炫技堆叠复杂效果。</p>
<p>更重要的是，这次改版让我重新意识到：博客不是一次搭好就结束的项目。它会随着我写什么、做什么、在意什么而继续变化。</p>
<p>AI 只是这次变化里的一个工具。真正决定博客长什么样的，还是我想怎样表达自己。</p>]]></content>
    <category term="AI" />
    <category term="Blog" />
    <category term="Frontend" />
  </entry>
  <entry>
    <title>tmux 使用技巧与配置</title>
    <link href="https://www.ethyoung.me//posts/tmux-tips" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/tmux-tips</id>
    <updated>2026-03-05T00:00:00.000Z</updated>
    <published>2026-03-05T00:00:00.000Z</published>
    <author>
      <name>Ethan</name>
    </author>
    <summary type="text">整理 tmux 终端复用器的实用技巧、基础配置、插件管理、会话恢复、fzf 集成和日常开发中提升效率的命令集合。</summary>
    <content type="html"><![CDATA[<img src="https://www.ethyoung.me/_astro/cover.Epj1Dgd6_ZhWB93.webp" alt="tmux 使用技巧与配置" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h3>快捷键使用说明</h3>
<p>一个常见的误区：<code>Ctrl+b d</code> 分离会话时需要<strong>先按 <code>Ctrl+b</code> 并松开</strong>，然后再按 <code>d</code>，而不是同时按下所有键。</p>
<h2>完整配置方案</h2>
<h3>一、安装依赖</h3>
<pre><code># 安装 fzf (模糊查找工具)
brew install fzf
$(brew --prefix)/opt/fzf/install

# 安装 tmux
brew install tmux

# 安装 Oh My Zsh (可选，用于 shell 增强)
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

# 安装 TPM (tmux 插件管理器)
git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm
</code></pre>
<h3>二、tmux 配置 (~/.tmux.conf)</h3>
<h4>基础设置</h4>
<pre><code># 启用鼠标支持
set -g mouse on

# 历史记录行数
set -g history-limit 10000

# 窗口和面板索引从 1 开始
set -g base-index 1
setw -g pane-base-index 1

# 减少按键延迟
set -s escape-time 0

# 终端颜色支持
set-option -g default-terminal "screen-256color"
</code></pre>
<h4>状态栏美化</h4>
<pre><code># 状态栏样式
set -g status-style bg=black,fg=white
set -g status-left-length 30
set -g status-right-length 150

# 左侧显示：会话名称
set -g status-left "#[fg=green]#S #[fg=yellow]|#[default]"

# 右侧显示：日期时间
set -g status-right "#[fg=cyan]%Y-%m-%d %H:%M:%S#[default]"
</code></pre>
<h4>插件管理</h4>
<pre><code># TPM 插件列表
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-sensible'
set -g @plugin 'tmux-plugins/tmux-resurrect'    # 会话保存/恢复
set -g @plugin 'tmux-plugins/tmux-continuum'    # 自动保存会话

# 自动保存/恢复配置
set -g @continuum-restore 'on'              # 启动时自动恢复
set -g @continuum-save-interval '5'         # 每 5 分钟自动保存
</code></pre>
<h4>自定义快捷键</h4>
<pre><code># 修改前缀键为 Ctrl+a
unbind C-b
set-option -g prefix C-a
bind-key C-a send-prefix

# 分割窗口快捷键
bind | split-window -h    # 垂直分割
bind - split-window -v    # 水平分割

# 重载配置文件
bind r source-file ~/.tmux.conf \; display-message "Config reloaded!"
</code></pre>
<h4>初始化 TPM</h4>
<pre><code># 必须放在配置文件末尾
run '~/.tmux/plugins/tpm/tpm'
</code></pre>
<blockquote><p><strong>提示</strong>：首次使用时，进入 tmux 后按 <code>Ctrl+a</code> + <code>I</code> (大写) 安装所有插件。</p></blockquote>
<h3>三、智能命令别名 (~/.zshrc)</h3>
<h4>基础命令别名</h4>
<pre><code># 常用命令简化
alias t='tmux'                    # 启动 tmux
alias tn='tmux new -s'            # 创建新会话
alias ta='tmux attach -t'         # 附加到会话
alias tls='tmux ls'               # 列出会话
alias tk='tmux kill-session -t'   # 删除会话
alias tka='tmux kill-server'      # 关闭所有会话
alias tr='tmux rename-session -t' # 重命名会话
alias tw='tmux new-window -n'     # 创建新窗口
alias tl='tmux list-windows'      # 列出窗口
alias td='tmux detach'            # 分离会话
</code></pre>
<h4>智能会话管理 (集成 fzf)</h4>
<pre><code># 智能附加/创建会话
tma() {
    # 检查是否在交互式终端中
    if [ ! -t 1 ]; then
        echo "❌ 当前环境不是交互式终端，无法使用 fzf 选择"
        echo "请在终端中运行 tma 命令"
        return 1
    fi

    # 无参数时使用 fzf 选择会话
    if [ -z "$1" ]; then
        local session
        session=$(tmux ls 2&gt;/dev/null | awk -F: '{print $1}' | fzf --height 40% --reverse --border)
        if [ -n "$session" ]; then
            tmux attach -t "$session"
        else
            echo "未选择任何 tmux 会话"
        fi
    else
        # 有参数时尝试附加，失败则创建新会话
        tmux attach -t "$1" 2&gt;/dev/null || tmux new -s "$1"
    fi
}
</code></pre>
<h4>快速切换窗口</h4>
<pre><code># 使用 fzf 快速切换窗口
tmw() {
    # 检查是否在交互式终端中
    if [ ! -t 1 ]; then
        echo "❌ 当前环境不是交互式终端，无法使用 fzf 选择窗口"
        echo "请在终端中运行 tmw 命令"
        return 1
    fi

    # 确认是否在 tmux 会话中
    if [ -z "$TMUX" ]; then
        echo "⚠️ 当前不在 tmux 会话中，请先进入 tmux 再使用 tmw"
        return 1
    fi

    # 获取窗口列表并使用 fzf 选择
    local target
    target=$(tmux list-windows -F '#I: #W' | fzf --height 40% --reverse --border)

    if [ -n "$target" ]; then
        local win_id
        win_id=$(echo "$target" | cut -d: -f1 | tr -d ' ')
        tmux select-window -t "$win_id"
    else
        echo "未选择任何窗口"
    fi
}
</code></pre>
<h4>会话保存与恢复</h4>
<pre><code># 手动保存/恢复会话
alias tsave='tmux run-shell ~/.tmux/plugins/tmux-resurrect/scripts/save.sh'
alias trestore='tmux run-shell ~/.tmux/plugins/tmux-resurrect/scripts/restore.sh'
</code></pre>
<h4>常用会话快捷方式</h4>
<pre><code># 快速进入预定义会话
alias tdev='tmux attach -t dev || tmux new -s dev'
alias twork='tmux attach -t work || tmux new -s work'
alias tplay='tmux attach -t play || tmux new -s play'
</code></pre>
<h4>FZF 增强配置</h4>
<pre><code># FZF 默认选项
export FZF_DEFAULT_OPTS='--height 40% --reverse --border --preview-window=down:3:hidden:wrap'

# FZF 文件搜索命令 (需要安装 fd)
export FZF_CTRL_T_COMMAND='fd --type f --hidden --follow --exclude .git'
</code></pre>
<h2>使用示例</h2>
<pre><code># 创建或附加到名为 "work" 的会话
tma work

# 不带参数时弹出 fzf 选择器
tma

# 在 tmux 会话中使用 fzf 快速切换窗口
tmw
</code></pre>]]></content>
    <category term="tmux" />
  </entry>
  <entry>
    <title>Tmux 环境下的 Shell 配置隔离问题与解决方案</title>
    <link href="https://www.ethyoung.me//posts/tmux-shell-isolation" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/tmux-shell-isolation</id>
    <updated>2026-01-27T00:00:00.000Z</updated>
    <published>2026-01-27T00:00:00.000Z</published>
    <author>
      <name>Ethan Yang</name>
    </author>
    <summary type="text">分析 tmux 会话中 Shell 配置隔离与热更新不一致的问题，记录排查时间线、重载方案和 zsh/tmux 配置管理建议。</summary>
    <content type="html"><![CDATA[<img src="https://www.ethyoung.me/_astro/cover.Epj1Dgd6_ZhWB93.webp" alt="Tmux 环境下的 Shell 配置隔离问题与解决方案" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h2>问题场景</h2>
<p>最近在开发一个 tmux 窗口快速切换功能时，遇到了一个有趣的问题：</p>
<pre><code># 定义了一个窗口切换函数
tmw() {
  # 使用 fzf 选择并切换 tmux 窗口
  local target=$(tmux list-windows -F '#{window_index}:#{window_name}' | fzf ...)
  tmux select-window -t "${target%%:*}"
}
</code></pre>
<p>奇怪的是：</p>
<ul>
<li>✅ 在外层 shell 中 <code>source ~/.zshrc</code> 后，函数定义正常</li>
<li>❌ 但在 tmux 会话内运行 <code>tmw</code> 时，执行的仍是旧版本</li>
<li>🤔 即使多次 <code>source ~/.zshrc</code>，tmux 内部的函数也不更新</li>
</ul>
<h2>根本原因：Shell 环境隔离</h2>
<h3>1. 进程隔离机制</h3>
<pre><code>外层 Shell (PID: 1000)
├── tmux server (PID: 1001)
    ├── tmux session "dev"
        ├── window 0: zsh (PID: 1002) ← 独立的 shell 进程
        ├── window 1: zsh (PID: 1003) ← 另一个独立的 shell 进程
        └── window 2: vim (PID: 1004)
</code></pre>
<p><strong>关键理解</strong>：tmux 中的每个窗口都运行着<strong>独立的 shell 进程</strong>，它们：</p>
<ul>
<li>有自己的环境变量空间</li>
<li>有自己的函数定义作用域</li>
<li>不会自动继承外层 shell 的配置更新</li>
</ul>
<h3>2. 配置加载时机</h3>
<pre><code># 时间线分析
10:00 - 启动外层 shell，加载 ~/.zshrc (版本A)
10:05 - 进入 tmux，创建新 shell 进程，继承当时的配置 (版本A)
10:10 - 修改配置文件 (版本B)
10:15 - 外层 shell: source ~/.zshrc  ← 只更新外层 shell 为版本B
10:20 - tmux 内运行函数  ← 仍使用版本A！
</code></pre>
<h2>解决方案对比</h2>
<h3>方案一：tmux 内部重新加载 ⭐️</h3>
<pre><code># 在 tmux 会话内执行
source ~/.zshrc
# 或者直接加载特定配置
source ~/.config/zsh/.tmux_cfg
</code></pre>
<p><strong>优点</strong>：快速、精确
<strong>缺点</strong>：每个窗口都需要单独执行</p>
<h3>方案二：Detach &amp; Reattach</h3>
<pre><code># 退出当前会话
tmux detach

# 重新进入（会创建新的 shell 进程）
tmux attach -t session-name
</code></pre>
<p><strong>优点</strong>：一次性解决所有窗口
<strong>缺点</strong>：中断当前工作流</p>
<h3>方案三：tmux 广播命令 🚀</h3>
<pre><code># 向所有窗口发送重载命令
tmux send-keys -t session-name: 'source ~/.zshrc' C-m

# 或者写成函数
treload() {
  local session="${1:-$(tmux display-message -p '#S')}"
  tmux list-windows -t "$session" -F '#{window_index}' | \
  while read window; do
    tmux send-keys -t "${session}:${window}" 'source ~/.zshrc' C-m
  done
}
</code></pre>
<p><strong>优点</strong>：批量更新，不中断工作流
<strong>缺点</strong>：稍复杂，可能干扰正在运行的命令</p>
<h2>最佳实践建议</h2>
<h3>1. 开发时的配置管理</h3>
<pre><code># ~/.config/zsh/dev-utils.zsh
# 开发期间的快速重载函数
dev_reload() {
  echo "🔄 重载配置..."
  source ~/.zshrc
  echo "✅ 配置已更新"

  # 如果在 tmux 中，提醒其他窗口
  if [[ -n "$TMUX" ]]; then
    echo "💡 提示：其他 tmux 窗口需要手动重载"
  fi
}

alias dr='dev_reload'
</code></pre>
<h3>2. 生产环境的配置策略</h3>
<pre><code># 在 ~/.zshrc 中添加版本检查
export ZSH_CONFIG_VERSION="2024.01.27"

check_config_version() {
  local expected_version="2024.01.27"
  if [[ "$ZSH_CONFIG_VERSION" != "$expected_version" ]]; then
    echo "⚠️  配置版本过期，建议重载: source ~/.zshrc"
  fi
}

# 在 tmux 窗口启动时检查
if [[ -n "$TMUX" ]]; then
  check_config_version
fi
</code></pre>
<h3>3. 自动化解决方案</h3>
<pre><code># ~/.tmux.conf
# 绑定快捷键快速重载所有窗口配置
bind R run-shell '\
  for window in $(tmux list-windows -F "#{window_index}"); do \
    tmux send-keys -t :$window "source ~/.zshrc" C-m; \
  done; \
  tmux display-message "已重载所有窗口配置"'
</code></pre>
<h2>延伸思考</h2>
<p>这个问题揭示了几个重要的系统概念：</p>
<ol>
<li><strong>进程隔离</strong>：每个进程有独立的内存空间和环境</li>
<li><strong>配置继承</strong>：子进程只继承创建时父进程的环境</li>
<li><strong>状态管理</strong>：分布式环境下的配置同步挑战</li>
</ol>
<p>类似的场景还出现在：</p>
<ul>
<li>Docker 容器内的环境变量更新</li>
<li>SSH 会话中的配置同步</li>
<li>IDE 集成终端的环境隔离</li>
</ul>
<h2>总结</h2>
<p>Tmux 的 shell 环境隔离是一个设计特性，不是 bug。理解这个机制有助于：</p>
<ul>
<li>🎯 更好地管理开发环境</li>
<li>🔧 避免配置不生效的困扰</li>
<li>🚀 设计更健壮的配置管理策略</li>
</ul>
<p>记住：<strong>修改配置后，别忘了在 tmux 内部也要重新加载！</strong></p>]]></content>
    <category term="tmux" />
  </entry>
  <entry>
    <title>2025 年终随笔</title>
    <link href="https://www.ethyoung.me//posts/2025-year-end" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/2025-year-end</id>
    <updated>2026-01-04T00:00:00.000Z</updated>
    <published>2026-01-04T00:00:00.000Z</published>
    <author>
      <name>Ethan Yang</name>
    </author>
    <summary type="text">回顾 2025 年在生活、工作和个人成长上的变化，记录阶段性选择、日常感受、经验复盘和下一阶段想继续推进的方向。</summary>
    <content type="html"><![CDATA[
<p>拾掇了一下 2025 年的计划，最终还是决定把它们合入到 2026 年的计划里。</p>
<p>回头看这一年，整体状态基本维持原样。年初计划去做的一些事情，如今回想起来，可以说是几乎全军覆没。</p>
<p>时常会感叹：人生里的梦想铺得很大，而现实真正能施展的力量却始终有限。</p>
<h2>生活</h2>
<p>生活上基本没有什么改变，依然是朝九晚五的工作节奏，偶尔加班，日子一天天往前走。</p>
<p>今年最大的变化，来自小朋友。慢慢长大，开始上幼儿园，生活的重心也随之发生了变化。外出游玩的次数比往年多了一些，大概去了三座城市，看了些不同的风景。</p>
<p>总体而言，生活依旧平稳。</p>
<h2>技术</h2>
<p>技术上谈不上有什么突破，依然是熟悉的技术栈：前端为主，搭配一些后端工作。</p>
<p>这一年更多是在对过去的项目做升级和改造，让它们变得更稳定、更可维护，新的技术尝试并不多。</p>
<p>倒是慢慢学会了如何去 <em>vibe coding</em>。程序员这份职业有时候确实够”狠”——优化和重构起来，干掉过去的自己从不手软。</p>
<h2>读书</h2>
<p>基本没怎么看书。</p>
<h2>写作</h2>
<p>写作方面同样没有太多进展，博客更新频率依然偏低。</p>
<p>主要还是零散地写了一些技术文章，记录并分享工作中的经验。整体来看，输出依旧不多，离自己期望的状态还有不小的距离。</p>
<h2>Finally</h2>
<p>总体而言，2025 年过得相对平淡，没有明显的高峰，也没有太深的低谷。</p>
<p>希望 2026 年能在这种平稳之中找到一些新的可能。至于具体计划，这里就先不展开了——顺其自然吧。</p>]]></content>
    <category term="总结" />
    <category term="Life" />
  </entry>
  <entry>
    <title>我也来回答这些问题吧</title>
    <link href="https://www.ethyoung.me//posts/blog-interview" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/blog-interview</id>
    <updated>2025-08-18T00:00:00.000Z</updated>
    <published>2025-08-18T00:00:00.000Z</published>
    <author>
      <name>Ethan Yang</name>
    </author>
    <summary type="text">以问答形式整理一次博客访谈，记录写作动机、内容选择、博客维护习惯、个人表达方式和对独立站持续更新的理解。</summary>
    <content type="html"><![CDATA[
<p>最近看到 <a href="https://anotherdayuw.com/2024/5962/" rel="noopener noreferrer" target="_blank">博客作者呀，我想采访你这 9 个问题！</a> 和 <a href="https://flowersink.com/blog/blog-detail/54" rel="noopener noreferrer" target="_blank">好耶，我来回答这 9 个问题！——博客问卷</a> 的文章，觉得挺有意思的，所以我也来回答这些问题吧。</p>
<h2>简单介绍下自己或者你的博客</h2>
<p>我叫 Ethan Young，也可叫 charles Young， 反正代号而已， 是一名常驻南京的前端 web 开发，博客地址是 <a href="https://ethanyoung.me" rel="noopener noreferrer" target="_blank">https://ethanyoung.me</a>。博客主要记录我的生活、分享技术、表达思考和见证成长。充当自己的树洞吧。</p>
<h2>什么契机让你开始写博客？</h2>
<p>其实我并不擅长写作，表达能力也有限，加上性格偏内向。有时候觉得写博客就像是把自己的内心世界暴露在公共场合，仿佛在众人面前”裸奔”。但转念一想，观众本就不多，而且大家彼此并不熟悉。或许这种”坦诚相见”、剖析自我的过程，反而能让我更清楚地认识自己。</p>
<p>人生其中一道命题就是：我是谁？</p>
<p>你的经历，你自己的决定，决定了你是谁吧。</p>
<p>答案暂定吧～</p>
<h2>你是如何完成创作的？</h2>
<p>我通常会在工作之余抽出时间来写作。使用 Obsidian 记录新的想法或经历，慢慢整理成一篇文章。</p>
<h2>运营博客的过程中是否有失去过动力？如果有，是为什么恢复的？如果没有，请问您又是如何保持创作的激情？</h2>
<p>失去过；有过多次，但每次都能恢复；第一版博客因为成本问题，需要服务器，前端技术又不够成熟，页面各种 bug，放弃了；</p>
<p>第二版博客因为工作太忙，没时间维护，放弃了；</p>
<p>第三版，一直维持到现在</p>
<h2>如何搭建博客，以及运营博客每年需要投入的资金？</h2>
<ol>
<li>博客使用的是 大名鼎鼎的 nextjs，没有前后端分离,UI 使用的是 tailwindCss，内容使用 markdown 或者 MDX 语法编写。</li>
<li>博客部署在 Vercel 上，免费版就够用了。</li>
<li>域名买的是 namesilo，年费大概 99 元。</li>
<li>DNS 使用的是 Cloudflare，免费版。</li>
</ol>
<h2>推荐 1 篇你博客中的文章，并推荐一个你喜欢读的博客，聊聊原因</h2>
<p>自己推荐博客是<a href="https://ethyoung.me/2025-01-29" rel="noopener noreferrer" target="_blank">2024：转折与成长的一年</a></p>
<p>推荐<a href="https://darmau.co/zh/article/which-country-do-you-love" rel="noopener noreferrer" target="_blank">你爱的是哪个国？</a> 虽然文章所阐述的部分观点我不认同，但文章还是不错的。</p>
<h2>推荐 1 个近期喜欢的事物？</h2>
<p>电视《异形：地球》，对于异形系列的科幻迷来说，这部剧集是一个不错的补充，剧情还行可看。</p>
<p>电影《超人》，将以前的超人由神降为了人，有点弱，剧情一般般；不是很好看</p>
<h2>想做还没有做的事，或想尝试还没有尝试的主题？</h2>
<p>想做的事情很多，画画，摄影，骑行，或者做个小小的开源项目, 只是一直没有开始而已</p>
<h2>写到这里，闭上你的眼睛，深呼吸几分钟，或是出去溜达一圈，然后回来写任何你想写的东西</h2>
<p>人生路漫漫，做好自己</p>]]></content>
    <category term="Life" />
    <category term="Interview" />
  </entry>
  <entry>
    <title>学习 Vim 的过程</title>
    <link href="https://www.ethyoung.me//posts/learn-vim" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/learn-vim</id>
    <updated>2025-07-29T00:00:00.000Z</updated>
    <published>2025-07-29T00:00:00.000Z</published>
    <author>
      <name>Ethan</name>
    </author>
    <summary type="text">记录学习 Vim 的入门过程、常用模式、移动编辑习惯和配置理解，梳理从不熟悉到能在日常开发中使用的实践路径。</summary>
    <content type="html"><![CDATA[
<blockquote><p>学习 vim 时，其实就是尽量减少手指离开键盘的频率</p></blockquote>
<h2>过程</h2>
<p>其实入门简单，我是直接在控制台上面输入<code>vimtutor</code>，然后跟着教程一步步来。各个操作都可以在教程中找到。</p>
<p>问题点在于，实际使用中，总有些快捷键不记得。需要不断的进行去查找。</p>
<p>后来看到一款编辑器，叫 neovim，基于 vim 的改进版，支持插件系统，社区活跃。</p>
<p>试着去配置时，有种写代码感觉，本身配置语言就是 Lua，配置文件也可以像代码一样进行组织</p>
<p>但还是感觉很麻烦，看了一圈配置，最终还是选择 lazvim 进行一站式配置。</p>
<p>目前使用的 lazyvim 配置， 逐步熟悉 vim 操作，关键在于大量的练习，改变过去基于 vscode 按键肌肉记忆，有点难度。</p>
<h2>问题</h2>
<ul>
<li>习惯了 vscode 的快捷键，切换到 vim 时，按键会有些不适应， 目前 vscode 借助的 neovim 插件，总感觉体验有点割裂</li>
<li>由于项目使用 vue2+pug，这样在编辑器环境中，这些语法高亮和代码提示都不太好，目前只能使用 webstorm 进行编辑，
但 webstorm 的快捷键又和 vim 不对，使用了 ideaVim 插件，配置部分 <a href="https://gist.github.com/mikeslattery/d2f2562e5bbaa7ef036cf9f5a13deff5" rel="noopener noreferrer" target="_blank">mikeslattery/.idea-lazy.vim</a>
感觉还行，95% 的快捷键都能使用。</li>
<li>同时切换到 vim 时，git GUI 使用了 lazygit 和 webstorm 相互配置工作。 webstorm git 解决冲突， lazygit 进行提交和查看日志。</li>
</ul>
<h2>finally</h2>
<ul>
<li>希望自己能坚持吧～</li>
</ul>]]></content>
    <category term="vim" />
    <category term="编辑器" />
    <category term="学习笔记" />
  </entry>
  <entry>
    <title>Surge 自定义配置，踩坑记录</title>
    <link href="https://www.ethyoung.me//posts/surge-custom-config" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/surge-custom-config</id>
    <updated>2025-06-23T00:00:00.000Z</updated>
    <published>2025-06-23T00:00:00.000Z</published>
    <author>
      <name>Ethan Yang</name>
    </author>
    <summary type="text">记录在 Surge 中设置自定义策略组时遇到的问题和解决方案，说明规则匹配、流量走向、策略组选择和配置调试思路。</summary>
    <content type="html"><![CDATA[
<h2>背景</h2>
<p>在 surge 的自定义策略组中，我设置了 github 策略组，并且设置了一个默认的策略组为 <code>github</code>，用于将所有 github 所有数据走当前的策略组。</p>
<p>而我将此策略组模式选择了 US 策略，以告之当前机器的 github 全部流量走 US 节点。</p>
<h2>问题</h2>
<p>问题在于我设置了 US 节点，但是没有任何作用，通过查看器观察到的流量还是走的默认节点。</p>
<img src="./assets/1750666842698.png" alt="1750666842698.png" />
<h2>分析</h2>
<p>经过分析，发现 surge 的策略组是自上而下穿透的，因此需要将 github 策略组放在全局策略组之前。</p>
<img src="./assets/1750667025869.png" alt="1750667025869.png" />]]></content>
    <category term="surge" />
    <category term="Tech" />
  </entry>
  <entry>
    <title>记录一次循环依赖导致 undefined 的问题</title>
    <link href="https://www.ethyoung.me//posts/circle-denpenth" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/circle-denpenth</id>
    <updated>2025-06-04T00:00:00.000Z</updated>
    <published>2025-06-04T00:00:00.000Z</published>
    <author>
      <name>Ethan</name>
    </author>
    <summary type="text">记录在 JavaScript 项目中引入循环依赖导致 undefined 错误的排查过程，分析模块加载顺序、依赖方向和可落地的重构方案。</summary>
    <content type="html"><![CDATA[
<h2>是什么</h2>
<p>循环依赖(Circular dependency)是指在模块系统中，两个或多个模块形成相互引用的依赖关系。这种情况会导致模块加载和初始化过程变得复杂且难以预测。</p>
<h3>循环依赖示例</h3>
<p>如下图所示，循环依赖可以是直接的相互引用，也可以是间接的依赖环：</p>
<pre><code>flowchart TD
    subgraph "循环依赖结构"
        ModuleA((模块A)) --&gt;|导入| ModuleB((模块B))
        ModuleB --&gt;|导入| ModuleA
    end

    subgraph "间接循环依赖"
        ModC((模块C)) --&gt;|导入| ModD((模块D))
        ModD --&gt;|导入| ModE((模块E))
        ModE --&gt;|导入| ModC
    end
</code></pre>
<h3>循环依赖引发的常见问题</h3>
<pre><code>flowchart TD
    subgraph "引发问题"
        Problems[循环依赖] --&gt; P1[未完成的对象初始化]
        Problems --&gt; P2[undefined值]
        Problems --&gt; P3[执行顺序不可预测]
        Problems --&gt; P4[调试困难]
    end
</code></pre>
<h3>问题详解</h3>
<ol>
<li><strong>未完成的对象初始化</strong>：模块在被完全初始化前就被引用，导致访问到不完整的对象</li>
<li><strong>undefined值</strong>：尝试访问尚未定义的导出，导致运行时错误</li>
<li><strong>执行顺序不可预测</strong>：不同的模块系统处理循环依赖的方式不同，造成代码行为难以预测</li>
<li><strong>调试困难</strong>：错误堆栈跟踪混乱，难以定位问题根源</li>
</ol>
<h2>为什么？</h2>
<pre><code>flowchart TD
    subgraph "模块加载过程"
        Start[开始加载] --&gt; LoadA[开始加载模块A]
        LoadA --&gt; A1[解析模块A的依赖]
        A1 --&gt; A2[发现模块A依赖模块B]
        A2 --&gt; LoadB[开始加载模块B]
        LoadB --&gt; B1[解析模块B的依赖]
        B1 --&gt; B2[发现模块B依赖模块A]

        B2 --&gt; Decision{模块系统如何处理?}

        Decision --&gt;|CommonJS| CJS[返回未完成的A]
        CJS --&gt; B3[完成模块B的初始化]
        B3 --&gt; A3[继续模块A的初始化]
        A3 --&gt; Done[加载完成]

        Decision --&gt;|ES Modules| ESM[完成构造阶段]
        ESM --&gt; ESM2[执行模块A代码]
        ESM2 --&gt; ESM3[执行模块B代码]
        ESM3 --&gt; Done

        Decision --&gt;|未处理| Error[运行时错误]
    end
</code></pre>
<h2>如何解决？</h2>

























<table><thead><tr><th>方法</th><th>示例</th><th>说明</th></tr></thead><tbody><tr><td>✅ 延迟加载</td><td><code>require()</code> 写到函数内部</td><td>避免初始化阶段访问未定义</td></tr><tr><td>✅ 提取公共模块</td><td>建立 shared.js</td><td>A 和 B 不直接依赖彼此</td></tr><tr><td>✅ 重构依赖方向</td><td>抽象高层逻辑模块</td><td>减少相互耦合</td></tr></tbody></table>
<h2>实际案例分析</h2>
<h2>全国易捷工单退款的循环依赖问题</h2>
<h3>现象</h3>
<p>当从工单列表进入工单详情时，导致页面展示空白。代码报错：</p>
<pre><code>hook.js:608 TypeError: _pages_cashRegister_ThirdPlatformService_SinopecCompService__WEBPACK_IMPORTED_MODULE_16__.SinopecCompService is not a constructor
    at ./src/pages/cashRegister/ThirdPlatformService/service.js (service.js:12:68)
    at __webpack_require__ (bootstrap:853:1)
    at fn (bootstrap:150:1)
    at ./src/class/cashRegister/GatheringInfo.js (42.js:725:107)
    at __webpack_require__ (bootstrap:853:1)
    at fn (bootstrap:150:1)
    at ./src/class/cashRegister/index.js (index.js:1:1)
    at __webpack_require__ (bootstrap:853:1)
    at fn (bootstrap:150:1)
    at ./src/pages/cashRegister/ThirdPlatformService/SinopecCompService.js (42.js:3437:78)
overrideMethod @ hook.js:608
s2.f6yc.comnull/:1
</code></pre>
<h3>定位过程</h3>
<p>错误日志中关键信息：</p>
<p><code>_pages_cashRegister_ThirdPlatformService_SinopecCompService__WEBPACK_IMPORTED_MODULE_16__.SinopecCompService is not a constructor</code></p>
<p>报错位置在：</p>
<p><code>at ./src/pages/cashRegister/ThirdPlatformService/service.js (service.js:12:68)</code></p>
<p>相关代码：</p>
<pre><code>import { SinopecCompService } from '@/pages/cashRegister/ThirdPlatformService/SinopecCompService'
import { ChinaSinopecCashPayService } from '@/pages/cashRegister/ThirdPlatformService/ChinaSinopecCashPayService'
import { ChinaSinopecService } from '@/pages/cashRegister/ThirdPlatformService/ChinaSinopecService'

const serviceList = [
  new SinopecService(),
  new MasterTooService(),
  new SinopecCompService(),
  new ChinaSinopecCashPayService(),
  new ChinaSinopecService(),
]
</code></pre>
<p>报错原因是SinopecCompService不是一个构造函数。我们尝试打印SinopecCompService：</p>
<img src="./images/6846f96863ce6.png" alt="CleanShot_2025-06-09_at_10.06.10.png" />
<p>报错信息表明<code>SinopecCompService</code>在被实例化时不是一个构造函数，实际上它在调用时是<code>undefined</code>。</p>
<h3>调试过程</h3>
<p>我们将断点打到SinopecCompService对象上面：</p>
<img src="./images/6846f96b344bf.png" alt="CleanShot_2025-06-09_at_10.27.13.png" />
<p>得到以下的堆栈，分析调用问题：</p>
<pre><code>./src/pages/cashRegister/ThirdPlatformService/service.js (service.js:13)
__webpack_require__ (bootstrap:853)
fn (bootstrap:150)
./src/class/cashRegister/GatheringInfo.js (42.js:725)
__webpack_require__ (bootstrap:853)
fn (bootstrap:150)
./src/class/cashRegister/index.js (index.js:1)
__webpack_require__ (bootstrap:853)
fn (bootstrap:150)
./src/pages/cashRegister/ThirdPlatformService/SinopecCompService.js (42.js:3437)
__webpack_require__ (bootstrap:853)
fn (bootstrap:150)
./node_modules/cache-loader/dist/cjs.js?!./node_modules/babel-loader/lib/index.js!./node_modules/cache-loader/dist/cjs.js?!./node_modules/vue-loader/lib/index.js?!./src/pages/newMaintain/pages/ViewDetail/index.vue?vue&amp;type=script&amp;lang=js&amp; (NewMaintainDetailView.js:1028)
__webpack_require__ (bootstrap:853)
fn (bootstrap:150)
./src/pages/newMaintain/pages/ViewDetail/index.vue?vue&amp;type=script&amp;lang=js&amp; (index.vue:1)
__webpack_require__ (bootstrap:853)
fn (bootstrap:150)
./src/pages/newMaintain/pages/ViewDetail/index.vue (index.vue:1)
__webpack_require__ (bootstrap:853)....
</code></pre>
<p>通过调用堆栈，分析出循环依赖路径：</p>
<ol>
<li>
<p>从<code>ViewDetail/index.vue</code>开始，导入了<code>SinopecCompService.js</code></p>
</li>
<li>
<p>然后<code>SinopecCompService.js</code>导入了<code>Payment</code>类<code>from '@/class/cashRegister'</code></p>
</li>
<li>
<p><code>class/cashRegister/index.js</code>中导出了<code>GatheringInfo</code>等模块</p>
<pre><code>export { GatheringInfo } from './GatheringInfo'
export { GatheringConfig } from './GatheringConfig'
export { Payment } from './Payment'
export { CzkCard } from './CzkCard'
export { Coupon } from './Coupon'
</code></pre>
</li>
<li>
<p><code>GatheringInfo.js</code>导入了<code>service.js</code>中的<code>hiddenPayment</code></p>
<pre><code>import { hiddenPayment } from '@/pages/cashRegister/ThirdPlatformService/service'
</code></pre>
</li>
<li>
<p>最后<code>service.js</code>又导入了<code>SinopecCompService</code>，从而形成了循环依赖</p>
</li>
</ol>
<p>分析过程如图：</p>
<img src="./images/6846f967d4fdd.png" alt="CleanShot_2025-06-09_at_11.48.28.png" />
<h3>解决方案</h3>
<p>解决方法很简单：将原先从<code>ViewDetail/index.vue</code>开始导入<code>SinopecCompService.js</code>的逻辑移出去，打破循环依赖链。</p>]]></content>
    <category term="javascript" />
    <category term="debug" />
    <category term="循环依赖" />
  </entry>
  <entry>
    <title>群晖 SSL 证书替换</title>
    <link href="https://www.ethyoung.me//posts/replace-ssl" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/replace-ssl</id>
    <updated>2025-05-12T00:00:00.000Z</updated>
    <published>2025-05-12T00:00:00.000Z</published>
    <author>
      <name>Ethan Yang</name>
    </author>
    <summary type="text">记录在群晖 NAS 中替换 SSL 证书的操作过程，包含证书文件准备、后台导入步骤、常见问题和 HTTPS 访问验证。</summary>
    <content type="html"><![CDATA[
<h2>问题</h2>
<p>如果通过群晖直接申请 Let’s Encrypt 证书，会提示证书申请失败，原因是国内的 Let’s Encrypt 服务器无法访问。</p>
<h2>如何解决</h2>
<p>基本流程：</p>
<ol>
<li>从域名服务商那里申请一个免费的 SSL 证书，或者购买一个付费的 SSL 证书。然后将证书文件下载到本地。选择 nginx 格式的证书文件。
<img src="./assets/CleanShot-2025-05-10-at-19.00.15.png" alt="CleanShot 2025-05-10 at 19.00.15.png" /></li>
<li>下载解压缩包，解压缩后会得到以下文件：
<ul>
<li>域名.com_bundle.crt</li>
<li>域名.com_bundle.pem</li>
<li>域名.com.csr</li>
<li>域名.com.key</li>
</ul>
</li>
<li>将证书文件上传到群晖的 NAS 上，进入”控制面板” -&gt; “安全性” -&gt; “证书”，点击”导入”按钮，选择”从文件导入”，然后选择刚才下载的证书文件。</li>
<li>在”导入证书”页面中，选择”从文件导入”选项，然后点击”浏览”按钮，选择需要导入的证书文件。</li>
<li>证书选择 域名.com_bundle.pem，私钥选择域名.com.key。</li>
<li>点击”确定”按钮，等待证书导入完成。</li>
</ol>
<h2>特殊</h2>
<blockquote><p>由于阿里云的证书有效时间是 1 月，所有每个月都得申请，太麻烦，因此我采取曲线救国的方式。</p></blockquote>
<ol>
<li>从腾讯云申请一个免费的 SSL 证书，腾讯云的证书有效时间是 3 个月。</li>
<li>由于腾讯云需要验证 DNS 解析，所以需要在域名服务商那里添加一条 TXT 记录，验证通过后就可以申请证书了。
<img src="./assets/CleanShot-2025-05-14-at-09.11.29.png" alt="CleanShot 2025-05-14 at 09.11.29.png" /></li>
<li>进入阿里云的域名解析，添加一条 TXT 记录，内容为腾讯云提供的验证信息。</li>
<li>等待腾讯云的验证通过后，就可以申请证书了。</li>
<li>采取以上的基本流程，就可以完成替换 SSL 证书了。</li>
</ol>]]></content>
    <category term="NAS" />
    <category term="Tech" />
  </entry>
  <entry>
    <title>小小计划</title>
    <link href="https://www.ethyoung.me//posts/tiny-plan" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/tiny-plan</id>
    <updated>2025-02-27T00:00:00.000Z</updated>
    <published>2025-02-27T00:00:00.000Z</published>
    <author>
      <name>Ethan Yang</name>
    </author>
    <summary type="text">记录近期的小计划和阶段目标，涵盖 IPTV 项目开发、博客体验优化、设备选购思考以及个人时间安排上的取舍。</summary>
    <content type="html"><![CDATA[
<h2>近期计划</h2>
<p>今天，打算开始着手 IPTV 项目的开发。服务端目前先实现最基本的功能，主要是资源的检测功能。待有更好的资源后，再逐步开发后续功能。</p>
<p>同时，我也计划对博客进行升级，增加”我看过的书籍”和”电影”这两个新功能板块。今天就正式开始！</p>
<h2>关于设备</h2>
<p>其实我一直想买一台 Mac Mini，但又担心会造成性能浪费，毕竟家里通过 NAS 已经搭建了不少服务。不过 Surge 的网络管理功能确实很吸引我，这方面还是很心动的。</p>
<h2>博客优化计划</h2>
<p>在开发过程中，我注意到博客在页面切换时存在明显的闪烁问题，这影响了用户体验。计划通过以下方式解决：</p>
<ol>
<li>添加页面过渡动画效果</li>
<li>实现内容预加载机制</li>
<li>优化资源加载策略</li>
</ol>
<h2>博客计划清单</h2>
<p>接下来的博客内容规划如下：</p>
<ul>
<li> 通过 NAS 自建的各类服务介绍</li>
<li> 家庭网络架构梳理</li>
<li> IPTV 项目的开发日志</li>
<li> 博客添加 TDL（Today I Learn）板块</li>
<li> 个人作品展示页面</li>
<li> 解决页面切换闪烁问题</li>
</ul>
<h2>changelog</h2>
<ul>
<li>2025-05-29 还是无法解决博客中闪烁问题，不知道是因为 Astro 的问题还是我自己的问题。暂时先放弃了，等有时间再来解决。
<ul>
<li>博客由 Astro 切换到 Next.js，主要部署在 Vercel 上，毕竟是自家产品，而且 Next.js 的生态也更完善。</li>
</ul>
</li>
</ul>]]></content>
    <category term="Life" />
  </entry>
  <entry>
    <title>群晖安装 jenkins 遇到的问题</title>
    <link href="https://www.ethyoung.me//posts/synology-install-jenkins-tips" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/synology-install-jenkins-tips</id>
    <updated>2025-01-31T00:00:00.000Z</updated>
    <published>2025-01-31T00:00:00.000Z</published>
    <author>
      <name>Ethan Yang</name>
    </author>
    <summary type="text">记录在群晖 NAS 中通过 Docker in Docker 安装 Jenkins 遇到的问题，整理容器权限、挂载路径和启动配置的处理经验。</summary>
    <content type="html"><![CDATA[
<h2>背景：</h2>
<p>前几天在群晖上安装 jenkins 时，遇到了一些问题，记录一下。由于需要打包 docker 镜像同时通过 jenkins 进行部署。</p>
<h2>问题</h2>
<ol>
<li>首先执行 docker 命令，发现提示没有权限；</li>
</ol>
<p>此时我们需要映射群晖系统系统上的 docker.sock 文件，这样 jenkins 就可以使用 docker 命令。记住需要 root 用户，不然启动失败。</p>
<ol>
<li>其次执行 docker 命令，提示 <code>docker is not command</code>；</li>
</ol>
<p>这个问题是因为 jenkins 容器中没有安装 docker 客户端，我们需要安装 docker 客户端。</p>
<h2>解决方案</h2>
<p>群晖安装 jenkins 时，需要注意的几点：</p>
<ol>
<li>
<p>一般我们在群晖上面安装 jenkins，会使用 docker 安装，这样可以方便的管理 jenkins 的版本，同时也不会影响群晖的其他服务。</p>
</li>
<li>
<p>安装 jenkins 的时候，需要映射 docker.sock 文件，这样 jenkins 可以使用 docker 命令，同时也可以使用 docker 命令启动其他容器。</p>
<pre><code>/var/run/docker.sock:/var/run/docker.sock
</code></pre>
</li>
<li>
<p>TIPS: 安装 jenkins 的时候，需要安装 docker 客户端，这样 jenkins 可以使用 docker 命令，同时也可以使用 docker 命令启动其他容器。</p>
</li>
</ol>
<h2>如何安装 docker 客户端</h2>
<h3>第一种方式</h3>
<ol>
<li>
<p>我们首先进入 docker 容器，然后安装 docker 客户端</p>
<pre><code>docker exec -it jenkins bash
</code></pre>
</li>
<li>
<p>安装 docker 客户端</p>
<pre><code>apt-get update
apt-get install -y docker.io
</code></pre>
</li>
<li>
<p>安装完成后，我们可以使用 docker 命令</p>
<pre><code> docker --version
</code></pre>
<blockquote><p>这种情况有个问题，就是每次重启 jenkins 容器，都需要重新安装 docker 客户端。</p></blockquote>
</li>
</ol>
<h3>第二种方式</h3>
<ol>
<li>
<p>我们可以通过流水线的方式，进行安装，但是需要注意的是，我们需要在 jenkins 的容器中安装 docker 客户端，这样我们可以使用 docker 命令。</p>
<pre><code>pipeline {
    agent any
    stages {
        stage('Install Docker') {
            steps {
                sh 'apt-get update'
                sh 'apt-get install -y docker.io'
            }
        }
    }
}
</code></pre>
<blockquote><p>每次构建都会安装 docker 客户端，这样就可以使用 docker 命令。但是这种方式也有个问题，就是每次构建都会安装 docker 客户端，这样会浪费时间。</p></blockquote>
</li>
</ol>]]></content>
    <category term="NAS" />
    <category term="Tech" />
  </entry>
  <entry>
    <title>2024：转折与成长的一年</title>
    <link href="https://www.ethyoung.me//posts/2024-review" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/2024-review</id>
    <updated>2025-01-29T00:00:00.000Z</updated>
    <published>2025-01-29T00:00:00.000Z</published>
    <author>
      <name>Ethan Yang</name>
    </author>
    <summary type="text">回顾 2024 年的工作转变、生活片段和个人成长，记录阶段性选择、情绪变化、实践经验以及对未来节奏的重新规划。</summary>
    <content type="html"><![CDATA[
<h2>工作</h2>
<h3>艰难的开始</h3>
<p>年初时，我调离原先的岗位来到公司待岗，准备转战新项目。从这一刻直到 5 月份，噩梦才刚刚开始。</p>
<p>项目接手后可以说是一穷二白：没有环境、没有代码仓库，更为恐怖的是需求不明确。有些需求连客户自己也不清楚，需要我们反向讲解。就这样，我们踏上了一条破船，准备扬帆起航。</p>
<h3>人员变动</h3>
<p>我们团队最初由一个产品、两个后端、一个前端，外加一个待产的测试组成。项目开展一个月后，第一次考验来临。作为前端，我在完成与后端的接口对接后就开始独立开发。然而在开发完第一波需求页面，准备联调时，意外发生了——对接的后端同事要离职。</p>
<p>当然前期他和说过一次，毕业五年工资没涨，因此今天去提了一下，发现被拒绝了，于是准备走人。我寻思着这哥们儿走了之后，总得有人做他的活吧。就这样等了十五天，在这十五天后端的活硬生生的停滞了。我们的 leader 也在找解决办法，问了我一句：你会不会 java？我说我只看的懂一点，要是开发不太行。他说，这样啊，这个项目比较重要，而且实在找不到人了，这样你先写点，到时候公司给你部分奖金。我无法拒绝答应了，开始了磕磕绊绊的后端代码之路。</p>
<p>其实看了一波下来，发现功能不是很复杂，就是同步数据入库，难点在于对接系统的客户给的 api 文档不明确，导致很多字段对应不上。在此我也明白了那哥们儿不容易。总之一个坑。</p>
<h3>测试着重细节</h3>
<p>还有个事儿，待产的测试回家生娃了，又来一个测试。我原以为已经黑暗了，没想到，至暗时刻才刚刚开始。一开始我们和测试讨论了下，先不用在意细节，我们先把大体流程跑通，这个很关键。可是测试像是生活在另个时空，完全就没有听我们的意见，每天过来就是盯着细节测试。什么这边颜色不对，那边按钮间距有点问题，一些无伤大雅的问题。后来我们也各退一步，不管了先让她测吧，反正到时候流程问题也会测试到。就这样我们测试流程就上来了。</p>
<h3>再将一军</h3>
<p>等到交付时候的，环境出问题了。我们没有足够的机器，导致部署的时候一台环境完全不够用。问题影响呢就是客户完全看不了我们做的 demo，很多的问题。由于当前环境的不够用，外加没有人懂后端部署，由于本人有搭建自己的博客的经验，因此临时又担当部署系统的负责人。我被迫放弃现在手头的活花了三天时间，将测试环境搭建完成。</p>
<h3>彻底失衡</h3>
<p>时间来到交付的阶段，问题累计是显而易见的。无法交付！功能没有打通。我们 4 个人之后开启了地狱般的加班模式。连续一周的通宵，那几天让我想到了什么叫生不如死，萌生离职的想法。在这样我的身体肯定是吃不消的。</p>
<h3>转机</h3>
<p>也许上天的眷顾吧，一天别的项目需要一个资深前端，让我去面试了，那边很满意。让我尽快参与到项目中去。这边的工作我就全部交出。我就在接下来的几天编写交接文档和部署文档。这样在一周后我就投入了下个项目中，算是临时解脱了。</p>
<h3>来到另一天地</h3>
<p>来到新团队后，一切都变得不同。不用加班，完成既定工作就能准时下班，不必承担额外工作。在这里，我学到了很多新知识（不仅仅是前端相关）。同事们都很专业，每个人都致力于将问题处理得尽善尽美。</p>
<p>我的问题处理思路也发生了改变：从原来的简单修复，转变为先理解问题的根本原因再进行修复。在开发需求时，更注重”可扩展性和面向对象”的思想。当时还和同事讨论为什么在前端逻辑中使用面向对象编程——虽然前端通常推崇函数式编程，但由于项目的”历史债务问题”（没有使用 TypeScript 进行类型定义），现在只能通过 class 方式来确保数据的一致性。再加上 Vue2 对 TypeScript 的支持有限，最终我们选择了这种折中的方案。</p>
<h2>博客篇</h2>
<p>我将框架由原先的 Next.js 换成了 Astro，原因在于它更加方便、简洁、迅速。此外域名续费买了三年，算是作为接下来写博客的动力吧。</p>
<h3>新计划</h3>
<p>接下来博客还是持续更新，包括功能，也许我会增加模块：</p>
<ol>
<li><strong>TIL（Today I Learned）</strong>：记录每天的学习收获</li>
<li><strong>项目进展</strong>：记录正在进行的项目。这样的记录既能减轻写作压力，又可以积累经验，方便未来回顾。</li>
</ol>
<h2>生活</h2>
<h3>家庭</h3>
<ul>
<li><strong>儿子</strong>：逐年长大的小家伙，兴趣爱好越来越多。喜欢积木，喜欢奥特曼（和老子一个德行），今年给他买了不少玩具
<img src="assets/679a3d7fa003f.png" alt="在线压缩图片 IMG 6730.png" /></li>
<li><strong>老婆</strong>：和往常一样，日常上班，下班刷剧，没什么特别的兴趣</li>
</ul>
<h3>数码生活</h3>
<p>迈入了典型的中年兴趣领域：</p>
<ul>
<li><strong>NAS</strong>：使用 Synology 920+</li>
<li><strong>充电器</strong>：Anker 240W</li>
<li><strong>路由器</strong>：软路由，刷了 iostoreOS
<img src="assets/679a3df74fe6b.png" alt="1738161652624.png" /></li>
<li><strong>智能家居</strong>：搭建了 HA，连接 Apple HomeApp 和米家应用</li>
<li><strong>HomeLab</strong>：搭建了一个轻量级的环境，支持日常应用使用（虽然在影音解码上还有些问题）</li>
</ul>
<h3>展望未来</h3>
<p>给自己立个 flag：完成一直想做的 IPTV 应用。虽然之前几次都半途而废，但 2025 年一定要推出第一个版本！</p>
<h2>年终感悟</h2>
<p>时光飞逝，白驹过隙。转眼间从校园毕业已有 10 年，从一个热血少年变成了一个略显发福的中年人。有时坐在阳台上，回想起当年在银杏树下的自己，不知是否曾经畅想过今天的模样。</p>
<h3>告别 2024</h3>
<ul>
<li>后疫情时代的一年</li>
<li>略显疲惫的一年</li>
<li>不知不觉溜走的一年</li>
</ul>]]></content>
    <category term="总结" />
    <category term="Life" />
  </entry>
  <entry>
    <title>记录一次在 Namesilo 上的封禁</title>
    <link href="https://www.ethyoung.me//posts/namesilo-ban" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/namesilo-ban</id>
    <updated>2025-01-23T00:00:00.000Z</updated>
    <published>2025-01-23T00:00:00.000Z</published>
    <author>
      <name>Ethan Yang</name>
    </author>
    <summary type="text">记录在 Namesilo 注册域名时遇到账号封禁的处理过程，整理排查步骤、沟通经验、风险提醒和后续域名管理注意事项。</summary>
    <content type="html"><![CDATA[
<p>去年的时间点，我在 Namesilo 上注册了一个账号, 用来注册域名。由于一些原因，我在 Namesilo 上注册的域名被封禋了。这里记录一下这次封禁的经过。</p>
<h2>注册原因</h2>
<p>生活在国内，在使用任何公共服务你都的需要备案; 而且备案的过程也是非常繁琐的， 因此在国外网站注册域名是一个非常好的选择。</p>
<h2>封禁原因</h2>
<p>一天早上邮箱收到如下信息：</p>
<img src="assets/67930b801fe95.png" alt="1737689980932.png" />
<blockquote><p>原因是我的信息不完整，需要我提供一些信息。</p></blockquote>
<p>想起来当时注册信息的时候，我确实没有填写真实的信息，因为我不想让我的信息泄露出去。</p>
<h2>解封</h2>
<p>不知道如何解封，直接在回了一封邮件；寻问如何解封？ 用 chatGPT 写了封英文邮件；</p>
<p><img src="assets/67930c46b5511.png" alt="1737690178829.png" />
原以为会要等几天，没想到几个小时过后，收到回复。</p>
<img src="assets/67930e9de6cd1.png" alt="1737690779910.png" />
<p>按照邮件给出的地址，填写相关信息，等了几个小时，域名就解封了。</p>
<h2>TIPS</h2>
<p>在域名被封禁的情况下，域名管理界面是没有已经购买的域名的；</p>]]></content>
    <category term="Domain" />
    <category term="Namesilo" />
  </entry>
  <entry>
    <title>博客使用 view-transition-api 添加黑暗模式</title>
    <link href="https://www.ethyoung.me//posts/custom-darkmode" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/custom-darkmode</id>
    <updated>2025-01-08T00:00:00.000Z</updated>
    <published>2025-01-08T00:00:00.000Z</published>
    <author>
      <name>Ethan Yang</name>
    </author>
    <summary type="text">记录在博客中使用 view-transition-api 实现黑暗模式切换时的实现细节和遇到的问题，分享相关经验和解决方案。</summary>
    <content type="html"><![CDATA[
<p>最近进行博客重构，因此参照了 <a href="https://antfu.me/" rel="noopener noreferrer" target="_blank">antfu</a> 大神的博客的样式。</p>
<p>基本样式参照了其中，可是对于其中黑暗模式切换，被深深的吸引了，如下：</p>
<img src="assets/677e9ae05ca6f.gif" alt="CleanShot 2025-01-08 at 23.32.33.gif" />
<p>当时在想，这个是怎么做到的，为何次动画如此丝滑，我得研究下；
<img src="assets/67828a6c00acf.png" alt="1736608357113.png" /></p>
<p>放弃了～～～</p>
<p>直接吧啦源码，看到如下关键信息：</p>
<pre><code>::view-transition-old(view-transition-name)
::view-transition-new(view-transition-name)
</code></pre>
<p>印象中，这段使用的是 <strong>View Transition API</strong> 中的内容；于是乎去翻阅各种文档；发现大佬这边文章 <a href="https://arc.net/l/quote/lnuwtrci" rel="noopener noreferrer" target="_blank">页面级可视动画 View Transitions API 初体验</a>，大佬深入浅出详细说明了此 API 的作用以及使用；这里不做过多的说明；</p>
<blockquote><p>mark：View Transitions API 简化了复杂动画的实现，无需手动处理位置计算或动画控制，尤其适合页面级的场景切换和动画增强。</p></blockquote>
<img src="assets/6783c733778da.png" alt="1736689455217.png" />
<h3>流程图说明</h3>
<ol>
<li><strong>触发动画</strong>：用户通过调用 <code>document.startViewTransition()</code> 开始动画。</li>
<li><strong>捕获状态快照</strong>：浏览器在 DOM 更新前后分别捕获旧状态和新状态。</li>
<li><strong>生成动画</strong>：浏览器对比新旧快照的差异，生成过渡动画。</li>
<li><strong>执行动画</strong>：根据 CSS 控制动画的伪元素定义的规则执行动画。</li>
<li><strong>动画完成</strong>：动画结束后更新页面状态。</li>
<li><strong>移除伪元素</strong>：动画伪元素被移除，最终状态呈现。</li>
</ol>
<h2>如何扩散</h2>
<p>从上图显示效果而言，扩散是从一个点扩散到浏览器整体视窗；而视窗的最大半径我们可以通过鼠标点击或者 touch 事件来触发获取元素的 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect" rel="noopener noreferrer" target="_blank"><strong><code>Element.getBoundingClientRect()</code></strong></a>：</p>
<pre><code>const rect = this.themeLabel?.getBoundingClientRect()
</code></pre>
<p>boundingClientRect 包含两个 x 和 y，代码当前相对于视窗相对位置，借用 MDN 图：</p>
<img src="assets/element-box-diagram.png" alt="Pasted image 20250111235731.png" />
<p>而此时我们需要计算由我们点击位置向外扩散圆的半径；如下图：</p>
<img src="assets/678297d3cd958.png" alt="1736611793245.png" />
<pre><code>const radius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y))
</code></pre>
<ul>
<li><code>Math.hypot</code> 计算直角三角形的斜边长度，确保动画覆盖整个视窗。</li>
<li><code>Math.max</code> 确定从触发点到视窗边缘的最远距离，确保动画从中心点覆盖整个页面。</li>
</ul>
<h3>关于 <code>innerWidth</code></h3>
<p><code>innerWidth</code> 是一个只读属性，返回窗口的文档显示区的宽度（以像素为单位）。它包括滚动条的宽度（如果有）。在计算动画扩散半径时，我们使用 <code>innerWidth</code> 来确定从触发点到视窗边缘的最远距离。</p>
<p>接下来我们定义动画绘画路径：</p>
<pre><code>const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${radius}px at ${x}px ${y}px)`]
</code></pre>
<ul>
<li><code>clipPath</code> 是一个数组，表示动画的起始和结束状态：
<ul>
<li>起始状态：半径为 0 的圆（即无显示）。</li>
<li>结束状态：覆盖整个页面的圆（最大半径）。</li>
</ul>
</li>
</ul>
<h3>创建动画</h3>
<pre><code>await document.documentElement.animate(
  { clipPath: currentTheme === 'dark' ? clipPath.reverse() : clipPath },
  {
    duration: 350,
    easing: 'ease-out',
    pseudoElement: currentTheme === 'dark' ? '::view-transition-old(root)' : '::view-transition-new(root)',
  }
).finished
</code></pre>
<p><strong>动画执行效果</strong></p>
<ol>
<li><strong>切换到深色模式</strong>：
<ul>
<li>动画从大圆过渡到小圆（反转 <code>clipPath</code>）。</li>
<li>动画目标是 <code>::view-transition-new(root)</code>。</li>
</ul>
</li>
<li><strong>切换到浅色模式</strong>：
<ul>
<li>动画从小圆过渡到大圆。</li>
<li>动画目标是 <code>::view-transition-new(root)</code>。</li>
</ul>
</li>
</ol>
<p>结合上面的流程图，在 <code>view-transition</code> 之后，使用 <code>pseudoElement</code> 精细控制新旧主题之间的过渡。</p>
<p>最终代码如下：【本次使用的是 WebComponent 进行组件的抽取】</p>
<pre><code>&lt;script&gt;
import { themeAtom } from "~/store";

class ThemeSwitcher extends HTMLElement {
  private themeLabel: HTMLLabelElement | null = null;
  private themeInput: HTMLInputElement | null = null;

  constructor() {
    super();
    this.initTheme();
  }

  private getSystemTheme(): "light" | "dark" {
    return window.matchMedia("(prefers-color-scheme: dark)").matches
      ? "dark"
      : "light";
  }

  private initTheme(): "light" | "dark" {
    const localTheme = window.localStorage.getItem("theme");
    return localTheme === "auto"
      ? this.getSystemTheme()
      : (localTheme as "light" | "dark") || "light";
  }

  private updateTheme(theme: "light" | "dark"): void {
    document.documentElement.classList.toggle("dark", theme === "dark");
    document.documentElement.style.colorScheme = theme;
    document.documentElement.setAttribute("data-theme", theme);
    this.themeLabel?.classList.toggle("swap-active", theme === "light");
    themeAtom.set(theme);
  }

  private async animateThemeTransition(x: number, y: number): Promise&lt;void&gt; {
    const currentTheme = this.initTheme();
    const radius = Math.hypot(
      Math.max(x, innerWidth - x),
      Math.max(y, innerHeight - y)
    );

    const clipPath = [
      `circle(0px at ${x}px ${y}px)`,
      `circle(${radius}px at ${x}px ${y}px)`,
    ];

    try {
      await document.documentElement.animate(
        { clipPath: currentTheme === "dark" ? clipPath.reverse() : clipPath },
        {
          duration: 350,
          easing: "ease-out",
          pseudoElement:
            currentTheme === "dark"
              ? "::view-transition-old(root)"
              : "::view-transition-new(root)",
        }
      ).finished;
    } catch (error) {
      console.error("Animation failed:", error);
    }
  }

  connectedCallback(): void {
    this.themeLabel = this.querySelector("label");
    this.themeInput = this.querySelector("input");

    if (!this.themeLabel || !this.themeInput) {
      console.error("Required elements not found");
      return;
    }

    const currentTheme = this.initTheme();
    this.updateTheme(currentTheme);

    this.themeInput.addEventListener("click", async (event) =&gt; {
      const currentTheme = this.initTheme();
      const newTheme = currentTheme === "dark" ? "light" : "dark";

      try {
        const transition = document.startViewTransition(async () =&gt; {
          this.updateTheme(newTheme);
          window.localStorage.setItem("theme", newTheme);
        });

        await transition.ready;
        const rect = this.themeLabel?.getBoundingClientRect();
        if (rect) {
          await this.animateThemeTransition(rect.x, rect.y);
        }
      } catch (error) {
        console.error("Theme switch failed:", error);
        // 回退方案
        this.updateTheme(newTheme);
        window.localStorage.setItem("theme", newTheme);
      }
    });
  }

  disconnectedCallback(): void {
    this.themeInput?.removeEventListener("click", () =&gt; {});
  }
}

customElements.define("switch-theme", ThemeSwitcher);
&lt;/script&gt;
</code></pre>]]></content>
    <category term="Web" />
    <category term="CSS" />
  </entry>
  <entry>
    <title>十月随笔</title>
    <link href="https://www.ethyoung.me//posts/october-essay" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/october-essay</id>
    <updated>2024-10-23T00:00:00.000Z</updated>
    <published>2024-10-23T00:00:00.000Z</published>
    <author>
      <name>Ethan Yang</name>
    </author>
    <summary type="text">一篇关于时间、孤独和自我感受的生活随笔，记录在安静时刻里的情绪流动、内心观察和对日常节奏的个人理解。</summary>
    <content type="html"><![CDATA[
<p>今日偶听《知我》，心中竟泛起涟漪，仿若一池春水被细雨轻扰。那久违的醉酒青春，在旋律间悄然复苏，少时的梦想，竟在刹那间被唤醒。人生在世，几许沧桑，几度无常，但无非是尽力过好自己罢了。</p>
<p>时常迷茫，不知前路何在，亦不晓心之所欲。倒不如停下脚步，将喧嚣隔绝，静心回望，细细品味那曾踏过的泥泞与辉煌。人这一生，是否必须不停向前？也许，并不需要。偶尔坐下来，倚一片青山，看众人行过，听风吹叶落，或许便是另一种圆满。</p>
<p>我不求同行者，不愿奔波争渡。只想独自坐在岁月的岸边，看时间从指间悄然流走，在沉静中，让心灵与天地共老。</p>]]></content>
    <category term="Life" />
    <category term="Essay" />
  </entry>
  <entry>
    <title>Docker 入门</title>
    <link href="https://www.ethyoung.me//posts/docker-tutorial" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/docker-tutorial</id>
    <updated>2024-05-06T00:00:00.000Z</updated>
    <published>2024-05-06T00:00:00.000Z</published>
    <author>
      <name>Ethan Yang</name>
    </author>
    <summary type="text">面向前端开发者整理 Docker 入门知识，覆盖镜像、容器、Dockerfile、常用命令和本地开发部署中需要理解的基础概念。</summary>
    <content type="html"><![CDATA[
<h2>是什么？</h2>
<p>想象一下，你要搬家，却不想一件件搬所有的家当，而是把所有东西都装进一个统一的箱子，不管目的地是哪个城市，随时打开箱子就能继续生活。Docker 就是这样的”箱子”！它是一种开源的容器化技术，可以把应用程序和它需要的环境”打包”起来，让它能随时随地跑起来，无需担心”跑不动”。</p>
<p>Docker 的主要特点包括：</p>
<ol>
<li><strong>轻量级</strong>：Docker 容器共享主机的操作系统内核，因此相比于虚拟机，它们占用的资源更少，启动更快速。</li>
<li><strong>可移植性</strong>：Docker 容器可以在任何支持 Docker 的环境中运行，无论是开发、测试还是生产环境，保持一致性。</li>
<li><strong>快速部署</strong>：Docker 镜像包含了应用程序及其所有依赖项，因此可以快速部署和启动容器，无需进行繁琐的配置和安装。</li>
<li><strong>环境一致性</strong>：Docker 容器将应用程序与其依赖项打包在一起，确保在不同环境中运行时具有一致的行为。</li>
<li><strong>资源隔离</strong>：Docker 使用 Linux 内核的容器功能，实现了容器之间的资源隔离，保证容器之间互不影响。</li>
</ol>
<h2>概念</h2>
<ul>
<li>镜像（Image）：像做饭的”菜谱”，告诉你怎么搭建应用。</li>
<li>容器（Container）：根据”菜谱”做出来的实际饭菜，一个运行中的实例。</li>
<li>仓库（Repository）：存储”菜谱”的地方，随时可以拿出来用，比如全球知名的”菜谱库”——Docker Hub。</li>
</ul>
<h2>如何工作</h2>
<p>这里借用图片 <a href="http://blog.bytebytego.com/" rel="noopener noreferrer" target="_blank">bytebytego</a>
<img src="assets/docker-diagram.jpeg" alt="Untitled.jpeg" /></p>
<h3>Docker 的工作原理</h3>
<ol>
<li>编写一个 Dockerfile
它是一张”TODO 清单”，告诉 Docker 需要用什么基础环境、拷贝哪些代码、安装哪些依赖。</li>
<li>构建一个 镜像
Docker 根据 Dockerfile 把你的应用和环境”打包”成一个文件。</li>
<li>运行 容器
用镜像”生成”一个容器，容器里跑的就是你的应用，随时可以启动、停止。</li>
</ol>
<h4>编写 Dockerfile</h4>
<p>Dockerfile 是一个文本文件，包含了一条条的指令（Instruction），每一条指令构建一层，因此每一条指令的内容都会对镜像产生影响。Dockerfile 的基本格式如下：</p>
<pre><code># Base image
FROM node:14    # 指定基础镜像

# Author
MAINTAINER matrixpunk &lt;
# Set working directory
WORKDIR /app    # 设置工作目录

# Copy source code
COPY . /app     # 拷贝文件

# Install dependencies
RUN npm install # 安装依赖

# Expose port
EXPOSE 3000     # 暴露端口

# Start app
CMD ["npm", "start"] # 启动命令

</code></pre>
<h4>构建 Docker 镜像</h4>
<pre><code>docker build -t my-node-app .
</code></pre>
<h4>运行 Docker 容器</h4>
<pre><code>docker run -d -p 3000:3000 my-node-app
</code></pre>
<blockquote><p>运行 docker 容器时，可以使用 <code>-d</code> 参数让容器在后台运行，<code>-p</code> 参数指定端口映射。
剩余参数如下：</p><ul>
<li><code>-i</code>：以交互模式运行容器</li>
<li><code>-t</code>：分配一个伪终端</li>
<li><code>--name</code>：指定容器名称</li>
<li><code>-v</code>：挂载数据卷</li>
<li><code>--rm</code>：容器停止后自动删除</li>
</ul></blockquote>
<h3>其余命令</h3>
<h4>查看所有容器</h4>
<pre><code>docker ps -a
</code></pre>
<h4>停止容器</h4>
<pre><code>docker stop &lt;container_name&gt;
</code></pre>
<h4>删除容器</h4>
<pre><code>docker rm &lt;container_name&gt;
</code></pre>
<h4>列举所有容器</h4>
<pre><code>docker ps
</code></pre>
<h4>列举所有容器 (包括停止的)</h4>
<pre><code>docker ps -a
</code></pre>
<h4>列举所有容器 (包括停止的)</h4>
<pre><code>docker ps -a
</code></pre>
<h4>列举所有镜像</h4>
<pre><code>docker images
</code></pre>
<h4>删除镜像</h4>
<pre><code>docker rmi &lt;image_name&gt;
</code></pre>
<h4>拉取 Docker 镜像</h4>
<pre><code>docker pull &lt;image_name&gt;
</code></pre>
<h4>推送 Docker 镜像</h4>
<pre><code>docker push &lt;image_name&gt;
</code></pre>
<h4>查看 Docker 容器信息</h4>
<pre><code>docker inspect &lt;container_name&gt;
</code></pre>
<h4>查看 Docker 容器日志</h4>
<pre><code>docker logs &lt;container_name&gt;
</code></pre>
<h4>Docker 容器操作</h4>
<pre><code>docker exec &lt;container_name&gt; &lt;command&gt;
</code></pre>
<h4>进入 Docker 容器</h4>
<pre><code>docker exec -it &lt;container_name&gt; /bin/bash
</code></pre>
<blockquote><p>当我们使用 docker 构建多个镜像时，我们可以使用 docker-compose 来管理多个容器</p></blockquote>
<h3>Docker Compose</h3>
<p>如果你的项目涉及多个容器，比如一个跑应用、一个跑数据库，那么你需要 Docker Compose。它像一本多菜谱的菜单，一键可以上齐所有菜。</p>
<ul>
<li><strong>多容器应用程序</strong>：当您的应用程序由多个容器组成时，可以使用 Docker Compose 来定义、管理和运行这些容器。</li>
<li><strong>开发环境</strong>：在开发过程中，使用 Docker Compose 可以轻松地设置开发环境，包括数据库、缓存和其他服务，以便团队成员可以快速启动整个开发环境。</li>
<li><strong>测试环境</strong>：您可以使用 Docker Compose 在测试环境中快速部署和管理多个容器，以便进行集成测试和端到端测试。</li>
<li><strong>简化部署</strong>：通过在生产环境中使用 Docker Compose，您可以轻松地部署整个应用程序栈，而不必手动设置每个容器。</li>
<li><strong>快速原型</strong>：使用 Docker Compose 可以快速创建原型和演示环境，而无需手动安装和配置多个服务。</li>
</ul>
<blockquote><p>通常用于本地开发环境，生产环境建议使用 Docker Swarm 或 Kubernetes。</p></blockquote>
<h4>使用 Docker Compose</h4>
<blockquote><p>确保当前目录存在 docker-compose.yml compose.yml 文件
10.10.10.10 服务器的 docker-compose.yml 文件在/home/ocs/docker 目录下</p></blockquote>
<h4>Yml 文件如下，以及参数说明</h4>
<pre><code>version: '3' # 版本
services:
  sample: # 服务名
    build: # 构建镜像目录
      context: ./
      dockerfile: ./docker/Dockerfile
      args:
        NODE_ENV: production
    restart: always # 是否重启之后进行重启
    image: sampleName #  镜像名
    ports: # 端口
      - 3007:80 # 映射端口方式   -&gt;  主机端口:容器端口
    container_name: sampleNameContainer # 容器名
    environment: # 环境变量
      - NGINX_PORT=80
      - API_ENV=api
      - API_URL=http://10.10.10.10:8081/horizon/
      - NODE_ENV=production
      - WEBAPP=horizon
    volumes: # 映射路径地址 -&gt;  主机路径：容器路径
      - /home/nginx/conf.d:/etc/nginx/conf.d
    command: /bin/sh -c "envsubst '$$API_ENV,$$NGINX_PORT,$$API_URL,$$WEBAPP' &lt; /etc/nginx/conf.d/https.template &gt; /etc/nginx/conf.d/default.conf  &amp;&amp; exec nginx -g 'daemon off;'" # 启动命令
    networks: # 虚拟网络名字
      - app-net
networks:
  app-net:
    external:
      name: app-net
</code></pre>
<h4>启动</h4>
<pre><code>docker-compose start 服务名
</code></pre>
<h4>停止</h4>
<pre><code>docker-compose stop 服务名
</code></pre>
<h4>删除容器</h4>
<pre><code>docker-compose rm 服务名
</code></pre>
<h4>构建镜像</h4>
<pre><code>docker-compose build 服务名
</code></pre>
<h4>构建容器</h4>
<pre><code>docker-compose up -d 服务名
</code></pre>
<h4>查看日志 最后 500 行</h4>
<pre><code>docker-compose logs --tail 500 服务名
</code></pre>
<h3>参考</h3>
<ul>
<li><a href="https://docs.docker.com/" rel="noopener noreferrer" target="_blank">Docker 官方文档</a></li>
<li><a href="https://yeasy.gitbook.io/docker_practice/" rel="noopener noreferrer" target="_blank">Docker — 从入门到实践</a></li>
<li><a href="https://www.runoob.com/docker/docker-tutorial.html" rel="noopener noreferrer" target="_blank">Docker 教程</a></li>
</ul>]]></content>
    <category term="Docker" />
    <category term="DevOps" />
  </entry>
  <entry>
    <title>使用 ncc 打包编译 NestJS 的问题记录</title>
    <link href="https://www.ethyoung.me//posts/ncc-build-node" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/ncc-build-node</id>
    <updated>2022-06-22T00:00:00.000Z</updated>
    <published>2022-06-22T00:00:00.000Z</published>
    <author>
      <name>Ethan Yang</name>
    </author>
    <summary type="text">记录在使用 ncc 打包编译 nestjs 时遇到的问题及解决方案，特别是与 hbs 模版引擎相关的配置调整。</summary>
    <content type="html"><![CDATA[
<h2>背景：</h2>
<p>最近在使用<a href="https://github.com/vercel/ncc" rel="noopener noreferrer" target="_blank">ncc</a> 打包编译 nestjs，由于 nestjs 使用 hbs 作为模版引擎。</p>
<p>原先官网样例：</p>
<pre><code>npm install --save hbs
</code></pre>
<pre><code>import { NestFactory } from '@nestjs/core'
import { NestExpressApplication } from '@nestjs/platform-express'
import { join } from 'path'
import { AppModule } from './app.module'

async function bootstrap() {
  const app = await NestFactory.create&lt;NestExpressApplication&gt;(AppModule)

  app.useStaticAssets(join(__dirname, '..', 'public'))
  app.setBaseViewsDir(join(__dirname, '..', 'views'))
  //直接使用设置模版
  app.setViewEngine('hbs')

  await app.listen(3000)
}
bootstrap()
</code></pre>
<p>这样通过 ncc 编译，会存在丢失问题：</p>
<p>因此做如下设置即可：</p>
<pre><code>import { NestFactory } from '@nestjs/core'
import { NestExpressApplication } from '@nestjs/platform-express'
import { join } from 'path'
import { AppModule } from './app.module'
import * as HBS from 'hbs'

async function bootstrap() {
  const app = await NestFactory.create&lt;NestExpressApplication&gt;(AppModule)
  app.useStaticAssets(join(__dirname, '..', 'public'))
  app.setBaseViewsDir(join(__dirname, '..', 'views'))
  //手动重写
  app.set('view engine', 'hbs')
  app.engine('hbs', HBS.__express)

  await app.listen(3000)
}
bootstrap()
</code></pre>
<p>通过将其引入，再次编译即可。</p>]]></content>
    <category term="Nodejs" />
    <category term="Tech" />
  </entry>
  <entry>
    <title>所谓谣言</title>
    <link href="https://www.ethyoung.me//posts/rumors" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/rumors</id>
    <updated>2019-12-22T00:00:00.000Z</updated>
    <published>2019-12-22T00:00:00.000Z</published>
    <author>
      <name>Ethan Yang</name>
    </author>
    <summary type="text">围绕“谣言”与预警信号的关系展开的一篇生活随笔，思考信息判断、风险意识和在不确定环境中保持清醒的方式。</summary>
    <content type="html"><![CDATA[
<p>今天听说有传言说 2003 年的”非典”又回来了，还提到一些武汉的消息，说是有人传播病毒，但没有证据，应该只是传言。</p>
<p>2003 年非典的时候，我还在上小学，印象很深。每天进学校，老师都会拿着电子温度计给我们测体温。那时候，非典传播得很快，很多人感染了，新闻里说病毒是因为有人吃野味传播的。大家都很害怕，不敢出门，也不敢去人多的地方。记得老爸那时想从外地回家，奶奶硬是把他劝住了。</p>
<p>现在听到类似的消息，心里也不免有些担心，怕会像 2003 年一样，病毒快速传播，很多人受影响。</p>
<p>微博上看到不少人对李文亮先生的指责，说他散布谣言，但我觉得他只是出于好意，提醒大家注意防护。这些指责好像都没有真正的依据，只是站在一种权威的立场上批评他。</p>
<p>我当时就和老婆说，先去买些口罩吧，毕竟多做准备总没坏处。</p>
<h4>似乎有些’谣言’，成了 ‘遥遥领先的预言’</h4>
<blockquote><p>在面对不确定的信息时，与其急于否定，不如多一分审慎和反思，毕竟防患于未然远比亡羊补牢来得重要。</p></blockquote>]]></content>
    <category term="Life" />
  </entry>
  <entry>
    <title>2018 年终总结</title>
    <link href="https://www.ethyoung.me//posts/2018-year-end" rel="alternate" type="text/html"/>
    <id>https://www.ethyoung.me//posts/2018-year-end</id>
    <updated>2018-12-04T00:00:00.000Z</updated>
    <published>2018-12-04T00:00:00.000Z</published>
    <author>
      <name>Ethan Yang</name>
    </author>
    <summary type="text">回顾 2018 年的工作、生活、旅行和个人成长，用年终总结的方式记录忙碌奔跑的一年，以及迈向 2019 年的新计划。</summary>
    <content type="html"><![CDATA[
<p>天，越来越冷，听说过几天还要下雪。想想 2018 年就快要过去，满心的不舍。写点东西，就算是年终总结。</p>
<p>最近几天想想，时间过的真快啊，再过两年三十了，都说是三十而立。可是总感觉在某些自己还没有准备好。比如说自己工资还赶不上花销。好像这一年所有的事情都在催着往前走！像是一头被鞭子抽了之后的疯牛，漫无目的，口吐白沫；疯狂地撒丫子向前狂奔。</p>
<p>这一年计划，想想完了 90%，这里心里还有些欣慰！</p>
<p>有时候发现，年龄越大，认识的事情方式也随着改变。看到一些不爽的事物，在以前总会义愤填膺。现在想想，何必呢，有些事情你是无法改变的；还不如顺其自然，人嘛！对得起自己，问心无愧吧。</p>
<p>就在前一天，自己还想着减肥呢。可是懒啊！只能慢慢来了。</p>
<h2>1 月 2 日，领取结婚证 😃</h2>
<p>去年过年前夕，在老丈人家举办了婚礼。原本打算把领取结婚证日子往后拖拖，可是父母一直在那儿催着，没办法啊。</p>
<p>这天，天空灰蒙蒙的，还是很冷。于是早上起了大早，喊上了思思，走领证去。</p>
<p>骑着老妈的小电驴，以 30 迈的速度晃悠悠的向着民政局走着！到了民政局，本以为会像电视剧里面一样：去排队号，等待叫号递交材料，宣誓，最后盖上钢印。工作人员会说一下：恭喜二位哈。</p>
<p>看来是我想多了，在我们还没有照相时，还没有等我们坐好，一张照片就好了，我俩还没有整理呢。太快了！然后收取了一个强制购买盒子。妈蛋啊！50 啊。</p>
<p>不过不去想这么多了，因为迈入了一个里程了！我结婚了。</p>
<h2>5 月 9 日，举办婚礼 😃</h2>
<p>怎么说呢，不怎么喜欢这个日子；刚刚过完五一假，然后再去请婚假，而且好多亲朋好友都得要请个假才能过来，怪麻烦人家的！奈何没有办法，老妈算的日子啊。</p>
<p>其实我一直认为婚礼就是过个形式，有没有都无所谓的；主要大家在一起吃个饭就行。后来我发现我错了。</p>
<p><code>生活中，有时候还是需要一些仪式感</code></p>
<p>白天零零碎碎的忙碌，都是为晚上正餐做准备的。</p>
<p>晚上，婚礼开始了；参加过好多别人的婚礼，没有想过自己的婚礼会是什么样式。晚宴之前，司仪把我俩叫过去，去对流程。</p>
<p>… 说了很多，没怎么记得。因为有点紧张，手心一直在冒汗。交流结束，还好老婆在旁边提醒着，稍微记得一些。晚上接近八点，婚礼开始了。</p>
<p>在她牵着父亲的手，来到我身边时。</p>
<p>也许是旁边音乐的原因</p>
<p>也许是朋友的祝福</p>
<p>或许是现场的气氛</p>
<p>那一刻我明白，我明白了我守护的人，余生的牵绊的人，就是她了。我的妻子！</p>
<p>那一刻，说实话我差点儿就哭了。只不过我忍住了。</p>
<h2>度蜜月 😚</h2>
<p>其实我是想去看山的，而她要去看海；综合对比了下家庭地位，我输了！当然了看海就去<strong>三亚</strong>，<strong>毛里求斯</strong>等地方了，由于办理签证需要时间，加上的咱的假期就剩下 12 天了。最终决定了去<strong>三亚</strong>。</p>
<p>飞机 😄</p>
<p>长这么大，还没有坐过飞机，因此还有些期待！嗯！期待着上天！</p>
<p>原本原为飞机以为飞机应该和电视剧里面一样是那种大飞机！可事实呢。好小啊。</p>
<p>算了！！</p>
<p>飞起~~~~</p>
<p>嗯~~ 天很蓝，没有雾霾，我喜欢！</p>
<img src="assets/82s74kD.jpg" alt="蓝天" />
<p>呼呼了两个小时，我们来到了三亚。下完飞机！走在的沿海的沙滩上面。哇！真的是海天一色。</p>
<img src="assets/MM4AnMD.jpg" alt="海天一色" />
<p><strong>第二天</strong></p>
<p>来到蜈支洲岛</p>
<p>感受到了三亚的太阳，真毒！就昨天下飞机后三个小时，我的脚背已经晒伤了！</p>
<img src="assets/V2xiVWg.jpg" alt="蜈支洲岛" />
<img src="assets/dvLFAOA.jpg" alt="蜈支洲岛" />
<p>下午看了下三亚千古情：</p>
<img src="assets/OilORNg.jpg" alt="三亚千古情" />
<p>晚上吃了一顿海鲜大餐(其实我内心在说，真少，不够吃啊)：</p>
<img src="assets/uqclQ64.jpg" alt="海鲜大餐" />
<p><strong>第三天</strong></p>
<p>我们早上来到了<strong>玫瑰谷</strong>，感觉这里不像是景区，倒是人家的产业园。</p>
<p>各种各样的玫瑰，顺便科普到一个知识：玫瑰和月季，其实是同一个物种！有点惊讶。</p>
<p>白玫瑰</p>
<img src="assets/6HRxxCN.jpg" alt="白玫瑰" />
<p>红玫瑰</p>
<img src="assets/S03O9kn.jpg" alt="红玫瑰" />
<p>上了一座山，具体不知道叫啥，站在山上看海吧！！</p>
<img src="assets/ruyguAH.jpg" alt="山上看海" />
<img src="assets/9OKXDWd.jpg" alt="山上看海" />
<p><strong>拽根</strong>的雕塑</p>
<img src="assets/2RcGb1B.jpg" alt="拽根雕塑" />
<p><strong>亚龙湾</strong>，好多水上运动，很可惜没有玩啊</p>
<img src="assets/8yGC3LX.jpg" alt="亚龙湾" />
<p>晚上，坐着大船看看，经典建筑</p>
<img src="assets/4hqbas2.jpg" alt="经典建筑" />
<img src="assets/mzCah3d.jpg" alt="经典建筑" />
<img src="assets/sAW0HZc.jpg" alt="经典建筑" />
<p><strong>第四天</strong>，早早的起床，去南山（佛教圣地）</p>
<img src="assets/LsB3d4v.jpg" alt="南山" />
<blockquote><p>只让拍这里。。。。。</p></blockquote>
<p>下午，我们来到了<strong>天涯海角</strong>,貌似是海南的最南端了</p>
<img src="assets/V2Nxqyy.png" alt="天涯海角" />
<img src="assets/uuGwVTk.jpg" alt="天涯海角" />
<p>让她很自然的笑</p>
<img src="assets/3soN1ZM.jpg" alt="自然的笑" />
<p>就这样的第四天行程结束！意味着三亚之行也就结束了。</p>
<p>坐飞机回家~~~~~~好累啊</p>
<img src="assets/tehgBaK.jpg" alt="坐飞机回家" />
<p>趁着夜色的降临</p>
<img src="assets/8mg9kGN.jpg" alt="夜色" />
<p>结束了这一次的旅程！！！</p>
<img src="assets/DGCKknW.jpg" alt="2018再见" />
<p>不过还有一点；就是很累啊！😩</p>
<h2>买车子 😄</h2>
<p>其实买车子，原本不再考虑范围的，但是考虑到老丈人家，比较远。大过年的拎着东西回家，很不方便。于是乎着手去买车。</p>
<p>或许因为自己的不太懂车，买车的速度堪比买鞋了！快，😄</p>
<p>买车的全程都是销售帮我看，算账啊。我们俩客客气气的，弄的销售都有点不好意思。不过感觉遇到了一个好人吧。好多注意点，销售都帮我们处理的比较好。没有任何担心！</p>
<p><strong>第一次开车</strong>，</p>
<img src="assets/Cwmeyh5.jpg" alt="第一次开车" />
<p>老婆在旁边全程紧张 😱，能够明显感觉出来，她说话的语气有点抖！好在，本人开车比较稳。慢悠悠的开回家了。</p>
<h2>拿房子 😔</h2>
<p><strong>消息</strong></p>
<p>新房拿到手，大家都会着手装修；即便是精装修都会稍微整修一下。然而从这次装修中，总有了一些不愉快的经历！</p>
<p>9 月 30 日，开发商那边打电话，说是我们可以拿房了。由于前期看过样板房，所以这次拿房新鲜感就很少。</p>
<p>前期没有多少的电话，😔 从各个风声中传来我们是最后一批交房，心里咯噔一下；是不是因为我们前期闹装修问题，闹的太狠，导致这次把我们安排在最后一批？</p>
<img src="assets/ygLkkhu.jpg" alt="新房" />
<p><strong>拿房</strong></p>
<p>9 月 30 日，起了个大早，晃晃悠悠的坐上地铁，匆匆赶到了新家，密密麻麻的楼群。</p>
<img src="assets/GfYEUkb.jpg" alt="楼群" />
<p>拿着一个箱子，里面钥匙啊，合同啊，遥控器什么的，全部都放在里面。感觉还是蛮方便的。</p>
<p>就进去验房了。</p>
<p>反正我不喜欢这样的精装修，于是和家里人商量下，我们稍微整修一下。</p>
<p>目前还在装修中，不过看着房子一天一天的朝着自己构想在变化，还是很满意的！</p>
<p><strong>2018</strong> 年，年初列了一个计划清单，现在上面基本上完成的差不多了。</p>
<p>整一年都是很忙碌，都是在奔跑中。</p>
<p><strong>2019</strong> 年的计划还没有列出来。</p>
<p>总之写到这里，不知道写是不是年终总结了，不纠结了。就当是给自己做个汇报吧！</p>
<p>2018 年 886~~ 😄</p>
<p>2019 年 见</p>]]></content>
    <category term="总结" />
    <category term="Life" />
  </entry>
</feed>