<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<atom:link href="https://zb81.icu/feed" rel="self" type="application/rss+xml"/>
<title>沉默是金</title>
<link>https://zb81.icu</link>
<description>夜风凛凛，独回望旧事前尘</description>
<language>zh-CN</language>
<copyright>© 沉默是金 </copyright>
<pubDate>Mon, 04 May 2026 11:36:51 GMT</pubDate>
<generator>Mix Space CMS (https://github.com/mx-space)</generator>
<docs>https://mx-space.js.org</docs>
<image>
    <url>https://cdn.zb81.icu/IMG_0754.JPG</url>
    <title>沉默是金</title>
    <link>https://zb81.icu</link>
</image>
<item>
    <title>使用 next-intl 在 Next.js 应用中开启国际化</title>
    <link>https://zb81.icu/posts/nextjs/i18n-with-next-intl</link>
    <pubDate>Wed, 01 Jan 2025 09:43:12 GMT</pubDate>
    <description>目前 Next.js 推荐的路由方式是 App Router，所以本文不再介绍 Pages Rout</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://zb81.icu/posts/nextjs/i18n-with-next-intl'>https://zb81.icu/posts/nextjs/i18n-with-next-intl</a></blockquote>
      <p>目前 Next.js 推荐的路由方式是 App Router，所以本文不再介绍 Pages Router，感兴趣的请阅读官方文档 <a href="https://next-intl.dev/docs/getting-started/pages-router">next-intl Pages Router</a>。</p>
<p><a href="https://next-intl.dev/docs/getting-started">next-intl</a> 为 App Router 提供了两种配置选项：</p>
<ul>
<li>带有路由：使用路由片段 locale 或者域名，例如 <code>/en/about</code> 和 <code>en.example.com/about</code>。</li>
<li>不带路由：基于用户设置。</li>
</ul>
<p>由于第一种方式在后续的路由跳转必须携带 locale 参数，比较不方便，因此本文选择介绍第二种不带路由的方式。</p>
<h2>一、准备工作</h2>
<p>使用 pnpm 创建项目</p>
<pre><code class="language-sh">pnpm create next-app</code></pre><img alt="项目选项" src="https://cdn.zb81.icu/36bb3fe785450246cd9f4a096f3f695caa914a04a11ce639c5b0b66c77f4caad.png" />  


<p>运行项目，本文在此页面上进行配置</p>
<img alt="初始页面" src="https://cdn.zb81.icu/09bc4f4fef69433862379bb1f8501d4bcdcd0d7b246997387e2b336d5b571b12.png" />  

<h2>二、安装和配置</h2>
<h3>1. 安装 next-intl</h3>
<pre><code class="language-sh">pnpm add next-intl</code></pre><h3>2. 在根目录创建 <code>messages</code> 文件夹，并在此文件夹下创建 <code>zh.json</code> 和 <code>en.json</code></h3>
<blockquote>
<p><code>Home</code> 为命名空间，后面需要用到，可以在 json 中声明多个，表示不同场景。</p>
</blockquote>
<ul>
<li>en.json</li>
</ul>
<pre><code class="language-json">{
  "Home": {
    "step1": "Get started by editing",
    "step2": "Save and see your changes instantly.",
    "deploy": "Deploy now",
    "docs": "Read our docs",
    "learn": "Learn",
    "examples": "Examples",
    "goto": "Go to"
  },
  "List": {
    "loading": "Loading...",
    "desc": "There is nothing here.",
    "text": "Click me"
  }
}</code></pre><ul>
<li>zh.json</li>
</ul>
<pre><code class="language-json">{
  "Home": {
    "step1": "开始编辑",
    "step2": "保存并立即查看您的更改。",
    "deploy": "现在部署",
    "docs": "阅读文档",
    "learn": "学习",
    "examples": "例子",
    "goto": "前往"
  },
  "List": {
    "loading": "加载中...",
    "desc": "这里什么也没有。",
    "text": "点击我"
  }
}</code></pre><h3>3. 配置 <code>next.config.mjs</code></h3>
<pre><code class="language-js">import createNextIntlPlugin from 'next-intl/plugin';
 
const withNextIntl = createNextIntlPlugin();
 
/** @type {import('next').NextConfig} */
const nextConfig = {};
 
export default withNextIntl(nextConfig);</code></pre><h3>4. 创建 <code>i18n/config.js</code>，保存配置</h3>
<pre><code class="language-js">// 系统支持的语言列表
export const locales = ['en', 'zh']

export const defaultLocale = 'en'</code></pre><h3>5. 创建 <code>i18n/service.js</code>，获取、设置区域</h3>
<p>next-intl 在 cookie 中设置了 <code>NEXT_LOCALE</code> 字段，用来保存区域配置。</p>
<p>获取区域配置优先级如下：</p>
<ul>
<li>从 cookies 中读取 <code>NEXT_LOCALE</code>，有值则直接返回</li>
<li>从 headers 中读取解析 <code>accept-language</code>，并判断是否在系统支持的语言中</li>
</ul>
<pre><code class="language-js">'use server';

import { cookies, headers } from 'next/headers';

import { defaultLocale, locales } from './config';

const COOKIE_NAME = 'NEXT_LOCALE';

export async function getUserLocale() {
  // 读取 cookie
  const locale = (await cookies()).get(COOKIE_NAME)?.value
  if (locale) return locale

  // 读取请求头 accept-language
  const acceptLanguage = (await headers()).get('accept-language')

  // 解析请求头
  const parsedLocale = acceptLanguage?.split(',')[0].split('-')[0]

  // 如果不在系统支持的语言列表，使用默认语言
  return locales.includes(parsedLocale) ? parsedLocale : defaultLocale;
}

export async function setUserLocale(locale) {
  (await cookies()).set(COOKIE_NAME, locale);
}</code></pre><h3>6. 创建 <code>i18n/request.js</code>，返回国际化配置</h3>
<pre><code class="language-js">import { getRequestConfig } from 'next-intl/server';

import { getUserLocale } from './service'

export default getRequestConfig(async () =&gt; {
  const locale = await getUserLocale()

  return {
    locale,
    messages: (await import(`../messages/${locale}.json`)).default
  };
});</code></pre><h3>7. 修改 <code>app/layout.js</code></h3>
<p>使用 <code>getLocale</code> 和 <code>getMessages</code> 可以获取 <code>i18n/request.js</code> 返回的语言配置，将 locale 设置到 html 的 lang 属性，将 messages 传递给 <code>NextIntlClientProvider</code>。</p>
<pre><code class="language-jsx">import { Geist, Geist_Mono } from "next/font/google";
import { NextIntlClientProvider } from 'next-intl';
import { getLocale, getMessages } from 'next-intl/server';

import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default async function RootLayout({ children }) {
  const locale = await getLocale()
  const messages = await getMessages()

  return (
    &lt;html lang={locale}&gt;
      &lt;body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      &gt;
        &lt;NextIntlClientProvider messages={messages}&gt;
          {children}
        &lt;/NextIntlClientProvider&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}</code></pre><h2>三、使用</h2>
<h3>同步服务端组件与客户端组件</h3>
<blockquote>
<p>文本数量较少的情况下，建议将文本通过 props 传递给客户端组件。在客户端组件中调用 <code>useTranslations</code> 会将 next-intl 代码打包进客户端 js 中，导致 bundle 体积增大，影响加载性能。</p>
</blockquote>
<ol>
<li>调用 <code>useTranslations</code>，传入命名空间</li>
</ol>
<pre><code class="language-js">const t = useTranslations('Home')</code></pre><ol start="2">
<li>在 jsx 中调用</li>
</ol>
<pre><code class="language-js"><p>{t('docs')}</p></code></pre><h3>异步服务端组件</h3>
<pre><code class="language-js">import { getTranslations } from 'next-intl/server'

const sleep = (ms) =&gt; new Promise(r =&gt; setTimeout(r, ms))

export default async function Page() {
  await sleep(2000)
  const t = await getTranslations('List')

  return (
    <div>
      {t('desc')}
    </div>
  )
}</code></pre><blockquote>
<p>关于其他更多用法，例如插值语法、富文本、HTML 标记、数组、数字格式、日期时间格式等，请参考文档 <a href="https://next-intl.dev/docs/usage/messages">https://next-intl.dev/docs/usage/messages</a>。</p>
</blockquote>
<h2>四、语言切换</h2>
<p>创建 <code>components/LocaleSwitcher.js</code> 组件，并在 <code>app/page.js</code> 中引入</p>
<pre><code class="language-jsx">'use client'

import { useTransition } from 'react'

import { setUserLocale } from '@/i18n/service'
import { defaultLocale } from '@/i18n/config';

export default function LocaleSwitcher({ defaultValue = defaultLocale }) {
  const [isPending, startTransition] = useTransition();

  function onChange(locale) {
    startTransition(() =&gt; {
      setUserLocale(locale);
    });
  }

  return (
    &lt;select
      className="bg-transparent"
      disabled={isPending}
      defaultValue={defaultValue}
      onChange={e =&gt; onChange(e.target.value)}
    &gt;
      &lt;option className="bg-gray-600" value='en'&gt;English&lt;/option&gt;
      &lt;option className="bg-gray-600" value='zh'&gt;中文&lt;/option&gt;
    &lt;/select&gt;
  )
}</code></pre><p>由于我们之前在 Server Action <code>setUserLocale</code> 中通过 cookies 设置了语言区域，Next.js 会将新的 cookie 和 DOM 返回给客户端。</p>
<img alt="RSC Payload" src="https://cdn.zb81.icu/44c50bdb87c64ab57410a51b13347082cb34b9af35dfbffa41673592e86d55d7.png" />  

<p>效果如下：</p>
<img alt="切换语言效果图" src="https://cdn.zb81.icu/327368ecec59b2623afaebcd44b563d1cee41b9602329f1613ec988496d08125.gif" />

<h2>五、最后</h2>
<p>在线预览地址：<a href="https://next-intl-study.vercel.app/">https://next-intl-study.vercel.app/</a></p>
<p>完整代码仓库：<a href="https://github.com/zb81/next-intl-study">https://github.com/zb81/next-intl-study</a></p>

      <p style='text-align: right'>
      <a href='https://zb81.icu/posts/nextjs/i18n-with-next-intl#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">67750e30b757bce29809f67a</guid>
  <category>Post</category>
<category>Next.js</category>
 </item>
  <item>
    <title>动态排版</title>
    <link>https://zb81.icu/posts/fe/dynamic-composing</link>
    <pubDate>Wed, 11 Dec 2024 06:42:34 GMT</pubDate>
    <description>问题一

因为不用考虑每一个区域的大小，而且只能在一个矩形容器中放入三种矩形内容，所以排版方式是有穷</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://zb81.icu/posts/fe/dynamic-composing'>https://zb81.icu/posts/fe/dynamic-composing</a></blockquote>
      <h2>问题一</h2>
<p>因为不用考虑每一个区域的大小，而且只能在一个矩形容器中放入三种矩形内容，所以排版方式是有穷的，共 36 种。</p>
<p>计算方式如下：</p>
<ol>
<li><p>布局组合：</p>
<p> 一行三列、上一下二、上二下一、左一右二、左二右一、三行一列。</p>
<p> 共 6 种布局组合。</p>
</li>
<li><p>文本、图片、列表的排列可能：</p>
<p> <code>A(3,3) = 6</code></p>
</li>
<li><p>两者相乘，得到 36 种排版.</p>
</li>
</ol>
<h2>问题二</h2>
<p>可以用一个嵌套数组表示排版结构，外层数组表示行，内层数组表示每一行包括的列。例如：</p>
<pre><code class="language-javascript">// 左一右二布局
[
  [
    {
      type: 'img', // 图片
      content: 'https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg', // 图片链接
    },
    {
      type: 'list', // 列表
      content: ['apple', 'banana', 'orange']
    },
  ],
  [
    {
      type: 'img', // 图片
      content: 'https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg', // 图片链接
    },
    {
      type: 'text', // 文本
      content: 'Hello, world! Hello, world!', // 文本内容
    },
  ],
]</code></pre><h2>问题三</h2>
<p>通过 grid 布局以及 <code>grid-area</code> 和 <code>grid-template-areas</code> 可以实现。</p>
<p>完整代码如下：</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;

&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Document&lt;/title&gt;
  &lt;style&gt;
    p {
      margin: 0;
    }

    ul {
      margin: 0;
      padding: 0;
      list-style: none;
    }

    #container {
      width: 50vw;
      height: 50vw;
      margin: 0 auto;
      display: grid;
    }

    #text {
      background-color: burlywood;
      grid-area: text;
    }

    #list {
      background-color: skyblue;
      grid-area: list;
    }

    #img {
      grid-area: img;
      width: 100%;
      height: 100%;
    }
  &lt;/style&gt;
&lt;/head&gt;

&lt;body&gt;
  <div>
    <p></p>
    <img />
    <ul></ul>
  </div>

  &lt;script&gt;
    // 假设后端返回的排版数据如下，左一右二布局
    const res = [
      [
        {
          type: 'img', // 图片
          content: 'https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg', // 图片链接
        },
        {
          type: 'list', // 列表
          content: ['apple', 'banana', 'orange']
        },
      ],
      [
        {
          type: 'img', // 图片
          content: 'https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg', // 图片链接
        },
        {
          type: 'text', // 文本
          content: 'Hello, world! Hello, world!', // 文本内容
        },
      ],
    ]

    let areas = []

    // 遍历设置元素内容
    res.forEach(row =&gt; {
      let rowAreas = []
      row.forEach(item =&gt; {
        const { type, content } = item
        rowAreas.push(type)
        const el = document.getElementById(type)

        switch (type) {
          case 'text': {
            el.textContent = content
            break
          }
          case 'img': {
            el.src = content
            break
          }
          case 'list': {
            if (!el.hasChildNodes()) {
              content.forEach(c =&gt; {
                const li = document.createElement('li')
                li.textContent = c
                el.appendChild(li)
              })
            }
          }
        }
      })
      areas.push(rowAreas)
    })

    // 设置 `grid-template-areas`
    const container = document.getElementById('container')
    container.style.gridTemplateAreas = areas.map(row =&gt; `'${row.join(' ')}'`).join(' ')
  &lt;/script&gt;
&lt;/body&gt;

&lt;/html&gt;</code></pre><p>效果如图：</p>
<img alt="效果图" src="https://cdn.zb81.icu/9fb865982ad007ba0a49051c83fd22ebdf4a7883fe268534e115e21f140ac82b.png" />
      <p style='text-align: right'>
      <a href='https://zb81.icu/posts/fe/dynamic-composing#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">6759345a7d1dabd5a341c721</guid>
  <category>Post</category>
<category>前端</category>
 </item>
  <item>
    <title>给七牛云域名添加 https 证书</title>
    <link>https://zb81.icu/posts/toss/qiniu-https</link>
    <pubDate>Mon, 29 Apr 2024 06:12:37 GMT</pubDate>
    <description>本文以腾讯云服务为例，介绍如何给七牛云自定义域名添加 https 证书。

关于 certbot 的</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://zb81.icu/posts/toss/qiniu-https'>https://zb81.icu/posts/toss/qiniu-https</a></blockquote>
      <p>本文以腾讯云服务为例，介绍如何给七牛云自定义域名添加 https 证书。</p>
<p>关于 certbot 的安装，请参考官方文档：<a href="https://certbot.eff.org/">certbot.eff.org</a> 。</p>
<h2>一、先将域名指定到云服务器</h2>
<p>添加一条 A 类型的解析记录，指定到云服务器实例 IP：</p>
<img alt="picture 0" src="https://cdn.zb81.icu/30e40d74eda4f9d98e1f8754d11b83b6dc8cae379c725018c1d5b0491b5c5dc2.png" />  

<h2>二、配置云服务器 Nginx</h2>
<p>配置 Nginx 的原因是 Let&#39;s Encrypt 需要验证网站的所有权才能颁发证书，我们使用 HTTP-01 的方式：</p>
<pre><code class="language-text"># nginx.conf
server {
    listen 80;
    server_name cdn.example.com;
    
    location ^~  /.well-known/acme-challenge/ {
        default_type "text/plain";
        root  /home/letsencrypt/;
    }
}</code></pre><p>重启 Nginx 服务：</p>
<pre><code class="language-bash">nginx -s reload</code></pre><h2>三、签发证书</h2>
<pre><code class="language-bash">certbot certonly --email xxx@xxx.com --webroot -w /home/letsencrypt -d cdn.example.com</code></pre><p>输出以下结果，代表签发成功：</p>
<img alt="picture 2" src="https://cdn.zb81.icu/eba02978e11b91f408e933b300b1c7c3f7b2981099c06141ab8471783eee79e8.png" />  

<h2>四、上传证书</h2>
<img alt="picture 3" src="https://cdn.zb81.icu/e401e4695a732c2850ff538fdad10c92a9dbbf5386b3ab8c9cbbdc09b4fa64a5.png" />  

<h2>五、将域名指向七牛云</h2>
<p>将刚刚添加的 A 类型解析记录修改为 CNAME 类型，值填写七牛云的域名（xxxxxx-asdfc.qiniudns.com）：</p>
<img alt="picture 4" src="https://cdn.zb81.icu/31a77ed52eb1468493624ffd8456a243ee3f7a326297d53e433ffe355fe90b32.png" />
      <p style='text-align: right'>
      <a href='https://zb81.icu/posts/toss/qiniu-https#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">662f3a557d1dabd5a33d7efa</guid>
  <category>Post</category>
<category>折腾</category>
 </item>
  <item>
    <title>从零搭建一个自定义明暗主题系统</title>
    <link>https://zb81.icu/posts/fe/build-light-dark-theme</link>
    <pubDate>Mon, 08 Apr 2024 14:59:30 GMT</pubDate>
    <description>最近准备用 React + Antd + UnoCSS 开发一个和 NestJS Admin 配套的</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://zb81.icu/posts/fe/build-light-dark-theme'>https://zb81.icu/posts/fe/build-light-dark-theme</a></blockquote>
      <p>最近准备用 <a href="https://react.dev/">React</a> + <a href="https://ant-design.antgroup.com/docs/react/introduce-cn">Antd</a> + <a href="https://unocss.dev/">UnoCSS</a> 开发一个和 NestJS Admin 配套的系统，想加个自定义主题功能，效果如下图，也可以点击 <a href="https://nest-admin.zb81.icu">这里</a> 或 <a href="https://zb81.github.io/my-theme/">这里</a> 体验。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/efe9bdb3de214527a6a11f736a7b7b6f~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1440&h=900&s=4457104&e=gif&f=276&b=fdf9fe"/></p>
<h1>一、需求</h1>
<ul>
<li>用户可以自定义主题颜色，需要实时响应；</li>
<li>用户可以切换明暗模式，需要实时改变背景和文字颜色；</li>
<li>当用户切换系统主题时，网页需要作出响应；</li>
<li>主题颜色和明暗模式需要缓存至 <code>localStorage</code>。</li>
</ul>
<h1>二、准备工作</h1>
<blockquote>
<p>这里使用的是 pnpm，用 npm 或 yarn 等包管理工具的记得替换命令。</p>
</blockquote>
<h2>1. 创建项目</h2>
<p>首先，拉取 vite 模板：</p>
<pre><code class="language-sh">pnpm create vite my-theme --template react-ts</code></pre><p>清空 src 目录：</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c8f50fd8cfd04d3b93ac9d6bc3e6339b~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=572&h=402&s=32058&e=png&b=f8f8f8"/></p>
<pre><code class="language-tsx">// main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'

ReactDOM.createRoot(document.getElementById('root')!).render(
  &lt;React.StrictMode&gt;
    &lt;App /&gt;
  &lt;/React.StrictMode&gt;,
)</code></pre><pre><code class="language-tsx">// App.tsx
function App() {
  return (
    <div>App</div>
  )
}

export default App</code></pre><p>启动项目：</p>
<pre><code class="language-sh">pnpm run dev</code></pre><h2>2. 安装 Antd</h2>
<pre><code class="language-sh">pnpm add antd</code></pre><pre><code class="language-tsx">// App.tsx
import { Button } from 'antd'

function App() {
  return (
    <div>
      &lt;Button type="primary"&gt;123&lt;/Button&gt;
      &lt;Button&gt;zzz&lt;/Button&gt;
    </div>
  )
}

export default App</code></pre><p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/80d8013c670b46de87b27414ceeaa383~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=367&h=251&s=15297&e=png&b=fefefe"/></p>
<h2>3. 安装并配置 UnoCSS</h2>
<p>开始之前，先推荐两个 VSCode 插件：</p>
<ul>
<li><p>UnoCSS</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/12db2a6258c14f9e9deaa23cd298d386~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=563&h=176&s=22558&e=png&b=fdfdfd"/></p>
<p>这个插件会读取 <code>uno.config.ts</code> ，提供了类名的提示以及预览：</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/adc64dec12a645279f20a0448ee2074d~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=931&h=385&s=71635&e=png&b=fbfbfb"/></p>
</li>
<li><p>Iconify IntelliSense</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7d315a5ceb6346b4a22f47b2cf2f068f~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=581&h=179&s=29338&e=png&b=fefefe"/></p>
<p>这个插件提供了图标名称的提示和预览功能：</p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/acca4d6993c7486c8ac64d22ac5025d2~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=673&h=282&s=38447&e=png&b=fbfafa"/></p>
</li>
</ul>
<h3>1) 安装并引入</h3>
<p>因为后续会用的 CSS 图标，这里顺带安装一下图标库。(体积很大，70M，你想要的 SVG 图标 <a href="https://icones.js.org/">这里</a> 都有)</p>
<pre><code class="language-sh">pnpm add unocss @iconify/json -D</code></pre><p>配置 <code>vite.config.ts</code>：</p>
<pre><code class="language-ts">// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import UnoCSS from 'unocss/vite'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), UnoCSS()],
})</code></pre><p>在 <code>main.tsx</code> 中引入：</p>
<pre><code class="language-tsx">// main.tsx
import 'virtual:uno.css'</code></pre><h3>2) 配置文件</h3>
<p>在项目根目录创建 <code>uno.config.ts</code> 配置文件：</p>
<pre><code class="language-ts">// uno.config.ts
import { defineConfig, presetIcons, presetUno } from 'unocss'

export default defineConfig({
  presets: [
    presetUno(),
    presetIcons({
      extraProperties: {
        'display': 'inline-block',
        'height': '1.2em',
        'width': '1.2em',
        'vertical-align': 'middle',
      },
      warn: true,
    }),
  ],
})</code></pre><p>更多配置选项，请阅读 <a href="https://unocss.dev/config/">UnoCSS 文档</a>。</p>
<p>关于图标的使用方法和配置，请看 <a href="https://unocss.dev/presets/icons">这里</a>。</p>
<h3>3) 样式重置</h3>
<pre><code class="language-sh">pnpm add @unocss/reset</code></pre><p>在 <code>main.tsx</code> 中引入：</p>
<pre><code class="language-tsx">import '@unocss/reset/tailwind-compat.css'</code></pre><h3>4) 测试</h3>
<p>在 <code>App.tsx</code> 中随便写点代码：</p>
<pre><code class="language-tsx">// App.tsx
import { Button } from 'antd'

function App() {
  return (
    <div>
      &lt;Button type="primary"&gt;123&lt;/Button&gt;
      &lt;Button&gt;zzz&lt;/Button&gt;

      <h1>Hello, world!</h1>
      <div>
        <div></div>
        <div></div>
      </div>
    </div>
  )
}

export default App</code></pre><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/33980a0f4eec454f8a1a200522c5d697~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=386&h=399&s=21140&e=png&b=ffffff"/></p>
<h1>三、需求实现</h1>
<h2>1. 自定义主题颜色</h2>
<h3>1) 组件引入并绑定状态</h3>
<pre><code class="language-tsx">// App.tsx
import { Button, ColorPicker } from "antd";
import { useState } from "react";

function App() {
  const [primaryColor, setPrimaryColor] = useState("#01bfff");

  return (
    <div>
      &lt;ColorPicker
        value={primaryColor}
        onChange={(_, c) =&gt; setPrimaryColor(c)}
      /&gt;

      <span>{}</span>

      &lt;Button type="primary"&gt;123&lt;/Button&gt;
      &lt;Button&gt;zzz&lt;/Button&gt;
    </div>
  );
}

export default App;</code></pre><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3478ee104f364f4498bb080ecb33bd72~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=309&h=338&s=85149&e=png&b=fcfcfc"/></p>
<h3>2) 和 Antd 组件同步</h3>
<p>新版本的 Antd 采用了 CSS-in-JS 方案以及 <a href="https://ant-design.antgroup.com/docs/react/customize-theme-cn#%E6%A2%AF%E5%BA%A6%E5%8F%98%E9%87%8Fmap-token">梯度变量演变</a> 算法，只需要提供一个基础变量 <code>colorPrimary</code> ，主题相关的其它配色就能推算出来，比如按钮点击的波纹颜色等等。</p>
<p>所以，我们只需要将 <code>primaryColro</code> 通过 <a href="https://ant-design.antgroup.com/docs/react/customize-theme-cn#%E4%BF%AE%E6%94%B9%E4%B8%BB%E9%A2%98%E5%8F%98%E9%87%8F">ConfigProvider</a> 提供给 Antd 就可以了：</p>
<pre><code class="language-tsx">// App.tsx
import { Button, ColorPicker, ConfigProvider, ThemeConfig } from "antd";
import { useState } from "react";

function App() {
  const [primaryColor, setPrimaryColor] = useState("#01bfff");

  const antdTheme: ThemeConfig = {
    token: { colorPrimary: primaryColor },
  };

  return (
    &lt;ConfigProvider theme={antdTheme}&gt;
      <div>
        <div>
          &lt;ColorPicker
            value={primaryColor}
            onChange={(_, c) =&gt; setPrimaryColor(c)}
          /&gt;

          <span>{primaryColor}</span>

          &lt;Button type="primary"&gt;123&lt;/Button&gt;
          &lt;Button&gt;zzz&lt;/Button&gt;
        </div>
      </div>
    &lt;/ConfigProvider&gt;
  );
}

export default App;</code></pre><p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c9aa2b6f2fdd4e4b9fdc979c845e2f91~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=312&h=315&s=747874&e=gif&f=86&b=fbf8fc"/></p>
<h3>3) 和其他颜色同步</h3>
<p>这里使用 CSS 变量的方案来保持颜色同步：</p>
<ul>
<li>给根元素添加一个 CSS 变量 <code>--primary-color</code>;</li>
<li>给 UnoCSS 添加一个颜色 <code>primary: &#39;var(--primary-color)&#39;</code>；</li>
<li>添加一个副作用，让 <code>primaryColro</code> 和 <code>--primary-color</code> 保持同步。</li>
</ul>
<pre><code class="language-ts">// uno.config.ts
import { defineConfig, presetIcons, presetUno } from 'unocss'

export default defineConfig({
  theme: {
    colors: {
      primary: 'var(--primary-color)', // 这里定义了一个颜色，通过 `text-primary` 的方式使用
    },
  },
  // ... rest config
})</code></pre><pre><code class="language-tsx">// App.tsx
import { Button, ColorPicker, ConfigProvider, ThemeConfig } from "antd";
import { useEffect, useState } from "react";

function App() {
  const [primaryColor, setPrimaryColor] = useState("#01bfff");

  useEffect(() =&gt; {
    document.documentElement.style.setProperty("--primary-color", primaryColor);
  }, [primaryColor]);

  const antdTheme: ThemeConfig = {
    token: { colorPrimary: primaryColor },
  };

  return (
    &lt;ConfigProvider theme={antdTheme}&gt;
      <div>
        &lt;ColorPicker
          value={primaryColor}
          onChange={(_, c) =&gt; setPrimaryColor(c)}
        /&gt;

        {/* 这里使用了在 UnoCSS 中定义的 primary */}
        <span>{primaryColor}</span>

        &lt;Button type="primary"&gt;123&lt;/Button&gt;
        &lt;Button&gt;zzz&lt;/Button&gt;
      </div>
    &lt;/ConfigProvider&gt;
  );
}

export default App;</code></pre><p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/48d110d8cbe94c04a5c41cb3dbcbcb2c~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=323&h=313&s=918500&e=gif&f=78&b=fcfcfc"/></p>
<h2>2. 明暗模块切换</h2>
<p>安装 <code>classnames</code> 方便组装类名：</p>
<pre><code class="language-sh">pnpm add classnames</code></pre><h3>1) 封装切换组件</h3>
<p>先给图标按钮加个 shortcut 组合类：</p>
<pre><code class="language-ts">// uno.config.ts
import { defineConfig, presetIcons, presetUno } from 'unocss'

export default defineConfig({
  shortcuts: {
    btn: 'p-2 font-semibold rounded-lg select-none cursor-pointer hover:bg-[#8882] dark:hover:bg-[#fff2]',
  },
  // ... rest config
})</code></pre><p>创建组件 <code>ToggleTheme.tsx</code>：</p>
<pre><code class="language-tsx">// ToggleTheme.tsx
import { Popover } from "antd";
import classnames from "classnames";

function upperFirst(str: string) {
  return `${str[0].toUpperCase()}${str.slice(1)}`;
}

export type ColorMode = "light" | "dark" | "auto";

const iconMap = {
  light: <div />,
  dark: <div />,
  auto: <div />,
};

const modes = ["light", "dark", "auto"] as const;

interface Props {
  mode: ColorMode;
  onChange: (mode: ColorMode) =&gt; void;
}

function ToggleTheme({ mode, onChange }: Props) {
  const modeList = (
    <ul>
      {modes.map((m) =&gt; (
        <li> onChange(m)}
        &gt;
          {iconMap[m]}
          <span>{upperFirst(m)}</span>
        </li>
      ))}
    </ul>
  );

  return (
    &lt;Popover
      placement="bottom"
      arrow={false}
      content={modeList}
      trigger="click"
    &gt;
      {/* 这里也使用了 shortcut `btn` */}
      <a>{iconMap[mode!]}</a>
    &lt;/Popover&gt;
  );
}

export default ToggleTheme;</code></pre><h3>2) 引入组件</h3>
<pre><code class="language-tsx">// App.tsx
import { Button, ColorPicker, ConfigProvider, ThemeConfig } from "antd";
import { useEffect, useState } from "react";
import ToggleTheme, { ColorMode } from "./ToggleTheme";

function App() {
  // 其他代码

  // 保存明暗模式
  const [mode, setMode] = useState&lt;ColorMode&gt;("light");

  return (
    &lt;ConfigProvider theme={antdTheme}&gt;
      <div>
        {/* 其他代码 */}

        &lt;ToggleTheme mode={mode} onChange={(m) =&gt; setMode(m)} /&gt;
      </div>
    &lt;/ConfigProvider&gt;
  );
}

export default App;</code></pre><p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c37dc89ea91f40f8bd8c5c2a76a6f028~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=375&h=364&s=23481&e=png&b=fdfdfd"/></p>
<h3>3) 绑定 dark 类</h3>
<p>目前常用的黑暗模式方案是给根元素添加一个 <code>dark</code> 类，然后在代码中通过 <code>dark:text-yellow</code> 指定黑暗模式下的样式：</p>
<pre><code class="language-css">.dark .dark\:text-yellow {
    --un-text-opacity: 1;
    color: rgb(250 204 21 / var(--un-text-opacity));
}</code></pre><p>使用 <code>useEffect</code> 同步 dark 类：</p>
<pre><code class="language-tsx">// App.tsx
import { Button, ColorPicker, ConfigProvider, ThemeConfig } from "antd";
import { useEffect, useState } from "react";
import ToggleTheme, { ColorMode } from "./ToggleTheme";

function App() {
  const [mode, setMode] = useState&lt;ColorMode&gt;("light");
  useEffect(() =&gt; {
    document.documentElement.classList.toggle("dark", mode === "dark");
  }, [mode]);

  return (
    &lt;ConfigProvider theme={antdTheme}&gt;
            {/* ... */}
      {/* 使用 dark:text-yellow 指定黑暗模式下的文字颜色 */}
      <h1>Light or dark</h1>
    &lt;/ConfigProvider&gt;
  );
}

export default App;</code></pre><p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/11802d34c5c64d8e86782745787763bd~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=323&h=313&s=250205&e=gif&f=70&b=fdfcfc"/></p>
<h3>4) 使用 CSS 变量同步颜色</h3>
<p>新建 <code>main.css</code>：</p>
<pre><code class="language-css">/* main.css */
:root {
  /* 明亮模式的颜色 */
  --c-bg: #fff;
  --c-scrollbar: #eee;
  --c-scrollbar-hover: #bbb;
  --c-text-color: #333;
}

html {
  /* 使用 CSS 变量 */
  background-color: var(--c-bg);
  transition: background-color .25s;
  color: var(--c-text-color);
  overflow-x: hidden;
  overflow-y: scroll;
}

html.dark {
  /* 黑暗模式的颜色 */
  --c-bg: #333;
  --c-scrollbar: #111;
  --c-scrollbar-hover: #222;
  --c-text-color: #fff;
}

* {
  scrollbar-color: var(--c-scrollbar) var(--c-bg);
}

::-webkit-scrollbar {
  width: 6px;
}

::-webkit-scrollbar:horizontal {
  height: 6px;
}

::-webkit-scrollbar-track,
::-webkit-scrollbar-corner {
  background: var(--c-bg);
  border-radius: 10px;
}

::-webkit-scrollbar-thumb {
  background: var(--c-scrollbar);
  border-radius: 10px;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--c-scrollbar-hover);
}</code></pre><p>引入 <code>main.tsx</code>：</p>
<pre><code class="language-tsx">// main.tsx
import './main.css'</code></pre><p>效果如下：</p>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ea55ba6b31344681849988fcf6dcaddc~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=429&h=404&s=270564&e=gif&f=70&b=fefefe"/></p>
<h3>5) 同步 Antd</h3>
<p>Antd 暴露的 <code>theme</code> 提供了几种颜色算法，我们需要用到这两种：</p>
<ul>
<li>defaultAlgorithm 默认算法</li>
<li>darkAlgorithm 黑暗模式的算法</li>
</ul>
<p>我们需要根据 <code>mode</code> 给 <code>ConfgProvider</code> 提供不同的算法：</p>
<pre><code class="language-tsx">// App.tsx
import { /* ... */ theme } from "antd"; // 引入 theme
import { useEffect, useState } from "react";
import ToggleTheme, { ColorMode } from "./ToggleTheme";

function App() {
    // ...

  const antdTheme: ThemeConfig = {
    token: { colorPrimary: primaryColor },
    // 黑暗模式使用 darkAlgorithm
    algorithm: mode === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm,
  };

  return (
    &lt;ConfigProvider theme={antdTheme}&gt;
      {/* ... */}
    &lt;/ConfigProvider&gt;
  );
}

export default App;</code></pre><p>效果如下：(注意看 zzz 按钮的背景颜色)</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/53adb659dfd347e19e30c13c0d1838b4~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=572&h=538&s=313899&e=gif&f=70&b=fdfdfd"/></p>
<h2>3. 监听系统主题</h2>
<p>刚刚我们实现了手动切换明暗模式，现在来实现根据当前的系统主题使用对应的模式。</p>
<h3>1) 获取并监听系统主题</h3>
<p>CSS 提供了媒体查询 <code>prefers-color-scheme: dark</code> 用来监听系统明暗模式，如果我们想读取，需要调用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Window/matchMedia"><code>window.matchMedia</code></a>，该方法需要传入一个查询字符串，并返回一个 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/MediaQueryList"><code>MediaQueryList</code></a>对象：</p>
<ul>
<li>matches 布尔值</li>
<li>addEventListener 添加监听事件处理函数</li>
</ul>
<p>为了更好的逻辑封装和复用，创建一个自定义 hook <code>usePreferredDark.ts</code>，返回系统是否处于黑暗模式：</p>
<pre><code class="language-ts">import { useState } from 'react'

export function usePreferredDark() {
  // 使用系统当前的明暗模式作为初始值
  const [matches, setMatches] = useState(window.matchMedia('(prefers-color-scheme: dark)').matches)

  // 监听系统的明暗模式变化
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) =&gt; {
    setMatches(e.matches)
  })

  return matches
}</code></pre><p>测试：</p>
<pre><code class="language-tsx">// App.tsx
import { usePreferredDark } from "./usePreferredDark";

function App() {
    // ...
  const preferredDark = usePreferredDark()

  useEffect(() =&gt; {
    document.documentElement.classList.toggle("dark", preferredDark);
  }, [preferredDark]);

  const antdTheme: ThemeConfig = {
    token: { colorPrimary: primaryColor },
    algorithm: preferredDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
  };

  return (
    &lt;ConfigProvider theme={antdTheme}&gt;
      {/* ... */}
    &lt;/ConfigProvider&gt;
  );
}

export default App;</code></pre><p>效果如下：</p>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ae2f5dcba20e4ef986dad084dd2f875b~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=959&h=539&s=1657532&e=gif&f=108&b=6a6662"/></p>
<h3>2) 结合 mode</h3>
<p>监听系统主题我们实现了，现在需要把 <code>preferredDark</code> 和 <code>mode</code> 结合起来判断当前网页是否处于黑暗模式，封装一个自定义 hook <code>useDark.ts</code>，如果是黑暗模式，返回 true：</p>
<pre><code class="language-ts">// useDark.ts
import { useMemo } from 'react'
import { usePreferredDark } from './usePreferredDark'
import type { ColorMode } from './ToggleTheme'

export function useDark(mode: ColorMode) { // 外部传入，用户选择的明暗模式
  const preferredDark = usePreferredDark() // 当前系统是否是黑暗模式
  const isDark = useMemo(() =&gt; {
    return mode === 'dark' || (preferredDark && mode !== 'light') // 简化后的逻辑
  }, [mode, preferredDark])

  return isDark
}
</code></pre><p>逻辑解释：</p>
<ul>
<li>因为 mode 是用户选择的，所以它优先级最高，如果 mode === &#39;dark&#39;，直接短路返回 true；</li>
<li>如果 mode === &#39;light&#39;，返回 false</li>
<li>如果 mode === &#39;auto&#39;，返回当前系统是否处于黑暗模式</li>
</ul>
<p>测试：</p>
<pre><code class="language-tsx">// App.tsx
import { useDark } from "./useDark";

function App() {
     // ...
  const [mode, setMode] = useState&lt;ColorMode&gt;("light");
  // 当前是否处于黑暗模式
  const isDark = useDark(mode);
  useEffect(() =&gt; {
    document.documentElement.classList.toggle("dark", isDark);
  }, [isDark]);

  const antdTheme: ThemeConfig = {
    token: { colorPrimary: primaryColor },
    algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
  };

  return (
    &lt;ConfigProvider theme={antdTheme}&gt;
        {/* ... */}
    &lt;/ConfigProvider&gt;
  );
}

export default App;</code></pre><p>效果如下：</p>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/54280a03cab84ec8af6f116cdfefedfe~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=471&h=285&s=325804&e=gif&f=174&b=fefefe"/></p>
<p>最后修改 mode 的初始值为 <code>auto</code>：</p>
<pre><code class="language-tsx">// App.tsx
function App() {
  // ...

  const [mode, setMode] = useState&lt;ColorMode&gt;("auto"); // 这里

  return (
    // ...
  );
}

export default App;</code></pre><h2>4. 缓存至 localStorage</h2>
<p>这个实现起来很简单，直接使用 ahooks 提供的 <code>useLocalStorageState</code> 即可。</p>
<h3>1) 安装 ahooks</h3>
<pre><code class="language-sh">pnpm add ahooks</code></pre><h3>2) 替换 <code>useState</code></h3>
<pre><code class="language-tsx">// App.tsx
import { useLocalStorageState } from "ahooks"; // 引入

// 定义 key 常量
const PRIMARY_COLOR_KEY = "app_primary_color";
const COLOR_MODE_KEY = "app_color_mode";

function App() {
  const [primaryColor, setPrimaryColor] = useLocalStorageState(
    PRIMARY_COLOR_KEY,
    {
      defaultValue: "#01bfff",
      serializer: (v) =&gt; v, // 因为我们存的本身就是字符串，不需要 JSON 序列化
      deserializer: (v) =&gt; v,
    }
  );

  useEffect(() =&gt; {
    document.documentElement.style.setProperty(
      "--primary-color",
      primaryColor! // 注意这里非空断言
    );
  }, [primaryColor]);

  const [mode, setMode] = useLocalStorageState&lt;ColorMode&gt;(COLOR_MODE_KEY, {
    defaultValue: "auto",
    serializer: (v) =&gt; v,
    deserializer: (v) =&gt; v as ColorMode,
  });

  const isDark = useDark(mode!);
  useEffect(() =&gt; {
    document.documentElement.classList.toggle("dark", isDark);
  }, [isDark]);

  const antdTheme: ThemeConfig = {
    token: { colorPrimary: primaryColor },
    algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
  };

  return (
    &lt;ConfigProvider theme={antdTheme}&gt;
      <div>
        &lt;ColorPicker
          value={primaryColor}
          onChange={(_, c) =&gt; setPrimaryColor(c)}
        /&gt;

        <span>
          {primaryColor}
        </span>

        &lt;Button type="primary"&gt;123&lt;/Button&gt;
        &lt;Button&gt;zzz&lt;/Button&gt;

        {/* 注意这里非空断言 */}
        &lt;ToggleTheme mode={mode!} onChange={(m) =&gt; setMode(m)} /&gt;
      </div>

      <h1>Light or dark</h1>
    &lt;/ConfigProvider&gt;
  );
}

export default App;</code></pre><p>效果如下：</p>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a0f1d6d92c074787ac6da94e8ea3c527~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=471&h=285&s=284198&e=gif&f=110&b=7e7e7e"/></p>
<h3>3) 背景闪烁</h3>
<p>刷新页面的时候，明显可以感觉到背景颜色闪烁了一下。这是因为根元素的 dark 类是通过 JS 设置的，我们的代码会在 html 创建之后执行。</p>
<p>解决方案：在 <code>index.html</code> 的 <code>head</code> 中插入一段脚本：</p>
<pre><code class="language-html">
&lt;!doctype html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="UTF-8" /&gt;
    &lt;link rel="icon" type="image/svg+xml" href="/vite.svg" /&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0" /&gt;
    &lt;title&gt;Vite + React + TS&lt;/title&gt;
    &lt;script&gt;
      // 这段脚本会先执行
      ;(function () {
        const prefersDark =
          window.matchMedia &&
          window.matchMedia('(prefers-color-scheme: dark)').matches
        const setting = localStorage.getItem('app_color_mode') || 'auto'
        if (setting === 'dark' || (prefersDark && setting !== 'light'))
          document.documentElement.classList.toggle('dark', true)
      })()
    &lt;/script&gt;
  &lt;/head&gt;
  &lt;body&gt;
    <div></div>
    &lt;script type="module" src="/src/main.tsx"&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre><h1>四、总结技术要点</h1>
<ul>
<li><code>window.matchMedia</code> API</li>
<li>Antd <code>ConfigProvider</code></li>
<li><code>useLocalStorageState</code></li>
<li>CSS 变量</li>
</ul>
<p>完整代码见 <a href="https://github.com/zb81/my-theme">GitHub</a>。</p>

      <p style='text-align: right'>
      <a href='https://zb81.icu/posts/fe/build-light-dark-theme#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">6614065f7d1dabd5a33cf5fa</guid>
  <category>Post</category>
<category>前端</category>
 </item>
  <item>
    <title>攒钱！努力！</title>
    <link>https://zb81.icu/notes/1</link>
    <pubDate>Fri, 29 Mar 2024 09:17:29 GMT</pubDate>
    <description>保时米给我的刺激

昨晚看了小米汽车的发布会直播，不得不说，顶配版 29.99 万的售价真的很不错。</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://zb81.icu/notes/1'>https://zb81.icu/notes/1</a></blockquote>
      <h2>保时米给我的刺激</h2>
<p>昨晚看了小米汽车的发布会直播，不得不说，顶配版 29.99 万的售价真的很不错。然而对于没攒什么钱的我来说，卖 19.99 万也还是买不起。这是我的问题，不是小米的问题。</p>
<p>发布会结束后，我一直在想最近两年冲动消费了多少次，到现在想买大件的时候才发现手里根本没几个钱。工作三年了，除了那一堆赛博垃圾，啥也没有。</p>
<p>在这里立个 Flag ：理智消费，逐渐享受攒钱的快乐，<strong>今年下半年提辆车，开回家过年</strong>。</p>
<h2>折腾博客主题</h2>
<p>年前就在推特上发现了这个颜值很高的博客主题 <a href="https://github.com/Innei/Shiro">Shiro</a> ，直到最近想学习 <a href="https://nextjs.org/docs">Next.js</a> 和 <a href="https://docs.nestjs.com/">Nest.js</a> 才决定折腾一波。昨晚住在楼上的两个韩国人太吵，吵得我到一点还没睡着。TMD 不睡了，直接起床开整。一开始我没去看文档，硬生生折腾到四点钟，终于通了，上床睡觉。</p>
<p>早上来到公司打开控制台发现客户端的请求会报跨域，我就想是不是哪个地方没配置。打开<a href="https://mx-space.js.org/docs">文档</a>才看到，后端服务在 <code>docker-compose</code> 之前要创建 <code>.env</code> 环境变量，配置 JWT Secret 、Allow Origins 什么的。这一次很快，十分钟不到就重新部署了一遍。</p>
<p>中午吃完饭把前端从 Vercel 迁移到了腾讯云，速度提升了很多。</p>
<h2>后续计划</h2>
<ul>
<li><p><input disabled="" type="checkbox"> 
搞明白 <a href="https://mx-space.js.org/usage">文档</a> 中提到的文本宏、云函数等功能的用法和实现原理</p>
</li>
<li><p><input disabled="" type="checkbox"> 
学习前端 <a href="https://github.com/Innei/Shiro">Shiro</a> 和后端 <a href="https://github.com/mx-space/core">mx-core</a> 的源码 (相当于学习 Next 和 Nest)</p>
</li>
</ul>

      <p style='text-align: right'>
      <a href='https://zb81.icu/notes/1#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">66068729f7ad3850872139eb</guid>
  <category>Note</category>
false
 </item>
  <item>
    <title>ECharts 部署教程</title>
    <link>https://zb81.icu/posts/toss/echarts-deploy</link>
    <pubDate>Tue, 10 Oct 2023 14:57:19 GMT</pubDate>
    <description>国内的网络环境访问 ECharts 文档经常会抽风，访问速度很慢，或者压根加载不出来。这篇教程会教你</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://zb81.icu/posts/toss/echarts-deploy'>https://zb81.icu/posts/toss/echarts-deploy</a></blockquote>
      <p>国内的网络环境访问 ECharts 文档经常会抽风，访问速度很慢，或者压根加载不出来。这篇教程会教你如何在本机或内网部署一个站点，提升文档访问速度。</p>
<h2>一、准备工作</h2>
<p>新建一个文件夹，用于存放站点相关源码，直接使用命令创建并进入：</p>
<pre><code class="language-sh">mkdir echarts-deploy && cd echarts-deploy</code></pre><p>在此目录下，将下面所有仓库的代码全部克隆下来（仓库体积较大，耐心等待）：</p>
<p><a href="https://github.com/apache/echarts-website">echarts-website</a></p>
<pre><code class="language-sh">git clone https://github.com/apache/echarts-website.git</code></pre><p><a href="https://github.com/apache/echarts-www">echarts-www</a></p>
<pre><code class="language-sh">git clone https://github.com/apache/echarts-www.git</code></pre><p><a href="https://github.com/apache/echarts-examples">echarts-examples</a></p>
<pre><code class="language-sh">git clone https://github.com/apache/echarts-examples.git</code></pre><p><a href="https://github.com/apache/echarts-doc">echarts-doc</a></p>
<pre><code class="language-sh">git clone https://github.com/apache/echarts-doc.git</code></pre><p><a href="https://github.com/apache/echarts-theme-builder">echarts-theme-builder</a></p>
<pre><code class="language-sh">git clone https://github.com/apache/echarts-theme-builder.git</code></pre><p><a href="https://github.com/apache/echarts-handbook">echarts-handbook</a></p>
<pre><code class="language-sh">git clone https://github.com/apache/echarts-handbook.git</code></pre><h2>二、构建</h2>
<p>这几个仓库分为主仓库（echarts-website）和子仓库（其他），我们要先构建子仓库的代码，构建成功后，脚本会将构建产物复制到 <code>echarts-website</code> 目录下。也就是说，<code>echarts-website</code> 目录就是网站的根目录，后面要注意修改配置文件中的路径。</p>
<p>项目比较老，文档中没有说明 Node 版本，踩了不少坑。最好使用 Node 14，如果报错，再尝试 Node 16。</p>
<h3>1. echarts-handbook</h3>
<ul>
<li><p>进入 <code>echarts-handbook</code> 目录，执行 <code>npm i</code> 安装依赖</p>
</li>
<li><p>修改 <code>configs/config.localsite.js</code> 中的地址，改成你要部署的主机 IP 和端口，我这里放在本地的 3000 端口</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets/images/image-20231010094153193.png"/></p>
</li>
<li><p>执行 <code>npm run build:localsite</code> 打包，打印出下面的日志，说明构建成功</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets/images/image-20231010094548907.png"/></p>
</li>
</ul>
<h3>2. echarts-examples</h3>
<ul>
<li><p>进入 <code>echarts-examples</code> 目录，执行 <code>npm i</code> 安装依赖</p>
</li>
<li><p>执行 <code>npm run localsite</code> 打包，打印出下面的日志，说明构建成功</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets/images/image-20231010094856192.png"/></p>
</li>
</ul>
<h3>3. echarts-doc</h3>
<ul>
<li><p>进入 <code>echarts-doc</code> 目录，执行 <code>npm i</code> 安装依赖</p>
</li>
<li><p>修改 <code>config/env.localsite.js</code> 中的地址，改成你要部署的主机 IP 和端口，我这里放在本地的 3000 端口</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets/images/image-20231010095055812.png"/></p>
</li>
<li><p>执行 <code>npm run localsite</code> 打包，打印出下面的日志，说明构建成功</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets/images/image-20231010095237853.png"/></p>
</li>
</ul>
<h3>4. echarts-www</h3>
<ul>
<li><p>进入 <code>echarts-doc</code> 目录，执行 <code>npm i</code> 安装依赖</p>
</li>
<li><p>修改 <code>config/env.localsite.js</code> 中的地址，改成你要部署的主机 IP 和端口，我这里放在本地的 3000 端口</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets/images/image-20231010093520617.png"/></p>
</li>
<li><p>执行 <code>npm run localsite</code> 打包，打印出下面的日志，说明构建成功（这个过程比较长，耐心等待）</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets/images/image-20231010095858363.png"/></p>
</li>
</ul>
<h2>三、测试</h2>
<p>我这里使用 node 的 serve 包来测试。</p>
<ul>
<li>全局安装 <code>serve</code></li>
</ul>
<pre><code class="language-sh">npm i serve -g</code></pre><ul>
<li><p>回到 <code>echarts-website</code> 目录</p>
</li>
<li><p>启动服务（-p 指定端口，默认 3000）</p>
</li>
</ul>
<pre><code class="language-sh">serve . -p 3000</code></pre><ul>
<li><p>访问 <a href="http://localhost:3000">http://localhost:3000</a></p>
<img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets/images/image-20231010100326997.png" alt="image-20231010100326997" style="zoom:50%;" />

<img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets/images/image-20231010100350577.png" alt="image-20231010100350577" style="zoom:50%;" />

<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets/images/image-20231010100431028.png"/></p>
</li>
</ul>
<h2>四、其他</h2>
<p>最终生成的 <code>echarts-website</code> 目录就是网站的根目录，包含所有文件，可以在这个目录的基础上构建 Docker 镜像，或者使用 Nginx 提供服务。</p>
<p>关于 Docker 和 Nginx 的用法不在本文讨论范围，感兴趣的朋友可以自己折腾。</p>

      <p style='text-align: right'>
      <a href='https://zb81.icu/posts/toss/echarts-deploy#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">661405d67d1dabd5a33cf5e2</guid>
  <category>Post</category>
<category>折腾</category>
 </item>
  <item>
    <title>给 React 事件添加类型的几种姿势</title>
    <link>https://zb81.icu/posts/react/annotate-types-for-react-events</link>
    <pubDate>Mon, 22 May 2023 13:23:56 GMT</pubDate>
    <description>本文参考了 Matt Pocock 的文章 Event Types in React and Typ</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://zb81.icu/posts/react/annotate-types-for-react-events'>https://zb81.icu/posts/react/annotate-types-for-react-events</a></blockquote>
      <p>本文参考了 <a href="https://twitter.com/mattpocockuk">Matt Pocock</a> 的文章 <a href="https://www.totaltypescript.com/event-types-in-react-and-typescript">Event Types in React and TypeScript</a> ，有条件的可以阅读英文原版。</p>
<h2>一、前言</h2>
<p>用 React 和 TypeScript 开发时，会经常碰到这个报错：</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets@v0.0.1/images/image-20231221092602795.png"/></p>
<p>当我们给不同的 DOM 元素添加事件处理函数时，<code>onChange</code> 中参数 <code>e</code> 接收到的事件类型是不同的，我们必须为<code>e</code> 指定正确的类型。</p>
<h2>二、解决方案</h2>
<h3>1. 鼠标悬浮</h3>
<p>我们把鼠标悬浮在 <code>onChange</code> 上，IDE 会给出相应的类型，直接复制，再将类型添加给声明的事件处理函数：</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets@v0.0.1/images/image-20231221093203173.png"/></p>
<blockquote>
<p>我们需要的是 <code>?:</code> 后面的这部分代码。</p>
</blockquote>
<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets@v0.0.1/images/image-20231221093241375.png"/></p>
<h3>2. 内联函数</h3>
<p>当我们只想为 <code>e</code> 添加类型时，首先要知道 <code>e</code> 的类型是什么。</p>
<p>创建一个内联事件处理函数，将鼠标悬浮在参数 <code>e</code> 上，IDE 会给出正确的类型，选中并复制：</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets@v0.0.1/images/image-20231221094156684.png"/></p>
<p>然后添加到我们的处理函数中：</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets@v0.0.1/images/image-20231221094300770.png"/></p>
<h3>3. 使用 <code>React.ComponentProps</code></h3>
<p><a href="https://www.totaltypescript.com/react-component-props-type-helper"><code>React.ComponentProps</code></a> 是一个类型工具，用于获取组件或元素的类型。</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets@v0.0.1/images/image-20231221095115296.png"/></p>
<h3>4. <code>EventFor</code> 工具类型</h3>
<p>第三种方案非常方便，但是如果我们只是想给 <code>e</code> 添加类型，有没有更优雅的写法呢？</p>
<p>我们可以用 TypeScript 内置的工具类型 <code>Parameters</code> 、<code>NonNullable</code> 以及索引访问来实现：</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets@v0.0.1/images/image-20231221095612679.png"/></p>
<p>解释一下上面的代码：</p>
<ul>
<li><code>React.ComponentProps&lt;&#39;input&#39;&gt;[&#39;onChange&#39;]</code> 在第三种方案中用过，但是这个类型包含 <code>undefined</code> ，需要使用 <code>NonNullable</code> 把它去掉；</li>
<li><code>NonNullable</code> 用于去除某个类型中的 <code>null</code> 和 <code>undefined</code> ；</li>
<li><code>Parameters</code> 用于获取某个函数的所有参数类型，返回一个类型数组，再使用 <code>[0]</code> 取出第一个参数的类型。</li>
</ul>
<p>如此一来，我们就拿到了参数 <code>e</code> 的正确类型。</p>
<p>但是，上面这种写法有点冗杂，我们可以再进一步，封装一个工具类型：</p>
<pre><code class="language-typescript">type GetEventHandlers&lt;
  T extends keyof JSX.IntrinsicElements,
&gt; = Extract&lt;keyof JSX.IntrinsicElements[T], `on${string}`&gt;

type EventFor&lt;
  TElement extends keyof JSX.IntrinsicElements,
  THandler extends GetEventHandlers&lt;TElement&gt;,
&gt; = JSX.IntrinsicElements[TElement][THandler] extends
  | (((e: infer TEvent) =&gt; any) | undefined)
  ? TEvent
  : never</code></pre><p>上面这段代码需要一些类型体操的知识，这里只解释大概原理：</p>
<ul>
<li><p><code>JSX.IntrinsicElements</code> 是一个 <code>interface</code> ，其中声明了所有 HTML 元素的类型；</p>
</li>
<li><p><code>GetEventHandlers</code> 接收一个元素类型，通过 <code>Extract</code> 提取该元素所有 <code>onXxxx</code> 形式的属性，也就是所有的事件：</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets@v0.0.1/images/image-20231221101337607.png"/></p>
</li>
<li><p><code>EventFor</code> 接收两个泛型，分别代表元素类型和事件名称，根据这两个泛型从 <code>JSX.IntrinsicElements</code> 中取出对应事件的处理函数的类型声明，然后通过 <code>extends</code> 以及 <code>infer</code> 关键字推断参数 <code>e</code> 的类型并返回。</p>
</li>
</ul>
<p>效果：</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets@v0.0.1/images/image-20231221102009771.png"/></p>

      <p style='text-align: right'>
      <a href='https://zb81.icu/posts/react/annotate-types-for-react-events#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">6606265fefb8bb07d7fac195</guid>
  <category>Post</category>
<category>React</category>
 </item>
  <item>
    <title>OMZ 美化</title>
    <link>https://zb81.icu/posts/dev-env/omz-beautify</link>
    <pubDate>Wed, 29 Jun 2022 12:46:18 GMT</pubDate>
    <description>仅支持 MacOS / Linux / WSL

由于经常折腾开发环境，将 shell 美化过程记录</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://zb81.icu/posts/dev-env/omz-beautify'>https://zb81.icu/posts/dev-env/omz-beautify</a></blockquote>
      <blockquote>
<p>仅支持 MacOS / Linux / WSL</p>
</blockquote>
<p>由于经常折腾开发环境，将 shell 美化过程记录一下，方便以后查阅。</p>
<p>效果图：</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb980921/blog-assets/images/Kapture%202023-03-26%20at%2021.04.41.gif"/></p>
<h2>一、安装 oh-my-zsh</h2>
<pre><code class="language-bash">sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"</code></pre><p>安装成功后，之前的 <code>.zshrc</code> 文件会被重命名为 <code>.zshrc.pre-oh-my-zsh</code> ，你需要将 shell 配置迁移到新的 <code>.zshrc</code> 。</p>
<h2>二、安装 spaceship 主题</h2>
<h3>1. 克隆仓库</h3>
<pre><code class="language-bash">git clone https://github.com/spaceship-prompt/spaceship-prompt.git "$ZSH_CUSTOM/themes/spaceship-prompt" --depth=1</code></pre><h3>2. 链接主题目录至 oh-my-zsh</h3>
<pre><code class="language-bash">ln -s "$ZSH_CUSTOM/themes/spaceship-prompt/spaceship.zsh-theme" "$ZSH_CUSTOM/themes/spaceship.zsh-theme"</code></pre><h3>3. 编辑 <code>.zshrc</code></h3>
<pre><code class="language-bash">ZSH_THEME="spaceship"

SPACESHIP_TIME_SHOW="true" # 显示时间
SPACESHIP_USER_SHOW="always" # 显示用户名
SPACESHIP_USER_COLOR="212" # 猛男粉配色</code></pre><h2>三、安装插件</h2>
<h3>1. autojump</h3>
<p>这个插件会根据输入的关键字进行目录联想，假设在某一时刻进入过 <code>~/work/test</code> 目录。当你在终端输入 <code>j te</code> 时，插件会进行联想，匹配到 <code>~/work/test</code> 目录，然后进入。</p>
<ul>
<li><p>安装</p>
<ul>
<li><p>MacOS</p>
<pre><code class="language-bash">brew install autojump</code></pre></li>
<li><p>Debian / Ubuntu</p>
<pre><code class="language-bash">sudo apt install autojump</code></pre></li>
</ul>
</li>
<li><p>修改 <code>.zshrc</code> ，在 <code>plugins</code> 中添加 <code>autojump</code></p>
<pre><code class="language-bash">plugins=(
  git 
  autojump
)</code></pre></li>
</ul>
<h3>2. zsh-autosuggestions</h3>
<p>输入命令时，终端会自动提示你接下来可能要输入的命令，这时按下方向右键 <code>→</code> 便可输出这些命令，非常方便。</p>
<ul>
<li><p>安装</p>
<pre><code class="language-bash">git clone https://gitee.com/phpxxo/zsh-autosuggestions.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions</code></pre></li>
<li><p>修改 <code>.zshrc</code> ，在 <code>plugins</code> 中添加</p>
<pre><code class="language-bash">plugins=(
  git
  autojump
  zsh-autosuggestions
)</code></pre></li>
</ul>
<h3>3. zsh-syntax-highlighting</h3>
<p>输入命令时，错误的命令将显示为红色，正确的命令显示为绿色。</p>
<ul>
<li><p>安装</p>
<pre><code class="language-bash">git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting</code></pre></li>
<li><p>修改 <code>.zshrc</code> ，在 <code>plugins</code> 中添加</p>
<pre><code class="language-bash">plugins=(
  git
  autojump
  zsh-autosuggestions
  zsh-syntax-highlighting
)</code></pre></li>
</ul>

      <p style='text-align: right'>
      <a href='https://zb81.icu/posts/dev-env/omz-beautify#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">6605bb73fcac1134cabcb050</guid>
  <category>Post</category>
<category>开发环境</category>
 </item>
  <item>
    <title>C 语言中输入缓冲区的坑</title>
    <link>https://zb81.icu/posts/basics/pitfalls-of-input-buffer-in-c</link>
    <pubDate>Sat, 16 Apr 2022 02:10:41 GMT</pubDate>
    <description>最近在学 C，跟着《C Primer Plus》做案例的时候碰到了一个问题，导致程序陷入了死循环，代</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://zb81.icu/posts/basics/pitfalls-of-input-buffer-in-c'>https://zb81.icu/posts/basics/pitfalls-of-input-buffer-in-c</a></blockquote>
      <p>最近在学 C，跟着《C Primer Plus》做案例的时候碰到了一个问题，导致程序陷入了死循环，代码如下：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include "hotel.h"

int menu(void) {
    int code, status;

    printf(
        "\n%s%s\n"
        "1) Fairfield Arms           2) Hotel Olympic\n"
        "3) Chertworthy Plaza        4) The Stockton\n"
        "5) quit\n"
        "Enter the number of the desired hotel: ",
        STARS, STARS
    );

    while((status = scanf("%d", &code)) != 1 || (code&lt;1 || code&gt;5)) {
        printf("Enter an integer from 1 to 5, please: ");
    }

    return code;
}</code></pre><p>这个函数的功能是，打印一个菜单供用户选择，将用户选择的选项结果返回。程序需要对错误的输入进行处理：</p>
<ul>
<li>用户输入的不是数字</li>
<li>用户输入的数字不在 1-5 的范围内</li>
</ul>
<p>如果是上面两种情况之一，则进入 while 循环，提示用户输入正确的数字。</p>
<p>针对第二种错误，上面的代码可以正常运行：</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb980921/blog-assets/images/Kapture%202023-06-29%20at%2014.59.32.gif"/></p>
<p>但是，当我们输入一个字母后，问题出现了：</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb980921/blog-assets/images/Kapture%202023-06-29%20at%2015.01.56.gif"/></p>
<p>出现这个 bug 的时候，我记得之前好像在哪里也碰到过，就把《C Primer Plus》的目录往前翻，翻到 4.4(printf() 和 scanf()) 那一章，找到了答案：</p>
<blockquote>
<p>如果遇到一个非数字字符，scanf() 会将该字符放回<strong>输入</strong>，不会把值赋给指定变量。程序在下一次读取<strong>输入</strong>时，首先读到的就是刚刚放回的字符。</p>
<p>所以，while 循环条件表达式中的 <code>scanf(&quot;%d&quot;, &amp;code)</code> 每次从<strong>输入</strong>中读到的都是字符 &#39;a&#39;，条件永远为 true，程序崩溃。</p>
</blockquote>
<h2>二、输入缓冲区</h2>
<p>上面这段话有几个加粗的地方：<strong>输入</strong>。</p>
<p>这里的 “输入” 是啥？有什么用？其实，“输入” 指的就是输入缓冲区，这里我们只讨论行缓冲(通过换行符刷新缓冲区)。</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets/images/Kapture%202023-06-29%20at%2020.01.40.gif"/></p>
<p>首先，我们粗略地分析这张 gif 图片展示的输入输出的流程：</p>
<pre><code class="language-">输入设备依次输入字符(键盘) -&gt; 字符进入缓冲区 -&gt; 按下回车 -&gt; 输出设备依次输入字符(显示器)</code></pre><p>如果没有缓冲区，每次按下键盘，程序会立即将刚刚输入的字符输出到显示器上，就像这样：</p>
<pre><code class="language-">hheelllloo,,wwoorrlldd!!</code></pre><p>是不是觉得非常难受？而且，由于输入的字符立即被输出，当我们不小心输错字符的时候，无法通过退格键进行修改，输入缓冲区帮助我们解决了这些问题。每输入一个字符，程序会将该字符暂时存放在某一块内存中，按下回车键，程序将这些字符作为一个块进行依次读取。</p>
<h3>读取缓冲区字符的方式</h3>
<h4>1. scanf()</h4>
<pre><code class="language-c">int n;
while(scanf("%d", &n) != 1) {
  printf("Enter an integer, please: ");
}</code></pre><p>假设缓冲区中有这些字符：<code>123abcd</code></p>
<p><code>scanf()</code> 函数从缓冲区中读取字符(每读取一个，缓冲区中的字符就会少一个)，发现第一个字符是 &#39;1&#39;，可以转成 int，于是继续往后读取，直到遇见 &#39;a&#39;。然后，通过转换说明 &quot;%d&quot; 将 &quot;123&quot; 转换为 int 类型写入内存，返回成功操作的个数 1，退出 while 循环。此时，缓冲区剩下的字符是 <code>abcd</code>，供后续的函数读取，比如 <code>scanf(&quot;%*s&quot;)</code>、<code>getchar()</code> 等。</p>
<p>但是，如果缓冲区中的字符一开始就是 <code>abcd</code> 呢？程序会陷入死循环，因为缓冲区的字符没人读了，一直卡在那里，while 循环的条件一直成立。</p>
<p>所以，解决方案呼之欲出，只要我们在循环内部将这些垃圾字符读走就 ok 了。</p>
<pre><code class="language-C">int n;
while(scanf("%d", &n) != 1) {
  scanf("%*s");
  printf("Enter an integer, please: ");
}</code></pre><p>我们在循环内部加了一行 <code>scanf(&quot;%*s&quot;);</code>，修饰符 &#39;*&#39; 的作用是跳过赋值操作，这行语句可以帮助我们将没用的字符读走，直到下一个空格。</p>
<blockquote>
<p>其实，这种做法还有一个问题，如果缓冲区中是 <code>abcd efg</code> ，中间有个空格的话，循环会多跑一遍，打印的提示语句会重复。</p>
</blockquote>
<h4>2. getchar()</h4>
<p>除了上面的做法，更常用、更优秀的方案是使用 <code>getchar()</code> 函数，该函数用于读取一个字符，并将结果返回：</p>
<pre><code class="language-C">char ch;
while((ch = getchar()) != EOF){
  printf("%c", ch);
}</code></pre><p>上面代码的效果有点类似于 <code>echo</code> ，读取缓冲区中的字符并将其输出。</p>
<p>所以，我们可以修改一下之前的代码：</p>
<pre><code class="language-C">int n;
while(scanf("%d", &n) != 1) {
  while(getchar() != '\n') continue;
  printf("Enter an integer, please: ");
}</code></pre><p>这一次，我们在循环内部加了一行代码 <code>while(getchar() != &#39;\n&#39;) continue;</code> (这里的 <code>continue</code> 写不写无所谓)，将缓冲区中的垃圾字符全部读走。</p>
<p>注意这里的条件 <code>getchar() != &#39;\n&#39;</code> ，为什么要判断？</p>
<p>如果不加这个条件，程序将陷入无限循环，会一直“吞掉”用户的输入，后续的代码永远无法运行，尽管用户按了回车键(换行符 &#39;\n&#39; 也会存入缓冲区)。</p>
<p>当用户按下回车，意味着程序需要开启一段新的逻辑，所以，如果缓冲区中出现了换行符，应该退出循环，将后续的字符交给其他函数处理。</p>
<h2>三、总结</h2>
<p>当 <code>scanf()</code> 函数的赋值操作失败时，会将读取的字符放回输入缓冲区，我们需要及时地将这些无效的字符处理掉(使用 <code>getchar()</code> 函数)。</p>
<p><img src="https://cdn.jsdelivr.net/gh/zb81/blog-assets/images/Kapture%202023-06-29%20at%2021.22.38.gif"/></p>
<p>此外，开发终端应用时，应充分考虑用户的错误输入并妥善地处理。</p>

      <p style='text-align: right'>
      <a href='https://zb81.icu/posts/basics/pitfalls-of-input-buffer-in-c#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">6606233cefb8bb07d7fabff8</guid>
  <category>Post</category>
<category>编程基础</category>
 </item>
  
</channel>
</rss>