跳到主要内容

禁用 SVGR 的 classname 前缀

· 阅读需 4 分钟
Gemini
Google Gemini
1adybug
子虚伊人

以下内容均为 Gemini 2.5 Pro 生成

告别恼人前缀:如何配置 SVGR 不再修改 SVG 的 className

在现代前端开发中,尤其是在 React 项目里,将 SVG 作为组件直接导入是一种优雅且高效的方式。强大的 SVGR 工具使得这一切变得轻而易举。但你可能很快会发现一个“小惊喜”:你精心命名的 SVG className 在转换后,被自动加上了 文件名_svg__ 这样的前缀。

比如,一个 className="icon-user" 可能会变成 my-icon_svg__icon-user

虽然这是 SVGR 出于好意做的保护措施,但在某些场景下,我们确实需要原汁原味的类名。今天,我们就来深入探讨这个问题背后的原因,并提供一个清晰的解决方案。

问题背后:为什么 SVGR 要添加前缀?

首先,要明确一点:这不是一个 Bug,而是一个 Feature。

在大型项目中,你可能会从不同的设计师或图标库中引入多个 SVG 文件。这些文件很有可能包含相同的类名,例如 <svg class="icon">。如果 SVGR 不做任何处理,直接将它们转换成组件,这些相同的类名就会在全局 CSS 环境中产生样式冲突样式污染,导致一个组件的样式意外地影响到另一个。

为了避免这种混乱,SVGR 内部集成的优化工具 SVGO (SVG Optimizer) 默认启用了一个名为 prefixIds 的插件。这个插件会自动扫描 SVG 内容,并为所有的 idclass 名称添加一个基于文件名的唯一前缀,从而确保其在项目中的唯一性。

解决方案:掌控 SVGO 配置

既然知道了问题的根源在于 SVGO 的 prefixIds 插件,那么解决方案就很明确了:覆盖这个插件的默认配置

我们可以通过 SVGR 的配置文件,告诉它我们不希望 prefixIds 插件来修改我们的 className

推荐方式:使用 svgr.config.js

在项目根目录下创建一个独立的配置文件,是管理 SVGR 行为最清晰的方式。

  1. 创建文件:在你的项目根目录(与 package.json同级)下创建 svgr.config.js 文件。

  2. 添加配置:将以下代码复制到文件中。

    // svgr.config.js
    module.exports = {
    // 传递给 SVGO 的配置
    svgoConfig: {
    plugins: [
    {
    // 插件名称,我们要修改的就是这个
    name: "prefixIds",
    // 插件参数
    params: {
    // 禁用为 ID 添加前缀
    prefixIds: false,
    // 禁用为 className 添加前缀 (这是解决问题的关键!)
    prefixClassNames: false,
    },
    },
    ],
    },
    }

    这段配置清晰地告诉 SVGR:“请在运行时,使用我提供的 svgoConfig。对于 prefixIds 这个插件,请将其 prefixClassNamesprefixIds 参数都设置为 false。”

备选方式:在 Webpack 中配置

如果你的项目使用 Webpack 并且已经配置了 @svgr/webpack,你也可以直接在 webpack.config.js 中完成设置。

// webpack.config.js
module.exports = {
// ... 其他 Webpack 配置
module: {
rules: [
// ... 其他 rules
{
test: /\.svg$/,
use: [
{
loader: "@svgr/webpack",
options: {
// 在 loader 的 options 中直接传入 svgoConfig
svgoConfig: {
plugins: [
{
name: "prefixIds",
params: {
prefixIds: false,
prefixClassNames: false,
},
},
],
},
},
},
],
},
],
},
}

使用 rsbuild 配置

import { defineConfig } from "@rsbuild/core"
import { pluginReact } from "@rsbuild/plugin-react"
import { pluginSvgr } from "@rsbuild/plugin-svgr"

export default defineConfig({
plugins: [
pluginReact(),
pluginSvgr({
svgrOptions: {
exportType: "default",
svgoConfig: {
plugins: [
{
name: "prefixIds",
params: {
prefixIds: false,
prefixClassNames: false,
},
},
],
},
},
}),
],
})

两种方式效果完全相同,选择哪一种取决于你的项目结构和个人偏好。

重要提醒:禁用前缀的潜在风险

当你禁用类名前缀后,避免样式冲突的责任就回到了你的肩上。请确保:

  1. 命名规范:在不同的 SVG 文件中,使用具有唯一性的类名。可以采用 BEM (block__element--modifier) 这样的命名约定来降低冲突概率。
  2. CSS 作用域:使用 CSS Modules, Styled Components, หรือ Tailwind CSS 等技术,它们能从根本上解决全局样式污染的问题。

总结

SVGR 默认添加 className 前缀是为了防止样式冲突,是一个贴心的设计。但通过简单的配置,我们可以轻松禁用它,以满足特定的开发需求。核心在于通过 svgoConfig 选项,将 prefixIds 插件的 prefixClassNames 参数设置为 false

希望这篇文章能帮助你更好地掌控你的 SVG 工作流,写出更干净、更可控的代码。Happy Coding!

回车键提交表单

· 阅读需 4 分钟
Claude
Anthropic Claude
1adybug
子虚伊人

以下内容均为 Claude 4 sonnet 生成

作为前端开发者,你是否遇到过这样的困惑:有时候在表单输入框中按下 Enter 键会自动提交表单,有时候却没有反应?今天我们就来深入了解一下 HTML 表单中 Enter 键的行为规律。

神奇的现象

让我们先来看几个有趣的例子:

  • 单个搜索框

    <form onsubmit="alert('搜索提交了!')">
    <input type="text" name="q" placeholder="请输入搜索关键词" />
    </form>

    在这个搜索框中按Enter键,即使没有提交按钮,表单也会自动提交。

  • 登录表单(无按钮)

    <form onsubmit="alert('登录提交了!')">
    <input type="text" name="username" placeholder="用户名" />
    <input type="password" name="password" placeholder="密码" />
    </form>

    在这个表单的任何输入框中按Enter键,什么都不会发生。

  • 登录表单(有按钮)

    <form onsubmit="alert('登录提交了!')">
    <input type="text" name="username" placeholder="用户名" />
    <input type="password" name="password" placeholder="密码" />
    <button type="submit">登录</button>
    </form>

    添加了提交按钮后,按Enter键又能正常提交了。

是不是感觉很神奇?这背后其实有着清晰的逻辑。

规律总结

HTML 表单的 Enter 键行为遵循以下规律:

  • 单输入字段规则

    当表单只包含一个文本输入字段时,无论是否有提交按钮,按Enter键都会触发表单提交。这主要是为了优化用户体验,特别是搜索框这类常见场景。

  • 多输入字段规则

    当表单包含多个输入字段时:

    • 有提交按钮:按Enter键会触发提交
    • 无提交按钮:按Enter键不会触发提交
  • 提交按钮的定义

    以下元素都被认为是提交按钮:

    • <input type="submit">
    • <button type="submit">
    • <button>(默认type为submit)

设计思路

这种看似复杂的行为设计,实际上体现了Web标准制定者的深思熟虑:

  • 用户体验优先

    • 搜索框这类单字段表单支持Enter快速提交,符合用户习惯
    • 复杂表单需要明确的提交按钮,避免误操作
  • 渐进增强

    • 简单功能(搜索)开箱即用
    • 复杂功能(多字段表单)需要明确的交互设计
  • 向后兼容

    • 保持与早期 HTML 标准的兼容性
    • 确保现有网站功能正常

实际应用建议

  • 搜索功能

    <!-- 推荐:简洁的搜索表单 -->
    <form action="/search" method="GET">
    <input type="search" name="q" placeholder="搜索商品" />
    </form>
  • 登录/注册表单

    <!-- 推荐:明确的提交按钮 -->
    <form action="/login" method="POST">
    <input type="text" name="username" placeholder="用户名" />
    <input type="password" name="password" placeholder="密码" />
    <button type="submit">登录</button>
    </form>
  • 复杂表单

    <!-- 推荐:多个操作按钮时明确指定类型 -->
    <form action="/profile" method="POST">
    <input type="text" name="name" placeholder="姓名" />
    <input type="email" name="email" placeholder="邮箱" />
    <button type="button" onclick="preview()">预览</button>
    <button type="submit">保存</button>
    </form>

注意事项

  1. 避免意外提交:对于复杂表单,确保有明确的提交按钮
  2. 用户体验:搜索类功能应该支持Enter键快速操作
  3. 测试充分:在不同浏览器中测试表单的Enter键行为
  4. 无障碍访问:确保键盘用户能够正常使用表单

总结

HTML表单的Enter键行为虽然看起来复杂,但背后的逻辑是为了平衡用户体验和功能安全。理解这些规律不仅能帮助我们写出更好的表单,也能避免一些常见的用户体验问题。

下次当你的表单出现意外的Enter键行为时,不妨回想一下这篇文章的内容,相信你会很快找到解决方案!


希望这篇文章对你有帮助。如果你有其他前端开发的疑问,欢迎继续交流!

自托管 Next.js

· 阅读需 6 分钟
1adybug
子虚伊人

使用 Next.js 开发网站后,在自己的服务器上部署比较麻烦,有两种比较简单的解决方案:

第一种不用过多介绍,这里主要介绍第二种方案:

  1. next.config.ts 中添加 output: "standalone"

  2. 安装 Docker

  3. 在项目根目录下创建 Dockerfile 文件

    # syntax=docker.io/docker/dockerfile:1

    FROM node:22-alpine AS base

    # Install dependencies only when needed
    FROM base AS deps

    WORKDIR /app

    # Install dependencies based on the preferred package manager
    COPY package.json ./
    RUN npm install --registry=https://registry.npmmirror.com

    # Rebuild the source code only when needed
    FROM base AS builder
    WORKDIR /app
    COPY --from=deps /app/node_modules ./node_modules
    COPY . .

    # Next.js collects completely anonymous telemetry data about general usage.
    # Learn more here: https://nextjs.org/telemetry
    ENV NEXT_TELEMETRY_DISABLED=1

    RUN npx prisma generate
    RUN npm run build

    # Production image, copy all the files and run next
    FROM base AS runner
    WORKDIR /app

    ENV NODE_ENV=production
    # Uncomment the following line in case you want to disable telemetry during runtime.
    ENV NEXT_TELEMETRY_DISABLED=1

    COPY --from=builder /app/public ./public

    # Automatically leverage output traces to reduce image size
    # https://nextjs.org/docs/advanced-features/output-file-tracing
    COPY --from=builder /app/.next/standalone ./
    COPY --from=builder /app/.next/static ./.next/static

    EXPOSE 3000

    ENV PORT=3000

    # server.js is created by next build from the standalone output
    # https://nextjs.org/docs/pages/api-reference/config/next-config-js/output
    ENV HOSTNAME="0.0.0.0"

    CMD ["node", "server.js"]
  4. 在项目根目录下创建 .dockerignore 文件,主要内容应该和 .gitignore 类似,忽略一些不需要的文件,比如 node_modules.next 目录之类的

  5. package.json 中添加 "build:docker": "docker build -t your-app-name ."

Dockerfile 中可以看出,自托管的核心就是 standalone 模式,这个模式下,Next.js 会生成一个 server.js 文件,这个文件是 Next.js 的入口文件,会自动监听 3000 端口,并启动 Next.js 应用。

其实,本质上最重要的产物就是 .next/standalone 目录,这个目录就是最终的根目录。所以最终目录结构应该是这样的:

.next/standaloneapp

.next/staticapp/.next/static

publicapp/public

只要明白了这个原理,我们就可以再实现一个 Next.js 的“安装”程序:

在项目根目录下创建一个 scripts/createInstaller.ts 文件,内容如下:

import { readdir, rm, writeFile } from "fs/promises"
import { resolve } from "path"

import { spawnAsync, zip } from "soda-nodejs"

const reg = /^--target=(windows|linux)$/

const target = (process.argv.find(item => reg.test(item))?.match(reg)?.[1] ?? "windows") as "windows" | "linux"

await rm("scripts/install.ts", { force: true })

await spawnAsync("bunx prisma generate", { shell: true, stdio: "inherit" })

await spawnAsync("bun run build:standalone", { shell: true, stdio: "inherit" })

const input = await readdir(".next/standalone")

await zip({ input, output: "../standalone.zip", cwd: ".next/standalone" })

const input2 = await readdir(".next/static")

await zip({ input: input2, output: "../static.zip", cwd: ".next/static" })

const input3 = await readdir("public")

await zip({ input: input3, output: "../.next/public.zip", cwd: "public" })

const script = `import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "fs/promises"
import { join, parse, resolve } from "path"
import { Readable } from "stream"
import { ReadableStream } from "stream/web"
import { styleText } from "util"
import { file } from "bun"
import { unzip } from "soda-nodejs"

import publicPath from "../.next/public.zip" with { type: "file" }
import standalonePath from "../.next/standalone.zip" with { type: "file" }
import staticPath from "../.next/static.zip" with { type: "file" }

${target === "windows" ? `import windowsPath from "../prisma/generated/query_engine-windows.dll.node" with { type: "file" }` : `import debianPath from "../prisma/generated/libquery_engine-debian-openssl-3.0.x.so.node" with { type: "file" }`}

const from = \`${resolve(".").replace(/\\/g, "\\\\")}\`.replace(/[\\\\/]$/, "")
const to = resolve(".").replace(/^[a-zA-Z]+/, m => m.toUpperCase()).replace(/[\\\\/]$/, "")
const from2 = encodeURIComponent(from)
const to2 = encodeURIComponent(to)
const from3 = encodeURIComponent(from + "/")
const to3 = encodeURIComponent(to + "/")
const from4 = encodeURIComponent(from + "\\\\")
const to4 = encodeURIComponent(to + "\\\\")

function escapeRegExp(str: string) {
return str.replace(/[.*+?^\${}()|[\\]\\\\]/g, "\\\\$&")
}

const reg = new RegExp(
\`\${escapeRegExp(from.replace(/[\\\\/]/g, "__PLACEHOLDER__")).replace(/__PLACEHOLDER__/g, "\\\\\\\\{0,3}[\\\\\\\\/]")}(\\\\\\\\{0,3}[\\\\\\\\/])?\`,
"gi",
)

async function replacePath(dir: string) {
const { name } = parse(dir)
if (name === "node_modules") return
const files = await readdir(dir)
for (const file of files) {
const path = join(dir, file)
const status = await stat(path)
if (status.isDirectory()) {
await replacePath(path)
} else {
if (/\\.([mc]?js|json)$/i.test(path)) {
const content = await readFile(path, "utf-8")
const newContent = content
.replace(reg, (m, p) => {
const prefix = m.match(/(\\\\{0,3})[\\\\/]/)?.[1] ?? ""
const split = \`\${prefix}\${m.includes("/") ? "/" : "\\\\"}\`
return \`\${to.replace(/[\\\\/]/g, split)}\${p ? split : ""}\`
})
.replaceAll(from2, to2)
.replaceAll(from3, to3)
.replaceAll(from4, to4)
await writeFile(path, newContent)
}
}
}
}

async function main() {
const publicStream = Readable.fromWeb(file(publicPath).stream() as ReadableStream)
const standaloneStream = Readable.fromWeb(file(standalonePath).stream() as ReadableStream)
const staticStream = Readable.fromWeb(file(staticPath).stream() as ReadableStream)
${target === "windows" ? `const windowsStream = Readable.fromWeb(file(windowsPath).stream() as ReadableStream)` : `const debianStream = Readable.fromWeb(file(debianPath).stream() as ReadableStream)`}

await rm(".temp", { recursive: true, force: true })
await mkdir(".temp", { recursive: true })

await writeFile(".temp/public.zip", publicStream)
await writeFile(".temp/standalone.zip", standaloneStream)
await writeFile(".temp/static.zip", staticStream)

await unzip({ input: ".temp/public.zip", output: ".temp/public" })
await unzip({ input: ".temp/standalone.zip", output: ".temp/standalone" })
await unzip({ input: ".temp/static.zip", output: ".temp/static" })

const dir = await readdir(".temp/standalone")

for (const item of dir) {
await rm(item, { recursive: true, force: true })
await rename(\`.temp/standalone/\${item}\`, item)
}

await rm("public", { recursive: true, force: true })
await rename(".temp/public", "public")
await rm(".next/static", { recursive: true, force: true })
await rename(".temp/static", ".next/static")

await replacePath(to)

await mkdir("prisma/generated", { recursive: true })
await ${target === "windows" ? `writeFile("prisma/generated/query_engine-windows.dll.node", windowsStream)` : `writeFile("prisma/generated/libquery_engine-debian-openssl-3.0.x.so.node", debianStream)`}

await rm(".temp", { recursive: true, force: true })

console.log(styleText("greenBright", "Task completed, the program will exit in 3 seconds..."))

setTimeout(() => 0, 3000)
}

main()
`

await writeFile("scripts/install.ts", script)

await spawnAsync(`bun build --compile --target=bun-${target}-x64 --minify --sourcemap --bytecode scripts/install.ts --outfile installer`, {
shell: true,
stdio: "inherit",
})

await rm("scripts/install.ts", { force: true })

await rm(".next/standalone.zip", { force: true })

await rm(".next/static.zip", { force: true })

await rm(".next/public.zip", { force: true })

package.json 中添加以下命令:

{
"scripts": {
"build:standalone": "npx cross-env NEXT_OUTPUT=standalone next build",
"build:windows": "bun scripts/createInstaller.ts --target=windows",
"build:linux": "bun scripts/createInstaller.ts --target=linux"
}
}

因为我的项目中涉及到 Prisma ,所以需要生成 Prisma 的客户端,所以需要先执行 prisma generate 命令,然后执行 build:standalone 命令,生成 standalone 模式下的 Next.js 应用。又因为最终的平台涉及 WindowsLinux ,所以需要生成两个版本的 .node 文件,需要在 schema.prisma 中添加如下内容:

generator client {
binaryTargets = ["windows", "debian-openssl-3.0.x"]
}

原理也很简单,就是将产物都使用 bun 打包,执行时再释放出来。

在 Prisma 中创建一对一映射关系

· 阅读需 1 分钟
1adybug
子虚伊人

Prisma 中,创建一对一映射关系非常简单。只需要在 schema.prisma 文件中添加以下代码:

model User {
id String @id @default(uuid())
name String
profiles Profile[] @relation("UserProfile")
currentProfileId String? @unique
currentProfile Profile? @relation("CurrentProfile", fields: [currentProfileId], references: [id])
}

model Profile {
id String @id @default(uuid())
name String
userId String
user User @relation("UserProfile", fields: [userId], references: [id])
currentUser User? @relation("CurrentProfile")
}

使用 Node.js 作为 Next.js 中间件的运行时

· 阅读需 1 分钟
1adybug
子虚伊人

Next.js 中,如果你在中间件使用了 Node.jsAPI, 那么可能会产生以下报错

Error: The edge runtime does not support Node.js 'crypto' module.

解决方法如下:

  1. 安装 next@canary

  2. next.config.ts 中添加以下配置

    import { NextConfig } from "next"

    const nextConfig: NextConfig = {
    experimental: {
    nodeMiddleware: true,
    },
    }

    export default nextConfig
  3. middleware.ts 中添加以下配置

    export const runtime = "nodejs"

当然,最好的做法是不使用 Node.js 作为中间件的运行时,而是使用 Edge 作为中间件的运行时,尽量使用 Edge 的 API 来实现中间件的功能。

在 Docker 中使用 Clash

· 阅读需 6 分钟
1adybug
子虚伊人

在图形化的操作系统中,我们可以使用 Clash Verge 来使用代理,但是在服务器上,就没有这么方便了。这时候我们可以使用 Clash 的核心 mihomo 来实现代理。然而,mihomo 的安装也是比较麻烦,所以我们可以使用 Docker 来安装 mihomo

安装教程

  1. 安装 Docker

  2. 创建 config.yaml 文件

    # url 里填写自己的订阅,名称不能重复
    proxy-providers:
    <你的订阅名称>:
    url: "<你的订阅链接>"
    type: http
    interval: 86400
    health-check: { enable: true, url: "https://www.gstatic.com/generate_204", interval: 300 }

    proxies:
    - name: "直连"
    type: direct
    udp: true

    mixed-port: 7890
    ipv6: true
    allow-lan: true
    unified-delay: false
    tcp-concurrent: true
    external-controller: 0.0.0.0:9090
    external-ui: ui
    external-ui-url: "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip"
    # 如果你的端口开放了所有 IP 访问,建议取消注释 secret 并设置密码
    # secret: <你的密码>

    geodata-mode: true
    geox-url:
    geoip: "https://mirror.ghproxy.com/https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip-lite.dat"
    geosite: "https://mirror.ghproxy.com/https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
    mmdb: "https://mirror.ghproxy.com/https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country-lite.mmdb"
    asn: "https://mirror.ghproxy.com/https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb"

    find-process-mode: strict
    global-client-fingerprint: chrome

    profile:
    store-selected: true
    store-fake-ip: true

    sniffer:
    enable: true
    sniff:
    HTTP:
    ports: [80, 8080-8880]
    override-destination: true
    TLS:
    ports: [443, 8443]
    QUIC:
    ports: [443, 8443]
    skip-domain:
    - "Mijia Cloud"
    - "+.push.apple.com"

    tun:
    enable: true
    stack: mixed
    dns-hijack:
    - "any:53"
    - "tcp://any:53"
    auto-route: true
    auto-redirect: true
    auto-detect-interface: true

    dns:
    enable: true
    ipv6: true
    enhanced-mode: fake-ip
    fake-ip-filter:
    - "*"
    - "+.lan"
    - "+.local"
    - "+.market.xiaomi.com"
    default-nameserver:
    - tls://223.5.5.5
    - tls://223.6.6.6
    nameserver:
    - https://doh.pub/dns-query
    - https://dns.alidns.com/dns-query

    proxy-groups:
    - name: 默认
    type: select
    proxies: [自动选择, 直连, 香港, 台湾, 日本, 新加坡, 美国, 其它地区, 全部节点]

    - name: Google
    type: select
    proxies: [默认, 香港, 台湾, 日本, 新加坡, 美国, 其它地区, 全部节点, 自动选择, 直连]

    - name: Telegram
    type: select
    proxies: [默认, 香港, 台湾, 日本, 新加坡, 美国, 其它地区, 全部节点, 自动选择, 直连]

    - name: Twitter
    type: select
    proxies: [默认, 香港, 台湾, 日本, 新加坡, 美国, 其它地区, 全部节点, 自动选择, 直连]

    - name: 哔哩哔哩
    type: select
    proxies: [默认, 香港, 台湾, 日本, 新加坡, 美国, 其它地区, 全部节点, 自动选择, 直连]

    - name: 巴哈姆特
    type: select
    proxies: [默认, 香港, 台湾, 日本, 新加坡, 美国, 其它地区, 全部节点, 自动选择, 直连]

    - name: YouTube
    type: select
    proxies: [默认, 香港, 台湾, 日本, 新加坡, 美国, 其它地区, 全部节点, 自动选择, 直连]

    - name: NETFLIX
    type: select
    proxies: [默认, 香港, 台湾, 日本, 新加坡, 美国, 其它地区, 全部节点, 自动选择, 直连]

    - name: Spotify
    type: select
    proxies: [默认, 香港, 台湾, 日本, 新加坡, 美国, 其它地区, 全部节点, 自动选择, 直连]

    - name: Github
    type: select
    proxies: [默认, 香港, 台湾, 日本, 新加坡, 美国, 其它地区, 全部节点, 自动选择, 直连]

    - name: 国内
    type: select
    proxies: [直连, 默认, 香港, 台湾, 日本, 新加坡, 美国, 其它地区, 全部节点, 自动选择]

    - name: 其他
    type: select
    proxies: [默认, 香港, 台湾, 日本, 新加坡, 美国, 其它地区, 全部节点, 自动选择, 直连]

    #分隔,下面是地区分组
    - name: 香港
    type: select
    include-all: true
    exclude-type: direct
    filter: "(?i)港|hk|hongkong|hong kong"

    - name: 台湾
    type: select
    include-all: true
    exclude-type: direct
    filter: "(?i)台|tw|taiwan"

    - name: 日本
    type: select
    include-all: true
    exclude-type: direct
    filter: "(?i)日|jp|japan"

    - name: 美国
    type: select
    include-all: true
    exclude-type: direct
    filter: "(?i)美|us|unitedstates|united states"

    - name: 新加坡
    type: select
    include-all: true
    exclude-type: direct
    filter: "(?i)(新|sg|singapore)"

    - name: 其它地区
    type: select
    include-all: true
    exclude-type: direct
    filter: "(?i)^(?!.*(?:🇭🇰|🇯🇵|🇺🇸|🇸🇬|🇨🇳|港|hk|hongkong|台|tw|taiwan|日|jp|japan|新|sg|singapore|美|us|unitedstates)).*"

    - name: 全部节点
    type: select
    include-all: true
    exclude-type: direct

    - name: 自动选择
    type: url-test
    include-all: true
    exclude-type: direct
    tolerance: 10

    rules:
    - GEOIP,lan,直连,no-resolve
    - GEOSITE,github,Github
    - GEOSITE,twitter,Twitter
    - GEOSITE,youtube,YouTube
    - GEOSITE,google,Google
    - GEOSITE,telegram,Telegram
    - GEOSITE,netflix,NETFLIX
    - GEOSITE,bilibili,哔哩哔哩
    - GEOSITE,bahamut,巴哈姆特
    - GEOSITE,spotify,Spotify
    - GEOSITE,CN,国内
    - GEOSITE,geolocation-!cn,其他

    - GEOIP,google,Google
    - GEOIP,netflix,NETFLIX
    - GEOIP,telegram,Telegram
    - GEOIP,twitter,Twitter
    - GEOIP,CN,国内
    - MATCH,其他
  3. 创建 docker-compose.yaml 文件

    version: "3"

    services:
    metacubexd:
    container_name: metacubexd
    image: ghcr.io/metacubex/metacubexd
    restart: always
    ports:
    - "9097:80"

    mihomo:
    container_name: mihomo
    image: docker.io/metacubex/mihomo:latest
    restart: always
    ports:
    # 尽量使用 127.0.0.1 而不是 0.0.0.0,否则可能让代理暴露在公网
    - "127.0.0.1:7890:7890"
    - "9090:9090"
    volumes:
    - <保存 config.yaml 文件的目录>:/root/.config/mihomo
  4. 启动容器 docker compose up -d

使用教程

在需要使用代理的地方,设置代理为 http://127.0.0.1:7890 即可。

比如为终端设置代理:

  1. 编辑 ~/.bashrc 文件,添加以下内容:

    export http_proxy="http://127.0.0.1:7890"
    export https_proxy="http://127.0.0.1:7890"
    export all_proxy="socks5://127.0.0.1:7890"
    export no_proxy="localhost,127.0.0.1,*.local"
  2. 保存并退出,然后执行 source ~/.bashrc 使配置生效。

  3. 使用 curl https://www.google.com 测试是否成功。

注意事项

  1. 尽量不要使用 MobaXterm 等终端创建或者编辑配置文件,可以会出现默认编码非 UTF-8 或者缺少字符集的问题,导致配置文件无法正常使用。
  2. config.yamlexternal-controller 的端口和 docker-compose.yamlmetacubexd 的端口都需要在防火墙中开放,建议只允许特定 IP 访问,如果全放开的话,建议取消注释 secret 并设置密码。
  3. docker-compose.yamlmihomovolumes 需要指向 config.yaml 文件在主机的目录而非文件。
  4. 如果 mihomo 的容器的日志中出现了下载文件的错误,请到 meta-rules-dat 仓库中手动下载文件并保存到 config.yaml 所在的目录

代码片段关键字替换

· 阅读需 1 分钟
1adybug
子虚伊人

有的时候我们需要在代码片段同时进行两种替换,比如下方,我们要将 api 移除,并将其转换为驼峰式命名法:

烤肉串式命名法 (Kebab-case)驼峰式命名法 (CamelCase)
api.my-filemyFile
api.another-example-fileanotherExampleFile
api.yet-another-oneyetAnotherOne

这种替换的难点在我们无法确定 - 的数量为多少

这时候可以使用以下代码实现:

const prefix = "api\\\\.([^-]+)"

const unit = "-([^-])([^-]*)"

const max = 6

const regExp = Array(max)
.fill(prefix)
.map((item, index) => ["^", item, ...Array(index).fill(unit), "$"].join(""))
.join("|")

const replacer = Array(max)
.fill(0)
.reduce(
(acc, item, index) => [
...acc,
[
(acc.at(-1)?.at(-1) ?? 0) + 1,
...Array(index * 2)
.fill(0)
.map((item2, index2) => (acc.at(-1)?.at(-1) ?? 0) + 1 + index2 + 1),
],
],
[],
)
.map(item => item.map((item2, index2) => (index2 % 2 === 0 ? `$${item2}` : `$\{${item2}:/upcase}`)).join(""))
.join("")

const body = `\${TM_FILENAME_BASE/${regExp}/${replacer}/}`

console.log(body)

想要最大支持多少个 - ,只需要修改 max 的值即可。

重新学习 React Router

· 阅读需 2 分钟
1adybug
子虚伊人

前言

React Router 在推出 Remix 后变成了一个全栈框架,在 V7 版本之后,更是将 React RouterRemix 合并在一起,提供了一个全栈的解决方案。

常见问题

  1. loaderclientLoader 有什么区别?

    • 执行环境

      • loader: 在服务器端执行
      • clientLoader: 在客户端(浏览器)执行
    • 运行时机

      • loader: 在页面初始加载和每次导航时在服务器上运行
      • clientLoader: 在客户端导航时运行,首次页面加载时不执行
    • 数据获取能力

      • loader: 可以访问服务器资源(数据库、文件系统、内部API等)
      • clientLoader: 只能访问浏览器可用的资源(如公共API、localStorage等)
    • 安全性

      • loader: 可以包含敏感逻辑和凭证,因为代码不会发送到客户端
      • clientLoader: 所有代码会发送到浏览器,不应包含敏感信息
    • 用途场景

      • loader: 适用于需要服务器权限或敏感数据的操作
      • clientLoader: 适用于提升客户端导航体验,减轻服务器负担的场景

    这两个函数通常可以配合使用:loader 用于初始数据加载和服务器相关操作,clientLoader 用于优化后续的客户端导航体验。

    loader 将在你首次打开页面就是这个路由时执行,clientLoader 将在你由其他路由导航到这个路由时执行。clientLoader 在首次页面加载时不会执行。在 clientLoader 中,你可以使用 serverLoader 调用 loader

    import type { Route } from "./+types/product"

    // route("products/:pid", "./product.tsx");
    import { fakeDb } from "../db"

    export async function loader({ params }: Route.LoaderArgs) {
    return fakeDb.getProduct(params.pid)
    }

    export async function clientLoader({ serverLoader, params }: Route.ClientLoaderArgs) {
    const res = await fetch(`/api/products/${params.pid}`)
    const serverData = await serverLoader()
    return { ...serverData, ...res.json() }
    }

    export default function Product({ loaderData }: Route.ComponentProps) {
    const { name, description } = loaderData

    return (
    <div>
    <h1>{name}</h1>
    <p>{description}</p>
    </div>
    )
    }