普通视图

发现新文章,点击刷新页面。
昨天以前首页

一个专业的前端如何在国内安装 `pnpm`

作者 Legend80s
2025年10月10日 21:57

the-nutcracker-ballet.jpeg

本文以 macOS 为例,但思路也适用于 Windows 系统。

对于 pnpm 我们有多种安装方式,可以使用现有的包管理器比如 npm npm i -g pnpm

但是 npm 一般是通过 nvm 安装的,如果 nvm 切换到其他 node.js 版本,则无法使用 pnpm(command not found: pnpm),还得继续安装一遍,颇为麻烦。

所以 pnpm 官方一般推荐通过 shell 脚本的方式安装,以下安装命令来自 pnpm 官网:pnpm.io/installatio…

curl -fsSL https://get.pnpm.io/install.sh | sh -

但是如果直接运行我们会发现超时以及报错。通过下载安装脚本 get.pnpm.io/install.sh 和搜索关键词 github 我们在 94 行发现:

archive_url="https://github.com/pnpm/pnpm/releases/download/v${version}/pnpm-${platform}-${arch}"

原因很清楚了国内无法访问 github,修复也很简单找一个 proxy 即可,这里我用的是 gh-proxy.com/ (2025-09-27 可用):

第一步:移除无用包

可选。主要是为了删除无用包,减少磁盘浪费,以及避免冲突。切换到曾经安装过 pnpm 的 node.js 版本。

nvm use 20
npm uninstall -g pnpm

第二步:替换成可用 proxy

将下载到本地的 install.sh 修改成如下:

archive_url="https://gh-proxy.com/https://github.com/pnpm/pnpm/releases/download/v${version}/pnpm-${platform}-${arch}"

然后执行:

sh install.sh

等待 10s 即可安装成功,并且 .zshrc 文件末尾将自动增加:

# pnpm
export PNPM_HOME="/Users/legend80s/Library/pnpm"
case ":$PATH:" in
  *":$PNPM_HOME:"*) ;;
  *) export PATH="$PNPM_HOME:$PATH" ;;
esac
# pnpm end

重新开一个 terminal 让更新后的 .zshrc 生效或者直接 source .zshrc 然后,

试试 pnpm -v 输出 10.17.1(2025-09-27)。

再试试 pnpx pnpx ydd -e -s -c=a hefty

一样成功 🎉。

全栈视角:从零构建一个现代化的 Todo 应用

作者 4_0_4
2025年10月10日 16:47

Todo 应用是学习全栈开发的“Hello World”,但它能完美地串联起现代 Web 开发的所有核心概念。我们将构建一个具备增删改查、实时更新等功能的单页面应用。

tRPC-×-Drizzle-×-Next-js-Todo-10-09-2025_05_55_PM

技术选型

  • Next.js 15: 用于构建服务器渲染的 React 应用。
  • TypeScript: 增加类型检查,提升代码质量。
  • Tailwind CSS: 用于快速构建响应式界面。
  • Drizzle ORM: 作为 ORM 管理数据库。
  • PostgreSQL: 作为数据库存储 Todo 项。
  • tRPC: 用于构建类型安全的 API。
  • Zod: 用于验证和解析 API 请求参数。
  • @tanstack/react-query: 用于管理客户端数据缓存和状态。

开发环境

  • Node.js 18+: 确保安装最新版本的 Node.js。
  • npm: 或使用 Yarn/PNPM 作为包管理器。
  • PostgreSQL: 安装并运行 PostgreSQL 数据库。
  • Drizzle ORM: 全局安装 Drizzle ORM 命令行工具。

用 Docker 起本地 PG(可选):

docker run --name pg13 -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=app -p 5432:5432 -d postgres:13

连接串(示例):postgres://postgres:postgres@localhost:5432/app

项目结构

.
├── drizzle
│   ├── meta
│   │   ├── 0000_snapshot.json
│   │   └── _journal.json
│   └── 0000_lively_gravity.sql
├── src
│   ├── app
│   │   ├── api/trpc/[trpc]
│   │   │   └── route.ts
│   │   ├── globals.css
│   │   ├── layout.tsx
│   │   ├── page.tsx
│   │   └── trpc-provider.tsx
│   ├── server
│   │   ├── db
│   │   │   ├── index.ts
│   │   │   └── schema.ts
│   │   ├── routers
│   │   │   └── todo.ts
│   │   ├── root.ts
│   │   └── trpc.ts
│   └── trpc
│       └── react.ts
├── README.md
├── biome.json
├── drizzle.config.ts
├── next.config.ts
├── package-lock.json
├── package.json
├── pnpm-lock.yaml
├── postcss.config.mjs
└── tsconfig.json

项目初始化

npx create-next-app@latest todo --ts
cd todo

选择 App Router、ESLint、Tailwind。

安装与配置 PostgreSQL、Drizzle 及相关依赖

pnpm add drizzle-orm drizzle-kit pg zod @trpc/server @trpc/client @trpc/react-query @tanstack/react-query superjson

新增开发脚本(package.json)

{
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build --turbopack",
    "start": "next start",
    "lint": "biome check",
    "format": "biome format --write",
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:studio": "drizzle-kit studio"
  }
}

环境变量(.env):

DATABASE_URL=postgres://postgres:postgres@localhost:5432/app
NODE_ENV=development

Drizzle 配置(drizzle.config.ts):

import type { Config } from "drizzle-kit";

export default {
  schema: "./src/server/db/schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
} satisfies Config;

数据库连接(src/server/db/index.ts):

import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool);

数据表

Schema(src/server/db/schema.ts):

import { pgTable, serial, varchar, boolean, timestamp } from "drizzle-orm/pg-core";

export const todos = pgTable("todos", {
  id: serial("id").primaryKey(),
  title: varchar("title", { length: 200 }).notNull(),
  completed: boolean("completed").notNull().default(false),
  createdAt: timestamp("created_at").notNull().defaultNow(),
});

生成并执行迁移:

pnpm db:generate
pnpm db:migrate

执行后会在 drizzle/ 目录看到迁移 SQL,并把表建好。 (可选)打开可视化:

pnpm db:studio

初始化 tRPC(服务器与客户端)

  1. 服务器端基础(src/server/trpc.ts):

    import { initTRPC } from "@trpc/server";
    import { ZodError } from "zod";
    
    const t = initTRPC.context<{}>().create({
      errorFormatter({ shape, error }) {
        return {
          code: -1,
          message:
            error.cause instanceof ZodError
              ? error.cause.issues.map((issue) => issue.message).join("")
              : shape.message,
          data: null,
        };
      },
    });
    
    export const publicProcedure = t.procedure;
    export const router = t.router;
    
  2. tRPC 适配 Next.js App Router(src/app/api/trpc/[trpc]/route.ts):

    import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
    import { appRouter } from "@/server/root";
    
    const handler = (req: Request) => {
      return fetchRequestHandler({
        endpoint: "/api/trpc",
        req,
        router: appRouter,
      });
    };
    
    export { handler as GET, handler as POST };
    
  3. tRPC todo 路由(src/server/routers/todo.ts):

    import { desc, eq } from "drizzle-orm";
    import { z } from "zod";
    import { db } from "@/server/db";
    import { todos } from "@/server/db/schema";
    import { publicProcedure, router } from "@/server/trpc";
    
    export const todoRouter = router({
      list: publicProcedure.query(async () => {
        return db.select().from(todos).orderBy(desc(todos.createdAt));
      }),
    
      create: publicProcedure
        .input(z.object({ title: z.string().min(1).max(200) }))
        .mutation(async ({ input }) => {
          const [row] = await db
            .insert(todos)
            .values({ title: input.title })
            .returning();
          return row;
        }),
    
      toggle: publicProcedure
        .input(z.object({ id: z.number(), completed: z.boolean() }))
        .mutation(async ({ input }) => {
          const [row] = await db
            .update(todos)
            .set({ completed: input.completed })
            .where(eq(todos.id, input.id))
            .returning();
          return row;
        }),
    
      remove: publicProcedure
        .input(z.object({ id: z.number() }))
        .mutation(async ({ input }) => {
          await db.delete(todos).where(eq(todos.id, input.id));
          return { ok: true };
        }),
    });
    
  4. tRPC 根接口(src/server/root.ts):

    import { todoRouter } from "./routers/todo";
    import { router } from "./trpc";
    
    export const appRouter = router({
      todo: todoRouter,
    });
    
    export type AppRouter = typeof appRouter;
    
  5. 客户端(src/trpc/react.ts):

    import { createTRPCReact } from "@trpc/react-query";
    import type { AppRouter } from "@/server/root";
    
    export const trpc = createTRPCReact<AppRouter>();
    
  6. 创建 tRPC Provider src/app/trpc-provider.tsx

    "use client";
    
    import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
    import { httpBatchLink } from "@trpc/client";
    import { type ReactNode, useState } from "react";
    import { trpc } from "@/trpc/react";
    
    export function TRPCProviders({ children }: { children: ReactNode }) {
      const [queryClient] = useState(() => new QueryClient());
      const [trpcClient] = useState(() =>
        trpc.createClient({
          links: [
            httpBatchLink({
              url: "/api/trpc",
            }),
          ],
        }),
      );
    
      return (
        <trpc.Provider client={trpcClient} queryClient={queryClient}>
          <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
        </trpc.Provider>
      );
    }
    
  7. 在 app/layout.tsx 中挂载 Provider:

    import type { Metadata } from "next";
    import { Geist, Geist_Mono } from "next/font/google";
    import "./globals.css";
    import { TRPCProviders } from "./trpc-provider";
    
    const geistSans = Geist({
      variable: "--font-geist-sans",
      subsets: ["latin"],
    });
    
    const geistMono = Geist_Mono({
      variable: "--font-geist-mono",
      subsets: ["latin"],
    });
    
    export const metadata: Metadata = {
      title: "Create Next App",
      description: "Generated by create next app",
    };
    
    export default function RootLayout({
      children,
    }: Readonly<{
      children: React.ReactNode;
    }>) {
      return (
        <html lang="en">
          <body
            className={`${geistSans.variable} ${geistMono.variable} antialiased`}
          >
            <TRPCProviders>{children}</TRPCProviders>
          </body>
        </html>
      );
    }
    

Todo CRUD 端到端打通

  1. 前端页面(src/app/page.tsx):
    "use client";
    
    import { useState } from "react";
    import { trpc } from "@/trpc/react";
    
    export default function HomePage() {
      const utils = trpc.useUtils();
      const [title, setTitle] = useState("");
    
      const { data: todos = [], isLoading } = trpc.todo.list.useQuery();
    
      const createTodo = trpc.todo.create.useMutation({
        onSuccess: () => utils.todo.list.invalidate(),
      });
    
      const toggleTodo = trpc.todo.toggle.useMutation({
        onSuccess: () => utils.todo.list.invalidate(),
      });
    
      const removeTodo = trpc.todo.remove.useMutation({
        onSuccess: () => utils.todo.list.invalidate(),
      });
    
      const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        if (!title.trim()) return;
        createTodo.mutate({ title });
        setTitle("");
      };
    
      return (
        <main className="mx-auto max-w-xl p-6 space-y-4">
          <h1 className="text-2xl font-bold">tRPC × Drizzle × Next.js Todo</h1>
    
          <form className="flex gap-2" onSubmit={handleSubmit}>
            <input
              className="flex-1 border rounded px-3 py-2"
              placeholder="添加待办..."
              value={title}
              onChange={(e) => setTitle(e.target.value)}
            />
            <button
              className="px-4 py-2 rounded bg-black text-white disabled:opacity-50"
              type="submit"
              disabled={createTodo.isPending}
            >
              {createTodo.isPending ? "添加中..." : "添加"}
            </button>
          </form>
    
          {isLoading ? (
            <p>加载中...</p>
          ) : (
            <ul className="space-y-2">
              {todos.map((t) => (
                <li
                  key={t.id}
                  className="flex items-center gap-3 border p-2 rounded"
                >
                  <input
                    type="checkbox"
                    checked={t.completed}
                    onChange={(e) =>
                      toggleTodo.mutate({ id: t.id, completed: e.target.checked })
                    }
                  />
                  <span className={t.completed ? "line-through text-gray-500" : ""}>
                    {t.title}
                  </span>
                  <button
                    className="ml-auto text-red-600"
                    onClick={() => removeTodo.mutate({ id: t.id })}
                  >
                    删除
                  </button>
                </li>
              ))}
            </ul>
          )}
        </main>
      );
    }
    

完整项目地址

github.com/letconstvar…

深入理解 JavaScript 函数:从基础到高阶应用

作者 San30
2025年10月9日 18:36

函数是 JavaScript 的核心概念,掌握函数意味着真正理解 JavaScript 编程的精髓

1. 函数基础:减少重复代码的利器

1.1 函数的定义与调用

// 函数声明
function sayHello() {
    console.log("Hello, World!");
}

// 函数调用
sayHello(); // 输出: Hello, World!

函数提升现象值得注意:

// 此处可以正常调用,因为函数声明会提升
test(); // 输出: "函数已调用"

function test() {
    console.log("函数已调用");
}

1.2 参数与返回值

function calculateSum(a, b) {
    return a + b;
}

const result = calculateSum(5, 3);
console.log(result); // 输出: 8

// 未传递参数的情况
function greet(name) {
    if (name === undefined) {
        return "Hello, stranger!";
    }
    return `Hello, ${name}!`;
}

console.log(greet()); // 输出: Hello, stranger!

2. 作用域与闭包:JavaScript 的"结界"

2.1 作用域链理解

var globalVar = "我是全局变量";

function outer() {
    var outerVar = "我是外部函数变量";
    
    function inner() {
        var innerVar = "我是内部函数变量";
        console.log(globalVar);    // 可以访问
        console.log(outerVar);     // 可以访问
        console.log(innerVar);     // 可以访问
    }
    
    inner();
    // console.log(innerVar); // 错误:无法访问内部函数变量
}

outer();

2.2 立即执行函数(IIFE)

// 传统写法 - 会污染全局作用域
var count = 0;
function increment() {
    return ++count;
}

// IIFE写法 - 不会污染全局作用域
const counter = (function() {
    let count = 0;
    
    return {
        increment: function() {
            return ++count;
        },
        decrement: function() {
            return --count;
        },
        getCount: function() {
            return count;
        }
    };
})();

console.log(counter.getCount()); // 0
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2

3. 函数的本质:一等公民的对象

3.1 函数作为值传递

// 函数赋值给变量
const multiply = function(a, b) {
    return a * b;
};

// 函数作为参数传递
function operate(a, b, operation) {
    return operation(a, b);
}

console.log(operate(5, 3, multiply)); // 输出: 15

// 函数作为返回值
function createMultiplier(factor) {
    return function(number) {
        return number * factor;
    };
}

const double = createMultiplier(2);
console.log(double(5)); // 输出: 10

3.2 this 关键字的奥秘

const person = {
    name: "张三",
    sayName: function() {
        console.log(this.name);
    }
};

person.sayName(); // 输出: "张三"

// this 绑定丢失的情况
const sayName = person.sayName;
sayName(); // 输出: undefined (在严格模式下会报错)

4. 构造函数:创建对象的工厂

4.1 构造函数的使用

function Person(name, age) {
    this.name = name;
    this.age = age;
    
    this.introduce = function() {
        return `大家好,我是${this.name},今年${this.age}岁`;
    };
}

// 使用 new 关键字创建实例
const person1 = new Person("李四", 25);
const person2 = new Person("王五", 30);

console.log(person1.introduce()); // 大家好,我是李四,今年25岁
console.log(person2.introduce()); // 大家好,我是王五,今年30岁

4.2 new.target 的应用

function Vehicle(type) {
    if (!new.target) {
        throw new Error("必须使用 new 关键字调用构造函数");
    }
    this.type = type;
}

// 正确使用
const car = new Vehicle("汽车");

// 错误使用
// const bike = Vehicle("自行车"); // 抛出错误

5. 递归:函数自我调用的艺术

5.1 经典递归案例:斐波那契数列

function fibonacci(n) {
    if (n <= 0) return 0;
    if (n === 1) return 1;
    
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// 优化版本:使用缓存
function fibonacciMemo(n, memo = {}) {
    if (n in memo) return memo[n];
    if (n <= 0) return 0;
    if (n === 1) return 1;
    
    memo[n] = fibonacciMemo(n - 1, memo) + fibonacciMemo(n - 2, memo);
    return memo[n];
}

console.log(fibonacci(7)); // 13
console.log(fibonacciMemo(50)); // 12586269025 (优化后可以快速计算)

5.2 汉诺塔问题的递归解法

function hanoiTower(source, auxiliary, target, disks) {
    if (disks === 1) {
        console.log(`将盘子从 ${source} 移动到 ${target}`);
        return;
    }
    
    // 将 n-1 个盘子从源柱移动到辅助柱
    hanoiTower(source, target, auxiliary, disks - 1);
    
    // 将最大的盘子从源柱移动到目标柱
    console.log(`将盘子从 ${source} 移动到 ${target}`);
    
    // 将 n-1 个盘子从辅助柱移动到目标柱
    hanoiTower(auxiliary, source, target, disks - 1);
}

// 测试 3 个盘子的汉诺塔
hanoiTower('A', 'B', 'C', 3);

执行过程可视化:

A->C
A->B
C->B
A->C
B->A
B->C
A->C

6. 执行栈:理解函数调用的底层机制

通过下面的例子理解执行栈的工作原理:

<!DOCTYPE html>
<html>
<head>
    <title>执行栈演示</title>
</head>
<body>
    <script>
        function A() {
            console.log("A开始执行");
            B();
            console.log("A执行结束");
        }

        function B() {
            console.log("B开始执行");
            C();
            console.log("B执行结束");
        }

        function C() {
            console.log("C开始执行");
            console.log("C执行结束");
        }

        console.log("全局开始");
        A();
        console.log("全局结束");
    </script>
</body>
</html>

控制台输出顺序:

全局开始
A开始执行
B开始执行
C开始执行
C执行结束
B执行结束
A执行结束
全局结束

7. 高级技巧与最佳实践

7.1 尾递归优化

// 普通递归 - 可能导致栈溢出
function factorial(n) {
    if (n === 1) return 1;
    return n * factorial(n - 1);
}

// 尾递归优化版本
function factorialTail(n, accumulator = 1) {
    if (n === 1) return accumulator;
    return factorialTail(n - 1, n * accumulator);
}

console.log(factorial(5)); // 120
console.log(factorialTail(5)); // 120

7.2 函数柯里化

function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        } else {
            return function(...args2) {
                return curried.apply(this, args.concat(args2));
            };
        }
    };
}

// 使用柯里化
function add(a, b, c) {
    return a + b + c;
}

const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6

总结

JavaScript 函数是语言的灵魂,从基础的参数传递到高级的闭包、递归等概念,理解函数意味着掌握了 JavaScript 编程的精髓。通过本文的讲解和代码示例,希望你能:

  1. 深入理解函数的作用域和闭包机制
  2. 掌握构造函数和原型链的关系
  3. 学会使用递归解决复杂问题
  4. 理解 JavaScript 的执行机制

函数的学习是一个循序渐进的过程,多实践、多思考,你会发现 JavaScript 函数的世界既深邃又美妙!

NestJS入门(1)——TODO项目创建及概念初步了解

2025年10月9日 09:19

通过实现一个TODO需求来了解NestJS,需要实现的功能包括:

  1. 用户注册、登录
  2. 备忘录创建、修改、查询、删除

创建项目

❗注意:Node版本需≥20

全局安装 Nest CLI 脚手架,并初始化项目,项目名称为 DoraemonNotebook (哆啦A梦记事本)

$ npm i -g @nestjs/cli 
$ nest new DoraemonNotebook

了解项目结构及基础概念

创建完成后项目的目录结构如图:

image.png

其中核心文件为 src 目录下的几个文件:

文件 说明
app.controller.spec.ts 控制器的单元测试
app.controller.ts 一个具有单个路由的基本控制器
app.module.ts 应用程序的根模块
app.service.ts 一个具有单个方法的基本服务
main.ts 应用程序的入口文件,它使用核心函数 NestFactory 来创建 Nest 应用程序实例

根据这几个核心文件及数据流向我们来了解一下NestJS中的几个核心概念,在入口文件中我们首先会接触到根模块 AppModule

模块

image.png

Nest项目中至少需要一个根模块,即目前的 app.module.ts 文件,它是 Nest 构建应用程序的起点。我们在组织程序功能架构时推荐使用模块作为组织组件的方式,将一组密切相关的功能作为一个模块,方便管理。

在模块中我们可以看到引入了 AppControllerAppService,接下来我们先了解控制器(AppController)的概念。

控制器

image.png

控制器类似于前端的路由,它告诉程序请求将用哪个控制器处理,一个控制器可以具有多个路由,每个路由执行不同的操作。 具体的操作实现也就是业务逻辑我们一般放在提供者(AppService)中实现。

提供者

提供者(Provider)负责封装特定功能(如业务逻辑、数据访问、工具函数等),并可通过依赖注入(@Injectable())的方式在控制器或其他服务中使用。

默认情况下提供者是单例模式,全局共享实例状态,也可以通过声明作用域将生命周期改为请求作用域(@Injectable({scope: Scope.REQUEST}))、临时作用域(@Injectable({scope: Scope.TRANSIENT}))。

其他

本章只是简单了解NestJS的目录及架构,其实还有很多概念没介绍到,比如装饰器、依赖注入、中间件、异常过滤器、拦截器等,后续的文章中我们将通过在完成实际TODO需求中一一使用并详细讲解。

项目启动

项目开发过程中我们可以通过以下命令启动程序:

$ npm run start:dev

此命令将监视您的文件,自动重新编译并重新加载服务器。通过浏览器访问 http://localhost:3000,我们可以看到返回的 Hello World! 消息。

Electron IPC 自动化注册方案:模块化与热重载的完美结合

作者 _AaronWong
2025年10月9日 07:21

背景

在 Electron 应用开发中,我们经常需要在主进程和渲染进程之间进行通信。随着应用功能不断增加,IPC处理器的数量也会急剧增长。传统的手动注册方式会导致代码臃肿、难以维护。本文将介绍一种自动化的 IPC 处理器注册机制。

核心实现

1. 自动化扫描与注册

首先,我们来看核心的注册机制实现:

// project/electron/ipc/index.js
const path = require("path");
const { readdirSync } = require("fs");
const importSync = require("import-sync");

const getIcpMainHandler = () => {
    let allHandler = {};
    // 扫描 handlers 目录下的所有文件
    const dirs = readdirSync(path.join(__dirname, "handlers"), "utf8");
    
    for (const file of dirs) {
        const filePath = path.join(__dirname, "handlers", file);
        const handlersTemp = importSync(filePath);
        let handlers = {}
        
        // 分析每个导出的处理器
        for (const key in handlersTemp) {
            const handler = handlersTemp[key];
            let handlerType = Object.prototype.toString.call(handler);
            const match = handlerType.match(/^\[object (\w+)\]$/);
            handlerType = match[1];
            
            handlers[key] = {
                key,
                type: handlerType,
                val: handler,
            };
            
            allHandler = {
                ...allHandler,
                ...handlers,
            };
        }
    }
    return allHandler;    
}

module.exports.registerHandlerForIcpMain = () => {
    const ipcMainHandlers = getIcpMainHandler();
    // 只执行函数类型的处理器
    for (const key in ipcMainHandlers) {
        const handler = ipcMainHandlers[key];
        if (handler.type.indexOf("Function") > -1) {
            handler.val()
        }
    }
};

2. 模块化的处理器定义

将不同功能的 IPC 处理器分类到不同的文件中:

// project/electron/ipc/handlers/file.js
const { ipcMain } = require("electron");

module.exports.fileHander = () => {
    // 处理文件夹清理请求
    ipcMain.handle("clear-folder", async (event, path) => {
        // 具体的文件操作逻辑
        console.log("Clearing folder:", path);
    });
    
    // 可以注册更多的文件相关处理器
    ipcMain.handle("read-file", async (event, filePath) => {
        // 文件读取逻辑
    });
}
// project/electron/ipc/handlers/win.js
const { ipcMain, BrowserWindow } = require("electron");

module.exports.winHander = () => {
    // 处理窗口打开请求
    ipcMain.on('open-window', async (event) => {
        // 创建新窗口的逻辑
        const win = new BrowserWindow({ width: 800, height: 600 });
    });
    
    // 窗口管理相关处理器
    ipcMain.on('close-window', async (event) => {
        // 窗口关闭逻辑
    });
}

3. 主进程集成

在主进程启动时注册所有 IPC 处理器:

// project/electron/main.js
const { registerHandlerForIcpMain } = require("./ipc/index.js")

app.whenReady().then(() => {
    // 自动注册所有 IPC 处理器
    registerHandlerForIcpMain();
    
    // ... 其他初始化逻辑
});

4. 构建配置优化

为了支持开发时的热重载,我们在 Vite 配置中动态扫描 IPC 文件:

// vite.config.js
import fs from "fs"
import path from "path"

// 递归扫描目录(支持子目录)
const scanDeep = (dir) => {
    let results = []
    const list = fs.readdirSync(dir, { withFileTypes: true })

    for (const item of list) {
        const fullPath = path.join(dir, item.name)
        if (item.isDirectory()) {
            results = results.concat(scanDeep(fullPath))
        } else if (item.isFile() && [".js", ".cjs", ".mjs"].includes(path.extname(item.name))) {
            results.push(fullPath)
        }
    }
    return results
}

// 生成 Electron 配置
export const getElectronConfig = () => {
    const electronDir = path.join(__dirname, "../electron")
    const ipcEntries = scanDeep(path.join(electronDir, "ipc")).map((file) => ({
        // 从项目根目录计算相对路径
        entry: path.relative(process.cwd(), file)
    }))
    
    return [
        // 主进程
        {
            entry: path.join(process.cwd(), "electron/main.js")
        },
        // 预加载脚本
        {
            entry: path.join(process.cwd(), "electron/preload/index.js"),
            onstart(args) {
                args.reload()
            }
        },
        // 动态 IPC 入口
        ...ipcEntries
    ]
}

技术优势

1. 模块化与可维护性

  • 将相关功能的 IPC 处理器分组到不同的文件中
  • 新功能的添加不会影响现有代码结构
  • 便于团队协作开发

2. 自动化管理

  • 自动扫描并注册所有处理器,无需手动导入
  • 减少遗漏注册的风险
  • 统一的处理器管理入口

3. 开发体验优化

  • 支持开发时的热重载
  • 清晰的目录结构便于定位问题
  • 类型检查确保处理器格式正确

总结

这种自动化的 IPC 处理器注册机制为 Electron 应用开发带来了显著的好处:

  • 可扩展性:轻松添加新的 IPC 处理器
  • 可维护性:清晰的代码组织结构
  • 开发效率:自动化注册减少手动配置
  • 团队协作:统一的代码规范

这种模式特别适合中大型 Electron 项目,能够有效管理复杂的进程间通信需求。

超长定时器 long-timeout

作者 jump_jump
2025年10月8日 19:19

在 JavaScript 开发中,定时器是常用的异步编程工具。然而,原生的 setTimeoutsetInterval 存在一个鲜为人知的限制:它们无法处理超过 24.8 天的定时任务。

对于前端开发来说,该限制不太会出现问题,但是需要设置超长定时的后端应用场景,如长期提醒、周期性数据备份、订阅服务到期提醒等,这个限制可能会导致严重的功能缺陷。

JavaScript 定时器的限制

原理

JavaScript 中 setTimeoutsetInterval 的延时参数存在一个最大值限制,这源于底层实现的整数类型限制。具体来说:

// JavaScript 定时器的最大延时值(单位:毫秒)
const TIMEOUT_MAX = 2 ** 31 - 1; // 2147483647 毫秒

// 转换为天数
const MAX_DAYS = TIMEOUT_MAX / 1000 / 60 / 60 / 24; // 约 24.855 天

console.log(TIMEOUT_MAX); // 输出: 2147483647
console.log(MAX_DAYS);   // 输出: 24.855134814814818

这一限制的根本原因在于 JavaScript 引擎内部使用 32 位有符号整数来存储延时值。当提供的延时值超过这个范围时,JavaScript 会将其视为 0 处理,导致定时器立即执行。

问题示例

以下代码演示了超出限制时的问题:

// 尝试设置 30 天的延时(超出 24.8 天的限制)
setTimeout(() => {
  console.log("应该在 30 天后执行");
}, 1000 * 60 * 60 * 24 * 30); // 2592000000 毫秒

// 实际结果:回调函数会立即执行,而不是在 30 天后

在控制台中执行上述代码,会发现回调函数立即执行,而不是像预期那样在 30 天后执行。这是因为 2592000000 毫秒超过了 2147483647 毫秒的最大值限制。

long-timeout 库

long-timeout 是一个专门解决 JavaScript 定时器时间限制问题的轻量级库。它提供了与原生 API 兼容的接口,同时支持处理超过 24.8 天的延时任务。

主要特性

  • 完全兼容原生 setTimeoutsetInterval API
  • 支持任意时长的延时,不受 24.8 天限制
  • 轻量级实现,无外部依赖
  • 同时支持 Node.js 和浏览器环境
  • 提供与原生方法对应的清除定时器函数

安装与基本使用

安装

可以通过 npm 或 yarn 安装 long-timeout 库:

# 使用 npm
npm install long-timeout

# 使用 yarn
yarn add long-timeout

pnpm add long-timeout

基本用法

long-timeout 库提供了与原生 API 几乎相同的接口,使用非常简单:

// 引入 long-timeout 库
import lt from 'long-timeout';

// 设置一个 30 天的超时定时器
// 返回一个定时器引用,用于清除定时器
const timeoutRef = lt.setTimeout(() => {
  console.log('30 天后执行的代码');
}, 1000 * 60 * 60 * 24 * 30); // 2592000000 毫秒

// 清除超时定时器
// lt.clearTimeout(timeoutRef);

// 设置一个每 30 天执行一次的间隔定时器
const intervalRef = lt.setInterval(() => {
  console.log('每 30 天执行一次的代码');
}, 1000 * 60 * 60 * 24 * 30);

// 清除间隔定时器
// 同上
// lt.clearInterval(intervalRef);

实现原理

long-timeout 库的核心实现原理是将超长延时分解为多个不超过 24.8 天的小延时,通过递归调用 setTimeout 来实现对超长延时的支持。同时 node-cron 库也是基于该原理实现的。

核心实现代码

以下是 long-timeout 库的核心实现逻辑:

// 定义 32 位有符号整数的最大值
const TIMEOUT_MAX = 2147483647;

// 定时器构造函数
function Timeout(after, listener) {
  this.after = after;
  this.listener = listener;
  this.timeout = null;
}

// 启动定时器的方法
Timeout.prototype.start = function() {
  // 如果延时小于最大值,直接使用 setTimeout
  if (this.after <= TIMEOUT_MAX) {
    this.timeout = setTimeout(this.listener, this.after);
  } else {
    const self = this;
    // 否则,先设置一个最大值的延时,然后递归调用
    this.timeout = setTimeout(function() {
      // 减去已经等待的时间
      self.after -= TIMEOUT_MAX;
      // 继续启动定时器
      self.start();
    }, TIMEOUT_MAX);
  }
};

// 清除定时器的方法
Timeout.prototype.clear = function() {
  if (this.timeout !== null) {
    clearTimeout(this.timeout);
    this.timeout = null;
  }
};

工作流程图解

long-timeout 库的工作流程可以概括为以下几个步骤:

  1. 接收用户设置的延时时间和回调函数
  2. 检查延时是否超过 2147483647 毫秒(约 24.8 天)
  3. 如果未超过最大值,直接使用原生 setTimeout
  4. 如果超过最大值,将延时分解为多个最大值的段,通过递归调用实现
  5. 每完成一个时间段,更新剩余延时并继续设置下一个定时器
  6. 当所有时间段完成后,执行用户提供的回调函数
[用户设置超长延时][检查是否超过 TIMEOUT_MAX] ── 否 ─→ [直接使用 setTimeout]
                       └── 是 ─→ [分解为多个 TIMEOUT_MAX 段][递归调用 setTimeout][所有段完成后执行回调]

注意事项与最佳实践

内存管理

对于长时间运行的应用,应当注意及时清除不再需要的定时器,以避免内存泄漏:

import lt from 'long-timeout';

let timeoutRef = lt.setTimeout(() => {
  console.log('任务执行');
}, 1000 * 60 * 60 * 24 * 30); // 30 天

// 当不再需要该定时器时,及时清除
function cancelTask() {
  if (timeoutRef) {
    lt.clearTimeout(timeoutRef);
    timeoutRef = null; // 释放引用
    console.log('定时器已清除');
  }
}

应用重启的处理

需要注意的是,long-timeout 仅在应用运行期间有效。如果应用重启或进程终止,所有未执行的定时器都会丢失。对于需要持久化的定时任务,建议结合数据库存储:

// 引入 long-timeout 库
import lt from 'long-timeout';
// 假设的数据库模块
import db from './database'; 

// 从数据库加载未完成的定时任务
async function loadPendingTasks() {
  const tasks = await db.getPendingTasks();
  
  tasks.forEach(task => {
    const now = Date.now();
    const delay = task.executeTime - now;
    
    if (delay > 0) {
      // 重新设置定时器
      const timeoutId = lt.setTimeout(async () => {
        await executeTask(task.id);
        await db.markTaskAsCompleted(task.id);
      }, delay);
      
      // 保存 timeoutId 以便后续可能的取消操作
      db.updateTaskTimeoutId(task.id, timeoutId);
    } else {
      // 任务已过期,基于业务和当前时刻来决定是否执行或取消
      // 如电商大促发送短信提醒用户
      
      // 这里简单假设任务已过期,直接执行
      await executeTask(task.id);
      await db.markTaskAsCompleted(task.id);
    }
  });
}

精确性考虑

虽然 long-timeout 成功解决了定时器时间范围的限制问题,但定时器的执行精度仍受 JavaScript 事件循环机制和系统调度的影响。在实际运行中,任务可能无法按照预设时间精准执行。

为了减少系统调度带来的误差,可在每次定时器触发时记录当前时间戳,并在回调函数中计算实际执行时间,以此对时间误差进行补偿。不过这种方法仅能缓解部分精度问题,无法完全消除误差。

对于对计时精度要求高的场景,long-timeout 可能无法满足需求。开发者可以通过以下方案来解决:

  • Web Workers:可在后台线程执行任务,不阻塞主线程,一定程度上能提升计时精度。不过存在通信开销大及实现复杂的问题。
  • Node.js 的 process.hrtime():提供高精度的时间测量,可用于需要精确计时的场景,结合适当的逻辑可实现较精确的定时任务。
  • 操作系统级定时任务:如 Linux 的 cron 或 Windows 的任务计划程序,借助系统层面的调度能力,能保证较高的计时精度,不过需要与应用程序进行交互集成。

替代方案与技术对比

除了 long-timeout 库外,还有其他几种处理超长定时任务的方法:

表格

方案 优点 缺点
long-timeout 库 API 友好,使用简单,轻量级 仅在应用运行期间有效,不支持持久化
自定义递归 setTimeout 不需要额外依赖 实现复杂,管理困难
Web Workers 不阻塞主线程 通信开销大,实现复杂
服务端定时任务 持久化,不受客户端限制 需要服务器资源,网络依赖
浏览器闹钟 API 系统级支持,应用关闭后仍可工作 浏览器兼容性问题,用户权限要求

Node 版本管理还在手动重装全局包?这个方案让你效率翻倍

作者 嚴寒
2025年10月7日 17:54

前言

作为前端开发,你一定遇到过这样的场景:

  • 测试说:"能在 Node 16 下跑一下吗?"
  • 你切过去,发现常用的 CLI 工具全没了
  • 然后开始漫长的 npm install -g xxx 之旅

我统计了一下,自己平时全局装了 18 个包。手动重装一遍,保守估计要 5 分钟。如果一个月切换 10 次版本,就是 50 分钟

这篇文章分享一个更优雅的解决方案,让这个过程缩短到 30 秒

问题本质:全局包为什么会丢?

先理解一下底层原理。

当你用 nvmn 管理 Node 版本时,每个版本都有独立的安装目录:

~/.nvm/versions/node/v14.17.0/lib/node_modules/
~/.nvm/versions/node/v18.17.0/lib/node_modules/

切换版本 = 切换可执行文件路径 = 原来的全局包找不到了。

这是合理的设计,因为不同 Node 版本可能需要不同版本的包。但对开发者来说,确实增加了使用成本。

解决思路:自动化迁移

核心思路分两步:

第一步:保存全局包列表

npm list -g --depth=0 --json

这个命令返回 JSON 格式的全局包信息:

{
  "dependencies": {
    "@vue/cli": {
      "version": "5.0.8"
    },
    "typescript": {
      "version": "5.2.2"
    }
  }
}

把这个信息存到配置文件,就完成了备份。

第二步:批量安装

切换版本后,读取配置文件,循环安装:

for package in packages:
    npm install -g $package

工具实现:global-pack-sync

基于上面的思路,我实现了一个 CLI 工具。

快速开始

# 安装
npm install -g global-pack-sync

# 三步走
gps save      # 保存
nvm use 18    # 切换
gps restore   # 恢复

进阶用法

1. 多配置管理

可以为不同场景保存不同配置:

gps save work-env     # 工作环境
gps save side-project # 个人项目

# 按需恢复
gps restore work-env

2. 选择性安装

不想全部恢复?交互式选择:

gps select

> [x] typescript       # 要
  [ ] create-react-app # 不要
  [x] nodemon          # 要

3. 版本控制

默认安装最新版(推荐),也可以锁定版本:

gps restore --exact-version

性能优化

工具做了几个优化:

1. 并行安装

默认并发度为 3,可自定义:

gps restore --concurrency 5

测试数据(15 个包):

  • 串行:180 秒
  • 并发 3:65 秒
  • 并发 5:48 秒

2. 智能去重

已安装的包会自动跳过,避免重复安装。

3. 失败重试

网络抖动导致失败?自动生成重试脚本:

cat ~/.global-pack-sync/retry-failed.sh

#!/bin/bash
npm install -g package-that-failed

配置文件解析

配置存储在 ~/.global-pack-sync/packages.json

{
  "default": {
    "nodeVersion": "v18.17.0",
    "npmVersion": "9.6.7",
    "packageManager": "npm",
    "packages": {
      "@vue/cli": "5.0.8",
      "typescript": "5.2.2",
      "nodemon": "3.0.1"
    },
    "savedAt": "2025-01-15T10:30:00Z",
    "packagesCount": 15
  }
}

这个文件可以加入 Git,团队共享配置。

最佳实践

基于几个月的使用,总结几个经验:

1. 定期更新配置

装了新的全局包后,记得重新保存:

npm install -g new-package
gps save

2. 区分环境配置

工作和个人项目分开管理:

gps save company-tools
gps save personal-tools

3. 配置备份

定期备份配置文件:

cp ~/.global-pack-sync/packages.json ~/Dropbox/

4. 团队协作

团队统一工具链:

# Leader 保存
gps save team-standard

# 成员恢复
gps restore team-standard

兼容性说明

  • ✅ 支持 npm、yarn、pnpm
  • ✅ 支持 macOS、Linux、Windows
  • ✅ 支持 Node 14+
  • ✅ 自动检测包管理器类型

对比其他方案

方案 优点 缺点
手动记录 简单 易遗漏,费时
自定义脚本 灵活 维护成本高
Docker 环境隔离 本地开发较重
global-pack-sync 自动化、快速 需要安装工具

总结

切换 Node 版本是常见操作,但手动处理全局包迁移确实繁琐。使用自动化工具可以:

  • ⏱️ 节省时间:从 5 分钟到 30 秒
  • 🎯 减少遗漏:不会忘记某个包
  • 👥 团队协作:统一工具链配置
  • 🔄 提升效率:专注业务开发

如果你也有类似痛点,不妨试试这个工具。

项目地址: global-pack-sync


你平时怎么管理全局包的?欢迎评论区分享~

原文移步个人博客

❌
❌