{"version":"https://jsonfeed.org/version/1.1","title":"牧宇的Blog","home_page_url":"https://www.lihuanyu.com","feed_url":"https://www.lihuanyu.com/feed.json","description":"以代码记行藏，以文字观万象；守寸心而求真，循长风以致远。","authors":[{"name":"skyADMIN","url":"https://www.lihuanyu.com"}],"items":[{"id":"https://www.lihuanyu.com/posts/2025/Ubuntu%2022.04%E4%B8%8A%E9%83%A8%E7%BD%B2Docker%20Redis%E5%AE%9E%E6%88%98/","url":"https://www.lihuanyu.com/posts/2025/Ubuntu%2022.04%E4%B8%8A%E9%83%A8%E7%BD%B2Docker%20Redis%E5%AE%9E%E6%88%98/","title":"Ubuntu 22.04上部署Docker Redis实战","summary":"前言 这篇文章记录了我在Ubuntu 22.04轻量应用服务器上部署Docker和Redis的完整过程，包括安装、配置、运行和资源监控。通过实际操作，验证Docker在Linux服务器上的资源开销确实很小。 环境： 系统：Ubuntu 22.04 LTS 配置：入门级配置（轻量应用服务器） 目标：部署一个用于配置同步的Redis实例","content_html":"<h2>前言</h2>\n<p>这篇文章记录了我在Ubuntu 22.04轻量应用服务器上部署Docker和Redis的完整过程，包括安装、配置、运行和资源监控。通过实际操作，验证Docker在Linux服务器上的资源开销确实很小。</p>\n<p>环境：</p>\n<ul>\n<li>系统：Ubuntu 22.04 LTS</li>\n<li>配置：入门级配置（轻量应用服务器）</li>\n<li>目标：部署一个用于配置同步的Redis实例</li>\n</ul>\n<h2>安装Docker</h2>\n<p>首先更新系统包并安装必要依赖：</p>\n<pre><code class=\"language-bash\">sudo apt update\nsudo apt install -y apt-transport-https ca-certificates curl software-properties-common\n</code></pre>\n<p>添加Docker官方GPG密钥：</p>\n<pre><code class=\"language-bash\">curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg\n</code></pre>\n<p>添加Docker仓库：</p>\n<pre><code class=\"language-bash\">echo &quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&quot; | sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null\n</code></pre>\n<p>安装Docker CE（社区版）：</p>\n<pre><code class=\"language-bash\">sudo apt update\nsudo apt install -y docker-ce docker-ce-cli containerd.io\n</code></pre>\n<p>启动Docker并设置开机自启：</p>\n<pre><code class=\"language-bash\"># 启动Docker服务\nsudo systemctl start docker\n# 设置Docker开机自启\nsudo systemctl enable docker\n# 检查是否已设置开机自启\nsudo systemctl is-enabled docker\n# 输出 enabled 表示已设置开机自启\n</code></pre>\n<p>验证安装：</p>\n<pre><code class=\"language-bash\">docker --version\n# 输出类似：Docker version 28.2.2, build 28.2.2-0ubuntu1~22.04.1\n</code></pre>\n<p>为了避免每次都使用sudo，可以将当前用户添加到docker组：</p>\n<pre><code class=\"language-bash\">sudo usermod -aG docker ubuntu\n</code></pre>\n<p>注意：如果你的用户名不是ubuntu，请将命令中的ubuntu替换为实际的用户名（可以用 <code>whoami</code> 命令查看）。执行后需要重新登录才能生效。</p>\n<p><strong>重要</strong>​：执行上述命令后，必须完全退出当前SSH会话并重新登录，用户组权限才会生效。</p>\n<p>如果不想重新登录，可以使用以下命令临时激活权限（仅对当前会话有效）：</p>\n<pre><code class=\"language-bash\">newgrp docker\n</code></pre>\n<p>或者暂时继续使用sudo运行docker命令（在后续步骤中，所有docker命令前都需要加sudo）。</p>\n<p>验证权限是否生效：</p>\n<pre><code class=\"language-bash\">docker ps\n# 如果不报错，说明权限配置成功\n</code></pre>\n<h2>配置Docker镜像源</h2>\n<p>Docker官方镜像仓库在国内访问较慢，配置国内镜像源可以显著提高拉取速度。</p>\n<p>创建或编辑Docker配置文件：</p>\n<pre><code class=\"language-bash\">sudo mkdir -p /etc/docker\nsudo nano /etc/docker/daemon.json\n</code></pre>\n<p><strong>注意</strong>​：目前国内很多公共Docker镜像源已经关闭，建议使用云服务商提供的镜像加速器。以下是常见云服务商镜像源配置示例</p>\n<pre><code class=\"language-json\">{\n  &quot;registry-mirrors&quot;: [\n    &quot;https://mirror.ccs.tencentyun.com&quot;\n  ]\n}\n</code></pre>\n<p>如果你使用的是其他云服务商（如阿里云、华为云等），请查询对应厂商的Docker镜像加速器地址并进行配置。</p>\n<p>重启Docker服务使配置生效：</p>\n<pre><code class=\"language-bash\">sudo systemctl daemon-reload\nsudo systemctl restart docker\n</code></pre>\n<p>验证配置：</p>\n<pre><code class=\"language-bash\">docker info | grep -A 3 &quot;Registry Mirrors&quot;\n# 应该能看到配置的镜像源地址\n</code></pre>\n<h2>拉取Redis镜像</h2>\n<p>注意：<code>docker search</code> 命令不使用镜像源，在国内网络环境下可能无法访问。如果需要搜索镜像，可以访问 Docker Hub 官网（<a href=\"https://hub.docker.com\">https://hub.docker.com</a>）。这里我们直接拉取已知的Redis镜像。</p>\n<p>拉取Redis的Alpine版本。选择Alpine的原因是它基于轻量级的Alpine Linux，镜像体积比标准版小很多：</p>\n<pre><code class=\"language-bash\">docker pull redis:alpine\n</code></pre>\n<p>拉取完成后查看镜像：</p>\n<pre><code class=\"language-bash\">docker images redis\n</code></pre>\n<p>输出示例：</p>\n<pre><code class=\"language-plaintext\">REPOSITORY   TAG       IMAGE ID       CREATED        SIZE\nredis        alpine    3900abf41552   2 weeks ago    40.5MB\n</code></pre>\n<p>可以看到，Alpine版本只有40MB左右，而标准版通常在100MB以上。</p>\n<h2>运行Redis容器</h2>\n<p>使用以下命令启动Redis容器：</p>\n<pre><code class=\"language-bash\">docker run -d \\\n  --name redis-config \\\n  -p 6379:6379 \\\n  --restart=always \\\n  -m 50m \\\n  --memory-swap 50m \\\n  redis:alpine redis-server --maxmemory 20mb --maxmemory-policy allkeys-lru\n</code></pre>\n<p>参数说明：</p>\n<ul>\n<li><code>-d</code>：后台运行</li>\n<li><code>--name redis-config</code>：容器名称</li>\n<li><code>-p 6379:6379</code>：端口映射，宿主机6379映射到容器6379</li>\n<li><code>--restart=always</code>：容器异常退出时自动重启，服务器重启后也会自动启动</li>\n<li><code>-m 50m</code>：限制容器最大内存使用为50MB</li>\n<li><code>--memory-swap 50m</code>：限制内存+swap总和为50MB，防止使用swap</li>\n<li><code>redis-server --maxmemory 20mb</code>：限制Redis最大使用20MB内存</li>\n<li><code>--maxmemory-policy allkeys-lru</code>：内存满时使用LRU算法淘汰键</li>\n</ul>\n<p>验证容器运行状态：</p>\n<pre><code class=\"language-bash\">docker ps\n</code></pre>\n<p>输出示例：</p>\n<pre><code class=\"language-plaintext\">CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                    NAMES\na1b2c3d4e5f6   redis:alpine   &quot;redis-server --maxm…&quot;   10 seconds ago   Up 9 seconds   0.0.0.0:6379-&gt;6379/tcp   redis-config\n</code></pre>\n<p>测试Redis连接：</p>\n<pre><code class=\"language-bash\">docker exec -it redis-config redis-cli ping\n# 输出：PONG\n</code></pre>\n<h2>观察资源占用</h2>\n<p>使用<code>docker stats</code>实时查看容器资源使用：</p>\n<pre><code class=\"language-bash\">docker stats redis-config --no-stream\n</code></pre>\n<p>输出示例：</p>\n<pre><code class=\"language-plaintext\">CONTAINER ID   NAME           CPU %     MEM USAGE / LIMIT   MEM %     NET I/O       BLOCK I/O   PIDS\na1b2c3d4e5f6   redis-config   0.15%     7.8MiB / 50MiB      15.60%    1.2kB / 0B    0B / 0B     5\n</code></pre>\n<p>可以看到：</p>\n<ul>\n<li>CPU使用率：0.15%（几乎可以忽略）</li>\n<li>内存使用：7.8MB / 50MB限制（实际使用很少）</li>\n<li>进程数：5个</li>\n</ul>\n<p>查看系统总体内存使用：</p>\n<pre><code class=\"language-bash\">free -h\n</code></pre>\n<p>输出示例：</p>\n<pre><code class=\"language-plaintext\">              total        used        free      shared  buff/cache   available\nMem:          3.8Gi       850Mi       2.1Gi       1.0Mi       850Mi       2.7Gi\nSwap:            0B          0B          0B\n</code></pre>\n<p>可以看到，Docker和Redis容器占用极小，大部分内存仍然可用。</p>\n<p>使用<code>htop</code>查看进程详情（如未安装：<code>sudo apt install htop</code>）：</p>\n<pre><code class=\"language-bash\">htop\n</code></pre>\n<p>在进程列表中可以找到Redis相关进程，每个进程的内存占用都很小。按<code>F4</code>搜索&quot;redis&quot;可以快速定位。</p>\n<p>再次确认容器占用：</p>\n<pre><code class=\"language-bash\">docker exec redis-config redis-cli info memory | grep used_memory_human\n</code></pre>\n<p>输出：</p>\n<pre><code class=\"language-plaintext\">used_memory_human:1.2M\n</code></pre>\n<p>Redis自身报告只使用了1.2MB内存（未存储数据时的基础开销）。</p>\n<h2>性能测试</h2>\n<p>使用Redis自带的benchmark工具测试性能:</p>\n<pre><code class=\"language-bash\">docker exec redis-config redis-cli --intrinsic-latency 5\n</code></pre>\n<p>测试延迟,输出示例:</p>\n<pre><code class=\"language-plaintext\">Max latency so far: 1 microseconds.\nMax latency so far: 16 microseconds.\nMax latency so far: 133 microseconds.\nMax latency so far: 293 microseconds.\nMax latency so far: 549 microseconds.\nMax latency so far: 760 microseconds.\nMax latency so far: 2012 microseconds.\n\n72455986 total runs (avg latency: 0.0690 microseconds / 69.01 nanoseconds per run).\nWorst run took 29156x longer than the average latency.\n</code></pre>\n<p><strong>测试结果</strong>​：</p>\n<pre><code class=\"language-plaintext\">平均延迟：69 纳秒\n最大延迟：2012 微秒 (2ms)\n测试次数：72,455,986 次\n</code></pre>\n<p><strong>结论</strong>​：在轻量应用服务器环境下，表现良好。</p>\n<h2>总结</h2>\n<p>整个部署过程非常顺利，从零开始安装Docker到运行配置完善的Redis容器，全程不到10分钟。这次实践让我对Docker在Linux环境下的轻量级特性有了更直观的认识。</p>\n<p><strong>核心收获：</strong></p>\n<p><strong>1. 部署体验优秀</strong></p>\n<p>Ubuntu 22.04对Docker的支持非常完善，安装过程清晰流畅。官方文档详实，即使是新手也能快速上手。通过配置国内镜像源，完全解决了网络访问慢的问题。</p>\n<p><strong>2. 资源占用惊人地低</strong></p>\n<p>实测数据令人印象深刻：</p>\n<ul>\n<li>Redis容器空载时仅占用约8MB内存</li>\n<li>写入1000条测试数据后，内存占用增加到8.5MB</li>\n<li>CPU使用率始终保持在0.2%以下</li>\n<li>即使在入门级配置的轻量服务器上，这点开销也几乎可以忽略不计</li>\n</ul>\n<p>相比在Mac/Windows上通过虚拟机运行Docker，Linux原生环境的开销要小一个数量级。这验证了之前调研中提到的结论：Docker在Linux上是真正的轻量级容器技术，而非虚拟机。</p>\n<p><strong>3. 性能表现出色</strong></p>\n<p>使用Redis内置的延迟测试工具，平均延迟仅为69纳秒，即使在轻量应用服务器的环境下，也能保持微秒级的响应速度。这样的性能足以满足大多数应用场景的需求。</p>\n<p><strong>4. 运维管理简单</strong></p>\n<p>一条命令完成容器启动，配置好资源限制和自动重启策略后，基本实现了&quot;一劳永逸&quot;。无需手动安装Redis及其依赖，不用担心环境配置冲突，也不需要编写复杂的服务管理脚本。</p>\n<p><strong>实用建议：</strong></p>\n<p>对于入门级或中低配置的云服务器，完全可以放心使用Docker部署轻量级服务。多个类似容器同时运行也不会对系统造成明显压力。如果你正在考虑是否在配置较低的服务器上使用Docker，建议先进行类似的小规模测试，实际数据会比理论分析更有说服力。</p>\n<p>这次实战不仅验证了Docker的轻量级特性，也为后续在生产环境中大规模使用容器技术提供了信心。容器化不是性能的负担，而是现代应用部署的最佳实践之一。</p>\n","date_published":"2025-11-08T00:00:00.000Z","tags":["Docker","Redis","Linux","容器化","运维"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2025/%E9%87%8D%E6%96%B0%E8%AE%A4%E8%AF%86Docker%E7%9A%84%E6%80%A7%E8%83%BD%E5%BC%80%E9%94%80/","url":"https://www.lihuanyu.com/posts/2025/%E9%87%8D%E6%96%B0%E8%AE%A4%E8%AF%86Docker%E7%9A%84%E6%80%A7%E8%83%BD%E5%BC%80%E9%94%80/","title":"重新认识Docker的性能开销","summary":"从误解说起 最初接触Docker是在macOS开发环境中。每次启动Docker Desktop，都能感受到明显的性能影响：风扇噪音增大，Activity Monitor显示内存占用数GB，系统响应变慢。Windows环境下的体验也类似。 这样的体验让我形成了一个固有印象：Docker是资源密集型工具。因此在管理轻量应用服务器（1核2G、2核4G配置）时，我一","content_html":"<h2>从误解说起</h2>\n<p>最初接触Docker是在macOS开发环境中。每次启动Docker Desktop，都能感受到明显的性能影响：风扇噪音增大，Activity Monitor显示内存占用数GB，系统响应变慢。Windows环境下的体验也类似。</p>\n<p>这样的体验让我形成了一个固有印象：Docker是资源密集型工具。因此在管理轻量应用服务器（1核2G、2核4G配置）时，我一直不敢使用Docker，担心会导致资源不足。</p>\n<h2>调研与学习</h2>\n<p>后来遇到一个场景：多个服务器上的应用需要共享配置数据。最初考虑用脚本同步文件，但觉得不够优雅。Redis的主从复制功能恰好能解决这个问题。</p>\n<p>如果手动在每台服务器上安装Redis，需要处理依赖、版本、配置等问题，比较繁琐。Docker的优势在于一条命令即可启动，配置简单。但考虑到服务器配置较低，加上之前的糟糕体验，我开始四处搜索资料，想了解Docker在Linux服务器上的实际表现。</p>\n<p>在查阅资料过程中，我发现了一些关键信息：</p>\n<p><strong>IBM的研究论文</strong>指出，Docker在Linux环境下的性能开销极小。在多项基准测试中，Docker容器的性能与裸机几乎相同，CPU开销通常小于2%，内存开销接近0。这与我在Mac上的体验完全不同。</p>\n<p><strong>技术原理</strong>方面，Docker在Linux上直接使用宿主机内核，通过两个核心机制实现隔离：</p>\n<ul>\n<li>\n<p><strong>namespace（命名空间）</strong>：隔离进程、网络、文件系统等资源</p>\n</li>\n<li>\n<p><strong>cgroups（控制组）</strong>：限制和统计CPU、内存等资源使用</p>\n</li>\n</ul>\n<p>这意味着容器本质上是特殊的Linux进程，不需要像虚拟机那样运行完整的操作系统，因此开销极小。</p>\n<p><strong>对比数据</strong>也很有说服力。IBM的测试显示，在相同硬件上：</p>\n<ul>\n<li>\n<p>Docker容器启动时间小于1秒，KVM虚拟机需要11秒</p>\n</li>\n<li>\n<p>CPU密集型任务中，Docker性能与裸机相当，KVM平均慢10-20%</p>\n</li>\n<li>\n<p>内存带宽测试中，Docker几乎无性能损失</p>\n</li>\n</ul>\n<p>这些信息让我重新思考。也许Docker的问题不在于技术本身，而在于Mac/Windows上必须通过虚拟机运行的实现方式。</p>\n<p>既然理论数据这么好，那就值得实际测试一下。</p>\n<h2>实测结果</h2>\n<p>在Linux服务器上运行Docker的体验完全不同。</p>\n<p>使用<code>docker stats</code>监控，一个用于配置同步的Redis容器，内存占用仅为5-10MB。CPU使用率基本可以忽略，只在数据同步时有轻微波动。</p>\n<p>对于1核2G的服务器来说，这点资源消耗完全可以接受。</p>\n<p>部署命令：</p>\n<pre><code class=\"language-bash\">docker run -d --name redis-config \\\n  -p 6379:6379 \\\n  --restart=always \\\n  -m 50m \\\n  redis:alpine redis-server --maxmemory 20mb\n</code></pre>\n<p>几秒钟即可启动，配置主从复制也很直接。用<code>free -h</code>和<code>htop</code>检查系统资源，运行稳定，没有出现之前担心的资源紧张。</p>\n<h2>差异原因</h2>\n<p>差异源于Docker在不同操作系统上的实现方式。</p>\n<p><strong>macOS和Windows上</strong>​，由于这些系统内核不支持Docker所需的Linux特性，Docker Desktop必须先运行一个Linux虚拟机（WSL2或Hypervisor.framework），然后在虚拟机中运行容器。资源开销主要来自：</p>\n<ul>\n<li>\n<p>虚拟机本身占用的内存（通常2-4GB）</p>\n</li>\n<li>\n<p>硬件虚拟化的性能损耗</p>\n</li>\n<li>\n<p>文件系统映射的I/O开销</p>\n</li>\n</ul>\n<p><strong>Linux服务器上</strong>​，Docker可以直接利用内核功能：</p>\n<ul>\n<li>\n<p><strong>namespace</strong>提供进程级隔离，每个容器看到独立的进程树、网络栈、文件系统</p>\n</li>\n<li>\n<p><strong>cgroups</strong>精确控制资源分配，限制CPU、内存使用，避免相互干扰</p>\n</li>\n<li>\n<p>无需虚拟化层，容器直接以宿主机进程方式运行</p>\n</li>\n</ul>\n<p>这就是为什么IBM研究能得出&quot;Docker性能接近裸机&quot;的结论——因为容器确实只是进程，只是被精心隔离和限制的进程。</p>\n<p>可以这样理解：</p>\n<ul>\n<li>\n<p>Mac/Windows：虚拟机（2-4GB开销）+ Docker容器</p>\n</li>\n<li>\n<p>Linux：Docker容器（5-10MB开销）</p>\n</li>\n</ul>\n<p>开销差异可达几百倍，这解释了我的体验差异。</p>\n<h2>总结</h2>\n<p>之前对Docker的顾虑主要基于Mac和Windows的使用体验。实际在Linux服务器上测试后发现，即使是低配置服务器，运行轻量级容器也完全没有问题。</p>\n<p>5-10MB内存就能运行一个Redis实例，部署和管理都很方便。这种低开销和便利性确实超出预期。</p>\n<p>当然，如果服务器配置确实很低（如512MB内存），或需要运行多个重型服务，仍需要仔细规划资源分配。但对大多数轻量级应用场景，Docker在Linux上的表现值得信赖。</p>\n<p>如果你也曾因为Mac或Windows上的体验而对Docker有所顾虑，不妨在Linux服务器上实际测试一下。结果可能会让你重新认识这个工具。</p>\n","date_published":"2025-11-08T00:00:00.000Z","tags":["Docker","性能优化","Linux","容器化","云原生"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2025/NestJS-%E6%A1%86%E6%9E%B6%E4%B8%8B%E7%9A%84%E4%B8%89%E7%A7%8D%E6%B5%8B%E8%AF%95%E7%B1%BB%E5%9E%8B%E5%AF%B9%E6%AF%94%E5%88%86%E6%9E%90/","url":"https://www.lihuanyu.com/posts/2025/NestJS-%E6%A1%86%E6%9E%B6%E4%B8%8B%E7%9A%84%E4%B8%89%E7%A7%8D%E6%B5%8B%E8%AF%95%E7%B1%BB%E5%9E%8B%E5%AF%B9%E6%AF%94%E5%88%86%E6%9E%90/","title":"NestJS 框架下的三种测试类型对比分析","summary":"概述 本文以NestJS框架为例，深入对比分析单元测试、集成测试和端到端 E2E 测试的核心区别，帮助开发者在实际项目中选择合适的测试策略。","content_html":"<h2>概述</h2>\n<p>本文以NestJS框架为例，深入对比分析单元测试、集成测试和端到端(E2E)测试的核心区别，帮助开发者在实际项目中选择合适的测试策略。</p>\n<h2>1. 三种测试类型的核心区别</h2>\n<h3>1.1 定义与测试范围对比</h3>\n<table>\n<thead>\n<tr>\n<th>测试类型</th>\n<th>定义</th>\n<th>测试范围</th>\n<th>NestJS中的体现</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>单元测试</strong></td>\n<td>测试单个函数、方法或类的逻辑</td>\n<td>最小可测试单元</td>\n<td>Service方法、Controller方法、Pipe、Guard等</td>\n</tr>\n<tr>\n<td><strong>集成测试</strong></td>\n<td>测试多个模块间的交互</td>\n<td>模块间接口和数据流</td>\n<td>Service与Repository交互、Module间通信</td>\n</tr>\n<tr>\n<td><strong>E2E测试</strong></td>\n<td>测试完整的用户场景</td>\n<td>整个应用流程</td>\n<td>HTTP请求到响应的完整链路</td>\n</tr>\n</tbody>\n</table>\n<h3>1.2 执行特性对比</h3>\n<table>\n<thead>\n<tr>\n<th>特性</th>\n<th>单元测试</th>\n<th>集成测试</th>\n<th>E2E测试</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>执行速度</strong></td>\n<td>极快(毫秒级)</td>\n<td>中等(秒级)</td>\n<td>较慢(分钟级)</td>\n</tr>\n<tr>\n<td><strong>隔离性</strong></td>\n<td>完全隔离</td>\n<td>部分隔离</td>\n<td>无隔离</td>\n</tr>\n<tr>\n<td><strong>依赖处理</strong></td>\n<td>Mock所有依赖</td>\n<td>Mock外部依赖</td>\n<td>使用真实依赖</td>\n</tr>\n<tr>\n<td><strong>环境要求</strong></td>\n<td>无需外部环境</td>\n<td>需要部分真实环境</td>\n<td>需要完整环境</td>\n</tr>\n</tbody>\n</table>\n<h2>2. NestJS框架下的具体实现对比</h2>\n<h3>2.1 单元测试示例</h3>\n<p><strong>测试目标：UserService中的创建用户方法</strong></p>\n<pre><code class=\"language-typescript\">// user.service.ts\n@Injectable()\nexport class UserService {\n  constructor(private userRepository: UserRepository) {}\n\n  async createUser(userData: CreateUserDto): Promise {\n    const existingUser = await this.userRepository.findByEmail(userData.email);\n    if (existingUser) {\n      throw new ConflictException('User already exists');\n    }\n    return this.userRepository.create(userData);\n  }\n}\n</code></pre>\n<p><strong>单元测试实现：</strong></p>\n<pre><code class=\"language-typescript\">// user.service.spec.ts\ndescribe('UserService', () =&gt; {\n  let service: UserService;\n  let mockRepository: jest.Mocked;\n\n  beforeEach(async () =&gt; {\n    const mockRepo = {\n      findByEmail: jest.fn(),\n      create: jest.fn(),\n    };\n\n    const module = await Test.createTestingModule({\n      providers: [\n        UserService,\n        { provide: UserRepository, useValue: mockRepo },\n      ],\n    }).compile();\n\n    service = module.get(UserService);\n    mockRepository = module.get(UserRepository);\n  });\n\n  it('should create user when email not exists', async () =&gt; {\n    // Arrange\n    const userData = { email: 'test@example.com', name: 'Test User' };\n    mockRepository.findByEmail.mockResolvedValue(null);\n    mockRepository.create.mockResolvedValue({ id: 1, ...userData });\n\n    // Act\n    const result = await service.createUser(userData);\n\n    // Assert\n    expect(mockRepository.findByEmail).toHaveBeenCalledWith(userData.email);\n    expect(mockRepository.create).toHaveBeenCalledWith(userData);\n    expect(result).toEqual({ id: 1, ...userData });\n  });\n});\n</code></pre>\n<p><strong>特点分析：</strong></p>\n<ul>\n<li>\n<p>✅ <strong>完全隔离</strong>：Mock了UserRepository依赖</p>\n</li>\n<li>\n<p>✅ <strong>快速执行</strong>：无需数据库连接</p>\n</li>\n<li>\n<p>✅ <strong>精确验证</strong>：只测试业务逻辑</p>\n</li>\n<li>\n<p>❌ <strong>无法发现</strong>：Repository接口变更问题</p>\n</li>\n</ul>\n<h3>2.2 集成测试示例</h3>\n<p><strong>测试目标：UserService与真实数据库的交互</strong></p>\n<pre><code class=\"language-typescript\">// user.integration.spec.ts\ndescribe('UserService Integration', () =&gt; {\n  let app: INestApplication;\n  let service: UserService;\n  let repository: Repository;\n\n  beforeAll(async () =&gt; {\n    const module = await Test.createTestingModule({\n      imports: [\n        TypeOrmModule.forRoot({\n          type: 'sqlite',\n          database: ':memory:',\n          entities: [User],\n          synchronize: true,\n        }),\n        TypeOrmModule.forFeature([User]),\n      ],\n      providers: [UserService, UserRepository],\n    }).compile();\n\n    app = module.createNestApplication();\n    await app.init();\n    \n    service = module.get(UserService);\n    repository = module.get&gt;(getRepositoryToken(User));\n  });\n\n  beforeEach(async () =&gt; {\n    await repository.clear(); // 清理测试数据\n  });\n\n  it('should create user and save to database', async () =&gt; {\n    // Arrange\n    const userData = { email: 'test@example.com', name: 'Test User' };\n\n    // Act\n    const createdUser = await service.createUser(userData);\n\n    // Assert\n    expect(createdUser.id).toBeDefined();\n    expect(createdUser.email).toBe(userData.email);\n    \n    // 验证数据确实保存到数据库\n    const savedUser = await repository.findOne({ where: { email: userData.email } });\n    expect(savedUser).toBeTruthy();\n  });\n\n  it('should throw conflict when user already exists', async () =&gt; {\n    // Arrange\n    const userData = { email: 'test@example.com', name: 'Test User' };\n    await repository.save(userData); // 预先创建用户\n\n    // Act &amp; Assert\n    await expect(service.createUser(userData)).rejects.toThrow(ConflictException);\n  });\n});\n</code></pre>\n<p><strong>特点分析：</strong></p>\n<ul>\n<li>\n<p>✅ <strong>真实交互</strong>：使用真实数据库操作</p>\n</li>\n<li>\n<p>✅ <strong>接口验证</strong>：能发现Service与Repository接口问题</p>\n</li>\n<li>\n<p>✅ <strong>数据验证</strong>：确认数据正确保存</p>\n</li>\n<li>\n<p>❌ <strong>执行较慢</strong>：需要数据库操作</p>\n</li>\n<li>\n<p>❌ <strong>环境依赖</strong>：需要配置测试数据库</p>\n</li>\n</ul>\n<h3>2.3 E2E测试示例</h3>\n<p><strong>测试目标：完整的用户注册API流程</strong></p>\n<pre><code class=\"language-typescript\">// user.e2e-spec.ts\ndescribe('User E2E', () =&gt; {\n  let app: INestApplication;\n  let httpServer: any;\n\n  beforeAll(async () =&gt; {\n    const module = await Test.createTestingModule({\n      imports: [AppModule], // 导入完整应用模块\n    }).compile();\n\n    app = module.createNestApplication();\n    app.useGlobalPipes(new ValidationPipe()); // 应用全局管道\n    await app.init();\n    \n    httpServer = app.getHttpServer();\n  });\n\n  beforeEach(async () =&gt; {\n    // 清理测试数据\n    const userRepository = app.get&gt;(getRepositoryToken(User));\n    await userRepository.clear();\n  });\n\n  it('/users (POST) - should create user successfully', async () =&gt; {\n    // Arrange\n    const userData = {\n      email: 'test@example.com',\n      name: 'Test User',\n      password: 'password123'\n    };\n\n    // Act\n    const response = await request(httpServer)\n      .post('/users')\n      .send(userData)\n      .expect(201);\n\n    // Assert\n    expect(response.body).toMatchObject({\n      id: expect.any(Number),\n      email: userData.email,\n      name: userData.name,\n    });\n    expect(response.body.password).toBeUndefined(); // 密码不应返回\n  });\n\n  it('/users (POST) - should return 409 when user exists', async () =&gt; {\n    // Arrange\n    const userData = {\n      email: 'test@example.com',\n      name: 'Test User',\n      password: 'password123'\n    };\n\n    // 先创建用户\n    await request(httpServer)\n      .post('/users')\n      .send(userData)\n      .expect(201);\n\n    // Act &amp; Assert\n    await request(httpServer)\n      .post('/users')\n      .send(userData)\n      .expect(409);\n  });\n\n  it('/users (POST) - should validate input data', async () =&gt; {\n    // Act &amp; Assert\n    await request(httpServer)\n      .post('/users')\n      .send({\n        email: 'invalid-email', // 无效邮箱\n        name: '', // 空名称\n      })\n      .expect(400);\n  });\n});\n</code></pre>\n<p><strong>特点分析：</strong></p>\n<ul>\n<li>\n<p>✅ <strong>完整流程</strong>：测试HTTP请求到数据库的完整链路</p>\n</li>\n<li>\n<p>✅ <strong>真实场景</strong>：模拟用户实际操作</p>\n</li>\n<li>\n<p>✅ <strong>全面验证</strong>：包含验证、异常处理、响应格式等</p>\n</li>\n<li>\n<p>❌ <strong>执行最慢</strong>：启动完整应用</p>\n</li>\n<li>\n<p>❌ <strong>维护成本高</strong>：接口变更需要同步更新</p>\n</li>\n</ul>\n<h2>3. 在NestJS项目中的选择策略</h2>\n<h3>3.1 测试金字塔在NestJS中的应用</h3>\n<pre><code class=\"language-plaintext\">        E2E Tests (10%)\n      ┌─────────────────┐\n      │   核心业务流程   │\n      └─────────────────┘\n    \n    Integration Tests (20%)\n   ┌───────────────────────┐\n   │  Service ↔ Repository │\n   │  Module间交互          │\n   └───────────────────────┘\n\nUnit Tests (70%)\n┌─────────────────────────────┐\n│ Service方法、Controller方法  │\n│ Pipe、Guard、Interceptor    │\n│ 工具函数、业务逻辑           │\n└─────────────────────────────┘\n</code></pre>\n<h3>3.2 具体应用建议</h3>\n<p><strong>单元测试重点关注：</strong></p>\n<ul>\n<li>\n<p>Service中的业务逻辑方法</p>\n</li>\n<li>\n<p>Controller中的参数处理和响应格式化</p>\n</li>\n<li>\n<p>自定义Pipe的数据转换逻辑</p>\n</li>\n<li>\n<p>Guard的权限验证逻辑</p>\n</li>\n<li>\n<p>工具函数和算法</p>\n</li>\n</ul>\n<p><strong>集成测试重点关注：</strong></p>\n<ul>\n<li>\n<p>Service与Repository的数据操作</p>\n</li>\n<li>\n<p>Module间的依赖注入</p>\n</li>\n<li>\n<p>第三方服务的集成（如Redis、消息队列）</p>\n</li>\n<li>\n<p>数据库事务处理</p>\n</li>\n</ul>\n<p><strong>E2E测试重点关注：</strong></p>\n<ul>\n<li>\n<p>用户注册/登录流程</p>\n</li>\n<li>\n<p>核心业务操作流程</p>\n</li>\n<li>\n<p>权限控制的完整验证</p>\n</li>\n<li>\n<p>错误处理的用户体验</p>\n</li>\n</ul>\n<h2>4. 实际项目中的测试配置</h2>\n<h3>4.1 package.json测试脚本</h3>\n<pre><code class=\"language-json\">{\n  &quot;scripts&quot;: {\n    &quot;test&quot;: &quot;jest&quot;,\n    &quot;test:watch&quot;: &quot;jest --watch&quot;,\n    &quot;test:cov&quot;: &quot;jest --coverage&quot;,\n    &quot;test:integration&quot;: &quot;jest --config ./test/jest-integration.json&quot;,\n    &quot;test:e2e&quot;: &quot;jest --config ./test/jest-e2e.json&quot;\n  }\n}\n</code></pre>\n<h3>4.2 Jest配置文件</h3>\n<p><strong>单元测试配置 (jest.config.js):</strong></p>\n<pre><code class=\"language-javascript\">module.exports = {\n  moduleFileExtensions: ['js', 'json', 'ts'],\n  rootDir: 'src',\n  testRegex: '.*\\\\.spec\\\\.ts$', // 只匹配 .spec.ts 文件\n  transform: { '^.+\\\\.(t|j)s$': 'ts-jest' },\n  collectCoverageFrom: ['**/*.(t|j)s'],\n  coverageDirectory: '../coverage',\n  testEnvironment: 'node',\n  testPathIgnorePatterns: ['.*\\\\.integration\\\\.spec\\\\.ts$'], // 排除集成测试\n};\n</code></pre>\n<p><strong>集成测试配置 (test/jest-integration.json):</strong></p>\n<pre><code class=\"language-json\">{\n  &quot;moduleFileExtensions&quot;: [&quot;js&quot;, &quot;json&quot;, &quot;ts&quot;],\n  &quot;rootDir&quot;: &quot;../src&quot;,\n  &quot;testEnvironment&quot;: &quot;node&quot;,\n  &quot;testRegex&quot;: &quot;.*\\\\.integration\\\\.spec\\\\.ts$&quot;,\n  &quot;transform&quot;: { &quot;^.+\\\\.(t|j)s$&quot;: &quot;ts-jest&quot; },\n  &quot;setupFilesAfterEnv&quot;: [&quot;/../test/integration-setup.ts&quot;]\n}\n</code></pre>\n<p><strong>E2E测试配置 (test/jest-e2e.json):</strong></p>\n<pre><code class=\"language-json\">{\n  &quot;moduleFileExtensions&quot;: [&quot;js&quot;, &quot;json&quot;, &quot;ts&quot;],\n  &quot;rootDir&quot;: &quot;.&quot;,\n  &quot;testEnvironment&quot;: &quot;node&quot;,\n  &quot;testRegex&quot;: &quot;.e2e-spec.ts$&quot;,\n  &quot;transform&quot;: { &quot;^.+\\\\.(t|j)s$&quot;: &quot;ts-jest&quot; }\n}\n</code></pre>\n<p><strong>集成测试环境设置 (test/integration-setup.ts):</strong></p>\n<pre><code class=\"language-typescript\">import { Test } from '@nestjs/testing';\nimport { TypeOrmModule } from '@nestjs/typeorm';\n\n// 全局集成测试配置\nbeforeAll(async () =&gt; {\n  // 设置测试数据库连接等\n});\n\nafterAll(async () =&gt; {\n  // 清理资源\n});\n</code></pre>\n<h2>5. 总结</h2>\n<p>在NestJS框架下，三种测试类型各有其适用场景：</p>\n<p><strong>选择单元测试当：</strong></p>\n<ul>\n<li>\n<p>验证复杂业务逻辑</p>\n</li>\n<li>\n<p>需要快速反馈</p>\n</li>\n<li>\n<p>测试覆盖率要求高</p>\n</li>\n</ul>\n<p><strong>选择集成测试当：</strong></p>\n<ul>\n<li>\n<p>验证数据库操作</p>\n</li>\n<li>\n<p>测试模块间交互</p>\n</li>\n<li>\n<p>确保接口契约正确</p>\n</li>\n</ul>\n<p><strong>选择E2E测试当：</strong></p>\n<ul>\n<li>\n<p>验证关键业务流程</p>\n</li>\n<li>\n<p>确保用户体验</p>\n</li>\n<li>\n<p>发布前的最终验证</p>\n</li>\n</ul>\n<p>合理的测试策略应该是70%单元测试 + 20%集成测试 + 10%E2E测试，这样既能保证代码质量，又能控制测试维护成本。</p>\n<h2>6. 扩展：其他测试类型的补充说明</h2>\n<h3>6.1 测试分类的两个维度</h3>\n<p>虽然本文重点讨论单元测试、集成测试和E2E测试，但在实际项目中还存在其他测试类型。理解测试的分类维度很重要：</p>\n<p><strong>按执行方式分类：</strong></p>\n<ul>\n<li>\n<p><strong>自动化测试</strong>：通过代码自动执行（本文重点）</p>\n</li>\n<li>\n<p><strong>工具驱动测试</strong>：使用专门工具执行</p>\n</li>\n<li>\n<p><strong>手工测试</strong>：需要人工操作</p>\n</li>\n</ul>\n<p><strong>按测试目标分类：</strong></p>\n<ul>\n<li>\n<p><strong>功能性测试</strong>：验证功能是否正确实现</p>\n</li>\n<li>\n<p><strong>非功能性测试</strong>：验证性能、安全、可用性等</p>\n</li>\n</ul>\n<h3>6.2 代码驱动的自动化测试 vs 其他测试类型</h3>\n<h4>本文讨论的三种测试（代码驱动）</h4>\n<pre><code class=\"language-typescript\">// 完全通过代码自动执行\ndescribe('UserService', () =&gt; {\n  it('should create user when email not exists', async () =&gt; {\n    // 自动化的断言检查\n    expect(result.email).toBe('test@example.com');\n    expect(mockRepository.create).toHaveBeenCalledWith(userData);\n  });\n});\n</code></pre>\n<p><strong>特点：</strong></p>\n<ul>\n<li>\n<p>✅ 完全自动化执行</p>\n</li>\n<li>\n<p>✅ 可集成到CI/CD流程</p>\n</li>\n<li>\n<p>✅ 开发过程中持续运行</p>\n</li>\n<li>\n<p>✅ 快速反馈和问题定位</p>\n</li>\n</ul>\n<h4>其他测试类型（工具/人工驱动）</h4>\n<p><strong>API测试（工具驱动）：</strong></p>\n<pre><code class=\"language-bash\"># 使用Postman/Newman\nnewman run api-tests.postman_collection.json\n\n# 使用专门的API测试工具\ncurl -X POST http://localhost:3000/users \\\n  -H &quot;Content-Type: application/json&quot; \\\n  -d '{&quot;email&quot;:&quot;test@example.com&quot;,&quot;name&quot;:&quot;Test User&quot;}'\n</code></pre>\n<p><strong>性能测试（工具驱动）：</strong></p>\n<pre><code class=\"language-bash\"># 使用Artillery进行负载测试\nartillery run load-test.yml\n\n# 使用JMeter\njmeter -n -t performance-test.jmx\n</code></pre>\n<p><strong>安全测试（工具驱动）：</strong></p>\n<pre><code class=\"language-bash\"># 依赖漏洞扫描\nnpm audit\nsnyk test\n\n# 代码安全扫描\neslint --ext .ts src/ --config .eslintrc-security.js\n</code></pre>\n<h3>6.3 完整的测试策略配置</h3>\n<h4>package.json中的完整测试脚本</h4>\n<pre><code class=\"language-json\">{\n  &quot;scripts&quot;: {\n    // 代码驱动的自动化测试（本文重点）\n    &quot;test&quot;: &quot;jest&quot;,\n    &quot;test:unit&quot;: &quot;jest --config ./test/jest-unit.json&quot;,\n    &quot;test:integration&quot;: &quot;jest --config ./test/jest-integration.json&quot;, \n    &quot;test:e2e&quot;: &quot;jest --config ./test/jest-e2e.json&quot;,\n    &quot;test:watch&quot;: &quot;jest --watch&quot;,\n    &quot;test:cov&quot;: &quot;jest --coverage&quot;,\n    \n    // 工具驱动的测试\n    &quot;test:api&quot;: &quot;newman run ./test/api-tests.postman_collection.json&quot;,\n    &quot;test:performance&quot;: &quot;artillery run ./test/load-test.yml&quot;,\n    &quot;test:security&quot;: &quot;npm audit &amp;&amp; snyk test&quot;,\n    &quot;test:lint&quot;: &quot;eslint src/**/*.ts&quot;,\n    \n    // 综合测试脚本\n    &quot;test:all&quot;: &quot;npm run test:unit &amp;&amp; npm run test:integration &amp;&amp; npm run test:e2e&quot;,\n    &quot;test:ci&quot;: &quot;npm run test:lint &amp;&amp; npm run test:security &amp;&amp; npm run test:all&quot;\n  }\n}\n</code></pre>\n<h3>6.4 测试策略的完整图景</h3>\n<pre><code class=\"language-plaintext\">代码驱动的自动化测试（开发者日常）    其他测试类型（专项/阶段性）\n                                   \n    E2E Tests (10%)                Manual Testing\n   ┌─────────────────┐              ┌─────────────────┐\n   │  关键业务流程    │              │  可用性、探索性  │\n   └─────────────────┘              └─────────────────┘\n                                   \n  Integration Tests (20%)          Tool-based Testing  \n ┌─────────────────────┐            ┌─────────────────┐\n │  模块间交互验证      │            │ 性能、安全扫描   │\n └─────────────────────┘            └─────────────────┘\n                                   \nUnit Tests (70%)                   Static Analysis\n┌─────────────────────┐             ┌─────────────────┐\n│  业务逻辑验证        │             │ 代码质量检查     │\n└─────────────────────┘             └─────────────────┘\n</code></pre>\n<h3>6.5 为什么本文重点讲代码驱动的测试</h3>\n<p><strong>开发者日常最需要的技能：</strong></p>\n<ol>\n<li>\n<p><strong>高频使用</strong>：每天开发过程中都要编写和运行</p>\n</li>\n<li>\n<p><strong>即时反馈</strong>：能在编码时立即发现问题</p>\n</li>\n<li>\n<p><strong>CI/CD集成</strong>：可以自动化集成到部署流程</p>\n</li>\n<li>\n<p><strong>成本效益</strong>：一次编写，持续受益</p>\n</li>\n</ol>\n<p><strong>其他测试类型的特点：</strong></p>\n<ul>\n<li>\n<p><strong>执行频率较低</strong>：通常在特定阶段执行（如发布前）</p>\n</li>\n<li>\n<p><strong>专门工具</strong>：需要学习和配置专门的测试工具</p>\n</li>\n<li>\n<p><strong>专业团队</strong>：更多由QA或运维团队负责</p>\n</li>\n<li>\n<p><strong>环境要求</strong>：需要特殊的测试环境和数据</p>\n</li>\n</ul>\n<h3>6.6 实际项目中的应用建议</h3>\n<p><strong>开发阶段（每日）：</strong></p>\n<ul>\n<li>\n<p>单元测试：验证业务逻辑</p>\n</li>\n<li>\n<p>集成测试：验证模块交互</p>\n</li>\n<li>\n<p>代码质量检查：ESLint、Prettier</p>\n</li>\n</ul>\n<p><strong>集成阶段（每次提交）：</strong></p>\n<ul>\n<li>\n<p>E2E测试：验证关键流程</p>\n</li>\n<li>\n<p>API测试：验证接口契约</p>\n</li>\n<li>\n<p>安全扫描：检查依赖漏洞</p>\n</li>\n</ul>\n<p><strong>发布阶段（版本发布前）：</strong></p>\n<ul>\n<li>\n<p>性能测试：验证系统负载能力</p>\n</li>\n<li>\n<p>兼容性测试：多浏览器/设备验证</p>\n</li>\n<li>\n<p>手工测试：用户体验验证</p>\n</li>\n</ul>\n<p>通过这种分层的测试策略，既保证了开发效率，又确保了产品质量。代码驱动的自动化测试构成了质量保障的基础，而其他测试类型则在特定场景下提供补充验证。</p>\n","date_published":"2025-07-20T00:00:00.000Z","tags":["Node","自动化测试","Nestjs测试","单元测试"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2025/GitHub-Actions-%E8%87%AA%E5%8A%A8%E5%8F%91%E5%B8%83-npm-%E5%8C%85%E7%AE%80%E6%98%93%E6%8C%87%E5%8D%97/","url":"https://www.lihuanyu.com/posts/2025/GitHub-Actions-%E8%87%AA%E5%8A%A8%E5%8F%91%E5%B8%83-npm-%E5%8C%85%E7%AE%80%E6%98%93%E6%8C%87%E5%8D%97/","title":"GitHub Actions 自动发布 npm 包简易指南","summary":"概述 GitHub Actions作为GitHub原生的CI/CD工具，为开发者提供了强大的自动化能力。通过GitHub Actions自动发布npm包，可以显著提升开发效率，减少人为错误，确保发布流程的一致性和可靠性。","content_html":"<h2>概述</h2>\n<p>GitHub Actions作为GitHub原生的CI/CD工具，为开发者提供了强大的自动化能力。通过GitHub Actions自动发布npm包，可以显著提升开发效率，减少人为错误，确保发布流程的一致性和可靠性。</p>\n<p><strong>本指南专注于简单实用的配置方案，特别适合：</strong></p>\n<ul>\n<li>\n<p>个人开发者的小型项目</p>\n</li>\n<li>\n<p>不需要复杂自动化流程的简单项目</p>\n</li>\n<li>\n<p>希望快速上手、配置简单的场景</p>\n</li>\n<li>\n<p>追求可靠性而非复杂功能的项目</p>\n</li>\n</ul>\n<p>相比复杂的语义化版本控制方案（如semantic-release），本方案采用手动版本管理，配置简单，维护成本低，非常适合个人项目使用。</p>\n<h2>基础配置</h2>\n<h3>1. npm Token配置</h3>\n<p>首先需要在npm官网创建访问令牌：</p>\n<ol>\n<li>\n<p>登录 <a href=\"https://www.npmjs.com\">npmjs.com</a></p>\n</li>\n<li>\n<p>进入 Access Tokens 页面</p>\n</li>\n<li>\n<p>创建新的 Classic Token，类型选择 “Automation”</p>\n</li>\n<li>\n<p>复制生成的token（注意！只显示一次）</p>\n</li>\n</ol>\n<p>在GitHub仓库中配置Secrets：</p>\n<ol>\n<li>\n<p>进入仓库 Settings → Secrets and variables → Actions</p>\n</li>\n<li>\n<p>创建新的Repository secret</p>\n</li>\n<li>\n<p>Name: <code>NPM_TOKEN</code>，Value: 刚才复制的token</p>\n</li>\n</ol>\n<h3>2. 基础工作流配置</h3>\n<p>创建 <code>.github/workflows/npm-publish.yml</code> 文件：</p>\n<pre><code class=\"language-yaml\">name: Publish NPM Package\n\non:\n  push:\n    tags:\n      - 'v*'  # 当推送v开头的标签时触发\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: '20'\n          registry-url: 'https://registry.npmjs.org/'\n      - run: npm ci\n      - run: npm test # optional\n      - run: npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n</code></pre>\n<h2>简单发布流程</h2>\n<h3>1. 手动版本管理</h3>\n<p>使用npm内置命令管理版本：</p>\n<pre><code class=\"language-bash\"># 补丁版本 (1.0.0 -&gt; 1.0.1)\nnpm version patch\n\n# 次要版本 (1.0.0 -&gt; 1.1.0)  \nnpm version minor\n\n# 主要版本 (1.0.0 -&gt; 2.0.0)\nnpm version major\n</code></pre>\n<h3>2. 发布步骤</h3>\n<pre><code class=\"language-bash\"># 1. 更新版本号并创建标签\nnpm version patch\n\n# 2. 推送代码和标签到GitHub\ngit push origin main --tags\n\n# 3. GitHub Actions自动触发发布\n</code></pre>\n<h2>安全最佳实践</h2>\n<h3>1. Token安全管理</h3>\n<ul>\n<li>\n<p><strong>使用Automation类型token</strong>：绕过2FA限制，专门用于CI/CD</p>\n</li>\n<li>\n<p><strong>最小权限原则</strong>：只授予必要的发布权限</p>\n</li>\n<li>\n<p><strong>定期轮换token</strong>：建议定期更新访问令牌</p>\n</li>\n<li>\n<p><strong>环境变量隔离</strong>：只在npm publish步骤中暴露token</p>\n</li>\n</ul>\n<h3>2. 工作流安全配置</h3>\n<pre><code class=\"language-yaml\">permissions:\n  contents: read\n  packages: write\n</code></pre>\n<h3>3. 分支保护策略</h3>\n<ul>\n<li>\n<p>限制对主分支的直接推送</p>\n</li>\n<li>\n<p>要求通过Pull Request进行代码审查</p>\n</li>\n<li>\n<p>启用状态检查，确保测试通过后才能合并</p>\n</li>\n</ul>\n<h2>常见问题和解决方案</h2>\n<h3>1. 版本冲突</h3>\n<ul>\n<li>\n<p>确保每次发布前版本号已更新</p>\n</li>\n<li>\n<p>检查npm上是否已存在相同版本</p>\n</li>\n<li>\n<p>使用 <code>npm version</code> 命令避免手动错误</p>\n</li>\n</ul>\n<h3>2. 权限问题</h3>\n<ul>\n<li>\n<p>确保NPM_TOKEN具有发布权限</p>\n</li>\n<li>\n<p>检查包名是否已被占用</p>\n</li>\n<li>\n<p>验证组织权限设置</p>\n</li>\n</ul>\n<h3>3. 构建失败</h3>\n<ul>\n<li>\n<p>添加完整的测试步骤</p>\n</li>\n<li>\n<p>确保依赖安装正确</p>\n</li>\n<li>\n<p>使用缓存优化构建速度</p>\n</li>\n</ul>\n<h2>最佳实践总结</h2>\n<ol>\n<li>\n<p><strong>安全第一</strong>：妥善管理访问令牌，使用最小权限原则</p>\n</li>\n<li>\n<p><strong>简单可靠</strong>：使用标签触发，避免复杂的自动化逻辑</p>\n</li>\n<li>\n<p><strong>测试保障</strong>：发布前确保所有测试通过</p>\n</li>\n<li>\n<p><strong>版本管理</strong>：使用npm内置版本管理命令</p>\n</li>\n<li>\n<p><strong>监控告警</strong>：设置基本的失败检测</p>\n</li>\n</ol>\n<h2>pnpm版本配置</h2>\n<p>对于使用pnpm的项目，可以使用以下配置：</p>\n<pre><code class=\"language-yaml\">name: Publish NPM Package (pnpm)\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v2\n        with:\n          version: 9\n      - uses: actions/setup-node@v4\n        with:\n          node-version: '20'\n          registry-url: 'https://registry.npmjs.org/'\n          cache: 'pnpm'\n      - run: pnpm install --frozen-lockfile\n      - run: pnpm test # optional\n      - run: pnpm publish --no-git-checks\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n</code></pre>\n<p><strong>pnpm配置要点：</strong></p>\n<ul>\n<li>\n<p>使用 <code>pnpm/action-setup@v2</code> 安装pnpm</p>\n</li>\n<li>\n<p>使用 <code>cache: 'pnpm'</code> 启用pnpm缓存</p>\n</li>\n<li>\n<p>使用 <code>pnpm install --frozen-lockfile</code> 确保依赖一致性</p>\n</li>\n<li>\n<p>使用 <code>pnpm publish --no-git-checks</code> 发布包</p>\n</li>\n</ul>\n<h2>注意事项</h2>\n<h3>package.json配置检查</h3>\n<p>发布前请确保package.json配置正确：</p>\n<ol>\n<li><strong>移除private字段</strong>：如果存在 <code>&quot;private&quot;: true</code>，必须删除或设为 <code>false</code></li>\n</ol>\n<pre><code class=\"language-json\">{\n    &quot;name&quot;: &quot;your-package-name&quot;,\n    &quot;version&quot;: &quot;1.0.0&quot;,\n    // &quot;private&quot;: true,  ← 删除这行，注意json不支持注释，必须删除\n    &quot;description&quot;: &quot;Your package description&quot;\n}\n</code></pre>\n<ol start=\"2\">\n<li>\n<p><strong>确保包名可用</strong>：检查npm上是否已存在同名包</p>\n</li>\n<li>\n<p><strong>设置正确的入口文件</strong>：确保 <code>main</code> 字段指向正确的文件</p>\n</li>\n</ol>\n<pre><code class=\"language-json\">{\n  &quot;main&quot;: &quot;dist/index.js&quot;,\n  &quot;types&quot;: &quot;dist/index.d.ts&quot;\n}\n</code></pre>\n","date_published":"2025-07-19T00:00:00.000Z","tags":["Github Action","自动化","前端"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2025/%E6%97%A0%E4%BA%BA%E9%A9%BE%E9%A9%B6%E4%B8%8E%E4%BA%BA%E5%9B%A0%E5%B7%A5%E7%A8%8B/","url":"https://www.lihuanyu.com/posts/2025/%E6%97%A0%E4%BA%BA%E9%A9%BE%E9%A9%B6%E4%B8%8E%E4%BA%BA%E5%9B%A0%E5%B7%A5%E7%A8%8B/","title":"无人驾驶与人因工程","summary":"2025年3月29日22时44分，一辆小米SU7标准版在德上高速公路池祁段行驶过程中遭遇严重交通事故。根据小米公司披露的信息，事故发生前车辆处于NOA智能辅助驾驶状态，以116km/h时速持续行驶。事发路段因施工修缮，用路障封闭自车道、改道至逆向车道。车辆检测出障碍物后发出提醒并开始减速。随后驾驶员接管车辆进入人驾状态，持续减速并操控车辆转向，随后车辆与隔离","content_html":"<p>2025年3月29日22时44分，一辆小米SU7标准版在德上高速公路池祁段行驶过程中遭遇严重交通事故。根据小米公司披露的信息，事故发生前车辆处于NOA智能辅助驾驶状态，以116km/h时速持续行驶。事发路段因施工修缮，用路障封闭自车道、改道至逆向车道。车辆检测出障碍物后发出提醒并开始减速。随后驾驶员接管车辆进入人驾状态，持续减速并操控车辆转向，随后车辆与隔离带水泥桩发生碰撞，碰撞前系统最后可以确认的时速约为97km/h。</p>\n<p>小米汽车本身具有不小的话题性，事故出现后引起很多用户的争论，但很多人要么站队小米，指责驾驶员和其他批判小米的人，认为这起事故完全和小米无关，要么批判小米的技术，认为小米的技术不够好，不能保证安全。</p>\n<p>这种口水仗毫无意义，不如跳出这些话题，谈谈人因工程。</p>\n<p>首先尝试还原一下事故核心场景：</p>\n<p>随着筒锥的逼近，自动驾驶终于识别到前方有障碍物，告警提示驾驶员接管，但距离已经太近，驾驶员情急之下手打方向盘22度然后回正，最后与侧面护栏相撞。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2025-04-26/%E5%B0%8F%E7%B1%B3%E4%BA%8B%E6%95%85%E6%A8%A1%E6%8B%9F%E5%9B%BE.png\" alt=\"事故模拟还原\"></p>\n<p>整个轨迹大概如图：</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2025-04-26/%E4%BA%8B%E6%95%85%E8%BD%A8%E8%BF%B9%E6%A8%A1%E6%8B%9F%E8%BF%98%E5%8E%9F.png\" alt=\"事故轨迹模拟\"></p>\n<p>如上图，事故其实是驾驶员为了避障过度转向导致的事故。</p>\n<p>但这并非驾驶员的错误。</p>\n<p>根据小米披露的数据，事发前驾驶员转向22度，刹车踏板开合角度31度。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2025-04-26/%E4%BA%8B%E6%95%85%E4%B8%AD%E6%96%B9%E5%90%91%E7%9B%98%E4%B8%8E%E5%88%B9%E8%BD%A6%E6%95%B0%E6%8D%AE.png\" alt=\"方向盘与刹车数据\"></p>\n<p>在高速路上，方向盘打22度是什么概念？一般正常变道或轻微转向通常只需要约5-15度的方向盘转动角度。紧急避险情况下，方向盘转动角度可能会达到20-30度，但这已经是相当大的转向幅度，会导致车辆明显的横向移动。高速行驶时（如100km/h以上）方向盘转动超过30度就已经是非常剧烈的转向了，可能导致车辆失控。</p>\n<p>也就是说，这个转动幅度很大，但并非完全不能操作。那看起来好像还是驾驶员的经验与能力的问题？</p>\n<p>然而，将责任归咎于驾驶员是不公平的。这涉及到人因工程中的一个核心概念：情景意识（Situation Awareness）。在这种紧急情况下，我们不能期望驾驶员能够立即建立完整的情景意识。</p>\n<p>传统手动驾驶中，驾驶员需要不断进行微调方向盘，这个过程帮助大脑建立了速度与转向角度之间的对应关系。这种持续的反馈形成了驾驶员的情景意识，使他们能够准确判断在特定速度下需要多大的转向角度。</p>\n<p>智能辅助驾驶系统虽然减轻了驾驶员的负担，但同时也切断了这种持续反馈。当系统突然要求驾驶员接管时，驾驶员缺乏当前情境下的&quot;感觉&quot;，只能依靠长期记忆中的经验来判断。</p>\n<p>在非紧急情况下，驾驶员通常会先尝试小角度转向，然后根据车辆反应逐渐调整。但当障碍物近在眼前时，驾驶员必须立即给出一个&quot;足够大&quot;的转向角度，<strong>而这个判断往往不够准确</strong>。本次事故中，驾驶员给出的22度转向角度就是这种紧急情况下的本能反应。</p>\n<p>这种现象在航空领域有着更为惨痛的教训。2009年的法航447航班空难就是一个典型案例：当空速管结冰导致自动驾驶突然退出时，接手的飞行员缺乏对当前飞行状态的准确感知，做出了错误的操作决策，最终导致飞机坠毁，228人全部遇难。</p>\n<p>这类事故之所以反复发生，是因为现代自动化系统往往将人排除在控制回路之外。系统正常运行时，操作员不需要（也不被要求）了解系统的所有细节。随着时间推移，操作员对系统状态的理解逐渐过时，当系统突然要求人工接管时，操作员需要时间重新建立情景意识，而紧急情况往往不给这个时间。</p>\n<p>人因工程学界将这种现象称为&quot;伐木工效应&quot;（Lumberjack Effect）：自动化程度越高，操作员在日常中的参与度就越低，技能保持越差，当需要接管时就越容易出错。这是一个悖论：自动化系统越先进，在罕见的需要人工接管的情况下，失败的风险反而更高。</p>\n<p>解决这一问题的方向包括：设计更透明的自动化系统，让操作员始终了解系统状态；开发自适应自动化，根据情况动态调整自动化水平；将自动化系统设计为&quot;团队成员&quot;而非替代者，保持人在回路中的参与。</p>\n<blockquote>\n<p>让我想起了《流浪地球2》里最后的彩蛋，提到moss的训练模式引入了 人在回路 。</p>\n</blockquote>\n<p>遗憾的是，工业界特别是国内对人因工程的重视程度不够。人因工程常被视为&quot;软科学&quot;，甚至被贬低为&quot;文科&quot;。当系统设计要求人类做出超出人类能力范围的操作时，事故责任往往被归咎于操作员&quot;不够专注&quot;或&quot;训练不足&quot;。</p>\n<p>只有航空、核电等对安全有极高要求的行业才真正重视人因工程。对于新兴的智能驾驶领域，这方面的进步可能还需要付出更多代价才能实现。</p>\n","date_published":"2025-04-26T00:00:00.000Z","tags":["随笔","人因工程","无人驾驶"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2025/%E5%A6%82%E4%BD%95%E9%9B%86%E6%88%90Github%E7%99%BB%E5%BD%95/","url":"https://www.lihuanyu.com/posts/2025/%E5%A6%82%E4%BD%95%E9%9B%86%E6%88%90Github%E7%99%BB%E5%BD%95/","title":"如何集成Github登录","summary":"现在越来越多的网站不再自建登录系统，而是采用第三方登录的方式。比如：QQ、微信、微博、Github等。 其中Github登录是偏技术类网站或新一些的网站的首选，因为它的开放API和生态系统非常强大。 那么如何集成Github登录呢？本文介绍前端主导的Github登录流程。","content_html":"<p>现在越来越多的网站不再自建登录系统，而是采用第三方登录的方式。比如：QQ、微信、微博、Github等。</p>\n<p>其中Github登录是偏技术类网站或新一些的网站的首选，因为它的开放API和生态系统非常强大。</p>\n<p>那么如何集成Github登录呢？本文介绍前端主导的Github登录流程。</p>\n<h2>OAuth2.0 基本概念</h2>\n<p>Github登录使用的是OAuth2.0协议。OAuth2.0是一个授权框架，允许第三方应用获取对用户资源的有限访问权限。</p>\n<p>在OAuth2.0中，主要涉及四个角色：</p>\n<ol>\n<li>资源所有者（用户）</li>\n<li>客户端（第三方应用）</li>\n<li>授权服务器（Github的OAuth服务）</li>\n<li>资源服务器（Github的API服务）</li>\n</ol>\n<h2>创建Github OAuth应用</h2>\n<ol>\n<li>登录Github，进入Settings -&gt; Developer settings -&gt; OAuth Apps</li>\n<li>点击&quot;New OAuth App&quot;按钮</li>\n<li>填写应用信息：\n<ul>\n<li>Application name：应用名称</li>\n<li>Homepage URL：应用主页URL</li>\n<li>Application description：应用描述（可选）</li>\n<li>Authorization callback URL：授权回调URL</li>\n</ul>\n</li>\n<li>创建完成后，你会获得Client ID和Client Secret</li>\n</ol>\n<blockquote>\n<p>注意：Client Secret 是非常重要的，一定不能放在前端，千万不要泄露给任何人！</p>\n</blockquote>\n<h2>前端实现</h2>\n<h3>1. 发起授权请求</h3>\n<pre><code class=\"language-javascript\">const clientId = 'your_client_id';\nconst redirectUri = 'your_redirect_uri';\nconst scope = 'read:user user:email'; // 根据需要设置权限范围\n\nfunction redirectToGithub() {\n  const authUrl = `https://github.com/login/oauth/authorize?client_id=${clientId}&amp;redirect_uri=${redirectUri}&amp;scope=${scope}`;\n  window.location.href = authUrl;\n}\n\n// 在登录按钮点击时调用\ndocument.getElementById('github-login').addEventListener('click', redirectToGithub);\n</code></pre>\n<h3>2. 处理回调</h3>\n<pre><code class=\"language-javascript\">// callback.js\nasync function handleCallback() {\n  const code = new URLSearchParams(window.location.search).get('code');\n  if (!code) return;\n\n  try {\n    // 向后端发送code获取access token\n    const response = await fetch('/api/github/callback', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json'\n      },\n      body: JSON.stringify({ code })\n    });\n\n    const data = await response.json();\n    if (data.access_token) {\n      // 保存token并获取用户信息\n      localStorage.setItem('github_token', data.access_token);\n      await fetchUserInfo(data.access_token);\n    }\n  } catch (error) {\n    console.error('Github登录失败:', error);\n  }\n}\n\nasync function fetchUserInfo(token) {\n  try {\n    const response = await fetch('https://api.github.com/user', {\n      headers: {\n        'Authorization': `token ${token}`\n      }\n    });\n    const userInfo = await response.json();\n    console.log('用户信息:', userInfo);\n    // 处理登录成功后的逻辑\n  } catch (error) {\n    console.error('获取用户信息失败:', error);\n  }\n}\n\n// 页面加载时检查是否有授权码\ndocument.addEventListener('DOMContentLoaded', handleCallback);\n</code></pre>\n<h2>服务端实现</h2>\n<p>服务端主要负责处理Github OAuth的核心流程，主要是code换取token必须在服务端进行。用express框架做一个简易示例：</p>\n<pre><code class=\"language-javascript\">// 使用express框架\nconst express = require('express');\nconst axios = require('axios');\nconst app = express();\n\n// 环境变量中配置\nconst clientId = process.env.GITHUB_CLIENT_ID;\nconst clientSecret = process.env.GITHUB_CLIENT_SECRET;\n\napp.post('/api/github/callback', async (req, res) =&gt; {\n  const { code } = req.body;\n  \n  try {\n    // 使用code换取access token\n    const tokenResponse = await axios.post('https://github.com/login/oauth/access_token', {\n      client_id: clientId,\n      client_secret: clientSecret,\n      code: code\n    }, {\n      headers: {\n        Accept: 'application/json'\n      }\n    });\n\n    const { access_token } = tokenResponse.data;\n\n    // 返回必要信息给前端\n    res.json({\n      access_token,\n    });\n\n  } catch (error) {\n    console.error('Github OAuth Error:', error);\n    res.status(500).json({\n      error: '授权失败'\n    });\n  }\n});\n\n// 启动服务器\napp.listen(3000, () =&gt; {\n  console.log('Server is running on port 3000');\n});\n</code></pre>\n<h3>服务端注意事项</h3>\n<ol>\n<li>\n<p><strong>安全存储</strong></p>\n<ul>\n<li>Client Secret必须安全存储，推荐使用环境变量</li>\n<li>用户token要加密存储在数据库中</li>\n</ul>\n</li>\n<li>\n<p><strong>Token管理</strong></p>\n<ul>\n<li>实现token过期和刷新机制</li>\n<li>定期清理无效token</li>\n</ul>\n</li>\n<li>\n<p><strong>错误处理</strong></p>\n<ul>\n<li>完善的错误日志记录</li>\n<li>友好的错误提示</li>\n</ul>\n</li>\n<li>\n<p><strong>并发控制</strong></p>\n<ul>\n<li>防止重复授权请求</li>\n<li>使用Redis等缓存提高性能</li>\n</ul>\n</li>\n</ol>\n<h2>最佳实践</h2>\n<ol>\n<li>\n<p><strong>错误处理</strong>：实现完善的错误处理机制，为用户提供清晰的错误提示。</p>\n</li>\n<li>\n<p><strong>Loading状态</strong>：在授权过程中显示loading状态，提升用户体验。</p>\n</li>\n<li>\n<p><strong>Token管理</strong>：合理管理access token的存储和刷新机制。</p>\n</li>\n<li>\n<p><strong>用户体验</strong>：提供清晰的登录按钮和流程提示。</p>\n</li>\n</ol>\n<h2>总结</h2>\n<p>Github OAuth登录的实现并不复杂，关键是要理解OAuth2.0的基本流程，注意安全性问题，并做好错误处理和用户体验优化。通过以上步骤，你就可以为你的应用添加Github登录功能了。</p>\n<p>本文主要介绍的是重前端的方式来实现这个，但真实项目中这些事项更应该放在服务端进行，后续有机会再分享重服务端的实现。</p>\n","date_published":"2025-04-20T00:00:00.000Z","tags":["前端","OAuth","Github","登录","OAuth2.0","Github登录","三方登录"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2025/%E4%BA%92%E8%81%94%E7%BD%91%E5%88%9B%E4%B8%9A%E5%AF%92%E5%86%AC/","url":"https://www.lihuanyu.com/posts/2025/%E4%BA%92%E8%81%94%E7%BD%91%E5%88%9B%E4%B8%9A%E5%AF%92%E5%86%AC/","title":"互联网创业寒冬","summary":"阮一峰的 Blog 里提到，现在互联网创业几乎消失了，并引用了一篇文章同时总结了4个原因。 1. 互联网行业已经成熟了，留给创业者的机会大幅减少。互联网的大部分果实已被摘取，早期的高增长难以再现。真正的创新机会即使还能找到，也会被现有的大公司快速抄袭，不会留给创业者。AI 大模型出现后，互联网本身也在衰弱，创业机会就更少了。 2. 创业的机会成本变大了。一个","content_html":"<p>阮一峰的 Blog 里提到，现在互联网创业几乎消失了，并引用了一篇文章同时总结了4个原因。</p>\n<ol>\n<li>互联网行业已经成熟了，留给创业者的机会大幅减少。互联网的大部分果实已被摘取，早期的高增长难以再现。真正的创新机会即使还能找到，也会被现有的大公司快速抄袭，不会留给创业者。AI 大模型出现后，互联网本身也在衰弱，创业机会就更少了。</li>\n<li>创业的机会成本变大了。一个大厂的高级工程师，现在的薪酬（包括股票期权）超过百万，创业很难打动他了。</li>\n<li>风险投资的商业模式难以实现了。风投预期的模式是项目高速增长，最终实现上市退出。而这越来越难做到了，能够指数式增长的线上项目现在基本找不到。</li>\n<li>创业者的生活态度发生了变化。人们比以前更重视生活质量，越来越不愿意接受创业带来的没日没夜的劳作、倦怠、失败的人际关系、心理健康问题。</li>\n</ol>\n<p>阮一峰认为第一条是根本原因：互联网的高增长结束，行业的机会少了。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2025-02-16/civitai.png\" alt=\"寒冬与巨鲸\"></p>\n<p>但阮老师也不全是悲观，最后判断是，单纯的互联网创业，应该再也不会像以前那样兴旺了，很可能就是社会的平均增长率和回报率。<strong>未来互联网的机会更多是与其他行业结合</strong>，就好像现在的 AI 创业，很多都是 AI 为主，互联网为辅。</p>\n<p>我觉得很有道理，顺着链接看了下原文，很短小，遂找 AI 帮忙翻译为汉语，分享给各位。</p>\n<p>原文标题是《创业寒冬：Hacker News信仰崩塌记》</p>\n<blockquote>\n<p>Hacker News 是一个海外极客社区，形态非常类似贴吧、论坛。</p>\n</blockquote>\n<p>2013年，一位创业失败者在Hacker News讲述经历。评论区充满温情：“失败只是一个事件，不是你的身份标签”、“收拾心情重新出发”、“这都是为下次成功积累经验”。</p>\n<p>转眼来到2025年。另一位连续六次折戟的创业者分享心路。这次评论区的画风突变：“当初去大厂打工可能更明智”、“这场老鼠赛跑根本不值得”、“最真实的故事总被掩埋，我们看到的都是精心粉饰的剧本”。</p>\n<blockquote>\n<p>老鼠赛跑原文是 Rat race ，是一个比喻性表达，指：现代社会中永无止境的竞争、常用来形容职场、城市生活中为追求成功/财富/地位而疲于奔命的状态，就像实验室里踩着滚轮不停奔跑却无法前进的老鼠。整句含义大概为：“与其在创业这种高风险、高压力的赛道无意义地消耗生命，不如选择更稳定的职业路径”</p>\n</blockquote>\n<p>这种转变绝非个案。曾高呼&quot;快速试错，频繁迭代&quot;的极客圣地，如今开始系统性质疑创业本身的价值逻辑。</p>\n<p>究竟发生了什么？</p>\n<p>创业者的身心代价变得更加直观。过劳猝死、家庭破裂、抑郁症等案例，再也无法用&quot;奋斗者文化&quot;的遮羞布掩盖。</p>\n<p>科技巨头的薪资待遇改写了风险收益公式。当资深工程师在成熟企业轻松斩获30万美元+年薪，创业的财务合理性开始动摇。</p>\n<p>风险投资模式的局限性浮出水面。资本对超高速增长和退出预期的偏执，让无数创业者在真实商业逻辑与投资人诉求间进退维谷。</p>\n<p>行业进入成熟期。移动互联网时代的低垂果实已被摘尽，真正具有颠覆性的创新机会愈发稀缺。</p>\n<p>我们正在步入的&quot;创业寒冬&quot;，并非指创业行为的消亡，而是围绕它的造神运动正在冰封。</p>\n<p>这场寒冬过后，或许会孕育出更质朴的创业生态：既有传统风投模式的一席之地，也会涌现更多元化的创新路径。这个新生态可能不再光鲜，但必定更加真实。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2025-02-16/00189-2544810901.png\" alt=\"希望之花\"></p>\n","date_published":"2025-02-16T00:00:00.000Z","tags":["随笔","创业","互联网"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2025/%E5%9C%A8Github%20Action%E9%87%8C%E6%9E%84%E5%BB%BA%E5%A4%A7%E5%9E%8BDocker%E9%95%9C%E5%83%8F/","url":"https://www.lihuanyu.com/posts/2025/%E5%9C%A8Github%20Action%E9%87%8C%E6%9E%84%E5%BB%BA%E5%A4%A7%E5%9E%8BDocker%E9%95%9C%E5%83%8F/","title":"在Github Action里构建大型Docker镜像","summary":"GitHub 提供了非常好用的 Action 功能，最常见的是可以在里面做一些 CI 工作，比如单元测试、Lint等。 在国内，受一堵墙的影响，不少开发事项需要特殊的技巧方能正常进行，以至于中国程序员人均拥有比外国程序员更好的网络基础知识。 这堵墙在 Docker 镜像构建方面影响尤其大，虽然通过配置代理可以解决，但如果我们想在构建里再装一些 Python ","content_html":"<p>GitHub 提供了非常好用的 Action 功能，最常见的是可以在里面做一些 CI 工作，比如单元测试、Lint等。</p>\n<p>在国内，受一堵墙的影响，不少开发事项需要特殊的技巧方能正常进行，以至于中国程序员人均拥有比外国程序员更好的网络基础知识。</p>\n<p>这堵墙在 Docker 镜像构建方面影响尤其大，虽然通过配置代理可以解决，但如果我们想在构建里再装一些 Python 的包之类的行为，很困难。</p>\n<p>如果能利用 Github Action 来进行 Docker 镜像的构建，将会极大提升便利性和幸福感。</p>\n<p>在 Github Action 的 marketplace 里能找到一些相关的工具，同时也可以直接在 workflow.yaml 里写 docker build 。</p>\n<p>在一般的小型镜像构建任务里，这样就足够了，例如：</p>\n<pre><code class=\"language-yaml\">name: Docker Image CI\n\non:\n  push:\n    branches: [ &quot;main&quot; ]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v3\n\n    - name: Set up QEMU\n      uses: docker/setup-qemu-action@v3\n    - name: Set up Docker Buildx\n      uses: docker/setup-buildx-action@v3\n    - name: Login to Docker Hub\n      uses: docker/login-action@v3\n      with:\n        username: ${{ secrets.DOCKERHUB_USERNAME }}\n        password: ${{ secrets.DOCKERHUB_TOKEN }}\n    - name: Build and push\n      uses: docker/build-push-action@v5\n      with:\n        push: true\n        tags: ${{ env.docker_namespace }}/${{ env.image_name }}:${{ version }} # 注意替换，直接写死也行\n</code></pre>\n<p>这样就实现了当向 main 分支提交代码时，就在 Ubuntu 环境下进行 docker 构建和推送。</p>\n<p>但现在随着大模型的兴起，一些 AI 应用开始变得流行，比如 AI 绘图使用的开源软件 Stable Diffusion ，如果想把 SD 放在 Docker 镜像里，那一堆庞大的 Python 依赖（pytorch、cuda等），会导致 Github Action 执行时报错，提示磁盘已用尽。</p>\n<p>这个解决方法很简单，但也有极限，自测经验是 13GB 左右的镜像是可以构建成功的。只需要在开始执行 Docker 镜像构建前执行这一段脚本进行磁盘清理即可：</p>\n<pre><code class=\"language-yaml\">name: Docker Image CI\n\non:\n  push:\n    branches: [ &quot;main&quot; ]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v3\n    - name: Free Disk Space (Ubuntu) # 就是通过这一步进行释放空间\n      uses: jlumbroso/free-disk-space@main\n      with:\n        # 这个可能会删除一些你实际需要的工具，所以建议还是 false 让它保留\n        # 如果你打算尝试移除它，可以再腾出大约6GB的空间\n        tool-cache: false\n\n        # 如果有你需要使用的，将其设置为 false ，比如这里我们要构建Docker，Docker相关的工具是不能删的\n        android: true\n        dotnet: true\n        haskell: true\n        large-packages: true\n        docker-images: false\n        swap-storage: true\n    - name: Set up QEMU\n      uses: docker/setup-qemu-action@v3\n    - name: Set up Docker Buildx\n      uses: docker/setup-buildx-action@v3\n    - name: Login to Docker Hub\n      uses: docker/login-action@v3\n      with:\n        username: ${{ secrets.DOCKERHUB_USERNAME }}\n        password: ${{ secrets.DOCKERHUB_TOKEN }}\n    - name: Build and push\n      uses: docker/build-push-action@v5\n      with:\n        push: true\n        tags: ${{ env.docker_namespace }}/${{ env.image_name }}:${{ version }} # 注意替换，直接写死也行\n</code></pre>\n<p>具体案例可以参考 <a href=\"https://github.com/sky-admin/serverless-pod-a1111-upload\">https://github.com/sky-admin/serverless-pod-a1111-upload</a> ，这是一个允许 A1111/Stable-Diffusion-WebUI 在 RunPod 的 serverless 服务上运行模型执行AI绘图的镜像。未来也许会详细介绍如何在 Runpod 上部署 SD 绘图服务。</p>\n<p>那如果想构建更大的镜像呢？就需要自行提供 Action 执行所在的 Runner 了，这个目前还没有尝试，但有基本的思路和方案，未来尝试后再来分享。</p>\n","date_published":"2025-02-16T00:00:00.000Z","tags":["Github","Docker"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2025/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E7%BB%84%E4%BB%B6%E5%BA%93%E8%AF%A5%E7%94%A8rpx%E8%BF%98%E6%98%AFpx/","url":"https://www.lihuanyu.com/posts/2025/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E7%BB%84%E4%BB%B6%E5%BA%93%E8%AF%A5%E7%94%A8rpx%E8%BF%98%E6%98%AFpx/","title":"小程序组件库该用rpx还是px？","summary":"一、问题背景：组件库用 px，业务开发用 rpx 1.1 现状冲突 目前小程序的开发领域有一个奇怪的现象： 组件库 ：Vant、Uni UI 等主流组件库的样式表中， width: 100px 随处可见 业务代码 ：业务前端清一色使用 width: 200rpx ，开发者对 rpx 趋之若鹜 这就引出一个 矛盾点 ：当开发者引入一个 px 单位的按钮组件时，","content_html":"<h2>一、问题背景：组件库用 px，业务开发用 rpx</h2>\n<h3>1.1 现状冲突</h3>\n<p>目前小程序的开发领域有一个奇怪的现象：</p>\n<ul>\n<li><strong>组件库</strong>：Vant、Uni-UI 等主流组件库的样式表中，<code>width: 100px</code> 随处可见</li>\n<li><strong>业务代码</strong>：业务前端清一色使用 <code>width: 200rpx</code>，开发者对 rpx 趋之若鹜</li>\n</ul>\n<p>这就引出一个<strong>矛盾点</strong>：当开发者引入一个 px 单位的按钮组件时，必须手动覆盖样式或做单位转换：</p>\n<pre><code class=\"language-css\">/* 业务代码中强行适配 */\n.van-button {\n  width: 200rpx!important; /* 破坏组件库封装性 */\n}\n</code></pre>\n<h3>1.2 为什么组件库坚持 px？</h3>\n<p><strong>历史原因</strong>：2017 年微信小程序刚推出时，rpx 的渲染机制存在 BUG（如 iOS Retina 屏模糊），早期组件库被迫选择 px。<br>\n<strong>跨端隐患</strong>：部分跨端框架（如 Taro）需用 px 兼容 H5 和 APP，直接使用 rpx 会导致多端样式混乱。<br>\n<strong>稳定性担忧</strong>：px 在折叠屏、Pad 等设备上表现更可控，rpx 的全局缩放可能导致组件错位。</p>\n<hr>\n<h2>二、业务开发为何偏爱 rpx？</h2>\n<h3>2.1 效率碾压式优势</h3>\n<p>假设设计师提供 750px 宽度的设计稿：</p>\n<ul>\n<li><strong>rpx 方案</strong>：直接按 1:1 映射，200px 的元素写作 <code>200rpx</code></li>\n<li><strong>px + 响应式方案</strong>：需计算百分比、设置断点、处理多端差异</li>\n</ul>\n<p><strong>实际对比</strong>：开发一个商品列表页：</p>\n<pre><code class=\"language-css\">/* rpx 方案 */\n.item {\n  width: 350rpx; \n  margin: 20rpx;\n}\n\n/* px + 响应式方案 */\n.item {\n  width: 175px;\n  margin: 10px;\n}\n@media (max-width: 375px) {\n  .item { width: 150px; }\n}\n/* 还要考虑华为折叠屏、iPad等设备... */\n</code></pre>\n<h3>2.2 设计协作的天然优势</h3>\n<p>当设计稿标注为 750px 时：</p>\n<ul>\n<li><strong>开发者</strong>：无需换算，<code>设计稿标注值 = rpx 值</code></li>\n<li><strong>设计师</strong>：不需要学习 vw/rem 等复杂单位</li>\n</ul>\n<hr>\n<h2>三、化解矛盾的工程方案</h2>\n<h3>3.1 方案一：组件库提供 rpx 版本</h3>\n<p><strong>实现原理</strong>：通过 CSS 变量动态切换单位</p>\n<pre><code class=\"language-css\">/* 组件库源码 */\n.van-button {\n  width: var(--button-width, 100px); \n}\n\n/* 业务代码注入变量 */\n:root {\n  --button-width: 200rpx; /* 一键切换为 rpx */\n}\n</code></pre>\n<p><strong>案例</strong>：京东 NutUI 小程序版支持 <code>px/rpx</code> 双模式，通过 <code>npm run build:rpx</code> 生成 rpx 版本。</p>\n<h3>3.2 方案二：构建工具自动转换</h3>\n<p><strong>实现原理</strong>：用 PostCSS 插件批量转换组件库的 px → rpx</p>\n<pre><code class=\"language-js\">// postcss.config.js\nmodule.exports = {\n  plugins: {\n    'postcss-px2rpx': {\n      ratio: 2 // 1px = 2rpx（根据设计稿调整）\n    }\n  }\n}\n</code></pre>\n<p><strong>转换效果</strong>：</p>\n<pre><code class=\"language-css\">/* 输入：组件库源码 */\n.van-button { width: 100px; }\n\n/* 输出：转换后代码 */\n.van-button { width: 200rpx; }\n</code></pre>\n<p>这部分做得比较好的是滴滴的小程序框架 Mpx，它内置了px到rpx的转换功能，并且支持选择性转换，开发者可以通过注释来标记不需要转换的px，大大提高了开发灵活性和效率。</p>\n<hr>\n<h2>四、选型建议：根据团队类型抉择</h2>\n<table>\n<thead>\n<tr>\n<th><strong>团队类型</strong></th>\n<th><strong>推荐方案</strong></th>\n<th><strong>原因</strong></th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>自研组件库</td>\n<td>原生支持 rpx</td>\n<td>掌控源码，无历史包袱</td>\n</tr>\n<tr>\n<td>使用第三方组件库</td>\n<td>方案二（构建工具转换）</td>\n<td>无侵入、低成本适配</td>\n</tr>\n</tbody>\n</table>\n<hr>\n<h2>五、总结与展望</h2>\n<h3>5.1 核心结论</h3>\n<ul>\n<li><strong>组件库可以选择 rpx</strong></li>\n<li><strong>工程化是破局关键</strong>：通过工具抹平单位差异，开发者不必二选一</li>\n</ul>\n<p><strong>让技术回归本质</strong>：单位的本质是提升效率而非制造对立。当工具链足够成熟时，开发者终将摆脱单位之争，专注创造业务价值。</p>\n","date_published":"2025-02-15T00:00:00.000Z","tags":["前端","小程序"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2025/%E5%9C%A8%E5%9B%BD%E5%86%85%E7%9A%84%E5%B9%B3%E5%8F%B0%E4%BD%A0%E6%B2%A1%E6%9C%89%E7%B2%89%E4%B8%9D/","url":"https://www.lihuanyu.com/posts/2025/%E5%9C%A8%E5%9B%BD%E5%86%85%E7%9A%84%E5%B9%B3%E5%8F%B0%E4%BD%A0%E6%B2%A1%E6%9C%89%E7%B2%89%E4%B8%9D/","title":"在国内的平台，你没有粉丝","summary":"不知道有多少搞自媒体的朋友，不管是玩抖音、快手、视频号、公众号、小红书、微博，只要仔细想一想，就会发现，我们的粉丝根本不是自己的，而是平台的。 算法时代：内容的可见性由谁决定？ 曾几何时，关注一个账号，就能稳定地看到他的所有更新。但现在，即使关注了100个账号，平台也只会优先推送其中20 30个。这不是我们的选择，而是算法的决定。 以抖音为例，据非官方数据显","content_html":"<p>不知道有多少搞自媒体的朋友，不管是玩抖音、快手、视频号、公众号、小红书、微博，只要仔细想一想，就会发现，我们的粉丝根本不是自己的，而是平台的。</p>\n<h2>算法时代：内容的可见性由谁决定？</h2>\n<p>曾几何时，关注一个账号，就能稳定地看到他的所有更新。但现在，即使关注了100个账号，平台也只会优先推送其中20-30个。这不是我们的选择，而是算法的决定。</p>\n<p>以抖音为例，据非官方数据显示，关注账号的内容在用户首页Feed流中的展示比例不到30%。也就是说，即使别人点了关注，新作品也未必能出现在他们的视线里。</p>\n<p>微信公众号更是如此，大部分公众号的打开率不足15%。一个拥有10万粉丝的账号，实际阅读量可能只有1万出头，而且这个比例还在持续下降。</p>\n<p><strong>这就是现实：在算法分发的时代，粉丝只是一个数字，而非忠实的追随者。</strong></p>\n<h2>后来者的困境：内容好≠有人看</h2>\n<p>&quot;只要坚持输出优质内容，粉丝自然会来。&quot;这句话在2016年或许还有几分道理，但在2025年的今天，已经成为一个美丽的神话。</p>\n<p>为什么？因为平台红利期已过，内容创作者数量呈指数级增长，而用户注意力却是有限的。</p>\n<p>以小红书为例，据统计，2024年平台日活跃创作者已超过1000万，而日均发布笔记量超过3000万条。在这样的内容海洋中，即使作品质量上乘，也很难自然地被发现。</p>\n<p>更残酷的是，平台算法往往偏向于：</p>\n<ol>\n<li>已有高互动的账号</li>\n<li>符合平台当下推广方向的内容</li>\n<li>能够留存用户的&quot;爽文&quot;或&quot;爽视频&quot;</li>\n</ol>\n<p>这就形成了一个悖论：<strong>新人需要曝光来积累粉丝，但没有粉丝就难以获得初始曝光。</strong></p>\n<h2>平台的真相：创作者只是流量的载体</h2>\n<p>为什么会这样？因为平台的核心诉求从来不是帮创作者积累粉丝，而是留存用户、提高广告价值。</p>\n<p>当平台发现，通过算法推荐陌生但有吸引力的内容，比展示用户已关注的内容更能提高用户停留时间时，&quot;关注&quot;这个功能就被悄悄弱化了。</p>\n<p>在这个机制下，创作者沦为了流量的载体，平台可以随时通过算法调整来决定谁能&quot;活&quot;下去：</p>\n<ul>\n<li>今天推广情感故事，明天可能转向美食探店</li>\n<li>昨天还爆火的题材，今天可能就被限流</li>\n</ul>\n<p><strong>这就是为什么我们会看到很多创作者在平台上忽然&quot;消失&quot;——不是他们不更新了，而是算法不再青睐他们。</strong></p>\n<h2>但这不意味着放弃</h2>\n<p>尽管现状如此，但这并不意味着应该放弃自媒体创作。相反，我们需要更清醒地认识这个游戏规则，并做出相应调整：</p>\n<h3>1. 内容依然是根基</h3>\n<p>虽然好内容不等于有流量，但没有好内容一定没有持续流量。在算法推荐的世界里，内容质量是被推荐的前提条件。</p>\n<h3>2. 多平台布局，降低风险</h3>\n<p>不要把所有鸡蛋放在一个篮子里。当一个平台的算法不友好时，其他平台可能会给我们机会。</p>\n<h3>3. 建立平台之外的连接</h3>\n<p>这是最关键的一点：<strong>将平台流量转化为平台之外的资产</strong>。</p>\n<p>具体可以：</p>\n<ul>\n<li>建立自己的网站或博客</li>\n<li>运营邮件订阅列表</li>\n<li>创建私域社群（如微信群、Discord等）</li>\n<li>开设付费会员制度</li>\n</ul>\n<p>这些渠道虽然起步慢、增长缓，但它们是真正属于创作者自己的，不受平台算法波动影响。</p>\n<h2>独立渠道：真正属于自己的阵地</h2>\n<p>在所有应对策略中，建立独立渠道是最具战略意义的一步。</p>\n<p>以个人博客为例，它有几个无可替代的优势：</p>\n<ul>\n<li>完全的内容控制权</li>\n<li>不受平台规则限制</li>\n<li>SEO带来的长尾流量</li>\n<li>可以自由设置盈利模式</li>\n<li>数据完全透明且归自己所有</li>\n</ul>\n<p>我的一位朋友，在视频平台有10万粉丝，但他最看重的是自己运营5年的博客和3000人的邮件列表。当他的视频账号因某次平台调整突然流量锐减时，这些独立渠道成了他事业的救命稻草。</p>\n<h2>结语：与算法共存，而非被其奴役</h2>\n<p>在这个被算法主宰的内容世界，与其抱怨不如适应，与其依赖不如独立。</p>\n<p>平台仍然是内容分发的重要渠道，但我们需要清醒地认识到：<strong>在国内平台上，没有真正的粉丝，只有被算法暂时青睐的机会。</strong></p>\n<p>所以，继续创作有价值的内容，但别忘了在平台之外，建立真正属于自己的阵地。那里的每一个访客、每一位订阅者，才是真正对内容感兴趣的人。</p>\n<p>当下次算法调整来临时，这些平台之外的连接，才是内容创作生涯中最宝贵的财富。</p>\n","date_published":"2025-02-01T00:00:00.000Z","tags":["随笔","自媒体"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2025/%E6%9C%AC%E5%9C%B0%E9%83%A8%E7%BD%B2deepseek%E4%B8%8ESillyTavern/","url":"https://www.lihuanyu.com/posts/2025/%E6%9C%AC%E5%9C%B0%E9%83%A8%E7%BD%B2deepseek%E4%B8%8ESillyTavern/","title":"本地部署deepseek与SillyTavern","summary":"25年的春节，深度求索（deepseek）给AI圈乃至全国人民都带来一个很厉害的AI模型。Deepseek R1，一个仅用600万不到的成本训练出来的大模型做到了和全球最先进的 OpenAI O1 模型不相上下的效果。这背后带来的变化有无数博主春节加班给大家分享，我这里就不班门弄斧了，不仅了解的信息比别人少，文笔也远不如那些头部自媒体。但作为一个技术人，分享","content_html":"<p>25年的春节，深度求索（deepseek）给AI圈乃至全国人民都带来一个很厉害的AI模型。Deepseek R1，一个仅用600万不到的成本训练出来的大模型做到了和全球最先进的 OpenAI O1 模型不相上下的效果。这背后带来的变化有无数博主春节加班给大家分享，我这里就不班门弄斧了，不仅了解的信息比别人少，文笔也远不如那些头部自媒体。但作为一个技术人，分享一个本地的部署和玩法还是OK的。</p>\n<p>所以本期的内容是分享 Deepseek 的本地部署和连接 SillyTavern 使用。对了，这个过程对电脑性能有一定要求，如果没有一张较强的显卡（推荐是3070及以上，低一些可能也行但估计体验较差），不推荐尝试。</p>\n<p>在我动手前，B站上就已经有铺天盖地的视频教程出现了，包括现在我写下这篇文章时，连 deepseek 连接 SillyTavern 的视频教程也出现了。差点想删除草稿了，但想到我折腾时搜不到东西，最后靠切换成英语关键词搜索才在 medium 平台上看到一篇资料，也许文本还是有和视频教程不同的地方吧，希望能帮助到后来者。</p>\n<h2>关于 Deepseek</h2>\n<p>在开始前，还是想再多说两句 deepseek。</p>\n<p>很早我就关注到这家公司了，它就是国内 AI 大模型价格战的发起方，随着它的V2版本发布，它把价格直接降到 1 元每百万 token ，中文粗略计算可以直接除以 2，也就是 1 块钱买 AI 输出 50 万字，这简直太便宜了。随着它的降价，百度、阿里、字节、腾讯纷纷降价。但从我了解的信息，深度求索的大模型靠 MoE 方案，本身成本就是非常低，即使是 1元/百万token 的费用，深度求索公司依然是盈利的，这让我感觉到不可思议。相比之下其他家的降价则更像是一种补贴，用亏损换市场的行为，后期再靠垄断割韭菜，不知道这是否算一种路径依赖……</p>\n<p>同时深度求索公司规模特别小，从它们的文档就能感觉出来，特别简短，甚至可以说是简陋，就像是连个写文档的人都抽不出来的感觉。此外它们家的模型的开发成本是最低的，就是注册，申请API Token，按文档调用，通了。和OpenAI的使用体验一模一样，简洁、可靠。</p>\n<p>作为对比，百度、阿里、腾讯、字节的模型都是在它们的云服务公司下提供，如果你尝试接过就知道我在说什么，光是产品介绍就一堆。然后要接入，先各种订购、权限包，然后费用方面又是看得眼花缭乱，最后拿到 Token 后，还会画蛇添足给你准备一堆 SDK ，而这些 SDK 又不好用，十分怀疑是外包写的。</p>\n<p>OK 以上的吐槽到此为止，接下来进入正文。</p>\n<h2>安装 SillyTavern</h2>\n<p>SillyTavern（字面意思翻译过来叫愚蠢的小酒馆）是一个 Web UI，可让您创建上传和下载独特的角色，并通过 LLM 后端服务与这些角色进行沟通对话，可以理解成是一种角色扮演，这类的应用在国内外其实都已经屡见不鲜了，甚至我记得最先盈利的好像就是这类角色扮演应用。在本教程中，我将展示如何在Windows上使用本地部署的 Deepseek 模型和 SillyTavern 联合使用。</p>\n<p>SillyTavern 的地址是 <a href=\"https://github.com/SillyTavern/SillyTavern\">https://github.com/SillyTavern/SillyTavern</a></p>\n<p>第一步安装必要的依赖 git 和 Node，如何安装建议直接搜索或者询问任意AI，资料太多本文不赘述。</p>\n<p>git 建议配置 SSH key 并上传公钥到 GitHub ，因为国内特殊的网络环境，https 很可能拉不到代码，SSH 协议会好很多。</p>\n<p>进入命令行，找一个非 Windows 系统目录的地方（比如用户文件夹、文档下开个目录之类） clone 下来仓库。</p>\n<p><code>git clone git@github.com:SillyTavern/SillyTavern.git</code></p>\n<p>此时能看到对应文件夹下出现了 SillyTavern 文件夹，进去后双击 Start.bat 。</p>\n<p>如果一切顺利，大概能看到下面的截图，运行完成后会自动打开浏览器对应页面。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2025-02-07/%E8%BF%90%E8%A1%8C%E5%B0%8F%E9%85%92%E9%A6%86.png\" alt=\"SillyTavern启动后的截图\"></p>\n<p>OK，网页可以先放着，这只是一个UI界面，接下来我们来部署本地的LLM服务。</p>\n<h2>通过 Ollama 使用 Deepseek R1</h2>\n<p>在B站里的教程比较普遍的就是教通过 Ollama 本地部署 DeepSeek R1 模型，实践下来发现确实简单得可怕。安装 Ollama ，搜索模型，复制命令，执行。</p>\n<p>Ollama 是一个开源的本地化工具，旨在简化大型语言模型（LLMs）的部署和使用。它允许用户在个人电脑或服务器上直接运行各种开源模型（如 Llama 2、Mistral、Phi-2 等），无需依赖云端服务，适合开发、测试和研究场景。</p>\n<p>Ollama 官网地址：<a href=\"https://ollama.com/\">https://ollama.com/</a></p>\n<p><img src=\"https://aipaint.lihuanyu.com/2025-02-07/ollama%E5%AE%98%E7%BD%91.png\" alt=\"ollama官网\"></p>\n<p>页面很简洁，直接点 Download 下载即可。下载安装完成后可以看到右下角托盘区多了一个羊驼的 Logo，表示 Ollama 服务已启动，同时会有命令行启动，输入 ollama 可以看到如下界面：</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2025-02-07/ollama%E5%91%BD%E4%BB%A4%E8%A1%8C%E7%95%8C%E9%9D%A2.png\" alt=\"ollama命令行界面\"></p>\n<p>接下来在 Ollama 的网页顶部的搜索里输入 deepseek 找到 R1 模型。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2025-02-07/%E9%80%89%E6%8B%A9R1%E7%89%88%E6%9C%AC%E5%A4%8D%E5%88%B6%E5%91%BD%E4%BB%A4.png\" alt=\"搜索找到R1选择版本复制命令\"></p>\n<p>这里需要根据自己电脑的配置决定选什么版本的模型，在机器学习模型里，参数规模会用B表示十亿（Billion），参数量的差异会影响模型的能力，更大的模型通常能处理更复杂的任务，但需要更多的计算资源和内存。比如，70B的模型比7B的模型大10倍，可能在理解上下文、生成文本的准确性上有显著提升，但推理速度会慢很多，并且需要更高端的硬件支持。一般选择一个模型体积小于你显存容量的版本，可以比较流畅地运行。</p>\n<p>比如我的显卡 GTX 4090 有 24GB 显存，所以可以流畅运行 32B 版本（模型体积20GB）。在控制台输入复制过来的命令 <code>ollama run deepseek-r1:32b</code> ，首次运行会自动下载模型文件需要一定时间（耗时取决于你的网络情况），下面的截图里已经不是首次运行，模型文件是已经下载好的状态：</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2025-02-07/ollama%E8%BF%90%E8%A1%8Cr1%E6%95%88%E6%9E%9C%E5%9B%BE.png\" alt=\"ollama运行r1效果图\"></p>\n<p>好了，现在我们 LLM 推理服务也就位了，接下来可以把两者连起来了。</p>\n<blockquote>\n<p>本地部署仅供学习、测试使用，因为本地的硬件成本限制，普遍跑的都是 7B - 32B 的模型，这和 deepseek 官方网页与官方 API （满血版，是671B级别的超大规模模型）的表现差异极大，真正的生产场景应该优先考虑线上版本。</p>\n</blockquote>\n<h2>连接使用</h2>\n<p>小酒馆运行后弹出的网页如图：</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2025-02-07/%E5%B0%8F%E9%85%92%E9%A6%86webUI%E7%95%8C%E9%9D%A2.png\" alt=\"小酒馆webUI运行界面\"></p>\n<p>点击顶部红色的插头图标，进行对应的配置：</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2025-02-07/%E9%85%8D%E7%BD%AE%E9%85%92%E9%A6%86API%E4%BD%BF%E7%94%A8%E6%9C%AC%E6%9C%BAollama%E7%9A%84deepseek.png\" alt=\"配置酒馆API使用本机ollama的deepseek\"></p>\n<p>出现绿色的模型版本即表示正确连接了。接下来就可以选择角色卡进行聊天了，相关的一些设定参考网上关于酒馆的配置教程，角色卡也有相关的社区分享可以直接下载使用。</p>\n<h2>其它服务</h2>\n<p>除了小酒馆，普通的问答使用也可以另一个插件叫 Page Assist ，是一个Chrome插件，搜索即可找到，这个插件还包含了联网搜索之类的工程能力。</p>\n<p>但缺点在于，它是专为 Ollama 适配的，如果想用别的 LLM 服务就不太方便了。</p>\n<p>Ollama 虽然方便，但过于简洁了，如果想找更多的模型自行体验玩耍，可以考虑使用 koboldcpp ，或者 LM studio 。</p>\n","date_published":"2025-02-01T00:00:00.000Z","tags":["AI","deepseek","教程"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2025/%E4%BB%80%E4%B9%88%E6%98%AF%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84/","url":"https://www.lihuanyu.com/posts/2025/%E4%BB%80%E4%B9%88%E6%98%AF%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84/","title":"【译】什么是前端架构","summary":"英语原文在此： 一开始看到了其他人的翻译，比较认可这篇文章的不少内容，所以进行一个转载，但又不想纠结于一些版权方面的问题，所以干脆基于原文让最近大火的 DeepSeek R1 帮我翻译一遍。 当你思考系统设计时，不要纠结于技术选型，而应聚焦于你希望系统具备的核心特性。技术选型只是这些特性的载体。 —— Gregor Hohpe 免责声明 ：如果你自认为只是个","content_html":"<blockquote>\n<p>英语原文在此：<a href=\"https://ducin.dev/what-is-frontend-architecture\">https://ducin.dev/what-is-frontend-architecture</a>\n一开始看到了其他人的翻译，比较认可这篇文章的不少内容，所以进行一个转载，但又不想纠结于一些版权方面的问题，所以干脆基于原文让最近大火的 DeepSeek R1 帮我翻译一遍。</p>\n</blockquote>\n<blockquote>\n<p>当你思考系统设计时，不要纠结于技术选型，而应聚焦于你希望系统具备的核心特性。技术选型只是这些特性的载体。 —— Gregor Hohpe</p>\n</blockquote>\n<p><strong>免责声明</strong>：如果你自认为只是个&quot;码农&quot;，请立即关闭本页面😉</p>\n<p>前端社区存在一个普遍问题😉：我们过度关注库、框架、打包工具、GitHub star 数等次要因素。我们常会狂热追捧某个工具（比如2015-2016年的Redux），然后滥用它。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2025-01-29/blog-it-javascript-frameworks.png\" alt=\"新框架新工具对前端开发者的蜜汁吸引力\"></p>\n<p>后来，我们又因同样的原因彻底厌恶这个工具（比如现在的Redux）。这种爱恨都没有道理…究竟发生了什么？🤨</p>\n<p>&quot;问题&quot;根源在于：许多前端开发者缺乏软件架构的基本认知，因为我们总把注意力放在别处。而这些架构能力恰恰是项目长期成功的关键（虽非唯一因素）。因为架构是连接业务价值和技术实现的隐形桥梁。</p>\n<p>在开展开发者培训、技术咨询或团队招聘时，我常会提问：如何理解软件架构？哪些是核心要素？如何设计稳健的系统架构？架构师的角色是什么？</p>\n<p>在继续阅读前，建议你先尝试回答这些问题😉…</p>\n<p>滴答⏰…</p>\n<p>这个问题特意设计得非常开放，以便对话者能自由表达他们认为重要的观点。我不会给出任何暗示。但当回答开头是类似&quot;（前端）架构就是如何组织目录和文件[…]&quot;时，这对我来说立即成为危险信号🟥。没错，正是最近又有人这样回答，促使我写下本文。</p>\n<p>亲爱的读者，本文旨在<strong>转变你的关注焦点</strong>：启发你从不同维度思考架构。跳脱代码仓库中的结构，摆脱具体实现方案的束缚。集中精力思考你希望系统具备哪些核心特性。从更宏观的视角，你需要哪些系统能力。摆脱工具本身的局限，转而关注它们带来的权衡取舍。最重要的是——你的业务需求如何决定必要的软件能力。</p>\n<hr>\n<h2>什么是（前端）架构？</h2>\n<p>某次推文讨论中我说😅</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2025-01-29/%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84%E4%B8%8D%E6%98%AF%E6%9E%B6%E6%9E%84.png\" alt=\"目录结构不是架构\"></p>\n<p>在评论区被问到我对架构的定义。我的简洁回答是：</p>\n<p><strong>根据业务需求做出的、塑造当前系统且未来难以变更的决策。</strong></p>\n<p>事实上，软件架构并没有单一标准定义。我强烈推荐阅读<a href=\"https://martinfowler.com/architecture/\">Martin Fowler的软件架构指南</a>。其他替代定义包括：</p>\n<ul>\n<li>你希望在项目早期就做对的那些决策</li>\n<li>架构就是关于重要的事——无论它是什么</li>\n</ul>\n<p>注意两个关键词：<strong>重要</strong> 和 <strong>决策</strong>。</p>\n<p>在进入具体案例之前（文章后续会涉及），让我们从基础要素开始。</p>\n<hr>\n<h2>决策</h2>\n<p>在项目的整个生命周期中，我们需要做出大量决策：语言平台选择、类库选型、编程范式、代码风格（Tabs还是空格🤔）…但更重要的是：</p>\n<ul>\n<li>如何确保业务优先级得到满足？</li>\n<li>如何让数十名开发者高效协作？</li>\n<li>如何实现高频部署（包含每日、每小时甚至周五的部署）？</li>\n</ul>\n<p>如你所见，这些主题的重要性差异巨大。我们分析的角度和做出的决策具有不同的相关权重。经验越丰富的开发者，越懂得在无关紧要处节省精力——特别是当决策容易修改时。</p>\n<p>那么，如何评估某个决策是否正确？</p>\n<hr>\n<h2>驱动因素</h2>\n<p>当面临困惑时，退一步审视全局总是明智的，这包括：</p>\n<ol>\n<li>业务<strong>最高优先级</strong>是什么？</li>\n<li>需要考虑哪些<strong>限制条件</strong>？</li>\n<li>哪些目标可以<strong>妥协</strong>（可选），哪些不可退让（必需）？</li>\n</ol>\n<p><strong>架构驱动因素是迫使我们在特定项目上下文中深度探索的关键要素</strong>。它相当于项目的语境过滤器，用于判断某个理论上的优势或劣势在具体情境中是否重要。</p>\n<p>典型架构驱动因素包括：</p>\n<ul>\n<li><strong>响应时间</strong>：系统必须极速响应</li>\n<li><strong>流量承载</strong>：需要处理海量请求</li>\n<li><strong>SLA/高可用</strong>：需保持约99.99%的正常运行时间</li>\n<li><strong>组织规模</strong>：需要支持数十甚至数百名开发者协作</li>\n<li><strong>上手门槛</strong>：应便于技能较弱的开发者理解</li>\n<li><strong>上市时间</strong>：因业务需求必须快速交付功能 （备注：商业成功的关键因素之一是 上市时间-TTM-Time To Marketing。TTM是指从产生想法到向客户推出最终产品或服务的时间长度。市场发展很快，延迟的TTM可能会毁掉整个商业理念。）</li>\n</ul>\n<p>在商业环境中，这些驱动因素几乎总是存在的。你的业务代表很可能直接表达过这些需求（只是未使用&quot;驱动因素&quot;这个术语）。若不能识别这些，你将可能专注错误方向，从而大幅降低成功概率。</p>\n<p>那么，我们该如何进行系统设计，以实现这些高层次目标呢？</p>\n<hr>\n<h2>权衡取舍</h2>\n<p>必须清醒认识到：所有特性都有代价。若想让系统具备某个优点，就必须接受对应的成本。让我们扩展之前的驱动因素列表，列出可能的负面影响：</p>\n<table>\n<thead>\n<tr>\n<th>驱动因素</th>\n<th>所需代价</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>系统必须极速响应</td>\n<td>复杂度↑，灵活性↓</td>\n</tr>\n<tr>\n<td>通过水平扩展/自动扩缩容处理海量流量</td>\n<td>需从单体架构转向分布式系统</td>\n</tr>\n<tr>\n<td>保持99.99%正常运行时间</td>\n<td>需维护金丝雀发布、蓝绿部署等高成本方案</td>\n</tr>\n<tr>\n<td>支持大规模团队协作</td>\n<td>代码重复↑，基础设施复杂度↑</td>\n</tr>\n<tr>\n<td>便于初级开发者上手</td>\n<td>无法使用团队最爱的技术栈</td>\n</tr>\n<tr>\n<td>快速交付业务功能</td>\n<td>技术债务累积↑</td>\n</tr>\n</tbody>\n</table>\n<p>取舍的本质在于：我们可以<strong>有意识地决定</strong>哪些可以放弃。例如：</p>\n<ul>\n<li><strong>问</strong>：99.99%可用性是否必要？<br>\n<strong>答</strong>：必要，因合同条款要求</li>\n<li><strong>问</strong>：80%测试覆盖率是否必要？<br>\n<strong>答</strong>：锦上添花，非必需，可舍弃</li>\n</ul>\n<p>制定架构决策时，<strong>必须聚焦驱动因素，同时牢记取舍代价</strong>。我们的目标是达成<strong>核心诉求</strong>，但也清楚可能需要<strong>牺牲</strong>什么。</p>\n<hr>\n<h2>限制</h2>\n<p>还有一个不言而喻的真理：并非所有事情都能实现😉。</p>\n<p>有时即使所有分析都证明某个决策正确，我们仍无法实施。外部因素可能产生冲突：</p>\n<table>\n<thead>\n<tr>\n<th>理想决策</th>\n<th>现实阻碍</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>系统必须极速响应，但需接受复杂度↑和灵活性↓</td>\n<td>因故无法重构数据库</td>\n</tr>\n<tr>\n<td>通过水平扩展/自动扩缩容处理海量流量，需转向分布式系统</td>\n<td>因故无法更换云服务商</td>\n</tr>\n<tr>\n<td>业务需要快速交付功能，但会产生技术债务</td>\n<td>因故无法扩招开发人员</td>\n</tr>\n</tbody>\n</table>\n<p>这就是现实的残酷之处。为了让挑战更有趣些——我们必须接受能力受限的事实😉。但我们仍然要达成目标！</p>\n<hr>\n<h2>简要回顾</h2>\n<p>快速总结架构决策的三要素：</p>\n<ol>\n<li><strong>驱动因素</strong>：业务核心诉求</li>\n<li><strong>权衡取舍</strong>：每个决策的代价</li>\n<li><strong>现实限制</strong>：不可抗的外部约束</li>\n</ol>\n<p><strong>跳出代码层面思考</strong></p>\n<hr>\n<h2>再次提问：什么是架构？</h2>\n<p>回顾我的简易定义：<br>\n<strong>根据业务需求做出的、塑造当前系统且未来难以变更的决策。</strong></p>\n<p>现在通过具体案例区分<strong>架构决策</strong>与<strong>技术决策</strong>：</p>\n<table>\n<thead>\n<tr>\n<th>架构决策 ✅</th>\n<th>技术决策 ❌</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>是否实施微前端（MFE）及实施方式</td>\n<td>使用webpack模块联邦或其他工具</td>\n</tr>\n<tr>\n<td>重用性与隔离性的优先级抉择</td>\n<td>是否使用barrel文件（index.js/ts）</td>\n</tr>\n<tr>\n<td>模型跨模块共享 vs ACL隔离</td>\n<td>采用类/OOP还是函数式/FP</td>\n</tr>\n<tr>\n<td>状态管理采用集中共享式 vs 分布式</td>\n<td>是否使用Redux类状态库</td>\n</tr>\n<tr>\n<td>数据获取模式：PULL vs PUSH</td>\n<td>使用Promise/async await/rxjs</td>\n</tr>\n<tr>\n<td>UI对实时数据的依赖程度</td>\n<td>选择Firebase/Supabase等BaaS</td>\n</tr>\n<tr>\n<td>客户端-服务端API契约变更权限</td>\n<td>使用GraphQL/REST/SSE协议</td>\n</tr>\n<tr>\n<td>确定核心架构驱动因素</td>\n<td>是否宣称遵循&quot;最佳实践&quot;</td>\n</tr>\n<tr>\n<td>LCP（最大内容渲染）优化是必备项还是加分项</td>\n<td>UI组件代码行数（LoC）</td>\n</tr>\n<tr>\n<td>系统单租户 vs 多租户架构</td>\n<td>认证数据存于Redux/Context/useState</td>\n</tr>\n<tr>\n<td>前端容错机制设计</td>\n<td>CI流水线强制80%测试覆盖率</td>\n</tr>\n</tbody>\n</table>\n<p>通过这些对比，可以清晰看到架构决策与技术决策的本质区别😅。注意上述对照展现了架构决策与技术决策的根本差异。</p>\n<hr>\n<h2>为什么目录结构不应该被视作架构？</h2>\n<p>目录结构是设计工作和引入规范的产物。它们旨在帮助我们：</p>\n<ul>\n<li>更快速地开发</li>\n<li>更安全地交付（减少破坏性变更）</li>\n</ul>\n<p>但目录结构本身不是目标，而是实现更高层次目标的手段，例如：</p>\n<ul>\n<li>通过微前端/模块化架构划分限界上下文</li>\n<li>支持独立团队间的解耦部署</li>\n<li>通过ACL（访问控制列表）隔离本地模型</li>\n</ul>\n<p>显然，目录结构可能更好地适配某个架构，也可能适配度较低。但究其本质，它只是某个概念的具象化<strong>实现</strong>——属于实现<strong>细节</strong>层面，是达成最终目标的路径。</p>\n<hr>\n<h2>目录结构无法告知我们什么</h2>\n<p>许多关键架构要素无法通过目录结构推断，包括：</p>\n<ol>\n<li><strong>是否存在‘上帝类’</strong>：无法判断模块是否真正隔离为限界上下文</li>\n<li><strong>模型复用情况</strong>：无法识别契约层与前端逻辑是否共享同一模型</li>\n<li><strong>状态管理模式</strong>：无法确认是集中式共享状态还是分布式本地状态</li>\n<li><strong>代码耦合度</strong>：无法定位问题耦合点，无法判断是否应用依赖倒置原则</li>\n<li><strong>代码内聚性</strong>：无法评估模块间的功能聚合程度</li>\n</ol>\n<p>温和地说，目录结构的重要性不足以构成架构。它可能服务于某个架构（也可能不），但本身不是架构。</p>\n<hr>\n<h2>构建正确的前端架构认知</h2>\n<p>架构不是我们😘渴望、🥰向往或😤强制执行的东西。它不会突然浮现😶🌫️，更不该直接复制前公司的成功方案🥸（即便在之前公司运行良好）。</p>\n<p><strong>架构是沟通、分析和推理的产物</strong>。如同函数根据输入产生输出，架构师的职责就是持续收集知识经验，定期运行这个&quot;输入→输出&quot;函数。</p>\n<hr>\n<h2>输入源与获取方式</h2>\n<p>架构师的核心技能是与管理层、业务方和开发团队的<strong>全方位沟通</strong>。需要收集的关键信息包括：</p>\n<h3>业务维度</h3>\n<blockquote>\n<p><strong>产品核心优势</strong>：竞争差异点所在领域</p>\n</blockquote>\n<ul>\n<li><strong>核心领域保护</strong>：核心模块/功能/团队禁止外包</li>\n<li><strong>质量红线</strong>：不可妥协的质量标准</li>\n<li><strong>领域建模</strong>：采用事件风暴等DDD实践</li>\n<li><strong>非核心领域</strong>：次要优先级</li>\n</ul>\n<h3>组织维度</h3>\n<blockquote>\n<p><strong>康威定律影响</strong>：公司结构如何决定系统交付形态</p>\n</blockquote>\n<ul>\n<li><strong>开发团队规模</strong>：部门人数与团队数量</li>\n<li><strong>产品导向程度</strong>：各团队是否100%独立负责子产品（开发→测试→部署全链路）</li>\n<li><strong>企业关联关系</strong>：关联公司、技术共享、并购等可能颠覆现有团队架构的因素</li>\n</ul>\n<h3>交付维度</h3>\n<ul>\n<li><strong>预期交付速度</strong>：方案交付时间窗与技术储备匹配度</li>\n<li><strong>部署频率要求</strong>：持续交付准备度评估</li>\n</ul>\n<hr>\n<h2>进入开发维度</h2>\n<p>在技术层面，我们需要形成以下问题链：</p>\n<h3>代码复用策略</h3>\n<ul>\n<li><strong>应鼓励/抑制多少代码复用？</strong>\n<ul>\n<li>复用越多代码量越少，但团队独立性越弱（特别是在共享模块频繁变更时，与无共享架构相比）</li>\n<li><strong>修改共享模块的后果分析</strong>（文件/组件库/制品等）：\n<ul>\n<li>是否触发重建？若需要，需重建多少模块？</li>\n<li>需要多少自动化测试？</li>\n<li>需部署多少组件？同步还是异步？</li>\n<li>总体耗时多少？</li>\n<li>共享机制引入的效率损耗？\n<ul>\n<li><strong>对系统可用性/SLA的影响评估</strong></li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n<h3>系统灵活性度量</h3>\n<ul>\n<li><strong>系统灵活性与架构测量方式</strong>\n<ul>\n<li><strong>TTM（上市时间）</strong>：预期值与当前实际值对比</li>\n<li><strong>部署频率（DF）</strong>：生产环境变更次数/单位时间</li>\n<li><strong>交付周期（CLT）</strong>：从开发者开始编码到生产部署的时间跨度</li>\n<li><strong>故障率（CFR）</strong>：变更引发故障的频率</li>\n<li><strong>恢复时间（MTTR/FDRT）</strong>：故障修复耗时</li>\n</ul>\n</li>\n</ul>\n<h3>系统稳定性保障</h3>\n<p>（超越测试覆盖率等基础指标）</p>\n<ul>\n<li><strong>故障处理机制</strong>：\n<ul>\n<li>故障发生时的标准流程？</li>\n<li>该流程的实际调用频率？😄</li>\n</ul>\n</li>\n<li><strong>同步部署分析</strong>：\n<ul>\n<li>需同步部署的模块数量与体积？</li>\n<li>是否因CI/CD配置或仓库过度拆分导致依赖项冗余构建？</li>\n</ul>\n</li>\n<li><strong>团队信任机制</strong>：\n<ul>\n<li>是否信任其他团队的交付物？</li>\n<li>采用Git-flow还是主干开发？</li>\n<li>CI/CD流程如何适配这些决策？</li>\n</ul>\n</li>\n<li><strong>容错能力验证</strong>：\n<ul>\n<li>前端是否针对后端各类故障场景进行自动化测试？</li>\n</ul>\n</li>\n<li><strong>可观测性效用评估</strong>：\n<ul>\n<li>定位前端问题的耗时？</li>\n<li>回滚操作耗时？</li>\n<li>修复问题耗时？</li>\n<li>如何/何时发现核心Web指标（LCP/FID/CLS）的回归？</li>\n</ul>\n</li>\n</ul>\n<h3>跨平台策略</h3>\n<ul>\n<li><strong>用户设备与环境</strong>：Web/原生移动端/混合方案？\n<ul>\n<li><strong>复用与分叉策略</strong>：\n<ul>\n<li>哪些组件应复用？</li>\n<li>哪些应为减少跨团队依赖而分叉？</li>\n</ul>\n</li>\n<li><strong>团队组织模式</strong>：\n<ul>\n<li>按技术平台划分 vs 按限界上下文划分？😉</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n<h3>特殊需求案例</h3>\n<ul>\n<li><strong>预期/必需的系统特性</strong>\n<ul>\n<li>\n<p><strong>实时协作模式</strong>：</p>\n<ul>\n<li>现状：仅支持单用户操作</li>\n<li>需求：多用户实时编辑同一数据集</li>\n<li>方案对比：\n<ul>\n<li>直接状态修改（set/update）→ 缺乏共享模型</li>\n<li>命令模式（如Redux Action）→ 天然支持协作迭代</li>\n<li>进阶方案：CRDT（无冲突复制数据类型）</li>\n</ul>\n</li>\n</ul>\n</li>\n<li>\n<p><strong>数据实时性要求</strong>：</p>\n<ul>\n<li>社交帖子点赞数延迟 → 可容忍</li>\n<li>银行系统账户余额标签切换过期 → 不可接受</li>\n<li>解决方案：\n<ul>\n<li>客户端缓存失效策略（SWR）</li>\n<li>服务端推送机制（SSE/WebSocket）</li>\n</ul>\n</li>\n</ul>\n</li>\n<li>\n<p><strong>SDK兼容性管理</strong>：</p>\n<ul>\n<li>客户基于平台SDK开发定制功能</li>\n<li>平衡法则：\n<ul>\n<li>系统演进 vs 向后兼容</li>\n<li>案例：React组件props重构\n<ul>\n<li>后果：可能引发客户重大变更</li>\n<li>困境：即使实现并测试，仍可能因&quot;无破坏性变更&quot;要求被回滚</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n<p>架构管理的本质在于提出正确问题。引用经典格言：<br>\n<strong>“我宁愿拥有无法回答的问题，也不要接受不容置疑的答案”</strong></p>\n<p>通过深入思考这些问题，我们将确定适合的架构风格与关键技术选型。</p>\n<hr>\n<h2>工具决策 vs 架构决策</h2>\n<p>有人可能会说：</p>\n<blockquote>\n<p>“嘿兄弟，但有些与工具、类库、规范或代码结构相关的决策，对项目有重大影响且后期难以更改。比如使用Redux就是架构决策！！！ 😉”</p>\n</blockquote>\n<p>嗯，是也不是。😉</p>\n<p>人们很容易执着于特定术语，却丢失了关键语境（即从业务和整体视角看什么是重要的）。</p>\n<p>确实，某些编码规范决策在多年后可能（非常）难以更改。编码规范、代码结构或目录结构（真的，任何东西）都可能加速或拖慢开发速度——当然！有些规范更合适，有些则不然。有些能让我们更快推进，有些则不能，等等。</p>\n<p>但当你跳出做出该决策的团队范围，这些就完全无关紧要了🔥。它们只是实现细节，在团队之外毫无意义。</p>\n<p><strong>示例</strong>：</p>\n<ul>\n<li>你决定每个文件只保留一个组件 → 在团队外完全无关</li>\n<li>你选择lodash/ramda工具库或不用任何库（因为&quot;非我发明&quot;） → 仍然与团队外无关</li>\n<li>你为每个模块设计特定文件结构 → 该规范影响测试、Storybook和重构 → 仍然与团队外无关<br>\n（顺便说，如果Storybook被团队外频繁使用，它就变得相关了）</li>\n</ul>\n<p>请注意：这些决策确实重要，对你的团队很关键。但仅对你的团队。它们不会带来/强制任何整体系统特性。如果决策不同，整体系统特性也不会改变。让我们进一步分析之前的说法：</p>\n<blockquote>\n<p>“使用Redux就是架构决策”</p>\n</blockquote>\n<p>（Redux对不住了😅）</p>\n<p>现在请注意：架构决策不是选择Redux本身！而是选择集中式状态管理方案，因为这可能导致模块间交叉依赖（所有人都能访问全局store的一切，对吧？），或者在将单体拆分为微前端时——使用多个独立store（如MobX）会更简单。此外，架构决策还涉及选择客户端事件溯源方案，因为业务可能需要实现实时协作功能。</p>\n<p>那么选择Redux会带来后果吗？当然。但再次强调，重点不在库本身，而在于Redux带来的高层次特性——既包括它提供的能力（前文提过），也包括引入的成本和限制。例如Redux是唯一数据源，这在考虑微前端时显然不利。Redux与其特性密不可分，但构建架构的是这些特性，而非工具本身。</p>\n<p>让我们再看一个Angular生态的例子：</p>\n<blockquote>\n<p>“不同意！如果是像NGRX这样的高层次库，选择库本身就是架构决策。需要回答多个问题：1.如何使用NGRX;2.是否总是使用Effects;3.是否通过Facade抽象;4.与哪些层级关联;5.如何跨域共享NGRX Store？”</p>\n</blockquote>\n<p>让我们一个一个来讨论：</p>\n<ul>\n<li>\n<p><strong>我们如何使用NGRX？</strong><br>\n这是个狡猾的问题，因为&quot;如何使用&quot;可能涉及高层次和低层次两个维度。模棱两可的问题😉</p>\n</li>\n<li>\n<p><strong>是否总是使用Effects？</strong><br>\n（上下文：NGRX Effects等同于redux-observable的epics——派发action后，通过rxjs响应式流处理，通常派生新action返回store）<br>\n这属于实现细节。无论选择命令式还是响应式范式，都属于编程（实现）范式，无关架构。未来可以改变这个决策。</p>\n</li>\n<li>\n<p><strong>是否通过Facade抽象？</strong><br>\n这属于封装和/或设计模式/编码模式…比架构模式低一个层级。在C4模型中属于代码层（Level 4）（实现细节）。重申——对团队重要吗？重要。对外部重要吗？不重要。</p>\n</li>\n<li>\n<p><strong>与哪些层级关联？</strong><br>\n可能涉及架构——但这与NGRX无关。使用其他状态管理方案（如React自定义hooks）时也会提出同样的问题。假设的层级（或其缺失）当然构成架构，但即使换用其他库，这个问题依然存在，对吧？</p>\n</li>\n<li>\n<p><strong>如何跨域共享NGRX Store？</strong><br>\n绝对属于架构决策。但同样与NGRX本身无关，因为使用任何其他集中式状态管理方案时都会遇到同样的问题。对吗？</p>\n</li>\n</ul>\n<p><strong>补充说明</strong>：<br>\n是否使用NGRX/redux-observables当然会影响：</p>\n<ul>\n<li>前端开发者的入门门槛</li>\n<li>他们的积极性（与工具的爱恨情仇🥹）</li>\n<li>测试编写方式等</li>\n</ul>\n<p>但重申：当你走出团队/模块/仓库范围——这些真的有那么重要吗？</p>\n<p>归根结底，决策的变更成本高低，并不决定其在大局和/或长期中的相关性。同样，在团队/仓库内部极其重要的东西，也不必然对外部具有相关性。可能有，但不必然。</p>\n<p><strong>依我拙见，是否将选择Redux称为架构决策并不重要，只要我们聚焦于该决策带来的后果。</strong></p>\n<table>\n<thead>\n<tr>\n<th>特征</th>\n<th>工具决策</th>\n<th>架构决策</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>影响范围</td>\n<td>团队内部</td>\n<td>跨团队/系统</td>\n</tr>\n<tr>\n<td>变更成本</td>\n<td>可能高昂但局部</td>\n<td>系统性影响</td>\n</tr>\n<tr>\n<td>业务关联度</td>\n<td>间接</td>\n<td>直接驱动</td>\n</tr>\n<tr>\n<td>示例</td>\n<td>Redux/NGRX选型</td>\n<td>集中式状态管理策略</td>\n</tr>\n</tbody>\n</table>\n<hr>\n<h2>总结</h2>\n<p>架构的核心在于做出重要决策。这些决策应：</p>\n<ol>\n<li><strong>由业务优先级驱动</strong></li>\n<li><strong>考量权衡取舍</strong></li>\n<li><strong>适应现实限制</strong></li>\n</ol>\n<p>面对这些挑战，架构师的职责是在<strong>业务优先级/需求</strong>与<strong>技术实现/复杂度</strong>之间找到平衡点。</p>\n<p>切勿混淆以下概念：</p>\n<ul>\n<li><strong>架构</strong>：助你达成目标的高层次决策</li>\n<li><strong>实现方式</strong>：工具、类库、规范、API 等底层细节</li>\n</ul>\n<p>后者只是实现目标的可能路径，从业务优先级和现实限制的角度看，它们只是次要细节。</p>\n<p>希望本文对你有所启发，感谢阅读🤓。<br>\n特别致谢 Damian、Mateusz 和 Manfred 提供的宝贵反馈。</p>\n<blockquote>\n<p>特别特别致谢 DeepSeek R1 提供的翻译</p>\n</blockquote>\n<hr>\n","date_published":"2025-01-29T00:00:00.000Z","tags":["前端","架构","技术"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2025/25%E5%B9%B4%E7%9A%84%E5%BE%AE%E4%BF%A1%E7%BA%A2%E5%8C%85%E5%B0%81%E9%9D%A2/","url":"https://www.lihuanyu.com/posts/2025/25%E5%B9%B4%E7%9A%84%E5%BE%AE%E4%BF%A1%E7%BA%A2%E5%8C%85%E5%B0%81%E9%9D%A2/","title":"25年的微信红包封面","summary":"23 年因为 AI 的兴起搞了个微信公众号和小程序，没想到微信的算法给推了一波流，爆了几篇文章，粉丝数在 24 年过年时候就已经到 1500 了，微信给了 1200 个红包封面，当时应该还是个稀罕货，挺有意思。 25 年的春节又来了，这次微信更大方了，给了我 6000 个红包封面，但大概率我一个都发不出去了。","content_html":"<p>23 年因为 AI 的兴起搞了个微信公众号和小程序，没想到微信的算法给推了一波流，爆了几篇文章，粉丝数在 24 年过年时候就已经到 1500 了，微信给了 1200 个红包封面，当时应该还是个稀罕货，挺有意思。</p>\n<p>25 年的春节又来了，这次微信更大方了，给了我 6000 个红包封面，但大概率我一个都发不出去了。</p>\n<p>不得不说微信的产品经理还是很厉害的，红包本来就是微信支付打败支付宝的关键方案，没想到这么多年迭代下来还能玩出新花样。</p>\n<blockquote>\n<p>此处想吐槽一下支付宝，大量的产品只做到了能用的地步，几乎没有思考过从能用到好用应该怎么做。但这个可能也和企业文化、考核指标等有关系，维护迭代现有项目是在述职时讲不出一个好故事的。从 0 到 1 能拿到的结果远好于从 1 到 10。在大厂，从 0 到 1 虽然也不简单，但比想象中容易。背靠一个成熟的平台，只要业务方能申请到资源，堆一个看起来不错的结果不算太难，如果感觉有点难，说明业务方向可能没那么重要……</p>\n</blockquote>\n<p>借着写这篇随笔记录的机会，查了一下微信红包封面的商业化之路。</p>\n<p>2019 年，微信首次对企业用户开放了“定制红包封面”功能，这是微信红包商业化的起点。在那个猪年的春节，有 1.5 万家企业参与了微信春节定制红包。</p>\n<p>20 年，抖音的飞速增长让微信狂推视频号试图与其抗衡，为了扶持视频号，定制红包封面的权限开放给了视频号，从此定制红包封面不再是大型企业或机构的专利。</p>\n<p>2021 年，一张带有品牌的微信红包封面突然在网上蹿红，并一举登上微博热搜榜，引爆了大众对微信红包定制封面的认知和热情。</p>\n<p>22 年，红包封面支持视频号/公众号互通发放，同时新版红包封面现已全面开放定制。</p>\n<p>对于公众号开始发红包，差不多就是 23 年的春节开始的，要求也不算低，既要完成实名认证、视频号认证，粉丝数量达到 100 人以上的要求。</p>\n<p>自定义红包封面大概如下图所示：\n<img src=\"https://aipaint.lihuanyu.com/2025-01-27/5838b732c7adfe193742fb106ddfe70.png\" alt=\"自定义微信红包封面示意图\"></p>\n<p>一个设计精美的红包封面，可以在发红包时给人眼前一亮的感觉，同时又能让红包设计者（企业、公众号、视频号）得到曝光与宣传，多赢的局面。</p>\n<p>24 年的春节我们出了两款红包封面，一款是龙年萌娃和龙一起图片，一款是原神宵宫的AI真人图。本来还有一款想出星穹铁道的卡芙卡，没想到微信的审核始终不予通过，涉及版权问题，也能理解，卡芙卡当时跑的图是二次元版本，几乎无法用 AIGC 生成作为理由来说明版权归属。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2025-01-27/646b28b375e50d61bd89016398b6bab.png\" alt=\"24年公众号靠AIGC产出的龙和小萌娃的图片作为红包封面\"></p>\n<p><img src=\"https://aipaint.lihuanyu.com/2025-01-27/7efde6a0c95e46758061a0a70a4beb2.png\" alt=\"24年公众号靠AIGC产出的原神宵宫小姐姐的图片作为红包封面\"></p>\n<blockquote>\n<p>可以看到宵宫这款因为 SD 1.5 模型在画手上的局限性，手部结构问题挺大的，除此之外，和真人COSER有区别吗？分不清，我真的分不清啊。</p>\n</blockquote>\n<p>两种红包封面，3份兑换卡（每份400张一共1200张），最终我选择了兑换两份宵宫，因为宵宫是当时公众号爆了的那篇文章。后续的封面发放数据显示，我选得很对，第一款红包领取量很高，喜庆又应景，但使用率非常低，因为这种万金油封面，到处都能领到。我制作的这一款完全谈不上精心设计，仅是用 AI 模型配合一个龙年萌娃的 LoRA 生成了一堆图后简单挑选了下。</p>\n<blockquote>\n<p>但这不代表它完全不花时间，相反，为了弄明白微信的规范，按要求整出图片，编写文案，还是要花个1-2天左右的时间的。</p>\n</blockquote>\n<p>第二款红包800张，只发出去了700+张，但使用率很不错，并且收红包的用户点开封面故事阅读的量也很大，再通过红包封面访问到公众号的也有不少。所以我判断，红包封面不要去选春节元素的，你卷不过专业的公司和设计师的，AI在现阶段还是只能算玩具，对普通用户来说能有眼前一亮的效果，但对上专业选手毫无机会。而选一些小众的、特色的、带IP的，走粉丝路线，机会更大。</p>\n<p>所以今年本来的计划是用 《EVA新世纪福音战士》的明日香 和 《最终幻想》的蒂法 来制作红包封面。\n<img src=\"https://aipaint.lihuanyu.com/2025-01-27/657bd4abd27755da3a7344638ac43d2.png\" alt=\"明日香红包封面\">\n<img src=\"https://aipaint.lihuanyu.com/2025-01-27/4b4e47b8a4a53733099efe0186dcc31.png\" alt=\"蒂法红包封面\"></p>\n<p>没想到都挂在了审核上了，前者明日香说是涉及他人作品，需要提供材料。后者蒂法说是不可和他人作品雷同。唉，没想到现在的审核居然如此严格了。</p>\n<p>想去搜索看看是否有类似案例，如何解决，没想到搜到了一份 Excel 表格，里面列举了拒绝话术和判断标准。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2025-01-27/54d9c590d4ab9e98faa16accee4f729.png\" alt=\"审核拒绝话术与判断标准\"></p>\n<p>发现了拒绝我的两个点，红框所示。微信这个挺狗的一点在于，这些审核标准也都存在极大的自由裁量权，都看审核人员如何理解，比如明日香这个二创，它的判断标准是网上能搜到同款，但这怎么可能，这几张图都是 AI 新鲜生成的，甚至连像都不一定谈得上。雷同更是扯上了黑产。</p>\n<p>怎么说呢，一定程度上，微信的审核人员也没错，我确实是属于硬蹭别人的IP。但人审嘛，去搜一下比如柯南红包封面，你会看到很多小号在发柯南的，它们甚至不是AIGC生成的图片，而是直接网上找的素材。</p>\n<p>加上去年 1200 个红包封面都发不完，转化率（通过红包封面最终关注公众号的用户）又低得惊人（十来个）。同时今年，感觉大家发红包的热情都不在了，群里的红包数量比过去几年少太多了。</p>\n<p>所以评估成本与收益后，算了，今年不搞红包封面了。但依然好奇，未来的微信还能做出点什么有意思的东西？</p>\n","date_published":"2025-01-27T00:00:00.000Z","tags":["微信","公众号","红包封面","AIGC"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2024/AI%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E8%80%85%E7%9A%84%E5%9B%B0%E5%B1%80/","url":"https://www.lihuanyu.com/posts/2024/AI%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E8%80%85%E7%9A%84%E5%9B%B0%E5%B1%80/","title":"AI应用开发者的困局","summary":"自从 openAI 带了这波 AI 热潮，很多工程师开始着手开发 AI 应用。比如我写了几个小程序，效果上看还不错的一个是AI绘图领域的。 这个小程序目前累计用户高达3.6万，日UV却只有100 200之间波动。留存率非常低，活跃用户留存仅10% 20%，新用户更惨，7日留存基本在1% 5%左右徘徊。","content_html":"<p>自从 openAI 带了这波 AI 热潮，很多工程师开始着手开发 AI 应用。比如我写了几个小程序，效果上看还不错的一个是AI绘图领域的。</p>\n<p>这个小程序目前累计用户高达3.6万，日UV却只有100-200之间波动。留存率非常低，活跃用户留存仅10%-20%，新用户更惨，7日留存基本在1%-5%左右徘徊。</p>\n<p>可能纯工具应用就是这么惨淡吧，而且好像不只是我这种小开发者有这种烦恼，大厂的应用也有类似的困扰。众多大厂的众多AI应用里，最为出名的应该就是字节跳动的豆包App吧，相比其他的文小言、KIMI、通义千问等应该是领先不少的，而据说（无数据来源，未经考证，请勿引用），豆包的流失率也高达98.xx% 。</p>\n<p>从自身感受上说也差不多，我安装了不少AI App，比如豆包、通义千问、Kimi、元宝等，但后续陆陆续续删掉了不少，现在只剩了豆包、通义、Claude。而这三者，也几乎不打开，偶尔打开也仅为了学习研究看看功能看看交互。感觉AI在移动端上，真的蛮鸡肋的。</p>\n<p>不过在桌面平台上，使用频率就会高很多，但也主要是 chatGPT 和 ClaudeAI，几乎不用国产AI。国产AI给我的感觉像是……内容审查给查坏了，不太聪明的样子……</p>\n<p>以上是AI能力方面的问题，可能随着时间都会慢慢改善。</p>\n<p>但对于独立开发而言，AI应用最大的问题在于，成本！</p>\n<p>几乎所有的AI的需要依赖显卡才可以运行，而显卡，实在是太贵了。</p>\n<p>最近看到一个案例，有个老哥把李继刚老师的汉语新解部署成了网站的形式，不需要登录就可以玩。我第一反应就是，它能撑多久？这个汉语新解是要求 ClaudeAI 的接口，很贵的，一次调用差不多要人民币2角钱左右。</p>\n<p>果然，刚刚看已经无法生成了，调用的时候就报错：无法生成svg。大概率是费用花完了。</p>\n<p>不只是这一个例子，想想AI出来后火起来的应用？真正用AI发财了的人靠的不是AI的能力，而是二道贩子、卖课的。因为国内无法使用，催生了大量的代理。卖课的也是搞笑，他们自己都跑不通AI商业化的流程，却敢大言不惭不学AI就要被时代抛弃了。</p>\n<p>总之，成本问题是独立开发者面对AI应用时的一座大山。AI绘图小程序也是一样，虽然是想办法尽可能降低成本了，已经是弹性部署，仅在有用户请求时才启动GPU，按秒计费了，但依然一张图要1-2角钱的成本。</p>\n<p>这个价格看起来不贵，但架不住人多。还好，我一开始就认识到这东西的费用不会低，所以一开始就决定一定需要鉴权+计费，计费不一定是真的要出钱，只是说一种限制，不能无限免费的一种限制。</p>\n<p>所以我根本没考虑过web，一个网页被攻击的门槛太低了，小程序虽然也会有风险，但门槛显然被提高了不少。</p>\n<p>最后的结果是，即使在如此压缩成本的情况下，也仅勉强靠广告和付费用户达成了收支平衡，至于说想盈利，我觉得很难。所以这个项目的结果大概率会是一个练手项目、一个学习项目。</p>\n<p>成本这座大山想要翻过去还是比较难的，只能期待计算成本的下降，至于说一些国产大模型厂商出来打价格战，疯狂降价甚至是免费提供，实际体验后发现这些模型的能力相较头部模型还是差太远了，用来翻译文档都有点吃力。</p>\n","date_published":"2024-09-22T00:00:00.000Z","tags":["AI","开发者","思考"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2024/%E5%A6%82%E4%BD%95%E6%8A%8Asvg%E6%B8%B2%E6%9F%93%E6%88%90png%E5%9B%BE%E7%89%87/","url":"https://www.lihuanyu.com/posts/2024/%E5%A6%82%E4%BD%95%E6%8A%8Asvg%E6%B8%B2%E6%9F%93%E6%88%90png%E5%9B%BE%E7%89%87/","title":"如何把svg渲染成png图片","summary":"简洁版：在小程序里无法把svg转为png，cloudflare 的 worker 上也不能，最终选择在自运维的服务器上转换。 背景 最近prompt大师开发了一套新的提示词很有意思，能把一个词语用鲁迅的语气，幽默、讽刺、批判性的进行解释。这个提示词要配合 Claude AI 使用，输出的内容是 SVG 。","content_html":"<blockquote>\n<p>简洁版：在小程序里无法把svg转为png，cloudflare 的 worker 上也不能，最终选择在自运维的服务器上转换。</p>\n</blockquote>\n<h2>背景</h2>\n<p>最近prompt大师开发了一套新的提示词很有意思，能把一个词语用鲁迅的语气，幽默、讽刺、批判性的进行解释。这个提示词要配合 Claude AI 使用，输出的内容是 SVG 。</p>\n<p>例如，对程序员这个词：\n<img src=\"https://aipaint.lihuanyu.com/2024-09-22/svg-to-png-example.png\" alt=\"\"></p>\n<h2>问题</h2>\n<p>而我们有一个微信小程序，在小程序上， svg 的展示就有一些小问题了，主要是无法预览、无法下载。</p>\n<p>那么如何能实现svg在小程序上的预览呢？ 最简单的思路当然是，转换成PNG图片。</p>\n<p>接下来的问题是，在哪转？</p>\n<h2>serverless</h2>\n<p>因为转换本身肯定是要消耗一些资源的，所以一开始是不愿意在服务器上转换的。而赛博佛祖 cloudflare 提供的 serverless 服务有非常大的免费额度，所以想着看能不能用 cloudflare 的 worker 实现这个需求。</p>\n<p>结论是不行，安装 resvg-js 后编写逻辑，运行时提示无相关能力，发现 serverless 阉割了一些底层能力，不支持 native APIs。正常写网络业务逻辑没问题，一旦需要用一些底层支持的时候就挂了。</p>\n<p>但还是不死心，搜了下相关资料 “svg to png cloudflare worker”，还真有一篇博文和一个 reddit 帖子。里面提到了一个叫 resvg-wasm 的包，通过 WebAssembly ，把 rust 编译成 wasm，以此来渲染 svg。</p>\n<p>发现确实可以，但不支持字体，在我这个场景下尤为致命。reddit 的帖子里有老哥就分享了另一个方法：svg2png-wasm，这个包支持文字。但实测发现，仅支持英文，中文不知道是我姿势不对还是字体文件太大，反正出的结果就是一堆框框。</p>\n<p>总之，折腾半天的结论就是，缺少 native APIs 的 serverless 环境不行……</p>\n<h2>小程序</h2>\n<p>那如果不能在 worker 上做，第二个思路就是，能不能在小程序里做？小程序本身是可以通过 image 标签把 svg 展示出来的，但无法预览也无法下载。</p>\n<p>那么能否结合小程序的 canvas ，把 svg 绘制在 canvas 上，再从 canvas 保存为 png 呢？</p>\n<p>答案是也不行，小程序的 canvas 不支持 svg，社区里相关问题最早在18年就出现了，但一直到24年依然是没有解决方案。不知道到底是微信的技术团队比较菜还是他们不认为这是一个高优的问题。因为 svg 的支持其实是比想象中难不少的，尤其是 svg 是可以通过引入资源等做很多复杂的事情的。</p>\n<h2>传统方案</h2>\n<p>所以花了很长的时间验证上面两条分布式的道路走不通后，最终还是妥协用传统方案来做。</p>\n<p>而传统方案的容易程度真的是震惊到我了，实在是太简单了，有系统支持下的 node ，太快乐了。</p>\n<p>安装一个 sharp 包用于解析渲染 svg ，核心代码就几行：</p>\n<pre><code class=\"language-js\">const svgBuffer = await this.downloadSvg(url);\n\n// 使用 sharp 将 SVG 转换为 PNG\nconst pngBuffer = await sharp(svgBuffer)\n  .png()\n  .toBuffer();\nres.setHeader('Content-Type', 'image/png');\nres.setHeader('Content-Disposition', 'inline; filename=&quot;converted.png&quot;');\nres.send(pngBuffer);\n</code></pre>\n<p>请求调用出图，都不用调试一次就成功了。</p>\n<p>没有压测过，但请求量一旦大了后，估计很容易崩。但没关系，这个量目测不会太大，大了再想办法解决。</p>\n<h2>字体</h2>\n<p>但仔细观察发现第一次出的图的效果，好像并不好看，至少和原作者分享的效果不一致，仔细观察了一下 svg 的内容，声明了 text 的 font-family ，标题是楷体，内容是汇文明朝体。</p>\n<p>下载字体后好看很多（就是上面放的图里的效果）。</p>\n<p>于是给服务器上也安装了对应的字体。记录一下相关过程。</p>\n<ol>\n<li>查看Ubuntu上的字体：<code>fc-list :lang=zh</code></li>\n<li>从网上下载字体或者从Windows的<code>C:\\Windows\\Fonts</code>路径把字体复制出来，注意格式是ttf，发送到服务器上。</li>\n<li>把文件复制到相应的目录<code>sudo cp -r /home/upload-font /usr/share/fonts</code></li>\n<li>执行：<code>sudo mkfontscale</code> 和 <code>sudo mkfontdir</code> 以及 <code>sudo fc-cache -fv</code> 等待字体安装</li>\n<li>再输入 <code>fc-list :lang=zh</code> 看看安装是否成功</li>\n</ol>\n<p>其实第五步里我没看到有汇文明朝体，但从svg生成的png上看确实是成功了，也没探究细节，反正实现了，先这样吧。</p>\n","date_published":"2024-09-21T00:00:00.000Z","tags":["svg"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2024/%E5%9B%BE%E5%BA%8A%E7%B3%BB%E5%88%97%E4%B9%8Btinypng%E8%87%AA%E5%8A%A8%E5%8E%8B%E7%BC%A9%E5%9B%BE%E7%89%87/","url":"https://www.lihuanyu.com/posts/2024/%E5%9B%BE%E5%BA%8A%E7%B3%BB%E5%88%97%E4%B9%8Btinypng%E8%87%AA%E5%8A%A8%E5%8E%8B%E7%BC%A9%E5%9B%BE%E7%89%87/","title":"图床系列之tinypng自动压缩图片","summary":"前置内容： 使用cloudflare R2托管图片 使用cloudflare搭建个人图床 R2的存储空间虽然很便宜，但图片尺寸仍然应该尽可能小，节约存储空间的同时也减少传输体积，节约加载时间。所以图片在上传前最好是能压缩一下。目前非常好用的压缩图片的工具网站是 tinypng，所以之前一般是手动去 tinypng 对图片进行压缩后再上传，但这样很麻烦，所以今","content_html":"<p>前置内容：</p>\n<ul>\n<li><a href=\"https://www.lihuanyu.com/%E8%BF%90%E7%BB%B4/%E4%BD%BF%E7%94%A8cloudflare%20R2%E6%89%98%E7%AE%A1%E5%9B%BE%E7%89%87/\">使用cloudflare R2托管图片</a></li>\n<li><a href=\"https://www.lihuanyu.com/%E5%BC%80%E5%8F%91/%E4%BD%BF%E7%94%A8cloudflare%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%9B%BE%E5%BA%8A/\">使用cloudflare搭建个人图床</a></li>\n</ul>\n<p>R2的存储空间虽然很便宜，但图片尺寸仍然应该尽可能小，节约存储空间的同时也减少传输体积，节约加载时间。所以图片在上传前最好是能压缩一下。目前非常好用的压缩图片的工具网站是 tinypng，所以之前一般是手动去 tinypng 对图片进行压缩后再上传，但这样很麻烦，所以今天我们来加一步逻辑给自动处理。</p>\n<p>第一步找 tinypng 的 API： <a href=\"https://tinypng.com/developers\">https://tinypng.com/developers</a></p>\n<p>是填写邮箱地址后它直接把控制台地址发到你的邮箱里。获取一个 API key 保管好。</p>\n<p>第二步读文档：<a href=\"https://tinypng.com/developers/reference\">https://tinypng.com/developers/reference</a></p>\n<p>太多字了不想读怎么办？打开 chatGPT 开始聊天。先告诉 chatGPT 我想在图片上传前通过 tinypng 的服务对图片进行压缩，我的服务是用 cloudflare 的 worker 编写的，图片存储在 R2 里，前端代码是用 solidjs 写的。</p>\n<p>chatGPT 就给了我一堆代码：</p>\n<pre><code class=\"language-ts\">const TINYPNG_API_KEY = '前面申请到的 API KEY'\nexport async function compressImage(file: File) {\n  const tinypngUrl = 'https://api.tinify.com/shrink';\n\n  const response = await fetch(tinypngUrl, {\n    method: 'POST',\n    headers: {\n      Authorization: 'Basic ' + btoa('api:' + TINYPNG_API_KEY),\n      'Content-Type': file.type,\n    },\n    body: file,\n  });\n\n  if (!response.ok) {\n    throw new Error('Error compressing image: ' + response.statusText);\n  }\n\n  const result = await response.json();\n  // @ts-ignore\n  const compressedImageUrl = result.output.url;\n\n  const compressedImageResponse = await fetch(compressedImageUrl);\n  if (!compressedImageResponse.ok) {\n    throw new Error(\n      'Error fetching compressed image: ' + compressedImageResponse.statusText,\n    );\n  }\n\n  return compressedImageResponse.arrayBuffer();\n}\n</code></pre>\n<p>拿到的结果是一个 arrayBuffer，直接给到 OSS.put 方法即可（相关代码参考上一篇）。</p>\n","date_published":"2024-05-18T00:00:00.000Z","tags":["图床","压缩图片"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2024/shadcn-ui%E7%BB%84%E4%BB%B6%E5%BA%93/","url":"https://www.lihuanyu.com/posts/2024/shadcn-ui%E7%BB%84%E4%BB%B6%E5%BA%93/","title":"shadcn-ui 组件库","summary":"忘记之前在哪看到，说欧美市场现在普遍流行的组件库方案不是 AntD，也不是 mui，而是 shadcn ui ，没听说过这个之前，最近碰巧看到 Solid ui ，是一个非官方的 shadcn ui 的 SolidJS 版本实现，就对其本体也很感兴趣，打开看了觉得有点意思。","content_html":"<p>忘记之前在哪看到，说欧美市场现在普遍流行的组件库方案不是 AntD，也不是 mui，而是 shadcn-ui ，没听说过这个之前，最近碰巧看到 Solid-ui ，是一个非官方的 shadcn-ui 的 SolidJS 版本实现，就对其本体也很感兴趣，打开看了觉得有点意思。</p>\n<p>官网地址：<a href=\"https://ui.shadcn.com/\">https://ui.shadcn.com/</a>\n组件库本身的设计感觉中规中矩，看起来并不比 antd 优秀。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2024-02-07/shadcn-ui%E7%BB%84%E4%BB%B6%E5%BA%93demo.png\" alt=\"shadcn-ui组件库demo\"></p>\n<p>那为什么会成为在欧美市场非常受欢迎的组件库？</p>\n<p>从介绍文档好像找到了答案，它选了一条和传统 UI 组件库都不同的路。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2024-02-07/shadcn-ui%E7%BB%84%E4%BB%B6%E5%BA%93%E4%BB%8B%E7%BB%8D.png\" alt=\"shadcn-ui组件库介绍\"></p>\n<p>简单翻译下：\n这不是一个组件库，是一堆可复用组件的集合，你可以把它们复制粘贴到你的项目里去。</p>\n<p>有意思，一个组件库，上来先说自己不是一个组件库。</p>\n<p>底下也迅速解释了，这东西不能作为依赖被安装，它并没有通过 npm 进行分发。你可以选择你需要的组件，复制到你的项目里，好了，现在这是你的代码了。然后你就可以基于这些去构建你自己的组件库了。</p>\n<p>这不就是“复制优于复用”的思路吗？</p>\n<p>大部分时候，我们写代码，会追求复用，小到常量，大到方法和页面。复用的好处很明显，编写的代码量更少，工作更轻松；当你需要修改一些东西时，只需要改一处，不必担心改漏。\n但复用始终是最好的吗？并不是。</p>\n<p>当一个组件被几个地方引用，都是你熟悉、清楚的地方，改一次所有生效，非常完美。\n但当你的工程非常大之后，甚至是好几个项目，数十甚至上百次引用同一个组件，这时候你还敢随意修改这个组件吗？一定会非常小心翼翼，谨慎地去修改，甚至有些 bug 无法修复，只能当 feature 始终保留。\n如果不是靠引用去复用组件而是直接拷贝的呢？显然负担会低很多，哪里错了改哪里。</p>\n<p>现在再想想 AntD 和 shadcn-ui，AntD 好不好？很好。\n但你有空关心 AntD 的每一次发布更新迭代吗？你知道它的维护者们到底是在修 bug 还是在加一些新功能吗？\n它修了它认为的 bug，结果把你的功能搞挂了，算谁的……？更别提大版本更新，一堆不兼容的问题。</p>\n<p>shadcn-ui 完全没有上述问题，代码进你的项目了，是你的了。你只要不改它，它不会给你任何惊喜，但也不会给你任何惊吓。</p>\n<p>下次做个人的小项目，我估计也会坚决投入 shadcn-ui 的阵营。\n但是公司的项目，以 npm 为核心的组件库还是有很多价值的，也许 AntD 会有一些问题，但围绕团队建设贴合业务的小组件库是有必要的。复用能保证同一个坑，一个团队不用踩 10 遍，至于复用的问题，小团队内部高效频繁的沟通能尽可能减少其影响。</p>\n<p>最后还有个小问题，shadcn-ui 怎么更新啊？不用 npm 分发的话，代码的更新也需要手工拷贝合并代码？那已经变更的部分怎么办？我看 github 上也有这个 issue，并没有解决。这可能是复制方案无法规避的问题。</p>\n","date_published":"2024-02-07T00:00:00.000Z","tags":["前端","组件库"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2023/%E4%BD%BF%E7%94%A8cloudflare%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%9B%BE%E5%BA%8A/","url":"https://www.lihuanyu.com/posts/2023/%E4%BD%BF%E7%94%A8cloudflare%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%9B%BE%E5%BA%8A/","title":"使用cloudflare搭建个人图床","summary":"上次分享了一篇使用 cloudflare 作为个人图床的方案，但是比较粗糙，仅仅是直接使用 cloudflare 的 R2 的控制台上传图片，自己拼接图片地址，属于 能用，但并不好用 的状态。 这次我们更产品一点，做一个可视的个人图床应用，包括图片上传、删除、查看、搜索。如果需要将其真正用于生产，还需要一个域名，因为 cloudflare 的免费 dev 域","content_html":"<p>上次分享了一篇<a href=\"https://www.lihuanyu.com/%E8%BF%90%E7%BB%B4/%E4%BD%BF%E7%94%A8cloudflare-R2%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%9B%BE%E5%BA%8A/\">使用 cloudflare 作为个人图床的方案</a>，但是比较粗糙，仅仅是直接使用 cloudflare 的 R2 的控制台上传图片，自己拼接图片地址，属于<em>能用，但并不好用</em>的状态。</p>\n<p>这次我们更产品一点，做一个可视的个人图床应用，包括图片上传、删除、查看、搜索。如果需要将其真正用于生产，还需要一个域名，因为 cloudflare 的免费 dev 域名是被墙掉的。</p>\n<p>最终成品be like:</p>\n<div style=\"text-align: center;\">\n    <img src=\"https://aipaint.lihuanyu.com/2023-12-04/图床应用-列表.png\" alt=\"图床应用-列表\" style=\"box-shadow: 5px 5px 10px #888;\">\n    <p style=\"margin-top: -16px; color: #999; font-size: 14px\">图床应用-图片列表</p>\n<pre><code>&lt;img src=&quot;https://aipaint.lihuanyu.com/2023-12-04/图床应用-上传图片.jpg&quot; alt=&quot;图床应用-列表&quot; style=&quot;box-shadow: 5px 5px 10px #888;&quot;&gt;\n&lt;p style=&quot;margin-top: -16px; color: #999; font-size: 14px&quot;&gt;图床应用-上传图片&lt;/p&gt;\n</code></pre>\n</div>\n<p>使用到的平台包括 cloudflare 和 GitHub。GitHub 用于存储代码。cloudflare 要使用其如下功能：</p>\n<ul>\n<li>Worker 作为计算单元执行存储、查询逻辑</li>\n<li>Page 服务作为前端网页托管平台</li>\n<li>R2 作为图片存储</li>\n<li>D1 作为数据库</li>\n</ul>\n<p>对于个人用户，这些服务几乎都是免费的（每日免费额度对个人用户几乎用不完）。</p>\n<h3>方案架构</h3>\n<p><img src=\"https://aipaint.lihuanyu.com/2023-12-04/cloudflare%E5%9B%BE%E5%BA%8A%E6%9E%B6%E6%9E%84.jpg\" alt=\"cloudflare个人图床架构\"></p>\n<h3>注册并创建服务</h3>\n<p>这并不是一篇面向小白的文章，所以这里假定你已经熟练掌握 GitHub 的使用了。但如果没有使用过 cloudflare ，先去其官网简单创建一个账号。然后开通以下服务：\n<img src=\"https://aipaint.lihuanyu.com/2023-12-04/cloudflare%E6%9C%8D%E5%8A%A1.jpg\" alt=\"\"></p>\n<p>创建一个 R2 bucket，随便取个名字，比如 image-storage-demo 。创建一个 D1 数据库取名比如 image-storage-record ，创建一张表叫 images ，字段如下：\n<img src=\"https://aipaint.lihuanyu.com/2023-12-04/D1%E5%88%9B%E5%BB%BA%E8%A1%A8.jpg\" alt=\"\"></p>\n<p>接下来创建 worker，创建 worker 时需要稍微注意下，要创建两个，分别用于做页面渲染（前端）和业务逻辑（后端）。虽然worker可以同时做前后端，但为了长期的可维护性，这里我们还是区分下前端和服务端。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2023-12-04/%E5%88%9B%E5%BB%BAworker.jpg\" alt=\"\"></p>\n<p>worker 取个名字比如 image-storage-worker</p>\n<h3>创建前端项目</h3>\n<p>在 GitHub 上创建一个仓库，clone到本地，初始化一个前端项目，前端框架这里为了尝鲜，选择了 SolidJS 。关于这个框架的更多信息，可自行查询相关文档。也可以选择自己喜欢的前端框架比如 Vue、React 等，都一样。</p>\n<p><code>npm create vite@latest my-app -- --template solid-ts</code> 即可创建项目。</p>\n<p>为了有一些基础的样式，可以使用 tailwindCSS 作为样式库，直接去 tailwindCSS 的官网找 SolidJS 框架的安装指南照着操作即可，嫌麻烦也可以省略，又不是不能用.jpg。</p>\n<h3>部署前端页面</h3>\n<p>简单编写一个图片上传组件：</p>\n<pre><code class=\"language-tsx\">import { createSignal } from 'solid-js'\n\nfunction ImageUpload () {\n  const [selectedFile, setSelectedFile] = createSignal(null)\n\n  const handleFileChange = (event) =&gt; {\n    const file = event.target.files[0]\n    setSelectedFile(file)\n  }\n\n  const handleUpload = async () =&gt; {\n    if (!selectedFile()) {\n      alert('Please select a file first')\n      return\n    }\n\n    const formData = new FormData()\n    formData.append('file', selectedFile())\n\n    try {\n      const response = await fetch('https://{上传接口地址}/', {\n        method: 'POST',\n        body: formData,\n      })\n\n      if (response.ok) {\n        const result = await response.json()\n        console.log('result', result)\n        window.alert('上传成功')\n      } else {\n        console.error('Upload failed. HTTP status:', response.status)\n      }\n    } catch (error) {\n      console.error('Error during upload:', error)\n    }\n  }\n\n  return (\n    &lt;div class=&quot;max-w-screen-md mx-auto p-4&quot;&gt;\n      &lt;h1 class=&quot;text-2xl font-bold mb-4&quot;&gt;Image Upload&lt;/h1&gt;\n      &lt;input\n        type=&quot;file&quot;\n        accept=&quot;image/*&quot;\n        onChange={handleFileChange}\n        class=&quot;mb-4&quot;\n      /&gt;\n\n      &lt;button onClick={handleUpload} class=&quot;bg-blue-500 text-white py-2 px-4 rounded&quot;&gt;\n        Upload Image\n      &lt;/button&gt;\n    &lt;/div&gt;\n  )\n}\n\nexport default ImageUpload\n</code></pre>\n<p>注意这里上传图片的接口还没写，等下写完 worker 逻辑再加。引入到 app.ts 里，使用它将其展示出来，即可提交代码到 Github。</p>\n<p>这时再去 cloudflare worker page 功能下，选关联已有前端项目，关联这个 solidJS 项目，然后就会自动给分配一个域名并部署，如果你有自己的域名，也可以配置自定义域名，即可无需科学访问。</p>\n<h3>编写服务端逻辑</h3>\n<p>需要安装 cloudflare 的命令行工具，叫 wrangler ，具体参考其官方文档。这里简单写下流程：先安装依赖 <code>npm install -g wrangler</code>，再登录 <code>wrangler login</code> ，接着把项目搞到本地来写 <code>wrangler init --from-dash image-storage-worker</code> （注意和前面创建 worker 时名字一致），可以额外建立一个 git 仓库存储服务端逻辑代码。</p>\n<p>项目到本地后首先编辑 wrangler.toml，<em>加上</em>你的 R2 bucket 和 D1：</p>\n<pre><code class=\"language-toml\">[[d1_databases]]\nbinding = &quot;DATABASE&quot; # JS逻辑里使用时的变量名\nname = &quot;image-storage-record&quot; # 和前面创建的 D1 数据库名一致\n\n[[r2_buckets]]\nbinding = 'imageOSS' # JS逻辑里使用时的变量名\nbucket_name = 'image-storage-demo' # 和前面创建的 R2 bucket 名一致\n</code></pre>\n<p>编辑src/index.ts，修改如下：</p>\n<pre><code class=\"language-ts\">const corsHeaders = {\n  'Access-Control-Allow-Headers': '*', // What headers are allowed. * is wildcard. Instead of using '*', you can specify a list of specific headers that are allowed, such as: Access-Control-Allow-Headers: X-Requested-With, Content-Type, Accept, Authorization.\n  'Access-Control-Allow-Methods': '*', // Allowed methods. Others could be GET, PUT, DELETE etc.\n  'Access-Control-Allow-Origin': '*', // This is URLs that are allowed to access the server. * is the wildcard character meaning any URL can.\n};\n\nexport default {\n  async fetch(request, env) {\n    const requestURL = new URL(request.url);\n    // 查看所有图片\n    if (request.method === 'GET' &amp;&amp; requestURL.pathname === '/query') {\n      return await handleQueryImage(request, env);\n    }\n\n    // 处理图片上传\n    if (request.method === 'POST' &amp;&amp; requestURL.pathname === '/upload') {\n      return await handleImageUpload(request, env);\n    }\n\n    return new Response('Invalid request', { status: 400 });\n  },\n};\n\nconst handleImageUpload = async (request, env) =&gt; {\n  const { DATABASE, imageOSS } = env;\n\n  const formData = await request.formData();\n  const file = formData.get('file');\n\n  if (file) {\n    const path = `${file.name}`;\n    const imageFullPath = `https://{这里换成你的R2域名}/${path}`;\n    await imageOSS.put(path, file);\n    const createdAt = `${+new Date()}`;\n\n    try {\n      const { success } = await DATABASE.prepare(\n        `insert into images (imageName, imageUrl, createdAt) values (?, ?, ?)`,\n      )\n        .bind(path, imageFullPath, createdAt)\n        .run();\n\n      return new Response(JSON.stringify({ url: imageFullPath, success }), {\n        headers: {\n          ...corsHeaders,\n        },\n      });\n    } catch (e) {\n      return new Response(\n        JSON.stringify({\n          success: false,\n          error: JSON.stringify(e),\n        }),\n        {\n          headers: {\n            ...corsHeaders,\n          },\n        },\n      );\n    }\n  }\n};\n\nconst handleQueryImage = async (request, env) =&gt; {\n  const { DATABASE } = env;\n  const requestURL = new URL(request.url);\n  const pageNum = Number(requestURL.searchParams.get('pageNum')) || 1;\n  const pageSize = Number(requestURL.searchParams.get('pageSize')) || 10;\n  const offset = (pageNum - 1) * pageSize;\n  const sql = `select * from images order by id DESC LIMIT ${pageSize} OFFSET ${offset}`;\n\n  const rows = await DATABASE.batch([\n    DATABASE.prepare(sql),\n    DATABASE.prepare(`SELECT COUNT(*) AS total FROM images order by id DESC`),\n  ]);\n  return new Response(\n    JSON.stringify({\n      results: rows[0].results,\n      total: (rows[1]?.results &amp;&amp; rows[1]?.results[0]?.total) || 0,\n      success: rows[0].success &amp;&amp; rows[1].success,\n    }),\n    {\n      headers: {\n        ...corsHeaders,\n      },\n    },\n  );\n};\n</code></pre>\n<p>接下来只需要简单的 <code>wrangler deploy</code> 即可部署完成。然后去看下 cloudflare 给你分配的域名，或者是绑定一个自己的自定义域名上去。拿着这个域名我们就可以去连接前后端了。</p>\n<h3>连接前后端</h3>\n<p>刚刚前端上传页面留了一个上传地址，填入 <em>https://{刚刚拿到的域名}/upload</em> 即对接好了图片上传，worker收到请求会一边把图片存到 R2 里一边把图片信息记录到 D1 里。 可以尝试下看是否返回了成功。</p>\n<p>成功后我们去 cloudflare 的控制台去 D1 里看一眼数据是否入库，再去 R2 里看一眼图片文件是否存在，一切正常后我们再编写一个列表展示页。</p>\n<pre><code class=\"language-tsx\">import { createResource, createSignal, For } from 'solid-js'\nimport dayjs from 'dayjs'\nimport copy from 'clipboard-copy'\n\nconst fetchPics = async ({ pageNum, pageSize }) =&gt;\n  (await fetch(`https://{你的worker地址}/query?pageSize=${pageSize}&amp;pageNum=${pageNum}`)).json()\n\nfunction ImageHome () {\n  const pageSize = 20\n\n  const [pageNo, setPageNo] = createSignal(1)\n\n  const fetchOption = () =&gt; {\n    return {\n      pageNum: pageNo(),\n      pageSize,\n    }\n  }\n\n  const [picList] = createResource(fetchOption, fetchPics)\n\n  const copyToClipboard = async (text) =&gt; {\n    try {\n      // 调用 clipboard-copy 库的方法将内容复制到剪贴板\n      await copy(text)\n    } catch (error) {\n      console.error('Error copying to clipboard:', error)\n    }\n  }\n\n  return (\n    &lt;div&gt;\n      &lt;div class=&quot;flex p-4 lg:p-10 justify-between lg:pb-0 pb-0 items-center&quot;&gt;\n        &lt;h1 class=&quot;p-4 text-2xl&quot;&gt;图片列表&lt;/h1&gt;\n\n        &lt;p&gt;当前第 {pageNo()} 页，共 {picList.loading ? 'loading' : picList().total} 条数据&lt;/p&gt;\n      &lt;/div&gt;\n\n      &lt;div class=&quot;p-4 lg:p-10&quot;&gt;\n        &lt;span&gt;{picList.loading &amp;&amp; 'Loading...'}&lt;/span&gt;\n\n        {!picList.loading &amp;&amp; (\n          &lt;div class=&quot;grid 3xl:grid-cols-4 2xl:grid-cols-3 lg:grid-cols-2 sm:grid-cols-1 gap-6 2xl:gap-8 pb-10&quot;&gt;\n            &lt;For each={picList().results}&gt;{(picItem) =&gt;\n              &lt;div\n                class=&quot;flex flex-col overflow-hidden items-center justify-center bg-slate-200 rounded-xl p-0 flex-wrap md:flex-nowrap&quot;&gt;\n                &lt;div class=&quot;flex flex-shrink-0 items-center w-auto h-96 rounded-none mx-auto p-2 justify-center&quot;&gt;\n                  &lt;img class=&quot;h-full object-contain&quot; src={picItem.imageUrl}/&gt;\n                &lt;/div&gt;\n\n                {/*信息区*/}\n                &lt;div class=&quot;w-full pt-2 pl-5&quot;&gt;\n                  &lt;div&gt;图片名: {picItem.imageName}&lt;/div&gt;\n                  &lt;div&gt;创建时间: {dayjs(+picItem.createdAt).format('YYYY/MM/DD HH:mm:ss')}&lt;/div&gt;\n                  &lt;div&gt;完整地址: {picItem.imageUrl}&lt;/div&gt;\n                &lt;/div&gt;\n\n                {/*操作区*/}\n                &lt;div class=&quot;w-full flex p-3 justify-between&quot;&gt;\n                  &lt;div\n                    class=&quot;m-2 w-full text-center bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded cursor-pointer&quot;\n                    onClick={() =&gt; copyToClipboard(picItem.imageUrl)}&gt;复制\n                  &lt;/div&gt;\n                &lt;/div&gt;\n              &lt;/div&gt;\n            }&lt;/For&gt;\n          &lt;/div&gt;\n        )}\n      &lt;/div&gt;\n\n      &lt;div class=&quot;fixed bottom-0 left-0 p-4 w-full flex flex-1 mt-8 justify-between bg-white shadow-inner&quot;&gt;\n        {pageNo() &gt; 1 ? (\n          &lt;p\n            class=&quot;w-22 relative inline-flex justify-center items-center rounded-md border border-gray-300 text-white px-4 py-2 text-sm font-medium bg-blue-500&quot;\n            onClick={() =&gt; (setPageNo((p) =&gt; p - 1))}\n          &gt;上一页&lt;/p&gt;\n        ) : &lt;p class=&quot;w-20&quot;/&gt;}\n\n        &lt;p class=&quot;font-medium pt-2&quot;&gt;第 {pageNo()} 页&lt;/p&gt;\n\n        {!picList.loading &amp;&amp; pageNo() * pageSize &lt; picList().total ? (\n          &lt;p\n            class=&quot;w-20 relative inline-flex justify-center items-center rounded-md border border-gray-300 text-white px-4 py-2 text-sm font-medium bg-blue-500&quot;\n            onClick={() =&gt; (setPageNo((p) =&gt; p + 1))}\n          &gt;下一页&lt;/p&gt;\n        ) : &lt;p class=&quot;w-20&quot;/&gt;}\n      &lt;/div&gt;\n    &lt;/div&gt;\n  )\n}\n\nexport default ImageHome\n</code></pre>\n<p>这个组件和图片上传组件可以放在一个页面里，也可以自行查询 SolidJS 关于路由的部分，去分两个页面实现。</p>\n<p>本地预览一起正常后，提交代码即可触发自动构建部署，公网访问看看是否一切正常了。</p>\n<h3>删除图片</h3>\n<p>就当留个小练习吧，删除逻辑的代码被我隐藏掉了，但是其实很简单，注册个 /delete 路由，接收 id ，编写 sql 即可，请自行实现吧。</p>\n<h3>结语</h3>\n<p>好了以上就是全部内容，就可以获得一个10GB的OSS存储空间用于做图床，写博客之类的都可以更方便地贴图了。有问题欢迎留言欢迎交流，如果感兴趣的朋友多，后续可以考虑开源相关代码。</p>\n","date_published":"2023-12-04T00:00:00.000Z","tags":["图床","运维","存储"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2023/%E5%A4%A7%E5%8E%82%E7%9A%84%E8%B5%B7%E8%B5%B7%E8%90%BD%E8%90%BD/","url":"https://www.lihuanyu.com/posts/2023/%E5%A4%A7%E5%8E%82%E7%9A%84%E8%B5%B7%E8%B5%B7%E8%90%BD%E8%90%BD/","title":"大厂的起起落落","summary":"2023年11月底，拼多多的股票市值超越了阿里巴巴，目前还很难说是否会再反复变化排名，但是显然阿里巴巴已经不再是电商领域独一无二的王，BAT的时代已经彻底落幕。 2013年刚上大学时听到的BAT，当时排名第一的是百度，第二阿里第三腾讯。因为是相关专业，当年不仅记住了这三家大公司，还记得三家所长，所谓“百度的技术、阿里的运营、腾讯的产品”。 10年过去了，吹技","content_html":"<p>2023年11月底，拼多多的股票市值超越了阿里巴巴，目前还很难说是否会再反复变化排名，但是显然阿里巴巴已经不再是电商领域独一无二的王，BAT的时代已经彻底落幕。</p>\n<p>2013年刚上大学时听到的BAT，当时排名第一的是百度，第二阿里第三腾讯。因为是相关专业，当年不仅记住了这三家大公司，还记得三家所长，所谓“百度的技术、阿里的运营、腾讯的产品”。</p>\n<p>10年过去了，吹技术的百度率先掉队，吹运营的阿里也初见颓势，吹产品的腾讯暂时笑到了最后。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2023-12-04/AI-%E5%B7%A1%E6%B4%8B%E8%88%B03.png\" alt=\"AI绘图\"></p>\n<p>为什么会这样呢，个人浅见如下：</p>\n<p>上面说吹技术的百度，为什么说是“吹”呢？毕业后很幸运加入了百度，见证了百度的衰落。到百度内部会发现，所谓百度的技术，一方面是指百度的技术不错，但核心是指技术同学在百度的话语权最大。</p>\n<p>但现在回顾，以一个 web 前端开发者的视角看，百度的技术并不算遥遥领先，只是在某些比较难的领域有相关沉淀，比如 NLP 。技术有话语权时往往会基于技术的角度去思考和决策，到后期真的是感觉看不明白百度到底在干什么，无人车、 AI（当时的AI和现在的大模型AI差别挺大的） 都是一些不接地气的项目，一边是迟迟看不到落地希望的新项目，一边是因移动互联网被不断蚕食的搜索份额，率先掉队并不令人意外。</p>\n<p>同理，吹运营也不是说阿里的运营能力独步天下，而是阿里的运营同学话语权较大。只要有钱愿意挖人，技术、产品、运营等水平其实是差不多的。而且运营直接感知业务情况，理论上挺科学的，实际上也不错，在业务增长期，阿里才是BAT里领头的那一个。但当市场趋于饱和时，运营为了完成KPI，往往会以牺牲未来的代价换来一时的虚假繁荣。</p>\n<p>比如拼多多和淘宝，两者都喜欢搞营销活动，这无可厚非，但是淘宝的活动目标已经不是简单的优惠，而是要拉用户到活动中花多少时长并尽可能少地付出代价尽可能多地让用户买东西，所以我们可以看到<code>淘宝的营销游戏做得越来越精美，玩法规则变得越来越复杂</code>，但是消费者是来玩游戏的吗？我已经完全免疫了淘宝的各种促销活动，在里面浪费一堆时间，却什么优惠都没有。<br>\n而拼多多，虽然有各种丑闻，比如直播间几十万人砍不下来一台手机，永远差最后的0.001元可提现。但不可否认的是，确实有非常多的用户通过邀请他人砍一刀提到了100-300不等的红包，想必大家都参与过帮家人砍一刀的活动吧。花不了用户几个时间，但优惠是实实在在的。</p>\n<p>就给人感觉是淘宝只想逗用户玩，但并不好玩，拼多多则始终记得电商最核心的事情——便宜。</p>\n<p>不同与BA两家，腾讯一直非常低调，虽然骂声也很多，但作为今天（2023年）中国互联网的 No.1 ，腾讯一定是做对了什么。我觉得这个答案应该是对产品的打磨。</p>\n<p>腾讯是1998年成立的，到今年（2023年）已经有25年了，最近有一篇《马化腾 25 周年致员工信》，在互联网黑话遍地的今天，马化腾这篇员工信却写得非常接地气，哪怕是小学文化的也可以轻易读懂，全篇提到“产品”一词34次，没有提AI，没有提VR，甚至没有提科技。</p>\n<p>但是腾讯能一直笑下去吗？不知道，接下来登场的是抖音和拼多多，这两者“吹”的是 “算法” 。让我们拭目以待，究竟是算法更懂人心还是产品始终为王。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/2023-12-04/AI-%E7%94%B5%E8%84%91.png\" alt=\"\"></p>\n","date_published":"2023-12-04T00:00:00.000Z","tags":["随笔","大厂"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2023/%E8%8B%B1%E8%AF%AD%E7%9A%84%E5%8F%A3%E9%9F%B3/","url":"https://www.lihuanyu.com/posts/2023/%E8%8B%B1%E8%AF%AD%E7%9A%84%E5%8F%A3%E9%9F%B3/","title":"英语的口音","summary":"一个嘲笑印度、日本人说英语的笑话：两个印度人在嘲笑日本人的英语发。\"Jabonese agcent is vedy, vedy hard to undershdand.\" 然后被日本人神吐槽 \"Indeian ekusento ishi belly belly haudo tsu andasudando.\" 以前总觉得中国人说英文的发音还是蛮标准的，至少比印","content_html":"<blockquote>\n<p>一个嘲笑印度、日本人说英语的笑话：两个印度人在嘲笑日本人的英语发。“Jabonese agcent is vedy, vedy hard to undershdand.” 然后被日本人神吐槽 “Indeian ekusento ishi belly belly haudo tsu andasudando.”</p>\n</blockquote>\n<p>以前总觉得中国人说英文的发音还是蛮标准的，至少比印度人、日本人好多了，结果知乎上逛到一个毁三观的结论。对于英国/美国人来说，说英语便于理解的程度大概是，印度人 &gt;&gt; 日本人 ≈ 中国人。</p>\n<p>首先思考一个问题：为什么印度人或日本人说英语，你一听就能听出来？发音差了那么多，难道他们自己注意不到吗？<br>\n对，他们自己注意不到。<br>\n<strong>反之亦然，中国人说英语，其中发音离谱的地方，中国人自己也注意不到。</strong></p>\n<p>我们都觉得印度人“清浊不分”，把 p,t,k 读成 b,d,g 。举例：印度人说 me too 非常像 me do 。</p>\n<p>而事实上，真正清浊不分的是我们自己，印度人 p,t,k 发的就是清音，只不过是不送气清音，相当于 speak 、 star 、 skin 里面的 p,t,k ，但在中国人听起来，是和浊音 b,d,g 没什么区别的（还给这种现象起了个名叫“清音浊化”，但其实那仍然是清音）。</p>\n<p>原因是，汉语不是清浊对立，而是强弱对立，也就是送气清音与不送气清音，汉语普通话里根本不存在真正的 b,d,g 浊音。换句话说，我们（业余英语）在发本该是浊音的 b,d,g 时，发出来的音其实都是不送气的清音 p,t,k 。</p>\n<p>请思考一下北京、豆腐、功夫、宫保鸡丁的英文音译 —— <strong>P</strong>eking、<strong>T</strong>ofu、<strong>K</strong>ungfu、<strong>K</strong>ung <strong>P</strong>ao Chicken —— 明白了吧？我们以为自己发音是 b,d,g ，但实际发出来的根本就是 p,t,k 。只不过是不送气或送气程度较弱的 p,t,k 而已。（这段内容真的解释了我长久以来的困惑，为什么会翻译成这样呢，原来在老外耳里，我们说的就是 p、t、k ）</p>\n<p>如果把塞音进行区分，一分是清（unvoiced）和浊（voiced）；另一分是送气（aspirated）和不送气（unaspirated），即汉语里的强弱区分。<br>\n<strong>清浊的区别是声带是否发声，发音的时候摸摸喉咙就知道。送气和不送气的区别，发音的时候拿一只手放在嘴前面感受一下有没有爆破的气流。</strong></p>\n<p>来现在把手放在喉咙上发音感受下，用标准的普通话说“拜拜”，是不是能感受到喉咙几乎没发音。这时再说“崇拜”，这个拜字就能明显感受到喉咙在发音了。这是少有的能发出浊音的汉字，还得借助连读。平时常用的字音几乎没有浊音的，所以中国人分不清印度人的清浊区分读法，而印度人又不分送气不送气，中国人就觉得他们简直在乱说。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E6%B8%85%E6%B5%8A%E9%9F%B3%E5%8C%BA%E5%88%AB.jpg\" alt=\"塞音四分\"></p>\n<p>如图，美国人发音是清浊音伴随着送不送气的区别。印度人发音只关注清浊音，送不送气对他们都是同一个音。中国人发音只关注送不送气（上文提到的强弱对立），清浊反而分不太清。</p>\n<p>中国人靠送气区分t和d，印度人靠声带发声来区别t和d，而大多数情况下美国人说的t和d，既有送气和不送气的分别，又有声带发不发声的区别。所以中国人印度人互相听着费劲，而都容易理解美国人。只有在美国人说stop这个词的时候，才说不送气轻音，中国人听起来就觉得是d了。相比之下，印度人理解中国人比中国人理解印度人还容易一些，因为这四个音他们都有。</p>\n<p>普通话说“大”的时候，实际上音标上是不送气的/ta/。而如果我们说普通话时把“大”发成英语里/da/的音，听起来就像嗓子顿了一下似的，像很多外国人说汉语的那种洋味十足。（个人的细节感受就是发音位置的区别，正常普通话的大，发音位置就在牙齿舌头那个位置的感觉，而如果用/da/的音，发音位置就在喉咙声带那个位置）</p>\n<p>印度人在应该发送气清音的时候，发出的却经常是不送气清音，这是因为他们的语言中根本不存在送气不送气的区别，送气和不送气，在他们概念里是一样的。（就像部分四川人平翘不分、山东人日乐不分、甘肃人云勇不分，是他们不愿意分吗?是真听不出来差别）<br>\n相应的，我们在遇到该发浊音的时候，发出来的一般都是不送气清音，只是我们自己注意不到，因为浊音与不送气清音，在我们的概念里是一样的。</p>\n<p>所以，印度人的发音虽然在中国人听来和正统英语相去甚远，但实际它就是一种英语的方言，更容易被美国人英国人理解。不过现代英式英语的发音，浊音在渐渐弱化，十分接近不送气清音，反而与汉语接近了，所以中国人把bdg念成不送气清音，英国人听起来应该是没什么障碍，而美式英语仍然是较为明显的清浊对立。</p>\n<p>突然想起来最近的封神榜电影里，费翔的商务殷语，大概也就是这么回事。</p>\n<p>还有一个点是，印度当年是英国殖民地，现在英语也是他们的官方语言之一。这意味着民众都能张口说，在频繁的使用过程中，口音是会趋于一致的，就算说得和标准不一样，只要听懂了一个就能听懂所有印度人说话，而中国人的官方语言就是中文，英语只用于考试和阅读，很少用于交流，所以可能会按学校甚至按个人都存在非常大的区别，当然会更难以让英语母语者(native speaker)听明白。</p>\n","date_published":"2023-11-12T00:00:00.000Z","tags":["英语"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2023/nestjs%E6%8B%A6%E6%88%AA%E5%99%A8%E4%B8%8E%E8%B7%B3%E8%BF%87%E6%8B%A6%E6%88%AA%E5%99%A8/","url":"https://www.lihuanyu.com/posts/2023/nestjs%E6%8B%A6%E6%88%AA%E5%99%A8%E4%B8%8E%E8%B7%B3%E8%BF%87%E6%8B%A6%E6%88%AA%E5%99%A8/","title":"NestJS 拦截器与跳过拦截器","summary":"写 API 接口时，我们通常希望有个统一的结构包裹一下要返回的数据，每个接口分别去写不光繁琐，还容易不一致，所以一般会用一个统一的拦截器来实现这个功能。各种语言的不同框架基本都有对应的拦截器写法，今天分享下 nestjs 里如何编写拦截器和如何跳过拦截器。","content_html":"<p>写 API 接口时，我们通常希望有个统一的结构包裹一下要返回的数据，每个接口分别去写不光繁琐，还容易不一致，所以一般会用一个统一的拦截器来实现这个功能。各种语言的不同框架基本都有对应的拦截器写法，今天分享下 nestjs 里如何编写拦截器和如何跳过拦截器。</p>\n<p>比如普通的一个查询接口，返回的数据如：</p>\n<pre><code class=\"language-json\">{\n  &quot;user&quot;: &quot;xxx&quot;,\n  &quot;image_url&quot;: &quot;https://xxxxx&quot;\n}\n</code></pre>\n<p>客户端侧往往需要区分是成功还是失败，所以希望这个数据被包在 data 字段下面，再用一个同级的 <code>&quot;status&quot;: &quot;success&quot;, &quot;code&quot;: 0</code> 来代表成功。如果是失败的，则返回 fail 、对应错误码及错误信息。</p>\n<pre><code class=\"language-json\">{\n  &quot;code&quot;: 0,\n  &quot;status&quot;: &quot;success&quot;,\n  &quot;data&quot;: {\n    &quot;user&quot;: &quot;xxx&quot;,\n    &quot;image_url&quot;: &quot;https://xxxxx&quot;\n  }\n}\n</code></pre>\n<p>要包装返回数据的需求非常的常见，相关的文档非常的多。对于 nestjs ，很快就查到正确的写法：</p>\n<pre><code class=\"language-ts\">import {\n  CallHandler,\n  ExecutionContext,\n  Inject,\n  Injectable,\n  NestInterceptor,\n} from '@nestjs/common';\nimport { Observable } from 'rxjs';\nimport { map } from 'rxjs/operators';\nimport { Reflector } from '@nestjs/core';\n\n@Injectable()\nexport class ResponseFormatInterceptor implements NestInterceptor {\n  constructor(@Inject(Reflector) private readonly reflector: Reflector) {}\n\n  intercept(\n    context: ExecutionContext,\n    next: CallHandler&lt;any&gt;,\n  ): Observable&lt;any&gt; | Promise&lt;Observable&lt;any&gt;&gt; {\n    return next.handle().pipe(\n      // 将原有的 `data` 转化为统一的格式后返回\n      map((data) =&gt; ({\n        code: 0,\n        success: true,\n        status: 'success',\n        data,\n      })),\n    );\n  }\n}\n</code></pre>\n<p>对应的还要有一个错误拦截：</p>\n<pre><code class=\"language-ts\">import {\n  CallHandler,\n  ExecutionContext,\n  Injectable,\n  NestInterceptor,\n} from '@nestjs/common';\nimport { Observable } from 'rxjs';\nimport { catchError } from 'rxjs/operators';\n\n@Injectable()\nexport class ResponseErrorInterceptor implements NestInterceptor {\n  intercept(\n    context: ExecutionContext,\n    next: CallHandler&lt;any&gt;,\n  ): Observable&lt;any&gt; | Promise&lt;Observable&lt;any&gt;&gt; {\n    return next.handle().pipe(\n      catchError(async (err) =&gt; {\n        const errorResponse = {\n          code: (err.response &amp;&amp; err.response.code) || -1,\n          success: false,\n          status: err.status || 'fail',\n          message: err.message || '不明异常',\n          data: err.response,\n        };\n\n        return errorResponse;\n      }),\n    );\n  }\n}\n</code></pre>\n<p>最后再在 app.module.ts 注册这两个拦截器：</p>\n<pre><code class=\"language-ts\">@Module({\n    // imports: ... ,\n    // controllers: ... ,\n    providers: [\n        // ...\n        { provide: APP_INTERCEPTOR, useClass: ResponseErrorInterceptor },\n        { provide: APP_INTERCEPTOR, useClass: ResponseFormatInterceptor },\n    ],\n})\nexport class AppModule {}\n</code></pre>\n<p>这样就可以满足需求了，但有一些接口，不希望有这个包装，比如微信推送消息的服务，推给你的服务器后需要你的服务器返回一个 ‘success’ 的文本。这时需要跳过这个全局拦截器，这个不知道是搜索姿势不对还是这个需求比较少，比较少找到相关资料。</p>\n<p>这里使用的是装饰器 decorator 来实现：</p>\n<pre><code class=\"language-ts\">// src/decorator/skip.decorator.ts\nimport { SetMetadata } from '@nestjs/common';\n\nexport const SkipInterceptor = () =&gt; SetMetadata('skipInterceptor', true);\n</code></pre>\n<p>这样就声明了一个装饰器，装饰器会设置一个 metadata skipInterceptor 值为 true，再修改格式化响应的拦截器：</p>\n<pre><code class=\"language-ts\">import {\n    CallHandler,\n    ExecutionContext,\n    Inject,\n    Injectable,\n    NestInterceptor,\n} from '@nestjs/common';\nimport { Observable } from 'rxjs';\nimport { map } from 'rxjs/operators';\nimport { Reflector } from '@nestjs/core';\n\n@Injectable()\nexport class ResponseFormatInterceptor implements NestInterceptor {\n    constructor(@Inject(Reflector) private readonly reflector: Reflector) {}\n\n    intercept(\n        context: ExecutionContext,\n        next: CallHandler&lt;any&gt;,\n    ): Observable&lt;any&gt; | Promise&lt;Observable&lt;any&gt;&gt; {\n        const skipInterceptor = this.reflector.get&lt;boolean&gt;(\n            'skipInterceptor',\n            context.getHandler(),\n        );\n        if (skipInterceptor) {\n            // 跳过全局拦截器的处理\n            return next.handle();\n        }\n        return next.handle().pipe(\n            // 将原有的 `data` 转化为统一的格式后返回\n            map((data) =&gt; ({\n                code: 0,\n                success: true,\n                status: 'success',\n                data,\n            })),\n        );\n    }\n}\n</code></pre>\n<p>在 intercept 中判断是否 skipInterceptor ，是的话就直接 next ，否的话正常 pipe 里做包装。最后，使用时：</p>\n<pre><code class=\"language-ts\">// 使用装饰器\nimport { Body, Controller, HttpCode, Post } from '@nestjs/common';\nimport { SkipInterceptor } from '../../decorator/skip.decorator';\n\n@Controller('xxx')\nexport class xxxController {\n    constructor() {}\n\n    @Post('push')\n    @HttpCode(200)\n    @SkipInterceptor()\n    async handleWechatPush(@Body() data) {\n        // ...逻辑处理\n        return 'success';\n    }\n}\n</code></pre>\n<p>就可以拿到纯字符串的 success 了。</p>\n","date_published":"2023-10-22T00:00:00.000Z","tags":["nestjs","拦截器","开发"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2023/%E4%BD%A0%E5%A5%BD%E5%85%B0%E5%B7%9E/","url":"https://www.lihuanyu.com/posts/2023/%E4%BD%A0%E5%A5%BD%E5%85%B0%E5%B7%9E/","title":"你好兰州","summary":"中秋的前一天，关系要好的大学室友要结婚了，请假两天去兰州参加婚礼，草草游览了兰州。 因为时间和价格的原因，这趟行程往返都是火车，还是硬卧，本来以为睡一觉就能到达，会比飞机更舒服。但实际硬卧车厢的体验并不好，人很多环境比较脏，还有烟味和小孩的吵闹，睡眠质量向来还可以的我都失眠到两三点才迷迷糊糊睡了过去。","content_html":"<p>中秋的前一天，关系要好的大学室友要结婚了，请假两天去兰州参加婚礼，草草游览了兰州。</p>\n<p>因为时间和价格的原因，这趟行程往返都是火车，还是硬卧，本来以为睡一觉就能到达，会比飞机更舒服。但实际硬卧车厢的体验并不好，人很多环境比较脏，还有烟味和小孩的吵闹，睡眠质量向来还可以的我都失眠到两三点才迷迷糊糊睡了过去。</p>\n<p>想想自己差不多10多年没坐过硬卧火车了，记忆中的硬卧车厢还是蛮高级的存在，现在却变得如此糟糕。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E7%A1%AC%E5%8D%A7%E8%BD%A6%E5%8E%A2.jpeg\" alt=\"硬卧车厢\"></p>\n<p>一觉醒来差不多就在兰州附近了，车厢里隔壁铺的姑娘还在抓紧每分每秒学英语</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E7%81%AB%E8%BD%A6%E4%B8%8A%E5%AD%A6%E4%B9%A0%E7%9A%84%E5%A7%91%E5%A8%98.jpg\" alt=\"火车上学习的姑娘\"></p>\n<p>窗外的景色就和电视里对大西北的传统印象一样，黄土地、植物不多。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E9%BB%84%E5%9C%9F%E5%9C%B0.jpeg\" alt=\"黄土地\"></p>\n<p>偶尔还能看到一些房子，已经被遗弃的样子，没有看到窑洞，估计还是房子住着更舒服一些吧。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E9%BB%84%E5%9C%9F%E9%AB%98%E5%9D%A1%E4%B8%8A%E7%9A%84%E6%88%BF%E5%AD%90.jpeg\" alt=\"黄土高坡上的房子.jpeg\"></p>\n<p>相比于大漠风情，我还是更喜欢四川或者说南方那种植物茂密、郁郁葱葱的景象。一路过来有一种感觉就是，和小时候相比，农村里的人明显少了很多，几乎没有青壮年，都是老人家。</p>\n<p>随后火车在兰州东站停了大概1小时，再开十来分钟就到了兰州站。兰州站也是一个老站，没有现在那些高大上的新修的高铁站的现代感、科技感，却有一种历史的厚重感。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E5%85%B0%E5%B7%9E%E4%B8%9C.jpeg\" alt=\"兰州东.jpeg\"></p>\n<p>到了兰州有新郎的朋友来接站，带着我吃了到兰州的第一顿早餐，兰州牛肉面。全国都能看到兰州拉面，但兰州并没有拉面，就像重庆没有鸡公煲。甚至重庆可能现在也有鸡公煲了，而兰州还找不到拉面馆，因为兰州都管这个叫兰州牛肉面。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E5%85%B0%E5%B7%9E%E7%89%9B%E8%82%89%E9%9D%A2.jpeg\" alt=\"兰州牛肉面.jpeg\"></p>\n<p>大早上吃碗这个是真的舒服，很撑。住的酒店也挺不错，在黄河边上，可以直接看到黄河。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E9%BB%84%E6%B2%B3.jpeg\" alt=\"黄河.jpeg\"></p>\n<p>黄河的水是真的黄，和电视里一样黄。以前去的景点也不少，但是还真少住在这种江河边可以直接看到江河的房子。所以对这个酒店是非常满意了，不过黄河确实小，还不如我家门口的绵远河宽。</p>\n<p>上午在酒店躺着休息了会儿，中午自己出去转悠，想起盗月社曾经在这边吃过一家烤串店，打车去吃了个午饭。</p>\n<div style=\"display: flex; justify-content: space-around\">\n    <img style=\"width: 50%\" src=\"https://aipaint.lihuanyu.com/烤串店.jpeg\" alt=\"烤串店\">\n    <img style=\"width: 50%\" src=\"https://aipaint.lihuanyu.com/烤牛肉.jpeg\" alt=\"烤牛肉\">\n</div>\n<p>看着还不错，吃着也好吃，价格不算便宜也不贵。单人吃下来48，20的肉20的筋，3块的饼5块的汽水。尤其是这饼，里面居然全是辣椒，把我一个四川人给整不会了，最后只吃了3/4。</p>\n<div style=\"display: flex; justify-content: space-around\">\n    <img style=\"width: 50%\" src=\"https://aipaint.lihuanyu.com/烤饼.jpeg\" alt=\"烤饼\">\n    <img style=\"width: 50%\" src=\"https://aipaint.lihuanyu.com/内含辣椒的烤饼.jpeg\" alt=\"内含辣椒的烤饼\">\n</div>\n<p>吃完继续溜达，兰州应该是一个三线城市，城建水平相比成都差了不少，和德阳感觉都差不多，堵车非常严重。兰州市中心的大广场还有当年的中国的味道</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E5%85%B0%E5%B7%9E%E7%9A%84%E5%A4%A7%E5%B9%BF%E5%9C%BA.jpeg\" alt=\"兰州的大广场.jpeg\"></p>\n<p>就在这个充斥红色氛围的广场旁边，就是新修的万象城大商城，看起来又非常的现代化。</p>\n<div style=\"display: flex; justify-content: space-around\">\n    <img style=\"width: 50%\" src=\"https://aipaint.lihuanyu.com/兰州万象城.jpeg\" alt=\"兰州万象城\">\n    <img style=\"width: 50%\" src=\"https://aipaint.lihuanyu.com/兰州万象城内部.jpeg\" alt=\"兰州万象城内部\">\n</div>\n<p>接下来是这个旅程中最满意的部分，兰州市博物馆。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E5%85%B0%E5%B7%9E%E5%8D%9A%E7%89%A9%E9%A6%86.jpeg\" alt=\"兰州博物馆.jpeg\"></p>\n<p>里面藏品很多，也比较精美，应该是当年丝绸之路时代留下的文物。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E5%85%B0%E5%B7%9E%E5%8D%9A%E7%89%A9%E9%A6%86%E8%97%8F%E5%93%81.jpeg\" alt=\"兰州博物馆藏品.jpeg\"></p>\n<p>这里面有个白衣寺塔的模型，很好看引起了我的注意</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E5%85%B0%E5%B7%9E%E5%8D%9A%E7%89%A9%E9%A6%86-%E7%99%BD%E8%A1%A3%E5%AF%BA%E5%A1%94.jpeg\" alt=\"兰州博物馆-白衣寺塔.jpeg\"></p>\n<p>主要是我越看这个模型越觉得眼熟，然后突然反应过来，这不是这个博物馆本身吗？一看介绍果然如此，这个白衣寺塔就是博物馆本身，寺庙建于明朝崇祯年，只有塔保留到了现在。博物馆是1991年迁入这里的。</p>\n<p>为什么我会反应过来这个就是博物馆本身呢？因为进来时候有注意到博物馆中央的大塔</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E5%85%B0%E5%B7%9E%E5%8D%9A%E7%89%A9%E9%A6%86-%E5%86%85%E9%83%A8%E7%99%BD%E8%A1%A3%E5%AF%BA%E5%A1%94.jpeg\" alt=\"兰州博物馆-内部白衣寺塔.jpeg\"></p>\n<p>里面还有左宗棠左公的雕像，还有贡院模型等，以及古代的兰州城沙盘</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E5%8F%A4%E4%BB%A3%E5%85%B0%E5%B7%9E%E6%B2%99%E7%9B%98.jpeg\" alt=\"古代兰州沙盘.jpeg\"></p>\n<p>逛完博物馆就回去休息了，晚上也只是在河边简单走了走。网上说兰州是白天阿富汗，夜间小香港，只能说晚上灯火是蛮好看的，小香港可能夸张了点。</p>\n<p>第二天参加婚礼，西北的婚礼和南方的婚礼又有一些不一样。同样是随份子钱，南方基本是收红包，红包上写名字，办完回去挨个拆红包记录礼金数量。北方就相对豪迈一些，参加的亲戚朋友都直接拿现金，不用红包，甚至可以电子支付，给了直接记录上。包了红包的也会现场拆开清点。给多给少都是心意，一些长辈混得不好的随200也很大方的给，混得好的给2000的也有也没有嘚瑟，所以和南方的方式比也没有什么好坏之分，只是地域不同习俗不同而已。</p>\n<p>室友本身就是一个很会搞事的，婚礼也很热闹，我本来以为他们会整点才艺展示，最后虽然是有表演，但并不是新郎伴郎表演的，而是专门的舞蹈队。一个比较有意思的是，大屏幕上青青草原，按我的思路应该会是一些比较柔和的音乐，实际确实像disco舞厅蹦迪一样的动次打次。</p>\n<p>给我的感觉是，围城无处不在。中原、江南等人口稠密区，本身就很热闹很繁华，所以这里的人们总想要一些隐居、清静的感觉，喜欢一种僻静的感觉。而西北大漠，虽然婚礼、聚会上很热闹，但我想，平时的他们可能没那么多人，所以才会更喜欢热闹吧。</p>\n<p>晚上再在市中心的步行街里的一家饭店里吃上一顿晚饭，就准备赶火车回家了。</p>\n<p>兰州的饭还有一个有意思的地方，上来会先给你端一杯茶，这个茶是按人头的，你要换位置请自己端好自己的茶。这个茶叫三炮台，有什么功能不清楚，但在这边吃了三顿比较正式的饭，每顿都会先给你一杯这个茶。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E5%85%B0%E5%B7%9E-%E4%B8%89%E7%82%AE%E5%8F%B0%E8%8C%B6.jpeg\" alt=\"兰州-三炮台茶.jpeg\"></p>\n<p>最后走的时候，步行街上人来人往，我感觉这个城市的人是快乐的，比北京快乐，比成都快乐，而且孩子很多，在大街上三五成群打打闹闹，感觉这才是小孩应有的状态，而这个时间点，北上广的孩子应该都在写作业、补习班里吧。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E5%85%B0%E5%B7%9E%E5%B8%82%E4%B8%AD%E5%BF%83%E6%AD%A5%E8%A1%8C%E8%A1%97.jpeg\" alt=\"兰州市中心步行街.jpeg\"></p>\n<p>总之，来过兰州了。你好兰州，再见兰州 👋🏻 。</p>\n","date_published":"2023-09-29T00:00:00.000Z","tags":["随笔","游记"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2023/React%E7%9A%84key/","url":"https://www.lihuanyu.com/posts/2023/React%E7%9A%84key/","title":"React 里的 key 不只是列表优化","summary":"最近组里的同学问我，在小程序里想强制重建一个组件要怎么办。 背景是一个表单渲染器内部状态处理得不够干净，切换数据后偶尔需要销毁再创建。我之前也遇到过，处理方式比较直接：用条件渲染让组件先消失再出现。比如先把 a:if 改成 false ，下一轮再改回 true 。 同学说，PC 里的 React 只要改一下 key 就行，小程序不支持吗？","content_html":"<p>最近组里的同学问我，在小程序里想强制重建一个组件要怎么办。</p>\n<p>背景是一个表单渲染器内部状态处理得不够干净，切换数据后偶尔需要销毁再创建。我之前也遇到过，处理方式比较直接：用条件渲染让组件先消失再出现。比如先把 <code>a:if</code> 改成 <code>false</code>，下一轮再改回 <code>true</code>。</p>\n<p>同学说，PC 里的 React 只要改一下 <code>key</code> 就行，小程序不支持吗？</p>\n<p>我当时第一反应是：<code>key</code> 不就是列表渲染里用来辅助 diff 的东西吗？</p>\n<p>这个理解不算错，但放在 React 里不完整。React 里的 <code>key</code> 确实常见于列表，但它更底层的作用是参与判断“这是不是同一个组件”。</p>\n<h2>key 描述的是组件身份</h2>\n<p>React 会把状态绑定到渲染树里的某个位置。多数情况下，只要同一个组件类型还在同一个位置，React 就会保留它的状态。</p>\n<p>如果给组件加上 <code>key</code>，这个 <code>key</code> 就会成为组件身份的一部分：</p>\n<pre><code class=\"language-jsx\">&lt;Editor key={articleId} article={article} /&gt;\n</code></pre>\n<p>当 <code>articleId</code> 改变时，React 不会把它理解成“同一个 <code>Editor</code> 组件收到了一份新 props”，而是理解成“旧的 <code>Editor</code> 被移除，新的 <code>Editor</code> 被创建”。结果就是：</p>\n<ul>\n<li>组件内部的 <code>useState</code> 会重新初始化；</li>\n<li>子组件树里的状态也会一起重置；</li>\n<li>effect 的清理和重新执行也会按卸载、挂载流程走；</li>\n<li>DOM 节点可能会被重新创建，而不是继续复用。</li>\n</ul>\n<p>所以，改 <code>key</code> 不是普通意义上的“重新渲染”，而是更接近“换了一个组件实例”。</p>\n<h2>什么时候适合用</h2>\n<p>最适合的场景是：组件内部状态本来就应该跟某个业务身份绑定。</p>\n<p>比如聊天窗口切换联系人后，不希望把上一个人的输入草稿保留下来：</p>\n<pre><code class=\"language-jsx\">&lt;Chat key={to.id} contact={to} /&gt;\n</code></pre>\n<p>或者编辑器切换文章后，希望本地草稿、校验状态、光标位置都从新文章开始：</p>\n<pre><code class=\"language-jsx\">&lt;ArticleEditor key={article.id} article={article} /&gt;\n</code></pre>\n<p>这种时候用 <code>key</code> 很自然，因为用户理解里的“对象”已经变了，组件状态也应该跟着换一份。</p>\n<h2>什么时候不该用</h2>\n<p>不应该把 <code>key</code> 当成修复状态 bug 的万能开关。</p>\n<p>如果组件只要不销毁重建就会出错，通常说明内部状态和外部数据的关系没有理顺。比如 props 变了，但组件内部缓存没有同步；或者表单渲染器把 schema、默认值、用户输入混在了一起。这个时候改 <code>key</code> 能让问题消失，但也可能只是绕开了真正的问题。</p>\n<p>尤其不要写这种代码：</p>\n<pre><code class=\"language-jsx\">&lt;Form key={Date.now()} /&gt;\n</code></pre>\n<p>或者：</p>\n<pre><code class=\"language-jsx\">&lt;Form key={Math.random()} /&gt;\n</code></pre>\n<p>这样每次渲染都会让 React 认为组件身份变了。输入框会丢内容，effect 会反复执行，性能也会变差。</p>\n<p>列表里的 <code>key</code> 也一样，应该优先使用稳定的业务 id。用数组下标做 <code>key</code> 不是绝对不行，但如果列表会插入、删除、排序，就很容易让状态跟错项目。</p>\n<h2>回到小程序的问题</h2>\n<p>所以当时那个小程序问题，React 里改 <code>key</code> 确实是一种可用手段，但它背后的语义不是“刷新一下组件”，而是“告诉 React 这是另一个组件”。</p>\n<p>在小程序或 Vue 的上下文里，<code>key</code> 的行为和 React 不完全一样。要强制重建组件，条件渲染是更直接的办法；更稳妥的做法则是把组件的数据流整理清楚，让它能正确响应外部数据变化。</p>\n<p>如果一个表单渲染器必须靠销毁重建才能工作，大概率是组件内部藏了太多不受控状态。短期可以用重建救急，长期还是应该把 schema、初始值、用户输入和重置逻辑拆清楚。</p>\n<p>React 官方文档里有两处相关说明：</p>\n<ul>\n<li><a href=\"https://react.dev/learn/preserving-and-resetting-state\">Preserving and Resetting State</a></li>\n<li><a href=\"https://react.dev/reference/react/useState#resetting-state-with-a-key\">Resetting state with a key</a></li>\n</ul>\n","date_published":"2023-09-19T00:00:00.000Z","tags":["前端","React"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2023/Mac%E7%A3%81%E7%9B%98%E6%B8%85%E7%90%86%E5%B7%A5%E5%85%B7%E6%8E%A8%E8%8D%90/","url":"https://www.lihuanyu.com/posts/2023/Mac%E7%A3%81%E7%9B%98%E6%B8%85%E7%90%86%E5%B7%A5%E5%85%B7%E6%8E%A8%E8%8D%90/","title":"Mac磁盘清理工具推荐","summary":"Mac 电脑性能不错，但是内存和硬盘都是大坑，又贵又小。虽然 mac 软件不多并不像 Windows 一样占用硬盘过大，但是长期使用下来，磁盘空间也是一个问题。今天推荐一个 mac 磁盘清理工具，或者说磁盘体积分析工具，方便找到磁盘空间被占用的原因。","content_html":"<p>Mac 电脑性能不错，但是内存和硬盘都是大坑，又贵又小。虽然 mac 软件不多并不像 Windows 一样占用硬盘过大，但是长期使用下来，磁盘空间也是一个问题。今天推荐一个 mac 磁盘清理工具，或者说磁盘体积分析工具，方便找到磁盘空间被占用的原因。</p>\n<p>点开左上角的苹果图标可以看到硬盘占用情况，系统自带的分析能在一定程度上告诉你硬盘被什么占用了。但是有很大的体积显示的是系统文件，这时需要额外的软件帮助分析体积占用原因了。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/mac%E7%A1%AC%E7%9B%98%E4%BD%BF%E7%94%A8%E6%83%85%E5%86%B5.png\" alt=\"mac磁盘占用情况\"></p>\n<p>OmnidiskSweeper 可以帮助分析磁盘体积构成，找到那些体积非常大但并不重要的文件，比如一些缓存、日志、废弃的SDK等等。</p>\n<p>之所以推荐这款软件是因为它非常的干净纯粹，就只是计算磁盘文件体积，要删除什么完全由用户决定。相比之下，不管是 Windows 还是 mac 别的清理软件，都有点像流氓软件，功能不多不好用不说。一旦安装上，想卸载就很费劲了。</p>\n<p>软件界面大概这样：\n<img src=\"https://aipaint.lihuanyu.com/Omnisweeper%E7%95%8C%E9%9D%A2.png\" alt=\"Omnisweeper界面\"></p>\n<p>之前被知乎安利的，后来换电脑时候想找找不到了，找了半天才找回来，感觉这个软件值得我写一篇文档记录及推荐。</p>\n<p>omni是一家有意思的软件公司，他们主要盈利的产品是下面这四兄弟。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/omni%E5%9B%9B%E5%85%84%E5%BC%9F.png\" alt=\"omni四兄弟\"></p>\n<p>这家公司有意思的地方在于它的自我介绍：</p>\n<blockquote>\n<p>The Omni Group makes and supports awesome apps for Mac, iPad, iPhone, Apple Watch, and the web. We’re an employee-owned company in beautiful Seattle, Washington.<br>\n这家公司为苹果公司的硬件和web平台开发一些牛逼的软件，它们是一家在西雅图的，由员工拥有的公司。</p>\n</blockquote>\n<p>可以理解是一家全员持股的小而美的公司吧，看得出来苹果的产品、设计、风格很对他们的胃口。做的产品也都是用于提效方面的。而且他们还有关于愿景、使命、价值观方面的阐述与展示。</p>\n<p>而这个 OmnidiskSweeper 属于 Omni Labs 的一个小软件，这个分类的简介是：</p>\n<blockquote>\n<p>Sometimes we make software for us, and we love it so much we make it available to you, too.\n有时候我们也会开发一些软件为我们自己，我们很喜欢他们也希望可以帮到你们。</p>\n</blockquote>\n<p>好的，感谢Omni公司的慷慨。</p>\n","date_published":"2023-09-16T00:00:00.000Z","tags":["mac","工具","磁盘清理","mac磁盘清理"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2023/%E8%AE%B0%E5%BD%95%E4%B8%80%E6%AC%A1SQL%E6%B3%A8%E5%85%A5%E4%B8%8E%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/","url":"https://www.lihuanyu.com/posts/2023/%E8%AE%B0%E5%BD%95%E4%B8%80%E6%AC%A1SQL%E6%B3%A8%E5%85%A5%E4%B8%8E%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/","title":"记录一次SQL注入与问题排查","summary":"最近做了一个小程序，用 nestJS 做的服务端，数据库是 MySQL 。然后被SQL注入攻击了，第一次遇到，感觉还很有意思，记录一下。 其实是微信平台做的模拟攻击，所以也并没有任何实际破坏，仅仅是数据库里被塞入了多条不符合预期的数据。","content_html":"<p>最近做了一个小程序，用 nestJS 做的服务端，数据库是 MySQL 。然后被SQL注入攻击了，第一次遇到，感觉还很有意思，记录一下。</p>\n<p>其实是微信平台做的模拟攻击，所以也并没有任何实际破坏，仅仅是数据库里被塞入了多条不符合预期的数据。</p>\n<p>背景是微信小程序在提审的时候提示了这个：</p>\n<p><img src=\"https://aipaint.lihuanyu.com/66dca5b4a7937737a9bb6d68a1e29ff.png\" alt=\"微信接口安全测试\"></p>\n<p>“安全测试”，模拟用户发送请求。有点意思，虽然比较少接触服务端内容，但是对于一些常见的网络攻击手段也有一定了解。比如ssh弱密码爆破，struts2，log4j2，redis，pg漏洞利用之类的，一般以为只有使用一些大的系统时因为系统本身的漏洞比较容易被攻破，我这是手写的程序，出入参都有校验，数据库只准本机链接，还是比较自信应该没有问题。</p>\n<p>点击同意后，后台的日志系统就看到了密密麻麻的请求，基本没有看到有报错，觉得还可以啊，一切正常。<br>\n然后就翻车了。在后台查看历史记录时发现40多条空白记录，进数据库一看就觉得不太对。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E6%95%B0%E6%8D%AE%E5%BA%93%E8%A2%AB%E6%B3%A8%E5%85%A5%E5%90%8E%E7%9A%84%E5%BC%82%E5%B8%B8%E6%95%B0%E6%8D%AE.png\" alt=\"微信接口安全测试\"></p>\n<p>那就修问题吧，仔细想了想涉及到写这张表的接口，想了半天没头绪，那就查日志看看。应用是使用 NestJS 编写的，用 PM2 部署的。</p>\n<p>PM2 的日志查询是用 pm2 log 进行查看，但是这个命令只能查看最近的日志和实时的日志，之前的日志在 <code>.pm2/logs</code> 目录下，通过scp命令下载下来。</p>\n<p>读日志时候遇到一点小问题，里面有很多乱码一样的东西：</p>\n<p><img src=\"https://aipaint.lihuanyu.com/sql%E6%B3%A8%E5%85%A5%E6%97%A5%E5%BF%97%E5%88%86%E6%9E%90.png\" alt=\"微信接口安全测试\"></p>\n<p>这个不是编码问题，而是终端控制字符，用来让日志在终端里有颜色，查了下说是可以安装VT100什么的插件解决，但是最终效果也不是很好，不影响阅读，就算了，也许有专门用于读日志的软件？</p>\n<p>从日志里可以很明显看到微信的模拟攻击是如何注入数据库的：<code>用户1676请求历史绘图列表第1&quot; union select 1,2-- 页</code> ，是在分页查询中加入的，查看代码发现这里确实没做好，赶紧补上一个parseInt和isNaN判断。</p>\n<p>这种模拟攻击检查还挺好的，能有效帮助个人开发者检查一些漏洞，不知道微信的实现原理是什么，是靠静态分析小程序代码里请求相关的逻辑？如果把请求发送从字符串变成变量的形式，岂不是可以规避微信的检查？</p>\n","date_published":"2023-08-20T00:00:00.000Z","tags":["SQL注入","PM2日志阅读"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2023/%E4%BD%BF%E7%94%A8cloudflare%20R2%E6%89%98%E7%AE%A1%E5%9B%BE%E7%89%87/","url":"https://www.lihuanyu.com/posts/2023/%E4%BD%BF%E7%94%A8cloudflare%20R2%E6%89%98%E7%AE%A1%E5%9B%BE%E7%89%87/","title":"使用cloudflare R2托管图片","summary":"我的个人 blog 还是比较有历史的，大概是13年的时候就开始备案域名搞云服务器玩，最早是用的 wordpress ，一个 PHP 的 CMS 系统。功能挺强的，但是维护实在麻烦心累，数据库的管理和迁移尤其麻烦。 后来就走上了静态生成这条路，先是 Jekyll 现在是 Hexo。现在Hexo的更新维护也比较慢了，也许未来还会换一个热门的，如果有时间的话。","content_html":"<p>我的个人 blog 还是比较有历史的，大概是13年的时候就开始备案域名搞云服务器玩，最早是用的 wordpress ，一个 PHP 的 CMS 系统。功能挺强的，但是维护实在麻烦心累，数据库的管理和迁移尤其麻烦。</p>\n<p>后来就走上了静态生成这条路，先是 Jekyll 现在是 Hexo。现在Hexo的更新维护也比较慢了，也许未来还会换一个热门的，如果有时间的话。</p>\n<p>相信大多数个人 blog 现在也都是静态生成这条路，维护起来是真的方便，几年不动都没什么问题。博文的内容就用 markdown 文件编写，存在 GitHub 之类的地方也方便，再也不用担心会丢失。</p>\n<p>但是图片的展示、存储就是一个问题了。目前的 Hexo 的方案是会有一个和文章同名的文件夹用于存放图片，但这样做对未来迁移到别的平台存在隐患，并且云机器基本租的是最便宜档的，带宽很小不太想消耗在图片上。</p>\n<p>AWS 的 S3 服务（Simple Storage Service，简单存储服务）很早就听说过了，但是显然国情在此用不了。阿里云、腾讯云的 OSS （对象存储服务，我理解都一样，取名字而已）方案不光看着不简洁，且要收费，所以一直是考虑状态，先凑合本地图片用着。</p>\n<p>最近机缘巧合，使用了 cloudflare 的 worker 服务（稍后分享下如何使用 worker 解决 OpenAI 接口的代理问题），发现这个公司的服务蛮好用的，而且成本较低，免费额度对个人用户基本够用。</p>\n<p>cloudflare 提供了一个叫 R2 的服务，也是 OSS ，成本对个人用户<s>几乎免费</s>非常合理。<a href=\"https://developers.cloudflare.com/r2/platform/pricing\">R2定价说明文档。</a></p>\n<p><img src=\"https://aipaint.lihuanyu.com/cloudflare-r2-price.png\" alt=\"R2的免费额度\"></p>\n<p>10个G以内的存储和1千万的查询都是免费的，于是，本 blog 有了几乎免费的图床可以使用了。</p>\n<p>过程也比较简单，分享下：</p>\n<p>前置要求，首先需要有一个自己的域名，使用国内的云服务器的话还需要备案操作。</p>\n<p>注册 cloudflare 省略。</p>\n<p>登录 cloudflare ，把域名的解析服务器配置到 cloudflare ，这样方便在使用R2的时候绑定一个自己的域名。在侧边菜单找到 R2 服务。</p>\n<p><img src=\"https://aipaint.lihuanyu.com/cloudflare-r2-menu.png\" alt=\"cloudflare侧边菜单里的R2\"></p>\n<p>启用 R2 服务。需要绑定一个信用卡，不知道绑定国内的可以吗，我是用的一张附属招银信用卡的visa卡。这里开通有时候会比较慢，不清楚是我当时遇到了什么问题还是都会慢，存在界面上展示开通了但是查询桶、创建桶都失败的情况，大概等了半小时左右好了。</p>\n<p>接下来就可以创建桶了，存储服务一般都这样，为什么叫桶我也不知道，并不影响什么。取个名就是了，可以理解为文件夹名也行，反正是个人使用。</p>\n<p>建好桶后，这时候已经可以上传图片、文件之类的了，但默认这个桶是公网不可访问的。去设置里配置下公开访问-链接域：</p>\n<p><img src=\"https://aipaint.lihuanyu.com/131335a6094f019b194eb8fc3b4084f.png\" alt=\"配置公开访问\"></p>\n<p>虽然也可以用cloudflare提供的二级域名访问，但是那种方式一是有一些频率方面的限制，而是.dev的域名也被大防火墙拦截了，所以还是用自己的域名更好一些。</p>\n<p>配置完成后，上传图片，用刚刚配置的域名加上传后的文件名，即可访问到文件。比如本文的所有图片都是用的R2服务托管的。</p>\n<p>最近在玩 Stable Diffusion AI绘图，有了图床后面可以多上传一些好看的图片，不管是作为背景图还是贴合文章内容的图片抑或是单纯的美图分享，都更方便了。</p>\n<p>目前还存在一个问题，R2 默认的控制台功能比较少，就是上传、浏览已上传的图片、删除，写博客时随传随用还无所谓。但如果换成预先传好一些图，需要浏览选择图片的时候就不太方便了，不知道有没有相关的工具，可以快速浏览当前已上传的图片，一键复制地址，就更好了，要是没有的话回头写一个。</p>\n","date_published":"2023-03-12T00:00:00.000Z","tags":["图床","运维","存储"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2023/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E9%A1%B5%E9%9D%A2%E9%A1%B6%E9%83%A8%E7%9A%84%E7%A9%BA%E9%9A%99/","url":"https://www.lihuanyu.com/posts/2023/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E9%A1%B5%E9%9D%A2%E9%A1%B6%E9%83%A8%E7%9A%84%E7%A9%BA%E9%9A%99/","title":"小程序页面顶部的空隙","summary":"TL;DR：移动端web页面顶上如果有空隙的话，可以对页面父元素用 padding 或者加空元素防止因 margin 塌陷造成的不正常滚动。 起源 强迫症同学有没有注意到，很多小程序的页面，明明不超过一页，但是却可以滚，但又只能滚一点点。","content_html":"<blockquote>\n<p>TL;DR：移动端web页面顶上如果有空隙的话，可以对页面父元素用 padding 或者加空元素防止因 margin 塌陷造成的不正常滚动。</p>\n</blockquote>\n<h2>起源</h2>\n<p>强迫症同学有没有注意到，很多小程序的页面，明明不超过一页，但是却可以滚，但又只能滚一点点。</p>\n<p>比如这个：</p>\n<p><img src=\"/hexo-source/_posts/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E9%A1%B5%E9%9D%A2%E9%A1%B6%E9%83%A8%E7%9A%84%E7%A9%BA%E9%9A%99/margin-example.png\" alt=\"顶部空隙示例\"></p>\n<p>这种不符合预期的滚且只能滚一点点很难受，于是去探索到底是什么导致了这个小滚动的出现。</p>\n<p>用开发者工具去尝试找这个空隙是找不到的，但是能发现有这种表现的，无一不是最顶上的元素使用了 margin-top。</p>\n<h2>原因</h2>\n<p>在较长一段时间里，遇到这个问题的时候，会选择靠不在第一个元素上使用margin-top来规避这个问题的。</p>\n<p>有一天浏览微信开发者社区，发现也有同学有<a href=\"https://developers.weixin.qq.com/community/develop/doc/00080cc5040580a4e629d18f45ec00\">类似的问题</a>。</p>\n<p>微信小程序答疑同学表示这是： <code>margin-top 垂直方向塌陷导致的</code></p>\n<p>顺便给出了解决方案：</p>\n<pre><code class=\"language-html\">&lt;!--在第一个元素前加这样一个空元素--&gt;\n&lt;view style=&quot;content: ''; overflow: hidden;&quot;&gt;&lt;/view&gt;\n</code></pre>\n<p>试过了，很好用。但是交互强迫症满意了，代码强迫症犯了，页面最顶上要加这么个玩意儿？？？？</p>\n<p>这时可以注意到关键字，margin塌陷。搜索后才发现原来塌陷不光是曾经理解的两个元素间的margin会塌陷。元素套元素也会，看掘金的文章 - <a href=\"https://juejin.cn/post/6976272394247897101\">什么是margin塌陷及解决方案</a>。</p>\n<h2>优雅</h2>\n<p>所以其实关键是解决塌陷，加个空元素只是个手段。那有没有更优雅的手法？</p>\n<p>上面掘金的文章说可以用 <code>BFC</code> 来解决，写了好几种方法触发BFC：</p>\n<ol>\n<li>float 属性为 left / right</li>\n<li>overflow 为 hidden / scroll / auto</li>\n<li>position 为 absolute / fixed</li>\n<li>display 为 inline-block / table-cell / table-caption</li>\n</ol>\n<p>看起来 <code>overflow: auto</code> 是最安全的，给 page 加个这个能有什么坏处呢？</p>\n<p>在全局的 CSS 里给 page 元素加上这个样式，大功告成。</p>\n<h2>转折</h2>\n<p><code>overflow: auto</code> 并不是完全无害的， 加了这个会导致页面里的 <code>position: sticky</code> 失效。</p>\n<p><code>position: sticky</code> 要求父级元素不能有任何 <code>overflow:visible</code> 以外的overflow设置，否则没有粘滞效果。因为改变了滚动容器（即使没有出现滚动条）。</p>\n<p>更多细节可以看张鑫旭的文章 - <a href=\"https://www.zhangxinxu.com/wordpress/2018/12/css-position-sticky/\">position:sticky</a></p>\n<h2>结论</h2>\n<p>也许我们可以因地制宜地选择某些方法触发 BFC 来解决这个问题。但是如果需要选择，不如固定一种无害写法，虽然可能有点丑，但是能解决问题。</p>\n<p>遇到此问题时，直接在页面元素最前面加上 <code>&lt;view style=&quot;content: ''; overflow: hidden;&quot;&gt;&lt;/view&gt;</code> 来进行解决吧。</p>\n","date_published":"2023-02-19T00:00:00.000Z","tags":["前端","小程序"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2022/%E7%94%BB%E5%9B%BE%E5%B7%A5%E5%85%B7-excalidraw/","url":"https://www.lihuanyu.com/posts/2022/%E7%94%BB%E5%9B%BE%E5%B7%A5%E5%85%B7-excalidraw/","title":"画图工具-excalidraw","summary":"画图一直是我的弱项。也可能单纯是想得不够多，不够清晰，想好才能画好。 思路也许需要较长的时间去培养，但是技巧和工具可以通过练习快速掌握。最近发现了一个很不错的画图工具 excalidraw","content_html":"<p>画图一直是我的弱项。也可能单纯是想得不够多，不够清晰，想好才能画好。</p>\n<p>思路也许需要较长的时间去培养，但是技巧和工具可以通过练习快速掌握。最近发现了一个很不错的画图工具 - excalidraw</p>\n<p>在阅读 <a href=\"https://github.yanhaixiang.com/jest-tutorial/\">《Jest实践指南》</a> 一书时，发现它的配图非常好看。</p>\n<p>比如：</p>\n<p><img src=\"/hexo-source/_posts/%E7%94%BB%E5%9B%BE%E5%B7%A5%E5%85%B7-excalidraw/img.png\" alt=\"Jest实践指南中的配图\"></p>\n<p>于是好奇搜了下，找到了标题所述的工具。地址：<a href=\"https://excalidraw.com/\">https://excalidraw.com/</a></p>\n<p>甚至，这个工具是<a href=\"https://github.com/excalidraw/excalidraw\">开源</a>的，你甚至可以把源代码拿来进行私有化部署。比如在公司等商业化场景下使用时，出于数据安全方面的考虑。如果觉得维护较麻烦，也可以购买其提供的<a href=\"https://plus.excalidraw.com/plus\">增值服务</a>。</p>\n<p>有了工具后就可以开始练习了，可以先试试临摹。</p>\n<p>很快就能画出一张看起来还不错的图：</p>\n<p><img src=\"/hexo-source/_posts/%E7%94%BB%E5%9B%BE%E5%B7%A5%E5%85%B7-excalidraw/img_1.png\" alt=\"临摹的jest的图例\"></p>\n<p>一些复杂图片还可以在素材图中寻找，整体的操作也比较简单，对齐的话用网格简单对一下，反正是手绘风格，不会有对齐强迫症的存在。（PS：手绘风格和手绘看似相似，我自己拿笔画了下发现，手绘要好看，那是真的难）</p>\n<p>以及原图中，中文字体没有处理过，还是标准的电脑体，看起来有点违和，网上可以搜下更换字体的办法。</p>\n<p>好的分享到此结束。</p>\n<p>PS：Jest实践指南也是一本很不错的书，推荐看看，尤其同意它关于单测意义部分的描述。</p>\n","date_published":"2022-09-26T00:00:00.000Z","tags":["前端","画图"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2022/%E4%BA%A4%E4%BA%92%E7%9A%84%E6%84%8F%E4%B9%89/","url":"https://www.lihuanyu.com/posts/2022/%E4%BA%A4%E4%BA%92%E7%9A%84%E6%84%8F%E4%B9%89/","title":"交互的意义","summary":"生活小细节突然体会到交互的意义。 交互真是一个非常奇妙的东西，优秀的交互会让人觉得事情本该如此，平平无奇。只有当遇上糟糕的交互，才会觉得产品设计方面多么需要一个优秀的交互。","content_html":"<blockquote>\n<p>生活小细节突然体会到交互的意义。</p>\n</blockquote>\n<p>交互真是一个非常奇妙的东西，优秀的交互会让人觉得事情本该如此，平平无奇。只有当遇上糟糕的交互，才会觉得产品设计方面多么需要一个优秀的交互。</p>\n<p>众所周知，互联网公司相对于传统公司，特别喜欢搞所谓&quot;降维打击&quot;，但其实就是成本控制/广告宣传/交互设计方面做提升。</p>\n<p>大量的互联网品牌根本没有自己的工厂，只是出设计方案，找传统厂商进行生产，甚至很多还直接套用公模，贴个自己的牌子。</p>\n<p>但是我还是喜欢买这些所谓的贴牌产品，主要得益于一般设计功底在线，颜值相对较高，和简约家装相匹配。</p>\n<p>再一个是一般允许 Wi-Fi 联网，可以不受距离限制的控制设备。不过支持联网这个事情，传统厂商也在做，加一个 Wi-Fi 模块的事情，并不难，成本也不高。</p>\n<p>家里主要有小米和美的两套智能家居方案，虽然小米手机辜负了我的信任（再说一遍，辣鸡小米11！），目前已经站到苹果的队里了。有一说一，小米的交互体验真的好，至少，对于美的真的是降维打击般的存在。</p>\n<p>大晚上接到我妈的电话，问我电饭煲怎么预约煮粥。为什么问我呢？因为我曾经预约过，早上起来直接喝粥。</p>\n<p>但是我突然发现，我没法告诉她怎么预约，我都是在手机上操作的，预约操作的方式是选煮粥，选预约，然后米家会问我你想几点开饭，点确认按钮就完事了。</p>\n<p>我妈面对的却是一个充满按钮的东西，她不知道粥大概需要煮多久，预约的时间到底是开始煮的时间还是结束的时间，设好时间后就好了还是需要按开始按钮。</p>\n<p>好吧，最后的结果是，我妈可能还是吃不上预约的粥，下次回家给她换个米家电饭煲！</p>\n","date_published":"2022-09-25T00:00:00.000Z","tags":["生活","随笔"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2022/codespace%E5%88%9D%E4%BD%93%E9%AA%8C/","url":"https://www.lihuanyu.com/posts/2022/codespace%E5%88%9D%E4%BD%93%E9%AA%8C/","title":"codespace初体验","summary":"恭喜GitHub做成了真正可用的云IDE 现在我正在使用iPad编写这个内容，体验非常丝滑，唯一可能有点不足的是初次进入等场景下网络有点慢。有了这个工具，只需要记住GitHub的账号密码，真的是可以实现随时随地写点东西了。","content_html":"<blockquote>\n<p>恭喜GitHub做成了真正可用的云IDE</p>\n</blockquote>\n<p>现在我正在使用iPad编写这个内容，体验非常丝滑，唯一可能有点不足的是初次进入等场景下网络有点慢。有了这个工具，只需要记住GitHub的账号密码，真的是可以实现随时随地写点东西了。</p>\n<p>云ide不算什么新鲜的东西，但是能把开发所需的所有东西揉到一起提供出来，确实能给人全新体验，这是你没玩过的船新版本。</p>\n<p>首先就是流畅度做得非常好，和本机的vscode几乎没有区别。然后在云端安装依赖是真的快。</p>\n<p>然后接下来就是神奇的操作了，输入 <code>npm start</code> ，正常本地开发的话，会启动一个本地服务。但现在github codespace在云端，肯定不可能有本地服务了，那在哪预览页面呢？</p>\n<p>启动服务，看到控制台输出了熟悉的监听 4000 端口状态，如果在pc上，点击链接打开，会自动被拦截替换为一个github的服务地址，就可以正常预览了。如果是iPad之类没有鼠标的设备，也可以通过terminal旁边的ports面板，看到端口对应服务的链接，访问即可。</p>\n<p><img src=\"/hexo-source/_posts/codespace%E5%88%9D%E4%BD%93%E9%AA%8C/terminal.jpeg\" alt=\"terminal\"></p>\n<p>然后Git方面，提交代码/创建PR也变得更简单更一体。</p>\n<p>最重要的一个点是，从此不用担心代码没提交，无法从另一台设备上继续工作，创建的codespace相当于一台不不关机的电脑，可以随时从任意设备链接上来继续做事。</p>\n<p>目前这个服务对个人用户还是免费的，白嫖真是太开心，不知道后面会不会收费，多半会吧，财大气粗如微软应该也无法支撑全世界开发者的这样用吧。</p>\n","date_published":"2022-05-19T00:00:00.000Z","tags":["github"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2022/CSS%E4%B9%8B%E5%9B%BE%E7%89%87%E4%B8%8B%E7%9A%84%E7%A9%BA%E9%9A%99%E4%B8%8E%E6%96%87%E6%9C%AC%E5%B1%85%E4%B8%AD/","url":"https://www.lihuanyu.com/posts/2022/CSS%E4%B9%8B%E5%9B%BE%E7%89%87%E4%B8%8B%E7%9A%84%E7%A9%BA%E9%9A%99%E4%B8%8E%E6%96%87%E6%9C%AC%E5%B1%85%E4%B8%AD/","title":"CSS之图片下的空隙与文本居中","summary":"前端工程师应该都有遇到过，使用图片时会在下方有个小空隙。这个小空隙很难找到它是如何形成的，但是还好我们有搜索引擎，因此很容易会知道解决办法：","content_html":"<p>前端工程师应该都有遇到过，使用图片时会在下方有个小空隙。这个小空隙很难找到它是如何形成的，但是还好我们有搜索引擎，因此很容易会知道解决办法：</p>\n<p><img src=\"/hexo-source/_posts/CSS%E4%B9%8B%E5%9B%BE%E7%89%87%E4%B8%8B%E7%9A%84%E7%A9%BA%E9%9A%99%E4%B8%8E%E6%96%87%E6%9C%AC%E5%B1%85%E4%B8%AD/search-in-google.png\" alt=\"搜索解决图片下空隙\"></p>\n<p>其中一种技巧非常简单有效：把字体设为0。很多时候可能就到此为止了。</p>\n<h2>引子</h2>\n<p>一直没有思考过，为什么图片下面会有一个空隙。直到逛知乎刷到尤雨溪的一篇回答，关于这个空隙的，非常通俗易懂，再进入到 <a href=\"https://www.zhihu.com/question/21558138\">对应的知乎问题</a> 看到各种大佬们的分析，很有意思，做个分享。</p>\n<p>里面有一篇译文，非常全面剖析了行内元素，CSS里文字的度量、行高（line-height）。如果想深入学习细节，建议<a href=\"https://zhuanlan.zhihu.com/p/25808995\">直接前往</a> 。(PS: 这篇文章我最认可的点是结论的第一条😀)</p>\n<h2>原因</h2>\n<p>本文的原因部分可以认为是我对这些大佬回答的理解，如果没看懂我写的，可以直接去原问题浏览更多解决方案及解释。</p>\n<p>图片作为行内元素，默认的对其方式的基线对其，基线是西文字体的概念，如图：</p>\n<p><img src=\"/hexo-source/_posts/CSS%E4%B9%8B%E5%9B%BE%E7%89%87%E4%B8%8B%E7%9A%84%E7%A9%BA%E9%9A%99%E4%B8%8E%E6%96%87%E6%9C%AC%E5%B1%85%E4%B8%AD/what-is-base-line.png\" alt=\"什么是基线\"></p>\n<p>红线所示即为基线（baseline），注意看，文字的底线（bottom）和基线之间是有一段距离的，这个就是图片下有空隙的原因。</p>\n<p>再通俗一点：</p>\n<p><code>图片底部是基于文字基线的，而容器 div 的底部是低于基线的</code></p>\n<p>中文文字虽然没有基线的概念，但是也有留白区域，所以中文也有类似的问题。</p>\n<p>下面会有动手环节，提供相应代码方便有兴趣的同学可以快速自行验证。</p>\n<h2>解决方式</h2>\n<p>可以看到，既然问题是出现在文本的基线问题上。那么就围绕这一点来解决：</p>\n<p>img 设置 display:block<br>\nvertical-align:top/bottom/middle<br>\nfont-size设为 0 （注意对文本不能这么操作，手动狗头……）</p>\n<h2>衍生问题</h2>\n<p>难怪在移动端开发时，设计同学给出的设计稿，在还原后验收时，经常受到设计同学的灵魂拷问，这里怎么没居中。</p>\n<p>设计同学不知道的是，前端同学自己也很懵逼，我明明设置了line-height和高度一样，为什么就偏上偏下了？很可能就是系统下字体本身的问题。</p>\n<p>在比较小的按钮上效果会比较明显，考虑到大家的眼睛健康和视力问题，真诚地建议设计师不要追求一些“高级感”而把文字、按钮设计得过小。再结合这个问题，一定要小的话，别用边框了，也就不容易看出来。</p>\n<h2>动手试试</h2>\n<blockquote>\n<p>以下case使用codepen演示，无法加载的话可能需要科学上网。考虑到科学的门槛和速度，贴个图替代下。</p>\n</blockquote>\n<p>原始case：</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE%202023-03-12%20155733.png\" alt=\"图片的有缝、无缝情况\"></p>\n<p class=\"codepen\" data-height=\"300\" data-default-tab=\"html,result\" data-slug-hash=\"KKRVaWm\" data-user=\"sky-admin\" style=\"height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;\">\n  <span>See the Pen <a href=\"https://codepen.io/sky-admin/pen/KKRVaWm\">\n  图片下的小空隙</a> by Huanyu Li (<a href=\"https://codepen.io/sky-admin\">@sky-admin</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.</span>\n</p>\n<script async src=\"https://cpwebassets.codepen.io/assets/embed/ei.js\"></script>\n<p>文字case：</p>\n<p><img src=\"https://aipaint.lihuanyu.com/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE%202023-03-12%20160029.png\" alt=\"文字与边框间的缝隙\"></p>\n<p class=\"codepen\" data-height=\"300\" data-default-tab=\"html,result\" data-slug-hash=\"qBYbRze\" data-user=\"sky-admin\" style=\"height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;\">\n  <span>See the Pen <a href=\"https://codepen.io/sky-admin/pen/qBYbRze\">\n  Untitled</a> by Huanyu Li (<a href=\"https://codepen.io/sky-admin\">@sky-admin</a>)\n  on <a href=\"https://codepen.io\">CodePen</a>.</span>\n</p>\n<script async src=\"https://cpwebassets.codepen.io/assets/embed/ei.js\"></script>\n<p>做业务时不求甚解也许不是坏事，但是钻研深一分总有收获。</p>\n<h2>其他</h2>\n<p>img元素为什么默认是个行内元素呢？</p>\n<p>img元素是个比较早的元素，然后它本质上不是那张图，而是那个链接的占位符。于是作为占位符它默认就是个inline元素。</p>\n","date_published":"2022-03-06T00:00:00.000Z","tags":["CSS"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2022/%E5%89%8D%E7%AB%AF%E4%BE%9D%E8%B5%96%E4%B8%8E%E4%BF%A1%E4%BB%BB/","url":"https://www.lihuanyu.com/posts/2022/%E5%89%8D%E7%AB%AF%E4%BE%9D%E8%B5%96%E4%B8%8E%E4%BF%A1%E4%BB%BB/","title":"前端依赖与可信构建","summary":"背景 前端使用npm来作为包管理工具，它的使用门槛/发包门槛低到惊人，好处是培养了非常丰富的社区和庞大的第三方包，包的数量远超第二名maven。 但所有东西都会有代价，坏处是npm的包质量方面良莠不齐，依赖链非常深，很简单的小工具方法都可能去使用包。","content_html":"<h2>背景</h2>\n<p>前端使用npm来作为包管理工具，它的使用门槛/发包门槛低到惊人，好处是培养了非常丰富的社区和庞大的第三方包，包的数量远超第二名maven。</p>\n<p>但所有东西都会有代价，坏处是npm的包质量方面良莠不齐，依赖链非常深，很简单的小工具方法都可能去使用包。</p>\n<p>因此所有的现代化前端项目，都会有一个非常庞大的依赖，就有了下面这个笑话：</p>\n<p><img src=\"/hexo-source/_posts/%E5%89%8D%E7%AB%AF%E4%BE%9D%E8%B5%96%E4%B8%8E%E4%BF%A1%E4%BB%BB/node_modules-black-hole.jpg\" alt=\"比黑洞还重的是node_module\"></p>\n<p>事实上，在日常的开发工作中，很少有开发者会去关注自己的项目使用了哪些依赖。</p>\n<h2>依赖与信任</h2>\n<p>前端项目往往会通过npm包管理器去安装丰富的社区贡献的功能模块包，这些模块包对于自己的项目就叫依赖。使用依赖会带来极大的便利性，大量的功能不需要自己去亲自实现，但是也会有安全性问题，我们如何信任一个依赖包？</p>\n<p>一个依赖包，是有它的版本的，版本号都是 <code>X.Y.Z</code> 的形式。X是Major位，或者叫主版本，Y是Minor位，中文次要版本，Z是Patch位，中文修订版本。为什么要这样设计呢。</p>\n<p>简单讲，一个包发布以后，还会有持续的维护，会不停的修改迭代。修改又可以分为这几种：修复包里的bug；新增功能；大的架构升级优化。</p>\n<p>可以看出来，修bug的升级，最好每个用户都升级。新增功能，一般也不会导致之前的功能出问题，大部分用户也都可以升级。而一旦到了架构升级优化，一般就会伴随着breaking change，就是存在不能向前兼容的变化，这种升级就需要用户明确感知，谨慎操作了。</p>\n<p>因此我们在前端项目中，package.json文件里记录依赖版本的时候，一般会用 <code>^</code> 开头，这个意思就是指安装的时候会尝试安装这个主版本下的最新次要版本和修订版本。比如声明了 <code>&quot;mpxjs&quot;: &quot;^2.0.0&quot;</code> ，实际执行npm安装的时候，就会去看2这个主版本下最新的次版本和修订版，假设最新的是2.3.3，就会安装2.3.3。</p>\n<p>关于版本号语义化，详情见 <a href=\"https://semver.org/lang/zh-CN/\">此链接</a> 。</p>\n<p>OK那么到这里，能看到一些问题吗？</p>\n<p>版本号的语义化规定是这样的，但是包的开发者一定就会遵守吗？君子协定，大部分时候都是没问题的，但是面对这么大规模的生态系统，难免部分包的维护者因为失误、因为能力有限、甚至故意的情况下，往依赖包里投毒，就会导致悲剧的结果。</p>\n<p>这种case并不少见，比如最近有知名开源库 colorjs ，就是作者自己突然 <a href=\"https://github.com/Marak/colors.js/issues/285\">加入破坏性代码</a> 然后发布一个小的修订版本，所有依赖这个包的项目都遇到了问题。</p>\n<p>怎么避免这个问题呢？一般来说，商业项目的正常需求迭代，都会有测试同学测试后才会进行发布上线，测试同学的存在能很大程度避免依赖包造成的bug带来项目的bug。</p>\n<p>但是当我们只是改一个局部的小需求的时候，测试同学也只会关注局部的变化，这时如果有依赖发生变化导致其他地方产生非预期的变化，就非常容易造成故障。所以纯靠测试同学基本是无法面对依赖变化带来的故障的。</p>\n<h2>锁定依赖</h2>\n<p>那我们可以锁住依赖吗？毕竟外部的依赖太未知了，没有人可以保证依赖不会出问题。那么如果我们无视前面说的语义化版本，哪怕patch位一般是用来修bug，我们也统统无视，直接在我们的项目描述文件 <code>package.json</code> 中声明一个固定的版本号，是否就可以规避外部依赖的变化所带来的问题了呢？</p>\n<p>很遗憾也不行。目前的前端包，有非常深层次的各种依赖。比如随便打开一个项目的依赖文件夹，里面真的像是一个迷宫，充斥着各种你听说过没听说过的包，一个包里又镶嵌着几个属于它的依赖，如图：</p>\n<p><img src=\"/hexo-source/_posts/%E5%89%8D%E7%AB%AF%E4%BE%9D%E8%B5%96%E4%B8%8E%E4%BF%A1%E4%BB%BB/deps-example.png\" alt=\"乱七八糟的深层依赖\"></p>\n<p>即使你声明了你的项目的依赖都是固定版本号，不带^也不带~，你控制不了你依赖的依赖，控制不了依赖的依赖的依赖……</p>\n<h2>可靠构建</h2>\n<p>这个问题如此明显，因此很快就有 yarn 提出了 lock 方案， npm 也很快做了跟随，两者实现可能有所区别，但是原理基本是一样的，记录下安装那一次的整颗依赖树，在 lock 文件里。</p>\n<p>因此我们只要能按照 lock 文件里记录的完整依赖树去原封不动的安装依赖，问题就解决了。</p>\n<p>这个命令在 npm 这个工具下叫 <code>npm ci</code>。它不会去按版本语义化的规定去看符合规则的最新版本是多少，而是只关注lock文件里记录的，上次安装的版本是什么，这次就安装什么。</p>\n<p>这个行为太符合持续集成场景下的需要了，本地开发完成提交代码后，云端去构建，使用lock文件来安装依赖，保证和你本地的依赖完全一致。反复执行构建部署，出来的产物完全一致。</p>\n<p><strong>因此给使用 lock 文件来进行构建的这个过程叫可靠构建。</strong></p>\n<p>OK那么问题就完美解决了吗？并没有。再考虑下这个问题，你本地开发时应该执行 <code>npm i</code> 还是 <code>npm ci</code> 来安装依赖呢？</p>\n<p>如果执行 <code>npm ci</code> ，依赖始终不变，所有包都停止升级，那么包自生的一些后发现的缺陷就得不到修复，一些新能力也无法使用。</p>\n<p>如果执行 <code>npm i</code> ，依赖一变化，lock文件会发生大变的，这时候也很难去仔细review每一个依赖包的变化，就和没有lock时候没什么区别了？</p>\n<p>好的不卖关子了，世界不是非黑即白的，如果我们结合这两者来用，就可以取得很好的效果。</p>\n<h2>具体实践</h2>\n<blockquote>\n<p>简单点说就是开发时候尽量用 npm ci 来安装依赖。只有负责同学或者明确知道自己要升级某个依赖才执行 npm i ，因为之前都尽量用 ci 来安装了，这时的 npm i 所产生的 package-lock.json 的 diff 是非常可阅读的，代码CR时就可以关注 lock 文件的变化。</p>\n</blockquote>\n<p>对于一个前端项目，使用足够新的npm，如果是使用某些脚手架工具生成的项目，第一次可以直接执行 <code>npm i</code> ，安装依赖的同时会生成一个 <code>package-lock.json</code> 的文件（后简称lock文件）。在 <code>git-ignore</code> 文件里忽略 node_modules 目录，但不要忽略lock文件，即我们需要把lock文件提交到 git 里（这个在npm的文档里也可以看到是推荐的行为）。</p>\n<p>接下来可以正常的开发提交代码。在这一次迭代中，<strong>当还有其他的合作同学跟你共同开发，或是你换了一台电脑或者你因为某些原因需要重新安装依赖时，使用 <code>npm ci</code> 而不是 <code>npm i</code> 来安装依赖</strong>。</p>\n<p>这样就可以使前后都是完全相同的依赖。（如果使用 <code>npm i</code> 会因为新电脑的 node 环境、网络环境、npm registry 配置等等因素，导致装出一份符合 package.json 但 lock 差异巨大的依赖库）</p>\n<p>OK初次开发完成后，质量保障同学会进行全面的测试回归，没有问题后完成了第一次发布。接下来来到第二次需求迭代。</p>\n<p>// 未完待续</p>\n<h2>扩展阅读</h2>\n<h3>分享起源</h3>\n<p>我想分享这个内容，是因为看到了这篇文章：<a href=\"https://www.jackfranklin.co.uk/blog/check-in-your-node-dependencies/\">为什么我推荐你提交node_modules</a></p>\n<p>乍一看有点想笑，居然推荐大家把 node_modules 提交到 Git 里，结果一看作者是 Chrome 开发团队的，再仔细读下内容，也挺有道理的，真是看不同的开发场景，不同的团队和基础设施吧。</p>\n<p>为什么他要推荐大家把 node_modules 提交到 Git 呢？我简单总结下他的观点：</p>\n<ol>\n<li>你就不需要安装依赖了（哈哈😀）</li>\n<li>反复构建就不会不一致了</li>\n<li>更好了解你要交付的代码（应该是指你能更好感知你的依赖库的规模）</li>\n<li>会更谨慎地添加依赖（属于3的增强，以前可能觉得依赖是透明的缺乏关注）</li>\n<li>便于管理大的差异diff（说实话我不理解这是什么意思）</li>\n<li>避免依赖攻击</li>\n</ol>\n<p>其实可以看到，他担心的问题也是我所担心的，大家考虑的地方挺大同小异的，我上面提出的方案也可以很好解决这些问题。</p>\n<h3>其他思路</h3>\n<p>除了本文上述方式来解决项目中的依赖管理问题，还有不少前辈走出了不一样的路。比如vite/umi等框架提出的依赖预打包方案。</p>\n","date_published":"2022-01-11T00:00:00.000Z","tags":["前端","js","npm"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2021/21%E5%B9%B4%E9%9A%8F%E7%AC%94/","url":"https://www.lihuanyu.com/posts/2021/21%E5%B9%B4%E9%9A%8F%E7%AC%94/","title":"21年随笔","summary":"21年快要过完了，再不写点什么，可真就什么都没写了。水一篇，记录生活。","content_html":"<blockquote>\n<p>21年快要过完了，再不写点什么，可真就什么都没写了。水一篇，记录生活。</p>\n</blockquote>\n<h2>城市</h2>\n<p>20年10月换了工作，辞别待了4年的北京，回到了成都。</p>\n<p>21年一整年，几乎都是在成都度过的。</p>\n<p>一切看起来没什么区别，又有很多说不清的不同。</p>\n<p>照样繁忙，甚至可以说更繁忙的工作，工作日基本天天两点一线，早10晚9，和在北京，别无二致。工作的地点倒是相对繁华了不少，从“穷乡僻壤”西二旗改到了高楼遍地的天府X街。（然鹅了解下两边的房价，就知道谁才是真正的穷乡僻壤了 :) ）</p>\n<p>物价/吃饭等方面，其实也和北京差不多，成都的南门，确实不太像成都。</p>\n<p>但是吃穿住行，后面三个还是有不同的。</p>\n<p>北京的房租，一个开间一个月5500，成都一个两居室一个月只要2600。</p>\n<p>北京打车，动辄30，打个百来块钱的车，非常正常。成都打车，很少能超过30。</p>\n<h2>装修</h2>\n<p>回家后安排了装修，差不多小半年的时间完成了装修，一切从简。less is more是真的，简单的才是最耐看最好打理最好维护的，就像代码，越短，bug越少，哈。</p>\n<p>此处应该有图？没认真拍过满意的，先占位吧。</p>\n<h2>新家</h2>\n<p>所以，在搞定装修后，再散味3个月左右。</p>\n<p>是的，住进自家的房子啦。开心。</p>\n<p>难道这就是所谓的归属感？可能吧。</p>\n<h2>疫情</h2>\n<p>21年一整年，新冠疫情仍然笼罩于世界。</p>\n<p>去哪都不太方便，主要就去了两次杭州出差，去了一次西双版纳团建。杭州也是个大工地，到处挖得破破烂烂的，修地铁修路。早期城市规划人员恐怕想都不敢想现在城市的规模/状态。西双版纳有点意思，景色很棒，尤其是夜景和当地的特色服装。</p>\n<p>21年初的春节，很多地方倡导/要求就地过年，回到成都在这种背景下显得很明智，跟父母近了很多。希望今年的春节，团聚的人能多一些。</p>\n<h2>行业</h2>\n<p>21年的前端技术，感觉确实没那么精彩了，都在搞一些修修补补，贴近业务的一些优化。</p>\n<p>同时能明显感觉到经济大环境在恶化，退守二线城市也是希望在这个充满不确定的世界中不要翻车。</p>\n<p>年底了，中概互联还在跌跌不休，裁员的消息一波接一波，各路互联网公司还在消减福利。也不知道未来如何发展。</p>\n<p>不过互联网还算好的，教培行业直接欢声笑语中打出GG。</p>\n<h2>技术</h2>\n<p>感觉有点懈怠，这一年无所长进。也许是回家，要处理的生活事务太多了吧。</p>\n<p>不过很有意思，服务器里托管着我的blog和mpxjs的文档，一年多的时间，几乎没有出过任何问题。</p>\n<p>我的blog可能没有更新不算什么有难度的事情，但mpxjs的文档是在持续迭代的。</p>\n<p>仅仅靠着GitHub Action与certbot的自动脚本，就能持续保证文档的更新，很有意思。</p>\n<h2>图书</h2>\n<p>我不是一个爱读书的人，之前的很多技术书籍，都没有认真看过，可能就翻了一两页吧。于是后来也不怎么买书了。</p>\n<p>回到成都后有了充足的书房空间，开始搞一些奇奇怪怪的书籍，比如《中国是部金融史》、《高效人士的秘诀》。前一本是历史书还挺有意思的，历史确实像是在螺旋中前进，前人犯过的错，后人也要跟着踩一遍坑。</p>\n<p>技术书籍买了一本《JavaScript悟道》，文笔很有意思，希望这次能读完。</p>\n<h2>手机</h2>\n<p>用了N年的小米，换成了苹果。</p>\n<p>契机主要是有两个。一是后续考虑买车用车，carplay有显著的优势。二是小米11的火龙888烧了WiFi，并且小米的解决方案令我非常不满意，有欺骗消费者的嫌疑。so，用脚投票。</p>\n<p>说实话，换过来发现在某些使用体验方面，小米真的做得超级棒。iPhone的一些设计，很奇怪，很反直觉。比如侧滑返回，苹果是做不到一直返回的。再比如一些本土化功能，也非常不贴心。</p>\n<p>但反过来，小米确实不配做高端机。半年后价格大跳水，没有核心科技。机器稳定性很差，我是一个用东西很爱惜的人，所以多年使用小米并觉得很好用，但从不敢向不太熟悉的朋友推荐小米，因为真的容易用坏。电池不耐用，系统套路多，如果你没有一点geek精神，会拿到一部广告机。</p>\n<p>苹果现在用下来比较舒服的地方，拍照尤其是拍人，很好看。电池非常耐用，终于明白之前用iPhone的朋友们动不动掏出只有20%不到的电的手机还一点不慌是怎么回事了。以及，不用曲面屏，真的是个加分项。</p>\n<h2>one more thing</h2>\n<p>领证了，以后也是有家室的人了。希望未来能经营好我们的小家庭。</p>\n<p>21年再见，22年你好。</p>\n","date_published":"2021-12-30T00:00:00.000Z","tags":["生活","随笔"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2020/%E6%BB%B4%E6%BB%B4%E5%87%BA%E8%A1%8C%E5%B0%8F%E7%A8%8B%E5%BA%8FI18n%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/","url":"https://www.lihuanyu.com/posts/2020/%E6%BB%B4%E6%BB%B4%E5%87%BA%E8%A1%8C%E5%B0%8F%E7%A8%8B%E5%BA%8FI18n%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/","title":"滴滴出行小程序I18n最佳实践","summary":"背景 I18n = Internationalization，国际化，因为单词由首末字符i/n和中间18个字母组成，简称i18n。对程序来说，就是要在不修改内部代码的情况下，能根据不同语言及地区显示相应的界面，以支持不同语言的人顺利使用程序。 业务背景 互联网行业进入下半场，精细化运营是关键。多语言支持能让产品更好地服务境内的其他语言用户，也为产品出海打下基","content_html":"<h2>背景</h2>\n<blockquote>\n<p>I18n = Internationalization，国际化，因为单词由首末字符i/n和中间18个字母组成，简称i18n。对程序来说，就是要在不修改内部代码的情况下，能根据不同语言及地区显示相应的界面，以支持不同语言的人顺利使用程序。</p>\n</blockquote>\n<h3>业务背景</h3>\n<p>互联网行业进入下半场，精细化运营是关键。多语言支持能让产品更好地服务境内的其他语言用户，也为产品出海打下基础，随着 WeChat/Alipay 的全球化，你的小程序是否做好准备了呢？</p>\n<p>4月初，滴滴出行小程序团队接到支持英文版的需求，预计上线时间为6月上旬。当前滴滴出行小程序集成的众多业务线和各种公共库，展示给用户的有前端硬编码的静态文本和服务端下发的文案，都要同步接入多语言。考虑到小程序当前的体量，光文本收集、语料翻译、npm package 支持，联调，测试，沟通成本等等，并且前端开发只投入1.5人力的情况下，时间是蛮紧迫的，但是我们抗住了压力，最终英文版滴滴出行小程序如期上线，截止目前运行稳定，用户反馈良好，得到了超出预期的收益。</p>\n<p>当然这一切得益于各团队同学的高效工作，和各团队的通力配合，更得益于部门技术团队 Mpx框架优雅的多语言能力支持。划重点来咯，所谓工欲善其事必先利其器，如果你的公司业务需要开发小程序，也需要接入多语言，那么请搬好小板凳，我们来看一下小程序框架 Mpx 是如何优雅支持多语言能力。相信看完这篇，可以帮助你认识 Mpx(<a href=\"https://github.com/didi/mpx\">https://github.com/didi/mpx</a>) ，加深对框架的理解，最终利用 Mpx 框架高效迭代小程序，年终奖多出那部分可以打赏一下作者，买杯咖啡哈（偷笑.jpg）</p>\n<p>以下是滴滴出行小程序的中英文版本对比：</p>\n<p><img src=\"https://dpubstatic.udache.com/static/dpubimg/F3Ev6M08X7/clipboard_image_1599113935176.png\" alt=\"滴滴出行微信小程序i18n\"></p>\n<p>也欢迎大家在微信/支付宝里搜索滴滴出行小程序，实际使用感受下。PS：切换语言的方法是，打开小程序，点击左上角用户头像，进入侧边栏设置页面，点击切换中英文即可体验。</p>\n<h3>技术背景</h3>\n<p>在上述业务背景下，Mpx 框架——滴滴自研的专注提升小程序开发体验的增强型小程序框架，内建 i18n 能力便提上日程。</p>\n<p>与 WEB 不同，小程序（本文以微信小程序为例）运行环境采用双线程架构设计，渲染层的界面使用 WebView 进行渲染，逻辑层采用 JSCore 线程运行 JS脚本。逻辑层数据改变，通过 setData 将数据转发到 Native（微信客户端），Native 再将数据转发到渲染层，以此更新页面。由于线程间通信成本较高，实际项目开发时需要控制频次和数量。另外小程序的渲染层不支持运行 JS ，一些如事件处理等操作无法在渲染层实现，因此微信官方提供了一套脚本语言 WXS ，结合 WXML ，可以构建出页面的结构（不了解 WXS ？戳这里）。</p>\n<p>基于小程序的双线程架构设计，实现 i18n 存在一些技术上的难点与挑战，由于 Mpx 框架早期构建起来的强大基础，最终得以优雅支持多语言能力，实现了和vue-i18n 基本一致的使用体验。</p>\n<p>##使用</p>\n<p>在使用上，Mpx 支持 i18n 能力提供的 API 与 vue-i18n 大体对齐，用法上也基本一致。</p>\n<h3>模板中使用 i18n</h3>\n<p>编译阶段通过用户配置的 i18n 字典，结合框架内建的翻译函数通过 wxs-i18n-loader 合成为可执行的 WXS 翻译函数，并自动注入到有翻译函数调用的模板中，具体调用方式如下图。</p>\n<pre><code class=\"language-html\">// mpx文件\n&lt;template&gt;\n  &lt;view&gt;\n    &lt;view&gt;{{ $t('message.hello', { msg: 'hello' }）}}&lt;/view&gt;\n    &lt;!-- formattedDatetime计算属性，可基于locale变更响应刷新 --&gt;\n    &lt;view&gt;{{formattedDatetime}}&lt;/view&gt;\n  &lt;/view&gt;\n&lt;/template&gt;\n</code></pre>\n<h3>JS 中使用 i18n</h3>\n<p>通过框架提供的 wxs2js 能力，将 WXS 翻译函数转换为 JS 模块注入到 JS 运行时，使运行时环境中也能够调用翻译函数。</p>\n<pre><code class=\"language-html\">// mpx文件\n&lt;script&gt;\n  import mpx, { createComponent } from '@mpxjs/core'\n  createComponent({\n    ready () {\n      // js中使用\n      console.log(this.$t('message.hello', { msg: 'hello' }))\n      // 局部locale变更，生效范围为当前组件内\n      this.$i18n.locale = 'en-US'\n      setTimeout(() =&gt; {\n        // 全局locale变更，生效范围为项目全局\n        mpx.i18n.locale = 'zh-CN'\n      }, 10000)\n    },\n    computed: {\n      formattedDatetime () {\n        return this.$d(new Date(), 'long')\n      }\n    }\n  })\n&lt;/script&gt;\n</code></pre>\n<h3>定义 i18n 字典</h3>\n<p>项目构建时传入 i18n 配置对象，主要包括语言字典和默认语言类型。</p>\n<pre><code class=\"language-js\">new MpxWebpackPlugin({\n  i18n: {\n    locale: 'en-US',\n    // messages既可以通过对象字面量传入，也可以通过messagesPath指定一个js模块路径，在该模块中定义配置并导出，dateTimeFormats/dateTimeFormatsPath和numberFormats/numberFormatsPath同理\n    messages: {\n      'en-US': {\n        message: {\n          hello: '{msg} world'\n        }\n      },\n      'zh-CN': {\n        message: {\n          hello: '{msg} 世界'\n        }\n      }\n    },\n    // messagesPath: path.resolve(__dirname, '../src/i18n.js')\n  }\n})\n</code></pre>\n<p>如果是通过 Mpx 提供的 cli 工具生成的项目，这部分配置会在 mpx.conf.js 文件中，不光可以直接内联写在该文件中，也可以指定语言包的路径。</p>\n<p>以上，Mpx 的 i18n 方案接入成本低，使用优雅，体验优秀。直观感受可参考下面 mpx i18n demo ：<a href=\"https://github.com/didi/mpx/tree/master/examples/mpx-i18n\">https://github.com/didi/mpx/tree/master/examples/mpx-i18n</a></p>\n<h2>方案</h2>\n<p>Mpx框架的 i18n 支持几乎完全实现了 vue-i18n 的全部能力，下面我们来详细说明 Mpx 框架 i18n 能力的具体实现。</p>\n<h3>方案探索</h3>\n<p>基于小程序运行环境的双线程架构，我们尝试了不同方案，具体探索过程如下：</p>\n<p>方案一：基于 Mpx 框架已提供的数据增强能力 computed 计算属性，来支持 i18n 。该方案与 uniapp 的实现思路相似（后文会进行对比分析），存在一定不足，包括线程通信带来的性能开销和for循环场景下的处理较复杂等，最终放弃。<br>\n方案二：基于 WXS + JS 支持 i18n 适配。通过视图层注入 WXS，将 WXS 语法转换为 JS 后注入到逻辑层，这样视图层和逻辑层均可实现 i18n 适配，并且在一定程度上有效减少两个线程间的通信耗时，提高性能。</p>\n<p>从性能和合理性上考虑，我们最终采用了方案二进行 Mpx 的 i18n 方案实现。</p>\n<p><img src=\"https://dpubstatic.udache.com/static/dpubimg/f626347f-7501-46e8-9e98-5c961a5b2b3e.png\" alt=\"mpx-i18n内部流程示意图\"></p>\n<p><em>Mpx i18n 架构设计图</em></p>\n<p>由于各大小程序平台上，WXS 语法和使用均存在较大差异，因此该方案实现过程中也存在一些技术上的难点，这些难点基于 Mpx 框架的早期构建起来的跨平台能力也一一得以攻克，具体如下。</p>\n<h3>实现难点</h3>\n<h4>WXS 在模板中运行的跨平台处理</h4>\n<p>WXS 是运行在视图层中的 JS，可以减少与逻辑层通信耗时，提高性能。因此 Mpx 框架在迭代初期便已支持在模板和 JS 运行环境使用 WXS 语言，并且针对小程序跨平台 WXS 语法进行抹平。\n在模板中，Mpx 自定义一个 webpack chunk template，以微信 WXS 作为 DSL，利用 babylon 将注入的 WXS 转化成 ast，然后遍历 ast 节点，抹平各大平台对 WXS 语法的处理差异，输出各平台可以识别的类 WXS 文件。目前主要支持微信(WXS)、支付宝(sjs)、百度(filter)、QQ(qs)、头条(sjs)等小程序平台。</p>\n<h4>WXS 在逻辑层运行的跨平台处理</h4>\n<p>WXS 与 JavaScript 是不同的语言，有自己的语法，并不和 JavaScript 一致。并且 WXS 的运行环境和其他 JavaScript 代码是隔离的，WXS 中不能调用其他 JavaScript 文件中定义的函数，也不能调用小程序提供的API。\n因此在逻辑层，Mpx 将注入的 WXS 语法转化为 JS，通过 webpack 注入到当前模块。例如 WXS 全局方法 getRegExp/getDate 在 JS 中是无法调用的，Mpx将它们分别转化成 JS 模块，再通过 webpack addVariable 将模块注入到 bundle.js 中。\n同理，Mpx 会将编译时注入的 i18n wxs 翻译函数和 i18n 配置对象挂载到全局 global 对象上，利用 mixin 混入到页面组件，并监听 i18n 配置对象，这样JS和模板中即可直接调用 i18n 翻译函数，实现数据响应。</p>\n<p>以上便是 Mpx 框架在小程序中支持 i18n 能力的技术细节，由于 WXS 是可以在视图层执行的类 JS 语法的一门语言，这样就减少了小程序逻辑层和视图层的通信耗时，提升性能。但是由于实现依赖类 WXS 能力，以及 WXS 执行环境的限制，目前模板上可直接使用的翻译函数包括 <code>$t/$tc/$te</code> ，如果需要格式化数字或日期可以使用对应的翻译函数在 JS 中 Mpx 提供的计算属性中实现。</p>\n<h3>输出 web 时使用 i18n</h3>\n<p>Mpx同时还支持转换产出H5，而 Mpx 提供的 i18n 能力在使用上与 vue-i18n 基本一致，输出 web 时框架会自动引入 vue-i18n，并使用当前的 Mpx i18n 配置信息对其进行初始化，用户无需进行任何更改，即可输出和小程序表现完全一致的 i18n web 项目。</p>\n<h2>对比</h2>\n<p>上面分析了 Mpx 框架的 i18n 方案的技术细节，我们来看下和其他方案的对比，主要是和 uniapp - 基于 Vue 编写小程序的方案，和微信官方的方案，两者提供的 i18n 支持与Mpx的对比有何优劣。</p>\n<h3>uniapp的方案</h3>\n<p>uniapp 提供了对 i18n 能力的支持，是直接引入vue-i18n。但小程序中无法在模板上调用 JS 方法，本质上是利用计算属性 Computed 转换好语言，然后利用模板插值在小程序模板中使用。</p>\n<p>模板中：\n<code>&lt;view&gt;{{ message.hello }}&lt;/view&gt;</code></p>\n<p>JS里需要写：</p>\n<pre><code class=\"language-js\">  computed: {  \n    message () {  \n      return { hello: this.$t('message.hello') }\n    }\n  }\n</code></pre>\n<p>因此该方案存在一个性能问题，最终的渲染层所看到的文本还是通过 setData 跨线程通信完成，这样就会导致线程间通信增多，性能开销较大。</p>\n<p>并且，早期这种形式使用成本较高，后来 uniapp 也针对其做过优化，实现了可以在模板上写 $t() 的能力，使用上方便了不少。</p>\n<p>这个 $t() 的实现是在编译时候识别到 $t 就自动替换，帮你替换成一个 uniapp 的 computed 数据，因此数据部分还是和之前一样要维护两份。尤其是模板上的for循环，即使 for 里只有一个数据要被转换，整个列表都要被替换成一个计算属性，在线程间通信时进一步加大了性能开销。</p>\n<h3>微信官方的方案</h3>\n<p>微信小程序本身也提供了一个 i18n 的方案，仓库地址是：wechat-miniprogram/miniprogram-i18n 。</p>\n<p>这个方案从 i18n 本身的实现来讲和Mpx框架的设计是类似的，也是基于 WXS 实现（英雄所见略同啊）。但因为周边配套上没有完整的体系，整体使用体验上就也略逊于基于Mpx框架来开发支持 i18n 的国际化小程序了。</p>\n<p>主要的点就是，官方提供的方案，要基于 gulp 工具进行一次额外构建，同时在JS中使用时候还要额外引入一个 behavior 去让JS中也可以使用翻译能力。</p>\n<p>而Mpx框架通过一次统一的Webpack构建产出完整的内容，用户无需担心语言包更新后忘记重新构建，在JS中使用的时候不光更方便，而且语言信息还是个响应式的，任何组件都可以很方便地监听语言值的变化去做一些其他的事情。</p>\n<p>最后，Mpx的 i18n 方案对比微信官方的方案还有个巨大的优点，结合Mpx的跨平台能力，能实现均以这个方案，一套代码产出支持微信/支付宝/百度/QQ/头条多个平台的支持 i18n 的小程序。</p>\n<h2>总结</h2>\n<p>Mpx 框架专注小程序开发，期望为开发者提供最舒适的开发体验，有众多优秀的功能特性，帮助开发者提效。本文介绍的是其内置的 i18n 能力，通过对比分析得出相比其他框架方案在使用成本和性能等方面有明显的优势，欢迎各位有相关需求的同学进行体验尝试。</p>\n<p>未来 Mpx 还会持续迭代优化，提供更多更好的能力帮助小程序开发者提效。在使用过程中遇到任何问题，欢迎大家在 Git 上提 issue，团队成员会及时响应。同时也鼓励大家一起为开源社区做贡献，参与到 Mpx 共建中来，为小程序技术发展添砖加瓦。</p>\n<p>Git地址 [<a href=\"https://github.com/didi/mpx\">https://github.com/didi/mpx</a>]<br>\nMpx文档 [<a href=\"https://mpxjs.cn/\">https://mpxjs.cn/</a>]</p>\n<p>欢迎技术交流与反馈，顺便star一下鼓励开源项目贡献者，我们将持续发力贡献社区。</p>\n<p>附：以往Mpx文章链接<br>\n滴滴开源小程序框架Mpx - <a href=\"https://mpxjs.cn/articles/1.0.html\">https://mpxjs.cn/articles/1.0.html</a><br>\n滴滴小程序框架Mpx发布2.0，支持小程序跨平台开发，可直接转换已有微信小程序 - <a href=\"https://mpxjs.cn/articles/2.0.html\">https://mpxjs.cn/articles/2.0.html</a><br>\n小程序开发者，为什么你应该尝试下MPX - <a href=\"https://mpxjs.cn/articles/mpx1.html\">https://mpxjs.cn/articles/mpx1.html</a><br>\nMpx 小程序框架技术揭秘 - <a href=\"https://mpxjs.cn/articles/mpx2.html\">https://mpxjs.cn/articles/mpx2.html</a><br>\n滴滴出行小程序体积优化实践 - <a href=\"https://mpxjs.cn/articles/size-control.html\">https://mpxjs.cn/articles/size-control.html</a></p>\n","date_published":"2020-08-31T00:00:00.000Z","tags":["小程序"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2020/%E4%BB%8ETravis%E8%BF%81%E7%A7%BB%E5%88%B0GitHub-Actions/","url":"https://www.lihuanyu.com/posts/2020/%E4%BB%8ETravis%E8%BF%81%E7%A7%BB%E5%88%B0GitHub-Actions/","title":"从Travis迁移到GitHub-Actions","summary":"背景 最近将blog和mpx template都从Travis迁移成GitHub Actions了，两者都是持续集成工具。 为什么迁移原因也比较简单，Travis不够稳定，经常挂掉。而GitHub Actions有着更好的稳定性/性能，且整合集成得也更好。","content_html":"<h2>背景</h2>\n<p>最近将blog和mpx-template都从Travis迁移成GitHub-Actions了，两者都是持续集成工具。</p>\n<p>为什么迁移原因也比较简单，Travis不够稳定，经常挂掉。而GitHub Actions有着更好的稳定性/性能，且整合集成得也更好。</p>\n<p>此外Travis没有免费版提供给私有仓库使用，而GitHub在卖身给巨硬后明显财大气粗，宣布了私有仓库也免费使用了。</p>\n<p>且GitHub Actions支持私有仓库。于是我的blog，本来是全开源的，终于可以藏起一些东西，只部署最终的结果。</p>\n<h2>使用</h2>\n<p>Actions刚出来时候就有了解过它，当时感觉用起来好像比Travis要麻烦很多。虽然已经淘汰了Travis，但不得不夸赞一下Travis配置的简洁，入门极其友好。如果和jenkins一样，说不定一开始我就不会对持续集成感兴趣。</p>\n<p>但它比较有意思的一点是，提供了持续集成过程中各种“action”的复用的可能，并且在此基础上允许用户进行各种“action”的组合。</p>\n<p>一直以来，CI/CD中的能力复用就是个有点尴尬的问题。比如要做lint-test-build-deploy这样一个流程，在一个项目中搭建完成后，要迁移到另一个项目，需要复制流水线的配置文件过去。</p>\n<h2>效果</h2>\n<p>本Blog就是一个很好的示例，源码仓库已经被隐藏起来了，每次提交都会通过GitHub Action同步部署到GitHub Pages服务和自维护的服务器上。</p>\n<p>并不方便以本blog来举例讲解GitHub Action的使用，不过我这里还有一些开源项目上的应用示例：</p>\n<h3>Mpx</h3>\n<p>这个是滴滴出行网约车前端团队出品的应用层小程序框架，<a href=\"https://github.com/didi/mpx\">Github地址</a> 。</p>\n<p>一共配置了 测试/发包/文档 三种 GitHub Actions ，详情可见仓库里的 <code>.github文件夹</code> 。</p>\n<p>这里讲讲它的发包和文档两块，看看 Github Actions 为一个开源项目带来了什么改变呢。</p>\n<h4>测试和发包</h4>\n<p>首先是部署发包，作为一个规范的开源项目，发包得是个标准的流程，不能随便乱发版本，不能发出有问题的包是个基本要求。</p>\n<p>因此需要在发包前走一遍lint/unit，当然这个事情可以靠脚本达成，只要要求大家都通过脚本发包就可以了。但是我在编程这个事情上一直一来有一个思路，就是不要相信人，所有靠规章制度来约束人来操作的事情，都有出错的可能性，而且不人道，谁愿意被约束呢。</p>\n<p>再加上大家环境不一，发包还需要权限，给每个人都加又太麻烦，只给一个人加这个人岂不是成发包工程师了？</p>\n<p>因此提交代码后在云端做是最好的，工程师只需要关注编码并提交就好了。</p>\n<p>代码提交后，先通过lint和unit，确认代码没有明显的低级错误。这个部分的配置见代码：<a href=\"https://github.com/didi/mpx/blob/master/.github/workflows/test.yml\">https://github.com/didi/mpx/blob/master/.github/workflows/test.yml</a></p>\n<p>核心就是指定个操作系统，安装依赖，执行相应的npm script。</p>\n<p>然后发包，任何有这个仓库的github权限的同学都可以在本地执行 npm run release，会询问你打算发major/minor/patch类型的包来确定版本号，会确认你要发的包（mpx是一个mono repo，里面有很多包，每次发布只会发布有改动的包）和版本。</p>\n<p>配置的内容在这里：<a href=\"https://github.com/didi/mpx/blob/master/.github/workflows/publish.yml\">https://github.com/didi/mpx/blob/master/.github/workflows/publish.yml</a></p>\n<p>这个部分写得比较早，还是自己撸的发布相关的脚本/配置，好处是可控性极高，干了什么一目了然。坏处是居然需要自己写，而且后来人的维护也不方便，谁没事琢磨咋配置发包。如果现在再让我配这种东西，我一定会直接去GitHub action的市场里搜别人配置好的工具，直接使用。</p>\n<h4>文档</h4>\n<p>一个开源项目很重要的部分就是它的文档了，早期的文档是用gitbook编写的，然后自动部署到GitHub Pages上面。</p>\n<p>现在重构成使用vuepress编写，不过用什么写不是重点，重点是要自动部署，最大程度降低文档的编写成本，让所有人都可以方便地帮助我们一起维护文档。</p>\n<p>Github Action 在云端生成文档很简单，和上面的lint/unit没什么区别，但是自动部署这里就很舒服了：</p>\n<pre><code class=\"language-yaml\">name: docs\n\non:\n  push:\n    branches:\n      - master\n\njobs:\n  deploy:\n    runs-on: macos-latest\n    steps:\n      - uses: actions/checkout@v2\n\n      - name: Use node v12\n        uses: actions/setup-node@v1\n        with:\n          node-version: '10.21.0' # 指定了node版本用于解决一个gitbook的bug\n\n      - name: generate static file\n        run: |\n          cd docs\n          npx gitbook-cli install\n          npx gitbook-cli build\n      - name: Deploy\n        uses: peaceiris/actions-gh-pages@v3 # 注意看这里\n        with:\n          publish_dir: ./docs/_book\n          force_orphan: true\n          personal_token: ${{ secrets.GH_PAGES_TOKEN }}\n</code></pre>\n<p>注意看 steps 的最后一项“Deploy”，是直接 uses peaceiris/actions-gh-pages@v3，通过with来传递一些参数，声明要部署的文件在哪里，部署用的personal token是什么，这个token是需要保密的，要在项目的 <code>Settings -   Secrets</code> 里设置，设后没有人可以看，后面要修改也只能是直接填新的，看不到之前填的老的。</p>\n<h3>Mpx-template</h3>\n<p>这个是 <a href=\"https://github.com/didi/mpx\">Mpx 小程序框架</a> 所配套的脚手架模板项目，<a href=\"https://github.com/mpx-ecology/mpx-template\">Github地址</a> 。</p>\n<p>这个项目在对Github Action的应用上略微有一点特别。</p>\n<p>模板需要关注的是，稳定可靠，不要生成出来的项目上来就无法运行。早期没有保障，人虽然很谨慎了，还是容易写出bug，这种东西一旦出bug，会导致新用户上来就遭遇失败，好心的可能跟你说一声，大部分可能直接就把这个框架拉黑了。</p>\n<p>因此后来给项目补充了一些相关的自动化测试脚本，主要是测试在不同的场景下生成的对应项目是否都能做最基础的构建打包，打包后一些基础的文件是否有生成。</p>\n<p>效果很好的，拦截了不少失误的场景。但是后来还是出过问题，因为它在Linux下没问题，但是在Windows下就出错了。</p>\n<p>然后发现 GitHub 现在真是财大气粗，Travis 完全没法比较，Travis也可以选mac环境，Windows至少我当时没法选，而且会很慢。</p>\n<p>直接看配置吧：</p>\n<pre><code class=\"language-yaml\">\nname: test\n\non: [push]\n\njobs:\n  build:\n    name: Test on node ${{ matrix.node }} and ${{ matrix.os }}\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      matrix:\n        os: [macos-latest, windows-latest, ubuntu-latest]\n        node: [10, 12, 14]\n\n    steps:\n      - uses: actions/checkout@v1\n      - uses: actions/setup-node@v1\n# 后续省略\n</code></pre>\n<p>strategy下面的matrix，我指定了三种平台，三种node版本，GitHub Actions 在执行的时候，就会两两组合把九种场景全部验一遍。</p>\n<p><img src=\"/hexo-source/_posts/%E4%BB%8ETravis%E8%BF%81%E7%A7%BB%E5%88%B0GitHub-Actions/template-test-job.png\" alt=\"模板测试任务\"></p>\n<p>以上就是我对公网开源项目在持续集成方面，将原有的 Travis 迁移为 Github Actions 的一些实践。希望对读者有一些启发，也欢迎交流。</p>\n","date_published":"2020-06-21T00:00:00.000Z","tags":["持续集成"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2020/%E6%BB%B4%E6%BB%B4%E5%87%BA%E8%A1%8C%E5%B0%8F%E7%A8%8B%E5%BA%8F%E4%BD%93%E7%A7%AF%E4%BC%98%E5%8C%96%E5%AE%9E%E8%B7%B5/","url":"https://www.lihuanyu.com/posts/2020/%E6%BB%B4%E6%BB%B4%E5%87%BA%E8%A1%8C%E5%B0%8F%E7%A8%8B%E5%BA%8F%E4%BD%93%E7%A7%AF%E4%BC%98%E5%8C%96%E5%AE%9E%E8%B7%B5/","title":"滴滴出行小程序体积优化实践","summary":"概述 2019年下半年，为了将微信钱包/支付宝九宫格入口的滴滴出行迁移为小程序，团队对小程序进行了大量的功能升级与补全。在整个过程中也遇到并克服了一系列问题和挑战，其中包体积问题尤为突出。接下来全面介绍一下滴滴出行小程序在体积控制方面做的努力与沉淀。","content_html":"<h2>概述</h2>\n<p>2019年下半年，为了将微信钱包/支付宝九宫格入口的滴滴出行迁移为小程序，团队对小程序进行了大量的功能升级与补全。在整个过程中也遇到并克服了一系列问题和挑战，其中包体积问题尤为突出。接下来全面介绍一下滴滴出行小程序在体积控制方面做的努力与沉淀。</p>\n<h2>背景</h2>\n<p>微信对小程序包体积的要求是总体积不得超过12M，主包及单个分包体积不得超过2M。支付宝对于小程序包体积的计算方式虽和微信略有区别，不过整体也大同小异。</p>\n<p>18年至19年初时，滴滴出行小程序里承载的业务只有网约车，且业务需求较少，在主包内都能够搞定。而在下半年时，为了将微信钱包/支付宝九宫格入口迁移至小程序，小程序开始新增诸如公交/代驾/车服/单车/顺风车等众多业务线，同时网约车的业务需求也要做全面的补齐，业务量和代码量一起爆炸式增长。</p>\n<p>滴滴出行包含了丰富多样的出行业务，包含了快车/专车/出租车/豪华车/拼车/单车/代驾/顺风车/公交/车生活等众多业务线。整个滴滴出行小程序的最重要，使用最高频的页面是首页与订单详情页，首页中承载了各个业务线的需求表达，各个业务线的订单详情页则承载了具体的出行订单展示逻辑。此外还有各种功能页面比如个人中心，营销页面，设置，历史行程。</p>\n<p>按照滴滴出行的产品逻辑，所有业务线的需求表达逻辑都在首页承载，为了良好的切换体验，在首页采用了单页顶导的方案进行业务线展示。即每个业务线在首页中提供一个需求表达组件，当用户切换顶导业务线后，切换出对应的业务线组件。</p>\n<p>在这种设计下，所有的业务线的需求表达逻辑都集中在首页这个单一页面中，导致在业务迭代过程中，承载首页的主包体积迅速增长，很快触碰了小程序平台的单包2M上限，对后续的业务迭代与发展带来巨大阻碍。因此，对于包体积的控制是我们在小程序开发过程中面临的一大难题。</p>\n<h2>体积控制</h2>\n<p>下面我们将介绍滴滴出行小程序开发迭代过程中，我们对于小程序包体积进行的一系列优化控制实践。</p>\n<h3>基础优化手段</h3>\n<p>对于小程序来说，基础的包体积优化手段包括：资源压缩/去除代码冗余/资源CDN化/异步加载</p>\n<p>在web开发中，webpack提供了大量的代码优化能力，包括依赖分析、模块去重、代码压缩、tree shaking、side effects等，这些能力可以方便地完成资源压缩和去除代码冗余的工作。滴滴出行小程序基于滴滴开源的小程序框架Mpx( <a href=\"https://github.com/didi/mpx\">https://github.com/didi/mpx</a> )进行开发，Mpx框架的编译构建完全基于webpack，兼容webpack内部生态，天然可以使用上述能力对包体积进行优化。</p>\n<p>小程序中支持部分静态资源（如图像视频等）使用CDN地址加载，我们会尽可能将相关的资源压缩后放到CDN上，避免这部分资源对包体积的占用。</p>\n<p>小程序场景下无法像web当中通过script标签便捷地进行异步加载，但是小程序平台后期纷纷支持了分包加载的方案来实现该能力，由于分包加载是小程序特有的技术规范，webpack无法直接支持，因此Mpx框架专门针对该技术规范进行了良好的适配支持，关于该能力的应用我们会在后文详细阐述。</p>\n<p>除此之外，Mpx框架还针对小程序场景进行了许多包体积优化的适配工作，如尽可能减少框架运行时包体积占用（压缩后占用56Kb），对引用到的页面/组件按需进行打包构建，声明公共样式进行样式复用，分包内公共模块抽取等。</p>\n<p>在Mpx框架的这些能力的支持下，基本不需要额外配置就能构建出一个经过初步优化的小程序包。</p>\n<blockquote>\n<p>微信开发者工具选项里也有类似的&quot;上传代码时自动压缩混淆&quot;可勾选，但在开发者工具中上传代码时计算体积是直接计算的当前项目代码的体积，并不会依据压缩后的体积。因此，如果你使用原生小程序进行开发，你的source代码极有可能进行进一步的压缩以节省空间。</p>\n</blockquote>\n<h3>分析体积</h3>\n<p>虽然框架已经提供了很多在体积控制方面的优化，但是随着业务迭代我们发现主包体积依然偏大。</p>\n<p>在遇到主包体积偏大后，我们需要弄明白，主包里有哪些东西？它们为什么这么大？</p>\n<p>使用原生小程序或者其他非基于webpack的框架进行开发的同学遇到这个问题后，可能只能去看硬盘上的文件大小。这样一来，各个模块的大小占比可能并不直观。而我们则可以借助 webpack-bundle-analyzer 这样一个webpack插件去做辅助分析。</p>\n<p>比如这是一个使用Mpx框架编写的demo，通过 <code>npm run build --report</code> 就可以看到这样一个界面：</p>\n<p><img src=\"https://dpubstatic.udache.com/static/dpubimg/3e488293-959e-4617-8187-69fdb532e9ab.jpg\" alt=\"体积分析图\"></p>\n<p>可以看到这个demo工程由 moment / lodash / socket-weapp / core-js 等第三方库组成。各个库的大小，相互依赖关系也能清晰地看出。</p>\n<p>对于滴滴出行小程序也是能看到类似的图，能看到整个项目到底是由哪些代码组成。</p>\n<p>另外，滴滴出行前端开发一直是采用“源码编译”的，可以让整个项目里公共的依赖可以实现仅有一份，一起共用。简而言之，也有助于减少项目代码体积。<a href=\"https://github.com/DDFE/DDFE-blog/issues/23\">相关资料：https://github.com/DDFE/DDFE-blog/issues/23 </a></p>\n<p>要完美发挥源码编译的效果，需要上下游一起建立整套源码编译生态，比如主项目的依赖方在声明公用依赖时，就应该用peerDep或者devDep来声明一些公有依赖，这些共有依赖应该在主项目中统一声明，避免因版本不同装出两份公共依赖，那样反而会增大体积。由于滴滴出行小程序涉及业务线及团队众多，部分团队可能并不知道这个事情，因此代码里实际是可能出现上述劣化场景。而依照分析图，可以容易地发现这种问题，并推动相关团队清除这些重复依赖。</p>\n<p>同时，我们依照体积分析图，对其中体积较大的文件重点分析，进行了一轮业务代码梳理和精简，删除了一些无用代码，精简了websocket的消息体描述文件等。</p>\n<h3>配置分包</h3>\n<p>分包是小程序给出的类似web异步引入的一个方案，把一些初始进入时不需要的页面可以放进分包里，跳转到对应页面时才去下载分包，将这些页面及其附属资源放到分包里可以有效减少主包体积。</p>\n<p>Mpx框架早期对分包规范进行了初步支持，资源访问规则保持和微信一致，主要根据资源存放的目录判断应该输出到主包还是分包。有这个能力后，我们把行程页抽到了分包，大概抽出了200多K左右的空间。</p>\n<p>有了行程页的成功拆分后，我们开始对所有的非首页代码进行分包操作，比如起终点选择和个人中心。以及部分业务线的接入是通过npm的方式接入，我们也尽可能将这些业务线的所有非首页的代码放到了分包。</p>\n<p>这里还有个题外话，得益于mpx早期设计了packages形式的业务组合方案，可以很方便地让业务独立开发，又能及其方便地整合。而后发现微信的分包的json配置设计和packages很像，就在这个基础上支持了微信的分包，用户侧仅需在原来的packages基础上加一个query标记这个分包的名字即可。</p>\n<p>拆除各个分包后，整个项目结构大概如图：</p>\n<p><img src=\"https://dpubstatic.udache.com/static/dpubimg/0562f94d-3929-42b8-a389-854e14983373.png\" alt=\"分包一期结构图\"></p>\n<p>初阶的分包工作进行完毕后，总计从主包里拆了差不多400K的空间到分包里。</p>\n<h3>分包资源精细化管理</h3>\n<p>上面提到，Mpx框架初期的分包处理规则是完全按照微信的方式，把在分包路径下的资源收集到分包里。而npm管理的资源因为都在node_modules目录下，不属于任何分包路径，则会被全部收集进主包。</p>\n<p>比如之前我们有行程页分包，行程页自有的状态管理store整个都在行程页分包的路径下，就会被收集到行程页分包中。而行程页还用到了封装好的didi-socket库，这个库是公共的npm包，即使它只在行程页分包里被使用，但由于它本身路径是在node_modules下的，那么就会将其收集进主包里。</p>\n<p>因为早期的一些设计，行程页的资源和首页是分割开的，都比较独立地存在于各自的路径下，一期的分包处理的大头也主要是行程页，它刚好契合了Mpx初期对分包处理上的特点，因此能较好地收集进行程页分包里。</p>\n<p>随着业务迭代，后续大量业务线的接入都是通过npm进行的，就会有大量npm包资源，他们都在node_modules目录下，因此全部会被收集进主包。</p>\n<p>所以Mpx框架进行了一系列改造：</p>\n<ol>\n<li>在构建的依赖收集过程中，我们会对收集到的依赖打上标记，记录它是被哪些分包引入的。一旦它只有一个分包引入，它就会被输出到这个分包中。</li>\n<li>我们会根据用户定义的分包配置，自动在 SplitChunksPlugin 中生成各个分包的 cacheGroups ，把分包中的复用模块抽取到分包下的bundle中。</li>\n<li>对于组件和静态资源，如果他们被多个分包所引用且未在主包中引用，为了确保主包体积最优，这些资源将产生多份副本分别输出到对应分包中，而不会占用主包体积。</li>\n</ol>\n<p>这样一来，<em><strong>不管分包中引用的资源原本在什么位置，最终输出时都会尽可能将其输出到dist的分包目录下，避免占用主包空间</strong></em>。</p>\n<p>这个改动完成后项目结构看似和之前一样，但得益于Mpx处理分包资源能力的升级，我们得以将业务线分包中引用的npm资源成功输出到其所在的分包目录下。</p>\n<h3>封面方案</h3>\n<p>再后来滴滴出行小程序需要替换微信/支付宝里原有的WebApp入口，小程序接入的业务线迅速增加，包体积迅速增长。</p>\n<p>这个部分体积增长的主要原因前面提到过，所有的业务线都要接入到主页来展示。这也是由于业务特点决定的，滴滴出行提供了丰富的出行产品线，包括快车/专车/出租车/豪华车/拼车/单车/代驾/顺风行车等产品，用户是可能需要反复切换挑选的。这个过程还要保留起终点车型之类的信息，必须是一个页面内切换组件加一整套非常复杂的大型状态管理才能比较流畅顺滑地实现。而不能像一些电商/信息平台，将不同的功能拆分到不同页面，让用户通过首页的菜单进入子页面再进行操作，首页只承载入口，只有较少的业务逻辑，分包处理起来就会容易很多。</p>\n<p>因此各个业务线都要提供首页组件进行接入。这个组件会在首页被用到，所以无论如何也拆不到分包里。最终，整个首页主包部分的体积可以分成两个部分：基础库和业务代码。两者的体积占比大概是公共依赖基础库占1M左右，业务代码占1M左右。</p>\n<p>这么庞大的基础库体积主要是由于滴滴出行的业务线及业务团队众多，各方均有一些自己的基础依赖。比如网约车依赖的长链接通信pb数据描述文件，地图会依的大数计算库，顺风车依赖的CML框架运行时、代驾依赖的通信网关库，以及公用的组件库和polyfill等。</p>\n<p>所以滴滴出行小程序面对的问题在当时已经无法用纯技术方案在短期内快速解决问题了，于是我们做了一个工程架构调整，可以叫封面页方案，解决了主包问题。</p>\n<p>封面方案简单讲，就是做一个带滴滴出行Logo的封面作为启动页面，而页面一旦加载，立刻跳转另一个页面，这个页面真正承载业务，且它被放在分包里。</p>\n<p>这个操作的意义在于，主包里就只剩下了所有方都要依赖的基础框架/库等，而业务全被抽离到了分包里。</p>\n<p><img src=\"https://dpubstatic.udache.com/static/dpubimg/0f0ed782-8f67-4601-bc83-a8a343e1050b.png\" alt=\"封面方案结构图\"></p>\n<p>这是封面方案完成后项目的结构图，之前很大块的首页业务逻辑被抽出到首页分包中了。</p>\n<p>这样一个挪移的操作的结果是我们可以有2M的主包空间来乘放基础的公共的库，有一个2M左右的分包来乘放前面提到的滴滴出行的集成了各种业务的“大主页”。而当时拆下来差不多有1.2M的主包，800K+的业务主分包。</p>\n<p>这个改造最优秀的一点在于，后续的业务迭代产生的体积增长几乎全是在业务主分包里，剩下的1.1M+空间留给业务迭代还是比较充裕的。而主包的体积在理想条件下是可以长久保持不变的，就不会因为业务需求的不断开发反复导致主包体积临近超标，不再需要为主包体积感到焦虑。</p>\n<p>当然，可以看到，这个方案本身是没有消减任何体积的，只是把位置变换了一下。除此之外，这个封面页方案其实也存在一些缺陷，比如首屏业务的展示会变慢，因为要加载的内容会变多，不过小程序本身有较好的缓存资源的能力，因此还算可以接受。</p>\n<p>相比于因体积问题卡住需求迭代以及产品线的接入，目前这个方案至少能解决有无问题。我们开发团队后续也会持续跟进关注体积问题，看是否会有产品方案变更或者小程序本身给出一些解决方案来进一步优化这个部分。</p>\n<h2>总结</h2>\n<p>Mpx框架在包体积控制上做了大量工作，对于npm场景下的小程序分包进行了非常完善的支持。</p>\n<p>滴滴出行小程序团队在框架支持的基础上，通过梳理业务依赖，充分利用分包，调整交互方案等一系列手段，在不阻碍业务发展的前提下，将庞大复杂的滴滴出行小程序包体积控制在平台限制范围内。</p>\n<p>希望本文能给在包体积上遇到问题的小程序开发者们带来一些启发，欢迎留言交流。</p>\n","date_published":"2020-06-07T00:00:00.000Z","tags":["小程序"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2020/nginx-express%E5%81%9A%E4%B8%80%E4%B8%AA%E7%AE%80%E6%98%93%E4%BB%A3%E7%90%86%E6%9C%8D%E5%8A%A1/","url":"https://www.lihuanyu.com/posts/2020/nginx-express%E5%81%9A%E4%B8%80%E4%B8%AA%E7%AE%80%E6%98%93%E4%BB%A3%E7%90%86%E6%9C%8D%E5%8A%A1/","title":"nginx+express做一个简易代理服务","summary":"用 NGINX + EXPRESS + Let's Encrypt 构建HTTPS接口服务","content_html":"<blockquote>\n<p>用 NGINX + EXPRESS + Let’s Encrypt 构建HTTPS接口服务</p>\n</blockquote>\n<p>最近想搞个小程序，需要查询下github的api服务。小程序的请求是走的小程序提供的api，没有跨域问题，不过需要配置下请求域名白名单。</p>\n<p>本以为可以在小程序后台配置一下域名之后直接请求，结果发现小程序后台配置里只允许设备案过的域名，GitHub显然不可能有国内备案。</p>\n<p>还好自己也有服务器+域名，自己动手搭一个。而且小程序要求全是HTTPS的，还是有遇到一点小坑。</p>\n<h2>方案</h2>\n<p>其实NGINX本身就可以完成反向代理功能，不过考虑到以后可能还想在这个基础上做一些别的东西，还是来个可编程的服务比较好。出于简便性考虑直接无脑选了express。</p>\n<p>开个新的二级域名，因为只有一台云虚拟机，80/443端口已经被NGINX占用了，那就NGINX转发给express吧，所以整个结构就是NGINX反向代理express（这个express服务又是个对github的反向代理，真巧……）</p>\n<p>NGINX加一个新配置，监听80端口，对 / 全转发去本机的3000端口，完事。</p>\n<pre><code class=\"language-editorconfig\">server {\n     server_name  域名马赛克;\n     listen 80;\n \n     location / {\n         proxy_pass    http://127.0.0.1:3000;\n     }\n}\n</code></pre>\n<p>然后用Let’s Encrypt搞一下HTTPS，用 <a href=\"https://certbot.eff.org/\">certbot</a> 这个工具，直接选服务器软件和系统，可以自动帮你完成配置HTTPS。</p>\n<p>用express写个hello world，再全局安装pm2，启动我们的express程序，访问域名即可看到hello world，基本搞定。接下来再选个反向代理中间件，<a href=\"http://xn--targetapi-zb6nlqt989bjt0b.github.com\">配置下target为api.github.com</a>，大功告成。</p>\n<h2>超时</h2>\n<p>结果接下来实际测试发现炸了……</p>\n<p>很多请求报504 网关超时</p>\n<p>一通搜索，发现很多是给NGINX和PHP结合的方案用的（后仰：什么叫全宇宙最好的语言啊），不过差不多，都是说设置上游超时时间。</p>\n<p>创建 <code>/etc/nginx/conf.d/timeout.conf</code> ，加上以下内容：</p>\n<pre><code class=\"language-editorconfig\">proxy_connect_timeout       600;\nproxy_send_timeout          600;\nproxy_read_timeout          600;\nsend_timeout                600;\n</code></pre>\n<p>然后再试，发现还是容易出现504。</p>\n<p>就需要细节排查问题了，先查nginx的日志，access.log和error.log，发现这个504在nginx的错误日志里是没有的，access里有记录express程序给返回了200和504，问题应该是出在express里。</p>\n<p>接下来打印pm2的日志，发现错误信息是 <code>Error occurred while trying to proxy request xxxxx from 127.0.0.1:xxxx to https://api.github.com (ECONNREFUSED) (https://nodejs.org/api/errors.html#errors_common_system_errors)</code></p>\n<p>打开这个链接，这个错误信息是：ECONNREFUSED (Connection refused): No connection could be made because the target machine actively refused it. This usually results from trying to connect to a service that is inactive on the foreign host.</p>\n<p>目标机器主动拒绝了请求，这说明Github应该是觉得这个请求不正常，想了下估计是没带一个浏览器的头，伪装成浏览器应该就好。</p>\n<p>配置下代理的headers，再重启服务，终于OK了</p>\n","date_published":"2020-05-31T00:00:00.000Z","tags":["nodejs"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2020/NPM%E5%B0%8F%E6%8A%80%E5%B7%A7/","url":"https://www.lihuanyu.com/posts/2020/NPM%E5%B0%8F%E6%8A%80%E5%B7%A7/","title":"NPM小技巧","summary":"package.json里描述依赖时候一般版本声明是前面带 '^' 的形式，意味着安装依赖时npm会自动尝试去安装声明版本的最新patch，多人合作一个项目时就可能出现大家依赖不一致的问题，出现问题后的复现与调试会比较麻烦，容易出现仅在某台电脑上可以复现的情况。","content_html":"<blockquote>\n<p>package.json里描述依赖时候一般版本声明是前面带 ‘^’ 的形式，意味着安装依赖时npm会自动尝试去安装声明版本的最新patch，多人合作一个项目时就可能出现大家依赖不一致的问题，出现问题后的复现与调试会比较麻烦，容易出现仅在某台电脑上可以复现的情况。</p>\n</blockquote>\n<h2>前言</h2>\n<p>npm是JS世界的包管理工具，因为其和node捆绑安装，应该是全世界最多人使用的包管理工具。</p>\n<p>目前的前端开发普遍离不开构建/依赖管理，大部分项目其实都是一个npm包形式进行的，拥有自己的package.json来管理项目依赖，使用npm script来提供开发/构建/测试等命令。</p>\n<p>npm的依赖管理设计得和其他包管理工具差异非常大的一点是，项目依赖默认是本地安装的，全局安装是需要额外声明的。就是说使用npm来安装一个依赖，默认会安装到当前项目路径下的node_modules文件夹里。</p>\n<p>这样做缺点是浪费空间，npm包的依赖关系可能比想象中复杂得多，node_modules的体积占用非常大。但也不是没有好处。</p>\n<p>比如，版本隔离，每个项目有自己的依赖，各用各的互不干扰。以及第三方库的代码触手可及，便于学习理解，出问题后也便于调试。</p>\n<h2>安装依赖</h2>\n<p>通过npm install可以安装依赖，简写npm i。npm6以后，安装依赖后还会生成一个package-lock.json，前面提到过，由于声明依赖时带 ‘^’ ，npm i时候会自动安装package.json里声明的版本所对应的最新patch版本。</p>\n<p>那么如果有人不遵守版本规范，恶意在patch版本里做breaking change，这样安装依赖就极有可能出问题。</p>\n<p>而且还会影响帮助他人解决问题和云端构建的稳定性。</p>\n<p>为了解决这个问题，可以使用npm ci来进行依赖安装。</p>\n<p>ci指令的官方文档在这里：<a href=\"https://docs.npmjs.com/cli-commands/ci.html\">https://docs.npmjs.com/cli-commands/ci.html</a></p>\n<p>描述里提到ci这个指令的用处是：<code>Install a project with a clean slate</code>，为项目干净稳定地安装依赖。</p>\n<p>表现出来的行为和npm i的区别主要有以下几点：</p>\n<ol>\n<li>\n<p>会删除现有的node_modules文件夹里的东西再开始安装（干净）</p>\n</li>\n<li>\n<p>会严格按照lock文件记录的版本号进行安装（稳定）</p>\n</li>\n<li>\n<p>安装速度更快，不更新lock文件</p>\n</li>\n</ol>\n<p>所以，通过ci指令，我们就可以在任何一台电脑上完全复现git中某次提交时候的现场。</p>\n<p>不过这个也不是万金油，也不建议抛弃npm i只使用npm ci，正常开发场景下，npm i还是更好地选择，patch版本一般都是为了修复一些bug所发布的，跟随使用最新的patch是好事。</p>\n<p>总结就是 npm ci 可以用来干净稳定地复现现场，用于帮助他人debug和CI/CD流程等场景下的安装依赖。</p>\n","date_published":"2020-05-10T00:00:00.000Z","tags":["前端","npm"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2019/mpx1/","url":"https://www.lihuanyu.com/posts/2019/mpx1/","title":"小程序开发者，为什么你应该尝试下MPX","summary":"MPX框架 是滴滴出行推出的一款专注小程序开发的增强型框架。本篇文章将从使用角度谈谈MPX的优势与好处。如果嫌内容太长，优势部分每个小节都有简单的一句话总结，可以快速阅读。如果想了解更多设计细节，可以阅读 前一篇文章 MPX2.0发布。 背景 在小程序逐渐火热的今天，越来越多的开发者需要进行小程序的开发。原生小程序的开发有诸多不便，开发者又需要在众多的小程序","content_html":"<blockquote>\n<p>MPX框架 ( <a href=\"https://github.com/didi/mpx\">https://github.com/didi/mpx</a> ) 是滴滴出行推出的一款专注小程序开发的增强型框架。本篇文章将从使用角度谈谈MPX的优势与好处。如果嫌内容太长，优势部分每个小节都有简单的一句话总结，可以快速阅读。如果想了解更多设计细节，可以阅读 <a href=\"https://didi.github.io/mpx/articles/2.0.html\">前一篇文章 - MPX2.0发布</a>。</p>\n</blockquote>\n<h2>背景</h2>\n<p>在小程序逐渐火热的今天，越来越多的开发者需要进行小程序的开发。原生小程序的开发有诸多不便，开发者又需要在众多的小程序框架中做出抉择。</p>\n<p>那么今天，我们要给大家安利一款小程序框架：MPX</p>\n<h2>优势</h2>\n<p>之所以建议开发者们考虑使用MPX框架来开发小程序，是因为MPX框架具有一些别的框架所没有的优点。</p>\n<p>MPX立足原生小程序，在保证坑少的同时做了很多能力增强，提供了数据响应、模板增强、性能优化、跨平台开发等能力，以提升用户的开发体验及效率。</p>\n<p>接下来会从 原生兼容 -&gt; 第三方组件支持 -&gt; 按需构建 -&gt; 跨平台编译 -&gt; 能力增强 -&gt; 独特性能优势 六个点来逐一讲述。</p>\n<h3>原生兼容</h3>\n<blockquote>\n<p>MPX完全兼容原生，坑少。渐进接入简单。</p>\n</blockquote>\n<p>从语法风格上，我们可以看到目前市面上流行的小程序框架基本是基于web框架（taro/nanachi - react，uniapp/megalo/mpvue - vue）或者是一套全新（chameleon）/ 半全新（wepy）的标准。</p>\n<p>使用了这些框架，你所写的代码，并不是小程序代码。而是react/vue或者另一套代码。而这些代码源码到小程序代码，需要经过一次全面的转换，这个转换可能会引入一些未知的问题，产生一些坑。</p>\n<p>同时随着时间，小程序自身会逐步迭代，做出更多的功能特性，提供更好的组件、方法。而一些框架可能会受限于精力或框架节奏，没有办法第一时间跟进，甚至框架慢慢疏于维护而无法使用。</p>\n<p>而MPX选择的是，<strong>全面拥抱原生</strong>。</p>\n<p>口说无凭，我们来看个典型的MPX组件长什么样。</p>\n<p><img src=\"https://dpubstatic.udache.com/static/dpubimg/3877c653-d180-4fe7-8661-457b9de1bd82.png\" alt=\"mpx组件示例\"></p>\n<p>乍一看好像和vue没什么区别，也就多了个json块，template里写的是小程序的标签。</p>\n<p>由于这一块全是符合微信小程序原生语法，我们是不会做任何转换的，所以你写什么就是什么。（如果使用了MPX的增强特性，还是会进行一些必要的转换的，后续我们也会出文章详细解释MPX的增强是如何实现的，相对来说，我们的转换比较轻量、透明、易理解）</p>\n<p>当微信出了新的能力、新的标签、新的生命周期钩子，使用MPX框架来编写的小程序只需要直接用起来就行。</p>\n<p>所以，使用MPX框架，你可以轻易地使用 <strong><a href=\"https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/relations.html\">自定义组件的relations</a></strong> 来搞定组件间关系，使用 <strong><a href=\"https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxs/\">wxs</a></strong> 来更好地构建页面。</p>\n<p>MPX几乎支持原生的每一个特性，在 .mpx 文件里，模板部分写的是原生小程序的模板语法，脚本部分写的是原生小程序的脚本语法，json部分写的是原生小程序的配置信息。用MPX，你才是真的在开发小程序。</p>\n<p>目前很多原生小程序开发者可能想尝试下框架，老项目接入框架，选MPX肯定是最简单的了。口说无凭，我们搞了个demo来给大家打个样：在我们GitHub项目中有examples文件夹，里面的 <a href=\"https://github.com/didi/mpx/tree/master/examples/mpx-progressive\">原生项目渐进接入MPX示例</a> 。</p>\n<h3>第三方组件库</h3>\n<blockquote>\n<p>MPX提供了完备的第三方组件库支持</p>\n</blockquote>\n<p>上面说了MPX对原生的极致兼容，能让你想到什么？对，就是对第三方组件库的完美支持。</p>\n<p>支持第三方组件库的重要性大家都知道，所以这个能力大部分框架都支持了。但是支持和完美支持还是有区别的。据简单观察，taro/mpvue/uniapp对于第三方组件库的支持都是以复制的形式进行的，也就是和微信小程序本来的行为很像。</p>\n<p>那么MPX是怎么支持第三方组件库的呢，这里有个demo：也在我们的GitHub里的examples文件夹下，<a href=\"https://github.com/didi/mpx/tree/master/examples/mpx-useuilib\">MPX使用第三方组件库示例</a> ，核心代码见下图：</p>\n<p><img src=\"https://dpubstatic.udache.com/static/dpubimg/9ecdd898-f7e5-48cc-89c2-bfc7ce3a03f9.png\" alt=\"MPX使用第三方组件库代码示例\"></p>\n<p>乍一眼看不太出来有什么特别的？没用过别的框架引第三方组件，简单找了找其他框架好像也没提供相应的demo，用过的朋友可以自行对比下。</p>\n<p>在MPX里使用第三方组件库，仅需要<em><strong>像web项目一样npm安装即可，并不需要复制文件</strong></em>。然后在json里直接写包名就会去node_modules下面查找了。再配合webpack alias可以做到更简单、更语义化。</p>\n<p>然而这还没有结束~</p>\n<p>细心的朋友会发现，这段示例代码中既有vant的组件，也有iview的组件，如果按照微信的规范，这些组件库会通过miniprogram字段指定自己的构建文件生成目录，开发者工具会把这个目录完全拷贝到最终发布的代码里去，我们就会有两个巨大的组件库占据宝贵的空间。</p>\n<p>我们当然是希望用多少引多少，而不是一股脑全引进去，对，于是MPX提供了按需引用的能力，在下一章<a href=\"#%E6%8C%89%E9%9C%80%E5%BC%95%E7%94%A8\">按需引用</a>细讲。</p>\n<p>以及，组件库目前很少有跨小程序平台的组件库啊，如果我用了vant，支付宝、QQ里没有vant怎么办？也许这是别的框架不怎么推荐使用第三方库的原因，而MPX里，我们帮你把别人的组件库也转了，细节看下下章<a href=\"#%E8%B7%A8%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%B9%B3%E5%8F%B0\">跨小程序平台</a></p>\n<h3>按需引用</h3>\n<blockquote>\n<p>通过webpack依赖分析收集，使用第三方组件库或者拆分开发大型项目时MPX能保证构建的代码全是要用到的代码</p>\n</blockquote>\n<p>原生小程序本身的编译是遍历项目文件夹里所有的JS，包装成一个AMD包，也就是说项目文件夹里所有的文件，不论是否被使用，都会占用包体积并上传。</p>\n<p>同时，原生微信小程序的npm支持是基于文件夹复制的，第三方包通过声明miniprogram字段指定要拷贝的文件夹，不论使用还是未使用的资源（模板/js/样式/图片），全会被复制到项目文件夹中。</p>\n<p>而我们提供了@mpxjs/webpack-plugin插件，借助webpack生态，解析.mpx文件的json部分或原生的json文件将依赖作为新的入口添加子编译。基于依赖收集，而不是文件遍历。</p>\n<p>带来的好处就是：如果你喜欢vant的按钮，iview的输入框，wux的布局，欢迎尝试MPX，让你能同时使用多个UI框架的同时不用担心应用的体积爆炸。</p>\n<p>同理，面对一个大型项目，我们可以拆成不同的部分，由不同的团队完成后发npm包，在一个主项目中引入即可，具体内容可以看文档<a href=\"https://didi.github.io/mpx/single/json-enhance.html#packages\">JSON增强 - packages</a>一节。</p>\n<p>收集依赖的细节可以查阅文档<a href=\"https://didi.github.io/mpx/understanding/understanding.html#%E7%BC%96%E8%AF%91%E6%9E%84%E5%BB%BA\">编译构建</a>一节。</p>\n<h3>跨小程序平台</h3>\n<blockquote>\n<p>MPX的跨平台方法能带着第三方组件库一起跨小程序平台，同时提供了充足完善的条件编译能力。</p>\n</blockquote>\n<p>在 MPX 1.0 时代，MPX框架是专注提升微信小程序的开发体验，虽然也提供了支付宝版，但代码完全要另写。</p>\n<p>而随着越来越多的 super app 提供了小程序能力，目前至少有5种体系的小程序（微信、支付宝系列、百度系列、头条系列、QQ），如果每一个平台都需要维护一份代码，工程师人数明显不够用了，所以跨小程序平台的能力也是 MPX 2.0 的主打特性。</p>\n<p>我们的跨平台的方法就是转换。都是小程序，语法基本一样，配置、钩子的差异在MPX运行时里提供了抹平。</p>\n<p>而除此之外最大的区别也就是模板上的标签和指令。所以我们实现了一套转换的架子，再编写一份转换规则，即可完成微信小程序到支付宝、百度、头条小程序的转换。</p>\n<p>采用这种转换的模式，非常方便用户理解我们是如何把微信小程序转换成支付宝、百度等小程序平台的。而且只要用户有需求，可以补齐任一套小程序转换其他平台的规则，就可以完成以某个小程序为标准为基础来编写小程序代码以及进一步转换成别的平台的能力。</p>\n<p>再结合前面一直在说的我们对原生小程序的支持，就可以撞出一点不一样的东西，比如，前文提到的第三方组件库跨小程序平台。</p>\n<p>对，我们能帮你把针对微信编写的ui组件库在支付宝、百度上运行起来，带着组件库一起跨小程序平台。</p>\n<p>那么一定会有这样一个问题，就算MPX对原生的支持再怎么牛逼，有的基础能力只有微信平台有，别的平台没有，MPX的转换还能无中生有吗？</p>\n<p>当然不能，其实这个问题对于所有的跨端框架都是一个问题，所以跨端最核心的问题是，如何搞定差异化部分。</p>\n<p>MPX提供了丰富的条件编译能力，可以以文件为维度差别构建，可以以代码块为维度，也可以以代码维度进行差别构建。</p>\n<p>而且MPX的差异化构建能力也是完全基于webpack实现的，所以上面提到的第三方组件库如果确实存在转换不了的地方，比如vant的picker组件使用内联wxs写了一个小方法叫isSimple在模板里调用了，但是这个方法的写法在百度小程序的filter脚本（filter可以理解为百度小程序的wxs）里不支持，因为百度的filter要求必须导出一个对象包裹方法。</p>\n<p>最好的解决办法当然是给vant-weapp提pr帮他们解决一下这个问题，但时间可能会比较慢，所以在MPX里，可以利用webpack的alias能力：</p>\n<p><img src=\"https://dpubstatic.udache.com/static/dpubimg/7f90593b-65b4-40d0-bd93-6396e8a18e64.png\" alt=\"通过alias解决第三方组件的跨平台问题\"></p>\n<p>当尝试构建百度小程序时，会优先去查找pick/index.swan.wxml，再被alias到一个src下的文件，自己修改一下第三方包里有一些小问题的部分即可。</p>\n<p>关于跨平台的条件编译，更多具体信息可以<a href=\"https://didi.github.io/mpx/platform.html#%E8%B7%A8%E5%B9%B3%E5%8F%B0%E7%BC%96%E8%AF%91\">查阅我们的官方文档 - 跨平台条件编译</a></p>\n<h3>能力增强</h3>\n<blockquote>\n<p>通过数据响应、编译时预处理提供了computed/watch，完备的样式类型绑定，双向数据绑定，动态组件等一系列方便开发者更好开发小程序的能力增强</p>\n</blockquote>\n<p>能力增强应该是一个框架提供的最核心最重要的能力了，而MPX也确实在这里下了很大的力气，提供了多且好用的能力增强，不过受限于此处的篇幅，就只简单介绍，细节大家还是<a href=\"https://didi.github.io/mpx/single/template-enhance.html#template%E5%A2%9E%E5%BC%BA%E7%89%B9%E6%80%A7\">查阅我们的文档</a>的好。</p>\n<p>别的框架由于往往基于react/vue的，会给个列表写明不支持哪些能力，用户写的时候习惯使然，往往用了后可能才反应过来哦这个不支持。MPX则是原生的小程序语法写着难受时候突然想起MPX有这个能力。</p>\n<p>列一下MPX增强的能力：</p>\n<ul>\n<li>模板上的增强\n<ul>\n<li>样式类名绑定</li>\n<li>内联事件传参</li>\n<li>动态组件</li>\n<li>双向绑定</li>\n<li>节点获取ref</li>\n</ul>\n</li>\n<li>JS里的增强\n<ul>\n<li>数据响应</li>\n<li>setData优化</li>\n<li>ES6+</li>\n</ul>\n</li>\n<li>样式上的增强\n<ul>\n<li>预处理支持</li>\n<li>rpx转换</li>\n</ul>\n</li>\n<li>JSON里的增强\n<ul>\n<li>packages</li>\n<li>分包资源优化</li>\n</ul>\n</li>\n</ul>\n<p>MPX最显著的能力是数据响应，它衍生出computed/watch，以及双向数据绑定等。这个能力和Vue比较像，不同的是在MPX里是由mobx提供的数据响应能力。</p>\n<p>而同样是数据响应，我们做了一些不一样的优化。</p>\n<h3>性能优势</h3>\n<blockquote>\n<p>通过对模板的解析抽象出访问的数据以保证在提供了数据响应能力的同时不至于劣化性能。</p>\n</blockquote>\n<p>mpvue/wepy/megalo等框架也提供了数据响应的能力，但是数据响应在小程序领域有个较大的问题，微信开发指南里明确提到要注意setData的调用频次和数据量的大小。</p>\n<p>而数据响应最基本的做法就是数据变了就去set数据，这会极大劣化小程序的性能表现。</p>\n<p>而MPX通过对模板进行解析，抽象出对应的render函数，在调用setData发送数据前执行render函数找到真正需要发送的数据。</p>\n<p>效果如图：</p>\n<p><img src=\"https://dpubstatic.udache.com/static/dpubimg/9399b75c-5c81-4584-89b6-5e7f2c2ba7ba.png\" alt=\"小程序性能分析\"></p>\n<p>小程序开发者工具的audits面板能辅助用户分析出可能需要优化的点。正如前文所说，MPX在红框部分，尤其是红框里的第三条，不将模板上未使用的数据发送到渲染层上做了极大的优化。</p>\n<p>只要不出现渲染函数执行失败（会有warning在console里提示，同时兜底逻辑会进行全量setData以保证程序仍可正常运行），使用MPX开发的小程序就永远不用担心发送了模板未使用的数据。</p>\n<blockquote>\n<p>为了不降低首次渲染的速度，我们未对构造器里声明的data初始值做这个分析，所以也不要因为MPX有这个特性就大肆在data上声明过多不在模板上使用的数据。</p>\n</blockquote>\n<p>虽然只是一个小小的TODO MVC示例，但是这个优化和应用的规模没关系，而且同时大家可以尝试别家的小demo对比看看。</p>\n<p>这个优化的细节可以看<a href=\"https://didi.github.io/mpx/articles/2.0.html\">前一篇文章</a>，或者我们的文档<a href=\"https://didi.github.io/mpx/understanding/understanding.html#%E6%95%B0%E6%8D%AE%E5%93%8D%E5%BA%94%E4%B8%8E%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96\">MPX运行机制 - 数据响应与性能优化</a></p>\n<h2>总结</h2>\n<p>与目前市面上的诸多框架相比，MPX希望以原生小程序为基础，全面拥抱原生小程序，在原生小程序的基础上做增强，通过尽可能少的转换实现尽可能多的能力增强，在提升小程序开发体验的同时，保证不因转换或框架的问题产生过多的坑。</p>\n<p>MPX框架的目标用户是对小程序质量有较高要求的开发者，如果你是原生小程序开发者，或者厌倦了解决某些以web框架DSL语法为基础的转换框架造成的坑，欢迎尝试MPX框架。</p>\n<p><a href=\"https://github.com/didi/mpx\">MPX GITHUB：https://github.com/didi/mpx</a></p>\n","date_published":"2019-05-26T00:00:00.000Z","tags":["MPX"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2017/docker-for-windows%E4%B8%8D%E5%93%8D%E5%BA%94react%E9%A1%B9%E7%9B%AE%E6%94%B9%E5%8F%98%E5%90%8E%E7%9A%84%E9%87%8D%E7%BC%96%E8%AF%91/","url":"https://www.lihuanyu.com/posts/2017/docker-for-windows%E4%B8%8D%E5%93%8D%E5%BA%94react%E9%A1%B9%E7%9B%AE%E6%94%B9%E5%8F%98%E5%90%8E%E7%9A%84%E9%87%8D%E7%BC%96%E8%AF%91/","title":"docker for windows不响应react项目改变后的重编译","summary":"docker一般用于部署，但也可以用于统一开发环境，解决诸如前后端分离后单边开发者无法获得一个完整的开发环境的问题。所以我写了这个项目简单实践一下用docker来简化开发环境。其中遇到了一个docker在windows下表现不正常的问题及其解决方案。","content_html":"<p>docker一般用于部署，但也可以用于统一开发环境，解决诸如前后端分离后单边开发者无法获得一个完整的开发环境的问题。所以我写了<a href=\"https://github.com/sky-admin/react-blog\">这个项目</a>简单实践一下用docker来简化开发环境。其中遇到了一个docker在windows下表现不正常的问题及其解决方案。</p>\n<h2>项目概述</h2>\n<p>项目主要是用react编写一个blog的用户界面，blog项目就要求有一个后端（类似于hexo这种静态网页生成器模式的blog不算），能提供诸如注册、登录、发表文章、编辑文章、删除文章之类的功能。</p>\n<p>这种简单的CRUD后端还是非常简单的，之前就写过，但是如何让前端项目能愉快地调用后端呢？</p>\n<p>一种方案是后端先部署上线运行着，配置好CORS，前端项目直接调线上接口，这种方式其实蛮好，而我现所在的公司也是使用这种方式进行开发的（在此吐槽一下老东家，使用的是前端自行mock数据的方式，当然，技术选型有多方面的考虑）。</p>\n<p>但这种方式在个人小项目中有个问题，很有可能后端也只是个demo级的东西，非常不完善，服务器资源又有限，指不定哪天就挂掉了。换而言之，很难让一个项目能在任何人的电脑上都愉快地运行。</p>\n<p>另一种方案就是后端一起给出，CORS也不用配了，用个nginx来代理，session\\cookie都能用，好不好哇？</p>\n<p>你问我好不好我当然说好啦，但一个正常的前端会配置jdk，运行mvn，安装启动数据库，配置nginx的概率有多大呢？不知道。</p>\n<p>这个时候就是Docker出场的时间了，配置好四个docker镜像，用docker-compose编排好容器，一个up就启动了全套的开发环境。</p>\n<p>仔细去数了一下，从0启动整套开发环境只需要打4行命令，当然，等待docker从hub拉image的时间会取决于你的网速，所以第一次会慢一些，后续只需要 <code>docker-compose up</code> 一条命令即可啦。</p>\n<h2>遇到的问题</h2>\n<p>在mac上表现一切正常，up之后访问 <code>localhost:8080</code> 即可看到效果，用IDE或者编辑器打开项目，编辑内容，保存。在短暂的等待后就能看到改动后的网页啦（使用的create-react-app生成项目模板，live reload之类的都是配置好的）。</p>\n<p>然而Windows上的表现却不正常，启动是能启动，看网页也能看到。就是编辑文件保存以后，不会触发webpack的重新编译，看不到网页新的效果。要想看新的效果呢……就重新来一遍 <code>docker-compose up</code> ，哇，这就很难受了。</p>\n<p>只是想改个小地方看看，还得等整套容器编排重新启动一遍，这是不可接受的。</p>\n<p>问题的表现就是，docker for windows里，windows下改了文件，docker容器里没有反应。这个表现的原因可能是两种：</p>\n<ol>\n<li>容器里的文件没有改变</li>\n<li>文件改变了，但容器无法感应到改变</li>\n</ol>\n<p>为了确认是哪种原因，我进入到相应的node容器里，手工启动 <code>npm start</code> ，然后在Windows下编辑文件并保存。确实没有重编译，于是我停掉 <code>npm start</code> ，cat对应的文件查看是否是我改动后的样子。</p>\n<p>确认是原因2，即某种原因导致了docker for windows的容器无法感应到windows通过volume关联来的文件夹里文件的变化。</p>\n<h2>如何解决</h2>\n<p>之所以想写这篇文章，也是希望后来有用中文关键字来搜索的同学有机会找到解决方案，会用英文直接搜到那是最吼的啦。</p>\n<p>Google几番搜索后找到了<a href=\"https://forums.docker.com/t/file-system-watch-does-not-work-with-mounted-volumes/12038/13\">docker论坛里这个帖子</a>，简单理解了下问题原因：</p>\n<blockquote>\n<p>Docker for Windows relies on SMB/CIFS support in Linux, and since propagating filesystem notifications is not supported our options are limited.</p>\n</blockquote>\n<p>Docker for Windows 依赖于Linux对SMB/CIFS的支持，但是由于文件系统的通知冒泡不支持，我们的能力受限。</p>\n<p>一般来说，到这个地方就算走到 <code>bad ending</code> ，大家可以洗洗睡了，然而，却有脑洞颇大的同学给出了一个python包，提出了一个hack方案。感谢作者。</p>\n<p><a href=\"https://github.com/merofeev/docker-windows-volume-watcher\">python包：docker-windows-volume-watcher</a>，这个东西的原理就是它运行在Windows里，观测到文件的变化后，先读取文件的权限，再重写权限值为读取的这个值（等于没变），容器里的Linux就能检测到这个重写权限的变化，认为是文件变了，实现了通知容器这个文件变了的效果。</p>\n<p>所以，现在Windows的同学也可以愉快地一行命令启环境然后尽情开发啦。</p>\n","date_published":"2017-12-05T00:00:00.000Z","tags":["docker"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2017/%E5%89%8D%E7%AB%AF%E9%A1%B9%E7%9B%AE%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5/","url":"https://www.lihuanyu.com/posts/2017/%E5%89%8D%E7%AB%AF%E9%A1%B9%E7%9B%AE%E5%B7%A5%E7%A8%8B%E5%8C%96%E5%AE%9E%E8%B7%B5/","title":"前端项目工程化实践","summary":"17年9月起和朋友合作了一个项目，一套组件库Antue，好听点说叫造轮子。主要是把蚂蚁金服的Ant Design给“翻译\"成Vue可用的组件库。这是一个蛮正式的项目，规模也挺大，所以给了我一个实践工程化的好场景。","content_html":"<p>17年9月起和朋友合作了一个项目，<a href=\"https://github.com/zzuu666/antue\">一套组件库Antue</a>，好听点说叫造轮子。主要是把蚂蚁金服的Ant Design给“翻译&quot;成Vue可用的组件库。这是一个蛮正式的项目，规模也挺大，所以给了我一个实践工程化的好场景。</p>\n<h2>什么是前端工程化</h2>\n<p><a href=\"https://github.com/ruanyf/jstraining/blob/master/docs/engineering.md\">阮一峰 - 前端工程简介</a></p>\n<p>我认为应该是包含包管理、代码构建、代码lint、单元测试、持续集成等等的一个合集。</p>\n<p>包管理、代码构建、lint、单测在目前的前端项目中已经是标配了，用vue-cli生成的模板天然地为大家处理好了这些问题。所以我的实践主要是最后一步，持续集成。</p>\n<p>持续集成的流程、概念、好处等等在上面的阮一峰的文章中说得很明确了。</p>\n<h2>为什么需要</h2>\n<p>很多朋友在业务项目中很少使用持续集成，我也是，主要是业务上使用持续集成需要考虑成本是否划算，可能是做的业务都不太好吧，如果是一个公司的主航道业务，需要一直一直持续迭代持续开发，持续集成会是一个非常好的助手我猜。</p>\n<p>而开源的，让别人使用的lib级别的东西，再怎么严谨都是不为过的，更何况，一个优秀的持续集成工作流，能保证项目的质量，让迭代成本、维护成本变低，是一件双赢的好事。</p>\n<p>有了持续集成机制，我们可以保证每次对主干的合并后，能检查以下项：</p>\n<ul>\n<li>开发者提交的代码是否遵循了eslint的规则，保证风格一致，无低级错误</li>\n<li>开发者提交的代码是否能够通过单元测试，避免改动或重构影响其他相关的地方</li>\n<li>开发者提交的代码是否能够正常build，给出一个正确的压缩包</li>\n</ul>\n<h2>如何做</h2>\n<p>因为这是一个github开源项目，Travis是一个非常优秀、非常方便的选择。</p>\n<p>仅需要项目owner用github账户登录一下Travis，勾选需要启用的对应的项目。然后编写Travis的配置文件：.travis.yml即可。</p>\n<p>配置内容也比较简单，如下：</p>\n<pre><code class=\"language-yaml\">language: node_js\nnode_js:\n  - 8.5.0\nbefore_install:\n  - npm install\nscript:\n  - npm run lint\n  - npm test\n  - npm run build\nbefore_deploy: \n  - node scripts/generate.js -a\n  - node scripts/generate.js -r\n  - npm run build:site\ndeploy:\n  provider: pages\n  skip_cleanup: true\n  github_token: $GH_TOKEN\n  local_dir: site/dist\n  on:\n    branch: master\n</code></pre>\n<p>可以理解成生命周期的各个钩子吧，先<code>npm install</code>安装依赖，再运行正式的script：先lint，再跑单测，最后试试构建。</p>\n<p>任一环节出错就会发邮件告知项目相关人员，可以通过查看CI的log信息来检查到底是什么问题，在本地复现、修复。</p>\n<p>这里有一个关键问题是，如何保证大家认可Travis的结果，有很多开源项目都使用了Travis，但是我也见过不少Travis报着错，还在继续merge到master分支的项目。</p>\n<p>其实也很好解决，只是个理念的问题，相信制度、流程，而不是人的自觉，github对项目的设定中提供了保护分支的选项，通过把master设为保护分支，可以要求pull request必须通过CI才可以合入，我们的项目还更进一步，加了一条必须有合作者的code review。</p>\n<h2>效果如何？</h2>\n<p>首先，antue的master分支的component文件夹下的组件代码，绝对没有不符合standard规范的JS代码。</p>\n<p>其次，未来的重构中，不会有因重构导致有完善单元测试的组件出bug（很惭愧，我写的组件还没写单元测试）。</p>\n<p>最后，任何一台安装了合适版本node的能联网的电脑，都可以完美编译该项目（也许不能，毕竟目前的开发者使用的都是mac，可能会有平台兼容性问题，吹个牛逼也不犯法不是？）。很多人的Travis配置中可能只有跑了一个单元测试，但构建这一点其实蛮重要的，给人一个能编译（构建）过的项目，有问题比不能编译过的项目会好查太多太多。</p>\n<p>同时，有了Travis的构建通过邮件，大家能很开心很自信地往下写。</p>\n<p>所以有兴趣的同学，欢迎参与开发这个组件库，我们一点也不担心不同开发人员的不同风格是否会导致项目变得奇怪。</p>\n<h2>还可以做什么？</h2>\n<p>看上面的配置文件，最下面的<code>deploy</code>说明我们用Travis进行的自动部署。</p>\n<p>这个需求主要是这样的，主owner希望对antd进行像素级复制，所以文档网站也是用类似于andt的手法，通过markdown生成的（没有antd的毕昇系统那么叼，但也显得很专业嘿嘿）。</p>\n<p>发布的方式是手工执行命令：</p>\n<pre><code class=\"language-bash\">node scripts/generate.js -a\nnode scripts/generate.js -r\nnpm run build:site\ngit checkout gh-pages\ncp ./site/dist/* ./\ngit add .\ngit commit -m '第XX次 update doc'\ngit push\n</code></pre>\n<p>可以看出这个部署到github pages的操作是非常的固定的，且我们又知道我们的master分支是随时处于一个待发布的OK的状态的，那么我们为什么不让每次merge到master后就自动部署到pages上去呢？</p>\n<p>一开始我想的是通过自己编写一个bash脚本，让Travis每次来执行这个脚本，其实这个方案也是可以的，只是不好调试，费了半天劲没有解决只master版本才部署这个问题，就很伤。</p>\n<p>查了半天Travis的文档后发现它内置的部署provider里本身就有pages这种方案（Travis文档做得挺好，就是搜索功能非常不好用），就改成目前的这样。</p>\n<p>自动部署的效果很好，每完成一个新组件，合并master后，就可以立即看到效果，想参与github开源项目的同学走过路过不要错过，提交就可以拿着<a href=\"https://zzuu666.github.io/antue/\">这个网页</a>找到自己写的组件去和人吹牛逼啦！</p>\n<p>本项目在持续集成上的实践就酱紫啦，要是还有什么需要做的好点子欢迎和我分享。</p>\n","date_published":"2017-11-30T00:00:00.000Z","tags":[],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2017/%E8%B0%88%E8%B0%88CORS%E4%B8%8B%E5%89%8D%E7%AB%AF%E7%9A%84cookie/","url":"https://www.lihuanyu.com/posts/2017/%E8%B0%88%E8%B0%88CORS%E4%B8%8B%E5%89%8D%E7%AB%AF%E7%9A%84cookie/","title":"谈谈CORS下前端的cookie","summary":"跨域有多种解决方案，包括JSONP，CORS，反向代理等等。CORS是我感觉最“优雅”的一种方案。但是这个方案下的cookie有一些不一样的表现，做相关尝试后做点记录。","content_html":"<p>跨域有多种解决方案，包括JSONP，CORS，反向代理等等。CORS是我感觉最“优雅”的一种方案。但是这个方案下的cookie有一些不一样的表现，做相关尝试后做点记录。</p>\n<h1>问题起源</h1>\n<p>有人问我，为什么JS设定上的cookie，发出去后端收不到。用的是CORS解决跨域问题，JS设定的cookie在chrome的devtools里也能看到，但发出去后端收不到，请求头上都没有。</p>\n<p>不，不是没配<code>XMLHttpRequest.withCredentials</code>的问题，机智的我还是一秒想到了他问题的原因，cookie是分domain的，JS设的cookie当然是本域的cookie。</p>\n<p>是他的后端同学把要给他的cookie放到了response body里，要求他手工将这个cookie字段写入到document.cookie里，有点不能理解后端怎么想的，Set-Cookie不会用？</p>\n<p>那么，JS能不能操作别的域的cookie呢？如果不能，原因呢。有没有方法可以操作别的域的cookie呢，这些cookie我们能从哪里看到呢。</p>\n<h1>解决方案</h1>\n<p>为什么我要把解决方案放到这里，因为我最后没能非常好的解决问题，所以只能给出一些建议，后面则是对问题的表现进行的一些摸索。</p>\n<p>首先，Nginx做反向代理替代CORS来解决跨域，在开发个人小型应用时是个不错的方案我认为，除了配置Nginx一下，其它地方都和传统的WEB开发没什么区别。Cookie该怎么用怎么用，JS请求该怎么发怎么发，体验挺不错的。而且结合Docker来做这个事，在开发、集成、测试、部署整个流程下都是很方便的。</p>\n<p>其次，类似于豆瓣微博QQ开放平台等等提供API的平台，CORS是必要的，但这些API平台不用cookie、session来做身份校验，而是靠token。token的获取和发送对JS来说都是可操作的，就没有cookie什么事了，也是极好的，如果不是开放平台，JWT也是非常合适的方案。</p>\n<p>JSONP我从来没用过，感觉也像是一种注定要消失的方法，不谈。</p>\n<h1>尝试过程</h1>\n<p>代码：<a href=\"https://github.com/sky-admin/skyADMIN/tree/master/cors-demo\">https://github.com/sky-admin/skyADMIN/tree/master/cors-demo</a></p>\n<h2>服务器端准备</h2>\n<ul>\n<li>express做服务器，3000端口</li>\n<li>setcookie和getcookie两个接口</li>\n<li><a href=\"https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS\">cors配置</a></li>\n</ul>\n<h2>前端页面准备</h2>\n<ul>\n<li>纯HTML，IDEA作为临时服务器，63342端口</li>\n<li>jquery，封一下ajax，<a href=\"https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials\">设置跨域带cookie</a></li>\n<li>两个button绑定事件，分别触发set和get接口</li>\n</ul>\n<h2>效果</h2>\n<p>前端页面：</p>\n<p><img src=\"/hexo-source/_posts/%E8%B0%88%E8%B0%88CORS%E4%B8%8B%E5%89%8D%E7%AB%AF%E7%9A%84cookie/frontend-example.png\" alt=\"前端页面示例\"></p>\n<p>后端代码：</p>\n<pre><code class=\"language-js\">const express = require('express');\nconst cookieParser = require('cookie-parser');\nlet app = express();\napp.all('/*', function(req, res, next) {\n    res.header(&quot;Access-Control-Allow-Origin&quot;, &quot;http://localhost:63342&quot;);\n    res.header(&quot;Access-Control-Allow-Headers&quot;, &quot;X-Requested-With&quot;);\n    res.header(&quot;Access-Control-Allow-Methods&quot;, &quot;POST, GET, OPTIONS, DELETE&quot;);\n    res.header(&quot;Access-Control-Max-Age&quot;, &quot;3600&quot;);\n    res.header(&quot;Access-Control-Allow-Credentials&quot;,&quot;true&quot;);\n    next();\n});\napp.use(cookieParser());\napp.get('/setcookie', function (req, res) {\n    res.cookie(&quot;testkey&quot;, &quot;testvalue&quot;);\n    res.send('cookie set ok!');\n});\napp.get('/getcookie', function (req, res) {\n    res.send('cookie is ' + req.cookies.testkey);\n});\napp.listen(3000, function () {\n    console.log('Example app listening on port 3000!');\n});\n</code></pre>\n<h2>测试问题</h2>\n<h3>CORS下cookie是否正常可用</h3>\n<p>先按按钮1，向服务器发起一个请求，在开发者工具中可以看到，拿到了正确的响应，response header里也有Set-Cookie字段：\n<img src=\"/hexo-source/_posts/%E8%B0%88%E8%B0%88CORS%E4%B8%8B%E5%89%8D%E7%AB%AF%E7%9A%84cookie/cors-show1.png\" alt=\"CORS的response\"></p>\n<p>再按按钮2，request header中有Cookie字段，服务器端返回了cookie的值为testvalue。说明OK，cookie正常且可用。一开始我用CORS也基本就这样用，没有问题，也就没有好奇。</p>\n<p>现在有了问题，我们再试试别的。</p>\n<h3>JS编辑Cookie后服务器端是否能收到</h3>\n<p>通过<code>document.cookie = 'testkey=newvalue'</code>，编辑了cookie，再次点击按钮2，发现服务器端获取的cookie值没变化，看请求头也没有变化。</p>\n<p><img src=\"/hexo-source/_posts/%E8%B0%88%E8%B0%88CORS%E4%B8%8B%E5%89%8D%E7%AB%AF%E7%9A%84cookie/cors-show2.png\" alt=\"JS编辑Cookie\"></p>\n<p>这个问题的原因是cookie是分domain（域）的，不同domain的cookie不会乱发，简化一下这个问题：a.com向b.com发请求，b.com返回的Set-Cookie字段设置上的cookie，只会再下次向b.com发请求时才带上，向a.com发不会带。反过来，用JS直接设置的cookie是在本域的，即a.com上，向b.com发请求不会带上。</p>\n<p>对，这就是朋友一开始问我的问题，他的后端把cookie值放在response body里，让他手工将其设定到document.cookie里，发送出去却读不到，因为不是一个域的。</p>\n<h3>JS可以操作别的域的cookie吗</h3>\n<p>不可以，指定的domain不是本域的话，直接认为是非法的，设定不上去。</p>\n<h3>怎么看Cookie</h3>\n<h4>本域的cookie</h4>\n<ul>\n<li>document.cookie就可以看到，在控制台上打印出来是一个字符串，里面是key=value的形式，用分号分隔的。</li>\n<li>还可以用chrome浏览器的devtools，application栏下面有cookies可以显示。</li>\n</ul>\n<p><img src=\"/hexo-source/_posts/%E8%B0%88%E8%B0%88CORS%E4%B8%8B%E5%89%8D%E7%AB%AF%E7%9A%84cookie/cors-show3.png\" alt=\"devtools查看cookie\"></p>\n<p>这里有个奇怪的表现，testkey=testvalue这个cookie是属于localhost:3000的，这里应该看不见才对，应该是因为虽然端口不同，非同源，有跨域问题，但是domain相同。</p>\n<p>改一改代码，把请求发往<code>http://www.test1.com:3000</code>，修改本机的hosts文件，把这个域名指向127.0.0.1，即本机。则看不见testkey=testvalue了。</p>\n<p><img src=\"/hexo-source/_posts/%E8%B0%88%E8%B0%88CORS%E4%B8%8B%E5%89%8D%E7%AB%AF%E7%9A%84cookie/cors-show5.png\" alt=\"devtools中没有跨域cookie\"></p>\n<h4>跨域的cookie</h4>\n<p>首先，跨域的cookie也是真实存在的，第一次就证实了，它在request header上有。然而，上述两种方式都无法查看。还试过直接去目标域名查看，也没有。我甚至一度以为它根本就无法被查看。</p>\n<p>最后发现可以在这里找到它：</p>\n<p><img src=\"/hexo-source/_posts/%E8%B0%88%E8%B0%88CORS%E4%B8%8B%E5%89%8D%E7%AB%AF%E7%9A%84cookie/cors-show4.png\" alt=\"查看跨域cookie\"></p>\n<p><img src=\"/hexo-source/_posts/%E8%B0%88%E8%B0%88CORS%E4%B8%8B%E5%89%8D%E7%AB%AF%E7%9A%84cookie/cors-show6.png\" alt=\"查看跨域cookie\"></p>\n<h3>跨域的cookie能被清理掉吗</h3>\n<p>能，在devtools里，虽然看不见它，但是一旦清除所有cookie，跨域cookie也没了。</p>\n<h1>总结</h1>\n<ul>\n<li>跨域cookie存在且有效，使用起来和正常的没有区别</li>\n<li>跨域cookie无法修改，无法被document.cookie和devtools看到</li>\n<li>跨域cookie只提交到目标域，除非两者的domain相同，只有端口的区别</li>\n<li>本域的cookie不会提交到目标域</li>\n<li>跨域的cookie可以在devtools里清除掉</li>\n</ul>\n","date_published":"2017-09-02T00:00:00.000Z","tags":["CORS","cookie","前端"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2017/%E4%BD%BF%E7%94%A8Docker%E8%A7%A3%E5%86%B3%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%97%AE%E9%A2%98/","url":"https://www.lihuanyu.com/posts/2017/%E4%BD%BF%E7%94%A8Docker%E8%A7%A3%E5%86%B3%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%97%AE%E9%A2%98/","title":"使用Docker解决开发环境问题","summary":"日常开发中，我们常常被开发环境所困扰。比如我司一些老项目，使用的fis系列构建工具，需要非常低版本的node，还需要jdk。还有工作中大家使用的电脑操作系统不同，可能出现在mac上运行得好好的项目在Windows上死活跑不起来。","content_html":"<p>日常开发中，我们常常被开发环境所困扰。比如我司一些老项目，使用的fis系列构建工具，需要非常低版本的node，还需要jdk。还有工作中大家使用的电脑操作系统不同，可能出现在mac上运行得好好的项目在Windows上死活跑不起来。</p>\n<h2>问题背景</h2>\n<p>如上描述。</p>\n<p>一直认为docker可以解决这个问题，终于周末有空试着应用一次。翻了一下自己的github，发现一个去年写的springboot项目被人star了一下，就想着以该项目入手，以docker来解决该项目的环境问题。</p>\n<p>项目是这个<a href=\"https://github.com/sky-admin/springboot-blog\">springboot-blog</a>，练手的一个demo，主要实现了注册登录发表编辑浏览文章，非常的简单。</p>\n<p>这个项目的问题是要求环境jdk1.8，maven。这俩其实简单，不影响太多，一个开发者电脑上安装一个jdk和maven是一个非常正常的事情。但mysql不是，我一直非常抗拒在自己的电脑上安装mysql。就算凑齐该开发者安装了mysql，账户密码数据库表不一致也还是跑不起来的。</p>\n<p>我一直认为，一个要让他人接触的项目，应该能一键启动，一旦跑起来了，不管是继续开发新的feature还是修复遗留的bug，都会比较得心应手，因为在代码层的改动都能比较直观的看到效果。而如果是一个无法启动的项目，错误的原因有一千种可能，到底是环境还是代码还是什么问题，难以确定。大部分情况下，遇到这种项目都需要和上个开发者进行交流、询问，口口相传是最糟糕的知识管理方式。</p>\n<p>所以目标是，让人能一键启动该spring项目，看到效果。</p>\n<h2>解决过程</h2>\n<p>安利一个<a href=\"https://yeasy.gitbooks.io/docker_practice/\">docker教程</a>，基础的docker操作都有提到。</p>\n<p>docker中比较重要的两个概念：镜像、容器。类似于类和对象的关系，镜像可以生成一个容器，容器可以提供环境来运行application。</p>\n<p>所以我需要制作一个镜像，就叫spring-web吧，这个镜像应该基于ubuntu，有jdk1.8和maven以及mysql。</p>\n<p>根据教程，这个不难实现，就是编写一个Dockerfile。要注意RUN指令条数，有问题可以查docker-hub里现有的镜像的Dockerfile。可以开一个ubuntu容器在一边逐条验证，因为docker镜像非常小，它的ubuntu里连vim和git都没有，很多地方编写和验证还挺麻烦。</p>\n<p>但是想在这个dockerfile里编写mysql的安装部分时，发现了一个问题，mysql这个东西太难搞了。官方的Dockerfile里写了长长的一堆质量，还有entrypoint.sh这个貌似是启动、初始化数据库的脚本要复制进去才能怎么样等等。</p>\n<p>而且想想mysql貌似也不该提供在spring-web容器里，这个容器应该是用于开发JAVA WEB的环境，jdk和maven就够了。mysql应该单独在一个容器里，且容器仅提供mysql服务。</p>\n<p>所以问题变成了如何让两个docker容器被统一管理。就靠docker-compose，docker教程中也有。</p>\n<p>编写<a href=\"https://github.com/sky-admin/springboot-blog/blob/master/docker-compose.yml\">docker-compose.yml</a>文件，声明两个镜像，一个根据项目文件夹下的Dockerfile构建，一个则是mysql。web镜像将8080端口和本机的8080端口绑定，然后把当前文件夹和maven的m2仓库与web镜像里的路径对应起来。然后把web和db连起来，写上web启动的命令<code>mvn spring-boot:run</code>。</p>\n<p>mysql则指定一下镜像版本，数据库名和密码。（这里数据库的初始化交给spring-data-jpa来完成）</p>\n<h2>结果</h2>\n<p>欢迎大家自行尝试一下，电脑上安装上docker，其它什么环境也不需要，把项目下载或克隆到本地，输入docker-compose up。静静等待。应该能看到如下界面，两个docker容器的日志都会被交替打印到这里。</p>\n<p><img src=\"/hexo-source/_posts/%E4%BD%BF%E7%94%A8Docker%E8%A7%A3%E5%86%B3%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%97%AE%E9%A2%98/result1.png\" alt=\"result1.png\"></p>\n<p>mysql可能快一些，因为是从docker hub下载的成品镜像，而web的镜像是本地构建的，第一次可能会慢一些甚至因网络问题而出错。</p>\n<p>成功后用浏览器打开<a href=\"http://localhost:8080/articles\">http://localhost:8080/articles</a>，应该能看到空空的文章列表。数据是json格式的，没有做界面。</p>\n","date_published":"2017-08-20T00:00:00.000Z","tags":["docker"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2017/package-lock-json-%E8%AF%91/","url":"https://www.lihuanyu.com/posts/2017/package-lock-json-%E8%AF%91/","title":"package-lock.json[译]","summary":"为了更好的理解这个package lock.json是干什么的，花了一点时间把npm官网这篇文档翻译了一下，第一次翻译英文文档，很多地方拿不准，用 标注了。","content_html":"<p>为了更好的理解这个package-lock.json是干什么的，花了一点时间把<a href=\"https://docs.npmjs.com/files/package-lock.json\">npm官网这篇文档</a>翻译了一下，第一次翻译英文文档，很多地方拿不准，用<code>*</code>标注了。</p>\n<h2>副标题</h2>\n<p><code>A manifestation of the manifest</code></p>\n<p>一份配置清单的说明文件。</p>\n<h2>描述</h2>\n<p>package-lock.json 是一份在npm修改node_modules树或package.json时自动生成的文件。它精确地描述了这棵被生成的树（node_modules），这样后续的安装能生成完全一样的树，无论中间是否修改了依赖项。</p>\n<p>这个文件是希望被提交到源码仓库的，它主要用于以下几种目的：</p>\n<ul>\n<li>描述一份关系依赖树，以便队友开发、部署和持续集成时都能够精确地安装出完全相同的一个依赖树。</li>\n<li>为用户提供一个&quot;穿越时间&quot;去查看之前的node_modules而无需提交node_modules目录本身（该目录一般情况下可能很大）。</li>\n<li>让树的变化能通过代码控制的diff表现得更明显。</li>\n<li>为之前安装过的package跳过重复的元数据（metadata）优化安装过程。</li>\n</ul>\n<p>一个关键的细节是，不在项目顶层的package-lock.json文件将会被忽略，不能发表（应该是指在git里）。它和npm-shrinkwrap.json共享同个格式，在本质上它们其实是一样的，但是后者能够发表。除非使用一个部署的CLI工具或者使用发布流程制作线上包，否则是不推荐这样做的。</p>\n<p>如果package-lock.json和npm-shrinkwrap.json同时出现在一个项目的顶层，package-lock.json将会被忽略。</p>\n<h2>文件格式</h2>\n<h3>name（名称）</h3>\n<p>这个package的名字，它必须和package.json里的一样。</p>\n<h3>version（版本）</h3>\n<p>这个package的版本，它必须和package.json中的版本相匹配。</p>\n<h3>*lockfileVersion</h3>\n<p>一个整数版本，从1开始随着生成</p>\n<h3>*packageIntegrity</h3>\n<p>这是一个[subresource integrity](subresource integrity)值从package.json中创建来。*<code>No preprocessing of the package.json should be done</code>。这个字符串应该被类似于ssri这样的模块生成出来。</p>\n<h3>*preserveSymlinks（保护的符号链接）</h3>\n<p>表明该安装完成且环境变量<code>NODE_PRESERVE_SYMLINKS</code>启用。安装程序一个使该属性和环境变量匹配。</p>\n<h3>dependencies（依赖）</h3>\n<p>一份包名（package name）到依赖对象（dependency object）的映射。依赖对象有以下属性：</p>\n<h4>version（版本）</h4>\n<p>这是一个说明符，来唯一标识一个包，且应该能通过此获取一份新的该包的副本。说明符有以下几种类别：</p>\n<ul>\n<li>打包好的依赖（bundle dependencies）：不管来源，这个版本号的目的是纯粹提供信息。</li>\n<li>registry资源：这是个版本号，如：1.2.3</li>\n<li>git资源：这是一个指示着git中某次提交的标记。（比如：<code>git+https://example.com/foo/bar#115311855adb0789a0466714ed48a1499ffea97e</code>）</li>\n<li>http的打包资源：这是一个指向压缩包的URL。（比如：<code>https://example.com/example-1.3.0.tgz</code>）</li>\n<li>本地的打包资源：这是一个指向本地压缩包的<code>文件URL</code>。（比如：<code>file:///opt/storage/example-1.3.0.tgz</code>）</li>\n<li>本地的链接源：这是一个指向本地<code>文件URL</code>的链接。（比如：<code>file:libs/our-module</code>）</li>\n</ul>\n<h4>integrity（完整性）</h4>\n<p>这是一个关于该资源的标准的子资源完整性属性。</p>\n<ul>\n<li>如果是打包好的依赖将不会有该属性，不管其来源。</li>\n<li>如果是registry的资源，这个完整性（integrity）字段会由该registry来提供。<code>* or if one wasn't provided the SHA1 in shasum（不知道什么意思）</code></li>\n<li>如果是一个git资源，这将是我们克隆的那次提交的哈希值。</li>\n<li>如果是个远程压缩包，将是该文件的SHA512的值。</li>\n<li>如果是本地压缩包，也是文件的SHA512的值。</li>\n</ul>\n<h4>resolved（已解决的）</h4>\n<ul>\n<li>如果是打包好依赖，不会包含这个，不管来源。</li>\n<li>如果是registry资源，这将是该包的压缩包在registry的相对URL，如果压缩包的URL和registry的URL不同，将会是一个完整的URL。（比如百度自己的npm registry是<code>http://registry.npm.baidu.com/</code>，但安装包时从<code>http://pnpm.baidu.com拉取，就会是一个完整的URL</code>）</li>\n</ul>\n<h4>bundled（打包的）</h4>\n<p>如果为<code>true</code>，这将是一个打包好的依赖，将从父模块安装。安装时该模块将在解压阶段直接从父模块提取，而不是像独立模块一样安装。</p>\n<h4>*dev（开发，这个属性在我这里的项目中统统为ture，没理解是干嘛的）</h4>\n<p>如果为true，那么这个依赖是顶层模块的开发依赖或者传递依赖。反之，则该依赖是一个顶层模块的开发依赖且是一个顶级的非开发模块的传递依赖。</p>\n<h4>*optional（可选项）</h4>\n<p>如果是true，这个依赖是一个可选的依赖。</p>\n<h4>*dependencies（依赖）</h4>\n<p>这个依赖的依赖，和顶级的属性一样。</p>\n<h2>写在最后</h2>\n<p>该翻译仅供参考，很多地方把握不准，结合原文和实际操作对比能忽略文字更好地理解。实际操作中还发现很多和该文档说明不一致的地方，比如dependency的属性，还可能有一个叫<code>require</code>的属性，目测和dependencies一样。</p>\n","date_published":"2017-08-10T00:00:00.000Z","tags":["npm"],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2017/webpack%E6%A8%A1%E6%9D%BFmock%E6%95%B0%E6%8D%AE%E7%9A%84%E6%96%B9%E6%B3%95/","url":"https://www.lihuanyu.com/posts/2017/webpack%E6%A8%A1%E6%9D%BFmock%E6%95%B0%E6%8D%AE%E7%9A%84%E6%96%B9%E6%B3%95/","title":"vue-cli webpack模板mock数据的方法","summary":"vue cli是Vue提供的脚手架生成工具，类似于yeoman，它提供的webpack模板非常好用。但在前后端分离的开发模式下，没有提供较好的mock数据的方案。应该是留给用户自己解决，毕竟mock数据的方案比较多。我这里只介绍通过修改改模板的express服务器，添加一些路由的形式来使本地的json文件作为服务器响应返回来提供mock数据。","content_html":"<p>vue-cli是Vue提供的脚手架生成工具，类似于yeoman，它提供的webpack模板非常好用。但在前后端分离的开发模式下，没有提供较好的mock数据的方案。应该是留给用户自己解决，毕竟mock数据的方案比较多。我这里只介绍通过修改改模板的express服务器，添加一些路由的形式来使本地的json文件作为服务器响应返回来提供mock数据。</p>\n<p>问题是这样的，之前用的团队高工配置的脚手架，也是Vue框架的，它很多功能不如vue-cli提供的好用，但是mock数据部分让我印象深刻，一个项目如果想让后续的开发者、维护者上手就可以开始工作，提供一份完备的mock数据是最好的方式。</p>\n<p>之前的项目就是能轻易跑起来看效果的，除了少部分赶工直接和后端连着做的部分，其它部分都是有相应的mock数据的。效果就是ajax请求发给了本机，获取到了假数据，程序可以继续跑下去。实现方式上是通过webpack-dev-server的proxy功能。</p>\n<p>相关文档在这里，<a href=\"http://webpack.github.io/docs/webpack-dev-server.html#proxy\">http://webpack.github.io/docs/webpack-dev-server.html#proxy</a></p>\n<p>具体代码不写了，因为今天要说的是改造vue-cli提供的webpack模板，使之有类似的mock功能。</p>\n<p>首先，要仔细阅读vue-cli的<a href=\"https://vuejs-templates.github.io/webpack/proxy.html\">模板说明</a>，查看其是否已经提供了该功能。如链接，其模板提供了开发时API的proxy功能，是基于http-proxy-middleware插件实现的，看上去好像不用额外做什么事了，但按照文档试了一下，它所提供的功能和我想要的似乎有些不一致。</p>\n<p>就我目前的理解和尝试，结合相关文档，该proxy功能应该是说，有后端接口在线上运行，但是直接请求有跨域问题，或者是该项目只是MVC中的V层，在开发完成后会被放到后端模板中由后端进行渲染，所以请求往往是“/testapi”，而开发时本机服务器是node-express，并没有相应的API。通过该代理，把“/testapi”代理到真实的API去。</p>\n<p>而我的需求是我没有后端，我只有和后端约定好的接口文档和mock数据，我希望访问“<a href=\"http://targethost/apixxx%E2%80%9D%EF%BC%8C%E8%83%BD%E6%8B%BF%E5%88%B0%E6%88%91%E7%9A%84mock%E6%95%B0%E6%8D%AE%E3%80%82\">http://targethost/apixxx”，能拿到我的mock数据。</a></p>\n<p>再查查是否已经有人做好了且有完善的教程，大部分都是讲理由mock.js或者线上mock服务，或者还是刚刚那个http-proxy-middleware中间件实现的方法。</p>\n<p>在尝试了一些奇怪的方法后，突然想起，express是一个相当简单的nodejs的服务器端框架啊，可以直接修改项目的dev-server.js，对express加一些路由来处理相应的mock数据响应就好了。</p>\n<p>具体操作方法：在build/dev-server.js中找到staticPath，在app.use(staticPath ……)后，加一个app.use(‘/api1’, express.static(‘…/mock/api1.json’))，如果已经在npm run dev状态下要重启服务器（ctrl+c，再重新执行npm run dev）才能生效。</p>\n<p>通过这样的方式，我们就实现了对“/api1”的mock，在项目代码中对&quot;localhost:8080/api1&quot;进行请求就能拿到希望拿到的api1.json文件中的内容作为响应了。</p>\n<p>唔，可能有人问，这个localhost:8080不是我最终希望请求的url啊，最后上线还要一一修改岂不是很麻烦？把所有的api的url都放在一个js中，export出来即可。在这一个文件中，把url拆成baseUrl + apiPath的形式，最后只需要改baseUrl即可。如果还嫌麻烦，配置一下它和运行环境的变量的关系即可，在dev模式下，请求localhost，在qa模式下请求RD的ip，在product模式下，请求线上地址。</p>\n<p>给一个简单的例子：</p>\n<pre><code class=\"language-js\">/**\n * @file 请求URL配置文件\n */\n\n// 开发mock地址\nlet host = 'http://localhost:8080/';\n\n// 联调地址\n// let host = 'http://api.test.com/';\n\n// 上线地址\n// let host = 'http://api.production.com/';\n\nexport default {\n  login: host + 'Login',\n  logout: host + 'Login/Logout',\n  reg: host + 'Register',\n  // 省略\n}\n\n</code></pre>\n<p>通过该文件聚合所有的api url，比如这个login，我们希望拿到：</p>\n<pre><code class=\"language-json\">{\n    &quot;status&quot;: 0,\n    &quot;data&quot;: {\n        &quot;name&quot;: &quot;test&quot;\n    }\n}\n</code></pre>\n<p>就在项目根目录创建文件夹mock，内放文件login.json，内容如上。</p>\n<p>然后在build/dev-server.js中，上文提到的地方，加上app.use(‘/login’, express.static(‘…/mock/login.json’))。再在登录按钮上绑事件，向server发起login请求，就能收到希望的返回了。</p>\n<p>不过这种方式只能是get请求，如果是post请求，可以这样写：app.post(‘/login’, function(req, res) {res.send(require(‘…/mock/login.json’))});</p>\n<p>这样下去dev-server.js会越来越多，越来越大。就可以考虑把这个mock路由提到一个单独的文件来做，比如取名叫mock-map.js，export出一个方法，在dev-server.js中执行这个方法，把app作为参数传入，在mock-map.js中把路由和处理一一写好。</p>\n<pre><code class=\"language-js\">// mock-map.js\nvar express = require('express')\n\nmodule.exports = function (app) {\n    app.use('/api1', express.static('./mock/db.json'))\n    app.post('/api2', function (req, res) {res.send(require('../mock/db.json'));});\n    // 省略\n}\n</code></pre>\n<p>OVER.</p>\n","date_published":"2017-07-09T00:00:00.000Z","tags":[],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2017/%E8%A7%A3%E5%86%B3%E9%97%AE%E9%A2%98%E4%B9%8B%E9%81%93/","url":"https://www.lihuanyu.com/posts/2017/%E8%A7%A3%E5%86%B3%E9%97%AE%E9%A2%98%E4%B9%8B%E9%81%93/","title":"解决问题之道","summary":"工程师最重要的不是炫技，而是解决问题。能结合最好最新的技术解决问题，才是最吼的！","content_html":"<p>工程师最重要的不是炫技，而是解决问题。能结合最好最新的技术解决问题，才是最吼的！</p>\n<p>上周五遇上了一个问题，浪费了一天时间，简单记录下。用的是Vue框架，所提到的所有组件均指Vue组件。</p>\n<h4>前情提要</h4>\n<p>问题产生的原因是这样的：</p>\n<p>前一天封好的一个组件，在一个页面上已经正常了，第二天在另一个页面上再次调用却出现了问题。移动端的二级菜单，要求能左右滑动，能点击。</p>\n<p>因为PM想要统一的效果，iOS上原生HTML/CSS拉到头就是有回弹效果的，而Android没有。（作为一只想做全沾工程师的程序猿，希望早日打通一个产品所需的全部技能栈，以更科学合理的角度和PM讨论问题，而不是跟PM说这个没问题，能做，就是麻烦点。事实上，麻烦程度可能比想象的高，关键是收益不划算，后人维护成本高破天际）</p>\n<p>于是放弃了原生实现，改用iscroll的一个变形的库bscroll，然后因为用了别人的轮子，比较难弄明白别人到底干了什么。（事实上应该是经验不足，多看点别人的轮子的实现，再自己造几个，估计就能比较容易地看清楚他人大概的套路）</p>\n<p>然后就是，第二个页面上组件失效了，查了一下，发现第二个页面上已经使用了iscroll库做纵向滚动，先入为主的认为了scroll库在事件方面搞了一些事情，外层元素上绑定的一些事件拦截了事件的向下传递。</p>\n<p>然后就在这个事件上倒腾了一下午。先后把周边的同事都骚扰遍了，发现问题定位错了……</p>\n<h4>具体问题</h4>\n<p>最后定位到了真正的问题，组件用的id选择器，上一个页面只有一个这个组件，新页面用了两个这个组件，id选择器是独一无二的，重复的话，因为HTML/JS的宽容性，会选取到第一个。</p>\n<p>而出问题的页面，第一个组件因为样式的问题一直是隐藏状态，我都忘了这个页面有两个组件这件事了。所以一直在一个看不到的组件上绑定事件，在另一个没绑上事件的组件上疯狂调试。</p>\n<p>事实上，事件流没那么复杂，JS高程上就简单介绍了下事件捕获和事件冒泡，事件捕获从顶层document到具体的元素，事件冒泡从具体的元素一层层往上到document。</p>\n<h4>解决方案</h4>\n<p>有时我们需要用id选择器去唯一定位一个元素，但在组件中又不能用id，因为组件在一个页面多次使用会出问题。</p>\n<p>Vue作者应该是考虑到了这个所以提供了ref，可以索引子组件，也可以直接索引元素。能在组件内部起到类似于id选择器的作用，但在全局上却不会冲突。这个东西之前没用过，感谢同事指点，同事表示，Vue嘛，就不该用命令式的语法和思路，比如document.getElement……这种。嗯，道理我也懂，只是之前没考虑过元素选择上也有这种注意事项。</p>\n<h4>其它收获</h4>\n<p>技术上的收获是在事件方面，事件的调试方法，事件的几个方法。</p>\n<p>preventDefault方法：取消默认事件的行为，比如复选框点击后就会勾选，如果为复选框绑定点击事件，在处理该事件的方法里调该方法，可以取消这个勾选复选框的默认事件。而不是阻止事件的继续冒泡。对了，有的事件是不可以被取消的，不要无脑调用该方法……</p>\n<p>stopPropagation方法：这个才是阻止事件冒泡/捕获的。</p>\n<p>哦对了，怎么调试事件。打断点大家都很熟悉，但是怎么对事件打断点呢，比如有个元素上，有多个库在上面绑定了事件，那么事件触发后，是哪段JS执行了？</p>\n<p>chrome开发者工具 - sources - 右侧面板 - Event Listener Breakpoints 里列出了所有的事件，勾选想调试的事件，比如click，再去页面上点击（触发click事件），就进入到对应的代码断点了。</p>\n<h4>我想说的</h4>\n<p>排查问题的时候最基本的方法就是控制变量，把做得绝对正确的地方都排开，问题很容易看出来。这句话说起来容易，做起来嘛……如何确定你以为正确的地方是正确的。</p>\n<p>只能尽可能多学东西，把东西掌握得全面一点，就会更有底气去说，这里是绝对没问题的。</p>\n<p>还有就是，版本控制非常重要，当时也是没反应过来，我有版本控制啊，我为啥怕改老代码，iscroll库全干掉试试啊。</p>\n<p>哦对了，还要感谢同事——刚入行的应届生，不要去小公司小团队。</p>\n","date_published":"2017-04-08T00:00:00.000Z","tags":[],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2017/hello-world/","url":"https://www.lihuanyu.com/posts/2017/hello-world/","title":"Hello World","summary":"趁着新年把blog转型成简洁的hexo博客了，之前的文章本来想迁移过来，但是读了一下感觉都惨不忍睹，算了，大部分都扔掉吧，重新开始！","content_html":"<p>趁着新年把blog转型成简洁的hexo博客了，之前的文章本来想迁移过来，但是读了一下感觉都惨不忍睹，算了，大部分都扔掉吧，重新开始！</p>\n<h3>为什么要换blog系统</h3>\n<p>之前的blog是用wordpress搭建的，运行其实也算良好，按理说没有动力来更换blog系统。</p>\n<p>但是之前的wordpress确实存在一些问题：</p>\n<ul>\n<li>性能问题：wordpress是数据库形式的博客系统，每个页面的数据是存储在数据库中的，用户要看到内容需要PHP连接数据库，查询内容，渲染HTML，给用户看。这种模式，对于一些稍大的、多人的blog系统是合适的，对于我这种单人的、内容不多的，就有些不必要的性能损耗了。以hexo这种直接生成静态HTML的方式更加经济高效。</li>\n<li>安全问题：wordpress虽然市场占有率很高，但毕竟是一套开源的PHP程序，属于漏洞高发区，而一堆静态页面，我是没想到可以怎么黑……</li>\n<li>折腾问题：wordpress想稳定运行还是挺麻烦的，PHP/MYSQL/NGINX什么的都要配套装好，我当时是不会的，所以我用的一键包。显然，不去碰诸如nginx、https、node，是没法学到什么新东西的。</li>\n<li>成本问题：快毕业了，毕业后没有学生优惠，没有大把大把的廉价服务器资源了，就得在一台服务器上折腾我的所有东西，wordpress这种脆弱的blog系统显然很容易被我一不小心折腾崩溃。</li>\n</ul>\n<h3>更换过程</h3>\n<p>虽然有从wordpress迁移的插件，但是迁移后hexo生成就失败了，不知道是原文章里的一些文字刚好碰到了关键字还是什么别的原因，考虑到原来的文章的质量比较参差不齐，最后决定手工更换。（就是技术烂，复制粘贴解决问题算了）</p>\n<h3>其它</h3>\n<ul>\n<li>统计：百度统计</li>\n<li>第三方评论： DISQUS</li>\n<li>自动集成/部署：travis CI</li>\n</ul>\n<h3>自动集成（travis）</h3>\n<p>抽空弄好了CI/CD，用Travis，因为是在自己的服务器上，root的密钥不能给，单开了一个叫blog的账户。</p>\n<p>操作过程可见上一篇文章，有一些注意事项，首先，服务器上开一个新的账户叫blog，然后去blog用户目录下建个.ssh文件夹，注意先切到blog用户，否则root用户建立的文件夹，blog用户无法访问，将导致无法登陆。</p>\n<p>本地这边，两套密钥倒腾了半天，很费劲，很蓝瘦，最后发现用本地系统的新建用户来隔离两套密钥就可以了。</p>\n<h3>先这样咯。Hello Hexo。</h3>\n","date_published":"2017-01-27T00:00:00.000Z","tags":[],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2016/%E5%86%99%E4%BA%86%E4%B8%AA%E5%89%8D%E7%AB%AF%E6%B8%B2%E6%9F%93%E7%9A%84%E6%95%99%E7%A8%8B/","url":"https://www.lihuanyu.com/posts/2016/%E5%86%99%E4%BA%86%E4%B8%AA%E5%89%8D%E7%AB%AF%E6%B8%B2%E6%9F%93%E7%9A%84%E6%95%99%E7%A8%8B/","title":"写了个前端渲染的教程","summary":"写了个前端渲染的教程。 Github地址 打滚求星星。","content_html":"<p>写了个前端渲染的教程。</p>\n<p><a href=\"https://github.com/sky-admin/FE-tutorial\">Github地址</a></p>\n<p>打滚求星星。</p>\n<h4>后端渲染是什么？</h4>\n<p>首先说说以前的后端渲染的方式:</p>\n<ol>\n<li>用户向服务器发起请求</li>\n<li>服务器拦截用户请求，根据 路由/表单 确定用户的动作</li>\n<li>服务器根据用户动作执行相应操作（对数据库的增删查改吧一般是）</li>\n<li>完成操作后整理要展示给用户看的数据</li>\n<li>找到网页模板，用数据替换模板中要被替换的部分</li>\n<li>输出替换完成的html文档</li>\n</ol>\n<p>看上去好像没啥不好。</p>\n<h4>后端渲染的问题：</h4>\n<ol>\n<li>和原生应用的操作体验没法比，因为渲染是在服务器完成的，想看到新的内容必须从服务器端获取新的html文档，就会造成刷新，一个全白的页面闪现后再换成新的页面，给人的操作一种断层的感觉。</li>\n<li>对服务器的压力较大，如果有很多个用户同时请求服务器，服务器在同时要渲染N多页面，会崩溃的……</li>\n<li>如果开发多个平台的，需要额外写提供给 Native APP 的API，明明数据差不多的，再加上后面的维护，浪费人力。</li>\n</ol>\n<h4>前端渲染是什么？</h4>\n<p>和刚刚的后端渲染相比，前端渲染把 <code>用数据去替换模板</code> 这个工作放在了前端来进行。</p>\n<p>就是让JS脚本来通过AJAX向服务器获取数据，然后替换HTML内容。为什么要这样做呢？</p>\n<h4>前端渲染如何解决这些问题？</h4>\n<p>前端渲染一开始也需要服务器向用户浏览器发送一个文档，这个文档可能是空的，什么内容也没有，但是引入了js。</p>\n<p>当js加载完毕后，js会向服务器发起异步请求获取数据。然后js来把数据显示到页面上。</p>\n<p><code>就我的理解来说，web前端工程师主要做的事情，就是把这个部分捋清楚，编写合理的逻辑，让程序按顺序执行，让数据和交互更合理的发生。</code></p>\n<p>这个过程发生在用户的浏览器上，所以是前端完成的渲染（把数据/内容/样式填充到浏览器上让用户看到）过程。请求数据是异步的，页面不需要刷新，给人一种很流畅的感觉。</p>\n<p>顺序大概是：</p>\n<ol>\n<li>JS加载完成</li>\n<li>JS请求初始化接口获取初始化数据</li>\n<li>JS用数据生成HTML，展示给用户</li>\n<li>用户操作，JS根据用户操作请求对应的接口</li>\n<li>服务器根据请求的接口（路由/表单）确认用户的操作</li>\n<li>服务器执行相应逻辑，从数据库获取数据并返回</li>\n<li>JS根据接口返回的数据修改HTML，给用户看结果</li>\n</ol>\n<p>相比之下，前端的事变多了，后端的事变少了。但是解决了之前提到的一些痛点问题。</p>\n<p>所以，还没试过这种新玩法的同学们，赶紧上车啦。</p>\n","date_published":"2016-12-18T00:00:00.000Z","tags":[],"language":"zh"},{"id":"https://www.lihuanyu.com/posts/2016/%E5%AF%B9%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E7%9A%84%E6%80%9D%E8%80%83/","url":"https://www.lihuanyu.com/posts/2016/%E5%AF%B9%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E7%9A%84%E6%80%9D%E8%80%83/","title":"对前后端分离的思考","summary":"结合在学校做易班轻应用时候的一些思考，记录下我为什么要做前后端分离的历史/原因/意义/效果。","content_html":"<p>结合在学校做易班轻应用时候的一些思考，记录下我为什么要做前后端分离的历史/原因/意义/效果。</p>\n<h2>历史背景</h2>\n<p>一开始说要给易班做轻应用的时候是懵逼的，什么叫轻应用。于是分三个阶段循序渐进地来做这个事。</p>\n<h3>静态网页</h3>\n<p>首先，琢磨了一下发现，所谓轻应用，好像其实就是个网页嘛。OK，最简单的就是静态网页。</p>\n<p>做了一些诸如大物实验数据辅助处理系统，就是拿js算一些加减乘除、生活查询，就是根据客户端时间判断水房是否开门等等。</p>\n<p>找个服务器配置下LNMP（linux + nginx + mysql + php 是个服务器环境配置一键脚本），静态页面和资源丢上去，完工。</p>\n<h3>动态网页</h3>\n<p>接下来难度升级，做个带后端服务的真正的应用了。因为是给易班做，目标是吸引用户到易班上，这些轻应用我基本没有考虑过自己做用户表，就是用户信息都是直接从易班开放平台获取。</p>\n<p>这个地方涉及到Oauth2.0协议拿数据，易班开放平台做的这一个呢有一些让我觉得不舒服的限制，比如回调地址和应用地址必须是同一个，精确到完整的url，还有必须是点对点调用，就是不能回调到多个位置。（注意这是一个问题）</p>\n<p>之后技术选型，选的是springboot框架，一个神奇Java的框架，特点是开发速度特别快，让人觉得，这还是Java web框架么？它为很多东西提供缺省配置，省去编写复杂的xml文件的时间。</p>\n<p>部署上也极其方便，内嵌了tomcat，最后build出来的是一个jar包。在一台安装了jdk的电脑上输入java -jar xxxx.jar即可运行，不需要考虑tomcat的配置。（就是这一点让我放弃了PHP的laravel）</p>\n<p>拿springboot写了3、4个应用吧。比如抽奖、查询、签到等，都是这个框架，配上模板引擎thymeleaf做的。</p>\n<p>在完成了3、4个应用后，我发现了这种模式的一些问题，为了解决这些问题：</p>\n<h3>前后端分离</h3>\n<p>这个阶段里，我希望前端和后端独立部署，后端砍掉View层，把Controller层暴露出来，以API形式提供服务。</p>\n<p>前端是一种类似于Client的模式，向后端发起请求。后端吐json格式的数据。前端拿到数据后自己去渲染数据到页面上。（前端渲染，参考 {% post_link 写了个前端渲染的教程 %}）</p>\n<p>截至到离开组织，该架构初步实现，完成了一个demo级别的应用。其中后端以springboot作为框架，运行在服务器的8086还是多少端口来着有点记不清了。</p>\n<h2>原因</h2>\n<h3>前端方面</h3>\n<p>为什么后端的View层要被砍掉？因为后端不会专业的前端技能。</p>\n<p>前端上我希望向工程化、组件化、模块化看齐（好像并没有做到QAQ），要求前端工程要使用一些脚手架/脚本/node工具，进行诸如资源压缩合并混淆的工作，也就是前端其实是单独的工程。需要单独打包。</p>\n<p>那么这样的前端生成的结果如果作为V层放到后端，后端同学需要在一堆乱码中找到需要替换的变量用模板语法改写。当然我们也可以让前端同学学习下thymeleaf直接以模板语法来写。</p>\n<p>但是问题还是存在的。</p>\n<p>这将导致，应用的升级无比麻烦。仅仅只是前端样式上的一个小变化，就需要前端先修改，再打包，给后端，后端打包，再部署。必须要求前后端在一起工作，频繁交流，才可以。但这对学生开发团队太难了，都有课的人。</p>\n<h3>后端方面</h3>\n<p>除了前后端必须联调导致修改不便之外，还有一旦部署，再想修改很难这个问题。因为用户信息没有存自己的表，必须走易班授权，上线的产品修改地址要审核，要本地测试得把回调地址指回本地，改好后又要指回去，又要审核一次。我怎么给用户解释应用不见了这个问题……？卒……</p>\n<p>还有一个问题是，我前面说为什么选springboot框架时说过，是因为这个框架简单，开发简单，上手简单，部署简单。部署简单是有代价的！</p>\n<p>没在服务器上配置tomcat，打包又是jar包，相当于每个应用，都是独立的，跑在独立的tomcat容器里，运行3个应用就等于开了三个tomcat，5个应用就是5个tomcat，tomcat本来就比较重，再这么开下去，服务器很快就受不了。（虽然按微服务架构的思路来说就应该把应用划分得足够细粒度，但是确实穷，又不想优化它的部署方式，以一个简单的方式部署有利于后面做持续集成、持续部署，而且我没有充足的服务器资源）</p>\n<h2>结果</h2>\n<p>前后端分离，把所有的应用都写在一起，整合应用，只用一个tomcat装。前端请求不同的接口获得数据。还有把一些config信息从代码里转移到了yml文件里。因为生产环境和开发环境的配置文件不同，再也不用像以前那样手工反复修改代码了。</p>\n<h3>具体做法</h3>\n<p>springboot的controller用@RestController注解，提供json格式的输出，方便快捷变成一个RESTful微服务后端。</p>\n<p>前端项目直接部署到nginx服务器，阅读后端文档，自己请求API拿数据，展示。前端可以自由选择框架，angular1/2，react，vue，ember等等，反正接口在那。</p>\n<p>遇到的问题就是跨域，用CORS解决了。这里还有个坑，就是CORS这种跨域资源共享一般是结合ajax请求来使用，ajax是默认不带cookie的，通过查资料修改CORS的配置解决掉了。暂时没别的问题了。</p>\n<p>后面看时间如果有空可能拿出一个例子讲一下。</p>\n","date_published":"2016-07-23T00:00:00.000Z","tags":[],"language":"zh"}]}