<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>一心的小屋</title><description>一心的小屋～❤️</description><link>https://www.redon.cc/</link><item><title>使用 VConsole 调试移动端网页</title><link>https://www.redon.cc/posts/vconsole/</link><guid isPermaLink="true">https://www.redon.cc/posts/vconsole/</guid><description>移动端网页调试</description><pubDate>Mon, 23 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在 &lt;code&gt;index.html&lt;/code&gt; 页面 &lt;code&gt;head&lt;/code&gt; 或者 &lt;code&gt;body&lt;/code&gt; 尾部添加以下脚本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 在页面上添加VConsole方便调试--&amp;gt;
&amp;lt;script src=&quot;https://unpkg.com/vconsole/dist/vconsole.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
  var vConsole = new window.VConsole()
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>VConsole</category><category>Mobile</category><author>ChanZhaoYu</author></item><item><title>vite 项目更新后客户端更新提示</title><link>https://www.redon.cc/posts/vite%E9%A1%B9%E7%9B%AE%E6%9B%B4%E6%96%B0%E5%90%8E%E5%AE%A2%E6%88%B7%E7%AB%AF%E6%9B%B4%E6%96%B0%E6%8F%90%E7%A4%BA/</link><guid isPermaLink="true">https://www.redon.cc/posts/vite%E9%A1%B9%E7%9B%AE%E6%9B%B4%E6%96%B0%E5%90%8E%E5%AE%A2%E6%88%B7%E7%AB%AF%E6%9B%B4%E6%96%B0%E6%8F%90%E7%A4%BA/</guid><description>vite 打包的项目通过时间比对进行客户端每次更新提示</description><pubDate>Thu, 26 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;原理&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;打包时在 &lt;code&gt;index.html&lt;/code&gt; 写入打包时间&lt;/li&gt;
&lt;li&gt;定时检测服务端的 &lt;code&gt;index.html&lt;/code&gt; 中的时间&lt;/li&gt;
&lt;li&gt;比对当前浏览器缓存中的时间和服务器时间&lt;/li&gt;
&lt;li&gt;弹窗提示更新&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Vite 部分配置&lt;/h2&gt;
&lt;p&gt;在项目根目录添加一个 &lt;code&gt;build&lt;/code&gt; 文件夹，存放 &lt;code&gt;vite&lt;/code&gt; 相关的 &lt;code&gt;.ts&lt;/code&gt; 文件。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;build&lt;/code&gt; 文件夹添加 &lt;code&gt;html.ts&lt;/code&gt;，编写一个 &lt;code&gt;vite&lt;/code&gt; 插件，作用是打包时候在 &lt;code&gt;index.html&lt;/code&gt; 文件的 &lt;code&gt;head&lt;/code&gt; 内插入当前打包的时间。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 路径 /build/html.ts
import type { Plugin } from &apos;vite&apos;

export function setupHtmlPlugin(buildTime: string) {
  const plugin: Plugin = {
    name: &apos;html-plugin&apos;,
    apply: &apos;build&apos;,
    transformIndexHtml(html) {
      return html.replace(&apos;&amp;lt;head&amp;gt;&apos;, `&amp;lt;head&amp;gt;\n    &amp;lt;meta name=&quot;buildTime&quot; content=&quot;${buildTime}&quot;&amp;gt;`)
    }
  }

  return plugin
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;build&lt;/code&gt; 文件夹添加 &lt;code&gt;time.ts&lt;/code&gt;，并且安装 &lt;code&gt;dayjs&lt;/code&gt; 库，编写一个获取时间的函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 路径 /build/time.ts
import dayjs from &apos;dayjs&apos;
import timezone from &apos;dayjs/plugin/timezone&apos;
import utc from &apos;dayjs/plugin/utc&apos;

export function getBuildTime() {
  dayjs.extend(utc)
  dayjs.extend(timezone)

  const buildTime = dayjs.tz(Date.now(), &apos;Asia/Shanghai&apos;).format(&apos;YYYY-MM-DD HH:mm:ss&apos;)

  return buildTime
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;vite.config.ts&lt;/code&gt; 文件中引用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import process from &apos;node:process&apos;
import { fileURLToPath, URL } from &apos;node:url&apos;
import { defineConfig, loadEnv } from &apos;vite&apos;
import { setupHtmlPlugin } from &apos;./build/html&apos;
import { getBuildTime } from &apos;./build/time&apos;

export default defineConfig(() =&amp;gt; {
  // 当前打包时间
  const buildTime = getBuildTime()

  return {
    base: &apos;/&apos;,
    plugins: [
      vue(),
      // 使用插件
      setupHtmlPlugin(buildTime)
    ],
    define: {
      // 注入全局的 BUILD_TIME 变量
      BUILD_TIME: JSON.stringify(buildTime)
    },
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;添加类型声明&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// global.d.ts
export { }

declare global {
  export interface Window {
    //
  }

  export const BUILD_TIME: string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;项目配置部分&lt;/h2&gt;
&lt;p&gt;添加 &lt;code&gt;src/plugins&lt;/code&gt; 目录，添加 &lt;code&gt;app.ts&lt;/code&gt; 文件，编写相关逻辑，这里使用 &lt;code&gt;Vue3&lt;/code&gt; 和 &lt;code&gt;Naive UI&lt;/code&gt; 作为参考&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 路径 src/plugin/app.ts
import type { App } from &apos;vue&apos;
import { NButton } from &apos;naive-ui&apos;
import { h } from &apos;vue&apos;

const UPDATE_CHECK_INTERVAL = 3 * 60 * 1000

export function setupAppVersionNotification() {
  let isShow = false
  let updateInterval: ReturnType&amp;lt;typeof setInterval&amp;gt; | undefined

  const shouldCheckForUpdates = [!isShow, document.visibilityState === &apos;visible&apos;, !import.meta.env.DEV].every(Boolean)

  const checkForUpdates = async () =&amp;gt; {
    if (!shouldCheckForUpdates)
      return

    const buildTime = await getHtmlBuildTime()

    if (buildTime === BUILD_TIME) {
      return
    }

    isShow = true

    const n = window.$notification?.create({
      title: &apos;系统版本更新通知&apos;,
      content: &apos;检测到系统有新版本发布，是否立即刷新页面？&apos;,
      action() {
        return h(&apos;div&apos;, { style: { display: &apos;flex&apos;, justifyContent: &apos;end&apos;, gap: &apos;12px&apos;, width: &apos;325px&apos; } }, [
          h(
            NButton,
            {
              onClick() {
                n?.destroy()
              }
            },
            () =&amp;gt; &apos;稍后再说&apos;
          ),
          h(
            NButton,
            {
              type: &apos;primary&apos;,
              onClick() {
                location.reload()
              }
            },
            () =&amp;gt; &apos;立即刷新&apos;
          )
        ])
      },
      onClose() {
        isShow = false
      }
    })
  }

  const startUpdateInterval = () =&amp;gt; {
    if (updateInterval) {
      clearInterval(updateInterval)
    }
    updateInterval = setInterval(checkForUpdates, UPDATE_CHECK_INTERVAL)
  }

  if (shouldCheckForUpdates) {
    document.addEventListener(&apos;visibilitychange&apos;, () =&amp;gt; {
      if (document.visibilityState === &apos;visible&apos;) {
        checkForUpdates()
        startUpdateInterval()
      }
    })

    startUpdateInterval()
  }
}

async function getHtmlBuildTime() {
  const baseUrl = &apos;/&apos;

  const res = await fetch(`${baseUrl}index.html?time=${Date.now()}`)

  const html = await res.text()

  const match = html.match(/&amp;lt;meta name=&quot;buildTime&quot; content=&quot;(.*)&quot;&amp;gt;/)

  const buildTime = match?.[1] || &apos;&apos;

  return buildTime
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;src/main.ts&lt;/code&gt; 文件引入刚刚文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createApp } from &apos;vue&apos;
import App from &apos;./App.vue&apos;
import { setupAppVersionNotification } from &apos;./plugins/app&apos;

async function bootstrap() {
  const app = createApp(App)
  // 如果是 react，同理也是在 dom 加载前
  setupAppVersionNotification()
  app.mount(&apos;#app&apos;)
}

bootstrap()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;其它方案&lt;/h2&gt;
&lt;p&gt;使用 &lt;code&gt;vite-plugin-version&lt;/code&gt; 插件在打包时读取 &lt;code&gt;package.json&lt;/code&gt; 内的版本号并生成 &lt;code&gt;version.json&lt;/code&gt; 文件，比对文件版本进行提示更新，原理其实一样。&lt;/p&gt;
&lt;h2&gt;备注&lt;/h2&gt;
&lt;p&gt;代码部份来自 &lt;a href=&quot;https://docs.soybeanjs.cn/zh/&quot;&gt;SoybeanAdmin&lt;/a&gt;，有改动&lt;/p&gt;
&lt;p&gt;完&lt;/p&gt;
</content:encoded><category>Vite</category><author>ChanZhaoYu</author></item><item><title>在 Next.js 中使用 Zustand 的持久中间件</title><link>https://www.redon.cc/posts/%E5%9C%A8-nextjs-%E4%B8%AD%E4%BD%BF%E7%94%A8-zustand-%E7%9A%84%E6%8C%81%E4%B9%85%E4%B8%AD%E9%97%B4%E4%BB%B6/</link><guid isPermaLink="true">https://www.redon.cc/posts/%E5%9C%A8-nextjs-%E4%B8%AD%E4%BD%BF%E7%94%A8-zustand-%E7%9A%84%E6%8C%81%E4%B9%85%E4%B8%AD%E9%97%B4%E4%BB%B6/</guid><description>在本文中，我们将讨论在 Next.js 中使用 Zustand 的持久化中间件时出现的常见错误。你可能收到过诸如 “文本内容与服务器渲染的 HTML 不匹配”、“由于初始用户界面与服务器上渲染的内容不匹配，水合作用（hydration）失败” 以及 “水合过程中出现错误。</description><pubDate>Tue, 17 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;原文来自 &lt;a href=&quot;https://dev.to/abdulsamad/how-to-use-zustands-persist-middleware-in-nextjs-4lb5&quot;&gt;How to use Zustand&apos;s persist middleware in Next.js&lt;/a&gt;，此作翻译存档。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;使用持久化中间件创建存储&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { create } from &quot;zustand&quot;;
import { persist } from &quot;zustand/middleware&quot;;

// Custom types for theme
import { User } from &quot;./types&quot;;

interface AuthState {
  isAuthenticated: boolean;
  user: null | User;
  token: null | string;
  login: (email: string, password: string) =&amp;gt; Promise&amp;lt;void&amp;gt;;
  register: (userInfo: FormData) =&amp;gt; Promise&amp;lt;void&amp;gt;;
  logout: () =&amp;gt; void;
}

const useAuthStore = create&amp;lt;AuthState&amp;gt;()(
  persist(
    (set) =&amp;gt; ({
      isAuthenticated: false,
      user: null,
      token: null,
      login: async (email, password) =&amp;gt; {
        // Login user code
      },
      register: async (userInfo) =&amp;gt; {
        // Registering user code
      },
      logout: () =&amp;gt; {
        // Logout user code
      },
    }),
    {
      name: &quot;auth&quot;,
    }
  )
);

export default useAuthStore;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;如果我们尝试在组件中直接访问上述存储，当用户已登录时，就会出现水合错误，因为这与存储的初始状态不匹配。&lt;/p&gt;
&lt;p&gt;我们会出现水合错误，是因为 &lt;code&gt;Zustand&lt;/code&gt; 包含了来自持久化中间件（如本地存储等）的数据，而此时水合过程尚未完成，并且服务器渲染的存储具有初始状态值，这就导致了存储的状态数据出现不匹配的情况。&lt;/p&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;我通过创建一个状态来解决这个问题，在 &lt;code&gt;useEffect&lt;/code&gt; 钩子函数内将 &lt;code&gt;Zustand &lt;/code&gt;存储的状态值写入该状态中，然后使用这个状态来访问存储的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useState, useEffect } from &apos;react&apos;;

const useStore = &amp;lt;T, F&amp;gt;(
  store: (callback: (state: T) =&amp;gt; unknown) =&amp;gt; unknown,
  callback: (state: T) =&amp;gt; F
) =&amp;gt; {
  const result = store(callback) as F;
  const [data, setData] = useState&amp;lt;F&amp;gt;();

  useEffect(() =&amp;gt; {
    setData(result);
  }, [result]);

  return data;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;必须在组件中通过这个 &lt;code&gt;hook&lt;/code&gt; 来访问存储。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const store = useStore(useAuthStore, (state) =&amp;gt; state)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>JS</category><category>Next</category><author>ChanZhaoYu</author></item><item><title>新系统 Debian 优化配置</title><link>https://www.redon.cc/posts/debian%E6%96%B0%E6%9C%BA%E5%99%A8%E4%BC%98%E5%8C%96%E9%85%8D%E7%BD%AE/</link><guid isPermaLink="true">https://www.redon.cc/posts/debian%E6%96%B0%E6%9C%BA%E5%99%A8%E4%BC%98%E5%8C%96%E9%85%8D%E7%BD%AE/</guid><description>对于新系统 Debian 系统的优化配置</description><pubDate>Sun, 29 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;原文来自 &lt;a href=&quot;https://linux.do/t/topic/160305&quot;&gt;【配置优化】我拿到VPS服务器必做的那些事&lt;/a&gt;，备份记录&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;一、系统设置&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;更新软件库&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;apt update -y &amp;amp;&amp;amp; apt upgrade -y&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;更新、安装必备软件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;apt install sudo curl wget nano&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;校正系统时间&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将时区改为上海：&lt;code&gt;sudo timedatectl set-timezone Asia/Shanghai&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;查看当前时区：&lt;code&gt;timedatectl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;列出所有时区：&lt;code&gt;timedatectl list-timezones&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;系统参数调优&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内核参数调整：例如，增加 TCP 缓冲区大小、修改系统队列长度等，这些改变有助于提高网络吞吐量和减少延迟。&lt;/li&gt;
&lt;li&gt;性能优化：安装和配置 Tuned 和其他系统性能优化工具来自动调整和优化服务器的运行状态。&lt;/li&gt;
&lt;li&gt;资源限制：例如，设置文件打开数量的限制，这可以防止某些类型的资源耗尽攻击。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实现方法：&lt;code&gt;bash &amp;lt;(wget -qO- https://raw.githubusercontent.com/jerry048/Tune/main/tune.sh) -t&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;二、BBR&lt;/h2&gt;
&lt;p&gt;BBR 是 Google 提出的一种新型拥塞控制算法（Bottleneck Bandwidth and RTT），全称为瓶颈带宽和往返传播时间。&lt;/p&gt;
&lt;p&gt;在 Linux 系统中，BBR 主要有以下特点和作用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;提高网络性能：它可以显著提高吞吐量和降低 TCP 连接的延迟，使数据传输更加高效。&lt;/li&gt;
&lt;li&gt;适应不同网络环境：适合高延迟、高带宽的网络链路，以及慢速接入网络的用户，能在一定丢包率的网络链路上充分利用带宽，并降低网络链路上的缓冲区占用率从而降低延迟。&lt;/li&gt;
&lt;li&gt;优化拥塞控制：BBR 改变了传统基于丢包反馈的拥塞控制机制，通过精确测量往返传播时间（RTT）和瓶颈带宽等参数来更有效地控制数据发送速率，避免了传统算法中因单纯丢包判断拥塞而导致的带宽利用率不高和端到端延迟大等问题。&lt;/li&gt;
&lt;li&gt;提升网络稳定性：有助于减少网络拥塞和数据包丢失，提高网络的稳定性和可靠性。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因为我本人是 PT 玩家，所以接触到了一位大佬自己魔改的 BBR 版本，也就是 BBRx。该版本调整了类似 startup（启动阶段）、drain（排空阶段）、probe_bw（探测带宽阶段）、probe_rtt（探测往返时间阶段）等状态下的一些关键参数，如 pacing_gain（发送速率增益）、cwnd_gain（拥塞窗口增益）等，个人来说觉得比原版 BBR 的效果更好，如果不喜欢的话，可以选择原版 BBR 进行安装。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;开启 BBRX 加速
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bash &amp;lt;(wget -qO- https://raw.githubusercontent.com/jerry048/Tune/main/tune.sh) -x&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;重启 VPS、使内核更新和 BBR 设置都生效
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sudo reboot&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;确认 BBR 开启
&lt;ul&gt;
&lt;li&gt;如果你想确认 BBR 是否正确开启，可以使用下面的命令：&lt;code&gt;lsmod | grep bbr&lt;/code&gt;，此时应该返回这样的结果：&lt;code&gt;tcp_bbrx&lt;/code&gt;、&lt;code&gt;tcp_bbr&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;再次重启 VPS
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sudo reboot&lt;/code&gt;，如果只有&lt;code&gt;tcp_bbr&lt;/code&gt;则再等几分钟&lt;code&gt;reboot&lt;/code&gt;。此时再进行查询：&lt;code&gt;lsmod | grep bbr&lt;/code&gt;，此时应该返回这样的结果：&lt;code&gt;tcp_bbrx&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;开启 BBR 加速（备选）
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wget --no-check-certificate https://github.com/teddysun/across/raw/master/bbr.sh &amp;amp;&amp;amp; chmod +x bbr.sh &amp;amp;&amp;amp;./bbr.sh&lt;/code&gt;，也是重启 VPS 生效。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;三、添加 SWAP&lt;/h2&gt;
&lt;p&gt;在 Linux 系统中，SWAP（交换空间）是指一块磁盘空间，用于在物理内存（RAM）不足时，作为临时的扩展内存来使用。当系统的物理内存使用量接近饱和，Linux 内核会将一些不常使用的内存页交换到 SWAP 分区中，从而为当前运行的程序腾出更多的物理内存。当这些被交换出去的内存页再次被需要时，它们会被重新换回到物理内存中。SWAP 分区的存在可以在一定程度上避免由于物理内存不足导致系统性能严重下降或进程被强制终止的情况。&lt;/p&gt;
&lt;p&gt;因此，SWAP 对于内存小的 VPS 非常有必要，可以提高我们的运行效率。&lt;/p&gt;
&lt;p&gt;这里我们用脚本来添加。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wget -O swap.sh https://raw.githubusercontent.com/yuju520/Script/main/swap.sh &amp;amp;&amp;amp; chmod +x swap.sh &amp;amp;&amp;amp; clear &amp;amp;&amp;amp;./swap.sh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;查看当前内存：&lt;code&gt;free -m&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;四、安装 Docker、Docker-compose 以及修改配置&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Docker
&lt;ul&gt;
&lt;li&gt;Docker 安装
&lt;ul&gt;
&lt;li&gt;非大陆服务器：&lt;code&gt;wget -qO- get.docker.com | bash&lt;/code&gt; 或 &lt;code&gt;curl -fsSL https://get.docker.com -o get-docker.sh &amp;amp;&amp;amp; sh get-docker.sh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;大陆服务器 Docker 安装：&lt;code&gt;curl https://install.1panel.live/docker-install -o docker-install &amp;amp;&amp;amp; sudo bash./docker-install &amp;amp;&amp;amp; rm -f./docker-install&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;查看 Docker 版本：&lt;code&gt;docker -v&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;开机自动启动：&lt;code&gt;sudo systemctl enable docker&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;卸载 Docker：&lt;code&gt;sudo apt-get purge docker-ce docker-ce-cli containerd.io&lt;/code&gt;、&lt;code&gt;sudo apt-get remove docker docker-engine&lt;/code&gt;、&lt;code&gt;sudo rm -rf /var/lib/docker&lt;/code&gt;、&lt;code&gt;sudo rm -rf /var/lib/containerd&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Docker-compose 安装
&lt;ul&gt;
&lt;li&gt;经佬友反馈，Docker 从 18.06.0-ce 版本就开始自带 Docker Compose 工具，因此，我们只需要检验 Docker Compose 的版本。&lt;/li&gt;
&lt;li&gt;查看 Docker Compose 版本：&lt;code&gt;docker compose version&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;五、修改 SSH 端口&lt;/h2&gt;
&lt;p&gt;修改 SSH 端口通常有以下几个主要原因：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;增强安全性：SSH 服务默认使用的 22 端口是攻击者经常扫描和尝试攻击的目标。通过将端口修改为一个不常见的数值，可以减少自动攻击和暴力破解的风险，因为攻击者通常会首先针对常见的默认端口进行攻击。
&lt;ul&gt;
&lt;li&gt;例如，如果攻击者使用自动化工具扫描大量服务器，这些工具可能主要集中在 22 端口。而修改了端口后，就降低了被这类工具轻易发现和攻击的可能性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;减少误连接和非法访问尝试：一些网络环境中可能存在大量的随机连接请求或非法访问尝试，针对默认的 22 端口。更改端口可以减少这类无意义的连接请求。
&lt;ul&gt;
&lt;li&gt;假设您的服务器处于一个公共网络环境中，经常会收到大量的随机连接尝试，其中很多是针对常见端口的。修改 SSH 端口可以减少这类不必要的干扰。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;将默认的 22 端口修改为 55520（暗戳戳地表白）@wanwan：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sudo sed -i &apos;s/^#\?Port 22.*/Port 55520/g&apos; /etc/ssh/sshd_config&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;重启 sshd 服务：&lt;code&gt;sudo systemctl restart sshd&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;六、密钥登录&lt;/h2&gt;
&lt;p&gt;一键生成你的密钥：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wget -O key.sh https://raw.githubusercontent.com/yuju520/Script/main/key.sh &amp;amp;&amp;amp; chmod +x key.sh &amp;amp;&amp;amp; clear &amp;amp;&amp;amp;./key.sh&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：请牢记你生成的密钥，否则会有无法连接 SSH 的后果。&lt;/p&gt;
&lt;h2&gt;七、安装 fail2ban&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;安装 fail2ban：&lt;code&gt;apt install fail2ban&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;配置 fail2ban
&lt;ul&gt;
&lt;li&gt;fail2ban 的配置文件通常位于 &lt;code&gt;/etc/fail2ban/&lt;/code&gt; 目录下，fail2ban 的.conf 配置文件都是可以被.local 覆盖，所以配置方式建议是添加.local 文件，不修改原来的配置文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nano /etc/fail2ban/jail.local&lt;/code&gt;，配置文件如下：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;[DEFAULT]
#忽略的 IP 列表,不受设置限制（白名单）
ignoreip = 127.0.0.1

#允许 ipv6
allowipv6 = auto

#日志修改检测机制（gamin、polling 和 auto 这三种）
backend = systemd

#针对各服务的检查配置，如设置 bantime、findtime、maxretry 和全局冲突，服务优先级大于全局设置

[sshd]

#是否激活此项（true/false）
enabled = true

#过滤规则 filter 的名字，对应 filter.d 目录下的 sshd.conf
filter = sshd

#ssh 端口
port = ssh

#动作的相关参数
action = iptables[name=SSH, port=ssh, protocol=tcp]

#检测的系统的登陆日志文件
logpath = /var/log/secure

#屏蔽时间，单位：秒
bantime = 86400

#这个时间段内超过规定次数会被 ban 掉
findtime = 86400

#最大尝试次数
maxretry = 3
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Ctrl+S 保存并退出。&lt;/li&gt;
&lt;li&gt;设置开机自动启动 fail2ban：&lt;code&gt;sudo systemctl enable fail2ban&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;重新启动 fail2ban：&lt;code&gt;sudo systemctl restart fail2ban&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;查看 fail2ban 的状态：&lt;code&gt;sudo systemctl status fail2ban&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;查看所有可用 jail 的状态：&lt;code&gt;fail2ban-client status&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>Linux</category><category>Debian</category><author>ChanZhaoYu</author></item><item><title>在 Debian 系统中开放端口</title><link>https://www.redon.cc/posts/%E5%9C%A8debian%E7%B3%BB%E7%BB%9F%E4%B8%AD%E5%BC%80%E6%94%BE%E7%AB%AF%E5%8F%A3/</link><guid isPermaLink="true">https://www.redon.cc/posts/%E5%9C%A8debian%E7%B3%BB%E7%BB%9F%E4%B8%AD%E5%BC%80%E6%94%BE%E7%AB%AF%E5%8F%A3/</guid><description>在 Debian 系统中开放端口几种方式</description><pubDate>Sat, 14 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一、使用 iptables（如果使用传统的 iptables 防火墙）&lt;/h2&gt;
&lt;p&gt;1、查看当前的 iptables 规则&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  sudo iptables -L -n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、添加允许端口的规则（以 38042 为例）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo iptables -A INPUT -p tcp --dport 38042 -j ACCEPT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3、 保存规则（如果需要永久生效，需要安装iptables-persistent）&lt;/p&gt;
&lt;p&gt;安装iptables-persistent（如果未安装）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install iptables-persistent
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存规则&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo netfilter-persistent save
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二、使用 ufw（Uncomplicated Firewall，简单防火墙）&lt;/h2&gt;
&lt;p&gt;1、启用 ufw（如果尚未启用）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw enable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、允许 38042 端口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow 38042/tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三、使用 firewalld（如果系统安装并使用 firewalld）&lt;/h2&gt;
&lt;p&gt;1、检查 firewalld 状态&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl status firewalld
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、添加端口到 firewalld 区域（例如 public 区域）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --zone=public --add-port=38042/tcp --permanent
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3、重新加载 firewalld 配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --reload
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>Linux</category><category>Debian</category><author>ChanZhaoYu</author></item><item><title>HarmonyOS Next真机无线调试</title><link>https://www.redon.cc/posts/harmonyosnext%E7%9C%9F%E6%9C%BA%E6%97%A0%E7%BA%BF%E8%B0%83%E8%AF%95/</link><guid isPermaLink="true">https://www.redon.cc/posts/harmonyosnext%E7%9C%9F%E6%9C%BA%E6%97%A0%E7%BA%BF%E8%B0%83%E8%AF%95/</guid><description>HarmonyOS Next 使用无线调试连接方式</description><pubDate>Fri, 09 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前提条件&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在Phone或Tablet上查看设置 &amp;gt; 系统中开发者模式是否存在，如果不存在，可在设置 &amp;gt; 关于手机/关于平板中，连续七次单击“版本号”，直到提示“开启开发者模式”，点击确认开启后输入PIN码（如果已设置），设备将自动重启，请等待设备完成重启。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在设备运行应用/服务需要根据&lt;a href=&quot;https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide-signing-0000001587684945-V5&quot;&gt;为应用/服务进行签名&lt;/a&gt;章节，提前对应用/服务进行签名。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;使用无线调试连接方式&lt;/h2&gt;
&lt;p&gt;1、将 Phone/Tablet 和 PC 连接到同一 WLAN 网络。&lt;/p&gt;
&lt;p&gt;2、在开发者模式中，打开“无线调试”开关，并获取 Phone/Tablet 端的 IP 地址和端口号。（如果未开启开发者模式，先多次点击系统版本号开启）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20240807193555.91650601177176208890172059267861:50001231000000:2800:7DA189BF74B83584FFF1B21597C4481F833A995E97188D77631A3EA575A878F8.png?needInitFileName=true?needInitFileName=true&quot; alt=&quot;无限调试&quot; /&gt;&lt;/p&gt;
&lt;p&gt;3、在PC中执行如下命令连接设备&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hdc tconn 设备IP地址:端口号
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4、选择无线设备调试&lt;/p&gt;
</content:encoded><category>Harmony</category><author>ChanZhaoYu</author></item><item><title>Linux commands - basics</title><link>https://www.redon.cc/posts/linux-commands/</link><guid isPermaLink="true">https://www.redon.cc/posts/linux-commands/</guid><description>Linux commands - basics</description><pubDate>Mon, 05 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;/posts/linux-commands.png&quot; alt=&quot;commands&quot; /&gt;&lt;/p&gt;
</content:encoded><category>Linux</category><author>ChanZhaoYu</author></item><item><title>GitHub全局和本地代理</title><link>https://www.redon.cc/posts/github%E5%85%A8%E5%B1%80%E5%92%8C%E6%9C%AC%E5%9C%B0%E4%BB%A3%E7%90%86/</link><guid isPermaLink="true">https://www.redon.cc/posts/github%E5%85%A8%E5%B1%80%E5%92%8C%E6%9C%AC%E5%9C%B0%E4%BB%A3%E7%90%86/</guid><description>GitHub全局和本地代理设置，解决部份时候网络问题</description><pubDate>Tue, 09 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;为 &lt;code&gt;Git&lt;/code&gt; 设置代理，代理地址每个人不一致，请查看自己的代理 &lt;code&gt;PAC&lt;/code&gt; 设置&lt;/p&gt;
&lt;h2&gt;全局代理&lt;/h2&gt;
&lt;h3&gt;设置 &lt;code&gt;http&lt;/code&gt; 代理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git config --global http.proxy http://127.0.0.1:1087
git config --global https.proxy https://127.0.0.1:1087

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;设置 &lt;code&gt;scoks&lt;/code&gt; 代理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git config --global http.proxy socks5://127.0.0.1:1080
git config --global https.proxy socks5://127.0.0.1:1080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事实上使用 &lt;code&gt;socks5h&lt;/code&gt; 更佳，因为 &lt;code&gt;socks5&lt;/code&gt; 包含 &lt;code&gt;http(s)&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git config --global http.proxy socks5h://127.0.0.1:1080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;h&lt;/code&gt; 代表 &lt;code&gt;host&lt;/code&gt; ，包括了域名解析，即域名解析也强制走这个 &lt;code&gt;proxy&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;取消设置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git config --global --unset http.proxy
git config --global --unset https.proxy
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;设置当前项目 &lt;code&gt;Git&lt;/code&gt; 代理&lt;/h2&gt;
&lt;p&gt;可以给某一个项目单独设置代理。和全局代理的区别就是关键字为 &lt;code&gt;--local&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;设置 &lt;code&gt;http&lt;/code&gt; 代理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git config --local http.proxy http://127.0.0.1:1087
git config --local https.proxy https://127.0.0.1:1087

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;设置 &lt;code&gt;scoks&lt;/code&gt; 代理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git config --local http.proxy socks5://127.0.0.1:1080
git config --local https.proxy socks5://127.0.0.1:1080
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;取消设置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git config --local --unset http.proxy
git config --local --unset https.proxy
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;参看代理配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git config --global http.proxy
git config --global https.proxy
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;只给 GitHub 代理&lt;/h2&gt;
&lt;p&gt;假如你只需要给 &lt;code&gt;GitHub&lt;/code&gt; 全局代理加速，而不影响自部署仓库，那么可以只配置 &lt;code&gt;GitHub&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git config --global http.https://github.com.proxy socks5://127.0.0.1:1080
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>GitHub</category><author>ChanZhaoYu</author></item><item><title>Node 提取 env 环境变量</title><link>https://www.redon.cc/posts/node%E6%8F%90%E5%8F%96env%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F/</link><guid isPermaLink="true">https://www.redon.cc/posts/node%E6%8F%90%E5%8F%96env%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F/</guid><description>Node 中安全提取环境变量方法</description><pubDate>Mon, 03 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;验证方法&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 验证并提取环境变量
const getEnvVariable = (key: string): string | undefined =&amp;gt; {
  const value = process.env[key];
  if (value === undefined) {
    return null;
  }
  return value;
};

// 将环境变量转换为数值
const getNumericEnvVariable = (key: string, defaultValue: number): number =&amp;gt; {
  const value = getEnvVariable(key) ?? String(defaultValue);
  const parsedValue = parseInt(value, 10);
  if (isNaN(parsedValue)) {
    return defaultValue;
  }
  return parsedValue;
};

// 将环境变量转换为布尔值
const getBooleanEnvVariable = (key: string, defaultValue: boolean): boolean =&amp;gt; {
  const value = getEnvVariable(key) ?? String(defaultValue);
  return value.toLowerCase() === &quot;true&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;config 使用示例&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import &quot;dotenv/config&quot;;

export type Config = {
  PORT: number;
  ALLOWED_DOMAIN: string;
  USE_LOG: boolean;
};

export const config: Config = {
  PORT: getNumericEnvVariable(&quot;PORT&quot;, 3000),
  ALLOWED_DOMAIN: getEnvVariable(&quot;ALLOWED_DOMAIN&quot;) || &quot;*&quot;,
  USE_LOG: getBooleanEnvVariable(&quot;USE_LOG&quot;, true),
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>Node</category><category>typescript</category><author>ChanZhaoYu</author></item><item><title>useFixedHeader</title><link>https://www.redon.cc/posts/use-fixed-header/</link><guid isPermaLink="true">https://www.redon.cc/posts/use-fixed-header/</guid><description>一个 vue hook，用于元素滚动条时隐藏</description><pubDate>Tue, 30 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;演示&lt;/h2&gt;
&lt;p&gt;&amp;lt;video width=&quot;320&quot; height=&quot;240&quot; controls&amp;gt;
&amp;lt;source src=&quot;/useFixedHeader.mp4&quot; type=&quot;video/mp4&quot;&amp;gt;
&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;h2&gt;用法&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup&amp;gt;
import { ref } from &apos;vue&apos;
import { useFixedHeader } from &apos;./useFixedHeader&apos;

const headerRef = ref(null)

const { styles } = useFixedHeader(headerRef)
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
   &amp;lt;header class=&quot;Header&quot; ref=&quot;headerRef&quot; :style=&quot;styles&quot;&amp;gt;
      
   &amp;lt;/header&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;style scoped&amp;gt;
.Header {
   position: fixed;
   top: 0;
}
&amp;lt;/style&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;useFixedHeader.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {
   shallowRef,
   ref,
   unref,
   watch,
   computed,
   readonly,
   type ComputedRef,
   type CSSProperties as CSS,
} from &apos;vue&apos;

import { useReducedMotion, isBrowser } from &apos;./utils&apos;
import { TRANSITION_STYLES } from &apos;./constants&apos;

import type { UseFixedHeaderOptions, MaybeTemplateRef } from &apos;./types&apos;

enum State {
   READY,
   ENTER,
   LEAVE,
}

export function useFixedHeader(
   target: MaybeTemplateRef,
   options: Partial&amp;lt;UseFixedHeaderOptions&amp;gt; = {},
): {
   styles: Readonly&amp;lt;CSS&amp;gt;
   isLeave: ComputedRef&amp;lt;boolean&amp;gt;
   isEnter: ComputedRef&amp;lt;boolean&amp;gt;
} {
   // Config

   const { enterStyles, leaveStyles } = TRANSITION_STYLES

   const isReduced = useReducedMotion()

   // State

   const internal = {
      resizeObserver: undefined as ResizeObserver | undefined,
      initResizeObserver: false,
      isListeningScroll: false,
      isHovering: false,
   }

   const styles = shallowRef&amp;lt;CSS&amp;gt;({})
   const state = ref&amp;lt;State&amp;gt;(State.READY)

   const setStyles = (newStyles: CSS) =&amp;gt; (styles.value = newStyles)
   const removeStyles = () =&amp;gt; (styles.value = {})
   const setState = (newState: State) =&amp;gt; (state.value = newState)

   // Utils

   function getRoot() {
      const _root = unref(options.root)
      if (_root != null) return _root

      return document.documentElement
   }

   const getScrollTop = () =&amp;gt; getRoot().scrollTop

   function getScrollRoot() {
      const root = getRoot()
      return root === document.documentElement ? document : root
   }

   function isFixed() {
      const el = unref(target)
      if (!el) return false

      const { position, display } = window.getComputedStyle(el)
      return (position === &apos;fixed&apos; || position === &apos;sticky&apos;) &amp;amp;&amp;amp; display !== &apos;none&apos;
   }

   function getHeaderHeight() {
      const el = unref(target)
      if (!el) return 0

      let headerHeight = el.scrollHeight

      const { marginTop, marginBottom } = window.getComputedStyle(el)
      headerHeight += Number.parseFloat(marginTop) + Number.parseFloat(marginBottom)

      return headerHeight
   }

   // Callbacks

   /**
    * Resize observer is added wheter or not the header is fixed/sticky
    * as it is in charge of toggling scroll/pointer listeners if it
    * turns from fixed/sticky to something else and vice-versa.
    */
   function addResizeObserver() {
      internal.resizeObserver = new ResizeObserver(() =&amp;gt; {
         // Skip the initial call
         if (!internal.initResizeObserver) return (internal.initResizeObserver = true)
         toggleListeners()
      })

      internal.resizeObserver.observe(getRoot())
   }

   function onVisible() {
      if (state.value === State.ENTER) return

      removeTransitionListener()

      setStyles({
         ...enterStyles,
         ...(unref(options.transitionOpacity) ? { opacity: 1 } : {}),
         visibility: &apos;&apos; as CSS[&apos;visibility&apos;],
      })

      setState(State.ENTER)
   }

   function onHidden() {
      if (state.value === State.LEAVE) return

      setStyles({ ...leaveStyles, ...(unref(options.transitionOpacity) ? { opacity: 0 } : {}) })

      setState(State.LEAVE)

      addTransitionListener()
   }

   // Transition Events

   function onTransitionEnd(e: TransitionEvent) {
      removeTransitionListener()

      if (!unref(target) || e.target !== unref(target) || e.propertyName !== &apos;transform&apos;) return

      /**
       * In some edge cases this might be called when the header
       * is visible, so we need to check the transform value.
       */
      const { transform } = window.getComputedStyle(unref(target)!)
      if (transform === &apos;matrix(1, 0, 0, 1, 0, 0)&apos;) return // translateY(0px)

      setStyles({
         ...leaveStyles,
         visibility: &apos;hidden&apos;,
      })
   }

   function addTransitionListener() {
      const el = unref(target)
      if (!el) return

      el.addEventListener(&apos;transitionend&apos;, onTransitionEnd as EventListener)
   }

   function removeTransitionListener() {
      const el = unref(target)
      if (!el) return

      el.removeEventListener(&apos;transitionend&apos;, onTransitionEnd as EventListener)
   }

   // Scroll Events

   function createScrollHandler() {
      let prevTop = isBrowser ? getScrollTop() : 0

      return () =&amp;gt; {
         const scrollTop = getScrollTop()

         const isTopReached = scrollTop &amp;lt;= getHeaderHeight()
         const isScrollingUp = scrollTop &amp;lt; prevTop
         const isScrollingDown = scrollTop &amp;gt; prevTop

         const step = Math.abs(scrollTop - prevTop)

         if (isTopReached) return onVisible()
         if (step &amp;lt; 10) return

         if (!internal.isHovering) {
            if (isScrollingUp) {
               onVisible()
            } else if (isScrollingDown) {
               onHidden()
            }
         }

         prevTop = scrollTop
      }
   }

   const onScroll = createScrollHandler()

   function addScrollListener() {
      getScrollRoot().addEventListener(&apos;scroll&apos;, onScroll, { passive: true })
      internal.isListeningScroll = true
   }

   function removeScrollListener() {
      getScrollRoot().removeEventListener(&apos;scroll&apos;, onScroll)
      internal.isListeningScroll = false
   }

   // Pointer Events

   const onPointerEnter = () =&amp;gt; (internal.isHovering = true)
   const onPointerLeave = () =&amp;gt; (internal.isHovering = false)

   function addPointerListener() {
      unref(target)?.addEventListener(&apos;pointerenter&apos;, onPointerEnter)
      unref(target)?.addEventListener(&apos;pointerleave&apos;, onPointerLeave)
   }

   function removePointerListener() {
      unref(target)?.removeEventListener(&apos;pointerenter&apos;, onPointerEnter)
      unref(target)?.removeEventListener(&apos;pointerleave&apos;, onPointerLeave)
   }

   // Listeners

   function toggleListeners() {
      const isValid = isFixed()

      if (internal.isListeningScroll) {
         // If the header is not anymore fixed or sticky
         if (!isValid) {
            removeListeners()
            removeStyles()
         }
         // If was not listening and now is fixed or sticky
      } else {
         if (isValid) {
            addScrollListener()
            addPointerListener()
         }
      }
   }

   function removeListeners() {
      removeScrollListener()
      removePointerListener()
   }

   isBrowser &amp;amp;&amp;amp;
      watch(
         () =&amp;gt; [unref(target), getRoot(), isReduced.value, unref(options.watch)],
         ([headerEl, rootEl, isReduced], _, onCleanup) =&amp;gt; {
            const shouldInit = !isReduced &amp;amp;&amp;amp; headerEl &amp;amp;&amp;amp; (rootEl || rootEl === null)

            if (shouldInit) {
               addResizeObserver()
               toggleListeners()
            }

            onCleanup(() =&amp;gt; {
               removeListeners()
               removeStyles()
               internal.resizeObserver?.disconnect()
               internal.initResizeObserver = false
            })
         },
         { immediate: true, flush: &apos;post&apos; },
      )

   return {
      styles: readonly(styles),
      isLeave: computed(() =&amp;gt; state.value === State.LEAVE),
      isEnter: computed(() =&amp;gt; state.value === State.ENTER),
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;constants.ts&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const EASING = &apos;cubic-bezier(0.16, 1, 0.3, 1)&apos;

export const TRANSITION_STYLES = {
   enterStyles: {
      transition: `all 0.35s ${EASING} 0s`,
      transform: &apos;translateY(0px)&apos;,
   },
   leaveStyles: {
      transition: `all 0.5s ${EASING} 0s`,
      transform: &apos;translateY(-101%)&apos;,
   },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;utils.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { onBeforeUnmount, onMounted, ref } from &apos;vue&apos;

export const isBrowser = typeof window !== &apos;undefined&apos;

export function useReducedMotion() {
   const isReduced = ref(false)

   if (!isBrowser) return isReduced

   const query = window.matchMedia(&apos;(prefers-reduced-motion: reduce)&apos;)

   const onMatch = () =&amp;gt; (isReduced.value = query.matches)

   onMounted(() =&amp;gt; {
      onMatch()
      query.addEventListener?.(&apos;change&apos;, onMatch)
   })

   onBeforeUnmount(() =&amp;gt; {
      query.removeEventListener?.(&apos;change&apos;, onMatch)
   })

   return isReduced
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;types.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { Ref, ComputedRef } from &apos;vue&apos;

export type MaybeTemplateRef = HTMLElement | null | Ref&amp;lt;HTMLElement | null&amp;gt;

export interface UseFixedHeaderOptions&amp;lt;T = any&amp;gt; {
   /**
    * Scrolling container. Matches `document.documentElement` if `null`.
    *
    * @default null
    */
   root: MaybeTemplateRef
   /**
    * Signal without `.value` (ref or computed) to be watched
    * for automatic behavior toggling.
    *
    * @default null
    */
   watch: Ref&amp;lt;T&amp;gt; | ComputedRef&amp;lt;T&amp;gt;
   /**
    * Whether to transition `opacity` property from 0 to 1
    * and vice versa along with the `transform` property
    *
    * @default false
    */
   transitionOpacity: boolean | Ref&amp;lt;boolean&amp;gt; | ComputedRef&amp;lt;boolean&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>Vue</category><category>hooks</category><author>ChanZhaoYu</author></item><item><title>TSConfig备忘录</title><link>https://www.redon.cc/posts/tsconfig%E5%A4%87%E5%BF%98%E5%BD%95/</link><guid isPermaLink="true">https://www.redon.cc/posts/tsconfig%E5%A4%87%E5%BF%98%E5%BD%95/</guid><description>tsconfig.json 配置选项说明</description><pubDate>Tue, 30 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;原文来自：&lt;a href=&quot;https://www.totaltypescript.com/tsconfig-cheat-sheet&quot;&gt;The TSConfig Cheat Sheet (Matt Pocock)&lt;/a&gt;，本人翻译备份用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;tsconfig.json&lt;/code&gt; 文件让很多人望而生畏，因为它是一个庞大的文件，包含着大量的配置选项。&lt;/p&gt;
&lt;p&gt;但实际上，只需要关注其中较少的几个配置选项就行了。让我们来理清这些选项，并做成简易参考。&lt;/p&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;p&gt;基本代码示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    /* 基础选项: */
    &quot;esModuleInterop&quot;: true,
    &quot;skipLibCheck&quot;: true,
    &quot;target&quot;: &quot;es2022&quot;,
    &quot;allowJs&quot;: true,
    &quot;resolveJsonModule&quot;: true,
    &quot;moduleDetection&quot;: &quot;force&quot;,
    &quot;isolatedModules&quot;: true,
    &quot;verbatimModuleSyntax&quot;: true,
    /* 严格模式 */
    &quot;strict&quot;: true,
    &quot;noUncheckedIndexedAccess&quot;: true,
    &quot;noImplicitOverride&quot;: true,
    /* 如果使用 TypeScript 转码: */
    &quot;module&quot;: &quot;NodeNext&quot;,
    &quot;outDir&quot;: &quot;dist&quot;,
    &quot;sourceMap&quot;: true,
    /* 如果要构建一个库: */
    &quot;declaration&quot;: true,
    /* 如果要在单一仓库中构建一个库: */
    &quot;composite&quot;: true,
    &quot;declarationMap&quot;: true,
    /* 如果不使用 TypeScript 进行转码: */
    &quot;module&quot;: &quot;preserve&quot;,
    &quot;noEmit&quot;: true,
    /* 如果代码运行在 DOM 中: */
    &quot;lib&quot;: [&quot;es2022&quot;, &quot;dom&quot;, &quot;dom.iterable&quot;],
    /* 如果代码不运行在 DOM 中: */
    &quot;lib&quot;: [&quot;es2022&quot;]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;完整说明&lt;/h2&gt;
&lt;h3&gt;基本选项&lt;/h3&gt;
&lt;p&gt;以下是我推荐所有项目都使用的基础选项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;esModuleInterop&quot;: true,
    &quot;skipLibCheck&quot;: true,
    &quot;target&quot;: &quot;es2022&quot;,
    &quot;allowJs&quot;: true,
    &quot;resolveJsonModule&quot;: true,
    &quot;moduleDetection&quot;: &quot;force&quot;,
    &quot;isolatedModules&quot;: true,
    &quot;verbatimModuleSyntax&quot;: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;esModuleInterop&lt;/code&gt;: 有助于弥合 &lt;code&gt;CommonJS&lt;/code&gt; 和 &lt;code&gt;ES Modules&lt;/code&gt; 之间的某些差距。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;skipLibCheck&lt;/code&gt;: 跳过对 &lt;code&gt;.d.ts&lt;/code&gt; 文件的类型检查。这对于性能很重要，否则所有 &lt;code&gt;node_modules&lt;/code&gt; 都将被检查。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;target&lt;/code&gt;: 你要编译的 &lt;code&gt;JavaScript&lt;/code&gt; 版本。 我推荐使用 &lt;code&gt;es2022&lt;/code&gt; 而非 &lt;code&gt;esnext&lt;/code&gt; 以获得更好的稳定性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;allowJs&lt;/code&gt; 和 &lt;code&gt;resolveJsonModule&lt;/code&gt;: 允许你导入 &lt;code&gt;.js&lt;/code&gt; 和 &lt;code&gt;.json&lt;/code&gt; 文件。始终很有用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;moduleDetection&lt;/code&gt;: 此选项强制 &lt;code&gt;TypeScript&lt;/code&gt; 将所有文件视为模块。 这有助于避免“无法重新声明块级作用域变量”错误。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;isolatedModules&lt;/code&gt;: 此选项阻止了一些在将模块视为独立文件时不安全的 &lt;code&gt;TS&lt;/code&gt; 功能。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;verbatimModuleSyntax&lt;/code&gt;: 此选项强制你使用 &lt;code&gt;import type&lt;/code&gt; 和 &lt;code&gt;export type&lt;/code&gt;，从而带来更可预测的行为和减少不必要的导入。 结合&lt;code&gt;module: NodeNext&lt;/code&gt;，它还强制你使用正确的 &lt;code&gt;ESM&lt;/code&gt; 或 &lt;code&gt;CJS&lt;/code&gt; 导入语法。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;严格模式&lt;/h3&gt;
&lt;p&gt;以下是我推荐所有项目使用的严格模式选项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;strict&quot;: true,
    &quot;noUncheckedIndexedAccess&quot;: true,
    &quot;noImplicitOverride&quot;: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;strict&lt;/code&gt;: 启用所有严格的类型检查选项。必要。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;noUncheckedIndexedAccess&lt;/code&gt;: 防止你在不首先检查数组或对象是否定义的情况下进行访问。 这是一种防止运行时错误的好方法，并且应该真正包含在 &lt;code&gt;strict&lt;/code&gt; 中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;noImplicitOverride&lt;/code&gt;: 使 &lt;code&gt;override&lt;/code&gt; 关键字在类中真正有用。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;许多人推荐 &lt;code&gt;tsconfig/bases&lt;/code&gt; 中的严格模式选项，这是一个很棒的存储库，它列举了 &lt;code&gt;TSConfig&lt;/code&gt; 选项。 但这些选项包含了许多我认为过于“繁琐”的规则，例如 &lt;code&gt;noImplicitReturns&lt;/code&gt;、&lt;code&gt;noUnusedLocals&lt;/code&gt;、&lt;code&gt;noUnusedParameters&lt;/code&gt; 和 &lt;code&gt;noFallthroughCasesInSwitch&lt;/code&gt;。 我建议仅在你想要它们时才将这些规则添加到你的&lt;code&gt; tsconfig.json&lt;/code&gt; 中。&lt;/p&gt;
&lt;h3&gt;使用 &lt;code&gt;TypeScript&lt;/code&gt; 进行转码&lt;/h3&gt;
&lt;p&gt;如果你正在使用 &lt;code&gt;tsc&lt;/code&gt; 转码你的代码（创建 &lt;code&gt;JavaScript&lt;/code&gt; 文件），则需要以下选项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;module&quot;: &quot;NodeNext&quot;,
    &quot;outDir&quot;: &quot;dist&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;module&lt;/code&gt;: 告诉 &lt;code&gt;TypeScript&lt;/code&gt; 要使用什么模块语法。 &lt;code&gt;NodeNext&lt;/code&gt; 是适用于 &lt;code&gt;Node&lt;/code&gt; 的最佳选项。 &lt;code&gt;moduleResolution: NodeNext&lt;/code&gt; 可以从这个选项推断出来。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;outDir&lt;/code&gt;: 告诉 &lt;code&gt;TypeScript&lt;/code&gt; 将编译后的 &lt;code&gt;JavaScript&lt;/code&gt; 文件放在哪里。 &lt;code&gt;dist&lt;/code&gt; 是我的首选约定，但这由你决定。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;构建库&lt;/h3&gt;
&lt;p&gt;如果你要构建一个库，则需要 &lt;code&gt;declaration: true&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;declaration&quot;: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;declaration&lt;/code&gt;: 在 &lt;code&gt;tsconfig.json&lt;/code&gt; 文件中告诉 &lt;code&gt;TypeScript&lt;/code&gt; 声明 &lt;code&gt;.d.ts&lt;/code&gt; 文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;单仓库中的库构建&lt;/h3&gt;
&lt;p&gt;如果您要为单仓库中的库构建，那么您还需要以下选项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;declaration&quot;: true,
    &quot;composite&quot;: true,
    &quot;sourceMap&quot;: true,
    &quot;declarationMap&quot;: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;composite&lt;/code&gt;: 告诉 &lt;code&gt;TypeScript&lt;/code&gt; 声明 &lt;code&gt;.tsbuildinfo&lt;/code&gt; 文件。这会告诉 &lt;code&gt;TypeScript&lt;/code&gt; 你的项目是单仓库，并且还有助于缓存构建以更快地运行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sourceMap&lt;/code&gt; 和 &lt;code&gt;declarationMap&lt;/code&gt;: 告诉 &lt;code&gt;TypeScript&lt;/code&gt; 声明映射。当库的使用者进行调试时，需要这些选项以便他们可以使用跳转到定义功能来跳转到原始源代码。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;不使用 TypeScript 进行转译&lt;/h3&gt;
&lt;p&gt;如果您不使用 &lt;code&gt;tsc&lt;/code&gt; 转译您的代码，即更像使用 &lt;code&gt;TypeScript&lt;/code&gt; 作为 &lt;code&gt;linter&lt;/code&gt;，那么您将需要以下选项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;module&quot;: &quot;preserve&quot;,
    &quot;noEmit&quot;: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;module&lt;/code&gt;: &lt;code&gt;preserve&lt;/code&gt; 是最佳选项，因为它最接近捆绑器处理模块的方式。由此选项暗示了 &lt;code&gt;moduleResolution: Bundler&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;noEmit&lt;/code&gt;: 告诉 &lt;code&gt;TypeScript&lt;/code&gt; 不发出任何文件。当您使用捆绑器时，这很重要，这样就不会发出无用的 &lt;code&gt;.js&lt;/code&gt; 文件。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;在 &lt;code&gt;DOM&lt;/code&gt; 中运行&lt;/h3&gt;
&lt;p&gt;如果你的代码在 &lt;code&gt;DOM&lt;/code&gt; 中运行，则需要以下选项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;lib&quot;: [&quot;es2022&quot;, &quot;dom&quot;, &quot;dom.iterable&quot;]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lib&lt;/code&gt;: 告诉 &lt;code&gt;TypeScript&lt;/code&gt; 要包含哪些内置类型。&lt;code&gt;es2022&lt;/code&gt; 是稳定性的最佳选择。&lt;code&gt;dom&lt;/code&gt; 和 &lt;code&gt;dom.iterable&lt;/code&gt; 为您提供 &lt;code&gt;window&lt;/code&gt;、&lt;code&gt;document&lt;/code&gt; 等的类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;不在 &lt;code&gt;DOM&lt;/code&gt; 中运行&lt;/h3&gt;
&lt;p&gt;如果你的代码不在 &lt;code&gt;DOM&lt;/code&gt; 中运行，则您需要 &lt;code&gt;lib: [&quot;es2022&quot;]&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;lib&quot;: [&quot;es2022&quot;]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这与上面相同，但没有 &lt;code&gt;dom&lt;/code&gt; 和 &lt;code&gt;dom.iterable&lt;/code&gt; 类型。&lt;/p&gt;
</content:encoded><category>TypeScript</category><author>ChanZhaoYu</author></item><item><title>NESTJS: BUILDING THE BASE</title><link>https://www.redon.cc/posts/nestjs-building-the-base/</link><guid isPermaLink="true">https://www.redon.cc/posts/nestjs-building-the-base/</guid><description>NestJS 是一个用于构建应用程序服务器端的框架。在这里，我将解释如何构建具有基本安全功能的应用程序的基础。</description><pubDate>Thu, 07 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;原文来自：&lt;a href=&quot;https://rashintha.com/2021/06/13/nestjs-building-the-base/&quot;&gt;Rashintha Maduneth&lt;/a&gt;，这是中文译文。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%BC%80%E5%A7%8B%E9%A1%B9%E7%9B%AE&quot;&gt;开始项目&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%99%BB%E5%BD%95%E9%AA%8C%E8%AF%81&quot;&gt;登录验证&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E9%9B%86%E6%88%90-jwt-%E8%BA%AB%E4%BB%BD%E9%AA%8C%E8%AF%81&quot;&gt;集成 JWT 身份验证&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E9%9B%86%E6%88%90%E6%8E%88%E6%9D%83&quot;&gt;集成授权&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#integrating-helmet&quot;&gt;Integrating Helmet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0%E4%BF%9D%E6%8A%A4&quot;&gt;跨站请求伪造保护&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E9%98%B2%E6%AD%A2%E6%9A%B4%E5%8A%9B%E8%AF%B7%E6%B1%82&quot;&gt;防止暴力请求&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#cors%E8%B7%A8%E5%9F%9F%E5%90%AF%E7%94%A8&quot;&gt;CORS（跨域）启用&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F&quot;&gt;环境变量&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%8E%8B%E7%BC%A9&quot;&gt;压缩&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;NestJS&lt;/code&gt; 是一个用于构建应用程序服务器端的框架。这篇文章介绍如何构建具有基础安全功能的应用程序。&lt;/p&gt;
&lt;h2&gt;开始项目&lt;/h2&gt;
&lt;p&gt;首先，使用以下命令安装 &lt;code&gt;NestJS CLI&lt;/code&gt;。可能需要 &lt;code&gt;root&lt;/code&gt; 权限才能执行此命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; npm i -g @nestjs/cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用以下命令创建一个新项目。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nest new project_name
cd project_name
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;登录验证&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;npm install --save @nestjs/passport passport passport-local
npm install --save-dev @types/passport-local
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用以下命令生成带有服务的 &lt;code&gt;Auth&lt;/code&gt; 和 &lt;code&gt;User&lt;/code&gt; 模块。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nest g module auth
nest g service auth
nest g module users
nest g service users
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在让我们使用以下命令生成一个代表用户的 &lt;code&gt;interface&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nest g interface interfaces/user
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加以下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export interface UserData {
  id: number;
  username: string;
  password: string;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在打开 &lt;code&gt;users/users.service.ts&lt;/code&gt; 文件并输入以下代码。在这里，我创建了两个示例用户和一个用于查询用户的函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Injectable } from &quot;@nestjs/common&quot;;
import { UserData } from &quot;src/interfaces/user.interface&quot;;

@Injectable()
export class UsersService {
  private readonly users: Array&amp;lt;UserData&amp;gt; = [
    {
      id: 1,
      username: &quot;admin&quot;,
      password: &quot;1234&quot;,
    },
    {
      id: 2,
      username: &quot;user&quot;,
      password: &quot;4567&quot;,
    },
  ];

  async find(username: string): Promise&amp;lt;UserData | undefined&amp;gt; {
    return this.users.find((user) =&amp;gt; user.username === username);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导出 &lt;code&gt;users/users.module.ts&lt;/code&gt; 文件中的 &lt;code&gt;UsersService&lt;/code&gt; ，以便模块外部可见。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Module } from &quot;@nestjs/common&quot;;
import { UsersService } from &quot;./users.service&quot;;

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开 &lt;code&gt;auth/auth.service.ts&lt;/code&gt; 文件并输入以下代码。这里创建了一个函数来在 &lt;code&gt;UsersService&lt;/code&gt; 的支持下验证用户。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Injectable } from &quot;@nestjs/common&quot;;
import { UsersService } from &quot;src/users/users.service&quot;;

@Injectable()
export class AuthService {
  constructor(private usersService: UsersService) {}

  async validateUser(username: string, password: string): Promise&amp;lt;any&amp;gt; {
    const user = await this.usersService.find(username);

    if (user &amp;amp;&amp;amp; user.password === password) {
      const { password, ...result } = user;
      return result;
    }

    return null;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编辑 &lt;code&gt;auth/auth.module.ts&lt;/code&gt; 文件将 &lt;code&gt;UsersService&lt;/code&gt; 导入到身份验证模块。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Module } from &quot;@nestjs/common&quot;;
import { AuthService } from &quot;./auth.service&quot;;
import { UsersModule } from &quot;../users/users.module&quot;;

@Module({
  imports: [UsersModule],
  providers: [AuthService],
})
export class AuthModule {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在在 &lt;code&gt;auth&lt;/code&gt; 文件夹中新建一个名为 &lt;code&gt;local.strategy.ts&lt;/code&gt; 的文件来实现本地认证策略。并在其中输入以下代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Strategy } from &quot;passport-local&quot;;
import { PassportStrategy } from &quot;@nestjs/passport&quot;;
import { Injectable, UnauthorizedException } from &quot;@nestjs/common&quot;;
import { AuthService } from &quot;./auth.service&quot;;

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise&amp;lt;any&amp;gt; {
    const user = await this.authService.validateUser(username, password);

    if (!user) {
      throw new UnauthorizedException();
    }

    return user;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导入 &lt;code&gt;PassportModule&lt;/code&gt; 并引入 &lt;code&gt;LocalStrategy&lt;/code&gt; 来配置 &lt;code&gt;auth&lt;/code&gt; 模块以使用本地策略，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Module } from &quot;@nestjs/common&quot;;
import { AuthService } from &quot;./auth.service&quot;;
import { UsersModule } from &quot;../users/users.module&quot;;
import { PassportModule } from &quot;@nestjs/passport&quot;;
import { LocalStrategy } from &quot;./local.strategy&quot;;
@Module({
  imports: [UsersModule, PassportModule],
  providers: [AuthService, LocalStrategy],
})
export class AuthModule {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;auth&lt;/code&gt; 文件夹中生成一个名为 &lt;code&gt;local-auth.guard.ts&lt;/code&gt; 的文件来实现将在登录请求中启动的本地防护，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nest g gu auth/guards/local-auth
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入以下代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Injectable } from &quot;@nestjs/common&quot;;
import { AuthGuard } from &quot;@nestjs/passport&quot;;
@Injectable()
export class LocalAuthGuard extends AuthGuard(&quot;local&quot;) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用以下命令在 &lt;code&gt;auth&lt;/code&gt; 中创建一个控制器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nest g co auth
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在其中创建 &lt;code&gt;POST&lt;/code&gt; 请求并启动 &lt;code&gt;LocalAuthGuard&lt;/code&gt;，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Controller, Post, Request, UseGuards } from &quot;@nestjs/common&quot;;
import { LocalAuthGuard } from &quot;./guards/local-auth.guard&quot;;
@Controller(&quot;auth&quot;)
export class AuthController {
  constructor() {}
  @UseGuards(LocalAuthGuard)
  @Post(&quot;login&quot;)
  async login(@Request() req) {
    return req.user;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此基本的登录验证码就完成了。使用包含用户名和密码的 &lt;code&gt;JSON&lt;/code&gt; 正文创建对 &lt;code&gt;http://localhost:3000/auth/login&lt;/code&gt; 的 &lt;code&gt;POST&lt;/code&gt; 请求。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -X POST http://localhost:3000/auth/login -d &apos;{&quot;username&quot;: &quot;admin&quot;, &quot;password&quot;: &quot;1234&quot;}&apos; -H &quot;Content-Type: application/json&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果登录成功，你应该获得用户信息，如果登录失败，它应该给你一条错误消息。&lt;/p&gt;
&lt;h2&gt;集成 JWT 身份验证&lt;/h2&gt;
&lt;p&gt;在将 &lt;code&gt;JWT&lt;/code&gt; 集成到上述项目之前，需要安装以下依赖项和 &lt;code&gt;dev&lt;/code&gt; 依赖项。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后打开 &lt;code&gt;auth/auth.service.ts&lt;/code&gt; 文件并创建一个方法来使用所需的详细信息对用户进行签名，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Injectable } from &quot;@nestjs/common&quot;;
import { UsersService } from &quot;src/users/users.service&quot;;
import { JwtService } from &quot;@nestjs/jwt&quot;;
@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService
  ) {}
  async validateUser(username: string, password: string): Promise&amp;lt;any&amp;gt; {
    const user = await this.usersService.find(username);
    if (user &amp;amp;&amp;amp; user.password === password) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
  async sign(user: any) {
    const payload = { username: user.username, sub: user.id };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建一个密钥和过期时间以在 &lt;code&gt;JWT&lt;/code&gt; 方法中使用。在名为 &lt;code&gt;auth.config.ts&lt;/code&gt; 的 &lt;code&gt;src/config&lt;/code&gt; 文件夹中创建一个新的配置文件，并按如下所示输入密钥。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const jwtConfig = {
  secret: &quot;secretKey&quot;,
  expireTime: &quot;1h&quot;,
  expireIgnore: false,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按如下方式更新 &lt;code&gt;auth.module.ts&lt;/code&gt; 文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Module } from &quot;@nestjs/common&quot;;
import { AuthService } from &quot;./auth.service&quot;;
import { UsersModule } from &quot;../users/users.module&quot;;
import { PassportModule } from &quot;@nestjs/passport&quot;;
import { LocalStrategy } from &quot;./local.strategy&quot;;
import { AuthController } from &quot;./auth.controller&quot;;
import { JwtModule } from &quot;@nestjs/jwt&quot;;
import { jwtConfig } from &quot;src/config/auth.config&quot;;
@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: jwtConfig.secret,
      signOptions: {
        expiresIn: jwtConfig.expireTime,
      },
    }),
  ],
  providers: [AuthService, LocalStrategy],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按如下方式更新 &lt;code&gt;auth.controller.ts&lt;/code&gt; 文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Controller, Post, Request, UseGuards } from &quot;@nestjs/common&quot;;
import { AuthService } from &quot;./auth.service&quot;;
import { LocalAuthGuard } from &quot;./guards/local-auth.guard&quot;;
@Controller(&quot;auth&quot;)
export class AuthController {
  constructor(private authService: AuthService) {}
  @UseGuards(LocalAuthGuard)
  @Post(&quot;login&quot;)
  async login(@Request() req) {
    return this.authService.sign(req.user);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在再次发送之前的 &lt;code&gt;POS&lt;/code&gt;T 请求，你应该会获得 &lt;code&gt;JWT&lt;/code&gt; 令牌作为响应。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -X POST http://localhost:3000/auth/login -d &apos;{&quot;username&quot;: &quot;admin&quot;, &quot;password&quot;: &quot;1234&quot;}&apos; -H &quot;Content-Type: application/json&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我将为每个请求启动 &lt;code&gt;JWT&lt;/code&gt; 令牌保护。&lt;/p&gt;
&lt;p&gt;现在，你必须在 auth 文件夹中创建一个名为 &lt;code&gt;jwt.strategy.ts&lt;/code&gt; 的文件，并在其中实现 &lt;code&gt;JWT&lt;/code&gt; 验证策略，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Injectable } from &quot;@nestjs/common&quot;;
import { PassportStrategy } from &quot;@nestjs/passport&quot;;
import { Strategy, ExtractJwt } from &quot;passport-jwt&quot;;
import { jwtConfig } from &quot;src/config/auth.config&quot;;
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: jwtConfig.expireIgnore,
      secretOrKey: jwtConfig.secret,
    });
  }
  async validate(payload: any) {
    return { id: payload.sub, username: payload.username };
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后将 &lt;code&gt;JwtStrategy&lt;/code&gt; 添加到 &lt;code&gt;auth.module.ts&lt;/code&gt; 文件的 &lt;code&gt;providers&lt;/code&gt; 列表中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Module } from &quot;@nestjs/common&quot;;
import { AuthService } from &quot;./auth.service&quot;;
import { UsersModule } from &quot;../users/users.module&quot;;
import { PassportModule } from &quot;@nestjs/passport&quot;;
import { LocalStrategy } from &quot;./local.strategy&quot;;
import { AuthController } from &quot;./auth.controller&quot;;
import { JwtModule } from &quot;@nestjs/jwt&quot;;
import { JwtStrategy } from &quot;./jwt.strategy&quot;;
import { jwtConfig } from &quot;src/config/auth.config&quot;;
@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: jwtConfig.secret,
      signOptions: {
        expiresIn: jwtConfig.expireTime,
      },
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在在 &lt;code&gt;auth&lt;/code&gt; 文件夹中生成 &lt;code&gt;jwt-auth.guard.ts&lt;/code&gt; 文件，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nest g gu auth/guards/jwt-auth
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并实现将在每个请求中启动的 &lt;code&gt;JWT&lt;/code&gt; 防护，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Injectable } from &quot;@nestjs/common&quot;;
import { AuthGuard } from &quot;@nestjs/passport&quot;;
@Injectable()
export class JwtAuthGuard extends AuthGuard(&quot;jwt&quot;) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在让我们为用户创建一个控制器，并使用 &lt;code&gt;JWT&lt;/code&gt; 防护在其中实现受保护的路由。首先，按如下方式创建控制器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nest g co users
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后按如下方式更新 &lt;code&gt;users.service.ts&lt;/code&gt; 文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Injectable } from &quot;@nestjs/common&quot;;
import { UserData } from &quot;src/interfaces/user.interface&quot;;
@Injectable()
export class UsersService {
  private readonly users: Array&amp;lt;UserData&amp;gt; = [
    {
      id: 1,
      username: &quot;admin&quot;,
      password: &quot;1234&quot;,
    },
    {
      id: 2,
      username: &quot;user&quot;,
      password: &quot;1234&quot;,
    },
  ];
  async find(username: string): Promise&amp;lt;UserData | undefined&amp;gt; {
    return this.users.find((user) =&amp;gt; user.username === username);
  }
  async findByID(id: number): Promise&amp;lt;UserData | undefined&amp;gt; {
    return this.users.find((user) =&amp;gt; user.id == id);
  }
  async findAll(): Promise&amp;lt;Array&amp;lt;UserData&amp;gt;&amp;gt; {
    return this.users;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 &lt;code&gt;users.controller.ts&lt;/code&gt; 文件中创建一些受保护的 &lt;code&gt;API&lt;/code&gt; 调用，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {
  Controller,
  Get,
  NotFoundException,
  Param,
  UseGuards,
} from &quot;@nestjs/common&quot;;
import { JwtAuthGuard } from &quot;src/auth/guards/jwt-auth.guard&quot;;
import { UsersService } from &quot;./users.service&quot;;
@Controller(&quot;users&quot;)
export class UsersController {
  constructor(private userService: UsersService) {}
  @UseGuards(JwtAuthGuard)
  @Get()
  async getAllUsers() {
    const users = await this.userService.findAll();
    let results = [];
    users.forEach((user) =&amp;gt; {
      const { password, ...result } = user;
      results.push(result);
    });
    return results;
  }
  @UseGuards(JwtAuthGuard)
  @Get(&quot;:id&quot;)
  async getUser(@Param() params) {
    const user = await this.userService.findByID(Number(params.id));
    if (user) {
      const { password, ...result } = user;
      return result;
    } else {
      throw new NotFoundException();
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在你需要使用从登录请求中获取的令牌来发送请求，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl http://localhost:3000/users/1 -H &quot;Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;集成授权&lt;/h2&gt;
&lt;p&gt;这里我就来讲解一下如何给项目添加 &lt;code&gt;Permission-based&lt;/code&gt; 授权。&lt;/p&gt;
&lt;p&gt;首先，让我们创建一些权限。在 &lt;code&gt;src/enums&lt;/code&gt; 文件夹中创建一个名为 &lt;code&gt;permission.enum.ts&lt;/code&gt; 的文件，并设置一些权限，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export enum Permission {
  GET_USER = &quot;get_user&quot;,
  GET_ALL_USERS = &quot;get_all_users&quot;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;user.interface.ts&lt;/code&gt; 文件引入如下类型，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Permission } from &apos;src/enums/permission.enum&apos;;

export interface UserData {
  id: number;
  username: string;
  password: string;
  permissions: Permission[];
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;users.service.ts&lt;/code&gt; 文件添加如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Injectable } from &quot;@nestjs/common&quot;;
import { Permission } from &quot;src/enums/permission.enum&quot;;
import { UserData } from &quot;src/interfaces/user.interface&quot;;
@Injectable()
export class UsersService {
  private readonly users: Array&amp;lt;UserData&amp;gt; = [
    {
      id: 1,
      username: &quot;admin&quot;,
      password: &quot;1234&quot;,
      permissions: [Permission.GET_USER, Permission.GET_ALL_USERS],
    },
    {
      id: 2,
      username: &quot;user&quot;,
      password: &quot;1234&quot;,
      permissions: [Permission.GET_USER],
    },
  ];
  async find(username: string): Promise&amp;lt;UserData | undefined&amp;gt; {
    return this.users.find((user) =&amp;gt; user.username === username);
  }
  async findByID(id: number): Promise&amp;lt;UserData | undefined&amp;gt; {
    return this.users.find((user) =&amp;gt; user.id == id);
  }
  async findAll(): Promise&amp;lt;Array&amp;lt;UserData&amp;gt;&amp;gt; {
    return this.users;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，你应该创建一个装饰器，如下所示，用于指定访问资源所需的权限。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nest g d decorators/permissions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并实现代码如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { SetMetadata } from &quot;@nestjs/common&quot;;
import { Permission } from &quot;src/enums/permission.enum&quot;;
export const PERMISSIONS_KEY = &quot;permissions&quot;;
export const Permissions = (...permissions: Permission[]) =&amp;gt;
  SetMetadata(PERMISSIONS_KEY, permissions);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在让我们生成一个权限保护，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nest g gu auth/guards/permissions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;permissions.guard.ts&lt;/code&gt; 文件中，实现用户的权限比较方法如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { CanActivate, ExecutionContext, Injectable } from &quot;@nestjs/common&quot;;
import { Reflector } from &quot;@nestjs/core&quot;;
import { PERMISSIONS_KEY } from &quot;src/decorators/permissions.decorator&quot;;
import { Permission } from &quot;src/enums/permission.enum&quot;;
@Injectable()
export class PermissionsGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  canActivate(context: ExecutionContext): boolean {
    const requiredPermissions = this.reflector.getAllAndOverride&amp;lt;Permission[]&amp;gt;(
      PERMISSIONS_KEY,
      [context.getHandler(), context.getClass()]
    );
    if (!requiredPermissions) {
      return true;
    }
    const { user } = context.switchToHttp().getRequest();
    return requiredPermissions.some((permission) =&amp;gt;
      user.permissions?.includes(permission)
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在打开 &lt;code&gt;jwt.strategy.ts&lt;/code&gt; 文件，并在 &lt;code&gt;validate&lt;/code&gt; 函数的 &lt;code&gt;return&lt;/code&gt; 中添加权限，这样 &lt;code&gt;PermissionGuard&lt;/code&gt; 就会获得权限进行比较。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Injectable } from &quot;@nestjs/common&quot;;
import { PassportStrategy } from &quot;@nestjs/passport&quot;;
import { Strategy, ExtractJwt } from &quot;passport-jwt&quot;;
import { jwtConfig } from &quot;src/config/auth.config&quot;;
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: jwtConfig.expireIgnore,
      secretOrKey: jwtConfig.secret,
    });
  }
  async validate(payload: any) {
    return {
      id: payload.sub,
      username: payload.username,
      permissions: payload.permissions,
    };
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后打开 &lt;code&gt;auth.service.ts&lt;/code&gt; 文件并在 &lt;code&gt;payload&lt;/code&gt; 中输入 &lt;code&gt;permissions&lt;/code&gt; ，这样签名的有效负载就有权限。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Injectable } from &quot;@nestjs/common&quot;;
import { UsersService } from &quot;src/users/users.service&quot;;
import { JwtService } from &quot;@nestjs/jwt&quot;;
@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService
  ) {}
  async validateUser(username: string, password: string): Promise&amp;lt;any&amp;gt; {
    const user = await this.usersService.find(username);
    if (user &amp;amp;&amp;amp; user.password === password) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
  async sign(user: any) {
    const payload = {
      username: user.username,
      sub: user.id,
      permissions: user.permissions,
    };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在打开 &lt;code&gt;users.controllers.ts&lt;/code&gt; 文件并根据需要将 &lt;code&gt;@Permission(Permission.PERMISSION)&lt;/code&gt; 装饰器添加到请求中。&lt;/p&gt;
&lt;p&gt;由于 &lt;code&gt;JwtAuthGuard&lt;/code&gt; 和 &lt;code&gt;PermissionsGuard&lt;/code&gt; 都在此处的每个请求中使用，因此将它们移至类的顶部。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {
  Controller,
  Get,
  NotFoundException,
  Param,
  UseGuards,
} from &quot;@nestjs/common&quot;;
import { JwtAuthGuard } from &quot;src/auth/guards/jwt-auth.guard&quot;;
import { PermissionsGuard } from &quot;src/auth/guards/permissions.guard&quot;;
import { Permissions } from &quot;src/decorators/permissions.decorator&quot;;
import { Permission } from &quot;src/enums/permission.enum&quot;;
import { UsersService } from &quot;./users.service&quot;;
@Controller(&quot;users&quot;)
@UseGuards(JwtAuthGuard, PermissionsGuard)
export class UsersController {
  constructor(private userService: UsersService) {}
  @Get()
  @Permissions(Permission.GET_ALL_USERS)
  async getAllUsers() {
    const users = await this.userService.findAll();
    let results = [];
    users.forEach((user) =&amp;gt; {
      const { password, ...result } = user;
      results.push(result);
    });
    return results;
  }
  @Get(&quot;:id&quot;)
  @Permissions(Permission.GET_USER)
  async getUser(@Param() params) {
    const user = await this.userService.findByID(Number(params.id));
    if (user) {
      const { password, ...result } = user;
      return result;
    } else {
      throw new NotFoundException();
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你也可以在模块中提供这些应用程序防护，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;providers: [
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard
    },
    {
      provide: APP_GUARD,
      useClass: PermissionsGuard
    }
  ],
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但在这里我没有这样做，因为 &lt;code&gt;UsersModule&lt;/code&gt; 也被导入到 &lt;code&gt;AuthModule&lt;/code&gt; 中。因此，如果我在模块中提供它们，它将应用于身份验证模块，并且还将检查登录请求的 &lt;code&gt;JWT&lt;/code&gt; 访问权限。&lt;/p&gt;
&lt;h2&gt;Integrating Helmet&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Helmet&lt;/code&gt; 包通过设置各种 &lt;code&gt;HTTP&lt;/code&gt; 标头来帮助保护应用程序。首先，让我们安装该软件包。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i --save helmet
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在让我们使用以下命令创建一个全局中间件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nest g mi app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在将以下代码放入 &lt;code&gt;app.middleware.ts&lt;/code&gt; 文件中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { INestApplication } from &quot;@nestjs/common&quot;;
import * as helmet from &quot;helmet&quot;;

export function middleware(app: INestApplication): INestApplication {
  const isProduction = process.env.NODE_ENV === &quot;production&quot;;

  app.use(helmet({ contentSecurityPolicy: isProduction }));

  return app;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并打开 &lt;code&gt;main.ts&lt;/code&gt; 文件并调用中间件函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { NestFactory } from &quot;@nestjs/core&quot;;
import { middleware } from &quot;./app.middleware&quot;;
import { AppModule } from &quot;./app.module&quot;;

declare const module: any;

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  middleware(app);

  await app.listen(3000);

  if (module.hot) {
    module.hot.accept();
    module.hot.dispose(() =&amp;gt; app.close());
  }
}
bootstrap();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在要设置环境，打开 &lt;code&gt;package.json&lt;/code&gt; 并修改 &lt;code&gt;start:dev&lt;/code&gt; 和 &lt;code&gt;start:prod&lt;/code&gt; 脚本，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;start:dev&quot;: &quot;NODE_ENV=development nest build --webpack --webpackPath webpack-hmr.config.js --watch&quot;,
&quot;start:prod&quot;: &quot;NODE_ENV=production node dist/main&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;跨站请求伪造保护&lt;/h2&gt;
&lt;p&gt;由于我们不使用 &lt;code&gt;cookie&lt;/code&gt;，因此实际上并不需要 &lt;code&gt;CSRF&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;防止暴力请求&lt;/h2&gt;
&lt;p&gt;为了防止暴力请求，让我们安装 &lt;code&gt;throttler&lt;/code&gt; 包。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i --save @nestjs/throttler
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后将 &lt;code&gt;TTL&lt;/code&gt; 和请求限制时间包含在 &lt;code&gt;auth.config.ts&lt;/code&gt; 文件中，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const jwtConfig = {
  secret: &quot;secretKey&quot;,
  expireTime: &quot;1h&quot;,
  expireIgnore: false,
};

export const bruteForceLimits = {
  requestLimit: 10,
  ttl: 60,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后修改 &lt;code&gt;app.module.ts&lt;/code&gt; 如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Module } from &quot;@nestjs/common&quot;;
import { AppController } from &quot;./app.controller&quot;;
import { AppService } from &quot;./app.service&quot;;
import { AuthModule } from &quot;./auth/auth.module&quot;;
import { UsersModule } from &quot;./users/users.module&quot;;
import { ThrottlerGuard, ThrottlerModule } from &quot;@nestjs/throttler&quot;;
import { bruteForceLimits } from &quot;./config/auth.config&quot;;
import { APP_GUARD } from &quot;@nestjs/core&quot;;

@Module({
  imports: [
    AuthModule,
    UsersModule,
    ThrottlerModule.forRoot({
      ttl: bruteForceLimits.ttl,
      limit: bruteForceLimits.requestLimit,
    }),
  ],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_GUARD,
      useClass: ThrottlerGuard,
    },
  ],
})
export class AppModule {}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;CORS（跨域）启用&lt;/h2&gt;
&lt;p&gt;将 &lt;code&gt;CORS&lt;/code&gt; 配置插入 &lt;code&gt;auth.config.ts&lt;/code&gt; 中，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const jwtConfig = {
  secret:
    &quot;l0XNT38sJ/OkVVpKTPNixGyH2SW5yF1NJqB9BAGczf1JKHIjenyqaQPOX9Tt9LBC3WHCoG08yhZe+MkQtATYgr5dLhFoTifsczfXNNMBTrXmJN5DV+WzesYS/daFMDC0vXmG/20ImrFsw22EKDWl4+VDAE1slVx/B41t5gNGt4ffd0UPE0wpekd23FOECD0EoTCLYsM7nSnMhUlKB4ONvAlOgObXLAgCgMkDe1g69kspxT1ev7/MyXv+xDRUikJgvPOuy7lZVMQ5eOC4ouELNT5L18yc9hbYEfQXDsUe6zCN6DNbASCYt2Eg/ki2nwpJb+NUT69ObWzxG9ZGJpgrqA==&quot;,
  expireTime: &quot;1h&quot;,
  expireIgnore: false,
};

export const bruteForceLimits = {
  requestLimit: 10,
  ttl: 60,
};

export const corsConfig = {
  origin: &quot;*&quot;,
  methods: &quot;GET, PUT, POST, DELETE&quot;,
  allowedHeaders: &quot;Content-Type, Authorization&quot;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后修改 &lt;code&gt;app.middleware.ts&lt;/code&gt; 如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { INestApplication } from &quot;@nestjs/common&quot;;
import * as helmet from &quot;helmet&quot;;
import { corsConfig } from &quot;./config/auth.config&quot;;

export function middleware(app: INestApplication): INestApplication {
  const isProduction = process.env.NODE_ENV === &quot;production&quot;;

  app.use(helmet({ contentSecurityPolicy: isProduction }));
  app.enableCors({
    origin: corsConfig.origin,
    methods: corsConfig.methods,
    allowedHeaders: corsConfig.allowedHeaders,
  });

  return app;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;环境变量&lt;/h2&gt;
&lt;p&gt;首先，你应该在创建环境变量之前安装以下软件包。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i --save @nestjs/config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在根文件夹中创建 &lt;code&gt;development.env&lt;/code&gt; 和 &lt;code&gt;production.env&lt;/code&gt; 文件。&lt;/p&gt;
&lt;p&gt;让我们将原始配置移动到 development.env 文件中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ORIGIN=http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在打开 auth.config.ts 文件并进行如下修改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const jwtConfig = {
  secret: &quot;secretKey&quot;,
  expireTime: &quot;1h&quot;,
  expireIgnore: false,
};

export const bruteForceLimits = {
  requestLimit: 10,
  ttl: 60,
};

export const corsConfig = {
  methods: &quot;GET, PUT, POST, DELETE&quot;,
  allowedHeaders: &quot;Content-Type, Authorization&quot;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在打开 &lt;code&gt;app.module.ts&lt;/code&gt; 文件并按如下所示修改它以导入环境变量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Module } from &quot;@nestjs/common&quot;;
import { AppController } from &quot;./app.controller&quot;;
import { AppService } from &quot;./app.service&quot;;
import { AuthModule } from &quot;./auth/auth.module&quot;;
import { UsersModule } from &quot;./users/users.module&quot;;
import { ThrottlerGuard, ThrottlerModule } from &quot;@nestjs/throttler&quot;;
import { bruteForceLimits } from &quot;./config/auth.config&quot;;
import { APP_GUARD } from &quot;@nestjs/core&quot;;
import { ConfigModule } from &quot;@nestjs/config&quot;;

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: `${process.env.NODE_ENV || &quot;development&quot;}.env`,
      isGlobal: true,
    }),
    AuthModule,
    UsersModule,
    ThrottlerModule.forRoot({
      ttl: bruteForceLimits.ttl,
      limit: bruteForceLimits.requestLimit,
    }),
  ],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_GUARD,
      useClass: ThrottlerGuard,
    },
  ],
})
export class AppModule {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，打开 &lt;code&gt;app.middleware.ts&lt;/code&gt; 文件并插入环境中的源。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { INestApplication } from &quot;@nestjs/common&quot;;
import * as helmet from &quot;helmet&quot;;
import { corsConfig } from &quot;./config/auth.config&quot;;

export function middleware(app: INestApplication): INestApplication {
  const isProduction = process.env.NODE_ENV === &quot;production&quot;;

  app.use(helmet({ contentSecurityPolicy: isProduction }));
  app.enableCors({
    origin: process.env.ORIGIN,
    methods: corsConfig.methods,
    allowedHeaders: corsConfig.allowedHeaders,
  });

  return app;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请记住在运行生产构建之前将所有内容添加到 &lt;code&gt;production.env&lt;/code&gt; 中。&lt;/p&gt;
&lt;h2&gt;压缩&lt;/h2&gt;
&lt;p&gt;添加压缩非常重要，这样响应正文会更小，应用程序会更快。要添加压缩，您需要安装以下软件包。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i --save compression
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后修改 &lt;code&gt;app.middleware.ts&lt;/code&gt; 文件如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { INestApplication } from &quot;@nestjs/common&quot;;
import * as helmet from &quot;helmet&quot;;
import { corsConfig } from &quot;./config/auth.config&quot;;
import * as compression from &quot;compression&quot;;

export function middleware(app: INestApplication): INestApplication {
  const isProduction = process.env.NODE_ENV === &quot;production&quot;;

  app.use(helmet({ contentSecurityPolicy: isProduction }));
  app.use(compression());

  app.enableCors({
    origin: process.env.ORIGIN || &quot;*&quot;,
    methods: corsConfig.methods,
    allowedHeaders: corsConfig.allowedHeaders,
  });

  return app;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>Nest</category><category>TypeScript</category><author>ChanZhaoYu</author></item><item><title>Redis常用命令</title><link>https://www.redon.cc/posts/redis%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/</link><guid isPermaLink="true">https://www.redon.cc/posts/redis%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/</guid><description>Redis常用命令记录</description><pubDate>Thu, 29 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;SET key value: 设置指定 key 的值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GET key: 获取指定 key 的值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DEL key: 删除指定 key&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;INCR key: 将 key 中储存的数字值增一&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DECR key: 将 key 中储存的数字值减一&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;EXPIRE key seconds: 设置 key 的过期时间，单位为秒&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;KEYS pattern: 查找所有符合给定模式 pattern 的 key&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;EXISTS key: 检查 key 是否存在&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TTL key: 获取 key 的剩余过期时间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HSET key field value: 设置 key 中的哈希表 field 的值为 value&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HGET key field: 获取 key 中哈希表 field 的值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HDEL key field: 删除 key 中的哈希表 field&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HMSET key field1 value1 field2 value2 ...: 同时设置多个哈希表 field 的值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HMGET key field1 field2 ...: 获取多个哈希表 field 的值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LPUSH key value1 value2 ...: 将一个或多个值插入到列表头部&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RPUSH key value1 value2 ...: 将一个或多个值插入到列表尾部&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LPOP key: 移除并返回列表的第一个元素&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RPOP key: 移除并返回列表的最后一个元素&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LLEN key: 获取列表的长度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LRANGE key start stop: 获取列表指定范围内的元素&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SADD key member1 member2 ...: 向集合添加一个或多个成员&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SMEMBERS key: 获取集合中的所有成员&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SREM key member1 member2 ...: 从集合中移除一个或多个成员&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SUNION key1 key2 ...: 返回多个集合的并集&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ZADD key score1 member1 score2 member2 ...: 向有序集合添加一个或多个成员，或更新已存在成员的分数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ZRANGE key start stop: 按分数从小到大的顺序，返回有序集合中指定范围内的成员&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ZREM key member1 member2 ...: 从有序集合中移除一个或多个成员&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ZSCORE key member: 获取有序集合中指定成员的分数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FLUSHALL: 删除所有数据库中的所有 key&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>Redis</category><author>ChanZhaoYu</author></item><item><title>常用正则</title><link>https://www.redon.cc/posts/%E5%B8%B8%E7%94%A8%E6%AD%A3%E5%88%99/</link><guid isPermaLink="true">https://www.redon.cc/posts/%E5%B8%B8%E7%94%A8%E6%AD%A3%E5%88%99/</guid><description>常用正则大全，来源 any-rule</description><pubDate>Thu, 22 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;数据来源于 &lt;a href=&quot;https://github.com/any86/any-rule&quot;&gt;any-rule 项目&lt;/a&gt;，方便自身查询&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;火车车次&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[GCDZTSPKXLY1-9]\d{1,4}$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;手机机身码(IMEI)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^\d{15,17}$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;必须带端口号的网址(或ip)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^((ht|f)tps?:\/\/)?[\w-]+(\.[\w-]+)+:\d{1,5}\/?$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;网址(URL)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(((ht|f)tps?):\/\/)?([^!@#$%^&amp;amp;*?.\s-]([^!@#$%^&amp;amp;*?.\s]{0,63}[^!@#$%^&amp;amp;*?.\s])?\.)+[a-z]{2,6}\/?/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;统一社会信用代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;统一社会信用代码(宽松匹配)(15位/18位/20位数字/字母)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(([0-9A-Za-z]{15})|([0-9A-Za-z]{18})|([0-9A-Za-z]{20}))$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;迅雷链接&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^thunderx?:\/\/[a-zA-Z\d]+=$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ed2k链接(宽松匹配)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^ed2k:\/\/\|file\|.+\|\/$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;磁力链接(宽松匹配)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^magnet:\?xt=urn:btih:[0-9a-fA-F]{40,}.*$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;子网掩码(不包含 0.0.0.0)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(254|252|248|240|224|192|128)\.0\.0\.0|255\.(254|252|248|240|224|192|128|0)\.0\.0|255\.255\.(254|252|248|240|224|192|128|0)\.0|255\.255\.255\.(255|254|252|248|240|224|192|128|0)$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;linux&quot;隐藏文件&quot;路径&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^\/(?:[^/]+\/)*\.[^/]*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;linux文件夹路径&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^\/(?:[^/]+\/)*$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;linux文件路径&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^\/(?:[^/]+\/)*[^/]+$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;window&quot;文件夹&quot;路径&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[a-zA-Z]:\\(?:\w+\\?)*$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;window下&quot;文件&quot;路径&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[a-zA-Z]:\\(?:\w+\\)*\w+\.\w+$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;股票代码(A股)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(s[hz]|S[HZ])(000[\d]{3}|002[\d]{3}|300[\d]{3}|600[\d]{3}|60[\d]{4})$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;大于等于0, 小于等于150, 支持小数位出现5, 如145.5, 用于判断考卷分数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^150$|^(?:\d|[1-9]\d|1[0-4]\d)(?:\.5)?$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;html注释&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/&amp;lt;!--[\s\S]*?--&amp;gt;/g
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;md5格式(32位)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[a-fA-F0-9]{32}$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;GUID/UUID&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[a-f\d]{4}(?:[a-f\d]{4}-){4}[a-f\d]{12}$/i
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;版本号(version)格式必须为X.Y.Z&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^\d+(?:\.\d+){2}$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;视频(video)链接地址（视频格式可按需增删）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^https?:\/\/(.+\/)+.+(\.(swf|avi|flv|mpg|rm|mov|wav|asf|3gp|mkv|rmvb|mp4))$/i
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;图片(image)链接地址（图片格式可按需增删）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^https?:\/\/(.+\/)+.+(\.(gif|png|jpg|jpeg|webp|svg|psd|bmp|tif))$/i
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;24小时制时间（HH:mm:ss）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;12小时制时间（hh:mm:ss）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(?:1[0-2]|0?[1-9]):[0-5]\d:[0-5]\d$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;base64格式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^\s*data:(?:[a-z]+\/[a-z0-9-+.]+(?:;[a-z-]+=[a-z0-9-]+)?)?(?:;base64)?,([a-z0-9!$&amp;amp;&apos;,()*+;=\-._~:@/?%\s]*?)\s*$/i
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数字/货币金额（支持负数、千分位分隔符）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^-?\d{1,3}(,\d{3})*(\.\d{1,2})?$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;银行卡号（10到30位, 覆盖对公/私账户, 参考&lt;a href=&quot;https://pay.weixin.qq.com/wiki/doc/api/xiaowei.php?chapter=22_1&quot;&gt;微信支付&lt;/a&gt;）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[1-9]\d{9,29}$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;中文姓名&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(?:[\u4e00-\u9fa5·]{2,16})$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;英文姓名&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/(^[a-zA-Z][a-zA-Z\s]{0,20}[a-zA-Z]$)/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;车牌号(新能源)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z](([DF]((?![IO])[a-zA-Z0-9](?![IO]))[0-9]{4})|([0-9]{5}[DF]))$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;车牌号(非新能源)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;车牌号(新能源+非新能源)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;手机号(mobile phone)中国(严谨), 根据工信部2019年最新公布的手机号段&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(?:(?:\+|00)86)?1(?:(?:3[\d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\d])|(?:9[01256789]))\d{8}$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;手机号(mobile phone)中国(宽松), 只要是13,14,15,16,17,18,19开头即可&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(?:(?:\+|00)86)?1[3-9]\d{9}$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;手机号(mobile phone)中国(最宽松), 只要是1开头即可, 如果你的手机号是用来接收短信, 优先建议选择这一条&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(?:(?:\+|00)86)?1\d{10}$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;日期(宽松)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^\d{1,4}(-)(1[0-2]|0?[1-9])\1(0?[1-9]|[1-2]\d|30|31)$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;日期(严谨, 支持闰年判断)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})-(((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01]))|((0[469]|11)-(0[1-9]|[12][0-9]|30))|(02-(0[1-9]|[1][0-9]|2[0-8]))))|((([0-9]{2})(0[48]|[2468][048]|[13579][26])|((0[48]|[2468][048]|[3579][26])00))-02-29)$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;中国省&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^浙江|上海|北京|天津|重庆|黑龙江|吉林|辽宁|内蒙古|河北|新疆|甘肃|青海|陕西|宁夏|河南|山东|山西|安徽|湖北|湖南|江苏|四川|贵州|云南|广西|西藏|江西|广东|福建|台湾|海南|香港|澳门$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;可以被moment转化成功的时间 YYYYMMDD HH:mm:ss&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^\d{4}([/:-\S])(1[0-2]|0?[1-9])\1(0?[1-9]|[1-2]\d|30|31) (?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;email(邮箱)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(([^&amp;lt;&amp;gt;()[\]\\.,;:\s@&quot;]+(\.[^&amp;lt;&amp;gt;()[\]\\.,;:\s@&quot;]+)*)|(&quot;.+&quot;))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;座机(tel phone)电话(国内),如: 0341-86091234&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(?:(?:\d{3}-)?\d{8}|^(?:\d{4}-)?\d{7,8})(?:-\d+)?$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;身份证号(1代,15位数字)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[1-9]\d{7}(?:0\d|10|11|12)(?:0[1-9]|[1-2][\d]|30|31)\d{3}$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;身份证号(2代,18位数字),最后一位是校验位,可能为数字或字符X&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;身份证号, 支持1/2代(15位/18位数字)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^\d{6}((((((19|20)\d{2})(0[13-9]|1[012])(0[1-9]|[12]\d|30))|(((19|20)\d{2})(0[13578]|1[02])31)|((19|20)\d{2})02(0[1-9]|1\d|2[0-8])|((((19|20)([13579][26]|[2468][048]|0[48]))|(2000))0229))\d{3})|((((\d{2})(0[13-9]|1[012])(0[1-9]|[12]\d|30))|((\d{2})(0[13578]|1[02])31)|((\d{2})02(0[1-9]|1\d|2[0-8]))|(([13579][26]|[2468][048]|0[048])0229))\d{2}))(\d|X|x)$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;护照（包含香港、澳门）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/(^[EeKkGgDdSsPpHh]\d{8}$)|(^(([Ee][a-fA-F])|([DdSsPp][Ee])|([Kk][Jj])|([Mm][Aa])|(1[45]))\d{7}$)/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;帐号是否合法(字母开头，允许5-16字节，允许字母数字下划线组合&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[a-zA-Z]\w{4,15}$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;中文/汉字&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])+$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;小数(支持科学计数)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[+-]?(\d+([.]\d*)?([eE][+-]?\d+)?|[.]\d+([eE][+-]?\d+)?)$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;只包含数字&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^\d+$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;html标签(宽松匹配)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/&amp;lt;(\w+)[^&amp;gt;]*&amp;gt;(.*?&amp;lt;\/\1&amp;gt;)?/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;匹配中文汉字和中文标点&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/[\u4e00-\u9fa5|\u3002|\uff1f|\uff01|\uff0c|\u3001|\uff1b|\uff1a|\u201c|\u201d|\u2018|\u2019|\uff08|\uff09|\u300a|\u300b|\u3008|\u3009|\u3010|\u3011|\u300e|\u300f|\u300c|\u300d|\ufe43|\ufe44|\u3014|\u3015|\u2026|\u2014|\uff5e|\ufe4f|\uffe5]/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;qq号格式正确&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[1-9][0-9]{4,10}$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数字和字母组成&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[A-Za-z0-9]+$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;英文字母&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[a-zA-Z]+$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;小写英文字母组成&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[a-z]+$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;大写英文字母&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[A-Z]+$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;密码强度校验，最少6位，包括至少1个大写字母，1个小写字母，1个数字，1个特殊字符&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&amp;amp;*? ])\S*$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;用户名校验，4到16位（字母，数字，下划线，减号）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[\w-]{4,16}$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ip-v4[:端口]&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])(?::(?:[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ip-v6[:端口]&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/(^(?:(?:(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$)|(^\[(?:(?:(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))\](?::(?:[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$)/i
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;16进制颜色&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3}|[a-fA-F0-9]{8}|[a-fA-F0-9]{4})$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;微信号(wx)，6至20位，以字母开头，字母，数字，减号，下划线&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[a-zA-Z][-_a-zA-Z0-9]{5,19}$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;邮政编码(中国)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(0[1-7]|1[0-356]|2[0-7]|3[0-6]|4[0-7]|5[1-7]|6[1-7]|7[0-5]|8[013-6])\d{4}$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;中文和数字&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^((?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])|(\d))+$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;不能包含字母&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[^A-Za-z]*$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;java包名&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^([a-zA-Z_]\w*)+([.][a-zA-Z_]\w*)+$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;mac地址&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(([a-f0-9][0,2,4,6,8,a,c,e]:([a-f0-9]{2}:){4})|([a-f0-9][0,2,4,6,8,a,c,e]-([a-f0-9]{2}-){4}))[a-f0-9]{2}$/i
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;匹配连续重复的字符&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/(.)\1+/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数字和英文字母组成，并且同时含有数字和英文字母&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(?=.*[a-zA-Z])(?=.*\d).+$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;香港身份证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[a-zA-Z]\d{6}\([\dA]\)$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;澳门身份证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[1|5|7]\d{6}\(\d\)$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;台湾身份证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[a-zA-Z][0-9]{9}$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;大写字母，小写字母，数字，特殊符号 &lt;code&gt;@#$%^&amp;amp;*&lt;/code&gt;~()-+=` 中任意3项密码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z\W_!@#$%^&amp;amp;*`~()-+=]+$)(?![a-z0-9]+$)(?![a-z\W_!@#$%^&amp;amp;*`~()-+=]+$)(?![0-9\W_!@#$%^&amp;amp;*`~()-+=]+$)[a-zA-Z0-9\W_!@#$%^&amp;amp;*`~()-+=]/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ASCII码表中的全部的特殊字符&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;正整数，不包含0&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^\+?[1-9]\d*$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;负整数，不包含0&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^-[1-9]\d*$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;整数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(?:0|(?:-?[1-9]\d*))$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;浮点数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(-?[1-9]\d*\.\d+|-?0\.\d*[1-9]\d*|0\.0+)$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;浮点数(严格)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^(-?[1-9]\d*\.\d+|-?0\.\d*[1-9])$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;email(支持中文邮箱)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;域名(非网址, 不包含协议)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^([0-9a-zA-Z-]{1,}\.)+([a-zA-Z]{2,})$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;军官/士兵证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/^[\u4E00-\u9FA5](字第)([0-9a-zA-Z]{4,8})(号?)$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;户口薄&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>JS</category><category>正则表达式</category><author>ChanZhaoYu</author></item><item><title>使用 document.referrer 判断上一个页面URL地址</title><link>https://www.redon.cc/posts/%E4%BD%BF%E7%94%A8documentreferrer%E5%88%A4%E6%96%AD%E4%B8%8A%E4%B8%80%E4%B8%AA%E9%A1%B5%E9%9D%A2url%E5%9C%B0%E5%9D%80/</link><guid isPermaLink="true">https://www.redon.cc/posts/%E4%BD%BF%E7%94%A8documentreferrer%E5%88%A4%E6%96%AD%E4%B8%8A%E4%B8%80%E4%B8%AA%E9%A1%B5%E9%9D%A2url%E5%9C%B0%E5%9D%80/</guid><description>js 获取前一个访问页面的URL地址 document.referrer </description><pubDate>Wed, 21 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;平常在移动端内页左上角的返回按钮在比方说用户是通过微信分享进来的，直接进入了内页，此时是没有上一页的，返回按钮再怎么点击都没有任何反应。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (document.referrer) {
  // 返回上一页
} else {
  // 返回首页
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>JS</category><author>ChanZhaoYu</author></item><item><title>使用 PM2 部署 Node.js 应用</title><link>https://www.redon.cc/posts/pm2%E9%83%A8%E7%BD%B2nodejs%E5%BA%94%E7%94%A8/</link><guid isPermaLink="true">https://www.redon.cc/posts/pm2%E9%83%A8%E7%BD%B2nodejs%E5%BA%94%E7%94%A8/</guid><description>PM2是一个用于管理和保持Node.js应用状态的进程管理器。它可以用于部署和监控你的Node.js应用程序。本文将介绍如何使用PM2部署Node.js应用。</description><pubDate>Mon, 22 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;原文来自 &lt;a href=&quot;https://aaronnotes.com/2023/04/deploy-nodejs-using-pm2/&quot;&gt;aaronnotes&lt;/a&gt; 博客&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;PM2 是一个用于管理和保持 Node.js 应用状态的进程管理器。它可以用于部署和监控你的 Node.js 应用程序。本文将介绍如何使用 PM2 部署 Node.js 应用。&lt;/p&gt;
&lt;h2&gt;为什么要使用 PM2&lt;/h2&gt;
&lt;p&gt;使用 PM2 部署 Node.js 应用程序有几个好处:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;进程管理。PM2 可以启动、停止、重启和守护你的 Node.js 应用程序，它将应用程序作为进程守护在后台运行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;持久化。PM2 可以在服务器重启后自动重启你的 Node.js 应用，这意味着你的应用将始终处于运行状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;负载均衡。PM2 可以轻松制作一个本地的负载均衡器来平衡你的 Node.js 应用的多个实例之间的传入请求。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;监控。PM2 提供一个简单的仪表板来监控你所有的 Node.js 应用程序（CPU、内存、进程数等）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;日志管理。PM2 聚合了所有应用程序的日志，可以轻松管理和查询日志。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;使用 PM2 部署 Node.js 应用&lt;/h2&gt;
&lt;p&gt;安装 PM2：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install pm2@latest -g
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入你的应用程序目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd your-app/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动应用程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pm2 start npm --name &quot;your-app-name&quot; -- run start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看应用程序状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pm2 list
# 或 pm2 ls
# 或 pm2 status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你将看到你的应用程序名称以及相关信息（进程 ID、内存使用情况、重启次数等）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌────┬────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name       │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├────┼────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0  │ app1       │ default     │ 2.16.3  │ cluster │ 18259    │ 37s    │ 0    │ online    │ 0%       │ 93.9mb   │ root     │ disabled │
│ 1  │ app2       │ default     │ 2.16.3  │ cluster │ 18289    │ 11s    │ 0    │ online    │ 0%       │ 114.5mb  │ root     │ disabled │
└────┴────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启应用程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pm2 restart your-app-name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;停止应用程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pm2 stop your-app-name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PM2 也有一个简单的仪表板可以监控你的应用程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pm2 monit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，你可以通过运行以下命令，在服务器重启后使 PM2 自动启动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pm2 startup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pm2 startup&lt;/code&gt; 是 PM2 的一个命令，它的主要作用是创建一个系统服务，以便在系统重启时自动启动 PM2 进程管理器和已经被管理的应用程序。&lt;/p&gt;
&lt;p&gt;具体来说，&lt;code&gt;pm2 startup&lt;/code&gt; 执行以下操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;检测当前系统使用哪种初始化系统（例如，&lt;code&gt;systemd&lt;/code&gt;、&lt;code&gt;upstart&lt;/code&gt;、&lt;code&gt;SysV&lt;/code&gt;等），并生成相应的启动脚本。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将这个启动脚本复制到系统服务目录中（例如，&lt;code&gt;/etc/systemd/system&lt;/code&gt;目录），并设置服务启动时的用户、环境变量以及其他相关信息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后，它会生成一条命令，要求用户以管理员权限运行该命令，并将其复制到终端中执行。这个命令将启用刚刚创建的系统服务，并在系统重启时自动启动 PM2 进程管理器和已经被管理的应用程序。这样，用户就不必手动启动这些进程，从而提高了系统的可靠性和稳定性。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果停用自动启动，运行命令：&lt;code&gt;pm2 unstartup&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;以下是执行完命令 &lt;code&gt;pm2 startup&lt;/code&gt; 的输出信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[PM2] Init System found: systemd
Platform systemd
Template
[Unit]
Description=PM2 process manager
Documentation=https://pm2.keymetrics.io/
After=network.target

[Service]
Type=forking
User=root
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
Environment=PATH=/root/.nvm/versions/node/v18.15.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
Environment=PM2_HOME=/root/.pm2
PIDFile=/root/.pm2/pm2.pid
Restart=on-failure

ExecStart=/root/.nvm/versions/node/v18.15.0/lib/node_modules/pm2/bin/pm2 resurrect
ExecReload=/root/.nvm/versions/node/v18.15.0/lib/node_modules/pm2/bin/pm2 reload all
ExecStop=/root/.nvm/versions/node/v18.15.0/lib/node_modules/pm2/bin/pm2 kill

[Install]
WantedBy=multi-user.target

Target path
/etc/systemd/system/pm2-root.service
Command list
[ &apos;systemctl enable pm2-root&apos; ]
[PM2] Writing init configuration in /etc/systemd/system/pm2-root.service
[PM2] Making script booting at startup...
[PM2] [-] Executing: systemctl enable pm2-root...
Created symlink /etc/systemd/system/multi-user.target.wants/pm2-root.service → /etc/systemd/system/pm2-root.service.
[PM2] [v] Command successfully executed.
+---------------------------------------+
[PM2] Freeze a process list on reboot via:
$ pm2 save

[PM2] Remove init script via:
$ pm2 unstartup systemd
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;systemd&lt;/code&gt; 是一个 Linux 系统初始化系统和服务管理器，是目前主流 Linux 发行版（如 Ubuntu、Debian、CentOS、Red Hat 等）的默认初始化系统。它负责启动系统服务、管理进程、处理日志、控制系统休眠和唤醒等任务。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;systemd&lt;/code&gt; 的主要目标是提高系统启动速度和效率，统一系统管理，简化配置文件格式，提高可靠性和可维护性。 它采用一种事件驱动的方式管理系统，所有的系统服务都以单独的进程运行，并由 &lt;code&gt;systemd&lt;/code&gt; 统一管理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;使用 &lt;code&gt;ecosystem.config.js&lt;/code&gt; 管理应用程序&lt;/h2&gt;
&lt;p&gt;PM2 可以使用 &lt;code&gt;ecosystem.config.js&lt;/code&gt; 文件来管理多个应用程序，这比手动启动每个应用程序更加结构化和可维护。&lt;code&gt;ecosystem.config.js&lt;/code&gt; 是一个模块，它导出用于定义应用程序环境的配置。&lt;/p&gt;
&lt;p&gt;下面这个简单示例用来启动 &lt;code&gt;Nuxt&lt;/code&gt; 应用程序的两个进程：&lt;code&gt;app1&lt;/code&gt; 和 &lt;code&gt;app2&lt;/code&gt;，分别使用端口 &lt;code&gt;3000&lt;/code&gt; 和 &lt;code&gt;3001&lt;/code&gt;。同时在 &lt;code&gt;Nginx&lt;/code&gt; 中配置了 &lt;code&gt;upstream server&lt;/code&gt; 指向这两个端口做负载均衡，以方便应用升级的时候可以不影响后台服务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  apps: [
    {
      name: &quot;app1&quot;,
      exec_mode: &quot;cluster&quot;,
      port: 3000,
      instances: &quot;4&quot;, // Or a number of instances
      script: &quot;./node_modules/nuxt/bin/nuxt.js&quot;,
      args: &quot;start&quot;,
    },
    {
      name: &quot;app2&quot;,
      exec_mode: &quot;cluster&quot;,
      port: 3001,
      instances: &quot;4&quot;, // Or a number of instances
      script: &quot;./node_modules/nuxt/bin/nuxt.js&quot;,
      args: &quot;start&quot;,
    },
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要启动此配置，运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pm2 start ecosystem.config.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者可以单独启动某一个应用进程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pm2 start app1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们需要升级更新该 Nuxt 应用，我们只需将代码更新到服务器中，依次运行下面的命令，即可做到在升级的时候后台服务不需间断。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run build
pm2 stop app1
pm2 start app1
pm2 stop app2
pm2 start app2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;PM2 的进程执行模式&lt;/h2&gt;
&lt;p&gt;在上面的示例中，我们看到一个配置 &lt;code&gt;exec_mode: &apos;cluster&apos;&lt;/code&gt;，这表示 PM2 的进程执行模式选择 &lt;code&gt;cluster&lt;/code&gt; 模式。&lt;/p&gt;
&lt;p&gt;PM2 有两种进程执行模式：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fork&lt;/code&gt; 模式是默认的执行模式，它使用单个主进程来管理所有的子进程。在这种模式下，每个应用程序都会被复制多次，以创建多个独立的子进程。每个子进程都可以独立地处理请求，但它们之间不会共享状态或内存。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cluster&lt;/code&gt; 模式是一种高级模式，它使用多个主进程来管理所有的子进程。在这种模式下，每个应用程序只会被复制一次，并由多个子进程共享。这些子进程可以共享状态和内存，并且可以使用 IPC 通信来协调工作。&lt;/p&gt;
&lt;p&gt;这两种模式的主要区别在于 PM2 使用 Node.js 的 &lt;code&gt;child_process.fork api&lt;/code&gt; 或 &lt;code&gt;cluster api&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;在 fork 模式下，PM2 为你的应用程序的每个实例创建一个单独的 &lt;code&gt;Node.js&lt;/code&gt; 进程。每个进程独立运行，不与其他进程共享任何资源。这意味着，如果一个进程崩溃，它不会影响其他进程。在这个模式下，允许改变 &lt;code&gt;exec_interpreter&lt;/code&gt;（用来启动子进程的 “命令”），所以你可以用 PM2 运行一个 PHP 或 Python 服务器 。默认情况下，PM2 会使用 node，所以 &lt;code&gt;pm2 start server.js&lt;/code&gt; 类似于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require(&apos;child_process&apos;).spwn(&apos;node&apos;, [&apos;server.js&apos;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种模式非常有用，因为它可以实现很多可能性。例如，你可以在预先确定的端口上启动多个服务器，然后由 HAProxy 或 Nginx 进行负载均衡。&lt;/p&gt;
&lt;p&gt;cluster 只在 node 作为执行解释器的情况下工作，&lt;code&gt;cluster&lt;/code&gt; 模式使用内置的 &lt;code&gt;Node.js cluster&lt;/code&gt; 模块来创建一个进程集群，这些进程都共享同一个服务器端口。这使 &lt;code&gt;Node.js&lt;/code&gt; 能够利用多个 CPU 核心，并使需要处理大量并发请求的应用程序具有更好的性能和可扩展性。在 &lt;code&gt;cluster&lt;/code&gt; 模式下，PM2 创建一个主进程和几个工作进程。主进程管理工作进程，并将传入的请求分配给它们。&lt;/p&gt;
&lt;p&gt;一般来说，&lt;code&gt;fork&lt;/code&gt; 模式适用于中低流量的简单应用，而 &lt;code&gt;cluster&lt;/code&gt; 模式更适合于需要更好性能和可扩展性的高流量应用。然而，需要注意的是，&lt;code&gt;cluster&lt;/code&gt; 模式比 &lt;code&gt;fork&lt;/code&gt; 模式需要更多的内存和 CPU 资源，所以它可能不适合所有的应用。&lt;/p&gt;
&lt;p&gt;要切换模式，在部署应用程序时使用 &lt;code&gt;--exec-mode&lt;/code&gt; 参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pm2 start app.js --exec-mode=cluster
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或在 &lt;code&gt;ecosystem.config.js&lt;/code&gt; 文件中设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  apps: [
    {
      exec_mode: &quot;cluster&quot;,
    },
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;例子&lt;/h2&gt;
&lt;h3&gt;Nuxt&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  apps: [
    {
      name: &quot;nuxt-app&quot;,
      exec_mode: &quot;cluster&quot;,
      port: 3000,
      script: &quot;./node_modules/nuxt/bin/nuxt.js&quot;,
      args: &quot;start&quot;,
    },
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Next&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  apps: [
    {
      name: &quot;next-app&quot;,
      exec_mode: &quot;cluster&quot;,
      port: 3000,
      script: &quot;./node_modules/next/dist/bin/next&quot;,
      args: &quot;start&quot;,
    },
  ],
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>PM2</category><category>Node</category><author>ChanZhaoYu</author></item><item><title>卡片发光效果</title><link>https://www.redon.cc/posts/%E5%8D%A1%E7%89%87%E5%8F%91%E5%85%89%E6%95%88%E6%9E%9C/</link><guid isPermaLink="true">https://www.redon.cc/posts/%E5%8D%A1%E7%89%87%E5%8F%91%E5%85%89%E6%95%88%E6%9E%9C/</guid><description>一个常见的用于落地页卡片介绍的样式效果，使用 React 来做演示</description><pubDate>Tue, 02 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://codepen.io/jh3y/pen/WNmQXyE&quot;&gt;CodePen 在线地址&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;HTML&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div id=&quot;app&quot;&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CSS&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:root {
  --backdrop: hsl(0 0% 60% / 0.12);
  --radius: 14;
  --border: 3;
  --backup-border: var(--backdrop);
  --size: 200;
}

article:first-of-type {
  --base: 80;
  --spread: 500;
  --outer: 1;
}
article:last-of-type {
  --outer: 1;
  --base: 220;
  --spread: 200;
}

*,
*:after,
*:before {
  box-sizing: border-box;
}
body {
  display: grid;
  place-items: center;
  min-height: 100vh;
  overflow: hidden;
  background: hsl(0 0% 4%);
}

.wrapper {
  position: relative;
}

article {
  aspect-ratio: 3 / 4;
  border-radius: calc(var(--radius) * 1px);
  width: 260px;
  position: relative;
  grid-template-rows: 1fr auto;
  box-shadow: 0 1rem 2rem -1rem black;
  padding: 1rem;
  display: grid;
  border: 1px solid hsl(0 0% 100% / 0.15);
  backdrop-filter: blur(calc(var(--cardblur, 5) * 1px));
  /* For demo purposes. Means you get the effect on mobile */
  touch-action: none;
}
main {
  display: flex;
  gap: 2rem;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  width: 120ch;
  max-width: calc(100vw - 2rem);
  position: relative;
}

/* Glow specific styles */
[data-glow] {
  --border-size: calc(var(--border, 2) * 1px);
  --spotlight-size: calc(var(--size, 150) * 1px);
  --hue: calc(var(--base) + (var(--xp, 0) * var(--spread, 0)));
  background-image: radial-gradient(
    var(--spotlight-size) var(--spotlight-size) at calc(var(--x, 0) * 1px) calc(
        var(--y, 0) * 1px
      ),
    hsl(
      var(--hue, 210) calc(var(--saturation, 100) * 1%) calc(
          var(--lightness, 70) * 1%
        ) / var(--bg-spot-opacity, 0.1)
    ),
    transparent
  );
  background-color: var(--backdrop, transparent);
  background-size: calc(100% + (2 * var(--border-size))) calc(
      100% + (2 * var(--border-size))
    );
  background-position: 50% 50%;
  background-attachment: fixed;
  border: var(--border-size) solid var(--backup-border);
  position: relative;
  touch-action: none;
}

[data-glow]::before,
[data-glow]::after {
  pointer-events: none;
  content: &quot;&quot;;
  position: absolute;
  inset: calc(var(--border-size) * -1);
  border: var(--border-size) solid transparent;
  border-radius: calc(var(--radius) * 1px);
  background-attachment: fixed;
  background-size: calc(100% + (2 * var(--border-size))) calc(
      100% + (2 * var(--border-size))
    );
  background-repeat: no-repeat;
  background-position: 50% 50%;
  mask: linear-gradient(transparent, transparent), linear-gradient(white, white);
  mask-clip: padding-box, border-box;
  mask-composite: intersect;
}

/* This is the emphasis light */
[data-glow]::before {
  background-image: radial-gradient(
    calc(var(--spotlight-size) * 0.75) calc(var(--spotlight-size) * 0.75) at
      calc(var(--x, 0) * 1px) calc(var(--y, 0) * 1px),
    hsl(
      var(--hue, 210) calc(var(--saturation, 100) * 1%) calc(
          var(--lightness, 50) * 1%
        ) / var(--border-spot-opacity, 1)
    ),
    transparent 100%
  );
  filter: brightness(2);
}
/* This is the spotlight */
[data-glow]::after {
  background-image: radial-gradient(
    calc(var(--spotlight-size) * 0.5) calc(var(--spotlight-size) * 0.5) at calc(
        var(--x, 0) * 1px
      ) calc(var(--y, 0) * 1px),
    hsl(0 100% 100% / var(--border-light-opacity, 1)),
    transparent 100%
  );
}
[data-glow] &amp;gt; [data-glow]:not(:is(a, button)) {
  position: absolute;
  inset: 0;
  will-change: filter;
  opacity: var(--outer, 1);
}
[data-glow] &amp;gt; [data-glow]:not(:is(a, button)) {
  border-radius: calc(var(--radius) * 1px);
  border-width: calc(var(--border-size) * 20);
  filter: blur(calc(var(--border-size) * 10));
  background: none;
  pointer-events: none;
}
[data-glow] &amp;gt; [data-glow]:not(:is(a, button))::before {
  inset: -10px;
  border-width: 10px;
}
[data-glow] &amp;gt; [data-glow] {
  border: none;
}
[data-glow] :is(a, button) {
  border-radius: calc(var(--radius) * 1px);
  border: var(--border-size) solid transparent;
}
[data-glow] :is(a, button) [data-glow] {
  background: none;
}
[data-glow] :is(a, button) [data-glow]::before {
  inset: calc(var(--border-size) * -1);
  border-width: calc(var(--border-size) * 1);
}

article button {
  padding: 0.75rem 2rem;
  align-self: end;
  color: hsl(0 0% 80%);
}

button[data-glow] span {
  font-weight: bold;
  background-image: radial-gradient(
    var(--spotlight-size) var(--spotlight-size) at calc(var(--x, 0) * 1px) calc(
        var(--y, 0) * 1px
      ),
    hsl(
      var(--hue, 210) calc(var(--saturation, 100) * 1%) calc(
          var(--lightness, 70) * 1%
        ) / var(--bg-spot-opacity, 1)
    ),
    transparent
  );
  background-color: var(--backdrop, transparent);
  background-position: 50% 50%;
  background-attachment: fixed;
  background-clip: text;
  filter: brightness(1.5);
  color: transparent;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JavaScript&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React from &quot;https://cdn.skypack.dev/react&quot;;
import { render } from &quot;https://cdn.skypack.dev/react-dom&quot;;

const ROOT_NODE = document.querySelector(&quot;#app&quot;);

/**
 * Tiny hook that you can use where you need it
 */
const usePointerGlow = () =&amp;gt; {
  const [status, setStatus] = React.useState(null);
  React.useEffect(() =&amp;gt; {
    const syncPointer = ({ x: pointerX, y: pointerY }) =&amp;gt; {
      const x = pointerX.toFixed(2);
      const y = pointerY.toFixed(2);
      const xp = (pointerX / window.innerWidth).toFixed(2);
      const yp = (pointerY / window.innerHeight).toFixed(2);
      document.documentElement.style.setProperty(&quot;--x&quot;, x);
      document.documentElement.style.setProperty(&quot;--xp&quot;, xp);
      document.documentElement.style.setProperty(&quot;--y&quot;, y);
      document.documentElement.style.setProperty(&quot;--yp&quot;, yp);
      setStatus({ x, y, xp, yp });
    };
    document.body.addEventListener(&quot;pointermove&quot;, syncPointer);
    return () =&amp;gt; {
      document.body.removeEventListener(&quot;pointermove&quot;, syncPointer);
    };
  }, []);
  return [status];
};

const App = () =&amp;gt; {
  const [status] = usePointerGlow();
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;article data-glow&amp;gt;
        &amp;lt;span data-glow /&amp;gt;
        &amp;lt;button data-glow&amp;gt;
          &amp;lt;span&amp;gt;Glow Up&amp;lt;/span&amp;gt;
        &amp;lt;/button&amp;gt;
      &amp;lt;/article&amp;gt;
      &amp;lt;article data-glow&amp;gt;
        &amp;lt;span data-glow /&amp;gt;
        &amp;lt;button data-glow&amp;gt;
          &amp;lt;span&amp;gt;Glow Up&amp;lt;/span&amp;gt;
        &amp;lt;/button&amp;gt;
      &amp;lt;/article&amp;gt;
      &amp;lt;article data-glow&amp;gt;
        &amp;lt;span data-glow /&amp;gt;
        &amp;lt;button data-glow&amp;gt;
          &amp;lt;span&amp;gt;Glow Up&amp;lt;/span&amp;gt;
        &amp;lt;/button&amp;gt;
      &amp;lt;/article&amp;gt;
    &amp;lt;/main&amp;gt;
  );
};

render(&amp;lt;App /&amp;gt;, ROOT_NODE);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>React</category><category>CSS</category><author>ChanZhaoYu</author></item><item><title>useFetch 封装</title><link>https://www.redon.cc/posts/usefetch%E5%B0%81%E8%A3%85/</link><guid isPermaLink="true">https://www.redon.cc/posts/usefetch%E5%B0%81%E8%A3%85/</guid><description>对 fetch 的 hooks 封装</description><pubDate>Thu, 28 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;对 fetch 的 hooks 封装，适用于 React、Vue 等&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;export type RequestModel = {
  params?: object;
  headers?: object;
  signal?: AbortSignal;
};

export type RequestWithBodyModel = RequestModel &amp;amp; {
  body?: object | FormData;
};

export const useFetch = () =&amp;gt; {
  const handleFetch = async (
    url: string,
    request: any,
    signal?: AbortSignal
  ) =&amp;gt; {
    const requestUrl = request?.params ? `${url}${request.params}` : url;

    const requestBody = request?.body
      ? request.body instanceof FormData
        ? { ...request, body: request.body }
        : { ...request, body: JSON.stringify(request.body) }
      : request;

    const headers = {
      ...(request?.headers
        ? request.headers
        : request?.body &amp;amp;&amp;amp; request.body instanceof FormData
        ? {}
        : { &quot;Content-type&quot;: &quot;application/json&quot; }),
    };

    return fetch(requestUrl, { ...requestBody, headers, signal })
      .then((response) =&amp;gt; {
        if (!response.ok) throw response;

        const contentType = response.headers.get(&quot;content-type&quot;);
        const contentDisposition = response.headers.get(&quot;content-disposition&quot;);

        const headers = response.headers;

        const result =
          contentType &amp;amp;&amp;amp;
          (contentType?.indexOf(&quot;application/json&quot;) !== -1 ||
            contentType?.indexOf(&quot;text/plain&quot;) !== -1)
            ? response.json()
            : contentDisposition?.indexOf(&quot;attachment&quot;) !== -1
            ? response.blob()
            : response;

        return result;
      })
      .catch(async (err) =&amp;gt; {
        const contentType = err.headers.get(&quot;content-type&quot;);

        const errResult =
          contentType &amp;amp;&amp;amp; contentType?.indexOf(&quot;application/problem+json&quot;) !== -1
            ? await err.json()
            : err;

        throw errResult;
      });
  };

  return {
    get: async &amp;lt;T&amp;gt;(url: string, request?: RequestModel): Promise&amp;lt;T&amp;gt; =&amp;gt; {
      return handleFetch(url, { ...request, method: &quot;get&quot; });
    },
    post: async &amp;lt;T&amp;gt;(
      url: string,
      request?: RequestWithBodyModel
    ): Promise&amp;lt;T&amp;gt; =&amp;gt; {
      return handleFetch(url, { ...request, method: &quot;post&quot; });
    },
    put: async &amp;lt;T&amp;gt;(url: string, request?: RequestWithBodyModel): Promise&amp;lt;T&amp;gt; =&amp;gt; {
      return handleFetch(url, { ...request, method: &quot;put&quot; });
    },
    patch: async &amp;lt;T&amp;gt;(
      url: string,
      request?: RequestWithBodyModel
    ): Promise&amp;lt;T&amp;gt; =&amp;gt; {
      return handleFetch(url, { ...request, method: &quot;patch&quot; });
    },
    delete: async &amp;lt;T&amp;gt;(url: string, request?: RequestModel): Promise&amp;lt;T&amp;gt; =&amp;gt; {
      return handleFetch(url, { ...request, method: &quot;delete&quot; });
    },
  };
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>React</category><category>Vue</category><category>typescript</category><author>ChanZhaoYu</author></item><item><title>Node 压缩打包后的 Dist 文件</title><link>https://www.redon.cc/posts/node%E5%8E%8B%E7%BC%A9%E6%89%93%E5%8C%85%E5%90%8E%E7%9A%84dist%E6%96%87%E4%BB%B6/</link><guid isPermaLink="true">https://www.redon.cc/posts/node%E5%8E%8B%E7%BC%A9%E6%89%93%E5%8C%85%E5%90%8E%E7%9A%84dist%E6%96%87%E4%BB%B6/</guid><description>vite 项目打包后自动压缩打包后的 dist 文件夹，生成 zip 包，方便部署</description><pubDate>Mon, 11 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;用于 vite 项目打包后自动压缩打包后的 dist 文件夹，生成 zip 包，方便部署&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;安装 archiver&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pnpm add archiver
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;压缩打包&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// compress.js
const fs = require(&quot;fs&quot;);
const path = require(&quot;path&quot;);
const archiver = require(&quot;archiver&quot;);
const dayjs = require(&quot;dayjs&quot;);
const pkg = require(&quot;./package.json&quot;);

const folderName = &quot;dist&quot;;

const compressName = `Application_${pkg.version}_Release.zip`;

// 删除已经存在的同名压缩包
const compressFile = path.resolve(__dirname, compressName);
fs.exists(compressFile, function (exists) {
  if (exists) {
    fs.unlinkSync(compressFile);
  }
});

// 判断文件夹是否存在
const folder = path.resolve(__dirname, folderName);
fs.stat(folder, function (err, stats) {
  if (!stats) {
    console.log(`[compress]: 未找到 ${folderName} 文件夹`);
  } else {
    compressFn();
  }
});

// 压缩函数
function compressFn() {
  const date = dayjs().format(&quot;MM-DD&quot;);
  const output = fs.createWriteStream(`${__dirname}/${compressName}`);
  const archive = archiver(&quot;zip&quot;, { zlib: { level: 9 } });
  archive.pipe(output);
  archive.directory(`${folderName}/`, false);
  archive.finalize();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;命令&lt;/h2&gt;
&lt;p&gt;可手动 Node 运行，也可以加到 package.json 脚本或打包命令后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;compress&quot;: &quot;node compress.js&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>Node</category><author>ChanZhaoYu</author></item><item><title>Prisma 常用命令</title><link>https://www.redon.cc/posts/prisma%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/</link><guid isPermaLink="true">https://www.redon.cc/posts/prisma%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/</guid><description>总结 Prisma 常用命令</description><pubDate>Fri, 08 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://www.prisma.io/&quot;&gt;Prisma Site&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pnpm add prisma -D
pnpm add @prisma/client
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;初始化&lt;/h2&gt;
&lt;p&gt;可选数据库：sqlite、postgresql、mysql、sqlserver&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx prisma init --datasource-provider sqlite
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;prisma.ts 文件&lt;/h2&gt;
&lt;p&gt;数据库的操作一般都是引入 prisma.ts 文件进行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { PrismaClient } from &quot;@prisma/client&quot;;

import { env } from &quot;@/env&quot;;

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const db =
  globalForPrisma.prisma ??
  new PrismaClient({
    log:
      env.NODE_ENV === &quot;development&quot; ? [&quot;query&quot;, &quot;error&quot;, &quot;warn&quot;] : [&quot;error&quot;],
  });

if (env.NODE_ENV !== &quot;production&quot;) globalForPrisma.prisma = db;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;模型示例&lt;/h2&gt;
&lt;p&gt;schema.prisma 文件中创建示例模型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  published Boolean @default(false)
  author    User    @relation(fields: [authorId], references: [id])
  authorId  Int
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;更新模型&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;npx prisma migrate dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会生成一个新的迁移，并会让 Prisma 帮我们完成这些工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根据 schema.prisma 中的模型定义，生成了创建这些模型所需的 SQL 语句&lt;/li&gt;
&lt;li&gt;迁移文件的名称包含了 migrate 命令中可以指定 &quot;--name&quot; 参数，便于版本控制&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;生成 Prisma Client&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;npx prisma generate
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Prisma Client 是一个类型安全的数据库访问库，它是根据 schema.prisma 中的模型定义生成的&lt;/li&gt;
&lt;li&gt;Prisma Client 会根据 schema.prisma 中的模型定义生成一个强类型的数据访问层，这样我们就可以在代码中使用它了&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Prisma Studio&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;npx prisma studio
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Prisma Studio 是一个可视化的数据库管理工具，它可以让我们在浏览器中查看和管理数据库中的数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Push &amp;amp; Pull&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;npx prisma db push
npx prisma db pull
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;db push 命令会将本地的 schema.prisma 文件中的模型定义推送到数据库中&lt;/li&gt;
&lt;li&gt;db pull 命令会将数据库中的模型定义拉取到本地的 schema.prisma 文件中&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;更新生产环境数据库&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;npx prisma migrate deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在本地运行：npx prisma migrate dev --name add_new_field 来生成新的迁移文件&lt;/li&gt;
&lt;li&gt;将生成的迁移文件更新到生产环境数据库：npx prisma migrate deploy&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;重置数据库&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;npx prisma migrate reset
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;重置数据库会删除所有的表，然后重新运行所有的迁移&lt;/li&gt;
&lt;li&gt;重置数据库会删除所有的数据，所以在生产环境中不要使用这个命令&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;批准迁移&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;npx prisma migrate resolve
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果想让迁移生效，我们通常需要运行 npx prisma migrate deploy&lt;/li&gt;
&lt;li&gt;但是，如果我们想要在生产环境中运行迁移，但又不想运行 deploy 命令，我们可以使用 resolve 命令&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Prisma</category><author>ChanZhaoYu</author></item><item><title>Prisma 查询方法</title><link>https://www.redon.cc/posts/prisma%E6%9F%A5%E8%AF%A2%E6%96%B9%E6%B3%95/</link><guid isPermaLink="true">https://www.redon.cc/posts/prisma%E6%9F%A5%E8%AF%A2%E6%96%B9%E6%B3%95/</guid><description>总结 Prisma 查询方法</description><pubDate>Fri, 08 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://www.prisma.io/&quot;&gt;Prisma Site&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://playground.prisma.io/examples/reading/find/find-all?host=playground.prisma.io&amp;amp;path=examples&quot;&gt;Prisma Playground&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;findUnique&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;使用唯一条件来获取单个数据记录，如根据 id 查询&lt;/li&gt;
&lt;li&gt;支持关键字：where、select、include&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const result = await prisma.user.findUnique({
  where: {
    email: &quot;alice@prisma.io&quot;,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;findFirst&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;返回第一个匹配条件的记录&lt;/li&gt;
&lt;li&gt;支持关键字：select、include、rejectOnNotFound、where、orderBy、cursor、take、skip、distinct&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 获取 title 字段以 A test 开头的第一个 Post 记录，并反转列表（take）
async function main() {
  const a = await prisma.post.create({
    data: {
      title: &quot;A test 1&quot;,
    },
  });

  const b = await prisma.post.create({
    data: {
      title: &quot;A test 2&quot;,
    },
  });

  const c = await prisma.post.findFirst({
    where: {
      title: {
        startsWith: &quot;A test&quot;,
      },
    },
    orderBy: {
      title: &quot;asc&quot;,
    },
    take: -1, // 反转列表
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;findMany&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;返回多条记录&lt;/li&gt;
&lt;li&gt;支持关键字：select、include、where、orderBy、cursor、take、skip、distinct&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const user = await prisma.user.findMany({
  where: { name: &quot;Alice&quot; },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;create&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;创建一条新的数据库记录&lt;/li&gt;
&lt;li&gt;支持关键字：data、select、include&lt;/li&gt;
&lt;li&gt;Prisma Client 当前不支持在数据库级别批量插入，可手动通过循环实现&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const user = await prisma.user.findMany({
  where: { name: &quot;Alice&quot; },
});
const user = await prisma.user.create({
  data: { email: &quot;alice@prisma.io&quot; },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;update&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;更新数据&lt;/li&gt;
&lt;li&gt;支持关键字：data、where、select、include&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const user = await prisma.user.update({
  where: { id: 1 },
  data: { email: &quot;alice@prisma.io&quot; },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;upsert&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;更新现有、或创建新的数据库记录&lt;/li&gt;
&lt;li&gt;支持关键字：create、update、where、select、include&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 更新（如果存在）或创建一条 email 为 alice@prisma.io 的 User 记录

const user = await prisma.user.upsert({
  where: { id: 1 },
  update: { email: &quot;alice@prisma.io&quot; },
  create: { email: &quot;alice@prisma.io&quot; },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;delete&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;删除现有的数据库记录&lt;/li&gt;
&lt;li&gt;只支持根据 id ，或者 unique 属性进行删除&lt;/li&gt;
&lt;li&gt;支持关键字：where、select、include&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const user = await prisma.user.delete({
  where: { id: 1 },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;deleteMany&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;删除多条记录，可根据筛选条件批量删除&lt;/li&gt;
&lt;li&gt;支持关键字：where&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 删除所有 name 为 Alice 的 User 记录
const deletedUserCount = await prisma.user.deleteMany({
  where: { name: &quot;Alice&quot; },
});

// 删除所有User
const deletedUserCount = await prisma.user.deleteMany({});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;createMany&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;在一个事务中创建多个记录，并返回成功插入数&lt;/li&gt;
&lt;li&gt;支持关键字：data、skipDuplicates&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const users = await prisma.user.createMany({
  data: [
    { name: &quot;Sonali&quot;, email: &quot;sonali@prisma.io&quot; },
    { name: &quot;Alex&quot;, email: &quot;alex@prisma.io&quot; },
  ],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;updateMany&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;更新一批已存在的数据库记录，并返回更新的记录数&lt;/li&gt;
&lt;li&gt;支持关键字：data、where&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const updatedUserCount = await prisma.user.updateMany({
  where: { name: &quot;Alice&quot; },
  data: { name: &quot;ALICE&quot; },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;count&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;返回符合条件的数据计数&lt;/li&gt;
&lt;li&gt;支持关键字：where、cursor、skip、take、orderBy、distinct、select&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 查询所有记录总数，查询 name 字段非空的总数，查询 city 字段非空的总数
const c = await prisma.user.count({
  select: {
    _all: true,
    city: true,
    name: true,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;aggregate&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;支持关键字：where、orderBy、cursor、skip、take、distinct、&lt;em&gt;count、_avg、_sum、_min、&lt;/em&gt; max&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 返回所有 User 记录的 profileViews 的 _min、_max 和 _count
const minMaxAge = await prisma.user.aggregata({
  _count: {
    _all: true,
  },
  _max: {
    profileViews: true,
  },
  _min: {
    profileViews: true,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;groupBy&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;聚合操作&lt;/li&gt;
&lt;li&gt;支持关键字：where、orderBy、by、having、skip、take、_count、_avg、_sum、_min、_max&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 按平均 profileViews 大于 200 的 country/city 分组，并返回每组 profileViews 的 _sum
const groupUsers = await prisma.user.groupBy({
  by: [&quot;country&quot;, &quot;city&quot;],
  _count: {
    _all: true,
    city: true,
  },
  _sum: {
    profileViews: true,
  },
  orderBy: {
    country: &quot;desc&quot;,
  },
  having: {
    profileViews: {
      _avg: {
        gt: 200,
      },
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>Prisma</category><author>ChanZhaoYu</author></item><item><title>The Unbearable Lightness of Being</title><link>https://www.redon.cc/posts/the-unbearable-lightness-of-being/</link><guid isPermaLink="true">https://www.redon.cc/posts/the-unbearable-lightness-of-being/</guid><pubDate>Tue, 24 Jan 1984 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The idea of eternal return is a mysterious one, and Nietzsche has often perplexed other philosophers with it: to think that everything recurs as we once experienced it, and that the recurrence itself recurs ad infinitum! What does this mad myth signify?&lt;/p&gt;
&lt;p&gt;Putting it negatively, the myth of eternal return states that a life which disappears once and for all, which does not return, is like a shadow, without weight, dead in advance, and whether it was horrible, beautiful, or sublime, its horror, sublimity, and beauty mean nothing. We need take no more note of it than of a war between two African kingdoms in the fourteenth century, a war that altered nothing in the destiny of the world, even if a hundred thousand blacks perished in excruciating torment.&lt;/p&gt;
&lt;p&gt;Will the war between two African kingdoms in the fourteenth century itself be altered if it recurs again and again, in eternal return?&lt;/p&gt;
&lt;p&gt;It will: it will become a solid mass, permanently protuberant, its inanity irreparable.&lt;/p&gt;
&lt;p&gt;If the French Revolution were to recur eternally, French historians would be less proud of Robespierre. But because they deal with something that will not return, the bloody years of the Revolution have turned into mere words, theories, and discussions, have become lighter than feathers, frightening no one. There is an infinite difference between a Robespierre who occurs only once in history and a Robespierre who eternally returns, chopping off French heads.&lt;/p&gt;
&lt;p&gt;Let us therefore agree that the idea of eternal return implies a perspective from which things appear other than as we know them: they appear without the mitigating circumstance of their transitory nature. This mitigating circumstance prevents us from coming to a verdict. For how can we condemn something that is ephemeral, in transit?&lt;/p&gt;
&lt;p&gt;In the sunset of dissolution, everything is illuminated by the aura of nostalgia, even the guillotine.&lt;/p&gt;
&lt;p&gt;Not long ago, I caught myself experiencing a most incredible sensation. Leafing through a book on Hitler, I was touched by some of his portraits: they reminded me of my childhood. I grew up during the war; several members of my family perished in Hitler’s concentration camps; but what were their deaths compared with the memories of a lost period in my life, a period that would never return?&lt;/p&gt;
&lt;p&gt;This reconciliation with Hitler reveals the profound moral perversity of a world that rests essentially on the nonexistence of return, for in this world everything is pardoned in advance and therefore everything cynically permitted.&lt;/p&gt;
&lt;p&gt;If every second of our lives recurs an infinite number of times, we are nailed to eternity as Jesus Christ was nailed to the cross. It is a terrifying prospect. In the world of eternal return the weight of unbearable responsibility lies heavy on every move we make. That is why Nietzsche called the idea of eternal return the heaviest of burdens (das schwerste Gewicht).&lt;/p&gt;
&lt;p&gt;If eternal return is the heaviest of burdens, then our lives can stand out against it in all their splendid lightness.&lt;/p&gt;
&lt;p&gt;But is heaviness truly deplorable and lightness splendid?&lt;/p&gt;
&lt;p&gt;The heaviest of burdens crushes us, we sink beneath it, it pins us to the ground. But in the love poetry of every age, the woman longs to be weighed down by the man’s body.&lt;/p&gt;
&lt;p&gt;The heaviest of burdens is therefore simultaneously an image of life’s most intense fulfillment. The heavier the burden, the closer our lives come to the earth, the more real and truthful they become.&lt;/p&gt;
&lt;p&gt;Conversely, the absolute absence of a burden causes man to be lighter than air, to soar into the heights, take leave of the earth and his earthly being, and become only half real, his movements as free as they are insignificant.&lt;/p&gt;
&lt;p&gt;What then shall we choose? Weight or lightness?&lt;/p&gt;
&lt;p&gt;Parmenides posed this very question in the sixth century before Christ. He saw the world divided into pairs of opposites:&lt;/p&gt;
&lt;p&gt;light/darkness, fineness/coarseness, warmth/cold, being/non-being. One half of the opposition he called positive (light, fineness, warmth, being), the other negative. We might find this division into positive and negative poles childishly simple except for one difficulty: which one is positive, weight or lightness?&lt;/p&gt;
&lt;p&gt;Parmenides responded: lightness is positive, weight negative.Was he correct or not?&lt;/p&gt;
&lt;p&gt;That is the question. The only certainty is: the lightness/weight opposition is the most mysterious, most ambiguous of all.&lt;/p&gt;
&lt;p&gt;I have been thinking about Tomas for many years. But only in the light of these reflections did I see him clearly. I saw him standing at the window of his flat and looking across the courtyard at the opposite walls, not knowing what to do.&lt;/p&gt;
&lt;p&gt;He had first met Tereza about three weeks earlier in a small Czech town. They had spent scarcely an hour together. She had accompanied him to the station and waited with him until he boarded the train. Ten days later she paid him a visit. They made love the day she arrived. That night she came down with a fever and stayed a whole week in his flat with the flu.&lt;/p&gt;
&lt;p&gt;He had come to feel an inexplicable love for this all but complete stranger; she seemed a child to him, a child someone had put in a bulrush basket daubed with pitch and sent downstream for Tomas to fetch at the riverbank of his bed.&lt;/p&gt;
&lt;p&gt;She stayed with him a week, until she was well again, then went back to her town, some hundred and twenty-five miles from Prague. And then came the time I have just spoken of and see as the key to his life: Standing by the window, he looked out over the courtyard at the walls opposite him and deliberated.&lt;/p&gt;
&lt;p&gt;Should he call her back to Prague for good? He feared the responsibility. If he invited her to come, then come she would, and offer him up her life.&lt;/p&gt;
&lt;p&gt;Or should he refrain from approaching her? Then she would remain a waitress in a hotel restaurant of a provincial town and he would never see her again.&lt;/p&gt;
&lt;p&gt;Did he want her to come or did he not?&lt;/p&gt;
&lt;p&gt;He looked out over the courtyard at the opposite walls, seeking an answer.&lt;/p&gt;
&lt;p&gt;He kept recalling her lying on his bed; she reminded him of no one in his former life.&lt;/p&gt;
&lt;p&gt;She was neither mistress nor wife. She was a child whom he had taken from a bulrush basket that had been daubed with pitch and sent to the riverbank of his bed. She fell asleep. He knelt down next to her. Her feverous breath quickened and she gave out a weak moan. He pressed his face to hers and whispered calming words into her sleep.&lt;/p&gt;
&lt;p&gt;After a while he felt her breath return to normal and her face rise unconsciously to meet his. He smelled the delicate aroma of her fever and breathed it in, as if trying to glut himself with the intimacy of her body. And all at once he fancied she had been with him for many years and was dying. He had a sudden clear feeling that he would not survive her death. He would lie down beside her and want to die with her. He pressed his face into the pillow beside her head and kept it there for a long time.&lt;/p&gt;
&lt;p&gt;Now he was standing at the window trying to call that moment to account. What could it have been if not love declaring itself to him?&lt;/p&gt;
&lt;p&gt;But was it love? The feeling of wanting to die beside her was clearly exaggerated: he had seen her only once before in his life! Was it simply the hysteria of a man who, aware deep down of his inaptitude for love, felt the self-deluding need to simulate it?&lt;/p&gt;
&lt;p&gt;His unconscious was so cowardly that the best partner it could choose for its little comedy was this miserable provincial waitress with practically no chance at all to enter his life!&lt;/p&gt;
&lt;p&gt;Looking out over the courtyard at the dirty walls, he realized he had no idea whether it was hysteria or love.&lt;/p&gt;
&lt;p&gt;And he was distressed that in a situation where a real man would instantly have known how to act, he was vacillating and therefore depriving the most beautiful moments he had ever experienced (kneeling at her bed and thinking he would not survive her death) of their meaning.&lt;/p&gt;
&lt;p&gt;He remained annoyed with himself until he realized that not knowing what he wanted was actually quite natural.&lt;/p&gt;
&lt;p&gt;We can never know what to want, because, living only one life, we can neither compare it with our previous lives nor perfect it in our lives to come.&lt;/p&gt;
&lt;p&gt;Was it better to be with Tereza or to remain alone?&lt;/p&gt;
&lt;p&gt;There is no means of testing which decision is better, because there is no basis for comparison. We live everything as it comes, without warning, like an actor going on cold. And what can life be worth if the first rehearsal for life is life itself? That is why life is always like a sketch. No, sketch is not quite the word, because a sketch is an outline of something, the groundwork for a picture, whereas the sketch that is our life is a sketch for nothing, an outline with no picture.&lt;/p&gt;
&lt;p&gt;Einmal ist keinmal, says Tomas to himself. What happens but once, says the German adage, might as well not have happened at all. If we have only one life to live,we might as well not have lived at all.&lt;/p&gt;
&lt;p&gt;But then one day at the hospital, during a break between operations, a nurse called him to the telephone. He heard Tereza’s voice coming from the receiver. She had phoned him from the railway station. He was overjoyed. Unfortunately, he had something on that evening and could not invite her to his place until the next day. The moment he hung up, he reproached himself for not telling her to go straight there. He had time enough to cancel his plans, after all! He tried to imagine what Tereza would do in Prague during the thirty-six long hours before they were to meet, and had half a mind to jump into his car and drive through the streets looking for her.&lt;/p&gt;
&lt;p&gt;She arrived the next evening, a handbag dangling from her shoulder, looking more elegant than before. She had a thick book under her arm. It was Anna Karenina. She seemed in a good mood, even a little boisterous, and tried to make him think she had just happened to drop in, things had just worked out that way: she was in Prague on business, perhaps (at this point she became rather vague) to find a job.&lt;/p&gt;
&lt;p&gt;Later, as they lay naked and spent side by side on the bed, he asked her where she was staying. It was night by then, and he offered to drive her there. Embarrassed, she answered that she still had to find a hotel and had left her suitcase at the station.&lt;/p&gt;
&lt;p&gt;Only two days ago, he had feared that if he invited her to Prague she would offer him up her life. When she told him her suitcase was at the station, he immediately realized that the suitcase contained her life and that she had left it at the station only until she could offer it up to him.&lt;/p&gt;
&lt;p&gt;The two of them got into his car, which was parked in front of the house, and drove to the station. There he claimed the suitcase (it was large and enormously heavy) and took it and her home.&lt;/p&gt;
&lt;p&gt;How had he come to make such a sudden decision when for nearly a fortnight he had wavered so much that he could not even bring himself to send a postcard asking her how she was?&lt;/p&gt;
&lt;p&gt;He himself was surprised. He had acted against his principles. Ten years earlier, when he had divorced his wife, he celebrated the event the way others celebrate a marriage.&lt;/p&gt;
&lt;p&gt;He understood he was not born to live side by side with any woman and could be fully himself only as a bachelor. He tried to design his life in such a way that no woman could move in with a suitcase. That was why his flat had only the one bed. Even though it was wide enough, Tomas would tell his mistresses that he was unable to fall asleep with anyone next to him, and drive them home after midnight. And so it was not the flu that kept him from sleeping with Tereza on her first visit. The first night he had slept in his large armchair, and the rest of that week he drove each night to the hospital, where he had a cot in his office.&lt;/p&gt;
&lt;p&gt;But this time he fell asleep by her side. When he woke up the next morning, he found Tereza, who was still asleep, holding his hand. Could they have been hand in hand all night? It was hard to believe.&lt;/p&gt;
&lt;p&gt;And while she breathed the deep breath of sleep and held his hand (firmly: he was unable to disengage it from her grip), the enormously heavy suitcase stood by the bed.&lt;/p&gt;
&lt;p&gt;He refrained from loosening his hand from her grip for fear of waking her, and turned carefully on his side to observe her better.&lt;/p&gt;
&lt;p&gt;Again it occurred to him that Tereza was a child put in a pitch-daubed bulrush basket and sent downstream. He couldn’t very well let a basket with a child in it float down a stormy river! If the Pharaoh’s daughter hadn’t snatched the basket carrying little Moses from the waves, there would have been no Old Testament, no civilization as we now know it! How many ancient myths begin with the rescue of an abandoned child! If Polybus hadn’t taken in the young Oedipus, Sophocles wouldn’t have written his most beautiful tragedy!&lt;/p&gt;
&lt;p&gt;Tomas did not realize at the time that metaphors are dangerous. Metaphors are not to be trifled with. A single metaphor can give birth to love.&lt;/p&gt;
&lt;p&gt;He lived a scant two years with his wife, and they had a son. At the divorce proceedings, the judge awarded the infant to its mother and ordered Tomas to pay a third of his salary for its support. He also granted him the right to visit the boy every other week.&lt;/p&gt;
&lt;p&gt;But each time Tomas was supposed to see him, the boy’s mother found an excuse to keep him away. He soon realized that bringing them expensive gifts would make things a good deal easier, that he was expected to bribe the mother for the son’s love. He saw a future of quixotic attempts to inculcate his views in the boy, views opposed in every way to the mother’s. The very thought of it exhausted him. When, one Sunday, the boy’s mother again canceled a scheduled visit, Tomas decided on the spur of the moment never to see him again.&lt;/p&gt;
&lt;p&gt;Why should he feel more for that child, to whom he was bound by nothing but a single improvident night, than for any other? He would be scrupulous about paying support; he just didn’t want anybody making him fight for his son in the name of paternal sentiments!&lt;/p&gt;
&lt;p&gt;Needless to say, he found no sympathizers. His own parents condemned him roundly: if Tomas refused to take an interest in his son, then they, Tomas’s parents, would no longer take an interest in theirs. They made a great show of maintaining good relations with their daughter-in-law and trumpeted their exemplary stance and sense of justice.&lt;/p&gt;
&lt;p&gt;Thus in practically no time he managed to rid himself of wife, son, mother, and father.&lt;/p&gt;
&lt;p&gt;The only thing they bequeathed to him was a fear of women. Tomas desired but feared them. Needing to create a compromise between fear and desire, he devised what he called erotic friendship. He would tell his mistresses: the only relationship that can make both partners happy is one in which sentimentality has no place and neither partner makes any claim on the life and freedom of the other.&lt;/p&gt;
&lt;p&gt;To ensure that erotic friendship never grew into the aggression of love, he would meet each of his long-term mistresses only at intervals. He considered this method flawless and propagated it among his friends: The important thing is to abide by the rule of threes. Either you see a woman three times in quick succession and then never again, or you maintain relations over the years but make sure that the rendezvous are at least three weeks apart.&lt;/p&gt;
&lt;p&gt;The rule of threes enabled Tomas to keep intact his liaisons with some women while continuing to engage in short-term affairs with many others. He was not always understood. The woman who understood him best was Sabina. She was a painter. The reason I like you, she would say to him, is you’re the complete opposite of kitsch. In the kingdom of kitsch you would be a monster.&lt;/p&gt;
&lt;p&gt;It was Sabina he turned to when he needed to find a job for Tereza in Prague.&lt;/p&gt;
&lt;p&gt;Following the unwritten rules of erotic friendship, Sabina promised to do everything in her power, and before long she had in fact located a place for Tereza in the darkroom of an illustrated weekly. Although her new job did not require any particular qualifications, it raised her status from waitress to member of the press. When Sabina herself introduced Tereza to everyone on the weekly, Tomas knew he had never had a better friend as a mistress than Sabina.&lt;/p&gt;
&lt;p&gt;The unwritten contract of erotic friendship stipulated that Tomas should exclude all love from his life. The moment he violated that clause of the contract, his other mistresses would assume inferior status and become ripe for insurrection.&lt;/p&gt;
&lt;p&gt;Accordingly, he rented a room for Tereza and her heavy suitcase. He wanted to be able to watch over her, protect her, enjoy her presence, but felt no need to change his way of life. He did not want word to get out that Tereza was sleeping at his place: spending the night together was the corpus delicti of love.&lt;/p&gt;
&lt;p&gt;He never spent the night with the others. It was easy enough if he was at their place: he could leave whenever he pleased. It was worse when they were at his and he had to explain that come midnight he would have to drive them home because he was an insomniac and found it impossible to fall asleep in close proximity to another person.&lt;/p&gt;
&lt;p&gt;Though it was not far from the truth, he never dared tell them the whole truth: after making love he had an uncontrollable craving to be by himself; waking in the middle of the night at the side of an alien body was distasteful to him, rising in the morning with an intruder repellent; he had no desire to be overheard brushing his teeth in the bathroom, nor was he enticed by the thought of an intimate breakfast.&lt;/p&gt;
&lt;p&gt;That is why he was so surprised to wake up and find Tereza squeezing his hand tightly.&lt;/p&gt;
&lt;p&gt;Lying there looking at her, he could not quite understand what had happened. But as he ran through the previous few hours in his mind, he began to sense an aura of hitherto unknown happiness emanating from them.&lt;/p&gt;
&lt;p&gt;From that time on they both looked forward to sleeping together. I might even say that the goal of their lovemaking was not so much pleasure as the sleep that followed it. She especially was affected. Whenever she stayed overnight in her rented room (which quickly became only an alibi for Tomas), she was unable to fall asleep; in his arms she would fall asleep no matter how wrought up she might have been. He would whisper impromptu fairy tales about her, or gibberish, words he repeated monotonously, words soothing or comical, which turned into vague visions lulling her through the first dreams of the night. He had complete control over her sleep: she dozed off at the second he chose.&lt;/p&gt;
&lt;p&gt;While they slept, she held him as on the first night, keeping a firm grip on wrist, finger, or ankle. If he wanted to move without waking her, he had to resort to artifice. After freeing his finger (wrist, ankle) from her clutches, a process which, since she guarded him carefully even in her sleep, never failed to rouse her partially, he would calm her by slipping an object into her hand (a rolled-up pajama top, a slipper, a book), which she then gripped as tightly as if it were a part of his body.&lt;/p&gt;
&lt;p&gt;Once, when he had just lulled her to sleep but she had gone no farther than dream’s antechamber and was therefore still responsive to him, he said to her, Good-bye, I’m going now. Where? she asked in her sleep. Away, he answered sternly. Then I’m going with you, she said, sitting up in bed. No, you can’t. I’m going away for good, he said, going out into the hall. She stood up and followed him out, squinting. She was naked beneath her short nightdress. Her face was blank, expressionless, but she moved energetically. He walked through the hall of the flat into the hall of the building (the hall shared by all the occupants), closing the door in her face. She flung it open and continued to follow him, convinced in her sleep that he meant to leave her for good and she had to stop him. He walked down the stairs to the first landing and waited for her there. She went down after him, took him by the hand, and led him back to bed.&lt;/p&gt;
&lt;p&gt;Tomas came to this conclusion: Making love with a woman and sleeping with a woman are two separate passions, not merely different but opposite. Love does not make itself felt in the desire for copulation (a desire that extends to an infinite number of women) but in the desire for shared sleep (a desire limited to one woman).&lt;/p&gt;
&lt;p&gt;In the middle of the night she started moaning in her sleep. Tomas woke her up, but when she saw his face she said, with hatred in her voice, Get away from me! Get away from me! Then she told him her dream: The two of them and Sabina had been in a big room together. There was a bed in the middle of the room. It was like a platform in the theater. Tomas ordered her to stand in the corner while he made love to Sabina. The sight of it caused Tereza intolerable suffering. Hoping to alleviate the pain in her heart by pains of the flesh, she jabbed needles under her fingernails. It hurt so much, she said, squeezing her hands into fists as if they actually were wounded.&lt;/p&gt;
&lt;p&gt;He pressed her to him, and she gradually (trembling violently for a long time) fell asleep in his arms.&lt;/p&gt;
&lt;p&gt;Thinking about the dream the next day, he remembered something. He opened a desk drawer and took out a packet of letters Sabina had written to him. He was not long in finding the following passage: I want to make love to you in my studio. It will be like a stage surrounded by people. The audience won’t be allowed up close, but they won’t be able to take their eyes off us….&lt;/p&gt;
&lt;p&gt;The worst of it was that the letter was dated. It was quite recent, written long after Tereza had moved in with Tomas.&lt;/p&gt;
&lt;p&gt;So you’ve been rummaging in my letters!&lt;/p&gt;
&lt;p&gt;She did not deny it. Throw me out, then!&lt;/p&gt;
&lt;p&gt;But he did not throw her out. He could picture her pressed against the wall of Sabina’s studio jabbing needles up under her nails. He took her fingers between his hands and stroked them, brought them to his lips and kissed them, as if they still had drops of blood on them.&lt;/p&gt;
&lt;p&gt;But from that time on, everything seemed to conspire against him. Not a day went by without her learning something about his secret life.&lt;/p&gt;
&lt;p&gt;At first he denied it all. Then, when the evidence became too blatant, he argued that his polygamous way of life did not in the least run counter to his love for her. He was inconsistent: first he disavowed his infidelities, then he tried to justify them.&lt;/p&gt;
&lt;p&gt;Once he was saying good-bye after making a date with a woman on the phone, when from the next room came a strange sound like the chattering of teeth.By chance she had come home without his realizing it. She was pouring something from a medicine bottle down her throat, and her hand shook so badly the glass bottle clicked against her teeth.&lt;/p&gt;
&lt;p&gt;He pounced on her as if trying to save her from drowning. The bottle fell to the floor, spotting the carpet with valerian drops. She put up a good fight, and he had to keep her in a straitjacket-like hold for a quarter of an hour before he could calm her.&lt;/p&gt;
&lt;p&gt;He knew he was in an unjustifiable situation, based as it was on complete inequality.&lt;/p&gt;
&lt;p&gt;One evening, before she discovered his correspondence with Sabina, they had gone to a bar with some friends to celebrate Tereza’s new job. She had been promoted at the weekly from darkroom technician to staff photographer. Because he had never been much for dancing, one of his younger colleagues took over. They made a splendid couple on the dance floor, and Tomas found her more beautiful than ever. He looked on in amazement at the split-second precision and deference with which Tereza anticipated her partner’s will. The dance seemed to him a declaration that her devotion, her ardent desire to satisfy his every whim, was not necessarily bound to his person, that if she hadn’t met Tomas, she would have been ready to respond to the call of any other man she might have met instead. He had no difficulty imagining Tereza and his young colleague as lovers. And the ease with which he arrived at this fiction wounded him. He realized that Tereza’s body was perfectly thinkable coupled with any male body, and the thought put him in a foul mood. Not until late that night, at home, did he admit to her he was jealous.&lt;/p&gt;
&lt;p&gt;This absurd jealousy, grounded as it was in mere hypotheses, proved that he considered her fidelity an unconditional postulate of their relationship. How then could he begrudge her her jealousy of his very real mistresses?&lt;/p&gt;
&lt;p&gt;During the day, she tried (though with only partial success) to believe what Tomas told her and to be as cheerful as she had been before. But her jealousy thus tamed by day burst forth all the more savagely in her dreams, each of which ended in a wail he could silence only by waking her.&lt;/p&gt;
&lt;p&gt;Her dreams recurred like themes and variations or television series. For example, she repeatedly dreamed of cats jumping at her face and digging their claws into her skin.&lt;/p&gt;
&lt;p&gt;We need not look far for an interpretation: in Czech slang the word cat means a pretty woman. Tereza saw herself threatened by women, all women. All women were potential mistresses for Tomas, and she feared them all.&lt;/p&gt;
&lt;p&gt;In another cycle she was being sent to her death. Once, when he woke her as she screamed in terror in the dead of night, she told him about it. I was at a large indoor swimming pool. There were about twenty of us. All women. We were naked and had to march around the pool. There was a basket hanging from the ceiling and a man standing in the basket. The man wore a broad-brimmed hat shading his face, but I could see it was you. You kept giving us orders. Shouting at us. We had to sing as we marched, sing and do kneebends. If one of us did a bad kneebend, you would shoot her with a pistol and she would fall dead into the pool. Which made everybody laugh and sing even louder. You never took your eyes off us, and the minute we did something wrong, you would shoot. The pool was full of corpses floating just below the surface. And I knew I lacked the strength to do the next kneebend and you were going to shoot me!&lt;/p&gt;
&lt;p&gt;In a third cycle she was dead.&lt;/p&gt;
&lt;p&gt;bying in a hearse as big as a furniture van, she was surrounded by dead women. There were so many of them that the back door would not close and several legs dangled out.&lt;/p&gt;
&lt;p&gt;But I’m not dead! Tereza cried. I can still feel!&lt;/p&gt;
&lt;p&gt;So can we, the corpses laughed.&lt;/p&gt;
&lt;p&gt;They laughed the same laugh as the live women who used to tell her cheerfully it was perfectly normal that one day she would have bad teeth, faulty ovaries, and wrinkles, because they all had bad teeth, faulty ovaries, and wrinkles. Laughing the same laugh, they told her that she was dead and it was perfectly all right!&lt;/p&gt;
&lt;p&gt;Suddenly she felt a need to urinate. You see, she cried. I need to pee. That’s proof positive I’m not dead!&lt;/p&gt;
&lt;p&gt;But they only laughed again. Needing to pee is perfectly normal! they said. You’ll go on feeling that kind of thing for a long time yet. Like a person who has an arm cut off and keeps feeling it’s there. We may not have a drop of pee left in us, but we keep needing to pee.&lt;/p&gt;
&lt;p&gt;Tereza huddled against Tomas in bed. And the way they talked to me! Like old friends, people who’d known me forever. I was appalled at the thought of having to stay with them forever.&lt;/p&gt;
&lt;p&gt;All languages that derive from Latin form the word compassion by combining the prefix meaning with (corn-) and the root meaning suffering (Late Latin, passio). In other languages—Czech, Polish, German, and Swedish, for instance— this word is translated by a noun formed of an equivalent prefix combined with the word that means feeling (Czech, sou-cit; Polish, wspol-czucie; German, Mit-gefuhl; Swedish, med-kansia).&lt;/p&gt;
&lt;p&gt;In languages that derive from Latin, compassion means: we cannot look on coolly as others suffer; or, we sympathize with those who suffer. Another word with approximately the same meaning, pity (French, pitie; Italian, pieta; etc.), connotes a certain condescension towards the sufferer. To take pity on a woman means that we are better off than she, that we stoop to her level, lower ourselves.&lt;/p&gt;
&lt;p&gt;That is why the word compassion generally inspires suspicion; it designates what is considered an inferior, second-rate sentiment that has little to do with love. To love someone out of compassion means not really to love.&lt;/p&gt;
&lt;p&gt;In languages that form the word compassion not from the root suffering but from the root feeling, the word is used in approximately the same way, but to contend that it designates a bad or inferior sentiment is difficult. The secret strength of its etymology floods the word with another light and gives it a broader meaning: to have compassion (co-feeling) means not only to be able to live with the other’s misfortune but also to feel with him any emotion—joy, anxiety, happiness, pain. This kind of compassion (in the sense of souc/r, wspofczucie, Mitgefuhl, medkansia) therefore signifies the maximal capacity of affective imagination, the art of emotional telepathy. In the hierarchy of sentiments, then, it is supreme.&lt;/p&gt;
&lt;p&gt;By revealing to Tomas her dream about jabbing needles under her fingernails, Tereza unwittingly revealed that she had gone through his desk. If Tereza had been any other&lt;/p&gt;
&lt;p&gt;woman, Tomas would never have spoken to her again. Aware of that, Tereza said to him, Throw me out! But instead of throwing her out, he seized her hand and kissed the tips of her fingers, because at that moment he himself felt the pain under her fingernails as surely as if the nerves of her fingers led straight to his own brain.&lt;/p&gt;
&lt;p&gt;Anyone who has failed to benefit from the Devil’s gift of compassion (co-feeling) will condemn Tereza coldly for her deed, because privacy is sacred and drawers containing intimate correspondence are not to be opened. But because compassion was Tomas’s fate (or curse), he felt that he himself had knelt before the open desk drawer, unable to tear his eyes from Sabina’s letter. He understood Tereza, and not only was he incapable of being angry with her, he loved her all the more.&lt;/p&gt;
&lt;p&gt;Her gestures grew abrupt and unsteady. Two years had elapsed since she discovered he was unfaithful, and things had grown worse. There was no way out.&lt;/p&gt;
&lt;p&gt;Was he genuinely incapable of abandoning his erotic friendships? He was. It would have torn him apart. He lacked the strength to control his taste for other women.&lt;/p&gt;
&lt;p&gt;Besides, he failed to see the need. No one knew better than he how little his exploits threatened Tereza. Why then give them up? He saw no more reason for that than to deny himself soccer matches.&lt;/p&gt;
&lt;p&gt;But was it still a matter of pleasure? Even as he set out to visit another woman, he found her distasteful and promised himself he would not see her again. He constantly had Tereza’s image before his eyes, and the only way he could erase it was by quickly getting drunk. Ever since meeting Tereza, he had been unable to make love to other women without alcohol! But alcohol on his breath was a sure sign to Tereza of infidelity.&lt;/p&gt;
&lt;p&gt;He was caught in a trap: even on his way to see them, he found them distasteful, but one day without them and he was back on the phone, eager to make contact.&lt;/p&gt;
&lt;p&gt;He still felt most comfortable with Sabina. He knew she was discreet and would not divulge their rendezvous. Her studio greeted him like a memento of his past, his idyllic bachelor past.&lt;/p&gt;
&lt;p&gt;Perhaps he himself did not realize how much he had changed: he was now afraid to come home late, because Tereza would be waiting up for him. Then one day Sabina caught him glancing at his watch during intercourse and trying to hasten its conclusion.&lt;/p&gt;
&lt;p&gt;Afterwards, still naked and lazily walking across the studio, she stopped before an easel with a half-finished painting and watched him sidelong as he threw on his clothes.&lt;/p&gt;
&lt;p&gt;When he was fully dressed except for one bare foot, he looked around the room, and then got down on all fours to continue the search under a table.&lt;/p&gt;
&lt;p&gt;You seem to be turning into the theme of all my paintings, she said. The meeting of two worlds. A double exposure. Showing through the outline of Tomas the libertine, incredibly, the face of a romantic lover. Or, the other way, through a Tristan, always thinking of his Tereza, I see the beautiful, betrayed world of the libertine.&lt;/p&gt;
&lt;p&gt;Tomas straightened up and, distractedly, listened to Sabina’s words.&lt;/p&gt;
&lt;p&gt;What are you looking for? she asked.&lt;/p&gt;
&lt;p&gt;A sock.&lt;/p&gt;
&lt;p&gt;She searched all over the room with him, and again he got down on all fours to look under the table.&lt;/p&gt;
&lt;p&gt;Your sock isn’t anywhere to be seen, said Sabina. You must have come without it.&lt;/p&gt;
&lt;p&gt;How could I have come without it? cried Tomas, looking at his watch. I wasn’t wearing only one sock when I came, was I?&lt;/p&gt;
&lt;p&gt;It’s not out of the question. You’ve been very absent-minded lately. Always rushing somewhere, looking at your watch. It wouldn’t surprise me in the least if you forgot to put on a sock.&lt;/p&gt;
&lt;p&gt;He was just about to put his shoe on his bare foot. It’s cold out, Sabina said. I’ll lend you one of my stockings.&lt;/p&gt;
&lt;p&gt;She handed him a long, white, fashionable, wide-net stocking.&lt;/p&gt;
&lt;p&gt;He knew very well she was getting back at him for glancing at his watch while making love to her. She had hidden his sock somewhere. It was indeed cold out, and he had no choice but to take her up on the offer. He went home wearing a sock on one foot and a wide-net stocking rolled down over his ankle on the other.&lt;/p&gt;
&lt;p&gt;He was in a bind: in his mistresses’ eyes, he bore the stigma of his love for Tereza; in Tereza’s eyes, the stigma of his exploits with the mistresses.&lt;/p&gt;
&lt;p&gt;To assuage Tereza’s sufferings, he married her (they could finally give up the room, which she had not lived in for quite some time) and gave her a puppy.&lt;/p&gt;
&lt;p&gt;It was born to a Saint Bernard owned by a colleague. The sire was a neighbor’s German shepherd. No one wanted the little mongrels, and his colleague was loath to kill them.&lt;/p&gt;
&lt;p&gt;Looking over the puppies, Tomas knew that the ones he rejected would have to die. He felt like the president of the republic standing before four prisoners condemned to death and empowered to pardon only one of them. At last he made his choice: a bitch whose body seemed reminiscent of the German shepherd and whose head belonged to its Saint Bernard mother. He took it home to Tereza, who picked it up and pressed it to her breast. The puppy immediately peed on her blouse.&lt;/p&gt;
&lt;p&gt;Then they tried to come up with a name for it. Tomas wanted the name to be a clear indication that the dog was Tereza’s, and he thought of the book she was clutching under her arm when she arrived unannounced in Prague. He suggested they call the puppy Tolstoy.&lt;/p&gt;
&lt;p&gt;It can’t be Tolstoy, Tereza said. It’s a girl. How about Anna Karenina?&lt;/p&gt;
&lt;p&gt;It can’t be Anna Karenina, said Tomas. No woman could possibly have so funny a face.&lt;/p&gt;
&lt;p&gt;It’s much more like Karenin. Yes, Anna’s husband. That’s just how I’ve always pictured him.&lt;/p&gt;
&lt;p&gt;But won’t calling her Karenin affect her sexuality?&lt;/p&gt;
&lt;p&gt;It is entirely possible, said Tomas, that a female dog addressed continually by a male name will develop lesbian tendencies.&lt;/p&gt;
&lt;p&gt;Strangely enough, Tomas’s words came true. Though bitches are usually more affectionate to their masters than to their mistresses, Karenin proved an exception, deciding that he was in love with Tereza. Tomas was grateful to him for it. He would stroke the puppy’s head and say, Well done, Karenin! That’s just what I wanted you for.&lt;/p&gt;
&lt;p&gt;Since I can’t cope with her by myself, you must help me.&lt;/p&gt;
&lt;p&gt;But even with Karenin’s help Tomas failed to make her happy. He became aware of his failure some years later, on approximately the tenth day after his country was occupied by Russian tanks. It was August 1968, and Tomas was receiving daily phone calls from a hospital in Zurich. The director there, a physician who had struck up a friendship with Tomas at an international conference, was worried about him and kept offering him a job.&lt;/p&gt;
&lt;p&gt;If Tomas rejected the Swiss doctor’s offer without a second thought, it was for Tereza’s sake. He assumed she would not want to leave. She had spent the whole first week of the occupation in a kind of trance almost resembling happiness. After roaming the streets with her camera, she would hand the rolls of film to foreign journalists, who actually fought over them. Once, when she went too far and took a close-up of an officer pointing his revolver at a group of people, she was arrested and kept overnight at Russian military headquarters. There they threatened to shoot her, but no sooner did they let her go than she was back in the streets with her camera.&lt;/p&gt;
&lt;p&gt;That is why Tomas was surprised when on the tenth day of the occupation she said to him, Why is it you don’t want to go to Switzerland? ‘&lt;/p&gt;
&lt;p&gt;Why should I?&lt;/p&gt;
&lt;p&gt;They could make it hard for you here.&lt;/p&gt;
&lt;p&gt;They can make it hard for anybody, replied Tomas with a wave of the hand. What about you? Could you live abroad?&lt;/p&gt;
&lt;p&gt;Why not?&lt;/p&gt;
&lt;p&gt;You’ve been out there risking your life for this country. How can you be so nonchalant about leaving it?&lt;/p&gt;
&lt;p&gt;Now that Dubcek is back, things have changed, said Tereza.&lt;/p&gt;
&lt;p&gt;It was true: the general euphoria lasted no longer than the first week. The representatives of the country had been hauled away like criminals by the Russian army, no one knew where they were, everyone feared for the men’s lives, and hatred for the Russians drugged people like alcohol. It was a drunken carnival of hate. Czech towns were decorated with thousands of hand-painted posters bearing ironic texts, epigrams, poems, and cartoons of Brezhnev and his soldiers, jeered at by one and all as a circus of illiterates. But no carnival can go on forever. In the meantime, the Russians had forced the Czech representatives to sign a compromise agreement in Moscow. When Dubcek returned with them to Prague, he gave a speech over the radio.&lt;/p&gt;
&lt;p&gt;He was so devastated after his six-day detention he could hardly talk; he kept stuttering and gasping for breath, making long pauses between sentences, pauses lasting nearly thirty seconds.&lt;/p&gt;
&lt;p&gt;The compromise saved the country from the worst: the executions and mass deportations to Siberia that had terrified everyone. But one thing was clear: the country would have to bow to the conqueror. For ever and ever, it will stutter, stammer, gasp for air like Alexander Dubcek. The carnival was over. Workaday humiliation had begun.&lt;/p&gt;
&lt;p&gt;Tereza had explained all this to Tomas and he knew that it was true. But he also knew that underneath it all hid still another, more fundamental truth, the reason why she wanted to leave Prague: she had never really been happy before.&lt;/p&gt;
&lt;p&gt;The days she walked through the streets of Prague taking pictures of Russian soldiers and looking danger in the face were the best of her life. They were the only time when the television series of her dreams had been interrupted and she had enjoyed a few happy nights. The Russians had brought equilibrium to her in their tanks, and now that the carnival was over, she feared her nights again and wanted to escape them. She now knew there were conditions under which she could feel strong and fulfilled, and she longed to go off into the world and seek those conditions somewhere else.&lt;/p&gt;
&lt;p&gt;It doesn’t bother you that Sabina has also emigrated to Switzerland? Tomas asked.&lt;/p&gt;
&lt;p&gt;Geneva isn’t Zurich, said Tereza. She’ll be much less of a difficulty there than she was in Prague.&lt;/p&gt;
&lt;p&gt;A person who longs to leave the place where he lives is an unhappy person. That is why Tomas accepted Tereza’s wish to emigrate as the culprit accepts his sentence, and one day he and Tereza and Karenin found themselves in the largest city in Switzerland.&lt;/p&gt;
&lt;p&gt;He bought a bed for their empty flat (they had no money yet for other furniture) and threw himself into his work with the frenzy of a man of forty beginning a new life.&lt;/p&gt;
&lt;p&gt;He made several telephone calls to Geneva. A show of Sabina’s work had opened there by chance a week after the Russian invasion, and in a wave of sympathy for her tiny country, Geneva’s patrons of the arts bought up all her paintings.&lt;/p&gt;
&lt;p&gt;Thanks to the Russians, I’m a rich woman, she said, laughing into the telephone. She invited Tomas to come and see her new studio, and assured him it did not differ greatly from the one he had known in Prague.&lt;/p&gt;
&lt;p&gt;He would have been only too glad to visit her, but was unable to find an excuse to explain his absence to Tereza. And so Sabina came to Zurich. She stayed at a hotel.&lt;/p&gt;
&lt;p&gt;Tomas went to see her after work. He phoned first from the reception desk, then went upstairs. When she opened the door, she stood before him on her beautiful long legs wearing nothing but panties and bra. And a black bowler hat. She stood there staring, mute and motionless. Tomas did the same. Suddenly he realized how touched he was.&lt;/p&gt;
&lt;p&gt;He removed the bowler from her head and placed it on the bedside table. Then they made love without saying a word.&lt;/p&gt;
&lt;p&gt;Leaving the hotel for his Hat (which by now had acquired table, chairs, couch, and carpet), he thought happily that he carried his way of living with him as a snail carries his house. Tereza and Sabina represented the two poles of his life, separate and irreconcilable, yet equally appealing.&lt;/p&gt;
&lt;p&gt;But the fact that he carried his life-support system with him everywhere like a part of his body meant that Tereza went on having her dreams.&lt;/p&gt;
&lt;p&gt;They had been in Zurich for six or seven months when he came home late one evening to find a letter on the table telling him she had left for Prague. She had left because she lacked the strength to live abroad. She knew she was supposed to bolster him up, but did not know how to go about it. She had been silly enough to think that going abroad would change her. She thought that after what she had been through during the invasion she would stop being petty and grow up, grow wise and strong, but she had overestimated herself. She was weighing him down and would do so no longer. She had drawn the necessary conclusions before it was too late. And she apologized for taking Karenin with her.&lt;/p&gt;
&lt;p&gt;He took some sleeping pills but still did not close his eyes until morning. Luckily it was Saturday and he could stay at home. For the hundred and fiftieth time he went over the situation: the borders between his country and the rest of the world were no longer open. No telegrams or telephone calls could bring her back. The authorities would never let her travel abroad. Her departure was staggeringly definitive.&lt;/p&gt;
&lt;p&gt;The realization that he was utterly powerless was like the blow of a sledgehammer, yet it was curiously calming as well. No one was forcing him into a decision. He felt no need to stare at the walls of the houses across the courtyard and ponder whether to live with her or not. Tereza had made the decision herself.&lt;/p&gt;
&lt;p&gt;He went to a restaurant for lunch. He was depressed, but as he ate, his original desperation waned, lost its strength, and soon all that was left was melancholy. Looking back on the years he had spent with her, he came to feel that their story could have had no better ending. If someone had invented the story, this is how he would have had to end it.&lt;/p&gt;
&lt;p&gt;One day Tereza came to him uninvited. One day she left the same way. She came with a heavy suitcase. She left with a heavy suitcase.&lt;/p&gt;
&lt;p&gt;He paid the bill, left the restaurant, and started walking through the streets, his melancholy growing more and more beautiful. He had spent seven years of life with Tereza, and now he realized that those years were more attractive in retrospect than they were when he was living them.&lt;/p&gt;
&lt;p&gt;His love for Tereza was beautiful, but it was also tiring: he had constantly had to hide things from her, sham, dissemble, make amends, buck her up, calm her down, give her evidence of his feelings, play the defendant to her jealousy, her suffering, and her dreams, feel guilty, make excuses and apologies. Now what was tiring had disappeared and only the beauty remained.&lt;/p&gt;
&lt;p&gt;Saturday found him for the first time strolling alone through Zurich, breathing in the heady smell of his freedom. New adventures hid around each corner. The future was again a secret. He was on his way back to the bachelor life, the life he had once felt destined for, the life that would let him be what he actually was.&lt;/p&gt;
&lt;p&gt;For seven years he had lived bound to her, his every step subject to her scrutiny. She might as well have chained iron balls to his ankles. Suddenly his step was much lighter.&lt;/p&gt;
&lt;p&gt;He soared. He had entered Parmenides’ magic field: he was enjoying the sweet lightness of being.&lt;/p&gt;
&lt;p&gt;(Did he feel like phoning Sabina in Geneva? Contacting one or another of the women he had met during his several months in Zurich? No, not in the least. Perhaps he sensed that any woman would make his memory of Tereza unbearably painful.) This curious melancholic fascination lasted until Sunday evening. .On Monday, everything changed. Tereza forced her way into his thoughts: he imagined her sitting there writing her farewell letter; he felt her hands trembling; he saw her lugging her heavy suitcase in one hand and leading Karenin on his leash with the other; he pictured her unlocking their Prague flat, and suffered the utter abandonment breathing her in the face as she opened the door.&lt;/p&gt;
&lt;p&gt;During those two beautiful days of melancholy, his compassion (that curse of emotional telepathy) had taken a holiday. It had slept the sound Sunday sleep of a miner who, after a hard week’s work, needs to gather strength for his Monday shift.&lt;/p&gt;
&lt;p&gt;Instead of the patients he was treating, Tomas saw Tereza.&lt;/p&gt;
&lt;p&gt;He tried to remind himself. Don’t think about her! Don’t think about her! He said to himself, I’m sick with compassion. It’s good that she’s gone and that I’ll never see her again, though it’s not Tereza I need to be free of—it’s that sickness, compassion, which I thought I was immune to until she infected me with it.&lt;/p&gt;
&lt;p&gt;On Saturday and Sunday, he felt the sweet lightness of being rise up to him out of the depths of the future. On Monday, he was hit by a weight the likes of which he had never known. The tons of steel of the Russian tanks were nothing compared with it. For there&lt;/p&gt;
&lt;p&gt;is nothing heavier than compassion. Not even one’s own pain weighs so heavy as the pain one feels with someone, for someone, a pain intensified by the imagination and prolonged by a hundred echoes.&lt;/p&gt;
&lt;p&gt;He kept warning himself not to give in to compassion, and compassion listened with bowed head and a seemingly guilty conscience. Compassion knew it was being presumptuous, yet it quietly stood its ground, and on the fifth day after her departure Tomas informed the director of his hospital (the man who had phoned him daily in Prague after the Russian invasion) that he had to return at once. He was ashamed. He knew that the move would appear irresponsible, inexcusable to the man. He thought to unbosom himself and tell him the story of Tereza and the letter she had left on the table for him. But in the end he did not. From the Swiss doctor’s point of view Tereza’s move could only appear hysterical and abhorrent. And Tomas refused to allow anyone an opportunity to think ill of her. The director of the hospital was in fact offended. Tomas shrugged his shoulders and said, Es muss sein. Es muss sein.&lt;/p&gt;
&lt;p&gt;It was an allusion. The last movement of Beethoven’s last quartet is based on the following two motifs:&lt;/p&gt;
&lt;p&gt;To make the meaning of the words absolutely clear, Beethoven introduced the movement with a phrase, Der schwer gefasste Entschluss, which is commonly translated as the difficult resolution.&lt;/p&gt;
&lt;p&gt;This allusion to Beethoven was actually Tomas’s first step back to Tereza, because she was the one who had induced him to buy records of the Beethoven quartets and sonatas.&lt;/p&gt;
&lt;p&gt;The allusion was even more pertinent than he had thought because the Swiss doctor was a great music lover. Smiling serenely, he asked, in the melody of Beethoven’s motif, Muss es sein?&lt;/p&gt;
&lt;p&gt;]a, es muss sein! Tomas said again.&lt;/p&gt;
&lt;p&gt;Unlike Parmenides, Beethoven apparently viewed weight as something positive. Since the German word schwer means both difficult and heavy, Beethoven’s difficult resolution may also be construed as a heavy or weighty resolution. The weighty resolution is at one with the voice of Fate ( Es muss sein! ); necessity, weight, and value are three concepts inextricably bound: only necessity is heavy, and only what is heavy has value.&lt;/p&gt;
&lt;p&gt;This is a conviction born of Beethoven’s music, and although we cannot ignore the possibility (or even probability) that it owes its origins more to Beethoven’s commentators than to Beethoven himself, we all more or less share, it: we believe that the greatness of man stems from the fact that he bears his fate as Atlas bore the heavens on his shoulders. Beethoven’s hero is a lifter of metaphysical weights.&lt;/p&gt;
&lt;p&gt;Tomas approached the Swiss border. I imagine a gloomy, shock-headed Beethoven, in person, conducting the local firemen’s brass band in a farewell to emigration, an Es Muss Sein march.&lt;/p&gt;
&lt;p&gt;Then Tomas crossed the Czech border and was welcomed by columns of Russian tanks. He had to stop his car and wait a half hour before they passed. A terrifying soldier in the black Uniform of the armored forces stood at the crossroads directing traffic as if every road in the country belonged to him and him alone.&lt;/p&gt;
&lt;p&gt;Es muss sein! Tomas repeated to himself, but then he began to doubt. Did it really have to be?&lt;/p&gt;
&lt;p&gt;Yes, it was unbearable for him to stay in Zurich imagining Tereza living on her own in Prague.&lt;/p&gt;
&lt;p&gt;But how long would he have been tortured by compassion? All his life? A year? Or a month? Or only a week?&lt;/p&gt;
&lt;p&gt;How could he have known? How could he have gauged it? Any schoolboy can do experiments in the physics laboratory to test various scientific hypotheses. But man, because he has only one life to live, cannot conduct experiments to test whether to follow his passion (compassion) or not.&lt;/p&gt;
&lt;p&gt;It was with these thoughts in mind that he opened the door to his flat. Karenin made the homecoming easier by jumping up on him and licking his face. The desire to fall into Tereza’s arms (he could still feel it while getting into his car in Zurich) had completely disintegrated. He fancied himself standing opposite her in the midst of a snowy plain, the two of them shivering from the cold.&lt;/p&gt;
&lt;p&gt;From the very beginning of the occupation, Russian military airplanes had flown over Prague all night long. Tomas, no longer accustomed to the noise, was unable to fall asleep.&lt;/p&gt;
&lt;p&gt;Twisting and turning beside the slumbering Tereza, he recalled something she had told him a long time before in the course of an insignificant conversation. They had been talking about his friend Z. when she announced, If I hadn’t met you, I’d certainly have fallen in love with him.&lt;/p&gt;
&lt;p&gt;Even then, her words had left Tomas in a strange state of melancholy, and now he realized it was only a matter of chance that Tereza loved him and not his friend Z. Apart from her consummated love for Tomas, there were, in the realm of possibility, an infinite number of unconsummated loves for other men.&lt;/p&gt;
&lt;p&gt;We all reject out of hand the idea that the love of our life may be something light or weightless; we presume our love is what must be, that without it our life would no longer be the same; we feel that Beethoven himself, gloomy and awe-inspiring, is playing the Es muss sein! to our own great love.&lt;/p&gt;
&lt;p&gt;Tomas often thought of Tereza’s remark about his friend Z. and came to the conclusion that the love story of his life exemplified not Es muss sein! (It must be so), but rather Es konnte auch anders sein (It could just as well be otherwise).&lt;/p&gt;
&lt;p&gt;Seven years earlier, a complex neurological case happened to have been discovered at the hospital in Tereza’s town. They called in the chief surgeon of Tomas’s hospital in Prague for consultation, but the chief surgeon of Tomas’s hospital happened to be suffering from sciatica, and because he could not move he sent Tomas to the provincial hospital in his place. The town had several hotels, but Tomas happened to be given a room in the one where Tereza was employed. He happened to have had enough free time before his train left to stop at the hotel restaurant. Tereza happened to be on duty, and happened to be serving Tomas’s table. It had taken six chance happenings to push Tomas towards Tereza, as if he had little inclination to go to her on his own.&lt;/p&gt;
&lt;p&gt;He had gone back to Prague because of her. So fateful a decision resting on so fortuitous a love, a love that would not even have existed had it not been for the chief surgeon’s sciatica seven years earlier. And that woman, that personification of absolute fortuity, now again lay asleep beside him, breathing deeply.&lt;/p&gt;
&lt;p&gt;It was late at night. His stomach started acting up as it tended to do in times of psychic stress.&lt;/p&gt;
&lt;p&gt;Once or twice her breathing turned into mild snores. Tomas felt no compassion. All he felt was the pressure in his stomach and the despair of having returned.&lt;/p&gt;
</content:encoded><category>Articles</category><author>ChanZhaoYu</author></item><item><title>羅生門</title><link>https://www.redon.cc/posts/rashomon/</link><guid isPermaLink="true">https://www.redon.cc/posts/rashomon/</guid><pubDate>Fri, 05 Mar 1971 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;ある日の暮方の事である。一人の下人げにんが、羅生門らしょうもんの下で雨やみを待っていた。&lt;/p&gt;
&lt;p&gt;広い門の下には、この男のほかに誰もいない。ただ、所々丹塗にぬりの剥はげた、大きな円柱まるばしらに、蟋蟀きりぎりすが一匹とまっている。羅生門が、朱雀大路すざくおおじにある以上は、この男のほかにも、雨やみをする市女笠いちめがさや揉烏帽子もみえぼしが、もう二三人はありそうなものである。それが、この男のほかには誰もいない。&lt;/p&gt;
&lt;p&gt;何故かと云うと、この二三年、京都には、地震とか辻風つじかぜとか火事とか饑饉とか云う災わざわいがつづいて起った。そこで洛中らくちゅうのさびれ方は一通りではない。旧記によると、仏像や仏具を打砕いて、その丹にがついたり、金銀の箔はくがついたりした木を、路ばたにつみ重ねて、薪たきぎの料しろに売っていたと云う事である。洛中がその始末であるから、羅生門の修理などは、元より誰も捨てて顧る者がなかった。するとその荒れ果てたのをよい事にして、狐狸こりが棲すむ。盗人ぬすびとが棲む。とうとうしまいには、引取り手のない死人を、この門へ持って来て、棄てて行くと云う習慣さえ出来た。そこで、日の目が見えなくなると、誰でも気味を悪るがって、この門の近所へは足ぶみをしない事になってしまったのである。&lt;/p&gt;
&lt;p&gt;その代りまた鴉からすがどこからか、たくさん集って来た。昼間見ると、その鴉が何羽となく輪を描いて、高い鴟尾しびのまわりを啼きながら、飛びまわっている。ことに門の上の空が、夕焼けであかくなる時には、それが胡麻ごまをまいたようにはっきり見えた。鴉は、勿論、門の上にある死人の肉を、啄ついばみに来るのである。――もっとも今日は、刻限こくげんが遅いせいか、一羽も見えない。ただ、所々、崩れかかった、そうしてその崩れ目に長い草のはえた石段の上に、鴉の糞ふんが、点々と白くこびりついているのが見える。下人は七段ある石段の一番上の段に、洗いざらした紺の襖あおの尻を据えて、右の頬に出来た、大きな面皰にきびを気にしながら、ぼんやり、雨のふるのを眺めていた。&lt;/p&gt;
&lt;p&gt;作者はさっき、「下人が雨やみを待っていた」と書いた。しかし、下人は雨がやんでも、格別どうしようと云う当てはない。ふだんなら、勿論、主人の家へ帰る可き筈である。所がその主人からは、四五日前に暇を出された。前にも書いたように、当時京都の町は一通りならず衰微すいびしていた。今この下人が、永年、使われていた主人から、暇を出されたのも、実はこの衰微の小さな余波にほかならない。だから「下人が雨やみを待っていた」と云うよりも「雨にふりこめられた下人が、行き所がなくて、途方にくれていた」と云う方が、適当である。その上、今日の空模様も少からず、この平安朝の下人の Sentimentalisme に影響した。申さるの刻こく下さがりからふり出した雨は、いまだに上るけしきがない。そこで、下人は、何をおいても差当り明日あすの暮しをどうにかしようとして――云わばどうにもならない事を、どうにかしようとして、とりとめもない考えをたどりながら、さっきから朱雀大路にふる雨の音を、聞くともなく聞いていたのである。&lt;/p&gt;
&lt;p&gt;雨は、羅生門をつつんで、遠くから、ざあっと云う音をあつめて来る。夕闇は次第に空を低くして、見上げると、門の屋根が、斜につき出した甍いらかの先に、重たくうす暗い雲を支えている。&lt;/p&gt;
&lt;p&gt;どうにもならない事を、どうにかするためには、手段を選んでいる遑いとまはない。選んでいれば、築土ついじの下か、道ばたの土の上で、饑死うえじにをするばかりである。そうして、この門の上へ持って来て、犬のように棄てられてしまうばかりである。選ばないとすれば――下人の考えは、何度も同じ道を低徊ていかいした揚句あげくに、やっとこの局所へ逢着ほうちゃくした。しかしこの「すれば」は、いつまでたっても、結局「すれば」であった。下人は、手段を選ばないという事を肯定しながらも、この「すれば」のかたをつけるために、当然、その後に来る可き「盗人ぬすびとになるよりほかに仕方がない」と云う事を、積極的に肯定するだけの、勇気が出ずにいたのである。&lt;/p&gt;
&lt;p&gt;下人は、大きな嚔くさめをして、それから、大儀たいぎそうに立上った。夕冷えのする京都は、もう火桶ひおけが欲しいほどの寒さである。風は門の柱と柱との間を、夕闇と共に遠慮なく、吹きぬける。丹塗にぬりの柱にとまっていた蟋蟀きりぎりすも、もうどこかへ行ってしまった。&lt;/p&gt;
&lt;p&gt;下人は、頸くびをちぢめながら、山吹やまぶきの汗袗かざみに重ねた、紺の襖あおの肩を高くして門のまわりを見まわした。雨風の患うれえのない、人目にかかる惧おそれのない、一晩楽にねられそうな所があれば、そこでともかくも、夜を明かそうと思ったからである。すると、幸い門の上の楼へ上る、幅の広い、これも丹を塗った梯子はしごが眼についた。上なら、人がいたにしても、どうせ死人ばかりである。下人はそこで、腰にさげた聖柄ひじりづかの太刀たちが鞘走さやばしらないように気をつけながら、藁草履わらぞうりをはいた足を、その梯子の一番下の段へふみかけた。&lt;/p&gt;
&lt;p&gt;それから、何分かの後である。羅生門の楼の上へ出る、幅の広い梯子の中段に、一人の男が、猫のように身をちぢめて、息を殺しながら、上の容子ようすを窺っていた。楼の上からさす火の光が、かすかに、その男の右の頬をぬらしている。短い鬚の中に、赤く膿うみを持った面皰にきびのある頬である。下人は、始めから、この上にいる者は、死人ばかりだと高を括くくっていた。それが、梯子を二三段上って見ると、上では誰か火をとぼして、しかもその火をそこここと動かしているらしい。これは、その濁った、黄いろい光が、隅々に蜘蛛くもの巣をかけた天井裏に、揺れながら映ったので、すぐにそれと知れたのである。この雨の夜に、この羅生門の上で、火をともしているからは、どうせただの者ではない。&lt;/p&gt;
&lt;p&gt;下人は、守宮やもりのように足音をぬすんで、やっと急な梯子を、一番上の段まで這うようにして上りつめた。そうして体を出来るだけ、平たいらにしながら、頸を出来るだけ、前へ出して、恐る恐る、楼の内を覗のぞいて見た。&lt;/p&gt;
&lt;p&gt;見ると、楼の内には、噂に聞いた通り、幾つかの死骸しがいが、無造作に棄ててあるが、火の光の及ぶ範囲が、思ったより狭いので、数は幾つともわからない。ただ、おぼろげながら、知れるのは、その中に裸の死骸と、着物を着た死骸とがあるという事である。勿論、中には女も男もまじっているらしい。そうして、その死骸は皆、それが、かつて、生きていた人間だと云う事実さえ疑われるほど、土を捏こねて造った人形のように、口を開あいたり手を延ばしたりして、ごろごろ床の上にころがっていた。しかも、肩とか胸とかの高くなっている部分に、ぼんやりした火の光をうけて、低くなっている部分の影を一層暗くしながら、永久に唖おしの如く黙っていた。&lt;/p&gt;
&lt;p&gt;下人げにんは、それらの死骸の腐爛ふらんした臭気に思わず、鼻を掩おおった。しかし、その手は、次の瞬間には、もう鼻を掩う事を忘れていた。ある強い感情が、ほとんどことごとくこの男の嗅覚を奪ってしまったからだ。&lt;/p&gt;
&lt;p&gt;下人の眼は、その時、はじめてその死骸の中に蹲うずくまっている人間を見た。檜皮色ひわだいろの着物を着た、背の低い、痩やせた、白髪頭しらがあたまの、猿のような老婆である。その老婆は、右の手に火をともした松の木片きぎれを持って、その死骸の一つの顔を覗きこむように眺めていた。髪の毛の長い所を見ると、多分女の死骸であろう。&lt;/p&gt;
&lt;p&gt;下人は、六分の恐怖と四分の好奇心とに動かされて、暫時ざんじは呼吸いきをするのさえ忘れていた。旧記の記者の語を借りれば、「頭身とうしんの毛も太る」ように感じたのである。すると老婆は、松の木片を、床板の間に挿して、それから、今まで眺めていた死骸の首に両手をかけると、丁度、猿の親が猿の子の虱しらみをとるように、その長い髪の毛を一本ずつ抜きはじめた。髪は手に従って抜けるらしい。&lt;/p&gt;
&lt;p&gt;その髪の毛が、一本ずつ抜けるのに従って、下人の心からは、恐怖が少しずつ消えて行った。そうして、それと同時に、この老婆に対するはげしい憎悪が、少しずつ動いて来た。――いや、この老婆に対すると云っては、語弊ごへいがあるかも知れない。むしろ、あらゆる悪に対する反感が、一分毎に強さを増して来たのである。この時、誰かがこの下人に、さっき門の下でこの男が考えていた、饑死うえじにをするか盗人ぬすびとになるかと云う問題を、改めて持出したら、恐らく下人は、何の未練もなく、饑死を選んだ事であろう。それほど、この男の悪を憎む心は、老婆の床に挿した松の木片きぎれのように、勢いよく燃え上り出していたのである。&lt;/p&gt;
&lt;p&gt;下人には、勿論、何故老婆が死人の髪の毛を抜くかわからなかった。従って、合理的には、それを善悪のいずれに片づけてよいか知らなかった。しかし下人にとっては、この雨の夜に、この羅生門の上で、死人の髪の毛を抜くと云う事が、それだけで既に許すべからざる悪であった。勿論、下人は、さっきまで自分が、盗人になる気でいた事なぞは、とうに忘れていたのである。&lt;/p&gt;
&lt;p&gt;そこで、下人は、両足に力を入れて、いきなり、梯子から上へ飛び上った。そうして聖柄ひじりづかの太刀に手をかけながら、大股に老婆の前へ歩みよった。老婆が驚いたのは云うまでもない。&lt;/p&gt;
&lt;p&gt;老婆は、一目下人を見ると、まるで弩いしゆみにでも弾はじかれたように、飛び上った。&lt;/p&gt;
&lt;p&gt;「おのれ、どこへ行く。」&lt;/p&gt;
&lt;p&gt;下人は、老婆が死骸につまずきながら、慌てふためいて逃げようとする行手を塞ふさいで、こう罵ののしった。老婆は、それでも下人をつきのけて行こうとする。下人はまた、それを行かすまいとして、押しもどす。二人は死骸の中で、しばらく、無言のまま、つかみ合った。しかし勝敗は、はじめからわかっている。下人はとうとう、老婆の腕をつかんで、無理にそこへ※(「てへん＋丑」、第4水準2-12-93)ねじ倒した。丁度、鶏にわとりの脚のような、骨と皮ばかりの腕である。&lt;/p&gt;
&lt;p&gt;「何をしていた。云え。云わぬと、これだぞよ。」&lt;/p&gt;
&lt;p&gt;下人は、老婆をつき放すと、いきなり、太刀の鞘さやを払って、白い鋼はがねの色をその眼の前へつきつけた。けれども、老婆は黙っている。両手をわなわなふるわせて、肩で息を切りながら、眼を、眼球めだまが※(「目＋匡」、第3水準1-88-81)まぶたの外へ出そうになるほど、見開いて、唖のように執拗しゅうねく黙っている。これを見ると、下人は始めて明白にこの老婆の生死が、全然、自分の意志に支配されていると云う事を意識した。そうしてこの意識は、今までけわしく燃えていた憎悪の心を、いつの間にか冷ましてしまった。後あとに残ったのは、ただ、ある仕事をして、それが円満に成就した時の、安らかな得意と満足とがあるばかりである。そこで、下人は、老婆を見下しながら、少し声を柔らげてこう云った。&lt;/p&gt;
&lt;p&gt;「己おれは検非違使けびいしの庁の役人などではない。今し方この門の下を通りかかった旅の者だ。だからお前に縄なわをかけて、どうしようと云うような事はない。ただ、今時分この門の上で、何をして居たのだか、それを己に話しさえすればいいのだ。」&lt;/p&gt;
&lt;p&gt;すると、老婆は、見開いていた眼を、一層大きくして、じっとその下人の顔を見守った。※(「目＋匡」、第3水準1-88-81)まぶたの赤くなった、肉食鳥のような、鋭い眼で見たのである。それから、皺で、ほとんど、鼻と一つになった唇を、何か物でも噛んでいるように動かした。細い喉で、尖った喉仏のどぼとけの動いているのが見える。その時、その喉から、鴉からすの啼くような声が、喘あえぎ喘ぎ、下人の耳へ伝わって来た。&lt;/p&gt;
&lt;p&gt;「この髪を抜いてな、この髪を抜いてな、鬘かずらにしようと思うたのじゃ。」&lt;/p&gt;
&lt;p&gt;下人は、老婆の答が存外、平凡なのに失望した。そうして失望すると同時に、また前の憎悪が、冷やかな侮蔑ぶべつと一しょに、心の中へはいって来た。すると、その気色けしきが、先方へも通じたのであろう。老婆は、片手に、まだ死骸の頭から奪った長い抜け毛を持ったなり、蟇ひきのつぶやくような声で、口ごもりながら、こんな事を云った。&lt;/p&gt;
&lt;p&gt;「成程な、死人しびとの髪の毛を抜くと云う事は、何ぼう悪い事かも知れぬ。じゃが、ここにいる死人どもは、皆、そのくらいな事を、されてもいい人間ばかりだぞよ。現在、わしが今、髪を抜いた女などはな、蛇を四寸しすんばかりずつに切って干したのを、干魚ほしうおだと云うて、太刀帯たてわきの陣へ売りに往いんだわ。疫病えやみにかかって死ななんだら、今でも売りに往んでいた事であろ。それもよ、この女の売る干魚は、味がよいと云うて、太刀帯どもが、欠かさず菜料さいりように買っていたそうな。わしは、この女のした事が悪いとは思うていぬ。せねば、饑死をするのじゃて、仕方がなくした事であろ。されば、今また、わしのしていた事も悪い事とは思わぬぞよ。これとてもやはりせねば、饑死をするじゃて、仕方がなくする事じゃわいの。じゃて、その仕方がない事を、よく知っていたこの女は、大方わしのする事も大目に見てくれるであろ。」&lt;/p&gt;
&lt;p&gt;老婆は、大体こんな意味の事を云った。&lt;/p&gt;
&lt;p&gt;下人は、太刀を鞘さやにおさめて、その太刀の柄つかを左の手でおさえながら、冷然として、この話を聞いていた。勿論、右の手では、赤く頬に膿を持った大きな面皰にきびを気にしながら、聞いているのである。しかし、これを聞いている中に、下人の心には、ある勇気が生まれて来た。それは、さっき門の下で、この男には欠けていた勇気である。そうして、またさっきこの門の上へ上って、この老婆を捕えた時の勇気とは、全然、反対な方向に動こうとする勇気である。下人は、饑死をするか盗人になるかに、迷わなかったばかりではない。その時のこの男の心もちから云えば、饑死などと云う事は、ほとんど、考える事さえ出来ないほど、意識の外に追い出されていた。&lt;/p&gt;
&lt;p&gt;「きっと、そうか。」&lt;/p&gt;
&lt;p&gt;老婆の話が完おわると、下人は嘲あざけるような声で念を押した。そうして、一足前へ出ると、不意に右の手を面皰にきびから離して、老婆の襟上えりがみをつかみながら、噛みつくようにこう云った。&lt;/p&gt;
&lt;p&gt;「では、己おれが引剥ひはぎをしようと恨むまいな。己もそうしなければ、饑死をする体なのだ。」&lt;/p&gt;
&lt;p&gt;下人は、すばやく、老婆の着物を剥ぎとった。それから、足にしがみつこうとする老婆を、手荒く死骸の上へ蹴倒した。梯子の口までは、僅に五歩を数えるばかりである。下人は、剥ぎとった檜皮色ひわだいろの着物をわきにかかえて、またたく間に急な梯子を夜の底へかけ下りた。&lt;/p&gt;
&lt;p&gt;しばらく、死んだように倒れていた老婆が、死骸の中から、その裸の体を起したのは、それから間もなくの事である。老婆はつぶやくような、うめくような声を立てながら、まだ燃えている火の光をたよりに、梯子の口まで、這って行った。そうして、そこから、短い白髪しらがを倒さかさまにして、門の下を覗きこんだ。外には、ただ、黒洞々こくとうとうたる夜があるばかりである。&lt;/p&gt;
&lt;p&gt;下人の行方ゆくえは、誰も知らない。&lt;/p&gt;
</content:encoded><category>Articles</category><author>ChanZhaoYu</author></item><item><title>容忍与自由</title><link>https://www.redon.cc/posts/tolerance-and-freedom/</link><guid isPermaLink="true">https://www.redon.cc/posts/tolerance-and-freedom/</guid><pubDate>Mon, 16 Mar 1959 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;十七八年前，我最后一次会见我的母校康耐儿大学的史学大师布尔先生（George Lincoln Burr）。我们谈到英国史学大师阿克顿（Lord Acton）一生准备要著作一部《自由之史》，没有写成他就死了。布尔先生那天谈话很多，有一句话我至今没有忘记。他说，“我年纪越大，越感觉到容忍（tolerance）比自由更重要”。&lt;/p&gt;
&lt;p&gt;布尔先生死了十多年了，他这句话我越想越觉得是一句不可磨灭的格言。我自己也有“年纪越大，越觉得容忍比自由还更重要”的感想。有时我竟觉得容忍是一切自由的根本：没有容忍，就没有自由。&lt;/p&gt;
&lt;p&gt;我十七岁的时候（1908）曾在《竞业旬报》上发表几条《无鬼丛话》，其中有一条是痛骂小说《西游记》和《封神榜》的，我说：&lt;/p&gt;
&lt;p&gt;《王制》有之：“假于鬼神时日卜筮以疑众，杀。”吾独怪夫数千年来之排治权者，之以济世明道自期者，乃懵然不之注意，惑世诬民之学说得以大行，遂举我神州民族投诸极黑暗之世界！&lt;/p&gt;
&lt;p&gt;这是一个小孩子很不容忍的“卫道”态度。我在那时候已是一个无鬼论者、无神论者，所以发出那种摧除迷信的狂论，要实行《王制》（《礼记》的一篇）的“假于鬼神时日卜筮以疑众，杀”的一条经典！&lt;/p&gt;
&lt;p&gt;我在那时候当然没有梦想到说这话的小孩子在十五年后（1923）会很热心的给《西游记》作两万字的考证！我在那时候当然更没有想到那个小孩子在二、三十年后还时时留心搜求可以考证《封神榜》的作者的材料！我在那时候也完全没有想想《王制》那句话的历史意义。那一段《王制》的全文是这样的：&lt;/p&gt;
&lt;p&gt;析言破律，乱名改作，执左道以乱政，杀。作淫声异服奇技奇器以疑众，杀。行伪而坚，言伪而辩，学非而博，顺非而泽以疑众，杀。假于鬼神时日卜筮以疑众，杀。此四诛者，不以听。&lt;/p&gt;
&lt;p&gt;我在五十年前，完全没有懂得这一段话的“诛”正是中国专制政体之下禁止新思想、新学术、新信仰、新艺术的经典的根据。我在那时候抱着“破除迷信”的热心，所以拥护那“四诛”之中的第四诛：“假于鬼神时日卜筮以疑众，杀。”我当时完全没有想到第四诛的“假于鬼神……以疑众”和第一诛的“执左道以乱政”的两条罪名都可以用来摧残宗教信仰的自由。我当时也完全没有注意到郑玄注里用了公输般作“奇技异器”的例子；更没有注意到孔颖达《正义》里举了“孔子为鲁司寇七日而诛少正卯”的例子来解释“行伪而坚，言伪而辩，学非而博，顺非而泽以疑众，杀”。故第二诛可以用来禁绝艺术创作的自由，也可以用来“杀”许多发明“奇技异器”的科学家。故第三诛可以用来摧残思想的自由，言论的自由，著作出版的自由。&lt;/p&gt;
&lt;p&gt;我在五十年前引用《王制》第四诛，要“杀”《西游记》《封神榜》的作者。那时候我当然没有梦想到十年之后我在北京大学教书时就有一些同样“卫道”的正人君子也想引用《王制》的第三诛，要“杀”我和我的朋友们。当年我要“杀”人，后来人要“杀”我，动机是一样的：都只因为动了一点正义的火气，就都失掉容忍的度量了。&lt;/p&gt;
&lt;p&gt;我自己叙述五十年前主张“假于鬼神时日卜筮以疑众，杀”的故事，为的是要说明我年纪越大，越觉得“容忍”比“自由”还更重要。&lt;/p&gt;
&lt;p&gt;我到今天还是一个无神论者，我不信有一个有意志的神，我也不信灵魂不朽的说法。但我的无神论和共产党的无神论有一点最根本的不同。我能够容忍一切信仰有神的宗教，也能够容忍一切诚心信仰宗教的人。共产党自己主张无神论，就要消灭一切有神的信仰，要禁绝一切信仰有神的宗教，——这就是我五十年前幼稚而又狂妄的不容忍的态度了。&lt;/p&gt;
&lt;p&gt;我自己总觉得，这个国家、这个社会、这个世界，绝大多数人是信神的，居然能有这雅量，能容忍我的无神论，能容忍我这个不信神也不信灵魂不灭的人，能容忍我在国内和国外自由发表我的无神论的思想，从没有人因此用石头掷我，把我关在监狱里，或把我捆在柴堆上用火烧死。我在这个世界里居然享受了四十多年的容忍与自由。我觉得这个国家、这个社会、这个世界对我的容忍度量是可爱的，是可以感激的。&lt;/p&gt;
&lt;p&gt;所以我自己总觉得我应该用容忍的态度来报答社会对我的容忍。所以我自己不信神，但我能诚心的谅解一切信神的人，也能诚心的容忍并且敬重一切信仰有神的宗教。&lt;/p&gt;
&lt;p&gt;我要用容忍的态度来报答社会对我的容忍，因为我年纪越大，我越觉得容忍的重要意义。若社会没有这点容忍的气度，我决不能享受四十多年大胆怀疑的自由，公开主张无神论的自由了。&lt;/p&gt;
&lt;p&gt;在宗教自由史上，在思想自由史上，在政治自由史上，我们都可以看见容忍的态度是最难得，最稀有的态度。人类的习惯总是喜同而恶异的，总不喜欢和自己不同的信仰、思想、行为。这就是不容忍的根源。不容忍只是不能容忍和我自己不同的新思想和新信仰。一个宗教团体总相信自己的宗教信仰是对的，是不会错的，所以它总相信那些和自己不同的宗教信仰必定是错的，必定是异端，邪教。一个政治团体总相信自己的政治主张是对的，是不会错的，所以它总相信那些和自己不同的政治见解必定是错的，必定是敌人。&lt;/p&gt;
&lt;p&gt;一切对异端的迫害，一切对“异已”的摧残，一切宗教自由的禁止，一切思想言论的被压迫，都由于这一点深信自己是不会错的心理。因为深信自己是不会错的，所以不能容忍任何和自己不同的思想信仰了。&lt;/p&gt;
&lt;p&gt;试看欧洲的宗教革新运动的历史。马丁路德（Martin Luther）和约翰高尔文（John Calvin）等人起来革新宗教，本来是因为他们不满意于罗马旧教的种种不容忍，种种不自由。但是新教在中欧北欧胜利之后，新教的领袖们又都渐渐走上了不容忍的路上去，也不容许别人起来批评他们的新教条了。高尔文在日内瓦掌握了宗教大权，居然会把一个敢独立思想，敢批评高尔文的教条的学者塞维图斯（Servetus）定了“异端邪说”的罪名，把他用铁链锁在木桩上，堆起柴来，慢慢的活烧死。这是1553年10月23日的事。&lt;/p&gt;
&lt;p&gt;这个殉道者塞维图斯的惨史，最值得人们的追念和反省。宗教革新运动原来的目标是要争取“基督教的人的自由”和“良心的自由”。何以高尔文和他的信徒们居然会把一位独立思想的新教徒用慢慢的火烧死呢？何以高尔文的门徒（后来继任高尔文为日内瓦的宗教独裁者）柏时（de Beze）竟会宣言“良心的自由是魔鬼的教条”呢？&lt;/p&gt;
&lt;p&gt;基本的原因还是那一点深信我自己是“不会错的”的心理。像高尔文那样虔诚的宗教改革家，他自己深信他的良心确是代表上帝的命令，他的口和他的笔确是代表上帝的意志，那末他的意见还会错吗？他还有错误的可能吗？在塞维图斯被烧死之后，高尔文曾受到不少人的批评。1554年，高尔文发表一篇文字为他自己辩护，他毫不迟疑的说，“严厉惩治邪说者的权威是无可疑的，因为这就是上帝自己说话。……这工作是为上帝的光荣战斗”。&lt;/p&gt;
&lt;p&gt;上帝自己说话，还会错吗？为上帝的光荣作战，还会错吗？这一点“我不会错”的心理，就是一切不容忍的根苗。深信我自己的信念没有错误的可能（infallible），我的意见就是“正义”，反对我的人当然都是“邪说”了。我的意见代表上帝的意旨，反对我的人的意见当然都是“魔鬼的教条”了。&lt;/p&gt;
&lt;p&gt;这是宗教自由史给我们的教训：容忍是一切自由的根本；没有容忍“异己”的雅量，就不会承认“异己”的宗教信仰可以享自由。但因为不容忍的态度是基于“我的信念不会错”的心理习惯，所以容忍“异己”是最难得，最不容易养成的雅量。&lt;/p&gt;
&lt;p&gt;在政治思想上，在社会问题的讨论上，我们同样的感觉到不容忍是常见的，而容忍总是很稀有的，我试举一个死了的老朋友的故事作例子。四十多年前，我们在《新青年》杂志上开始提倡白话文学的运动，我曾从美国寄信给陈独秀，我说：&lt;/p&gt;
&lt;p&gt;此事之是非，非一朝一夕所能定，亦非一二人所能定。甚愿国中人士能平心静气与吾辈同力研究此问题。讨论既熟，是非自明。吾辈已张革命之旗，虽不容退缩，然亦决不敢以吾辈所主张为必是而不容他人之匡正也。&lt;/p&gt;
&lt;p&gt;独秀在《新青年》上答我道：&lt;/p&gt;
&lt;p&gt;鄙意容纳异议，自由讨论，固为学术发达之原则，独于改良中国文学当以白话为正宗之说，其是非甚明，必不容反对者有讨论之余地；必以吾辈所主张者为绝对之是，而不容他人之匡正也。&lt;/p&gt;
&lt;p&gt;我当时看了就觉得这是很武断的态度。现在在四十多年之后，我还忘不了独秀这一句话，我还觉得这种“必以吾辈所主张者为绝对之是”的态度是很不容忍的态度，是最容易引起别人的恶感，是最容易引起反对的。&lt;/p&gt;
&lt;p&gt;我曾说过，我应该用容忍的态度来报答社会对我的容忍。我现在常常想我们还得戒律自己：我们若想别人容忍谅解我们的见解，我们必须先养成能够容忍谅解别人的见解的度量。至少至少我们应该戒约自己决不可“以吾辈所主张者为绝对之是”。我们受过实验主义的训练的人，本来就不承认有“绝对之是”，更不可以“以吾辈所主张者为绝对之是”。&lt;/p&gt;
&lt;p&gt;四八、三、十二晨&lt;/p&gt;
&lt;p&gt;（原载1959年3月16日台北《自由中国》第20卷第6期）&lt;/p&gt;
</content:encoded><category>Articles</category><author>ChanZhaoYu</author></item><item><title>故鄉</title><link>https://www.redon.cc/posts/hometown/</link><guid isPermaLink="true">https://www.redon.cc/posts/hometown/</guid><pubDate>Mon, 10 Jan 1921 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我冒了嚴寒，回到相隔二千餘里，別了二十餘年的故鄉去。&lt;/p&gt;
&lt;p&gt;時候既然是深冬；漸近故鄉時，天氣又陰晦了，冷風吹進船艙中，嗚嗚的響，從蓬隙向外一望，蒼黃的天底下，遠近橫著幾個蕭索的荒村，沒有一些活氣。我的心禁不住悲涼起來了。&lt;/p&gt;
&lt;p&gt;阿！這不是我二十年來時時記得的故鄉？&lt;/p&gt;
&lt;p&gt;我所記得的故鄉全不如此。我的故鄉好得多了。但要我記起他的美麗，說出他的佳處來，卻又沒有影像，沒有言辭了。仿佛也就如此。於是我自己解釋說：故鄉本也如此，——雖然沒有進步，也未必有如我所感的悲涼，這只是我自己心情的改變罷了，因為我這次回鄉，本沒有什麼好心緒。&lt;/p&gt;
&lt;p&gt;我這次是專為了別他而來的。我們多年聚族而居的老屋，已經公同賣給別姓了，交屋的期限，只在本年，所以必須趕在正月初一以前，永別了熟識的老屋，而且遠離了熟識的故鄉，搬家到我在謀食的異地去。&lt;/p&gt;
&lt;p&gt;第二日清早晨我到了我家的門口了。瓦楞上許多枯草的斷莖當風抖著，正在說明這老屋難免易主的原因。幾房的本家大約已經搬走了，所以很寂靜。我到了自家的房外，我的母親早已迎著出來了，接著便飛出了八歲的侄兒宏兒。&lt;/p&gt;
&lt;p&gt;我的母親很高興，但也藏著許多淒涼的神情，教我坐下，歇息，喝茶，且不談搬家的事。宏兒沒有見過我，遠遠的對面站著只是看。&lt;/p&gt;
&lt;p&gt;但我們終於談到搬家的事。我說外間的寓所已經租定了，又買了幾件傢具，此外須將家裡所有的木器賣去，再去增添。母親也說好，而且行李也略已齊集，木器不便搬運的，也小半賣去了，只是收不起錢來。&lt;/p&gt;
&lt;p&gt;「你休息一兩天，去拜望親戚本家一回，我們便可以走了。」母親說。&lt;/p&gt;
&lt;p&gt;「是的。」&lt;/p&gt;
&lt;p&gt;「還有閏土，他每到我家來時，總問起你，很想見你一回面。我已經將你到家的大約日期通知他，他也許就要來了。」&lt;/p&gt;
&lt;p&gt;這時候，我的腦裡忽然閃出一幅神異的圖畫來：深藍的天空中掛著一輪金黃的圓月，下面是海邊的沙地，都種著一望無際的碧綠的西瓜，其間有一個十一二歲的少年，項帶銀圈，手捏一柄鋼叉，向一匹猹盡力的刺去，那猹卻將身一扭，反從他的胯下逃走了。&lt;/p&gt;
&lt;p&gt;這少年便是閏土。我認識他時，也不過十多歲，離現在將有三十年了；那時我的父親還在世，家景也好，我正是一個少爺。那一年，我家是一件大祭祀的值年。這祭祀，說是三十多年才能輪到一回，所以很鄭重；正月裡供祖像，供品很多，祭器很講究，拜的人也很多，祭器也很要防偷去。我家只有一個忙月（我們這裡給人做工的分三種：整年給一定人家做工的叫長工；按日給人做工的叫短工；自己也種地，只在過年過節以及收租時候來給一定人家做工的稱忙月），忙不過來，他便對父親說，可以叫他的兒子閏土來管祭器的。&lt;/p&gt;
&lt;p&gt;我的父親允許了；我也很高興，因為我早聽到閏土這名字，而且知道他和我仿佛年紀，閏月生的，五行缺土，所以他的父親叫他閏土。他是能裝弶捉小鳥雀的。&lt;/p&gt;
&lt;p&gt;我於是日日盼望新年，新年到，閏土也就到了。好容易到了年末，有一日，母親告訴我，閏土來了，我便飛跑的去看。他正在廚房裡，紫色的圓臉，頭戴一頂小氈帽，頸上套一個明晃晃的銀項圈，這可見他的父親十分愛他，怕他死去，所以在神佛面前許下願心，用圈子將他套住了。他見人很怕羞，只是不怕我，沒有旁人的時候，便和我說話，於是不到半日，我們便熟識了。&lt;/p&gt;
&lt;p&gt;我們那時候不知道談些什麼，只記得閏土很高興，說是上城之後，見了許多沒有見過的東西。&lt;/p&gt;
&lt;p&gt;第二日，我便要他捕鳥。他說：&lt;/p&gt;
&lt;p&gt;“這不能。須大雪下了才好。我們沙地上，下了雪，我掃出一塊空地來，用短棒支起一個大竹匾，撒下秕穀，看鳥雀來吃時，我遠遠地將縛在棒上的繩子只一拉，那鳥雀就罩在竹匾下了。什麼都有：稻雞，角雞，鵓鴣，藍背……”&lt;/p&gt;
&lt;p&gt;我於是又很盼望下雪。&lt;/p&gt;
&lt;p&gt;閏土又對我說：&lt;/p&gt;
&lt;p&gt;“現在太冷，你夏天到我們這裡來。我們日裡到海邊撿貝殼去，紅的綠的都有，鬼見怕也有，觀音手也有。晚上我和爹管西瓜去，你也去。”&lt;/p&gt;
&lt;p&gt;“管賊麽？”&lt;/p&gt;
&lt;p&gt;“不是。走路的人口渴了摘一個瓜吃，我們這裡是不算偷的。要管的是獾豬，刺蝟，猹。月亮底下，你聽，啦啦的響了，猹在咬瓜了。你便捏了胡叉，輕輕地走去……”&lt;/p&gt;
&lt;p&gt;我那時並不知道這所謂猹的是怎麼一件東西——便是現在也沒有知道——只是無端的覺得狀如小狗而很兇猛。&lt;/p&gt;
&lt;p&gt;“他不咬人麽？”&lt;/p&gt;
&lt;p&gt;“有胡叉呢。走到了，看見猹了，你便刺。這畜生很伶俐，倒向你奔來，反從胯下竄了。他的皮毛是油一般的滑……”&lt;/p&gt;
&lt;p&gt;我素不知道天下有這許多新鮮事：海邊有如許五色的貝殼；西瓜有這樣危險的經歷，我先前單知道他在水果店裡出賣罷了。&lt;/p&gt;
&lt;p&gt;“我們沙地裡，潮汛要來的時候，就有許多跳魚兒只是跳，都有青蛙似的兩個腳……”&lt;/p&gt;
&lt;p&gt;阿！閏土的心裡有無窮無盡的希奇的事，都是我往常的朋友所不知道的。他們不知道一些事，閏土在海邊時，他們都和我一樣只看見院子裡高牆上的四角的天空。&lt;/p&gt;
&lt;p&gt;可惜正月過去了，閏土須回家裡去，我急得大哭，他也躲到廚房裡，哭著不肯出門，但終於被他父親帶走了。他後來還托他的父親帶給我一包貝殼和幾支很好看的鳥毛，我也曾送他一兩次東西，但從此沒有再見面。&lt;/p&gt;
&lt;p&gt;現在我的母親提起了他，我這兒時的記憶，忽而全都閃電似的蘇生過來，似乎看到了我的美麗的故鄉了。我應聲說：&lt;/p&gt;
&lt;p&gt;“這好極！他，——怎樣？……”&lt;/p&gt;
&lt;p&gt;“他？……他景況也很不如意……”母親說著，便向房外看，“這些人又來了。說是買木器，順手也就隨便拿走的，我得去看看。”&lt;/p&gt;
&lt;p&gt;母親站起身，出去了。門外有幾個女人的聲音。我便招宏兒走近面前，和他閑話：問他可會寫字，可願意出門。&lt;/p&gt;
&lt;p&gt;“我們坐火車去麽？”&lt;/p&gt;
&lt;p&gt;“我們坐火車去。”&lt;/p&gt;
&lt;p&gt;“船呢？”&lt;/p&gt;
&lt;p&gt;“先坐船，……”&lt;/p&gt;
&lt;p&gt;“哈！這模樣了！鬍子這麼長了！”一種尖利的怪聲突然大叫起來。&lt;/p&gt;
&lt;p&gt;我吃了一嚇，趕忙抬起頭，卻見一個凸顴骨、薄嘴唇、五十歲上下的女人站在我面前，兩手搭在髀間，沒有繫裙，張著兩腳，正像一個畫圖儀器裡細腳伶仃的圓規。&lt;/p&gt;
&lt;p&gt;我愕然了。&lt;/p&gt;
&lt;p&gt;“不認識了麽？我還抱過你咧！”&lt;/p&gt;
&lt;p&gt;我愈加愕然了。幸而我的母親也就進來，從旁說：&lt;/p&gt;
&lt;p&gt;“他多年出門，統忘卻了。你該記得罷，”便向著我說，“這是斜對門的楊二嫂，……開豆腐店的。”&lt;/p&gt;
&lt;p&gt;哦，我記得了。我孩子時候，在斜對門的豆腐店裡確乎終日坐著一個楊二嫂，人都叫伊“豆腐西施”。但是擦著白粉，顴骨沒有這麼高，嘴唇也沒有這麼薄，而且終日坐著，我也從沒有見過這圓規式的姿勢。那時人說：因為伊，這豆腐店的買賣非常好。但這大約因為年齡的關係，我卻並未蒙著一毫感化，所以竟完全忘卻了。然而圓規很不平，顯出鄙夷的神色，仿佛嗤笑法國人不知道拿破侖，美國人不知道華盛頓似的，冷笑說：&lt;/p&gt;
&lt;p&gt;“忘了？這真是貴人眼高……”&lt;/p&gt;
&lt;p&gt;“那有這事……我……”我惶恐著，站起來說。&lt;/p&gt;
&lt;p&gt;“那麼，我對你說。迅哥兒，你闊了，搬動又笨重，你還要什麼這些破爛木器，讓我拿去罷。我們小戶人家，用得著。”&lt;/p&gt;
&lt;p&gt;“我並沒有闊哩。我須賣了這些，再去……”&lt;/p&gt;
&lt;p&gt;“阿呀呀，你放了道台了，還說不闊？你現在有三房姨太太；出門便是八抬的大轎，還說不闊？嚇，什麼都瞞不過我。”&lt;/p&gt;
&lt;p&gt;我知道無話可說了，便閉了口，默默的站著。&lt;/p&gt;
&lt;p&gt;“阿呀阿呀，真是愈有錢，便愈是一毫不肯放鬆，愈是一毫不肯放鬆，便愈有錢……”圓規一面憤憤的迴轉身，一面絮絮的說，慢慢向外走，順便將我母親的一副手套塞在褲腰裡，出去了。&lt;/p&gt;
&lt;p&gt;此後又有近處的本家和親戚來訪問我。我一面應酬，偷空便收拾些行李，這樣的過了三四天。&lt;/p&gt;
&lt;p&gt;一日是天氣很冷的午後，我吃過午飯，坐著喝茶，覺得外面有人進來了，便回頭去看。我看時，不由的非常出驚，慌忙站起身，迎著走去。&lt;/p&gt;
&lt;p&gt;這來的便是閏土。雖然我一見便知道是閏土，但又不是我這記憶上的閏土了。他身材增加了一倍；先前的紫色的圓臉，已經變作灰黃，而且加上了很深的皺紋；眼睛也像他父親一樣，周圍都腫得通紅，這我知道，在海邊種地的人，終日吹著海風，大抵是這樣的。他頭上是一頂破氈帽，身上只一件極薄的棉衣，渾身瑟索著；手裡提著一個紙包和一支長煙管，那手也不是我所記得的紅活圓實的手，卻又粗又笨而且開裂，像是松樹皮了。&lt;/p&gt;
&lt;p&gt;我這時很興奮，但不知道怎麼說才好，只是說：&lt;/p&gt;
&lt;p&gt;“阿！閏土哥，——你來了？……”&lt;/p&gt;
&lt;p&gt;我接著便有許多話，想要連珠一般湧出：角雞，跳魚兒，貝殼，猹，……但又總覺得被什麼擋著似的，單在腦裡面迴旋，吐不出口外去。&lt;/p&gt;
&lt;p&gt;他站住了，臉上現出歡喜和淒涼的神情；動著嘴唇，卻沒有作聲。他的態度終於恭敬起來了，分明的叫道：&lt;/p&gt;
&lt;p&gt;“老爺！……”&lt;/p&gt;
&lt;p&gt;我似乎打了一個寒噤；我就知道，我們之間已經隔了一層可悲的厚障壁了。我也說不出話。&lt;/p&gt;
&lt;p&gt;他回過頭去說，“水生，給老爺磕頭。”便拖出躲在背後的孩子來，這正是一個廿年前的閏土，只是黃瘦些，頸子上沒有銀圈罷了。“這是第五個孩子，沒有見過世面，躲躲閃閃……”&lt;/p&gt;
&lt;p&gt;母親和宏兒下樓來了，他們大約也聽到了聲音。&lt;/p&gt;
&lt;p&gt;“老太太。信是早收到了。我實在喜歡的不得了，知道老爺回來……”閏土說。&lt;/p&gt;
&lt;p&gt;“阿，你怎的這樣客氣起來。你們先前不是哥弟稱呼麽？還是照舊：迅哥兒。”母親高興的說。&lt;/p&gt;
&lt;p&gt;“阿呀，老太太真是……這成什麼規矩。那時是孩子，不懂事……”閏土說著，又叫水生上來打拱，那孩子卻害羞，緊緊的只貼在他背後。&lt;/p&gt;
&lt;p&gt;“他就是水生？第五個？都是生人，怕生也難怪的；還是宏兒和他去走走。”母親說。&lt;/p&gt;
&lt;p&gt;宏兒聽得這話，便來招水生，水生卻鬆鬆爽爽同他一路出去了。母親叫閏土坐，他遲疑了一回，終於就了坐，將長煙管靠在桌旁，遞過紙包來，說：&lt;/p&gt;
&lt;p&gt;“冬天沒有什麼東西了。這一點乾青豆倒是自家曬在那裡的，請老爺……”&lt;/p&gt;
&lt;p&gt;我問問他的景況。他只是搖頭。&lt;/p&gt;
&lt;p&gt;“非常難。第六個孩子也會幫忙了，卻總是吃不夠……又不太平……什麼地方都要錢，沒有規定……收成又壞。種出東西來，挑去賣，總要捐幾回錢，折了本；不去賣，又只能爛掉……”&lt;/p&gt;
&lt;p&gt;他只是搖頭；臉上雖然刻著許多皺紋，卻全然不動，仿佛石像一般。他大約只是覺得苦，卻又形容不出，沉默了片時，便拿起煙管來默默的吸煙了。&lt;/p&gt;
&lt;p&gt;母親問他，知道他的家裡事務忙，明天便得回去；又沒有吃過午飯，便叫他自己到廚下炒飯吃去。&lt;/p&gt;
&lt;p&gt;他出去了；母親和我都嘆息他的景況：多子，饑荒，苛稅，兵，匪，官，紳，都苦得他像一個木偶人了。母親對我說，凡是不必搬走的東西，盡可以送他，可以聽他自己去揀擇。&lt;/p&gt;
&lt;p&gt;下午，他揀好了幾件東西：兩條長桌，四個椅子，一副香爐和燭臺，一桿抬秤。他又要所有的草灰（我們這裡煮飯是燒稻草的，那灰，可以做沙地的肥料），待我們啟程的時候，他用船來載去。&lt;/p&gt;
&lt;p&gt;夜間，我們又談些閑天，都是無關緊要的話；第二天早晨，他就領了水生回去了。&lt;/p&gt;
&lt;p&gt;又過了九日，是我們啟程的日期。閏土早晨便到了，水生沒有同來，卻只帶著一個五歲的女兒管船隻。我們終日很忙碌，再沒有談天的工夫。來客也不少，有送行的，有拿東西的，有送行兼拿東西的。待到傍晚我們上船的時候，這老屋裡的所有破舊大小粗細東西，已經一掃而空了。&lt;/p&gt;
&lt;p&gt;我們的船向前走，兩岸的青山在黃昏中，都裝成了深黛顏色，連著退向船後梢去。&lt;/p&gt;
&lt;p&gt;宏兒和我靠著船窗，同看外面模糊的風景，他忽然問道：&lt;/p&gt;
&lt;p&gt;“大伯！我們什麼時候回來？”&lt;/p&gt;
&lt;p&gt;“回來？你怎麼還沒有走就想回來了。”&lt;/p&gt;
&lt;p&gt;“可是，水生約我到他家玩去咧……”他睜著大的黑眼睛，癡癡的想。&lt;/p&gt;
&lt;p&gt;我和母親也都有些惘然，於是又提起閏土來。母親說，那豆腐西施的楊二嫂，自從我家收拾行李以來，本是每日必到的，前天伊在灰堆裡，掏出十多個碗碟來，議論之後，便定說是閏土埋著的，他可以在運灰的時候，一齊搬回家裡去；楊二嫂發見了這件事，自己很以為功，便拿了那狗氣殺（這是我們這裡養雞的器具，木盤上面有著柵欄，內盛食料，雞可以伸進頸子去啄，狗卻不能，只能看著氣死），飛也似的跑了，虧伊裝著這麼高低的小腳，竟跑得這樣快。&lt;/p&gt;
&lt;p&gt;老屋離我愈遠了；故鄉的山水也都漸漸遠離了我，但我卻並不感到怎樣的留戀。我只覺得我四面有看不見的高牆，將我隔成孤身，使我非常氣悶；那西瓜地上的銀項圈的小英雄的影像，我本來十分清楚，現在卻忽地模糊了，又使我非常的悲哀。&lt;/p&gt;
&lt;p&gt;母親和宏兒都睡著了。&lt;/p&gt;
&lt;p&gt;我躺著，聽船底潺潺的水聲，知道我在走我的路。我想：我竟與閏土隔絕到這地步了，但我們的後輩還是一氣，宏兒不是正在想念水生麽。我希望他們不再像我，又大家隔膜起來……然而我又不願意他們因為要一氣，都如我的辛苦展轉而生活，也不願意他們都如閏土的辛苦麻木而生活，也不願意都如別人的辛苦恣睢而生活。他們應該有新的生活，為我們所未經生活過的。&lt;/p&gt;
&lt;p&gt;我想到希望，忽然害怕起來了。閏土要香爐和燭臺的時候，我還暗地裡笑他，以為他總是崇拜偶像，什麼時候都不忘卻。現在我所謂希望，不也是我自己手製的偶像麽？只是他的願望切近，我的願望茫遠罷了。&lt;/p&gt;
&lt;p&gt;我在朦朧中，眼前展開一片海邊碧綠的沙地來，上面深藍的天空中掛著一輪金黃的圓月。我想：希望本是無所謂有，無所謂無的。這正如地上的路；其實地上本沒有路，走的人多了，也便成了路。&lt;/p&gt;
&lt;p&gt;一九二一年一月&lt;/p&gt;
</content:encoded><category>Articles</category><author>ChanZhaoYu</author></item></channel></rss>