<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://in-x.cc</id>
    <title>Liang's Blog</title>
    <updated>2026-08-23T09:04:49.768Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <author>
        <name>Liang</name>
        <email>liang@in-x.cc</email>
        <uri>https://in-x.cc/</uri>
    </author>
    <link rel="alternate" href="https://in-x.cc/"/>
    <link rel="self" href="https://in-x.cc/rss.xml"/>
    <subtitle>写代码、折腾工具、偶尔拍照。记录那些让我觉得「有意思」的东西。</subtitle>
    <entry>
        <title type="html"><![CDATA[curl 使用说明]]></title>
        <id>https://in-x.cc/blog/curl-guide</id>
        <link href="https://in-x.cc/blog/curl-guide"/>
        <updated>2026-07-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[curl 命令行工具常用参数与使用技巧，涵盖网页获取、文件下载、HTTP 请求发送等场景。]]></summary>
        <content type="html"><![CDATA[<p>curl 是一个功能极其强大的命令行工具，它的基本语法非常简单：</p>
<pre><code class="language-bash">curl [options] [URL...]
</code></pre>
<h2>获取网页内容</h2>
<p>直接在控制台打印网页内容。</p>
<pre><code class="language-bash">curl http://example.com
</code></pre>
<p><code>-L</code> 跟随 301/302 等重定向响应。</p>
<pre><code class="language-bash">curl -L http://www.example.com
</code></pre>
<p><code>-i</code> 显示响应头信息。</p>
<pre><code class="language-bash">curl -i http://example.com
# [响应 Header]
# [响应内容]
</code></pre>
<p><code>-I</code> 仅显示响应头信息。</p>
<pre><code class="language-bash">curl -I http://example.com
# [响应 Header]
</code></pre>
<p><code>-v</code> 显示详细通信过程，包括DNS解析、建立连接、发送的请求头和收到的响应头到网页正文整个通信过程的详细信息。</p>
<pre><code class="language-bash">curl -v http://example.com
# [DNS 解析]
# [TCP 连接]
# [TLS 握手]
# [HTTP 请求]
# ……
# [HTTP 响应]
</code></pre>
<h2>下载文件</h2>
<p><code>-o</code> （小写）指定文件名保存。</p>
<pre><code class="language-bash{2}">curl \
  -o local-file.zip \
  http://example.com/remote-file.zip
</code></pre>
<p><code>-O</code> （大写）使用远程文件名保存。</p>
<pre><code class="language-bash">curl -O http://example.com/remote-file.zip
</code></pre>
<p><code>-C -</code> 断点续传。</p>
<pre><code class="language-bash">curl -C - -O http://example.com/large-file.zip
</code></pre>
<h2>发送 HTTP 请求</h2>
<p>curl 默认就是 GET 请求。</p>
<pre><code class="language-bash">curl &quot;http://httpbin.org/get?name=Tom&amp;age=25&quot;
</code></pre>
<p>发送 POST 请求，需要设置参数：</p>
<p><code>-X</code> 指定请求方法：<code>GET</code> / <code>POST</code> / <code>PUT</code> / <code>DELETE</code> 等。</p>
<p><code>-d</code> 发送数据。</p>
<pre><code class="language-bash">curl \
  -X POST \
  -d &quot;name=Tom&amp;age=25&quot; \
  http://example.com/resource
</code></pre>
<p>发送 JSON 数据，注意必须设置 <code>Content-Type</code> 请求头，因为默认的 <code>Content-Type</code> 为 <code>application/x-www-form-urlencoded</code>。</p>
<p><code>-H</code> 指定请求头，可重复使用添加多个。</p>
<pre><code class="language-bash{3,4}">curl \
  -X POST \
  -H &quot;Content-Type: application/json&quot; \
  -d '{&quot;name&quot;:&quot;Tom&quot;, &quot;age&quot;: 25}' \
  http://example.com/api/users
</code></pre>
<p><code>@</code> 可以在 curl 参数中引用文件路径：</p>
<p>比如要发送的数据很复杂，可以保存到文件，然后通过 <code>@</code> 引用文件路径。</p>
<p>::: code-group</p>
<pre><code class="language-bash{4}">curl \
  -X POST \
  -H &quot;Content-Type: application/json&quot; \
  -d @data.json \
  http://example.com/api/users
</code></pre>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;Tom&quot;,
  &quot;age&quot;: 25
}
</code></pre>
<p>:::</p>
<p>如果要发送的请求头比较多，也可以保存到文件（需要 curl 7.55.0+）。</p>
<p>::: code-group</p>
<pre><code class="language-bash{3,4}">curl \
  -X POST \
  -H @headers.txt \
  -d @data.json \
  http://example.com/api/users
</code></pre>
<pre><code class="language-txt">User-Agent: MyClient
Authorization: Bearer abc123
Accept: application/json
</code></pre>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;Tom&quot;,
  &quot;age&quot;: 25
}
</code></pre>
<p>:::</p>
<p><code>-F</code> 可以模拟表单上传文件，这里的 <code>@</code> 与前面描述的作用一样。</p>
<pre><code class="language-bash{2}">curl \
  -F &quot;avatar=@avatar.jpg&quot; \
  http://example.com/upload
</code></pre>
<h2>其他参数</h2>
<p><code>-s</code> 静默模式，不输出任何非请求结果本身的任何信息。</p>
<p>主要作用就是只输出 HTTP 响应体，可以配合管道传输干净的数据，不会出现任何打印给人类阅读的信息或提示。</p>
<p>如果请求在底层连接阶段就失败了（例如 DNS 解析失败、无法连接到服务器、SSL 握手报错），将不会输出任何内容。</p>
<pre><code class="language-bash">curl -s http://example.com
# [响应内容]
</code></pre>
<p><code>-#</code> 下载时显示进度条。</p>
<pre><code class="language-bash">curl -# -O http://example.com/file.zip
# ######################## 100.0%
</code></pre>
<p><code>-x</code> 指定 HTTP 代理服务器进行连接。</p>
<pre><code class="language-bash{2}">curl \
  -x http://proxy.example.com \
  http://example.com
</code></pre>
<p><code>-k</code> 当访问自签名证书的HTTPS网站时，可以用此参数忽略证书错误。</p>
<pre><code class="language-bash{2}">curl -k https://example.com
</code></pre>
<p><code>--limit-rate</code> 限制下载速度，单位为 <code>B</code> / <code>K</code> / <code>M</code> / <code>G</code>。</p>
<pre><code class="language-bash{2}">curl \
  --limit-rate 1M \
  -O \
  http://example.com/file.zip
</code></pre>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Bore Cli 内网穿透指南]]></title>
        <id>https://in-x.cc/blog/bore-guide</id>
        <link href="https://in-x.cc/blog/bore-guide"/>
        <updated>2026-07-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Bore 是一个用 Rust 编写的现代、简单的 TCP 隧道工具，可将本地端口暴露给远程服务器，从而绕过标准的 NAT 连接防火墙。它的功能仅此而已：不多也不少。]]></summary>
        <content type="html"><![CDATA[<p><a href="https://github.com/ekzhang/bore">Bore</a> 是一个用 Rust 编写的现代、简单的 TCP 隧道工具，可将本地端口暴露给远程服务器，从而绕过标准的 NAT 连接防火墙。</p>
<h2>客户端</h2>
<p>macOS 使用 Homebrew 安装：</p>
<pre><code class="language-bash">brew install bore-cli
</code></pre>
<p>暴露本机 3000 端口穿透到 Bore 提供的官方服务器：</p>
<pre><code class="language-bash">bore local 3000 --to bore.pub

# 输出日志
# connected to server remote_port=37749
# listening at bore.pub:37749
</code></pre>
<p>此时 <code>localhost:3000</code> 已经可以通过 <code>bore.pub:37749</code> 直接访问。</p>
<p>如果不指定端口，Bore 会随机分配一个可用端口。你也可以通过 <code>--port</code> 参数指定一个固定端口：</p>
<pre><code class="language-bash">bore local 3000 --to bore.pub --port 45678
</code></pre>
<blockquote>
<p>注意：如果指定的端口已被占用，连接会失败。</p>
</blockquote>
<h2>服务端</h2>
<p>虽然 Bore 提供了一个官方服务器，但是从国内访问非常慢，基本不能用，所以还是需要自己部署一个服务端。</p>
<p>根据自己服务器的 Linux 发行版本和 CPU 架构前往 <a href="https://github.com/ekzhang/bore/releases">Github 发布页</a> 下载预编译的二进制文件。</p>
<pre><code class="language-bash"># 查看 Linux 发行版本
cat /etc/os-release

# 查看 CPU 架构
uname -m
</code></pre>
<pre><code class="language-bash"># 服务端：启动服务
bore server

# 客户端：连接自定义服务器
bore local &lt;local-port&gt; --to &lt;server-ip&gt;
</code></pre>
<h2>配置防火墙开放端口</h2>
<p>以阿里云为例，需要登录阿里云服务器后台开放 <code>7835</code> 端口作为 Bore 的控制端口，如果服务器上安装了宝塔面板，可能还需要到宝塔防火墙中开放对应端口。</p>
<p>当 Bore 客户端通过 <code>7835</code> 端口连接服务器的时候，Bore 会在默认端口范围随机分配端口用于穿透，所以我们还需要再开放一组端口用于分配。</p>
<p>不建议直接使用默认范围（1024-65535），可以先在防火墙开放一小段端口，然后在运行服务端时配置分配端口最小值、最大值：</p>
<pre><code class="language-bash"># 配置分配端口范围：40000-40100
bore server --min-port 40000 --max-port 40100
</code></pre>
<h2>配置密码认证</h2>
<p>默认不设置密码，一旦运行起来服务端，任何知道服务器 IP 的人都可以像你一样，通过 Bore 直接进行内网穿透。</p>
<blockquote>
<p>我们当然不希望被别人白嫖！</p>
</blockquote>
<p>其实最主要还是网络安全风险，防止被不法分子通过我们的服务器分发非法内容。</p>
<pre><code class="language-bash"># 服务端：启动服务并配置密码
bore server -s &lt;password&gt;

# 客户端：连接自定义服务器
bore local &lt;local-port&gt; --to &lt;server-ip&gt; -s &lt;password&gt;
</code></pre>
<p>只要客户端输入的密码与服务端配置的一致就可以正常连接了。</p>
<h2>总结</h2>
<p>至此，你就拥有了一个属于自己的内网穿透服务。相比 frp 等传统方案，Bore 的配置极其简单，不需要写任何配置文件，几条命令就能跑起来。</p>
<p>如果你只是偶尔需要把本地服务暴露出去调试一下，Bore 是一个刚刚好的选择。</p>
<blockquote>
<p>That’s all it does: no more, and no less</p>
<p>它的功能仅此而已：不多也不少。</p>
</blockquote>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[论 “保安执法权”]]></title>
        <id>https://in-x.cc/blog/lun-bao-an-zhi-fa-quan</id>
        <link href="https://in-x.cc/blog/lun-bao-an-zhi-fa-quan"/>
        <updated>2026-04-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[近些年，每次出现诸如保安与某某发生冲突的新闻时，社会舆论中总会涌现一些关于 “保安执法权” 的讨论。这个议题乍一听似乎很有价值，但问题在于 —— 保安是否在执法？]]></summary>
        <content type="html"><![CDATA[<blockquote>
<p><a href="https://www.zhihu.com/question/2030333131283226860/answer/2030765574314308996">如何评价福建三明一女子脚踹保安后继续上手打对方耳光却被保安正当防卫反扇耳光？</a></p>
</blockquote>
<p>近些年，每次出现诸如保安与某某发生冲突的新闻时，社会舆论中总会涌现一些关于 “保安执法权” 的讨论。</p>
<p>这个议题乍一听似乎很有价值，但问题在于 —— <strong>保安是否在执法？</strong></p>
<p>当女司机违停，保安劝阻，此时并没有涉及到所谓执法权的问题，任何公民都可以对他人的破坏规则的行为进行劝阻。</p>
<p>而后，当双方发生口角并产生肢体接触的瞬间，首先动手的人已经超出了一名合法公民对另一名合法公民可以动用的合法手段。</p>
<p>事态已经从对于规则的维护升级为某一方的违法行为，更不存在所谓的执法权问题。</p>
<p>那么我们到底在讨论什么？或者说我们到底在回避什么根本的问题？</p>
<blockquote>
<p>这两年频繁冲上热搜的保安争议，本质是当前社会的规则和共识在破灭</p>
<p>保安只是这个规则的具象化体现而已</p>
<p>……</p>
<p>但在我看来这是一种回避，回避了最尖锐的问题 —— <strong>那就是有人破坏社会规则时，该怎么办。</strong></p>
<p>—— <a href="https://www.zhihu.com/question/2030333131283226860/answer/2030765574314308996">卢诗翰</a></p>
</blockquote>
<p>我认为，中国的很多规矩就是建立在多方互相妥协的社会共识之下。</p>
<p>你越线，我制止，你退回去，这件事就算完了，社会成本极低。</p>
<p>但是现在社会上有一部分人，精致利己，当他们破坏社会软规则，如果有人劝阻，他们则咬定对方没有权利管他，必须上升到公权力的硬规则介入才肯罢休。</p>
<p>这部分人就是抓住了大部分人其实并不希望付出太多的成本来解决这件小事的心理，获得了 “打破规则的红利”。</p>
<p>当不遵守规则的人获得奖励，那其他遵守规则的人等同于受到了 “规则的惩罚”。</p>
<p>“凭什么他随便停就没事儿，我还要费时间去找车位？”</p>
<p>于是越来越多的人开始破坏规则，并且都学会了那句话：“你管得着吗？叫警察来。”</p>
<p>当足够多的人拒绝为社会默契 “充值”，而只愿意为硬规则 “买单” 时，整个社会的运行成本就会急剧上升，最终导致所有人都被锁死在一个低效率、高对抗的系统中。</p>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[寓言故事一则]]></title>
        <id>https://in-x.cc/blog/fable</id>
        <link href="https://in-x.cc/blog/fable"/>
        <updated>2026-04-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[禅宗寓言一则 —— 谁之过？]]></summary>
        <content type="html"><![CDATA[<p>知云和尚去参访石头禅师，两人聊得高兴，走到江边。船夫把沙滩上的渡船用力推入江中，船底压死了一堆虾螺蟹。</p>
<p>知云和尚看了心生怜悯，问石头禅师：“这是乘客的过错，还是船夫的过错？”</p>
<p>石头禅师直接回：“既不是乘客的过，也不是船夫的过。”</p>
<p>知云追问：“那到底是谁的过？”</p>
<p>石头厉声答：“是你的过错！”</p>
<p>知云懵了。</p>
<p>石头禅师解释：船夫推船是为谋生，乘客坐船是为过江，虾螺躲在沙里也是求生，大家都是自然行为，无心造业。罪业由心生，无心怎能算罪？倒是你，好端端的事，非要强行分别是非、贴上“杀生罪过”的标签，无中生有、自造分别心，这才是真正的过错。</p>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[yt-dlp 下载 YouTube/Bilibili 视频]]></title>
        <id>https://in-x.cc/blog/yt-dlp-download</id>
        <link href="https://in-x.cc/blog/yt-dlp-download"/>
        <updated>2026-03-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[使用yt-dlp命令行工具下载YouTube和Bilibili视频的实用教程，包括格式选择和Cookies配置。]]></summary>
        <content type="html"><![CDATA[<p>安装 <a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a></p>
<pre><code class="language-shell">scoop install yt-dlp  # windows
brew install yt-dlp   # macOS
</code></pre>
<p>下载视频</p>
<pre><code class="language-shell">yt-dlp [video_url]
</code></pre>
<p>使用浏览器 Cookies 下载视频</p>
<pre><code class="language-shell">yt-dlp --cookies-from-browser [browser] [video_url]
</code></pre>
<blockquote>
<p>支持的浏览器：brave、chrome、chromium、edge、firefox、opera、safari、vivaldi、whale。</p>
</blockquote>
<p>查看可下载的文件格式，不下载</p>
<pre><code class="language-shell">yt-dlp --list-formats [video_url]

# 输出
ID     EXT RESOLUTION FPS │   FILESIZE   TBR PROTO │ VCODEC          VBR ACODEC      ABR
────────────────────────────────────────────────────────────────────────────────────────
30280  m4a audio only     │ ≈ 35.30MiB  207k https │ audio only          mp4a.40.2  207k
100026 mp4 1920x1080   30 │ ≈143.41MiB  839k https │ av01.0.00M.10  839k video only
</code></pre>
<p>下载指定的文件格式</p>
<pre><code class="language-shell">yt-dlp --format [format_id] [video_url]

# 示例
yt-dlp --format 30280 [video_url]         # 下载音频
yt-dlp --format 100026 [video_url]        # 下载视频
yt-dlp --format 30280+100026 [video_url]  # 下载音频+视频，合并成视频
</code></pre>
<p>查看更多支持下载的视频网站：<a href="https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md">Supported sites</a></p>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[macOS Launchd 入门]]></title>
        <id>https://in-x.cc/blog/mac-launchd</id>
        <link href="https://in-x.cc/blog/mac-launchd"/>
        <updated>2026-03-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[学习macOS Launchd系统服务管理工具的入门指南，掌握配置文件语法、目录结构和常用命令，实现后台服务的自动化管理。]]></summary>
        <content type="html"><![CDATA[<p>Launchd 是 macOS 的系统服务管理工具（相当于 Windows 服务和 Linux systemd），它允许用户创建、启动、停止和配置 macOS 系统服务。</p>
<h2>Launchd 的基本概念</h2>
<p>launchd 分为两个部分：<code>launchd</code> 与 <code>launchctl</code>。</p>
<p><code>launchd</code> 是常驻后台的守护进程，负责读取各种 <code>*.plist</code> 配置进行启动、停止、重启服务等操作，是真正干活的力工。</p>
<p><code>launchctl</code> 是用来管理 <code>launchd</code> 的操作台，负责向 <code>launchd</code> 发送指令，属于下发命令的管理层。</p>
<h2>Launchd 配置文件</h2>
<p>Launchd 配置是一个 XML 格式的文本文件：</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&gt;

&lt;plist version=&quot;1.0&quot;&gt;
&lt;dict&gt;
    &lt;!-- 必填：全局唯一 ID，通常反域名 --&gt;
    &lt;key&gt;Label&lt;/key&gt;
    &lt;string&gt;com.example.hello&lt;/string&gt;

    &lt;!-- 必填：启动命令与参数 --&gt;
    &lt;key&gt;ProgramArguments&lt;/key&gt;
    &lt;array&gt;
        &lt;string&gt;/bin/bash&lt;/string&gt;
        &lt;string&gt;/Users/you/scripts/hello.sh&lt;/string&gt;
    &lt;/array&gt;

    &lt;!-- 可选：指定启动命令程序的工作目录 --&gt;
    &lt;key&gt;WorkingDirectory&lt;/key&gt;
    &lt;string&gt;/Users/you&lt;/string&gt;

    &lt;!-- 可选：开机/登录时立即运行一次 --&gt;
    &lt;key&gt;RunAtLoad&lt;/key&gt;
    &lt;true/&gt;

    &lt;!-- 可选：每 300 秒执行一次 --&gt;
    &lt;key&gt;StartInterval&lt;/key&gt;
    &lt;integer&gt;300&lt;/integer&gt;

    &lt;!-- 可选：每天 09:30 执行一次 --&gt;
    &lt;key&gt;StartCalendarInterval&lt;/key&gt;
    &lt;dict&gt;
        &lt;key&gt;Hour&lt;/key&gt;
        &lt;integer&gt;9&lt;/integer&gt;
        &lt;key&gt;Minute&lt;/key&gt;
        &lt;integer&gt;30&lt;/integer&gt;
    &lt;/dict&gt;

    &lt;!-- 可选：保持存活；true 时退出后会被拉起 --&gt;
    &lt;key&gt;KeepAlive&lt;/key&gt;
    &lt;true/&gt;

    &lt;!-- 可选：常规日志输出 --&gt;
    &lt;key&gt;StandardOutPath&lt;/key&gt;
    &lt;string&gt;/tmp/com.example.hello.out.log&lt;/string&gt;

    &lt;!-- 可选：异常日志输出 --&gt;
    &lt;key&gt;StandardErrorPath&lt;/key&gt;
    &lt;string&gt;/tmp/com.example.hello.err.log&lt;/string&gt;
&lt;/dict&gt;
&lt;/plist&gt;
</code></pre>
<p>Launchd 配置文件必须以 <code>.plist</code> 作为后缀名，文件名通常与配置中的 <code>Label</code> 一致。</p>
<p>Launchd 配置文件通常放在以下目录中，放置目录会决定自动加载行为、运行权限和启动时机：</p>
<table>
<thead>
<tr>
<th>目录</th>
<th>运行权限</th>
<th>启动时机</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong><code>/Library/LaunchDaemons/</code></strong></td>
<td><code>root</code></td>
<td>系统启动时</td>
<td>需要高权限的系统级服务，如网络守护进程、硬件监控、系统备份等</td>
</tr>
<tr>
<td><strong><code>/Library/LaunchAgents/</code></strong></td>
<td>登录用户</td>
<td>所有用户登录时</td>
<td>对所有用户可用的服务，如全局快捷键、多用户应用助手等</td>
</tr>
<tr>
<td><strong><code>~/Library/LaunchAgents/</code></strong></td>
<td>当前用户</td>
<td>当前用户登录时</td>
<td><strong>最常用</strong>，仅当前用户需要的服务，如个人定时任务、应用通知、同步工具等</td>
</tr>
</tbody>
</table>
<p>以上目录会被系统在启动/登录流程中自动扫描并加载。</p>
<p>除了等待登录时自动加载外，也可以手动 <code>bootstrap</code> 指定任意可访问路径的 <code>plist</code>。</p>
<p>使用 <code>bootstrap</code> 启动服务只是临时加载到目标域，当域的生命周期结束后丢失，下次系统启动/登录时只会自动加载配置目录中未被 <code>disable</code> 的 plist 配置。</p>
<h2>常用管理命令</h2>
<h3>配置校验</h3>
<pre><code class="language-bash"># 校验 plist 语法是否正确（推荐先执行）
plutil -lint ~/Library/LaunchAgents/com.example.hello.plist

# 查看 plist 内容（格式化输出）
plutil -p ~/Library/LaunchAgents/com.example.hello.plist
</code></pre>
<h3>服务加载与卸载</h3>
<pre><code class="language-bash"># 加载并激活服务
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.hello.plist

# 移除服务（与 bootstrap 相对）
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.example.hello.plist
</code></pre>
<p><strong>参数说明：</strong></p>
<ul>
<li><code>$(id -u)</code>：获取当前用户 ID</li>
</ul>
<h3>服务状态查看</h3>
<pre><code class="language-bash"># 列出所有已加载的服务
launchctl list

# 查找特定服务
launchctl list | grep com.example.hello

# 查看详细状态（推荐）
launchctl print gui/$(id -u)/com.example.hello

# 查看服务是否正在运行
ps aux | grep -i &quot;hello.sh&quot;
</code></pre>
<p><strong>状态字段说明：</strong></p>
<pre><code>PID    Status   Label
56789  0        com.example.hello
#      ↑        ↑
#      │        └─ 服务标识符
#      └─ 退出代码（0 表示正常）
</code></pre>
<h3>服务控制</h3>
<pre><code class="language-bash"># 重启服务（一键操作）
launchctl kickstart gui/$(id -u)/com.example.hello

# 强制停止服务（不卸载）
launchctl kill SIGTERM gui/$(id -u)/com.example.hello

# 立即触发一次运行（即使不在定时时间）
launchctl kickstart --immediate gui/$(id -u)/com.example.hello
</code></pre>
<p><strong>常用信号：</strong></p>
<table>
<thead>
<tr>
<th>信号</th>
<th>说明</th>
<th>使用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>SIGTERM</code></td>
<td>正常终止</td>
<td>优先尝试</td>
</tr>
<tr>
<td><code>SIGKILL</code></td>
<td>强制杀死</td>
<td>无响应时使用</td>
</tr>
<tr>
<td><code>SIGHUP</code></td>
<td>重新加载配置</td>
<td>支持热重载的服务</td>
</tr>
</tbody>
</table>
<h3>系统级服务（需要 sudo）</h3>
<pre><code class="language-bash"># 加载系统级服务
sudo launchctl bootstrap system /Library/LaunchDaemons/com.example.daemon.plist

# 卸载系统级服务
sudo launchctl bootout system /Library/LaunchDaemons/com.example.daemon.plist

# 查看系统服务状态
sudo launchctl print system/com.example.daemon
</code></pre>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[珂芝（Kzzi）K75 Lite 说明书]]></title>
        <id>https://in-x.cc/blog/kzzi-k75-lite</id>
        <link href="https://in-x.cc/blog/kzzi-k75-lite"/>
        <updated>2025-11-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[珂芝K75 Lite键盘使用指南，掌握电量查看、蓝牙配对、背光调节及Win/Mac系统切换等快捷键操作方法。]]></summary>
        <content type="html"><![CDATA[<p><a href="https://en.kzzi.com/media/files/20240705/20240705164922_9738.pdf">下载官方说明书</a></p>
<h2>电量指示灯</h2>
<p>按下 <code>Fn + Enter</code> 查看电量，通过数字区 <code>1~0</code> 按键背光显示剩余电量。</p>
<table>
<thead>
<tr>
<th>指示灯状态</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>红灯闪烁</td>
<td>电量低</td>
</tr>
<tr>
<td>常亮（接入电源）</td>
<td>充电中</td>
</tr>
<tr>
<td>熄灭（接入电源）</td>
<td>已充满</td>
</tr>
</tbody>
</table>
<h2>蓝牙连接</h2>
<p>将右上角开关拨动到中间位置，表示蓝牙连接模式。</p>
<p>短按 <code>Fn + Q / W / E</code> —— 切换蓝牙频道</p>
<p>长按 <code>Fn + Q / W / E</code> —— 进入配对模式</p>
<h2>键盘背光</h2>
<table>
<thead>
<tr>
<th>组合键</th>
<th>说明</th>
<th>组合键</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>Fn + \</td>
<td>切换背光效果</td>
<td>Fn + ↑</td>
<td>提高背光亮度</td>
</tr>
<tr>
<td>Fn + ,</td>
<td>切换背光颜色</td>
<td>Fn + ↓</td>
<td>降低背光亮度</td>
</tr>
</tbody>
</table>
<h2>切换修饰键</h2>
<p>按下 <code>Fn + A</code> —— 切换至 Win 系统</p>
<p>按下 <code>Fn + S</code> —— 切换至 Mac 系统</p>
<h2>功能键</h2>
<table>
<thead>
<tr>
<th>组合键</th>
<th>说明</th>
<th>组合键</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Fn + HOME</code></td>
<td>Pause</td>
<td><code>Fn + END</code></td>
<td>Insert</td>
</tr>
<tr>
<td><code>Fn + DEL</code></td>
<td>ScrLK</td>
<td><code>Fn + PGUP</code></td>
<td>PrtSc</td>
</tr>
</tbody>
</table>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[NSSM 使用说明]]></title>
        <id>https://in-x.cc/blog/nssm</id>
        <link href="https://in-x.cc/blog/nssm"/>
        <updated>2025-11-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[学习使用NSSM将普通程序注册为Windows系统服务的方法，实现开机自启和后台运行。]]></summary>
        <content type="html"><![CDATA[<p>NSSM（Non‑Sucking Service Manager）是一个轻量、稳定的 Windows 工具，用于将普通可执行程序注册为系统服务，使其能够随系统启动、在后台持续运行，并支持自动重启、进程优先级设置和日志管理，无需修改程序源码，广泛应用于后台任务、网络代理内核和长期运行的自定义工具管理。</p>
<h2>安装 NSSM</h2>
<p>前往官网 <a href="https://nssm.cc/download">下载安装</a>，或使用 WinGet 安装：</p>
<pre><code class="language-bash">winget install NSSM.NSSM
</code></pre>
<h2>NSSM 常用命令</h2>
<blockquote>
<p><code>[]</code> 表示可选参数，<code>&lt;&gt;</code> 表示必填参数。</p>
</blockquote>
<pre><code class="language-bash">nssm install [servicename]  # 新增服务
nssm remove [servicename]   # 移除服务
nssm edit &lt;servicename&gt;     # 编辑服务
</code></pre>
<pre><code class="language-bash">nssm start &lt;servicename&gt;    # 启动服务
nssm stop &lt;servicename&gt;     # 停止服务
nssm restart &lt;servicename&gt;  # 重启服务
nssm pause &lt;servicename&gt;    # 暂停服务
nssm continue &lt;servicename&gt; # 恢复服务
nssm status &lt;servicename&gt;   # 查询服务状态
</code></pre>
<p>更多命令请 <a href="https://nssm.cc/commands">查看官方文档</a>。</p>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[远程主机不满足运行 VS Code 服务器的先决条件]]></title>
        <id>https://in-x.cc/blog/vscode-remote-connection-failed</id>
        <link href="https://in-x.cc/blog/vscode-remote-connection-failed"/>
        <updated>2025-04-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[解决VS Code远程连接失败的常见问题，提供glibc兼容性和版本回退的解决方案。]]></summary>
        <content type="html"><![CDATA[<h2>问题描述</h2>
<p><a href="https://code.visualstudio.com/docs/remote/faq#_can-i-run-vs-code-server-on-older-linux-distributions">我可以在较旧的 Linux 发行版上运行 VS Code Server 吗？</a></p>
<blockquote>
<p>从 VS Code 版本 1.99（2025 年 3 月）开始，由 VS Code 分发的预构建服务器仅与基于 glibc 2.28 或更高版本的 Linux 发行版兼容。例如，这些包括 Debian 10、RHEL 8 或 Ubuntu 20.04。</p>
</blockquote>
<h2>临时解决方案</h2>
<p>这个可作为最简单的临时解决方案，先使用最后支持的旧版本连接远程服务器，后面再研究如何彻底解决问题。</p>
<p>下载 VS Code 最后可用的版本 <code>1.98.2</code> 重装即可正常连接远程服务器。</p>
<p>覆盖安装前先到设置中 <a href="vscode://settings/update.mode">关闭 VS Code 的自动更新</a>，某则重装后会自动更新。</p>
<p><a href="https://update.code.visualstudio.com/1.98.2/darwin/stable">下载 macOS Intel 芯片</a></p>
<p><a href="https://update.code.visualstudio.com/1.98.2/darwin-arm64/stable">下载 macOS Apple 芯片</a></p>
<p><a href="https://update.code.visualstudio.com/1.98.2/win32-x64/stable">下载 Windows x64</a></p>
<p><a href="https://update.code.visualstudio.com/1.98.2/win32-arm64/stable">下载 Windows Arm64</a></p>
<p>如果需要其他版本请到这里 <a href="https://code.visualstudio.com/docs/supporting/faq#_previous-release-versions">获取旧版本 VS Code 安装包</a>。</p>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[快速查询 Mac 磁盘信息]]></title>
        <id>https://in-x.cc/blog/mac-query-disk-info</id>
        <link href="https://in-x.cc/blog/mac-query-disk-info"/>
        <updated>2025-03-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[掌握使用smartmontools工具快速查询Mac磁盘健康信息的方法，包括温度、读写量、通电时间等关键指标。]]></summary>
        <content type="html"><![CDATA[<blockquote>
<p>参考：<a href="https://zhuanlan.zhihu.com/p/354385629">MacBook M1 查询硬盘写入教程 | 知乎</a></p>
</blockquote>
<h2>安装磁盘工具</h2>
<p><a href="https://www.smartmontools.org/">smartmontools</a> 是一款用于监控和分析硬盘健康状态的命令行工具。</p>
<p>通过 <a href="./homebrew-guide">Homebrew</a> 安装：</p>
<pre><code class="language-bash">brew install smartmontools
</code></pre>
<h2>查询磁盘信息</h2>
<p>安装后，即可查询磁盘信息：</p>
<pre><code class="language-bash">smartctl -a disk0
</code></pre>
<p>::: details 输出磁盘信息参考</p>
<pre><code class="language-bash">smartctl 7.4 2023-08-01 r5530 [Darwin 24.3.0 arm64] (local build)
Copyright (C) 2002-23, Bruce Allen, Christian Franke, www.smartmontools.org

=== START OF INFORMATION SECTION ===
Model Number:                       APPLE SSD AP2048Q
Serial Number:                      03a486784034c426
Firmware Version:                   499.0.9
PCI Vendor/Subsystem ID:            0x106b
IEEE OUI Identifier:                0x000000
Controller ID:                      0
NVMe Version:                       &lt;1.2
Number of Namespaces:               3
Local Time is:                      Tue Mar 11 19:05:25 2025 CST
Firmware Updates (0x02):            1 Slot
Optional Admin Commands (0x0004):   Frmw_DL
Optional NVM Commands (0x0004):     DS_Mngmt
Maximum Data Transfer Size:         256 Pages

Supported Power States
St Op     Max   Active     Idle   RL RT WL WT  Ent_Lat  Ex_Lat
 0 +     0.00W       -        -    0  0  0  0        0       0

=== START OF SMART DATA SECTION ===
SMART overall-health self-assessment test result: PASSED

SMART/Health Information (NVMe Log 0x02)
Critical Warning:                   0x00
Temperature:                        46 Celsius
Available Spare:                    100%
Available Spare Threshold:          99%
Percentage Used:                    0%
Data Units Read:                    2,122,071 [1.08 TB]
Data Units Written:                 1,233,010 [631 GB]
Host Read Commands:                 35,854,349
Host Write Commands:                14,027,979
Controller Busy Time:               0
Power Cycles:                       12
Power On Hours:                     11
Unsafe Shutdowns:                   0
Media and Data Integrity Errors:    0
Error Information Log Entries:      0

Read 1 entries from Error Information Log failed: GetLogPage failed: system=0x38, sub=0x0, code=745
</code></pre>
<p>:::</p>
<p>主要信息查看的信息就这些：</p>
<ul>
<li>Temperature：硬盘温度</li>
<li>Data Units Read：磁盘累计读取量</li>
<li>Data Units Written：磁盘累计写入量</li>
<li>Power On Hours：通电小时数</li>
</ul>
<p>懒得看的朋友，也可以直接复制所有信息丢给 AI 分析。</p>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Commitlint 使用指南]]></title>
        <id>https://in-x.cc/blog/commitlint</id>
        <link href="https://in-x.cc/blog/commitlint"/>
        <updated>2023-12-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Commitlint工具完整使用指南，结合Husky配置Git提交钩子，规范团队commit信息格式，提升代码库可读性与维护性。]]></summary>
        <content type="html"><![CDATA[<h2>简介</h2>
<p>Commitlint 是一个用于检查 Git 提交信息的工具，通过检查提交信息是否符合规范，以提高代码库的可读性和维护性。</p>
<ul>
<li>提供了一套规范化的检查规则，且支持自定义规则</li>
<li>与 Git 钩子集成，在创建提交时自动检查提交信息是否符合规范</li>
</ul>
<h2>安装</h2>
<h3>1. 安装并创建配置文件</h3>
<pre><code class="language-bash"># 安装 commitlint 和 commitlint 配置
npm install -D commitlint-cli commitlint-config-conventional
# 创建配置文件
echo &quot;import '@commitlint/config-conventional'

export default {
  extends: ['@commitlint/config-conventional'],
}&quot; &gt; commitlint.config.js
</code></pre>
<p>::: details 检查最新提交</p>
<p>检查当前当前仓库最后一次提交记录是否符合规范，如果检查未通过，会提示错误信息。</p>
<pre><code class="language-bash">npx commitlint --from HEAD~1 --to HEAD --verbose
# ✔   found 0 problems, 0 warnings
</code></pre>
<p>:::</p>
<h3>2. 配置 git hooks</h3>
<pre><code class="language-bash"># 安装 husky
npm install -D husky
# 激活 git hooks
npx husky install
# 添加 commit-msg 钩子
npx husky add .husky/commit-msg  'npx --no -- commitlint --edit ${1}'
</code></pre>
<p>::: details 测试 Git Hooks</p>
<p>可以通过简单地提交来测试该钩子，如果配置正常，您应该会看到提交失败的提示。</p>
<pre><code class="language-bash">git commit -m &quot;foo: 这条提交将会失败&quot;
</code></pre>
<p>:::</p>
<h3>3. 配置 script prepare</h3>
<p>在 package.json 中增加 prepare 脚本，用于在安装依赖时自动安装 husky。</p>
<pre><code class="language-json">{
  &quot;scripts&quot;: {
    &quot;prepare&quot;: &quot;husky install&quot;
    // other...
  }
}
</code></pre>
<h2>使用</h2>
<h3>提交格式</h3>
<blockquote>
<p>注：<code>?</code> 表示可选，不输入 <code>scope</code> 时可省略圆括号。</p>
</blockquote>
<pre><code>type(scope?): subject
body?
footer?
</code></pre>
<h3>type</h3>
<p>用于说明 commit 的类别，只允许使用下列值：</p>
<table>
<thead>
<tr>
<th>type</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>build</td>
<td>对项目构建系统或外部依赖项的更改。例如，修改构建脚本、配置文件等。</td>
</tr>
<tr>
<td>chore</td>
<td>非业务性的任务和更改。通常用于修改构建过程、辅助工具的代码，或者进行一些不涉及用户功能的工作。</td>
</tr>
<tr>
<td>ci</td>
<td>对持续集成 (Continuous Integration) 配置文件和脚本的更改。</td>
</tr>
<tr>
<td>docs</td>
<td>文档变更，包括但不限于 README 文件、文档生成器或注释的变更。</td>
</tr>
<tr>
<td>feat</td>
<td>新功能的添加。通常伴随着用户功能或其他明显的变更。</td>
</tr>
<tr>
<td>fix</td>
<td>修复 bug。</td>
</tr>
<tr>
<td>perf</td>
<td>改进性能的代码更改。</td>
</tr>
<tr>
<td>refactor</td>
<td>代码重构，不是修复 bug 也不是添加新功能的代码更改。</td>
</tr>
<tr>
<td>revert</td>
<td>撤销先前的提交。</td>
</tr>
<tr>
<td>style</td>
<td>与代码风格相关的更改，例如空格、格式化等，而不涉及功能性代码更改。</td>
</tr>
<tr>
<td>test</td>
<td>添加或修改测试。</td>
</tr>
</tbody>
</table>
<h3>body &amp; footer</h3>
<p><code>body</code> 用于提供更详细的说明，可以分成多行。</p>
<p><code>footer</code> 用于提供一些与提交信息不直接相关的信息，可以分成多行。</p>
<pre><code>feat: 防止请求竞争

引入一个请求ID和对最新请求的引用。忽略除最新请求外的所有传入响应。

移除用于解决竞争问题但现在已过时的超时机制。

参考：#123
</code></pre>
<h3>重大修改</h3>
<p>当仓库产生比较重大的修改时，例如删除了某个 API，或者改变了大量的 API，可以通过一下两种方式来表示：</p>
<h4>type!</h4>
<p>在提交信息的 <code>type</code> 后面添加 <code>!</code> 符号，表示该提交是重大更改。</p>
<pre><code>feat!: 新增了新的 API，移除了旧的 API
</code></pre>
<p><code>type</code> 可以是可选枚举值的任意一个。</p>
<h4>BREAKING CHANGE</h4>
<p>在提交信息的 <code>body</code> 中添加 <code>BREAKING CHANGE</code>，表示该提交是重大更改。</p>
<pre><code>feat: 新增了新的 API

BREAKING CHANGE: 移除了旧的 API
</code></pre>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[JSON Web Token 鉴权]]></title>
        <id>https://in-x.cc/blog/json-web-token</id>
        <link href="https://in-x.cc/blog/json-web-token"/>
        <updated>2023-12-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[JWT鉴权原理深度解析，详解Token签名生成与验证机制，结合Nuxt 3项目实践完整的用户身份认证流程。]]></summary>
        <content type="html"><![CDATA[<p>::: tip Nuxt 3 项目实例
这里提供了一个使用 Nuxt 3 开发的项目实例，你可以结合文章和项目实例来学习如何相对安全地存储用户密码。</p>
<p><a href="https://github.com/lianginx/jwt">查看项目实例 —— lianginx/jwt</a>
:::</p>
<p>JSON Web Token 是一种基于 JSON 的开放标准（RFC 7519），用于在各个层面上向用户提供身份验证。</p>
<blockquote>
<p>以下内容中将简称为 JWT 或 Token</p>
</blockquote>
<p>JWT 由三部分组成：</p>
<ul>
<li>Header</li>
<li>Payload</li>
<li>Signature</li>
</ul>
<p>其中，Header 和 Payload 都是 JSON 对象，Signature 是 Header 和 Payload 的签名，用于验证 Token 的完整性。</p>
<h2>Header</h2>
<p>Header 部分包含了 Token 签名所用的算法。</p>
<pre><code class="language-json">{
  &quot;alg&quot;: &quot;HS256&quot;,
  &quot;typ&quot;: &quot;JWT&quot;
}
</code></pre>
<pre><code class="language-ts">export interface JWTHeader {
  alg: string
  typ: string
}
</code></pre>
<h2>Payload</h2>
<p>Payload 部分包含了用户的身份信息，例如用户 ID、用户名、权限等。</p>
<pre><code class="language-json">{
  &quot;userId&quot;: &quot;1234567890&quot;,
  &quot;username&quot;: &quot;admin&quot;,
  &quot;write&quot;: true,
  &quot;exp&quot;: 1516239022
}
</code></pre>
<pre><code class="language-ts">export interface JWTPayload {
  userId: string // 用户ID
  username: string // 用户名
  write?: boolean // 管理界面权限
  exp?: number // 过期时间（暂未用到）
}
</code></pre>
<h2>Signature</h2>
<p>Signature 部分是 Header 和 Payload 的签名，用于验证 Token 的完整性。</p>
<p>首先将 Header 和 Payload 进行 Base64url 编码，然后使用 Header 中的签名算法进行签名，得出 Signature，最后将 Header、Payload 和 Signature 这三个 Base64url 字符串使用 <code>.</code> 进行拼接，就得到了完整的 Token。</p>
<pre><code class="language-ts">import { Buffer } from 'node:buffer'

/** Object 转 Base64url 字符串 */
function objectToBase64url(obj: object): string {
  return Buffer.from(JSON.stringify(obj)).toString('base64url')
}

const header = { alg: 'HS256', typ: 'JWT' }
const headerString = objectToBase64url(header)
const payloadString = objectToBase64url(payload)

const signatureString = crypto
  .createHmac('sha256', secret)
  .update(headerString)
  .update(payloadString)
  .digest('base64url')

return `${headerString}.${payloadString}.${signatureString}`
</code></pre>
<h3>验证 Token</h3>
<p>服务端验证 Token 是否有效，需要先解析 Header 获取到加密所用的算法，然后使用该算法对 Header、Payload 重新进行签名，然后比对新的签名是否与第三段签名一致，不一致则表示签名已被篡改。</p>
<pre><code class="language-ts">const [headerString, payloadString, oldSignatureString] = token.split('.')

const newSignatureString = crypto
  .createHmac('sha256', secret)
  .update(headerString)
  .update(payloadString)
  .digest('base64url')

if (newSignatureString !== oldSignatureString)
  throw new Error('无效签名')
</code></pre>
<h2>认证流程</h2>
<h3>1. 用户登录/注册</h3>
<p>用户在登录/注册页面输入用户名和密码，点击登录/注册按钮，服务端将用户名和密码在数据库中进行验证/创建，验证通过后使用用户身份信息生成一个 Token，并将 Token 返回给客户端。</p>
<h3>2. 客户端保存并使用 Token</h3>
<p>客户端将 Token 保存在 Cookie 中。</p>
<p>客户端每次请求都会自动带上 Cookie，服务器可以通过验证 Cookie 中 Token 的有效性来判断用户的身份。</p>
<h3>3. 服务端验证 Token</h3>
<p>服务端首先检查请求的 Cookie 中是否有 Token，如果没有则返回错误信息。</p>
<p>如果有 Token，则通过解析 Token 验证它的有效性，没有通过验证则返回错误信息。</p>
<p>验证 Token 的有效性包括但不限于：</p>
<ul>
<li>Token 是否被篡改</li>
<li>Token 是否过期</li>
</ul>
<p>如果验证通过，则继续返回用户需要的资源。</p>
<h2>权限控制</h2>
<p>目前实现比较简单，通过解析 JWT Payload 对象，获取并校验 <code>write</code> 字段来判断用户是否有权限访问管理界面。</p>
<pre><code class="language-ts">if (!payload.write) {
  // 处理没有权限的情况
}
</code></pre>
<h2>常见问题</h2>
<h3>1. 如何防止 Token 被篡改？</h3>
<p>在生成 JSON Web Token 时，服务器会使用一个只存在于服务端的密钥 <code>secret</code> 来生成每个 Token 的第三段的签名。</p>
<p>当客户端使用 Token 时，服务端会使用相同的密钥根据 Token 的第一段和第二段重新生成 Token 的签名，如果两个签名不一致，则说明 Token 被篡改了。</p>
<p><strong>所以存在服务端的密钥 <code>secret</code> 很关键，一定不要泄漏它！</strong></p>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[加密储存用户密码]]></title>
        <id>https://in-x.cc/blog/save-password</id>
        <link href="https://in-x.cc/blog/save-password"/>
        <updated>2023-12-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[学习用户密码加密存储的最佳实践，包括哈希函数选择、盐值使用和Node.js实现方法。]]></summary>
        <content type="html"><![CDATA[<p>::: tip Nuxt 3 项目实例
这里提供了一个使用 Nuxt 3 开发的项目实例，你可以结合文章和项目实例来学习如何相对安全地存储用户密码。</p>
<p><a href="https://github.com/lianginx/password">查看项目实例 —— lianginx/password</a>
:::</p>
<p>在设计登录注册系统时，存储用户密码的方式非常重要，因为这关系到用户数据的安全。以下是一些关于如何安全存储用户密码的建议：</p>
<ol>
<li>
<p><strong>不要明文存储密码</strong>：你绝对不应该在数据库中明文存储用户的密码。这是因为如果你的数据库被黑客攻击，那么用户的密码就会被直接暴露。</p>
</li>
<li>
<p><strong>使用哈希函数</strong>：你应该使用哈希函数来存储用户密码的哈希值，而不是密码本身。哈希函数可以将任意长度的输入转化为固定长度的输出，且具有单向性，即不能从哈希值反推出原始输入。</p>
</li>
<li>
<p><strong>使用盐值</strong>：为了防止彩虹表攻击，你应该为每个用户的密码添加一个随机的盐值，然后将密码和盐值一起进行哈希。盐值应该是足够长且随机的，以确保每个用户的盐值都是唯一的。</p>
</li>
<li>
<p><strong>使用强哈希函数</strong>：你应该使用被广泛认可的强哈希函数，如SHA-256，SHA-3，bcrypt，scrypt，Argon2 等。</p>
</li>
<li>
<p><strong>密码更新</strong>：如果用户更改了密码，你应该生成一个新的盐值，并使用新的盐值和新密码计算新的哈希值。</p>
</li>
<li>
<p><strong>使用多重哈希</strong>：为了增加黑客破解密码的难度，你可以对密码进行多次哈希。</p>
</li>
</ol>
<p>请注意，这些只是一些基本的建议，实际的密码存储策略可能需要根据你的具体需求和环境进行调整。在处理用户密码时，你应该始终遵循最佳的安全实践，并在可能的情况下，寻求专业的安全建议。</p>
<h2>密码储存、验证</h2>
<p>当用户注册时，你会生成一个随机的盐值，然后将这个盐值和用户的密码一起进行哈希，得到哈希值。你需要在数据库中存储这个哈希值和对应的盐值。</p>
<p>当用户登录时，他们会提供他们的用户名和密码。你首先需要从数据库中获取与该用户名对应的盐值和哈希值。然后，你将这个盐值和用户提供的密码一起进行同样的哈希操作，得到一个新的哈希值。如果这个新的哈希值和数据库中存储的哈希值相同，那么密码就是正确的。</p>
<p>这是因为哈希函数是单向的，你不能从哈希值反推出原始的密码。但是，如果你有原始的密码和盐值，你可以通过同样的哈希操作得到同样的哈希值。所以，你需要保存盐值，以便你可以验证用户提供的密码是否正确。但请注意，你应该保护好盐值，不要让它们泄露。</p>
<h2>盐值的作用</h2>
<p>如果数据库被泄露，盐值也会被泄露。但是，即使在这种情况下，使用盐值仍然可以提高安全性。</p>
<p>盐值的主要目的是防止彩虹表攻击。彩虹表是一种预先计算好的哈希值到原始密码的映射表。如果没有使用盐值，攻击者可以使用彩虹表来查找哈希值对应的原始密码。但是，如果使用了盐值，即使两个用户的密码相同，由于盐值的随机性，他们的哈希值也会不同。这就意味着攻击者不能使用同一份彩虹表来攻击所有用户，他们必须为每个盐值单独生成一份彩虹表，这在计算上是不可行的。</p>
<p>此外，即使数据库被泄露，攻击者也无法直接获取到用户的密码。他们必须通过尝试大量的可能的密码，找到一个和盐值一起哈希后能够得到同样哈希值的密码，这通常需要大量的时间和计算资源。</p>
<p>总的来说，虽然使用盐值不能完全防止密码被破解，但它可以大大增加破解的难度，从而提高系统的安全性。当然，最好的做法是采取多种措施来保护用户数据，包括使用强密码，定期更换密码，使用二次验证等。</p>
<h2>Node.js 生成盐值</h2>
<p>在 Node.js 中，你可以使用 <code>crypto</code> 库来生成一个安全的随机盐值。以下是一个例子：</p>
<pre><code class="language-javascript">const crypto = require('node:crypto')

// 生成一个16字节的盐值
const salt = crypto.randomBytes(16).toString('hex')

console.log(salt)
</code></pre>
<p>这段代码会生成一个 32 个字符长的十六进制字符串作为盐值。这个盐值是随机的，并且长度足够长，以确保每个用户的盐值都是唯一的。你可以将这个盐值和用户的密码一起进行哈希，然后将哈希值和盐值一起存储在数据库中。</p>
<p>当需要验证密码时，你可以使用同样的盐值和哈希函数来计算哈希值，然后将计算出的哈希值与存储在数据库中的哈希值进行比较。如果两个哈希值相同，那么密码就是正确的。</p>
<p>这样，即使数据库被泄露，攻击者也无法直接获取到用户的密码，因为他们需要知道原始的密码和盐值才能计算出正确的哈希值。这大大增加了攻击的难度，提高了系统的安全性。</p>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[二分查找]]></title>
        <id>https://in-x.cc/blog/binary-search</id>
        <link href="https://in-x.cc/blog/binary-search"/>
        <updated>2023-07-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[详解二分查找算法原理与JavaScript实现，掌握O(log n)时间复杂度的高效搜索方法，适用于有序数组查找场景。]]></summary>
        <content type="html"><![CDATA[<h2>简介</h2>
<p>二分查找（Binary Search）是一种高效的搜索算法，用于在 <strong>有序数组（或有序列表）</strong> 中查找特定元素的位置。它将目标值与数组的中间元素进行比较，并根据比较结果缩小搜索范围，直到找到目标值或确定目标值不存在。</p>
<p><strong>二分查找的关键点是每次迭代都能将搜索范围缩小一半</strong>，因此其时间复杂度为 <code>O(log n)</code> ，其中 n 是数组的长度。这使得二分查找成为处理大规模有序数据集的有效算法。</p>
<h2>代码演示</h2>
<p>二分查找的基本思想：</p>
<ol>
<li>首先，确定数组的左边界 <code>left</code> 和右边界 <code>right</code> ，通常初始时 <code>left = 0</code> ， <code>right = 数组长度 - 1</code> 。</li>
<li>在每一次迭代中，计算中间元素的索引 <code>mid</code> ，使用 <code>floor</code> 函数将中间值可能的小数部分向下向下取整。</li>
<li>将目标值与中间元素 <code>nums[mid]</code> 进行比较：
<ul>
<li>如果 <code>nums[mid]</code> 等于目标值，表示找到了目标值，返回 <code>mid</code> 。</li>
<li>如果 <code>nums[mid]</code> 大于目标值，表示目标值可能在左侧，更新查询边界 <code>right = mid - 1</code> 。</li>
<li>如果 <code>nums[mid]</code> 小于目标值，表示目标值可能在右侧，更新查询边界 <code>left = mid + 1</code> 。</li>
</ul>
</li>
<li>重复执行步骤 2 和步骤 3，直到找到目标值或搜索范围为空（ <code>left &gt; right</code> ），此时目标值不存在，返回 -1。</li>
</ol>
<p>以下是 JavaScript 代码演示：</p>
<pre><code class="language-js">function binarySearch(nums, target) {
  let left = 0
  let right = nums.length - 1

  while (left &lt;= right) {
    const mid = Math.floor((left + right) / 2)

    if (nums[mid] === target)
      return mid
    else if (nums[mid] &gt; target)
      right = mid - 1
    else
      left = mid + 1
  }

  return -1
}
</code></pre>
<h2>进阶练习</h2>
<p><a href="https://leetcode.cn/problems/search-insert-position/">LeetCode - 搜索插入位置</a></p>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[macOS 允许安装任何来源软件]]></title>
        <id>https://in-x.cc/blog/mac-app-corruption-issue</id>
        <link href="https://in-x.cc/blog/mac-app-corruption-issue"/>
        <updated>2023-06-27T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[解决macOS应用损坏无法打开及身份不明开发者问题，通过终端命令开启任何来源选项，允许安装第三方软件。]]></summary>
        <content type="html"><![CDATA[<h2>遇到的问题</h2>
<ul>
<li>“XXX”已损坏，无法打开。你应该将它移动到废纸篓。</li>
<li>已阻止使用“XXX”，因为来自身份不明的开发者。</li>
</ul>
<h2>解决方案</h2>
<blockquote>
<p>设置入口：系统设置 &gt; 安全性与隐私 &gt; 安全性 &gt; 允许从以下位置下载的应用程序</p>
</blockquote>
<h3>临时处理</h3>
<p>当遇到以上问题时，打开设置会看到应用被拦截的提示：</p>
<blockquote>
<p>已组织使用“XXX”，因为来自身份不明的开发者。</p>
</blockquote>
<p>并给予「<strong>仍要打开</strong>」的按钮，点击打开之后便可以正常使用。</p>
<h3>关闭拦截</h3>
<p>将「允许从以下位置下载的应用程序」修改为「<strong>任何来源</strong>」。</p>
<p>如果没有此选项，打开终端执行命令后重启系统设置将显示此选项：</p>
<pre><code class="language-sh">sudo spctl --master-disable
</code></pre>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Homebrew 安装与使用]]></title>
        <id>https://in-x.cc/blog/homebrew-guide</id>
        <link href="https://in-x.cc/blog/homebrew-guide"/>
        <updated>2023-06-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[macOS Homebrew包管理器安装与使用指南，掌握brew install、search、upgrade等常用命令，高效管理软件包。]]></summary>
        <content type="html"><![CDATA[<p>Homebrew 是一个软件包管理器，支持 macOS 和 Linux，它可以让你一个命令快速安装软件，而不需要再去下载之后用鼠标把 APP 拖到应用程序文件夹中。</p>
<h2>如何安装</h2>
<p>安装脚本来自：<a href="https://brew.idayer.com/">Homebrew 国内镜像</a>，安装成功后需要重启终端才能使用。</p>
<pre><code class="language-sh">/bin/bash -c &quot;$(curl -fsSL https://gitee.com/ineo6/homebrew-install/raw/master/install.sh)&quot;
</code></pre>
<blockquote>
<p>默认使用中科大镜像源，如果需要换源参考 <a href="https://brew.idayer.com/guide/change-source/">换源</a>。</p>
</blockquote>
<h2>常用命令</h2>
<table>
<thead>
<tr>
<th>命令</th>
<th>说明</th>
<th>其他</th>
</tr>
</thead>
<tbody>
<tr>
<td>brew install [–cask] [name]</td>
<td>安装软件</td>
<td>cask 表示 GUI 软件</td>
</tr>
<tr>
<td>brew uninstall [name]</td>
<td>卸载软件</td>
<td></td>
</tr>
<tr>
<td>brew search [name]</td>
<td>搜索软件</td>
<td><a href="https://formulae.brew.sh/">通过网页搜索</a></td>
</tr>
<tr>
<td>brew info [name]</td>
<td>查看软件信息</td>
<td></td>
</tr>
<tr>
<td>brew list</td>
<td>查看已安装软件</td>
<td></td>
</tr>
<tr>
<td>brew update</td>
<td>更新 Homebrew</td>
<td></td>
</tr>
<tr>
<td>brew upgrade [name]</td>
<td>更新全部（某个）软件</td>
<td></td>
</tr>
<tr>
<td>brew cleanup [name]</td>
<td>清理全部（某个）软件的历史版本</td>
<td></td>
</tr>
<tr>
<td>brew tap [user/repo]</td>
<td>添加 tap</td>
<td></td>
</tr>
<tr>
<td>brew tap</td>
<td>查看已添加的 tap</td>
<td></td>
</tr>
<tr>
<td>brew untap [user/repo]</td>
<td>删除 tap</td>
<td></td>
</tr>
</tbody>
</table>
<h2>常用软件包</h2>
<h3>命令行工具</h3>
<pre><code class="language-bash">brew install eza        # 现代化 ls（带图标和颜色）
brew install bat        # 现代化 cat（语法高亮）
brew install fzf        # 模糊搜索（Ctrl+R 历史命令）
brew install zoxide     # 快速跳转目录（cd 替代品）
brew install ripgrep    # 现代化 grep（速度更快）
brew install fd         # 快速查找文件
brew install gh         # GitHub CLI
brew install starship   # 终端美化提示符
brew install yazi       # 终端文件管理器
brew install yt-dlp     # 视频下载
brew install pyenv      # Python 版本管理
brew install jq         # JSON 处理
</code></pre>
<h3>GUI 应用</h3>
<pre><code class="language-bash">brew install --cask ghostty                 # 终端模拟器
brew install --cask visual-studio-code      # vscode
brew install --cask google-chrome           # Chrome 浏览器
brew install --cask wechat                  # 微信
brew install --cask telegram                # Telegram
brew install --cask obsidian                # markdown 编辑器
brew install --cask iina                    # 视频播放器
brew install --cask orbstack                # Docker 虚拟机（比 Docker Desktop 轻量）
brew install --cask karabiner-elements      # 键盘映射
brew install --cask qlmarkdown              # 空格预览 Markdown
brew install --cask chatgpt                 # ChatGPT 客户端
brew install --cask apifox                  # API 调试工具
brew install --cask netnewswire             # RSS 阅读器
brew install --cask font-maple-mono-nf-cn   # 编程字体
</code></pre>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Mac Dock Bar 易用性调整]]></title>
        <id>https://in-x.cc/blog/mac-dock-bar</id>
        <link href="https://in-x.cc/blog/mac-dock-bar"/>
        <updated>2023-06-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[通过终端命令优化macOS Dock栏体验，自定义自动隐藏延迟与动画速度，提升应用切换效率与操作流畅度。]]></summary>
        <content type="html"><![CDATA[<blockquote>
<p>参考：<a href="https://sspai.com/post/33366">通过终端命令调整 Dock 栏的隐藏速度｜一日一技 · Mac</a></p>
</blockquote>
<h2>前置条件</h2>
<p>右键点击 Dock 栏，选择 <code>启用隐藏</code> 后，再进行以下配置。</p>
<h2>移除显示延迟</h2>
<pre><code class="language-sh"># 移除延迟
defaults write com.apple.dock autohide-delay -int 0; killall Dock;
# 恢复默认
defaults delete com.apple.dock autohide-delay; killall Dock;
</code></pre>
<h2>调整动画速度</h2>
<pre><code class="language-sh"># 无动画
defaults write com.apple.dock autohide-time-modifier -float 0; killall Dock;
# 动画 0.5s
defaults write com.apple.dock autohide-time-modifier -float 0.5; killall Dock;
# 恢复默认
defaults delete com.apple.dock autohide-time-modifier; killall Dock;
</code></pre>
<h2>恢复出厂设置</h2>
<pre><code class="language-sh">defaults delete com.apple.dock; killall Dock;
</code></pre>
<p>此命令会把 Dock 栏恢复成出厂时的默认状态（包括图标）。</p>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Nuxt 3 如何使用 ECharts 图表]]></title>
        <id>https://in-x.cc/blog/nuxt3-use-echarts</id>
        <link href="https://in-x.cc/blog/nuxt3-use-echarts"/>
        <updated>2023-04-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[在Nuxt 3项目中集成ECharts图表库的完整教程，解决服务端渲染兼容性和组件配置问题。]]></summary>
        <content type="html"><![CDATA[<blockquote>
<p>参考： <a href="https://stackblitz.com/edit/github-unqktr">Vue Echarts Nuxt3</a></p>
</blockquote>
<h2>安装依赖</h2>
<pre><code class="language-bash">yarn add echarts vue-echarts
</code></pre>
<h2>通用/服务端渲染</h2>
<p>如果项目使用通用/服务端 <a href="https://nuxt.com.cn/docs/guide/concepts/rendering">渲染模式</a>（Nuxt 3 默认渲染模式），修改 <code>nuxt.config.ts</code> 配置文件，使用 <a href="https://www.babeljs.cn/docs/">Babel</a> 转译 <code>echarts</code> 依赖项，未使用服务端渲染不用转译。</p>
<pre><code class="language-ts">export default defineNuxtConfig({
  // ……
  build: {
    transpile: [/echarts/],
  },
})
</code></pre>
<p>另外，在使用图表组件时也须将组件放在 <a href="https://nuxt.com.cn/docs/api/components/client-only#clientonly"><code>&lt;ClientOnly&gt;</code></a> 标签中使用：</p>
<pre><code class="language-html">&lt;ClientOnly&gt;
  &lt;VChart :option=&quot;option&quot;/&gt;
&lt;/ClientOnly&gt;
</code></pre>
<h2>开始使用</h2>
<p>以下是在 Nuxt 3 服务端渲染项目中，封装的一个饼图组件示例：</p>
<pre><code class="language-vue">&lt;script setup&gt;
// 引入饼图
import { PieChart } from 'echarts/charts'

// 按需引入组件
import {
  LegendComponent,
  TitleComponent,
  TooltipComponent,
} from 'echarts/components'

// 引入 echarts 核心模块，核心模块提供了 echarts 使用必须要的接口。
import { use } from 'echarts/core'
// 引入 CanvasRenderer 或者 SVGRenderer 是，这必须的一步
import { CanvasRenderer } from 'echarts/renderers'
import { ref } from 'vue'
// 引入 vue-echarts 组件
import VChart, { THEME_KEY } from 'vue-echarts'

// 注册引入的组件
use([
  CanvasRenderer,
  PieChart,
  TitleComponent,
  TooltipComponent,
  LegendComponent,
])

// 设置黑暗模式主题
provide(THEME_KEY, 'dark')

// 图表数据
const option = ref({
  title: {
    text: 'Traffic Sources',
    left: 'center',
  },
  tooltip: {
    trigger: 'item',
    formatter: '{a} &lt;br/&gt;{b} : {c} ({d}%)',
  },
  legend: {
    orient: 'vertical',
    left: 'left',
    data: ['Direct', 'Email', 'Ad Networks', 'Video Ads', 'Search Engines'],
  },
  series: [
    {
      name: 'Traffic Sources',
      type: 'pie',
      radius: '55%',
      center: ['50%', '60%'],
      data: [
        { value: 335, name: 'Direct' },
        { value: 310, name: 'Email' },
        { value: 234, name: 'Ad Networks' },
        { value: 135, name: 'Video Ads' },
        { value: 1548, name: 'Search Engines' },
      ],
      emphasis: {
        itemStyle: {
          shadowBlur: 10,
          shadowOffsetX: 0,
          shadowColor: 'rgba(0, 0, 0, 0.5)',
        },
      },
    },
  ],
})
&lt;/script&gt;

&lt;template&gt;
  &lt;div&gt;
    &lt;!-- 客户端组件 --&gt;
    &lt;ClientOnly fallback-tag=&quot;div&quot; fallback=&quot;Loading comments...&quot;&gt;
      &lt;VChart class=&quot;chart&quot; :option=&quot;option&quot; /&gt;
    &lt;/ClientOnly&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;style scoped&gt;
.chart {
  height: 100vh;
}
&lt;/style&gt;
</code></pre>
<h2>相关链接</h2>
<h3>ECharts</h3>
<ul>
<li><a href="https://echarts.apache.org/zh/option.html">API 文档</a></li>
<li><a href="https://echarts.apache.org/examples/zh/index">全部图表示例</a></li>
<li><a href="https://echarts.apache.org/zh/option.html">配置项手册</a></li>
<li><a href="https://echarts.apache.org/zh/download-extension.html">扩展下载</a></li>
<li><a href="https://echarts.apache.org/zh/download-theme.html">主题下载</a></li>
</ul>
<h3>Vue-ECharts</h3>
<ul>
<li><a href="https://github.com/ecomfe/vue-echarts">使用文档</a></li>
<li><a href="https://vue-echarts.dev/">全部示例</a></li>
</ul>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Dexie.js 入门教程]]></title>
        <id>https://in-x.cc/blog/dexie-js-guide</id>
        <link href="https://in-x.cc/blog/dexie-js-guide"/>
        <updated>2023-04-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Dexie.js入门教程，简化浏览器IndexedDB数据库操作，提供JavaScript与TypeScript环境下的增删改查实践方案。]]></summary>
        <content type="html"><![CDATA[<h2>简介</h2>
<p>Dexie.js 是一款流行的客户端 JavaScript 库，它提供了一个简单而优雅的 API，用于操作 indexedDB，一种强大的基于浏览器的数据库系统。使用 Dexie.js，您可以轻松存储和检索数据，而无需直接处理 indexedDB 的复杂性。</p>
<p>本教程将介绍使用 Dexie.js 的基础知识，包括安装、创建数据库实例、定义对象存储区和索引、向数据库添加数据、更新数据、查询数据和删除数据。</p>
<p>如果你想先了解 indexedDB 基础可以学习 <a href="https://www.tangshuang.net/3735.html">IndexedDB 中文教程</a>，这篇文章详细介绍了 IndexedDB 的基础知识和原生 API 使用方法，非常推荐！</p>
<h2>安装</h2>
<p>您可以使用 npm 或 yarn 安装 Dexie.js：</p>
<pre><code class="language-bash">npm install dexie
</code></pre>
<pre><code class="language-bash">yarn add dexie
</code></pre>
<h2>快速开始</h2>
<h3>JavaScript</h3>
<pre><code class="language-js">// 导入
const Dexie = require('dexie')

// 创建并连接数据库实例
const db = new Dexie('myDatabase')

// 指定版本，创建对象仓库和索引
db.version(1).stores({
  friends: 'name, age', // 第一个索引将被作为主键
})

// 增加
db.friends.add({
  name: 'Liang',
  age: 18,
})

// 更新
db.friends.put({
  name: 'Liang', // 当主键存在时，将更新数据，不存在则创建
  age: 24,
})

// 查询
db.friends
  .get('Liang')
  .then((item) =&gt; {
    console.log(item?.age)
  })

// 删除
db.friends.delete('Liang')
</code></pre>
<h3>TypeScript</h3>
<blockquote>
<p>此方法来自 <a href="https://dexie.org/docs/Typescript">官方文档 &gt; TypeScript</a></p>
</blockquote>
<p>在 TypeScript 中，当你定义了 <code>friends</code> 对象仓库后，并不能直接像在 JavaScrip 中一样直接操作对象仓库进行增删改查：</p>
<pre><code class="language-js">db.friends.add({ // 报错：类型 “Dexie” 上不存在属性 “friends”
  name: 'Liang',
  age: 18
})
</code></pre>
<p>因为 TypeScript 无法检测到隐式定义的对象，此时可以使用 <code>Dexie.table()</code> 方法解决：</p>
<pre><code class="language-ts">db.table('friends').add({
  name: 'Liang',
  age: 18
})
</code></pre>
<p>但是，这不仅麻烦，而且丢失了 TypeScript 特性：智能代码补全和类型安全校验。</p>
<p>此时，可以通过扩展一个 <code>Dexie</code> 子类，并在子类中定义其运行时才存在 <code>Table</code> 属性：</p>
<pre><code class="language-ts">// 导入
import Dexie from 'dexie'

// 定义 friends 类型接口
interface IFriends {
  name: string
  age: number
}

class HappyDB extends Dexie {
  // 声明 friends 对象并指定其类型接口
  friends!: Dexie.Table&lt;IFriends&gt;

  constructor(databaseName: string) {
    super(databaseName)
    this.version(1).stores({
      friends: 'name, age',
    })
  }
}
</code></pre>
<blockquote>
<p><code>!</code> 是 TypeScript 中的非空断言操作符，用于告诉编译器当前变量肯定是非空的，从而避免出现空值异常。</p>
</blockquote>
<blockquote>
<p><code>super()</code> 是在子类构造函数中调用其父类构造函数的关键字，用于实现继承。当子类调用 <code>super()</code> 时，它会执行父类的构造函数，以便对父类的属性和方法进行初始化。</p>
</blockquote>
<p>现在你只需要像上面 JavaScrip 中一样使用它：</p>
<pre><code class="language-ts">const db = new HappyDB('HappyDatabase')

db.version(1).stores({
  friends: 'name, age',
})

// ……
</code></pre>
<h3>Node.js</h3>
<p>当你在 Node.js 环境中使用 Dexie.js 的时候，会提示缺少 indexedDB API：</p>
<blockquote>
<p>DexieError [DatabaseClosedError]: MissingAPIError 缺少 IndexedDB API。请访问 <a href="https://tinyurl.com/y2uuvskb">https://tinyurl.com/y2uuvskb</a> 。</p>
</blockquote>
<p>这是因为 indexedDB 是前端浏览器中的本地数据库，挂载在 window 上的全局对象，在 Node.js 中是没有的这个数据库和 API 的。</p>
<p>在 Dexie.js 报错信息提示的官方文档中，提供了解决方案：</p>
<ul>
<li><a href="https://github.com/dumbmatter/fakeIndexedDB#with-dexie-and-other-indexeddb-api-wrappers">将 Dexie 与 fakeIndexedDB 一起使用</a></li>
<li><a href="https://github.com/indexeddbshim/IndexedDBShim#node-set-up">将 Dexie 与 IndexedDBShim 一起使用</a></li>
</ul>
<p><code>fakeIndexedDB</code> 是通过纯 JS 内存实现的 IndexedDB API，它的主要用途是在 Node.js 中测试依赖 IndexedDB 的代码。</p>
<p><code>IndexedDBShim</code> 是一个可以在所有桌面和移动浏览器以及 Node.js 中使用的 indexedDB API。</p>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[本地存储：localStorage 和 sessionStorage 的差异]]></title>
        <id>https://in-x.cc/blog/js-local-storage</id>
        <link href="https://in-x.cc/blog/js-local-storage"/>
        <updated>2023-03-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[localStorage与sessionStorage差异详解，对比存储时效、作用域与使用场景，掌握前端本地存储最佳实践方案。]]></summary>
        <content type="html"><![CDATA[<h2>简介</h2>
<p>本地存储是一种在浏览器端存储数据的技术，它允许在不需要服务器支持的情况下，将数据保存在用户的本地浏览器中。</p>
<p>localStorage 和 sessionStorage 是 HTML5 引入的两个新API，它们都提供了在客户端存储数据的方式。他们都是基于键值对存储，可以用于在客户端存储字符串、数字、对象等类型的数据，与 Cookie 不同，它们不会被上传到服务端。</p>
<p>然而，两者之间还存在一些差异，主要在以下几个方面：</p>
<ol>
<li>
<p>存储时效性：localStorage 存储的数据没有过期时间，除非用户手动清除浏览器缓存或者使用代码删除；而 sessionStorage 存储的数据在当前会话结束后就会被清除。</p>
</li>
<li>
<p>存储大小限制：两者都有存储大小限制，一般为5MB左右，但具体大小限制可能因浏览器而异。</p>
</li>
<li>
<p>数据共享性：localStorage 保存在本地浏览器中当前域名的作用域下，同域名不同的页面或标签页都可以访问到，而 sessionStorage 只在当前页面或标签页中有效。</p>
</li>
</ol>
<p><img src="/images/localstorage-sessionstorage-scope.excalidraw.png" alt="localstorage-sessionstorage-scope.excalidraw"></p>
<h2>如何使用</h2>
<p>使用 localStorage 和 sessionStorage 非常简单且一致，只需要通过 JavaScript 的 localStorage 和 sessionStorage 对象调用相应的方法即可。</p>
<h3>存储数据</h3>
<pre><code class="language-js">// 存储数据到 localStorage
localStorage.setItem('key', 'value')

// 存储数据到 sessionStorage
sessionStorage.setItem('key', 'value')
</code></pre>
<h3>获取数据</h3>
<pre><code class="language-js">// 从 localStorage 中获取数据
const valueFromLocalStorage = localStorage.getItem('key')

// 从 sessionStorage 中获取数据
const valueFromSessionStorage = sessionStorage.getItem('key')
</code></pre>
<h3>删除数据</h3>
<pre><code class="language-js">// 从 localStorage 中删除指定的键值对
localStorage.removeItem('key')

// 从 sessionStorage 中删除指定的键值对
sessionStorage.removeItem('key')
</code></pre>
<h2>应用场景</h2>
<h3>1. 记住用户登录状态</h3>
<p>在用户登录成功后，可以使用 localStorage 或 sessionStorage 存储 token 或其他标识符，以便在用户下次访问时自动登录。</p>
<p>使用 localStorage 可以长期存储用户信息，即使浏览器关闭也不会丢失。而 sessionStorage 存储的数据只在当前会话中有效，当浏览器关闭后就会被清除。</p>
<h3>2. 缓存数据</h3>
<p>对于一些经常需要获取的数据，可以使用 localStorage 进行缓存，以便加快网站或应用程序的加载速度。例如，在一个电商网站中，可以使用 localStorage 存储用户的购物车信息、收货地址等常用数据，从而避免每次重新加载页面时都需要从服务器获取这些数据，提高用户体验。</p>
<h3>3. 记住用户偏好设置</h3>
<p>在用户进行一些个性化设置后，可以使用 localStorage 存储这些偏好设置，以便在下次访问时自动应用。例如，在一个新闻网站中，用户可以选择自己感兴趣的新闻分类，并将其存储到 localStorage 中，从而下次访问时直接显示用户所感兴趣的内容。</p>
<h3>4. 存储表单数据</h3>
<p>当用户填写表单时，可以使用 sessionStorage 存储表单数据，以便在用户重新加载页面或回退时恢复表单数据。而在需要长期存储数据的情况下，可以使用 localStorage。</p>
<p>localStorage 和 sessionStorage 都是 HTML5 中新增的本地存储方案，它们都可以在客户端保存键值对数据，并且可以被同源页面共享访问。但是它们之间还是有一些差异的。</p>
<h2>总结</h2>
<p>本文主要介绍了本地存储方案中的 localStorage 和 sessionStorage，它们的差异和使用场景。</p>
<p>两者相似又具有差异性的特性，是现代web开发中不可或缺的一部分，使得我们可以在业务中灵活运用它们来高效存取用户数据，提升用户体验。</p>
]]></content>
        <author>
            <name>Liang</name>
            <email>liang@in-x.cc</email>
            <uri>https://in-x.cc/</uri>
        </author>
    </entry>
</feed>